Parse HLS #EXT-X-RENDITION-REPORT tag

Issue: #5011
PiperOrigin-RevId: 340621758
This commit is contained in:
bachinger 2020-11-04 12:09:29 +00:00 committed by Andrew Lewis
parent ae17e6d6f8
commit 4332dc2304
3 changed files with 274 additions and 15 deletions

View file

@ -17,17 +17,20 @@ package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.net.Uri;
import androidx.annotation.IntDef;
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 com.google.common.collect.ImmutableMap;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** Represents an HLS media playlist. */
public final class HlsMediaPlaylist extends HlsPlaylist {
@ -297,6 +300,36 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
}
/**
* A rendition report for an alternative rendition defined in another media playlist.
*
* <p>See RFC 8216, section 4.4.5.1.4.
*/
public static final class RenditionReport {
/** The URI of the media playlist of the reported rendition. */
public final Uri playlistUri;
/** The last media sequence that is in the playlist of the reported rendition. */
public final long lastMediaSequence;
/**
* The last part index that is in the playlist of the reported rendition, or {@link
* C#INDEX_UNSET} if the rendition does not contain partial segments.
*/
public final int lastPartIndex;
/**
* Creates a new instance.
*
* @param playlistUri See {@link #playlistUri}.
* @param lastMediaSequence See {@link #lastMediaSequence}.
* @param lastPartIndex See {@link #lastPartIndex}.
*/
public RenditionReport(Uri playlistUri, long lastMediaSequence, int lastPartIndex) {
this.playlistUri = playlistUri;
this.lastMediaSequence = lastMediaSequence;
this.lastPartIndex = lastPartIndex;
}
}
/**
* Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link
* #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}.
@ -372,6 +405,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* 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 rendition reports of alternative rendition playlists. */
public final Map<Uri, RenditionReport> renditionReports;
/** The total duration of the playlist in microseconds. */
public final long durationUs;
/** The attributes of the #EXT-X-SERVER-CONTROL header. */
@ -396,6 +431,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param skippedSegmentCount See {@link #skippedSegmentCount}.
* @param trailingParts See {@link #trailingParts}.
* @param serverControl See {@link #serverControl}
* @param renditionReports See {@link #renditionReports}.
*/
public HlsMediaPlaylist(
@PlaylistType int playlistType,
@ -416,7 +452,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
List<Segment> segments,
int skippedSegmentCount,
List<Part> trailingParts,
ServerControl serverControl) {
ServerControl serverControl,
Map<Uri, RenditionReport> renditionReports) {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
this.startTimeUs = startTimeUs;
@ -432,6 +469,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.segments = ImmutableList.copyOf(segments);
this.skippedSegmentCount = skippedSegmentCount;
this.trailingParts = ImmutableList.copyOf(trailingParts);
this.renditionReports = ImmutableMap.copyOf(renditionReports);
if (!segments.isEmpty()) {
Segment last = segments.get(segments.size() - 1);
durationUs = last.relativeStartTimeUs + last.durationUs;
@ -517,7 +555,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
mergedSegments,
/* skippedSegmentCount= */ 0,
trailingParts,
serverControl);
serverControl,
renditionReports);
}
/**
@ -549,7 +588,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
segments,
skippedSegmentCount,
trailingParts,
serverControl);
serverControl,
renditionReports);
}
/**
@ -579,7 +619,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
segments,
skippedSegmentCount,
trailingParts,
serverControl);
serverControl,
renditionReports);
}
}

View file

@ -34,12 +34,14 @@ import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInf
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.RenditionReport;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.Iterables;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@ -94,6 +96,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
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 TAG_RENDITION_REPORT = "#EXT-X-RENDITION-REPORT";
private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO";
@ -155,6 +158,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ ":([\\d\\.]+)\\b");
private static final Pattern REGEX_MEDIA_TITLE =
Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)");
private static final Pattern REGEX_LAST_MSN = Pattern.compile("LAST-MSN" + "=(\\d+)\\b");
private static final Pattern REGEX_LAST_PART = Pattern.compile("LAST-PART" + "=(\\d+)\\b");
private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
+ ":(\\d+(?:@\\d+)?)\\b");
@ -600,12 +605,13 @@ 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<>();
List<Segment> segments = new ArrayList<>();
List<Part> parts = new ArrayList<>();
List<Part> trailingParts = new ArrayList<>();
@Nullable Part preloadPart = null;
Map<Uri, RenditionReport> renditionReports = new HashMap<>();
List<String> tags = new ArrayList<>();
long segmentDurationUs = 0;
@ -769,7 +775,22 @@ 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) {
} else if (line.startsWith(TAG_RENDITION_REPORT)) {
long defaultValue = mediaSequence + segments.size() - (trailingParts.isEmpty() ? 1 : 0);
long lastMediaSequence = parseOptionalLongAttr(line, REGEX_LAST_MSN, defaultValue);
List<Part> lastParts =
trailingParts.isEmpty() ? Iterables.getLast(segments).parts : trailingParts;
int defaultPartIndex =
partTargetDurationUs != C.TIME_UNSET ? lastParts.size() - 1 : C.INDEX_UNSET;
int lastPartIndex = parseOptionalIntAttr(line, REGEX_LAST_PART, defaultPartIndex);
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
Uri playlistUri = Uri.parse(UriUtil.resolve(baseUri, uri));
renditionReports.put(
playlistUri, new RenditionReport(playlistUri, lastMediaSequence, lastPartIndex));
} else if (line.startsWith(TAG_PRELOAD_HINT)) {
if (preloadPart != null) {
continue;
}
String type = parseStringAttr(line, REGEX_PRELOAD_HINT_TYPE, variableDefinitions);
if (!TYPE_PART.equals(type)) {
continue;
@ -790,7 +811,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
parts.add(
preloadPart =
new Part(
url,
initializationSegment,
@ -803,8 +824,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
byteRangeStart,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false));
seenPreloadPart = true;
/* isIndependent= */ false);
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
@ -836,7 +856,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
parts.add(
trailingParts.add(
new Part(
url,
initializationSegment,
@ -903,12 +923,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentByteRangeOffset,
segmentByteRangeLength,
hasGapTag,
parts));
trailingParts));
segmentStartTimeUs += segmentDurationUs;
partStartTimeUs = segmentStartTimeUs;
segmentDurationUs = 0;
segmentTitle = "";
parts = new ArrayList<>();
trailingParts = new ArrayList<>();
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
@ -917,6 +937,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
}
if (preloadPart != null) {
trailingParts.add(preloadPart);
}
return new HlsMediaPlaylist(
playlistType,
baseUri,
@ -935,8 +959,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistProtectionSchemes,
segments,
skippedSegmentCount,
parts,
serverControl);
trailingParts,
serverControl,
renditionReports);
}
private static DrmInitData getPlaylistProtectionSchemes(

View file

@ -636,6 +636,199 @@ public class HlsMediaPlaylistParserTest {
assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
}
@Test
public void parseMediaPlaylist_withRenditionReportWithoutPartTargetDuration_lastPartIndexUnset()
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-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(100);
assertThat(report0.lastPartIndex).isEqualTo(C.INDEX_UNSET);
}
@Test
public void
parseMediaPlaylist_withRenditionReportWithoutPartTargetDurationWithoutLastMsn_sameLastMsnAsCurrentPlaylist()
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-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(266);
assertThat(report0.lastPartIndex).isEqualTo(C.INDEX_UNSET);
}
@Test
public void parseMediaPlaylist_withRenditionReportLowLatency_parseAllAttributes()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-PART-INF:PART-TARGET=1\n"
+ "#EXT-X-VERSION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:266\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100,LAST-PART=2\n"
+ "#EXT-X-RENDITION-REPORT:"
+ "URI=\"http://foo.bar/rendition2.m3u8\",LAST-MSN=1000,LAST-PART=3\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(2);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(100);
assertThat(report0.lastPartIndex).isEqualTo(2);
HlsMediaPlaylist.RenditionReport report2 =
playlist.renditionReports.get(Uri.parse("http://foo.bar/rendition2.m3u8"));
assertThat(report2.lastMediaSequence).isEqualTo(1000);
assertThat(report2.lastPartIndex).isEqualTo(3);
}
@Test
public void
parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastMsn_sameMsnAsCurrentPlaylist()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-PART-INF:PART-TARGET=1\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,URI=\"part267.0.ts\"\n"
+ "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-PART=2\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(267);
assertThat(report0.lastPartIndex).isEqualTo(2);
}
@Test
public void
parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastPartIndex_sameLastPartIndexAsCurrentPlaylist()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-PART-INF:PART-TARGET=1\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,URI=\"part267.0.ts\"\n"
+ "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(100);
assertThat(report0.lastPartIndex).isEqualTo(0);
}
@Test
public void
parseMediaPlaylist_withRenditionReportLowLatencyWithoutLastPartIndex_ignoredPreloadPart()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-PART-INF:PART-TARGET=1\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,URI=\"part267.0.ts\"\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.1.ts\"\n"
+ "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\",LAST-MSN=100\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.trailingParts).hasSize(2);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(100);
assertThat(report0.lastPartIndex).isEqualTo(0);
}
@Test
public void parseMediaPlaylist_withRenditionReportLowLatencyFullSegment_rollingPartIndexUriParam()
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-PART-INF:PART-TARGET=1\n"
+ "#EXT-X-VERSION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:266\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.0.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.2.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.3.ts\"\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.0.ts\"\n"
+ "#EXT-X-RENDITION-REPORT:URI=\"/rendition0.m3u8\"\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.renditionReports).hasSize(1);
HlsMediaPlaylist.RenditionReport report0 =
playlist.renditionReports.get(Uri.parse("https://example.com/rendition0.m3u8"));
assertThat(report0.lastMediaSequence).isEqualTo(266);
assertThat(report0.lastPartIndex).isEqualTo(3);
}
@Test
public void multipleExtXKeysForSingleSegment() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");