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:
parent
4e4045b98e
commit
832d5b5f98
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user