Improve AudioCapabilities with AudioManager API in Android 13
PiperOrigin-RevId: 604700601
This commit is contained in:
parent
20053dcdc6
commit
ccd603acb0
@ -122,6 +122,7 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
@ -505,7 +506,7 @@ public final class Util {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
|
* Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
|
||||||
* both may be null.
|
* both may be {@code null}.
|
||||||
*
|
*
|
||||||
* @param o1 The first object.
|
* @param o1 The first object.
|
||||||
* @param o2 The second object.
|
* @param o2 The second object.
|
||||||
@ -516,6 +517,64 @@ public final class Util {
|
|||||||
return o1 == null ? o2 == null : o1.equals(o2);
|
return o1 == null ? o2 == null : o1.equals(o2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests two {@link SparseArray} instances for content equality, handling the case where one or
|
||||||
|
* both may be {@code null}.
|
||||||
|
*
|
||||||
|
* @see SparseArray#contentEquals(SparseArray)
|
||||||
|
* @param sparseArray1 The first {@link SparseArray} instance.
|
||||||
|
* @param sparseArray2 The second {@link SparseArray} instance.
|
||||||
|
* @return True if the two {@link SparseArray} instances are equal in contents.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public static <T> boolean contentEquals(
|
||||||
|
@Nullable SparseArray<T> sparseArray1, @Nullable SparseArray<T> sparseArray2) {
|
||||||
|
if (sparseArray1 == null) {
|
||||||
|
return sparseArray2 == null;
|
||||||
|
} else if (sparseArray2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Util.SDK_INT >= 31) {
|
||||||
|
return sparseArray1.contentEquals(sparseArray2);
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = sparseArray1.size();
|
||||||
|
if (size != sparseArray2.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
int key = sparseArray1.keyAt(index);
|
||||||
|
if (!Objects.equals(sparseArray1.valueAt(index), sparseArray2.get(key))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a hash code value for the contents of this {@link SparseArray}, combining the {@link
|
||||||
|
* Objects#hashCode(Object)} result of all its keys and values.
|
||||||
|
*
|
||||||
|
* @see SparseArray#contentHashCode()
|
||||||
|
* @param sparseArray The {@link SparseArray} instance.
|
||||||
|
* @return The hash code.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public static <T> int contentHashCode(SparseArray<T> sparseArray) {
|
||||||
|
if (Util.SDK_INT >= 31) {
|
||||||
|
return sparseArray.contentHashCode();
|
||||||
|
}
|
||||||
|
int hash = 17;
|
||||||
|
for (int index = 0; index < sparseArray.size(); index++) {
|
||||||
|
hash = 31 * hash + sparseArray.keyAt(index);
|
||||||
|
hash = 31 * hash + Objects.hashCode(sparseArray.valueAt(index));
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests whether an {@code items} array contains an object equal to {@code item}, according to
|
* Tests whether an {@code items} array contains an object equal to {@code item}, according to
|
||||||
* {@link Object#equals(Object)}.
|
* {@link Object#equals(Object)}.
|
||||||
|
@ -17,6 +17,8 @@ package androidx.media3.common.util;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Util.binarySearchCeil;
|
import static androidx.media3.common.util.Util.binarySearchCeil;
|
||||||
import static androidx.media3.common.util.Util.binarySearchFloor;
|
import static androidx.media3.common.util.Util.binarySearchFloor;
|
||||||
|
import static androidx.media3.common.util.Util.contentEquals;
|
||||||
|
import static androidx.media3.common.util.Util.contentHashCode;
|
||||||
import static androidx.media3.common.util.Util.escapeFileName;
|
import static androidx.media3.common.util.Util.escapeFileName;
|
||||||
import static androidx.media3.common.util.Util.getCodecsOfType;
|
import static androidx.media3.common.util.Util.getCodecsOfType;
|
||||||
import static androidx.media3.common.util.Util.getStringForTime;
|
import static androidx.media3.common.util.Util.getStringForTime;
|
||||||
@ -42,6 +44,7 @@ import android.net.Uri;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.util.SparseLongArray;
|
import android.util.SparseLongArray;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
@ -1523,6 +1526,77 @@ public class UtilTest {
|
|||||||
assertThat(roleFlags).containsExactly("describes-music", "easy-read");
|
assertThat(roleFlags).containsExactly("describes-music", "easy-read");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contentEquals_twoNullSparseArrays_returnsTrue() {
|
||||||
|
assertThat(contentEquals(null, null)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contentEquals_oneNullSparseArrayAndOneNonNullSparseArray_returnsFalse() {
|
||||||
|
SparseArray<Integer> sparseArray = new SparseArray<>();
|
||||||
|
sparseArray.put(1, 2);
|
||||||
|
|
||||||
|
assertThat(contentEquals(sparseArray, null)).isFalse();
|
||||||
|
assertThat(contentEquals(null, sparseArray)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||||
|
public void contentEquals_sparseArraysWithEqualContent_returnsTrue() {
|
||||||
|
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||||
|
sparseArray1.put(1, 2);
|
||||||
|
sparseArray1.put(3, 4);
|
||||||
|
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||||
|
sparseArray2.put(3, 4);
|
||||||
|
sparseArray2.put(1, 2);
|
||||||
|
|
||||||
|
assertThat(contentEquals(sparseArray1, sparseArray2)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||||
|
public void contentEquals_sparseArraysWithDifferentContents_returnsFalse() {
|
||||||
|
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||||
|
sparseArray1.put(1, 2);
|
||||||
|
sparseArray1.put(3, 4);
|
||||||
|
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||||
|
sparseArray2.put(3, 4);
|
||||||
|
SparseArray<Integer> sparseArray3 = new SparseArray<>();
|
||||||
|
sparseArray3.put(1, 3);
|
||||||
|
sparseArray3.put(3, 4);
|
||||||
|
|
||||||
|
assertThat(contentEquals(sparseArray1, sparseArray2)).isFalse();
|
||||||
|
assertThat(contentEquals(sparseArray1, sparseArray3)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||||
|
public void contentHashCode_sparseArraysWithEqualContent_returnsEqualContentHashCode() {
|
||||||
|
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||||
|
sparseArray1.put(1, 2);
|
||||||
|
sparseArray1.put(3, 4);
|
||||||
|
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||||
|
sparseArray2.put(3, 4);
|
||||||
|
sparseArray2.put(1, 2);
|
||||||
|
|
||||||
|
assertThat(contentHashCode(sparseArray1)).isEqualTo(contentHashCode(sparseArray2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||||
|
public void contentHashCode_sparseArraysWithDifferentContent_returnsDifferentContentHashCode() {
|
||||||
|
// In theory this is not guaranteed though, adding this test to ensure a sensible
|
||||||
|
// contentHashCode implementation.
|
||||||
|
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||||
|
sparseArray1.put(1, 2);
|
||||||
|
sparseArray1.put(3, 4);
|
||||||
|
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||||
|
sparseArray2.put(3, 2);
|
||||||
|
sparseArray2.put(1, 4);
|
||||||
|
|
||||||
|
assertThat(contentHashCode(sparseArray1)).isNotEqualTo(contentHashCode(sparseArray2));
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
|
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
|
||||||
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
||||||
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.audio;
|
package androidx.media3.exoplayer.audio;
|
||||||
|
|
||||||
|
import static android.media.AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -28,6 +30,7 @@ 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 android.util.Pair;
|
||||||
|
import android.util.SparseArray;
|
||||||
import androidx.annotation.DoNotInline;
|
import androidx.annotation.DoNotInline;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
@ -42,8 +45,11 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import java.util.Arrays;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/** Represents the set of audio formats that a device is capable of playing. */
|
/** Represents the set of audio formats that a device is capable of playing. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@ -51,12 +57,12 @@ public final class AudioCapabilities {
|
|||||||
|
|
||||||
// TODO(internal b/283945513): Have separate default max channel counts in `AudioCapabilities`
|
// TODO(internal b/283945513): Have separate default max channel counts in `AudioCapabilities`
|
||||||
// for PCM and compressed audio.
|
// for PCM and compressed audio.
|
||||||
private static final int DEFAULT_MAX_CHANNEL_COUNT = 10;
|
@VisibleForTesting /* package */ static final int DEFAULT_MAX_CHANNEL_COUNT = 10;
|
||||||
@VisibleForTesting /* package */ static final int DEFAULT_SAMPLE_RATE_HZ = 48_000;
|
@VisibleForTesting /* package */ static final int DEFAULT_SAMPLE_RATE_HZ = 48_000;
|
||||||
|
|
||||||
/** The minimum audio capabilities supported by all devices. */
|
/** The minimum audio capabilities supported by all devices. */
|
||||||
public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
|
public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
|
||||||
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, DEFAULT_MAX_CHANNEL_COUNT);
|
new AudioCapabilities(ImmutableList.of(AudioProfile.DEFAULT_AUDIO_PROFILE));
|
||||||
|
|
||||||
/** Encodings supported when the device specifies external surround sound. */
|
/** Encodings supported when the device specifies external surround sound. */
|
||||||
@SuppressLint("InlinedApi") // Compile-time access to integer constants defined in API 21.
|
@SuppressLint("InlinedApi") // Compile-time access to integer constants defined in API 21.
|
||||||
@ -68,7 +74,8 @@ public final class AudioCapabilities {
|
|||||||
* All surround sound encodings that a device may be capable of playing mapped to a maximum
|
* All surround sound encodings that a device may be capable of playing mapped to a maximum
|
||||||
* channel count.
|
* channel count.
|
||||||
*/
|
*/
|
||||||
private static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
|
@VisibleForTesting /* package */
|
||||||
|
static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
|
||||||
new ImmutableMap.Builder<Integer, Integer>()
|
new ImmutableMap.Builder<Integer, Integer>()
|
||||||
.put(C.ENCODING_AC3, 6)
|
.put(C.ENCODING_AC3, 6)
|
||||||
.put(C.ENCODING_AC4, 6)
|
.put(C.ENCODING_AC4, 6)
|
||||||
@ -134,6 +141,14 @@ public final class AudioCapabilities {
|
|||||||
: Util.SDK_INT >= 33
|
: Util.SDK_INT >= 33
|
||||||
? Api33.getDefaultRoutedDeviceForAttributes(audioManager, audioAttributes)
|
? Api33.getDefaultRoutedDeviceForAttributes(audioManager, audioAttributes)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (Util.SDK_INT >= 33 && (Util.isTv(context) || Util.isAutomotive(context))) {
|
||||||
|
// TV or automotive devices generally shouldn't support audio offload for surround encodings,
|
||||||
|
// so the encodings we get from AudioManager.getDirectProfilesForAttributes should include
|
||||||
|
// the PCM encodings and surround encodings for passthrough mode.
|
||||||
|
return Api33.getCapabilitiesInternalForDirectPlayback(audioManager, audioAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
// If a connection to Bluetooth device is detected, we only return the minimum capabilities that
|
// If a connection to Bluetooth device is detected, we only return the minimum capabilities that
|
||||||
// is supported by all the devices.
|
// is supported by all the devices.
|
||||||
if (Util.SDK_INT >= 23 && Api23.isBluetoothConnected(audioManager, currentDevice)) {
|
if (Util.SDK_INT >= 23 && Api23.isBluetoothConnected(audioManager, currentDevice)) {
|
||||||
@ -153,7 +168,7 @@ public final class AudioCapabilities {
|
|||||||
if (Util.SDK_INT >= 29 && (Util.isTv(context) || Util.isAutomotive(context))) {
|
if (Util.SDK_INT >= 29 && (Util.isTv(context) || Util.isAutomotive(context))) {
|
||||||
supportedEncodings.addAll(Api29.getDirectPlaybackSupportedEncodings(audioAttributes));
|
supportedEncodings.addAll(Api29.getDirectPlaybackSupportedEncodings(audioAttributes));
|
||||||
return new AudioCapabilities(
|
return new AudioCapabilities(
|
||||||
Ints.toArray(supportedEncodings.build()), DEFAULT_MAX_CHANNEL_COUNT);
|
getAudioProfiles(Ints.toArray(supportedEncodings.build()), DEFAULT_MAX_CHANNEL_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent != null && intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 1) {
|
if (intent != null && intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 1) {
|
||||||
@ -162,13 +177,17 @@ public final class AudioCapabilities {
|
|||||||
supportedEncodings.addAll(Ints.asList(encodingsFromExtra));
|
supportedEncodings.addAll(Ints.asList(encodingsFromExtra));
|
||||||
}
|
}
|
||||||
return new AudioCapabilities(
|
return new AudioCapabilities(
|
||||||
Ints.toArray(supportedEncodings.build()),
|
getAudioProfiles(
|
||||||
intent.getIntExtra(
|
Ints.toArray(supportedEncodings.build()),
|
||||||
AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));
|
intent.getIntExtra(
|
||||||
|
AudioManager.EXTRA_MAX_CHANNEL_COUNT,
|
||||||
|
/* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AudioCapabilities(
|
return new AudioCapabilities(
|
||||||
Ints.toArray(supportedEncodings.build()), /* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT);
|
getAudioProfiles(
|
||||||
|
Ints.toArray(supportedEncodings.build()),
|
||||||
|
/* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,28 +201,26 @@ public final class AudioCapabilities {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final int[] supportedEncodings;
|
private final SparseArray<AudioProfile> encodingToAudioProfile;
|
||||||
private final int maxChannelCount;
|
private final int maxChannelCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
|
* @deprecated Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo)} instead.
|
||||||
* count.
|
|
||||||
*
|
|
||||||
* <p>Applications should generally call {@link #getCapabilities(Context, AudioAttributes,
|
|
||||||
* AudioDeviceInfo)} to obtain an instance based on the capabilities advertised by the platform,
|
|
||||||
* rather than calling this constructor.
|
|
||||||
*
|
|
||||||
* @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
|
|
||||||
* {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are
|
|
||||||
* supported.
|
|
||||||
* @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {
|
public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {
|
||||||
if (supportedEncodings != null) {
|
this(getAudioProfiles(supportedEncodings, maxChannelCount));
|
||||||
this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
|
}
|
||||||
Arrays.sort(this.supportedEncodings);
|
|
||||||
} else {
|
private AudioCapabilities(List<AudioProfile> audioProfiles) {
|
||||||
this.supportedEncodings = new int[0];
|
encodingToAudioProfile = new SparseArray<>();
|
||||||
|
for (int i = 0; i < audioProfiles.size(); i++) {
|
||||||
|
AudioProfile audioProfile = audioProfiles.get(i);
|
||||||
|
encodingToAudioProfile.put(audioProfile.encoding, audioProfile);
|
||||||
|
}
|
||||||
|
int maxChannelCount = 0;
|
||||||
|
for (int i = 0; i < encodingToAudioProfile.size(); i++) {
|
||||||
|
maxChannelCount = max(maxChannelCount, encodingToAudioProfile.valueAt(i).maxChannelCount);
|
||||||
}
|
}
|
||||||
this.maxChannelCount = maxChannelCount;
|
this.maxChannelCount = maxChannelCount;
|
||||||
}
|
}
|
||||||
@ -215,7 +232,7 @@ public final class AudioCapabilities {
|
|||||||
* @return Whether this device supports playback the specified audio {@code encoding}.
|
* @return Whether this device supports playback the specified audio {@code encoding}.
|
||||||
*/
|
*/
|
||||||
public boolean supportsEncoding(@C.Encoding int encoding) {
|
public boolean supportsEncoding(@C.Encoding int encoding) {
|
||||||
return Arrays.binarySearch(supportedEncodings, encoding) >= 0;
|
return Util.contains(encodingToAudioProfile, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the maximum number of channels the device can play at the same time. */
|
/** Returns the maximum number of channels the device can play at the same time. */
|
||||||
@ -279,6 +296,8 @@ public final class AudioCapabilities {
|
|||||||
if (!supportsEncoding(encoding)) {
|
if (!supportsEncoding(encoding)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AudioProfile audioProfile = checkNotNull(encodingToAudioProfile.get(encoding));
|
||||||
int channelCount;
|
int channelCount;
|
||||||
if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) {
|
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
|
// In HLS chunkless preparation, the format channel count and sample rate may be unset. See
|
||||||
@ -287,16 +306,16 @@ public final class AudioCapabilities {
|
|||||||
int sampleRate =
|
int sampleRate =
|
||||||
format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ;
|
format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ;
|
||||||
channelCount =
|
channelCount =
|
||||||
getMaxSupportedChannelCountForPassthrough(encoding, sampleRate, audioAttributes);
|
audioProfile.getMaxSupportedChannelCountForPassthrough(sampleRate, audioAttributes);
|
||||||
} else {
|
} else {
|
||||||
channelCount = format.channelCount;
|
channelCount = format.channelCount;
|
||||||
// Some DTS:X TVs reports ACTION_HDMI_AUDIO_PLUG.EXTRA_MAX_CHANNEL_COUNT as 8
|
if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X) && Util.SDK_INT < 33) {
|
||||||
// instead of 10. See https://github.com/androidx/media/issues/396
|
// Some DTS:X TVs reports ACTION_HDMI_AUDIO_PLUG.EXTRA_MAX_CHANNEL_COUNT as 8
|
||||||
if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X)) {
|
// instead of 10. See https://github.com/androidx/media/issues/396
|
||||||
if (channelCount > 10) {
|
if (channelCount > 10) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (channelCount > maxChannelCount) {
|
} else if (!audioProfile.supportsChannelCount(channelCount)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,21 +335,21 @@ public final class AudioCapabilities {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
AudioCapabilities audioCapabilities = (AudioCapabilities) other;
|
AudioCapabilities audioCapabilities = (AudioCapabilities) other;
|
||||||
return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)
|
return Util.contentEquals(encodingToAudioProfile, audioCapabilities.encodingToAudioProfile)
|
||||||
&& maxChannelCount == audioCapabilities.maxChannelCount;
|
&& maxChannelCount == audioCapabilities.maxChannelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);
|
return maxChannelCount + 31 * Util.contentHashCode(encodingToAudioProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "AudioCapabilities[maxChannelCount="
|
return "AudioCapabilities[maxChannelCount="
|
||||||
+ maxChannelCount
|
+ maxChannelCount
|
||||||
+ ", supportedEncodings="
|
+ ", audioProfiles="
|
||||||
+ Arrays.toString(supportedEncodings)
|
+ encodingToAudioProfile
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,21 +358,6 @@ 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, AudioAttributes audioAttributes) {
|
|
||||||
// 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, audioAttributes);
|
|
||||||
}
|
|
||||||
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getChannelConfigForPassthrough(int channelCount) {
|
private static int getChannelConfigForPassthrough(int channelCount) {
|
||||||
if (Util.SDK_INT <= 28) {
|
if (Util.SDK_INT <= 28) {
|
||||||
// In passthrough mode the channel count used to configure the audio track doesn't affect how
|
// In passthrough mode the channel count used to configure the audio track doesn't affect how
|
||||||
@ -376,6 +380,152 @@ public final class AudioCapabilities {
|
|||||||
return Util.getAudioTrackChannelConfig(channelCount);
|
return Util.getAudioTrackChannelConfig(channelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suppression needed for IntDef casting.
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
@RequiresApi(33)
|
||||||
|
private static ImmutableList<AudioProfile> getAudioProfiles(
|
||||||
|
List<android.media.AudioProfile> audioProfiles) {
|
||||||
|
Map<Integer, Set<Integer>> formatToChannelMasks = new HashMap<>();
|
||||||
|
// Enforce the support of stereo 16bit-PCM.
|
||||||
|
formatToChannelMasks.put(C.ENCODING_PCM_16BIT, new HashSet<>(Ints.asList(CHANNEL_OUT_STEREO)));
|
||||||
|
for (int i = 0; i < audioProfiles.size(); i++) {
|
||||||
|
android.media.AudioProfile audioProfile = audioProfiles.get(i);
|
||||||
|
if ((audioProfile.getEncapsulationType()
|
||||||
|
== android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937)) {
|
||||||
|
// Skip the IEC61937 encapsulation because we don't support it yet.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int encoding = audioProfile.getFormat();
|
||||||
|
if (!Util.isEncodingLinearPcm(encoding)
|
||||||
|
&& !ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (formatToChannelMasks.containsKey(encoding)) {
|
||||||
|
checkNotNull(formatToChannelMasks.get(encoding))
|
||||||
|
.addAll(Ints.asList(audioProfile.getChannelMasks()));
|
||||||
|
} else {
|
||||||
|
formatToChannelMasks.put(
|
||||||
|
encoding, new HashSet<>(Ints.asList(audioProfile.getChannelMasks())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableList.Builder<AudioProfile> localAudioProfiles = ImmutableList.builder();
|
||||||
|
for (Map.Entry<Integer, Set<Integer>> formatAndChannelMasks : formatToChannelMasks.entrySet()) {
|
||||||
|
localAudioProfiles.add(
|
||||||
|
new AudioProfile(formatAndChannelMasks.getKey(), formatAndChannelMasks.getValue()));
|
||||||
|
}
|
||||||
|
return localAudioProfiles.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<AudioProfile> getAudioProfiles(
|
||||||
|
@Nullable int[] supportedEncodings, int maxChannelCount) {
|
||||||
|
ImmutableList.Builder<AudioProfile> audioProfiles = ImmutableList.builder();
|
||||||
|
if (supportedEncodings == null) {
|
||||||
|
supportedEncodings = new int[0];
|
||||||
|
}
|
||||||
|
for (int i = 0; i < supportedEncodings.length; i++) {
|
||||||
|
int encoding = supportedEncodings[i];
|
||||||
|
audioProfiles.add(new AudioProfile(encoding, maxChannelCount));
|
||||||
|
}
|
||||||
|
return audioProfiles.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AudioProfile {
|
||||||
|
|
||||||
|
public static final AudioProfile DEFAULT_AUDIO_PROFILE =
|
||||||
|
(Util.SDK_INT >= 33)
|
||||||
|
? new AudioProfile(
|
||||||
|
C.ENCODING_PCM_16BIT,
|
||||||
|
getAllChannelMasksForMaxChannelCount(DEFAULT_MAX_CHANNEL_COUNT))
|
||||||
|
: new AudioProfile(C.ENCODING_PCM_16BIT, DEFAULT_MAX_CHANNEL_COUNT);
|
||||||
|
|
||||||
|
public final @C.Encoding int encoding;
|
||||||
|
public final int maxChannelCount;
|
||||||
|
@Nullable private final ImmutableSet<Integer> channelMasks;
|
||||||
|
|
||||||
|
@RequiresApi(33)
|
||||||
|
public AudioProfile(@C.Encoding int encoding, Set<Integer> channelMasks) {
|
||||||
|
this.encoding = encoding;
|
||||||
|
this.channelMasks = ImmutableSet.copyOf(channelMasks);
|
||||||
|
int maxChannelCount = 0;
|
||||||
|
for (int channelMask : this.channelMasks) {
|
||||||
|
maxChannelCount = max(maxChannelCount, Integer.bitCount(channelMask));
|
||||||
|
}
|
||||||
|
this.maxChannelCount = maxChannelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioProfile(@C.Encoding int encoding, int maxChannelCount) {
|
||||||
|
this.encoding = encoding;
|
||||||
|
this.maxChannelCount = maxChannelCount;
|
||||||
|
this.channelMasks = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsChannelCount(int channelCount) {
|
||||||
|
if (channelMasks == null) {
|
||||||
|
return channelCount <= maxChannelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int channelMask = Util.getAudioTrackChannelConfig(channelCount);
|
||||||
|
if (channelMask == AudioFormat.CHANNEL_INVALID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return channelMasks.contains(channelMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSupportedChannelCountForPassthrough(
|
||||||
|
int sampleRate, AudioAttributes audioAttributes) {
|
||||||
|
if (channelMasks != null) {
|
||||||
|
// We built the AudioProfile on API 33.
|
||||||
|
return maxChannelCount;
|
||||||
|
} else if (Util.SDK_INT >= 29) {
|
||||||
|
return Api29.getMaxSupportedChannelCountForPassthrough(
|
||||||
|
encoding, sampleRate, audioAttributes);
|
||||||
|
}
|
||||||
|
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableSet<Integer> getAllChannelMasksForMaxChannelCount(int maxChannelCount) {
|
||||||
|
ImmutableSet.Builder<Integer> allChannelMasks = new ImmutableSet.Builder<>();
|
||||||
|
for (int i = 1; i <= maxChannelCount; i++) {
|
||||||
|
allChannelMasks.add(Util.getAudioTrackChannelConfig(i));
|
||||||
|
}
|
||||||
|
return allChannelMasks.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object other) {
|
||||||
|
if (this == other) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof AudioProfile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AudioProfile audioProfile = (AudioProfile) other;
|
||||||
|
return encoding == audioProfile.encoding
|
||||||
|
&& maxChannelCount == audioProfile.maxChannelCount
|
||||||
|
&& Util.areEqual(channelMasks, audioProfile.channelMasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = encoding;
|
||||||
|
result = 31 * result + maxChannelCount;
|
||||||
|
result = 31 * result + (channelMasks == null ? 0 : channelMasks.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AudioProfile[format="
|
||||||
|
+ encoding
|
||||||
|
+ ", maxChannelCount="
|
||||||
|
+ maxChannelCount
|
||||||
|
+ ", channelMasks="
|
||||||
|
+ channelMasks
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
private static final class Api23 {
|
private static final class Api23 {
|
||||||
private Api23() {}
|
private Api23() {}
|
||||||
@ -438,7 +588,7 @@ public final class AudioCapabilities {
|
|||||||
}
|
}
|
||||||
if (AudioTrack.isDirectPlaybackSupported(
|
if (AudioTrack.isDirectPlaybackSupported(
|
||||||
new AudioFormat.Builder()
|
new AudioFormat.Builder()
|
||||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
|
.setChannelMask(CHANNEL_OUT_STEREO)
|
||||||
.setEncoding(encoding)
|
.setEncoding(encoding)
|
||||||
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
|
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
|
||||||
.build(),
|
.build(),
|
||||||
@ -482,6 +632,17 @@ public final class AudioCapabilities {
|
|||||||
@RequiresApi(33)
|
@RequiresApi(33)
|
||||||
private static final class Api33 {
|
private static final class Api33 {
|
||||||
|
|
||||||
|
private Api33() {}
|
||||||
|
|
||||||
|
@DoNotInline
|
||||||
|
public static AudioCapabilities getCapabilitiesInternalForDirectPlayback(
|
||||||
|
AudioManager audioManager, AudioAttributes audioAttributes) {
|
||||||
|
List<android.media.AudioProfile> directAudioProfiles =
|
||||||
|
audioManager.getDirectProfilesForAttributes(
|
||||||
|
audioAttributes.getAudioAttributesV21().audioAttributes);
|
||||||
|
return new AudioCapabilities(getAudioProfiles(directAudioProfiles));
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@DoNotInline
|
@DoNotInline
|
||||||
public static AudioDeviceInfoApi23 getDefaultRoutedDeviceForAttributes(
|
public static AudioDeviceInfoApi23 getDefaultRoutedDeviceForAttributes(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user