Commit 949e26d1 by bachinger Committed by Oliver Woodman

Support delta updates for media playlists

Issue: #5011
PiperOrigin-RevId: 339093145
parent 78940445
...@@ -29,6 +29,7 @@ dependencies { ...@@ -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 @@ ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
...@@ -375,6 +380,50 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -375,6 +380,50 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} }
/** /**
* 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,
* {@code hasDiscontinuitySequence} is set to true. * {@code hasDiscontinuitySequence} is set to true.
...@@ -401,6 +450,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
} }
......
...@@ -300,6 +300,27 @@ public class HlsMediaPlaylistParserTest { ...@@ -300,6 +300,27 @@ public class HlsMediaPlaylistParserTest {
} }
@Test @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 { public void multipleExtXKeysForSingleSegment() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = String playlistString =
......
#EXTM3U
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2"
media0/playlist.m3u8
#EXTM3U
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2"
media0/playlist.m3u8?param1=1&param2=2
#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
#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
#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
#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
#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
#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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment