mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Fix threading issues, test with expoplayer
This commit is contained in:
@@ -147,6 +147,8 @@ dependencies {
|
||||
implementation("androidx.media3:media3-common:1.4.1")
|
||||
implementation("androidx.media3:media3-session:1.4.1")
|
||||
implementation("androidx.media3:media3-ui:1.4.1")
|
||||
implementation 'androidx.media3:media3-exoplayer:1.10.0'
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<service
|
||||
android:name="dev.zoriya.omni.OmniPlayerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
|
||||
@@ -48,9 +48,14 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
listener.onEvents(this, Player.Events(flags))
|
||||
}
|
||||
|
||||
private fun notifyListeners(eventFlag: Int, callback: (Player.Listener) -> Unit) {
|
||||
private fun notifyListeners(eventFlag: Int, callback: (Player.Listener) -> Unit) =
|
||||
notifyListeners(arrayOf(eventFlag), callback)
|
||||
|
||||
private fun notifyListeners(eventFlag: Array<Int>, callback: (Player.Listener) -> Unit) {
|
||||
val notifyAction = {
|
||||
listeners.queueEvent(eventFlag, callback)
|
||||
for (event in eventFlag) {
|
||||
listeners.queueEvent(event, callback)
|
||||
}
|
||||
listeners.flushEvents()
|
||||
}
|
||||
if (Looper.myLooper() == applicationLooper) {
|
||||
@@ -141,6 +146,7 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
startIndex: Int,
|
||||
startPositionMs: Long
|
||||
) {
|
||||
val prev = currentMediaItem
|
||||
val target = when {
|
||||
mediaItems.isEmpty() -> null
|
||||
startIndex in mediaItems.indices -> mediaItems[startIndex]
|
||||
@@ -165,6 +171,26 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val events = arrayListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != currentMediaItem) {
|
||||
events.add(EVENT_MEDIA_ITEM_TRANSITION)
|
||||
}
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != currentMediaItem) {
|
||||
it.onMediaItemTransition(
|
||||
currentMediaItem,
|
||||
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addMediaItems(
|
||||
@@ -179,9 +205,27 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
}
|
||||
|
||||
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
||||
val prev = currentMediaItem
|
||||
currentMediaItem = null
|
||||
playlistMetadata = MediaMetadata.EMPTY
|
||||
mpv.command(arrayOf("stop"))
|
||||
|
||||
var events = arrayListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != null) {
|
||||
events.add(EVENT_MEDIA_ITEM_TRANSITION)
|
||||
}
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != null) {
|
||||
it.onMediaItemTransition(null, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
@@ -431,6 +475,14 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
return ((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0).toFloat()
|
||||
}
|
||||
|
||||
override fun mute() {
|
||||
mpv.setPropertyBoolean("mute", true)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
mpv.setPropertyBoolean("mute", false)
|
||||
}
|
||||
|
||||
override fun clearVideoSurface() {
|
||||
mpv.setOptionString("vo", "null")
|
||||
mpv.setOptionString("force-window", "no")
|
||||
@@ -521,14 +573,11 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
it.onPlaybackStateChanged(STATE_BUFFERING)
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED -> {
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED ->
|
||||
notifyListeners(arrayOf(EVENT_PLAYBACK_STATE_CHANGED, EVENT_IS_PLAYING_CHANGED)) {
|
||||
it.onPlaybackStateChanged(STATE_READY)
|
||||
}
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) {
|
||||
it.onIsPlayingChanged(playWhenReady)
|
||||
}
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_SEEK,
|
||||
MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> {
|
||||
@@ -609,17 +658,14 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
when (property) {
|
||||
"pause" -> {
|
||||
notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) {
|
||||
"pause" ->
|
||||
notifyListeners(arrayOf(EVENT_PLAY_WHEN_READY_CHANGED, EVENT_IS_PLAYING_CHANGED)) {
|
||||
it.onPlayWhenReadyChanged(
|
||||
!value,
|
||||
PLAY_WHEN_READY_CHANGE_REASON_REMOTE
|
||||
)
|
||||
}
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) {
|
||||
it.onIsPlayingChanged(!value)
|
||||
}
|
||||
}
|
||||
|
||||
"core-idle", "eof-reached" ->
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
@@ -627,11 +673,8 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
}
|
||||
|
||||
"paused-for-cache" -> {
|
||||
notifyListeners(EVENT_IS_LOADING_CHANGED) {
|
||||
notifyListeners(arrayOf(EVENT_IS_LOADING_CHANGED, EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
it.onIsLoadingChanged(value)
|
||||
}
|
||||
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(getPlaybackState())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.SurfaceHolder
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
@@ -19,14 +19,19 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem.RequestMetadata
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import dev.zoriya.omni.utils.ThreadHelper.mainThreadProperty
|
||||
import dev.zoriya.omni.utils.ThreadHelper.runOnMainThread
|
||||
import dev.zoriya.omni.utils.ThreadHelper.runOnMainThreadSync
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
|
||||
val player = MpvPlayer(ctx)
|
||||
val player: Player = runOnMainThreadSync { ExoPlayer.Builder(ctx).build() }
|
||||
// MpvPlayer(ctx)
|
||||
override val eventMap = EventMap(player)
|
||||
|
||||
override var showNotification: Boolean? = false
|
||||
@@ -49,7 +54,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
super.dispose()
|
||||
|
||||
eventMap.dispose()
|
||||
player.release()
|
||||
runOnMainThread { player.release() }
|
||||
}
|
||||
|
||||
private var currentSource: Source? = null
|
||||
@@ -72,8 +77,8 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
// putLong(MpvPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
// }
|
||||
// }
|
||||
player.setMediaItem(
|
||||
MediaItem.Builder()
|
||||
|
||||
val item = MediaItem.Builder()
|
||||
.setUri(src.uri)
|
||||
.setMimeType(src.mimeType)
|
||||
.setMediaId(src.uri)
|
||||
@@ -101,108 +106,111 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
runOnMainThreadSync { player.setMediaItem(item) }
|
||||
}
|
||||
|
||||
fun setSurface(holder: SurfaceHolder?) {
|
||||
if (holder == null) {
|
||||
player.clearVideoSurface()
|
||||
} else {
|
||||
player.setVideoSurfaceHolder(holder)
|
||||
runOnMainThread {
|
||||
if (holder == null) {
|
||||
player.clearVideoSurface()
|
||||
} else {
|
||||
player.setVideoSurfaceHolder(holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val hasPrev get() = player.hasPreviousMediaItem()
|
||||
override val hasNext get() = player.hasNextMediaItem()
|
||||
override val status: PlayerStatus
|
||||
get() = when (player.playbackState) {
|
||||
override val hasPrev by mainThreadProperty { player.hasPreviousMediaItem() }
|
||||
override val hasNext by mainThreadProperty { player.hasNextMediaItem() }
|
||||
override val status by mainThreadProperty {
|
||||
when (player.playbackState) {
|
||||
Player.STATE_IDLE,
|
||||
Player.STATE_ENDED -> PlayerStatus.IDLE
|
||||
Player.STATE_BUFFERING -> PlayerStatus.LOADING
|
||||
else -> PlayerStatus.READYTOPLAY
|
||||
}
|
||||
}
|
||||
|
||||
override val isPlaying get() = player.isPlaying
|
||||
override var currentTime
|
||||
get() = player.currentPosition.toDouble() / 1000.0
|
||||
set(value) {
|
||||
player.seekTo((value.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
}
|
||||
override val buffered
|
||||
get() = (player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0)
|
||||
override val duration
|
||||
get() = if (player.duration == C.TIME_UNSET) 0.0 else (player.duration.toDouble() / 1000.0).coerceAtLeast(
|
||||
override val isPlaying by mainThreadProperty { player.isPlaying }
|
||||
override var currentTime by mainThreadProperty(
|
||||
get = { player.currentPosition.toDouble() / 1000.0 },
|
||||
set = { value -> player.seekTo((value.coerceAtLeast(0.0) * 1000.0).toLong()) }
|
||||
)
|
||||
|
||||
override val buffered by mainThreadProperty {
|
||||
(player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0)
|
||||
}
|
||||
override val duration by mainThreadProperty {
|
||||
if (player.duration == C.TIME_UNSET) 0.0 else (player.duration.toDouble() / 1000.0).coerceAtLeast(
|
||||
0.0
|
||||
)
|
||||
}
|
||||
|
||||
override var playbackRate
|
||||
get() = player.playbackParameters.speed.toDouble()
|
||||
set(value) {
|
||||
player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f))
|
||||
}
|
||||
override var playbackRate by mainThreadProperty(
|
||||
get = { player.playbackParameters.speed.toDouble() },
|
||||
set = { value -> player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f)) }
|
||||
)
|
||||
|
||||
var volumeMuted: Float? = null
|
||||
override var muted: Boolean
|
||||
get() = player.volume == 0.0f && volumeMuted != null
|
||||
set(value) {
|
||||
if (value) {
|
||||
volumeMuted = player.volume
|
||||
player.volume = 0.0f
|
||||
} else {
|
||||
player.volume = volumeMuted ?: 100.0f
|
||||
volumeMuted = null
|
||||
}
|
||||
}
|
||||
var _muted = false
|
||||
override var muted by mainThreadProperty(
|
||||
get = { _muted },
|
||||
set = { value -> if (value) player.mute() else player.unmute() }
|
||||
)
|
||||
|
||||
override var volume
|
||||
get() = player.volume.toDouble()
|
||||
set(value) {
|
||||
player.volume = value.toFloat().coerceIn(0f, 1f)
|
||||
}
|
||||
override var volume by mainThreadProperty(
|
||||
get = {player.volume.toDouble()},
|
||||
set = { value -> player.volume = value.toFloat().coerceIn(0f, 1f) }
|
||||
)
|
||||
|
||||
override val videos get() = tracksByType(C.TRACK_TYPE_VIDEO)
|
||||
override val audios get() = tracksByType(C.TRACK_TYPE_AUDIO)
|
||||
override val subtitles get() = tracksByType(C.TRACK_TYPE_TEXT)
|
||||
override val videos by mainThreadProperty { tracksByType(C.TRACK_TYPE_VIDEO) }
|
||||
override val audios by mainThreadProperty { tracksByType(C.TRACK_TYPE_AUDIO) }
|
||||
override val subtitles by mainThreadProperty { tracksByType(C.TRACK_TYPE_TEXT) }
|
||||
override val rendition: Array<Rendition> get() = emptyArray()
|
||||
|
||||
override fun play() {
|
||||
player.play()
|
||||
runOnMainThreadSync { player.play() }
|
||||
if (showNotification == true) {
|
||||
ContextCompat.startForegroundService(ctx, Intent(ctx, OmniPlayerService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
player.pause()
|
||||
runOnMainThreadSync { player.pause() }
|
||||
}
|
||||
|
||||
override fun seekBy(offset: Double) {
|
||||
val target = (player.currentPosition.toDouble() / 1000.0) + offset
|
||||
player.seekTo((target.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
runOnMainThreadSync {
|
||||
val target = (player.currentPosition.toDouble() / 1000.0) + offset
|
||||
player.seekTo((target.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
override fun playPrev() {
|
||||
player.seekToPreviousMediaItem()
|
||||
runOnMainThreadSync { player.seekToPreviousMediaItem() }
|
||||
}
|
||||
|
||||
override fun playNext() {
|
||||
player.seekToNextMediaItem()
|
||||
runOnMainThreadSync { player.seekToNextMediaItem() }
|
||||
}
|
||||
|
||||
override fun selectVideo(video: Track) {
|
||||
selectTrack(C.TRACK_TYPE_VIDEO, video)
|
||||
runOnMainThreadSync { selectTrack(C.TRACK_TYPE_VIDEO, video) }
|
||||
}
|
||||
|
||||
override fun selectAudio(audio: Track) {
|
||||
selectTrack(C.TRACK_TYPE_AUDIO, audio)
|
||||
runOnMainThreadSync { selectTrack(C.TRACK_TYPE_AUDIO, audio) }
|
||||
}
|
||||
|
||||
override fun selectSubtitle(subtitle: Track?) {
|
||||
if (subtitle == null) {
|
||||
player.trackSelectionParameters = player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
.build()
|
||||
return
|
||||
runOnMainThreadSync {
|
||||
if (subtitle == null) {
|
||||
player.trackSelectionParameters = player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
.build()
|
||||
} else {
|
||||
selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
||||
}
|
||||
}
|
||||
selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
||||
}
|
||||
|
||||
override fun selectRendition(rendition: Rendition?) {
|
||||
@@ -211,7 +219,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
private fun tracksByType(trackType: Int): Array<Track> {
|
||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||
if (groups.isEmpty()) return emptyArray()
|
||||
//
|
||||
|
||||
val result = ArrayList<Track>()
|
||||
for (group in groups) {
|
||||
val mediaGroup = group.mediaTrackGroup
|
||||
@@ -254,9 +262,8 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlayerService : MediaSessionService() {
|
||||
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
|
||||
private val player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
|
||||
var mediaSession: MediaSession = MediaSession.Builder(ctx, player).build()
|
||||
var mediaSession: MediaSession = MediaSession.Builder(this, player).build()
|
||||
|
||||
init {
|
||||
Log.e("omni", "service inited")
|
||||
@@ -266,7 +273,7 @@ class OmniPlayerService : MediaSessionService() {
|
||||
Log.e("omni", "service created")
|
||||
super.onCreate()
|
||||
setMediaNotificationProvider(
|
||||
DefaultMediaNotificationProvider.Builder(ctx).build()
|
||||
DefaultMediaNotificationProvider.Builder(this).build()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -282,8 +289,4 @@ class OmniPlayerService : MediaSessionService() {
|
||||
mediaSession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun getApplicationContext(): Context? {
|
||||
return NitroModules.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) { }
|
||||
) {
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
surfaceReady = false
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package dev.zoriya.omni.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.margelo.nitro.NitroModules
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.FutureTask
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
object ThreadHelper {
|
||||
@JvmStatic
|
||||
fun runOnMainThread(action: () -> Unit) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
action()
|
||||
return
|
||||
}
|
||||
|
||||
val ctx = NitroModules.applicationContext ?: throw Error("Missing context")
|
||||
Handler(ctx.mainLooper).post { action() }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T> runOnMainThreadSync(action: Callable<T>): T {
|
||||
return if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
action.call()
|
||||
} else {
|
||||
val futureTask = FutureTask(action)
|
||||
Handler(Looper.getMainLooper()).post(futureTask)
|
||||
futureTask.get()
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadProperty<Reference, Type>(
|
||||
private val get: Reference.() -> Type,
|
||||
private val set: (Reference.(Type) -> Unit)? = null
|
||||
) {
|
||||
operator fun getValue(thisRef: Reference, property: KProperty<*>): Type {
|
||||
return runOnMainThreadSync { thisRef.get() }
|
||||
}
|
||||
|
||||
operator fun setValue(thisRef: Reference, property: KProperty<*>, value: Type) {
|
||||
val setter = set ?: throw IllegalStateException("Property ${property.name} is read-only")
|
||||
runOnMainThread { thisRef.setter(value) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <Reference, T> mainThreadProperty(get: Reference.() -> T) = MainThreadProperty(get)
|
||||
fun <Reference, T> mainThreadProperty(
|
||||
get: Reference.() -> T,
|
||||
set: Reference.(T) -> Unit
|
||||
) = MainThreadProperty(get, set)
|
||||
}
|
||||
Reference in New Issue
Block a user