From dd4f9bcaae74cf50d9d10b8bd4d3cb729d7dc51d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 17:30:14 +0100 Subject: [PATCH] Add Timeline.Window.isLive This flag is currently merged into Window.isDynamic, which isn't always true because 1. A window can be dynamic for other reasons (e.g. when the duration is still missing). 2. A live stream can be become non-dynamic when it ends. Issue:#2668 Issue:#5973 PiperOrigin-RevId: 271999378 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 +- .../google/android/exoplayer2/Timeline.java | 14 ++++- .../exoplayer2/source/MaskingMediaSource.java | 1 + .../source/ProgressiveMediaPeriod.java | 18 +++--- .../source/ProgressiveMediaSource.java | 18 ++++-- .../exoplayer2/source/SilenceMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 17 +++++- .../source/SingleSampleMediaSource.java | 7 ++- .../exoplayer2/MediaPeriodQueueTest.java | 3 +- .../source/ClippingMediaSourceTest.java | 60 ++++++++++++++++--- .../source/SinglePeriodTimelineTest.java | 8 ++- .../source/dash/DashMediaSource.java | 15 +++-- .../exoplayer2/source/hls/HlsMediaSource.java | 2 + .../source/smoothstreaming/SsMediaSource.java | 5 +- .../exoplayer2/testutil/FakeTimeline.java | 1 + 17 files changed, 138 insertions(+), 41 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 349b904589..5743d9944c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -80,6 +80,9 @@ and move it to the core library. * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. +* Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). ### 2.10.5 (2019-09-20) ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index 2857141f8f..54ff7e6777 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -121,6 +121,7 @@ import java.util.Arrays; /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, + /* isLive= */ isDynamic, defaultPositionsUs[windowIndex], durationUs, /* firstPeriodIndex= */ windowIndex, diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 2995df4ab4..edaa4cde29 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -63,7 +63,8 @@ public class ImaAdsLoaderTest { private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = - new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); private static final Uri TEST_URI = Uri.EMPTY; private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index c496052f94..ce1a58822c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -66,8 +66,9 @@ import com.google.android.exoplayer2.util.Assertions; * duration is unknown, since it's continually extending as more content is broadcast. If content * only remains available for a limited period of time then the window may start at a non-zero * position, defining the region of content that can still be played. The window will have {@link - * Window#isDynamic} set to true if the stream is still live. Its default position is typically near - * to the live edge (indicated by the black dot in the figure above). + * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to + * true as long as we expect changes to the live window. Its default position is typically near to + * the live edge (indicated by the black dot in the figure above). * *

Live stream with indefinite availability

* @@ -158,8 +159,13 @@ public abstract class Timeline { public boolean isDynamic; /** - * The index of the first period that belongs to this window. + * Whether the media in this window is live. For informational purposes only. + * + *

Check {@link #isDynamic} to know whether this window may still change. */ + public boolean isLive; + + /** The index of the first period that belongs to this window. */ public int firstPeriodIndex; /** @@ -200,6 +206,7 @@ public abstract class Timeline { long windowStartTimeMs, boolean isSeekable, boolean isDynamic, + boolean isLive, long defaultPositionUs, long durationUs, int firstPeriodIndex, @@ -212,6 +219,7 @@ public abstract class Timeline { this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.defaultPositionUs = defaultPositionUs; this.durationUs = durationUs; this.firstPeriodIndex = firstPeriodIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 8727fc5ed9..891cb351c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -317,6 +317,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { /* isSeekable= */ false, // Dynamic window to indicate pending timeline updates. /* isDynamic= */ true, + /* isLive= */ false, /* defaultPositionUs= */ 0, /* durationUs= */ C.TIME_UNSET, /* firstPeriodIndex= */ 0, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index c768fe4981..4af5bafdda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -74,13 +74,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; interface Listener { /** - * Called when the duration or ability to seek within the period changes. + * Called when the duration, the ability to seek within the period, or the categorization as + * live stream changes. * * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. * @param isSeekable Whether the period is seekable. + * @param isLive Whether the period is live. */ - void onSourceInfoRefreshed(long durationUs, boolean isSeekable); - + void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive); } /** @@ -129,6 +130,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private int enabledTrackCount; private long durationUs; private long length; + private boolean isLive; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -551,7 +553,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - listener.onSourceInfoRefreshed(durationUs, isSeekable); + listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); } eventDispatcher.loadCompleted( loadable.dataSpec, @@ -740,14 +742,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } trackArray[i] = new TrackGroup(trackFormat); } - dataType = - length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET - ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE - : C.DATA_TYPE_MEDIA; + isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; + dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; preparedState = new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags); prepared = true; - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); Assertions.checkNotNull(callback).onPrepared(this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 80bcdcd029..c88972da62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -220,6 +220,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private long timelineDurationUs; private boolean timelineIsSeekable; + private boolean timelineIsLive; @Nullable private TransferListener transferListener; // TODO: Make private when ExtractorMediaSource is deleted. @@ -253,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; drmSessionManager.prepare(); - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); } @Override @@ -293,27 +294,32 @@ public final class ProgressiveMediaSource extends BaseMediaSource // ProgressiveMediaPeriod.Listener implementation. @Override - public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) { + if (timelineDurationUs == durationUs + && timelineIsSeekable == isSeekable + && timelineIsLive == isLive) { // Suppress no-op source info changes. return; } - notifySourceInfoRefreshed(durationUs, isSeekable); + notifySourceInfoRefreshed(durationUs, isSeekable, isLive); } // Internal methods. - private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. + timelineIsLive = isLive; + // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then + // indicate that the duration may change until it's known. See [internal: b/69703223]. refreshSourceInfo( new SinglePeriodTimeline( timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, + /* isLive= */ timelineIsLive, /* manifest= */ null, tag)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index 3bb7ada7e0..a950a95457 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -68,7 +68,8 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false)); + new SinglePeriodTimeline( + durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false)); } @Override 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 49d67935a5..45f64cacf2 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 @@ -35,6 +35,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; + private final boolean isLive; @Nullable private final Object tag; @Nullable private final Object manifest; @@ -44,9 +45,11 @@ public final class SinglePeriodTimeline extends Timeline { * @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. + * @param isLive Whether the window is live. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, isSeekable, isDynamic, /* manifest= */ null, /* tag= */ null); + public SinglePeriodTimeline( + long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) { + this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null); } /** @@ -55,6 +58,7 @@ public final class SinglePeriodTimeline extends Timeline { * @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. + * @param isLive Whether the window is live. * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Window#tag}. */ @@ -62,6 +66,7 @@ public final class SinglePeriodTimeline extends Timeline { long durationUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -71,6 +76,7 @@ public final class SinglePeriodTimeline extends Timeline { /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -87,6 +93,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. * @param manifest The manifest. May be (@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ @@ -97,6 +104,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -108,6 +116,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -127,6 +136,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ @@ -139,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; @@ -149,6 +160,7 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.manifest = manifest; this.tag = tag; } @@ -182,6 +194,7 @@ public final class SinglePeriodTimeline extends Timeline { windowStartTimeMs, isSeekable, isDynamic, + isLive, windowDefaultStartPositionUs, windowDurationUs, 0, 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 04ee3a153c..be939fd018 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 @@ -291,7 +291,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = new SinglePeriodTimeline( - durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* manifest= */ null, tag); + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index afcce904e9..1a0e13b6c1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -44,7 +44,8 @@ public final class MediaPeriodQueueTest { private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = - new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); private static final Uri AD_URI = Uri.EMPTY; private MediaPeriodQueue mediaPeriodQueue; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 89acb3ec3e..532ad61b85 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -64,7 +64,12 @@ public final class ClippingMediaSourceTest { @Test public void testNoClipping() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -78,7 +83,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingUnseekableWindowThrows() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, false, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* isLive= */ false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -93,7 +103,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingStart() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -105,7 +120,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -121,7 +141,8 @@ public final class ClippingMediaSourceTest { // to it having loaded sufficient data to establish its duration and seekability. Such timelines // should not result in clipping failure. Timeline timeline = - new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true); + new SinglePeriodTimeline( + C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); Timeline clippedTimeline = getClippedTimeline( @@ -139,7 +160,8 @@ public final class ClippingMediaSourceTest { new SinglePeriodTimeline( /* durationUs= */ TEST_PERIOD_DURATION_US, /* isSeekable= */ true, - /* isDynamic= */ false); + /* isDynamic= */ false, + /* isLive= */ false); // When clipping to the end, the clipped timeline should also have a duration. Timeline clippedTimeline = @@ -153,7 +175,10 @@ public final class ClippingMediaSourceTest { // Create a child timeline that has an unknown duration. Timeline timeline = new SinglePeriodTimeline( - /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ true, /* isDynamic= */ false); + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); // When clipping to the end, the clipped timeline should also have an unset duration. Timeline clippedTimeline = @@ -164,7 +189,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingStartAndEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline( @@ -185,6 +215,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -207,6 +238,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -217,6 +249,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -256,6 +289,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -266,6 +300,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -305,6 +340,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -315,6 +351,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -355,6 +392,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -365,6 +403,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -480,7 +519,10 @@ public final class ClippingMediaSourceTest { throws IOException { Timeline timeline = new SinglePeriodTimeline( - TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline) { @Override 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 index cb21db8212..6ff4f78fa2 100644 --- 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 @@ -41,7 +41,9 @@ public final class SinglePeriodTimelineTest { @Test public void testGetPeriodPositionDynamicWindowUnknownDuration() { - SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); // Should return null with any positive position projection. Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); assertThat(position).isNull(); @@ -62,6 +64,7 @@ public final class SinglePeriodTimelineTest { /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ false, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); // Should return null with a positive position projection beyond window duration. @@ -85,6 +88,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, /* tag= */ null); @@ -104,6 +108,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, tag); @@ -117,6 +122,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, /* tag= */ null); Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index ee168f0458..352131d70a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -1216,10 +1216,6 @@ public final class DashMediaSource extends BaseMediaSource { Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); - boolean isDynamic = - manifest.dynamic - && manifest.minUpdatePeriodMs != C.TIME_UNSET - && manifest.durationMs == C.TIME_UNSET; return window.set( Window.SINGLE_WINDOW_UID, windowTag, @@ -1227,7 +1223,8 @@ public final class DashMediaSource extends BaseMediaSource { presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, - isDynamic, + /* isDynamic= */ isMovingLiveWindow(manifest), + /* isLive= */ manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, @@ -1247,7 +1244,7 @@ public final class DashMediaSource extends BaseMediaSource { private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (!manifest.dynamic) { + if (!isMovingLiveWindow(manifest)) { return windowDefaultStartPositionUs; } if (defaultPositionProjectionUs > 0) { @@ -1292,6 +1289,12 @@ public final class DashMediaSource extends BaseMediaSource { Assertions.checkIndex(periodIndex, 0, getPeriodCount()); return firstPeriodId + periodIndex; } + + private static boolean isMovingLiveWindow(DashManifest manifest) { + return manifest.dynamic + && manifest.minUpdatePeriodMs != C.TIME_UNSET + && manifest.durationMs == C.TIME_UNSET; + } } private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 1bda50ba88..f058ad5ba2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -439,6 +439,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + /* isLive= */ true, manifest, tag); } else /* not live */ { @@ -455,6 +456,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, manifest, tag); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 9ddb1e3932..4c05353186 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -696,7 +696,8 @@ public final class SsMediaSource extends BaseMediaSource /* windowPositionInPeriodUs= */ 0, /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, - manifest.isLive, + /* isDynamic= */ manifest.isLive, + /* isLive= */ manifest.isLive, manifest, tag); } else if (manifest.isLive) { @@ -719,6 +720,7 @@ public final class SsMediaSource extends BaseMediaSource defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, manifest, tag); } else { @@ -732,6 +734,7 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, manifest, tag); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index af672f0da3..401fcf8034 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -189,6 +189,7 @@ public final class FakeTimeline extends Timeline { /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, + /* isLive= */ windowDefinition.isDynamic, /* defaultPositionUs= */ 0, windowDefinition.durationUs, periodOffsets[windowIndex],