diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2419e6797a..9d6aa87817 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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)). diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java index 01f2c9a3e0..64b069effb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java @@ -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 info = controllerRecords.get(controllerInfo); if (info != null) { - info.commandQueue.add( - () -> { - commandRunnable.run(); - return Futures.immediateVoidFuture(); - }); + info.commandQueue.add(asyncCommand); } } } 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 5a8892df1d..d895208b0b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -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 SessionTask sendSessionResultSuccess( - Consumer task) { + private static + SessionTask, K> sendSessionResultSuccess( + Consumer 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 SessionTask sendSessionResultWhenReady( - SessionTask, K> task) { - return (sessionImpl, controller, sequence) -> { - ListenableFuture 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 + SessionTask, K> sendSessionResultWhenReady( + SessionTask, 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 SessionTask, K> handleMediaItemsWhenReady( SessionTask>, 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 - SessionTask sendLibraryResultWhenReady( + SessionTask, K> sendLibraryResultWhenReady( SessionTask>, K> task) { - return (sessionImpl, controller, sequence) -> { - ListenableFuture> future = task.run(sessionImpl, controller, sequence); - future.addListener( - () -> { - LibraryResult 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 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 void dispatchSessionTaskWithPlayerCommand( - IMediaController caller, int seq, @Player.Command int command, SessionTask task) { + private void queueSessionTaskWithPlayerCommand( + IMediaController caller, + int seq, + @Player.Command int command, + SessionTask, K> task) { long token = Binder.clearCallingIdentity(); try { @SuppressWarnings({"unchecked", "cast.unsafe"}) @@ -248,13 +267,19 @@ import java.util.concurrent.ExecutionException; } private void dispatchSessionTaskWithSessionCommand( - IMediaController caller, int seq, @CommandCode int commandCode, SessionTask task) { + IMediaController caller, + int seq, + @CommandCode int commandCode, + SessionTask, K> task) { dispatchSessionTaskWithSessionCommand( caller, seq, /* sessionCommand= */ null, commandCode, task); } private void dispatchSessionTaskWithSessionCommand( - IMediaController caller, int seq, SessionCommand sessionCommand, SessionTask task) { + IMediaController caller, + int seq, + SessionCommand sessionCommand, + SessionTask, 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 task) { + SessionTask, K> task) { long token = Binder.clearCallingIdentity(); try { @SuppressWarnings({"unchecked", "cast.unsafe"}) @@ -308,6 +333,34 @@ import java.util.concurrent.ExecutionException; } } + private static ListenableFuture handleSessionTaskWhenReady( + K sessionImpl, + ControllerInfo controller, + int sequence, + SessionTask, K> task, + Consumer> futureResultHandler) { + if (sessionImpl.isReleased()) { + return Futures.immediateVoidFuture(); + } + ListenableFuture future = task.run(sessionImpl, controller, sequence); + SettableFuture 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, diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl index 0f611d7be5..5380bc1215 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl @@ -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 initialMediaItems, in List addedMediaItems, int seekIndex); // MediaBrowser methods Bundle getLibraryRoot(String controllerId, in Bundle libraryParams); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 1d5e92344f..076643c2a2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -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 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 initialItems = MediaTestUtils.createMediaItems(/* size= */ 2); + List 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() diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java index 7d263e9c71..dffcada3d3 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java @@ -659,6 +659,26 @@ public class MediaControllerProviderService extends Service { }); } + @Override + public void setMediaItemsPreparePlayAddItemsSeek( + String controllerId, + List initialMediaItems, + List 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 //////////////////////////////////////////////////////////////////////////////// diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java index 6b0449ce25..bf354f1f8d 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java @@ -292,6 +292,16 @@ public class RemoteMediaController { binder.setTrackSelectionParameters(controllerId, parameters.toBundle()); } + public void setMediaItemsPreparePlayAddItemsSeek( + List initialMediaItems, List addedMediaItems, int seekIndex) + throws RemoteException { + binder.setMediaItemsPreparePlayAddItemsSeek( + controllerId, + BundleableUtil.toBundleList(initialMediaItems), + BundleableUtil.toBundleList(addedMediaItems), + seekIndex); + } + //////////////////////////////////////////////////////////////////////////////// // Non-public methods ////////////////////////////////////////////////////////////////////////////////