diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 61eebbc15a5..c0c5252751c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.source; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; @@ -40,6 +44,7 @@ import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; +import org.mockito.Mockito; /** * Unit tests for {@link DynamicConcatenatingMediaSource} @@ -50,6 +55,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private Timeline timeline; private boolean timelineUpdated; + private boolean customRunnableCalled; public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; @@ -371,6 +377,180 @@ public void testIllegalArguments() { } } + public void testCustomCallbackBeforePreparationAddSingle() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultiple() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSource(createFakeMediaSource()); + + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationMove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), + runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationMove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + runnable); + } + }); + waitForCustomRunnable(); + } + + private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() + throws InterruptedException { + HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); + handlerThread.start(); + Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); + when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); + Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + handler.post(new Runnable() { + @Override + public void run() { + prepareAndListenToTimelineUpdates(mediaSource); + } + }); + waitForTimelineUpdate(); + return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); + } + private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { @Override @@ -385,16 +565,41 @@ public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Obje } private synchronized void waitForTimelineUpdate() throws InterruptedException { - long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS; + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; while (!timelineUpdated) { wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= timeoutMs) { + if (System.currentTimeMillis() >= deadlineMs) { fail("No timeline update occurred within timeout."); } } timelineUpdated = false; } + private Runnable createCustomRunnable() { + return new Runnable() { + @Override + public void run() { + synchronized (DynamicConcatenatingMediaSourceTest.this) { + assertTrue(timelineUpdated); + timelineUpdated = false; + customRunnableCalled = true; + DynamicConcatenatingMediaSourceTest.this.notify(); + } + } + }; + } + + private synchronized void waitForCustomRunnable() throws InterruptedException { + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; + while (!customRunnableCalled) { + wait(TIMEOUT_MS); + if (System.currentTimeMillis() >= deadlineMs) { + fail("No custom runnable call occurred within timeout."); + } + } + customRunnableCalled = false; + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -403,6 +608,10 @@ private static FakeMediaSource[] createMediaSources(int count) { return sources; } + private static FakeMediaSource createFakeMediaSource() { + return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + } + private static FakeTimeline createFakeTimeline(int index) { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } @@ -429,6 +638,19 @@ public void onContinueLoadingRequested(MediaPeriod source) {} } } + private static class DynamicConcatenatingMediaSourceAndHandler { + + public final DynamicConcatenatingMediaSource mediaSource; + public final Handler handler; + + public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, + Handler handler) { + this.mediaSource = mediaSource; + this.handler = handler; + } + + } + private static class LazyMediaSource implements MediaSource { private Listener listener; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 92a117ce4e3..6bfa4047a5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.NonNull; -import android.util.Pair; +import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -47,6 +49,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private static final int MSG_ADD_MULTIPLE = 1; private static final int MSG_REMOVE = 2; private static final int MSG_MOVE = 3; + private static final int MSG_ON_COMPLETION = 4; // Accessed on the app thread. private final List mediaSourcesPublic; @@ -95,7 +98,22 @@ public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) { * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(MediaSource mediaSource) { - addMediaSource(mediaSourcesPublic.size(), mediaSource); + addMediaSource(mediaSourcesPublic.size(), mediaSource, null); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); } /** @@ -109,11 +127,31 @@ public synchronized void addMediaSource(MediaSource mediaSource) { * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(int index, MediaSource mediaSource) { + addMediaSource(index, mediaSource, null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource))); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, + new MessageData<>(index, mediaSource, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -127,7 +165,24 @@ public synchronized void addMediaSource(int index, MediaSource mediaSource) { * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(Collection mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources); + addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(Collection mediaSources, + @Nullable Runnable actionOnCompletion) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); } /** @@ -142,6 +197,24 @@ public synchronized void addMediaSources(Collection mediaSources) { * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(int index, Collection mediaSources) { + addMediaSources(index, mediaSources, null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(int index, Collection mediaSources, + @Nullable Runnable actionOnCompletion) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); @@ -149,7 +222,9 @@ public synchronized void addMediaSources(int index, Collection medi mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - Pair.create(index, mediaSources))); + new MessageData<>(index, mediaSources, actionOnCompletion))); + } else if (actionOnCompletion != null){ + actionOnCompletion.run(); } } @@ -164,9 +239,28 @@ public synchronized void addMediaSources(int index, Collection medi * range of 0 <= index < {@link #getSize()}. */ public synchronized void removeMediaSource(int index) { + removeMediaSource(index, null); + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + *

+ * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being + * removed should not be re-added. If you want to move the instance use + * {@link #moveMediaSource(int, int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + */ + public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index)); + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, + new MessageData<>(index, null, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -179,13 +273,31 @@ public synchronized void removeMediaSource(int index) { * range of 0 <= index < {@link #getSize()}. */ public synchronized void moveMediaSource(int currentIndex, int newIndex) { + moveMediaSource(currentIndex, newIndex, null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public synchronized void moveMediaSource(int currentIndex, int newIndex, + @Nullable Runnable actionOnCompletion) { if (currentIndex == newIndex) { return; } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - Pair.create(currentIndex, newIndex))); + new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -215,7 +327,7 @@ public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSourc shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(null); } @Override @@ -263,31 +375,42 @@ public void releaseSource() { @Override @SuppressWarnings("unchecked") public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_ON_COMPLETION) { + ((EventDispatcher) message).dispatchEvent(); + return; + } preventListenerNotification = true; + EventDispatcher actionOnCompletion; switch (messageType) { case MSG_ADD: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, 1); - addMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, 1); + addMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_ADD_MULTIPLE: { - Pair> messageData = - (Pair>) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, messageData.second.size()); - addMediaSourcesInternal(messageData.first, messageData.second); + MessageData> messageData = + (MessageData>) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, + messageData.customData.size()); + addMediaSourcesInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_REMOVE: { - shuffleOrder = shuffleOrder.cloneAndRemove((Integer) message); - removeMediaSourceInternal((Integer) message); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + removeMediaSourceInternal(messageData.index); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_MOVE: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.first); - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.second, 1); - moveMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.customData, 1); + moveMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } default: { @@ -295,14 +418,18 @@ public void handleMessage(int messageType, Object message) throws ExoPlaybackExc } } preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(actionOnCompletion); } - private void maybeNotifyListener() { + private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { if (!preventListenerNotification) { listener.onSourceInfoRefreshed(this, new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); + if (actionOnCompletion != null) { + player.sendMessages( + new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + } } } @@ -359,7 +486,7 @@ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Time } } mediaSourceHolder.isPrepared = true; - maybeNotifyListener(); + maybeNotifyListener(null); } private void removeMediaSourceInternal(int index) { @@ -407,6 +534,9 @@ private int findMediaSourceHolderByPeriodIndex(int periodIndex) { return index; } + /** + * Data class to hold playlist media sources together with meta data needed to process them. + */ private static final class MediaSourceHolder implements Comparable { public final MediaSource mediaSource; @@ -432,6 +562,47 @@ public int compareTo(@NonNull MediaSourceHolder other) { } } + /** + * Can be used to dispatch a runnable on the thread the object was created on. + */ + private static final class EventDispatcher { + + public final Handler eventHandler; + public final Runnable runnable; + + public EventDispatcher(Runnable runnable) { + this.runnable = runnable; + this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() + : Looper.getMainLooper()); + } + + public void dispatchEvent() { + eventHandler.post(runnable); + } + + } + + /** + * Message used to post actions from app thread to playback thread. + */ + private static final class MessageData { + + public final int index; + public final CustomType customData; + public final @Nullable EventDispatcher actionOnCompletion; + + public MessageData(int index, CustomType customData, @Nullable Runnable actionOnCompletion) { + this.index = index; + this.actionOnCompletion = actionOnCompletion != null + ? new EventDispatcher(actionOnCompletion) : null; + this.customData = customData; + } + + } + + /** + * Timeline exposing concatenated timelines of playlist media sources. + */ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { private final int windowCount; @@ -514,6 +685,10 @@ public int getPeriodCount() { } + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the + * DeferredTimeline is used to keep the originally assigned first period ID. + */ private static final class DeferredTimeline extends Timeline { private static final Object DUMMY_ID = new Object(); @@ -582,6 +757,11 @@ public int getIndexOfPeriod(Object uid) { } + /** + * Media period used for periods created from unprepared media sources exposed through + * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes + * available. + */ private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { public final MediaSource mediaSource;