diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1507370be5..1456657629 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ * Ad playback: * Support changing ad break positions in the player logic ([#5067](https://github.com/google/ExoPlayer/issues/5067). +* HLS + * Use the PRECISE attribute in EXT-X-START to select the default start + position. ### 2.14.0 (2021-05-13) 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 7c7ab0ff75..bafa5764a3 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 @@ -503,7 +503,6 @@ 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; // For playlist types EVENT and VOD we know segments are never removed, so the presentation @@ -513,87 +512,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 = @@ -601,21 +640,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 698d0057eb..9141ba8681 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 @@ -391,8 +391,13 @@ 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. */ + /** + * 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 @@ -467,6 +472,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { String baseUri, List tags, long startOffsetUs, + boolean preciseStart, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, @@ -485,6 +491,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; @@ -562,6 +569,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { baseUri, tags, startOffsetUs, + preciseStart, startTimeUs, /* hasDiscontinuitySequence= */ true, discontinuitySequence, @@ -592,6 +600,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 4b0da0b701..fdb5349f32 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 @@ -217,6 +217,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser