diff --git a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java new file mode 100644 index 00000000000..76669aeb714 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2016 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.Log; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * Loops a {@link MediaSource}. + */ +public final class LoopingMediaSource implements MediaSource { + + private static final String TAG = "LoopingMediaSource"; + + private final MediaSource childSource; + private final int loopCount; + + private int childPeriodCount; + + /** + * Loops the provided source indefinitely. + * + * @param childSource The {@link MediaSource} to loop. + */ + public LoopingMediaSource(MediaSource childSource) { + this(childSource, Integer.MAX_VALUE); + } + + /** + * Loops the provided source a specified number of times. + * + * @param childSource The {@link MediaSource} to loop. + * @param loopCount The desired number of loops. Must be strictly positive. The actual number of + * loops will be capped at the maximum value that can achieved without causing the number of + * periods exposed by the source to exceed {@link Integer#MAX_VALUE}. + */ + public LoopingMediaSource(MediaSource childSource, int loopCount) { + Assertions.checkArgument(loopCount > 0); + this.childSource = childSource; + this.loopCount = loopCount; + } + + @Override + public void prepareSource(final Listener listener) { + childSource.prepareSource(new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + childPeriodCount = timeline.getPeriodCount(); + listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + childSource.maybeThrowSourceInfoRefreshError(); + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { + return childSource.createPeriod(index % childPeriodCount, callback, allocator, positionUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + childSource.releasePeriod(mediaPeriod); + } + + @Override + public void releaseSource() { + childSource.releaseSource(); + } + + private static final class LoopingTimeline extends Timeline { + + private final Timeline childTimeline; + private final int childPeriodCount; + private final int childWindowCount; + private final int loopCount; + + public LoopingTimeline(Timeline childTimeline, int loopCount) { + this.childTimeline = childTimeline; + childPeriodCount = childTimeline.getPeriodCount(); + childWindowCount = childTimeline.getWindowCount(); + // This is the maximum number of loops that can be performed without overflow. + int maxLoopCount = Integer.MAX_VALUE / childPeriodCount; + if (loopCount > maxLoopCount) { + if (loopCount != Integer.MAX_VALUE) { + Log.w(TAG, "Capped loops to avoid overflow:" + loopCount + " -> " + maxLoopCount); + } + this.loopCount = maxLoopCount; + } else { + this.loopCount = loopCount; + } + } + + @Override + public int getWindowCount() { + return childWindowCount * loopCount; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds) { + childTimeline.getWindow(windowIndex % childWindowCount, window, setIds); + int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; + window.firstPeriodIndex += periodIndexOffset; + window.lastPeriodIndex += periodIndexOffset; + return window; + } + + @Override + public int getPeriodCount() { + return childPeriodCount * loopCount; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds); + int loopCount = (periodIndex / childPeriodCount); + period.windowIndex += loopCount * childWindowCount; + if (setIds) { + period.uid = Pair.create(loopCount, period.uid); + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Pair)) { + return C.INDEX_UNSET; + } + Pair loopCountAndChildUid = (Pair) uid; + if (!(loopCountAndChildUid.first instanceof Integer)) { + return C.INDEX_UNSET; + } + int loopCount = (Integer) loopCountAndChildUid.first; + int periodIndexOffset = loopCount * childPeriodCount; + return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; + } + } + +}