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
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

View File

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

View File

@ -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);
}
}
}

View File

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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();
}