From d6b24c3e075b6afda814c1990febcd14d81df034 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:10:38 +0200 Subject: [PATCH] fix(android): prevent Dolby Vision seek crashes --- .../BufferedTransformingTrackOutput.kt | 56 +++++- .../exoplayer/DoviConvertingTrackOutput.kt | 59 +++--- .../plezy/exoplayer/DoviExtractorWrapper.kt | 23 ++- .../plezy/exoplayer/ZlibMatroskaExtractor.kt | 13 +- .../BufferedTransformingTrackOutputTest.kt | 68 ++++++- .../DoviConvertingTrackOutputTest.kt | 185 ++++++++++++++++++ .../exoplayer/ZlibInflatingTrackOutputTest.kt | 30 ++- 7 files changed, 376 insertions(+), 58 deletions(-) create mode 100644 android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutputTest.kt diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutput.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutput.kt index 07ff4905..cc8988ee 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutput.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutput.kt @@ -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) - if (transformedLength < 0) return + // 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 + } - outputParsable.reset(transformedBuffer, transformedLength) - delegate.sampleData(outputParsable, transformedLength, TrackOutput.SAMPLE_DATA_PART_MAIN) - delegate.sampleMetadata(timeUs, flags, transformedLength, 0, cryptoData) + 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) { diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutput.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutput.kt index 05ddd360..2d720eb1 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutput.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutput.kt @@ -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 "" diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviExtractorWrapper.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviExtractorWrapper.kt index cd8c9858..50caaa2b 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviExtractorWrapper.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/DoviExtractorWrapper.kt @@ -19,12 +19,19 @@ class DoviExtractorOutputWrapper( private val emitLog: ((String, String, String) -> Unit)?, private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit ) : ExtractorOutput { + private val trackOutputs = mutableListOf() + + 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() } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ZlibMatroskaExtractor.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ZlibMatroskaExtractor.kt index 0bbec6f3..9402190a 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ZlibMatroskaExtractor.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ZlibMatroskaExtractor.kt @@ -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() 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) } diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutputTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutputTest.kt index 7c15f86f..f4c65e5d 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutputTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/BufferedTransformingTrackOutputTest.kt @@ -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 diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutputTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutputTest.kt new file mode 100644 index 00000000..8e5d872e --- /dev/null +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/DoviConvertingTrackOutputTest.kt @@ -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() + 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() + 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() + 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 + } +} diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/ZlibInflatingTrackOutputTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/ZlibInflatingTrackOutputTest.kt index b903e84b..be08598e 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/ZlibInflatingTrackOutputTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/ZlibInflatingTrackOutputTest.kt @@ -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) }