From 1331f7adde9b140215ec92b90bea0abcadf94915 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 Nov 2017 03:14:22 -0800 Subject: [PATCH] Add custom callbacks to allows seeks after dynamic playlist modifications. These callbacks are executed on the app thread after the corresponding timeline update was triggered. This ensures that seek operations see the updated timelines and are therefore valid, even if the seek is performed into a window which didn't exist before. GitHub:#3407 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175136187 --- .../DynamicConcatenatingMediaSourceTest.java | 226 ++++++++++++++++- .../DynamicConcatenatingMediaSource.java | 228 ++++++++++++++++-- 2 files changed, 428 insertions(+), 26 deletions(-) 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;