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;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.MediaRouter2.ControllerCallback;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RoutingController;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
@ -42,12 +45,13 @@ import java.util.concurrent.Executor;
private final RouteCallback routeCallback;
private final Executor executor;
private boolean isEnabled;
@Nullable private ControllerCallback controllerCallback;
private boolean isPreviousSelectedOutputSuitableForPlayback;
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
this.router = MediaRouter2.getInstance(context);
this.routeCallback = new RouteCallback() {};
this.executor =
router = MediaRouter2.getInstance(context);
routeCallback = new RouteCallback() {};
executor =
new Executor() {
@Override
public void execute(Runnable command) {
@ -57,19 +61,38 @@ import java.util.concurrent.Executor;
}
@Override
public void setEnabled(boolean isEnabled) {
if (isEnabled && !this.isEnabled) {
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
this.isEnabled = true;
} else if (!isEnabled && this.isEnabled) {
router.unregisterRouteCallback(routeCallback);
this.isEnabled = false;
}
public void enable(Callback callback) {
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
controllerCallback =
new ControllerCallback() {
@Override
public void onControllerUpdated(RoutingController controller) {
boolean isCurrentSelectedOutputSuitableForPlayback =
isSelectedOutputSuitableForPlayback();
if (isPreviousSelectedOutputSuitableForPlayback
!= isCurrentSelectedOutputSuitableForPlayback) {
isPreviousSelectedOutputSuitableForPlayback =
isCurrentSelectedOutputSuitableForPlayback;
callback.onSelectedOutputSuitabilityChanged(
isCurrentSelectedOutputSuitableForPlayback);
}
}
};
router.registerControllerCallback(executor, controllerCallback);
isPreviousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback();
}
@Override
public boolean isSelectedRouteSuitableForPlayback() {
checkState(isEnabled, "SuitableOutputChecker is not enabled");
public void disable() {
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();
boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf();
for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) {

View File

@ -404,7 +404,7 @@ import java.util.concurrent.TimeoutException;
suitableOutputChecker = builder.suitableOutputChecker;
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.setEnabled(true);
suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged);
} else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
Api23.registerAudioDeviceCallback(
@ -1073,7 +1073,7 @@ import java.util.concurrent.TimeoutException;
playbackInfo = playbackInfo.copyWithEstimatedPosition();
}
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.setEnabled(false);
suitableOutputChecker.disable();
}
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
@ -2841,7 +2841,7 @@ import java.util.concurrent.TimeoutException;
private boolean hasSupportedAudioOutput() {
if (Util.SDK_INT >= 35 && suitableOutputChecker != null) {
return suitableOutputChecker.isSelectedRouteSuitableForPlayback();
return suitableOutputChecker.isSelectedOutputSuitableForPlayback();
} else if (Util.SDK_INT >= 23 && audioManager != null) {
return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
@ -2954,6 +2954,23 @@ import java.util.concurrent.TimeoutException;
/* 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) {
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)

View File

@ -21,20 +21,43 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
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)
@RestrictTo(LIBRARY_GROUP)
@UnstableApi
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
* {@code false}.
* <p>When the caller no longer requires updates on suitable outputs, they must call {@link
* #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.
@ -42,5 +65,5 @@ public interface SuitableOutputChecker {
* @throws IllegalStateException if this instance is not enabled to receive the updates on
* suitable media outputs.
*/
boolean isSelectedRouteSuitableForPlayback();
boolean isSelectedOutputSuitableForPlayback();
}

View File

@ -16,8 +16,9 @@
package androidx.media3.test.utils;
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.RestrictTo;
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
* SuitableOutputChecker#isSelectedRouteSuitableForPlayback()}. The default value is false.
* SuitableOutputChecker#isSelectedOutputSuitableForPlayback()}. The default value is false.
*/
@CanIgnoreReturnValue
public Builder setIsSuitableExternalOutputAvailable(boolean isSuitableOutputAvailable) {
@ -55,21 +56,43 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
}
}
private final boolean isSuitableOutputAvailable;
private boolean isEnabled;
private boolean isSelectedOutputSuitableForPlayback;
private boolean previousSelectedOutputSuitableForPlayback;
@Nullable private Callback callback;
public FakeSuitableOutputChecker(boolean isSuitableOutputAvailable) {
this.isSuitableOutputAvailable = isSuitableOutputAvailable;
public FakeSuitableOutputChecker(boolean isSelectedOutputSuitableForPlayback) {
this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
this.previousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
}
@Override
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
public void enable(Callback callback) {
this.callback = callback;
}
@Override
public boolean isSelectedRouteSuitableForPlayback() {
checkState(isEnabled, "SuitableOutputChecker is not enabled");
return isSuitableOutputAvailable;
public void disable() {
this.callback = null;
}
@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;
}
}
}