diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java
index 5ad4cf90ad..de3005b2bb 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Player.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Player.java
@@ -677,7 +677,8 @@ public interface Player {
* to the current {@link #getRepeatMode() repeat mode}.
*
*
Note that this callback is also called when the playlist becomes non-empty or empty as a
- * consequence of a playlist change.
+ * consequence of a playlist change or {@linkplain #onAvailableCommandsChanged(Commands) a
+ * change in available commands}.
*
*
{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
diff --git a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl
index d1a348cd3a..7c1eb001d2 100644
--- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl
+++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl
@@ -35,14 +35,19 @@ oneway interface IMediaController {
void onSetCustomLayout(int seq, in List commandButtonList) = 3003;
void onCustomCommand(int seq, in Bundle command, in Bundle args) = 3004;
void onDisconnected(int seq) = 3005;
- void onPlayerInfoChanged(int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006;
+ /** Deprecated: Use onPlayerInfoChangedWithExclusions from MediaControllerStub#VERSION_INT=2. */
+ void onPlayerInfoChanged(
+ int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006;
+ /** Introduced to deprecate onPlayerInfoChanged (from MediaControllerStub#VERSION_INT=2). */
+ void onPlayerInfoChangedWithExclusions(
+ int seq, in Bundle playerInfoBundle, in Bundle playerInfoExclusions) = 3012;
void onPeriodicSessionPositionInfoChanged(int seq, in Bundle sessionPositionInfo) = 3007;
void onAvailableCommandsChangedFromPlayer(int seq, in Bundle commandsBundle) = 3008;
void onAvailableCommandsChangedFromSession(
int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009;
void onRenderedFirstFrame(int seq) = 3010;
void onExtrasChanged(int seq, in Bundle extras) = 3011;
- // Next Id for MediaController: 3012
+ // Next Id for MediaController: 3013
void onChildrenChanged(
int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000;
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
index 4f7940b30a..6560cea856 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
@@ -23,6 +23,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.usToMs;
import static androidx.media3.session.MediaUtils.calculateBufferedPercentage;
import static androidx.media3.session.MediaUtils.intersect;
+import static androidx.media3.session.MediaUtils.mergePlayerInfo;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -42,6 +43,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.MediaBrowserCompat;
+import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -79,6 +81,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaController.MediaControllerImpl;
+import androidx.media3.session.PlayerInfo.BundlingExclusions;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -129,7 +132,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
@Nullable private IMediaSession iSession;
private long lastReturnedCurrentPositionMs;
private long lastSetPlayWhenReadyCalledTimeMs;
- @Nullable private Timeline pendingPlayerInfoUpdateTimeline;
+ @Nullable private PlayerInfo pendingPlayerInfo;
+ @Nullable private BundlingExclusions pendingBundlingExclusions;
public MediaControllerImplBase(
Context context,
@@ -2329,30 +2333,41 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
- void onPlayerInfoChanged(PlayerInfo newPlayerInfo, boolean isTimelineExcluded) {
+ void onPlayerInfoChanged(PlayerInfo newPlayerInfo, BundlingExclusions bundlingExclusions) {
if (!isConnected()) {
return;
}
+ if (pendingPlayerInfo != null && pendingBundlingExclusions != null) {
+ Pair mergedPlayerInfoUpdate =
+ mergePlayerInfo(
+ pendingPlayerInfo,
+ pendingBundlingExclusions,
+ newPlayerInfo,
+ bundlingExclusions,
+ intersectedPlayerCommands);
+ newPlayerInfo = mergedPlayerInfoUpdate.first;
+ bundlingExclusions = mergedPlayerInfoUpdate.second;
+ }
+ pendingPlayerInfo = null;
+ pendingBundlingExclusions = null;
if (!pendingMaskingSequencedFutureNumbers.isEmpty()) {
// We are still waiting for all pending masking operations to be handled.
- if (!isTimelineExcluded) {
- pendingPlayerInfoUpdateTimeline = newPlayerInfo.timeline;
- }
+ pendingPlayerInfo = newPlayerInfo;
+ pendingBundlingExclusions = bundlingExclusions;
return;
}
PlayerInfo oldPlayerInfo = playerInfo;
- if (isTimelineExcluded) {
- newPlayerInfo =
- newPlayerInfo.copyWithTimeline(
- pendingPlayerInfoUpdateTimeline != null
- ? pendingPlayerInfoUpdateTimeline
- : oldPlayerInfo.timeline);
- }
// Assigning class variable now so that all getters called from listeners see the updated value.
// But we need to use a local final variable to ensure listeners get consistent parameters.
- playerInfo = newPlayerInfo;
- PlayerInfo finalPlayerInfo = newPlayerInfo;
- pendingPlayerInfoUpdateTimeline = null;
+ playerInfo =
+ mergePlayerInfo(
+ oldPlayerInfo,
+ /* oldBundlingExclusions= */ BundlingExclusions.NONE,
+ newPlayerInfo,
+ /* newBundlingExclusions= */ bundlingExclusions,
+ intersectedPlayerCommands)
+ .first;
+ PlayerInfo finalPlayerInfo = playerInfo;
PlaybackException oldPlayerError = oldPlayerInfo.playerError;
PlaybackException playerError = finalPlayerInfo.playerError;
boolean errorsMatch =
@@ -2397,7 +2412,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
/* eventFlag= */ Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(finalPlayerInfo.shuffleModeEnabled));
}
- if (!isTimelineExcluded && !Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) {
+ if (!Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) {
listeners.queueEvent(
/* eventFlag= */ Player.EVENT_TIMELINE_CHANGED,
listener ->
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
index 59b49bdda5..f9673ccf05 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
@@ -26,6 +26,7 @@ import androidx.media3.common.Player.Commands;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.session.MediaLibraryService.LibraryParams;
+import androidx.media3.session.PlayerInfo.BundlingExclusions;
import java.lang.ref.WeakReference;
import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -35,7 +36,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
private static final String TAG = "MediaControllerStub";
/** The version of the IMediaController interface. */
- public static final int VERSION_INT = 1;
+ public static final int VERSION_INT = 2;
private final WeakReference controller;
@@ -169,8 +170,23 @@ import org.checkerframework.checker.nullness.qual.NonNull;
controller -> controller.notifyPeriodicSessionPositionInfoChanged(sessionPositionInfo));
}
+ /**
+ * @deprecated Use {@link #onPlayerInfoChangedWithExclusions} from {@link #VERSION_INT} 2.
+ */
@Override
+ @Deprecated
public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTimelineExcluded) {
+ onPlayerInfoChangedWithExclusions(
+ seq,
+ playerInfoBundle,
+ new BundlingExclusions(isTimelineExcluded, /* areCurrentTracksExcluded= */ true)
+ .toBundle());
+ }
+
+ /** Added in {@link #VERSION_INT} 2. */
+ @Override
+ public void onPlayerInfoChangedWithExclusions(
+ int seq, Bundle playerInfoBundle, Bundle playerInfoExclusions) {
PlayerInfo playerInfo;
try {
playerInfo = PlayerInfo.CREATOR.fromBundle(playerInfoBundle);
@@ -178,8 +194,15 @@ import org.checkerframework.checker.nullness.qual.NonNull;
Log.w(TAG, "Ignoring malformed Bundle for PlayerInfo", e);
return;
}
+ BundlingExclusions bundlingExclusions;
+ try {
+ bundlingExclusions = BundlingExclusions.CREATOR.fromBundle(playerInfoExclusions);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for BundlingExclusions", e);
+ return;
+ }
dispatchControllerTaskOnHandler(
- controller -> controller.onPlayerInfoChanged(playerInfo, isTimelineExcluded));
+ controller -> controller.onPlayerInfoChanged(playerInfo, bundlingExclusions));
}
@Override
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
index 9da5c30fe5..6b25c8d56c 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
@@ -1136,7 +1136,8 @@ public class MediaSession {
boolean excludeMediaItemsMetadata,
boolean excludeCues,
boolean excludeTimeline,
- boolean excludeTracks)
+ boolean excludeTracks,
+ int controllerInterfaceVersion)
throws RemoteException {}
default void onPeriodicSessionPositionInfoChanged(
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
index 705115745f..d01fb6eee3 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
@@ -15,6 +15,10 @@
*/
package androidx.media3.session;
+import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
+import static androidx.media3.common.Player.COMMAND_GET_TEXT;
+import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
+import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
@@ -274,7 +278,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
playerInfo = newPlayerWrapper.createPlayerInfoForBundling();
- onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
+ onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ false, /* excludeTracks= */ false);
}
public void release() {
@@ -374,7 +379,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
controller,
(callback, seq) ->
callback.onAvailableCommandsChangedFromSession(seq, sessionCommands, playerCommands));
- onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
+ onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ false, /* excludeTracks= */ false);
} else {
sessionLegacyStub
.getConnectedControllersManager()
@@ -387,7 +393,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
(controller, seq) -> controller.sendCustomCommand(seq, command, args));
}
- private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeTimeline) {
+ private void dispatchOnPlayerInfoChanged(
+ PlayerInfo playerInfo, boolean excludeTimeline, boolean excludeTracks) {
List controllers =
sessionStub.getConnectedControllersManager().getConnectedControllers();
@@ -395,8 +402,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
ControllerInfo controller = controllers.get(i);
try {
int seq;
- SequencedFutureManager manager =
- sessionStub.getConnectedControllersManager().getSequencedFutureManager(controller);
+ ConnectedControllersManager controllersManager =
+ sessionStub.getConnectedControllersManager();
+ SequencedFutureManager manager = controllersManager.getSequencedFutureManager(controller);
if (manager != null) {
seq = manager.obtainNextSequenceNumber();
} else {
@@ -410,19 +418,18 @@ import org.checkerframework.checker.initialization.qual.Initialized;
.onPlayerInfoChanged(
seq,
playerInfo,
- /* excludeMediaItems= */ !sessionStub
- .getConnectedControllersManager()
- .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TIMELINE),
- /* excludeMediaItemsMetadata= */ !sessionStub
- .getConnectedControllersManager()
- .isPlayerCommandAvailable(controller, Player.COMMAND_GET_MEDIA_ITEMS_METADATA),
- /* excludeCues= */ !sessionStub
- .getConnectedControllersManager()
- .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TEXT),
- excludeTimeline,
- /* excludeTracks= */ !sessionStub
- .getConnectedControllersManager()
- .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TRACKS));
+ /* excludeMediaItems= */ !controllersManager.isPlayerCommandAvailable(
+ controller, COMMAND_GET_TIMELINE),
+ /* excludeMediaItemsMetadata= */ !controllersManager.isPlayerCommandAvailable(
+ controller, COMMAND_GET_MEDIA_ITEMS_METADATA),
+ /* excludeCues= */ !controllersManager.isPlayerCommandAvailable(
+ controller, COMMAND_GET_TEXT),
+ excludeTimeline
+ || !controllersManager.isPlayerCommandAvailable(
+ controller, COMMAND_GET_TIMELINE),
+ excludeTracks
+ || !controllersManager.isPlayerCommandAvailable(controller, COMMAND_GET_TRACKS),
+ controller.getInterfaceVersion());
} catch (DeadObjectException e) {
onDeadObjectException(controller);
} catch (RemoteException e) {
@@ -745,7 +752,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithPlayerError(error);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onPlayerError(seq, error));
}
@@ -765,7 +773,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
// Note: OK to omit mediaItem here, because PlayerInfo changed message will copy playerInfo
// with sessionPositionInfo, which includes current window index.
session.playerInfo = session.playerInfo.copyWithMediaItemTransitionReason(reason);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onMediaItemTransition(seq, mediaItem, reason));
}
@@ -785,7 +794,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
session.playerInfo =
session.playerInfo.copyWithPlayWhenReady(
playWhenReady, reason, session.playerInfo.playbackSuppressionReason);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onPlayWhenReadyChanged(seq, playWhenReady, reason));
}
@@ -806,7 +816,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
session.playerInfo.playWhenReady,
session.playerInfo.playWhenReadyChangedReason,
reason);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onPlaybackSuppressionReasonChanged(seq, reason));
}
@@ -824,7 +835,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
session.playerInfo =
session.playerInfo.copyWithPlaybackState(playbackState, player.getPlayerError());
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> {
callback.onPlaybackStateChanged(seq, playbackState, player.getPlayerError());
@@ -843,7 +855,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithIsPlaying(isPlaying);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onIsPlayingChanged(seq, isPlaying));
}
@@ -860,7 +873,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithIsLoading(isLoading);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onIsLoadingChanged(seq, isLoading));
}
@@ -880,7 +894,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
session.playerInfo =
session.playerInfo.copyWithPositionInfos(oldPosition, newPosition, reason);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) ->
callback.onPositionDiscontinuity(seq, oldPosition, newPosition, reason));
@@ -898,7 +913,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithPlaybackParameters(playbackParameters);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onPlaybackParametersChanged(seq, playbackParameters));
}
@@ -915,7 +931,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithSeekBackIncrement(seekBackIncrementMs);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onSeekBackIncrementChanged(seq, seekBackIncrementMs));
}
@@ -932,7 +949,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithSeekForwardIncrement(seekForwardIncrementMs);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onSeekForwardIncrementChanged(seq, seekForwardIncrementMs));
}
@@ -951,7 +969,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
session.playerInfo =
session.playerInfo.copyWithTimelineAndSessionPositionInfo(
timeline, player.createSessionPositionInfoForBundling());
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ false, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onTimelineChanged(seq, timeline, reason));
}
@@ -964,7 +983,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
session.verifyApplicationThread();
session.playerInfo = session.playerInfo.copyWithPlaylistMetadata(playlistMetadata);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onPlaylistMetadataChanged(seq, playlistMetadata));
}
@@ -981,7 +1001,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithRepeatMode(repeatMode);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onRepeatModeChanged(seq, repeatMode));
}
@@ -998,7 +1019,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onShuffleModeEnabledChanged(seq, shuffleModeEnabled));
}
@@ -1015,7 +1037,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithAudioAttributes(attributes);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(controller, seq) -> controller.onAudioAttributesChanged(seq, attributes));
}
@@ -1028,7 +1051,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
session.verifyApplicationThread();
session.playerInfo = session.playerInfo.copyWithVideoSize(size);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onVideoSizeChanged(seq, size));
}
@@ -1041,7 +1065,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
session.verifyApplicationThread();
session.playerInfo = session.playerInfo.copyWithVolume(volume);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onVolumeChanged(seq, volume));
}
@@ -1058,7 +1083,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = new PlayerInfo.Builder(session.playerInfo).setCues(cueGroup).build();
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
}
@Override
@@ -1073,7 +1099,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithDeviceInfo(deviceInfo);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onDeviceInfoChanged(seq, deviceInfo));
}
@@ -1090,7 +1117,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithDeviceVolume(volume, muted);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onDeviceVolumeChanged(seq, volume, muted));
}
@@ -1106,7 +1134,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (player == null) {
return;
}
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
+ boolean excludeTracks = !availableCommands.contains(COMMAND_GET_TRACKS);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ false, excludeTracks);
session.dispatchRemoteControllerTaskWithoutReturn(
(callback, seq) -> callback.onAvailableCommandsChangedFromPlayer(seq, availableCommands));
@@ -1128,7 +1158,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithCurrentTracks(tracks);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ false);
session.dispatchRemoteControllerTaskWithoutReturn(
(callback, seq) -> callback.onTracksChanged(seq, tracks));
}
@@ -1145,7 +1176,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithTrackSelectionParameters(parameters);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskWithoutReturn(
(callback, seq) -> callback.onTrackSelectionParametersChanged(seq, parameters));
}
@@ -1162,7 +1194,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return;
}
session.playerInfo = session.playerInfo.copyWithMediaMetadata(mediaMetadata);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onMediaMetadataChanged(seq, mediaMetadata));
}
@@ -1190,7 +1223,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
session.playerInfo =
session.playerInfo.copyWithMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs);
- session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true);
+ session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
+ /* excludeTimeline= */ true, /* excludeTracks= */ true);
}
@Nullable
@@ -1224,10 +1258,12 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private static final int MSG_PLAYER_INFO_CHANGED = 1;
private boolean excludeTimeline;
+ private boolean excludeTracks;
public PlayerInfoChangedHandler(Looper looper) {
super(looper);
excludeTimeline = true;
+ excludeTracks = true;
}
@Override
@@ -1237,15 +1273,17 @@ import org.checkerframework.checker.initialization.qual.Initialized;
playerInfo.copyWithTimelineAndSessionPositionInfo(
getPlayerWrapper().getCurrentTimeline(),
getPlayerWrapper().createSessionPositionInfoForBundling());
- dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline);
+ dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline, excludeTracks);
excludeTimeline = true;
+ excludeTracks = true;
} else {
throw new IllegalStateException("Invalid message what=" + msg.what);
}
}
- public void sendPlayerInfoChangedMessage(boolean excludeTimeline) {
+ public void sendPlayerInfoChangedMessage(boolean excludeTimeline, boolean excludeTracks) {
this.excludeTimeline = this.excludeTimeline && excludeTimeline;
+ this.excludeTracks = this.excludeTracks && excludeTracks;
if (!onPlayerInfoChangedHandler.hasMessages(MSG_PLAYER_INFO_CHANGED)) {
onPlayerInfoChangedHandler.sendEmptyMessage(MSG_PLAYER_INFO_CHANGED);
}
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
index 7160d1e176..b13b4d61fb 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
@@ -70,6 +70,7 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Rating;
import androidx.media3.common.TrackSelectionParameters;
+import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.Log;
@@ -1596,17 +1597,32 @@ import java.util.concurrent.ExecutionException;
boolean excludeMediaItemsMetadata,
boolean excludeCues,
boolean excludeTimeline,
- boolean excludeTracks)
+ boolean excludeTracks,
+ int controllerInterfaceVersion)
throws RemoteException {
- iController.onPlayerInfoChanged(
- sequenceNumber,
- playerInfo.toBundle(
- excludeMediaItems,
- excludeMediaItemsMetadata,
- excludeCues,
- excludeTimeline,
- excludeTracks),
- /* isTimelineExcluded= */ excludeTimeline);
+ Assertions.checkState(controllerInterfaceVersion != 0);
+ if (controllerInterfaceVersion >= 2) {
+ iController.onPlayerInfoChangedWithExclusions(
+ sequenceNumber,
+ playerInfo.toBundle(
+ excludeMediaItems,
+ excludeMediaItemsMetadata,
+ excludeCues,
+ excludeTimeline,
+ excludeTracks),
+ new PlayerInfo.BundlingExclusions(excludeTimeline, excludeTracks).toBundle());
+ } else {
+ //noinspection deprecation
+ iController.onPlayerInfoChanged(
+ sequenceNumber,
+ playerInfo.toBundle(
+ excludeMediaItems,
+ excludeMediaItemsMetadata,
+ excludeCues,
+ excludeTimeline,
+ /* excludeTracks= */ true),
+ excludeTimeline);
+ }
}
@Override
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
index 64a25e0eb9..f58882351d 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java
@@ -62,6 +62,7 @@ import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
import android.text.TextUtils;
+import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media.AudioAttributesCompat;
import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
@@ -87,6 +88,7 @@ import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
+import androidx.media3.session.PlayerInfo.BundlingExclusions;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -1288,6 +1290,46 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return intersectCommandsBuilder.build();
}
+ /**
+ * Merges the excluded fields into the {@code newPlayerInfo} by taking the values of the {@code
+ * previousPlayerInfo} and taking into account the passed available commands.
+ *
+ * @param oldPlayerInfo The old {@link PlayerInfo}.
+ * @param oldBundlingExclusions The bundling exlusions in the old {@link PlayerInfo}.
+ * @param newPlayerInfo The new {@link PlayerInfo}.
+ * @param newBundlingExclusions The bundling exlusions in the new {@link PlayerInfo}.
+ * @param availablePlayerCommands The available commands to take into account when merging.
+ * @return A pair with the resulting {@link PlayerInfo} and {@link BundlingExclusions}.
+ */
+ public static Pair mergePlayerInfo(
+ PlayerInfo oldPlayerInfo,
+ BundlingExclusions oldBundlingExclusions,
+ PlayerInfo newPlayerInfo,
+ BundlingExclusions newBundlingExclusions,
+ Commands availablePlayerCommands) {
+ PlayerInfo mergedPlayerInfo = newPlayerInfo;
+ BundlingExclusions mergedBundlingExclusions = newBundlingExclusions;
+ if (newBundlingExclusions.isTimelineExcluded
+ && availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE)
+ && !oldBundlingExclusions.isTimelineExcluded) {
+ // Use the previous timeline if it is excluded in the most recent update.
+ mergedPlayerInfo = mergedPlayerInfo.copyWithTimeline(oldPlayerInfo.timeline);
+ mergedBundlingExclusions =
+ new BundlingExclusions(
+ /* isTimelineExcluded= */ false, mergedBundlingExclusions.areCurrentTracksExcluded);
+ }
+ if (newBundlingExclusions.areCurrentTracksExcluded
+ && availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS)
+ && !oldBundlingExclusions.areCurrentTracksExcluded) {
+ // Use the previous tracks if it is excluded in the most recent update.
+ mergedPlayerInfo = mergedPlayerInfo.copyWithCurrentTracks(oldPlayerInfo.currentTracks);
+ mergedBundlingExclusions =
+ new BundlingExclusions(
+ mergedBundlingExclusions.isTimelineExcluded, /* areCurrentTracksExcluded= */ false);
+ }
+ return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions);
+ }
+
private static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);
diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
index 0ea0a266b4..a4a94969d9 100644
--- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
+++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
@@ -66,6 +66,10 @@ import java.lang.annotation.Target;
*/
public static class BundlingExclusions implements Bundleable {
+ /** Bundling exclusions with no exclusions. */
+ public static final BundlingExclusions NONE =
+ new BundlingExclusions(
+ /* isTimelineExcluded= */ false, /* areCurrentTracksExcluded= */ false);
/** Whether the {@linkplain PlayerInfo#timeline timeline} is excluded. */
public final boolean isTimelineExcluded;
/** Whether the {@linkplain PlayerInfo#currentTracks current tracks} are excluded. */
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
index 8cb138e0f9..13f7d64d4e 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
@@ -1052,8 +1052,8 @@ public class MediaControllerListenerTest {
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
AtomicReference changedCurrentTracksFromParamRef = new AtomicReference<>();
AtomicReference changedCurrentTracksFromGetterRef = new AtomicReference<>();
- AtomicReference changedCurrentTracksFromOnEventsRef = new AtomicReference<>();
- AtomicReference eventsRef = new AtomicReference<>();
+ List changedCurrentTracksFromOnEvents = new ArrayList<>();
+ List capturedEvents = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(2);
Player.Listener listener =
new Player.Listener() {
@@ -1061,13 +1061,12 @@ public class MediaControllerListenerTest {
public void onTracksChanged(Tracks currentTracks) {
changedCurrentTracksFromParamRef.set(currentTracks);
changedCurrentTracksFromGetterRef.set(controller.getCurrentTracks());
- latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
- eventsRef.set(events);
- changedCurrentTracksFromOnEventsRef.set(player.getCurrentTracks());
+ capturedEvents.add(events);
+ changedCurrentTracksFromOnEvents.add(player.getCurrentTracks());
latch.countDown();
}
};
@@ -1081,13 +1080,22 @@ public class MediaControllerListenerTest {
});
player.notifyTracksChanged(currentTracks);
+ player.notifyIsLoadingChanged(true);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(initialCurrentTracksRef.get()).isEqualTo(Tracks.EMPTY);
assertThat(changedCurrentTracksFromParamRef.get()).isEqualTo(currentTracks);
assertThat(changedCurrentTracksFromGetterRef.get()).isEqualTo(currentTracks);
- assertThat(changedCurrentTracksFromOnEventsRef.get()).isEqualTo(currentTracks);
- assertThat(getEventsAsList(eventsRef.get())).containsExactly(Player.EVENT_TRACKS_CHANGED);
+ assertThat(capturedEvents).hasSize(2);
+ assertThat(getEventsAsList(capturedEvents.get(0))).containsExactly(Player.EVENT_TRACKS_CHANGED);
+ assertThat(getEventsAsList(capturedEvents.get(1)))
+ .containsExactly(Player.EVENT_IS_LOADING_CHANGED);
+ assertThat(changedCurrentTracksFromOnEvents).hasSize(2);
+ assertThat(changedCurrentTracksFromOnEvents.get(0)).isEqualTo(currentTracks);
+ assertThat(changedCurrentTracksFromOnEvents.get(1)).isEqualTo(currentTracks);
+ // Assert that an equal instance is not re-sent over the binder.
+ assertThat(changedCurrentTracksFromOnEvents.get(0))
+ .isSameInstanceAs(changedCurrentTracksFromOnEvents.get(1));
}
@Test
@@ -1142,6 +1150,9 @@ public class MediaControllerListenerTest {
assertThat(capturedCurrentTracks).containsExactly(Tracks.EMPTY);
assertThat(initialCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1);
assertThat(capturedCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1);
+ // Assert that an equal instance is not re-sent over the binder.
+ assertThat(initialCurrentTracksWithCommandAvailable.get())
+ .isSameInstanceAs(capturedCurrentTracksWithCommandAvailable.get());
}
@Test
@@ -1181,6 +1192,7 @@ public class MediaControllerListenerTest {
availableCommands.get().buildUpon().remove(Player.COMMAND_GET_TRACKS).build());
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
+ assertThat(capturedCurrentTracks).hasSize(2);
assertThat(capturedCurrentTracks.get(0).getGroups()).hasSize(1);
assertThat(capturedCurrentTracks.get(1)).isEqualTo(Tracks.EMPTY);
}
@@ -2203,7 +2215,7 @@ public class MediaControllerListenerTest {
}
@Test
- public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromPlayer()
+ public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAndMetadata()
throws Exception {
int testMediaItemsSize = 2;
List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize);
@@ -2217,7 +2229,7 @@ public class MediaControllerListenerTest {
AtomicReference timelineFromGetterRef = new AtomicReference<>();
List onEventsTimelines = new ArrayList<>();
AtomicReference metadataFromGetterRef = new AtomicReference<>();
- AtomicReference currentMediaItemGetterRef = new AtomicReference<>();
+ AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>();
List eventsList = new ArrayList<>();
Player.Listener listener =
new Player.Listener() {
@@ -2226,7 +2238,7 @@ public class MediaControllerListenerTest {
timelineFromParamRef.set(timeline);
timelineFromGetterRef.set(controller.getCurrentTimeline());
metadataFromGetterRef.set(controller.getMediaMetadata());
- currentMediaItemGetterRef.set(controller.getCurrentMediaItem());
+ isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null);
latch.countDown();
}
@@ -2244,24 +2256,7 @@ public class MediaControllerListenerTest {
remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithoutGetTimeline);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
- assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize);
- for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) {
- assertThat(
- timelineFromParamRef
- .get()
- .getWindow(/* windowIndex= */ i, new Timeline.Window())
- .mediaItem)
- .isEqualTo(MediaItem.EMPTY);
- }
- assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize);
- for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) {
- assertThat(
- timelineFromGetterRef
- .get()
- .getWindow(/* windowIndex= */ i, new Timeline.Window())
- .mediaItem)
- .isEqualTo(MediaItem.EMPTY);
- }
+ assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY);
assertThat(onEventsTimelines).hasSize(2);
for (int i = 0; i < onEventsTimelines.get(1).getWindowCount(); i++) {
assertThat(
@@ -2272,15 +2267,16 @@ public class MediaControllerListenerTest {
.isEqualTo(MediaItem.EMPTY);
}
assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY);
- assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY);
+ assertThat(isCurrentMediaItemNullRef.get()).isTrue();
assertThat(eventsList).hasSize(2);
assertThat(getEventsAsList(eventsList.get(0)))
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
- assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED);
+ assertThat(getEventsAsList(eventsList.get(1)))
+ .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION);
}
@Test
- public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromSession()
+ public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAndMetadata()
throws Exception {
int testMediaItemsSize = 2;
List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize);
@@ -2293,7 +2289,7 @@ public class MediaControllerListenerTest {
AtomicReference timelineFromParamRef = new AtomicReference<>();
AtomicReference timelineFromGetterRef = new AtomicReference<>();
AtomicReference metadataFromGetterRef = new AtomicReference<>();
- AtomicReference currentMediaItemGetterRef = new AtomicReference<>();
+ AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>();
List eventsList = new ArrayList<>();
Player.Listener listener =
new Player.Listener() {
@@ -2302,7 +2298,7 @@ public class MediaControllerListenerTest {
timelineFromParamRef.set(timeline);
timelineFromGetterRef.set(controller.getCurrentTimeline());
metadataFromGetterRef.set(controller.getMediaMetadata());
- currentMediaItemGetterRef.set(controller.getCurrentMediaItem());
+ isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null);
latch.countDown();
}
@@ -2319,30 +2315,14 @@ public class MediaControllerListenerTest {
remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithoutGetTimeline);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
- assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize);
- for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) {
- assertThat(
- timelineFromParamRef
- .get()
- .getWindow(/* windowIndex= */ i, new Timeline.Window())
- .mediaItem)
- .isEqualTo(MediaItem.EMPTY);
- }
- assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize);
- for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) {
- assertThat(
- timelineFromGetterRef
- .get()
- .getWindow(/* windowIndex= */ i, new Timeline.Window())
- .mediaItem)
- .isEqualTo(MediaItem.EMPTY);
- }
+ assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY);
assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY);
- assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY);
+ assertThat(isCurrentMediaItemNullRef.get()).isTrue();
assertThat(eventsList).hasSize(2);
assertThat(getEventsAsList(eventsList.get(0)))
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
- assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED);
+ assertThat(getEventsAsList(eventsList.get(1)))
+ .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION);
}
/** This also tests {@link MediaController#getAvailableCommands()}. */
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java
index 80dd8073b4..83c5a4e3f8 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java
@@ -20,6 +20,9 @@ import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABL
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS;
+import static androidx.media3.common.MimeTypes.AUDIO_AAC;
+import static androidx.media3.common.MimeTypes.VIDEO_H264;
+import static androidx.media3.common.MimeTypes.VIDEO_H265;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -36,11 +39,13 @@ import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media.AudioAttributesCompat;
import androidx.media.utils.MediaConstants;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
+import androidx.media3.common.Format;
import androidx.media3.common.HeartRating;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
@@ -49,10 +54,15 @@ import androidx.media3.common.Player;
import androidx.media3.common.Rating;
import androidx.media3.common.StarRating;
import androidx.media3.common.ThumbRating;
+import androidx.media3.common.Timeline;
+import androidx.media3.common.TrackGroup;
+import androidx.media3.common.Tracks;
+import androidx.media3.session.PlayerInfo.BundlingExclusions;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
@@ -623,4 +633,134 @@ public final class MediaUtilsTest {
state, /* metadataCompat= */ null, /* timeDiffMs= */ C.INDEX_UNSET);
assertThat(totalBufferedDurationMs).isEqualTo(testTotalBufferedDurationMs);
}
+
+ @Test
+ public void mergePlayerInfo_timelineAndTracksExcluded_correctMerge() {
+ Timeline timeline =
+ new Timeline.RemotableTimeline(
+ ImmutableList.of(new Timeline.Window()),
+ ImmutableList.of(new Timeline.Period()),
+ /* shuffledWindowIndices= */ new int[] {0});
+ Tracks tracks =
+ new Tracks(
+ ImmutableList.of(
+ new Tracks.Group(
+ new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
+ /* adaptiveSupported= */ false,
+ new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
+ /* trackSelected= */ new boolean[] {true}),
+ new Tracks.Group(
+ new TrackGroup(
+ new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
+ new Format.Builder().setSampleMimeType(VIDEO_H265).build()),
+ /* adaptiveSupported= */ true,
+ new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE},
+ /* trackSelected= */ new boolean[] {false, true})));
+ PlayerInfo oldPlayerInfo =
+ PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline);
+ PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT;
+ Player.Commands availableCommands =
+ Player.Commands.EMPTY
+ .buildUpon()
+ .add(Player.COMMAND_GET_TIMELINE)
+ .add(Player.COMMAND_GET_TRACKS)
+ .build();
+
+ Pair mergeResult =
+ MediaUtils.mergePlayerInfo(
+ oldPlayerInfo,
+ BundlingExclusions.NONE,
+ newPlayerInfo,
+ new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true),
+ availableCommands);
+
+ assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline);
+ assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks);
+ assertThat(mergeResult.second.isTimelineExcluded).isFalse();
+ assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse();
+ }
+
+ @Test
+ public void mergePlayerInfo_getTimelineCommandNotAvailable_emptyTimeline() {
+ Timeline timeline =
+ new Timeline.RemotableTimeline(
+ ImmutableList.of(new Timeline.Window()),
+ ImmutableList.of(new Timeline.Period()),
+ /* shuffledWindowIndices= */ new int[] {0});
+ Tracks tracks =
+ new Tracks(
+ ImmutableList.of(
+ new Tracks.Group(
+ new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
+ /* adaptiveSupported= */ false,
+ new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
+ /* trackSelected= */ new boolean[] {true}),
+ new Tracks.Group(
+ new TrackGroup(
+ new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
+ new Format.Builder().setSampleMimeType(VIDEO_H265).build()),
+ /* adaptiveSupported= */ true,
+ new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE},
+ /* trackSelected= */ new boolean[] {false, true})));
+ PlayerInfo oldPlayerInfo =
+ PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline);
+ PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT;
+ Player.Commands availableCommands =
+ Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TRACKS).build();
+
+ Pair mergeResult =
+ MediaUtils.mergePlayerInfo(
+ oldPlayerInfo,
+ BundlingExclusions.NONE,
+ newPlayerInfo,
+ new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true),
+ availableCommands);
+
+ assertThat(mergeResult.first.timeline).isSameInstanceAs(Timeline.EMPTY);
+ assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks);
+ assertThat(mergeResult.second.isTimelineExcluded).isTrue();
+ assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse();
+ }
+
+ @Test
+ public void mergePlayerInfo_getTracksCommandNotAvailable_emptyTracks() {
+ Timeline timeline =
+ new Timeline.RemotableTimeline(
+ ImmutableList.of(new Timeline.Window()),
+ ImmutableList.of(new Timeline.Period()),
+ /* shuffledWindowIndices= */ new int[] {0});
+ Tracks tracks =
+ new Tracks(
+ ImmutableList.of(
+ new Tracks.Group(
+ new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
+ /* adaptiveSupported= */ false,
+ new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
+ /* trackSelected= */ new boolean[] {true}),
+ new Tracks.Group(
+ new TrackGroup(
+ new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
+ new Format.Builder().setSampleMimeType(VIDEO_H265).build()),
+ /* adaptiveSupported= */ true,
+ new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE},
+ /* trackSelected= */ new boolean[] {false, true})));
+ PlayerInfo oldPlayerInfo =
+ PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline);
+ PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT;
+ Player.Commands availableCommands =
+ Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TIMELINE).build();
+
+ Pair mergeResult =
+ MediaUtils.mergePlayerInfo(
+ oldPlayerInfo,
+ BundlingExclusions.NONE,
+ newPlayerInfo,
+ new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true),
+ availableCommands);
+
+ assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline);
+ assertThat(mergeResult.first.currentTracks).isSameInstanceAs(Tracks.EMPTY);
+ assertThat(mergeResult.second.isTimelineExcluded).isFalse();
+ assertThat(mergeResult.second.areCurrentTracksExcluded).isTrue();
+ }
}