Skip to content

Commit

Permalink
Audio passthrough: handle unset audio format channel count
Browse files Browse the repository at this point in the history
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
  • Loading branch information
christosts authored and marcbaechinger committed Jun 9, 2022
1 parent cdb8038 commit 8697338
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 148 deletions.
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Expand Up @@ -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.
Expand Down
Expand Up @@ -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;

Expand All @@ -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<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
new ImmutableMap.Builder<Integer, Integer>()
.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";
Expand Down Expand Up @@ -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<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
public boolean equals(@Nullable Object other) {
if (this == other) {
Expand Down Expand Up @@ -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<Integer> 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;
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -767,7 +767,7 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int
outputMode = OUTPUT_MODE_PASSTHROUGH;
@Nullable
Pair<Integer, Integer> encodingAndChannelConfig =
getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities);
audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat);
if (encodingAndChannelConfig == null) {
throw new ConfigurationException(
"Unable to configure passthrough for: " + inputFormat, inputFormat);
Expand Down Expand Up @@ -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<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) {
if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
return false;
Expand Down

0 comments on commit 8697338

Please sign in to comment.