Skip to content

Commit

Permalink
Add ClippingMediaSource.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
andrewlewis authored and ojw28 committed Dec 14, 2016
1 parent 8a7628c commit f276eb2
Show file tree
Hide file tree
Showing 6 changed files with 627 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ public final int getState() {
}

@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);
Expand Down Expand Up @@ -200,8 +200,7 @@ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
* @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.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo,
*/
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;
Expand Down Expand Up @@ -637,7 +645,8 @@ private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
}

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) {
Expand Down Expand Up @@ -1147,22 +1156,30 @@ private void updatePeriods() throws ExoPlaybackException, 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();
Expand Down Expand Up @@ -1228,7 +1245,8 @@ private void maybeUpdateLoadingPeriod() throws 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);
Expand Down
27 changes: 23 additions & 4 deletions library/src/main/java/com/google/android/exoplayer2/Timeline.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,24 @@ public static final class Window {
*/
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.
Expand Down Expand Up @@ -363,7 +378,11 @@ public static final class Period {
*/
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;

/**
Expand Down
Loading

0 comments on commit f276eb2

Please sign in to comment.