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)
This commit is contained in:
christosts 2022-06-08 10:44:24 +00:00 committed by Marc Baechinger
parent f5dc99f596
commit 31c7ccbc49
3 changed files with 154 additions and 148 deletions

View File

@ -45,6 +45,10 @@
* Change the return type of `AudioAttributes.getAudioAttributesV21()` from * Change the return type of `AudioAttributes.getAudioAttributesV21()` from
`android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper `android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper
class, to prevent slow ART verification on API < 21. 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: * Ad playback / IMA:
* Decrease ad polling rate from every 100ms to every 200ms, to line up * Decrease ad polling rate from every 100ms to every 200ms, to line up
with Media Rating Council (MRC) recommendations. with Media Rating Council (MRC) recommendations.

View File

@ -15,22 +15,29 @@
*/ */
package androidx.media3.exoplayer.audio; package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.net.Uri; import android.net.Uri;
import android.provider.Settings.Global; import android.provider.Settings.Global;
import android.util.Pair;
import androidx.annotation.DoNotInline; import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.media3.common.C; 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.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.util.Arrays; import java.util.Arrays;
@ -54,18 +61,20 @@ public final class AudioCapabilities {
}, },
DEFAULT_MAX_CHANNEL_COUNT); DEFAULT_MAX_CHANNEL_COUNT);
/** Array of all surround sound encodings that a device may be capable of playing. */ /**
@SuppressWarnings("InlinedApi") * All surround sound encodings that a device may be capable of playing mapped to a maximum
private static final int[] ALL_SURROUND_ENCODINGS = * channel count.
new int[] { */
AudioFormat.ENCODING_AC3, private static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
AudioFormat.ENCODING_E_AC3, new ImmutableMap.Builder<Integer, Integer>()
AudioFormat.ENCODING_E_AC3_JOC, .put(C.ENCODING_AC3, 6)
AudioFormat.ENCODING_AC4, .put(C.ENCODING_AC4, 6)
AudioFormat.ENCODING_DOLBY_TRUEHD, .put(C.ENCODING_DTS, 6)
AudioFormat.ENCODING_DTS, .put(C.ENCODING_E_AC3_JOC, 6)
AudioFormat.ENCODING_DTS_HD, .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. */ /** Global settings key for devices that can specify external surround sound. */
private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled"; private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled";
@ -158,6 +167,62 @@ public final class AudioCapabilities {
return maxChannelCount; 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<Integer, Integer> 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 @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
@ -190,28 +255,93 @@ public final class AudioCapabilities {
&& ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER)); && ("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) @RequiresApi(29)
private static final class Api29 { 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 @DoNotInline
public static int[] getDirectPlaybackSupportedEncodings() { public static int[] getDirectPlaybackSupportedEncodings() {
ImmutableList.Builder<Integer> supportedEncodingsListBuilder = ImmutableList.builder(); ImmutableList.Builder<Integer> supportedEncodingsListBuilder = ImmutableList.builder();
for (int encoding : ALL_SURROUND_ENCODINGS) { for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) {
if (AudioTrack.isDirectPlaybackSupported( if (AudioTrack.isDirectPlaybackSupported(
new AudioFormat.Builder() new AudioFormat.Builder()
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.setEncoding(encoding) .setEncoding(encoding)
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ) .setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
.build(), .build(),
new android.media.AudioAttributes.Builder() DEFAULT_AUDIO_ATTRIBUTES)) {
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(0)
.build())) {
supportedEncodingsListBuilder.add(encoding); supportedEncodingsListBuilder.add(encoding);
} }
} }
supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT); supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT);
return Ints.toArray(supportedEncodingsListBuilder.build()); 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;
}
} }
} }

View File

@ -684,7 +684,7 @@ public final class DefaultAudioSink implements AudioSink {
if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) { if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY; return SINK_FORMAT_SUPPORTED_DIRECTLY;
} }
if (isPassthroughPlaybackSupported(format, audioCapabilities)) { if (audioCapabilities.isPassthroughPlaybackSupported(format)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY; return SINK_FORMAT_SUPPORTED_DIRECTLY;
} }
return SINK_FORMAT_UNSUPPORTED; return SINK_FORMAT_UNSUPPORTED;
@ -767,7 +767,7 @@ public final class DefaultAudioSink implements AudioSink {
outputMode = OUTPUT_MODE_PASSTHROUGH; outputMode = OUTPUT_MODE_PASSTHROUGH;
@Nullable @Nullable
Pair<Integer, Integer> encodingAndChannelConfig = Pair<Integer, Integer> encodingAndChannelConfig =
getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities); audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat);
if (encodingAndChannelConfig == null) { if (encodingAndChannelConfig == null) {
throw new ConfigurationException( throw new ConfigurationException(
"Unable to configure passthrough for: " + inputFormat, inputFormat); "Unable to configure passthrough for: " + inputFormat, inputFormat);
@ -1693,134 +1693,6 @@ public final class DefaultAudioSink implements AudioSink {
: writtenEncodedFrames; : 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<Integer, Integer> 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) { private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) {
if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) { if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
return false; return false;