From 31c7ccbc49634b10fce64e3bb837f06795467d60 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 8 Jun 2022 10:44:24 +0000 Subject: [PATCH] Audio passthrough: handle unset audio format channel count With HLS chunkless preparation, audio formats may have no value for channel count. In this case, the DefaultAudioSink will either query the platform for a supported channel count (API 29+) or assume a max channel count based on the encoding spec in order to decide whether the audio format can be played with audio passthrough. Issue: google/ExoPlayer#10204 #minor-release PiperOrigin-RevId: 453644548 (cherry picked from commit 86973382335156abaa76770c6897d28460fdde36) --- RELEASENOTES.md | 4 + .../exoplayer/audio/AudioCapabilities.java | 166 ++++++++++++++++-- .../exoplayer/audio/DefaultAudioSink.java | 132 +------------- 3 files changed, 154 insertions(+), 148 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 290a9d4bca..933615fcf0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,10 @@ * Change the return type of `AudioAttributes.getAudioAttributesV21()` from `android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper class, to prevent slow ART verification on API < 21. + * Query the platform (API 29+) or assume the audio encoding channel count + for audio passthrough when the format audio channel count is unset, + which occurs with HLS chunkless preparation + ([10204](https://github.com/google/ExoPlayer/issues/10204)). * Ad playback / IMA: * Decrease ad polling rate from every 100ms to every 200ms, to line up with Media Rating Council (MRC) recommendations. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java index b6d4f02aa0..9888db45a6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java @@ -15,22 +15,29 @@ */ package androidx.media3.exoplayer.audio; +import static androidx.media3.common.util.Assertions.checkNotNull; + import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.net.Uri; import android.provider.Settings.Global; +import android.util.Pair; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; import java.util.Arrays; @@ -54,18 +61,20 @@ public final class AudioCapabilities { }, DEFAULT_MAX_CHANNEL_COUNT); - /** Array of all surround sound encodings that a device may be capable of playing. */ - @SuppressWarnings("InlinedApi") - private static final int[] ALL_SURROUND_ENCODINGS = - new int[] { - AudioFormat.ENCODING_AC3, - AudioFormat.ENCODING_E_AC3, - AudioFormat.ENCODING_E_AC3_JOC, - AudioFormat.ENCODING_AC4, - AudioFormat.ENCODING_DOLBY_TRUEHD, - AudioFormat.ENCODING_DTS, - AudioFormat.ENCODING_DTS_HD, - }; + /** + * All surround sound encodings that a device may be capable of playing mapped to a maximum + * channel count. + */ + private static final ImmutableMap ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS = + new ImmutableMap.Builder() + .put(C.ENCODING_AC3, 6) + .put(C.ENCODING_AC4, 6) + .put(C.ENCODING_DTS, 6) + .put(C.ENCODING_E_AC3_JOC, 6) + .put(C.ENCODING_E_AC3, 8) + .put(C.ENCODING_DTS_HD, 8) + .put(C.ENCODING_DOLBY_TRUEHD, 8) + .buildOrThrow(); /** Global settings key for devices that can specify external surround sound. */ private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled"; @@ -158,6 +167,62 @@ public int getMaxChannelCount() { return maxChannelCount; } + /** Returns whether the device can do passthrough playback for {@code format}. */ + public boolean isPassthroughPlaybackSupported(Format format) { + return getEncodingAndChannelConfigForPassthrough(format) != null; + } + + /** + * Returns the encoding and channel config to use when configuring an {@link AudioTrack} in + * passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the + * format is unsupported. + * + * @param format The {@link Format}. + * @return The encoding and channel config to use, or {@code null} if passthrough of the format is + * unsupported. + */ + @Nullable + public Pair getEncodingAndChannelConfigForPassthrough(Format format) { + @C.Encoding + int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); + // Check that this is an encoding known to work for passthrough. This avoids trying to use + // passthrough with an encoding where the device/app reports it's capable but it is untested or + // known to be broken (for example AAC-LC). + if (!ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) { + return null; + } + + if (encoding == C.ENCODING_E_AC3_JOC && !supportsEncoding(C.ENCODING_E_AC3_JOC)) { + // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). + encoding = C.ENCODING_E_AC3; + } else if (encoding == C.ENCODING_DTS_HD && !supportsEncoding(C.ENCODING_DTS_HD)) { + // DTS receivers support DTS-HD streams (but decode only the core layer). + encoding = C.ENCODING_DTS; + } + if (!supportsEncoding(encoding)) { + return null; + } + int channelCount; + if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) { + // In HLS chunkless preparation, the format channel count and sample rate may be unset. See + // https://github.com/google/ExoPlayer/issues/10204 and b/222127949 for more details. + // For E-AC3 JOC, the format is object based so the format channel count is arbitrary. + int sampleRate = + format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ; + channelCount = getMaxSupportedChannelCountForPassthrough(encoding, sampleRate); + } else { + channelCount = format.channelCount; + if (channelCount > maxChannelCount) { + return null; + } + } + int channelConfig = getChannelConfigForPassthrough(channelCount); + if (channelConfig == AudioFormat.CHANNEL_INVALID) { + return null; + } + return Pair.create(encoding, channelConfig); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { @@ -190,28 +255,93 @@ private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() { && ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER)); } + /** + * Returns the maximum number of channels supported for passthrough playback of audio in the given + * encoding, or {@code 0} if the format is unsupported. + */ + private static int getMaxSupportedChannelCountForPassthrough( + @C.Encoding int encoding, int sampleRate) { + // From API 29 we can get the channel count from the platform, but before then there is no way + // to query the platform so we assume the channel count matches the maximum channel count per + // audio encoding spec. + if (Util.SDK_INT >= 29) { + return Api29.getMaxSupportedChannelCountForPassthrough(encoding, sampleRate); + } + return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0)); + } + + private static int getChannelConfigForPassthrough(int channelCount) { + if (Util.SDK_INT <= 28) { + // In passthrough mode the channel count used to configure the audio track doesn't affect how + // the stream is handled, except that some devices do overly-strict channel configuration + // checks. Therefore we override the channel count so that a known-working channel + // configuration is chosen in all cases. See [Internal: b/29116190]. + if (channelCount == 7) { + channelCount = 8; + } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { + channelCount = 6; + } + } + + // Workaround for Nexus Player not reporting support for mono passthrough. See + // [Internal: b/34268671]. + if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) { + channelCount = 2; + } + + return Util.getAudioTrackChannelConfig(channelCount); + } + @RequiresApi(29) private static final class Api29 { + private static final AudioAttributes DEFAULT_AUDIO_ATTRIBUTES = + new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(0) + .build(); + + private Api29() {} + @DoNotInline public static int[] getDirectPlaybackSupportedEncodings() { ImmutableList.Builder supportedEncodingsListBuilder = ImmutableList.builder(); - for (int encoding : ALL_SURROUND_ENCODINGS) { + for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) { if (AudioTrack.isDirectPlaybackSupported( new AudioFormat.Builder() .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setEncoding(encoding) .setSampleRate(DEFAULT_SAMPLE_RATE_HZ) .build(), - new android.media.AudioAttributes.Builder() - .setUsage(android.media.AudioAttributes.USAGE_MEDIA) - .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(0) - .build())) { + DEFAULT_AUDIO_ATTRIBUTES)) { supportedEncodingsListBuilder.add(encoding); } } supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT); return Ints.toArray(supportedEncodingsListBuilder.build()); } + + /** + * Returns the maximum number of channels supported for passthrough playback of audio in the + * given format, or {@code 0} if the format is unsupported. + */ + @DoNotInline + public static int getMaxSupportedChannelCountForPassthrough( + @C.Encoding int encoding, int sampleRate) { + // TODO(internal b/234351617): Query supported channel masks directly once it's supported, + // see also b/25994457. + for (int channelCount = DEFAULT_MAX_CHANNEL_COUNT; channelCount > 0; channelCount--) { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setEncoding(encoding) + .setSampleRate(sampleRate) + .setChannelMask(Util.getAudioTrackChannelConfig(channelCount)) + .build(); + if (AudioTrack.isDirectPlaybackSupported(audioFormat, DEFAULT_AUDIO_ATTRIBUTES)) { + return channelCount; + } + } + return 0; + } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 13c80a1228..c1a34adb68 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -684,7 +684,7 @@ public boolean supportsFormat(Format format) { if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } - if (isPassthroughPlaybackSupported(format, audioCapabilities)) { + if (audioCapabilities.isPassthroughPlaybackSupported(format)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } return SINK_FORMAT_UNSUPPORTED; @@ -767,7 +767,7 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int outputMode = OUTPUT_MODE_PASSTHROUGH; @Nullable Pair encodingAndChannelConfig = - getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities); + audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat); if (encodingAndChannelConfig == null) { throw new ConfigurationException( "Unable to configure passthrough for: " + inputFormat, inputFormat); @@ -1693,134 +1693,6 @@ private long getWrittenFrames() { : writtenEncodedFrames; } - private static boolean isPassthroughPlaybackSupported( - Format format, AudioCapabilities audioCapabilities) { - return getEncodingAndChannelConfigForPassthrough(format, audioCapabilities) != null; - } - - /** - * Returns the encoding and channel config to use when configuring an {@link AudioTrack} in - * passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the - * format is unsupported. - * - * @param format The {@link Format}. - * @param audioCapabilities The device audio capabilities. - * @return The encoding and channel config to use, or {@code null} if passthrough of the format is - * unsupported. - */ - @Nullable - private static Pair getEncodingAndChannelConfigForPassthrough( - Format format, AudioCapabilities audioCapabilities) { - @C.Encoding - int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); - // Check for encodings that are known to work for passthrough with the implementation in this - // class. This avoids trying to use passthrough with an encoding where the device/app reports - // it's capable but it is untested or known to be broken (for example AAC-LC). - boolean supportedEncoding = - encoding == C.ENCODING_AC3 - || encoding == C.ENCODING_E_AC3 - || encoding == C.ENCODING_E_AC3_JOC - || encoding == C.ENCODING_AC4 - || encoding == C.ENCODING_DTS - || encoding == C.ENCODING_DTS_HD - || encoding == C.ENCODING_DOLBY_TRUEHD; - if (!supportedEncoding) { - return null; - } - if (encoding == C.ENCODING_E_AC3_JOC - && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { - // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). - encoding = C.ENCODING_E_AC3; - } else if (encoding == C.ENCODING_DTS_HD - && !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) { - // DTS receivers support DTS-HD streams (but decode only the core layer). - encoding = C.ENCODING_DTS; - } - if (!audioCapabilities.supportsEncoding(encoding)) { - return null; - } - - int channelCount; - if (encoding == C.ENCODING_E_AC3_JOC) { - // E-AC3 JOC is object based so the format channel count is arbitrary. From API 29 we can get - // the channel count for this encoding, but before then there is no way to query it so we - // assume 6 channel audio is supported. - if (Util.SDK_INT >= 29) { - // Default to 48 kHz if the format doesn't have a sample rate (for example, for chunkless - // HLS preparation). See [Internal: b/222127949]. - int sampleRate = format.sampleRate != Format.NO_VALUE ? format.sampleRate : 48000; - channelCount = - getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, sampleRate); - if (channelCount == 0) { - Log.w(TAG, "E-AC3 JOC encoding supported but no channel count supported"); - return null; - } - } else { - channelCount = 6; - } - } else { - channelCount = format.channelCount; - if (channelCount > audioCapabilities.getMaxChannelCount()) { - return null; - } - } - int channelConfig = getChannelConfigForPassthrough(channelCount); - if (channelConfig == AudioFormat.CHANNEL_INVALID) { - return null; - } - - return Pair.create(encoding, channelConfig); - } - - /** - * Returns the maximum number of channels supported for passthrough playback of audio in the given - * format, or 0 if the format is unsupported. - */ - @RequiresApi(29) - private static int getMaxSupportedChannelCountForPassthroughV29( - @C.Encoding int encoding, int sampleRate) { - android.media.AudioAttributes audioAttributes = - new android.media.AudioAttributes.Builder() - .setUsage(android.media.AudioAttributes.USAGE_MEDIA) - .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) - .build(); - // TODO(internal b/25994457): Query supported channel masks directly once it's supported. - for (int channelCount = 8; channelCount > 0; channelCount--) { - AudioFormat audioFormat = - new AudioFormat.Builder() - .setEncoding(encoding) - .setSampleRate(sampleRate) - .setChannelMask(Util.getAudioTrackChannelConfig(channelCount)) - .build(); - if (AudioTrack.isDirectPlaybackSupported(audioFormat, audioAttributes)) { - return channelCount; - } - } - return 0; - } - - private static int getChannelConfigForPassthrough(int channelCount) { - if (Util.SDK_INT <= 28) { - // In passthrough mode the channel count used to configure the audio track doesn't affect how - // the stream is handled, except that some devices do overly-strict channel configuration - // checks. Therefore we override the channel count so that a known-working channel - // configuration is chosen in all cases. See [Internal: b/29116190]. - if (channelCount == 7) { - channelCount = 8; - } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { - channelCount = 6; - } - } - - // Workaround for Nexus Player not reporting support for mono passthrough. See - // [Internal: b/34268671]. - if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) { - channelCount = 2; - } - - return Util.getAudioTrackChannelConfig(channelCount); - } - private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) { if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) { return false;