Add MediaSession.getControllerForCurrentRequest

This is a helper method that can used to obtain information about
the controller that is currently calling a Player method.

PiperOrigin-RevId: 527268994
This commit is contained in:
tonihei 2023-04-26 16:09:44 +01:00 committed by Ian Baker
parent fab134f0b3
commit 3693ca4bbb
8 changed files with 460 additions and 58 deletions

View File

@ -53,11 +53,17 @@
* Fix issue where `MediaController` doesn't update its available commands
when connected to a legacy `MediaSessionCompat` that updates its
actions.
* Add helper method `MediaSession.getControllerForCurrentRequest` to
obtain information about the controller that is currently calling
a`Player` method.
* UI:
* Add Util methods `shouldShowPlayButton` and
`handlePlayPauseButtonAction` to write custom UI elements with a
play/pause button.
* Audio:
* Fix bug where some playbacks fail when tunneling is enabled and
`AudioProcessors` are active, e.g. for gapless trimming
([#10847](https://github.com/google/ExoPlayer/issues/10847)).
@ -76,10 +82,14 @@
`onRendererCapabilitiesChanged` events.
* Add `ChannelMixingAudioProcessor` for applying scaling/mixing to audio
channels.
* Metadata:
* Deprecate `MediaMetadata.folderType` in favor of `isBrowsable` and
`mediaType`.
* DRM:
* Reduce the visibility of several internal-only methods on
`DefaultDrmSession` that aren't expected to be called from outside the
DRM package:
@ -87,7 +97,9 @@
* `void provision()`
* `void onProvisionCompleted()`
* `onProvisionError(Exception, boolean)`
* Transformer:
* Remove `Transformer.Builder.setMediaSourceFactory(MediaSource.Factory)`.
Use `ExoPlayerAssetLoader.Factory(MediaSource.Factory)` and
`Transformer.Builder.setAssetLoaderFactory(AssetLoader.Factory)`
@ -99,20 +111,30 @@
an input frame was pending processing.
* Query codecs via `MediaCodecList` instead of using
`findDecoder/EncoderForFormat` utilities, to expand support.
* Muxer:
* Add a new muxer library which can be used to create an MP4 container
file.
* DASH:
* Remove the media time offset from `MediaLoadData.startTimeMs` and
`MediaLoadData.endTimeMs` for multi period DASH streams.
* RTSP:
* For MPEG4-LATM, use default profile-level-id value if absent in Describe
Response SDP message
([#302](https://github.com/androidx/media/issues/302)).
* IMA DAI extension:
* Fix a bug where a new ad group is inserted in live streams because the
calculated content position in consecutive timelines varies slightly.
* Remove deprecated symbols:
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
instead.
* Remove `HlsMasterPlaylist`, use `HlsMultivariantPlaylist` instead.

View File

@ -1704,6 +1704,7 @@ package androidx.media3.session {
@com.google.errorprone.annotations.DoNotMock public class MediaSession {
method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
method public final java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers();
method @Nullable public final androidx.media3.session.MediaSession.ControllerInfo getControllerForCurrentRequest();
method public final String getId();
method public final androidx.media3.common.Player getPlayer();
method @Nullable public final android.app.PendingIntent getSessionActivity();

View File

@ -259,6 +259,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
AtomicBoolean commandExecuting = new AtomicBoolean(true);
postOrRun(
sessionImpl.getApplicationHandler(),
sessionImpl.callWithControllerForCurrentRequestSet(
getController(info.controllerKey),
() ->
asyncCommand
.run()
@ -272,7 +274,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
}
},
MoreExecutors.directExecutor()));
MoreExecutors.directExecutor())));
commandExecuting.set(false);
}
}

View File

@ -660,6 +660,28 @@ public class MediaSession {
return impl.getConnectedControllers();
}
/**
* Returns the {@link ControllerInfo} for the controller that sent the current request for a
* {@link Player} method.
*
* <p>This method will return a non-null value while {@link Player} methods triggered by a
* controller are executed.
*
* <p>Note: If you want to prevent a controller from calling a method, specify the {@link
* ConnectionResult#availablePlayerCommands available commands} in {@link Callback#onConnect} or
* set them via {@link #setAvailableCommands}.
*
* <p>This method must be called on the {@linkplain Player#getApplicationLooper() application
* thread} of the underlying player.
*
* @return The {@link ControllerInfo} of the controller that sent the current request, or {@code
* null} if not applicable.
*/
@Nullable
public final ControllerInfo getControllerForCurrentRequest() {
return impl.getControllerForCurrentRequest();
}
/**
* Requests that controllers set the ordered list of {@link CommandButton} to build UI with it.
*

View File

@ -39,6 +39,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
@ -123,6 +124,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private PlayerInfo playerInfo;
private PlayerWrapper playerWrapper;
@Nullable private ControllerInfo controllerForCurrentRequest;
@GuardedBy("lock")
@Nullable
@ -278,6 +280,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return playerWrapper;
}
@CheckResult
public Runnable callWithControllerForCurrentRequestSet(
@Nullable ControllerInfo controllerForCurrentRequest, Runnable runnable) {
return () -> {
this.controllerForCurrentRequest = controllerForCurrentRequest;
runnable.run();
this.controllerForCurrentRequest = null;
};
}
public String getId() {
return sessionId;
}
@ -298,6 +310,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return controllers;
}
@Nullable
public ControllerInfo getControllerForCurrentRequest() {
return controllerForCurrentRequest;
}
public boolean isConnected(ControllerInfo controller) {
return sessionStub.getConnectedControllersManager().isConnected(controller)
|| sessionLegacyStub.getConnectedControllersManager().isConnected(controller);

View File

@ -655,6 +655,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return;
}
sessionImpl
.callWithControllerForCurrentRequestSet(
controller,
() -> {
try {
task.run(controller);
} catch (RemoteException e) {
@ -665,6 +669,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// - DeadSystemException means that errors around it can be ignored.
Log.w(TAG, "Exception in " + controller, e);
}
})
.run();
});
}
@ -788,6 +794,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
postOrRun(
sessionImpl.getApplicationHandler(),
sessionImpl.callWithControllerForCurrentRequestSet(
controller,
() -> {
PlayerWrapper player = sessionImpl.getPlayerWrapper();
MediaUtils.setMediaItemsWithStartIndexAndPosition(
@ -801,7 +809,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (play) {
player.playIfCommandAvailable();
}
});
}));
}
@Override
@ -836,13 +844,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public void onSuccess(List<MediaItem> mediaItems) {
postOrRun(
sessionImpl.getApplicationHandler(),
sessionImpl.callWithControllerForCurrentRequestSet(
controller,
() -> {
if (index == C.INDEX_UNSET) {
sessionImpl.getPlayerWrapper().addMediaItems(mediaItems);
} else {
sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems);
}
});
}));
}
@Override

View File

@ -204,12 +204,14 @@ import java.util.concurrent.ExecutionException;
mediaItems ->
postOrRunWithCompletion(
sessionImpl.getApplicationHandler(),
sessionImpl.callWithControllerForCurrentRequestSet(
controller,
() -> {
if (!sessionImpl.isReleased()) {
mediaItemPlayerTask.run(
sessionImpl.getPlayerWrapper(), controller, mediaItems);
}
},
}),
new SessionResult(SessionResult.RESULT_SUCCESS)));
};
}
@ -228,12 +230,14 @@ import java.util.concurrent.ExecutionException;
mediaItemsWithStartPosition ->
postOrRunWithCompletion(
sessionImpl.getApplicationHandler(),
sessionImpl.callWithControllerForCurrentRequestSet(
controller,
() -> {
if (!sessionImpl.isReleased()) {
mediaItemPlayerTask.run(
sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition);
}
},
}),
new SessionResult(SessionResult.RESULT_SUCCESS)));
};
}
@ -305,7 +309,10 @@ import java.util.concurrent.ExecutionException;
return;
}
if (command == COMMAND_SET_VIDEO_SURFACE) {
task.run(sessionImpl, controller, sequenceNumber);
sessionImpl
.callWithControllerForCurrentRequestSet(
controller, () -> task.run(sessionImpl, controller, sequenceNumber))
.run();
} else {
connectedControllersManager.addToCommandQueue(
controller, () -> task.run(sessionImpl, controller, sequenceNumber));

View File

@ -19,13 +19,20 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaControllerCompat;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.SimpleBasePlayer;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.util.Util;
import androidx.media3.test.session.common.HandlerThreadTestRule;
@ -38,6 +45,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
@ -836,6 +845,318 @@ public class MediaSessionPlayerTest {
assertThat(player.seekMediaItemIndex).isEqualTo(3);
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerAndSimplePlayerMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build())
.build();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session = new MediaSession.Builder(context, player).setId("test").build();
sessionReference.set(session);
Bundle controllerHints = new Bundle();
controllerHints.putString("key", "value");
MediaController controller =
new MediaController.Builder(context, session.getToken())
.setConnectionHints(controllerHints)
.buildAsync()
.get();
MainLooperTestRule.runOnMainSync(controller::play);
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key"))
.isEqualTo("value");
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerAndAsyncSetMediaItemMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(
new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).build())
.build();
}
@Override
protected ListenableFuture<?> handleSetMediaItems(
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session =
new MediaSession.Builder(context, player)
.setId("test")
.setCallback(
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
// Resolve media items asynchronously.
return Util.postOrRunWithCompletion(
threadTestRule.getHandler(), () -> {}, mediaItems);
}
})
.build();
sessionReference.set(session);
Bundle controllerHints = new Bundle();
controllerHints.putString("key", "value");
MediaController controller =
new MediaController.Builder(context, session.getToken())
.setConnectionHints(controllerHints)
.buildAsync()
.get();
MainLooperTestRule.runOnMainSync(() -> controller.setMediaItem(MediaItem.fromUri("test://")));
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key"))
.isEqualTo("value");
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerAndAsyncAddMediaItemMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(
new Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build())
.build();
}
@Override
protected ListenableFuture<?> handleAddMediaItems(int index, List<MediaItem> mediaItems) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session =
new MediaSession.Builder(context, player)
.setId("test")
.setCallback(
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
// Resolve media items asynchronously.
return Util.postOrRunWithCompletion(
threadTestRule.getHandler(), () -> {}, mediaItems);
}
})
.build();
sessionReference.set(session);
Bundle controllerHints = new Bundle();
controllerHints.putString("key", "value");
MediaController controller =
new MediaController.Builder(context, session.getToken())
.setConnectionHints(controllerHints)
.buildAsync()
.get();
MainLooperTestRule.runOnMainSync(() -> controller.addMediaItem(MediaItem.fromUri("test://")));
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key"))
.isEqualTo("value");
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerCompatAndSimplePlayerMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build())
.build();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session = new MediaSession.Builder(context, player).setId("test").build();
sessionReference.set(session);
MediaControllerCompat controller = session.getSessionCompat().getController();
controller.getTransportControls().play();
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0);
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerCompatAndAsyncPlayFromMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(
new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).build())
.build();
}
@Override
protected ListenableFuture<?> handleSetMediaItems(
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session =
new MediaSession.Builder(context, player)
.setId("test")
.setCallback(
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
// Resolve media items asynchronously.
return Util.postOrRunWithCompletion(
threadTestRule.getHandler(), () -> {}, mediaItems);
}
})
.build();
sessionReference.set(session);
MediaControllerCompat controller = session.getSessionCompat().getController();
controller.getTransportControls().playFromUri(Uri.parse("test://"), Bundle.EMPTY);
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0);
}
@Test
public void
getControllerForCurrentRequest_withMediaControllerCompatAndAsyncAddToQueueMethod_returnsControllerFromPlayerMethod()
throws Exception {
AtomicReference<MediaSession.ControllerInfo> controllerInfoFromPlayerMethod =
new AtomicReference<>();
AtomicReference<MediaSession> sessionReference = new AtomicReference<>();
CountDownLatch eventHandled = new CountDownLatch(1);
Player player =
new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(
new Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build())
.build();
}
@Override
protected ListenableFuture<?> handleAddMediaItems(int index, List<MediaItem> mediaItems) {
controllerInfoFromPlayerMethod.set(
sessionReference.get().getControllerForCurrentRequest());
eventHandled.countDown();
return Futures.immediateVoidFuture();
}
};
Context context = ApplicationProvider.getApplicationContext();
MediaSession session =
new MediaSession.Builder(context, player)
.setId("test")
.setCallback(
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
// Resolve media items asynchronously.
return Util.postOrRunWithCompletion(
threadTestRule.getHandler(), () -> {}, mediaItems);
}
})
.build();
sessionReference.set(session);
MainLooperTestRule.runOnMainSync(
() -> {
MediaControllerCompat controller = session.getSessionCompat().getController();
controller.addQueueItem(new MediaDescriptionCompat.Builder().setMediaId("id").build());
});
eventHandled.await();
session.release();
assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0);
}
private void changePlaybackTypeToRemote() throws Exception {
threadTestRule
.getHandler()