Run MediaSessionStub commands in order

Some commands are run asynchronously and subsequent commands need
to wait until the previous one finished. This can be supported
by returning a Future for each command and using the existing
command execution logic to wait for each Future to complete.

As some MediaSessionStub code is now executed delayed to when it
was originally created, we also need to check if the session is
not released before triggering any actions or sending result codes.

Issue: androidx/media#85
PiperOrigin-RevId: 462101136
(cherry picked from commit 7cb7636ed954f0737aaac2f9cf556715a2ee9b37)
This commit is contained in:
tonihei 2022-07-20 10:34:22 +00:00 committed by microkatz
parent d84662e5ce
commit eb823a9ab7
7 changed files with 242 additions and 120 deletions

View File

@ -2,6 +2,29 @@
### Unreleased changes
* Core library:
* Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for
the currently selected tracks
([#2518](https://github.com/google/ExoPlayer/issues/2518)).
* Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid
OutOfMemory errors when releasing multiple players at the same time
([#10057](https://github.com/google/ExoPlayer/issues/10057)).
* Metadata:
* `MetadataRenderer` can now be configured to render metadata as soon as
they are available. Create an instance with
`MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory,
boolean)` to specify whether the renderer will output metadata early or
in sync with the player position.
* Session:
* Ensure commands are always executed in the correct order even if some
require asynchronous resolution
([#85](https://github.com/androidx/media/issues/85)).
### 1.0.0-beta02 (2022-07-15)
This release corresponds to the
[ExoPlayer 2.18.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.1).
* Core library:
* Ensure that changing the `ShuffleOrder` with `ExoPlayer.setShuffleOrder`
results in a call to `Player.Listener#onTimelineChanged` with
@ -9,14 +32,11 @@
([#9889](https://github.com/google/ExoPlayer/issues/9889)).
* For progressive media, only include selected tracks in buffered position
([#10361](https://github.com/google/ExoPlayer/issues/10361)).
* Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for
the currently selected tracks
([#2518](https://github.com/google/ExoPlayer/issues/2518)).
* Allow custom logger for all ExoPlayer log output
([#9752](https://github.com/google/ExoPlayer/issues/9752)).
* Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid
OutOfMemory errors when releasing multiple players at the same time
([#10057](https://github.com/google/ExoPlayer/issues/10057)).
* Fix implementation of `setDataSourceFactory` in
`DefaultMediaSourceFactory`, which was non-functional in some cases
([#116](https://github.com/androidx/media/issues/116)).
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
@ -24,12 +44,6 @@
([#10316](https://github.com/google/ExoPlayer/issues/10316)).
* Fix parsing of bitrates from `esds` boxes
([#10381](https://github.com/google/ExoPlayer/issues/10381)).
* Metadata:
* `MetadataRenderer` can now be configured to render metadata as soon as
they are available. Create an instance with
`MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory,
boolean)` to specify whether the renderer will output metadata early or
in sync with the player position.
* DASH:
* Parse ClearKey license URL from manifests
([#10246](https://github.com/google/ExoPlayer/issues/10246)).

View File

@ -24,7 +24,6 @@ import androidx.collection.ArrayMap;
import androidx.media3.common.Player;
import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayDeque;
@ -227,15 +226,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
}
public void addToCommandQueue(ControllerInfo controllerInfo, Runnable commandRunnable) {
public void addToCommandQueue(ControllerInfo controllerInfo, AsyncCommand asyncCommand) {
synchronized (lock) {
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
if (info != null) {
info.commandQueue.add(
() -> {
commandRunnable.run();
return Futures.immediateVoidFuture();
});
info.commandQueue.add(asyncCommand);
}
}
}

View File

@ -80,8 +80,10 @@ import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.SessionCommand.CommandCode;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
@ -126,52 +128,66 @@ import java.util.concurrent.ExecutionException;
}
}
private static <K extends MediaSessionImpl> SessionTask<Void, K> sendSessionResultSuccess(
Consumer<PlayerWrapper> task) {
private static <K extends MediaSessionImpl>
SessionTask<ListenableFuture<Void>, K> sendSessionResultSuccess(
Consumer<PlayerWrapper> task) {
return (sessionImpl, controller, sequence) -> {
if (sessionImpl.isReleased()) {
return Futures.immediateVoidFuture();
}
task.accept(sessionImpl.getPlayerWrapper());
sendSessionResult(controller, sequence, new SessionResult(SessionResult.RESULT_SUCCESS));
return null;
return Futures.immediateVoidFuture();
};
}
private static <K extends MediaSessionImpl> SessionTask<Void, K> sendSessionResultWhenReady(
SessionTask<ListenableFuture<SessionResult>, K> task) {
return (sessionImpl, controller, sequence) -> {
ListenableFuture<SessionResult> future = task.run(sessionImpl, controller, sequence);
future.addListener(
() -> {
SessionResult result;
try {
result = checkNotNull(future.get(), "SessionResult must not be null");
} catch (CancellationException unused) {
result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException exception) {
result =
new SessionResult(
exception.getCause() instanceof UnsupportedOperationException
? SessionResult.RESULT_ERROR_NOT_SUPPORTED
: SessionResult.RESULT_ERROR_UNKNOWN);
}
sendSessionResult(controller, sequence, result);
},
MoreExecutors.directExecutor());
return null;
};
private static <K extends MediaSessionImpl>
SessionTask<ListenableFuture<Void>, K> sendSessionResultWhenReady(
SessionTask<ListenableFuture<SessionResult>, K> task) {
return (sessionImpl, controller, sequence) ->
handleSessionTaskWhenReady(
sessionImpl,
controller,
sequence,
task,
future -> {
SessionResult result;
try {
result = checkNotNull(future.get(), "SessionResult must not be null");
} catch (CancellationException unused) {
result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException exception) {
result =
new SessionResult(
exception.getCause() instanceof UnsupportedOperationException
? SessionResult.RESULT_ERROR_NOT_SUPPORTED
: SessionResult.RESULT_ERROR_UNKNOWN);
}
sendSessionResult(controller, sequence, result);
});
}
private static <K extends MediaSessionImpl>
SessionTask<ListenableFuture<SessionResult>, K> handleMediaItemsWhenReady(
SessionTask<ListenableFuture<List<MediaItem>>, K> mediaItemsTask,
MediaItemPlayerTask mediaItemPlayerTask) {
return (sessionImpl, controller, sequence) ->
transformFutureAsync(
mediaItemsTask.run(sessionImpl, controller, sequence),
mediaItems ->
postOrRunWithCompletion(
sessionImpl.getApplicationHandler(),
() -> mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems),
new SessionResult(SessionResult.RESULT_SUCCESS)));
return (sessionImpl, controller, sequence) -> {
if (sessionImpl.isReleased()) {
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
}
return transformFutureAsync(
mediaItemsTask.run(sessionImpl, controller, sequence),
mediaItems ->
postOrRunWithCompletion(
sessionImpl.getApplicationHandler(),
() -> {
if (!sessionImpl.isReleased()) {
mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems);
}
},
new SessionResult(SessionResult.RESULT_SUCCESS)));
};
}
private static void sendLibraryResult(
@ -184,29 +200,32 @@ import java.util.concurrent.ExecutionException;
}
private static <V, K extends MediaLibrarySessionImpl>
SessionTask<Void, K> sendLibraryResultWhenReady(
SessionTask<ListenableFuture<Void>, K> sendLibraryResultWhenReady(
SessionTask<ListenableFuture<LibraryResult<V>>, K> task) {
return (sessionImpl, controller, sequence) -> {
ListenableFuture<LibraryResult<V>> future = task.run(sessionImpl, controller, sequence);
future.addListener(
() -> {
LibraryResult<V> result;
try {
result = checkNotNull(future.get(), "LibraryResult must not be null");
} catch (CancellationException unused) {
result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException unused) {
result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN);
}
sendLibraryResult(controller, sequence, result);
},
MoreExecutors.directExecutor());
return null;
};
return (sessionImpl, controller, sequence) ->
handleSessionTaskWhenReady(
sessionImpl,
controller,
sequence,
task,
future -> {
LibraryResult<V> result;
try {
result = checkNotNull(future.get(), "LibraryResult must not be null");
} catch (CancellationException unused) {
result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException unused) {
result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN);
}
sendLibraryResult(controller, sequence, result);
});
}
private <K extends MediaSessionImpl> void dispatchSessionTaskWithPlayerCommand(
IMediaController caller, int seq, @Player.Command int command, SessionTask<Void, K> task) {
private <K extends MediaSessionImpl> void queueSessionTaskWithPlayerCommand(
IMediaController caller,
int seq,
@Player.Command int command,
SessionTask<ListenableFuture<Void>, K> task) {
long token = Binder.clearCallingIdentity();
try {
@SuppressWarnings({"unchecked", "cast.unsafe"})
@ -248,13 +267,19 @@ import java.util.concurrent.ExecutionException;
}
private <K extends MediaSessionImpl> void dispatchSessionTaskWithSessionCommand(
IMediaController caller, int seq, @CommandCode int commandCode, SessionTask<Void, K> task) {
IMediaController caller,
int seq,
@CommandCode int commandCode,
SessionTask<ListenableFuture<Void>, K> task) {
dispatchSessionTaskWithSessionCommand(
caller, seq, /* sessionCommand= */ null, commandCode, task);
}
private <K extends MediaSessionImpl> void dispatchSessionTaskWithSessionCommand(
IMediaController caller, int seq, SessionCommand sessionCommand, SessionTask<Void, K> task) {
IMediaController caller,
int seq,
SessionCommand sessionCommand,
SessionTask<ListenableFuture<Void>, K> task) {
dispatchSessionTaskWithSessionCommand(caller, seq, sessionCommand, COMMAND_CODE_CUSTOM, task);
}
@ -263,7 +288,7 @@ import java.util.concurrent.ExecutionException;
int seq,
@Nullable SessionCommand sessionCommand,
@CommandCode int commandCode,
SessionTask<Void, K> task) {
SessionTask<ListenableFuture<Void>, K> task) {
long token = Binder.clearCallingIdentity();
try {
@SuppressWarnings({"unchecked", "cast.unsafe"})
@ -308,6 +333,34 @@ import java.util.concurrent.ExecutionException;
}
}
private static <T, K extends MediaSessionImpl> ListenableFuture<Void> handleSessionTaskWhenReady(
K sessionImpl,
ControllerInfo controller,
int sequence,
SessionTask<ListenableFuture<T>, K> task,
Consumer<ListenableFuture<T>> futureResultHandler) {
if (sessionImpl.isReleased()) {
return Futures.immediateVoidFuture();
}
ListenableFuture<T> future = task.run(sessionImpl, controller, sequence);
SettableFuture<Void> outputFuture = SettableFuture.create();
future.addListener(
() -> {
if (sessionImpl.isReleased()) {
outputFuture.set(null);
return;
}
try {
futureResultHandler.accept(future);
outputFuture.set(null);
} catch (Throwable error) {
outputFuture.setException(error);
}
},
MoreExecutors.directExecutor());
return outputFuture;
}
public void connect(
IMediaController caller,
int controllerVersion,
@ -480,7 +533,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_STOP, sendSessionResultSuccess(Player::stop));
}
@ -529,7 +582,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::play));
}
@ -538,7 +591,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::pause));
}
@ -547,7 +600,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_PREPARE, sendSessionResultSuccess(Player::prepare));
}
@ -556,7 +609,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_TO_DEFAULT_POSITION,
@ -569,7 +622,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_TO_MEDIA_ITEM,
@ -582,7 +635,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
@ -596,7 +649,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_TO_MEDIA_ITEM,
@ -608,7 +661,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_SEEK_BACK, sendSessionResultSuccess(Player::seekBack));
}
@ -617,7 +670,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_SEEK_FORWARD, sendSessionResultSuccess(Player::seekForward));
}
@ -698,7 +751,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_SPEED_AND_PITCH,
@ -713,7 +766,7 @@ import java.util.concurrent.ExecutionException;
}
PlaybackParameters playbackParameters =
PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle);
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_SPEED_AND_PITCH,
@ -733,7 +786,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_MEDIA_ITEM,
@ -760,7 +813,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_MEDIA_ITEM,
@ -788,7 +841,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_MEDIA_ITEM,
@ -815,7 +868,7 @@ import java.util.concurrent.ExecutionException;
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -844,7 +897,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -874,7 +927,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -899,7 +952,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaMetadata", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_MEDIA_ITEMS_METADATA,
@ -918,7 +971,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -942,7 +995,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -968,7 +1021,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -997,7 +1050,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -1013,7 +1066,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -1026,7 +1079,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -1038,7 +1091,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, sendSessionResultSuccess(Player::clearMediaItems));
}
@ -1048,7 +1101,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -1061,7 +1114,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_CHANGE_MEDIA_ITEMS,
@ -1073,7 +1126,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
@ -1085,7 +1138,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
@ -1097,7 +1150,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_SEEK_TO_PREVIOUS, sendSessionResultSuccess(Player::seekToPrevious));
}
@ -1106,7 +1159,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller, seq, COMMAND_SEEK_TO_NEXT, sendSessionResultSuccess(Player::seekToNext));
}
@ -1116,7 +1169,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_REPEAT_MODE,
@ -1129,7 +1182,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_SHUFFLE_MODE,
@ -1142,7 +1195,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_VIDEO_SURFACE,
@ -1154,7 +1207,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_VOLUME,
@ -1166,7 +1219,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_DEVICE_VOLUME,
@ -1178,7 +1231,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_ADJUST_DEVICE_VOLUME,
@ -1190,7 +1243,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_ADJUST_DEVICE_VOLUME,
@ -1202,7 +1255,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_DEVICE_VOLUME,
@ -1214,7 +1267,7 @@ import java.util.concurrent.ExecutionException;
if (caller == null) {
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_PLAY_PAUSE,
@ -1258,7 +1311,7 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "Ignoring malformed Bundle for TrackSelectionParameters", e);
return;
}
dispatchSessionTaskWithPlayerCommand(
queueSessionTaskWithPlayerCommand(
caller,
seq,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,

View File

@ -81,6 +81,7 @@ interface IRemoteMediaController {
void release(String controllerId);
void stop(String controllerId);
void setTrackSelectionParameters(String controllerId, in Bundle parameters);
void setMediaItemsPreparePlayAddItemsSeek(String controllerId, in List<Bundle> initialMediaItems, in List<Bundle> addedMediaItems, int seekIndex);
// MediaBrowser methods
Bundle getLibraryRoot(String controllerId, in Bundle libraryParams);

View File

@ -19,19 +19,21 @@ 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.os.Handler;
import android.os.HandlerThread;
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.TrackSelectionParameters;
import androidx.media3.common.util.Util;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import org.junit.After;
@ -58,6 +60,7 @@ public class MediaSessionPlayerTest {
private MediaSession session;
private MockPlayer player;
private RemoteMediaController controller;
private HandlerThread asyncHandlerThread;
@Before
public void setUp() throws Exception {
@ -66,6 +69,9 @@ public class MediaSessionPlayerTest {
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.setMediaItems(/* itemCount= */ 5)
.build();
asyncHandlerThread = new HandlerThread("AsyncHandlerThread");
asyncHandlerThread.start();
Handler asyncHandler = new Handler(asyncHandlerThread.getLooper());
session =
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
.setCallback(
@ -84,7 +90,9 @@ public class MediaSessionPlayerTest {
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
return Futures.immediateFuture(mediaItems);
// Send empty message and return mediaItems once done to simulate asynchronous
// media item resolution.
return Util.postOrRunWithCompletion(asyncHandler, () -> {}, mediaItems);
}
})
.build();
@ -97,6 +105,7 @@ public class MediaSessionPlayerTest {
public void tearDown() throws Exception {
controller.release();
session.release();
asyncHandlerThread.quit();
}
@Test
@ -544,6 +553,26 @@ public class MediaSessionPlayerTest {
assertThat(player.trackSelectionParameters).isEqualTo(trackSelectionParameters);
}
@Test
public void mixedAsyncAndSyncCommands_calledInCorrectOrder() throws Exception {
List<MediaItem> initialItems = MediaTestUtils.createMediaItems(/* size= */ 2);
List<MediaItem> addedItems = MediaTestUtils.createMediaItems(/* size= */ 3);
controller.setMediaItemsPreparePlayAddItemsSeek(initialItems, addedItems, /* seekIndex= */ 3);
player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS);
boolean setMediaItemsCalledBeforePrepare =
player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS);
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS);
boolean addMediaItemsCalledBeforeSeek =
player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS);
assertThat(setMediaItemsCalledBeforePrepare).isTrue();
assertThat(addMediaItemsCalledBeforeSeek).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(player.mediaItems).hasSize(5);
assertThat(player.seekMediaItemIndex).isEqualTo(3);
}
private void changePlaybackTypeToRemote() throws Exception {
threadTestRule
.getHandler()

View File

@ -659,6 +659,26 @@ public class MediaControllerProviderService extends Service {
});
}
@Override
public void setMediaItemsPreparePlayAddItemsSeek(
String controllerId,
List<Bundle> initialMediaItems,
List<Bundle> addedMediaItems,
int seekIndex)
throws RemoteException {
runOnHandler(
() -> {
MediaController controller = mediaControllerMap.get(controllerId);
controller.setMediaItems(
BundleableUtil.fromBundleList(MediaItem.CREATOR, initialMediaItems));
controller.prepare();
controller.play();
controller.addMediaItems(
BundleableUtil.fromBundleList(MediaItem.CREATOR, addedMediaItems));
controller.seekTo(seekIndex, /* positionMs= */ 0);
});
}
////////////////////////////////////////////////////////////////////////////////
// MediaBrowser methods
////////////////////////////////////////////////////////////////////////////////

View File

@ -292,6 +292,16 @@ public class RemoteMediaController {
binder.setTrackSelectionParameters(controllerId, parameters.toBundle());
}
public void setMediaItemsPreparePlayAddItemsSeek(
List<MediaItem> initialMediaItems, List<MediaItem> addedMediaItems, int seekIndex)
throws RemoteException {
binder.setMediaItemsPreparePlayAddItemsSeek(
controllerId,
BundleableUtil.toBundleList(initialMediaItems),
BundleableUtil.toBundleList(addedMediaItems),
seekIndex);
}
////////////////////////////////////////////////////////////////////////////////
// Non-public methods
////////////////////////////////////////////////////////////////////////////////