fix(android): route decode policy by the video track mpv will select
The Dolby Vision and Hi10 decode policies run in mpv's on_preloaded hook, before track selection, and read the first demuxed video track. A file whose first video track is not the one mpv picks (attached pictures, a default-flagged second track, --vlang, an explicit vid) got the wrong routing. The hook now resolves one pending track for both policies: an explicit vid=no or vid=N is authoritative on its own, and auto asks the fork's pending-vid property, which runs mpv's own default selection ahead of time. On a libmpv without that property the first-track guess stays, logged once. The predecoder gate is unchanged (#2065). The fork patch lands separately through the native pin flow.
This commit is contained in:
@@ -48,6 +48,29 @@ internal object GpuVoPolicy {
|
||||
*/
|
||||
fun needsSoftwareDecode(codec: String?, codecProfile: String?, hardwareHigh10: Boolean): Boolean = !hardwareHigh10 && codec == "h264" && codecProfile?.startsWith("High 10") == true
|
||||
|
||||
/**
|
||||
* The video track the per-file policies ([needsDvReshaping],
|
||||
* [needsSoftwareDecode]) decide for, or null when no video track will be
|
||||
* selected. They run inside on_preloaded, where the track list is complete
|
||||
* but nothing is selected yet, so "the selected track" does not exist:
|
||||
* `vid` still reads the option value, and mpv's own selection
|
||||
* (default/forced flags, --vlang, attached pictures skipped) only runs
|
||||
* after the hook. [vid] is the `vid` property: `no` and an explicit id are
|
||||
* authoritative on their own; `auto` defers to [pendingVid], the fork's
|
||||
* `pending-vid` property, which runs that selection ahead of time. A null
|
||||
* [pendingVid] means the libmpv has no such property, and the first track
|
||||
* in [videoTrackIds] (track-list order) is the best guess left — wrong for
|
||||
* files whose first video track is not the one mpv picks.
|
||||
*/
|
||||
fun pendingVideoTrackId(vid: String?, pendingVid: String?, videoTrackIds: List<Long>): Long? {
|
||||
val requested = when {
|
||||
vid == null || vid == "auto" -> pendingVid ?: return videoTrackIds.firstOrNull()
|
||||
else -> vid
|
||||
}
|
||||
val id = requested.toLongOrNull() ?: return null
|
||||
return id.takeIf { it in videoTrackIds }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a GL vo session should drop to the cheap render tier: bilinear
|
||||
* scalers and no dither. Keyed on `GL_EXT_texture_norm16` being absent,
|
||||
|
||||
@@ -139,6 +139,9 @@ class MpvPlayerCore private constructor(
|
||||
|
||||
private val parkedHwdec = java.util.concurrent.atomic.AtomicReference<String?>()
|
||||
|
||||
/** Whether the missing `pending-vid` property was logged; hook-serial. */
|
||||
private var pendingVidUnavailableLogged = false
|
||||
|
||||
/** mpv option -> the default it carried before the cheap render tier
|
||||
* replaced it; empty while the tier is off. See [applyRenderTier]. */
|
||||
private val cheapRenderRestore = LinkedHashMap<String, String>()
|
||||
@@ -498,12 +501,15 @@ class MpvPlayerCore private constructor(
|
||||
// Per-file decode routing runs inside mpv's on_preloaded hook:
|
||||
// the demuxer has opened the file, no decoder exists yet, and
|
||||
// mpv waits for the answer. file-loaded would be too late — the
|
||||
// MediaCodec decoder is already created by then (#2065).
|
||||
// MediaCodec decoder is already created by then (#2065). Nothing
|
||||
// is selected yet either, so the track both policies decide for
|
||||
// is resolved once here, from mpv's pending selection.
|
||||
p.hookHandler = { name ->
|
||||
if (name == "on_preloaded" && !disposing) {
|
||||
withContext(mpvWriteDispatcher) {
|
||||
applyDvReshapePolicy(p)
|
||||
applySoftwareDecodePolicy(p)
|
||||
val track = pendingVideoTrack(p)
|
||||
applyDvReshapePolicy(p, track)
|
||||
applySoftwareDecodePolicy(p, track)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,12 +853,14 @@ class MpvPlayerCore private constructor(
|
||||
/**
|
||||
* Per-file Dolby Vision routing, decided from the bitstream: mpv exports
|
||||
* the DOVI configuration record's profile on the track list (never trust
|
||||
* server metadata for this — it mis-tags DV routinely). Re-evaluated on
|
||||
* every file-loaded, so a following non-P5 file restores hardware decode
|
||||
* and returns to the video plane.
|
||||
* server metadata for this — it mis-tags DV routinely; mpv omits the
|
||||
* field when the bitstream carries no record). Re-evaluated on every
|
||||
* file, so a following non-P5 file restores hardware decode and returns
|
||||
* to the video plane. [track] is the pending video track, see
|
||||
* [pendingVideoTrack].
|
||||
*/
|
||||
private suspend fun applyDvReshapePolicy(p: MpvPlayer) {
|
||||
val profile = selectedVideoDvProfile(p)
|
||||
private suspend fun applyDvReshapePolicy(p: MpvPlayer, track: org.json.JSONObject?) {
|
||||
val profile = track?.takeIf { it.has("dolby-vision-profile") }?.getLong("dolby-vision-profile")
|
||||
val needs = GpuVoPolicy.needsDvReshaping(
|
||||
dvProfile = profile,
|
||||
conversionMode = currentDvConversionMode,
|
||||
@@ -871,9 +879,9 @@ class MpvPlayerCore private constructor(
|
||||
* The profile comes from the container's avcC record (fork patch), so it
|
||||
* is known inside on_preloaded. Distinct from [GpuVoPolicy.REASON_SW_DECODE]:
|
||||
* that one follows `hwdec-current`, which is still blank at this point.
|
||||
* [track] is the pending video track, see [pendingVideoTrack].
|
||||
*/
|
||||
private suspend fun applySoftwareDecodePolicy(p: MpvPlayer) {
|
||||
val track = videoTracks(p).firstOrNull()
|
||||
private suspend fun applySoftwareDecodePolicy(p: MpvPlayer, track: org.json.JSONObject?) {
|
||||
val codec = track?.optString("codec")
|
||||
val codecProfile = track?.optString("codec-profile")
|
||||
val hardwareHigh10 = MediaCodecQuery.hardwareAvcHigh10Support()
|
||||
@@ -954,13 +962,29 @@ class MpvPlayerCore private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected video track's Dolby Vision profile, or null for non-DV content
|
||||
* (mpv omits the field when the bitstream carries no DOVI configuration
|
||||
* record).
|
||||
* The video track mpv is about to select, resolved inside on_preloaded
|
||||
* where nothing is selected yet. `vid=no` and an explicit `vid=N` answer
|
||||
* on their own (the hook never re-selects: an explicit choice stays the
|
||||
* user's); `auto` asks the fork's `pending-vid`, which runs mpv's own
|
||||
* default selection ahead of time. Without that property (a libmpv
|
||||
* predating the fork patch) the first track is the only guess left; the
|
||||
* gap is logged once so a wrong policy on a multi-video file is traceable.
|
||||
* Decision in [GpuVoPolicy.pendingVideoTrackId].
|
||||
*/
|
||||
private suspend fun selectedVideoDvProfile(p: MpvPlayer): Long? = videoTracks(p).firstOrNull()
|
||||
?.takeIf { it.has("dolby-vision-profile") }
|
||||
?.getLong("dolby-vision-profile")
|
||||
private suspend fun pendingVideoTrack(p: MpvPlayer): org.json.JSONObject? {
|
||||
val tracks = videoTracks(p)
|
||||
if (tracks.isEmpty()) return null
|
||||
val vid = p.getString("vid")
|
||||
val auto = vid == null || vid == "auto"
|
||||
val pendingVid = if (auto) p.getString("pending-vid") else null
|
||||
if (auto && pendingVid == null && !pendingVidUnavailableLogged) {
|
||||
pendingVidUnavailableLogged = true
|
||||
Log.w(TAG, "libmpv has no pending-vid property; decode routing assumes the first video track")
|
||||
}
|
||||
val id = GpuVoPolicy.pendingVideoTrackId(vid, pendingVid, tracks.map { it.optLong("id") })
|
||||
Log.d(TAG, "Pending video track: vid=$vid pending-vid=$pendingVid -> ${id ?: "none"} of ${tracks.size}")
|
||||
return tracks.firstOrNull { it.optLong("id") == id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Observed rather than derived from the hardware-decoding setting because
|
||||
|
||||
@@ -101,6 +101,40 @@ class GpuVoPolicyTest {
|
||||
assertFalse(GpuVoPolicy.needsSoftwareDecode(null, "High 10", hardwareHigh10 = false))
|
||||
}
|
||||
|
||||
// The per-file policies run inside on_preloaded, before mpv selects a
|
||||
// track, so the track they decide for comes from the pending selection.
|
||||
|
||||
@Test
|
||||
fun `auto selection follows mpv's pending choice, not track-list order`() {
|
||||
// Cover art first, the default-flagged feature second: mpv picks 2.
|
||||
assertEquals(2L, GpuVoPolicy.pendingVideoTrackId("auto", "2", listOf(1L, 2L)))
|
||||
assertEquals(1L, GpuVoPolicy.pendingVideoTrackId("auto", "1", listOf(1L, 2L)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `explicit vid answers on its own`() {
|
||||
assertEquals(2L, GpuVoPolicy.pendingVideoTrackId("2", pendingVid = null, videoTrackIds = listOf(1L, 2L)))
|
||||
// A user's explicit choice is never re-selected, even when mpv would
|
||||
// pick differently.
|
||||
assertEquals(1L, GpuVoPolicy.pendingVideoTrackId("1", "2", listOf(1L, 2L)))
|
||||
// No such track: mpv selects nothing, so nothing is routed.
|
||||
assertNull(GpuVoPolicy.pendingVideoTrackId("7", "2", listOf(1L, 2L)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no video selection yields no track`() {
|
||||
assertNull(GpuVoPolicy.pendingVideoTrackId("no", "1", listOf(1L, 2L)))
|
||||
assertNull(GpuVoPolicy.pendingVideoTrackId("auto", "no", listOf(1L, 2L)))
|
||||
assertNull(GpuVoPolicy.pendingVideoTrackId("auto", "1", emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `without the pending-vid property the first track is the fallback`() {
|
||||
assertEquals(1L, GpuVoPolicy.pendingVideoTrackId("auto", null, listOf(1L, 2L)))
|
||||
assertEquals(1L, GpuVoPolicy.pendingVideoTrackId(null, null, listOf(1L, 2L)))
|
||||
assertNull(GpuVoPolicy.pendingVideoTrackId("auto", null, emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cheap render tier needs a GL vo on a driver without norm16`() {
|
||||
assertTrue(GpuVoPolicy.needsCheapRenderTier(glVoActive = true, textureNorm16 = false))
|
||||
|
||||
Reference in New Issue
Block a user