Add initial support for EXT-X-GAP

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=185676652
This commit is contained in:
aquilescanta 2018-02-14 06:14:36 -08:00 committed by Oliver Woodman
parent c8e950537d
commit 1d4bc7dda7
6 changed files with 165 additions and 82 deletions

View File

@ -69,6 +69,7 @@
([#3622](https://github.com/google/ExoPlayer/issues/3622)).
* Use long for media sequence numbers
([#3747](https://github.com/google/ExoPlayer/issues/3747))
* Add initial support for the EXT-X-GAP tag.
* New Cast extension: Simplifies toggling between local and Cast playbacks.
* Audio:
* Support TrueHD passthrough for rechunked samples in Matroska files

View File

@ -33,7 +33,7 @@ import junit.framework.TestCase;
*/
public class HlsMediaPlaylistParserTest extends TestCase {
public void testParseMediaPlaylist() {
public void testParseMediaPlaylist() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
@ -69,76 +69,106 @@ public class HlsMediaPlaylistParserTest extends TestCase {
+ "#EXT-X-ENDLIST";
InputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist).isNotNull();
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD);
assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD);
assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000);
assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679);
assertThat(mediaPlaylist.version).isEqualTo(3);
assertThat(mediaPlaylist.hasEndTag).isTrue();
List<Segment> segments = mediaPlaylist.segments;
assertThat(segments).isNotNull();
assertThat(segments).hasSize(5);
assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679);
assertThat(mediaPlaylist.version).isEqualTo(3);
assertThat(mediaPlaylist.hasEndTag).isTrue();
List<Segment> segments = mediaPlaylist.segments;
assertThat(segments).isNotNull();
assertThat(segments).hasSize(5);
Segment segment = segments.get(0);
assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence)
.isEqualTo(4);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
assertThat(segment.encryptionIV).isNull();
assertThat(segment.byterangeLength).isEqualTo(51370);
assertThat(segment.byterangeOffset).isEqualTo(0);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts");
Segment segment = segments.get(0);
assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence)
.isEqualTo(4);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
assertThat(segment.encryptionIV).isNull();
assertThat(segment.byterangeLength).isEqualTo(51370);
assertThat(segment.byterangeOffset).isEqualTo(0);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts");
segment = segments.get(1);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2680");
assertThat(segment.encryptionIV).isEqualTo("0x1566B");
assertThat(segment.byterangeLength).isEqualTo(51501);
assertThat(segment.byterangeOffset).isEqualTo(2147483648L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts");
segment = segments.get(1);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2680");
assertThat(segment.encryptionIV).isEqualTo("0x1566B");
assertThat(segment.byterangeLength).isEqualTo(51501);
assertThat(segment.byterangeOffset).isEqualTo(2147483648L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts");
segment = segments.get(2);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
assertThat(segment.durationUs).isEqualTo(7941000);
assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
assertThat(segment.encryptionIV).isEqualTo(null);
assertThat(segment.byterangeLength).isEqualTo(51501);
assertThat(segment.byterangeOffset).isEqualTo(2147535149L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts");
segment = segments.get(2);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);
assertThat(segment.durationUs).isEqualTo(7941000);
assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
assertThat(segment.encryptionIV).isEqualTo(null);
assertThat(segment.byterangeLength).isEqualTo(51501);
assertThat(segment.byterangeOffset).isEqualTo(2147535149L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts");
segment = segments.get(3);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2682");
// 0xA7A == 2682.
assertThat(segment.encryptionIV).isNotNull();
assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7A");
assertThat(segment.byterangeLength).isEqualTo(51740);
assertThat(segment.byterangeOffset).isEqualTo(2147586650L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts");
segment = segments.get(3);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2682");
// 0xA7A == 2682.
assertThat(segment.encryptionIV).isNotNull();
assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7A");
assertThat(segment.byterangeLength).isEqualTo(51740);
assertThat(segment.byterangeOffset).isEqualTo(2147586650L);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts");
segment = segments.get(4);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2682");
// 0xA7B == 2683.
assertThat(segment.encryptionIV).isNotNull();
assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7B");
assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET);
assertThat(segment.byterangeOffset).isEqualTo(0);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
} catch (IOException exception) {
fail(exception.getMessage());
}
segment = segments.get(4);
assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);
assertThat(segment.durationUs).isEqualTo(7975000);
assertThat(segment.fullSegmentEncryptionKeyUri)
.isEqualTo("https://priv.example.com/key.php?r=2682");
// 0xA7B == 2683.
assertThat(segment.encryptionIV).isNotNull();
assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7B");
assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET);
assertThat(segment.byterangeOffset).isEqualTo(0);
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
}
public void testGapTag() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test2.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-TARGETDURATION:5\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2016-09-22T02:00:01+00:00\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://example.com/key?value=something\"\n"
+ "#EXTINF:5.005,\n"
+ "02/00/27.ts\n"
+ "#EXTINF:5.005,\n"
+ "02/00/32.ts\n"
+ "#EXT-X-KEY:METHOD=NONE\n"
+ "#EXTINF:5.005,\n"
+ "#EXT-X-GAP \n"
+ "../dummy.ts\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://key-service.bamgrid.com/1.0/key?"
+ "hex-value=9FB8989D15EEAAF8B21B860D7ED3072A\",IV=0x410C8AC18AA42EFA18B5155484F5FC34\n"
+ "#EXTINF:5.005,\n"
+ "02/00/42.ts\n"
+ "#EXTINF:5.005,\n"
+ "02/00/47.ts\n";
InputStream inputStream =
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.hasEndTag).isFalse();
assertThat(playlist.segments.get(1).hasGapTag).isFalse();
assertThat(playlist.segments.get(2).hasGapTag).isTrue();
assertThat(playlist.segments.get(3).hasGapTag).isFalse();
}
}

