Parse #EXT-X-PRELOAD-HINT tag

Issue: #5011
PiperOrigin-RevId: 339738292
This commit is contained in:
bachinger 2020-10-29 20:44:49 +00:00 committed by Oliver Woodman
parent 8f6b46f570
commit 0d6ec21b30
2 changed files with 202 additions and 0 deletions

View File

@ -93,11 +93,14 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
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 TAG_PRELOAD_HINT = "#EXT-X-PRELOAD-HINT";
private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO";
private static final String TYPE_SUBTITLES = "SUBTITLES";
private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
private static final String TYPE_PART = "PART";
private static final String TYPE_MAP = "MAP";
private static final String METHOD_NONE = "NONE";
private static final String METHOD_AES_128 = "AES-128";
@ -157,6 +160,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ ":(\\d+(?:@\\d+)?)\\b");
private static final Pattern REGEX_ATTR_BYTERANGE =
Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
private static final Pattern REGEX_BYTERANGE_START = Pattern.compile("BYTERANGE-START=(\\d+)\\b");
private static final Pattern REGEX_BYTERANGE_LENGTH =
Pattern.compile("BYTERANGE-LENGTH=(\\d+)\\b");
private static final Pattern REGEX_METHOD =
Pattern.compile(
"METHOD=("
@ -178,6 +184,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
+ "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
private static final Pattern REGEX_PRELOAD_HINT_TYPE =
Pattern.compile("TYPE=(" + TYPE_PART + "|" + TYPE_MAP + ")");
private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
@ -592,6 +600,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long partTargetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
boolean hasEndTag = false;
boolean seenPreloadPart = false;
@Nullable Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>();
HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
@ -760,6 +769,42 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
} else if (line.startsWith(TAG_PRELOAD_HINT) && !seenPreloadPart) {
String type = parseStringAttr(line, REGEX_PRELOAD_HINT_TYPE, variableDefinitions);
if (!TYPE_PART.equals(type)) {
continue;
}
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long byteRangeStart =
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ 0);
long byteRangeLength =
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.TIME_UNSET);
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
parts.add(
new Part(
url,
initializationSegment,
/* durationUs= */ 0,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
byteRangeStart,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false));
seenPreloadPart = true;
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
@ -1027,6 +1072,14 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap()));
}
private static long parseOptionalLongAttr(String line, Pattern pattern, long defaultValue) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return Long.parseLong(checkNotNull(matcher.group(1)));
}
return defaultValue;
}
private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap()));
}

View File

@ -487,6 +487,155 @@ public class HlsMediaPlaylistParserTest {
assertThat(part.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void parseMediaPlaylist_withPreloadHintTypePart_hasPreloadPartWithAllAttributes()
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-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-MAP:URI=\"map.mp4\"\n"
+ "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
+ ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,BYTERANGE-START=1234,BYTERANGE-LENGTH=1000,"
+ "URI=\"filePart267.2.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).parts).isEmpty();
assertThat(playlist.trailingParts).hasSize(2);
HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(1);
assertThat(preloadPart.durationUs).isEqualTo(0L);
assertThat(preloadPart.url).isEqualTo("filePart267.2.ts");
assertThat(preloadPart.byteRangeLength).isEqualTo(1000);
assertThat(preloadPart.byteRangeOffset).isEqualTo(1234);
assertThat(preloadPart.initializationSegment.url).isEqualTo("map.mp4");
assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void parseMediaPlaylist_withMultiplePreloadHintTypeParts_picksOnlyFirstPreloadPart()
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-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.3.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.trailingParts).hasSize(2);
assertThat(playlist.trailingParts.get(1).url).isEqualTo("filePart267.2.ts");
}
@Test
public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData()
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-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-KEY:METHOD=SAMPLE-AES,"
+ "KEYFORMAT=\"com.microsoft.playready\","
+ "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).parts).isEmpty();
assertThat(playlist.trailingParts).hasSize(1);
assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(1);
HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
assertThat(preloadPart.drmInitData.schemeType).isEqualTo("cbcs");
assertThat(preloadPart.drmInitData.schemeDataCount).isEqualTo(1);
assertThat(preloadPart.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
assertThat(preloadPart.drmInitData.get(0).data)
.isEqualTo(
PsshAtomUtil.buildPsshAtom(
C.PLAYREADY_UUID,
Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
}
@Test
public void parseMediaPlaylist_withPreloadHintTypePartAndNewAesPlayReadyKey_correctDrmInitData()
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-MEDIA-SEQUENCE:266\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-KEY:METHOD=SAMPLE-AES,"
+ "KEYFORMAT=\"com.microsoft.playready\","
+ "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).parts).isEmpty();
assertThat(playlist.trailingParts).hasSize(1);
assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(1);
HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
assertThat(preloadPart.drmInitData.schemeType).isEqualTo("cbcs");
assertThat(preloadPart.drmInitData.schemeDataCount).isEqualTo(1);
assertThat(preloadPart.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
assertThat(preloadPart.drmInitData.get(0).data)
.isEqualTo(
PsshAtomUtil.buildPsshAtom(
C.PLAYREADY_UUID,
Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
}
@Test
public void parseMediaPlaylist_withPreloadHintTypePartAndAes128_partHasDrmKeyUriAndIV()
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-MEDIA-SEQUENCE:266\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
+ ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).parts).isEmpty();
assertThat(playlist.trailingParts).hasSize(1);
HlsMediaPlaylist.Part preloadPart = playlist.trailingParts.get(0);
assertThat(preloadPart.drmInitData).isNull();
assertThat(preloadPart.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void multipleExtXKeysForSingleSegment() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");