Use routed device in AudioCapabilities

From API 23, we may have a preferred device that is most likely used
as the output device.
From API 24, the AudioTrack tells us the actual routed device that is
used for output and we can listen to changes happening mid-playback.
From API 33, we can directly query the default device that will
be used for audio output for the current attributes.

If the routed device is known by any of the methods above, we can add
more targeted checks in methods like isBluetoothConnected to avoid
iterating over all devices that are not relevant.

The knowledge about the routed device will also be useful to check
advanced output capabilities in the future (e.g. for lossless
playback)

PiperOrigin-RevId: 600384923
This commit is contained in:
tonihei 2024-01-22 01:38:10 -08:00 committed by Copybara-Service
parent cb0f5a7fff
commit b1c954fa84
4 changed files with 224 additions and 39 deletions

View File

@ -43,6 +43,7 @@ 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.List;
/** Represents the set of audio formats that a device is capable of playing. */
@UnstableApi
@ -83,11 +84,11 @@ public final class AudioCapabilities {
private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled";
/**
* @deprecated Use {@link #getCapabilities(Context, AudioAttributes)} instead.
* @deprecated Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo)} instead.
*/
@Deprecated
public static AudioCapabilities getCapabilities(Context context) {
return getCapabilities(context, AudioAttributes.DEFAULT);
return getCapabilities(context, AudioAttributes.DEFAULT, /* routedDevice= */ null);
}
/**
@ -95,28 +96,52 @@ public final class AudioCapabilities {
*
* @param context A context for obtaining the current audio capabilities.
* @param audioAttributes The {@link AudioAttributes} to obtain capabilities for.
* @param routedDevice The {@link AudioDeviceInfo} audio will be routed to if known, or null to
* assume the default route.
* @return The current audio capabilities for the device.
*/
public static AudioCapabilities getCapabilities(
Context context, AudioAttributes audioAttributes, @Nullable AudioDeviceInfo routedDevice) {
@Nullable
AudioDeviceInfoApi23 routedDeviceApi23 =
Util.SDK_INT >= 23 && routedDevice != null ? new AudioDeviceInfoApi23(routedDevice) : null;
return getCapabilitiesInternal(context, audioAttributes, routedDeviceApi23);
}
@SuppressWarnings("InlinedApi")
@SuppressLint("UnprotectedReceiver") // ACTION_HDMI_AUDIO_PLUG is protected since API 16
public static AudioCapabilities getCapabilities(
Context context, AudioAttributes audioAttributes) {
/* package */ static AudioCapabilities getCapabilitiesInternal(
Context context,
AudioAttributes audioAttributes,
@Nullable AudioDeviceInfoApi23 routedDevice) {
Intent intent =
context.registerReceiver(
/* receiver= */ null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
return getCapabilities(context, intent, audioAttributes);
return getCapabilitiesInternal(context, intent, audioAttributes, routedDevice);
}
@SuppressLint("InlinedApi")
/* package */ static AudioCapabilities getCapabilities(
Context context, @Nullable Intent intent, AudioAttributes audioAttributes) {
/* package */ static AudioCapabilities getCapabilitiesInternal(
Context context,
@Nullable Intent intent,
AudioAttributes audioAttributes,
@Nullable AudioDeviceInfoApi23 routedDevice) {
AudioManager audioManager =
(AudioManager) checkNotNull(context.getSystemService(Context.AUDIO_SERVICE));
AudioDeviceInfoApi23 currentDevice =
routedDevice != null
? routedDevice
: Util.SDK_INT >= 33
? Api33.getDefaultRoutedDeviceForAttributes(audioManager, audioAttributes)
: null;
// 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(context)) {
if (Util.SDK_INT >= 23 && Api23.isBluetoothConnected(audioManager, currentDevice)) {
return DEFAULT_AUDIO_CAPABILITIES;
}
ImmutableSet.Builder<Integer> supportedEncodings = new ImmutableSet.Builder<>();
supportedEncodings.add(C.ENCODING_PCM_16BIT);
if (deviceMaySetExternalSurroundSoundGlobalSetting()
&& Global.getInt(context.getContentResolver(), EXTERNAL_SURROUND_SOUND_KEY, 0) == 1) {
supportedEncodings.addAll(EXTERNAL_SURROUND_SOUND_ENCODINGS);
@ -142,12 +167,8 @@ public final class AudioCapabilities {
AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));
}
ImmutableSet<Integer> supportedEncodingsSet = supportedEncodings.build();
if (!supportedEncodingsSet.isEmpty()) {
return new AudioCapabilities(
Ints.toArray(supportedEncodingsSet), /* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT);
}
return DEFAULT_AUDIO_CAPABILITIES;
return new AudioCapabilities(
Ints.toArray(supportedEncodings.build()), /* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT);
}
/**
@ -168,9 +189,9 @@ public final class AudioCapabilities {
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
* count.
*
* <p>Applications should generally call {@link #getCapabilities(Context, AudioAttributes)} to
* obtain an instance based on the capabilities advertised by the platform, rather than calling
* this constructor.
* <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
@ -360,10 +381,13 @@ public final class AudioCapabilities {
private Api23() {}
@DoNotInline
public static boolean isBluetoothConnected(Context context) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
public static boolean isBluetoothConnected(
AudioManager audioManager, @Nullable AudioDeviceInfoApi23 currentDevice) {
// Check the current device if known or all devices otherwise.
AudioDeviceInfo[] audioDeviceInfos =
checkNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
currentDevice == null
? checkNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS)
: new AudioDeviceInfo[] {currentDevice.audioDeviceInfo};
ImmutableSet<Integer> allBluetoothDeviceTypesSet = getAllBluetoothDeviceTypes();
for (AudioDeviceInfo audioDeviceInfo : audioDeviceInfos) {
if (allBluetoothDeviceTypesSet.contains(audioDeviceInfo.getType())) {
@ -454,4 +478,25 @@ public final class AudioCapabilities {
return 0;
}
}
@RequiresApi(33)
private static final class Api33 {
@Nullable
@DoNotInline
public static AudioDeviceInfoApi23 getDefaultRoutedDeviceForAttributes(
AudioManager audioManager, AudioAttributes audioAttributes) {
List<AudioDeviceInfo> audioDevices =
checkNotNull(audioManager)
.getAudioDevicesForAttributes(
audioAttributes.getAudioAttributesV21().audioAttributes);
if (audioDevices.isEmpty()) {
// Can't find current device.
return null;
}
// List only has more than one element if output devices are duplicated, so we assume the
// first device in the list has all the information we need.
return new AudioDeviceInfoApi23(audioDevices.get(0));
}
}
}

View File

@ -61,20 +61,48 @@ public final class AudioCapabilitiesReceiver {
@Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver;
@Nullable private AudioCapabilities audioCapabilities;
@Nullable private AudioDeviceInfoApi23 routedDevice;
private AudioAttributes audioAttributes;
private boolean registered;
/**
* @deprecated Use {@link #AudioCapabilitiesReceiver(Context, Listener, AudioAttributes,
* AudioDeviceInfo)} instead.
*/
@Deprecated
public AudioCapabilitiesReceiver(Context context, Listener listener) {
this(context, listener, AudioAttributes.DEFAULT, /* routedDevice= */ (AudioDeviceInfo) null);
}
/**
* @param context A context for registering the receiver.
* @param listener The listener to notify when audio capabilities change.
* @param audioAttributes The {@link AudioAttributes}.
* @param routedDevice The {@link AudioDeviceInfo} audio will be routed to if known, or null to
* assume the default route.
*/
public AudioCapabilitiesReceiver(
Context context, Listener listener, AudioAttributes audioAttributes) {
Context context,
Listener listener,
AudioAttributes audioAttributes,
@Nullable AudioDeviceInfo routedDevice) {
this(
context,
listener,
audioAttributes,
Util.SDK_INT >= 23 && routedDevice != null ? new AudioDeviceInfoApi23(routedDevice) : null);
}
/* package */ AudioCapabilitiesReceiver(
Context context,
Listener listener,
AudioAttributes audioAttributes,
@Nullable AudioDeviceInfoApi23 routedDevice) {
context = context.getApplicationContext();
this.context = context;
this.listener = checkNotNull(listener);
this.audioAttributes = audioAttributes;
this.routedDevice = routedDevice;
handler = Util.createHandlerForCurrentOrMainLooper();
audioDeviceCallback = Util.SDK_INT >= 23 ? new AudioDeviceCallbackV23() : null;
hdmiAudioPlugBroadcastReceiver =
@ -94,7 +122,25 @@ public final class AudioCapabilitiesReceiver {
*/
public void setAudioAttributes(AudioAttributes audioAttributes) {
this.audioAttributes = audioAttributes;
onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, audioAttributes));
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice));
}
/**
* Updates the {@link AudioDeviceInfo} audio will be routed to.
*
* @param routedDevice The {@link AudioDeviceInfo} audio will be routed to if known, or null to
* assume the default route.
*/
@RequiresApi(23)
public void setRoutedDevice(@Nullable AudioDeviceInfo routedDevice) {
if (Util.areEqual(
routedDevice, this.routedDevice == null ? null : this.routedDevice.audioDeviceInfo)) {
return;
}
this.routedDevice = routedDevice != null ? new AudioDeviceInfoApi23(routedDevice) : null;
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, this.routedDevice));
}
/**
@ -126,7 +172,9 @@ public final class AudioCapabilitiesReceiver {
/* broadcastPermission= */ null,
handler);
}
audioCapabilities = AudioCapabilities.getCapabilities(context, stickyIntent, audioAttributes);
audioCapabilities =
AudioCapabilities.getCapabilitiesInternal(
context, stickyIntent, audioAttributes, routedDevice);
return audioCapabilities;
}
@ -163,7 +211,9 @@ public final class AudioCapabilitiesReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!isInitialStickyBroadcast()) {
onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, intent, audioAttributes));
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(
context, intent, audioAttributes, routedDevice));
}
}
}
@ -190,7 +240,8 @@ public final class AudioCapabilitiesReceiver {
@Override
public void onChange(boolean selfChange) {
onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, audioAttributes));
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice));
}
}
@ -198,12 +249,17 @@ public final class AudioCapabilitiesReceiver {
private final class AudioDeviceCallbackV23 extends AudioDeviceCallback {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, audioAttributes));
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, audioAttributes));
if (Util.contains(removedDevices, routedDevice)) {
routedDevice = null;
}
onNewAudioCapabilities(
AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice));
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
import android.media.AudioDeviceInfo;
import androidx.annotation.RequiresApi;
/** Wrapper class for the platform {@link AudioDeviceInfo}. */
@RequiresApi(23)
/* package */ final class AudioDeviceInfoApi23 {
/** The platform {@link AudioDeviceInfo}. */
public final AudioDeviceInfo audioDeviceInfo;
/**
* Creates the audio device info wrapper.
*
* @param audioDeviceInfo The platform {@link AudioDeviceInfo}.
*/
public AudioDeviceInfoApi23(AudioDeviceInfo audioDeviceInfo) {
this.audioDeviceInfo = audioDeviceInfo;
}
}

