diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 223824b79a..2444befe13 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -85,6 +85,8 @@ `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. * Add automatic audio becoming noisy handling to `SimpleExoPlayer`, available through `SimpleExoPlayer.setHandleAudioBecomingNoisy`. +* Post `AudioFocusManager.onAudioFocusChange` events to eventHandler, avoiding + multithreaded access to the player or audio focus manager. * Add `Timeline.Window.isLive` to indicate that a window is a live stream ([#2668](https://github.com/google/ExoPlayer/issues/2668) and [#5973](https://github.com/google/ExoPlayer/issues/5973)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java index 87d5c05bbe..c9aa9e54a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2; import android.content.Context; import android.media.AudioFocusRequest; import android.media.AudioManager; +import android.os.Handler; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -114,13 +115,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Constructs an AudioFocusManager to automatically handle audio focus for a player. * * @param context The current context. + * @param eventHandler A {@link Handler} to for the thread on which the player is used. * @param playerControl A {@link PlayerControl} to handle commands from this instance. */ - public AudioFocusManager(Context context, PlayerControl playerControl) { + public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) { this.audioManager = (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); this.playerControl = playerControl; - this.focusListener = new AudioFocusListener(); + this.focusListener = new AudioFocusListener(eventHandler); this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; } @@ -380,65 +382,75 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + private void handleAudioFocusChange(int focusChange) { + // Convert the platform focus change to internal state. + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (willPauseWhenDucked()) { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + } else { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; + } + break; + case AudioManager.AUDIOFOCUS_GAIN: + audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; + break; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + // Early return. + return; + } + + // Handle the internal state (change). + switch (audioFocusState) { + case AUDIO_FOCUS_STATE_NO_FOCUS: + // Focus was not requested; nothing to do. + break; + case AUDIO_FOCUS_STATE_LOST_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(/* forceAbandon= */ true); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: + playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: + // Volume will be adjusted by the code below. + break; + case AUDIO_FOCUS_STATE_HAVE_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + break; + default: + throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); + } + + float volumeMultiplier = + (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK + : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; + if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { + AudioFocusManager.this.volumeMultiplier = volumeMultiplier; + playerControl.setVolumeMultiplier(volumeMultiplier); + } + } + // Internal audio focus listener. private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { + private final Handler eventHandler; + + public AudioFocusListener(Handler eventHandler) { + this.eventHandler = eventHandler; + } + @Override public void onAudioFocusChange(int focusChange) { - // Convert the platform focus change to internal state. - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (willPauseWhenDucked()) { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - } else { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; - } - break; - case AudioManager.AUDIOFOCUS_GAIN: - audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; - break; - default: - Log.w(TAG, "Unknown focus change type: " + focusChange); - // Early return. - return; - } - - // Handle the internal state (change). - switch (audioFocusState) { - case AUDIO_FOCUS_STATE_NO_FOCUS: - // Focus was not requested; nothing to do. - break; - case AUDIO_FOCUS_STATE_LOST_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); - abandonAudioFocus(/* forceAbandon= */ true); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: - playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: - // Volume will be adjusted by the code below. - break; - case AUDIO_FOCUS_STATE_HAVE_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); - break; - default: - throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); - } - - float volumeMultiplier = - (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) - ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK - : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; - if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { - AudioFocusManager.this.volumeMultiplier = volumeMultiplier; - playerControl.setVolumeMultiplier(volumeMultiplier); - } + eventHandler.post(() -> handleAudioFocusChange(focusChange)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 035bc4e5e6..a33b2700ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -456,7 +456,7 @@ public class SimpleExoPlayer extends BasePlayer } audioBecomingNoisyManager = new AudioBecomingNoisyManager(context, eventHandler, componentListener); - audioFocusManager = new AudioFocusManager(context, componentListener); + audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); wakeLockManager = new WakeLockManager(context); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java index 7205a74853..9a44d6def6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java @@ -25,6 +25,8 @@ import static org.robolectric.annotation.Config.TARGET_SDK; import android.content.Context; import android.media.AudioFocusRequest; import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -54,7 +56,10 @@ public class AudioFocusManagerTest { testPlayerControl = new TestPlayerControl(); audioFocusManager = - new AudioFocusManager(ApplicationProvider.getApplicationContext(), testPlayerControl); + new AudioFocusManager( + ApplicationProvider.getApplicationContext(), + new Handler(Looper.myLooper()), + testPlayerControl); } @Test