diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 99bd5c6..711738f 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,6 +1,20 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt
index 5c70178..ba7d980 100644
--- a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt
+++ b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt
@@ -2,6 +2,7 @@ package dev.zoriya.omni
import android.annotation.SuppressLint
import android.content.Context
+import android.os.Handler
import android.os.Looper
import android.view.Surface
import android.view.SurfaceHolder
@@ -9,7 +10,6 @@ import android.view.SurfaceView
import android.view.TextureView
import androidx.media3.common.AudioAttributes
import androidx.media3.common.BasePlayer
-import androidx.media3.common.C
import androidx.media3.common.C.FORMAT_HANDLED
import androidx.media3.common.C.INDEX_UNSET
import androidx.media3.common.C.TIME_UNSET
@@ -32,14 +32,33 @@ import androidx.media3.common.Tracks
import androidx.media3.common.Tracks.Group
import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
+import androidx.media3.common.util.Clock
+import androidx.media3.common.util.ListenerSet
import androidx.media3.common.util.Size
import dev.jdtech.mpv.MPVLib
-import java.util.concurrent.CopyOnWriteArraySet
@SuppressLint("UnsafeOptInUsageError")
class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
private val applicationLooper: Looper = Looper.getMainLooper()
- private val listeners = CopyOnWriteArraySet()
+ private val applicationHandler = Handler(applicationLooper)
+ private val listeners = ListenerSet(
+ applicationLooper,
+ Clock.DEFAULT
+ ) { listener, flags ->
+ listener.onEvents(this, Player.Events(flags))
+ }
+
+ private fun notifyListeners(eventFlag: Int, callback: (Player.Listener) -> Unit) {
+ val notifyAction = {
+ listeners.queueEvent(eventFlag, callback)
+ listeners.flushEvents()
+ }
+ if (Looper.myLooper() == applicationLooper) {
+ notifyAction()
+ } else {
+ applicationHandler.post(notifyAction)
+ }
+ }
internal val mpv = (MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")).apply {
setOptionString("vo", "gpu-next")
@@ -112,12 +131,16 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun setMediaItems(mediaItems: List, resetPosition: Boolean) {
setMediaItems(
mediaItems,
- if (resetPosition) 0 else C.INDEX_UNSET,
- if (resetPosition) 0L else C.TIME_UNSET
+ if (resetPosition) 0 else INDEX_UNSET,
+ if (resetPosition) 0L else TIME_UNSET
)
}
- override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) {
+ override fun setMediaItems(
+ mediaItems: List,
+ startIndex: Int,
+ startPositionMs: Long
+ ) {
val target = when {
mediaItems.isEmpty() -> null
startIndex in mediaItems.indices -> mediaItems[startIndex]
@@ -130,7 +153,7 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
mpv.command(arrayOf("stop"))
- val targetMs = if (startPositionMs == C.TIME_UNSET) 0L else startPositionMs.coerceAtLeast(0L)
+ val targetMs = if (startPositionMs == TIME_UNSET) 0L else startPositionMs.coerceAtLeast(0L)
mpv.setPropertyDouble("start", targetMs / 1000.0)
if (target != null) {
@@ -193,9 +216,14 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun isLoading(): Boolean = mpv.getPropertyBoolean("paused-for-cache") ?: false
- override fun seekTo(mediaItemIndex: Int, positionMs: Long, seekCommand: Int, isRepeatingCurrentItem: Boolean) {
- if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) return
- val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
+ override fun seekTo(
+ mediaItemIndex: Int,
+ positionMs: Long,
+ seekCommand: Int,
+ isRepeatingCurrentItem: Boolean
+ ) {
+ if (mediaItemIndex != INDEX_UNSET && mediaItemIndex != 0) return
+ val targetMs = if (positionMs == TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
}
@@ -208,7 +236,9 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
}
override fun getPlaybackParameters(): PlaybackParameters {
- return PlaybackParameters((mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f))
+ return PlaybackParameters(
+ (mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)
+ )
}
override fun stop() {
@@ -226,55 +256,57 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
if (count <= 0) return Tracks.EMPTY
data class Entry(val id: Int, val format: Format)
+
val grouped = LinkedHashMap>()
for (i in 0 until count) {
- val base = "track-list/$i"
- val type = when (mpv.getPropertyString("$base/type")) {
- "video" -> TRACK_TYPE_VIDEO
- "audio" -> TRACK_TYPE_AUDIO
- "sub" -> TRACK_TYPE_TEXT
- else -> TRACK_TYPE_UNKNOWN
- }
- if (type == TRACK_TYPE_UNKNOWN) continue
-
- val id = mpv.getPropertyInt("$base/id") ?: continue
- val label = mpv.getPropertyString("$base/title")
- val language = mpv.getPropertyString("$base/lang")
- val codec = mpv.getPropertyString("$base/codec")
-
- val format = Format.Builder()
- .setId(id.toString())
- .setLabel(label)
- .setLanguage(language)
- .setCodecs(codec)
- .build()
-
- grouped.getOrPut>(type) { mutableListOf() }
- .add(Entry(id, format))
+ val base = "track-list/$i"
+ val type = when (mpv.getPropertyString("$base/type")) {
+ "video" -> TRACK_TYPE_VIDEO
+ "audio" -> TRACK_TYPE_AUDIO
+ "sub" -> TRACK_TYPE_TEXT
+ else -> TRACK_TYPE_UNKNOWN
}
+ if (type == TRACK_TYPE_UNKNOWN) continue
+
+ val id = mpv.getPropertyInt("$base/id") ?: continue
+ val label = mpv.getPropertyString("$base/title")
+ val language = mpv.getPropertyString("$base/lang")
+ val codec = mpv.getPropertyString("$base/codec")
+
+ val format = Format.Builder()
+ .setId(id.toString())
+ .setLabel(label)
+ .setLanguage(language)
+ .setCodecs(codec)
+ .build()
+
+ grouped.getOrPut>(type) { mutableListOf() }
+ .add(Entry(id, format))
+ }
val selectedVideo = mpv.getPropertyInt("vid")
val selectedAudio = mpv.getPropertyInt("aid")
val selectedSubtitle = mpv.getPropertyInt("sid")
val result = ArrayList()
for ((type, entries) in grouped) {
- if (entries.isEmpty()) continue
- val group = TrackGroup("mpv-$type", *entries.map { it.format }.toTypedArray())
- val selected = BooleanArray(entries.size) { idx ->
- val id = entries[idx].id
- when (type) {
- TRACK_TYPE_VIDEO -> selectedVideo == id
- TRACK_TYPE_AUDIO -> selectedAudio == id
- TRACK_TYPE_TEXT -> selectedSubtitle == id
- else -> false
- }
+ if (entries.isEmpty()) continue
+ val group = TrackGroup("mpv-$type", *entries.map { it.format }.toTypedArray())
+ val selected = BooleanArray(entries.size) { idx ->
+ val id = entries[idx].id
+ when (type) {
+ TRACK_TYPE_VIDEO -> selectedVideo == id
+ TRACK_TYPE_AUDIO -> selectedAudio == id
+ TRACK_TYPE_TEXT -> selectedSubtitle == id
+ else -> false
}
- val support = IntArray(entries.size) { FORMAT_HANDLED }
- result.add(Group(group, false, support, selected))
}
+ val support = IntArray(entries.size) { FORMAT_HANDLED }
+ result.add(Group(group, false, support, selected))
+ }
return Tracks(result)
}
- override fun getTrackSelectionParameters(): TrackSelectionParameters = currentTrackSelectionParameters
+ override fun getTrackSelectionParameters(): TrackSelectionParameters =
+ currentTrackSelectionParameters
override fun setTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) {
currentTrackSelectionParameters = trackSelectionParameters
@@ -301,7 +333,8 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
}
}
- override fun getMediaMetadata(): MediaMetadata = currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY
+ override fun getMediaMetadata(): MediaMetadata =
+ currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY
override fun getPlaylistMetadata(): MediaMetadata = playlistMetadata
@@ -315,15 +348,19 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
return object : Timeline() {
override fun getWindowCount(): Int = 1
- override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window {
+ override fun getWindow(
+ windowIndex: Int,
+ window: Window,
+ defaultPositionProjectionUs: Long
+ ): Window {
check(windowIndex == 0)
return window.set(
- Window.SINGLE_WINDOW_UID,
+ SINGLE_WINDOW_UID,
item,
null,
- C.TIME_UNSET,
- C.TIME_UNSET,
- C.TIME_UNSET,
+ TIME_UNSET,
+ TIME_UNSET,
+ TIME_UNSET,
true,
false,
item.liveConfiguration,
@@ -339,26 +376,26 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
check(periodIndex == 0)
- val uid = Window.SINGLE_WINDOW_UID
+ val uid = SINGLE_WINDOW_UID
val id = if (setIds) uid else null
return period.set(id, uid, 0, durationUs, 0L)
}
override fun getIndexOfPeriod(uid: Any): Int {
- return if (uid == Window.SINGLE_WINDOW_UID) 0 else C.INDEX_UNSET
+ return if (uid == SINGLE_WINDOW_UID) 0 else INDEX_UNSET
}
override fun getUidOfPeriod(periodIndex: Int): Any {
check(periodIndex == 0)
- return Window.SINGLE_WINDOW_UID
+ return SINGLE_WINDOW_UID
}
}
}
- override fun getCurrentPeriodIndex() = if (currentMediaItem == null) C.INDEX_UNSET else 0
+ override fun getCurrentPeriodIndex() = if (currentMediaItem == null) INDEX_UNSET else 0
override fun getCurrentMediaItemIndex() =
- if (currentMediaItem == null) C.INDEX_UNSET else 0
+ if (currentMediaItem == null) INDEX_UNSET else 0
override fun getDuration() =
((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
@@ -374,9 +411,9 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun isPlayingAd(): Boolean = false
- override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET
+ override fun getCurrentAdGroupIndex(): Int = INDEX_UNSET
- override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET
+ override fun getCurrentAdIndexInAdGroup(): Int = INDEX_UNSET
override fun getMaxSeekToPreviousPosition() = 3_000L
@@ -405,7 +442,7 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
}
override fun setVideoSurface(surface: Surface?) {
- mpv.attachSurface(surface ?: return clearVideoSurface() )
+ mpv.attachSurface(surface ?: return clearVideoSurface())
mpv.setOptionString("force-window", "yes")
mpv.setOptionString("vo", "gpu-next")
}
@@ -474,19 +511,24 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun setDeviceMuted(muted: Boolean, flags: Int) = Unit
- override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) = Unit
+ override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) =
+ Unit
override fun event(eventId: Int) {
when (eventId) {
- MPVLib.MpvEvent.MPV_EVENT_START_FILE -> listeners.forEach {
- it.onPlaybackStateChanged(STATE_BUFFERING)
- }
+ MPVLib.MpvEvent.MPV_EVENT_START_FILE ->
+ notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
+ it.onPlaybackStateChanged(STATE_BUFFERING)
+ }
- MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED ->
- listeners.forEach {
+ MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED -> {
+ notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(STATE_READY)
+ }
+ notifyListeners(EVENT_IS_PLAYING_CHANGED) {
it.onIsPlayingChanged(playWhenReady)
}
+ }
MPVLib.MpvEvent.MPV_EVENT_SEEK,
MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> {
@@ -502,16 +544,15 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
INDEX_UNSET,
INDEX_UNSET
)
- listeners.forEach {
- it.onPositionDiscontinuity(position, position, Player.DISCONTINUITY_REASON_SEEK)
+ notifyListeners(EVENT_POSITION_DISCONTINUITY) {
+ it.onPositionDiscontinuity(position, position, DISCONTINUITY_REASON_SEEK)
}
}
- MPVLib.MpvEvent.MPV_EVENT_END_FILE -> listeners.forEach {
- it.onPlaybackStateChanged(
- STATE_ENDED
- )
- }
+ MPVLib.MpvEvent.MPV_EVENT_END_FILE ->
+ notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
+ it.onPlaybackStateChanged(STATE_ENDED)
+ }
MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> {
playerError = PlaybackException(
@@ -519,7 +560,7 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
null,
PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK
)
- listeners.forEach {
+ notifyListeners(EVENT_PLAYER_ERROR) {
it.onPlayerErrorChanged(playerError)
it.onPlayerError(
playerError ?: PlaybackException(
@@ -538,7 +579,7 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun eventProperty(property: String, value: Long) {
when (property) {
"vid", "aid", "sid" -> {
- listeners.forEach { it.onTracksChanged(getCurrentTracks()) }
+ notifyListeners(EVENT_TRACKS_CHANGED) { it.onTracksChanged(getCurrentTracks()) }
}
}
}
@@ -546,17 +587,21 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun eventProperty(property: String, value: Double) {
when (property) {
"duration" ->
- listeners.forEach {
+ notifyListeners(EVENT_TIMELINE_CHANGED) {
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
}
"speed" ->
- listeners.forEach {
- it.onPlaybackParametersChanged(PlaybackParameters(value.toFloat().coerceAtLeast(0f)))
+ notifyListeners(EVENT_PLAYBACK_PARAMETERS_CHANGED) {
+ it.onPlaybackParametersChanged(
+ PlaybackParameters(
+ value.toFloat().coerceAtLeast(0f)
+ )
+ )
}
"volume" ->
- listeners.forEach {
+ notifyListeners(EVENT_VOLUME_CHANGED) {
it.onVolumeChanged((value.coerceIn(0.0, 100.0) / 100.0).toFloat())
}
}
@@ -564,23 +609,29 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
override fun eventProperty(property: String, value: Boolean) {
when (property) {
- "pause" ->
- listeners.forEach {
+ "pause" -> {
+ notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) {
it.onPlayWhenReadyChanged(
!value,
PLAY_WHEN_READY_CHANGE_REASON_REMOTE
)
+ }
+ notifyListeners(EVENT_IS_PLAYING_CHANGED) {
it.onIsPlayingChanged(!value)
}
+ }
"core-idle", "eof-reached" ->
- listeners.forEach {
+ notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(getPlaybackState())
}
"paused-for-cache" -> {
- listeners.forEach {
+ notifyListeners(EVENT_IS_LOADING_CHANGED) {
it.onIsLoadingChanged(value)
+ }
+
+ notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(getPlaybackState())
}
}
diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlaybackService.kt b/android/src/main/java/dev/zoriya/omni/OmniPlaybackService.kt
new file mode 100644
index 0000000..42644d5
--- /dev/null
+++ b/android/src/main/java/dev/zoriya/omni/OmniPlaybackService.kt
@@ -0,0 +1,86 @@
+package dev.zoriya.omni
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import androidx.core.content.ContextCompat
+import androidx.media3.common.Player
+import androidx.media3.session.DefaultMediaNotificationProvider
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import com.margelo.nitro.NitroModules
+import java.util.concurrent.atomic.AtomicReference
+
+@SuppressLint("UnsafeOptInUsageError")
+class OmniPlaybackService : MediaSessionService() {
+ private var mediaSession: MediaSession? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ serviceRef.set(this)
+ setMediaNotificationProvider(
+ DefaultMediaNotificationProvider.Builder(this)
+ .setChannelId(NOTIFICATION_CHANNEL_ID)
+ .build()
+ )
+ attachedPlayer.get()?.let { ensureSession(it) }
+ }
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
+ attachedPlayer.get()?.let { ensureSession(it) }
+ return mediaSession
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ if (!isPlaybackOngoing) {
+ pauseAllPlayersAndStopSelf()
+ }
+ }
+
+ override fun onDestroy() {
+ mediaSession?.release()
+ mediaSession = null
+ serviceRef.compareAndSet(this, null)
+ super.onDestroy()
+ }
+
+ private fun ensureSession(player: Player) {
+ if (mediaSession?.player === player) return
+ mediaSession?.release()
+ mediaSession = MediaSession.Builder(this, player)
+ .setId(SESSION_ID)
+ .build()
+ }
+
+ private fun clearSessionIf(player: Player) {
+ if (mediaSession?.player !== player) return
+ mediaSession?.release()
+ mediaSession = null
+ stopSelf()
+ }
+
+ companion object {
+ private const val SESSION_ID = "omni-session"
+ private const val NOTIFICATION_CHANNEL_ID = "omni_playback"
+ private val attachedPlayer = AtomicReference(null)
+ private val serviceRef = AtomicReference(null)
+
+ fun attachPlayer(player: Player) {
+ attachedPlayer.set(player)
+ serviceRef.get()?.ensureSession(player)
+ }
+
+ fun detachPlayer(player: Player) {
+ if (!attachedPlayer.compareAndSet(player, null)) return
+ serviceRef.get()?.clearSessionIf(player)
+ }
+
+ fun ensureStarted(player: Player) {
+ val context = NitroModules.Companion.applicationContext ?: return
+ attachPlayer(player)
+ ContextCompat.startForegroundService(
+ context,
+ Intent(context, OmniPlaybackService::class.java)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
index c9dd302..2e0f3f2 100644
--- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
+++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
@@ -20,10 +20,15 @@ class OmniPlayer : HybridOmniPlayerSpec() {
val player = MpvPlayer(ctx)
override val eventMap = EventMap(player)
+ init {
+ OmniPlaybackService.attachPlayer(player)
+ }
+
override fun dispose() {
super.dispose()
eventMap.dispose()
+ OmniPlaybackService.detachPlayer(player)
player.release()
}
@@ -99,6 +104,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
override val rendition: Array get() = emptyArray()
override fun play() {
+ OmniPlaybackService.ensureStarted(player)
player.play()
}