On re-preparation, suppress source info refresh until ack'ed

ExoPlayerImpl.prepare() replaces the timeline with an empty timeline. After this
happens, MSG_SOURCE_INFO_REFRESHED could be handled on the main thread and
could relate to the old source, so the player could expose a stale timeline.

Count pending prepares in ExoPlayerImpl so that source info refreshes can be
suppressed until preparation actually completes.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=152496255
This commit is contained in:
andrewlewis 2017-04-07 07:05:27 -07:00 committed by Oliver Woodman
parent cf4358b8fa
commit d8c71df255
3 changed files with 120 additions and 34 deletions

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.util.Pair;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
@ -32,6 +33,7 @@ import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -70,8 +72,7 @@ public final class ExoPlayerTest extends TestCase {
assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.formatReadCount);
assertEquals(0, renderer.bufferReadCount); assertEquals(0, renderer.bufferReadCount);
assertFalse(renderer.isEnded); assertFalse(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline); playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
assertNull(playerWrapper.manifest);
} }
/** /**
@ -89,9 +90,8 @@ public final class ExoPlayerTest extends TestCase {
assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount); assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded); assertTrue(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline);
assertEquals(manifest, playerWrapper.manifest);
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest));
} }
/** /**
@ -111,8 +111,7 @@ public final class ExoPlayerTest extends TestCase {
assertEquals(3, renderer.formatReadCount); assertEquals(3, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount); assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded); assertTrue(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline); playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
assertNull(playerWrapper.manifest);
} }
/** /**
@ -163,8 +162,60 @@ public final class ExoPlayerTest extends TestCase {
assertEquals(1, audioRenderer.positionResetCount); assertEquals(1, audioRenderer.positionResetCount);
assertTrue(videoRenderer.isEnded); assertTrue(videoRenderer.isEnded);
assertTrue(audioRenderer.isEnded); assertTrue(audioRenderer.isEnded);
assertEquals(timeline, playerWrapper.timeline); playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
assertNull(playerWrapper.manifest); }
public void testRepreparationGivesFreshSourceInfo() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
// Prepare the player with a source with the first manifest and a non-empty timeline
Object firstSourceManifest = new Object();
playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT),
renderer);
playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS);
// Prepare the player again with a source and a new manifest, which will never be exposed.
final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) {
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
super.prepareSource(player, isTopLevelSource, listener);
// We've queued a source info refresh on the playback thread's event queue. Allow the test
// thread to prepare the player with the third source, and block this thread (the playback
// thread) until the test thread's call to prepare() has returned.
queuedSourceInfoCountDownLatch.countDown();
try {
completePreparationCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
});
// Prepare the player again with a third source.
queuedSourceInfoCountDownLatch.await();
Object thirdSourceManifest = new Object();
playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT));
completePreparationCountDownLatch.countDown();
// Wait for playback to complete.
playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(0, playerWrapper.positionDiscontinuityCount);
assertEquals(1, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded);
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
// The first source's preparation completed with a non-empty timeline. When the player was
// re-prepared with the second source, it immediately exposed an empty timeline, but the source
// info refresh from the second source was suppressed as we re-prepared with the third source.
playerWrapper.assertSourceInfosEquals(
Pair.create(timeline, firstSourceManifest),
Pair.create(Timeline.EMPTY, null),
Pair.create(timeline, thirdSourceManifest));
} }
/** /**
@ -172,13 +223,13 @@ public final class ExoPlayerTest extends TestCase {
*/ */
private static final class PlayerWrapper implements ExoPlayer.EventListener { private static final class PlayerWrapper implements ExoPlayer.EventListener {
private final CountDownLatch sourceInfoCountDownLatch;
private final CountDownLatch endedCountDownLatch; private final CountDownLatch endedCountDownLatch;
private final HandlerThread playerThread; private final HandlerThread playerThread;
private final Handler handler; private final Handler handler;
private final LinkedList<Pair<Timeline, Object>> sourceInfos;
private ExoPlayer player; private ExoPlayer player;
private Timeline timeline;
private Object manifest;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private Exception exception; private Exception exception;
@ -186,17 +237,19 @@ public final class ExoPlayerTest extends TestCase {
private volatile int positionDiscontinuityCount; private volatile int positionDiscontinuityCount;
public PlayerWrapper() { public PlayerWrapper() {
sourceInfoCountDownLatch = new CountDownLatch(1);
endedCountDownLatch = new CountDownLatch(1); endedCountDownLatch = new CountDownLatch(1);
playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread = new HandlerThread("ExoPlayerTest thread");
playerThread.start(); playerThread.start();
handler = new Handler(playerThread.getLooper()); handler = new Handler(playerThread.getLooper());
sourceInfos = new LinkedList<>();
} }
// Called on the test thread. // Called on the test thread.
public void blockUntilEnded(long timeoutMs) throws Exception { public void blockUntilEnded(long timeoutMs) throws Exception {
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
exception = new TimeoutException("Test playback timed out."); exception = new TimeoutException("Test playback timed out waiting for playback to end.");
} }
release(); release();
// Throw any pending exception (from playback, timing out or releasing). // Throw any pending exception (from playback, timing out or releasing).
@ -205,6 +258,12 @@ public final class ExoPlayerTest extends TestCase {
} }
} }
public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Test playback timed out waiting for source info.");
}
}
public void setup(final MediaSource mediaSource, final Renderer... renderers) { public void setup(final MediaSource mediaSource, final Renderer... renderers) {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
@ -221,6 +280,19 @@ public final class ExoPlayerTest extends TestCase {
}); });
} }
public void prepare(final MediaSource mediaSource) {
handler.post(new Runnable() {
@Override
public void run() {
try {
player.prepare(mediaSource);
} catch (Exception e) {
handleError(e);
}
}
});
}
public void release() throws InterruptedException { public void release() throws InterruptedException {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
@ -246,6 +318,14 @@ public final class ExoPlayerTest extends TestCase {
endedCountDownLatch.countDown(); endedCountDownLatch.countDown();
} }
@SafeVarargs
public final void assertSourceInfosEquals(Pair<Timeline, Object>... sourceInfos) {
assertEquals(sourceInfos.length, this.sourceInfos.size());
for (Pair<Timeline, Object> sourceInfo : sourceInfos) {
assertEquals(sourceInfo, this.sourceInfos.remove());
}
}
// ExoPlayer.EventListener implementation. // ExoPlayer.EventListener implementation.
@Override @Override
@ -262,8 +342,8 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
this.timeline = timeline; sourceInfos.add(Pair.create(timeline, manifest));
this.manifest = manifest; sourceInfoCountDownLatch.countDown();
} }
@Override @Override
@ -352,7 +432,7 @@ public final class ExoPlayerTest extends TestCase {
* Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
* the period will return a {@link FakeMediaPeriod}. * the period will return a {@link FakeMediaPeriod}.
*/ */
private static final class FakeMediaSource implements MediaSource { private static class FakeMediaSource implements MediaSource {
private final Timeline timeline; private final Timeline timeline;
private final Object manifest; private final Object manifest;

View File

@ -53,6 +53,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean playWhenReady; private boolean playWhenReady;
private int playbackState; private int playbackState;
private int pendingSeekAcks; private int pendingSeekAcks;
private int pendingPrepareAcks;
private boolean isLoading; private boolean isLoading;
private Timeline timeline; private Timeline timeline;
private Object manifest; private Object manifest;
@ -142,6 +143,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
} }
} }
pendingPrepareAcks++;
internalPlayer.prepare(mediaSource, resetPosition); internalPlayer.prepare(mediaSource, resetPosition);
} }
@ -310,18 +312,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override @Override
public boolean isCurrentWindowDynamic() { public boolean isCurrentWindowDynamic() {
if (timeline.isEmpty()) { return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
} }
@Override @Override
public boolean isCurrentWindowSeekable() { public boolean isCurrentWindowSeekable() {
if (timeline.isEmpty()) { return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
} }
@Override @Override
@ -357,6 +353,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
// Not private so it can be called from an inner class without going through a thunk method. // Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) { /* package */ void handleEvent(Message msg) {
switch (msg.what) { switch (msg.what) {
case ExoPlayerImplInternal.MSG_PREPARE_ACK: {
pendingPrepareAcks--;
break;
}
case ExoPlayerImplInternal.MSG_STATE_CHANGED: { case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
playbackState = msg.arg1; playbackState = msg.arg1;
for (EventListener listener : listeners) { for (EventListener listener : listeners) {
@ -372,13 +372,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
break; break;
} }
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; if (pendingPrepareAcks == 0) {
tracksSelected = true; TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
trackGroups = trackSelectorResult.groups; tracksSelected = true;
trackSelections = trackSelectorResult.selections; trackGroups = trackSelectorResult.groups;
trackSelector.onSelectionActivated(trackSelectorResult.info); trackSelections = trackSelectorResult.selections;
for (EventListener listener : listeners) { trackSelector.onSelectionActivated(trackSelectorResult.info);
listener.onTracksChanged(trackGroups, trackSelections); for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
} }
break; break;
} }
@ -404,12 +406,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
SourceInfo sourceInfo = (SourceInfo) msg.obj; SourceInfo sourceInfo = (SourceInfo) msg.obj;
timeline = sourceInfo.timeline;
manifest = sourceInfo.manifest;
playbackInfo = sourceInfo.playbackInfo;
pendingSeekAcks -= sourceInfo.seekAcks; pendingSeekAcks -= sourceInfo.seekAcks;
for (EventListener listener : listeners) { if (pendingPrepareAcks == 0) {
listener.onTimelineChanged(timeline, manifest); timeline = sourceInfo.timeline;
manifest = sourceInfo.manifest;
playbackInfo = sourceInfo.playbackInfo;
for (EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}
} }
break; break;
} }

View File

@ -90,6 +90,7 @@ import java.io.IOException;
private static final String TAG = "ExoPlayerImplInternal"; private static final String TAG = "ExoPlayerImplInternal";
// External messages // External messages
public static final int MSG_PREPARE_ACK = 0;
public static final int MSG_STATE_CHANGED = 1; public static final int MSG_STATE_CHANGED = 1;
public static final int MSG_LOADING_CHANGED = 2; public static final int MSG_LOADING_CHANGED = 2;
public static final int MSG_TRACKS_CHANGED = 3; public static final int MSG_TRACKS_CHANGED = 3;
@ -383,6 +384,7 @@ import java.io.IOException;
} }
private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
eventHandler.sendEmptyMessage(MSG_PREPARE_ACK);
resetInternal(true); resetInternal(true);
loadControl.onPrepared(); loadControl.onPrepared();
if (resetPosition) { if (resetPosition) {