Improve ClippingMediaSource "cannot clip" behavior

This brings ClippingMediaSource clip failures in line with
what MergingMediaSource does when it cannot merge.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=176660123
This commit is contained in:
olly 2017-11-22 08:30:45 -08:00 committed by Oliver Woodman
parent d909dc1863
commit 494a41c8b2
7 changed files with 134 additions and 46 deletions

View file

@ -21,11 +21,13 @@ import com.google.android.exoplayer2.Player;
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.ClippingMediaSource.IllegalClippingException;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
/**
* Unit tests for {@link ClippingMediaSource}.
@ -40,11 +42,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
window = new Timeline.Window();
period = new Timeline.Period();
}
public void testNoClipping() {
public void testNoClipping() throws IOException {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false);
Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
@ -55,7 +58,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingUnseekableWindowThrows() {
public void testClippingUnseekableWindowThrows() throws IOException {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false);
// If the unseekable window isn't clipped, clipping succeeds.
@ -64,12 +67,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
// If the unseekable window is clipped, clipping fails.
getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
fail("Expected clipping to fail.");
} catch (IllegalArgumentException e) {
// Expected.
} catch (IllegalClippingException e) {
assertEquals(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START, e.reason);
}
}
public void testClippingStart() {
public void testClippingStart() throws IOException {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
@ -80,7 +83,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingEnd() {
public void testClippingEnd() throws IOException {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false);
Timeline clippedTimeline = getClippedTimeline(timeline, 0,
@ -91,7 +94,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingStartAndEnd() {
public void testClippingStartAndEnd() throws IOException {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
@ -102,7 +105,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testWindowAndPeriodIndices() {
public void testWindowAndPeriodIndices() throws IOException {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
@ -122,7 +125,8 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs)
throws IOException {
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import junit.framework.TestCase;
/**
@ -32,7 +33,7 @@ import junit.framework.TestCase;
*/
public final class ConcatenatingMediaSourceTest extends TestCase {
public void testEmptyConcatenation() {
public void testEmptyConcatenation() throws IOException {
for (boolean atomic : new boolean[] {false, true}) {
Timeline timeline = getConcatenatedTimeline(atomic);
TimelineAsserts.assertEmpty(timeline);
@ -45,7 +46,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
}
}
public void testSingleMediaSource() {
public void testSingleMediaSource() throws IOException {
Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 3);
@ -75,7 +76,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
}
}
public void testMultipleMediaSources() {
public void testMultipleMediaSources() throws IOException {
Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222),
createFakeTimeline(3, 333) };
Timeline timeline = getConcatenatedTimeline(false, timelines);
@ -121,7 +122,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
}
}
public void testNestedMediaSources() {
public void testNestedMediaSources() throws IOException {
Timeline timeline = getConcatenatedTimeline(false,
getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
@ -149,7 +150,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1);
}
public void testEmptyTimelineMediaSources() {
public void testEmptyTimelineMediaSources() throws IOException {
// Empty timelines in the front, back, and the middle (single and multiple in a row).
Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY,
Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333),
@ -197,7 +198,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
}
}
public void testPeriodCreationWithAds() throws InterruptedException {
public void testPeriodCreationWithAds() throws IOException, InterruptedException {
// Create media source with ad child source.
Timeline timelineContentOnly = new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
@ -231,7 +232,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
* the concatenated timeline.
*/
private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic,
Timeline... timelines) {
Timeline... timelines) throws IOException {
MediaSource[] mediaSources = new MediaSource[timelines.length];
for (int i = 0; i < timelines.length; i++) {
mediaSources[i] = new FakeMediaSource(timelines[i], null);

View file

@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import java.util.Arrays;
import junit.framework.TestCase;
import org.mockito.Mockito;
@ -55,7 +56,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
testRunner.release();
}
public void testPlaylistChangesAfterPreparation() {
public void testPlaylistChangesAfterPreparation() throws IOException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
@ -171,7 +172,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
childSources[3].assertReleased();
}
public void testPlaylistChangesBeforePreparation() {
public void testPlaylistChangesBeforePreparation() throws IOException {
FakeMediaSource[] childSources = createMediaSources(4);
mediaSource.addMediaSource(childSources[0]);
mediaSource.addMediaSource(childSources[1]);
@ -201,7 +202,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testPlaylistWithLazyMediaSource() {
public void testPlaylistWithLazyMediaSource() throws IOException {
// Create some normal (immediately preparing) sources and some lazy sources whose timeline
// updates need to be triggered.
FakeMediaSource[] fastSources = createMediaSources(2);
@ -290,7 +291,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testEmptyTimelineMediaSource() {
public void testEmptyTimelineMediaSource() throws IOException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
@ -426,7 +427,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
verify(runnable).run();
}
public void testCustomCallbackAfterPreparationAddSingle() {
public void testCustomCallbackAfterPreparationAddSingle() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -444,7 +445,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testCustomCallbackAfterPreparationAddMultiple() {
public void testCustomCallbackAfterPreparationAddMultiple() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -464,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testCustomCallbackAfterPreparationAddSingleWithIndex() {
public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -482,7 +483,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testCustomCallbackAfterPreparationAddMultipleWithIndex() {
public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -502,7 +503,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testCustomCallbackAfterPreparationRemove() {
public void testCustomCallbackAfterPreparationRemove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -528,7 +529,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testCustomCallbackAfterPreparationMove() {
public void testCustomCallbackAfterPreparationMove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
@ -556,7 +557,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
}
}
public void testPeriodCreationWithAds() throws InterruptedException {
public void testPeriodCreationWithAds() throws IOException, InterruptedException {
// Create dynamic media source with ad child source.
Timeline timelineContentOnly = new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));

View file

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import junit.framework.TestCase;
/**
@ -39,7 +40,7 @@ public class LoopingMediaSourceTest extends TestCase {
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333));
}
public void testSingleLoop() {
public void testSingleLoop() throws IOException {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
@ -57,7 +58,7 @@ public class LoopingMediaSourceTest extends TestCase {
}
}
public void testMultiLoop() {
public void testMultiLoop() throws IOException {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1);
@ -77,7 +78,7 @@ public class LoopingMediaSourceTest extends TestCase {
}
}
public void testInfiniteLoop() {
public void testInfiniteLoop() throws IOException {
Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
@ -94,7 +95,7 @@ public class LoopingMediaSourceTest extends TestCase {
}
}
public void testEmptyTimelineLoop() {
public void testEmptyTimelineLoop() throws IOException {
Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1);
TimelineAsserts.assertEmpty(timeline);
@ -109,7 +110,7 @@ public class LoopingMediaSourceTest extends TestCase {
* Wraps the specified timeline in a {@link LoopingMediaSource} and returns
* the looping timeline.
*/
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException {
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);

View file

@ -15,20 +15,68 @@
*/
package com.google.android.exoplayer2.source;
import android.support.annotation.IntDef;
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;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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.
* positions. The wrapped source must consist of a single period that starts at the beginning of the
* corresponding window.
*/
public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
/**
* Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source.
*/
public static final class IllegalClippingException extends IOException {
/**
* The reason the clipping failed.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW,
REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END})
public @interface Reason {}
/**
* The wrapped source doesn't consist of a single period.
*/
public static final int REASON_INVALID_PERIOD_COUNT = 0;
/**
* The wrapped source period doesn't start at the beginning of the corresponding window.
*/
public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1;
/**
* The wrapped source is not seekable and a non-zero clipping start position was specified.
*/
public static final int REASON_NOT_SEEKABLE_TO_START = 2;
/**
* The wrapped source ends before the specified clipping start position.
*/
public static final int REASON_START_EXCEEDS_END = 3;
/**
* The reason clipping failed.
*/
@Reason
public final int reason;
/**
* @param reason The reason clipping failed.
*/
public IllegalClippingException(@Reason int reason) {
this.reason = reason;
}
}
private final MediaSource mediaSource;
private final long startUs;
private final long endUs;
@ -36,6 +84,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final ArrayList<ClippingMediaPeriod> mediaPeriods;
private MediaSource.Listener sourceListener;
private IllegalClippingException clippingError;
/**
* Creates a new clipping source that wraps the specified source.
@ -88,6 +137,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (clippingError != null) {
throw clippingError;
}
mediaSource.maybeThrowSourceInfoRefreshError();
}
@ -115,8 +167,17 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
sourceListener.onSourceInfoRefreshed(this, new ClippingTimeline(timeline, startUs, endUs),
manifest);
if (clippingError != null) {
return;
}
ClippingTimeline clippingTimeline;
try {
clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
} catch (IllegalClippingException e) {
clippingError = e;
return;
}
sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest);
int count = mediaPeriods.size();
for (int i = 0; i < count; i++) {
mediaPeriods.get(i).setClipping(startUs, endUs);
@ -138,22 +199,30 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* @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.
* @throws IllegalClippingException If the timeline could not be clipped.
*/
public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
public ClippingTimeline(Timeline timeline, long startUs, long endUs)
throws IllegalClippingException {
super(timeline);
Assertions.checkArgument(timeline.getWindowCount() == 1);
Assertions.checkArgument(timeline.getPeriodCount() == 1);
if (timeline.getPeriodCount() != 1) {
throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT);
}
if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) {
throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW);
}
Window window = timeline.getWindow(0, new Window(), false);
long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs;
if (window.durationUs != C.TIME_UNSET) {
if (resolvedEndUs > window.durationUs) {
resolvedEndUs = window.durationUs;
}
Assertions.checkArgument(startUs == 0 || window.isSeekable);
Assertions.checkArgument(startUs <= resolvedEndUs);
if (startUs != 0 && !window.isSeekable) {
throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);
}
if (startUs > resolvedEndUs) {
throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END);
}
}
Period period = timeline.getPeriod(0, new Period());
Assertions.checkArgument(period.getPositionInWindowUs() == 0);
this.startUs = startUs;
this.endUs = resolvedEndUs;
}

View file

@ -45,11 +45,11 @@ public final class MergingMediaSource implements MediaSource {
@IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH})
public @interface Reason {}
/**
* The merge failed because one of the sources being merged has a dynamic window.
* One of the sources being merged has a dynamic window.
*/
public static final int REASON_WINDOWS_ARE_DYNAMIC = 0;
/**
* The merge failed because the sources have different period counts.
* The sources have different period counts.
*/
public static final int REASON_PERIOD_COUNT_MISMATCH = 1;

View file

@ -32,7 +32,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
@ -100,13 +100,25 @@ public class MediaSourceTestRunner {
*
* @return The initial {@link Timeline}.
*/
public Timeline prepareSource() {
public Timeline prepareSource() throws IOException {
final IOException[] prepareError = new IOException[1];
runOnPlaybackThread(new Runnable() {
@Override
public void run() {
mediaSource.prepareSource(player, true, mediaSourceListener);
try {
// TODO: This only catches errors that are set synchronously in prepareSource. To capture
// async errors we'll need to poll maybeThrowSourceInfoRefreshError until the first call
// to onSourceInfoRefreshed.
mediaSource.maybeThrowSourceInfoRefreshError();
} catch (IOException e) {
prepareError[0] = e;
}
}
});
if (prepareError[0] != null) {
throw prepareError[0];
}
return assertTimelineChangeBlocking();
}