diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 429204f88b..7ee9751640 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Make `AudioTrackBufferSizeProvider` a public interface. * Add `WrappingMediaSource` to simplify wrapping a single `MediaSource` ([#7279](https://github.com/google/ExoPlayer/issues/7279)). + * Add `ExoPlayer.setPreferredAudioDevice` to set the preferred audio + output device ([#135](https://github.com/androidx/media/issues/135)). * Metadata: * `MetadataRenderer` can now be configured to render metadata as soon as they are available. Create an instance with diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index 29614aa26a..84769d802f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkState; import android.content.Context; +import android.media.AudioDeviceInfo; import android.media.AudioTrack; import android.media.MediaCodec; import android.os.Looper; @@ -29,6 +30,7 @@ import android.view.TextureView; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; @@ -1473,6 +1475,16 @@ void setMediaSources( @UnstableApi void clearAuxEffectInfo(); + /** + * Sets the preferred audio device. + * + * @param audioDeviceInfo The preferred {@linkplain AudioDeviceInfo audio device}, or null to + * restore the default. + */ + @UnstableApi + @RequiresApi(23) + void setPreferredAudioDevice(@Nullable AudioDeviceInfo audioDeviceInfo); + /** * Sets whether skipping silences in the audio stream is enabled. * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index c02a601b4b..bf78533492 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -26,6 +26,7 @@ import static androidx.media3.exoplayer.Renderer.MSG_SET_AUX_EFFECT_INFO; import static androidx.media3.exoplayer.Renderer.MSG_SET_CAMERA_MOTION_LISTENER; import static androidx.media3.exoplayer.Renderer.MSG_SET_CHANGE_FRAME_RATE_STRATEGY; +import static androidx.media3.exoplayer.Renderer.MSG_SET_PREFERRED_AUDIO_DEVICE; import static androidx.media3.exoplayer.Renderer.MSG_SET_SCALING_MODE; import static androidx.media3.exoplayer.Renderer.MSG_SET_SKIP_SILENCE_ENABLED; import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER; @@ -38,6 +39,7 @@ import android.content.Context; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioTrack; import android.media.MediaFormat; @@ -1442,6 +1444,13 @@ public void clearAuxEffectInfo() { setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f)); } + @RequiresApi(23) + @Override + public void setPreferredAudioDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { + verifyApplicationThread(); + sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_PREFERRED_AUDIO_DEVICE, audioDeviceInfo); + } + @Override public void setVolume(float volume) { verifyApplicationThread(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index b66921faad..22274d0860 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -198,6 +198,13 @@ interface WakeupListener { *

The message payload must be a {@link WakeupListener} instance. */ int MSG_SET_WAKEUP_LISTENER = 11; + /** + * The type of a message that can be passed to audio renderers via {@link + * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be an {@link + * android.media.AudioDeviceInfo} instance representing the preferred audio device, or null to + * restore the default. + */ + int MSG_SET_PREFERRED_AUDIO_DEVICE = 12; /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * renderers. These custom constants must be greater than or equal to this value. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index 3d9f7e5f53..93c452c766 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -16,6 +16,7 @@ package androidx.media3.exoplayer; import android.content.Context; +import android.media.AudioDeviceInfo; import android.os.Looper; import android.view.Surface; import android.view.SurfaceHolder; @@ -23,6 +24,7 @@ import android.view.TextureView; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; @@ -628,6 +630,13 @@ public void clearAuxEffectInfo() { player.clearAuxEffectInfo(); } + @RequiresApi(23) + @Override + public void setPreferredAudioDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { + blockUntilConstructorFinished(); + player.setPreferredAudioDevice(audioDeviceInfo); + } + @Override public void setVolume(float volume) { blockUntilConstructorFinished(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java index b90f5a6a98..55f78781f1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java @@ -17,9 +17,11 @@ import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.AudioDeviceInfo; import android.media.AudioTrack; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.C; @@ -420,6 +422,15 @@ boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs, int encodedAcce /** Sets the auxiliary effect. */ void setAuxEffectInfo(AuxEffectInfo auxEffectInfo); + /** + * Sets the preferred audio device. + * + * @param audioDeviceInfo The preferred {@linkplain AudioDeviceInfo audio device}, or null to + * restore the default. + */ + @RequiresApi(23) + default void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {} + /** * Enables tunneling, if possible. The sink is reset if tunneling was previously disabled. * Enabling tunneling is only possible if the sink is based on a platform {@link AudioTrack}, and diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index b1ccd35438..0e56525f00 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -23,11 +23,14 @@ import static java.lang.Math.max; import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.AudioDeviceInfo; import android.os.Handler; import android.os.SystemClock; import androidx.annotation.CallSuper; +import androidx.annotation.DoNotInline; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.C; @@ -623,6 +626,11 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message case MSG_SET_AUDIO_SESSION_ID: audioSink.setAudioSessionId((Integer) message); break; + case MSG_SET_PREFERRED_AUDIO_DEVICE: + if (Util.SDK_INT >= 23) { + Api23.setAudioSinkPreferredDevice(audioSink, message); + } + break; case MSG_SET_CAMERA_MOTION_LISTENER: case MSG_SET_CHANGE_FRAME_RATE_STRATEGY: case MSG_SET_SCALING_MODE: @@ -795,4 +803,16 @@ public void onAudioSinkError(Exception audioSinkError) { eventDispatcher.audioSinkError(audioSinkError); } } + + @RequiresApi(23) + private static final class Api23 { + private Api23() {} + + @DoNotInline + public static void setAudioSinkPreferredDevice( + AudioSink audioSink, @Nullable Object messagePayload) { + @Nullable AudioDeviceInfo audioDeviceInfo = (AudioDeviceInfo) messagePayload; + audioSink.setPreferredDevice(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 1677c0636b..ac4135f805 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 @@ -24,6 +24,7 @@ import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.SuppressLint; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; @@ -565,6 +566,7 @@ public DefaultAudioSink build() { private boolean externalAudioSessionIdProvided; private int audioSessionId; private AuxEffectInfo auxEffectInfo; + @Nullable private AudioDeviceInfoApi23 preferredDevice; private boolean tunneling; private long lastFeedElapsedRealtimeMs; private boolean offloadDisabledUntilNextConfiguration; @@ -913,6 +915,9 @@ private boolean initializeAudioTrack() throws InitializationException { audioTrack.attachAuxEffect(auxEffectInfo.effectId); audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel); } + if (preferredDevice != null && Util.SDK_INT >= 23) { + Api23.setPreferredDeviceOnAudioTrack(audioTrack, preferredDevice); + } startMediaTimeUsNeedsInit = true; return true; @@ -1413,6 +1418,16 @@ public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { this.auxEffectInfo = auxEffectInfo; } + @RequiresApi(23) + @Override + public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { + this.preferredDevice = + audioDeviceInfo == null ? null : new AudioDeviceInfoApi23(audioDeviceInfo); + if (audioTrack != null) { + Api23.setPreferredDeviceOnAudioTrack(audioTrack, this.preferredDevice); + } + } + @Override public void enableTunnelingV21() { Assertions.checkState(Util.SDK_INT >= 21); @@ -2309,6 +2324,28 @@ public void clear() { } } + @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() {} + + @DoNotInline + public static void setPreferredDeviceOnAudioTrack( + AudioTrack audioTrack, @Nullable AudioDeviceInfoApi23 audioDeviceInfo) { + audioTrack.setPreferredDevice( + audioDeviceInfo == null ? null : audioDeviceInfo.audioDeviceInfo); + } + } + @RequiresApi(31) private static final class Api31 { private Api31() {} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java index 06fdf5afd1..c0c66d3df1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java @@ -15,7 +15,9 @@ */ package androidx.media3.exoplayer.audio; +import android.media.AudioDeviceInfo; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.Format; @@ -138,6 +140,12 @@ public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { sink.setAuxEffectInfo(auxEffectInfo); } + @RequiresApi(23) + @Override + public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { + sink.setPreferredDevice(audioDeviceInfo); + } + @Override public void enableTunnelingV21() { sink.enableTunnelingV21(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index d6305fd56f..9e32d6357e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -23,13 +23,16 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; import androidx.annotation.CallSuper; +import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.C; @@ -749,6 +752,11 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; audioSink.setAuxEffectInfo(auxEffectInfo); break; + case MSG_SET_PREFERRED_AUDIO_DEVICE: + if (Util.SDK_INT >= 23) { + Api23.setAudioSinkPreferredDevice(audioSink, message); + } + break; case MSG_SET_SKIP_SILENCE_ENABLED: audioSink.setSkipSilenceEnabled((Boolean) message); break; @@ -942,4 +950,16 @@ public void onAudioSinkError(Exception audioSinkError) { eventDispatcher.audioSinkError(audioSinkError); } } + + @RequiresApi(23) + private static final class Api23 { + private Api23() {} + + @DoNotInline + public static void setAudioSinkPreferredDevice( + AudioSink audioSink, @Nullable Object messagePayload) { + @Nullable AudioDeviceInfo audioDeviceInfo = (AudioDeviceInfo) messagePayload; + audioSink.setPreferredDevice(audioDeviceInfo); + } + } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index 0838b7e4ee..e1fa6e5034 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -15,6 +15,7 @@ */ package androidx.media3.test.utils; +import android.media.AudioDeviceInfo; import android.os.Looper; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; @@ -236,6 +237,11 @@ public void clearAuxEffectInfo() { throw new UnsupportedOperationException(); } + @Override + public void setPreferredAudioDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { + throw new UnsupportedOperationException(); + } + @Override public void setSkipSilenceEnabled(boolean skipSilenceEnabled) { throw new UnsupportedOperationException();