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));
|
int32_t ret = dovi_convert_rpu_with_mode(rpu, static_cast<uint8_t>(mode));
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
err = dovi_rpu_get_error(rpu);
|
err = dovi_rpu_get_error(rpu);
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
package com.edde746.plezy.exoplayer
|
package com.edde746.plezy.exoplayer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.media.MediaCodecInfo
|
import android.media.MediaCodecInfo
|
||||||
import android.media.MediaCodecList
|
import android.media.MediaCodecList
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Display
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
|
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
|
||||||
|
|
||||||
object DoviBridge {
|
object DoviBridge {
|
||||||
private const val TAG = "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 CONVERT_FAILED = -1
|
||||||
const val DESTINATION_TOO_SMALL = -2
|
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 {
|
private val nativeLoaded: Boolean by lazy {
|
||||||
try {
|
try {
|
||||||
System.loadLibrary("dovi_bridge")
|
System.loadLibrary("dovi_bridge")
|
||||||
@@ -29,46 +46,207 @@ object DoviBridge {
|
|||||||
|
|
||||||
fun isAvailable(): Boolean = conversionPathReady
|
fun isAvailable(): Boolean = conversionPathReady
|
||||||
|
|
||||||
private fun deviceSupportsDvProfile(profile: Int, minApi: Int = 0): Boolean {
|
private val dolbyVisionDecoders: List<DvDecoderCapability> by lazy {
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT < minApi) return false
|
|
||||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||||
return codecList.codecInfos.any { info ->
|
codecList.codecInfos.flatMap { info ->
|
||||||
!info.isEncoder &&
|
if (info.isEncoder) {
|
||||||
info.supportedTypes.any { type ->
|
emptyList()
|
||||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
} else {
|
||||||
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to query DV profile $profile support", e)
|
Log.w(TAG, "Failed to query native Dolby Vision decoder support", e)
|
||||||
return false
|
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 {
|
val deviceSupportsDvProfile7: Boolean by lazy {
|
||||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr)
|
deviceAdvertisesDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtb)
|
||||||
.also { Log.i(TAG, "Device DV Profile 7 support: $it") }
|
.also { Log.i(TAG, "Device advertises exact DV Profile 7 (DvheDtb): $it") }
|
||||||
}
|
}
|
||||||
|
|
||||||
val deviceSupportsDvProfile8: Boolean by lazy {
|
val deviceSupportsDvProfile8: Boolean by lazy {
|
||||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
|
deviceAdvertisesDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
|
||||||
.also { Log.i(TAG, "Device DV Profile 8 support: $it") }
|
.also { Log.i(TAG, "Device advertises DV Profile 8 (DvheSt): $it") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConversionMode(): DvConversionMode = when {
|
fun displaySupportsDolbyVision(context: Context): Boolean {
|
||||||
!isAvailable() -> DvConversionMode.DISABLED
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
deviceSupportsDvProfile7 -> DvConversionMode.DISABLED // try native first; ExoPlayerCore retries with conversion on failure
|
Log.i(TAG, "Display Dolby Vision support: false (HDR capabilities require API 24, device API=${Build.VERSION.SDK_INT})")
|
||||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
return false
|
||||||
else -> DvConversionMode.HEVC_STRIP
|
}
|
||||||
|
|
||||||
|
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. */
|
/** Get the fallback mode when native DV7 decoding fails. */
|
||||||
fun getDv7FallbackMode(): DvConversionMode = when {
|
fun getDv7FallbackMode(context: Context): DvConversionMode {
|
||||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
val bridgeReady = isAvailable()
|
||||||
else -> DvConversionMode.HEVC_STRIP
|
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(
|
fun convertRpuNalu(
|
||||||
payload: ByteArray,
|
payload: ByteArray,
|
||||||
payloadOffset: Int,
|
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:
|
* 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
|
* - DV81: Convert RPU NALs via libdovi mode 2 to Profile 8.1, present as
|
||||||
* with dvhe.08.XX codec string. Preserves dynamic tone mapping metadata.
|
* video/dolby-vision with dvhe.08.XX codec string. Conversion failures drop
|
||||||
* - HEVC_STRIP: Strip all DV enhancement layers, present as plain video/hevc.
|
* 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):
|
* Two modes of NAL framing (auto-detected):
|
||||||
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
|
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
|
||||||
@@ -22,8 +23,7 @@ import androidx.media3.extractor.TrackOutput
|
|||||||
* NAL processing:
|
* NAL processing:
|
||||||
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
|
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
|
||||||
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
|
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
|
||||||
* - nuh_layer_id > 0: Enhancement layer NAL → strip
|
* - All other NALs: pass through unchanged, matching Kodi's compatibility path
|
||||||
* - All retained NALs: normalize nuh_layer_id to 0
|
|
||||||
*
|
*
|
||||||
* All buffers are reused across samples to minimize GC pressure on the hot path.
|
* All buffers are reused across samples to minimize GC pressure on the hot path.
|
||||||
*/
|
*/
|
||||||
@@ -47,6 +47,10 @@ class DoviConvertingTrackOutput(
|
|||||||
private set
|
private set
|
||||||
var strippedNalCount = 0L
|
var strippedNalCount = 0L
|
||||||
private set
|
private set
|
||||||
|
var strippedRpuNalCount = 0L
|
||||||
|
private set
|
||||||
|
var strippedElNalCount = 0L
|
||||||
|
private set
|
||||||
var strippedInitNalCount = 0L
|
var strippedInitNalCount = 0L
|
||||||
private set
|
private set
|
||||||
var convertedRpuCount = 0L
|
var convertedRpuCount = 0L
|
||||||
@@ -79,11 +83,17 @@ class DoviConvertingTrackOutput(
|
|||||||
override fun format(format: Format) {
|
override fun format(format: Format) {
|
||||||
if (!conversionActive) {
|
if (!conversionActive) {
|
||||||
val codecs = format.codecs
|
val codecs = format.codecs
|
||||||
if (codecs != null && codecs.startsWith("dvhe.07")) {
|
if (isDvProfile7Codec(codecs)) {
|
||||||
|
val codecString = codecs ?: ""
|
||||||
conversionActive = true
|
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(
|
logInfo(
|
||||||
"Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
|
"Original format: mime=${format.sampleMimeType}, codecs=$codecString, " +
|
||||||
"initData=${format.initializationData.size} entries " +
|
"initData=${format.initializationData.size} entries " +
|
||||||
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})"
|
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})"
|
||||||
)
|
)
|
||||||
@@ -92,7 +102,7 @@ class DoviConvertingTrackOutput(
|
|||||||
val newFormat = when (dvMode) {
|
val newFormat = when (dvMode) {
|
||||||
DvConversionMode.DV81 -> {
|
DvConversionMode.DV81 -> {
|
||||||
// Parse DV level from codec string: "dvhe.07.06" → 6
|
// 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 newCodecs = "dvhe.08.%02d".format(level)
|
||||||
val dvConfigRecord = buildDv81ConfigRecord(level)
|
val dvConfigRecord = buildDv81ConfigRecord(level)
|
||||||
logInfo("DV81: rewriting to $newCodecs, config=${dvConfigRecord.size}B")
|
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)
|
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
||||||
outputLen += 4
|
outputLen += 4
|
||||||
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
||||||
normalizeLayerId(outputBuf, outputLen)
|
|
||||||
outputLen += nalLen
|
outputLen += nalLen
|
||||||
kept++
|
kept++
|
||||||
} else if (action == NalAction.CONVERT) {
|
} else if (action == NalAction.CONVERT) {
|
||||||
@@ -368,16 +377,15 @@ class DoviConvertingTrackOutput(
|
|||||||
if (convertedLen >= 0) {
|
if (convertedLen >= 0) {
|
||||||
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
||||||
outputLen += 4
|
outputLen += 4
|
||||||
normalizeLayerId(outputBuf, outputLen)
|
|
||||||
outputLen += convertedLen
|
outputLen += convertedLen
|
||||||
convertedRpuCount++
|
convertedRpuCount++
|
||||||
kept++
|
kept++
|
||||||
} else {
|
} else {
|
||||||
strippedNalCount++
|
recordStrippedNal(action)
|
||||||
stripped++
|
stripped++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strippedNalCount++
|
recordStrippedNal(action)
|
||||||
stripped++
|
stripped++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,7 +436,6 @@ class DoviConvertingTrackOutput(
|
|||||||
writeInt32BE(outputBuf, outputLen, nalLen)
|
writeInt32BE(outputBuf, outputLen, nalLen)
|
||||||
outputLen += 4
|
outputLen += 4
|
||||||
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
||||||
normalizeLayerId(outputBuf, outputLen)
|
|
||||||
outputLen += nalLen
|
outputLen += nalLen
|
||||||
kept++
|
kept++
|
||||||
} else if (action == NalAction.CONVERT) {
|
} else if (action == NalAction.CONVERT) {
|
||||||
@@ -436,16 +443,15 @@ class DoviConvertingTrackOutput(
|
|||||||
if (convertedLen >= 0) {
|
if (convertedLen >= 0) {
|
||||||
writeInt32BE(outputBuf, outputLen, convertedLen)
|
writeInt32BE(outputBuf, outputLen, convertedLen)
|
||||||
outputLen += 4
|
outputLen += 4
|
||||||
normalizeLayerId(outputBuf, outputLen)
|
|
||||||
outputLen += convertedLen
|
outputLen += convertedLen
|
||||||
convertedRpuCount++
|
convertedRpuCount++
|
||||||
kept++
|
kept++
|
||||||
} else {
|
} else {
|
||||||
strippedNalCount++
|
recordStrippedNal(action)
|
||||||
stripped++
|
stripped++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strippedNalCount++
|
recordStrippedNal(action)
|
||||||
stripped++
|
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 {
|
private fun convertRpuIntoOutput(nalStart: Int, nalLen: Int, outputOffset: Int): Int {
|
||||||
ensureOutputCapacity(outputOffset + MAX_CONVERTED_RPU_SIZE)
|
ensureOutputCapacity(outputOffset + MAX_CONVERTED_RPU_SIZE)
|
||||||
@@ -565,7 +581,6 @@ class DoviConvertingTrackOutput(
|
|||||||
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
||||||
outputLen += 4
|
outputLen += 4
|
||||||
System.arraycopy(data, nalStart, outputBuf, outputLen, nalLen)
|
System.arraycopy(data, nalStart, outputBuf, outputLen, nalLen)
|
||||||
normalizeLayerId(outputBuf, outputLen)
|
|
||||||
outputLen += nalLen
|
outputLen += nalLen
|
||||||
kept++
|
kept++
|
||||||
}
|
}
|
||||||
@@ -617,25 +632,22 @@ class DoviConvertingTrackOutput(
|
|||||||
/** Classify a NAL at sampleBuf[offset..offset+len) without copying. */
|
/** 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 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 {
|
private fun classifyNal(data: ByteArray, offset: Int, len: Int, convertRpu: Boolean): NalAction {
|
||||||
if (len < 2) return NalAction.KEEP
|
if (len < 2) return NalAction.KEEP
|
||||||
val nalType = (data[offset].toInt() ushr 1) and 0x3F
|
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 {
|
return when {
|
||||||
nalType == NAL_TYPE_UNSPEC62 && convertRpu -> NalAction.CONVERT
|
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
|
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) {
|
private fun writeInt32BE(buf: ByteArray, offset: Int, value: Int) {
|
||||||
buf[offset] = ((value ushr 24) and 0xFF).toByte()
|
buf[offset] = ((value ushr 24) and 0xFF).toByte()
|
||||||
buf[offset + 1] = ((value ushr 16) 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 dvMode: DvConversionMode = DvConversionMode.DISABLED
|
||||||
private var debugDvModeOverride: DvConversionMode? = null
|
private var debugDvModeOverride: DvConversionMode? = null
|
||||||
private var dv7RetryAttempted = false
|
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 activeDoviMkvWrapper: DoviExtractorWrapper? = null
|
||||||
|
|
||||||
@Volatile private var activeDoviMp4Wrapper: 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 {
|
fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
@@ -315,10 +318,10 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
|
|
||||||
tunnelingUserEnabled = tunnelingEnabled
|
tunnelingUserEnabled = tunnelingEnabled
|
||||||
this.dvMode = getConfiguredDvMode()
|
this.dvMode = getConfiguredDvMode()
|
||||||
|
DoviBridge.logSupportSummary(activity)
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
"DV conversion: mode=$dvMode, bridge=${DoviBridge.isAvailable()}, " +
|
"DV conversion: mode=$dvMode, override=${debugDvModeOverride?.name ?: "AUTO"}"
|
||||||
"deviceDV7=${DoviBridge.deviceSupportsDvProfile7}, deviceDV8=${DoviBridge.deviceSupportsDvProfile8}"
|
|
||||||
)
|
)
|
||||||
disposing = false
|
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 }
|
val audioGroup = tracks.groups.firstOrNull { it.type == C.TRACK_TYPE_AUDIO && it.isSelected }
|
||||||
if (videoGroup != null) {
|
if (videoGroup != null) {
|
||||||
val vf = videoGroup.mediaTrackGroup.getFormat(0)
|
val vf = videoGroup.mediaTrackGroup.getFormat(0)
|
||||||
|
currentVideoFormat = vf
|
||||||
val hdr = vf.colorInfo?.let { ci ->
|
val hdr = vf.colorInfo?.let { ci ->
|
||||||
val transfer = ci.colorTransfer
|
val transfer = ci.colorTransfer
|
||||||
if (transfer != null && transfer != 0) " HDR(transfer=$transfer)" else ""
|
if (transfer != null && transfer != 0) " HDR(transfer=$transfer)" else ""
|
||||||
} ?: ""
|
} ?: ""
|
||||||
emitLog("info", "tracks", "Video: ${vf.codecs} ${vf.width}x${vf.height}$hdr")
|
emitLog("info", "tracks", "Video: ${vf.codecs} ${vf.width}x${vf.height}$hdr")
|
||||||
|
logNativeDvSelectionIfNeeded(vf)
|
||||||
|
} else {
|
||||||
|
currentVideoFormat = null
|
||||||
}
|
}
|
||||||
if (audioGroup != null) {
|
if (audioGroup != null) {
|
||||||
val af = audioGroup.mediaTrackGroup.getFormat(0)
|
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),
|
* When native DV7 decoding fails, upgrade to DV7→8.1 conversion or HEVC strip
|
||||||
* upgrade to DV7→8.1 conversion or HEVC strip and reload the media.
|
* and reload the media. Returns true if retry was initiated.
|
||||||
* Returns true if retry was initiated.
|
|
||||||
*/
|
*/
|
||||||
private fun retryWithDvConversion(reason: String): Boolean {
|
private fun retryWithDvConversion(reason: String): Boolean {
|
||||||
if (dv7RetryAttempted) return false
|
if (dv7RetryAttempted) return false
|
||||||
if (debugDvModeOverride == DvConversionMode.DISABLED) return false
|
if (debugDvModeOverride == DvConversionMode.DISABLED) {
|
||||||
if (dvMode != DvConversionMode.DISABLED) return false
|
emitLog("debug", "dv-fallback", "Skipping DV conversion retry for $reason: native/disabled mode is forced")
|
||||||
if (!DoviBridge.isAvailable()) return false
|
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
|
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
|
dv7RetryAttempted = true
|
||||||
val newMode = DoviBridge.getDv7FallbackMode()
|
val newMode = DoviBridge.getDv7FallbackMode(activity)
|
||||||
dvMode = newMode
|
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")
|
emitLog("info", "dv-fallback", "DV7 native failed ($reason), retrying as $newMode")
|
||||||
|
|
||||||
return reloadCurrentMediaForDvMode()
|
return reloadCurrentMediaForDvMode()
|
||||||
@@ -1938,6 +2008,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
firstFrameRendered = true
|
firstFrameRendered = true
|
||||||
cancelDecoderHangCheck()
|
cancelDecoderHangCheck()
|
||||||
emitLog("debug", "decoder-hang", "First frame rendered — decoder OK")
|
emitLog("debug", "decoder-hang", "First frame rendered — decoder OK")
|
||||||
|
logNativeDvFirstFrameIfNeeded()
|
||||||
// STATE_READY fires when the player has enough buffered to start, but
|
// STATE_READY fires when the player has enough buffered to start, but
|
||||||
// the first frame may not be on screen yet (decoder init + keyframe
|
// the first frame may not be on screen yet (decoder init + keyframe
|
||||||
// decode). The MPV-parity `playback-restart` event consumers (Dart
|
// decode). The MPV-parity `playback-restart` event consumers (Dart
|
||||||
@@ -2078,6 +2149,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
|
|
||||||
decoderInitName = null
|
decoderInitName = null
|
||||||
audioDecoderInitName = null
|
audioDecoderInitName = null
|
||||||
|
currentVideoFormat = null
|
||||||
|
loggedNativeDvSelectionKey = null
|
||||||
|
loggedNativeDvFirstFrame = false
|
||||||
loggedDecodedTrueHdTunnelingGuard = false
|
loggedDecodedTrueHdTunnelingGuard = false
|
||||||
updateAudioDecoderPolicy("open")
|
updateAudioDecoderPolicy("open")
|
||||||
currentMediaUri = uri
|
currentMediaUri = uri
|
||||||
@@ -2209,6 +2283,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
detectedFrameRate = -1f
|
detectedFrameRate = -1f
|
||||||
fpsTimestampCount = 0
|
fpsTimestampCount = 0
|
||||||
firstFrameRendered = false
|
firstFrameRendered = false
|
||||||
|
currentVideoFormat = null
|
||||||
|
loggedNativeDvSelectionKey = null
|
||||||
|
loggedNativeDvFirstFrame = false
|
||||||
activeDoviMkvWrapper = null
|
activeDoviMkvWrapper = null
|
||||||
activeDoviMp4Wrapper = null
|
activeDoviMp4Wrapper = null
|
||||||
stopFrameWatchdog()
|
stopFrameWatchdog()
|
||||||
@@ -2606,6 +2683,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
|||||||
"dvConversionDebugMode" to (debugDvModeOverride?.name ?: "AUTO"),
|
"dvConversionDebugMode" to (debugDvModeOverride?.name ?: "AUTO"),
|
||||||
"dvStrippedInitNals" to (dovi?.strippedInitNalCount ?: 0L),
|
"dvStrippedInitNals" to (dovi?.strippedInitNalCount ?: 0L),
|
||||||
"dvStrippedNals" to (dovi?.strippedNalCount ?: 0L),
|
"dvStrippedNals" to (dovi?.strippedNalCount ?: 0L),
|
||||||
|
"dvStrippedRpuNals" to (dovi?.strippedRpuNalCount ?: 0L),
|
||||||
|
"dvStrippedElNals" to (dovi?.strippedElNalCount ?: 0L),
|
||||||
"dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L),
|
"dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L),
|
||||||
"dvRpuConversionFailures" to (dovi?.rpuConversionFailureCount ?: 0L),
|
"dvRpuConversionFailures" to (dovi?.rpuConversionFailureCount ?: 0L),
|
||||||
"dvRpuOutputTooSmall" to (dovi?.rpuOutputTooSmallCount ?: 0L),
|
"dvRpuOutputTooSmall" to (dovi?.rpuOutputTooSmallCount ?: 0L),
|
||||||
|
|||||||
Reference in New Issue
Block a user