diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java new file mode 100644 index 0000000000..0933fb858b --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -0,0 +1,143 @@ +/* + * 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 static org.mockito.Mockito.doAnswer; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.testutil.TestUtil; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit tests for {@link ClippingMediaSource}. + */ +public final class ClippingMediaSourceTest extends InstrumentationTestCase { + + private static final long TEST_PERIOD_DURATION_US = 1000000; + private static final long TEST_CLIP_AMOUNT_US = 300000; + + @Mock + private MediaSource mockMediaSource; + private Timeline clippedTimeline; + private Window window; + private Period period; + + @Override + protected void setUp() throws Exception { + TestUtil.setUpMockito(this); + window = new Timeline.Window(); + period = new Timeline.Period(); + } + + public void testNoClipping() { + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + + Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); + + assertEquals(1, clippedTimeline.getWindowCount()); + assertEquals(1, clippedTimeline.getPeriodCount()); + assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs()); + } + + public void testClippingUnseekableWindowThrows() { + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + + // If the unseekable window isn't clipped, clipping succeeds. + getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); + try { + // If the unseekable window is clipped, clipping fails. + getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US); + fail("Expected clipping to fail."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + public void testClippingStart() { + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + clippedTimeline.getPeriod(0, period).getDurationUs()); + } + + public void testClippingEnd() { + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + + Timeline clippedTimeline = getClippedTimeline(timeline, 0, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + clippedTimeline.getPeriod(0, period).getDurationUs()); + } + + public void testClippingStartAndEnd() { + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getPeriod(0, period).getDurationUs()); + } + + /** + * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. + */ + private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { + mockMediaSourceSourceWithTimeline(timeline); + new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true, + new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + clippedTimeline = timeline; + } + }); + return clippedTimeline; + } + + /** + * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info. + */ + private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2]; + listener.onSourceInfoRefreshed(timeline, null); + return null; + } + }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(), + Mockito.any(MediaSource.Listener.class)); + return mockMediaSource; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 2df6d862ee..447e39bf52 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -70,8 +70,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void enable(Format[] formats, SampleStream stream, long positionUs, - boolean joining, long offsetUs) throws ExoPlaybackException { + public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining, + long offsetUs) throws ExoPlaybackException { Assertions.checkState(state == STATE_DISABLED); state = STATE_ENABLED; onEnabled(joining); @@ -200,8 +200,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * @param joining Whether this renderer is being enabled to join an ongoing playback. * @throws ExoPlaybackException If an error occurs. */ - protected void onPositionReset(long positionUs, boolean joining) - throws ExoPlaybackException { + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { // Do nothing. } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 8f86a2db61..66be6b7478 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -137,6 +137,14 @@ import java.io.IOException; */ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; + /** + * Offset added to all sample timestamps read by renderers to make them non-negative. This is + * provided for convenience of sources that may return negative timestamps due to prerolling + * samples from a keyframe before their first sample with timestamp zero, so it must be set to a + * value greater than or equal to the maximum key-frame interval in seekable periods. + */ + private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000; + private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; @@ -637,7 +645,8 @@ import java.io.IOException; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + rendererPositionUs = playingPeriodHolder == null + ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US : playingPeriodHolder.toRendererTime(periodPositionUs); standaloneMediaClock.setPositionUs(rendererPositionUs); for (Renderer renderer : enabledRenderers) { @@ -1147,22 +1156,30 @@ import java.io.IOException; TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections; readingPeriodHolder = readingPeriodHolder.next; TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections; + + boolean initialDiscontinuity = + readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; TrackSelection oldSelection = oldTrackSelections.get(i); TrackSelection newSelection = newTrackSelections.get(i); - if (oldSelection != null) { - boolean isCurrentStreamFinal = renderer.isCurrentStreamFinal(); - if (newSelection != null && !isCurrentStreamFinal) { - // Replace the renderer's SampleStream so the transition to playing the next period can - // be seamless. + if (oldSelection == null) { + // The renderer has no current stream and will be enabled when we play the next period. + } else if (initialDiscontinuity) { + // The new period starts with a discontinuity, so the renderer will play out all data then + // be disabled and re-enabled when it starts playing the next period. + renderer.setCurrentStreamFinal(); + } else if (!renderer.isCurrentStreamFinal()) { + if (newSelection != null) { + // Replace the renderer's SampleStream so the transition to playing the next period + // can be seamless. Format[] formats = new Format[newSelection.length()]; for (int j = 0; j < formats.length; j++) { formats[j] = newSelection.getFormat(j); } renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); - } else if (!isCurrentStreamFinal) { + } else { // The renderer will be disabled when transitioning to playing the next period. Mark the // SampleStream as final to play out any remaining data. renderer.setCurrentStreamFinal(); @@ -1228,7 +1245,8 @@ import java.io.IOException; } } - long rendererPositionOffsetUs = loadingPeriodHolder == null ? newLoadingPeriodStartPositionUs + long rendererPositionOffsetUs = loadingPeriodHolder == null + ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US : (loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); timeline.getPeriod(newLoadingPeriodIndex, period, true); diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/Timeline.java index 32af48bd59..333dd25cbe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -262,9 +262,24 @@ public abstract class Timeline { */ public int lastPeriodIndex; - private long defaultPositionUs; - private long durationUs; - private long positionInFirstPeriodUs; + /** + * The default position relative to the start of the window at which to begin playback, in + * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. + */ + public long defaultPositionUs; + + /** + * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + + /** + * The position of the start of this window relative to the start of the first period belonging + * to it, in microseconds. + */ + public long positionInFirstPeriodUs; /** * Sets the data held by this window. @@ -363,7 +378,11 @@ public abstract class Timeline { */ public int windowIndex; - private long durationUs; + /** + * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + private long positionInWindowUs; /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java new file mode 100644 index 0000000000..c39bccda3d --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -0,0 +1,247 @@ +/* + * 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their + * samples. + */ +/* package */ final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaPeriod mediaPeriod; + private final ClippingMediaSource mediaSource; + + private MediaPeriod.Callback callback; + private long startUs; + private long endUs; + private ClippingSampleStream[] sampleStreams; + private boolean pendingInitialDiscontinuity; + + /** + * Creates a new clipping media period that provides a clipped view of the specified + * {@link MediaPeriod}'s sample streams. + * + * @param mediaPeriod The media period to clip. + * @param mediaSource The {@link ClippingMediaSource} to which this period belongs. + */ + public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) { + this.mediaPeriod = mediaPeriod; + this.mediaSource = mediaSource; + startUs = C.TIME_UNSET; + endUs = C.TIME_UNSET; + sampleStreams = new ClippingSampleStream[0]; + } + + @Override + public void prepare(MediaPeriod.Callback callback) { + this.callback = callback; + mediaPeriod.prepare(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + mediaPeriod.maybeThrowPrepareError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + sampleStreams = new ClippingSampleStream[streams.length]; + SampleStream[] internalStreams = new SampleStream[streams.length]; + for (int i = 0; i < streams.length; i++) { + sampleStreams[i] = (ClippingSampleStream) streams[i]; + internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; + } + long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, + internalStreams, streamResetFlags, positionUs + startUs); + Assertions.checkState(enablePositionUs == positionUs + startUs + || (enablePositionUs >= startUs && enablePositionUs <= endUs)); + for (int i = 0; i < streams.length; i++) { + if (internalStreams[i] == null) { + sampleStreams[i] = null; + } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { + sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, + pendingInitialDiscontinuity); + } + streams[i] = sampleStreams[i]; + } + return enablePositionUs - startUs; + } + + @Override + public long readDiscontinuity() { + if (pendingInitialDiscontinuity) { + for (ClippingSampleStream sampleStream : sampleStreams) { + if (sampleStream != null) { + sampleStream.clearPendingDiscontinuity(); + } + } + pendingInitialDiscontinuity = false; + // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. + long discontinuityUs = readDiscontinuity(); + return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; + } + long discontinuityUs = mediaPeriod.readDiscontinuity(); + if (discontinuityUs == C.TIME_UNSET) { + return C.TIME_UNSET; + } + Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs); + return discontinuityUs - startUs; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (bufferedPositionUs == C.TIME_END_OF_SOURCE || bufferedPositionUs >= endUs) { + return C.TIME_END_OF_SOURCE; + } + return Math.max(0, bufferedPositionUs - startUs); + } + + @Override + public long seekToUs(long positionUs) { + for (ClippingSampleStream sampleStream : sampleStreams) { + if (sampleStream != null) { + sampleStream.clearSentEos(); + } + } + long seekUs = mediaPeriod.seekToUs(positionUs + startUs); + Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs)); + return seekUs - startUs; + } + + @Override + public long getNextLoadPositionUs() { + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE || nextLoadPositionUs >= endUs) { + return C.TIME_END_OF_SOURCE; + } + return nextLoadPositionUs - startUs; + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod.continueLoading(positionUs + startUs); + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + startUs = mediaSource.getStartUs(); + endUs = mediaSource.getEndUs(); + Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); + // If the clipping start position is non-zero, the clipping sample streams will adjust + // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer + // timestamps can be negative, because sample streams provide buffers starting at a key-frame, + // which may be before the clipping start point. When the renderer reads a buffer with a + // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp + // read in the previous period. Renderer implementations may not allow this, so we signal a + // discontinuity which resets the renderers before they read the clipping sample stream. + pendingInitialDiscontinuity = startUs != 0; + callback.onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + /** + * Wraps a {@link SampleStream} and clips its samples. + */ + private static final class ClippingSampleStream implements SampleStream { + + private final MediaPeriod mediaPeriod; + private final SampleStream stream; + private final long startUs; + private final long endUs; + + private boolean pendingDiscontinuity; + private boolean sentEos; + + public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, + long endUs, boolean pendingDiscontinuity) { + this.mediaPeriod = mediaPeriod; + this.stream = stream; + this.startUs = startUs; + this.endUs = endUs; + this.pendingDiscontinuity = pendingDiscontinuity; + } + + public void clearPendingDiscontinuity() { + pendingDiscontinuity = false; + } + + public void clearSentEos() { + sentEos = false; + } + + @Override + public boolean isReady() { + return stream.isReady(); + } + + @Override + public void maybeThrowError() throws IOException { + stream.maybeThrowError(); + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + if (pendingDiscontinuity) { + return C.RESULT_NOTHING_READ; + } + if (sentEos) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + int result = stream.readData(formatHolder, buffer); + // TODO: Clear gapless playback metadata if a format was read (if applicable). + if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) { + buffer.clear(); + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + sentEos = true; + return C.RESULT_BUFFER_READ; + } + if (result == C.RESULT_BUFFER_READ) { + buffer.timeUs -= startUs; + } + return result; + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + stream.skipToKeyframeBefore(startUs + timeUs); + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java new file mode 100644 index 0000000000..e92dce8231 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -0,0 +1,185 @@ +/* + * 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end + * positions. The wrapped source may only have a single period/window and it must not be dynamic + * (live). The specified start position must correspond to a synchronization sample in the period. + */ +public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { + + private final MediaSource mediaSource; + private final long startUs; + private final long endUs; + + private MediaSource.Listener sourceListener; + private ClippingTimeline clippingTimeline; + + /** + * Creates a new clipping source that wraps the specified source. + * + * @param mediaSource The single-period, non-dynamic source to wrap. + * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to + * start providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples + * from the specified start point up to the end of the source. + */ + public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + Assertions.checkArgument(startPositionUs >= 0); + this.mediaSource = Assertions.checkNotNull(mediaSource); + startUs = startPositionUs; + endUs = endPositionUs; + } + + /** + * Returns the start position of the clipping source's timeline in microseconds. + */ + /* package */ long getStartUs() { + return clippingTimeline.startUs; + } + + /** + * Returns the end position of the clipping source's timeline in microseconds. + */ + /* package */ long getEndUs() { + return clippingTimeline.endUs; + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + this.sourceListener = listener; + mediaSource.prepareSource(player, false, this); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + return new ClippingMediaPeriod( + mediaSource.createPeriod(index, allocator, startUs + positionUs), this); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } + + @Override + public void releaseSource() { + mediaSource.releaseSource(); + } + + // MediaSource.Listener implementation. + + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); + sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); + } + + /** + * Provides a clipped view of a specified timeline. + */ + private static final class ClippingTimeline extends Timeline { + + private final Timeline timeline; + private final long startUs; + private final long endUs; + + /** + * Creates a new timeline that wraps the specified timeline. + * + * @param timeline The timeline to clip. + * @param startUs The number of microseconds to clip from the start of {@code timeline}. + * @param endUs The end position in microseconds for the clipped timeline relative to the start + * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. + */ + public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + Assertions.checkArgument(timeline.getWindowCount() == 1); + Assertions.checkArgument(timeline.getPeriodCount() == 1); + Window window = timeline.getWindow(0, new Window(), false); + Assertions.checkArgument(!window.isDynamic); + long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; + if (window.durationUs != C.TIME_UNSET) { + Assertions.checkArgument(startUs == 0 || window.isSeekable); + Assertions.checkArgument(resolvedEndUs <= window.durationUs); + Assertions.checkArgument(startUs <= resolvedEndUs); + } + Period period = timeline.getPeriod(0, new Period()); + Assertions.checkArgument(period.getPositionInWindowUs() == 0); + this.timeline = timeline; + this.startUs = startUs; + this.endUs = resolvedEndUs; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); + window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + if (window.defaultPositionUs != C.TIME_UNSET) { + window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); + window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs + : Math.min(window.defaultPositionUs, endUs); + window.defaultPositionUs -= startUs; + } + long startMs = C.usToMs(startUs); + if (window.presentationStartTimeMs != C.TIME_UNSET) { + window.presentationStartTimeMs += startMs; + } + if (window.windowStartTimeMs != C.TIME_UNSET) { + window.windowStartTimeMs += startMs; + } + return window; + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + period = timeline.getPeriod(0, period, setIds); + period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(uid); + } + + } + +}