mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
Merge pull request #6270 from TiVo:p-iframe-only-playlist
PiperOrigin-RevId: 306677468
This commit is contained in:
commit
88de774587
3 changed files with 106 additions and 10 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue