Unsuppress/suppress playback on suitable media output updates
PiperOrigin-RevId: 657111555
This commit is contained in:
parent
32c9d62d39
commit
f6dc02fa6a
@ -15,15 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer;
|
package androidx.media3.exoplayer;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.MediaRoute2Info;
|
import android.media.MediaRoute2Info;
|
||||||
import android.media.MediaRouter2;
|
import android.media.MediaRouter2;
|
||||||
|
import android.media.MediaRouter2.ControllerCallback;
|
||||||
import android.media.MediaRouter2.RouteCallback;
|
import android.media.MediaRouter2.RouteCallback;
|
||||||
|
import android.media.MediaRouter2.RoutingController;
|
||||||
import android.media.RouteDiscoveryPreference;
|
import android.media.RouteDiscoveryPreference;
|
||||||
import android.media.RoutingSessionInfo;
|
import android.media.RoutingSessionInfo;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -42,12 +45,13 @@ import java.util.concurrent.Executor;
|
|||||||
private final RouteCallback routeCallback;
|
private final RouteCallback routeCallback;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
|
|
||||||
private boolean isEnabled;
|
@Nullable private ControllerCallback controllerCallback;
|
||||||
|
private boolean isPreviousSelectedOutputSuitableForPlayback;
|
||||||
|
|
||||||
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
|
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
|
||||||
this.router = MediaRouter2.getInstance(context);
|
router = MediaRouter2.getInstance(context);
|
||||||
this.routeCallback = new RouteCallback() {};
|
routeCallback = new RouteCallback() {};
|
||||||
this.executor =
|
executor =
|
||||||
new Executor() {
|
new Executor() {
|
||||||
@Override
|
@Override
|
||||||
public void execute(Runnable command) {
|
public void execute(Runnable command) {
|
||||||
@ -57,19 +61,38 @@ import java.util.concurrent.Executor;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEnabled(boolean isEnabled) {
|
public void enable(Callback callback) {
|
||||||
if (isEnabled && !this.isEnabled) {
|
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
|
||||||
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
|
controllerCallback =
|
||||||
this.isEnabled = true;
|
new ControllerCallback() {
|
||||||
} else if (!isEnabled && this.isEnabled) {
|
@Override
|
||||||
router.unregisterRouteCallback(routeCallback);
|
public void onControllerUpdated(RoutingController controller) {
|
||||||
this.isEnabled = false;
|
boolean isCurrentSelectedOutputSuitableForPlayback =
|
||||||
}
|
isSelectedOutputSuitableForPlayback();
|
||||||
|
if (isPreviousSelectedOutputSuitableForPlayback
|
||||||
|
!= isCurrentSelectedOutputSuitableForPlayback) {
|
||||||
|
isPreviousSelectedOutputSuitableForPlayback =
|
||||||
|
isCurrentSelectedOutputSuitableForPlayback;
|
||||||
|
callback.onSelectedOutputSuitabilityChanged(
|
||||||
|
isCurrentSelectedOutputSuitableForPlayback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router.registerControllerCallback(executor, controllerCallback);
|
||||||
|
isPreviousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSelectedRouteSuitableForPlayback() {
|
public void disable() {
|
||||||
checkState(isEnabled, "SuitableOutputChecker is not enabled");
|
checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled");
|
||||||
|
router.unregisterControllerCallback(controllerCallback);
|
||||||
|
controllerCallback = null;
|
||||||
|
router.unregisterRouteCallback(routeCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSelectedOutputSuitableForPlayback() {
|
||||||
|
checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled");
|
||||||
int transferReason = router.getSystemController().getRoutingSessionInfo().getTransferReason();
|
int transferReason = router.getSystemController().getRoutingSessionInfo().getTransferReason();
|
||||||
boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf();
|
boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf();
|
||||||
for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) {
|
for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) {
|
||||||
|
@ -404,7 +404,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
|
|
||||||
suitableOutputChecker = builder.suitableOutputChecker;
|
suitableOutputChecker = builder.suitableOutputChecker;
|
||||||
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
|
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
|
||||||
suitableOutputChecker.setEnabled(true);
|
suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged);
|
||||||
} else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
|
} else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
|
||||||
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
Api23.registerAudioDeviceCallback(
|
Api23.registerAudioDeviceCallback(
|
||||||
@ -1073,7 +1073,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
playbackInfo = playbackInfo.copyWithEstimatedPosition();
|
playbackInfo = playbackInfo.copyWithEstimatedPosition();
|
||||||
}
|
}
|
||||||
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
|
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
|
||||||
suitableOutputChecker.setEnabled(false);
|
suitableOutputChecker.disable();
|
||||||
}
|
}
|
||||||
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
|
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
|
||||||
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
|
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
|
||||||
@ -2841,7 +2841,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
|
|
||||||
private boolean hasSupportedAudioOutput() {
|
private boolean hasSupportedAudioOutput() {
|
||||||
if (Util.SDK_INT >= 35 && suitableOutputChecker != null) {
|
if (Util.SDK_INT >= 35 && suitableOutputChecker != null) {
|
||||||
return suitableOutputChecker.isSelectedRouteSuitableForPlayback();
|
return suitableOutputChecker.isSelectedOutputSuitableForPlayback();
|
||||||
} else if (Util.SDK_INT >= 23 && audioManager != null) {
|
} else if (Util.SDK_INT >= 23 && audioManager != null) {
|
||||||
return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
|
return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
|
||||||
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
|
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
|
||||||
@ -2954,6 +2954,23 @@ import java.util.concurrent.TimeoutException;
|
|||||||
/* ignored */ false);
|
/* ignored */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback) {
|
||||||
|
if (isSelectedOutputSuitableForPlayback) {
|
||||||
|
if (playbackInfo.playbackSuppressionReason
|
||||||
|
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
|
||||||
|
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
|
||||||
|
playbackInfo.playWhenReady,
|
||||||
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||||
|
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
|
||||||
|
playbackInfo.playWhenReady,
|
||||||
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||||
|
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) {
|
private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) {
|
||||||
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
|
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
|
||||||
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
|
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
|
||||||
|
@ -21,20 +21,43 @@ import androidx.annotation.RequiresApi;
|
|||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
/** Provides methods to check the suitability of media outputs. */
|
/** Provides methods to check the suitability of selected media outputs. */
|
||||||
@RequiresApi(35)
|
@RequiresApi(35)
|
||||||
@RestrictTo(LIBRARY_GROUP)
|
@RestrictTo(LIBRARY_GROUP)
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public interface SuitableOutputChecker {
|
public interface SuitableOutputChecker {
|
||||||
|
|
||||||
|
/** Callback to notify changes in the suitability of the selected media output. */
|
||||||
|
interface Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when suitability of the selected output has changed.
|
||||||
|
*
|
||||||
|
* @param isSelectedOutputSuitableForPlayback true when selected output is suitable for
|
||||||
|
* playback.
|
||||||
|
*/
|
||||||
|
void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables the current instance to receive updates on the suitable media outputs.
|
* Enables the current instance to receive updates on the selected media outputs and sets the
|
||||||
|
* {@link Callback} to notify the updates on the suitability of the selected output.
|
||||||
*
|
*
|
||||||
* <p>When the caller no longer requires updated information, they must call this method with
|
* <p>When the caller no longer requires updates on suitable outputs, they must call {@link
|
||||||
* {@code false}.
|
* #disable()}.
|
||||||
*
|
*
|
||||||
* @param isEnabled True if this instance should receive the updates.
|
* @param callback To receive notifications of changes in suitable media output changes.
|
||||||
*/
|
*/
|
||||||
void setEnabled(boolean isEnabled);
|
void enable(Callback callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the current instance to receive updates on the selected media outputs and clears the
|
||||||
|
* {@link Callback}.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this instance is not enabled to receive the updates on
|
||||||
|
* suitable media outputs.
|
||||||
|
*/
|
||||||
|
void disable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether any audio output is suitable for the media playback.
|
* Returns whether any audio output is suitable for the media playback.
|
||||||
@ -42,5 +65,5 @@ public interface SuitableOutputChecker {
|
|||||||
* @throws IllegalStateException if this instance is not enabled to receive the updates on
|
* @throws IllegalStateException if this instance is not enabled to receive the updates on
|
||||||
* suitable media outputs.
|
* suitable media outputs.
|
||||||
*/
|
*/
|
||||||
boolean isSelectedRouteSuitableForPlayback();
|
boolean isSelectedOutputSuitableForPlayback();
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
package androidx.media3.test.utils;
|
package androidx.media3.test.utils;
|
||||||
|
|
||||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@ -37,7 +38,7 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the initial value to be returned from {@link
|
* Sets the initial value to be returned from {@link
|
||||||
* SuitableOutputChecker#isSelectedRouteSuitableForPlayback()}. The default value is false.
|
* SuitableOutputChecker#isSelectedOutputSuitableForPlayback()}. The default value is false.
|
||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setIsSuitableExternalOutputAvailable(boolean isSuitableOutputAvailable) {
|
public Builder setIsSuitableExternalOutputAvailable(boolean isSuitableOutputAvailable) {
|
||||||
@ -55,21 +56,43 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final boolean isSuitableOutputAvailable;
|
private boolean isSelectedOutputSuitableForPlayback;
|
||||||
private boolean isEnabled;
|
private boolean previousSelectedOutputSuitableForPlayback;
|
||||||
|
@Nullable private Callback callback;
|
||||||
|
|
||||||
public FakeSuitableOutputChecker(boolean isSuitableOutputAvailable) {
|
public FakeSuitableOutputChecker(boolean isSelectedOutputSuitableForPlayback) {
|
||||||
this.isSuitableOutputAvailable = isSuitableOutputAvailable;
|
this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
|
||||||
|
this.previousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEnabled(boolean isEnabled) {
|
public void enable(Callback callback) {
|
||||||
this.isEnabled = isEnabled;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSelectedRouteSuitableForPlayback() {
|
public void disable() {
|
||||||
checkState(isEnabled, "SuitableOutputChecker is not enabled");
|
this.callback = null;
|
||||||
return isSuitableOutputAvailable;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSelectedOutputSuitableForPlayback() {
|
||||||
|
checkStateNotNull(callback, "SuitableOutputChecker is not enabled");
|
||||||
|
return isSelectedOutputSuitableForPlayback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value to be returned by {@link
|
||||||
|
* SuitableOutputChecker#isSelectedOutputSuitableForPlayback()} and send callbacks to registered
|
||||||
|
* callers via {@link Callback#onSelectedOutputSuitabilityChanged(boolean)}.
|
||||||
|
*/
|
||||||
|
public void updateIsSelectedSuitableOutputAvailableAndNotify(
|
||||||
|
boolean isSelectedOutputSuitableForPlayback) {
|
||||||
|
this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
|
||||||
|
if (callback != null
|
||||||
|
&& previousSelectedOutputSuitableForPlayback != isSelectedOutputSuitableForPlayback) {
|
||||||
|
callback.onSelectedOutputSuitabilityChanged(isSelectedOutputSuitableForPlayback);
|
||||||
|
previousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user