From e52a044ec5704fe58059049abe35cfff8dad9fbe Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 21 Oct 2020 11:31:39 +0100 Subject: [PATCH] Parse #EXT-X-SERVER-CONTROL and #EXT-X-PART-INF in HLS media playlists. PiperOrigin-RevId: 338232910 --- .../source/hls/playlist/HlsMediaPlaylist.java | 70 +++++++++++++++- .../hls/playlist/HlsPlaylistParser.java | 79 +++++++++++++++++-- .../playlist/HlsMediaPlaylistParserTest.java | 75 +++++++++++++++++- 3 files changed, 211 insertions(+), 13 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 be771b92fc..022e68bc7d 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 @@ -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 { @@ -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 segments) { + List 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); } } 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 fd6efbf445..7e44bcfa51 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 @@ -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 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 variableDefinitions) { Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string); @@ -970,7 +1035,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 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");