From eee49c5e6bd7a6e3ece011486381ee68144adef5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Apr 2026 14:58:10 +0200 Subject: [PATCH] Properly implement `BasePlayer` interface --- .../src/main/java/dev/zoriya/omni/EventMap.kt | 216 +++-- .../main/java/dev/zoriya/omni/MpvPlayer.kt | 769 ++++++++++-------- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 124 ++- 3 files changed, 600 insertions(+), 509 deletions(-) diff --git a/android/src/main/java/dev/zoriya/omni/EventMap.kt b/android/src/main/java/dev/zoriya/omni/EventMap.kt index 3503e88..ba79318 100644 --- a/android/src/main/java/dev/zoriya/omni/EventMap.kt +++ b/android/src/main/java/dev/zoriya/omni/EventMap.kt @@ -1,15 +1,16 @@ package dev.zoriya.omni +import android.os.SystemClock +import androidx.media3.common.C +import androidx.media3.common.Player import com.margelo.nitro.omni.BoolProperty import com.margelo.nitro.omni.HybridOmniEventMapSpec import com.margelo.nitro.omni.NumberProperty import com.margelo.nitro.omni.PlayerStatus import com.margelo.nitro.omni.Rendition import com.margelo.nitro.omni.Track -import android.os.SystemClock -import dev.jdtech.mpv.MPVLib -class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.EventObserver { +class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Listener { val onPrevListeners = mutableSetOf<() -> Unit>() val onNextListeners = mutableSetOf<() -> Unit>() private val onEndListeners = mutableSetOf<() -> Unit>() @@ -22,30 +23,16 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev private val stateListeners = mutableMapOf Unit>>() private val stateBoolListeners = mutableMapOf Unit>>() private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>() - private var isSeeking = false - private var eofReached = false private var lastTimePosDispatchMs = 0L init { - player.addObserver(this) - player.observeProperty("vid", MPVLib.MpvFormat.MPV_FORMAT_INT64) - player.observeProperty("aid", MPVLib.MpvFormat.MPV_FORMAT_INT64) - player.observeProperty("sid", MPVLib.MpvFormat.MPV_FORMAT_INT64) - player.observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - player.observeProperty("mute", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - player.observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - player.observeProperty("core-idle", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - player.observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - player.observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) - player.observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) - player.observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) - player.observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) - player.observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) + player.addListener(this) + emitCoreState(forceCurrentTime = true) } private fun computePlayerStatus(): PlayerStatus { - val loading = player.getPropertyBoolean("paused-for-cache") ?: false - val idle = player.getPropertyBoolean("core-idle") ?: false + val loading = player.isLoading + val idle = player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED return when { loading -> PlayerStatus.LOADING idle -> PlayerStatus.IDLE @@ -53,122 +40,109 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev } } - private fun getTrackById(type: String, id: Long): Track? { - val count = player.getPropertyInt("track-list/count") ?: 0 - for (i in 0 until count) { - val base = "track-list/$i" - val trackType = player.getPropertyString("$base/type") ?: continue - if (trackType != type) continue - val trackId = player.getPropertyInt("$base/id") ?: continue - if (trackId.toLong() != id) continue - val selected = player.getPropertyBoolean("$base/selected") ?: false - val label = player.getPropertyString("$base/title") - ?: player.getPropertyString("$base/codec") - val language = player.getPropertyString("$base/lang") - return Track( - id = trackId.toString(), - label = label, - language = language, - selected = selected - ) + private fun selectedTrack(trackType: Int): Track? { + val groups = player.currentTracks.groups.filter { it.type == trackType } + for (group in groups) { + val mediaGroup = group.mediaTrackGroup + for (i in 0 until group.length) { + if (!group.isTrackSelected(i)) continue + val format = group.getTrackFormat(i) + return Track( + id = format.id ?: mediaGroup.id, + label = format.label, + language = format.language, + selected = true + ) + } } return null } - override fun event(event: Int) { - when (event) { - MPVLib.MpvEvent.MPV_EVENT_START_FILE -> - run { - eofReached = false - isSeeking = false - playerStatusListeners.forEach { it(PlayerStatus.LOADING) } - } - - MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED -> - playerStatusListeners.forEach { it(PlayerStatus.READYTOPLAY) } - - MPVLib.MpvEvent.MPV_EVENT_SEEK -> isSeeking = true - MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> isSeeking = false - MPVLib.MpvEvent.MPV_EVENT_END_FILE -> { - isSeeking = false - if (eofReached) { - onEndListeners.forEach { it() } - } else { - onErrorListeners.forEach { - it("end_file", "playback ended before reaching EOF") - } - } - playerStatusListeners.forEach { it(PlayerStatus.IDLE) } + override fun onEvents(player: Player, events: Player.Events) { + if ( + events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || + events.contains(Player.EVENT_IS_LOADING_CHANGED) + ) { + val status = computePlayerStatus() + playerStatusListeners.forEach { it(status) } + if (player.playbackState == Player.STATE_ENDED) { + onEndListeners.forEach { it() } } + } - MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> onErrorListeners.forEach { - it("queue_overflow", "mpv event queue overflow") + if ( + events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) || + events.contains(Player.EVENT_IS_PLAYING_CHANGED) + ) { + val isPlaying = player.isPlaying + onAudioFocusChangeListeners.forEach { it(if (isPlaying) "playing" else "paused") } + stateBoolListeners[BoolProperty.ISPLAYING]?.forEach { it(isPlaying) } + } + + if (events.contains(Player.EVENT_VOLUME_CHANGED)) { + val volume = player.volume.toDouble().coerceIn(0.0, 1.0) + stateListeners[NumberProperty.VOLUME]?.forEach { it(volume) } + stateBoolListeners[BoolProperty.MUTED]?.forEach { it(volume <= 0.0) } + } + + if (events.contains(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) { + stateListeners[NumberProperty.PLAYBACKRATE]?.forEach { + it(player.playbackParameters.speed.toDouble()) } } + + if ( + events.contains(Player.EVENT_POSITION_DISCONTINUITY) || + events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || + events.contains(Player.EVENT_IS_LOADING_CHANGED) + ) { + emitCoreState(forceCurrentTime = true) + } else { + emitCoreState(forceCurrentTime = false) + } + + if (events.contains(Player.EVENT_TRACKS_CHANGED)) { + selectedTrack(C.TRACK_TYPE_VIDEO)?.let { track -> + onVideoTrackChangeListeners.forEach { it(track) } + } + selectedTrack(C.TRACK_TYPE_AUDIO)?.let { track -> + onAudioTrackChangeListeners.forEach { it(track) } + } + onSubtitleChangeListeners.forEach { it(selectedTrack(C.TRACK_TYPE_TEXT)) } + } } - override fun eventProperty(property: String) = Unit - - override fun eventProperty(property: String, value: Long) { - when (property) { - "vid" -> { - val track = getTrackById("video", value) - if (track != null) onVideoTrackChangeListeners.forEach { it(track) } - } - - "aid" -> { - val track = getTrackById("audio", value) - if (track != null) onAudioTrackChangeListeners.forEach { it(track) } - } - - "sid" -> { - val track = getTrackById("sub", value) - onSubtitleChangeListeners.forEach { it(track) } - } + override fun onPlayerError(error: androidx.media3.common.PlaybackException) { + onErrorListeners.forEach { + it(error.errorCodeName, error.message ?: "unknown message") } } - override fun eventProperty(property: String, value: Double) { - when (property) { - "time-pos" -> if (isSeeking || SystemClock.elapsedRealtime() - lastTimePosDispatchMs >= 1000L) { - lastTimePosDispatchMs = SystemClock.elapsedRealtime() - stateListeners[NumberProperty.CURRENTTIME]?.forEach { it(value.coerceAtLeast(0.0)) } - } + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + emitCurrentTime(force = true) + } - "demuxer-cache-time" -> stateListeners[NumberProperty.BUFFERED] - ?.forEach { it(value.coerceAtLeast(0.0)) } - - "duration" -> stateListeners[NumberProperty.DURATION] - ?.forEach { it(value.coerceAtLeast(0.0)) } - - "speed" -> stateListeners[NumberProperty.PLAYBACKRATE] - ?.forEach { it(value) } - - "volume" -> stateListeners[NumberProperty.VOLUME] - ?.forEach { it((value / 100.0).coerceIn(0.0, 1.0)) } + private fun emitCoreState(forceCurrentTime: Boolean) { + emitCurrentTime(forceCurrentTime) + stateListeners[NumberProperty.BUFFERED]?.forEach { + it((player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0)) + } + stateListeners[NumberProperty.DURATION]?.forEach { + val duration = player.duration + it(if (duration == C.TIME_UNSET) 0.0 else (duration.toDouble() / 1000.0).coerceAtLeast(0.0)) } } - override fun eventProperty(property: String, value: Boolean) { - when (property) { - "pause" -> { - val isPlaying = !value - onAudioFocusChangeListeners.forEach { it(if (isPlaying) "playing" else "paused") } - stateBoolListeners[BoolProperty.ISPLAYING]?.forEach { it(isPlaying) } - } - - "mute" -> - stateBoolListeners[BoolProperty.MUTED]?.forEach { it(value) } - - "eof-reached" -> eofReached = value - "core-idle", "paused-for-cache" -> - playerStatusListeners.forEach { it(computePlayerStatus()) } - } - } - - override fun eventProperty(property: String, value: String) { - if (property == "sid" && value == "no") { - onSubtitleChangeListeners.forEach { it(null) } + private fun emitCurrentTime(force: Boolean) { + val now = SystemClock.elapsedRealtime() + if (!force && now - lastTimePosDispatchMs < 1000L) return + lastTimePosDispatchMs = now + stateListeners[NumberProperty.CURRENTTIME]?.forEach { + it((player.currentPosition.toDouble() / 1000.0).coerceAtLeast(0.0)) } } @@ -269,7 +243,7 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev } override fun dispose() { - player.removeObserver(this) + player.removeListener(this) super.dispose() } } diff --git a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt index bf62e48..7e3e3ee 100644 --- a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt @@ -2,29 +2,44 @@ package dev.zoriya.omni import android.annotation.SuppressLint import android.content.Context -import android.os.Bundle -import android.os.Handler import android.os.Looper import android.view.Surface import android.view.SurfaceHolder +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 +import androidx.media3.common.C.TRACK_TYPE_AUDIO +import androidx.media3.common.C.TRACK_TYPE_TEXT +import androidx.media3.common.C.TRACK_TYPE_UNKNOWN +import androidx.media3.common.C.TRACK_TYPE_VIDEO +import androidx.media3.common.DeviceInfo import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes +import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player -import androidx.media3.common.SimpleBasePlayer +import androidx.media3.common.Timeline +import androidx.media3.common.Timeline.Window.SINGLE_WINDOW_UID import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.Tracks -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture +import androidx.media3.common.Tracks.Group +import androidx.media3.common.VideoSize +import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Size import dev.jdtech.mpv.MPVLib +import java.util.concurrent.CopyOnWriteArraySet @SuppressLint("UnsafeOptInUsageError") -class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib.EventObserver { - private val mainHandler = Handler(Looper.getMainLooper()) +class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver { + private val applicationLooper: Looper = Looper.getMainLooper() + private val listeners = CopyOnWriteArraySet() internal val mpv = (MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")).apply { setOptionString("vo", "gpu-next") @@ -50,8 +65,6 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG) observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG) observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG) - observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) - observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE) @@ -60,18 +73,18 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib observeProperty("sid", MPVLib.MpvFormat.MPV_FORMAT_INT64) } -// private data class TrackGroupBinding(val group: TrackGroup, val mpvIds: IntArray) - -// private var currentMediaItem: MediaItem? = null -// private var currentTrackSelectionParameters: TrackSelectionParameters = -// TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT -// private var trackGroupsByType: Map> = emptyMap() + private var currentMediaItem: MediaItem? = null + private var currentTrackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + private var playerError: PlaybackException? = null + private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY private val availableCommands: Player.Commands = Player.Commands.Builder() .add(COMMAND_PLAY_PAUSE) + .add(COMMAND_PREPARE) .add(COMMAND_STOP) .add(COMMAND_RELEASE) .add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + .add(COMMAND_SEEK_TO_DEFAULT_POSITION) .add(COMMAND_SEEK_BACK) .add(COMMAND_SEEK_FORWARD) .add(COMMAND_SET_SPEED_AND_PITCH) @@ -86,357 +99,475 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib .add(COMMAND_SET_TRACK_SELECTION_PARAMETERS) .build() - override fun getState(): State { - val positionMs = - ((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() - val cacheMs = ((mpv.getPropertyDouble("demuxer-cache-time") - ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() - val durationMs = - ((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() + override fun getApplicationLooper(): Looper = applicationLooper - val builder = State.Builder() - .setAvailableCommands(availableCommands) - .setPlayWhenReady( - !(mpv.getPropertyBoolean("pause") ?: true), - Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST - ) - .setPlaybackState(getMpvPlaybackState()) - .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) - .setIsLoading(mpv.getPropertyBoolean("paused-for-cache") ?: false) - .setPlaybackParameters( - PlaybackParameters( - (mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f) - ) - ) - .setVolume( - (mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0).toFloat() / 100f - ) -// .setTrackSelectionParameters(currentTrackSelectionParameters) - .setContentPositionMs(positionMs) - .setContentBufferedPositionMs { cacheMs } - .setTotalBufferedDurationMs { cacheMs } - .setPlaylistMetadata(currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY) - - val item = currentMediaItem - if (item != null) { - val itemBuilder = MediaItemData.Builder(item.mediaId) - .setMediaItem(item) - .setMediaMetadata(item.mediaMetadata) - .setIsSeekable(true) -// .setTracks(buildTracks()) - - if (durationMs > 0L) { - itemBuilder.setDurationUs(durationMs * 1000L) - } - - builder - .setPlaylist(listOf(itemBuilder.build())) - .setCurrentMediaItemIndex(0) - } - - return builder.build() + override fun addListener(listener: Player.Listener) { + listeners.add(listener) } - override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { - mpv.setPropertyBoolean("pause", !playWhenReady) - return Futures.immediateVoidFuture() + override fun removeListener(listener: Player.Listener) { + listeners.remove(listener) } - override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> { - mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble()) - return Futures.immediateVoidFuture() + override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { + setMediaItems( + mediaItems, + if (resetPosition) 0 else C.INDEX_UNSET, + if (resetPosition) 0L else C.TIME_UNSET + ) } - override fun handleSeek( - mediaItemIndex: Int, - positionMs: Long, - seekCommand: Int - ): ListenableFuture<*> { - if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) { - return Futures.immediateVoidFuture() - } - val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L) - mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute")) - return Futures.immediateVoidFuture() - } - - override fun handleSetVolume(volume: Float): ListenableFuture<*> { - mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0) - return Futures.immediateVoidFuture() - } - - override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> { - when (videoOutput) { - is Surface -> { - mpv.attachSurface(videoOutput) - mpv.setOptionString("force-window", "yes") - mpv.setOptionString("vo", "gpu-next") - } - - is SurfaceHolder -> { - val surface = videoOutput.surface - if (surface != null && surface.isValid) { - mpv.attachSurface(surface) - mpv.setOptionString("force-window", "yes") - mpv.setOptionString("vo", "gpu-next") - - val frame = videoOutput.surfaceFrame - val width = frame?.width() ?: 0 - val height = frame?.height() ?: 0 - if (width > 0 && height > 0) { - mpv.setPropertyString("android-surface-size", "${width}x${height}") - } - } - } - } - return Futures.immediateVoidFuture() - } - - override fun handleClearVideoOutput(videoOutput: Any?): ListenableFuture<*> { - mpv.setOptionString("vo", "null") - mpv.setOptionString("force-window", "no") - mpv.detachSurface() - return Futures.immediateVoidFuture() - } - - override fun handleSetMediaItems( - mediaItems: List, - startIndex: Int, - startPositionMs: Long - ): ListenableFuture<*> { + override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { val target = when { mediaItems.isEmpty() -> null startIndex in mediaItems.indices -> mediaItems[startIndex] else -> mediaItems[0] } -// currentMediaItem = target -// trackGroupsByType = emptyMap() + currentMediaItem = target + playlistMetadata = target?.mediaMetadata ?: MediaMetadata.EMPTY + playerError = null + mpv.command(arrayOf("stop")) + val targetMs = if (startPositionMs == C.TIME_UNSET) 0L else startPositionMs.coerceAtLeast(0L) + mpv.setPropertyDouble("start", targetMs / 1000.0) + if (target != null) { val uri = target.localConfiguration?.uri?.toString() if (!uri.isNullOrEmpty()) { -// val requestExtras = target.requestMetadata.extras -// applyRequestHeaders(requestExtras) - -// val requestStartMs = requestExtras?.getLong(REQUEST_START_MS_KEY, C.TIME_UNSET) ?: C.TIME_UNSET -// val start = if (startPositionMs != C.TIME_UNSET) startPositionMs else requestStartMs -// if (start != C.TIME_UNSET && start >= 0L) { -// mpv.setPropertyDouble("start", start.toDouble() / 1000.0) -// } - mpv.command(arrayOf("loadfile", uri, "replace")) target.localConfiguration?.subtitleConfigurations?.forEach { subtitle -> mpv.command(arrayOf("sub-add", subtitle.uri.toString(), "cached")) } } } - - return Futures.immediateVoidFuture() } - override fun handleSetTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters): ListenableFuture<*> { -// currentTrackSelectionParameters = trackSelectionParameters -// applyTrackSelectionParameters(trackSelectionParameters) - return Futures.immediateVoidFuture() + override fun addMediaItems( + p0: Int, + p1: List? + ) = Unit + + override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) = Unit + + override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { + setMediaItems(mediaItems) } - override fun handleStop(): ListenableFuture<*> { + override fun removeMediaItems(fromIndex: Int, toIndex: Int) { + currentMediaItem = null + playlistMetadata = MediaMetadata.EMPTY mpv.command(arrayOf("stop")) - return Futures.immediateVoidFuture() } - override fun handleRelease(): ListenableFuture<*> { + override fun getAvailableCommands(): Player.Commands = availableCommands + + override fun prepare() = Unit + + override fun getPlaybackState(): Int { + val ended = mpv.getPropertyBoolean("eof-reached") ?: false + if (ended) return Player.STATE_ENDED + + val idle = mpv.getPropertyBoolean("core-idle") ?: false + if (idle) return Player.STATE_IDLE + + val loading = mpv.getPropertyBoolean("paused-for-cache") ?: false + return if (loading) Player.STATE_BUFFERING else Player.STATE_READY + } + + override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE + + override fun getPlayerError(): PlaybackException? = playerError + + override fun setPlayWhenReady(playWhenReady: Boolean) { + mpv.setPropertyBoolean("pause", !playWhenReady) + } + + override fun getPlayWhenReady(): Boolean = !(mpv.getPropertyBoolean("pause") ?: true) + + override fun setRepeatMode(repeatMode: Int) = Unit + + override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF + + override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) = Unit + + override fun getShuffleModeEnabled(): Boolean = false + + 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) + mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute")) + } + + override fun getSeekBackIncrement(): Long = 5_000L + + override fun getSeekForwardIncrement(): Long = 15_000L + + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { + mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble()) + } + + override fun getPlaybackParameters(): PlaybackParameters { + return PlaybackParameters((mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)) + } + + override fun stop() { + mpv.command(arrayOf("stop")) + } + + override fun release() { mpv.removeObserver(this) - mpv.detachSurface() + clearVideoSurface() mpv.destroy() - return Futures.immediateVoidFuture() } - override fun event(event: Int) { -// trackGroupsByType = buildTrackGroupsByType() - postInvalidateState() + override fun getCurrentTracks(): Tracks { + val count = mpv.getPropertyInt("track-list/count") ?: 0 + 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 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 + } + } + val support = IntArray(entries.size) { FORMAT_HANDLED } + result.add(Group(group, false, support, selected)) + } + return Tracks(result) } - override fun eventProperty(property: String) { -// if (property == "vid" || property == "aid" || property == "sid") { -// trackGroupsByType = buildTrackGroupsByType() -// } - postInvalidateState() - } + override fun getTrackSelectionParameters(): TrackSelectionParameters = currentTrackSelectionParameters - override fun eventProperty(property: String, value: Long) { -// if (property == "vid" || property == "aid" || property == "sid") { -// trackGroupsByType = buildTrackGroupsByType() -// } - postInvalidateState() - } + override fun setTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) { + currentTrackSelectionParameters = trackSelectionParameters + if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_VIDEO)) { + mpv.setPropertyString("vid", "no") + } + if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_AUDIO)) { + mpv.setPropertyString("aid", "no") + } + if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_TEXT)) { + mpv.setPropertyString("sid", "no") + } + for (override in trackSelectionParameters.overrides.values) { + val selectedIndex = override.trackIndices.firstOrNull() ?: continue + if (selectedIndex !in 0 until override.mediaTrackGroup.length) continue + val trackId = + override.mediaTrackGroup.getFormat(selectedIndex).id?.toIntOrNull() ?: continue - override fun eventProperty(property: String, value: Double) { - postInvalidateState() - } - - override fun eventProperty(property: String, value: Boolean) { - postInvalidateState() - } - - override fun eventProperty(property: String, value: String) { - postInvalidateState() - } - - private fun postInvalidateState() { - if (Looper.myLooper() == Looper.getMainLooper()) { - invalidateState() - } else { - mainHandler.post { invalidateState() } + when (override.type) { + TRACK_TYPE_VIDEO -> mpv.setPropertyInt("vid", trackId) + TRACK_TYPE_AUDIO -> mpv.setPropertyInt("aid", trackId) + TRACK_TYPE_TEXT -> mpv.setPropertyInt("sid", trackId) + } } } - private fun getMpvPlaybackState(): Int { - val ended = mpv.getPropertyBoolean("eof-reached") ?: false - if (ended) return STATE_ENDED + override fun getMediaMetadata(): MediaMetadata = currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY - val idle = mpv.getPropertyBoolean("core-idle") ?: false - if (idle) return STATE_IDLE + override fun getPlaylistMetadata(): MediaMetadata = playlistMetadata - val loading = mpv.getPropertyBoolean("paused-for-cache") ?: false - return if (loading) STATE_BUFFERING else STATE_READY + override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) { + playlistMetadata = mediaMetadata } -// private fun applyRequestHeaders(extras: Bundle?) { -// if (extras == null) { -// mpv.setPropertyString("http-header-fields", "") -// return -// } -// val names = extras.getStringArrayList(REQUEST_HEADER_NAMES_KEY) -// val values = extras.getStringArrayList(REQUEST_HEADER_VALUES_KEY) -// if (names.isNullOrEmpty() || values.isNullOrEmpty()) { -// mpv.setPropertyString("http-header-fields", "") -// return -// } -// val size = minOf(names.size, values.size) -// if (size <= 0) { -// mpv.setPropertyString("http-header-fields", "") -// return -// } -// val formatted = (0 until size).joinToString("\r\n") { idx -> "${names[idx]}: ${values[idx]}" } -// mpv.setPropertyString("http-header-fields", formatted) -// } + override fun getCurrentTimeline(): Timeline { + val item = currentMediaItem ?: return Timeline.EMPTY + val durationUs = if (duration == TIME_UNSET) TIME_UNSET else duration * 1000L + return object : Timeline() { + override fun getWindowCount(): Int = 1 -// private fun buildTrackGroupsByType(): Map> { -// val count = mpv.getPropertyInt("track-list/count") ?: 0 -// if (count <= 0) return emptyMap() -// -// val grouped = LinkedHashMap>>() -// for (i in 0 until count) { -// val base = "track-list/$i" -// val type = when (mpv.getPropertyString("$base/type")) { -// "video" -> C.TRACK_TYPE_VIDEO -// "audio" -> C.TRACK_TYPE_AUDIO -// "sub" -> C.TRACK_TYPE_TEXT -// else -> C.TRACK_TYPE_UNKNOWN -// } -// if (type == C.TRACK_TYPE_UNKNOWN) continue -// -// val id = mpv.getPropertyInt("$base/id") ?: continue -// val label = mpv.getPropertyString("$base/title") ?: mpv.getPropertyString("$base/codec") -// val language = mpv.getPropertyString("$base/lang") -// val codec = mpv.getPropertyString("$base/codec") -// -// val sampleMimeType = when (type) { -// C.TRACK_TYPE_VIDEO -> MimeTypes.VIDEO_UNKNOWN -// C.TRACK_TYPE_AUDIO -> MimeTypes.AUDIO_UNKNOWN -// C.TRACK_TYPE_TEXT -> MimeTypes.TEXT_UNKNOWN -// else -> null -// } -// -// val formatBuilder = Format.Builder() -// .setId(id) -// .setLabel(label) -// .setLanguage(language) -// .setCodecs(codec) -// sampleMimeType?.let { formatBuilder.setSampleMimeType(it) } -// -// grouped.getOrPut(type) { mutableListOf() }.add(id to formatBuilder.build()) -// } -// -// val result = LinkedHashMap>() -// for ((type, entries) in grouped) { -// if (entries.isEmpty()) continue -// val formats = entries.map { it.second }.toTypedArray() -// val ids = entries.map { it.first }.toIntArray() -// val group = TrackGroup("mpv-$type", *formats) -// result[type] = listOf(TrackGroupBinding(group, ids)) -// } -// return result -// } + override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window { + check(windowIndex == 0) + return window.set( + Window.SINGLE_WINDOW_UID, + item, + null, + C.TIME_UNSET, + C.TIME_UNSET, + C.TIME_UNSET, + true, + false, + item.liveConfiguration, + 0L, + durationUs, + 0, + 0, + 0L + ) + } -// private fun buildTracks(): Tracks { -// if (trackGroupsByType.isEmpty()) { -// trackGroupsByType = buildTrackGroupsByType() -// } -// if (trackGroupsByType.isEmpty()) return Tracks.EMPTY -// -// val selectedVideo = mpv.getPropertyInt("vid") -// val selectedAudio = mpv.getPropertyInt("aid") -// val selectedSubtitle = mpv.getPropertyInt("sid") -// -// val groups = ArrayList() -// for ((type, bindings) in trackGroupsByType) { -// val selectedId = when (type) { -// C.TRACK_TYPE_VIDEO -> selectedVideo -// C.TRACK_TYPE_AUDIO -> selectedAudio -// C.TRACK_TYPE_TEXT -> selectedSubtitle -// else -> null -// } -// for (binding in bindings) { -// val selected = BooleanArray(binding.mpvIds.size) { idx -> -// val id = binding.mpvIds[idx] -// selectedId != null && selectedId == id -// } -// val support = IntArray(binding.mpvIds.size) { C.FORMAT_HANDLED } -// groups.add(Tracks.Group(binding.group, false, support, selected)) -// } -// } -// -// return Tracks(groups) -// } + override fun getPeriodCount(): Int = 1 + + override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period { + check(periodIndex == 0) + val uid = Window.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 + } + + override fun getUidOfPeriod(periodIndex: Int): Any { + check(periodIndex == 0) + return Window.SINGLE_WINDOW_UID + } + } + } + + override fun getCurrentPeriodIndex() = if (currentMediaItem == null) C.INDEX_UNSET else 0 + + override fun getCurrentMediaItemIndex() = + if (currentMediaItem == null) C.INDEX_UNSET else 0 + + override fun getDuration() = + ((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() + + override fun getCurrentPosition() = + ((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() + + override fun getBufferedPosition() = + ((mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong() + .coerceAtLeast(0L) + + override fun getTotalBufferedDuration() = bufferedPosition + + override fun isPlayingAd(): Boolean = false + + override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET + + override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET + + override fun getMaxSeekToPreviousPosition() = 3_000L + + override fun getContentPosition() = getCurrentPosition() + + override fun getContentBufferedPosition() = getBufferedPosition() + + override fun getAudioAttributes(): AudioAttributes = AudioAttributes.DEFAULT + + override fun setVolume(volume: Float) { + mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0) + } + + override fun getVolume(): Float { + return ((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0).toFloat() + } + + override fun clearVideoSurface() { + mpv.setOptionString("vo", "null") + mpv.setOptionString("force-window", "no") + mpv.detachSurface() + } + + override fun clearVideoSurface(surface: Surface?) { + clearVideoSurface() + } + + override fun setVideoSurface(surface: Surface?) { + mpv.attachSurface(surface ?: return clearVideoSurface() ) + mpv.setOptionString("force-window", "yes") + mpv.setOptionString("vo", "gpu-next") + } + + override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) { + val holder = surfaceHolder ?: return clearVideoSurface() + val surface = holder.surface + if (surface != null && surface.isValid) { + setVideoSurface(surface) + val frame = holder.surfaceFrame + val width = frame?.width() ?: 0 + val height = frame?.height() ?: 0 + if (width > 0 && height > 0) { + mpv.setPropertyString("android-surface-size", "${width}x${height}") + } + } + } + + override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) { + clearVideoSurface() + } + + override fun setVideoSurfaceView(surfaceView: SurfaceView?) { + val view = surfaceView ?: return clearVideoSurface() + setVideoSurfaceHolder(view.holder) + } + + override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { + val view = surfaceView ?: return clearVideoSurface() + clearVideoSurfaceHolder(view.holder) + } + + override fun setVideoTextureView(textureView: TextureView?) = Unit + + override fun clearVideoTextureView(textureView: TextureView?) = Unit + + override fun getVideoSize(): VideoSize = VideoSize.UNKNOWN + + override fun getSurfaceSize(): Size = Size.UNKNOWN + + override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO + + override fun getDeviceInfo(): DeviceInfo = DeviceInfo.UNKNOWN + + override fun getDeviceVolume(): Int = 0 + + override fun isDeviceMuted(): Boolean = false + + @Deprecated("Deprecated in Java") + override fun setDeviceVolume(volume: Int) = Unit + + override fun setDeviceVolume(volume: Int, flags: Int) = Unit + + @Deprecated("Deprecated in Java") + override fun increaseDeviceVolume() = Unit + + override fun increaseDeviceVolume(flags: Int) = Unit + + @Deprecated("Deprecated in Java") + override fun decreaseDeviceVolume() = Unit + + override fun decreaseDeviceVolume(flags: Int) = Unit + + @Deprecated("Deprecated in Java") + override fun setDeviceMuted(muted: Boolean) = Unit + + override fun setDeviceMuted(muted: Boolean, flags: Int) = Unit + + override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) = Unit + + override fun event(eventId: Int) { + when (eventId) { + MPVLib.MpvEvent.MPV_EVENT_SEEK, + MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> { + val positionMs = getCurrentPosition() + val position = Player.PositionInfo( + SINGLE_WINDOW_UID, + getCurrentMediaItemIndex(), + getCurrentMediaItem(), + SINGLE_WINDOW_UID, + getCurrentPeriodIndex(), + positionMs, + positionMs, + INDEX_UNSET, + INDEX_UNSET + ) + listeners.forEach { + it.onPositionDiscontinuity(position, position, Player.DISCONTINUITY_REASON_SEEK) + } + } + + MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> { + playerError = PlaybackException( + "mpv event queue overflow", + null, + PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK + ) + listeners.forEach { + it.onPlayerErrorChanged(playerError) + it.onPlayerError(playerError) + } + } + } + } + + override fun eventProperty(property: String) = Unit + + override fun eventProperty(property: String, value: Long) { + when (property) { + "vid", "aid", "sid" -> { + listeners.forEach { it.onTracksChanged(getCurrentTracks()) } + } + } + } + + override fun eventProperty(property: String, value: Double) { + when (property) { + "duration" -> + listeners.forEach { + it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + } + + "speed" -> + listeners.forEach { + it.onPlaybackParametersChanged(PlaybackParameters(value.toFloat().coerceAtLeast(0f))) + } + + "volume" -> + listeners.forEach { + it.onVolumeChanged((value.coerceIn(0.0, 100.0) / 100.0).toFloat()) + } + } + } + + override fun eventProperty(property: String, value: Boolean) { + when (property) { + "pause" -> + listeners.forEach { + it.onPlayWhenReadyChanged( + !value, + PLAY_WHEN_READY_CHANGE_REASON_REMOTE + ) + it.onIsPlayingChanged(!value) + } + + "core-idle", "eof-reached" -> + listeners.forEach { it.onPlaybackStateChanged(getPlaybackState()) + it.onIsPlayingChanged(false) } + + "paused-for-cache" -> { + listeners.forEach { + it.onIsLoadingChanged(value) + it.onPlaybackStateChanged(getPlaybackState()) + } + } + } + } + + override fun eventProperty(property: String, value: String) = Unit -// private fun applyTrackSelectionParameters(parameters: TrackSelectionParameters) { -// if (trackGroupsByType.isEmpty()) { -// trackGroupsByType = buildTrackGroupsByType() -// } -// -// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_VIDEO)) { -// mpv.setPropertyString("vid", "no") -// } -// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_AUDIO)) { -// mpv.setPropertyString("aid", "no") -// } -// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_TEXT)) { -// mpv.setPropertyString("sid", "no") -// } -// -// for (override in parameters.overrides.values) { -// val binding = trackGroupsByType[override.getType()] -// ?.firstOrNull { it.group == override.mediaTrackGroup } -// ?: continue -// val selectedIndex = override.trackIndices.firstOrNull() ?: continue -// if (selectedIndex !in binding.mpvIds.indices) continue -// val selectedMpvId = binding.mpvIds[selectedIndex] -// -// when (override.getType()) { -// C.TRACK_TYPE_VIDEO -> mpv.setPropertyInt("vid", selectedMpvId) -// C.TRACK_TYPE_AUDIO -> mpv.setPropertyInt("aid", selectedMpvId) -// C.TRACK_TYPE_TEXT -> mpv.setPropertyInt("sid", selectedMpvId) -// } -// } -// } -// companion object { -// internal const val REQUEST_HEADER_NAMES_KEY = "omni.request.header_names" -// internal const val REQUEST_HEADER_VALUES_KEY = "omni.request.header_values" -// internal const val REQUEST_START_MS_KEY = "omni.request.start_ms" -// } } diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index fd6a369..c80b208 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -1,27 +1,24 @@ package dev.zoriya.omni import android.annotation.SuppressLint -import android.os.Bundle import android.view.SurfaceHolder import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.TrackSelectionOverride -import androidx.media3.common.TrackSelectionParameters import com.margelo.nitro.NitroModules import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.PlayerStatus import com.margelo.nitro.omni.Rendition import com.margelo.nitro.omni.Source import com.margelo.nitro.omni.Track -import java.util.ArrayList +import androidx.core.net.toUri +import androidx.media3.common.TrackSelectionOverride @SuppressLint("UnsafeOptInUsageError") class OmniPlayer : HybridOmniPlayerSpec() { private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!") val player = MpvPlayer(ctx) - override val eventMap = EventMap(player.mpv) - private var currentSource: Source? = null + override val eventMap = EventMap(player) override fun dispose() { super.dispose() @@ -30,6 +27,7 @@ class OmniPlayer : HybridOmniPlayerSpec() { player.release() } + private var currentSource: Source? = null override var source: Source get() = currentSource ?: throw IllegalStateException("source should be initialized before get") @@ -130,21 +128,14 @@ class OmniPlayer : HybridOmniPlayerSpec() { } override fun selectSubtitle(subtitle: Track?) { -// if (subtitle == null) { -// val params = player.trackSelectionParameters -// .buildUpon() -// .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) -// .build() -// player.trackSelectionParameters = params -// return -// } -// -// val params = player.trackSelectionParameters -// .buildUpon() -// .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false) -// .build() -// player.trackSelectionParameters = params -// selectTrack(C.TRACK_TYPE_TEXT, subtitle) + if (subtitle == null) { + player.trackSelectionParameters = player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) + .build() + return + } + selectTrack(C.TRACK_TYPE_TEXT, subtitle) } override fun selectRendition(rendition: Rendition?) { @@ -158,18 +149,18 @@ class OmniPlayer : HybridOmniPlayerSpec() { .setAlbumTitle(source.metadata?.album) .setArtist(source.metadata?.artist) .apply { - source.metadata?.imageLink?.let { setArtworkUri(android.net.Uri.parse(it)) } + source.metadata?.imageLink?.let { setArtworkUri(it.toUri()) } } .build() -// val subtitleConfigurations = source.subtitles.map { subtitle -> -// MediaItem.SubtitleConfiguration.Builder(android.net.Uri.parse(subtitle.link)) -// .setId(subtitle.id) -// .setLanguage(subtitle.language) -// .setLabel(subtitle.label) -// .setMimeType(subtitle.mimeType) -// .build() -// } + val subtitleConfigurations = source.subtitles.map { subtitle -> + MediaItem.SubtitleConfiguration.Builder(subtitle.link.toUri()) + .setId(subtitle.id) + .setLanguage(subtitle.language) + .setLabel(subtitle.label) + .setMimeType(subtitle.mimeType) + .build() + } // val headers = Bundle().apply { // putStringArrayList( @@ -186,7 +177,7 @@ class OmniPlayer : HybridOmniPlayerSpec() { // } val requestMetadata = MediaItem.RequestMetadata.Builder() - .setMediaUri(android.net.Uri.parse(src.uri)) + .setMediaUri(src.uri.toUri()) // .setExtras(headers) .build() @@ -195,54 +186,49 @@ class OmniPlayer : HybridOmniPlayerSpec() { .setMimeType(src.mimeType) .setMediaId(src.uri) .setMediaMetadata(mediaMetadata) -// .setSubtitleConfigurations(subtitleConfigurations) + .setSubtitleConfigurations(subtitleConfigurations) .setRequestMetadata(requestMetadata) return itemBuilder.build() } private fun tracksByType(trackType: Int): Array { - return emptyArray() -// val groups = player.currentTracks.groups.filter { it.type == trackType } -// if (groups.isEmpty()) return emptyArray() + 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 -// for (i in 0 until group.length) { -// val format = group.getTrackFormat(i) -// result.add( -// Track( -// id = format.id ?: mediaGroup.id, -// label = format.label, -// language = format.language, -// selected = group.isTrackSelected(i) -// ) -// ) -// } -// } -// return result.toTypedArray() + val result = ArrayList() + for (group in groups) { + val mediaGroup = group.mediaTrackGroup + for (i in 0 until group.length) { + val format = group.getTrackFormat(i) + result.add( + Track( + id = format.id ?: mediaGroup.id, + label = format.label, + language = format.language, + selected = group.isTrackSelected(i) + ) + ) + } + } + return result.toTypedArray() } private fun selectTrack(trackType: Int, track: Track) { - return -// val targetId = track.id -// val groups = player.currentTracks.groups.filter { it.type == trackType } -// for (group in groups) { -// val mediaGroup = group.mediaTrackGroup -// for (i in 0 until group.length) { -// val formatId = group.getTrackFormat(i).id ?: mediaGroup.id -// if (formatId != targetId) continue -// -// val override = TrackSelectionOverride(mediaGroup, i) -// val builder: TrackSelectionParameters.Builder = player.trackSelectionParameters -// .buildUpon() -// .setTrackTypeDisabled(trackType, false) -// .setOverrideForType(override) -// player.trackSelectionParameters = builder.build() -// return -// } -// } + val groups = player.currentTracks.groups.filter { it.type == trackType } + for (group in groups) { + for (i in 0 until group.length) { + val formatId = group.getTrackFormat(i).id ?: group.mediaTrackGroup.id + if (formatId != track.id) continue + + player.trackSelectionParameters = player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(trackType, false) + .setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, i)) + .build() + return + } + } } }