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:
+ *
*
* - A {@link MediaSource} that defines the media to be played, loads the media, and from
- * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)}
- * at the start of playback. The library modules provide default implementations for regular media
- * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource)
- * and HLS (HlsMediaSource), an implementation for loading single media samples
- * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and
- * implementations for building more complex MediaSources from simpler ones
- * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource},
- * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and
- * {@link ClippingMediaSource}).
+ * which the loaded media can be read. A MediaSource is injected via {@link
+ * #prepare(MediaSource)} at the start of playback. The library modules provide default
+ * implementations for regular media files ({@link ExtractorMediaSource}), DASH
+ * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
+ * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
+ * most often used for side-loaded subtitle files, and implementations for building more
+ * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link
+ * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link
+ * LoopingMediaSource} and {@link ClippingMediaSource}).
* - {@link Renderer}s that render individual components of the media. The library
- * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
- * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer
- * consumes media from the MediaSource being played. Renderers are injected when the player is
- * created.
+ * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
+ * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
+ * Renderer consumes media from the MediaSource being played. Renderers are injected when the
+ * player is created.
* - A {@link TrackSelector} that selects tracks provided by the MediaSource to be
- * consumed by each of the available Renderers. The library provides a default implementation
- * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when
- * the player is created.
+ * consumed by each of the available Renderers. The library provides a default implementation
+ * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
+ * when the player is created.
* - A {@link LoadControl} that controls when the MediaSource buffers more media, and how
- * much media is buffered. The library provides a default implementation
- * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the
- * player is created.
+ * much media is buffered. The library provides a default implementation ({@link
+ * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player
+ * is created.
*
+ *
* 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.
- *
- *
- *
+ *
+ * The figure below shows ExoPlayer's threading model.
+ *
+ *
*
*
- * - It is recommended that ExoPlayer instances are created and accessed from a single application
- * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
- * discouraged, however if an application does wish to do this then it may do so provided that it
- * ensures accesses are synchronized.
- * - Registered listeners are called on the thread that created the ExoPlayer instance, unless
- * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case,
- * registered listeners will be called on the application's main thread.
- * - An internal playback thread is responsible for playback. Injected player components such as
- * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
- * thread.
- * - When the application performs an operation on the player, for example a seek, a message is
- * delivered to the internal playback thread via a message queue. The internal playback thread
- * consumes messages from the queue and performs the corresponding operations. Similarly, when a
- * playback event occurs on the internal playback thread, a message is delivered to the application
- * thread via a second message queue. The application thread consumes messages from the queue,
- * updating the application visible state and calling corresponding listener methods.
- * - Injected player components may use additional background threads. For example a MediaSource
- * may use background threads to load data. These are implementation specific.
+ * - It is recommended that ExoPlayer instances are created and accessed from a single
+ * application thread. The application's main thread is ideal. Accessing an instance from
+ * multiple threads is discouraged, however if an application does wish to do this then it may
+ * do so provided that it ensures accesses are synchronized.
+ *
- Registered listeners are called on the thread that created the ExoPlayer instance, unless
+ * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
+ * case, registered listeners will be called on the application's main thread.
+ *
- An internal playback thread is responsible for playback. Injected player components such as
+ * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
+ * thread.
+ *
- When the application performs an operation on the player, for example a seek, a message is
+ * delivered to the internal playback thread via a message queue. The internal playback thread
+ * consumes messages from the queue and performs the corresponding operations. Similarly, when
+ * a playback event occurs on the internal playback thread, a message is delivered to the
+ * application thread via a second message queue. The application thread consumes messages
+ * from the queue, updating the application visible state and calling corresponding listener
+ * methods.
+ *
- Injected player components may use additional background threads. For example a MediaSource
+ * may use background threads to load data. These are implementation specific.
*
*/
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.
- *
- *
- *
+ *
+ *
*/
-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 extends Comparable super T>> 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();