fix(android): improve Dolby Vision fallback
This commit is contained in:
@@ -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,
|
||||
|
||||
+41
-29
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user