Convert Dolby Vision profile 7 to 8.1 on devices without a profile 7 decoder (#2017)
## Description **On Amlogic boxes (S905X4 and similar) Dolby Vision Profile 7 does not play as Dolby Vision.** The server transcodes it, or, with "Direct play Dolby Vision Profile 7" switched on by hand, the decoder answers `NoSupport` and fallback ends in HDR10. Devices with a native Profile 7 decoder, such as the Shield, are not affected and nothing changes for them. This PR converts Profile 7 to 8.1 on the fly. It has two halves and one decision. The preference `convertDolbyVisionProfile7` is proto field 6 in the experimental settings, off by default. In `WholphinRenderersFactory` the filter comes first in the list, followed by the HDR10+ masking from #1876. **What this PR does not do.** The enhancement layer is dropped; that is the price of the conversion, which is why devices with a native Profile 7 decoder are left alone. `BlockAdditional` (an enhancement layer outside the stream) is not read — like the rest of Wholphin today — so such a file stays HDR10, as it is now. ### Related issues Closes #244 Builds on #1876 (the `BitstreamFilter` list). Related to #1540. @fanni42 asked for exactly this on an S905X4 in #1876. ### Testing **Hardware** — a debug build of this branch, Homatics box (Amlogic S905X4, Android 14) + LG BX, Jellyfin 12.1.0, a Profile 7 FEL remux with the enhancement layer in band, played from the start for 39 seconds. Only the new preference was switched on; manual "Direct play Dolby Vision Profile 7" was **off**: * `DolbyVisionProfile7FilterTest` (14) — a fake rewriter on a direct buffer: offset ≠ 0, position/limit untouched, every "bytes stay as they were" branch, three-byte start codes, `UnsatisfiedLinkError` tried once; * `DolbyVisionProfile7ExtractorsFactoryTest` (5, mockk) — only the video track is rewritten; * `DolbyVisionProfile7ConversionTest` (6) — codec strings, DRM, `dvh1`. CI in my fork ran on this branch plus a debug signing commit: `pre-commit`, `testDefaultDebugUnitTest` and `assembleDefaultDebug` are all green. The hardware measurement above is that build. ## Screenshots The only UI change is one toggle in the experimental settings. ## AI or LLM usage The code was written with Claude (Claude Code): the filter, the extractor wrapper, the condition and the tests. I reviewed every line and can explain it. It was tested with the unit tests and the hardware measurement above. This description was written by me in Russian and translated into English with the same assistant. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
parent
6d029086e9
commit
2f7804fd5e
15 changed files with 1038 additions and 14 deletions
|
|
@ -0,0 +1,86 @@
|
|||
package com.github.damontecres.wholphin.util
|
||||
|
||||
import androidx.media3.common.DrmInitData
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DolbyVisionProfile7ConversionTest {
|
||||
@Test
|
||||
fun recognisesProfile7InBothSampleEntryForms() {
|
||||
assertTrue(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.07.06")))
|
||||
assertTrue(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvh1.07.06")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMistakeOtherProfilesOrCodecsForProfile7() {
|
||||
assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.08.06")))
|
||||
assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision("dvhe.05.06")))
|
||||
assertFalse(DolbyVisionProfile7Conversion.isProfile7(dolbyVision(null)))
|
||||
assertFalse(
|
||||
DolbyVisionProfile7Conversion.isProfile7(
|
||||
Format
|
||||
.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
||||
.setCodecs("dvhe.07.06")
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rewritesOnlyTheProfileOfTheCodecString() {
|
||||
assertEquals("dvhe.08.06", DolbyVisionProfile7Conversion.toProfile8("dvhe.07.06"))
|
||||
assertEquals("dvh1.08.09", DolbyVisionProfile7Conversion.toProfile8("dvh1.07.09"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisesAProfile7TrackAsProfile8AndKeepsTheRestOfTheFormat() {
|
||||
val format = dolbyVision("dvhe.07.06")
|
||||
|
||||
val advertised = DolbyVisionProfile7Conversion.advertiseAsProfile8(format)
|
||||
|
||||
assertEquals("dvhe.08.06", advertised.codecs)
|
||||
assertEquals(MimeTypes.VIDEO_DOLBY_VISION, advertised.sampleMimeType)
|
||||
assertEquals(format.width, advertised.width)
|
||||
assertEquals(format.height, advertised.height)
|
||||
assertEquals(format.initializationData, advertised.initializationData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesTracksWhichAreNotProfile7AsTheSameInstance() {
|
||||
val profile8 = dolbyVision("dvhe.08.06")
|
||||
val hevc = Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H265).build()
|
||||
|
||||
assertSame(profile8, DolbyVisionProfile7Conversion.advertiseAsProfile8(profile8))
|
||||
assertSame(hevc, DolbyVisionProfile7Conversion.advertiseAsProfile8(hevc))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnEncryptedProfile7TrackAlone() {
|
||||
// Encrypted samples take the secure path, which the bitstream filter does not touch
|
||||
val encrypted =
|
||||
dolbyVision("dvhe.07.06")
|
||||
.buildUpon()
|
||||
.setDrmInitData(DrmInitData(DrmInitData.SchemeData(java.util.UUID.randomUUID(), MimeTypes.VIDEO_MP4, byteArrayOf(1))))
|
||||
.build()
|
||||
|
||||
assertSame(encrypted, DolbyVisionProfile7Conversion.advertiseAsProfile8(encrypted))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun dolbyVision(codecs: String?): Format =
|
||||
Format
|
||||
.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
|
||||
.setCodecs(codecs)
|
||||
.setWidth(3840)
|
||||
.setHeight(2160)
|
||||
.setInitializationData(listOf(byteArrayOf(0x01, 0x02)))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
@file:OptIn(markerClass = [UnstableApi::class])
|
||||
|
||||
package com.github.damontecres.wholphin.util
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.extractor.Extractor
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.ExtractorsFactory
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The factory has to reach the video track's format through three layers of media3 forwarding,
|
||||
* rewrite it there and nowhere else, and let sample data through untouched.
|
||||
*/
|
||||
class DolbyVisionProfile7ExtractorsFactoryTest {
|
||||
private val videoTrack = mockk<TrackOutput>(relaxed = true)
|
||||
private val audioTrack = mockk<TrackOutput>(relaxed = true)
|
||||
private val output =
|
||||
mockk<ExtractorOutput>(relaxed = true) {
|
||||
every { track(VIDEO_ID, C.TRACK_TYPE_VIDEO) } returns videoTrack
|
||||
every { track(AUDIO_ID, C.TRACK_TYPE_AUDIO) } returns audioTrack
|
||||
}
|
||||
private val extractor = mockk<Extractor>(relaxed = true)
|
||||
private val delegate =
|
||||
mockk<ExtractorsFactory> {
|
||||
every { createExtractors() } returns arrayOf(extractor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advertisesTheVideoTrackAsProfile8() {
|
||||
val video = initialisedVideoTrack()
|
||||
|
||||
video.format(dolbyVision("dvhe.07.06"))
|
||||
|
||||
val published = slot<Format>()
|
||||
verify { videoTrack.format(capture(published)) }
|
||||
assertEquals("dvhe.08.06", published.captured.codecs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesOtherVideoFormatsAsTheyAre() {
|
||||
val video = initialisedVideoTrack()
|
||||
val hevc =
|
||||
Format
|
||||
.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
||||
.setCodecs("hvc1.2.4.L153")
|
||||
.build()
|
||||
|
||||
video.format(hevc)
|
||||
|
||||
verify { videoTrack.format(hevc) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesTheAudioTrackUnwrapped() {
|
||||
val wrapped = wrappedOutput()
|
||||
|
||||
assertSame(audioTrack, wrapped.track(AUDIO_ID, C.TRACK_TYPE_AUDIO))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun passesSampleDataStraightThrough() {
|
||||
val video = initialisedVideoTrack()
|
||||
val data = ParsableByteArray(byteArrayOf(1, 2, 3))
|
||||
|
||||
video.sampleData(data, 3)
|
||||
video.sampleMetadata(1_000L, C.BUFFER_FLAG_KEY_FRAME, 3, 0, null)
|
||||
|
||||
verify { videoTrack.sampleData(data, 3) }
|
||||
verify { videoTrack.sampleMetadata(1_000L, C.BUFFER_FLAG_KEY_FRAME, 3, 0, null) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrapsEveryExtractorOfTheDelegate() {
|
||||
val another = mockk<Extractor>(relaxed = true)
|
||||
every { delegate.createExtractors() } returns arrayOf(extractor, another)
|
||||
|
||||
val extractors = DolbyVisionProfile7ExtractorsFactory(delegate).createExtractors()
|
||||
|
||||
assertEquals(2, extractors.size)
|
||||
extractors[1].init(output)
|
||||
verify { another.init(any()) }
|
||||
}
|
||||
|
||||
private fun wrappedOutput(): ExtractorOutput {
|
||||
val extractors = DolbyVisionProfile7ExtractorsFactory(delegate).createExtractors()
|
||||
extractors.single().init(output)
|
||||
val wrapped = slot<ExtractorOutput>()
|
||||
verify { extractor.init(capture(wrapped)) }
|
||||
return wrapped.captured
|
||||
}
|
||||
|
||||
private fun initialisedVideoTrack(): TrackOutput = wrappedOutput().track(VIDEO_ID, C.TRACK_TYPE_VIDEO)
|
||||
|
||||
private companion object {
|
||||
const val VIDEO_ID = 1
|
||||
const val AUDIO_ID = 2
|
||||
|
||||
fun dolbyVision(codecs: String): Format =
|
||||
Format
|
||||
.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
|
||||
.setCodecs(codecs)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
package com.github.damontecres.wholphin.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* The RPU rewrite itself is libdovi's and runs on a device; here it is a fake, and what is under
|
||||
* test is everything around it: which access units are touched at all, that the enhancement layer
|
||||
* goes and the rest stays contiguous, that a refused rewrite leaves the decoder's buffer exactly as
|
||||
* it was, and that the filter keeps to the [BitstreamFilter] contract on a direct buffer.
|
||||
*/
|
||||
class DolbyVisionProfile7FilterTest {
|
||||
@Test
|
||||
fun dropsTheEnhancementLayerAndRewritesTheRpu() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7))
|
||||
val target = directBuffer(annexB(VPS, VCL, EL, EL, RPU))
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(annexB(VPS, VCL, CONVERTED_RPU).toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convertsAnRpuWhichArrivedWithoutAnEnhancementLayer() {
|
||||
// A dual layer remux whose enhancement layer lives in Matroska block additions, which the
|
||||
// extractor discards: the RPU still needs its rewrite for the profile 8 decoder
|
||||
val filter = filterWith(FakeRewriter(profile = 7))
|
||||
val target = directBuffer(annexB(VCL, RPU))
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(annexB(VCL, CONVERTED_RPU).toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun worksFromAnOffsetAndLeavesTheBytesAroundTheAccessUnitAlone() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7))
|
||||
val accessUnit = annexB(VCL, EL, RPU)
|
||||
val prefix = byteArrayOf(0x7A, 0x7B)
|
||||
val suffix = byteArrayOf(0x7C)
|
||||
val target = directBuffer(prefix + accessUnit + suffix)
|
||||
|
||||
val size = filter.filter(target, prefix.size, accessUnit.size)
|
||||
|
||||
assertEquals(annexB(VCL, CONVERTED_RPU).toList(), target.bytes(prefix.size, size))
|
||||
assertEquals(prefix.toList(), target.bytes(0, prefix.size))
|
||||
assertEquals(suffix.toList(), target.bytes(prefix.size + accessUnit.size, suffix.size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMoveThePositionOrLimitOfTheBuffer() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7))
|
||||
val target = directBuffer(annexB(VCL, EL, RPU))
|
||||
target.position(3)
|
||||
target.limit(target.capacity() - 1)
|
||||
|
||||
filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(3, target.position())
|
||||
assertEquals(target.capacity() - 1, target.limit())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnAccessUnitWithoutAnRpuAlone() {
|
||||
val rewriter = FakeRewriter(profile = 7)
|
||||
val filter = filterWith(rewriter)
|
||||
val accessUnit = annexB(VPS, VCL, EL)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, size)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, size))
|
||||
assertEquals(0, rewriter.conversions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnRpuOfAnotherProfileAlone() {
|
||||
val rewriter = FakeRewriter(profile = 8)
|
||||
val filter = filterWith(rewriter)
|
||||
val accessUnit = annexB(VCL, RPU)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, size)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, size))
|
||||
assertEquals(0, rewriter.conversions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnAccessUnitAloneWhenTheRewriteThrows() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7, throws = true))
|
||||
val accessUnit = annexB(VCL, EL, RPU)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, size)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnAccessUnitAloneWhenTheRewriteWouldNotFit() {
|
||||
// No enhancement layer to make room, and an RPU which comes out longer than it went in
|
||||
val filter = filterWith(FakeRewriter(profile = 7, converted = RPU + ByteArray(16) { 0x55 }))
|
||||
val accessUnit = annexB(VCL, RPU)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, size)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun takesALongerRpuWhenTheEnhancementLayerMadeRoomForIt() {
|
||||
val longerRpu = RPU + byteArrayOf(0x55, 0x56)
|
||||
val filter = filterWith(FakeRewriter(profile = 7, converted = longerRpu))
|
||||
val target = directBuffer(annexB(VCL, EL, RPU))
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(annexB(VCL, longerRpu).toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesAnAccessUnitAloneWhenTheRewriteReportsNonsense() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7, reportedSize = -1))
|
||||
val accessUnit = annexB(VCL, EL, RPU)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, size)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leavesDataWhichIsNotAnnexBAlone() {
|
||||
val rewriter = FakeRewriter(profile = 7)
|
||||
val filter = filterWith(rewriter)
|
||||
val data = byteArrayOf(0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x01, 0x7C)
|
||||
val target = directBuffer(data)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(data.size, size)
|
||||
assertEquals(data.toList(), target.bytes(0, size))
|
||||
assertEquals(0, rewriter.conversions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun walksThreeByteStartCodesToo() {
|
||||
val filter = filterWith(FakeRewriter(profile = 7))
|
||||
val accessUnit = SHORT_START_CODE + VCL + SHORT_START_CODE + EL + SHORT_START_CODE + RPU
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val size = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals((SHORT_START_CODE + VCL + SHORT_START_CODE + CONVERTED_RPU).toList(), target.bytes(0, size))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givesUpForGoodWhenTheNativeLibraryIsMissing() {
|
||||
var attempts = 0
|
||||
val filter =
|
||||
DolbyVisionProfile7Filter {
|
||||
attempts++
|
||||
throw UnsatisfiedLinkError("no libdovi_android for this ABI")
|
||||
}
|
||||
val accessUnit = annexB(VCL, EL, RPU)
|
||||
val target = directBuffer(accessUnit)
|
||||
|
||||
val first = filter.filter(target, 0, target.capacity())
|
||||
val second = filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(accessUnit.size, first)
|
||||
assertEquals(accessUnit.size, second)
|
||||
assertEquals(accessUnit.toList(), target.bytes(0, accessUnit.size))
|
||||
assertEquals("the library should be tried once, not on every access unit", 1, attempts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handsTheRewriterTheAccessUnitWithoutTheEnhancementLayer() {
|
||||
val rewriter = FakeRewriter(profile = 7)
|
||||
val filter = filterWith(rewriter)
|
||||
val target = directBuffer(annexB(VPS, VCL, EL, RPU, EL))
|
||||
|
||||
filter.filter(target, 0, target.capacity())
|
||||
|
||||
assertEquals(annexB(VPS, VCL, RPU).toList(), rewriter.lastFrame)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stands in for libdovi. It finds the RPU NAL unit the way the native code does, by walking the
|
||||
* Annex B start codes, and swaps its payload for [converted], moving whatever follows.
|
||||
*/
|
||||
private class FakeRewriter(
|
||||
private val profile: Int,
|
||||
private val converted: ByteArray = CONVERTED_RPU,
|
||||
private val throws: Boolean = false,
|
||||
private val reportedSize: Int? = null,
|
||||
) : RpuRewriter {
|
||||
var conversions = 0
|
||||
var lastFrame: List<Byte> = emptyList()
|
||||
|
||||
override fun profileOf(
|
||||
frame: ByteBuffer,
|
||||
size: Int,
|
||||
): Int {
|
||||
assertFalse("libdovi reads the buffer's address, so the frame has to be direct", frame.hasArray())
|
||||
return profile
|
||||
}
|
||||
|
||||
override fun convertToProfile8(
|
||||
frame: ByteBuffer,
|
||||
size: Int,
|
||||
): Int {
|
||||
conversions++
|
||||
lastFrame = frame.bytes(0, size)
|
||||
if (throws) {
|
||||
throw IllegalArgumentException("Unable to parse Dolby Vision RPU")
|
||||
}
|
||||
val bytes = frame.bytes(0, size).toByteArray()
|
||||
val rpuStart = indexOfNal(bytes, RPU)
|
||||
val rpuEnd = nextStartCode(bytes, rpuStart + 1)
|
||||
val rewritten = bytes.copyOfRange(0, rpuStart) + converted + bytes.copyOfRange(rpuEnd, bytes.size)
|
||||
check(rewritten.size <= frame.capacity()) { "the fake outgrew the scratch buffer" }
|
||||
rewritten.forEachIndexed { index, byte -> frame.put(index, byte) }
|
||||
return reportedSize ?: rewritten.size
|
||||
}
|
||||
|
||||
private fun indexOfNal(
|
||||
bytes: ByteArray,
|
||||
nal: ByteArray,
|
||||
): Int {
|
||||
for (i in 0..bytes.size - nal.size) {
|
||||
if (bytes.copyOfRange(i, i + nal.size).contentEquals(nal)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
throw AssertionError("RPU not found in the frame handed to the rewriter")
|
||||
}
|
||||
|
||||
private fun nextStartCode(
|
||||
bytes: ByteArray,
|
||||
from: Int,
|
||||
): Int {
|
||||
for (i in from..bytes.size - 3) {
|
||||
if (bytes[i] == 0.toByte() && bytes[i + 1] == 0.toByte() && bytes[i + 2] == 1.toByte()) {
|
||||
return if (i > from && bytes[i - 1] == 0.toByte()) i - 1 else i
|
||||
}
|
||||
}
|
||||
return bytes.size
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val START_CODE = byteArrayOf(0, 0, 0, 1)
|
||||
private val SHORT_START_CODE = byteArrayOf(0, 0, 1)
|
||||
|
||||
/** NAL unit header for [type]: forbidden_zero_bit 0, nuh_layer_id 0, nuh_temporal_id_plus1 1. */
|
||||
private fun nal(
|
||||
type: Int,
|
||||
vararg payload: Byte,
|
||||
): ByteArray = byteArrayOf((type shl 1).toByte(), 0x01) + payload
|
||||
|
||||
private val VPS = nal(32, 0x0C, 0x01, 0x7F)
|
||||
private val VCL = nal(1, 0x11, 0x22, 0x33, 0x44)
|
||||
private val EL = nal(63, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E)
|
||||
private val RPU = nal(62, 0x19, 0x07, 0x07, 0x07)
|
||||
private val CONVERTED_RPU = nal(62, 0x19, 0x08, 0x08)
|
||||
|
||||
private fun annexB(vararg nals: ByteArray): ByteArray = nals.fold(byteArrayOf()) { acc, nal -> acc + START_CODE + nal }
|
||||
|
||||
private fun filterWith(rewriter: RpuRewriter) = DolbyVisionProfile7Filter { rewriter }
|
||||
|
||||
/** Like the decoder input buffer on a device: direct, with no backing array. */
|
||||
private fun directBuffer(data: ByteArray): ByteBuffer =
|
||||
ByteBuffer.allocateDirect(data.size).also { buffer ->
|
||||
data.forEachIndexed { index, byte -> buffer.put(index, byte) }
|
||||
assertFalse(buffer.hasArray())
|
||||
}
|
||||
|
||||
private fun ByteBuffer.bytes(
|
||||
from: Int,
|
||||
count: Int,
|
||||
): List<Byte> = (from until from + count).map { get(it) }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue