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
This commit is contained in:
parent
3b633f81b2
commit
ec71c05e8b
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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<Integer> 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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}.
|
||||
*
|
||||
* <h3>Player components</h3>
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A <b>{@link MediaSource}</b> 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}).</li>
|
||||
* 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}).
|
||||
* <li><b>{@link Renderer}</b>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.</li>
|
||||
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
|
||||
* Renderer consumes media from the MediaSource being played. Renderers are injected when the
|
||||
* player is created.
|
||||
* <li>A <b>{@link TrackSelector}</b> 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.</li>
|
||||
* ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
|
||||
* when the player is created.
|
||||
* <li>A <b>{@link LoadControl}</b> 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.</li>
|
||||
* 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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <h3>Threading model</h3>
|
||||
* <p>The figure below shows ExoPlayer's threading model.</p>
|
||||
* <p align="center">
|
||||
* <img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading model">
|
||||
* </p>
|
||||
*
|
||||
* <p>The figure below shows ExoPlayer's threading model.
|
||||
*
|
||||
* <p align="center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading
|
||||
* model">
|
||||
*
|
||||
* <ul>
|
||||
* <li>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.</li>
|
||||
* <li>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.
|
||||
* <li>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.</li>
|
||||
* 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.
|
||||
* <li>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.</li>
|
||||
* thread.
|
||||
* <li>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.</li>
|
||||
* 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.
|
||||
* <li>Injected player components may use additional background threads. For example a MediaSource
|
||||
* may use background threads to load data. These are implementation specific.</li>
|
||||
* may use background threads to load data. These are implementation specific.
|
||||
* </ul>
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
/**
|
||||
|
@ -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<Player.EventListener> 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<PlayerMessage> 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
|
||||
|
@ -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<CustomMessageInfo> 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;
|
||||
}
|
||||
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<Integer, Long> periodPosition = resolveSeekPosition(seekPosition);
|
||||
Pair<Integer, Long> 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.
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
customMessagesProcessed++;
|
||||
notifyAll();
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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<Integer, Long> 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<Integer, Long> periodPosition = resolveSeekPosition(pendingInitialSeekPosition);
|
||||
Pair<Integer, Long> 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<Integer, Long> resolveSeekPosition(SeekPosition seekPosition) {
|
||||
private Pair<Integer, Long> 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);
|
||||
}
|
||||
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);
|
||||
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<CustomMessageInfo> {
|
||||
|
||||
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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
@ -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}.
|
||||
* <p>
|
||||
* Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
|
||||
*
|
||||
* <p>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.
|
||||
* <p align="center">
|
||||
* <img src="doc-files/renderer-states.svg" alt="Renderer state transitions">
|
||||
* </p>
|
||||
*
|
||||
* <p align="center"><img src="doc-files/renderer-states.svg" alt="Renderer state transitions">
|
||||
*/
|
||||
public interface Renderer extends ExoPlayerComponent {
|
||||
public interface Renderer extends PlayerMessage.Target {
|
||||
|
||||
/**
|
||||
* The renderer is disabled.
|
||||
|
@ -93,8 +93,6 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
|
||||
private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
|
||||
private final CopyOnWriteArraySet<AudioRendererEventListener> 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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
Pair<PlayerMessage, Listener> messageAndListener = (Pair<PlayerMessage, Listener>) msg.obj;
|
||||
try {
|
||||
message.target.handleMessage(message.messageType, message.message);
|
||||
messageAndListener
|
||||
.first
|
||||
.getTarget()
|
||||
.handleMessage(
|
||||
messageAndListener.first.getType(), messageAndListener.first.getMessage());
|
||||
messageAndListener.second.onMessageDelivered();
|
||||
messageAndListener.second.onMessageDeleted();
|
||||
} catch (ExoPlaybackException e) {
|
||||
fail("Unexpected ExoPlaybackException.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user