Parse #EXT-X-SERVER-CONTROL and #EXT-X-PART-INF in HLS media playlists.

PiperOrigin-RevId: 338232910
This commit is contained in:
christosts 2020-10-21 11:31:39 +01:00 committed by Oliver Woodman
parent 1051580a63
commit e52a044ec5
3 changed files with 211 additions and 13 deletions

View file

@ -29,6 +29,54 @@ import java.util.List;
/** Represents an HLS media playlist. */
public final class HlsMediaPlaylist extends HlsPlaylist {
/** Server control attributes. */
public static final class ServerControl {
/**
* The skip boundary for delta updates in microseconds, or {@link C#TIME_UNSET} if delta updates
* are not supported.
*/
public final long skipUntilUs;
/**
* Whether the playlist can produce delta updates that skip older #EXT-X-DATERANGE tags in
* addition to media segments.
*/
public final boolean canSkipDateRanges;
/**
* The server-recommended live offset in microseconds, or {@link C#TIME_UNSET} if none defined.
*/
public final long holdBackUs;
/**
* The server-recommended live offset in microseconds in low-latency mode, or {@link
* C#TIME_UNSET} if none defined.
*/
public final long partHoldBackUs;
/** Whether the server supports blocking playlist reload. */
public final boolean canBlockReload;
/**
* Creates a new instance.
*
* @param skipUntilUs See {@link #skipUntilUs}.
* @param canSkipDateRanges See {@link #canSkipDateRanges}.
* @param holdBackUs See {@link #holdBackUs}.
* @param partHoldBackUs See {@link #partHoldBackUs}.
* @param canBlockReload See {@link #canBlockReload}.
*/
public ServerControl(
long skipUntilUs,
boolean canSkipDateRanges,
long holdBackUs,
long partHoldBackUs,
boolean canBlockReload) {
this.skipUntilUs = skipUntilUs;
this.canSkipDateRanges = canSkipDateRanges;
this.holdBackUs = holdBackUs;
this.partHoldBackUs = partHoldBackUs;
this.canBlockReload = canBlockReload;
}
}
/** Media segment reference. */
@SuppressWarnings("ComparableType")
public static final class Segment implements Comparable<Long> {
@ -208,8 +256,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/
public final long targetDurationUs;
/**
* Whether the playlist contains the #EXT-X-ENDLIST tag.
* The target duration for segment parts, as defined by #EXT-X-PART-INF, or {@link C#TIME_UNSET}
* if undefined.
*/
public final long partTargetDurationUs;
/** Whether the playlist contains the #EXT-X-ENDLIST tag. */
public final boolean hasEndTag;
/**
* Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag.
@ -228,6 +279,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* 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;
/**
* @param playlistType See {@link #playlistType}.
@ -245,6 +298,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param protectionSchemes See {@link #protectionSchemes}.
* @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param segments See {@link #segments}.
* @param serverControl See {@link #serverControl}
*/
public HlsMediaPlaylist(
@PlaylistType int playlistType,
@ -257,11 +311,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
long mediaSequence,
int version,
long targetDurationUs,
long partTargetDurationUs,
boolean hasIndependentSegments,
boolean hasEndTag,
boolean hasProgramDateTime,
@Nullable DrmInitData protectionSchemes,
List<Segment> segments) {
List<Segment> segments,
ServerControl serverControl) {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
this.startTimeUs = startTimeUs;
@ -270,6 +326,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.mediaSequence = mediaSequence;
this.version = version;
this.targetDurationUs = targetDurationUs;
this.partTargetDurationUs = partTargetDurationUs;
this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime;
this.protectionSchemes = protectionSchemes;
@ -282,6 +339,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
: startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
this.serverControl = serverControl;
}
@Override
@ -337,11 +395,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
mediaSequence,
version,
targetDurationUs,
partTargetDurationUs,
hasIndependentSegments,
hasEndTag,
hasProgramDateTime,
protectionSchemes,
segments);
segments,
serverControl);
}
/**
@ -363,11 +423,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
mediaSequence,
version,
targetDurationUs,
partTargetDurationUs,
hasIndependentSegments,
/* hasEndTag= */ true,
hasProgramDateTime,
protectionSchemes,
segments);
segments,
serverControl);
}
}

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
@ -68,7 +70,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_VERSION = "#EXT-X-VERSION";
private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
private static final String TAG_DEFINE = "#EXT-X-DEFINE";
private static final String TAG_SERVER_CONTROL = "#EXT-X-SERVER-CONTROL";
private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
private static final String TAG_PART_INF = "#EXT-X-PART-INF";
private static final String TAG_I_FRAME_STREAM_INF = "#EXT-X-I-FRAME-STREAM-INF";
private static final String TAG_IFRAME = "#EXT-X-I-FRAMES-ONLY";
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
@ -122,9 +126,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ ":(\\d+)\\b");
private static final Pattern REGEX_PART_TARGET_DURATION =
Pattern.compile("PART-TARGET=([\\d\\.]+)\\b");
private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE
+ ":(.+)\\b");
private static final Pattern REGEX_CAN_SKIP_UNTIL =
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_HOLD_BACK = Pattern.compile("[:|,]HOLD-BACK=([\\d\\.]+)\\b");
private static final Pattern REGEX_PART_HOLD_BACK =
Pattern.compile("PART-HOLD-BACK=([\\d\\.]+)\\b");
private static final Pattern REGEX_CAN_BLOCK_RELOAD =
compileBooleanAttrPattern("CAN-BLOCK-RELOAD");
private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ ":(\\d+)\\b");
private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
@ -394,7 +409,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
new HlsTrackMetadataEntry(
/* groupId= */ null,
/* name= */ null,
Assertions.checkNotNull(urlToVariantInfos.get(variant.url)));
checkNotNull(urlToVariantInfos.get(variant.url)));
Metadata metadata = new Metadata(hlsMetadataEntry);
Format format = variant.format.buildUpon().setMetadata(metadata).build();
deduplicatedVariants.add(variant.copyWithFormat(format));
@ -566,6 +581,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long mediaSequence = 0;
int version = 1; // Default version == 1.
long targetDurationUs = C.TIME_UNSET;
long partTargetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
boolean hasEndTag = false;
@Nullable Segment initializationSegment = null;
@ -586,6 +602,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
boolean isIFrameOnly = false;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
HlsMediaPlaylist.ServerControl serverControl =
new HlsMediaPlaylist.ServerControl(
/* skipUntilUs= */ C.TIME_UNSET,
/* canSkipDateRanges= */ false,
/* holdBackUs= */ C.TIME_UNSET,
/* partHoldBackUs= */ C.TIME_UNSET,
/* canBlockReload= */ false);
DrmInitData playlistProtectionSchemes = null;
String fullSegmentEncryptionKeyUri = null;
@ -614,6 +637,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_SERVER_CONTROL)) {
serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) {
double partTargetDurationSeconds = parseDoubleAttr(line, REGEX_PART_TARGET_DURATION);
partTargetDurationUs = (long) (partTargetDurationSeconds * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_INIT_SEGMENT)) {
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
@ -786,6 +814,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasGapTag = false;
}
}
return new HlsMediaPlaylist(
playlistType,
baseUri,
@ -797,11 +826,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
mediaSequence,
version,
targetDurationUs,
partTargetDurationUs,
hasIndependentSegmentsTag,
hasEndTag,
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
playlistProtectionSchemes,
segments);
segments,
serverControl);
}
@C.SelectionFlags
@ -866,6 +897,33 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return null;
}
private static HlsMediaPlaylist.ServerControl parseServerControl(String line) {
double skipUntilSeconds =
parseOptionalDoubleAttr(line, REGEX_CAN_SKIP_UNTIL, /* defaultValue= */ C.TIME_UNSET);
long skipUntilUs =
skipUntilSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (skipUntilSeconds * C.MICROS_PER_SECOND);
boolean canSkipDateRanges =
parseOptionalBooleanAttribute(line, REGEX_CAN_SKIP_DATE_RANGES, /* defaultValue= */ false);
double holdBackSeconds =
parseOptionalDoubleAttr(line, REGEX_HOLD_BACK, /* defaultValue= */ C.TIME_UNSET);
long holdBackUs =
holdBackSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (holdBackSeconds * C.MICROS_PER_SECOND);
double partHoldBackSeconds = parseOptionalDoubleAttr(line, REGEX_PART_HOLD_BACK, C.TIME_UNSET);
long partHoldBackUs =
partHoldBackSeconds == C.TIME_UNSET
? C.TIME_UNSET
: (long) (partHoldBackSeconds * C.MICROS_PER_SECOND);
boolean canBlockReload =
parseOptionalBooleanAttribute(line, REGEX_CAN_BLOCK_RELOAD, /* defaultValue= */ false);
return new HlsMediaPlaylist.ServerControl(
skipUntilUs, canSkipDateRanges, holdBackUs, partHoldBackUs, canBlockReload);
}
private static String parseEncryptionScheme(String method) {
return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
? C.CENC_TYPE_cenc
@ -879,7 +937,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Integer.parseInt(Assertions.checkNotNull(matcher.group(1)));
return Integer.parseInt(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
@ -914,13 +972,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
@PolyNull String defaultValue,
Map<String, String> variableDefinitions) {
Matcher matcher = pattern.matcher(line);
@PolyNull
String value = matcher.find() ? Assertions.checkNotNull(matcher.group(1)) : defaultValue;
@PolyNull String value = matcher.find() ? checkNotNull(matcher.group(1)) : defaultValue;
return variableDefinitions.isEmpty() || value == null
? value
: replaceVariableReferences(value, variableDefinitions);
}
private static double parseOptionalDoubleAttr(String line, Pattern pattern, double defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Double.parseDouble(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
private static String replaceVariableReferences(
String string, Map<String, String> variableDefinitions) {
Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);
@ -970,7 +1035,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return true;
}
if (!extraLines.isEmpty()) {
next = Assertions.checkNotNull(extraLines.poll());
next = checkNotNull(extraLines.poll());
return true;
}
while ((next = reader.readLine()) != null) {
@ -992,7 +1057,5 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
throw new NoSuchElementException();
}
}
}
}

