diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c51846ed7b..2f6b60f65d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index add631c39b..97a5386b04 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -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 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 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(); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 04f61810bc..db0db47aee 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -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); } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 4be758993d..9e993aa27b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -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; } /** 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 77a4c9ed1d..9a9517e2d4 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 @@ -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 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 4deddc1869..acd0746e72 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 @@ -67,6 +67,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser