From 52ee246edb69b5b274f461f0ede1fff89c27e3df Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 12 May 2021 13:54:44 +0100 Subject: [PATCH] Merge pull request #8767 from uvjustin:hls-start-from-independent-part PiperOrigin-RevId: 373343326 --- RELEASENOTES.md | 2 + .../exoplayer2/source/hls/HlsMediaSource.java | 240 ++++++++++++------ .../source/hls/playlist/HlsMediaPlaylist.java | 9 +- .../hls/playlist/HlsPlaylistParser.java | 5 + .../source/hls/HlsMediaSourceTest.java | 219 ++++++++++++++-- 5 files changed, 382 insertions(+), 93 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ea5da4348d..d5598903f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,6 +18,8 @@ content being played ([#8776](https://github.com/google/ExoPlayer/issues/8776)). * HLS + * Use the `PRECISE` attribute in `EXT-X-START` to select the default start + position. * Fix a bug where skipping into spliced-in chunks triggered an assertion error ([#8937](https://github.com/google/ExoPlayer/issues/8937). * DRM: 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 1927ceb0cb..baae5fd607 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 @@ -514,9 +514,8 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { - SinglePeriodTimeline timeline; - long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) - : C.TIME_UNSET; + long windowStartTimeMs = + playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) : C.TIME_UNSET; // For playlist types EVENT and VOD we know segments are never removed, so the presentation // started at the same time as the window. Otherwise, we don't know the presentation start time. long presentationStartTimeMs = @@ -524,87 +523,127 @@ public final class HlsMediaSource extends BaseMediaSource || playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD ? windowStartTimeMs : C.TIME_UNSET; - long windowDefaultStartPositionUs = playlist.startOffsetUs; - // masterPlaylist is non-null because the first playlist has been fetched by now. + // The master playlist is non-null because the first playlist has been fetched by now. HlsManifest manifest = new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist); - if (playlistTracker.isLive()) { - long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist); - long targetLiveOffsetUs = - liveConfiguration.targetOffsetMs != C.TIME_UNSET - ? C.msToUs(liveConfiguration.targetOffsetMs) - : getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs); - // Ensure target live offset is within the live window and greater than the live edge offset. - targetLiveOffsetUs = - Util.constrainValue( - targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs); - maybeUpdateMediaItem(targetLiveOffsetUs); - - long offsetFromInitialStartTimeUs = - playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long periodDurationUs = - playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET; - List segments = playlist.segments; - if (!segments.isEmpty()) { - windowDefaultStartPositionUs = getWindowDefaultStartPosition(playlist, liveEdgeOffsetUs); - } else if (windowDefaultStartPositionUs == C.TIME_UNSET) { - windowDefaultStartPositionUs = 0; - } - timeline = - new SinglePeriodTimeline( - presentationStartTimeMs, - windowStartTimeMs, - /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, - periodDurationUs, - /* windowDurationUs= */ playlist.durationUs, - /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, - windowDefaultStartPositionUs, - /* isSeekable= */ true, - /* isDynamic= */ !playlist.hasEndTag, - manifest, - mediaItem, - liveConfiguration); - } else /* not live */ { - if (windowDefaultStartPositionUs == C.TIME_UNSET) { - windowDefaultStartPositionUs = 0; - } - timeline = - new SinglePeriodTimeline( - presentationStartTimeMs, - windowStartTimeMs, - /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, - /* periodDurationUs= */ playlist.durationUs, - /* windowDurationUs= */ playlist.durationUs, - /* windowPositionInPeriodUs= */ 0, - windowDefaultStartPositionUs, - /* isSeekable= */ true, - /* isDynamic= */ false, - manifest, - mediaItem, - /* liveConfiguration= */ null); - } + SinglePeriodTimeline timeline = + playlistTracker.isLive() + ? createTimelineForLive(playlist, presentationStartTimeMs, windowStartTimeMs, manifest) + : createTimelineForOnDemand( + playlist, presentationStartTimeMs, windowStartTimeMs, manifest); refreshSourceInfo(timeline); } + private SinglePeriodTimeline createTimelineForLive( + HlsMediaPlaylist playlist, + long presentationStartTimeMs, + long windowStartTimeMs, + HlsManifest manifest) { + long offsetFromInitialStartTimeUs = + playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long periodDurationUs = + playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET; + long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist); + long targetLiveOffsetUs; + if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) { + // Media item has a defined target offset. + targetLiveOffsetUs = C.msToUs(liveConfiguration.targetOffsetMs); + } else { + // Decide target offset from playlist. + targetLiveOffsetUs = getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs); + } + // Ensure target live offset is within the live window and greater than the live edge offset. + targetLiveOffsetUs = + Util.constrainValue( + targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs); + maybeUpdateLiveConfiguration(targetLiveOffsetUs); + long windowDefaultStartPositionUs = + getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs); + return new SinglePeriodTimeline( + presentationStartTimeMs, + windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + periodDurationUs, + /* windowDurationUs= */ playlist.durationUs, + /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, + windowDefaultStartPositionUs, + /* isSeekable= */ true, + /* isDynamic= */ !playlist.hasEndTag, + manifest, + mediaItem, + liveConfiguration); + } + + private SinglePeriodTimeline createTimelineForOnDemand( + HlsMediaPlaylist playlist, + long presentationStartTimeMs, + long windowStartTimeMs, + HlsManifest manifest) { + long windowDefaultStartPositionUs; + 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; + } else { + windowDefaultStartPositionUs = + findClosestPrecedingSegment(playlist.segments, startOffsetUs).relativeStartTimeUs; + } + } + return new SinglePeriodTimeline( + presentationStartTimeMs, + windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + /* periodDurationUs= */ playlist.durationUs, + /* windowDurationUs= */ playlist.durationUs, + /* windowPositionInPeriodUs= */ 0, + windowDefaultStartPositionUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + manifest, + mediaItem, + /* liveConfiguration= */ null); + } + private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) { return playlist.hasProgramDateTime ? C.msToUs(Util.getNowUnixTimeMs(elapsedRealTimeOffsetMs)) - playlist.getEndTimeUs() : 0; } - private long getWindowDefaultStartPosition(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) { - List segments = playlist.segments; - int segmentIndex = segments.size() - 1; - long minStartPositionUs = - playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs); - while (segmentIndex > 0 - && segments.get(segmentIndex).relativeStartTimeUs > minStartPositionUs) { - segmentIndex--; + 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); } - return segments.get(segmentIndex).relativeStartTimeUs; + long maxStartPositionUs = + playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs); + @Nullable + HlsMediaPlaylist.Part part = + findClosestPrecedingIndependentPart(playlist.trailingParts, maxStartPositionUs); + if (part != null) { + return part.relativeStartTimeUs; + } + if (playlist.segments.isEmpty()) { + return 0; + } + HlsMediaPlaylist.Segment segment = + findClosestPrecedingSegment(playlist.segments, maxStartPositionUs); + part = findClosestPrecedingIndependentPart(segment.parts, maxStartPositionUs); + if (part != null) { + return part.relativeStartTimeUs; + } + return segment.relativeStartTimeUs; } - private void maybeUpdateMediaItem(long targetLiveOffsetUs) { + private void maybeUpdateLiveConfiguration(long targetLiveOffsetUs) { long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs); if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) { liveConfiguration = @@ -612,21 +651,68 @@ public final class HlsMediaSource extends BaseMediaSource } } + /** + * Gets the target live offset, in microseconds, for a live playlist. + * + *

The target offset is derived by checking the following in this order: + * + *

    + *
  1. The playlist defines a start offset. + *
  2. The playlist defines a part hold back in server control and has part duration. + *
  3. The playlist defines a hold back in server control. + *
  4. Fallback to {@code 3 x target duration}. + *
+ * + * @param playlist The playlist. + * @param liveEdgeOffsetUs The current live edge offset. + * @return The selected target live offset, in microseconds. + */ private static long getTargetLiveOffsetUs(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) { HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl; - // Select part hold back only if the playlist has a part target duration. - long offsetToEndOfPlaylistUs; + long targetOffsetUs; if (playlist.startOffsetUs != C.TIME_UNSET) { - offsetToEndOfPlaylistUs = playlist.durationUs - playlist.startOffsetUs; + // 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; } else if (serverControl.partHoldBackUs != C.TIME_UNSET && playlist.partTargetDurationUs != C.TIME_UNSET) { - offsetToEndOfPlaylistUs = serverControl.partHoldBackUs; + // Select part hold back only if the playlist has a part target duration. + targetOffsetUs = serverControl.partHoldBackUs; } else if (serverControl.holdBackUs != C.TIME_UNSET) { - offsetToEndOfPlaylistUs = serverControl.holdBackUs; + targetOffsetUs = serverControl.holdBackUs; } else { // Fallback, see RFC 8216, Section 4.4.3.8. - offsetToEndOfPlaylistUs = 3 * playlist.targetDurationUs; + targetOffsetUs = 3 * playlist.targetDurationUs; } - return offsetToEndOfPlaylistUs + liveEdgeOffsetUs; + return targetOffsetUs + liveEdgeOffsetUs; + } + + @Nullable + private static HlsMediaPlaylist.Part findClosestPrecedingIndependentPart( + List parts, long positionUs) { + @Nullable HlsMediaPlaylist.Part closestPart = null; + for (int i = 0; i < parts.size(); i++) { + HlsMediaPlaylist.Part part = parts.get(i); + if (part.relativeStartTimeUs <= positionUs && part.isIndependent) { + closestPart = part; + } else if (part.relativeStartTimeUs > positionUs) { + break; + } + } + return closestPart; + } + + /** + * Gets the segment that contains {@code positionUs}, or the last sent if the position is beyond + * the segments list. + */ + private static HlsMediaPlaylist.Segment findClosestPrecedingSegment( + List segments, long positionUs) { + int segmentIndex = + Util.binarySearchFloor( + segments, positionUs, /* inclusive= */ true, /* stayInBounds= */ true); + return segments.get(segmentIndex); } } 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 ea7303656c..218f0b55a6 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 @@ -393,9 +393,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ @PlaylistType public final int playlistType; /** - * The start offset in microseconds, as defined by #EXT-X-START. + * The start offset in microseconds, as defined by #EXT-X-START, or {@link C#TIME_UNSET} if + * undefined. */ public final long startOffsetUs; + /** Whether the start position should be precise, as defined by #EXT-X-START. */ + public final boolean preciseStart; /** * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the @@ -480,6 +483,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { String baseUri, List tags, long startOffsetUs, + boolean preciseStart, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, @@ -498,6 +502,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; this.startTimeUs = startTimeUs; + this.preciseStart = preciseStart; this.hasDiscontinuitySequence = hasDiscontinuitySequence; this.discontinuitySequence = discontinuitySequence; this.mediaSequence = mediaSequence; @@ -575,6 +580,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { baseUri, tags, startOffsetUs, + preciseStart, startTimeUs, /* hasDiscontinuitySequence= */ true, discontinuitySequence, @@ -605,6 +611,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { baseUri, tags, startOffsetUs, + preciseStart, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 7b5aa3ef3f..c24676de88 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -208,6 +208,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser