From 6c2d1e1966625cd9132c0629649f8210764b205a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 19 Dec 2017 09:22:59 -0800 Subject: [PATCH] Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also attempts to update these when the source info is refreshed. A sorted list of pending posts is kept and the player triggers these posts when the playback position moves over the specified position. Issue:#2189 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179563355 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 403 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 ++++--- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 285 ++++++++++--- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 +++++++++++++ .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 ++- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 + .../android/exoplayer2/testutil/Action.java | 60 +++ .../exoplayer2/testutil/ActionSchedule.java | 63 +++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1281 insertions(+), 233 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java 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();