mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
Add ClippingMediaSource.
ClippingMediaSource wraps a single period/window video-on-demand source and exposes a specified time range within it. Issue: #1988 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=141991215
This commit is contained in:
parent
8a7628cb26
commit
f276eb2ce7
@ -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<Void>() {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -70,8 +70,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void enable(Format[] formats, SampleStream stream, long positionUs,
|
public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
|
||||||
boolean joining, long offsetUs) throws ExoPlaybackException {
|
long offsetUs) throws ExoPlaybackException {
|
||||||
Assertions.checkState(state == STATE_DISABLED);
|
Assertions.checkState(state == STATE_DISABLED);
|
||||||
state = STATE_ENABLED;
|
state = STATE_ENABLED;
|
||||||
onEnabled(joining);
|
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.
|
* @param joining Whether this renderer is being enabled to join an ongoing playback.
|
||||||
* @throws ExoPlaybackException If an error occurs.
|
* @throws ExoPlaybackException If an error occurs.
|
||||||
*/
|
*/
|
||||||
protected void onPositionReset(long positionUs, boolean joining)
|
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||||
throws ExoPlaybackException {
|
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +137,14 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
|
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 Renderer[] renderers;
|
||||||
private final RendererCapabilities[] rendererCapabilities;
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
private final TrackSelector trackSelector;
|
private final TrackSelector trackSelector;
|
||||||
@ -637,7 +645,8 @@ import java.io.IOException;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
|
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
|
||||||
rendererPositionUs = playingPeriodHolder == null ? periodPositionUs
|
rendererPositionUs = playingPeriodHolder == null
|
||||||
|
? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
|
||||||
: playingPeriodHolder.toRendererTime(periodPositionUs);
|
: playingPeriodHolder.toRendererTime(periodPositionUs);
|
||||||
standaloneMediaClock.setPositionUs(rendererPositionUs);
|
standaloneMediaClock.setPositionUs(rendererPositionUs);
|
||||||
for (Renderer renderer : enabledRenderers) {
|
for (Renderer renderer : enabledRenderers) {
|
||||||
@ -1147,22 +1156,30 @@ import java.io.IOException;
|
|||||||
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections;
|
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections;
|
||||||
readingPeriodHolder = readingPeriodHolder.next;
|
readingPeriodHolder = readingPeriodHolder.next;
|
||||||
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections;
|
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections;
|
||||||
|
|
||||||
|
boolean initialDiscontinuity =
|
||||||
|
readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
Renderer renderer = renderers[i];
|
Renderer renderer = renderers[i];
|
||||||
TrackSelection oldSelection = oldTrackSelections.get(i);
|
TrackSelection oldSelection = oldTrackSelections.get(i);
|
||||||
TrackSelection newSelection = newTrackSelections.get(i);
|
TrackSelection newSelection = newTrackSelections.get(i);
|
||||||
if (oldSelection != null) {
|
if (oldSelection == null) {
|
||||||
boolean isCurrentStreamFinal = renderer.isCurrentStreamFinal();
|
// The renderer has no current stream and will be enabled when we play the next period.
|
||||||
if (newSelection != null && !isCurrentStreamFinal) {
|
} else if (initialDiscontinuity) {
|
||||||
// Replace the renderer's SampleStream so the transition to playing the next period can
|
// The new period starts with a discontinuity, so the renderer will play out all data then
|
||||||
// be seamless.
|
// 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()];
|
Format[] formats = new Format[newSelection.length()];
|
||||||
for (int j = 0; j < formats.length; j++) {
|
for (int j = 0; j < formats.length; j++) {
|
||||||
formats[j] = newSelection.getFormat(j);
|
formats[j] = newSelection.getFormat(j);
|
||||||
}
|
}
|
||||||
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
|
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
|
||||||
readingPeriodHolder.getRendererOffset());
|
readingPeriodHolder.getRendererOffset());
|
||||||
} else if (!isCurrentStreamFinal) {
|
} else {
|
||||||
// The renderer will be disabled when transitioning to playing the next period. Mark the
|
// The renderer will be disabled when transitioning to playing the next period. Mark the
|
||||||
// SampleStream as final to play out any remaining data.
|
// SampleStream as final to play out any remaining data.
|
||||||
renderer.setCurrentStreamFinal();
|
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()
|
: (loadingPeriodHolder.getRendererOffset()
|
||||||
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
|
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
|
||||||
timeline.getPeriod(newLoadingPeriodIndex, period, true);
|
timeline.getPeriod(newLoadingPeriodIndex, period, true);
|
||||||
|
@ -262,9 +262,24 @@ public abstract class Timeline {
|
|||||||
*/
|
*/
|
||||||
public int lastPeriodIndex;
|
public int lastPeriodIndex;
|
||||||
|
|
||||||
private long defaultPositionUs;
|
/**
|
||||||
private long durationUs;
|
* The default position relative to the start of the window at which to begin playback, in
|
||||||
private long positionInFirstPeriodUs;
|
* 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.
|
* Sets the data held by this window.
|
||||||
@ -363,7 +378,11 @@ public abstract class Timeline {
|
|||||||
*/
|
*/
|
||||||
public int windowIndex;
|
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;
|
private long positionInWindowUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user