View File

@ -330,11 +330,27 @@ import java.util.List;
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(extractorFactory, mediaDataSource, dataSpec, initDataSpec,
selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs,
chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous,
mediaPlaylist.drmInitData, encryptionKey, encryptionIv);
out.chunk =
new HlsMediaChunk(
extractorFactory,
mediaDataSource,
dataSpec,
initDataSpec,
selectedUrl,
muxedCaptionFormats,
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
startTimeUs,
startTimeUs + segment.durationUs,
chunkMediaSequence,
discontinuitySequence,
segment.hasGapTag,
isTimestampMaster,
timestampAdjuster,
previous,
mediaPlaylist.drmInitData,
encryptionKey,
encryptionIv);
}
/**

View File

@ -66,6 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger;
private final DataSpec initDataSpec;
private final boolean isEncrypted;
private final boolean isMasterTimestampSource;
private final boolean hasGapTag;
private final TimestampAdjuster timestampAdjuster;
private final boolean shouldSpliceIn;
private final Extractor extractor;
@ -97,6 +98,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param endTimeUs The end time of the chunk in microseconds.
* @param chunkMediaSequence The media sequence number of the chunk.
* @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
* @param hasGapTag Whether the chunk is tagged with EXT-X-GAP.
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
@ -119,6 +121,7 @@ import java.util.concurrent.atomic.AtomicInteger;
long endTimeUs,
long chunkMediaSequence,
int discontinuitySequenceNumber,
boolean hasGapTag,
boolean isMasterTimestampSource,
TimestampAdjuster timestampAdjuster,
HlsMediaChunk previousChunk,
@ -141,6 +144,7 @@ import java.util.concurrent.atomic.AtomicInteger;
this.timestampAdjuster = timestampAdjuster;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
this.hasGapTag = hasGapTag;
Extractor previousExtractor = null;
if (previousChunk != null) {
shouldSpliceIn = previousChunk.hlsUrl != hlsUrl;
@ -211,7 +215,10 @@ import java.util.concurrent.atomic.AtomicInteger;
public void load() throws IOException, InterruptedException {
maybeLoadInitData();
if (!loadCanceled) {
loadMedia();
if (!hasGapTag) {
loadMedia();
}
loadCompleted = true;
}
}
@ -283,7 +290,6 @@ import java.util.concurrent.atomic.AtomicInteger;
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
/**

View File

@ -69,8 +69,16 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/
public final long byterangeLength;
/** Whether the segment is tagged with #EXT-X-GAP. */
public final boolean hasGapTag;
/**
* @param uri See {@link #url}.
* @param byterangeOffset See {@link #byterangeOffset}.
* @param byterangeLength See {@link #byterangeLength}.
*/
public Segment(String uri, long byterangeOffset, long byterangeLength) {
this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength);
this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false);
}
/**
@ -82,10 +90,18 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param encryptionIV See {@link #encryptionIV}.
* @param byterangeOffset See {@link #byterangeOffset}.
* @param byterangeLength See {@link #byterangeLength}.
* @param hasGapTag See {@link #hasGapTag}.
*/
public Segment(String url, long durationUs, int relativeDiscontinuitySequence,
long relativeStartTimeUs, String fullSegmentEncryptionKeyUri,
String encryptionIV, long byterangeOffset, long byterangeLength) {
public Segment(
String url,
long durationUs,
int relativeDiscontinuitySequence,
long relativeStartTimeUs,
String fullSegmentEncryptionKeyUri,
String encryptionIV,
long byterangeOffset,
long byterangeLength,
boolean hasGapTag) {
this.url = url;
this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
@ -94,6 +110,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
this.byterangeLength = byterangeLength;
this.hasGapTag = hasGapTag;
}
@Override

View File

@ -67,6 +67,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
private static final String TAG_KEY = "#EXT-X-KEY";
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
private static final String TAG_GAP = "#EXT-X-GAP";
private static final String TYPE_AUDIO = "AUDIO";
private static final String TYPE_VIDEO = "VIDEO";
@ -357,6 +358,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
String encryptionKeyUri = null;
String encryptionIV = null;
@ -449,6 +451,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
}
} else if (line.equals(TAG_GAP)) {
hasGapTag = true;
} else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
} else if (!line.startsWith("#")) {
String segmentEncryptionIV;
if (encryptionKeyUri == null) {
@ -462,19 +470,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (segmentByteRangeLength == C.LENGTH_UNSET) {
segmentByteRangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
segmentStartTimeUs, encryptionKeyUri, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeLength));
segments.add(
new Segment(
line,
segmentDurationUs,
relativeDiscontinuitySequence,
segmentStartTimeUs,
encryptionKeyUri,
segmentEncryptionIV,
segmentByteRangeOffset,
segmentByteRangeLength,
hasGapTag));
segmentStartTimeUs += segmentDurationUs;
segmentDurationUs = 0;
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
hasGapTag = false;
}
}
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, playlistStartTimeUs,