Add dynamic concatenating media source.

(GitHub issue #1706)

The media source allows adding or removing child sources before and after prepare() was called.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160516636
This commit is contained in:
tonihei 2017-06-29 06:01:49 -07:00 committed by Oliver Woodman
parent a98d5bbd0a
commit 69db6cb60b
3 changed files with 1144 additions and 7 deletions

View file

@ -63,7 +63,7 @@ public class TimelineTest extends TestCase {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0);
return period.set(periodIndex, null, 0, WINDOW_DURATION_US, 0);
}
@Override
@ -148,12 +148,33 @@ public class TimelineTest extends TestCase {
this.timeline = timeline;
}
public TimelineVerifier assertWindowIds(int... expectedWindowIds) {
public TimelineVerifier assertEmpty() {
assertWindowIds();
assertPeriodCounts();
return this;
}
/**
* @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important
* {@code null} can be passed to skip this window.
*/
public TimelineVerifier assertWindowIds(Object... expectedWindowIds) {
Window window = new Window();
assertEquals(expectedWindowIds.length, timeline.getWindowCount());
for (int i = 0; i < timeline.getWindowCount(); i++) {
timeline.getWindow(i, window, true);
assertEquals(expectedWindowIds[i], window.id);
if (expectedWindowIds[i] != null) {
assertEquals(expectedWindowIds[i], window.id);
}
}
return this;
}
public TimelineVerifier assertWindowIsDynamic(boolean... windowIsDynamic) {
Window window = new Window();
for (int i = 0; i < timeline.getWindowCount(); i++) {
timeline.getWindow(i, window, true);
assertEquals(windowIsDynamic[i], window.isDynamic);
}
return this;
}
@ -199,7 +220,6 @@ public class TimelineTest extends TestCase {
expectedWindowIndex++;
}
assertEquals(expectedWindowIndex, period.windowIndex);
assertEquals(i - accumulatedPeriodCounts[expectedWindowIndex], ((int[]) period.id)[1]);
if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window,
ExoPlayer.REPEAT_MODE_OFF));
@ -233,9 +253,7 @@ public class TimelineTest extends TestCase {
}
public void testEmptyTimeline() {
new TimelineVerifier(Timeline.EMPTY)
.assertWindowIds()
.assertPeriodCounts();
new TimelineVerifier(Timeline.EMPTY).assertEmpty();
}
public void testSinglePeriodTimeline() {

View file

@ -0,0 +1,532 @@
/*
* 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.source;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TimelineTest;
import com.google.android.exoplayer2.TimelineTest.FakeTimeline;
import com.google.android.exoplayer2.TimelineTest.StubMediaSource;
import com.google.android.exoplayer2.TimelineTest.TimelineVerifier;
import com.google.android.exoplayer2.source.MediaSource.Listener;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
import java.util.Arrays;
import junit.framework.TestCase;
/**
* Unit tests for {@link DynamicConcatenatingMediaSource}
*/
public final class DynamicConcatenatingMediaSourceTest extends TestCase {
private static final int TIMEOUT_MS = 10000;
private Timeline timeline;
private boolean timelineUpdated;
public void testPlaylistChangesAfterPreparation() throws InterruptedException {
timeline = null;
TimelineTest.StubMediaSource[] childSources = createMediaSources(7);
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
new TimelineVerifier(timeline).assertEmpty();
// Add first source.
mediaSource.addMediaSource(childSources[0]);
waitForTimelineUpdate();
assertNotNull(timeline);
new TimelineVerifier(timeline)
.assertPeriodCounts(1)
.assertWindowIds(111);
// Add at front of queue.
mediaSource.addMediaSource(0, childSources[1]);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(2, 1)
.assertWindowIds(222, 111);
// Add at back of queue.
mediaSource.addMediaSource(childSources[2]);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(2, 1, 3)
.assertWindowIds(222, 111, 333);
// Add in the middle.
mediaSource.addMediaSource(1, childSources[3]);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(2, 4, 1, 3)
.assertWindowIds(222, 444, 111, 333);
// Add bulk.
mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4],
(MediaSource) childSources[5], (MediaSource) childSources[6]));
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(2, 4, 1, 5, 6, 7, 3)
.assertWindowIds(222, 444, 111, 555, 666, 777, 333);
// Remove in the middle.
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(3);
waitForTimelineUpdate();
mediaSource.removeMediaSource(1);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(2, 1, 3)
.assertWindowIds(222, 111, 333);
for (int i = 3; i <= 6; i++) {
childSources[i].assertReleased();
}
// Remove at front of queue.
mediaSource.removeMediaSource(0);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(1, 3)
.assertWindowIds(111, 333);
childSources[1].assertReleased();
// Remove at back of queue.
mediaSource.removeMediaSource(1);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(1)
.assertWindowIds(111);
childSources[2].assertReleased();
// Remove last source.
mediaSource.removeMediaSource(0);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts()
.assertWindowIds();
childSources[3].assertReleased();
}
public void testPlaylistChangesBeforePreparation() throws InterruptedException {
timeline = null;
TimelineTest.StubMediaSource[] childSources = createMediaSources(4);
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
mediaSource.addMediaSource(childSources[0]);
mediaSource.addMediaSource(childSources[1]);
mediaSource.addMediaSource(0, childSources[2]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, childSources[3]);
assertNull(timeline);
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
assertNotNull(timeline);
new TimelineVerifier(timeline)
.assertPeriodCounts(3, 4, 2)
.assertWindowIds(333, 444, 222);
mediaSource.releaseSource();
for (int i = 1; i < 4; i++) {
childSources[i].assertReleased();
}
}
public void testPlaylistWithLazyMediaSource() throws InterruptedException {
timeline = null;
TimelineTest.StubMediaSource[] childSources = createMediaSources(2);
LazyMediaSource[] lazySources = new LazyMediaSource[4];
for (int i = 0; i < 4; i++) {
lazySources[i] = new LazyMediaSource();
}
//Add lazy sources before preparation
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
mediaSource.addMediaSource(lazySources[0]);
mediaSource.addMediaSource(0, childSources[0]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, lazySources[1]);
assertNull(timeline);
prepareAndListenToTimelineUpdates(mediaSource);
waitForTimelineUpdate();
assertNotNull(timeline);
new TimelineVerifier(timeline)
.assertPeriodCounts(1, 1)
.assertWindowIds(111, null)
.assertWindowIsDynamic(false, true);
lazySources[1].triggerTimelineUpdate(new FakeTimeline(9, 999));
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(1, 9)
.assertWindowIds(111, 999)
.assertWindowIsDynamic(false, false);
//Add lazy sources after preparation
mediaSource.addMediaSource(1, lazySources[2]);
waitForTimelineUpdate();
mediaSource.addMediaSource(2, childSources[1]);
waitForTimelineUpdate();
mediaSource.addMediaSource(0, lazySources[3]);
waitForTimelineUpdate();
mediaSource.removeMediaSource(2);
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(1, 1, 2, 9)
.assertWindowIds(null, 111, 222, 999)
.assertWindowIsDynamic(true, false, false, false);
lazySources[3].triggerTimelineUpdate(new FakeTimeline(8, 888));
waitForTimelineUpdate();
new TimelineVerifier(timeline)
.assertPeriodCounts(8, 1, 2, 9)
.assertWindowIds(888, 111, 222, 999)
.assertWindowIsDynamic(false, false, false, false);
mediaSource.releaseSource();
childSources[0].assertReleased();
childSources[1].assertReleased();
}
public void testIllegalArguments() {
DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource();
MediaSource validSource = new StubMediaSource(new FakeTimeline(1, 1));
// Null sources.
try {
mediaSource.addMediaSource(null);
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
MediaSource[] mediaSources = { validSource, null };
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
// Duplicate sources.
mediaSource.addMediaSource(validSource);
try {
mediaSource.addMediaSource(validSource);
fail("Duplicate mediaSource not allowed.");
} catch (IllegalArgumentException e) {
// Expected.
}
mediaSources = new MediaSource[] { new StubMediaSource(new FakeTimeline(1, 1)), validSource};
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Duplicate mediaSource not allowed.");
} catch (IllegalArgumentException e) {
// Expected.
}
}
private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) {
mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
timeline = newTimeline;
synchronized (DynamicConcatenatingMediaSourceTest.this) {
timelineUpdated = true;
DynamicConcatenatingMediaSourceTest.this.notify();
}
}
});
}
private synchronized void waitForTimelineUpdate() throws InterruptedException {
long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS;
while (!timelineUpdated) {
wait(TIMEOUT_MS);
if (System.currentTimeMillis() >= timeoutMs) {
fail("No timeline update occurred within timeout.");
}
}
timelineUpdated = false;
}
private TimelineTest.StubMediaSource[] createMediaSources(int count) {
TimelineTest.StubMediaSource[] sources = new TimelineTest.StubMediaSource[count];
for (int i = 0; i < count; i++) {
sources[i] = new TimelineTest.StubMediaSource(new FakeTimeline(i + 1, (i + 1) * 111));
}
return sources;
}
private static class LazyMediaSource implements MediaSource {
private Listener listener;
public void triggerTimelineUpdate(Timeline timeline) {
listener.onSourceInfoRefreshed(timeline, null);
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
this.listener = listener;
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return null;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
}
@Override
public void releaseSource() {
}
}
/**
* Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread.
*/
private static class StubExoPlayer implements ExoPlayer, Handler.Callback {
private final Handler handler;
public StubExoPlayer() {
HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper(), this);
}
@Override
public Looper getPlaybackLooper() {
throw new UnsupportedOperationException();
}
@Override
public void addListener(EventListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void removeListener(EventListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int getPlaybackState() {
throw new UnsupportedOperationException();
}
@Override
public void prepare(MediaSource mediaSource) {
throw new UnsupportedOperationException();
}
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
throw new UnsupportedOperationException();
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
throw new UnsupportedOperationException();
}
@Override
public boolean getPlayWhenReady() {
throw new UnsupportedOperationException();
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
throw new UnsupportedOperationException();
}
@Override
public int getRepeatMode() {
throw new UnsupportedOperationException();
}
@Override
public boolean isLoading() {
throw new UnsupportedOperationException();
}
@Override
public void seekToDefaultPosition() {
throw new UnsupportedOperationException();
}
@Override
public void seekToDefaultPosition(int windowIndex) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void seekTo(int windowIndex, long positionMs) {
throw new UnsupportedOperationException();
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
throw new UnsupportedOperationException();
}
@Override
public PlaybackParameters getPlaybackParameters() {
throw new UnsupportedOperationException();
}
@Override
public void stop() {
throw new UnsupportedOperationException();
}
@Override
public void release() {
throw new UnsupportedOperationException();
}
@Override
public void sendMessages(ExoPlayerMessage... messages) {
handler.obtainMessage(0, messages).sendToTarget();
}
@Override
public void blockingSendMessages(ExoPlayerMessage... messages) {
throw new UnsupportedOperationException();
}
@Override
public int getRendererCount() {
throw new UnsupportedOperationException();
}
@Override
public int getRendererType(int index) {
throw new UnsupportedOperationException();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
throw new UnsupportedOperationException();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
throw new UnsupportedOperationException();
}
@Override
public Object getCurrentManifest() {
throw new UnsupportedOperationException();
}
@Override
public Timeline getCurrentTimeline() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentPeriodIndex() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentWindowIndex() {
throw new UnsupportedOperationException();
}
@Override
public long getDuration() {
throw new UnsupportedOperationException();
}
@Override
public long getCurrentPosition() {
throw new UnsupportedOperationException();
}
@Override
public long getBufferedPosition() {
throw new UnsupportedOperationException();
}
@Override
public int getBufferedPercentage() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentWindowDynamic() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentWindowSeekable() {
throw new UnsupportedOperationException();
}
@Override
public boolean isPlayingAd() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentAdGroupIndex() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentAdIndexInAdGroup() {
throw new UnsupportedOperationException();
}
@Override
public boolean handleMessage(Message msg) {
ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj;
for (ExoPlayerMessage message : messages) {
try {
message.target.handleMessage(message.messageType, message.message);
} catch (ExoPlaybackException e) {
fail("Unexpected ExoPlaybackException.");
}
}
return true;
}
}
}