View File

@ -28,6 +28,8 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioRouting;
import android.media.AudioRouting.OnRoutingChangedListener;
import android.media.AudioTrack;
import android.media.PlaybackParams;
import android.media.metrics.LogSessionId;
@ -513,6 +515,7 @@ public final class DefaultAudioSink implements AudioSink {
@Nullable private AudioTrack audioTrack;
private AudioCapabilities audioCapabilities;
private @MonotonicNonNull AudioCapabilitiesReceiver audioCapabilitiesReceiver;
@Nullable private OnRoutingChangedListenerApi24 onRoutingChangedListener;
private AudioAttributes audioAttributes;
@Nullable private MediaPositionParameters afterDrainParameters;
@ -561,7 +564,9 @@ public final class DefaultAudioSink implements AudioSink {
context = builder.context;
audioAttributes = AudioAttributes.DEFAULT;
audioCapabilities =
context != null ? getCapabilities(context, audioAttributes) : builder.audioCapabilities;
context != null
? getCapabilities(context, audioAttributes, /* routedDevice= */ null)
: builder.audioCapabilities;
audioProcessorChain = builder.audioProcessorChain;
enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput;
preferAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
@ -834,6 +839,13 @@ public final class DefaultAudioSink implements AudioSink {
}
if (preferredDevice != null && Util.SDK_INT >= 23) {
Api23.setPreferredDeviceOnAudioTrack(audioTrack, preferredDevice);
if (audioCapabilitiesReceiver != null) {
audioCapabilitiesReceiver.setRoutedDevice(preferredDevice.audioDeviceInfo);
}
}
if (Util.SDK_INT >= 24 && audioCapabilitiesReceiver != null) {
onRoutingChangedListener =
new OnRoutingChangedListenerApi24(audioTrack, audioCapabilitiesReceiver);
}
startMediaTimeUsNeedsInit = true;
@ -1365,6 +1377,9 @@ public final class DefaultAudioSink implements AudioSink {
public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
this.preferredDevice =
audioDeviceInfo == null ? null : new AudioDeviceInfoApi23(audioDeviceInfo);
if (audioCapabilitiesReceiver != null) {
audioCapabilitiesReceiver.setRoutedDevice(audioDeviceInfo);
}
if (audioTrack != null) {
Api23.setPreferredDeviceOnAudioTrack(audioTrack, this.preferredDevice);
}
@ -1458,6 +1473,10 @@ public final class DefaultAudioSink implements AudioSink {
pendingConfiguration = null;
}
audioTrackPositionTracker.reset();
if (Util.SDK_INT >= 24 && onRoutingChangedListener != null) {
onRoutingChangedListener.release();
onRoutingChangedListener = null;
}
releaseAudioTrackAsync(audioTrack, releasingConditionVariable, listener, oldAudioTrackConfig);
audioTrack = null;
}
@ -1716,7 +1735,8 @@ public final class DefaultAudioSink implements AudioSink {
// current (playback) thread as the constructor is not called in the playback thread.
playbackLooper = Looper.myLooper();
audioCapabilitiesReceiver =
new AudioCapabilitiesReceiver(context, this::onAudioCapabilitiesChanged, audioAttributes);
new AudioCapabilitiesReceiver(
context, this::onAudioCapabilitiesChanged, audioAttributes, preferredDevice);
audioCapabilities = audioCapabilitiesReceiver.register();
}
}
@ -1876,6 +1896,42 @@ public final class DefaultAudioSink implements AudioSink {
}
}
@RequiresApi(24)
private static final class OnRoutingChangedListenerApi24 {
private final AudioTrack audioTrack;
private final AudioCapabilitiesReceiver capabilitiesReceiver;
@Nullable private OnRoutingChangedListener listener;
public OnRoutingChangedListenerApi24(
AudioTrack audioTrack, AudioCapabilitiesReceiver capabilitiesReceiver) {
this.audioTrack = audioTrack;
this.capabilitiesReceiver = capabilitiesReceiver;
this.listener = this::onRoutingChanged;
Handler handler = new Handler(Looper.myLooper());
audioTrack.addOnRoutingChangedListener(listener, handler);
}
@DoNotInline
public void release() {
audioTrack.removeOnRoutingChangedListener(checkNotNull(listener));
listener = null;
}
@DoNotInline
private void onRoutingChanged(AudioRouting router) {
if (listener == null) {
// Stale event.
return;
}
@Nullable AudioDeviceInfo routedDevice = router.getRoutedDevice();
if (routedDevice != null) {
capabilitiesReceiver.setRoutedDevice(router.getRoutedDevice());
}
}
}
@RequiresApi(29)
private final class StreamEventCallbackV29 {
private final Handler handler;
@ -1918,10 +1974,12 @@ public final class DefaultAudioSink implements AudioSink {
};
}
@DoNotInline
public void register(AudioTrack audioTrack) {
audioTrack.registerStreamEventCallback(handler::post, callback);
}
@DoNotInline
public void unregister(AudioTrack audioTrack) {
audioTrack.unregisterStreamEventCallback(callback);
handler.removeCallbacksAndMessages(/* token= */ null);
@ -2279,16 +2337,6 @@ public final class DefaultAudioSink implements AudioSink {
accumulatedSkippedSilenceDurationUs = 0;
}
@RequiresApi(23)
private static final class AudioDeviceInfoApi23 {
public final AudioDeviceInfo audioDeviceInfo;
public AudioDeviceInfoApi23(AudioDeviceInfo audioDeviceInfo) {
this.audioDeviceInfo = audioDeviceInfo;
}
}
@RequiresApi(23)
private static final class Api23 {
private Api23() {}