Skip to content

Commit

Permalink
Populate DeviceInfo in CastPlayer using MediaRouter2 info
Browse files Browse the repository at this point in the history
This enables linking the media session to a routing session.

Issue: #1056
PiperOrigin-RevId: 671425490
  • Loading branch information
AquilesCanta authored and copybara-github committed Sep 5, 2024
1 parent a1d2310 commit 4ea58a1
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 5 deletions.
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
* Cast Extension:
* Stop clearning the timeline after the CastSession disconnects, which
enables the sender app to resume playback locally after a disconnection.
* Populate CastPlayer's `DeviceInfo` when a `Context` is provided. This
enables linking the `MediaSession` to a `RoutingSession`, which is
necessary for integrating Output Switcher
([#1056](https://github.com/androidx/media/issues/1056)).
* Test Utilities:
* Demo app:
* Remove deprecated symbols:
Expand Down
148 changes: 144 additions & 4 deletions libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@

import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;

import android.content.Context;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RoutingController;
import android.media.MediaRouter2.TransferCallback;
import android.media.RouteDiscoveryPreference;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.BasePlayer;
Expand Down Expand Up @@ -83,8 +93,11 @@
@UnstableApi
public final class CastPlayer extends BasePlayer {

/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
public static final DeviceInfo DEVICE_INFO =
/**
* A {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote} {@link DeviceInfo} with a null {@link
* DeviceInfo#routingControllerId}.
*/
public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build();

static {
Expand Down Expand Up @@ -128,6 +141,7 @@ public final class CastPlayer extends BasePlayer {
// TODO: Allow custom implementations of CastTimelineTracker.
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
@Nullable private final Api30Impl api30Impl;

// Result callbacks.
private final StatusListener statusListener;
Expand All @@ -153,6 +167,7 @@ public final class CastPlayer extends BasePlayer {
private long pendingSeekPositionMs;
@Nullable private PositionInfo pendingMediaItemRemovalPosition;
private MediaMetadata mediaMetadata;
private DeviceInfo deviceInfo;

/**
* Creates a new cast player.
Expand Down Expand Up @@ -202,6 +217,7 @@ public CastPlayer(
@IntRange(from = 1) long seekBackIncrementMs,
@IntRange(from = 1) long seekForwardIncrementMs) {
this(
/* context= */ null,
castContext,
mediaItemConverter,
seekBackIncrementMs,
Expand All @@ -212,6 +228,8 @@ public CastPlayer(
/**
* Creates a new cast player.
*
* @param context A {@link Context} used to populate {@link #getDeviceInfo()}. If null, {@link
* #getDeviceInfo()} will always return {@link #DEVICE_INFO_REMOTE_EMPTY}.
* @param castContext The context from which the cast session is obtained.
* @param mediaItemConverter The {@link MediaItemConverter} to use.
* @param seekBackIncrementMs The {@link #seekBack()} increment, in milliseconds.
Expand All @@ -223,6 +241,7 @@ public CastPlayer(
* negative.
*/
public CastPlayer(
@Nullable Context context,
CastContext castContext,
MediaItemConverter mediaItemConverter,
@IntRange(from = 1) long seekBackIncrementMs,
Expand Down Expand Up @@ -260,6 +279,14 @@ public CastPlayer(
CastSession session = sessionManager.getCurrentCastSession();
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
updateInternalStateAndNotifyIfChanged();
if (SDK_INT >= 30 && context != null) {
api30Impl = new Api30Impl(context);
api30Impl.initialize();
deviceInfo = api30Impl.fetchDeviceInfo();
} else {
api30Impl = null;
deviceInfo = DEVICE_INFO_REMOTE_EMPTY;
}
}

/**
Expand Down Expand Up @@ -530,6 +557,10 @@ public void stop() {

@Override
public void release() {
// The SDK_INT check is not necessary, but it prevents a lint error for the release call.
if (SDK_INT >= 30 && api30Impl != null) {
api30Impl.release();
}
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
sessionManager.endCurrentSession(false);
Expand Down Expand Up @@ -782,10 +813,14 @@ public CueGroup getCurrentCues() {
return CueGroup.EMPTY_TIME_ZERO;
}

/** This method always returns {@link CastPlayer#DEVICE_INFO}. */
/**
* Returns a {@link DeviceInfo} describing the receiver device. Returns {@link
* #DEVICE_INFO_REMOTE_EMPTY} if no {@link Context} was provided at construction, or if the Cast
* {@link RoutingController} could not be identified.
*/
@Override
public DeviceInfo getDeviceInfo() {
return DEVICE_INFO;
return deviceInfo;
}

/** This method is not supported and always returns {@code 0}. */
Expand Down Expand Up @@ -1534,4 +1569,109 @@ public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
return pendingResultCallback == resultCallback;
}
}

@RequiresApi(30)
private final class Api30Impl {

private final MediaRouter2 mediaRouter2;
private final TransferCallback transferCallback;
private final RouteCallback emptyRouteCallback;
private final Handler handler;

public Api30Impl(Context context) {
mediaRouter2 = MediaRouter2.getInstance(context);
transferCallback = new MediaRouter2TransferCallbackImpl();
emptyRouteCallback = new MediaRouter2RouteCallbackImpl();
handler = new Handler(Looper.getMainLooper());
}

/** Acquires necessary resources and registers callbacks. */
@DoNotInline
public void initialize() {
mediaRouter2.registerTransferCallback(handler::post, transferCallback);
// We need at least one route callback registered in order to get transfer callback updates.
mediaRouter2.registerRouteCallback(
handler::post,
emptyRouteCallback,
new RouteDiscoveryPreference.Builder(ImmutableList.of(), /* activeScan= */ false)
.build());
}

/**
* Releases any resources acquired in {@link #initialize()} and unregisters any registered
* callbacks.
*/
@DoNotInline
public void release() {
mediaRouter2.unregisterTransferCallback(transferCallback);
mediaRouter2.unregisterRouteCallback(emptyRouteCallback);
handler.removeCallbacksAndMessages(/* token= */ null);
}

/** Updates the device info with an up-to-date value and notifies the listeners. */
@DoNotInline
private void updateDeviceInfo() {
DeviceInfo oldDeviceInfo = deviceInfo;
DeviceInfo newDeviceInfo = fetchDeviceInfo();
deviceInfo = newDeviceInfo;
if (!deviceInfo.equals(oldDeviceInfo)) {
listeners.sendEvent(
EVENT_DEVICE_INFO_CHANGED, listener -> listener.onDeviceInfoChanged(newDeviceInfo));
}
}

/**
* Returns a {@link DeviceInfo} with the {@link RoutingController#getId() id} that corresponds
* to the Cast session, or {@link #DEVICE_INFO_REMOTE_EMPTY} if not available.
*/
@DoNotInline
public DeviceInfo fetchDeviceInfo() {
// TODO: b/364833997 - Fetch this information from the AndroidX MediaRouter selected route
// once the selected route id matches the controller id.
List<RoutingController> controllers = mediaRouter2.getControllers();
// The controller at position zero is always the system controller (local playback). All other
// controllers are for remote playback, and could be the Cast one.
if (controllers.size() != 2) {
// There's either no remote routing controller, or there's more than one. In either case we
// don't populate the device info because either there's no Cast routing controller, or we
// cannot safely identify the Cast routing controller.
return DEVICE_INFO_REMOTE_EMPTY;
} else {
// There's only one remote routing controller. It's safe to assume it's the Cast routing
// controller.
RoutingController remoteController = controllers.get(1);
// TODO b/364580007 - Populate volume information, and implement Player volume-related
// methods.
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
.setRoutingControllerId(remoteController.getId())
.build();
}
}

/**
* Empty {@link RouteCallback} implementation necessary for registering the {@link MediaRouter2}
* instance with the system_server.
*
* <p>This callback must be registered so that the media router service notifies the {@link
* MediaRouter2TransferCallbackImpl} of transfer events.
*/
private final class MediaRouter2RouteCallbackImpl extends RouteCallback {}

/**
* {@link TransferCallback} implementation to listen for {@link RoutingController} creation and
* releases.
*/
private final class MediaRouter2TransferCallbackImpl extends TransferCallback {

@Override
public void onTransfer(RoutingController oldController, RoutingController newController) {
updateDeviceInfo();
}

@Override
public void onStop(RoutingController controller) {
updateDeviceInfo();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1902,7 +1902,7 @@ public void setMediaItems_equalMetadata_doesNotNotifyOnMediaMetadataChanged() {
public void getDeviceInfo_returnsCorrectDeviceInfoWithPlaybackTypeRemote() {
DeviceInfo deviceInfo = castPlayer.getDeviceInfo();

assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO);
assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO_REMOTE_EMPTY);
assertThat(deviceInfo.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
}

Expand Down

0 comments on commit 4ea58a1

Please sign in to comment.