fix(player): fall back to decoding when a TrueHD stream contradicts its container
Selection reads Format.sampleRate, but the rate family is only certain once a major sync is parsed. When a container announces the 48kHz family and the bitstream announces 44.1kHz, the packer emits nothing: handleBuffer consumed the input and reported success, so the stream played as silence for as long as it lasted. TrueHdMatPacker.reset also left the flag latched, so every later stream on that packer emitted nothing too. Leave the offending access unit in the buffer, signal the capability change, and let the decoder take the stream over. The packer clears the flag on reset. The latch has to outlive both flush and reset. media3 resets every renderer disabled by a new selection before enabling its replacement (ExoPlayerImplInternal.enableRenderers), and both audio renderers share this sink, so the outgoing renderer's reset arrives in the middle of the handover the latch exists to cause; clearing it there loops straight back into the mismatch. The real boundary is a new media item, which only ExoPlayerCore knows, so it signals one before setting a new source. The same-item recovery, DV-mode and subtitle reloads deliberately do not. It is a generation rather than a flag because that hook runs on the app thread while the mismatch is found on the playback thread: a late buffer from the outgoing stream would otherwise disable the carrier for its successor. Verified on the SEI Box R (Android 14, armv7) with a genuine 44.1kHz TrueHD stream in a container patched to announce 48000, so the bitstream and its checksums stay valid. The sink enters the carrier at 192kHz, reports the mismatch, hands over to FFmpeg and plays on. The device test asserts that sequence from the sink's own diagnostics, because the mismatch fires before the carrier opens an AudioTrack and the rate sequence alone cannot distinguish it from never having selected the carrier.
This commit is contained in:
Binary file not shown.
+119
-3
@@ -5,7 +5,6 @@ import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.Log
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
@@ -185,11 +184,128 @@ class TrueHdSpeedTransitionTest {
|
||||
fixture.delete()
|
||||
}
|
||||
|
||||
private fun copyFixture(context: android.content.Context): File {
|
||||
private fun copyFixture(
|
||||
context: android.content.Context,
|
||||
asset: String = "ffmpeg/truehd_speed_repro.mka"
|
||||
): File {
|
||||
val output = File.createTempFile("truehd-speed-", null, context.cacheDir)
|
||||
InstrumentationRegistry.getInstrumentation().context.assets
|
||||
.open("ffmpeg/truehd_speed_repro.mka")
|
||||
.open(asset)
|
||||
.use { input -> output.outputStream().use { input.copyTo(it) } }
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* The container announces 48kHz, which selection trusts, but the bitstream is genuinely 44.1kHz
|
||||
* TrueHD, which the carrier does not cover (#1804). The packer emits nothing in that state, so
|
||||
* the sink has to hand the stream to the decoder rather than play silence.
|
||||
*/
|
||||
@Test
|
||||
fun aRateFamilyMismatchFallsBackToTheDecoderInsteadOfGoingSilent() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
if (!supportsTrueHdMatCarrier(context)) {
|
||||
Log.i(TAG, "==== MISMATCH SKIPPED: device has no carrier route ====")
|
||||
return
|
||||
}
|
||||
val fixture = copyFixture(context, "ffmpeg/truehd_mismatch_repro.mka")
|
||||
val thread = HandlerThread("truehd-mismatch-test").apply { start() }
|
||||
val handler = Handler(thread.looper)
|
||||
|
||||
val playing = CountDownLatch(1)
|
||||
val audioDecoder = AtomicReference<String?>(null)
|
||||
val trackRate = AtomicReference(-1)
|
||||
val rateSequence = java.util.Collections.synchronizedList(mutableListOf<Int>())
|
||||
val diagnostics = java.util.Collections.synchronizedList(mutableListOf<String>())
|
||||
val player = AtomicReference<ExoPlayer?>(null)
|
||||
|
||||
handler.post {
|
||||
val factory = PlezyRenderersFactory(context).apply {
|
||||
setEnableDecoderFallback(true)
|
||||
setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
audioDiagnosticsLogger = { _, _, message ->
|
||||
diagnostics.add(message)
|
||||
Log.i(TAG, "mismatch sink: $message")
|
||||
}
|
||||
}
|
||||
val selector = DefaultTrackSelector(context).apply {
|
||||
setParameters(
|
||||
buildUponParameters().setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
|
||||
)
|
||||
}
|
||||
val exo = ExoPlayer.Builder(context, factory).setTrackSelector(selector).build()
|
||||
player.set(exo)
|
||||
exo.addAnalyticsListener(
|
||||
object : AnalyticsListener {
|
||||
override fun onAudioDecoderInitialized(
|
||||
eventTime: AnalyticsListener.EventTime,
|
||||
decoderName: String,
|
||||
initializedTimestampMs: Long,
|
||||
initializationDurationMs: Long
|
||||
) {
|
||||
audioDecoder.set(decoderName)
|
||||
Log.i(TAG, "mismatch audio decoder: $decoderName")
|
||||
}
|
||||
|
||||
override fun onAudioTrackInitialized(
|
||||
eventTime: AnalyticsListener.EventTime,
|
||||
config: AudioSink.AudioTrackConfig
|
||||
) {
|
||||
trackRate.set(config.sampleRate)
|
||||
rateSequence.add(config.sampleRate)
|
||||
Log.i(TAG, "mismatch AudioTrack rate=${config.sampleRate}")
|
||||
}
|
||||
|
||||
override fun onAudioInputFormatChanged(
|
||||
eventTime: AnalyticsListener.EventTime,
|
||||
format: androidx.media3.common.Format,
|
||||
decoderReuseEvaluation: androidx.media3.exoplayer.DecoderReuseEvaluation?
|
||||
) {
|
||||
Log.i(TAG, "mismatch INPUT format: mime=${format.sampleMimeType} rate=${format.sampleRate} ch=${format.channelCount}")
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(eventTime: AnalyticsListener.EventTime, isPlaying: Boolean) {
|
||||
if (isPlaying) playing.countDown()
|
||||
}
|
||||
}
|
||||
)
|
||||
val source = ProgressiveMediaSource.Factory(
|
||||
DefaultDataSource.Factory(context),
|
||||
DefaultExtractorsFactory()
|
||||
).createMediaSource(MediaItem.fromUri(Uri.fromFile(fixture)))
|
||||
exo.setMediaSource(source)
|
||||
exo.prepare()
|
||||
exo.playWhenReady = true
|
||||
}
|
||||
|
||||
assertTrue("playback never started", playing.await(30, TimeUnit.SECONDS))
|
||||
Thread.sleep(SETTLE_MS)
|
||||
val positionFirst = positionOf(handler, player)
|
||||
Thread.sleep(SETTLE_MS)
|
||||
val positionSecond = positionOf(handler, player)
|
||||
|
||||
val decoder = audioDecoder.get()
|
||||
val rate = trackRate.get()
|
||||
Log.i(
|
||||
TAG,
|
||||
"==== MISMATCH RESULT: rates=$rateSequence decoder=$decoder rate=$rate " +
|
||||
"position=${positionFirst}ms -> ${positionSecond}ms ===="
|
||||
)
|
||||
teardown(handler, player, thread, fixture)
|
||||
|
||||
// Without this the test would also pass on a build where the carrier was never selected at all:
|
||||
// the mismatch fires on the first access unit, before the carrier writes a burst, so no 192kHz
|
||||
// AudioTrack is ever opened and the rate sequence alone cannot tell the two apart.
|
||||
assertTrue(
|
||||
"the carrier must have been entered and then left at runtime, saw: $diagnostics",
|
||||
diagnostics.any { it.contains("MAT/IEC 61937 carrier") } &&
|
||||
diagnostics.any { it.contains("44.1kHz-family rate its container did not") }
|
||||
)
|
||||
assertTrue("a decoder must take the stream over instead of the carrier", decoder != null)
|
||||
assertNotEquals(
|
||||
"the stream must not still be riding the carrier",
|
||||
TrueHdMatPacker.CARRIER_SAMPLE_RATE,
|
||||
rate
|
||||
)
|
||||
assertTrue("playback must keep advancing after the fallback", positionSecond > positionFirst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3442,6 +3442,11 @@ class ExoPlayerCore(private val activity: Activity) :
|
||||
)
|
||||
emitSeekable(false, force = true)
|
||||
|
||||
// Only here: this is the one caller that is a genuinely new item. The recovery, DV-mode and
|
||||
// subtitle reloads all reuse setCurrentMediaSource for the *same* stream, and clearing
|
||||
// per-stream audio decisions there would undo them and loop.
|
||||
renderersFactory?.beginMediaItem()
|
||||
|
||||
exoPlayer?.apply {
|
||||
setCurrentMediaSource(this, uri, startPositionMs)
|
||||
prepare()
|
||||
|
||||
@@ -151,6 +151,16 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
)
|
||||
}
|
||||
|
||||
private var trueHdCarrierSink: TrueHdCarrierSink? = null
|
||||
|
||||
/**
|
||||
* Clears per-stream carrier state that must survive renderer resets but not a new media item.
|
||||
* Call before setting a new source; see [TrueHdCarrierSink.beginMediaItem].
|
||||
*/
|
||||
fun beginMediaItem() {
|
||||
trueHdCarrierSink?.beginMediaItem()
|
||||
}
|
||||
|
||||
override fun buildAudioSink(
|
||||
context: Context,
|
||||
enableFloatOutput: Boolean,
|
||||
@@ -201,7 +211,7 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
carrierRouteAvailable = { supportsTrueHdMatCarrier(context) },
|
||||
directOutputBlocked = { format -> shouldBlockDirectAudioOutput?.invoke(format) == true },
|
||||
log = audioDiagnosticsLogger
|
||||
)
|
||||
).also { trueHdCarrierSink = it }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.analytics.PlayerId
|
||||
import androidx.media3.exoplayer.audio.AudioSink
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Routes Dolby TrueHD through a MAT/IEC 61937 carrier, and everything else through the normal sink
|
||||
@@ -78,6 +79,33 @@ internal class TrueHdCarrierSink(
|
||||
private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
|
||||
private var sinkListener: AudioSink.Listener? = null
|
||||
|
||||
/**
|
||||
* Latched when a stream's bitstream contradicts the rate its container announced, which selection
|
||||
* was made from.
|
||||
*
|
||||
* Deliberately outlives [flush] and [reset]: media3 resets every renderer disabled by a new
|
||||
* selection before enabling the replacement (ExoPlayerImplInternal.enableRenderers), and both
|
||||
* audio renderers share this sink, so the outgoing renderer's reset lands here in the middle of
|
||||
* the very handover this latch exists to cause. Clearing it there would re-offer the carrier and
|
||||
* loop straight back into the mismatch.
|
||||
*
|
||||
* The real boundary is a new media item, which only the caller knows and which happens before
|
||||
* selection asks anything. [beginMediaItem] is that hook; without it one malformed item would
|
||||
* cost bitstreaming for every later item sharing the player.
|
||||
*
|
||||
* It is held as a generation rather than a flag because that hook is called on the app thread
|
||||
* while the mismatch is discovered on the playback thread. A late buffer from the outgoing stream
|
||||
* can land after the new item began; latching the generation the carrier was configured for makes
|
||||
* that write name the stream it belongs to instead of poisoning its successor.
|
||||
*/
|
||||
private val mediaGeneration = AtomicInteger(0)
|
||||
|
||||
/** Generation [mediaGeneration] held when the carrier was last configured; playback thread. */
|
||||
private var configuredGeneration = 0
|
||||
|
||||
@Volatile
|
||||
private var mismatchGeneration = -1
|
||||
|
||||
// --- Selection ---
|
||||
|
||||
/**
|
||||
@@ -93,6 +121,7 @@ internal class TrueHdCarrierSink(
|
||||
*/
|
||||
private fun shouldUseCarrier(format: Format): Boolean {
|
||||
if (format.sampleMimeType != MimeTypes.AUDIO_TRUEHD) return false
|
||||
if (mismatchGeneration == mediaGeneration.get()) return false
|
||||
if (!isCarrierRateFamily(format.sampleRate)) return false
|
||||
if (playbackParameters.speed != 1f) return false
|
||||
if (directOutputBlocked(format)) return false
|
||||
@@ -153,6 +182,7 @@ internal class TrueHdCarrierSink(
|
||||
)
|
||||
}
|
||||
carrierActive = useCarrier
|
||||
configuredGeneration = mediaGeneration.get()
|
||||
active = if (useCarrier) carrierSink else defaultSink
|
||||
discardCarrierState()
|
||||
|
||||
@@ -193,6 +223,23 @@ internal class TrueHdCarrierSink(
|
||||
return true
|
||||
}
|
||||
val burst = packer.packAccessUnit(remaining, offset, length)
|
||||
if (packer.unsupportedRateFamily) {
|
||||
// Selection is made from Format.sampleRate, so the bitstream disagrees with its container.
|
||||
// The packer emits nothing in that state; consuming here would turn the stream into
|
||||
// silence. Leave this unit in the buffer, latch the carrier off, and ask for reselection so
|
||||
// the decoder takes over and receives it.
|
||||
if (mismatchGeneration != configuredGeneration) {
|
||||
mismatchGeneration = configuredGeneration
|
||||
log?.invoke(
|
||||
"warn",
|
||||
"audio",
|
||||
"TrueHD bitstream announced a 44.1kHz-family rate its container did not; " +
|
||||
"leaving the carrier so it decodes"
|
||||
)
|
||||
sinkListener?.onAudioCapabilitiesChanged()
|
||||
}
|
||||
return false
|
||||
}
|
||||
offset += length
|
||||
buffer.position(buffer.position() + length)
|
||||
if (burst == null) continue
|
||||
@@ -205,16 +252,6 @@ internal class TrueHdCarrierSink(
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (packer.unsupportedRateFamily) {
|
||||
// Selection is made from Format.sampleRate, so this means the bitstream disagrees with the
|
||||
// container. Nothing is packed in that state, which would be silence rather than a glitch.
|
||||
log?.invoke(
|
||||
"warn",
|
||||
"audio",
|
||||
"TrueHD bitstream announced a 44.1kHz-family rate the container did not; carrier is producing nothing"
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -366,7 +403,16 @@ internal class TrueHdCarrierSink(
|
||||
carrierSink.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the owner immediately before a new media item is set, which is the only point where
|
||||
* "this stream lied about its rate" stops being true and the carrier can be offered again.
|
||||
*/
|
||||
fun beginMediaItem() {
|
||||
mediaGeneration.incrementAndGet()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
mediaGeneration.incrementAndGet()
|
||||
discardCarrierState()
|
||||
defaultSink.release()
|
||||
carrierSink.release()
|
||||
|
||||
@@ -105,6 +105,9 @@ internal class TrueHdMatPacker {
|
||||
previousTiming = 0
|
||||
previousSize = 0
|
||||
samplesPerFrame = 0
|
||||
// The family is re-learned from the next major sync. Leaving it latched here would make every
|
||||
// later stream on this packer emit nothing.
|
||||
unsupportedRateFamily = false
|
||||
java.util.Arrays.fill(matBuffers[0], 0)
|
||||
java.util.Arrays.fill(matBuffers[1], 0)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,130 @@ class TrueHdCarrierSinkTest {
|
||||
assertEquals(0, listener.capabilityInvalidations)
|
||||
}
|
||||
|
||||
// --- Rate-family mismatch discovered mid-stream ---
|
||||
|
||||
/**
|
||||
* Selection reads Format.sampleRate; the rate family is only certain once a major sync is parsed.
|
||||
* When they disagree the packer emits nothing, so consuming the input would turn the stream into
|
||||
* silence. The unit has to survive for whoever takes over.
|
||||
*/
|
||||
@Test
|
||||
fun aRateFamilyMismatchLeavesTheCarrierInsteadOfGoingSilent() {
|
||||
val carrier = FakeSink()
|
||||
val carrierSink = sink(carrier = carrier)
|
||||
val listener = RecordingSinkListener()
|
||||
carrierSink.setListener(listener)
|
||||
carrierSink.configure(trueHdFormat(), 0, null)
|
||||
|
||||
val buffer = ByteBuffer.wrap(fortyFourFamilyAccessUnits())
|
||||
val accepted = carrierSink.handleBuffer(buffer, 0L, 1)
|
||||
|
||||
assertFalse("a mismatch must apply back pressure, not report success", accepted)
|
||||
assertEquals("the offending access unit must stay in the buffer", 0, buffer.position())
|
||||
assertEquals("the renderer must be asked to reselect", 1, listener.capabilityInvalidations)
|
||||
assertEquals(
|
||||
"TrueHD must now decode rather than ride the carrier",
|
||||
AudioSink.SINK_FORMAT_UNSUPPORTED,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
assertEquals("nothing may reach the carrier delegate", 0, carrier.written.size())
|
||||
}
|
||||
|
||||
/**
|
||||
* media3 resets every renderer disabled by a new selection before enabling the replacement, and
|
||||
* both audio renderers share this sink. That reset lands mid-handover, so a latch cleared there
|
||||
* would re-offer the carrier and loop straight back into the mismatch.
|
||||
*/
|
||||
@Test
|
||||
fun theMismatchLatchSurvivesTheResetThatAccompaniesTheHandover() {
|
||||
val carrierSink = sink()
|
||||
carrierSink.setListener(RecordingSinkListener())
|
||||
carrierSink.configure(trueHdFormat(), 0, null)
|
||||
carrierSink.handleBuffer(ByteBuffer.wrap(fortyFourFamilyAccessUnits()), 0L, 1)
|
||||
|
||||
carrierSink.flush()
|
||||
carrierSink.reset()
|
||||
|
||||
assertEquals(
|
||||
"the carrier must stay off across the handover",
|
||||
AudioSink.SINK_FORMAT_UNSUPPORTED,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
}
|
||||
|
||||
/** The latch is scoped to the sink, so a released sink starts clean. */
|
||||
@Test
|
||||
fun releasingTheSinkClearsTheMismatchLatch() {
|
||||
val carrierSink = sink()
|
||||
carrierSink.setListener(RecordingSinkListener())
|
||||
carrierSink.configure(trueHdFormat(), 0, null)
|
||||
carrierSink.handleBuffer(ByteBuffer.wrap(fortyFourFamilyAccessUnits()), 0L, 1)
|
||||
|
||||
carrierSink.release()
|
||||
|
||||
assertEquals(
|
||||
AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Genuine 44.1kHz TrueHD access units, encoder-produced. Flipping the rate nibble in a 48kHz
|
||||
* stream would invalidate the major sync checksum, which is not the case being modelled.
|
||||
*/
|
||||
private val fortyFourFamilyUnits by lazy {
|
||||
checkNotNull(javaClass.classLoader?.getResourceAsStream("truehd_441_access_units.bin"))
|
||||
.use { it.readBytes() }
|
||||
}
|
||||
|
||||
private fun fortyFourFamilyAccessUnits(): ByteArray = fortyFourFamilyUnits
|
||||
|
||||
/**
|
||||
* The mismatch is a property of one stream. The owner signals the real boundary, because the sink
|
||||
* cannot see it: renderer resets and flushes happen constantly within a single item.
|
||||
*/
|
||||
@Test
|
||||
fun aNewMediaItemClearsTheMismatchLatch() {
|
||||
val carrierSink = sink()
|
||||
carrierSink.setListener(RecordingSinkListener())
|
||||
carrierSink.configure(trueHdFormat(), 0, null)
|
||||
carrierSink.handleBuffer(ByteBuffer.wrap(fortyFourFamilyAccessUnits()), 0L, 1)
|
||||
assertEquals(
|
||||
AudioSink.SINK_FORMAT_UNSUPPORTED,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
|
||||
carrierSink.beginMediaItem()
|
||||
|
||||
assertEquals(
|
||||
"a new item must be able to bitstream again",
|
||||
AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The boundary hook runs on the app thread while the mismatch is found on the playback thread, so
|
||||
* a buffer from the outgoing stream can be handled after the new item began. That write must name
|
||||
* the stream it came from rather than disabling the carrier for its successor.
|
||||
*/
|
||||
@Test
|
||||
fun aLateMismatchFromThePreviousItemDoesNotPoisonTheNewOne() {
|
||||
val carrierSink = sink()
|
||||
carrierSink.setListener(RecordingSinkListener())
|
||||
carrierSink.configure(trueHdFormat(), 0, null)
|
||||
|
||||
// The new item starts before the outgoing stream's last buffer is handled.
|
||||
carrierSink.beginMediaItem()
|
||||
carrierSink.handleBuffer(ByteBuffer.wrap(fortyFourFamilyAccessUnits()), 0L, 1)
|
||||
|
||||
assertEquals(
|
||||
"the stale mismatch belongs to the previous item",
|
||||
AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY,
|
||||
carrierSink.getFormatSupport(trueHdFormat())
|
||||
)
|
||||
}
|
||||
|
||||
/** Everything that is not TrueHD keeps going to the existing processed sink. */
|
||||
@Test
|
||||
fun otherFormatsAreLeftToTheNormalSink() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.edde746.plezy.exoplayer
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@@ -153,4 +154,27 @@ class TrueHdMatPackerTest {
|
||||
}
|
||||
|
||||
private fun readLittleEndianShort(data: ByteArray, offset: Int): Int = (data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
|
||||
/**
|
||||
* The flag gates every later call, so leaving it latched across a reset would make a packer that
|
||||
* once saw a 44.1kHz-family stream emit nothing for the rest of its life.
|
||||
*/
|
||||
@Test
|
||||
fun resetClearsTheUnsupportedRateFamilyFlag() {
|
||||
val packer = TrueHdMatPacker()
|
||||
val units = resource("truehd_441_access_units.bin")
|
||||
|
||||
val length = TrueHdMatPacker.accessUnitLength(units, 0, units.size)
|
||||
packer.packAccessUnit(units, 0, length)
|
||||
assertTrue("the fixture must actually announce the 44.1kHz family", packer.unsupportedRateFamily)
|
||||
|
||||
packer.reset()
|
||||
|
||||
assertFalse("reset must clear the flag", packer.unsupportedRateFamily)
|
||||
assertArrayEquals(
|
||||
"a clean packer must reproduce the stream after a poisoned one",
|
||||
golden,
|
||||
packAll(accessUnits, TrueHdMatPacker())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user