From 6e481178eaee66fca53cd98452b0153845083c1b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 17 Jan 2017 03:58:15 -0800 Subject: [PATCH] Track HLS discontinuities when playlist does not declare sequence This is an initial version that does not handle cross-playlists adjustment in an ideal way. Issue:#1789 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144692969 --- .../playlist/HlsMediaPlaylistParserTest.java | 94 ++++++++++--------- .../exoplayer2/source/hls/HlsChunkSource.java | 9 +- .../source/hls/playlist/HlsMediaPlaylist.java | 37 ++++---- .../hls/playlist/HlsPlaylistParser.java | 14 ++- .../hls/playlist/HlsPlaylistTracker.java | 78 +++++++++------ 5 files changed, 136 insertions(+), 96 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 67ec907d61..8eacecf9d3 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -73,59 +74,64 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(3, mediaPlaylist.version); - assertEquals(true, mediaPlaylist.hasEndTag); - List segments = mediaPlaylist.segments; + assertTrue(mediaPlaylist.hasEndTag); + List segments = mediaPlaylist.segments; assertNotNull(segments); assertEquals(5, segments.size()); - assertEquals(4, segments.get(0).discontinuitySequenceNumber); - assertEquals(7975000, segments.get(0).durationUs); - assertEquals(false, segments.get(0).isEncrypted); - assertEquals(null, segments.get(0).encryptionKeyUri); - assertEquals(null, segments.get(0).encryptionIV); - assertEquals(51370, segments.get(0).byterangeLength); - assertEquals(0, segments.get(0).byterangeOffset); - assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); + Segment segment = segments.get(0); + assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence); + assertEquals(7975000, segment.durationUs); + assertFalse(segment.isEncrypted); + assertEquals(null, segment.encryptionKeyUri); + assertEquals(null, segment.encryptionIV); + assertEquals(51370, segment.byterangeLength); + assertEquals(0, segment.byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url); - assertEquals(4, segments.get(1).discontinuitySequenceNumber); - assertEquals(7975000, segments.get(1).durationUs); - assertEquals(true, segments.get(1).isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); - assertEquals("0x1566B", segments.get(1).encryptionIV); - assertEquals(51501, segments.get(1).byterangeLength); - assertEquals(2147483648L, segments.get(1).byterangeOffset); - assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); + segment = segments.get(1); + assertEquals(0, segment.relativeDiscontinuitySequence); + assertEquals(7975000, segment.durationUs); + assertTrue(segment.isEncrypted); + assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri); + assertEquals("0x1566B", segment.encryptionIV); + assertEquals(51501, segment.byterangeLength); + assertEquals(2147483648L, segment.byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url); - assertEquals(4, segments.get(2).discontinuitySequenceNumber); - assertEquals(7941000, segments.get(2).durationUs); - assertEquals(false, segments.get(2).isEncrypted); - assertEquals(null, segments.get(2).encryptionKeyUri); - assertEquals(null, segments.get(2).encryptionIV); - assertEquals(51501, segments.get(2).byterangeLength); - assertEquals(2147535149L, segments.get(2).byterangeOffset); - assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); + segment = segments.get(2); + assertEquals(0, segment.relativeDiscontinuitySequence); + assertEquals(7941000, segment.durationUs); + assertFalse(segment.isEncrypted); + assertEquals(null, segment.encryptionKeyUri); + assertEquals(null, segment.encryptionIV); + assertEquals(51501, segment.byterangeLength); + assertEquals(2147535149L, segment.byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url); - assertEquals(5, segments.get(3).discontinuitySequenceNumber); - assertEquals(7975000, segments.get(3).durationUs); - assertEquals(true, segments.get(3).isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); + segment = segments.get(3); + assertEquals(1, segment.relativeDiscontinuitySequence); + assertEquals(7975000, segment.durationUs); + assertTrue(segment.isEncrypted); + assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); // 0xA7A == 2682. - assertNotNull(segments.get(3).encryptionIV); - assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault())); - assertEquals(51740, segments.get(3).byterangeLength); - assertEquals(2147586650L, segments.get(3).byterangeOffset); - assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); + assertNotNull(segment.encryptionIV); + assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault())); + assertEquals(51740, segment.byterangeLength); + assertEquals(2147586650L, segment.byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url); - assertEquals(5, segments.get(4).discontinuitySequenceNumber); - assertEquals(7975000, segments.get(4).durationUs); - assertEquals(true, segments.get(4).isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); + segment = segments.get(4); + assertEquals(1, segment.relativeDiscontinuitySequence); + assertEquals(7975000, segment.durationUs); + assertTrue(segment.isEncrypted); + assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); // 0xA7B == 2683. - assertNotNull(segments.get(4).encryptionIV); - assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault())); - assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength); - assertEquals(0, segments.get(4).byterangeOffset); - assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url); + assertNotNull(segment.encryptionIV); + assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault())); + assertEquals(C.LENGTH_UNSET, segment.byterangeLength); + assertEquals(0, segment.byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url); } catch (IOException exception) { fail(exception.getMessage()); } 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 b953fcf79c..edd3c735c1 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 @@ -270,8 +270,10 @@ import java.util.Locale; // Compute start time of the next chunk. long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + int discontinuitySequence = mediaPlaylist.discontinuitySequence + + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); + discontinuitySequence, startTimeUs); // Configure the data source and spec for the chunk. Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); @@ -279,9 +281,8 @@ import java.util.Locale; null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, - segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous, - encryptionKey, encryptionIv); + startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, + isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); } /** 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 079d4001fb..0b61b9781e 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 @@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final String url; public final long durationUs; - public final int discontinuitySequenceNumber; + public final int relativeDiscontinuitySequence; public final long relativeStartTimeUs; public final boolean isEncrypted; public final String encryptionKeyUri; @@ -43,12 +43,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); } - public Segment(String uri, long durationUs, int discontinuitySequenceNumber, + public Segment(String uri, long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = uri; this.durationUs = durationUs; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; + this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; this.isEncrypted = isEncrypted; this.encryptionKeyUri = encryptionKeyUri; @@ -67,6 +67,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final long startOffsetUs; public final long startTimeUs; + public final boolean hasDiscontinuitySequence; + public final int discontinuitySequence; public final int mediaSequence; public final int version; public final long targetDurationUs; @@ -76,11 +78,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final List segments; public final long durationUs; - public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs, int mediaSequence, - int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime, + public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs, + boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, + long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.startTimeUs = startTimeUs; + this.hasDiscontinuitySequence = hasDiscontinuitySequence; + this.discontinuitySequence = discontinuitySequence; this.mediaSequence = mediaSequence; this.version = version; this.targetDurationUs = targetDurationUs; @@ -123,19 +128,18 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Returns a playlist identical to this one except for the start time, which is set to the - * specified value. If the start time already equals the specified value then the playlist will - * return itself. + * Returns a playlist identical to this one except for the start time, the discontinuity sequence + * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values, + * {@code hasDiscontinuitySequence} is set to true. * * @param startTimeUs The start time for the returned playlist. + * @param discontinuitySequence The discontinuity sequence for the returned playlist. * @return The playlist. */ - public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) { - if (this.startTimeUs == startTimeUs) { - return this; - } - return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, mediaSequence, version, - targetDurationUs, hasEndTag, hasProgramDateTime, initializationSegment, segments); + public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { + return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, true, discontinuitySequence, + mediaSequence, version, targetDurationUs, hasEndTag, hasProgramDateTime, + initializationSegment, segments); } /** @@ -148,8 +152,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, mediaSequence, version, - targetDurationUs, true, hasProgramDateTime, initializationSegment, segments); + return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence, + discontinuitySequence, mediaSequence, version, targetDurationUs, true, 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 e9bd5b6696..7a52c4d0b5 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 @@ -266,7 +266,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); long segmentDurationUs = 0; - int discontinuitySequenceNumber = 0; + boolean hasDiscontinuitySequence = false; + int playlistDiscontinuitySequence = 0; + int relativeDiscontinuitySequence = 0; long playlistStartTimeUs = 0; long segmentStartTimeUs = 0; long segmentByteRangeOffset = 0; @@ -323,9 +325,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser oldSegments = oldPlaylist.segments; - int oldPlaylistSize = oldSegments.size(); + } + + private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null + ? primaryUrlSnapshot.discontinuitySequence : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist loadedPlaylist) { int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence; - if (mediaSequenceOffset <= oldPlaylistSize) { - long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize - ? oldPlaylist.getEndTimeUs() - : oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs; - return loadedPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs); - } - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return loadedPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs); + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; } /**