Skip to content

Commit bb105d3

Browse files
AquilesCantacopybara-github
authored andcommitted
Add suport for local playback in CastPlayer
API users can access the new functionality by creating a CastPlayer instance using the new builder. Existing constructors keep their previous behavior. The new functionality to handle both Cast and local playback simultaneously is implemented by CastPlayerImpl, which delegates playback to a local player or a RemoteCastPlayer instance, depending on the availability of a CastSession. PiperOrigin-RevId: 780647424
1 parent 6030c8d commit bb105d3

File tree

6 files changed

+652
-7
lines changed

6 files changed

+652
-7
lines changed

RELEASENOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
* MIDI extension:
6565
* Leanback extension:
6666
* Cast extension:
67+
* Add `CastPlayer.Builder`, which enables `CastPlayer` to do both local
68+
and Cast playback. To keep the old `CastPlayer` behavior of supporting
69+
only Cast playback, you can use `RemoteCastPlayer`. The pre-existing
70+
`CastPlayer` constructors keep their old behavior, but are deprecated in
71+
favour of using the `CastPlayer` or `RemoteCastPlayer` builders instead.
6772
* Test Utilities:
6873
* Remove deprecated symbols:
6974

libraries/cast/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
api 'com.google.android.gms:play-services-cast-framework:21.5.0'
2828
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
2929
api project(modulePrefix + 'lib-common')
30+
api project(modulePrefix + 'lib-exoplayer')
3031
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
3132
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
3233
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion

libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java

Lines changed: 168 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package androidx.media3.cast;
1717

18+
import static androidx.media3.common.util.Assertions.checkNotNull;
19+
1820
import android.content.Context;
1921
import androidx.annotation.IntRange;
2022
import androidx.annotation.Nullable;
@@ -23,19 +25,154 @@
2325
import androidx.media3.common.ForwardingPlayer;
2426
import androidx.media3.common.MediaItem;
2527
import androidx.media3.common.Player;
28+
import androidx.media3.common.PlayerTransferState;
29+
import androidx.media3.common.util.Assertions;
2630
import androidx.media3.common.util.UnstableApi;
31+
import androidx.media3.exoplayer.ExoPlayer;
2732
import com.google.android.gms.cast.framework.CastContext;
33+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
34+
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
2835

2936
/**
30-
* {@link Player} implementation that communicates with a Cast receiver app.
37+
* {@link Player} implementation that can control playback both on the local device, and on a remote
38+
* Cast device.
39+
*
40+
* <p>See {@link RemoteCastPlayer} for a {@link Player} that only supports playback on Cast
41+
* receivers.
42+
*
43+
* <p>This class works by delegating playback to a dedicated player depending on Cast session
44+
* availability. When a Cast session becomes available or unavailable, the following steps take
45+
* place:
3146
*
32-
* <p>This class is a passthrough wrapper of {@link RemoteCastPlayer}.
47+
* <ul>
48+
* <li>The new active player is a {@link RemoteCastPlayer} if a Cast session is active, or the
49+
* {@link Builder#setLocalPlayer local player} otherwise.
50+
* <li>A customizable {@link TransferCallback} receives both players to transfer state across
51+
* players.
52+
* <li>The inactive player is {@link Player#stop() stopped}.
53+
* </ul>
3354
*/
34-
// TODO: b/419816002 - Deprecate constructors in this class and update javadocs once
35-
// RemoteCastPlayer has a Builder, and this class is able to support local playback.
3655
@UnstableApi
3756
public final class CastPlayer extends ForwardingPlayer {
3857

58+
/**
59+
* Callback for moving state across players when transferring playback upon Cast session
60+
* availability changes.
61+
*/
62+
public interface TransferCallback {
63+
64+
TransferCallback DEFAULT =
65+
(sourcePlayer, targetPlayer) ->
66+
PlayerTransferState.fromPlayer(sourcePlayer).setToPlayer(targetPlayer);
67+
68+
/**
69+
* Called immediately before changing the active {@link Player}, with the intended use of
70+
* transferring playback state.
71+
*
72+
* @param sourcePlayer The {@link Player} from which playback is transferring, from which to
73+
* fetch the state.
74+
* @param targetPlayer The {@link Player} to which playback is transferring, to populate with
75+
* state.
76+
* @see PlayerTransferState
77+
*/
78+
void transferState(Player sourcePlayer, Player targetPlayer);
79+
}
80+
81+
/** Builder for {@link CastPlayer}. */
82+
public static final class Builder {
83+
84+
private final Context context;
85+
private TransferCallback transferCallback;
86+
private @MonotonicNonNull Player localPlayer;
87+
private @MonotonicNonNull RemoteCastPlayer remotePlayer;
88+
private boolean buildCalled;
89+
90+
/**
91+
* Creates a builder.
92+
*
93+
* <p>The builder uses the following default values:
94+
*
95+
* <ul>
96+
* <li>{@link TransferCallback}: {@link TransferCallback#DEFAULT}.
97+
* <li>{@link RemoteCastPlayer}: {@link RemoteCastPlayer new
98+
* RemoteCastPlayer.Builder(context).build()}.
99+
* <li>{@link #setLocalPlayer}: {@link ExoPlayer new ExoPlayer.Builder(context).build()}.
100+
* </ul>
101+
*
102+
* @param context A {@link Context}.
103+
*/
104+
public Builder(Context context) {
105+
this.context = checkNotNull(context);
106+
transferCallback = TransferCallback.DEFAULT;
107+
}
108+
109+
/**
110+
* Sets the {@link TransferCallback} to call when the active player changes.
111+
*
112+
* @param transferCallback A {@link TransferCallback}.
113+
* @return This builder.
114+
* @throws IllegalStateException If {@link #build()} has already been called on this builder
115+
* instance.
116+
*/
117+
@CanIgnoreReturnValue
118+
public Builder setTransferCallback(TransferCallback transferCallback) {
119+
Assertions.checkState(!buildCalled);
120+
this.transferCallback = checkNotNull(transferCallback);
121+
return this;
122+
}
123+
124+
/**
125+
* Sets the {@link Player} to use for local playback.
126+
*
127+
* @param localPlayer A {@link Player}.
128+
* @return This builder.
129+
* @throws IllegalStateException If {@link #build()} has already been called on this builder
130+
* instance.
131+
*/
132+
@CanIgnoreReturnValue
133+
public Builder setLocalPlayer(Player localPlayer) {
134+
Assertions.checkState(!buildCalled);
135+
this.localPlayer = checkNotNull(localPlayer);
136+
return this;
137+
}
138+
139+
/**
140+
* Sets the {@link RemoteCastPlayer} to use for remote playback.
141+
*
142+
* @param remotePlayer A {@link RemoteCastPlayer}.
143+
* @return This builder.
144+
* @throws IllegalStateException If {@link #build()} has already been called on this builder
145+
* instance.
146+
*/
147+
@CanIgnoreReturnValue
148+
public Builder setRemotePlayer(RemoteCastPlayer remotePlayer) {
149+
Assertions.checkState(!buildCalled);
150+
this.remotePlayer = checkNotNull(remotePlayer);
151+
return this;
152+
}
153+
154+
/**
155+
* Creates and returns the new {@link CastPlayerImpl} instance.
156+
*
157+
* @throws IllegalStateException If this method has already been called on this instance.
158+
*/
159+
public CastPlayer build() {
160+
Assertions.checkState(!buildCalled);
161+
buildCalled = true;
162+
if (localPlayer == null) {
163+
localPlayer = new ExoPlayer.Builder(context).build();
164+
}
165+
if (remotePlayer == null) {
166+
remotePlayer = new RemoteCastPlayer.Builder(context).build();
167+
}
168+
Player initialActivePlayer =
169+
remotePlayer.isCastSessionAvailable() ? remotePlayer : localPlayer;
170+
CastPlayerImpl castPlayerImpl =
171+
new CastPlayerImpl(localPlayer, remotePlayer, initialActivePlayer, transferCallback);
172+
return new CastPlayer(castPlayerImpl, remotePlayer);
173+
}
174+
}
175+
39176
/** Same as {@link RemoteCastPlayer#DEVICE_INFO_REMOTE_EMPTY}. */
40177
public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
41178
RemoteCastPlayer.DEVICE_INFO_REMOTE_EMPTY;
@@ -65,7 +202,11 @@ public CastPlayer(CastContext castContext) {
65202
*
66203
* @param castContext The context from which the cast session is obtained.
67204
* @param mediaItemConverter The {@link MediaItemConverter} to use.
205+
* @deprecated Use {@link RemoteCastPlayer.Builder} to create a {@link Player} for playback
206+
* exclusively on Cast receivers, or {@link Builder} for a {@link Player} that works both on
207+
* Cast receivers and locally.
68208
*/
209+
@Deprecated
69210
public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) {
70211
this(
71212
castContext,
@@ -83,7 +224,11 @@ public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter
83224
* @param seekForwardIncrementMs The {@link #seekForward()} increment, in milliseconds.
84225
* @throws IllegalArgumentException If {@code seekBackIncrementMs} or {@code
85226
* seekForwardIncrementMs} is non-positive.
227+
* @deprecated Use {@link RemoteCastPlayer.Builder} to create a {@link Player} for playback
228+
* exclusively on Cast receivers, or {@link Builder} for a {@link Player} that works both on
229+
* Cast receivers and locally.
86230
*/
231+
@Deprecated
87232
public CastPlayer(
88233
CastContext castContext,
89234
MediaItemConverter mediaItemConverter,
@@ -112,7 +257,11 @@ public CastPlayer(
112257
* @throws IllegalArgumentException If {@code seekBackIncrementMs} or {@code
113258
* seekForwardIncrementMs} is non-positive, or if {@code maxSeekToPreviousPositionMs} is
114259
* negative.
260+
* @deprecated Use {@link RemoteCastPlayer.Builder} to create a {@link Player} for playback
261+
* exclusively on Cast receivers, or {@link Builder} for a {@link Player} that works both on
262+
* Cast receivers and locally.
115263
*/
264+
@Deprecated
116265
public CastPlayer(
117266
@Nullable Context context,
118267
CastContext castContext,
@@ -135,7 +284,18 @@ private CastPlayer(RemoteCastPlayer remoteCastPlayer) {
135284
this.remoteCastPlayer = remoteCastPlayer;
136285
}
137286

138-
/** Returns whether a cast session is available. */
287+
private CastPlayer(CastPlayerImpl castPlayerImpl, RemoteCastPlayer remoteCastPlayer) {
288+
super(castPlayerImpl);
289+
this.remoteCastPlayer = remoteCastPlayer;
290+
}
291+
292+
/**
293+
* Returns whether a cast session is available.
294+
*
295+
* @deprecated Use {@link #getDeviceInfo()} instead, and check for {@link
296+
* DeviceInfo#PLAYBACK_TYPE_REMOTE}.
297+
*/
298+
@Deprecated
139299
public boolean isCastSessionAvailable() {
140300
return remoteCastPlayer.isCastSessionAvailable();
141301
}
@@ -144,7 +304,10 @@ public boolean isCastSessionAvailable() {
144304
* Sets a listener for updates on the cast session availability.
145305
*
146306
* @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
307+
* @deprecated Use {@link androidx.media3.common.Player.Listener#onDeviceInfoChanged} instead, and
308+
* check for {@link DeviceInfo#PLAYBACK_TYPE_REMOTE}.
147309
*/
310+
@Deprecated
148311
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
149312
remoteCastPlayer.setSessionAvailabilityListener(listener);
150313
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.cast;
17+
18+
import androidx.media3.common.ForwardingSimpleBasePlayer;
19+
import androidx.media3.common.Player;
20+
import com.google.common.util.concurrent.Futures;
21+
import com.google.common.util.concurrent.ListenableFuture;
22+
23+
/**
24+
* Contains the implementation details of {@link CastPlayer}, when created using {@link
25+
* CastPlayer.Builder}.
26+
*
27+
* <p>When created using the constructors, {@link CastPlayer} behaves as a thin wrapper of {@link
28+
* RemoteCastPlayer}, for API backwards compatibility. When created using {@link
29+
* CastPlayer.Builder}, {@link CastPlayer} will wrap this class instead, making it support both
30+
* local and remote playback, unlike {@link RemoteCastPlayer}, which supports only remote playback.
31+
*/
32+
/* package */ final class CastPlayerImpl extends ForwardingSimpleBasePlayer {
33+
34+
private final Player localPlayer;
35+
private final RemoteCastPlayer remotePlayer;
36+
private final CastPlayer.TransferCallback transferCallback;
37+
private final SessionAvailabilityListener sessionAvailabilityListener;
38+
39+
public CastPlayerImpl(
40+
Player localPlayer,
41+
RemoteCastPlayer remotePlayer,
42+
Player initialActivePlayer,
43+
CastPlayer.TransferCallback transferCallback) {
44+
super(initialActivePlayer);
45+
this.localPlayer = localPlayer;
46+
this.remotePlayer = remotePlayer;
47+
this.transferCallback = transferCallback;
48+
sessionAvailabilityListener = new SessionAvailabilityListenerImpl();
49+
remotePlayer.setInternalSessionAvailabilityListener(sessionAvailabilityListener);
50+
}
51+
52+
private void updateActivePlayer() {
53+
Player previousPlayer = getPlayer();
54+
Player newPlayer = remotePlayer.isCastSessionAvailable() ? remotePlayer : localPlayer;
55+
if (previousPlayer == newPlayer) {
56+
return;
57+
}
58+
transferCallback.transferState(previousPlayer, newPlayer);
59+
previousPlayer.stop();
60+
setPlayer(newPlayer);
61+
}
62+
63+
// SimpleBasePlayer implementation.
64+
65+
@Override
66+
protected State getState() {
67+
State currentState = super.getState();
68+
Commands availableCommands = currentState.availableCommands;
69+
if (!availableCommands.contains(COMMAND_RELEASE)) {
70+
// This player implementation supports release regardless of the forwarded Player, but we only
71+
// recreate the state when necessary. So as to avoid unnecessarily recreating the state object
72+
// every time.
73+
Commands newCommands = availableCommands.buildUpon().add(COMMAND_RELEASE).build();
74+
currentState = currentState.buildUpon().setAvailableCommands(newCommands).build();
75+
}
76+
return currentState;
77+
}
78+
79+
@Override
80+
protected ListenableFuture<?> handleRelease() {
81+
remotePlayer.release();
82+
remotePlayer.setInternalSessionAvailabilityListener(null);
83+
if (localPlayer.isCommandAvailable(COMMAND_RELEASE)) {
84+
localPlayer.release();
85+
}
86+
return Futures.immediateVoidFuture();
87+
}
88+
89+
// Internal classes.
90+
91+
private class SessionAvailabilityListenerImpl implements SessionAvailabilityListener {
92+
93+
@Override
94+
public void onCastSessionAvailable() {
95+
updateActivePlayer();
96+
}
97+
98+
@Override
99+
public void onCastSessionUnavailable() {
100+
updateActivePlayer();
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)