From f3086ac041187b04937435ca0deca95624a2274a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 22 Apr 2026 22:54:02 +0200 Subject: [PATCH] Fix threading issues, test with expoplayer --- android/build.gradle | 2 + android/src/main/AndroidManifest.xml | 2 +- .../main/java/dev/zoriya/omni/MpvPlayer.kt | 75 +++++++-- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 147 +++++++++--------- .../src/main/java/dev/zoriya/omni/OmniView.kt | 3 +- .../dev/zoriya/omni/utils/ThreadHelper.kt | 52 +++++++ 6 files changed, 191 insertions(+), 90 deletions(-) create mode 100644 android/src/main/java/dev/zoriya/omni/utils/ThreadHelper.kt diff --git a/android/build.gradle b/android/build.gradle index a1485fe..d052a8d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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()) { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 871a098..9dcfdd7 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ diff --git a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt index ba7d980..ec75106 100644 --- a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt @@ -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, 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()) } } diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 614530b..c2f0c51 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -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 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 { val groups = player.currentTracks.groups.filter { it.type == trackType } if (groups.isEmpty()) return emptyArray() -// + val result = ArrayList() 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 - } } diff --git a/android/src/main/java/dev/zoriya/omni/OmniView.kt b/android/src/main/java/dev/zoriya/omni/OmniView.kt index cd3e948..ca49b8d 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniView.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniView.kt @@ -77,7 +77,8 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH format: Int, width: Int, height: Int - ) { } + ) { + } override fun surfaceDestroyed(holder: SurfaceHolder) { surfaceReady = false diff --git a/android/src/main/java/dev/zoriya/omni/utils/ThreadHelper.kt b/android/src/main/java/dev/zoriya/omni/utils/ThreadHelper.kt new file mode 100644 index 0000000..ab667e1 --- /dev/null +++ b/android/src/main/java/dev/zoriya/omni/utils/ThreadHelper.kt @@ -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 runOnMainThreadSync(action: Callable): T { + return if (Looper.myLooper() == Looper.getMainLooper()) { + action.call() + } else { + val futureTask = FutureTask(action) + Handler(Looper.getMainLooper()).post(futureTask) + futureTask.get() + } + } + + class MainThreadProperty( + 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 mainThreadProperty(get: Reference.() -> T) = MainThreadProperty(get) + fun mainThreadProperty( + get: Reference.() -> T, + set: Reference.(T) -> Unit + ) = MainThreadProperty(get, set) +} \ No newline at end of file