Make ExtractorMediaSource timeline dynamic until duration is set

We (eventually - albeit possibly infinitely far in the future)
expect a timeline update with a window of known duration. This
also stops live radio stream playbacks transitioning to ended
state when their tracks are disabled.

As part of this fix, I found an issue where getPeriodPosition
could return null even when defaultPositionProjectionUs is 0,
which is not as documented.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=176492024
This commit is contained in:
olly 2017-11-21 04:46:52 -08:00 committed by Oliver Woodman
parent c49ae53699
commit 856c2f8d3e
5 changed files with 102 additions and 17 deletions

View File

@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
}
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 final class ClippingMediaSourceTest extends InstrumentationTestCase {
}
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 final class ClippingMediaSourceTest extends InstrumentationTestCase {
}
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 final class ClippingMediaSourceTest extends InstrumentationTestCase {
}
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 final class ClippingMediaSourceTest extends InstrumentationTestCase {
}
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);

View File

@ -327,8 +327,11 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
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);
}
}

View File

@ -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 final class SinglePeriodTimeline extends Timeline {
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 final class SinglePeriodTimeline extends Timeline {
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,

View File

@ -217,7 +217,7 @@ public final class SingleSampleMediaSource implements MediaSource {
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
timeline = new SinglePeriodTimeline(durationUs, true);
timeline = new SinglePeriodTimeline(durationUs, true, false);
}
// MediaSource implementation.

View File

@ -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<Integer, Long> 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<Integer, Long> 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);
}
}