mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
f5dc99f596
commit
31c7ccbc49
@ -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.
|
||||
|
@ -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<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";
|
||||
@ -158,6 +167,62 @@ public final class AudioCapabilities {
|
||||
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) {
|
||||
@ -190,28 +255,93 @@ public final class AudioCapabilities {
|
||||
&& ("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -684,7 +684,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
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 final class DefaultAudioSink implements AudioSink {
|
||||
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);
|
||||
@ -1693,134 +1693,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
: 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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user