fix(android): improve Dolby Vision fallback

This commit is contained in:
edde746
2026-05-27 21:02:48 +02:00
parent 7b4e399013
commit 3b4110d7e5
4 changed files with 331 additions and 62 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ extern "C" JNIEXPORT jint JNICALL Java_com_edde746_plezy_exoplayer_DoviBridge_na
}
}
// Convert to target profile (mode 2 = P8.1 with no-op curves)
// Mode 2 matches Kodi's P8.1 compatibility path and sets luma/chroma curves to no-op.
int32_t ret = dovi_convert_rpu_with_mode(rpu, static_cast<uint8_t>(mode));
if (ret != 0) {
err = dovi_rpu_get_error(rpu);
@@ -1,18 +1,35 @@
package com.edde746.plezy.exoplayer
import android.content.Context
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.os.Build
import android.util.Log
import android.view.Display
import android.view.WindowManager
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
object DoviBridge {
private const val TAG = "DoviBridge"
private val DOLBY_VISION_MIME_TYPES = setOf(
"video/dolby-vision",
"video/hevcdv",
"video/dv_hevc"
)
const val CONVERT_FAILED = -1
const val DESTINATION_TOO_SMALL = -2
private data class DvProfileLevel(val profile: Int, val level: Int)
private data class DvDecoderCapability(
val name: String,
val mimeType: String,
val profileLevels: List<DvProfileLevel>
)
private val nativeLoaded: Boolean by lazy {
try {
System.loadLibrary("dovi_bridge")
@@ -29,46 +46,207 @@ object DoviBridge {
fun isAvailable(): Boolean = conversionPathReady
private fun deviceSupportsDvProfile(profile: Int, minApi: Int = 0): Boolean {
private val dolbyVisionDecoders: List<DvDecoderCapability> by lazy {
try {
if (Build.VERSION.SDK_INT < minApi) return false
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
return codecList.codecInfos.any { info ->
!info.isEncoder &&
info.supportedTypes.any { type ->
type.equals("video/dolby-vision", ignoreCase = true) &&
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
codecList.codecInfos.flatMap { info ->
if (info.isEncoder) {
emptyList()
} else {
info.supportedTypes.mapNotNull { type ->
if (!DOLBY_VISION_MIME_TYPES.contains(type.lowercase())) return@mapNotNull null
val capabilities = runCatching { info.getCapabilitiesForType(type) }
.onFailure { Log.w(TAG, "Failed to query ${info.name} capabilities for $type", it) }
.getOrNull()
?: return@mapNotNull null
DvDecoderCapability(
name = info.name,
mimeType = type,
profileLevels = capabilities.profileLevels.map { DvProfileLevel(it.profile, it.level) }
)
}
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to query DV profile $profile support", e)
return false
Log.w(TAG, "Failed to query native Dolby Vision decoder support", e)
emptyList()
}
}
val hasNativeDolbyVisionDecoder: Boolean by lazy {
dolbyVisionDecoders.isNotEmpty().also {
Log.i(TAG, "Native Dolby Vision decoder available: $it; decoders=${describeDolbyVisionDecoders()}")
}
}
private fun deviceAdvertisesDvProfile(profile: Int, minApi: Int = 0): Boolean {
if (Build.VERSION.SDK_INT < minApi) return false
return dolbyVisionDecoders.any { decoder ->
decoder.profileLevels.any { it.profile == profile }
}
}
val deviceSupportsDvProfile7: Boolean by lazy {
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr)
.also { Log.i(TAG, "Device DV Profile 7 support: $it") }
deviceAdvertisesDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtb)
.also { Log.i(TAG, "Device advertises exact DV Profile 7 (DvheDtb): $it") }
}
val deviceSupportsDvProfile8: Boolean by lazy {
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
.also { Log.i(TAG, "Device DV Profile 8 support: $it") }
deviceAdvertisesDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
.also { Log.i(TAG, "Device advertises DV Profile 8 (DvheSt): $it") }
}
fun getConversionMode(): DvConversionMode = when {
!isAvailable() -> DvConversionMode.DISABLED
deviceSupportsDvProfile7 -> DvConversionMode.DISABLED // try native first; ExoPlayerCore retries with conversion on failure
deviceSupportsDvProfile8 -> DvConversionMode.DV81
else -> DvConversionMode.HEVC_STRIP
fun displaySupportsDolbyVision(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.i(TAG, "Display Dolby Vision support: false (HDR capabilities require API 24, device API=${Build.VERSION.SDK_INT})")
return false
}
val display = getCurrentDisplay(context)
if (display == null) {
Log.i(TAG, "Display Dolby Vision support: false (no active display)")
return false
}
val hdrTypes = runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
display.mode.supportedHdrTypes
} else {
@Suppress("DEPRECATION")
val legacyHdrTypes = display.hdrCapabilities.supportedHdrTypes
legacyHdrTypes
}
}.getOrElse { error ->
Log.w(TAG, "Display Dolby Vision support: false (failed to query HDR types)", error)
return false
}
val supported = hdrTypes.contains(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)
Log.i(TAG, "Display Dolby Vision support: $supported; hdrTypes=${describeHdrTypes(hdrTypes)}")
return supported
}
fun logSupportSummary(context: Context) {
Log.i(
TAG,
"DV support summary: bridgeReady=${isAvailable()}, displayDV=${displaySupportsDolbyVision(context)}, " +
"nativeDecoder=$hasNativeDolbyVisionDecoder, advertisedP7=$deviceSupportsDvProfile7, " +
"advertisedP8=$deviceSupportsDvProfile8, decoders=${describeDolbyVisionDecoders()}"
)
}
fun getConversionMode(context: Context): DvConversionMode {
val bridgeReady = isAvailable()
val displayDv = displaySupportsDolbyVision(context)
val nativeDecoder = hasNativeDolbyVisionDecoder
val advertisedP7 = deviceSupportsDvProfile7
val advertisedP8 = deviceSupportsDvProfile8
val mode = when {
!displayDv -> DvConversionMode.HEVC_STRIP
advertisedP7 -> DvConversionMode.DISABLED
advertisedP8 && bridgeReady -> DvConversionMode.DV81
else -> DvConversionMode.HEVC_STRIP
}
val reason = when {
!displayDv -> "active display does not support Dolby Vision; stripping DV metadata for HEVC fallback"
advertisedP7 -> "active display supports Dolby Vision and decoder advertises exact Profile 7; trying native DV first"
advertisedP8 && bridgeReady -> "active display supports Dolby Vision and decoder advertises Profile 8; converting to Profile 8.1"
!bridgeReady -> "conversion bridge unavailable; stripping DV metadata for HEVC fallback"
else -> "Dolby Vision output path is unavailable; stripping DV metadata for HEVC fallback"
}
Log.i(
TAG,
"AUTO DV decision: mode=$mode; reason=$reason; bridgeReady=$bridgeReady, " +
"displayDV=$displayDv, nativeDecoder=$nativeDecoder, advertisedP7=$advertisedP7, advertisedP8=$advertisedP8"
)
return mode
}
/** Get the fallback mode when native DV7 decoding fails. */
fun getDv7FallbackMode(): DvConversionMode = when {
deviceSupportsDvProfile8 -> DvConversionMode.DV81
else -> DvConversionMode.HEVC_STRIP
fun getDv7FallbackMode(context: Context): DvConversionMode {
val bridgeReady = isAvailable()
val displayDv = displaySupportsDolbyVision(context)
val advertisedP8 = deviceSupportsDvProfile8
val mode = if (displayDv && advertisedP8 && bridgeReady) DvConversionMode.DV81 else DvConversionMode.HEVC_STRIP
val reason = when {
mode == DvConversionMode.DV81 -> "display supports Dolby Vision and decoder advertises Profile 8"
!bridgeReady -> "conversion bridge unavailable; stripping DV metadata for HEVC fallback"
else -> "Dolby Vision output or Profile 8 support is unavailable"
}
Log.i(
TAG,
"DV7 fallback decision: mode=$mode; reason=$reason; bridgeReady=$bridgeReady, " +
"displayDV=$displayDv, advertisedP8=$advertisedP8"
)
return mode
}
private fun getCurrentDisplay(context: Context): Display? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display
} else {
@Suppress("DEPRECATION")
(context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager)?.defaultDisplay
}
private fun describeDolbyVisionDecoders(): String {
if (dolbyVisionDecoders.isEmpty()) return "none"
return dolbyVisionDecoders.joinToString { decoder ->
val profiles = if (decoder.profileLevels.isEmpty()) {
"none"
} else {
decoder.profileLevels.joinToString(prefix = "[", postfix = "]") {
"${describeDvProfile(it.profile)}/${describeDvLevel(it.level)}"
}
}
"${decoder.name}(${decoder.mimeType}, profiles=$profiles)"
}
}
private fun describeHdrTypes(hdrTypes: IntArray): String {
if (hdrTypes.isEmpty()) return "none"
return hdrTypes.joinToString(prefix = "[", postfix = "]") { type ->
when (type) {
Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION -> "DOLBY_VISION"
Display.HdrCapabilities.HDR_TYPE_HDR10 -> "HDR10"
Display.HdrCapabilities.HDR_TYPE_HLG -> "HLG"
Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS -> "HDR10_PLUS"
else -> "unknown(${hex(type)})"
}
}
}
private fun describeDvProfile(profile: Int): String = when (profile) {
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavPer -> "P0/DvavPer"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavPen -> "P1/DvavPen"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDer -> "P2/DvheDer"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDen -> "P3/DvheDen"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr -> "P4/DvheDtr"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheStn -> "P5/DvheStn"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDth -> "P6/DvheDth"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtb -> "P7/DvheDtb"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt -> "P8/DvheSt"
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavSe -> "P9/DvavSe"
else -> "unknown(${hex(profile)})"
}
private fun describeDvLevel(level: Int): String = when (level) {
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelHd24 -> "L1/Hd24"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelHd30 -> "L2/Hd30"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd24 -> "L3/Fhd24"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd30 -> "L4/Fhd30"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60 -> "L5/Fhd60"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd24 -> "L6/Uhd24"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd30 -> "L7/Uhd30"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd48 -> "L8/Uhd48"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd60 -> "L9/Uhd60"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd120 -> "L10/Uhd120"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevel8k30 -> "L11/8k30"
MediaCodecInfo.CodecProfileLevel.DolbyVisionLevel8k60 -> "L12/8k60"
else -> "unknown(${hex(level)})"
}
private fun hex(value: Int): String = "0x${value.toString(16)}"
fun convertRpuNalu(
payload: ByteArray,
payloadOffset: Int,
@@ -11,9 +11,10 @@ import androidx.media3.extractor.TrackOutput
/**
* TrackOutput wrapper that processes DV Profile 7 HEVC samples based on conversion mode:
*
* - DV81: Convert RPU NALs via libdovi to Profile 8.1, present as video/dolby-vision
* with dvhe.08.XX codec string. Preserves dynamic tone mapping metadata.
* - HEVC_STRIP: Strip all DV enhancement layers, present as plain video/hevc.
* - DV81: Convert RPU NALs via libdovi mode 2 to Profile 8.1, present as
* video/dolby-vision with dvhe.08.XX codec string. Conversion failures drop
* that RPU instead of forwarding Profile 7 metadata into a Profile 8.1 stream.
* - HEVC_STRIP: Strip DV RPU/EL NALs, present as plain video/hevc.
*
* Two modes of NAL framing (auto-detected):
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
@@ -22,8 +23,7 @@ import androidx.media3.extractor.TrackOutput
* NAL processing:
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
* - nuh_layer_id > 0: Enhancement layer NAL → strip
* - All retained NALs: normalize nuh_layer_id to 0
* - All other NALs: pass through unchanged, matching Kodi's compatibility path
*
* All buffers are reused across samples to minimize GC pressure on the hot path.
*/
@@ -47,6 +47,10 @@ class DoviConvertingTrackOutput(
private set
var strippedNalCount = 0L
private set
var strippedRpuNalCount = 0L
private set
var strippedElNalCount = 0L
private set
var strippedInitNalCount = 0L
private set
var convertedRpuCount = 0L
@@ -79,11 +83,17 @@ class DoviConvertingTrackOutput(
override fun format(format: Format) {
if (!conversionActive) {
val codecs = format.codecs
if (codecs != null && codecs.startsWith("dvhe.07")) {
if (isDvProfile7Codec(codecs)) {
val codecString = codecs ?: ""
conversionActive = true
logInfo("DV Profile 7 detected ($codecs), mode=$dvMode")
logInfo("DV Profile 7 detected ($codecString), mode=$dvMode")
when (dvMode) {
DvConversionMode.DV81 -> logInfo("DV81: Kodi-style libdovi conversion (convert RPU type 62, drop EL type 63)")
DvConversionMode.HEVC_STRIP -> logInfo("HEVC_STRIP: Kodi-compatible strip (drop RPU type 62 and EL type 63 only)")
else -> Unit
}
logInfo(
"Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
"Original format: mime=${format.sampleMimeType}, codecs=$codecString, " +
"initData=${format.initializationData.size} entries " +
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})"
)
@@ -92,7 +102,7 @@ class DoviConvertingTrackOutput(
val newFormat = when (dvMode) {
DvConversionMode.DV81 -> {
// Parse DV level from codec string: "dvhe.07.06" → 6
val level = codecs.split('.').getOrNull(2)?.toIntOrNull() ?: 6
val level = codecString.split('.').getOrNull(2)?.toIntOrNull() ?: 6
val newCodecs = "dvhe.08.%02d".format(level)
val dvConfigRecord = buildDv81ConfigRecord(level)
logInfo("DV81: rewriting to $newCodecs, config=${dvConfigRecord.size}B")
@@ -360,7 +370,6 @@ class DoviConvertingTrackOutput(
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
outputLen += 4
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
normalizeLayerId(outputBuf, outputLen)
outputLen += nalLen
kept++
} else if (action == NalAction.CONVERT) {
@@ -368,16 +377,15 @@ class DoviConvertingTrackOutput(
if (convertedLen >= 0) {
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
outputLen += 4
normalizeLayerId(outputBuf, outputLen)
outputLen += convertedLen
convertedRpuCount++
kept++
} else {
strippedNalCount++
recordStrippedNal(action)
stripped++
}
} else {
strippedNalCount++
recordStrippedNal(action)
stripped++
}
}
@@ -428,7 +436,6 @@ class DoviConvertingTrackOutput(
writeInt32BE(outputBuf, outputLen, nalLen)
outputLen += 4
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
normalizeLayerId(outputBuf, outputLen)
outputLen += nalLen
kept++
} else if (action == NalAction.CONVERT) {
@@ -436,16 +443,15 @@ class DoviConvertingTrackOutput(
if (convertedLen >= 0) {
writeInt32BE(outputBuf, outputLen, convertedLen)
outputLen += 4
normalizeLayerId(outputBuf, outputLen)
outputLen += convertedLen
convertedRpuCount++
kept++
} else {
strippedNalCount++
recordStrippedNal(action)
stripped++
}
} else {
strippedNalCount++
recordStrippedNal(action)
stripped++
}
@@ -461,7 +467,17 @@ class DoviConvertingTrackOutput(
}
}
private enum class NalAction { KEEP, STRIP, CONVERT }
private enum class NalAction { KEEP, STRIP_RPU, STRIP_EL, CONVERT }
private fun recordStrippedNal(action: NalAction) {
strippedNalCount++
// CONVERT reaches here only after conversion fails; raw P7 RPUs are not safe in P8.1 output.
when (action) {
NalAction.STRIP_RPU, NalAction.CONVERT -> strippedRpuNalCount++
NalAction.STRIP_EL -> strippedElNalCount++
NalAction.KEEP -> Unit
}
}
private fun convertRpuIntoOutput(nalStart: Int, nalLen: Int, outputOffset: Int): Int {
ensureOutputCapacity(outputOffset + MAX_CONVERTED_RPU_SIZE)
@@ -565,7 +581,6 @@ class DoviConvertingTrackOutput(
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
outputLen += 4
System.arraycopy(data, nalStart, outputBuf, outputLen, nalLen)
normalizeLayerId(outputBuf, outputLen)
outputLen += nalLen
kept++
}
@@ -617,25 +632,22 @@ class DoviConvertingTrackOutput(
/** Classify a NAL at sampleBuf[offset..offset+len) without copying. */
private fun processNalInline(offset: Int, len: Int): NalAction = classifyNal(sampleBuf, offset, len, convertRpu = dvMode == DvConversionMode.DV81)
private fun isDvProfile7Codec(codecs: String?): Boolean {
val normalized = codecs?.lowercase() ?: return false
return normalized.startsWith("dvhe.07") || normalized.startsWith("dvh1.07")
}
private fun classifyNal(data: ByteArray, offset: Int, len: Int, convertRpu: Boolean): NalAction {
if (len < 2) return NalAction.KEEP
val nalType = (data[offset].toInt() ushr 1) and 0x3F
val nuhLayerId = ((data[offset].toInt() and 1) shl 5) or
((data[offset + 1].toInt() ushr 3) and 0x1F)
return when {
nalType == NAL_TYPE_UNSPEC62 && convertRpu -> NalAction.CONVERT
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> NalAction.STRIP
nalType == NAL_TYPE_UNSPEC62 -> NalAction.STRIP_RPU
nalType == NAL_TYPE_UNSPEC63 -> NalAction.STRIP_EL
else -> NalAction.KEEP
}
}
private fun normalizeLayerId(data: ByteArray, offset: Int) {
if (data.size - offset >= 2) {
data[offset] = (data[offset].toInt() and 0xFE).toByte()
data[offset + 1] = (data[offset + 1].toInt() and 0x07).toByte()
}
}
private fun writeInt32BE(buf: ByteArray, offset: Int, value: Int) {
buf[offset] = ((value ushr 24) and 0xFF).toByte()
buf[offset + 1] = ((value ushr 16) and 0xFF).toByte()
@@ -300,12 +300,15 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
private var dvMode: DvConversionMode = DvConversionMode.DISABLED
private var debugDvModeOverride: DvConversionMode? = null
private var dv7RetryAttempted = false
private var currentVideoFormat: Format? = null
private var loggedNativeDvSelectionKey: String? = null
private var loggedNativeDvFirstFrame = false
@Volatile private var activeDoviMkvWrapper: DoviExtractorWrapper? = null
@Volatile private var activeDoviMp4Wrapper: DoviExtractorWrapper? = null
private fun getConfiguredDvMode(): DvConversionMode = debugDvModeOverride ?: DoviBridge.getConversionMode()
private fun getConfiguredDvMode(): DvConversionMode = debugDvModeOverride ?: DoviBridge.getConversionMode(activity)
fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean {
if (isInitialized) {
@@ -315,10 +318,10 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
tunnelingUserEnabled = tunnelingEnabled
this.dvMode = getConfiguredDvMode()
DoviBridge.logSupportSummary(activity)
Log.i(
TAG,
"DV conversion: mode=$dvMode, bridge=${DoviBridge.isAvailable()}, " +
"deviceDV7=${DoviBridge.deviceSupportsDvProfile7}, deviceDV8=${DoviBridge.deviceSupportsDvProfile8}"
"DV conversion: mode=$dvMode, override=${debugDvModeOverride?.name ?: "AUTO"}"
)
disposing = false
@@ -889,11 +892,15 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val audioGroup = tracks.groups.firstOrNull { it.type == C.TRACK_TYPE_AUDIO && it.isSelected }
if (videoGroup != null) {
val vf = videoGroup.mediaTrackGroup.getFormat(0)
currentVideoFormat = vf
val hdr = vf.colorInfo?.let { ci ->
val transfer = ci.colorTransfer
if (transfer != null && transfer != 0) " HDR(transfer=$transfer)" else ""
} ?: ""
emitLog("info", "tracks", "Video: ${vf.codecs} ${vf.width}x${vf.height}$hdr")
logNativeDvSelectionIfNeeded(vf)
} else {
currentVideoFormat = null
}
if (audioGroup != null) {
val af = audioGroup.mediaTrackGroup.getFormat(0)
@@ -967,22 +974,85 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
)
}
private fun isDvProfile7Format(format: Format): Boolean {
val codecs = format.codecs?.lowercase() ?: return false
return codecs.startsWith("dvhe.07") || codecs.startsWith("dvh1.07")
}
private fun findDv7VideoFormat(): Format? {
currentVideoFormat?.takeIf { isDvProfile7Format(it) }?.let { return it }
val tracks = exoPlayer?.currentTracks ?: return null
for (group in tracks.groups) {
if (group.type != C.TRACK_TYPE_VIDEO) continue
for (i in 0 until group.mediaTrackGroup.length) {
val format = group.mediaTrackGroup.getFormat(i)
if (isDvProfile7Format(format)) return format
}
}
return null
}
private fun describeVideoFormat(format: Format?): String {
if (format == null) return "none"
return "mime=${format.sampleMimeType}, codecs=${format.codecs}, size=${format.width}x${format.height}"
}
private fun logNativeDvSelectionIfNeeded(format: Format) {
if (dvMode != DvConversionMode.DISABLED || !isDvProfile7Format(format)) return
val key = "${format.sampleMimeType}|${format.codecs}|${format.width}x${format.height}"
if (loggedNativeDvSelectionKey == key) return
loggedNativeDvSelectionKey = key
emitLog(
"info",
"dv-native",
"Selected DV Profile 7 for native playback: ${describeVideoFormat(format)}, " +
"displayDV=${DoviBridge.displaySupportsDolbyVision(activity)}, " +
"nativeDecoder=${DoviBridge.hasNativeDolbyVisionDecoder}, " +
"advertisedP7=${DoviBridge.deviceSupportsDvProfile7}, advertisedP8=${DoviBridge.deviceSupportsDvProfile8}"
)
}
private fun logNativeDvFirstFrameIfNeeded() {
if (loggedNativeDvFirstFrame || dvMode != DvConversionMode.DISABLED) return
val format = currentVideoFormat ?: return
if (!isDvProfile7Format(format)) return
loggedNativeDvFirstFrame = true
emitLog(
"info",
"dv-native",
"Native DV Profile 7 playback confirmed: ${describeVideoFormat(format)}, decoder=${decoderInitName ?: "unknown"}"
)
}
/**
* When native DV7 decoding fails (device falsely advertises DV7 support),
* upgrade to DV7→8.1 conversion or HEVC strip and reload the media.
* Returns true if retry was initiated.
* When native DV7 decoding fails, upgrade to DV7→8.1 conversion or HEVC strip
* and reload the media. Returns true if retry was initiated.
*/
private fun retryWithDvConversion(reason: String): Boolean {
if (dv7RetryAttempted) return false
if (debugDvModeOverride == DvConversionMode.DISABLED) return false
if (dvMode != DvConversionMode.DISABLED) return false
if (!DoviBridge.isAvailable()) return false
if (debugDvModeOverride == DvConversionMode.DISABLED) {
emitLog("debug", "dv-fallback", "Skipping DV conversion retry for $reason: native/disabled mode is forced")
return false
}
if (dvMode != DvConversionMode.DISABLED) {
emitLog("debug", "dv-fallback", "Skipping DV conversion retry for $reason: conversion already active ($dvMode)")
return false
}
if (currentMediaUri == null) return false
val dv7Format = findDv7VideoFormat()
if (dv7Format == null) {
emitLog(
"debug",
"dv-fallback",
"Skipping DV conversion retry for $reason: current video is not DV Profile 7 (${describeVideoFormat(currentVideoFormat)})"
)
return false
}
dv7RetryAttempted = true
val newMode = DoviBridge.getDv7FallbackMode()
val newMode = DoviBridge.getDv7FallbackMode(activity)
dvMode = newMode
Log.i(TAG, "Native DV7 playback failed ($reason), retrying with $newMode")
Log.i(TAG, "Native DV7 playback failed ($reason, ${describeVideoFormat(dv7Format)}), retrying with $newMode")
emitLog("info", "dv-fallback", "DV7 native failed ($reason), retrying as $newMode")
return reloadCurrentMediaForDvMode()
@@ -1938,6 +2008,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
firstFrameRendered = true
cancelDecoderHangCheck()
emitLog("debug", "decoder-hang", "First frame rendered — decoder OK")
logNativeDvFirstFrameIfNeeded()
// STATE_READY fires when the player has enough buffered to start, but
// the first frame may not be on screen yet (decoder init + keyframe
// decode). The MPV-parity `playback-restart` event consumers (Dart
@@ -2078,6 +2149,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
decoderInitName = null
audioDecoderInitName = null
currentVideoFormat = null
loggedNativeDvSelectionKey = null
loggedNativeDvFirstFrame = false
loggedDecodedTrueHdTunnelingGuard = false
updateAudioDecoderPolicy("open")
currentMediaUri = uri
@@ -2209,6 +2283,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
detectedFrameRate = -1f
fpsTimestampCount = 0
firstFrameRendered = false
currentVideoFormat = null
loggedNativeDvSelectionKey = null
loggedNativeDvFirstFrame = false
activeDoviMkvWrapper = null
activeDoviMp4Wrapper = null
stopFrameWatchdog()
@@ -2606,6 +2683,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
"dvConversionDebugMode" to (debugDvModeOverride?.name ?: "AUTO"),
"dvStrippedInitNals" to (dovi?.strippedInitNalCount ?: 0L),
"dvStrippedNals" to (dovi?.strippedNalCount ?: 0L),
"dvStrippedRpuNals" to (dovi?.strippedRpuNalCount ?: 0L),
"dvStrippedElNals" to (dovi?.strippedElNalCount ?: 0L),
"dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L),
"dvRpuConversionFailures" to (dovi?.rpuConversionFailureCount ?: 0L),
"dvRpuOutputTooSmall" to (dovi?.rpuOutputTooSmallCount ?: 0L),