From 949e26d1baf62ead53cc29797fa4d313dfe10c23 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 26 Oct 2020 19:28:03 +0000 Subject: [PATCH] Support delta updates for media playlists Issue: #5011 PiperOrigin-RevId: 339093145 --- library/hls/build.gradle | 1 + .../playlist/DefaultHlsPlaylistTracker.java | 44 ++- .../source/hls/playlist/HlsMediaPlaylist.java | 63 +++- .../hls/playlist/HlsPlaylistParser.java | 7 + .../DefaultHlsPlaylistTrackerTest.java | 296 ++++++++++++++++++ .../playlist/HlsMediaPlaylistParserTest.java | 21 ++ .../assets/media/m3u8/live_low_latency_master | 5 + ...ve_low_latency_master_media_uri_with_param | 5 + .../m3u8/live_low_latency_media_can_not_skip | 16 + .../live_low_latency_media_can_not_skip_next | 16 + ...live_low_latency_media_can_skip_dateranges | 17 + .../live_low_latency_media_can_skip_skipped | 14 + ...skip_skipped_media_sequence_no_overlapping | 14 + .../live_low_latency_media_can_skip_until | 17 + 14 files changed, 517 insertions(+), 19 deletions(-) create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_master create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped_media_sequence_no_overlapping create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 2cc91a5105..cefa841816 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -29,6 +29,7 @@ dependencies { compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation project(modulePrefix + 'library-core') + testImplementation project(modulePrefix + 'robolectricutils') testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testdata') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index ccbcb986c1..c97cdd376a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import android.net.Uri; @@ -163,7 +164,7 @@ public final class DefaultHlsPlaylistTracker @Override public void addListener(PlaylistEventListener listener) { - Assertions.checkNotNull(listener); + checkNotNull(listener); listeners.add(listener); } @@ -390,7 +391,7 @@ public final class DefaultHlsPlaylistTracker } private HlsMediaPlaylist getLatestPlaylistSnapshot( - HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + @Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { if (!loadedPlaylist.isNewerThan(oldPlaylist)) { if (loadedPlaylist.hasEndTag) { // If the loaded playlist has an end tag but is not newer than the old playlist then we have @@ -408,7 +409,7 @@ public final class DefaultHlsPlaylistTracker } private long getLoadedPlaylistStartTimeUs( - HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + @Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { if (loadedPlaylist.hasProgramDateTime) { return loadedPlaylist.startTimeUs; } @@ -430,7 +431,7 @@ public final class DefaultHlsPlaylistTracker } private int getLoadedPlaylistDiscontinuitySequence( - HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + @Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { if (loadedPlaylist.hasDiscontinuitySequence) { return loadedPlaylist.discontinuitySequence; } @@ -464,7 +465,7 @@ public final class DefaultHlsPlaylistTracker private final Uri playlistUrl; private final Loader mediaPlaylistLoader; - private final ParsingLoadable mediaPlaylistLoadable; + private final DataSource mediaPlaylistDataSource; @Nullable private HlsMediaPlaylist playlistSnapshot; private long lastSnapshotLoadMs; @@ -477,12 +478,7 @@ public final class DefaultHlsPlaylistTracker public MediaPlaylistBundle(Uri playlistUrl) { this.playlistUrl = playlistUrl; mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = - new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - playlistUrl, - C.DATA_TYPE_MANIFEST, - mediaPlaylistParser); + mediaPlaylistDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST); } @Nullable @@ -533,7 +529,7 @@ public final class DefaultHlsPlaylistTracker @Override public void onLoadCompleted( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); + @Nullable HlsPlaylist result = loadable.getResult(); LoadEventInfo loadEventInfo = new LoadEventInfo( loadable.loadTaskId, @@ -631,6 +627,12 @@ public final class DefaultHlsPlaylistTracker // Internal methods. private void loadPlaylistImmediately() { + ParsingLoadable mediaPlaylistLoadable = + new ParsingLoadable<>( + mediaPlaylistDataSource, + getMediaPlaylistUriForRequest(playlistUrl, playlistSnapshot), + C.DATA_TYPE_MANIFEST, + mediaPlaylistParser); long elapsedRealtime = mediaPlaylistLoader.startLoading( mediaPlaylistLoadable, @@ -644,7 +646,11 @@ public final class DefaultHlsPlaylistTracker private void processLoadedPlaylist( HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) { - HlsMediaPlaylist oldPlaylist = playlistSnapshot; + @Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot; + loadedPlaylist = + loadedPlaylist.skippedSegmentCount > 0 + ? loadedPlaylist.expandSkippedSegments(checkNotNull(playlistSnapshot)) + : loadedPlaylist; long currentTimeMs = SystemClock.elapsedRealtime(); lastSnapshotLoadMs = currentTimeMs; playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); @@ -695,6 +701,18 @@ public final class DefaultHlsPlaylistTracker } } + private Uri getMediaPlaylistUriForRequest( + Uri playlistUri, @Nullable HlsMediaPlaylist currentMediaPlaylist) { + if (currentMediaPlaylist == null + || currentMediaPlaylist.serverControl.skipUntilUs == C.TIME_UNSET) { + return playlistUri; + } + Uri.Builder uriBuilder = playlistUri.buildUpon(); + uriBuilder.appendQueryParameter( + "_HLS_skip", currentMediaPlaylist.serverControl.canSkipDateRanges ? "v2" : "YES"); + return uriBuilder.build(); + } + /** * Excludes the playlist. * 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 022e68bc7d..1acc864fd3 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,8 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; + import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -23,6 +25,7 @@ import com.google.android.exoplayer2.offline.StreamKey; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -275,9 +278,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * The list of segments in the playlist. */ public final List segments; - /** - * The total duration of the playlist in microseconds. - */ + /** The number of skipped segments. */ + public int skippedSegmentCount; + /** The total duration of the playlist in microseconds. */ public final long durationUs; /** The attributes of the #EXT-X-SERVER-CONTROL header. */ public final ServerControl serverControl; @@ -317,6 +320,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { boolean hasProgramDateTime, @Nullable DrmInitData protectionSchemes, List segments, + int skippedSegmentCount, ServerControl serverControl) { super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; @@ -331,6 +335,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.hasProgramDateTime = hasProgramDateTime; this.protectionSchemes = protectionSchemes; this.segments = Collections.unmodifiableList(segments); + this.skippedSegmentCount = skippedSegmentCount; if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); durationUs = last.relativeStartTimeUs + last.durationUs; @@ -353,7 +358,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param other The playlist to compare. * @return Whether this playlist is newer than {@code other}. */ - public boolean isNewerThan(HlsMediaPlaylist other) { + public boolean isNewerThan(@Nullable HlsMediaPlaylist other) { if (other == null || mediaSequence > other.mediaSequence) { return true; } @@ -361,8 +366,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { return false; } // The media sequences are equal. - int segmentCount = segments.size(); - int otherSegmentCount = other.segments.size(); + int segmentCount = segments.size() + skippedSegmentCount; + int otherSegmentCount = other.segments.size() + other.skippedSegmentCount; return segmentCount > otherSegmentCount || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag); } @@ -374,6 +379,50 @@ public final class HlsMediaPlaylist extends HlsPlaylist { return startTimeUs + durationUs; } + /** + * Merges the skipped segments of the previous playlist and returns a copy with a {@link + * #skippedSegmentCount} of 0. + * + * @param previousPlaylist The previous playlist with a {@link #skippedSegmentCount} of zero. + * @return A new playlist with a complete list of segments. + */ + public HlsMediaPlaylist expandSkippedSegments(HlsMediaPlaylist previousPlaylist) { + if (skippedSegmentCount == 0) { + return this; + } + checkArgument(previousPlaylist.skippedSegmentCount == 0); + List mergedSegments = new ArrayList<>(); + long mediaSequence = this.mediaSequence; + int startIndex = (int) (mediaSequence - previousPlaylist.mediaSequence); + int endIndex = startIndex + skippedSegmentCount; + if (startIndex >= 0 && endIndex <= previousPlaylist.segments.size()) { + mergedSegments.addAll(previousPlaylist.segments.subList(startIndex, endIndex)); + } else { + // Adjust the media sequence if the old playlist doesn't contain all of the skipped segments. + mediaSequence += skippedSegmentCount; + } + mergedSegments.addAll(segments); + return new HlsMediaPlaylist( + playlistType, + baseUri, + tags, + startOffsetUs, + startTimeUs, + hasDiscontinuitySequence, + discontinuitySequence, + mediaSequence, + version, + targetDurationUs, + partTargetDurationUs, + hasIndependentSegments, + hasEndTag, + hasProgramDateTime, + protectionSchemes, + mergedSegments, + /* skippedSegmentCount= */ 0, + serverControl); + } + /** * 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, @@ -401,6 +450,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { hasProgramDateTime, protectionSchemes, segments, + skippedSegmentCount, serverControl); } @@ -429,6 +479,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { hasProgramDateTime, protectionSchemes, segments, + skippedSegmentCount, serverControl); } 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 7e44bcfa51..587d8c6a26 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 @@ -90,6 +90,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser dataSourceQueue = new ArrayDeque<>(); + dataSourceQueue.add(new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MASTER))); + dataSourceQueue.add( + new DataSourceList( + new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP)), + new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT)))); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + /* dataSourceFactory= */ dataSourceQueue::remove, + masterPlaylistUri, + /* awaitedMediaPlaylistCount= */ 2); + + HlsMediaPlaylist firstFullPlaylist = mediaPlaylists.get(0); + assertThat(firstFullPlaylist.mediaSequence).isEqualTo(10); + assertThat(firstFullPlaylist.segments.get(0).url).isEqualTo("fileSequence10.ts"); + assertThat(firstFullPlaylist.segments.get(5).url).isEqualTo("fileSequence15.ts"); + assertThat(firstFullPlaylist.segments).hasSize(6); + HlsMediaPlaylist secondFullPlaylist = mediaPlaylists.get(1); + assertThat(secondFullPlaylist.mediaSequence).isEqualTo(11); + assertThat(secondFullPlaylist.skippedSegmentCount).isEqualTo(0); + assertThat(secondFullPlaylist.segments.get(0).url).isEqualTo("fileSequence11.ts"); + assertThat(secondFullPlaylist.segments.get(5).url).isEqualTo("fileSequence16.ts"); + assertThat(secondFullPlaylist.segments).hasSize(6); + assertThat(secondFullPlaylist.segments).containsNoneIn(firstFullPlaylist.segments); + } + + @Test + public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments() + throws IOException, TimeoutException { + Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); + Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); + Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES"); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) + .setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) + .setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), + masterPlaylistUri, + /* awaitedMediaPlaylistCount= */ 2); + + HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0); + assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10); + assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); + HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1); + assertThat(mergedPlaylist.mediaSequence).isEqualTo(11); + assertThat(mergedPlaylist.skippedSegmentCount).isEqualTo(0); + assertThat(mergedPlaylist.segments).hasSize(6); + // First 2 segments of the merged playlist need to be copied from the previous playlist. + assertThat(mergedPlaylist.segments.subList(0, 2)) + .containsExactlyElementsIn(initialPlaylistWithAllSegments.segments.subList(1, 3)) + .inOrder(); + assertThat(mergedPlaylist.segments.get(2).url) + .isEqualTo(initialPlaylistWithAllSegments.segments.get(3).url); + } + + @Test + public void start_playlistCanSkip_missingSegments_correctedMediaSequence() + throws IOException, TimeoutException { + Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); + Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); + Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES"); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) + .setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) + .setData( + mediaPlaylistSkippedUri, + getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING)); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), + masterPlaylistUri, + /* awaitedMediaPlaylistCount= */ 2); + + HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0); + assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10); + assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); + HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1); + assertThat(mergedPlaylist.mediaSequence).isEqualTo(22); + assertThat(mergedPlaylist.skippedSegmentCount).isEqualTo(0); + assertThat(mergedPlaylist.segments).hasSize(4); + } + + @Test + public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2() + throws IOException, TimeoutException { + Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); + Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); + // Expect _HLS_skip parameter with value v2. + Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=v2"); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) + .setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES)) + .setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), + masterPlaylistUri, + /* awaitedMediaPlaylistCount= */ 2); + + // Finding the media sequence of the second playlist request asserts that the second request has + // been made with the correct uri parameter appended. + assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11); + } + + @Test + public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams() + throws IOException, TimeoutException { + Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); + Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8?param1=1¶m2=2"); + // Expect _HLS_skip parameter appended with an ampersand. + Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "&_HLS_skip=YES"); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM)) + .setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) + .setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), + masterPlaylistUri, + /* awaitedMediaPlaylistCount= */ 2); + + // Finding the media sequence of the second playlist request asserts that the second request has + // been made with the original uri parameters preserved and the additional param concatenated + // correctly. + assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11); + } + + private static List runPlaylistTrackerAndCollectMediaPlaylists( + DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount) + throws TimeoutException { + + DefaultHlsPlaylistTracker defaultHlsPlaylistTracker = + new DefaultHlsPlaylistTracker( + dataType -> dataSourceFactory.createDataSource(), + new DefaultLoadErrorHandlingPolicy(), + new DefaultHlsPlaylistParserFactory()); + + List mediaPlaylists = new ArrayList<>(); + AtomicInteger playlistCounter = new AtomicInteger(); + defaultHlsPlaylistTracker.start( + masterPlaylistUri, + new MediaSourceEventListener.EventDispatcher(), + mediaPlaylist -> { + mediaPlaylists.add(mediaPlaylist); + playlistCounter.addAndGet(1); + }); + + RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() == awaitedMediaPlaylistCount); + + defaultHlsPlaylistTracker.stop(); + return mediaPlaylists; + } + + private static byte[] getBytes(String filename) throws IOException { + return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), filename); + } + + private static final class DataSourceList implements DataSource { + + private final DataSource[] dataSources; + + private DataSource delegate; + private int index; + + /** + * Creates an instance. + * + * @param dataSources The data sources to delegate to. + */ + public DataSourceList(DataSource... dataSources) { + checkArgument(dataSources.length > 0); + this.dataSources = dataSources; + delegate = dataSources[index++]; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + for (DataSource dataSource : dataSources) { + dataSource.addTransferListener(transferListener); + } + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + checkState(index <= dataSources.length); + return delegate.open(dataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return delegate.read(buffer, offset, readLength); + } + + @Override + @Nullable + public Uri getUri() { + return delegate.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return delegate.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + delegate.close(); + if (index < dataSources.length) { + delegate = dataSources[index]; + } + index++; + } + } +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 563d8ab3ef..e92f9a8027 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -299,6 +299,27 @@ public class HlsMediaPlaylistParserTest { assertThat(playlist.serverControl.canSkipDateRanges).isTrue(); } + @Test + public void parseMediaPlaylist_withSkippedSegments_parsesNumberOfSkippedSegments() + throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n" + + "#EXT-X-MEDIA-SEQUENCE:266\n" + + "#EXT-X-SKIP:SKIPPED-SEGMENTS=1234\n" + + "#EXTINF:4.00008,\n" + + "fileSequence266.mp4"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + + assertThat(playlist.skippedSegmentCount).isEqualTo(1234); + } + @Test public void multipleExtXKeysForSingleSegment() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_master b/testdata/src/test/assets/media/m3u8/live_low_latency_master new file mode 100644 index 0000000000..e595fcaceb --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_master @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2" +media0/playlist.m3u8 diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param b/testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param new file mode 100644 index 0000000000..096472d491 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS + +#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2" +media0/playlist.m3u8?param1=1¶m2=2 diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip new file mode 100644 index 0000000000..410366456c --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:10 +#EXTINF:4.00000, +fileSequence10.ts +#EXTINF:4.00000, +fileSequence11.ts +#EXTINF:4.00000, +fileSequence12.ts +#EXTINF:4.00000, +fileSequence13.ts +#EXTINF:4.00000, +fileSequence14.ts +#EXTINF:4.00000, +fileSequence15.ts diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next new file mode 100644 index 0000000000..7683359742 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:11 +#EXTINF:4.00000, +fileSequence11.ts +#EXTINF:4.00000, +fileSequence12.ts +#EXTINF:4.00000, +fileSequence13.ts +#EXTINF:4.00000, +fileSequence14.ts +#EXTINF:4.00000, +fileSequence15.ts +#EXTINF:4.00000, +fileSequence16.ts diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges new file mode 100644 index 0000000000..b3ccbaad3c --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:10 +#EXTINF:4.00000, +fileSequence10.ts +#EXTINF:4.00000, +fileSequence11.ts +#EXTINF:4.00000, +fileSequence12.ts +#EXTINF:4.00000, +fileSequence13.ts +#EXTINF:4.00000, +fileSequence14.ts +#EXTINF:4.00000, +fileSequence15.ts +#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-SKIP-DATERANGES=YES diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped new file mode 100644 index 0000000000..05a9fdefb1 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:9 +#EXT-X-MEDIA-SEQUENCE:11 +#EXT-X-SKIP:SKIPPED-SEGMENTS=2 +#EXTINF:4.00000, +fileSequence13.ts +#EXTINF:4.00000, +fileSequence14.ts +#EXTINF:4.00000, +fileSequence15.ts +#EXTINF:4.00000, +fileSequence16.ts +#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24 diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped_media_sequence_no_overlapping b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped_media_sequence_no_overlapping new file mode 100644 index 0000000000..639b7f5af4 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped_media_sequence_no_overlapping @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:9 +#EXT-X-MEDIA-SEQUENCE:20 +#EXT-X-SKIP:SKIPPED-SEGMENTS=2 +#EXTINF:4.00000, +fileSequence22.ts +#EXTINF:4.00000, +fileSequence23.ts +#EXTINF:4.00000, +fileSequence24.ts +#EXTINF:4.00000, +fileSequence25.ts +#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24 diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until new file mode 100644 index 0000000000..140fe5556a --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:10 +#EXTINF:4.00000, +fileSequence10.ts +#EXTINF:4.00000, +fileSequence11.ts +#EXTINF:4.00000, +fileSequence12.ts +#EXTINF:4.00000, +fileSequence13.ts +#EXTINF:4.00000, +fileSequence14.ts +#EXTINF:4.00000, +fileSequence15.ts +#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24