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 bafa5764a3..6b454ebf50 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 @@ -572,15 +572,12 @@ public final class HlsMediaSource extends BaseMediaSource if (playlist.startOffsetUs == C.TIME_UNSET || playlist.segments.isEmpty()) { windowDefaultStartPositionUs = 0; } else { - // From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the - // beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the - // end of the playlist. - long startOffsetUs = Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs); - if (playlist.preciseStart || startOffsetUs == playlist.durationUs) { - windowDefaultStartPositionUs = startOffsetUs; + if (playlist.preciseStart || playlist.startOffsetUs == playlist.durationUs) { + windowDefaultStartPositionUs = playlist.startOffsetUs; } else { windowDefaultStartPositionUs = - findClosestPrecedingSegment(playlist.segments, startOffsetUs).relativeStartTimeUs; + findClosestPrecedingSegment(playlist.segments, playlist.startOffsetUs) + .relativeStartTimeUs; } } return new SinglePeriodTimeline( @@ -606,17 +603,16 @@ public final class HlsMediaSource extends BaseMediaSource private long getLiveWindowDefaultStartPositionUs( HlsMediaPlaylist playlist, long liveEdgeOffsetUs) { - if (playlist.startOffsetUs != C.TIME_UNSET && playlist.preciseStart) { - // From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the - // beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the - // end of the playlist. - return Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs); + long startPositionUs = + playlist.startOffsetUs != C.TIME_UNSET + ? playlist.startOffsetUs + : playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs); + if (playlist.preciseStart) { + return startPositionUs; } - long maxStartPositionUs = - playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs); @Nullable HlsMediaPlaylist.Part part = - findClosestPrecedingIndependentPart(playlist.trailingParts, maxStartPositionUs); + findClosestPrecedingIndependentPart(playlist.trailingParts, startPositionUs); if (part != null) { return part.relativeStartTimeUs; } @@ -624,8 +620,8 @@ public final class HlsMediaSource extends BaseMediaSource return 0; } HlsMediaPlaylist.Segment segment = - findClosestPrecedingSegment(playlist.segments, maxStartPositionUs); - part = findClosestPrecedingIndependentPart(segment.parts, maxStartPositionUs); + findClosestPrecedingSegment(playlist.segments, startPositionUs); + part = findClosestPrecedingIndependentPart(segment.parts, startPositionUs); if (part != null) { return part.relativeStartTimeUs; } @@ -660,11 +656,7 @@ public final class HlsMediaSource extends BaseMediaSource HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl; long targetOffsetUs; if (playlist.startOffsetUs != C.TIME_UNSET) { - // From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the - // beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the - // end of the playlist. - long startOffsetUs = Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs); - targetOffsetUs = playlist.durationUs - startOffsetUs; + targetOffsetUs = playlist.durationUs - playlist.startOffsetUs; } else if (serverControl.partHoldBackUs != C.TIME_UNSET && playlist.partTargetDurationUs != C.TIME_UNSET) { // Select part hold back only if the playlist has a part target duration. @@ -694,8 +686,8 @@ public final class HlsMediaSource extends BaseMediaSource } /** - * Gets the segment that contains {@code positionUs}, or the last sent if the position is beyond - * the segments list. + * Gets the segment that contains {@code positionUs}, or the last segment if the position is + * beyond the segments list. */ private static HlsMediaPlaylist.Segment findClosestPrecedingSegment( List segments, long positionUs) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 9141ba8681..695f41fbee 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import static java.lang.Math.max; +import static java.lang.Math.min; + import android.net.Uri; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -392,8 +395,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { /** The type of the playlist. See {@link PlaylistType}. */ @PlaylistType public final int playlistType; /** - * The start offset in microseconds, as defined by #EXT-X-START, or {@link C#TIME_UNSET} if - * undefined. + * The start offset in microseconds from the beginning of the playlist, as defined by + * #EXT-X-START, or {@link C#TIME_UNSET} if undefined. The value is guaranteed to be between 0 and + * {@link #durationUs}, inclusive. */ public final long startOffsetUs; /** Whether the start position should be precise, as defined by #EXT-X-START. */ @@ -513,10 +517,15 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } else { durationUs = 0; } + // From RFC 8216, section 4.4.2.2: If startOffsetUs is negative, it indicates the offset from + // the end of the playlist. If the absolute value exceeds the duration of the playlist, it + // indicates the beginning (if negative) or the end (if positive) of the playlist. this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET - : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; + : startOffsetUs >= 0 + ? min(durationUs, startOffsetUs) + : max(0, durationUs + startOffsetUs); this.serverControl = serverControl; } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java index 999134fc25..635c5615e1 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java @@ -337,6 +337,44 @@ public class HlsMediaSourceTest { assertThat(window.defaultPositionUs).isEqualTo(4000000); } + @Test + public void + loadLivePlaylist_withNonPreciseStartTimeAndUserDefinedLiveOffset_startsFromPrecedingSegment() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has a duration of 16 seconds, and part hold back, hold back and start time + // defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-START:TIME-OFFSET=-10\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence1.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence2.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence3.ts\n" + + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3\n"; + // The playlist finishes 1 second before the current time. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(3000).build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000); + // The default position points to the segment containing the start time. + assertThat(window.defaultPositionUs).isEqualTo(4000000); + } + @Test public void loadLivePlaylist_withPreciseStartTime_targetLiveOffsetFromStartTime() throws TimeoutException, ParserException { @@ -375,6 +413,43 @@ public class HlsMediaSourceTest { assertThat(window.defaultPositionUs).isEqualTo(6000000); } + @Test + public void loadLivePlaylist_withPreciseStartTimeAndUserDefinedLiveOffset_startsFromStartTime() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has a duration of 16 seconds, and part hold back, hold back and start time + // defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence1.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence2.ts\n" + + "#EXTINF:4.00000,\n" + + "fileSequence3.ts\n" + + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; + // The playlist finishes 1 second before the current time. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(3000).build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000); + // The default position points to the start time. + assertThat(window.defaultPositionUs).isEqualTo(6000000); + } + @Test public void loadLivePlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem() throws TimeoutException, ParserException {