diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd9..3c45c3449ad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler. + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 0a902e2efee..0f8df659599 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -119,9 +119,11 @@ public void run() { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, - LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - new VpxVideoSurfaceView(context))); + player + .createMessage(videoRenderer) + .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setMessage(new VpxVideoSurfaceView(context)) + .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 40b4b2d3832..9d8e2dcd9df 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,11 +17,13 @@ import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -942,4 +944,405 @@ public void run() { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + + public void testSendMessagesDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testMultipleSendMessages() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target80, /* positionMs= */ 80) + .sendMessage(target50, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target50.positionMs >= 50); + assertTrue(target80.positionMs >= 80); + assertTrue(target80.positionMs >= target50.positionMs); + } + + public void testMultipleSendMessagesAtSameTime() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* positionMs= */ 50) + .sendMessage(target2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target1.positionMs >= 50); + assertTrue(target2.positionMs >= 50); + } + + public void testSendMessagesMultiPeriodResolution() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); + long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) + .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) + .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline, null), + /* resetPosition= */ false, + /* resetState= */ true) + .waitForPlaybackState(Player.STATE_READY) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, targetStartFirstPeriod.windowIndex); + assertTrue(targetStartFirstPeriod.positionMs >= 0); + assertEquals(0, targetEndMiddlePeriod.windowIndex); + assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); + assertEquals(1, targetStartMiddlePeriod.windowIndex); + assertTrue(targetStartMiddlePeriod.positionMs >= 0); + assertEquals(1, targetEndLastPeriod.windowIndex); + assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); + } + + public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesRepeatDoesNotRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(1, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage( + target, + /* windowIndex= */ 0, + /* positionMs= */ 50, + /* deleteAfterDelivery= */ false) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(1, target.windowIndex); + } + + public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .waitForTimelineChanged(secondTimeline) + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(0, target.windowIndex); + } + + public void testSendMessagesNonLinearPeriodOrder() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) + .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) + .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPositionDiscontinuity() + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, target1.windowIndex); + assertEquals(1, target2.windowIndex); + assertEquals(2, target3.windowIndex); + } + + private static final class PositionGrabbingMessageTarget extends PlayerTarget { + + public int windowIndex; + public long positionMs; + public int messageCount; + + public PositionGrabbingMessageTarget() { + windowIndex = C.INDEX_UNSET; + positionMs = C.POSITION_UNSET; + } + + @Override + public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + messageCount++; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a4103787d1d..8ee9a13c556 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index cc767752bec..4bd28150bca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,40 +34,43 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * ExoPlayerFactory}. * *

Player components

+ * *

ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

+ * *

An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -81,30 +84,32 @@ * it's possible to load data from a non-standard source, or through a different network stack. * *

Threading model

- *

The figure below shows ExoPlayer's threading model.

- *

- * ExoPlayer's threading model - *

+ * + *

The figure below shows ExoPlayer's threading model. + * + *

ExoPlayer's threading
+ * model * *

*/ public interface ExoPlayer extends Player { @@ -115,54 +120,28 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

- * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { - - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ + /** @deprecated Use {@link PlayerMessage} instead. */ + @Deprecated final class ExoPlayerMessage { - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ + /** The target to receive the message. */ + public final PlayerMessage.Target target; + /** The type of the message. */ public final int messageType; - /** - * The message. - */ + /** The message. */ public final Object message; - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { + /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ + @Deprecated + public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } - } /** @@ -236,20 +215,25 @@ public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object messa void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ + @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. + * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link + * PlayerMessage#blockUntilDelivered()}. */ + @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2869a7668e8..afb6428fa56 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -31,6 +32,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -45,6 +48,7 @@ private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; + private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -113,6 +117,7 @@ public void handleMessage(Message msg) { shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ public void release() { @Override public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); + for (ExoPlayerMessage message : messages) { + createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + } + } + + @Override + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + List playerMessages = new ArrayList<>(); + for (ExoPlayerMessage message : messages) { + playerMessages.add( + createMessage(message.target) + .setType(message.messageType) + .setMessage(message.message) + .send()); + } + boolean wasInterrupted = false; + for (PlayerMessage message : playerMessages) { + boolean blockMessage = true; + while (blockMessage) { + try { + message.blockUntilDelivered(); + blockMessage = false; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 09b3231467e..f3d0e1794bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -22,10 +22,10 @@ import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,14 +40,19 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; - -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, - PlaybackParameterListener { +import java.util.ArrayList; +import java.util.Collections; + +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSource.Listener, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -108,6 +113,7 @@ private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,13 +126,12 @@ private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int customMessagesSent; - private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -166,6 +171,7 @@ public ExoPlayerImplInternal( rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); + customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -214,34 +220,15 @@ public void stop(boolean reset) { handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { + @Override + public synchronized void sendMessage( + PlayerMessage message, PlayerMessage.Sender.Listener listener) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); + listener.onMessageDeleted(); return; } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - boolean wasInterrupted = false; - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); } public synchronized void release() { @@ -349,7 +336,7 @@ public boolean handleMessage(Message msg) { reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ private void updatePlaybackPositions() throws ExoPlaybackException { } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -656,7 +644,8 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = resolveSeekPosition(seekPosition); + Pair periodPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -850,6 +839,11 @@ private void resetInternal( } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ private void resetInternal( } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendCustomMessagesToTarget(customMessageInfo); + } else if (playbackInfo.timeline == null) { + // Still waiting for initial timeline to resolve position. + customMessageInfos.add(customMessageInfo); + } else { + if (resolveCustomMessagePosition(customMessageInfo)) { + customMessageInfos.add(customMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(customMessageInfos); + } else { + customMessageInfo.listener.onMessageDeleted(); + } + } + } + + private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { + final Runnable handleMessageRunnable = + new Runnable() { + @Override + public void run() { + try { + customMessageInfo + .message + .getTarget() + .handleMessage( + customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + customMessageInfo.listener.onMessageDelivered(); + if (customMessageInfo.message.getDeleteAfterDelivery()) { + customMessageInfo.listener.onMessageDeleted(); + } + // The message may have caused something to change that now requires us to do + // work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + }; + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } + + private void resolveCustomMessagePositions() { + for (int i = customMessageInfos.size() - 1; i >= 0; i--) { + if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { + // Remove messages if new position can't be resolved. + customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.remove(i); } - if (playbackInfo.playbackState == Player.STATE_READY - || playbackInfo.playbackState == Player.STATE_BUFFERING) { - // The message may have caused something to change that now requires us to do work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + // Re-sort messages by playback order. + Collections.sort(customMessageInfos); + } + + private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + customMessageInfo.message.getTimeline(), + customMessageInfo.message.getWindowIndex(), + C.msToUs(customMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + customMessageInfo.setResolvedPosition( + periodPosition.first, + periodPosition.second, + playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + customMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { + if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions. + if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + oldPeriodPositionUs--; + } + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = playbackInfo.periodId.periodIndex; + CustomMessageInfo prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + while (prevInfo != null + && (prevInfo.resolvedPeriodIndex > currentPeriodIndex + || (prevInfo.resolvedPeriodIndex == currentPeriodIndex + && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextCustomMessageInfoIndex--; + prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + } + CustomMessageInfo nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextCustomMessageInfoIndex++; + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + sendCustomMessagesToTarget(nextInfo); + if (nextInfo.message.getDeleteAfterDelivery()) { + customMessageInfos.remove(nextCustomMessageInfoIndex); + } else { + nextCustomMessageInfoIndex++; } + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; } } @@ -1034,12 +1160,14 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1224,11 +1352,14 @@ private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1257,12 +1388,14 @@ private Pair resolveSeekPosition(SeekPosition seekPosition) { // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, - C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition( + timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; @@ -1802,7 +1935,45 @@ public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) { this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + + private static final class CustomMessageInfo implements Comparable { + + public final PlayerMessage message; + public final PlayerMessage.Sender.Listener listener; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + public @Nullable Object resolvedPeriodUid; + + public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + this.message = message; + this.listener = listener; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + @Override + public int compareTo(@NonNull CustomMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // CustomMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 978f4f7a974..593d3d1fcee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 00000000000..44a4b0c7c2e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** A listener for message events triggered by the sender. */ + interface Listener { + + /** Called when the message has been delivered. */ + void onMessageDelivered(); + + /** Called when the message has been deleted. */ + void onMessageDeleted(); + } + + /** + * Sends a message. + * + * @param message The message to be sent. + * @param listener The listener to listen to message events. + */ + void sendMessage(PlayerMessage message, Listener listener); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + private Object message; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isDeleted; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param messageType The custom message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param message The custom message. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setMessage(@Nullable Object message) { + Assertions.checkState(!isSent); + this.message = message; + return this; + } + + /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ + public Object getMessage() { + return message; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage( + this, + new Sender.Listener() { + @Override + public void onMessageDelivered() { + synchronized (PlayerMessage.this) { + isDelivered = true; + PlayerMessage.this.notifyAll(); + } + } + + @Override + public void onMessageDeleted() { + synchronized (PlayerMessage.this) { + isDeleted = true; + PlayerMessage.this.notifyAll(); + } + } + }); + return this; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(!isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isDelivered && !isDeleted) { + wait(); + } + return isDelivered; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 6def1591da5..d0a07930e0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,22 +15,20 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - *

- * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * + *

Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - *

- * Renderer state transitions - *

+ * + *

Renderer state transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 69369d4229e..e2d0ed14229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,8 +93,6 @@ void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; - private final int videoRendererCount; - private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -124,25 +122,6 @@ protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector track renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - default: - // Don't count other track types. - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; - // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -163,15 +142,15 @@ protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector track */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setMessage(videoScalingMode) + .send(); } } - player.sendMessages(messages); } /** @@ -352,15 +331,15 @@ public void setAudioStreamType(@C.StreamType int streamType) { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, - audioAttributes); + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setMessage(audioAttributes) + .send(); } } - player.sendMessages(messages); } /** @@ -377,14 +356,11 @@ public AudioAttributes getAudioAttributes() { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); } } - player.sendMessages(messages); } /** @@ -770,6 +746,11 @@ public void sendMessages(ExoPlayerMessage... messages) { player.sendMessages(messages); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + return player.createMessage(target); + } + @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -908,22 +889,25 @@ private void removeSurfaceCallbacks() { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + boolean surfaceReplaced = this.surface != null && this.surface != surface; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + PlayerMessage message = + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); + if (surfaceReplaced) { + try { + message.blockUntilDelivered(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } } - if (this.surface != null && this.surface != surface) { - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); + if (surfaceReplaced) { // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c410456e7bb..54537ba548d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,8 +23,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -42,7 +41,7 @@ * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { +public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -147,8 +146,11 @@ public synchronized void addMediaSource(int index, MediaSource mediaSource, Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, - new MessageData<>(index, mediaSource, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD) + .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -220,8 +222,11 @@ public synchronized void addMediaSources(int index, Collection medi } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - new MessageData<>(index, mediaSources, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -256,8 +261,11 @@ public synchronized void removeMediaSource(int index) { public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, - new MessageData<>(index, null, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_REMOVE) + .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -293,8 +301,11 @@ public synchronized void moveMediaSource(int currentIndex, int newIndex, } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_MOVE) + .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -427,8 +438,7 @@ private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.sendMessages( - new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index d796e6936f1..a5f5222820d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -561,6 +561,18 @@ public static int binarySearchCeil(List> lis return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ff0b8a6bc07..5ec45af29f7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -18,13 +18,17 @@ import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +349,63 @@ protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSe Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } + } + + /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ + public static final class SendMessages extends Action { + + private final Target target; + private final int windowIndex; + private final long positionMs; + private final boolean deleteAfterDelivery; + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param positionMs The position at which the message should be sent, in milliseconds. + */ + public SendMessages(String tag, Target target, long positionMs) { + this( + tag, + target, + /* windowIndex= */ C.INDEX_UNSET, + positionMs, + /* deleteAfterDelivery= */ true); + } + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param windowIndex The window index at which the message should be sent, or {@link + * C#INDEX_UNSET} for the current window. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + */ + public SendMessages( + String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + super(tag, "SendMessages"); + this.target = target; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.deleteAfterDelivery = deleteAfterDelivery; + } + + @Override + protected void doActionImpl( + final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (target instanceof PlayerTarget) { + ((PlayerTarget) target).setPlayer(player); + } + PlayerMessage message = player.createMessage(target); + if (windowIndex != C.INDEX_UNSET) { + message.setPosition(windowIndex, positionMs); + } else { + message.setPosition(positionMs); + } + message.setHandler(new Handler()); + message.setDeleteAfterDelivery(deleteAfterDelivery); + message.send(); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 477071f91f7..2ac487c98e5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -20,8 +20,11 @@ import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -29,6 +32,7 @@ import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -315,6 +319,44 @@ public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param positionMs The position in the current window at which the message should be sent, in + * milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, long positionMs) { + return apply(new SendMessages(tag, target, positionMs)); + } + + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, int windowIndex, long positionMs) { + return apply( + new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); + } + + /** + * Schedules to send a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + * @return The builder, for convenience. + */ + public Builder sendMessage( + Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -365,7 +407,28 @@ private Builder appendActionNode(ActionNode actionNode) { currentDelayMs = 0; return this; } + } + /** + * Provides a wrapper for a {@link Target} which has access to the player when handling messages. + * Can be used with {@link Builder#sendMessage(Target, long)}. + */ + public abstract static class PlayerTarget implements Target { + + private SimpleExoPlayer player; + + /** Handles the message send to the component and additionally provides access to the player. */ + public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); + + /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + handleMessage(player, messageType, message); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4a9d79f906d..797c09d6b6d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -170,7 +171,7 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? periodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -198,11 +199,13 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) { @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; + Period period = new Period(); + for (int i = 0; i < getPeriodCount(); i++) { + if (getPeriod(i, period, true).uid.equals(uid)) { + return i; + } } - int index = (Integer) uid; - return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + return C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 4f31a8b027e..93c14afc8fe 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,9 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -281,7 +283,8 @@ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object } - private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static class EventHandlingExoPlayer extends StubExoPlayer + implements Handler.Callback, PlayerMessage.Sender { private final Handler handler; @@ -290,23 +293,33 @@ public EventHandlingExoPlayer(Looper looper) { } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public PlayerMessage createMessage(PlayerMessage.Target target) { + return new PlayerMessage( + /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); } @Override + public void sendMessage(PlayerMessage message, Listener listener) { + handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + } + + @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + Pair messageAndListener = (Pair) msg.obj; + try { + messageAndListener + .first + .getTarget() + .handleMessage( + messageAndListener.first.getType(), messageAndListener.first.getMessage()); + messageAndListener.second.onMessageDelivered(); + messageAndListener.second.onMessageDeleted(); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); } return true; } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 1ea83bf1ecd..7164fa13ab6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -146,6 +147,11 @@ public void release() { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException();