diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 87e0aebb1c3..a8c1ab15f94 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -212,7 +212,8 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true, + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, + targetPositionUs - mediaPlaylist.startTimeUs, true, !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { // We try getting the next chunk without adapting in case that's the reason for falling @@ -259,16 +260,6 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu clearEncryptionData(); } - // Compute start time and sequence number of the next chunk. - long startTimeUs = segment.startTimeUs; - if (previous != null && !switchingVariant) { - startTimeUs = previous.getAdjustedEndTimeUs(); - } - Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - - TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - DataSpec initDataSpec = null; Segment initSegment = mediaPlaylist.initializationSegment; if (initSegment != null) { @@ -277,13 +268,20 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu initSegment.byterangeLength, null); } + // Compute start time of the next chunk. + long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( + segment.discontinuitySequenceNumber, startTimeUs); + // Configure the data source and spec for the chunk. + Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment, - chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, - encryptionIv); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, + segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous, + encryptionKey, encryptionIv); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index d0ad8d817f5..0edf39ccba6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; @@ -89,8 +88,10 @@ * @param hlsUrl The url of the playlist from which this chunk was obtained. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. - * @param segment The {@link Segment} for which this media chunk is created. + * @param startTimeUs The start time of the chunk in microseconds. + * @param endTimeUs The end time of the chunk in microseconds. * @param chunkIndex The media sequence number of the chunk. + * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. @@ -98,21 +99,21 @@ * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment, - int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, + HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, + long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, + boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, segment.startTimeUs, - segment.startTimeUs + segment.durationUs, chunkIndex); + trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.previousChunk = previousChunk; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; initDataSource = dataSource; - discontinuitySequenceNumber = segment.discontinuitySequenceNumber; adjustedEndTimeUs = endTimeUs; uid = UID_SOURCE.getAndIncrement(); } @@ -136,7 +137,7 @@ public long getAdjustedStartTimeUs() { } /** - * Returns the presentation time in microseconds of the last sample in the chunk + * Returns the presentation time in microseconds of the last sample in the chunk. */ public long getAdjustedEndTimeUs() { return adjustedEndTimeUs; @@ -231,8 +232,8 @@ private Extractor buildExtractor() { } private void maybeLoadInitData() throws IOException, InterruptedException { - if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted - || initDataSpec == null) { + if ((previousChunk != null && previousChunk.extractor == extractor) + || initLoadCompleted || initDataSpec == null) { return; } DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index f418bbded3e..a5ae09d2fc0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -103,10 +103,10 @@ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { SinglePeriodTimeline timeline; if (playlistTracker.isLive()) { // TODO: fix windowPositionInPeriodUs when playlist is empty. - long windowPositionInPeriodUs = playlist.getStartTimeUs(); + long windowPositionInPeriodUs = playlist.startTimeUs; List segments = playlist.segments; long windowDefaultStartPositionUs = segments.isEmpty() ? 0 - : segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs; + : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); } else /* not live */ { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 2962d656be1..41ea2a03b9c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.C; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,7 +32,7 @@ public static final class Segment implements Comparable { public final String url; public final long durationUs; public final int discontinuitySequenceNumber; - public final long startTimeUs; + public final long relativeStartTimeUs; public final boolean isEncrypted; public final String encryptionKeyUri; public final String encryptionIV; @@ -45,12 +44,12 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) { } public Segment(String uri, long durationUs, int discontinuitySequenceNumber, - long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, + long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = uri; this.durationUs = durationUs; this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.startTimeUs = startTimeUs; + this.relativeStartTimeUs = relativeStartTimeUs; this.isEncrypted = isEncrypted; this.encryptionKeyUri = encryptionKeyUri; this.encryptionIV = encryptionIV; @@ -59,64 +58,55 @@ public Segment(String uri, long durationUs, int discontinuitySequenceNumber, } @Override - public int compareTo(Long startTimeUs) { - return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); - } - - public Segment copyWithStartTimeUs(long startTimeUs) { - return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted, - encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); + public int compareTo(Long relativeStartTimeUs) { + return this.relativeStartTimeUs > relativeStartTimeUs + ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } } + public final long startTimeUs; public final int mediaSequence; public final int version; public final Segment initializationSegment; public final List segments; public final boolean hasEndTag; + public final boolean hasProgramDateTime; public final long durationUs; - public HlsMediaPlaylist(String baseUri, int mediaSequence, int version, - boolean hasEndTag, Segment initializationSegment, List segments) { + public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version, + boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, + List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); + this.startTimeUs = startTimeUs; this.mediaSequence = mediaSequence; this.version = version; this.hasEndTag = hasEndTag; + this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { - Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + last.durationUs - first.startTimeUs; + durationUs = last.relativeStartTimeUs + last.durationUs; } else { durationUs = 0; } } - public long getStartTimeUs() { - return segments.isEmpty() ? 0 : segments.get(0).startTimeUs; + public boolean isNewerThan(HlsMediaPlaylist other) { + return other == null || mediaSequence > other.mediaSequence + || (mediaSequence == other.mediaSequence && segments.size() > other.segments.size()) + || (hasEndTag && !other.hasEndTag); } public long getEndTimeUs() { - return getStartTimeUs() + durationUs; - } - - public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) { - long startTimeOffsetUs = newStartTimeUs - getStartTimeUs(); - int segmentsSize = segments.size(); - List newSegments = new ArrayList<>(segmentsSize); - for (int i = 0; i < segmentsSize; i++) { - Segment segment = segments.get(i); - newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs)); - } - return copyWithSegments(newSegments); + return startTimeUs + durationUs; } - public HlsMediaPlaylist copyWithSegments(List segments) { - return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag, - initializationSegment, segments); + public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) { + return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag, + hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 420500615a2..3829cbadbfa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -43,6 +44,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser oldSegments = oldPlaylist.segments; + List oldSegments = oldPlaylist.segments; int oldPlaylistSize = oldSegments.size(); - int newPlaylistSize = newPlaylist.segments.size(); - int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; - if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0 - && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { + if (!newPlaylist.isNewerThan(oldPlaylist)) { // Playlist has not changed. return oldPlaylist; } - if (mediaSequenceOffset < 0) { - // Playlist has changed but media sequence has regressed. - return oldPlaylist; - } + int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; if (mediaSequenceOffset <= oldPlaylistSize) { - // We can extrapolate the start time of new segments from the segments of the old snapshot. - ArrayList newSegments = new ArrayList<>(newPlaylistSize); - for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) { - newSegments.add(oldSegments.get(i)); - } - HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); - for (int i = newSegments.size(); i < newPlaylistSize; i++) { - lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( - lastSegment.startTimeUs + lastSegment.durationUs); - newSegments.add(lastSegment); - } - return newPlaylist.copyWithSegments(newSegments); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize + ? oldPlaylist.getEndTimeUs() + : oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs; + return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs); } + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs); } /** @@ -375,31 +368,19 @@ public void setCallback(PlaylistRefreshCallback callback) { } public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) { - ArrayList segments = new ArrayList<>(latestPlaylistSnapshot.segments); int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence; - if (indexOfChunk < 0) { + if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) { return; } - Segment actualSegment = segments.get(indexOfChunk); - long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs); + Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk); + long segmentAbsoluteStartTimeUs = + actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs; + long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs); if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { return; } - segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs)); - // Propagate the adjustment backwards. - for (int i = indexOfChunk - 1; i >= 0; i--) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs)); - } - // Propagate the adjustment forward. - int segmentsSize = segments.size(); - for (int i = indexOfChunk + 1; i < segmentsSize; i++) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs)); - } - latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments); + latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs( + adjustedStartTimeUs - actualSegment.relativeStartTimeUs); } // Loader.Callback implementation.