Update playback suppression states dynamically.

Instead of playing or pausing itself, the ExoPlayer implementation should only update the playback suppression reason as and when audio outputs are added or removed dynamically.

PiperOrigin-RevId: 544379033
This commit is contained in:
Googler 2023-06-29 16:27:50 +00:00 committed by Tianyi Feng
parent 4e4045b98e
commit 832d5b5f98
4 changed files with 97 additions and 154 deletions

View File

@ -19,12 +19,9 @@
`ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`. The playback `ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`. The playback
suppression reason will be updated as suppression reason will be updated as
`Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` if playback `Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` if playback
is attempted when no suitable audio outputs are available. is attempted when no suitable audio outputs are available, or if all
* Add handling for auto-resume or auto-pause of playback when audio output suitable outputs are disconnected during playback. The suppression
devices are added or removed dynamically during suppressed or ongoing reason will be removed when a suitable output is connected.
playback when the playback suppression due to no suitable output has
been enabled via
`ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`.
* Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are * Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are
created after the playlist is cleared. created after the playlist is cleared.
* Transformer: * Transformer:
@ -100,12 +97,6 @@
tests and Compose UI tests. This fixes a bug where playback advances tests and Compose UI tests. This fixes a bug where playback advances
non-deterministically during Espresso or Compose view interactions. non-deterministically during Espresso or Compose view interactions.
* Remove deprecated symbols: * 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 ## 1.1

View File

@ -723,6 +723,9 @@ public interface ExoPlayer extends Player {
* Player.Listener#onPlaybackSuppressionReasonChanged(int)} with the value {@link * Player.Listener#onPlaybackSuppressionReasonChanged(int)} with the value {@link
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}. * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}.
* *
* <p>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 * @param suppressPlaybackOnUnsuitableOutput Whether the player should suppress the playback
* when it is attempted on an unsuitable output. * when it is attempted on an unsuitable output.
* @return This builder. * @return This builder.

View File

@ -2748,6 +2748,14 @@ import java.util.concurrent.TimeoutException;
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) { && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
return; return;
} }
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
}
private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
@PlaybackSuppressionReason int playbackSuppressionReason) {
pendingOperationAcks++; pendingOperationAcks++;
// Position estimation and copy must occur before changing/masking playback state. // Position estimation and copy must occur before changing/masking playback state.
PlaybackInfo newPlaybackInfo = PlaybackInfo newPlaybackInfo =
@ -3368,14 +3376,20 @@ import java.util.concurrent.TimeoutException;
if (hasSupportedAudioOutput() if (hasSupportedAudioOutput()
&& playbackInfo.playbackSuppressionReason && playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) { == Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
play(); updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
} }
} }
@Override @Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
if (!hasSupportedAudioOutput()) { if (!hasSupportedAudioOutput()) {
pause(); updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
} }
} }
} }

View File

