From 9f28e7d81bd9cda5c463a9999b208192d4babdbe Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Tue, 6 Feb 2024 10:55:58 -0800 Subject: [PATCH] Improve AudioCapabilities with AudioManager API in Android 13 PiperOrigin-RevId: 604700601 (cherry picked from commit ccd603acb067c560840f7aa1499556733e204c39) --- .../androidx/media3/common/util/Util.java | 61 +++- .../androidx/media3/common/util/UtilTest.java | 74 +++++ .../exoplayer/audio/AudioCapabilities.java | 265 ++++++++++++++---- 3 files changed, 347 insertions(+), 53 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 3dbce4668a..e79c4392a1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -122,6 +122,7 @@ import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.TimeZone; import java.util.UUID; 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 - * both may be null. + * both may be {@code null}. * * @param o1 The first object. * @param o2 The second object. @@ -516,6 +517,64 @@ public final class Util { 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 boolean contentEquals( + @Nullable SparseArray sparseArray1, @Nullable SparseArray 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 int contentHashCode(SparseArray 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 * {@link Object#equals(Object)}. diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 965c681ca3..9d57fd1f88 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -17,6 +17,8 @@ package androidx.media3.common.util; import static androidx.media3.common.util.Util.binarySearchCeil; 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.getCodecsOfType; import static androidx.media3.common.util.Util.getStringForTime; @@ -42,6 +44,7 @@ import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.util.SparseArray; import android.util.SparseLongArray; import androidx.media3.common.C; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -1523,6 +1526,77 @@ public class UtilTest { 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 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 sparseArray1 = new SparseArray<>(); + sparseArray1.put(1, 2); + sparseArray1.put(3, 4); + SparseArray 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 sparseArray1 = new SparseArray<>(); + sparseArray1.put(1, 2); + sparseArray1.put(3, 4); + SparseArray sparseArray2 = new SparseArray<>(); + sparseArray2.put(3, 4); + SparseArray 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 sparseArray1 = new SparseArray<>(); + sparseArray1.put(1, 2); + sparseArray1.put(3, 4); + SparseArray 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 sparseArray1 = new SparseArray<>(); + sparseArray1.put(1, 2); + sparseArray1.put(3, 4); + SparseArray 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) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); 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 3ca80ae7d8..e9fba20498 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,7 +15,9 @@ */ package androidx.media3.exoplayer.audio; +import static android.media.AudioFormat.CHANNEL_OUT_STEREO; import static androidx.media3.common.util.Assertions.checkNotNull; +import static java.lang.Math.max; import android.annotation.SuppressLint; import android.content.Context; @@ -28,6 +30,7 @@ import android.media.AudioTrack; import android.net.Uri; import android.provider.Settings.Global; import android.util.Pair; +import android.util.SparseArray; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; 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.ImmutableSet; 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.Map; +import java.util.Set; /** Represents the set of audio formats that a device is capable of playing. */ @UnstableApi @@ -51,12 +57,12 @@ public final class AudioCapabilities { // TODO(internal b/283945513): Have separate default max channel counts in `AudioCapabilities` // 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; /** The minimum audio capabilities supported by all devices. */ 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. */ @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 * channel count. */ - private static final ImmutableMap ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS = + @VisibleForTesting /* package */ + static final ImmutableMap ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS = new ImmutableMap.Builder() .put(C.ENCODING_AC3, 6) .put(C.ENCODING_AC4, 6) @@ -134,6 +141,14 @@ public final class AudioCapabilities { : Util.SDK_INT >= 33 ? Api33.getDefaultRoutedDeviceForAttributes(audioManager, audioAttributes) : 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 // is supported by all the devices. 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))) { supportedEncodings.addAll(Api29.getDirectPlaybackSupportedEncodings(audioAttributes)); 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) { @@ -162,13 +177,17 @@ public final class AudioCapabilities { supportedEncodings.addAll(Ints.asList(encodingsFromExtra)); } return new AudioCapabilities( - Ints.toArray(supportedEncodings.build()), - intent.getIntExtra( - AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT)); + getAudioProfiles( + Ints.toArray(supportedEncodings.build()), + intent.getIntExtra( + AudioManager.EXTRA_MAX_CHANNEL_COUNT, + /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT))); } 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; } - private final int[] supportedEncodings; + private final SparseArray encodingToAudioProfile; private final int maxChannelCount; /** - * Constructs new audio capabilities based on a set of supported encodings and a maximum channel - * count. - * - *

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 Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo)} instead. */ + @Deprecated public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) { - if (supportedEncodings != null) { - this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length); - Arrays.sort(this.supportedEncodings); - } else { - this.supportedEncodings = new int[0]; + this(getAudioProfiles(supportedEncodings, maxChannelCount)); + } + + private AudioCapabilities(List audioProfiles) { + 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; } @@ -215,7 +232,7 @@ public final class AudioCapabilities { * @return Whether this device supports playback the specified audio {@code 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. */ @@ -279,6 +296,8 @@ public final class AudioCapabilities { if (!supportsEncoding(encoding)) { return null; } + + AudioProfile audioProfile = checkNotNull(encodingToAudioProfile.get(encoding)); 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 @@ -287,16 +306,16 @@ public final class AudioCapabilities { int sampleRate = format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ; channelCount = - getMaxSupportedChannelCountForPassthrough(encoding, sampleRate, audioAttributes); + audioProfile.getMaxSupportedChannelCountForPassthrough(sampleRate, audioAttributes); } else { channelCount = format.channelCount; - // Some DTS:X TVs reports ACTION_HDMI_AUDIO_PLUG.EXTRA_MAX_CHANNEL_COUNT as 8 - // instead of 10. See https://github.com/androidx/media/issues/396 - if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X)) { + if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X) && Util.SDK_INT < 33) { + // Some DTS:X TVs reports ACTION_HDMI_AUDIO_PLUG.EXTRA_MAX_CHANNEL_COUNT as 8 + // instead of 10. See https://github.com/androidx/media/issues/396 if (channelCount > 10) { return null; } - } else if (channelCount > maxChannelCount) { + } else if (!audioProfile.supportsChannelCount(channelCount)) { return null; } } @@ -316,21 +335,21 @@ public final class AudioCapabilities { return false; } AudioCapabilities audioCapabilities = (AudioCapabilities) other; - return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings) + return Util.contentEquals(encodingToAudioProfile, audioCapabilities.encodingToAudioProfile) && maxChannelCount == audioCapabilities.maxChannelCount; } @Override public int hashCode() { - return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings); + return maxChannelCount + 31 * Util.contentHashCode(encodingToAudioProfile); } @Override public String toString() { return "AudioCapabilities[maxChannelCount=" + maxChannelCount - + ", supportedEncodings=" - + Arrays.toString(supportedEncodings) + + ", audioProfiles=" + + encodingToAudioProfile + "]"; } @@ -339,21 +358,6 @@ 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, 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) { if (Util.SDK_INT <= 28) { // 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); } + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @RequiresApi(33) + private static ImmutableList getAudioProfiles( + List audioProfiles) { + Map> 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 localAudioProfiles = ImmutableList.builder(); + for (Map.Entry> formatAndChannelMasks : formatToChannelMasks.entrySet()) { + localAudioProfiles.add( + new AudioProfile(formatAndChannelMasks.getKey(), formatAndChannelMasks.getValue())); + } + return localAudioProfiles.build(); + } + + private static ImmutableList getAudioProfiles( + @Nullable int[] supportedEncodings, int maxChannelCount) { + ImmutableList.Builder 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 channelMasks; + + @RequiresApi(33) + public AudioProfile(@C.Encoding int encoding, Set 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 getAllChannelMasksForMaxChannelCount(int maxChannelCount) { + ImmutableSet.Builder 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) private static final class Api23 { private Api23() {} @@ -438,7 +588,7 @@ public final class AudioCapabilities { } if (AudioTrack.isDirectPlaybackSupported( new AudioFormat.Builder() - .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) + .setChannelMask(CHANNEL_OUT_STEREO) .setEncoding(encoding) .setSampleRate(DEFAULT_SAMPLE_RATE_HZ) .build(), @@ -482,6 +632,17 @@ public final class AudioCapabilities { @RequiresApi(33) private static final class Api33 { + private Api33() {} + + @DoNotInline + public static AudioCapabilities getCapabilitiesInternalForDirectPlayback( + AudioManager audioManager, AudioAttributes audioAttributes) { + List directAudioProfiles = + audioManager.getDirectProfilesForAttributes( + audioAttributes.getAudioAttributesV21().audioAttributes); + return new AudioCapabilities(getAudioProfiles(directAudioProfiles)); + } + @Nullable @DoNotInline public static AudioDeviceInfoApi23 getDefaultRoutedDeviceForAttributes(