diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 3c870f06f4a..c72188ad2c1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -45,7 +45,7 @@ protected void setUp() throws Exception { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public void testNoClipping() { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public void testClippingUnseekableWindowThrows() { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public void testClippingStart() { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public void testClippingEnd() { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 066953b9987..0839d06fddb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -327,8 +327,11 @@ public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - sourceListener.onSourceInfoRefreshed( - this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); + // If the duration is currently unset, we expect to be able to update the window when its + // duration eventually becomes known. + boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + sourceListener.onSourceInfoRefreshed(this, + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 6f354384449..9cce67f68c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { + this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); } /** @@ -63,7 +63,7 @@ public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,11 +106,16 @@ public Window getWindow(int windowIndex, Window window, boolean setIds, Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2aa8ccc7125..51afb8eee95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -217,7 +217,7 @@ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Fo this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true); + timeline = new SinglePeriodTimeline(durationUs, true, false); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java new file mode 100644 index 00000000000..94ca8b03f0d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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 com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SinglePeriodTimeline}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SinglePeriodTimelineTest { + + private Window window; + private Period period; + + @Before + public void setUp() throws Exception { + window = new Window(); + period = new Period(); + } + + @Test + public void testGetPeriodPositionDynamicWindowUnknownDuration() { + SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + // Should return null with any positive position projection. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + assertThat(position).isNull(); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + + @Test + public void testGetPeriodPositionDynamicWindowKnownDuration() { + long windowDurationUs = 1000; + SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, + 0, false, true); + // Should return null with a positive position projection beyond window duration. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, + windowDurationUs + 1); + assertThat(position).isNull(); + // Should return (0, duration) with a projection equal to window duration. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(windowDurationUs); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + +}