Parse HLS #EXT-X-PART tag

Issue: #5011
PiperOrigin-RevId: 339467702
This commit is contained in:
bachinger 2020-10-28 15:52:02 +00:00 committed by Oliver Woodman
parent aab6aef443
commit fe2acb5954
3 changed files with 415 additions and 75 deletions

View File

@ -22,11 +22,11 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.common.collect.ImmutableList;
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;
/** Represents an HLS media playlist. */
@ -82,57 +82,16 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
/** Media segment reference. */
@SuppressWarnings("ComparableType")
public static final class Segment implements Comparable<Long> {
public static final class Segment extends SegmentBase {
/**
* The url of the segment.
*/
public final String url;
/**
* The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if
* the media playlist does not define a media section for this segment. The same instance is
* used for all segments that share an EXT-X-MAP tag.
*/
@Nullable public final Segment initializationSegment;
/** The duration of the segment in microseconds, as defined by #EXTINF. */
public final long durationUs;
/** The human readable title of the segment. */
public final String title;
/**
* The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment.
*/
public final int relativeDiscontinuitySequence;
/**
* The start time of the segment in microseconds, relative to the start of the playlist.
*/
public final long relativeStartTimeUs;
/**
* DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM
* protection.
*/
@Nullable public final DrmInitData drmInitData;
/**
* The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use
* full segment encryption with identity key.
*/
@Nullable public final String fullSegmentEncryptionKeyUri;
/**
* The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not
* encrypted.
*/
@Nullable public final String encryptionIV;
/** The segment's byte range offset, as defined by #EXT-X-BYTERANGE. */
public final long byteRangeOffset;
/**
* The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if
* no byte range is specified.
*/
public final long byteRangeLength;
/** Whether the segment is tagged with #EXT-X-GAP. */
public final boolean hasGapTag;
/** The parts belonging to this segment. */
public final List<Part> parts;
/**
* Creates an instance to be used as init segment.
*
* @param uri See {@link #url}.
* @param byteRangeOffset See {@link #byteRangeOffset}.
* @param byteRangeLength See {@link #byteRangeLength}.
@ -157,10 +116,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
encryptionIV,
byteRangeOffset,
byteRangeLength,
/* hasGapTag= */ false);
/* hasGapTag= */ false,
/* parts= */ ImmutableList.of());
}
/**
* Creates an instance.
*
* @param url See {@link #url}.
* @param initializationSegment See {@link #initializationSegment}.
* @param title See {@link #title}.
@ -173,6 +135,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param byteRangeOffset See {@link #byteRangeOffset}.
* @param byteRangeLength See {@link #byteRangeLength}.
* @param hasGapTag See {@link #hasGapTag}.
* @param parts See {@link #parts}.
*/
public Segment(
String url,
@ -186,10 +149,136 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
@Nullable String encryptionIV,
long byteRangeOffset,
long byteRangeLength,
boolean hasGapTag,
List<Part> parts) {
super(
url,
initializationSegment,
durationUs,
relativeDiscontinuitySequence,
relativeStartTimeUs,
drmInitData,
fullSegmentEncryptionKeyUri,
encryptionIV,
byteRangeOffset,
byteRangeLength,
hasGapTag);
this.title = title;
this.parts = ImmutableList.copyOf(parts);
}
}
/** A media part. */
public static final class Part extends SegmentBase {
/** Whether the part is independent. */
public final boolean isIndependent;
/**
* Creates an instance.
*
* @param url See {@link #url}.
* @param initializationSegment See {@link #initializationSegment}.
* @param durationUs See {@link #durationUs}.
* @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
* @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
* @param drmInitData See {@link #drmInitData}.
* @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
* @param encryptionIV See {@link #encryptionIV}.
* @param byteRangeOffset See {@link #byteRangeOffset}.
* @param byteRangeLength See {@link #byteRangeLength}.
* @param hasGapTag See {@link #hasGapTag}.
* @param isIndependent See {@link #isIndependent}.
*/
public Part(
String url,
@Nullable Segment initializationSegment,
long durationUs,
int relativeDiscontinuitySequence,
long relativeStartTimeUs,
@Nullable DrmInitData drmInitData,
@Nullable String fullSegmentEncryptionKeyUri,
@Nullable String encryptionIV,
long byteRangeOffset,
long byteRangeLength,
boolean hasGapTag,
boolean isIndependent) {
super(
url,
initializationSegment,
durationUs,
relativeDiscontinuitySequence,
relativeStartTimeUs,
drmInitData,
fullSegmentEncryptionKeyUri,
encryptionIV,
byteRangeOffset,
byteRangeLength,
hasGapTag);
this.isIndependent = isIndependent;
}
}
/** The base for a {@link Segment} or a {@link Part} required for playback. */
@SuppressWarnings("ComparableType")
public static class SegmentBase implements Comparable<Long> {
/** The url of the segment. */
public final String url;
/**
* The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if
* the media playlist does not define a media initialization section for this segment. The same
* instance is used for all segments that share an EXT-X-MAP tag.
*/
@Nullable public final Segment initializationSegment;
/** The duration of the segment in microseconds, as defined by #EXTINF or #EXT-X-PART. */
public final long durationUs;
/** The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. */
public final int relativeDiscontinuitySequence;
/** The start time of the segment in microseconds, relative to the start of the playlist. */
public final long relativeStartTimeUs;
/**
* DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM
* protection.
*/
@Nullable public final DrmInitData drmInitData;
/**
* The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use
* full segment encryption with identity key.
*/
@Nullable public final String fullSegmentEncryptionKeyUri;
/**
* The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not
* encrypted.
*/
@Nullable public final String encryptionIV;
/**
* The segment's byte range offset, as defined by #EXT-X-BYTERANGE, #EXT-X-PART or
* #EXT-X-PRELOAD-HINT.
*/
public final long byteRangeOffset;
/**
* The segment's byte range length, as defined by #EXT-X-BYTERANGE, #EXT-X-PART or
* #EXT-X-PRELOAD-HINT, or {@link C#LENGTH_UNSET} if no byte range is specified or the byte
* range is open-ended.
*/
public final long byteRangeLength;
/** Whether the segment is marked as a gap. */
public final boolean hasGapTag;
private SegmentBase(
String url,
@Nullable Segment initializationSegment,
long durationUs,
int relativeDiscontinuitySequence,
long relativeStartTimeUs,
@Nullable DrmInitData drmInitData,
@Nullable String fullSegmentEncryptionKeyUri,
@Nullable String encryptionIV,
long byteRangeOffset,
long byteRangeLength,
boolean hasGapTag) {
this.url = url;
this.initializationSegment = initializationSegment;
this.title = title;
this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
@ -206,7 +295,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
return this.relativeStartTimeUs > relativeStartTimeUs
? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
}
}
/**
@ -280,6 +368,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final List<Segment> segments;
/** The number of skipped segments. */
public int skippedSegmentCount;
/**
* The list of parts at the end of the playlist for which the segment is not in the playlist yet.
*/
public final List<Part> trailingParts;
/** The total duration of the playlist in microseconds. */
public final long durationUs;
/** The attributes of the #EXT-X-SERVER-CONTROL header. */
@ -298,9 +390,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param targetDurationUs See {@link #targetDurationUs}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param hasEndTag See {@link #hasEndTag}.
* @param protectionSchemes See {@link #protectionSchemes}.
* @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param protectionSchemes See {@link #protectionSchemes}.
* @param segments See {@link #segments}.
* @param skippedSegmentCount See {@link #skippedSegmentCount}.
* @param trailingParts See {@link #trailingParts}.
* @param serverControl See {@link #serverControl}
*/
public HlsMediaPlaylist(
@ -321,6 +415,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
@Nullable DrmInitData protectionSchemes,
List<Segment> segments,
int skippedSegmentCount,
List<Part> trailingParts,
ServerControl serverControl) {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
@ -334,8 +429,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime;
this.protectionSchemes = protectionSchemes;
this.segments = Collections.unmodifiableList(segments);
this.segments = ImmutableList.copyOf(segments);
this.skippedSegmentCount = skippedSegmentCount;
this.trailingParts = ImmutableList.copyOf(trailingParts);
if (!segments.isEmpty()) {
Segment last = segments.get(segments.size() - 1);
durationUs = last.relativeStartTimeUs + last.durationUs;
@ -420,6 +516,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
protectionSchemes,
mergedSegments,
/* skippedSegmentCount= */ 0,
trailingParts,
serverControl);
}
@ -451,6 +548,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
protectionSchemes,
segments,
skippedSegmentCount,
trailingParts,
serverControl);
}
@ -480,6 +578,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
protectionSchemes,
segments,
skippedSegmentCount,
trailingParts,
serverControl);
}

View File

@ -33,6 +33,7 @@ import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;
import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
@ -73,6 +74,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
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_PART = "#EXT-X-PART";
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";
@ -127,6 +129,7 @@ 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_ATTR_DURATION = Pattern.compile("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");
@ -184,6 +187,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP");
private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE =
@ -333,7 +338,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int width;
int height;
if (resolutionString != null) {
String[] widthAndHeight = resolutionString.split("x");
String[] widthAndHeight = Util.split(resolutionString, "x");
width = Integer.parseInt(widthAndHeight[0]);
height = Integer.parseInt(widthAndHeight[1]);
if (width <= 0 || height <= 0) {
@ -591,6 +596,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
HashMap<String, String> variableDefinitions = new HashMap<>();
HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
List<Segment> segments = new ArrayList<>();
List<Part> parts = new ArrayList<>();
List<String> tags = new ArrayList<>();
long segmentDurationUs = 0;
@ -602,6 +608,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0;
long partByteRangeOffset = 0;
boolean isIFrameOnly = false;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
@ -614,12 +622,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* canBlockReload= */ false);
int skippedSegmentCount = 0;
DrmInitData playlistProtectionSchemes = null;
String fullSegmentEncryptionKeyUri = null;
String fullSegmentEncryptionIV = null;
@Nullable DrmInitData playlistProtectionSchemes = null;
@Nullable String fullSegmentEncryptionKeyUri = null;
@Nullable String fullSegmentEncryptionIV = null;
TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
String encryptionScheme = null;
DrmInitData cachedDrmInitData = null;
@Nullable String encryptionScheme = null;
@Nullable DrmInitData cachedDrmInitData = null;
String line;
while (iterator.hasNext()) {
@ -650,7 +658,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
if (byteRange != null) {
String[] splitByteRange = byteRange.split("@");
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
@ -730,7 +738,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
} else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
String[] splitByteRange = byteRange.split("@");
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
@ -752,16 +760,60 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
} else if (!line.startsWith("#")) {
String segmentEncryptionIV;
if (fullSegmentEncryptionKeyUri == null) {
segmentEncryptionIV = null;
} else if (fullSegmentEncryptionIV != null) {
segmentEncryptionIV = fullSegmentEncryptionIV;
} else {
segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long partDurationUs =
(long) (parseDoubleAttr(line, REGEX_ATTR_DURATION) * C.MICROS_PER_SECOND);
boolean isIndependent =
parseOptionalBooleanAttribute(line, REGEX_INDEPENDENT, /* defaultValue= */ false);
boolean isGap = parseOptionalBooleanAttribute(line, REGEX_GAP, /* defaultValue= */ false);
@Nullable
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
long partByteRangeLength = C.LENGTH_UNSET;
if (byteRange != null) {
String[] splitByteRange = Util.split(byteRange, "@");
partByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
partByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
if (partByteRangeLength == C.LENGTH_UNSET) {
partByteRangeOffset = 0;
}
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,
partDurationUs,
relativeDiscontinuitySequence,
partStartTimeUs,
cachedDrmInitData,
fullSegmentEncryptionKeyUri,
segmentEncryptionIV,
partByteRangeOffset,
partByteRangeLength,
isGap,
isIndependent));
partStartTimeUs += partDurationUs;
if (partByteRangeLength != C.LENGTH_UNSET) {
partByteRangeOffset += partByteRangeLength;
}
} else if (!line.startsWith("#")) {
@Nullable
String segmentEncryptionIV =
getSegmentEncryptionIV(
segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
segmentMediaSequence++;
String segmentUri = replaceVariableReferences(line, variableDefinitions);
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
@ -788,11 +840,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
for (int i = 0; i < schemeDatas.length; i++) {
playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
}
playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
@ -809,10 +857,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentEncryptionIV,
segmentByteRangeOffset,
segmentByteRangeLength,
hasGapTag));
hasGapTag,
parts));
segmentStartTimeUs += segmentDurationUs;
partStartTimeUs = segmentStartTimeUs;
segmentDurationUs = 0;
segmentTitle = "";
parts = new ArrayList<>();
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
@ -839,9 +890,32 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes,
segments,
skippedSegmentCount,
parts,
serverControl);
}
private static DrmInitData getPlaylistProtectionSchemes(
@Nullable String encryptionScheme, SchemeData[] schemeDatas) {
SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
for (int i = 0; i < schemeDatas.length; i++) {
playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
}
return new DrmInitData(encryptionScheme, playlistSchemeDatas);
}
@Nullable
private static String getSegmentEncryptionIV(
long segmentMediaSequence,
@Nullable String fullSegmentEncryptionKeyUri,
@Nullable String fullSegmentEncryptionIV) {
if (fullSegmentEncryptionKeyUri == null) {
return null;
} else if (fullSegmentEncryptionIV != null) {
return fullSegmentEncryptionIV;
}
return Long.toHexString(segmentMediaSequence);
}
@C.SelectionFlags
private static int parseSelectionFlags(String line) {
int flags = 0;

View File

@ -19,10 +19,12 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import android.util.Base64;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream;
@ -320,6 +322,171 @@ public class HlsMediaPlaylistParserTest {
assertThat(playlist.skippedSegmentCount).isEqualTo(1234);
}
@Test
public void parseMediaPlaylist_withParts_parsesPartWithAllAttributes() 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-PART:DURATION=2.00000,GAP=YES,"
+ "INDEPENDENT=YES,URI=\"part267.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,BYTERANGE=\"1000@1234\",URI=\"part267.2.ts\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence267.ts\n"
+ "#EXT-X-PART:DURATION=2.00000, BYTERANGE=\"1000@1234\",URI=\"part268.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part268.2.ts\", BYTERANGE=\"1000\"\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.segments.get(1).parts).hasSize(2);
assertThat(playlist.trailingParts).hasSize(2);
HlsMediaPlaylist.Part firstPart = playlist.segments.get(1).parts.get(0);
assertThat(firstPart.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
assertThat(firstPart.byteRangeOffset).isEqualTo(0);
assertThat(firstPart.durationUs).isEqualTo(2_000_000);
assertThat(firstPart.relativeStartTimeUs).isEqualTo(playlist.segments.get(0).durationUs);
assertThat(firstPart.isIndependent).isTrue();
assertThat(firstPart.hasGapTag).isTrue();
assertThat(firstPart.url).isEqualTo("part267.1.ts");
HlsMediaPlaylist.Part secondPart = playlist.segments.get(1).parts.get(1);
assertThat(secondPart.byteRangeLength).isEqualTo(1000);
assertThat(secondPart.byteRangeOffset).isEqualTo(1234);
// Assert trailing parts.
HlsMediaPlaylist.Part thirdPart = playlist.trailingParts.get(0);
assertThat(thirdPart.byteRangeLength).isEqualTo(1000);
assertThat(thirdPart.byteRangeOffset).isEqualTo(1234);
assertThat(thirdPart.relativeStartTimeUs).isEqualTo(8_000_000);
HlsMediaPlaylist.Part lastPart = playlist.trailingParts.get(1);
assertThat(lastPart.relativeStartTimeUs).isEqualTo(10_000_000);
assertThat(lastPart.hasGapTag).isFalse();
assertThat(lastPart.isIndependent).isFalse();
assertThat(lastPart.byteRangeLength).isEqualTo(1000);
assertThat(lastPart.byteRangeOffset).isEqualTo(2234);
}
@Test
public void parseMediaPlaylist_withPartAndAesPlayReadyKey_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"
+ "#EXT-X-KEY:METHOD=SAMPLE-AES,"
+ "KEYFORMAT=\"com.microsoft.playready\","
+ "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.ts\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.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.protectionSchemes.schemeType).isEqualTo("cbcs");
HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
assertThat(part.drmInitData.schemeType).isEqualTo("cbcs");
assertThat(part.drmInitData.schemeDataCount).isEqualTo(1);
assertThat(part.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
assertThat(part.drmInitData.get(0).data)
.isEqualTo(
PsshAtomUtil.buildPsshAtom(
C.PLAYREADY_UUID,
Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
}
@Test
public void parseMediaPlaylist_withPartAndAesPlayReadyWithOutPrecedingSegment_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"
+ "#EXT-X-KEY:METHOD=SAMPLE-AES,"
+ "KEYFORMAT=\"com.microsoft.playready\","
+ "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments).isEmpty();
assertThat(playlist.protectionSchemes.schemeType).isEqualTo("cbcs");
HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
assertThat(part.drmInitData.schemeType).isEqualTo("cbcs");
assertThat(part.drmInitData.schemeDataCount).isEqualTo(1);
assertThat(part.drmInitData.get(0).uuid).isEqualTo(C.PLAYREADY_UUID);
assertThat(part.drmInitData.get(0).data)
.isEqualTo(
PsshAtomUtil.buildPsshAtom(
C.PLAYREADY_UUID,
Base64.decode("RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==", Base64.DEFAULT)));
}
@Test
public void parseMediaPlaylist_withPartAndAes128_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"
+ "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
+ ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.ts\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).parts).isEmpty();
HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
assertThat(playlist.protectionSchemes).isNull();
assertThat(part.drmInitData).isNull();
assertThat(part.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
assertThat(part.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void parseMediaPlaylist_withPartAndAes128WithoutPrecedingSegment_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"
+ "#EXT-X-KEY:METHOD=AES-128,KEYFORMAT=\"identity\""
+ ", IV=0x410C8AC18AA42EFA18B5155484F5FC34,URI=\"fake://foo.bar/uri\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments).isEmpty();
HlsMediaPlaylist.Part part = playlist.trailingParts.get(0);
assertThat(playlist.protectionSchemes).isNull();
assertThat(part.drmInitData).isNull();
assertThat(part.fullSegmentEncryptionKeyUri).isEqualTo("fake://foo.bar/uri");
assertThat(part.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void multipleExtXKeysForSingleSegment() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");