refactor: deduplicate Dolby Vision conversion code
- Extract shared processNal() for NAL classification/conversion logic - Share DoviExtractorOutputWrapper between MKV and MP4 paths - Deduplicate DV profile codec queries via deviceSupportsDvProfile() - Collapse repeated doviTrackOutput access chain in stats
This commit is contained in:
@@ -23,55 +23,30 @@ object DoviBridge {
|
||||
fun isAvailable(): Boolean = nativeLoaded &&
|
||||
runCatching { nativeIsConversionPathReady() }.getOrDefault(false)
|
||||
|
||||
/**
|
||||
* Check if the device has a hardware decoder that supports Dolby Vision Profile 7.
|
||||
* Queries MediaCodecList for decoders supporting video/dolby-vision with
|
||||
* DolbyVisionProfileDvheDtr (profile 7).
|
||||
*/
|
||||
val deviceSupportsDvProfile7: Boolean by lazy {
|
||||
private fun deviceSupportsDvProfile(profile: Int, minApi: Int = 0): Boolean {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < minApi) return false
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val supported = codecList.codecInfos.any { info ->
|
||||
return codecList.codecInfos.any { info ->
|
||||
!info.isEncoder && info.supportedTypes.any { type ->
|
||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
||||
info.getCapabilitiesForType(type).profileLevels.any { pl ->
|
||||
pl.profile == MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr
|
||||
}
|
||||
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Device DV Profile 7 support: $supported")
|
||||
supported
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query DV7 support", e)
|
||||
false
|
||||
Log.w(TAG, "Failed to query DV profile $profile support", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device has a hardware decoder that supports Dolby Vision Profile 8
|
||||
* (DvheSt). DolbyVisionProfileDvheSt constant requires API 27+.
|
||||
*/
|
||||
val deviceSupportsDvProfile7: Boolean by lazy {
|
||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr)
|
||||
.also { Log.i(TAG, "Device DV Profile 7 support: $it") }
|
||||
}
|
||||
|
||||
val deviceSupportsDvProfile8: Boolean by lazy {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 27) {
|
||||
Log.i(TAG, "API < 27, cannot check DV Profile 8 support")
|
||||
return@lazy false
|
||||
}
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val supported = codecList.codecInfos.any { info ->
|
||||
!info.isEncoder && info.supportedTypes.any { type ->
|
||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
||||
info.getCapabilitiesForType(type).profileLevels.any { pl ->
|
||||
pl.profile == MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Device DV Profile 8 support: $supported")
|
||||
supported
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query DV8 support", e)
|
||||
false
|
||||
}
|
||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
|
||||
.also { Log.i(TAG, "Device DV Profile 8 support: $it") }
|
||||
}
|
||||
|
||||
fun getConversionMode(): DvConversionMode = when {
|
||||
|
||||
+43
-81
@@ -165,14 +165,42 @@ class DoviConvertingTrackOutput(
|
||||
// Sample counter for periodic logging
|
||||
private var sampleCount = 0L
|
||||
|
||||
/**
|
||||
* Process a single NAL: convert RPU (DV81), strip DV layers, or keep.
|
||||
* Returns processed NAL data to write, or null if stripped.
|
||||
*/
|
||||
private fun processNal(nalData: ByteArray): ByteArray? {
|
||||
if (nalData.size < 2) return nalData
|
||||
val nalType = (nalData[0].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((nalData[0].toInt() and 1) shl 5) or
|
||||
((nalData[1].toInt() ushr 3) and 0x1F)
|
||||
return when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> {
|
||||
val converted = DoviBridge.convertRpuNalu(nalData, LIBDOVI_MODE_TO_81)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted)
|
||||
convertedRpuCount++
|
||||
converted
|
||||
} else {
|
||||
strippedNalCount++
|
||||
null
|
||||
}
|
||||
}
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> {
|
||||
strippedNalCount++
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
normalizeLayerId(nalData)
|
||||
nalData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process NAL units in the sample data. Auto-detects format:
|
||||
* - Annex B (00 00 00 01 / 00 00 01 start codes) — used by MatroskaExtractor
|
||||
* - Length-prefixed (4-byte big-endian length) — used by Mp4Extractor
|
||||
*
|
||||
* Strips UNSPEC62 RPU NALs, UNSPEC63 EL NALs, and any NAL with nuh_layer_id > 0.
|
||||
* Normalizes nuh_layer_id to 0 on all retained NALs.
|
||||
* Output uses the same format as input.
|
||||
*/
|
||||
private fun processNalUnits(data: ByteArray): ByteArray {
|
||||
if (data.size < 4) return data
|
||||
@@ -202,12 +230,10 @@ class DoviConvertingTrackOutput(
|
||||
while (i < data.size - 2) {
|
||||
if (data[i] == 0.toByte() && data[i + 1] == 0.toByte()) {
|
||||
if (i + 3 < data.size && data[i + 2] == 0.toByte() && data[i + 3] == 1.toByte()) {
|
||||
// 4-byte start code: 00 00 00 01
|
||||
positions.add(Pair(i + 4, 4))
|
||||
i += 4
|
||||
continue
|
||||
} else if (data[i + 2] == 1.toByte()) {
|
||||
// 3-byte start code: 00 00 01
|
||||
positions.add(Pair(i + 3, 3))
|
||||
i += 3
|
||||
continue
|
||||
@@ -227,58 +253,25 @@ class DoviConvertingTrackOutput(
|
||||
val startCodes = findAnnexBStartCodes(data)
|
||||
if (startCodes.isEmpty()) {
|
||||
sampleCount++
|
||||
return data // No start codes found, pass through
|
||||
return data
|
||||
}
|
||||
|
||||
for (idx in startCodes.indices) {
|
||||
val nalStart = startCodes[idx].first
|
||||
val nalEnd = if (idx + 1 < startCodes.size) {
|
||||
// NAL ends where next start code begins (subtract its start code length area)
|
||||
// Find the start of the next start code pattern
|
||||
startCodes[idx + 1].first - startCodes[idx + 1].second
|
||||
} else {
|
||||
data.size
|
||||
}
|
||||
|
||||
if (nalEnd <= nalStart) continue
|
||||
val nalData = data.copyOfRange(nalStart, nalEnd)
|
||||
|
||||
if (nalData.size >= 2) {
|
||||
val nalType = (nalData[0].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((nalData[0].toInt() and 1) shl 5) or
|
||||
((nalData[1].toInt() ushr 3) and 0x1F)
|
||||
|
||||
when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> {
|
||||
// Convert RPU NAL via libdovi instead of stripping
|
||||
val converted = DoviBridge.convertRpuNalu(nalData, LIBDOVI_MODE_TO_81)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted)
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(converted)
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
else -> {
|
||||
normalizeLayerId(nalData)
|
||||
// Write with 4-byte start code (consistent output)
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(nalData)
|
||||
kept++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val result = processNal(data.copyOfRange(nalStart, nalEnd))
|
||||
if (result != null) {
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(nalData)
|
||||
output.write(result)
|
||||
kept++
|
||||
} else {
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +280,6 @@ class DoviConvertingTrackOutput(
|
||||
Log.d(TAG, "Sample #$sampleCount (AnnexB): ${data.size}B -> ${output.size()}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
@@ -311,41 +303,12 @@ class DoviConvertingTrackOutput(
|
||||
break
|
||||
}
|
||||
|
||||
val nalStart = pos + 4
|
||||
val nalData = data.copyOfRange(nalStart, nalStart + nalLen)
|
||||
|
||||
if (nalData.size >= 2) {
|
||||
val nalType = (nalData[0].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((nalData[0].toInt() and 1) shl 5) or
|
||||
((nalData[1].toInt() ushr 3) and 0x1F)
|
||||
|
||||
when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> {
|
||||
// Convert RPU NAL via libdovi instead of stripping
|
||||
val converted = DoviBridge.convertRpuNalu(nalData, LIBDOVI_MODE_TO_81)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted)
|
||||
writeLengthPrefixedNal(output, converted)
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
else -> {
|
||||
normalizeLayerId(nalData)
|
||||
writeLengthPrefixedNal(output, nalData)
|
||||
kept++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeLengthPrefixedNal(output, nalData)
|
||||
val result = processNal(data.copyOfRange(pos + 4, pos + 4 + nalLen))
|
||||
if (result != null) {
|
||||
writeLengthPrefixedNal(output, result)
|
||||
kept++
|
||||
} else {
|
||||
stripped++
|
||||
}
|
||||
|
||||
pos += 4 + nalLen
|
||||
@@ -356,7 +319,6 @@ class DoviConvertingTrackOutput(
|
||||
Log.d(TAG, "Sample #$sampleCount (LenPrefix): ${data.size}B -> ${output.size()}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,34 @@ import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that intercepts video track creation
|
||||
* to insert DoviConvertingTrackOutput for DV processing.
|
||||
* Shared by DoviExtractorWrapper (MP4) and DoviMatroskaExtractor (MKV).
|
||||
*/
|
||||
class DoviExtractorOutputWrapper(
|
||||
private val delegate: ExtractorOutput,
|
||||
private val dvMode: DvConversionMode,
|
||||
private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit,
|
||||
) : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, dvMode)
|
||||
onVideoTrackWrapped(wrapper)
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor decorator for Mp4/FragmentedMp4 containers.
|
||||
* Wraps the video TrackOutput with DoviConvertingTrackOutput to perform
|
||||
* DV Profile 7 → 8.1 conversion via inline NAL processing.
|
||||
*
|
||||
* For MP4, RPU (UNSPEC62) and EL (UNSPEC63) NALs are interleaved in sample data,
|
||||
* so no BlockAdditions handling is needed.
|
||||
*/
|
||||
class DoviExtractorWrapper(
|
||||
private val delegate: Extractor,
|
||||
@@ -27,20 +48,7 @@ class DoviExtractorWrapper(
|
||||
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
delegate.init(object : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = output.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, dvMode)
|
||||
doviTrackOutput = wrapper
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = output.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = output.seekMap(seekMap)
|
||||
})
|
||||
delegate.init(DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it })
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int =
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import androidx.media3.extractor.text.SubtitleParser
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
@@ -141,33 +139,11 @@ class DoviMatroskaExtractor(
|
||||
val field = extractorOutputField ?: return
|
||||
val output = field.get(this) as? ExtractorOutput ?: return
|
||||
if (output is DoviExtractorOutputWrapper) return
|
||||
field.set(this, DoviExtractorOutputWrapper(output, this))
|
||||
field.set(this, DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it })
|
||||
}
|
||||
|
||||
private fun clearAttachment() {
|
||||
currentAttachmentName = null
|
||||
currentAttachmentMime = null
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that intercepts video track creation
|
||||
* to insert DoviConvertingTrackOutput for DV processing.
|
||||
*/
|
||||
class DoviExtractorOutputWrapper(
|
||||
private val delegate: ExtractorOutput,
|
||||
private val extractor: DoviMatroskaExtractor,
|
||||
) : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, extractor.dvMode)
|
||||
extractor.doviTrackOutput = wrapper
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: androidx.media3.extractor.SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,13 +1452,15 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
"isPlaying" to player.isPlaying,
|
||||
"playbackState" to player.playbackState,
|
||||
// DV conversion (query extractor's track output, which is set during extraction)
|
||||
"dvConversionActive" to ((activeDoviMkvExtractor?.doviTrackOutput?.conversionActive
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.conversionActive) == true),
|
||||
"dvConversionMode" to dvMode.name,
|
||||
"dvStrippedNals" to ((activeDoviMkvExtractor?.doviTrackOutput?.strippedNalCount
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.strippedNalCount) ?: 0L),
|
||||
"dvConvertedRpus" to ((activeDoviMkvExtractor?.doviTrackOutput?.convertedRpuCount
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.convertedRpuCount) ?: 0L),
|
||||
*(activeDoviMkvExtractor?.doviTrackOutput
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput).let { dovi ->
|
||||
arrayOf(
|
||||
"dvConversionActive" to (dovi?.conversionActive == true),
|
||||
"dvConversionMode" to dvMode.name,
|
||||
"dvStrippedNals" to (dovi?.strippedNalCount ?: 0L),
|
||||
"dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user