From 385498c24ed395e143bdf2cbb74cf28552b6839f Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 13 Feb 2025 10:55:40 -0800 Subject: [PATCH] Move audio session id generation to playback thread PiperOrigin-RevId: 726556015 --- RELEASENOTES.md | 4 ++ .../media3/exoplayer/ExoPlayerImpl.java | 46 ++++++++++------ .../media3/exoplayer/ExoPlayerTest.java | 54 +++++++++++++++++++ .../DefaultAnalyticsCollectorTest.java | 9 ++-- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8e3cc628a3..6d3ea9ac62 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Upgrade Kotlin from 1.9.20 to 2.0.20 and use Compose Compiler Gradle plugin. Upgrade KotlinX Coroutines library from 1.8.1 to 1.9.0. * ExoPlayer: + * Initial audio session id is no longer immediately available after + creating the player. You can use + `AnalyticsListener.onAudioSessionIdChanged` to listen to the initial + update if required. * Transformer: * Add `MediaProjectionAssetLoader`, which provides media from a `MediaProjection` for screen recording, and add support for screen 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 23de247251..c89d3ab2a2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.common.C.AUDIO_SESSION_ID_UNSET; import static androidx.media3.common.C.TRACK_TYPE_AUDIO; import static androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION; import static androidx.media3.common.C.TRACK_TYPE_IMAGE; @@ -82,6 +83,7 @@ import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.text.CueGroup; +import androidx.media3.common.util.BackgroundThreadStateHandler; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.HandlerWrapper; @@ -178,6 +180,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Nullable private AudioManager audioManager; private final boolean suppressPlaybackOnUnsuitableOutput; @Nullable private final SuitableOutputChecker suitableOutputChecker; + private final BackgroundThreadStateHandler audioSessionIdState; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; @@ -205,7 +208,6 @@ import java.util.concurrent.CopyOnWriteArraySet; private Size surfaceSize; @Nullable private DecoderCounters videoDecoderCounters; @Nullable private DecoderCounters audioDecoderCounters; - private int audioSessionId; private AudioAttributes audioAttributes; private float volume; private boolean skipSilenceEnabled; @@ -390,7 +392,6 @@ import java.util.concurrent.CopyOnWriteArraySet; playlistMetadata = MediaMetadata.EMPTY; staticAndDynamicMediaMetadata = MediaMetadata.EMPTY; maskingWindowIndex = C.INDEX_UNSET; - audioSessionId = Util.generateAudioSessionIdV21(applicationContext); currentCueGroup = CueGroup.EMPTY_TIME_ZERO; throwsWhenUsingWrongThread = true; @@ -401,6 +402,17 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayer.experimentalSetForegroundModeTimeoutMs(builder.foregroundModeTimeoutMs); } + audioSessionIdState = + new BackgroundThreadStateHandler<>( + AUDIO_SESSION_ID_UNSET, + playbackLooper, + applicationLooper, + clock, + /* onStateChanged= */ this::onAudioSessionIdChanged); + audioSessionIdState.runInBackground( + () -> + audioSessionIdState.setStateInBackground( + Util.generateAudioSessionIdV21(applicationContext))); audioBecomingNoisyManager = new AudioBecomingNoisyManager( builder.context, playbackLooper, builder.looper, componentListener, clock); @@ -440,8 +452,6 @@ import java.util.concurrent.CopyOnWriteArraySet; surfaceSize = Size.UNKNOWN; internalPlayer.setAudioAttributes(audioAttributes); - sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); - sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes); sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_SCALING_MODE, videoScalingMode); sendRendererMessage( @@ -1491,24 +1501,22 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void setAudioSessionId(int audioSessionId) { verifyApplicationThread(); - if (this.audioSessionId == audioSessionId) { + if (audioSessionIdState.get() == audioSessionId) { return; } - if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioSessionId = Util.generateAudioSessionIdV21(applicationContext); - } - this.audioSessionId = audioSessionId; - sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); - sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); - int finalAudioSessionId = audioSessionId; - listeners.sendEvent( - EVENT_AUDIO_SESSION_ID, listener -> listener.onAudioSessionIdChanged(finalAudioSessionId)); + audioSessionIdState.updateStateAsync( + /* placeholderState= */ previousId -> + audioSessionId != AUDIO_SESSION_ID_UNSET ? audioSessionId : previousId, + /* backgroundStateUpdate= */ previousId -> + audioSessionId != AUDIO_SESSION_ID_UNSET + ? audioSessionId + : Util.generateAudioSessionIdV21(applicationContext)); } @Override public int getAudioSessionId() { verifyApplicationThread(); - return audioSessionId; + return audioSessionIdState.get(); } @Override @@ -2934,6 +2942,14 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private void onAudioSessionIdChanged(int oldAudioSessionId, int newAudioSessionId) { + verifyApplicationThread(); + sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, newAudioSessionId); + sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, newAudioSessionId); + listeners.sendEvent( + EVENT_AUDIO_SESSION_ID, listener -> listener.onAudioSessionIdChanged(newAudioSessionId)); + } + private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) { return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL) .setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 7c8520a82b..1296f720c7 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -7276,6 +7276,7 @@ public class ExoPlayerTest { .build(); player.setRepeatMode(Player.REPEAT_MODE_ONE); player.setMediaSource(mediaSource); + advance(player).untilPendingCommandsAreFullyHandled(); player.prepare(); advance(player).untilPendingCommandsAreFullyHandled(); @@ -12440,6 +12441,7 @@ public class ExoPlayerTest { @Test public void onEvents_correspondToListenerCalls() throws Exception { ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + advance(player).untilPendingCommandsAreFullyHandled(); Player.Listener listener = mock(Player.Listener.class); player.addListener(listener); Format formatWithStaticMetadata = @@ -16302,6 +16304,58 @@ public class ExoPlayerTest { assertThat(reportedSpeedChanges).containsExactly(2f, 1.5f, 1f).inOrder(); } + @Test + public void builderBuild_createsInitialAudioSessionId() throws Exception { + ExoPlayer player = new ExoPlayer.Builder(context).build(); + Player.Listener listener = mock(Player.Listener.class); + player.addListener(listener); + + int audioSessionIdAfterBuild = player.getAudioSessionId(); + advance(player).untilPendingCommandsAreFullyHandled(); + int audioSessionIdAfterInit = player.getAudioSessionId(); + player.release(); + + assertThat(audioSessionIdAfterBuild).isEqualTo(C.AUDIO_SESSION_ID_UNSET); + assertThat(audioSessionIdAfterInit).isNotEqualTo(C.AUDIO_SESSION_ID_UNSET); + verify(listener).onAudioSessionIdChanged(audioSessionIdAfterInit); + } + + @Test + public void setAudioSessionId_withDefinedId_updatesGetterAndListener() throws Exception { + ExoPlayer player = new ExoPlayer.Builder(context).build(); + Player.Listener listener = mock(Player.Listener.class); + player.addListener(listener); + + player.setAudioSessionId(1234); + int audioSessionId = player.getAudioSessionId(); + // Verify there are no further or duplicated updates. + advance(player).untilPendingCommandsAreFullyHandled(); + int audioSessionIdAfterIdle = player.getAudioSessionId(); + player.release(); + + assertThat(audioSessionId).isEqualTo(1234); + assertThat(audioSessionIdAfterIdle).isEqualTo(1234); + verify(listener).onAudioSessionIdChanged(anyInt()); + verify(listener).onAudioSessionIdChanged(1234); + } + + @Test + public void setAudioSessionId_withUndefinedId_updatesGetterAndListener() throws Exception { + ExoPlayer player = new ExoPlayer.Builder(context).build(); + advance(player).untilPendingCommandsAreFullyHandled(); + Player.Listener listener = mock(Player.Listener.class); + player.addListener(listener); + + int initialAudioSessionId = player.getAudioSessionId(); + player.setAudioSessionId(C.AUDIO_SESSION_ID_UNSET); + advance(player).untilPendingCommandsAreFullyHandled(); + int audioSessionId = player.getAudioSessionId(); + player.release(); + + assertThat(audioSessionId).isNotEqualTo(initialAudioSessionId); + verify(listener).onAudioSessionIdChanged(audioSessionId); + } + // Internal methods. private void addWatchAsSystemFeature() { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index 2f9bccd832..e114f0a082 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -135,6 +135,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -2092,7 +2093,7 @@ public final class DefaultAnalyticsCollectorTest { } } - private static ExoPlayer setupPlayer() { + private static ExoPlayer setupPlayer() throws TimeoutException { Clock clock = new FakeClock(/* isAutoAdvancing= */ true); return setupPlayer( /* renderersFactory= */ (eventHandler, @@ -2110,11 +2111,12 @@ public final class DefaultAnalyticsCollectorTest { clock); } - private static ExoPlayer setupPlayer(RenderersFactory renderersFactory) { + private static ExoPlayer setupPlayer(RenderersFactory renderersFactory) throws TimeoutException { return setupPlayer(renderersFactory, new FakeClock(/* isAutoAdvancing= */ true)); } - private static ExoPlayer setupPlayer(RenderersFactory renderersFactory, Clock clock) { + private static ExoPlayer setupPlayer(RenderersFactory renderersFactory, Clock clock) + throws TimeoutException { Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0)); ExoPlayer player = new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) @@ -2122,6 +2124,7 @@ public final class DefaultAnalyticsCollectorTest { .setRenderersFactory(renderersFactory) .build(); player.setVideoSurface(surface); + advance(player).untilPendingCommandsAreFullyHandled(); return player; }