diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 769304f485..72ad4efbc7 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -19,12 +19,9 @@
`ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`. The playback
suppression reason will be updated as
`Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` if playback
- is attempted when no suitable audio outputs are available.
- * Add handling for auto-resume or auto-pause of playback when audio output
- devices are added or removed dynamically during suppressed or ongoing
- playback when the playback suppression due to no suitable output has
- been enabled via
- `ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`.
+ is attempted when no suitable audio outputs are available, or if all
+ suitable outputs are disconnected during playback. The suppression
+ reason will be removed when a suitable output is connected.
* Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are
created after the playlist is cleared.
* Transformer:
@@ -100,12 +97,6 @@
tests and Compose UI tests. This fixes a bug where playback advances
non-deterministically during Espresso or Compose view interactions.
* Remove deprecated symbols:
- * Remove
- `TransformationRequest.Builder.setEnableRequestSdrToneMapping(boolean)`
- and
- `TransformationRequest.Builder.experimental_setEnableHdrEditing(boolean)`.
- Use `Composition.Builder.setHdrMode(int)` and pass the `Composition` to
- `Transformer.start(Composition, String)` instead.
## 1.1
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 be93e754aa..9beab13c4f 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java
@@ -723,6 +723,9 @@ public interface ExoPlayer extends Player {
* Player.Listener#onPlaybackSuppressionReasonChanged(int)} with the value {@link
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}.
*
+ *
Callers of this may also want to enable {@link #setHandleAudioBecomingNoisy(boolean)} to
+ * prevent playback from continuing on the built-in speaker when a headset is disconnected.
+ *
* @param suppressPlaybackOnUnsuitableOutput Whether the player should suppress the playback
* when it is attempted on an unsuitable output.
* @return This builder.
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 2cd0aee364..ae37756eca 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
@@ -2748,6 +2748,14 @@ import java.util.concurrent.TimeoutException;
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
return;
}
+ updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
+ playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
+ }
+
+ private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
+ boolean playWhenReady,
+ @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
+ @PlaybackSuppressionReason int playbackSuppressionReason) {
pendingOperationAcks++;
// Position estimation and copy must occur before changing/masking playback state.
PlaybackInfo newPlaybackInfo =
@@ -3368,14 +3376,20 @@ import java.util.concurrent.TimeoutException;
if (hasSupportedAudioOutput()
&& playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
- play();
+ updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
+ playbackInfo.playWhenReady,
+ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
+ Player.PLAYBACK_SUPPRESSION_REASON_NONE);
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
if (!hasSupportedAudioOutput()) {
- pause();
+ updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
+ playbackInfo.playWhenReady,
+ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
+ Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
}
}
}
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 14cdcbd9e3..78eff1f235 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -13313,151 +13313,119 @@ public final class ExoPlayerTest {
player.release();
}
+ /**
+ * Tests removal of playback suppression reason as {@link
+ * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when a suitable device is added.
+ */
@Test
- public void
- onAudioDeviceAdded_addSuitableDevicesWhenPlaybackSuppressed_shouldResumeSuppressedPlayback()
- throws Exception {
+ public void addSuitableDevicesWhenPlaybackSuppressed_shouldRemovePlaybackSuppression()
+ throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
+ List playbackSuppressionList = new ArrayList<>();
+ player.addListener(
+ new Player.Listener() {
+ @Override
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
+ }
+ });
player.prepare();
player.play();
player.pause();
runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
- player.addListener(
- new Player.Listener() {
- @Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (playWhenReady
- && player.getPlaybackSuppressionReason()
- != Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
- isPlaybackResumed.set(true);
- }
- }
- });
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackResumed.get()).isTrue();
+ assertThat(playbackSuppressionList)
+ .containsExactly(
+ Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT,
+ Player.PLAYBACK_SUPPRESSION_REASON_NONE);
player.release();
}
+ /**
+ * Tests no change in the playback suppression reason when an unsuitable device is connected while
+ * playback was suppressed earlier.
+ */
@Test
- public void
- onAudioDeviceAdded_addUnsuitableDevicesWithPlaybackSuppressed_shouldNotResumePlayback()
- throws Exception {
+ public void addUnsuitableDevicesWithPlaybackSuppressed_shouldNotRemovePlaybackSuppression()
+ throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
- player.prepare();
- runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
+ List playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (playWhenReady
- && player.getPlaybackSuppressionReason()
- != Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
- isPlaybackResumed.set(true);
- }
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
}
});
+ player.prepare();
+ player.play();
+ runUntilPlaybackState(player, Player.STATE_READY);
addConnectedAudioOutput(AudioDeviceInfo.TYPE_UNKNOWN, /* notifyAudioDeviceCallbacks= */ true);
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackResumed.get()).isFalse();
+ assertThat(playbackSuppressionList)
+ .containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
player.release();
}
+ /**
+ * Tests no change in the playback suppression reason when a suitable device is added but playback
+ * was not suppressed earlier.
+ */
@Test
- public void
- onAudioDeviceAdded_addSuitableDevicesWhenPlaybackNotSuppressed_shouldNotResumePlayback()
- throws Exception {
+ public void addSuitableDevicesWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression()
+ throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
- player.prepare();
- runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
+ List playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (playWhenReady
- && player.getPlaybackSuppressionReason()
- != Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
- isPlaybackResumed.set(true);
- }
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
}
});
+ player.prepare();
+ runUntilPlaybackState(player, Player.STATE_READY);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackResumed.get()).isFalse();
+ assertThat(playbackSuppressionList).isEmpty();
player.release();
}
+ /**
+ * Tests change in the playback suppression reason as {@link
+ * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when all the suitable audio outputs
+ * have been removed during an ongoing playback.
+ */
@Test
- public void onAudioDeviceAdded_addSuitableDevicesOnNonWearSurface_shouldResumeSuppressedPlayback()
+ public void removeAllSuitableDevicesWhenPlaybackOngoing_shouldSetPlaybackSuppression()
throws Exception {
- setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
- ExoPlayer player =
- new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
- player.setMediaItem(
- MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
- player.prepare();
- player.play();
- player.pause();
- runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
- player.addListener(
- new Player.Listener() {
- @Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (playWhenReady
- && player.getPlaybackSuppressionReason()
- != Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
- isPlaybackResumed.set(true);
- }
- }
- });
-
- addConnectedAudioOutput(
- AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
- player.stop();
- runUntilPlaybackState(player, Player.STATE_IDLE);
-
- assertThat(isPlaybackResumed.get()).isFalse();
- player.release();
- }
-
- @Test
- public void
- onAudioDeviceRemoved_removeSuitableDeviceWhenPlaybackOngoing_shouldPauseOngoingPlayback()
- throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
@@ -13468,15 +13436,12 @@ public final class ExoPlayerTest {
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
+ List playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (!playWhenReady) {
- isPlaybackPaused.set(true);
- }
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
}
});
@@ -13484,14 +13449,18 @@ public final class ExoPlayerTest {
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackPaused.get()).isTrue();
+ assertThat(playbackSuppressionList)
+ .containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
player.release();
}
+ /**
+ * Tests no change in the playback suppression reason when any unsuitable audio outputs has been
+ * removed during an ongoing playback.
+ */
@Test
- public void
- onAudioDeviceRemoved_removeUnsuitableDeviceLeavingOneSuitableDevice_shouldNotPausePlayback()
- throws Exception {
+ public void removeAnyUnsuitableDevicesWhenPlaybackOngoing_shouldNotSetPlaybackSuppression()
+ throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
@@ -13505,15 +13474,12 @@ public final class ExoPlayerTest {
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
+ List playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (!playWhenReady) {
- isPlaybackPaused.set(true);
- }
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
}
});
@@ -13522,13 +13488,18 @@ public final class ExoPlayerTest {
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackPaused.get()).isFalse();
+ assertThat(playbackSuppressionList).isEmpty();
player.release();
}
+ /**
+ * Tests no change in the playback suppression reason when any suitable audio outputs has been
+ * removed during an ongoing playback but at least one suitable audio output is still connected to
+ * the device.
+ */
@Test
public void
- onAudioDeviceRemoved_removeSuitableDeviceLeavingOneSuitableDevice_shouldNotPausePlayback()
+ removeAnySuitableDeviceButOneSuitableDeviceStillConnected_shouldNotSetPlaybackSuppression()
throws Exception {
addWatchAsSystemFeature();
setupConnectedAudioOutput(
@@ -13542,15 +13513,12 @@ public final class ExoPlayerTest {
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
+ List playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (!playWhenReady) {
- isPlaybackPaused.set(true);
- }
+ public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
+ playbackSuppressionList.add(playbackSuppressionReason);
}
});
@@ -13558,40 +13526,7 @@ public final class ExoPlayerTest {
player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE);
- assertThat(isPlaybackPaused.get()).isFalse();
- player.release();
- }
-
- @Test
- public void
- onAudioDeviceRemoved_removeSuitableDeviceOnNonWearSurface_shouldNotPauseOngoingPlayback()
- throws Exception {
- setupConnectedAudioOutput(
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
- ExoPlayer player =
- new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
- player.setMediaItem(
- MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
- player.prepare();
- player.play();
- runUntilPlaybackState(player, Player.STATE_READY);
- AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
- player.addListener(
- new Player.Listener() {
- @Override
- public void onPlayWhenReadyChanged(
- boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
- if (!playWhenReady) {
- isPlaybackPaused.set(true);
- }
- }
- });
-
- removeConnectedAudioOutput(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
- player.stop();
- runUntilPlaybackState(player, Player.STATE_IDLE);
-
- assertThat(isPlaybackPaused.get()).isFalse();
+ assertThat(playbackSuppressionList).isEmpty();
player.release();
}