From ec71c05e8bef510e91996ed24273dcd9bfda90eb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Dec 2017 07:26:49 -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=179683841 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 446 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 +++---- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 ++++++++--- .../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 | 64 +++ .../exoplayer2/testutil/ActionSchedule.java | 85 ++++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1349 insertions(+), 232 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd..3c45c3449a 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 0a902e2efe..0f8df65959 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 class VpxPlaybackTest extends InstrumentationTestCase { 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 40b4b2d383..70ff878e35 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 @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2; +import android.view.Surface; 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.PlayerRunnable; +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; @@ -34,8 +38,10 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.video.DummySurface; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -942,4 +948,444 @@ public final class ExoPlayerTest extends TestCase { 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); + } + + public void testSetAndSwitchSurfaceTest() throws Exception { + final List rendererMessages = new ArrayList<>(); + Renderer videoRenderer = + new FakeRenderer(Builder.VIDEO_FORMAT) { + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + super.handleMessage(what, object); + rendererMessages.add(what); + } + }; + final Surface surface1 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + final Surface surface2 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setAndSwitchSurfaceTest") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface1); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface2); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setRenderers(videoRenderer) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, Collections.frequency(rendererMessages, C.MSG_SET_SURFACE)); + } + + 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 a4103787d1..8ee9a13c55 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 abstract class BaseRenderer implements Renderer, RendererCapabilities { 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 cc767752be..4bd28150bc 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.upstream.DataSource; 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 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * 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 { + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * 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; - - } - - /** - * 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 interface ExoPlayer extends Player { 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 2869a7668e..afb6428fa5 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.os.Message; 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.TrackSelector; 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 @@ import java.util.concurrent.CopyOnWriteArraySet; 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 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ import java.util.concurrent.CopyOnWriteArraySet; @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 09b3231467..f3d0e1794b 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.Message; 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.TrackSelector; 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; +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 { +/** 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(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; } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - 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 @@ import java.io.IOException; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ import java.io.IOException; } 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ import java.io.IOException; } } - 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(); } - 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); + } + } + + 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); } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + } + // 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; } + 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; * 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 @@ import java.io.IOException; // 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 @@ import java.io.IOException; 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 978f4f7a97..593d3d1fce 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 abstract class NoSampleRenderer implements Renderer, RendererCapabilities 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 0000000000..420eb60a48 --- /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 6def1591da..d0a07930e0 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 69369d4229..e2d0ed1422 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 @@ public class SimpleExoPlayer implements ExoPlayer { 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 @@ public class SimpleExoPlayer implements ExoPlayer { 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 @@ public class SimpleExoPlayer implements ExoPlayer { */ 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 class SimpleExoPlayer implements ExoPlayer { */ 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 class SimpleExoPlayer implements ExoPlayer { */ 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 class SimpleExoPlayer implements ExoPlayer { 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 @@ public class SimpleExoPlayer implements ExoPlayer { 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 c410456e7b..54537ba548 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 android.util.SparseIntArray; 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 @@ import java.util.Map; * 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 final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl 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 final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } 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 final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl 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 final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } 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 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl 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 d796e6936f..a5f5222820 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 final class Util { 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 ff0b8a6bc0..8145aa0c56 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,18 @@ package com.google.android.exoplayer2.testutil; 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.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +350,63 @@ public abstract class Action { 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(); + } } /** @@ -555,6 +616,9 @@ public abstract class Action { @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (runnable instanceof PlayerRunnable) { + ((PlayerRunnable) runnable).setPlayer(player); + } runnable.run(); } 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 477071f91f..33ce846751 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.os.Looper; 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.ClearVideoSurface; 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 final class ActionSchedule { 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,50 @@ public final class ActionSchedule { 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); + } + } + + /** + * Provides a wrapper for a {@link Runnable} which has access to the player. Can be used with + * {@link Builder#executeRunnable(Runnable)}. + */ + public abstract static class PlayerRunnable implements Runnable { + + private SimpleExoPlayer player; + + /** Executes Runnable with reference to player. */ + public abstract void run(SimpleExoPlayer player); + + /** Sets the player to be passed to {@link #run(SimpleExoPlayer)} . */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void run() { + run(player); + } } /** 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 4a9d79f906..797c09d6b6 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 final class FakeTimeline extends Timeline { 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 final class FakeTimeline extends Timeline { @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 4f31a8b027..93c14afc8f 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.Handler; 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 class MediaSourceTestRunner { } - 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 class MediaSourceTestRunner { } @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 1ea83bf1ec..7164fa13ab 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 android.os.Looper; 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 abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException();