fix(android): prevent Dolby Vision seek crashes
This commit is contained in:
+42
-6
@@ -8,7 +8,15 @@ import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import java.io.EOFException
|
||||
|
||||
/** Shared whole-sample buffering protocol for TrackOutput transforms. */
|
||||
/**
|
||||
* Buffers transformed [TrackOutput] data until metadata identifies the exact sample range.
|
||||
*
|
||||
* Media3 may append multiple samples before reporting metadata, and [TrackOutput.sampleMetadata]
|
||||
* identifies a sample relative to all appended bytes using `size` and `offset`. Before calling
|
||||
* [transformSample], this class compacts only that sample to `inputBuffer[0 until inputLength]`.
|
||||
* Bytes after the sample remain buffered for later metadata; unreferenced bytes before it are
|
||||
* discarded. Extractor decorators must call [resetBufferedData] when a seek abandons pending data.
|
||||
*/
|
||||
abstract class BufferedTransformingTrackOutput(
|
||||
protected val delegate: TrackOutput,
|
||||
initialBufferSize: Int,
|
||||
@@ -26,7 +34,12 @@ abstract class BufferedTransformingTrackOutput(
|
||||
protected abstract val transformEnabled: Boolean
|
||||
protected abstract val transformedBuffer: ByteArray
|
||||
|
||||
/** Returns transformed length, or a negative value to drop the sample. */
|
||||
/**
|
||||
* Transforms the exact sample in `inputBuffer[0 until inputLength]`.
|
||||
*
|
||||
* Implementations must treat [inputBuffer] as read-only. Returns the number of bytes available
|
||||
* from [transformedBuffer], or a negative value to drop the sample.
|
||||
*/
|
||||
protected abstract fun transformSample(inputLength: Int, flags: Int): Int
|
||||
|
||||
init {
|
||||
@@ -80,15 +93,38 @@ abstract class BufferedTransformingTrackOutput(
|
||||
return
|
||||
}
|
||||
|
||||
buffering = false
|
||||
val sourceLength = inputLength
|
||||
inputLength = 0
|
||||
val transformedLength = transformSample(sourceLength, flags)
|
||||
// Offset can describe data for one or more later samples already appended by the extractor.
|
||||
if (size < 0 || offset < 0 || size > inputLength - offset) {
|
||||
resetBufferedData()
|
||||
return
|
||||
}
|
||||
|
||||
val sampleEnd = inputLength - offset
|
||||
val sampleStart = sampleEnd - size
|
||||
if (sampleStart > 0) {
|
||||
System.arraycopy(inputBuffer, sampleStart, inputBuffer, 0, size)
|
||||
}
|
||||
|
||||
try {
|
||||
val transformedLength = transformSample(size, flags)
|
||||
if (transformedLength < 0) return
|
||||
|
||||
outputParsable.reset(transformedBuffer, transformedLength)
|
||||
delegate.sampleData(outputParsable, transformedLength, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
delegate.sampleMetadata(timeUs, flags, transformedLength, 0, cryptoData)
|
||||
} finally {
|
||||
if (offset > 0) {
|
||||
System.arraycopy(inputBuffer, sampleEnd, inputBuffer, 0, offset)
|
||||
}
|
||||
inputLength = offset
|
||||
buffering = offset > 0
|
||||
}
|
||||
}
|
||||
|
||||
/** Drops sample bytes buffered before an extractor seek while retaining allocated storage. */
|
||||
internal fun resetBufferedData() {
|
||||
inputLength = 0
|
||||
buffering = false
|
||||
}
|
||||
|
||||
private fun appendInput(source: ByteArray, length: Int) {
|
||||
|
||||
+25
-34
@@ -138,28 +138,29 @@ class DoviConvertingTrackOutput(
|
||||
|
||||
override fun transformSample(inputLength: Int, flags: Int): Int {
|
||||
val processStartNs = System.nanoTime()
|
||||
outputIsProcessed = if ((flags and C.BUFFER_FLAG_ENCRYPTED) != 0) {
|
||||
if ((flags and C.BUFFER_FLAG_ENCRYPTED) != 0) {
|
||||
outputIsProcessed = false
|
||||
if (!loggedEncryptedSupplementalPassthrough && (flags and C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) {
|
||||
loggedEncryptedSupplementalPassthrough = true
|
||||
logWarn("Encrypted supplemental sample encountered, passing raw sample")
|
||||
}
|
||||
false
|
||||
} else {
|
||||
try {
|
||||
processSampleData(flags, inputLength)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logError("NAL processing failed, passing raw sample", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
val transformedLength = if (outputIsProcessed) outputLen else inputLength
|
||||
if (outputIsProcessed) {
|
||||
recordSampleProcessing((System.nanoTime() - processStartNs) / 1_000L)
|
||||
return if (inputLength == 0) -1 else inputLength
|
||||
}
|
||||
|
||||
// Skip empty samples (all NALs were DV layers) — don't confuse the decoder.
|
||||
return if (transformedLength == 0) -1 else transformedLength
|
||||
return try {
|
||||
if (!processSampleData(flags, inputLength)) {
|
||||
outputIsProcessed = true
|
||||
-1
|
||||
} else {
|
||||
outputIsProcessed = true
|
||||
recordSampleProcessing((System.nanoTime() - processStartNs) / 1_000L)
|
||||
if (outputLen == 0) -1 else outputLen
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputIsProcessed = false
|
||||
logError("NAL processing failed, passing raw sample", e)
|
||||
if (inputLength == 0) -1 else inputLength
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,23 +170,21 @@ class DoviConvertingTrackOutput(
|
||||
* [4-byte big-endian main sample size][main sample][supplemental data].
|
||||
* Only the main sample contains video NALs and should be rewritten.
|
||||
*/
|
||||
private fun processSampleData(flags: Int, dataLen: Int) {
|
||||
private fun processSampleData(flags: Int, dataLen: Int): Boolean {
|
||||
if ((flags and C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) == 0) {
|
||||
processNalUnits(0, dataLen)
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
if (dataLen < 4) {
|
||||
logWarn("Bad supplemental sample: ${dataLen}B is too small")
|
||||
copyRawSample(dataLen)
|
||||
return
|
||||
logWarn("Bad supplemental sample: ${dataLen}B is too small; dropping sample")
|
||||
return false
|
||||
}
|
||||
|
||||
val mainSampleLen = readInt32BE(inputBuffer, 0)
|
||||
if (mainSampleLen < 0 || mainSampleLen > dataLen - 4) {
|
||||
logWarn("Bad supplemental sample: mainLen=$mainSampleLen total=$dataLen")
|
||||
copyRawSample(dataLen)
|
||||
return
|
||||
logWarn("Bad supplemental sample: mainLen=$mainSampleLen total=$dataLen; dropping sample")
|
||||
return false
|
||||
}
|
||||
|
||||
val supplementalLen = dataLen - 4 - mainSampleLen
|
||||
@@ -200,10 +199,7 @@ class DoviConvertingTrackOutput(
|
||||
processNalUnits(4, mainSampleLen)
|
||||
|
||||
val processedMainLen = outputLen
|
||||
if (processedMainLen == 0) {
|
||||
outputLen = 0
|
||||
return
|
||||
}
|
||||
if (processedMainLen == 0) return true
|
||||
|
||||
ensureOutputCapacity(4 + processedMainLen + supplementalLen)
|
||||
System.arraycopy(outputBuf, 0, outputBuf, 4, processedMainLen)
|
||||
@@ -219,6 +215,7 @@ class DoviConvertingTrackOutput(
|
||||
"supplemental=${supplementalLen}B, total=${dataLen}B -> ${outputLen}B"
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,12 +607,6 @@ class DoviConvertingTrackOutput(
|
||||
((buf[offset + 2].toInt() and 0xFF) shl 8) or
|
||||
(buf[offset + 3].toInt() and 0xFF)
|
||||
|
||||
private fun copyRawSample(dataLen: Int) {
|
||||
ensureOutputCapacity(dataLen)
|
||||
System.arraycopy(inputBuffer, 0, outputBuf, 0, dataLen)
|
||||
outputLen = dataLen
|
||||
}
|
||||
|
||||
private fun formatBytes(data: ByteArray, offset: Int, length: Int): String {
|
||||
val end = minOf(data.size, offset + length)
|
||||
if (offset >= end) return ""
|
||||
|
||||
@@ -19,12 +19,19 @@ class DoviExtractorOutputWrapper(
|
||||
private val emitLog: ((String, String, String) -> Unit)?,
|
||||
private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit
|
||||
) : ExtractorOutput {
|
||||
private val trackOutputs = mutableListOf<DoviConvertingTrackOutput>()
|
||||
|
||||
fun resetTracks() {
|
||||
trackOutputs.forEach { it.resetBufferedData() }
|
||||
}
|
||||
|
||||
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, emitLog)
|
||||
onVideoTrackWrapped(wrapper)
|
||||
return wrapper
|
||||
return DoviConvertingTrackOutput(original, dvMode, emitLog).also {
|
||||
trackOutputs.add(it)
|
||||
onVideoTrackWrapped(it)
|
||||
}
|
||||
}
|
||||
return original
|
||||
}
|
||||
@@ -46,16 +53,22 @@ class DoviExtractorWrapper(
|
||||
|
||||
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
|
||||
private set
|
||||
private var outputWrapper: DoviExtractorOutputWrapper? = null
|
||||
|
||||
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
delegate.init(DoviExtractorOutputWrapper(output, dvMode, emitLog) { doviTrackOutput = it })
|
||||
val wrapper = DoviExtractorOutputWrapper(output, dvMode, emitLog) { doviTrackOutput = it }
|
||||
outputWrapper = wrapper
|
||||
delegate.init(wrapper)
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int = delegate.read(input, seekPosition)
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
|
||||
override fun seek(position: Long, timeUs: Long) {
|
||||
outputWrapper?.resetTracks()
|
||||
delegate.seek(position, timeUs)
|
||||
}
|
||||
|
||||
override fun release() = delegate.release()
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ class ZlibMatroskaExtractor(
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) {
|
||||
latmOutput?.resetTracks()
|
||||
zlibOutput?.resetTracks()
|
||||
super.seek(position, timeUs)
|
||||
}
|
||||
|
||||
@@ -115,18 +116,24 @@ class ZlibMatroskaExtractor(
|
||||
) : ExtractorOutput {
|
||||
|
||||
private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null
|
||||
private val trackOutputs = mutableListOf<ZlibInflatingTrackOutput>()
|
||||
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
val wrapper = ZlibInflatingTrackOutput(original)
|
||||
lastCreatedWrapper = wrapper
|
||||
return wrapper
|
||||
return ZlibInflatingTrackOutput(original).also {
|
||||
trackOutputs.add(it)
|
||||
lastCreatedWrapper = it
|
||||
}
|
||||
}
|
||||
|
||||
fun activateLast() {
|
||||
lastCreatedWrapper?.active = true
|
||||
}
|
||||
|
||||
fun resetTracks() {
|
||||
trackOutputs.forEach { it.resetBufferedData() }
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
|
||||
+67
-1
@@ -22,7 +22,7 @@ class BufferedTransformingTrackOutputTest {
|
||||
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(1, 2)), 2, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(3)), 1, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(42, C.BUFFER_FLAG_KEY_FRAME, 3, 7, null)
|
||||
output.sampleMetadata(42, C.BUFFER_FLAG_KEY_FRAME, 3, 0, null)
|
||||
|
||||
assertArrayEquals(byteArrayOf(2, 3, 4), delegate.bytes.toByteArray())
|
||||
assertEquals(42, delegate.timeUs)
|
||||
@@ -30,6 +30,65 @@ class BufferedTransformingTrackOutputTest {
|
||||
assertEquals(0, delegate.sampleOffset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun metadataBoundsDiscardAbandonedBytesAndPreserveTrailingSample() {
|
||||
val delegate = RecordingTrackOutput()
|
||||
val output = IncrementingTrackOutput(delegate)
|
||||
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(9, 9)), 2, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(1, 2)), 2, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(3, 4, 5)), 3, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
|
||||
output.sampleMetadata(1, C.BUFFER_FLAG_KEY_FRAME, 2, 3, null)
|
||||
output.sampleMetadata(2, 0, 3, 0, null)
|
||||
|
||||
assertArrayEquals(byteArrayOf(2, 3, 4, 5, 6), delegate.bytes.toByteArray())
|
||||
assertEquals(2, delegate.metadataCount)
|
||||
assertEquals(2, delegate.timeUs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun droppedSamplePreservesTrailingSample() {
|
||||
val delegate = RecordingTrackOutput()
|
||||
val output = IncrementingTrackOutput(delegate)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(1, 2, 3)), 3, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
|
||||
output.dropNext = true
|
||||
output.sampleMetadata(1, 0, 1, 2, null)
|
||||
output.sampleMetadata(2, 0, 2, 0, null)
|
||||
|
||||
assertArrayEquals(byteArrayOf(3, 4), delegate.bytes.toByteArray())
|
||||
assertEquals(1, delegate.metadataCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resetDiscardsPendingBytesWithoutReleasingCapacity() {
|
||||
val delegate = RecordingTrackOutput()
|
||||
val output = IncrementingTrackOutput(delegate, maxBufferedSampleBytes = 4)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(9, 9, 9, 9)), 4, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
|
||||
output.resetBufferedData()
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(1, 2, 3, 4)), 4, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(1, 0, 4, 0, null)
|
||||
|
||||
assertArrayEquals(byteArrayOf(2, 3, 4, 5), delegate.bytes.toByteArray())
|
||||
assertEquals(1, delegate.metadataCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidMetadataDropsBufferedBytesAndRecovers() {
|
||||
val delegate = RecordingTrackOutput()
|
||||
val output = IncrementingTrackOutput(delegate)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(1, 2)), 2, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
|
||||
output.sampleMetadata(1, 0, 3, 0, null)
|
||||
output.sampleData(ParsableByteArray(byteArrayOf(4)), 1, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(2, 0, 1, 0, null)
|
||||
|
||||
assertArrayEquals(byteArrayOf(5), delegate.bytes.toByteArray())
|
||||
assertEquals(1, delegate.metadataCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeTransformHonorsDataReaderEndOfInputContract() {
|
||||
val output = IncrementingTrackOutput(RecordingTrackOutput())
|
||||
@@ -77,6 +136,7 @@ class BufferedTransformingTrackOutputTest {
|
||||
initialBufferSize = 2,
|
||||
maxBufferedSampleBytes = maxBufferedSampleBytes
|
||||
) {
|
||||
var dropNext = false
|
||||
private var transformed = ByteArray(2)
|
||||
|
||||
override val transformEnabled = true
|
||||
@@ -84,6 +144,10 @@ class BufferedTransformingTrackOutputTest {
|
||||
get() = transformed
|
||||
|
||||
override fun transformSample(inputLength: Int, flags: Int): Int {
|
||||
if (dropNext) {
|
||||
dropNext = false
|
||||
return -1
|
||||
}
|
||||
if (transformed.size < inputLength) transformed = ByteArray(inputLength)
|
||||
for (index in 0 until inputLength) {
|
||||
transformed[index] = (inputBuffer[index] + 1).toByte()
|
||||
@@ -97,6 +161,7 @@ class BufferedTransformingTrackOutputTest {
|
||||
var timeUs = C.TIME_UNSET
|
||||
var sampleSize = -1
|
||||
var sampleOffset = -1
|
||||
var metadataCount = 0
|
||||
|
||||
override fun format(format: Format) = Unit
|
||||
|
||||
@@ -125,6 +190,7 @@ class BufferedTransformingTrackOutputTest {
|
||||
offset: Int,
|
||||
cryptoData: TrackOutput.CryptoData?
|
||||
) {
|
||||
metadataCount++
|
||||
this.timeUs = timeUs
|
||||
sampleSize = size
|
||||
sampleOffset = offset
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.DataReader
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.extractor.Extractor
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import java.io.ByteArrayOutputStream
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class DoviConvertingTrackOutputTest {
|
||||
|
||||
@Test
|
||||
fun malformedSupplementalSampleIsDroppedAndNextSampleRecovers() {
|
||||
val delegate = RecordingTrackOutput()
|
||||
val messages = mutableListOf<String>()
|
||||
val output = DoviConvertingTrackOutput(delegate, emitLog = { _, _, message -> messages.add(message) })
|
||||
activateProfile7(output)
|
||||
|
||||
feed(output, ByteArray(3), C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA)
|
||||
|
||||
val malformed = ByteArray(5_882)
|
||||
writeInt32Be(malformed, 0, 279_101)
|
||||
feed(output, malformed, C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA)
|
||||
|
||||
assertEquals(0, delegate.samples.size)
|
||||
assertTrue(messages.any { it.contains("mainLen=279101 total=5882; dropping sample") })
|
||||
assertTrue(messages.any { it.contains("3B is too small; dropping sample") })
|
||||
|
||||
val valid = supplementalSample(
|
||||
main = byteArrayOf(0, 0, 0, 1, 0x40, 0x01),
|
||||
supplemental = byteArrayOf(0x55)
|
||||
)
|
||||
feed(output, valid, C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA)
|
||||
|
||||
assertEquals(1, delegate.samples.size)
|
||||
assertArrayEquals(valid, delegate.samples.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repeatedExtractorSeeksDoNotLeakInterruptedSamplesIntoPlayback() {
|
||||
val extractor = RecordingExtractor()
|
||||
val delegateTrack = RecordingTrackOutput()
|
||||
val output = RecordingExtractorOutput(delegateTrack)
|
||||
val messages = mutableListOf<String>()
|
||||
val wrapper = DoviExtractorWrapper(extractor, emitLog = { _, _, message -> messages.add(message) })
|
||||
wrapper.init(output)
|
||||
val wrappedTrack = extractor.output.track(1, C.TRACK_TYPE_VIDEO)
|
||||
activateProfile7(wrappedTrack)
|
||||
|
||||
repeat(2) { index ->
|
||||
val interrupted = ByteArray(5_882)
|
||||
writeInt32Be(interrupted, 0, 279_101)
|
||||
wrappedTrack.sampleData(
|
||||
ParsableByteArray(interrupted),
|
||||
interrupted.size,
|
||||
TrackOutput.SAMPLE_DATA_PART_MAIN
|
||||
)
|
||||
wrapper.seek(1234L + index, 5_000L + index)
|
||||
}
|
||||
|
||||
val valid = supplementalSample(
|
||||
main = byteArrayOf(0, 0, 0, 1, 0x40, 0x01),
|
||||
supplemental = byteArrayOf(0x55)
|
||||
)
|
||||
feed(wrappedTrack, valid, C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA)
|
||||
|
||||
assertEquals(2, extractor.seekCount)
|
||||
assertEquals(1235L, extractor.seekPosition)
|
||||
assertEquals(5_001L, extractor.seekTimeUs)
|
||||
assertEquals(1, delegateTrack.samples.size)
|
||||
assertArrayEquals(valid, delegateTrack.samples.single())
|
||||
assertTrue(messages.none { it.startsWith("Bad supplemental sample") })
|
||||
}
|
||||
|
||||
private fun activateProfile7(output: TrackOutput) {
|
||||
output.format(
|
||||
Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
|
||||
.setCodecs("dvhe.07.06")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun feed(output: TrackOutput, sample: ByteArray, flags: Int) {
|
||||
output.sampleData(ParsableByteArray(sample), sample.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(1, flags, sample.size, 0, null)
|
||||
}
|
||||
|
||||
private fun supplementalSample(main: ByteArray, supplemental: ByteArray): ByteArray {
|
||||
val sample = ByteArray(4 + main.size + supplemental.size)
|
||||
writeInt32Be(sample, 0, main.size)
|
||||
main.copyInto(sample, destinationOffset = 4)
|
||||
supplemental.copyInto(sample, destinationOffset = 4 + main.size)
|
||||
return sample
|
||||
}
|
||||
|
||||
private fun writeInt32Be(target: ByteArray, offset: Int, value: Int) {
|
||||
target[offset] = ((value ushr 24) and 0xFF).toByte()
|
||||
target[offset + 1] = ((value ushr 16) and 0xFF).toByte()
|
||||
target[offset + 2] = ((value ushr 8) and 0xFF).toByte()
|
||||
target[offset + 3] = (value and 0xFF).toByte()
|
||||
}
|
||||
|
||||
private class RecordingTrackOutput : TrackOutput {
|
||||
val samples = mutableListOf<ByteArray>()
|
||||
private val pending = ByteArrayOutputStream()
|
||||
|
||||
override fun format(format: Format) = Unit
|
||||
|
||||
override fun sampleData(
|
||||
input: DataReader,
|
||||
length: Int,
|
||||
allowEndOfInput: Boolean,
|
||||
sampleDataPart: Int
|
||||
): Int {
|
||||
val buffer = ByteArray(length)
|
||||
val read = input.read(buffer, 0, length)
|
||||
if (read > 0) pending.write(buffer, 0, read)
|
||||
return read
|
||||
}
|
||||
|
||||
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
|
||||
val buffer = ByteArray(length)
|
||||
data.readBytes(buffer, 0, length)
|
||||
pending.write(buffer)
|
||||
}
|
||||
|
||||
override fun sampleMetadata(
|
||||
timeUs: Long,
|
||||
flags: Int,
|
||||
size: Int,
|
||||
offset: Int,
|
||||
cryptoData: TrackOutput.CryptoData?
|
||||
) {
|
||||
samples.add(pending.toByteArray())
|
||||
pending.reset()
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordingExtractor : Extractor {
|
||||
lateinit var output: ExtractorOutput
|
||||
var seekPosition = C.INDEX_UNSET.toLong()
|
||||
var seekTimeUs = C.TIME_UNSET
|
||||
var seekCount = 0
|
||||
|
||||
override fun sniff(input: ExtractorInput): Boolean = true
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
this.output = output
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int = Extractor.RESULT_END_OF_INPUT
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) {
|
||||
seekCount++
|
||||
this.seekPosition = position
|
||||
seekTimeUs = timeUs
|
||||
}
|
||||
|
||||
override fun release() = Unit
|
||||
}
|
||||
|
||||
private class RecordingExtractorOutput(
|
||||
private val trackOutput: TrackOutput
|
||||
) : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput = trackOutput
|
||||
override fun endTracks() = Unit
|
||||
override fun seekMap(seekMap: SeekMap) = Unit
|
||||
}
|
||||
}
|
||||
+25
-5
@@ -35,7 +35,7 @@ class ZlibInflatingTrackOutputTest {
|
||||
compressed.size - split,
|
||||
TrackOutput.SAMPLE_DATA_PART_MAIN
|
||||
)
|
||||
output.sampleMetadata(42L, C.BUFFER_FLAG_KEY_FRAME, compressed.size, 7, null)
|
||||
output.sampleMetadata(42L, C.BUFFER_FLAG_KEY_FRAME, compressed.size, 0, null)
|
||||
|
||||
assertArrayEquals(original, delegate.retained.toByteArray())
|
||||
assertEquals(42L, delegate.timeUs)
|
||||
@@ -45,6 +45,24 @@ class ZlibInflatingTrackOutputTest {
|
||||
assertEquals(1, delegate.metadataCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeTransformUsesMetadataBoundariesForBufferedSamples() {
|
||||
val first = ByteArray(4096) { (it % 19).toByte() }
|
||||
val second = ByteArray(2048) { (it % 23).toByte() }
|
||||
val firstCompressed = deflate(first)
|
||||
val secondCompressed = deflate(second)
|
||||
val combined = firstCompressed + secondCompressed
|
||||
val delegate = RecordingTrackOutput(retainBytes = true)
|
||||
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
|
||||
|
||||
output.sampleData(ParsableByteArray(combined), combined.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(1, 0, firstCompressed.size, secondCompressed.size, null)
|
||||
output.sampleMetadata(2, 0, secondCompressed.size, 0, null)
|
||||
|
||||
assertArrayEquals(first + second, delegate.retained.toByteArray())
|
||||
assertEquals(2, delegate.metadataCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exactlySixteenMiBInflatedSampleIsAccepted() {
|
||||
val size = 16 * 1024 * 1024
|
||||
@@ -120,15 +138,17 @@ class ZlibInflatingTrackOutputTest {
|
||||
@Test
|
||||
fun inactiveWrapperDelegatesBytesAndMetadataUnchanged() {
|
||||
val bytes = byteArrayOf(9, 8, 7, 6)
|
||||
val trailing = byteArrayOf(5, 4)
|
||||
val allBytes = bytes + trailing
|
||||
val delegate = RecordingTrackOutput(retainBytes = true)
|
||||
val output = ZlibInflatingTrackOutput(delegate)
|
||||
|
||||
output.sampleData(ParsableByteArray(bytes), bytes.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(99L, 3, bytes.size, 2, null)
|
||||
output.sampleData(ParsableByteArray(allBytes), allBytes.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
output.sampleMetadata(99L, 3, bytes.size, trailing.size, null)
|
||||
|
||||
assertArrayEquals(bytes, delegate.retained.toByteArray())
|
||||
assertArrayEquals(allBytes, delegate.retained.toByteArray())
|
||||
assertEquals(bytes.size, delegate.sampleSize)
|
||||
assertEquals(2, delegate.offset)
|
||||
assertEquals(trailing.size, delegate.offset)
|
||||
assertEquals(99L, delegate.timeUs)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user