fix(player): make TrueHD carrier-or-decode and never lose access units
Three defects in the carrier path, two of them found on hardware (#1804). Falling through to the normal sink when the carrier was unavailable handed TrueHD straight back to media3's raw ENCODING_DOLBY_TRUEHD path — the exact configuration this issue is about. TrueHD is now binary: the carrier, or reported unsupported so the bundled FFmpeg decoder takes it. media3's raw path has no demonstrated working case here and two broken ones, and even Kodi's raw fallback is a different thing, offered only after verifying at 192kHz. The 44.1kHz family was decided from a packer flag that is only set once a major sync has been parsed, long after selection. The carrier was therefore chosen for those streams and then packed nothing, which is silence rather than a glitch. It is decided from Format.sampleRate now, with the packer flag left as a loud runtime backstop for a bitstream that disagrees with its container. handleBuffer consumed the whole input buffer even when a burst was refused part-way through, dropping every access unit behind it — a media3 sample holds sixteen. The buffer position now advances per unit and the method returns false with the remainder intact, which is media3's own retry contract. A test rejects a burst mid-sample and asserts the carrier output is still byte-identical. The capability gate also needed tightening. getMinBufferSize answers yes for the 192kHz/7.1 IEC tuple on a Shield and the AudioTrack then fails to initialise: it reports that a buffer can be sized, not that the route will carry the format. Without getDirectPlaybackSupport there is no way to separate the two, so the carrier is not offered below API 33 and TrueHD decodes exactly as before. Verified on both connected boxes. SEI Box R (Android 14): carrier selected, AudioTrack built as IEC61937 at 192kHz/7.1, no decoder instantiated, clock tracks wall time. Nvidia Shield (Android 11): carrier declined, FFmpeg decoder selected, identical to its behaviour before this work.
This commit is contained in:
@@ -104,6 +104,12 @@ internal fun supportedMpvSpdifCodecs(context: Context): String {
|
|||||||
* decode or wedge.
|
* decode or wedge.
|
||||||
*/
|
*/
|
||||||
internal fun supportsTrueHdMatCarrier(context: Context): Boolean {
|
internal fun supportsTrueHdMatCarrier(context: Context): Boolean {
|
||||||
|
// getMinBufferSize alone is not sufficient. On a Shield it answers yes for the 192kHz/7.1 IEC
|
||||||
|
// tuple and the AudioTrack then fails to initialise; it reports that a buffer can be sized, not
|
||||||
|
// that the route will carry the format. Without getDirectPlaybackSupport there is no way to tell
|
||||||
|
// the two apart, so below API 33 the carrier is not offered and TrueHD decodes as before.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false
|
||||||
|
|
||||||
val rate = TrueHdMatPacker.CARRIER_SAMPLE_RATE
|
val rate = TrueHdMatPacker.CARRIER_SAMPLE_RATE
|
||||||
val mask = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
val mask = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||||
val sizedOk = try {
|
val sizedOk = try {
|
||||||
@@ -112,7 +118,6 @@ internal fun supportsTrueHdMatCarrier(context: Context): Boolean {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
if (!sizedOk) return false
|
if (!sizedOk) return false
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val audioAttributes = AudioAttributes.Builder()
|
val audioAttributes = AudioAttributes.Builder()
|
||||||
|
|||||||
@@ -85,15 +85,25 @@ internal class TrueHdCarrierSink(
|
|||||||
* Speed changes are excluded deliberately: a bitstream cannot be resampled, so anything other
|
* Speed changes are excluded deliberately: a bitstream cannot be resampled, so anything other
|
||||||
* than 1.0x has to decode. The same is true of the existing downmix/normalization blocks, which
|
* than 1.0x has to decode. The same is true of the existing downmix/normalization blocks, which
|
||||||
* [directOutputBlocked] already reports.
|
* [directOutputBlocked] already reports.
|
||||||
|
*
|
||||||
|
* The rate family is decided from the format rather than from the packer, which only learns it
|
||||||
|
* from a major sync once buffers are already flowing — far too late for a selection that happens
|
||||||
|
* before configure.
|
||||||
*/
|
*/
|
||||||
private fun shouldUseCarrier(format: Format): Boolean {
|
private fun shouldUseCarrier(format: Format): Boolean {
|
||||||
if (format.sampleMimeType != MimeTypes.AUDIO_TRUEHD) return false
|
if (format.sampleMimeType != MimeTypes.AUDIO_TRUEHD) return false
|
||||||
if (packer.unsupportedRateFamily) return false
|
if (!isCarrierRateFamily(format.sampleRate)) return false
|
||||||
if (playbackParameters.speed != 1f) return false
|
if (playbackParameters.speed != 1f) return false
|
||||||
if (directOutputBlocked(format)) return false
|
if (directOutputBlocked(format)) return false
|
||||||
return carrierRouteAvailable()
|
return carrierRouteAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 48kHz family rides the 192kHz carrier this path builds. The 44.1kHz family needs a 176.4kHz
|
||||||
|
* carrier, which would be a different AudioTrack tuple throughout, so it decodes instead.
|
||||||
|
*/
|
||||||
|
private fun isCarrierRateFamily(sampleRate: Int): Boolean = sampleRate == 48_000 || sampleRate == 96_000 || sampleRate == 192_000
|
||||||
|
|
||||||
/** The PCM-shaped format the carrier delegate is configured with. */
|
/** The PCM-shaped format the carrier delegate is configured with. */
|
||||||
private fun carrierFormat(): Format = Format.Builder()
|
private fun carrierFormat(): Format = Format.Builder()
|
||||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||||
@@ -102,11 +112,30 @@ internal class TrueHdCarrierSink(
|
|||||||
.setSampleRate(TrueHdMatPacker.CARRIER_SAMPLE_RATE)
|
.setSampleRate(TrueHdMatPacker.CARRIER_SAMPLE_RATE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun supportsFormat(format: Format): Boolean =
|
/**
|
||||||
if (shouldUseCarrier(format)) true else defaultSink.supportsFormat(format)
|
* TrueHD is deliberately binary: the carrier, or decoded PCM. Never media3's own raw TrueHD path.
|
||||||
|
*
|
||||||
|
* That path builds an `ENCODING_DOLBY_TRUEHD` track at the *stream* rate, which is the
|
||||||
|
* configuration this issue is about — one box takes a single write and never advances its
|
||||||
|
* playback head, another freezes for ten seconds, and the third declines it and decodes anyway.
|
||||||
|
* Even Kodi's raw fallback is a different thing: it only offers raw TrueHD after verifying it at
|
||||||
|
* 192kHz, which media3 never requests. So when the carrier is unavailable — no IEC route, a speed
|
||||||
|
* change, downmix, normalization, or a 44.1kHz-family stream — report unsupported and let the
|
||||||
|
* bundled FFmpeg decoder take it, exactly as the existing decoded-PCM fallback does.
|
||||||
|
*/
|
||||||
|
private fun isTrueHd(format: Format): Boolean = format.sampleMimeType == MimeTypes.AUDIO_TRUEHD
|
||||||
|
|
||||||
override fun getFormatSupport(format: Format): Int =
|
override fun supportsFormat(format: Format): Boolean = when {
|
||||||
if (shouldUseCarrier(format)) AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY else defaultSink.getFormatSupport(format)
|
shouldUseCarrier(format) -> true
|
||||||
|
isTrueHd(format) -> false
|
||||||
|
else -> defaultSink.supportsFormat(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFormatSupport(format: Format): Int = when {
|
||||||
|
shouldUseCarrier(format) -> AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY
|
||||||
|
isTrueHd(format) -> AudioSink.SINK_FORMAT_UNSUPPORTED
|
||||||
|
else -> defaultSink.getFormatSupport(format)
|
||||||
|
}
|
||||||
|
|
||||||
override fun configure(inputFormat: Format, specifiedBufferSize: Int, outputChannels: IntArray?) {
|
override fun configure(inputFormat: Format, specifiedBufferSize: Int, outputChannels: IntArray?) {
|
||||||
val useCarrier = shouldUseCarrier(inputFormat)
|
val useCarrier = shouldUseCarrier(inputFormat)
|
||||||
@@ -145,17 +174,26 @@ internal class TrueHdCarrierSink(
|
|||||||
pendingBurst = null
|
pendingBurst = null
|
||||||
}
|
}
|
||||||
if (!buffer.hasRemaining()) return true
|
if (!buffer.hasRemaining()) return true
|
||||||
|
|
||||||
val sample = ByteArray(buffer.remaining())
|
|
||||||
buffer.duplicate().get(sample)
|
|
||||||
if (carrierAnchorUs == C.TIME_UNSET) carrierAnchorUs = presentationTimeUs
|
if (carrierAnchorUs == C.TIME_UNSET) carrierAnchorUs = presentationTimeUs
|
||||||
|
|
||||||
|
// One media3 sample holds many access units. The buffer position advances per unit, so a burst
|
||||||
|
// the delegate refuses leaves the units behind it in the buffer for the retry rather than
|
||||||
|
// dropping them — media3 re-delivers the same buffer whenever this returns false.
|
||||||
|
val remaining = ByteArray(buffer.remaining())
|
||||||
|
buffer.duplicate().get(remaining)
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
while (offset < sample.size) {
|
while (offset < remaining.size) {
|
||||||
val length = TrueHdMatPacker.accessUnitLength(sample, offset, sample.size)
|
val length = TrueHdMatPacker.accessUnitLength(remaining, offset, remaining.size)
|
||||||
if (length == 0) break
|
if (length == 0) {
|
||||||
val burst = packer.packAccessUnit(sample, offset, length)
|
// Not a unit boundary we recognise. Consuming the tail keeps the stream moving; trying to
|
||||||
|
// resynchronise mid-carrier would splice a frame.
|
||||||
|
buffer.position(buffer.limit())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val burst = packer.packAccessUnit(remaining, offset, length)
|
||||||
offset += length
|
offset += length
|
||||||
|
buffer.position(buffer.position() + length)
|
||||||
if (burst == null) continue
|
if (burst == null) continue
|
||||||
|
|
||||||
val burstTimeUs = carrierAnchorUs + burstsSinceAnchor * CARRIER_BURST_DURATION_US
|
val burstTimeUs = carrierAnchorUs + burstsSinceAnchor * CARRIER_BURST_DURATION_US
|
||||||
@@ -163,15 +201,19 @@ internal class TrueHdCarrierSink(
|
|||||||
if (!carrierSink.handleBuffer(burst, burstTimeUs, 1)) {
|
if (!carrierSink.handleBuffer(burst, burstTimeUs, 1)) {
|
||||||
pendingBurst = burst
|
pendingBurst = burst
|
||||||
pendingBurstTimeUs = burstTimeUs
|
pendingBurstTimeUs = burstTimeUs
|
||||||
break
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (packer.unsupportedRateFamily) {
|
|
||||||
log?.invoke("info", "audio", "TrueHD is 44.1kHz-family; the MAT carrier does not cover it, decoding instead")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sample is consumed either way: a stashed burst is placed on the next call.
|
if (packer.unsupportedRateFamily) {
|
||||||
buffer.position(buffer.limit())
|
// 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package com.edde746.plezy.exoplayer
|
||||||
|
|
||||||
|
import android.media.AudioDeviceInfo
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.AudioAttributes
|
||||||
|
import androidx.media3.common.AuxEffectInfo
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.Format
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.PlaybackParameters
|
||||||
|
import androidx.media3.common.util.Clock
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.analytics.PlayerId
|
||||||
|
import androidx.media3.exoplayer.audio.AudioSink
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routing and back-pressure contract for the TrueHD MAT carrier (#1804).
|
||||||
|
*
|
||||||
|
* The carrier is bit-exact, so the two failure modes that matter here are losing bytes and letting
|
||||||
|
* the wrong sink handle TrueHD.
|
||||||
|
*/
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
class TrueHdCarrierSinkTest {
|
||||||
|
|
||||||
|
private val accessUnits: ByteArray =
|
||||||
|
checkNotNull(javaClass.classLoader?.getResourceAsStream("truehd_access_units.bin"))
|
||||||
|
.use { it.readBytes() }
|
||||||
|
|
||||||
|
private val golden: ByteArray =
|
||||||
|
checkNotNull(javaClass.classLoader?.getResourceAsStream("truehd_iec61937_golden.bin"))
|
||||||
|
.use { it.readBytes() }
|
||||||
|
|
||||||
|
private fun trueHdFormat(sampleRate: Int = 48_000): Format = Format.Builder()
|
||||||
|
.setSampleMimeType(MimeTypes.AUDIO_TRUEHD)
|
||||||
|
.setChannelCount(6)
|
||||||
|
.setSampleRate(sampleRate)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun sink(
|
||||||
|
carrier: FakeSink = FakeSink(),
|
||||||
|
normal: FakeSink = FakeSink(),
|
||||||
|
routeAvailable: Boolean = true,
|
||||||
|
blocked: Boolean = false
|
||||||
|
) = TrueHdCarrierSink(normal, carrier, { routeAvailable }, { blocked })
|
||||||
|
|
||||||
|
// --- Selection ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TrueHD is the carrier or it is decoded. Falling through to the normal sink would hand media3
|
||||||
|
* its raw ENCODING_DOLBY_TRUEHD path, which is the configuration that wedges on these devices.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun trueHdWithoutACarrierRouteIsReportedUnsupportedSoItDecodes() {
|
||||||
|
val normal = FakeSink().apply { formatSupport = AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY }
|
||||||
|
val carrierSink = sink(normal = normal, routeAvailable = false)
|
||||||
|
|
||||||
|
assertFalse(carrierSink.supportsFormat(trueHdFormat()))
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_UNSUPPORTED, carrierSink.getFormatSupport(trueHdFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trueHdWithACarrierRouteIsSupportedDirectly() {
|
||||||
|
val carrierSink = sink()
|
||||||
|
assertTrue(carrierSink.supportsFormat(trueHdFormat()))
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY, carrierSink.getFormatSupport(trueHdFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downmix and normalization already force decoding; the carrier must not override that. */
|
||||||
|
@Test
|
||||||
|
fun blockedDirectOutputDeclinesTheCarrier() {
|
||||||
|
val carrierSink = sink(blocked = true)
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_UNSUPPORTED, carrierSink.getFormatSupport(trueHdFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 44.1kHz-family TrueHD rides a 176.4kHz carrier this path does not build. It has to be decided
|
||||||
|
* from the format: the packer only learns the rate from a major sync, long after selection, and
|
||||||
|
* selecting the carrier for it would produce silence.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun theFortyFourPointOneFamilyDeclinesTheCarrierFromTheFormatAlone() {
|
||||||
|
val carrierSink = sink()
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_UNSUPPORTED, carrierSink.getFormatSupport(trueHdFormat(sampleRate = 44_100)))
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_UNSUPPORTED, carrierSink.getFormatSupport(trueHdFormat(sampleRate = 176_400)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A bitstream cannot be resampled, so any speed other than 1.0x decodes. */
|
||||||
|
@Test
|
||||||
|
fun aSpeedChangeDeclinesTheCarrier() {
|
||||||
|
val carrierSink = sink()
|
||||||
|
carrierSink.setPlaybackParameters(PlaybackParameters(1.5f))
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_UNSUPPORTED, carrierSink.getFormatSupport(trueHdFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Everything that is not TrueHD keeps going to the existing processed sink. */
|
||||||
|
@Test
|
||||||
|
fun otherFormatsAreLeftToTheNormalSink() {
|
||||||
|
val normal = FakeSink().apply { formatSupport = AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY }
|
||||||
|
val carrierSink = sink(normal = normal)
|
||||||
|
val ac3 = Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AC3).setSampleRate(48_000).build()
|
||||||
|
|
||||||
|
assertEquals(AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY, carrierSink.getFormatSupport(ac3))
|
||||||
|
assertTrue(carrierSink.supportsFormat(ac3))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Back pressure ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regression this exists for: a delegate that refuses a burst part-way through a sample must
|
||||||
|
* not cost the access units behind it. media3 re-delivers the same buffer after a false return,
|
||||||
|
* so the carrier has to resume from where it stopped and come out byte-identical.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun aRefusedBurstMidSampleLosesNoAccessUnits() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier)
|
||||||
|
carrierSink.configure(trueHdFormat(), 0, null)
|
||||||
|
|
||||||
|
// Refuse every third burst once, so rejections land inside samples rather than between them.
|
||||||
|
carrier.refuseEveryNth = 3
|
||||||
|
|
||||||
|
val produced = feedWholeStream(carrierSink)
|
||||||
|
assertArrayEquals("carrier output must survive back pressure unchanged", golden, produced)
|
||||||
|
assertTrue("the fake must actually have exercised the refusal path", carrier.refusals > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** With no back pressure the same stream must produce exactly the same bytes. */
|
||||||
|
@Test
|
||||||
|
fun theCarrierOutputMatchesTheGoldenStream() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier)
|
||||||
|
carrierSink.configure(trueHdFormat(), 0, null)
|
||||||
|
|
||||||
|
assertArrayEquals(golden, feedWholeStream(carrierSink))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A refused burst must be re-offered before any further input is taken. */
|
||||||
|
@Test
|
||||||
|
fun aPendingBurstIsPlacedBeforeMoreInputIsConsumed() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier)
|
||||||
|
carrierSink.configure(trueHdFormat(), 0, null)
|
||||||
|
carrier.refuseNext = true
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.wrap(accessUnits)
|
||||||
|
// Drive until the first refusal is observed.
|
||||||
|
while (buffer.hasRemaining() && carrierSink.handleBuffer(buffer, 0L, 1)) Unit
|
||||||
|
|
||||||
|
assertTrue("expected a refusal to leave input unconsumed", buffer.hasRemaining())
|
||||||
|
assertTrue(carrierSink.hasPendingData())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Routing of controls ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun persistentControlsReachBothDelegates() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val normal = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier, normal = normal)
|
||||||
|
|
||||||
|
carrierSink.setVolume(0.5f)
|
||||||
|
carrierSink.setAudioSessionId(7)
|
||||||
|
|
||||||
|
assertEquals(0.5f, carrier.lastVolume, 0f)
|
||||||
|
assertEquals(0.5f, normal.lastVolume, 0f)
|
||||||
|
assertEquals(7, carrier.sessionId)
|
||||||
|
assertEquals(7, normal.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Silence skipping deletes "silent" bytes, which would break the carrier's frame cadence. */
|
||||||
|
@Test
|
||||||
|
fun silenceSkippingNeverReachesTheCarrier() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val normal = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier, normal = normal)
|
||||||
|
|
||||||
|
carrierSink.setSkipSilenceEnabled(true)
|
||||||
|
|
||||||
|
assertTrue(normal.skipSilence)
|
||||||
|
assertFalse("the carrier delegate must never skip silence", carrier.skipSilence)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theCarrierDelegateIsConfiguredAsAFixedRatePcmCarrier() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier)
|
||||||
|
|
||||||
|
carrierSink.configure(trueHdFormat(), 0, null)
|
||||||
|
|
||||||
|
val configured = checkNotNull(carrier.configuredFormat)
|
||||||
|
assertEquals(MimeTypes.AUDIO_RAW, configured.sampleMimeType)
|
||||||
|
assertEquals(C.ENCODING_PCM_16BIT, configured.pcmEncoding)
|
||||||
|
assertEquals(TrueHdMatPacker.CARRIER_SAMPLE_RATE, configured.sampleRate)
|
||||||
|
assertEquals(TrueHdMatPacker.CARRIER_CHANNEL_COUNT, configured.channelCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nonCarrierAudioIsConfiguredOnTheNormalSink() {
|
||||||
|
val carrier = FakeSink()
|
||||||
|
val normal = FakeSink()
|
||||||
|
val carrierSink = sink(carrier = carrier, normal = normal)
|
||||||
|
val ac3 = Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AC3).setSampleRate(48_000).build()
|
||||||
|
|
||||||
|
carrierSink.configure(ac3, 0, null)
|
||||||
|
|
||||||
|
assertSame(ac3, normal.configuredFormat)
|
||||||
|
assertEquals(null, carrier.configuredFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun feedWholeStream(carrierSink: TrueHdCarrierSink): ByteArray {
|
||||||
|
val buffer = ByteBuffer.wrap(accessUnits)
|
||||||
|
var guard = 0
|
||||||
|
while (buffer.hasRemaining() && guard++ < 10_000) {
|
||||||
|
carrierSink.handleBuffer(buffer, 0L, 1)
|
||||||
|
}
|
||||||
|
val carrier = carrierSinkDelegate(carrierSink)
|
||||||
|
return carrier.written.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun carrierSinkDelegate(sink: TrueHdCarrierSink): FakeSink {
|
||||||
|
val field = TrueHdCarrierSink::class.java.getDeclaredField("carrierSink")
|
||||||
|
field.isAccessible = true
|
||||||
|
return field.get(sink) as FakeSink
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal AudioSink that records what it was handed and can apply back pressure. */
|
||||||
|
private class FakeSink : AudioSink {
|
||||||
|
val written = ByteArrayOutputStream()
|
||||||
|
var configuredFormat: Format? = null
|
||||||
|
var formatSupport: Int = AudioSink.SINK_FORMAT_UNSUPPORTED
|
||||||
|
var lastVolume: Float = -1f
|
||||||
|
var sessionId: Int = -1
|
||||||
|
var skipSilence: Boolean = false
|
||||||
|
var refuseNext: Boolean = false
|
||||||
|
var refuseEveryNth: Int = 0
|
||||||
|
var refusals: Int = 0
|
||||||
|
private var offered = 0
|
||||||
|
|
||||||
|
override fun handleBuffer(buffer: ByteBuffer, presentationTimeUs: Long, encodedAccessUnitCount: Int): Boolean {
|
||||||
|
offered++
|
||||||
|
val refuse = refuseNext || (refuseEveryNth > 0 && offered % refuseEveryNth == 0)
|
||||||
|
if (refuse) {
|
||||||
|
refuseNext = false
|
||||||
|
refusals++
|
||||||
|
// Refuse once per burst: the retry must be accepted or the stream cannot progress.
|
||||||
|
offered++
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val copy = ByteArray(buffer.remaining())
|
||||||
|
buffer.duplicate().get(copy)
|
||||||
|
written.write(copy)
|
||||||
|
buffer.position(buffer.limit())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configure(inputFormat: Format, specifiedBufferSize: Int, outputChannels: IntArray?) {
|
||||||
|
configuredFormat = inputFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun supportsFormat(format: Format) = formatSupport == AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY
|
||||||
|
override fun getFormatSupport(format: Format) = formatSupport
|
||||||
|
override fun setVolume(volume: Float) { lastVolume = volume }
|
||||||
|
override fun setAudioSessionId(audioSessionId: Int) { sessionId = audioSessionId }
|
||||||
|
override fun setSkipSilenceEnabled(skipSilenceEnabled: Boolean) { skipSilence = skipSilenceEnabled }
|
||||||
|
override fun getSkipSilenceEnabled() = skipSilence
|
||||||
|
|
||||||
|
override fun setListener(listener: AudioSink.Listener) = Unit
|
||||||
|
override fun setPlayerId(playerId: PlayerId?) = Unit
|
||||||
|
override fun setClock(clock: Clock) = Unit
|
||||||
|
override fun getCurrentPositionUs(sourceEnded: Boolean) = 0L
|
||||||
|
override fun play() = Unit
|
||||||
|
override fun handleDiscontinuity() = Unit
|
||||||
|
override fun playToEndOfStream() = Unit
|
||||||
|
override fun isEnded() = false
|
||||||
|
override fun hasPendingData() = false
|
||||||
|
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) = Unit
|
||||||
|
override fun getPlaybackParameters(): PlaybackParameters = PlaybackParameters.DEFAULT
|
||||||
|
override fun setAudioAttributes(audioAttributes: AudioAttributes) = Unit
|
||||||
|
override fun getAudioAttributes(): AudioAttributes? = null
|
||||||
|
override fun setAuxEffectInfo(auxEffectInfo: AuxEffectInfo) = Unit
|
||||||
|
override fun setPreferredDevice(audioDeviceInfo: AudioDeviceInfo?) = Unit
|
||||||
|
override fun getAudioTrackBufferSizeUs() = 0L
|
||||||
|
override fun enableTunnelingV21() = Unit
|
||||||
|
override fun disableTunneling() = Unit
|
||||||
|
override fun pause() = Unit
|
||||||
|
override fun flush() = Unit
|
||||||
|
override fun reset() = Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user