Merge pull request #6270 from TiVo:p-iframe-only-playlist

PiperOrigin-RevId: 306677468
This commit is contained in:
bachinger 2020-04-17 10:44:06 +01:00
commit 88de774587
3 changed files with 106 additions and 10 deletions

View file

@ -69,6 +69,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
private static final String TAG_DEFINE = "#EXT-X-DEFINE";
private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
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";
private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
@ -281,6 +283,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// We expose all tags through the playlist.
tags.add(line);
}
boolean isIFrameOnlyVariant = line.startsWith(TAG_I_FRAME_STREAM_INF);
if (line.startsWith(TAG_DEFINE)) {
variableDefinitions.put(
@ -301,8 +304,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
String scheme = parseEncryptionScheme(method);
sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
}
} else if (line.startsWith(TAG_STREAM_INF)) {
} else if (line.startsWith(TAG_STREAM_INF) || isIFrameOnlyVariant) {
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int roleFlags = isIFrameOnlyVariant ? C.ROLE_FLAG_TRICK_PLAY : 0;
int peakBitrate = parseIntAttr(line, REGEX_BANDWIDTH);
int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);
String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
@ -335,13 +339,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
String closedCaptionsGroupId =
parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
if (!iterator.hasNext()) {
throw new ParserException("#EXT-X-STREAM-INF tag must be followed by another line");
Uri uri;
if (isIFrameOnlyVariant) {
uri =
UriUtil.resolveToUri(baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions));
} else if (!iterator.hasNext()) {
throw new ParserException("#EXT-X-STREAM-INF must be followed by another line");
} else {
// The following line contains #EXT-X-STREAM-INF's URI.
line = replaceVariableReferences(iterator.next(), variableDefinitions);
uri = UriUtil.resolveToUri(baseUri, line);
}
line =
replaceVariableReferences(
iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.
Uri uri = UriUtil.resolveToUri(baseUri, line);
Format format =
new Format.Builder()
.setId(variants.size())
@ -352,6 +361,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
.setWidth(width)
.setHeight(height)
.setFrameRate(frameRate)
.setRoleFlags(roleFlags)
.build();
Variant variant =
new Variant(
@ -558,8 +568,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long targetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
boolean hasEndTag = false;
Segment initializationSegment = null;
@Nullable Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>();
HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
List<Segment> segments = new ArrayList<>();
List<String> tags = new ArrayList<>();
@ -572,6 +583,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
boolean isIFrameOnly = false;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
@ -598,6 +610,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if ("EVENT".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
}
} else if (line.equals(TAG_IFRAME)) {
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_INIT_SEGMENT)) {
@ -715,8 +729,25 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
segmentMediaSequence++;
String segmentUri = replaceVariableReferences(line, variableDefinitions);
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment is not byte range defined.
segmentByteRangeOffset = 0;
} else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
// The segment is a resource byte range without an initialization segment.
// As per RFC 8216, Section 4.3.3.6, we assume the initialization section exists in the
// bytes preceding the first segment in this segment's URL.
// We assume the implicit initialization segment is unencrypted, since there's no way for
// the playlist to provide an initialization vector for it.
inferredInitSegment =
new Segment(
segmentUri,
/* byteRangeOffset= */ 0,
segmentByteRangeOffset,
/* fullSegmentEncryptionKeyUri= */ null,
/* encryptionIV= */ null);
urlToInferredInitSegment.put(segmentUri, inferredInitSegment);
}
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
@ -733,8 +764,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segments.add(
new Segment(
replaceVariableReferences(line, variableDefinitions),
initializationSegment,
segmentUri,
initializationSegment != null ? initializationSegment : inferredInitSegment,
segmentTitle,
segmentDurationUs,
relativeDiscontinuitySequence,

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -207,6 +208,22 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n"
+ "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,URI=\"s1/en/prog_index.m3u8\"\n";
private static final String PLAYLIST_WITH_IFRAME_VARIANTS =
"#EXTM3U\n"
+ "#EXT-X-VERSION:5\n"
+ "#EXT-X-MEDIA:URI=\"AUDIO_English/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES\n"
+ "#EXT-X-MEDIA:URI=\"AUDIO_Spanish/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"es\",NAME=\"Spanish\",AUTOSELECT=YES\n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc1\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,INSTREAM-ID=\"CC1\"\n"
+ "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=400000,RESOLUTION=480x320,CODECS=\"mp4a.40.2,avc1.640015\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+ "400000/index.m3u8\n"
+ "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=848x480,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+ "1000000/index.m3u8\n"
+ "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3220000,RESOLUTION=1280x720,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+ "3220000/index.m3u8\n"
+ "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=8940000,RESOLUTION=1920x1080,CODECS=\"mp4a.40.2,avc1.640028\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+ "8940000/index.m3u8\n"
+ "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n";
@Test
public void parseMasterPlaylist_withSimple_success() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
@ -407,6 +424,19 @@ public class HlsMasterPlaylistParserTest {
.isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English"));
}
@Test
public void testIFrameVariant() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS);
assertThat(playlist.variants).hasSize(5);
for (int i = 0; i < 4; i++) {
assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0);
}
Variant iFramesOnlyVariant = playlist.variants.get(4);
assertThat(iFramesOnlyVariant.format.bitrate).isEqualTo(1313400);
assertThat(iFramesOnlyVariant.format.roleFlags & C.ROLE_FLAG_TRICK_PLAY)
.isEqualTo(C.ROLE_FLAG_TRICK_PLAY);
}
private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) {
return new Metadata(
new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos)));

View file

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
@ -367,6 +368,40 @@ public class HlsMediaPlaylistParserTest {
assertThat(segments.get(3).initializationSegment.url).isEqualTo("init2.ts");
}
@Test
public void noExplicitInitSegmentInIFrameOnly_infersInitSegment() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:5\n"
+ "#EXT-X-I-FRAMES-ONLY\n"
+ "#EXTINF:5.005,\n"
+ "#EXT-X-BYTERANGE:100@300\n"
+ "segment1.ts\n"
+ "#EXTINF:5.005,\n"
+ "#EXT-X-BYTERANGE:100@400\n"
+ "segment2.ts\n"
+ "#EXTINF:5.005,\n"
+ "#EXT-X-BYTERANGE:100@400\n"
+ "segment1.ts\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
List<Segment> segments = playlist.segments;
@Nullable Segment initializationSegment = segments.get(0).initializationSegment;
assertThat(initializationSegment.url).isEqualTo("segment1.ts");
assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
initializationSegment = segments.get(1).initializationSegment;
assertThat(initializationSegment.url).isEqualTo("segment2.ts");
assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
assertThat(initializationSegment.byteRangeLength).isEqualTo(400);
initializationSegment = segments.get(2).initializationSegment;
assertThat(initializationSegment.url).isEqualTo("segment1.ts");
assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
}
@Test
public void encryptedMapTag() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");