diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 434a0249df..2c10bfe6a0 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.os.Handler; import android.os.HandlerThread; +import android.util.Pair; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.MediaPeriod; 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 java.io.IOException; import java.util.ArrayList; +import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -70,8 +72,7 @@ public final class ExoPlayerTest extends TestCase { assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -89,9 +90,8 @@ public final class ExoPlayerTest extends TestCase { assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertEquals(manifest, playerWrapper.manifest); 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(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -163,8 +162,60 @@ public final class ExoPlayerTest extends TestCase { assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); assertTrue(audioRenderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); + } + + 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 final CountDownLatch sourceInfoCountDownLatch; private final CountDownLatch endedCountDownLatch; private final HandlerThread playerThread; private final Handler handler; + private final LinkedList> sourceInfos; private ExoPlayer player; - private Timeline timeline; - private Object manifest; private TrackGroupArray trackGroups; private Exception exception; @@ -186,17 +237,19 @@ public final class ExoPlayerTest extends TestCase { private volatile int positionDiscontinuityCount; public PlayerWrapper() { + sourceInfoCountDownLatch = new CountDownLatch(1); endedCountDownLatch = new CountDownLatch(1); playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); handler = new Handler(playerThread.getLooper()); + sourceInfos = new LinkedList<>(); } // Called on the test thread. public void blockUntilEnded(long timeoutMs) throws Exception { 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(); // 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) { handler.post(new Runnable() { @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 { handler.post(new Runnable() { @Override @@ -246,6 +318,14 @@ public final class ExoPlayerTest extends TestCase { endedCountDownLatch.countDown(); } + @SafeVarargs + public final void assertSourceInfosEquals(Pair... sourceInfos) { + assertEquals(sourceInfos.length, this.sourceInfos.size()); + for (Pair sourceInfo : sourceInfos) { + assertEquals(sourceInfo, this.sourceInfos.remove()); + } + } + // ExoPlayer.EventListener implementation. @Override @@ -262,8 +342,8 @@ public final class ExoPlayerTest extends TestCase { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - this.timeline = timeline; - this.manifest = manifest; + sourceInfos.add(Pair.create(timeline, manifest)); + sourceInfoCountDownLatch.countDown(); } @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 * 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 Object manifest; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b9ab94a543..4131b97954 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -53,6 +53,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean playWhenReady; private int playbackState; private int pendingSeekAcks; + private int pendingPrepareAcks; private boolean isLoading; private Timeline timeline; private Object manifest; @@ -142,6 +143,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -310,18 +312,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isCurrentWindowDynamic() { - if (timeline.isEmpty()) { - return false; - } - return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; } @Override public boolean isCurrentWindowSeekable() { - if (timeline.isEmpty()) { - return false; - } - return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; } @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. /* package */ void handleEvent(Message msg) { switch (msg.what) { + case ExoPlayerImplInternal.MSG_PREPARE_ACK: { + pendingPrepareAcks--; + break; + } case ExoPlayerImplInternal.MSG_STATE_CHANGED: { playbackState = msg.arg1; for (EventListener listener : listeners) { @@ -372,13 +372,15 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); + if (pendingPrepareAcks == 0) { + TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; + tracksSelected = true; + trackGroups = trackSelectorResult.groups; + trackSelections = trackSelectorResult.selections; + trackSelector.onSelectionActivated(trackSelectorResult.info); + for (EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } } break; } @@ -404,12 +406,14 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { SourceInfo sourceInfo = (SourceInfo) msg.obj; - timeline = sourceInfo.timeline; - manifest = sourceInfo.manifest; - playbackInfo = sourceInfo.playbackInfo; pendingSeekAcks -= sourceInfo.seekAcks; - for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline, manifest); + if (pendingPrepareAcks == 0) { + timeline = sourceInfo.timeline; + manifest = sourceInfo.manifest; + playbackInfo = sourceInfo.playbackInfo; + for (EventListener listener : listeners) { + listener.onTimelineChanged(timeline, manifest); + } } break; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f6fa0d39ac..916f5f6657 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -90,6 +90,7 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages + public static final int MSG_PREPARE_ACK = 0; public static final int MSG_STATE_CHANGED = 1; public static final int MSG_LOADING_CHANGED = 2; public static final int MSG_TRACKS_CHANGED = 3; @@ -383,6 +384,7 @@ import java.io.IOException; } private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { + eventHandler.sendEmptyMessage(MSG_PREPARE_ACK); resetInternal(true); loadControl.onPrepared(); if (resetPosition) {