From fe2acb59542496ed02e231c655443cb58609e5fe Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 28 Oct 2020 15:52:02 +0000 Subject: [PATCH] Parse HLS #EXT-X-PART tag Issue: #5011 PiperOrigin-RevId: 339467702 --- .../source/hls/playlist/HlsMediaPlaylist.java | 203 +++++++++++++----- .../hls/playlist/HlsPlaylistParser.java | 120 +++++++++-- .../playlist/HlsMediaPlaylistParserTest.java | 167 ++++++++++++++ 3 files changed, 415 insertions(+), 75 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 1acc864fd3..7fc6b11af1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -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 { + 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 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 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 { + /** 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 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 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 segments, int skippedSegmentCount, + List 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); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 587d8c6a26..1f9ad1b703 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -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 variableDefinitions = new HashMap<>(); HashMap urlToInferredInitSegment = new HashMap<>(); List segments = new ArrayList<>(); + List parts = new ArrayList<>(); List tags = new ArrayList<>(); long segmentDurationUs = 0; @@ -602,6 +608,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 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 1) { segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); @@ -730,7 +738,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 1) { segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); @@ -752,16 +760,60 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 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(); if (segmentByteRangeLength != C.LENGTH_UNSET) { segmentByteRangeOffset += segmentByteRangeLength; } @@ -839,9 +890,32 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser