mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
parent
aab6aef443
commit
fe2acb5954
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user