Properly implement BasePlayer interface

This commit is contained in:
2026-04-21 22:34:23 +02:00
parent ae5c69eb70
commit eee49c5e6b
3 changed files with 600 additions and 509 deletions
+95 -121
View File
@@ -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<NumberProperty, MutableSet<(Double) -> Unit>>()
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> 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()
}
}
+450 -319
View File
@@ -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<Player.Listener>()
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<Int, List<TrackGroupBinding>> = 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<MediaItem>, 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<MediaItem>,
startIndex: Int,
startPositionMs: Long
): ListenableFuture<*> {
override fun setMediaItems(mediaItems: List<MediaItem>, 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<MediaItem?>?
) = Unit
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) = Unit
override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List<MediaItem>) {
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<Int, MutableList<Entry>>()
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<Int, MutableList<Entry>>(type) { mutableListOf<Entry>() }
.add(Entry(id, format))
}
val selectedVideo = mpv.getPropertyInt("vid")
val selectedAudio = mpv.getPropertyInt("aid")
val selectedSubtitle = mpv.getPropertyInt("sid")
val result = ArrayList<Group>()
for ((type, entries) in grouped) {
if (entries.isEmpty()) continue
val group = TrackGroup("mpv-$type", *entries.map { it.format }.toTypedArray<Format>())
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<Int, List<TrackGroupBinding>> {
// val count = mpv.getPropertyInt("track-list/count") ?: 0
// if (count <= 0) return emptyMap()
//
// val grouped = LinkedHashMap<Int, MutableList<Pair<Int, Format>>>()
// 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<Int, List<TrackGroupBinding>>()
// 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<Tracks.Group>()
// 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"
// }
}
@@ -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<Track> {
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<Track>()
// 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<Track>()
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
}
}
}
}