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 index c39bccda3d..33aeef754d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -26,10 +26,12 @@ 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 class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + /** + * The {@link MediaPeriod} wrapped by this clipping media period. + */ public final MediaPeriod mediaPeriod; - private final ClippingMediaSource mediaSource; private MediaPeriod.Callback callback; private long startUs; @@ -40,18 +42,31 @@ import java.io.IOException; /** * Creates a new clipping media period that provides a clipped view of the specified * {@link MediaPeriod}'s sample streams. + *

+ * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} + * on the playback thread before preparation completes. * * @param mediaPeriod The media period to clip. - * @param mediaSource The {@link ClippingMediaSource} to which this period belongs. */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) { + public ClippingMediaPeriod(MediaPeriod mediaPeriod) { this.mediaPeriod = mediaPeriod; - this.mediaSource = mediaSource; startUs = C.TIME_UNSET; endUs = C.TIME_UNSET; sampleStreams = new ClippingSampleStream[0]; } + /** + * Sets the clipping start/end times for this period, in microseconds. + * + * @param startUs The clipping start time, in microseconds. + * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to + * indicate the end of the period. + */ + public void setClipping(long startUs, long endUs) { + this.startUs = startUs; + this.endUs = endUs; + } + @Override public void prepare(MediaPeriod.Callback callback) { this.callback = callback; @@ -80,7 +95,8 @@ import java.io.IOException; long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, internalStreams, streamResetFlags, positionUs + startUs); Assertions.checkState(enablePositionUs == positionUs + startUs - || (enablePositionUs >= startUs && enablePositionUs <= endUs)); + || (enablePositionUs >= startUs + && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); for (int i = 0; i < streams.length; i++) { if (internalStreams[i] == null) { sampleStreams[i] = null; @@ -110,14 +126,16 @@ import java.io.IOException; if (discontinuityUs == C.TIME_UNSET) { return C.TIME_UNSET; } - Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs); + Assertions.checkState(discontinuityUs >= startUs); + Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs); return discontinuityUs - startUs; } @Override public long getBufferedPositionUs() { long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - if (bufferedPositionUs == C.TIME_END_OF_SOURCE || bufferedPositionUs >= endUs) { + if (bufferedPositionUs == C.TIME_END_OF_SOURCE + || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } return Math.max(0, bufferedPositionUs - startUs); @@ -131,14 +149,16 @@ import java.io.IOException; } } long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs)); + Assertions.checkState(seekUs == positionUs + startUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); return seekUs - startUs; } @Override public long getNextLoadPositionUs() { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE || nextLoadPositionUs >= endUs) { + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE + || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } return nextLoadPositionUs - startUs; @@ -153,8 +173,6 @@ import java.io.IOException; @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 @@ -223,15 +241,15 @@ import java.io.IOException; } 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)) { + if (endUs != C.TIME_END_OF_SOURCE && ((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) { + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { buffer.timeUs -= startUs; } return result; 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 index e92dce8231..be15a07726 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -21,17 +21,19 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; /** * {@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. + * (live). */ public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { private final MediaSource mediaSource; private final long startUs; private final long endUs; + private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; private ClippingTimeline clippingTimeline; @@ -51,20 +53,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste 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; + mediaPeriods = new ArrayList<>(); } @Override @@ -80,12 +69,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - return new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator, startUs + positionUs), this); + ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( + mediaSource.createPeriod(index, allocator, startUs + positionUs)); + mediaPeriods.add(mediaPeriod); + mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); + return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { + Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } @@ -100,6 +93,13 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); + long startUs = clippingTimeline.startUs; + long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE + : clippingTimeline.endUs; + int count = mediaPeriods.size(); + for (int i = 0; i < count; i++) { + mediaPeriods.get(i).setClipping(startUs, endUs); + } } /** @@ -112,7 +112,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final long endUs; /** - * Creates a new timeline that wraps the specified timeline. + * Creates a new clipping 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}. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index f4a9665b10..31ee8df1e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; @@ -47,6 +48,10 @@ public interface MediaPeriod extends SequenceableLoader { *

* {@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. + *

+ * If preparation succeeds and results in a source timeline change (e.g. the period duration + * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} will be + * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes.