Unsuppress/suppress playback on suitable media output updates

PiperOrigin-RevId: 657111555
This commit is contained in:
Googler 2024-07-29 01:41:00 -07:00 committed by Copybara-Service
parent 32c9d62d39
commit f6dc02fa6a
4 changed files with 122 additions and 36 deletions

View File

@ -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()) {

View File

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

View File

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

View File

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