Handle rendition selection

This commit is contained in:
2026-05-13 18:23:09 +02:00
parent 9e4fc4aa67
commit e68628daba
18 changed files with 152 additions and 60 deletions
@@ -1,6 +1,6 @@
package dev.zoriya.omni
import android.os.SystemClock
import android.annotation.SuppressLint
import androidx.media3.common.C
import androidx.media3.common.C.TRACK_TYPE_AUDIO
import androidx.media3.common.C.TRACK_TYPE_TEXT
@@ -16,6 +16,7 @@ import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import com.margelo.nitro.omni.BoolProperty
import com.margelo.nitro.omni.HybridOmniEventMapSpec
import com.margelo.nitro.omni.NumberProperty
@@ -23,6 +24,7 @@ import com.margelo.nitro.omni.PlayerStatus
import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Track
@SuppressLint("UnsafeOptInUsageError")
class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Listener {
private val onPrevListeners = mutableSetOf<() -> Unit>()
private val onNextListeners = mutableSetOf<() -> Unit>()
@@ -36,8 +38,10 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
private val stateListeners = mutableMapOf<NumberProperty, MutableSet<(Double) -> Unit>>()
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> Unit>>()
private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>()
private var lastTimePosDispatchMs = 0L
private var lastMediaItemIndex = 0
private var lastRendition: Rendition? = null
private var lastIsAutoQuality: Boolean? = null
init {
player.addListener(this)
@@ -61,6 +65,53 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
return null
}
private fun getCurrentRendition(): Rendition? {
val group = player.currentTracks.groups.firstOrNull {
it.isSelected && it.type == TRACK_TYPE_VIDEO
} ?: return null
val isAuto = player.trackSelectionParameters.overrides.none {
it.key.type == TRACK_TYPE_VIDEO
}
val currentIndex = when {
isAuto -> {
if (player.videoSize.width > 0 && player.videoSize.height > 0) {
(0 until group.length).firstOrNull { i ->
val format = group.getTrackFormat(i)
format.width == player.videoSize.width && format.height == player.videoSize.height
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
} ?: return null
val format = group.getTrackFormat(currentIndex)
return Rendition(
id = currentIndex.toString(),
width = format.width.toDouble().coerceAtLeast(0.0),
height = format.height.toDouble().coerceAtLeast(0.0),
bitrate = format.bitrate.toDouble().coerceAtLeast(0.0),
selected = true
)
}
private fun emitIsAutoQualityChange() {
val isAuto = player.trackSelectionParameters.overrides.none {
it.key.type == C.TRACK_TYPE_VIDEO
}
if (isAuto == lastIsAutoQuality) return
lastIsAutoQuality = isAuto
stateBoolListeners[BoolProperty.ISAUTOQUALITY]?.forEach { it(isAuto) }
}
private fun emitRenditionChange() {
val rendition = getCurrentRendition() ?: return
if (rendition == lastRendition) return
lastRendition = rendition
onRenditionChangeListeners.forEach { it(rendition) }
}
override fun onPlaybackStateChanged(playbackState: Int) {
val state = when (player.playbackState) {
STATE_IDLE -> PlayerStatus.IDLE
@@ -108,6 +159,13 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
)
)
}
emitIsAutoQualityChange()
emitRenditionChange()
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
emitIsAutoQualityChange()
emitRenditionChange()
}
override fun onPlayerError(error: PlaybackException) {
@@ -135,11 +193,9 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
newPosition: Player.PositionInfo,
reason: Int
) {
emitCoreState(forceCurrentTime = true)
}
private fun emitCoreState(forceCurrentTime: Boolean) {
emitCurrentTime(forceCurrentTime)
stateListeners[NumberProperty.CURRENTTIME]?.forEach {
it((player.currentPosition.toDouble() / 1000.0).coerceAtLeast(0.0))
}
stateListeners[NumberProperty.BUFFERED]?.forEach {
it((player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0))
}
@@ -149,15 +205,6 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
}
}
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))
}
}
override fun addStateListener(key: NumberProperty, cb: (value: Double) -> Unit) {
stateListeners.getOrPut(key) { mutableSetOf() }.add(cb)
}
@@ -19,6 +19,7 @@ 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.Format.NO_VALUE
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
@@ -337,12 +338,15 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
val label = mpv.getPropertyString("$base/title")
val language = mpv.getPropertyString("$base/lang")
val codec = mpv.getPropertyString("$base/codec")
val bitrate = mpv.getPropertyInt("$base/hls-bitrate")
?: mpv.getPropertyInt("$base/demux-bitrate")
val format = Format.Builder()
.setId(id.toString())
.setLabel(label)
.setLanguage(language)
.setCodecs(codec)
.setAverageBitrate(bitrate ?: NO_VALUE)
.build()
grouped.getOrPut<Int, MutableList<Entry>>(type) { mutableListOf<Entry>() }
@@ -612,13 +616,15 @@ class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
EVENT_TIMELINE_CHANGED,
EVENT_MEDIA_METADATA_CHANGED,
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_IS_PLAYING_CHANGED
EVENT_IS_PLAYING_CHANGED,
EVENT_TRACKS_CHANGED
)
) {
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
it.onMediaMetadataChanged(mediaMetadata)
it.onPlaybackStateChanged(STATE_READY)
it.onIsPlayingChanged(playWhenReady)
it.onTracksChanged(getCurrentTracks())
}
MPVLib.MpvEvent.MPV_EVENT_SEEK,
@@ -1,10 +1,8 @@
package dev.zoriya.omni
import android.R
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.media.metrics.TrackChangeEvent.TRACK_TYPE_VIDEO
import android.util.Log
import android.view.SurfaceHolder
import androidx.media3.common.C
@@ -33,8 +31,8 @@ import dev.zoriya.omni.utils.ThreadHelper.runOnMainThreadSync
class OmniPlayer : HybridOmniPlayerSpec() {
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
val player: Player = runOnMainThreadSync {
// ExoPlayer.Builder(ctx).build()
MpvPlayer(ctx)
// ExoPlayer.Builder(ctx).build()
MpvPlayer(ctx)
}
override val eventMap = EventMap(player)
@@ -66,7 +64,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
get() = currentSource
?: throw IllegalStateException("source should be initialized before get")
set(value) {
Log.e("omni", "update source")
Log.i("omni", "update source")
currentSource = value
val src = value.src.firstOrNull()
if (src == null) {
@@ -204,6 +202,12 @@ class OmniPlayer : HybridOmniPlayerSpec() {
override val subtitles by mainThreadProperty { tracksByType(C.TRACK_TYPE_TEXT) }
override val rendition by mainThreadProperty { getRenditions() }
override var isAutoQuality by mainThreadProperty {
player.trackSelectionParameters.overrides.none {
it.key.type == C.TRACK_TYPE_VIDEO
}
}
override fun play() {
runOnMainThreadSync { player.play() }
}
@@ -286,16 +290,28 @@ class OmniPlayer : HybridOmniPlayerSpec() {
player.currentTracks.groups.firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
?: return emptyArray()
val currentIndex = when {
isAutoQuality -> {
if (player.videoSize.width > 0 && player.videoSize.height > 0) {
(0 until group.length).firstOrNull { i ->
val format = group.getTrackFormat(i)
format.width == player.videoSize.width && format.height == player.videoSize.height
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
}
val result = ArrayList<Rendition>()
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
result.add(
Rendition(
id = format.id ?: group.mediaTrackGroup.id,
id = i.toString(),
width = format.width.toDouble().coerceAtLeast(0.0),
height = format.height.toDouble().coerceAtLeast(0.0),
bitrate = format.bitrate.toDouble().coerceAtLeast(0.0),
selected = group.isTrackSelected(i)
selected = i == currentIndex
)
)
}
@@ -316,17 +332,10 @@ class OmniPlayer : HybridOmniPlayerSpec() {
player.currentTracks.groups.find { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
?: return@runOnMainThreadSync
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
val formatId = format.id ?: group.mediaTrackGroup.id
if (formatId == rendition.id) continue
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, i))
.build()
return@runOnMainThreadSync
}
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, rendition.id.toInt()))
.build()
}
}
@@ -361,7 +370,7 @@ class OmniPlayerService : MediaSessionService() {
.build()
setMediaNotificationProvider(DefaultMediaNotificationProvider.Builder(this).build().apply {
setSmallIcon(applicationInfo.icon.takeIf { it != 0 } ?: R.drawable.ic_media_play)
setSmallIcon(applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_media_play)
})
addSession(mediaSession)
setShowNotificationForIdlePlayer(SHOW_NOTIFICATION_FOR_IDLE_PLAYER_ALWAYS)