mirror of
https://github.com/samsonjs/media.git
synced 2026-03-28 09:55:48 +00:00
Support delta updates for media playlists
Issue: #5011 PiperOrigin-RevId: 339093145
This commit is contained in:
parent
78940445fe
commit
949e26d1ba
14 changed files with 517 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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¶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<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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
5
testdata/src/test/assets/media/m3u8/live_low_latency_master
vendored
Normal file
5
testdata/src/test/assets/media/m3u8/live_low_latency_master
vendored
Normal 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
|
||||
5
testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param
vendored
Normal file
5
testdata/src/test/assets/media/m3u8/live_low_latency_master_media_uri_with_param
vendored
Normal 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¶m2=2
|
||||
16
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip
vendored
Normal file
16
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip
vendored
Normal 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
|
||||
16
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next
vendored
Normal file
16
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_not_skip_next
vendored
Normal 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
|
||||
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges
vendored
Normal file
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_dateranges
vendored
Normal 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
|
||||
14
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped
vendored
Normal file
14
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_skipped
vendored
Normal 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
|
||||
|
|
@ -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
|
||||
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until
vendored
Normal file
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until
vendored
Normal 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
|
||||
Loading…
Reference in a new issue