fix(player): offer the TrueHD MAT carrier on API 29-32 routes
Carrier-or-decode gated the carrier on getDirectPlaybackSupport, which only exists on API 33, so every older route force-decoded TrueHD - including routes that bitstreamed it before the carrier existed. The #1863 Fire TV Stick 4K Max is Fire OS 8 (API 30): its HDMI route advertises raw TrueHD and IEC 61937 at 8 channels, 2.12.1 passed TrueHD through, and 2.13.0 hands the same stream to the FFmpeg decoder. The Shield is API 30 as well. API 29-32 now asks AudioTrack.isDirectPlaybackSupported about the exact 192kHz/7.1 IEC tuple before offering the carrier. It is coarser than the API 33 probe - it cannot tell bitstream from offload - but an IEC 61937 track is PCM-shaped by definition, so direct support means the route carries the frames. getMinBufferSize stays as the precondition on every tier, and a route that still lies fails AudioTrack initialisation, which the audio recovery path already answers by blocking direct output and force-decoding in place. Below API 29 nothing can vouch for the tuple, so the carrier is still not offered and TrueHD decodes as before. The tier decision is split from the platform probes so it is unit-testable; each probe is consulted only on the tiers where its API exists.
This commit is contained in:
+1
-1
@@ -203,7 +203,7 @@ class TrueHdSpeedTransitionTest {
|
||||
@Test
|
||||
fun aRateFamilyMismatchFallsBackToTheDecoderInsteadOfGoingSilent() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
if (!supportsTrueHdMatCarrier(context)) {
|
||||
if (!supportsTrueHdMatCarrier()) {
|
||||
Log.i(TAG, "==== MISMATCH SKIPPED: device has no carrier route ====")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.media.AudioTrack
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MimeTypes
|
||||
@@ -99,41 +100,88 @@ internal fun supportedMpvSpdifCodecs(context: Context): String {
|
||||
* layer about the encoding, which on the boxes measured for this issue answers "TrueHD is
|
||||
* offload-capable" and says nothing about whether a raw TrueHD track will ever drain.
|
||||
*
|
||||
* Both are consulted: `getMinBufferSize` proves a track can be built, and, where the API exists,
|
||||
* `getDirectPlaybackSupport` proves the route will actually bitstream it rather than silently
|
||||
* decode or wedge.
|
||||
* Both are consulted: `getMinBufferSize` proves a track can be built, and a direct-playback oracle
|
||||
* proves the route will actually bitstream it rather than silently decode or wedge. Sizing alone is
|
||||
* not sufficient — on a Shield it answers yes for this tuple and the AudioTrack then fails to
|
||||
* initialise.
|
||||
*
|
||||
* The oracle is tiered by what the platform offers:
|
||||
* - API 33+: `getDirectPlaybackSupport`, whose bitstream flag also rules out offload-only answers.
|
||||
* - API 29–32: `AudioTrack.isDirectPlaybackSupported` for the same tuple. Coarser — it cannot tell
|
||||
* bitstream from offload — but an IEC 61937 track is PCM-shaped by definition, so direct support
|
||||
* for it means the route carries the frames. Fire OS 8 (API 30) devices bitstream TrueHD this way
|
||||
* and lost passthrough entirely under an API 33 gate (#1863). A route that still lies here fails
|
||||
* AudioTrack initialisation, which the audio recovery path answers by force-decoding.
|
||||
* - Below API 29 there is no oracle at all, so the carrier is not offered and TrueHD decodes as
|
||||
* before.
|
||||
*/
|
||||
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 mask = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||
val sizedOk = try {
|
||||
AudioTrack.getMinBufferSize(rate, mask, AudioFormat.ENCODING_IEC61937) > 0
|
||||
} catch (error: Exception) {
|
||||
false
|
||||
internal fun supportsTrueHdMatCarrier(): Boolean = trueHdMatCarrierSupported(
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
canSizeCarrierBuffer = {
|
||||
try {
|
||||
AudioTrack.getMinBufferSize(
|
||||
TrueHdMatPacker.CARRIER_SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_OUT_7POINT1_SURROUND,
|
||||
AudioFormat.ENCODING_IEC61937
|
||||
) > 0
|
||||
} catch (error: Exception) {
|
||||
false
|
||||
}
|
||||
},
|
||||
// The SDK_INT guards repeat trueHdMatCarrierSupported's tiering only because lint's NewApi
|
||||
// check cannot see through the injected lambdas.
|
||||
bitstreamSupported = {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && iecCarrierBitstreamSupported()
|
||||
},
|
||||
directPlaybackSupported = {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && iecCarrierDirectPlaybackSupported()
|
||||
}
|
||||
if (!sizedOk) return false
|
||||
)
|
||||
|
||||
return try {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
.getPlatformAudioAttributes()
|
||||
val probe = AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_IEC61937)
|
||||
.setChannelMask(mask)
|
||||
.setSampleRate(rate)
|
||||
.build()
|
||||
val support = AudioManager.getDirectPlaybackSupport(probe, audioAttributes)
|
||||
(support and AudioManager.DIRECT_PLAYBACK_BITSTREAM_SUPPORTED) != 0
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "IEC 61937 carrier probe failed; not offering the TrueHD carrier", error)
|
||||
false
|
||||
}
|
||||
/**
|
||||
* [supportsTrueHdMatCarrier] with the platform probes injected. Probes are only consulted on the
|
||||
* API tiers where they exist: [bitstreamSupported] (`getDirectPlaybackSupport`) on 33+ and
|
||||
* [directPlaybackSupported] (`AudioTrack.isDirectPlaybackSupported`) on 29–32.
|
||||
*/
|
||||
internal fun trueHdMatCarrierSupported(
|
||||
sdkInt: Int,
|
||||
canSizeCarrierBuffer: () -> Boolean,
|
||||
bitstreamSupported: () -> Boolean,
|
||||
directPlaybackSupported: () -> Boolean
|
||||
): Boolean = when {
|
||||
sdkInt < Build.VERSION_CODES.Q -> false
|
||||
!canSizeCarrierBuffer() -> false
|
||||
sdkInt >= Build.VERSION_CODES.TIRAMISU -> bitstreamSupported()
|
||||
else -> directPlaybackSupported()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun iecCarrierBitstreamSupported(): Boolean = try {
|
||||
val support = AudioManager.getDirectPlaybackSupport(iecCarrierProbeFormat(), movieAudioAttributes())
|
||||
(support and AudioManager.DIRECT_PLAYBACK_BITSTREAM_SUPPORTED) != 0
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "IEC 61937 carrier probe failed; not offering the TrueHD carrier", error)
|
||||
false
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@Suppress("DEPRECATION") // Deprecated in favour of the API 33 probe the tier above uses.
|
||||
private fun iecCarrierDirectPlaybackSupported(): Boolean = try {
|
||||
AudioTrack.isDirectPlaybackSupported(iecCarrierProbeFormat(), movieAudioAttributes())
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "IEC 61937 carrier probe failed; not offering the TrueHD carrier", error)
|
||||
false
|
||||
}
|
||||
|
||||
/** The exact tuple the carrier's `AudioTrack` is built with; see [PlezyRenderersFactory]. */
|
||||
private fun iecCarrierProbeFormat(): AudioFormat = AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_IEC61937)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_7POINT1_SURROUND)
|
||||
.setSampleRate(TrueHdMatPacker.CARRIER_SAMPLE_RATE)
|
||||
.build()
|
||||
|
||||
private fun movieAudioAttributes(): android.media.AudioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
.getPlatformAudioAttributes()
|
||||
|
||||
@@ -208,7 +208,7 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
return TrueHdCarrierSink(
|
||||
defaultSink = processedSink,
|
||||
carrierSink = buildCarrierSink(context, bufferSizeProvider),
|
||||
carrierRouteAvailable = { supportsTrueHdMatCarrier(context) },
|
||||
carrierRouteAvailable = { supportsTrueHdMatCarrier() },
|
||||
directOutputBlocked = { format -> shouldBlockDirectAudioOutput?.invoke(format) == true },
|
||||
log = audioDiagnosticsLogger
|
||||
).also { trueHdCarrierSink = it }
|
||||
|
||||
@@ -82,4 +82,70 @@ class AudioOutputPolicyTest {
|
||||
fun spdifListIsEmptyForPcmOnlyRoutes() {
|
||||
assertEquals("", mpvSpdifCodecs { false })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun carrierIsNeverOfferedBelowApi29() {
|
||||
// No direct-playback oracle exists there, and getMinBufferSize alone is known to lie
|
||||
// (a Shield sizes the tuple, then the AudioTrack fails to initialise).
|
||||
assertFalse(
|
||||
trueHdMatCarrierSupported(
|
||||
sdkInt = 28,
|
||||
canSizeCarrierBuffer = { true },
|
||||
bitstreamSupported = { true },
|
||||
directPlaybackSupported = { true }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun carrierRequiresASizableBufferOnEveryTier() {
|
||||
for (sdkInt in intArrayOf(29, 30, 32, 33, 34)) {
|
||||
assertFalse(
|
||||
"api $sdkInt",
|
||||
trueHdMatCarrierSupported(
|
||||
sdkInt = sdkInt,
|
||||
canSizeCarrierBuffer = { false },
|
||||
bitstreamSupported = { true },
|
||||
directPlaybackSupported = { true }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun carrierOnApi29To32FollowsTheDirectPlaybackProbe() {
|
||||
// Fire OS 8 (API 30) bitstreams TrueHD over this route; an API 33 gate force-decoded it (#1863).
|
||||
for (supported in booleanArrayOf(true, false)) {
|
||||
for (sdkInt in intArrayOf(29, 30, 32)) {
|
||||
assertEquals(
|
||||
"api $sdkInt supported=$supported",
|
||||
supported,
|
||||
trueHdMatCarrierSupported(
|
||||
sdkInt = sdkInt,
|
||||
canSizeCarrierBuffer = { true },
|
||||
bitstreamSupported = { throw AssertionError("getDirectPlaybackSupport does not exist below API 33") },
|
||||
directPlaybackSupported = { supported }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun carrierOnApi33UsesTheBitstreamProbe() {
|
||||
// getDirectPlaybackSupport distinguishes bitstream from offload-only; the coarser API 29
|
||||
// probe must not shadow it where the platform can answer precisely.
|
||||
for (supported in booleanArrayOf(true, false)) {
|
||||
assertEquals(
|
||||
"supported=$supported",
|
||||
supported,
|
||||
trueHdMatCarrierSupported(
|
||||
sdkInt = 33,
|
||||
canSizeCarrierBuffer = { true },
|
||||
bitstreamSupported = { supported },
|
||||
directPlaybackSupported = { throw AssertionError("API 29 probe must not be consulted on API 33+") }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user