mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
testImplementation project(modulePrefix + 'robolectricutils')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation project(modulePrefix + 'testdata')
|
testImplementation project(modulePrefix + 'testdata')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.hls.playlist;
|
package com.google.android.exoplayer2.source.hls.playlist;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -163,7 +164,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(PlaylistEventListener listener) {
|
public void addListener(PlaylistEventListener listener) {
|
||||||
Assertions.checkNotNull(listener);
|
checkNotNull(listener);
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,7 +391,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
private HlsMediaPlaylist getLatestPlaylistSnapshot(
|
private HlsMediaPlaylist getLatestPlaylistSnapshot(
|
||||||
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
@Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
||||||
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
|
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
|
||||||
if (loadedPlaylist.hasEndTag) {
|
if (loadedPlaylist.hasEndTag) {
|
||||||
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
|
// 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(
|
private long getLoadedPlaylistStartTimeUs(
|
||||||
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
@Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
||||||
if (loadedPlaylist.hasProgramDateTime) {
|
if (loadedPlaylist.hasProgramDateTime) {
|
||||||
return loadedPlaylist.startTimeUs;
|
return loadedPlaylist.startTimeUs;
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +431,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getLoadedPlaylistDiscontinuitySequence(
|
private int getLoadedPlaylistDiscontinuitySequence(
|
||||||
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
@Nullable HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
|
||||||
if (loadedPlaylist.hasDiscontinuitySequence) {
|
if (loadedPlaylist.hasDiscontinuitySequence) {
|
||||||
return loadedPlaylist.discontinuitySequence;
|
return loadedPlaylist.discontinuitySequence;
|
||||||
}
|
}
|
||||||
|
|
@ -464,7 +465,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
|
|
||||||
private final Uri playlistUrl;
|
private final Uri playlistUrl;
|
||||||
private final Loader mediaPlaylistLoader;
|
private final Loader mediaPlaylistLoader;
|
||||||
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
|
private final DataSource mediaPlaylistDataSource;
|
||||||
|
|
||||||
@Nullable private HlsMediaPlaylist playlistSnapshot;
|
@Nullable private HlsMediaPlaylist playlistSnapshot;
|
||||||
private long lastSnapshotLoadMs;
|
private long lastSnapshotLoadMs;
|
||||||
|
|
@ -477,12 +478,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
public MediaPlaylistBundle(Uri playlistUrl) {
|
public MediaPlaylistBundle(Uri playlistUrl) {
|
||||||
this.playlistUrl = playlistUrl;
|
this.playlistUrl = playlistUrl;
|
||||||
mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist");
|
mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist");
|
||||||
mediaPlaylistLoadable =
|
mediaPlaylistDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST);
|
||||||
new ParsingLoadable<>(
|
|
||||||
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
|
|
||||||
playlistUrl,
|
|
||||||
C.DATA_TYPE_MANIFEST,
|
|
||||||
mediaPlaylistParser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -533,7 +529,7 @@ public final class DefaultHlsPlaylistTracker
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(
|
public void onLoadCompleted(
|
||||||
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
|
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
|
||||||
HlsPlaylist result = loadable.getResult();
|
@Nullable HlsPlaylist result = loadable.getResult();
|
||||||
LoadEventInfo loadEventInfo =
|
LoadEventInfo loadEventInfo =
|
||||||
new LoadEventInfo(
|
new LoadEventInfo(
|
||||||
loadable.loadTaskId,
|
loadable.loadTaskId,
|
||||||
|
|
@ -631,6 +627,12 @@ public final class DefaultHlsPlaylistTracker
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private void loadPlaylistImmediately() {
|
private void loadPlaylistImmediately() {
|
||||||
|
ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable =
|
||||||
|
new ParsingLoadable<>(
|
||||||
|
mediaPlaylistDataSource,
|
||||||
|
getMediaPlaylistUriForRequest(playlistUrl, playlistSnapshot),
|
||||||
|
C.DATA_TYPE_MANIFEST,
|
||||||
|
mediaPlaylistParser);
|
||||||
long elapsedRealtime =
|
long elapsedRealtime =
|
||||||
mediaPlaylistLoader.startLoading(
|
mediaPlaylistLoader.startLoading(
|
||||||
mediaPlaylistLoadable,
|
mediaPlaylistLoadable,
|
||||||
|
|
@ -644,7 +646,11 @@ public final class DefaultHlsPlaylistTracker
|
||||||
|
|
||||||
private void processLoadedPlaylist(
|
private void processLoadedPlaylist(
|
||||||
HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) {
|
HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) {
|
||||||
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
|
@Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot;
|
||||||
|
loadedPlaylist =
|
||||||
|
loadedPlaylist.skippedSegmentCount > 0
|
||||||
|
? loadedPlaylist.expandSkippedSegments(checkNotNull(playlistSnapshot))
|
||||||
|
: loadedPlaylist;
|
||||||
long currentTimeMs = SystemClock.elapsedRealtime();
|
long currentTimeMs = SystemClock.elapsedRealtime();
|
||||||
lastSnapshotLoadMs = currentTimeMs;
|
lastSnapshotLoadMs = currentTimeMs;
|
||||||
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
|
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.
|
* Excludes the playlist.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.hls.playlist;
|
package com.google.android.exoplayer2.source.hls.playlist;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
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.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -275,9 +278,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
* The list of segments in the playlist.
|
* The list of segments in the playlist.
|
||||||
*/
|
*/
|
||||||
public final List<Segment> segments;
|
public final List<Segment> segments;
|
||||||
/**
|
/** The number of skipped segments. */
|
||||||
* The total duration of the playlist in microseconds.
|
public int skippedSegmentCount;
|
||||||
*/
|
/** The total duration of the playlist in microseconds. */
|
||||||
public final long durationUs;
|
public final long durationUs;
|
||||||
/** The attributes of the #EXT-X-SERVER-CONTROL header. */
|
/** The attributes of the #EXT-X-SERVER-CONTROL header. */
|
||||||
public final ServerControl serverControl;
|
public final ServerControl serverControl;
|
||||||
|
|
@ -317,6 +320,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
boolean hasProgramDateTime,
|
boolean hasProgramDateTime,
|
||||||
@Nullable DrmInitData protectionSchemes,
|
@Nullable DrmInitData protectionSchemes,
|
||||||
List<Segment> segments,
|
List<Segment> segments,
|
||||||
|
int skippedSegmentCount,
|
||||||
ServerControl serverControl) {
|
ServerControl serverControl) {
|
||||||
super(baseUri, tags, hasIndependentSegments);
|
super(baseUri, tags, hasIndependentSegments);
|
||||||
this.playlistType = playlistType;
|
this.playlistType = playlistType;
|
||||||
|
|
@ -331,6 +335,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
this.hasProgramDateTime = hasProgramDateTime;
|
this.hasProgramDateTime = hasProgramDateTime;
|
||||||
this.protectionSchemes = protectionSchemes;
|
this.protectionSchemes = protectionSchemes;
|
||||||
this.segments = Collections.unmodifiableList(segments);
|
this.segments = Collections.unmodifiableList(segments);
|
||||||
|
this.skippedSegmentCount = skippedSegmentCount;
|
||||||
if (!segments.isEmpty()) {
|
if (!segments.isEmpty()) {
|
||||||
Segment last = segments.get(segments.size() - 1);
|
Segment last = segments.get(segments.size() - 1);
|
||||||
durationUs = last.relativeStartTimeUs + last.durationUs;
|
durationUs = last.relativeStartTimeUs + last.durationUs;
|
||||||
|
|
@ -353,7 +358,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
* @param other The playlist to compare.
|
* @param other The playlist to compare.
|
||||||
* @return Whether this playlist is newer than {@code other}.
|
* @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) {
|
if (other == null || mediaSequence > other.mediaSequence) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -361,8 +366,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The media sequences are equal.
|
// The media sequences are equal.
|
||||||
int segmentCount = segments.size();
|
int segmentCount = segments.size() + skippedSegmentCount;
|
||||||
int otherSegmentCount = other.segments.size();
|
int otherSegmentCount = other.segments.size() + other.skippedSegmentCount;
|
||||||
return segmentCount > otherSegmentCount
|
return segmentCount > otherSegmentCount
|
||||||
|| (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);
|
|| (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);
|
||||||
}
|
}
|
||||||
|
|
@ -374,6 +379,50 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
return startTimeUs + durationUs;
|
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
|
* 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,
|
* and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
|
||||||
|
|
@ -401,6 +450,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
hasProgramDateTime,
|
hasProgramDateTime,
|
||||||
protectionSchemes,
|
protectionSchemes,
|
||||||
segments,
|
segments,
|
||||||
|
skippedSegmentCount,
|
||||||
serverControl);
|
serverControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +479,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
hasProgramDateTime,
|
hasProgramDateTime,
|
||||||
protectionSchemes,
|
protectionSchemes,
|
||||||
segments,
|
segments,
|
||||||
|
skippedSegmentCount,
|
||||||
serverControl);
|
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_SESSION_KEY = "#EXT-X-SESSION-KEY";
|
||||||
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
|
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
|
||||||
private static final String TAG_GAP = "#EXT-X-GAP";
|
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_AUDIO = "AUDIO";
|
||||||
private static final String TYPE_VIDEO = "VIDEO";
|
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");
|
Pattern.compile("CAN-SKIP-UNTIL=([\\d\\.]+)\\b");
|
||||||
private static final Pattern REGEX_CAN_SKIP_DATE_RANGES =
|
private static final Pattern REGEX_CAN_SKIP_DATE_RANGES =
|
||||||
compileBooleanAttrPattern("CAN-SKIP-DATERANGES");
|
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_HOLD_BACK = Pattern.compile("[:|,]HOLD-BACK=([\\d\\.]+)\\b");
|
||||||
private static final Pattern REGEX_PART_HOLD_BACK =
|
private static final Pattern REGEX_PART_HOLD_BACK =
|
||||||
Pattern.compile("PART-HOLD-BACK=([\\d\\.]+)\\b");
|
Pattern.compile("PART-HOLD-BACK=([\\d\\.]+)\\b");
|
||||||
|
|
@ -609,6 +612,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
/* holdBackUs= */ C.TIME_UNSET,
|
/* holdBackUs= */ C.TIME_UNSET,
|
||||||
/* partHoldBackUs= */ C.TIME_UNSET,
|
/* partHoldBackUs= */ C.TIME_UNSET,
|
||||||
/* canBlockReload= */ false);
|
/* canBlockReload= */ false);
|
||||||
|
int skippedSegmentCount = 0;
|
||||||
|
|
||||||
DrmInitData playlistProtectionSchemes = null;
|
DrmInitData playlistProtectionSchemes = null;
|
||||||
String fullSegmentEncryptionKeyUri = null;
|
String fullSegmentEncryptionKeyUri = null;
|
||||||
|
|
@ -692,6 +696,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
segmentDurationUs =
|
segmentDurationUs =
|
||||||
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
|
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
|
||||||
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
|
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)) {
|
} else if (line.startsWith(TAG_KEY)) {
|
||||||
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
|
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
|
||||||
String keyFormat =
|
String keyFormat =
|
||||||
|
|
@ -832,6 +838,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
|
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
|
||||||
playlistProtectionSchemes,
|
playlistProtectionSchemes,
|
||||||
segments,
|
segments,
|
||||||
|
skippedSegmentCount,
|
||||||
serverControl);
|
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();
|
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
|
@Test
|
||||||
public void multipleExtXKeysForSingleSegment() throws Exception {
|
public void multipleExtXKeysForSingleSegment() throws Exception {
|
||||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
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