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 0dfe0eb8fc..d88d7b42ee 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 @@ -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 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 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. * - *

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. + *

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 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 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)); + } + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java index d329a776f2..6fe5ca14b0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -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)); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioDeviceInfoApi23.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioDeviceInfoApi23.java new file mode 100644 index 0000000000..a43e1febb8 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioDeviceInfoApi23.java @@ -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; + } +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 8346f4f9db..9fb053651f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -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() {}