@ -13313,151 +13313,119 @@ public final class ExoPlayerTest {
player.release(); player.release();
} }
/**
* Tests removal of playback suppression reason as {@link
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when a suitable device is added.
*/
@Test @Test
public void public void addSuitableDevicesWhenPlaybackSuppressed_shouldRemovePlaybackSuppression()
onAudioDeviceAdded_addSuitableDevicesWhenPlaybackSuppressed_shouldResumeSuppressedPlayback() throws Exception {
throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player = ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build(); new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem( player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
List<Integer> playbackSuppressionList = new ArrayList<>();
player.addListener(
new Player.Listener() {
@Override
public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
playbackSuppressionList.add(playbackSuppressionReason);
}
});
player.prepare(); player.prepare();
player.play(); player.play();
player.pause(); player.pause();
runUntilPlaybackState(player, Player.STATE_READY); 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( addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); 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(); player.release();
} }
/**
* Tests no change in the playback suppression reason when an unsuitable device is connected while
* playback was suppressed earlier.
*/
@Test @Test
public void public void addUnsuitableDevicesWithPlaybackSuppressed_shouldNotRemovePlaybackSuppression()
onAudioDeviceAdded_addUnsuitableDevicesWithPlaybackSuppressed_shouldNotResumePlayback() throws Exception {
throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player = ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build(); new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem( player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
player.prepare(); List<Integer> playbackSuppressionList = new ArrayList<>();
runUntilPlaybackState(player, Player.STATE_READY);
AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
player.addListener( player.addListener(
new Player.Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged( public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { playbackSuppressionList.add(playbackSuppressionReason);
if (playWhenReady
&& player.getPlaybackSuppressionReason()
!= Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
isPlaybackResumed.set(true);
}
} }
}); });
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
addConnectedAudioOutput(AudioDeviceInfo.TYPE_UNKNOWN, /* notifyAudioDeviceCallbacks= */ true); addConnectedAudioOutput(AudioDeviceInfo.TYPE_UNKNOWN, /* notifyAudioDeviceCallbacks= */ true);
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); runUntilPlaybackState(player, Player.STATE_IDLE);
assertThat(isPlaybackResumed.get()).isFalse(); assertThat(playbackSuppressionList)
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
player.release(); player.release();
} }
/**
* Tests no change in the playback suppression reason when a suitable device is added but playback
* was not suppressed earlier.
*/
@Test @Test
public void public void addSuitableDevicesWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression()
onAudioDeviceAdded_addSuitableDevicesWhenPlaybackNotSuppressed_shouldNotResumePlayback() throws Exception {
throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
ExoPlayer player = ExoPlayer player =
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build(); new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true).build();
player.setMediaItem( player.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
player.prepare(); List<Integer> playbackSuppressionList = new ArrayList<>();
runUntilPlaybackState(player, Player.STATE_READY);
AtomicBoolean isPlaybackResumed = new AtomicBoolean(false);
player.addListener( player.addListener(
new Player.Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged( public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { playbackSuppressionList.add(playbackSuppressionReason);
if (playWhenReady
&& player.getPlaybackSuppressionReason()
!= Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
isPlaybackResumed.set(true);
}
} }
}); });
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
addConnectedAudioOutput( addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); runUntilPlaybackState(player, Player.STATE_IDLE);
assertThat(isPlaybackResumed.get()).isFalse(); assertThat(playbackSuppressionList).isEmpty();
player.release(); 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 @Test
public void onAudioDeviceAdded_addSuitableDevicesOnNonWearSurface_shouldResumeSuppressedPlayback() public void removeAllSuitableDevicesWhenPlaybackOngoing_shouldSetPlaybackSuppression()
throws Exception { 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(); addWatchAsSystemFeature();
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
@ -13468,15 +13436,12 @@ public final class ExoPlayerTest {
player.prepare(); player.prepare();
player.play(); player.play();
runUntilPlaybackState(player, Player.STATE_READY); runUntilPlaybackState(player, Player.STATE_READY);
AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); List<Integer> playbackSuppressionList = new ArrayList<>();
player.addListener( player.addListener(
new Player.Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged( public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { playbackSuppressionList.add(playbackSuppressionReason);
if (!playWhenReady) {
isPlaybackPaused.set(true);
}
} }
}); });
@ -13484,14 +13449,18 @@ public final class ExoPlayerTest {
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); runUntilPlaybackState(player, Player.STATE_IDLE);
assertThat(isPlaybackPaused.get()).isTrue(); assertThat(playbackSuppressionList)
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
player.release(); player.release();
} }
/**
* Tests no change in the playback suppression reason when any unsuitable audio outputs has been
* removed during an ongoing playback.
*/
@Test @Test
public void public void removeAnyUnsuitableDevicesWhenPlaybackOngoing_shouldNotSetPlaybackSuppression()
onAudioDeviceRemoved_removeUnsuitableDeviceLeavingOneSuitableDevice_shouldNotPausePlayback() throws Exception {
throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
@ -13505,15 +13474,12 @@ public final class ExoPlayerTest {
player.prepare(); player.prepare();
player.play(); player.play();
runUntilPlaybackState(player, Player.STATE_READY); runUntilPlaybackState(player, Player.STATE_READY);
AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); List<Integer> playbackSuppressionList = new ArrayList<>();
player.addListener( player.addListener(
new Player.Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged( public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { playbackSuppressionList.add(playbackSuppressionReason);
if (!playWhenReady) {
isPlaybackPaused.set(true);
}
} }
}); });
@ -13522,13 +13488,18 @@ public final class ExoPlayerTest {
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); runUntilPlaybackState(player, Player.STATE_IDLE);
assertThat(isPlaybackPaused.get()).isFalse(); assertThat(playbackSuppressionList).isEmpty();
player.release(); 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 @Test
public void public void
onAudioDeviceRemoved_removeSuitableDeviceLeavingOneSuitableDevice_shouldNotPausePlayback() removeAnySuitableDeviceButOneSuitableDeviceStillConnected_shouldNotSetPlaybackSuppression()
throws Exception { throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
setupConnectedAudioOutput( setupConnectedAudioOutput(
@ -13542,15 +13513,12 @@ public final class ExoPlayerTest {
player.prepare(); player.prepare();
player.play(); player.play();
runUntilPlaybackState(player, Player.STATE_READY); runUntilPlaybackState(player, Player.STATE_READY);
AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); List<Integer> playbackSuppressionList = new ArrayList<>();
player.addListener( player.addListener(
new Player.Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged( public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { playbackSuppressionList.add(playbackSuppressionReason);
if (!playWhenReady) {
isPlaybackPaused.set(true);
}
} }
}); });
@ -13558,40 +13526,7 @@ public final class ExoPlayerTest {
player.stop(); player.stop();
runUntilPlaybackState(player, Player.STATE_IDLE); runUntilPlaybackState(player, Player.STATE_IDLE);
assertThat(isPlaybackPaused.get()).isFalse(); assertThat(playbackSuppressionList).isEmpty();
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();
player.release(); player.release();
} }