View file

@ -45,7 +45,7 @@ public class HlsMediaPlaylistParserTest {
"#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-START:TIME-OFFSET=-25"
+ "#EXT-X-START:TIME-OFFSET=-25\n"
+ "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
@ -86,6 +86,8 @@ public class HlsMediaPlaylistParserTest {
assertThat(mediaPlaylist.version).isEqualTo(3);
assertThat(mediaPlaylist.hasEndTag).isTrue();
assertThat(mediaPlaylist.protectionSchemes).isNull();
assertThat(mediaPlaylist.targetDurationUs).isEqualTo(8000000);
assertThat(mediaPlaylist.partTargetDurationUs).isEqualTo(C.TIME_UNSET);
List<Segment> segments = mediaPlaylist.segments;
assertThat(segments).isNotNull();
assertThat(segments).hasSize(5);
@ -219,6 +221,7 @@ public class HlsMediaPlaylistParserTest {
+ "https://priv.example.com/2.ts\n"
+ "#EXT-X-ENDLIST\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);
@ -226,6 +229,76 @@ public class HlsMediaPlaylistParserTest {
assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();
}
@Test
public void parseMediaPlaylist_withPartMediaInformation_succeeds() 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:PART-HOLD-BACK=1.234\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z\n"
+ "#EXT-X-MAP:URI=\"init.mp4\"\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence266.mp4";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.partTargetDurationUs).isEqualTo(500000);
}
@Test
public void parseMediaPlaylist_withoutServerControl_serverControlDefaultValues()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:8,\n"
+ "https://priv.example.com/1.ts\n"
+ "#EXT-X-ENDLIST\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.serverControl.canBlockReload).isFalse();
assertThat(playlist.serverControl.partHoldBackUs).isEqualTo(C.TIME_UNSET);
assertThat(playlist.serverControl.holdBackUs).isEqualTo(C.TIME_UNSET);
assertThat(playlist.serverControl.skipUntilUs).isEqualTo(C.TIME_UNSET);
assertThat(playlist.serverControl.canSkipDateRanges).isFalse();
}
@Test
public void parseMediaPlaylist_withServerControl_succeeds() 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-BLOCK-RELOAD=YES,HOLD-BACK=18.5,PART-HOLD-BACK=1.234,"
+ "CAN-SKIP-UNTIL=24.0,CAN-SKIP-DATERANGES=YES\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z\n"
+ "#EXT-X-MAP:URI=\"init.mp4\"\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence266.mp4";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.serverControl.canBlockReload).isTrue();
assertThat(playlist.serverControl.partHoldBackUs).isEqualTo(1234000);
assertThat(playlist.serverControl.holdBackUs).isEqualTo(18500000);
assertThat(playlist.serverControl.skipUntilUs).isEqualTo(24000000);
assertThat(playlist.serverControl.canSkipDateRanges).isTrue();
}
@Test
public void multipleExtXKeysForSingleSegment() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");