View file

@ -0,0 +1,587 @@
/*
* 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.source;
import android.util.Pair;
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.Timeline;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
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 {
private static final int MSG_ADD = 0;
private static final int MSG_ADD_MULTIPLE = 1;
private static final int MSG_REMOVE = 2;
// Accessed on the app thread.
private final List<MediaSource> mediaSourcesPublic;
// Accessed on the playback thread.
private final List<MediaSourceHolder> mediaSourceHolders;
private final MediaSourceHolder query;
private final Map<MediaPeriod, MediaSource> mediaSourceByMediaPeriod;
private final List<DeferredMediaPeriod> deferredMediaPeriods;
private ExoPlayer player;
private Listener listener;
private boolean preventListenerNotification;
private int windowCount;
private int periodCount;
public DynamicConcatenatingMediaSource() {
this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
this.mediaSourcesPublic = new ArrayList<>();
this.mediaSourceHolders = new ArrayList<>();
this.deferredMediaPeriods = new ArrayList<>(1);
this.query = new MediaSourceHolder(null, null, -1, -1, -1);
}
/**
* Appends a {@link MediaSource} to the playlist.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
public synchronized void addMediaSource(MediaSource mediaSource) {
addMediaSource(mediaSourcesPublic.size(), mediaSource);
}
/**
* Adds a {@link MediaSource} to the playlist.
*
* @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.
*/
public synchronized void addMediaSource(int index, MediaSource mediaSource) {
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)));
}
}
/**
* Appends multiple {@link MediaSource}s to the playlist.
*
* @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.
*/
public synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
addMediaSources(mediaSourcesPublic.size(), mediaSources);
}
/**
* Adds multiple {@link MediaSource}s to the playlist.
*
* @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.
*/
public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource);
Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
}
mediaSourcesPublic.addAll(index, mediaSources);
if (player != null && !mediaSources.isEmpty()) {
player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE,
Pair.create(index, mediaSources)));
}
}
/**
* Removes a {@link MediaSource} from the playlist.
*
* @param index The index at which the media source will be removed.
*/
public synchronized void removeMediaSource(int index) {
mediaSourcesPublic.remove(index);
if (player != null) {
player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index));
}
}
/**
* Returns the number of media sources in the playlist.
*/
public synchronized int getSize() {
return mediaSourcesPublic.size();
}
/**
* Returns the {@link MediaSource} at a specified index.
*
* @param index A index in the range of 0 <= index <= {@link #getSize()}.
* @return The {@link MediaSource} at this index.
*/
public synchronized MediaSource getMediaSource(int index) {
return mediaSourcesPublic.get(index);
}
@Override
public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource,
Listener listener) {
this.player = player;
this.listener = listener;
preventListenerNotification = true;
addMediaSourcesInternal(0, mediaSourcesPublic);
preventListenerNotification = false;
maybeNotifyListener();
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex);
MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild);
MediaPeriod mediaPeriod;
if (!holder.isPrepared) {
mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator);
deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod);
} else {
mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
}
mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource);
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod);
mediaSourceByMediaPeriod.remove(mediaPeriod);
if (mediaPeriod instanceof DeferredMediaPeriod) {
deferredMediaPeriods.remove(mediaPeriod);
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
} else {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void releaseSource() {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
mediaSourceHolder.mediaSource.releaseSource();
}
}
@Override
@SuppressWarnings("unchecked")
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
preventListenerNotification = true;
switch (messageType) {
case MSG_ADD: {
Pair<Integer, MediaSource> messageData = (Pair<Integer, MediaSource>) message;
addMediaSourceInternal(messageData.first, messageData.second);
break;
}
case MSG_ADD_MULTIPLE: {
Pair<Integer, Collection<MediaSource>> messageData =
(Pair<Integer, Collection<MediaSource>>) message;
addMediaSourcesInternal(messageData.first, messageData.second);
break;
}
case MSG_REMOVE: {
removeMediaSourceInternal((Integer) message);
break;
}
default: {
throw new IllegalStateException();
}
}
preventListenerNotification = false;
maybeNotifyListener();
}
private void maybeNotifyListener() {
if (!preventListenerNotification) {
listener.onSourceInfoRefreshed(
new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null);
}
}
private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
final MediaSourceHolder newMediaSourceHolder;
Object newUid = System.identityHashCode(newMediaSource);
DeferredTimeline newTimeline = new DeferredTimeline();
if (newIndex > 0) {
MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline,
previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(),
newUid);
} else {
newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid);
}
correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount());
mediaSourceHolders.add(newIndex, newMediaSourceHolder);
newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
updateMediaSourceInternal(newMediaSourceHolder, newTimeline);
}
});
}
private void addMediaSourcesInternal(int index, Collection<MediaSource> mediaSources) {
for (MediaSource mediaSource : mediaSources) {
addMediaSourceInternal(index++, mediaSource);
}
}
private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
if (mediaSourceHolder == null) {
throw new IllegalArgumentException();
}
DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;
if (deferredTimeline.getTimeline() == timeline) {
return;
}
int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild);
correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate);
}
mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
if (!mediaSourceHolder.isPrepared) {
for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) {
if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) {
deferredMediaPeriods.get(i).createPeriod();
deferredMediaPeriods.remove(i);
}
}
}
mediaSourceHolder.isPrepared = true;
maybeNotifyListener();
}
private void removeMediaSourceInternal(int index) {
MediaSourceHolder holder = mediaSourceHolders.get(index);
mediaSourceHolders.remove(index);
Timeline oldTimeline = holder.timeline;
correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount());
holder.mediaSource.releaseSource();
}
private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) {
windowCount += windowOffsetUpdate;
periodCount += periodOffsetUpdate;
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
}
}
private int findMediaSourceHolderByPeriodIndex(int periodIndex) {
query.firstPeriodIndexInChild = periodIndex;
int index = Collections.binarySearch(mediaSourceHolders, query);
return index >= 0 ? index : -index - 2;
}
private static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
public final MediaSource mediaSource;
public final Object uid;
public DeferredTimeline timeline;
public int firstWindowIndexInChild;
public int firstPeriodIndexInChild;
public boolean isPrepared;
public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window,
int period, Object uid) {
this.mediaSource = mediaSource;
this.timeline = timeline;
this.firstWindowIndexInChild = window;
this.firstPeriodIndexInChild = period;
this.uid = uid;
}
@Override
public int compareTo(MediaSourceHolder other) {
return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;
}
}
private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
private final int windowCount;
private final int periodCount;
private final int[] firstPeriodInChildIndices;
private final int[] firstWindowInChildIndices;
private final Timeline[] timelines;
private final int[] uids;
private final SparseIntArray childIndexByUid;
public ConcatenatedTimeline(Collection<MediaSourceHolder> mediaSourceHolders, int windowCount,
int periodCount) {
this.windowCount = windowCount;
this.periodCount = periodCount;
int childCount = mediaSourceHolders.size();
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
timelines = new Timeline[childCount];
uids = new int[childCount];
childIndexByUid = new SparseIntArray();
int index = 0;
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
timelines[index] = mediaSourceHolder.timeline;
firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
uids[index] = (int) mediaSourceHolder.uid;
childIndexByUid.put(uids[index], index++);
}
}
@Override
protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childDataHolder) {
int index = Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false);
setChildData(index, childDataHolder);
}
@Override
protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childDataHolder) {
int index = Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false);
setChildData(index, childDataHolder);
}
@Override
protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childDataHolder) {
if (!(childUid instanceof Integer)) {
return false;
}
int index = childIndexByUid.get((int) childUid, -1);
if (index == -1) {
return false;
}
setChildData(index, childDataHolder);
return true;
}
@Override
public int getWindowCount() {
return windowCount;
}
@Override
public int getPeriodCount() {
return periodCount;
}
private void setChildData(int srcIndex, ChildDataHolder dest) {
dest.setData(timelines[srcIndex], firstPeriodInChildIndices[srcIndex],
firstWindowInChildIndices[srcIndex], uids[srcIndex]);
}
}
private static final class DeferredTimeline extends Timeline {
private static final Object DUMMY_ID = new Object();
private static final Period period = new Period();
private final Timeline timeline;
private final Object replacedID;
public DeferredTimeline() {
timeline = null;
replacedID = null;
}
private DeferredTimeline(Timeline timeline, Object replacedID) {
this.timeline = timeline;
this.replacedID = replacedID;
}
public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0
? timeline.getPeriod(0, period, true).uid : replacedID);
}
public Timeline getTimeline() {
return timeline;
}
@Override
public int getWindowCount() {
return timeline == null ? 1 : timeline.getWindowCount();
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
return timeline == null
// Dynamic window to indicate pending timeline updates.
? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0,
C.TIME_UNSET, 0, 0, 0)
: timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
}
@Override
public int getPeriodCount() {
return timeline == null ? 1 : timeline.getPeriodCount();
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
if (timeline == null) {
return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET,
C.TIME_UNSET);
}
timeline.getPeriod(periodIndex, period, setIds);
if (period.uid == replacedID) {
period.uid = DUMMY_ID;
}
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET)
: timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid);
}
}
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final MediaSource mediaSource;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
public void createPeriod() {
mediaPeriod = mediaSource.createPeriod(id, allocator);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
mediaPeriod.discardBuffer(positionUs);
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
callback.onContinueLoadingRequested(this);
}
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
callback.onPrepared(this);
}
}
}