Support delta updates for media playlists

Issue: #5011
PiperOrigin-RevId: 339093145
This commit is contained in:
bachinger 2020-10-26 19:28:03 +00:00 committed by Oliver Woodman
parent 78940445fe
commit 949e26d1ba
14 changed files with 517 additions and 19 deletions

View file

@ -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

View file

@ -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<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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.
*

View file

@ -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<Segment> 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<Segment> 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<Segment> 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);
}

View file

@ -90,6 +90,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
private static final String TAG_GAP = "#EXT-X-GAP";
private static final String TAG_SKIP = "#EXT-X-SKIP";
private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO";
@ -135,6 +136,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
Pattern.compile("CAN-SKIP-UNTIL=([\\d\\.]+)\\b");
private static final Pattern REGEX_CAN_SKIP_DATE_RANGES =
compileBooleanAttrPattern("CAN-SKIP-DATERANGES");
private static final Pattern REGEX_SKIPPED_SEGMENTS =
Pattern.compile("SKIPPED-SEGMENTS=(\\d+)\\b");
private static final Pattern REGEX_HOLD_BACK = Pattern.compile("[:|,]HOLD-BACK=([\\d\\.]+)\\b");
private static final Pattern REGEX_PART_HOLD_BACK =
Pattern.compile("PART-HOLD-BACK=([\\d\\.]+)\\b");
@ -609,6 +612,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* holdBackUs= */ C.TIME_UNSET,
/* partHoldBackUs= */ C.TIME_UNSET,
/* canBlockReload= */ false);
int skippedSegmentCount = 0;
DrmInitData playlistProtectionSchemes = null;
String fullSegmentEncryptionKeyUri = null;
@ -692,6 +696,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentDurationUs =
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
} else if (line.startsWith(TAG_SKIP)) {
skippedSegmentCount = parseIntAttr(line, REGEX_SKIPPED_SEGMENTS);
} else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String keyFormat =
@ -832,6 +838,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
playlistProtectionSchemes,
segments,
skippedSegmentCount,
serverControl);
}

View file

@ -0,0 +1,296 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultHlsPlaylistTracker}. */
@RunWith(AndroidJUnit4.class)
public class DefaultHlsPlaylistTrackerTest {
private static final String SAMPLE_M3U8_LIVE_MASTER = "media/m3u8/live_low_latency_master";
private static final String SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM =
"media/m3u8/live_low_latency_master_media_uri_with_param";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL =
"media/m3u8/live_low_latency_media_can_skip_until";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES =
"media/m3u8/live_low_latency_media_can_skip_dateranges";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED =
"media/m3u8/live_low_latency_media_can_skip_skipped";
private static final String
SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING =
"media/m3u8/live_low_latency_media_can_skip_skipped_media_sequence_no_overlapping";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP =
"media/m3u8/live_low_latency_media_can_not_skip";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT =
"media/m3u8/live_low_latency_media_can_not_skip_next";
@Test
public void start_playlistCanNotSkip_requestsFullUpdate() throws IOException, TimeoutException {
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
Queue<DataSource> 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<HlsMediaPlaylist> 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<HlsMediaPlaylist> 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<HlsMediaPlaylist> 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<HlsMediaPlaylist> 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&param2=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<HlsMediaPlaylist> 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<HlsMediaPlaylist> runPlaylistTrackerAndCollectMediaPlaylists(
DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount)
throws TimeoutException {
DefaultHlsPlaylistTracker defaultHlsPlaylistTracker =
new DefaultHlsPlaylistTracker(
dataType -> dataSourceFactory.createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
new DefaultHlsPlaylistParserFactory());
List<HlsMediaPlaylist> 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<String, List<String>> getResponseHeaders() {
return delegate.getResponseHeaders();
}
@Override
public void close() throws IOException {
delegate.close();
if (index < dataSources.length) {
delegate = dataSources[index];
}
index++;
}
}
}

View file

@ -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");

View file

@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2"
media0/playlist.m3u8

View file

@ -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&param2=2

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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