mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add playback suppression for the attempted playbacks on an unsuitable output.
This CL introduces the new public API setSuppressPlaybackWhenUnsuitableOutput which if set to TRUE will cause suppression of a requested playback if that is going to happen on an unsuitable audio output (e.g. builtin speaker on a WearOS device). PiperOrigin-RevId: 538867212
This commit is contained in:
parent
28b8fb706a
commit
605af62d00
@ -13,6 +13,12 @@
|
|||||||
* Add additional action to manifest of main demo for making it easier to
|
* Add additional action to manifest of main demo for making it easier to
|
||||||
start the demo app with a custom `*.exolist.json` file
|
start the demo app with a custom `*.exolist.json` file
|
||||||
([#439](https://github.com/androidx/media/pull/439)).
|
([#439](https://github.com/androidx/media/pull/439)).
|
||||||
|
* Add suppression of playback on unsuitable audio output devices (e.g. the
|
||||||
|
built-in speaker on Wear OS devices) when this feature is enabled via
|
||||||
|
`ExoPlayer.Builder.setSuppressPlaybackWhenNoSuitableOutputAvailable`.
|
||||||
|
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.
|
||||||
* Transformer:
|
* Transformer:
|
||||||
* Parse EXIF rotation data for image inputs.
|
* Parse EXIF rotation data for image inputs.
|
||||||
* Track Selection:
|
* Track Selection:
|
||||||
|
5
api.txt
5
api.txt
@ -862,7 +862,8 @@ package androidx.media3.common {
|
|||||||
field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2
|
field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0
|
field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1
|
field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2; // 0x2
|
field public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3; // 0x3
|
||||||
|
field @Deprecated public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2; // 0x2
|
||||||
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; // 0x3
|
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; // 0x3
|
||||||
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; // 0x2
|
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; // 0x2
|
||||||
field public static final int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; // 0x5
|
field public static final int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; // 0x5
|
||||||
@ -944,7 +945,7 @@ package androidx.media3.common {
|
|||||||
@IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlayWhenReadyChangeReason {
|
@IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlayWhenReadyChangeReason {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlaybackSuppressionReason {
|
@IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlaybackSuppressionReason {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Player.PositionInfo {
|
public static final class Player.PositionInfo {
|
||||||
|
@ -1221,8 +1221,9 @@ public interface Player {
|
|||||||
/**
|
/**
|
||||||
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
|
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
|
||||||
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE}, {@link
|
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE}, {@link
|
||||||
* #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS} or {@link
|
* #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}, {@link
|
||||||
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE}.
|
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE} or {@link
|
||||||
|
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}.
|
||||||
*/
|
*/
|
||||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||||
// with Kotlin usages from before TYPE_USE was added.
|
// with Kotlin usages from before TYPE_USE was added.
|
||||||
@ -1232,7 +1233,8 @@ public interface Player {
|
|||||||
@IntDef({
|
@IntDef({
|
||||||
PLAYBACK_SUPPRESSION_REASON_NONE,
|
PLAYBACK_SUPPRESSION_REASON_NONE,
|
||||||
PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS,
|
PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS,
|
||||||
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE
|
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE,
|
||||||
|
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT
|
||||||
})
|
})
|
||||||
@interface PlaybackSuppressionReason {}
|
@interface PlaybackSuppressionReason {}
|
||||||
/** Playback is not suppressed. */
|
/** Playback is not suppressed. */
|
||||||
@ -1240,10 +1242,14 @@ public interface Player {
|
|||||||
/** Playback is suppressed due to transient audio focus loss. */
|
/** Playback is suppressed due to transient audio focus loss. */
|
||||||
int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1;
|
int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1;
|
||||||
/**
|
/**
|
||||||
* Playback is suppressed due to no suitable audio route, such as an attempt to use an internal
|
* @deprecated Use {@link #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} instead.
|
||||||
* speaker instead of bluetooth headphones on Wear OS.
|
|
||||||
*/
|
*/
|
||||||
int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2;
|
@Deprecated int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2;
|
||||||
|
/**
|
||||||
|
* Playback is suppressed due to attempt to play on an unsuitable audio output (e.g. attempt to
|
||||||
|
* play on built-in speaker on a Wear OS device).
|
||||||
|
*/
|
||||||
|
int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
|
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
|
||||||
|
@ -493,6 +493,7 @@ public interface ExoPlayer extends Player {
|
|||||||
/* package */ boolean usePlatformDiagnostics;
|
/* package */ boolean usePlatformDiagnostics;
|
||||||
@Nullable /* package */ Looper playbackLooper;
|
@Nullable /* package */ Looper playbackLooper;
|
||||||
/* package */ boolean buildCalled;
|
/* package */ boolean buildCalled;
|
||||||
|
/* package */ boolean suppressPlaybackWhenNoSuitableOutputAvailable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a builder.
|
* Creates a builder.
|
||||||
@ -712,6 +713,31 @@ public interface ExoPlayer extends Player {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the player should suppress playback when a suitable audio output is not
|
||||||
|
* available. An example of an unsuitable audio output is the built-in speaker on a Wear OS
|
||||||
|
* device (unless it is explicitly selected by the user).
|
||||||
|
*
|
||||||
|
* <p>If called with {@code suppressPlaybackWhenNoSuitableOutputAvailable = true}, then a
|
||||||
|
* playback attempt while no suitable output is available will result in calls to {@link
|
||||||
|
* Player.Listener#onPlaybackSuppressionReasonChanged(int)} with the value {@link
|
||||||
|
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}.
|
||||||
|
*
|
||||||
|
* @param suppressPlaybackWhenNoSuitableOutputAvailable Whether the player should suppress the
|
||||||
|
* playback when suitable media route is not available.
|
||||||
|
* @return This builder.
|
||||||
|
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
@UnstableApi
|
||||||
|
public Builder setSuppressPlaybackWhenNoSuitableOutputAvailable(
|
||||||
|
boolean suppressPlaybackWhenNoSuitableOutputAvailable) {
|
||||||
|
checkState(!buildCalled);
|
||||||
|
this.suppressPlaybackWhenNoSuitableOutputAvailable =
|
||||||
|
suppressPlaybackWhenNoSuitableOutputAvailable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link RenderersFactory} that will be used by the player.
|
* Sets the {@link RenderersFactory} that will be used by the player.
|
||||||
*
|
*
|
||||||
|
@ -40,10 +40,12 @@ import static java.lang.Math.min;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.media.AudioDeviceInfo;
|
import android.media.AudioDeviceInfo;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.media.metrics.LogSessionId;
|
import android.media.metrics.LogSessionId;
|
||||||
@ -172,6 +174,8 @@ import java.util.concurrent.TimeoutException;
|
|||||||
private final WakeLockManager wakeLockManager;
|
private final WakeLockManager wakeLockManager;
|
||||||
private final WifiLockManager wifiLockManager;
|
private final WifiLockManager wifiLockManager;
|
||||||
private final long detachSurfaceTimeoutMs;
|
private final long detachSurfaceTimeoutMs;
|
||||||
|
@Nullable private AudioManager audioManager;
|
||||||
|
private final boolean suppressPlaybackWhenNoSuitableOutputAvailable;
|
||||||
|
|
||||||
private @RepeatMode int repeatMode;
|
private @RepeatMode int repeatMode;
|
||||||
private boolean shuffleModeEnabled;
|
private boolean shuffleModeEnabled;
|
||||||
@ -274,6 +278,8 @@ import java.util.concurrent.TimeoutException;
|
|||||||
this.applicationLooper = builder.looper;
|
this.applicationLooper = builder.looper;
|
||||||
this.clock = builder.clock;
|
this.clock = builder.clock;
|
||||||
this.wrappingPlayer = wrappingPlayer == null ? this : wrappingPlayer;
|
this.wrappingPlayer = wrappingPlayer == null ? this : wrappingPlayer;
|
||||||
|
this.suppressPlaybackWhenNoSuitableOutputAvailable =
|
||||||
|
builder.suppressPlaybackWhenNoSuitableOutputAvailable;
|
||||||
listeners =
|
listeners =
|
||||||
new ListenerSet<>(
|
new ListenerSet<>(
|
||||||
applicationLooper,
|
applicationLooper,
|
||||||
@ -382,6 +388,9 @@ import java.util.concurrent.TimeoutException;
|
|||||||
audioBecomingNoisyManager.setEnabled(builder.handleAudioBecomingNoisy);
|
audioBecomingNoisyManager.setEnabled(builder.handleAudioBecomingNoisy);
|
||||||
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
|
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
|
||||||
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
|
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
|
||||||
|
if (suppressPlaybackWhenNoSuitableOutputAvailable) {
|
||||||
|
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
}
|
||||||
if (builder.deviceVolumeControlEnabled) {
|
if (builder.deviceVolumeControlEnabled) {
|
||||||
streamVolumeManager =
|
streamVolumeManager =
|
||||||
new StreamVolumeManager(builder.context, eventHandler, componentListener);
|
new StreamVolumeManager(builder.context, eventHandler, componentListener);
|
||||||
@ -2731,26 +2740,22 @@ import java.util.concurrent.TimeoutException;
|
|||||||
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
|
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
|
||||||
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
|
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
|
||||||
@PlaybackSuppressionReason
|
@PlaybackSuppressionReason
|
||||||
int playbackSuppressionReason =
|
int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady, playerCommand);
|
||||||
playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY
|
|
||||||
? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS
|
|
||||||
: Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
|
||||||
if (playbackInfo.playWhenReady == playWhenReady
|
if (playbackInfo.playWhenReady == playWhenReady
|
||||||
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
|
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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 playbackInfo =
|
PlaybackInfo newPlaybackInfo =
|
||||||
this.playbackInfo.sleepingForOffload
|
this.playbackInfo.sleepingForOffload
|
||||||
? this.playbackInfo.copyWithEstimatedPosition()
|
? this.playbackInfo.copyWithEstimatedPosition()
|
||||||
: this.playbackInfo;
|
: this.playbackInfo;
|
||||||
playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
|
newPlaybackInfo =
|
||||||
|
newPlaybackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
|
||||||
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
|
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
|
||||||
updatePlaybackInfo(
|
updatePlaybackInfo(
|
||||||
playbackInfo,
|
newPlaybackInfo,
|
||||||
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
||||||
playWhenReadyChangeReason,
|
playWhenReadyChangeReason,
|
||||||
/* positionDiscontinuity= */ false,
|
/* positionDiscontinuity= */ false,
|
||||||
@ -2760,6 +2765,35 @@ import java.util.concurrent.TimeoutException;
|
|||||||
/* repeatCurrentMediaItem= */ false);
|
/* repeatCurrentMediaItem= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PlaybackSuppressionReason
|
||||||
|
private int computePlaybackSuppressionReason(
|
||||||
|
boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
|
||||||
|
if (playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY) {
|
||||||
|
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
|
||||||
|
}
|
||||||
|
if (suppressPlaybackWhenNoSuitableOutputAvailable) {
|
||||||
|
if (playWhenReady && !hasSupportedAudioOutput()) {
|
||||||
|
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
|
||||||
|
}
|
||||||
|
if (!playWhenReady
|
||||||
|
&& playbackInfo.playbackSuppressionReason
|
||||||
|
== PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
|
||||||
|
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasSupportedAudioOutput() {
|
||||||
|
if (audioManager == null || Util.SDK_INT < 23) {
|
||||||
|
// The Audio Manager API to determine the list of connected audio devices is available only in
|
||||||
|
// API >= 23.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Api23.isSuitableAudioOutputPresentInAudioDeviceInfoList(
|
||||||
|
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
|
||||||
|
}
|
||||||
|
|
||||||
private void updateWakeAndWifiLock() {
|
private void updateWakeAndWifiLock() {
|
||||||
@State int playbackState = getPlaybackState();
|
@State int playbackState = getPlaybackState();
|
||||||
switch (playbackState) {
|
switch (playbackState) {
|
||||||
@ -3281,4 +3315,46 @@ import java.util.concurrent.TimeoutException;
|
|||||||
return new PlayerId(listener.getLogSessionId());
|
return new PlayerId(listener.getLogSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(23)
|
||||||
|
private static final class Api23 {
|
||||||
|
private Api23() {}
|
||||||
|
|
||||||
|
public static boolean isSuitableAudioOutputPresentInAudioDeviceInfoList(
|
||||||
|
Context context, AudioDeviceInfo[] audioDeviceInfos) {
|
||||||
|
if (!isRunningOnWear(context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (AudioDeviceInfo device : audioDeviceInfos) {
|
||||||
|
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_LINE_ANALOG
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_LINE_DIGITAL
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Util.SDK_INT >= 26 && device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Util.SDK_INT >= 28 && device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Util.SDK_INT >= 31
|
||||||
|
&& (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET
|
||||||
|
|| device.getType() == AudioDeviceInfo.TYPE_BLE_SPEAKER)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Util.SDK_INT >= 33 && device.getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRunningOnWear(Context context) {
|
||||||
|
PackageManager packageManager = context.getPackageManager();
|
||||||
|
return packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,9 @@ import static org.robolectric.Shadows.shadowOf;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.media.AudioDeviceInfo;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -112,6 +114,7 @@ import androidx.media3.common.PlaybackParameters;
|
|||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Player.DiscontinuityReason;
|
import androidx.media3.common.Player.DiscontinuityReason;
|
||||||
import androidx.media3.common.Player.Listener;
|
import androidx.media3.common.Player.Listener;
|
||||||
|
import androidx.media3.common.Player.PlayWhenReadyChangeReason;
|
||||||
import androidx.media3.common.Player.PositionInfo;
|
import androidx.media3.common.Player.PositionInfo;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.Timeline.Window;
|
import androidx.media3.common.Timeline.Window;
|
||||||
@ -201,8 +204,10 @@ import org.mockito.ArgumentMatchers;
|
|||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.robolectric.shadows.AudioDeviceInfoBuilder;
|
||||||
import org.robolectric.shadows.ShadowAudioManager;
|
import org.robolectric.shadows.ShadowAudioManager;
|
||||||
import org.robolectric.shadows.ShadowLooper;
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
|
import org.robolectric.shadows.ShadowPackageManager;
|
||||||
|
|
||||||
/** Unit test for {@link ExoPlayer}. */
|
/** Unit test for {@link ExoPlayer}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
@ -8564,7 +8569,8 @@ public final class ExoPlayerTest {
|
|||||||
Player.Listener playerListener1 =
|
Player.Listener playerListener1 =
|
||||||
new Player.Listener() {
|
new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
events.add(playWhenReadyChange1);
|
events.add(playWhenReadyChange1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8576,7 +8582,8 @@ public final class ExoPlayerTest {
|
|||||||
Player.Listener playerListener2 =
|
Player.Listener playerListener2 =
|
||||||
new Player.Listener() {
|
new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
events.add(playWhenReadyChange2);
|
events.add(playWhenReadyChange2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12807,8 +12814,203 @@ public final class ExoPlayerTest {
|
|||||||
player.release();
|
player.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests playback suppression for playback with only unsuitable route (e.g. builtin speaker) on
|
||||||
|
* Wear OS.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void play_withNoSuitableMediaRouteOnWear_shouldSuppressPlayback() throws Exception {
|
||||||
|
addWatchAsSystemFeature();
|
||||||
|
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
||||||
|
List<Integer> playbackSuppressionList = new ArrayList<>();
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(context)
|
||||||
|
.setSuppressOutputWhenNoSuitableOutputAvailable(true)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
playbackSuppressionList.add(player.getPlaybackSuppressionReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
player.stop();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
|
|
||||||
|
assertThat(playbackSuppressionList)
|
||||||
|
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests playback suppression for playback with suitable route (e.g. BluetoothA2DP) on Wear OS.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void play_withNoSuitableMediaRouteOnWear_shouldNotSuppressPlayback() throws Exception {
|
||||||
|
addWatchAsSystemFeature();
|
||||||
|
setupConnectedAudioOutput(
|
||||||
|
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
|
||||||
|
List<Integer> playbackSuppressionList = new ArrayList<>();
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(context)
|
||||||
|
.setSuppressOutputWhenNoSuitableOutputAvailable(true)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
playbackSuppressionList.add(player.getPlaybackSuppressionReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
player.stop();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
|
|
||||||
|
assertThat(playbackSuppressionList).containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests playback suppression for multiple play calls with only unsuitable route (e.g. builtin
|
||||||
|
* speaker) on Wear OS.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
play_call2TimesWithSubsequentPauseWithNoSuitableRouteOnWear_shouldSuppressionPlayback2Times()
|
||||||
|
throws Exception {
|
||||||
|
addWatchAsSystemFeature();
|
||||||
|
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
||||||
|
List<Integer> playbackSuppressionList = new ArrayList<>();
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(context)
|
||||||
|
.setSuppressOutputWhenNoSuitableOutputAvailable(true)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
|
player.pause();
|
||||||
|
if (playWhenReady) {
|
||||||
|
playbackSuppressionList.add(player.getPlaybackSuppressionReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
player.play();
|
||||||
|
player.stop();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
|
|
||||||
|
assertThat(playbackSuppressionList)
|
||||||
|
.containsExactly(
|
||||||
|
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT,
|
||||||
|
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests playback suppression for playback on the built-speaker on non-Wear OS surfaces. */
|
||||||
|
@Test
|
||||||
|
public void play_onBuiltinSpeakerWithoutWearSystemFeature_shouldNotSuppressPlayback()
|
||||||
|
throws Exception {
|
||||||
|
setupConnectedAudioOutput(
|
||||||
|
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
|
||||||
|
List<Integer> playbackSuppressionList = new ArrayList<>();
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(context)
|
||||||
|
.setSuppressOutputWhenNoSuitableOutputAvailable(true)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
playbackSuppressionList.add(player.getPlaybackSuppressionReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
player.stop();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
|
|
||||||
|
assertThat(playbackSuppressionList).containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests playback suppression for playback with only unsuitable route (e.g. builtin speaker) on
|
||||||
|
* Wear OS but {@link ExoPlayer.Builder#setSuppressPlaybackWhenNoSuitableOutputAvailable(boolean)}
|
||||||
|
* is not called with parameter as TRUE.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
play_withOnlyUnsuitableRoutesWithoutEnablingPlaybackSuppression_shouldNotSuppressPlayback()
|
||||||
|
throws Exception {
|
||||||
|
addWatchAsSystemFeature();
|
||||||
|
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
||||||
|
List<Integer> playbackSuppressionList = new ArrayList<>();
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
player.setMediaItem(
|
||||||
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
|
player.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyChanged(
|
||||||
|
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
playbackSuppressionList.add(player.getPlaybackSuppressionReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
player.stop();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
|
|
||||||
|
assertThat(playbackSuppressionList).containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
|
private void addWatchAsSystemFeature() {
|
||||||
|
ShadowPackageManager shadowPackageManager = shadowOf(context.getPackageManager());
|
||||||
|
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupConnectedAudioOutput(int... deviceTypes) {
|
||||||
|
ShadowAudioManager shadowAudioManager = shadowOf(context.getSystemService(AudioManager.class));
|
||||||
|
ImmutableList.Builder<AudioDeviceInfo> deviceListBuilder = ImmutableList.builder();
|
||||||
|
for (int deviceType : deviceTypes) {
|
||||||
|
deviceListBuilder.add(AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build());
|
||||||
|
}
|
||||||
|
shadowAudioManager.setOutputDevices(deviceListBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
||||||
final Surface surface1 = new Surface(new SurfaceTexture(/* texName= */ 0));
|
final Surface surface1 = new Surface(new SurfaceTexture(/* texName= */ 0));
|
||||||
final Surface surface2 = new Surface(new SurfaceTexture(/* texName= */ 1));
|
final Surface surface2 = new Surface(new SurfaceTexture(/* texName= */ 1));
|
||||||
|
@ -55,6 +55,7 @@ public class TestExoPlayerBuilder {
|
|||||||
private long seekBackIncrementMs;
|
private long seekBackIncrementMs;
|
||||||
private long seekForwardIncrementMs;
|
private long seekForwardIncrementMs;
|
||||||
private boolean deviceVolumeControlEnabled;
|
private boolean deviceVolumeControlEnabled;
|
||||||
|
private boolean suppressPlaybackWhenUnsuitableOutput;
|
||||||
|
|
||||||
public TestExoPlayerBuilder(Context context) {
|
public TestExoPlayerBuilder(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@ -301,6 +302,21 @@ public class TestExoPlayerBuilder {
|
|||||||
return seekForwardIncrementMs;
|
return seekForwardIncrementMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link ExoPlayer.Builder#setSuppressPlaybackWhenNoSuitableOutputAvailable(boolean)} for
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* @param suppressPlaybackWhenNoSuitableOutputAvailable Whether the player should suppress the
|
||||||
|
* playback when suitable media route is not available.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public TestExoPlayerBuilder setSuppressOutputWhenNoSuitableOutputAvailable(
|
||||||
|
boolean suppressPlaybackWhenNoSuitableOutputAvailable) {
|
||||||
|
this.suppressPlaybackWhenUnsuitableOutput = suppressPlaybackWhenNoSuitableOutputAvailable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds an {@link ExoPlayer} using the provided values or their defaults. */
|
/** Builds an {@link ExoPlayer} using the provided values or their defaults. */
|
||||||
public ExoPlayer build() {
|
public ExoPlayer build() {
|
||||||
Assertions.checkNotNull(
|
Assertions.checkNotNull(
|
||||||
@ -337,7 +353,8 @@ public class TestExoPlayerBuilder {
|
|||||||
.setLooper(looper)
|
.setLooper(looper)
|
||||||
.setSeekBackIncrementMs(seekBackIncrementMs)
|
.setSeekBackIncrementMs(seekBackIncrementMs)
|
||||||
.setSeekForwardIncrementMs(seekForwardIncrementMs)
|
.setSeekForwardIncrementMs(seekForwardIncrementMs)
|
||||||
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled);
|
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
|
||||||
|
.setSuppressPlaybackWhenNoSuitableOutputAvailable(suppressPlaybackWhenUnsuitableOutput);
|
||||||
if (mediaSourceFactory != null) {
|
if (mediaSourceFactory != null) {
|
||||||
builder.setMediaSourceFactory(mediaSourceFactory);
|
builder.setMediaSourceFactory(mediaSourceFactory);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user