Fix threading issues, test with expoplayer

This commit is contained in:
2026-04-23 13:10:07 +02:00
parent a0b43ae009
commit f3086ac041
6 changed files with 191 additions and 90 deletions
+2
View File
@@ -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()) {
+1 -1
View File
@@ -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)
}