From 42eecc84f992ca6a680c3a5fd46d1c300fe92a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Mon, 18 Apr 2022 19:22:30 +0200 Subject: [PATCH] feat(hls): parse EXT-X-GAP (#4134) Parse EXT-X-GAP HLS tag and add a status enum to shaka.media.SegmentReference. shaka.media.SegmentReference.Status.AVAILABLE --> Normal behaviour shaka.media.SegmentReference.Status. UNAVAILABLE --> Related to https://github.com/shaka-project/shaka-player/issues/2541 shaka.media.SegmentReference.Status. MISSING --> EXT-X-GAP in HLS Note: only the parsing is added, but the functionality is not yet implemented. Issue https://github.com/shaka-project/shaka-player/issues/1308 --- lib/hls/hls_parser.js | 6 +++ lib/media/segment_reference.js | 41 +++++++++++++- test/hls/hls_parser_unit.js | 99 ++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 16589c739f..97934283f2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1865,6 +1865,11 @@ shaka.hls.HlsParser = class { } } + let status = shaka.media.SegmentReference.Status.AVAILABLE; + if (shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-GAP')) { + status = shaka.media.SegmentReference.Status.MISSING; + } + // Create SegmentReferences for the partial segments. const partialSegmentRefs = []; if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) { @@ -1978,6 +1983,7 @@ shaka.hls.HlsParser = class { tilesLayout, tileDuration, syncTime, + status, ); } diff --git a/lib/media/segment_reference.js b/lib/media/segment_reference.js index 1dca44a4eb..f6fe9f77c2 100644 --- a/lib/media/segment_reference.js +++ b/lib/media/segment_reference.js @@ -162,12 +162,15 @@ shaka.media.SegmentReference = class { * @param {?number=} syncTime * A time value, expressed in the same scale as the start and end time, which * is used to synchronize between streams. + * @param {shaka.media.SegmentReference.Status=} status + * The segment status is used to indicate that a segment does not exist or is + * not available. */ constructor( startTime, endTime, uris, startByte, endByte, initSegmentReference, timestampOffset, appendWindowStart, appendWindowEnd, partialReferences = [], tilesLayout = '', tileDuration = null, - syncTime = null) { + syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE) { // A preload hinted Partial Segment has the same startTime and endTime. goog.asserts.assert(startTime <= endTime, 'startTime must be less than or equal to endTime'); @@ -220,6 +223,9 @@ shaka.media.SegmentReference = class { /** @type {?number} */ this.syncTime = syncTime; + + /** @type {shaka.media.SegmentReference.Status} */ + this.status = status; } /** @@ -315,6 +321,39 @@ shaka.media.SegmentReference = class { getTileDuration() { return this.tileDuration; } + + /** + * Returns the segment's status. + * + * @return {shaka.media.SegmentReference.Status} + * @export + */ + getStatus() { + return this.status; + } + + /** + * Mark the reference as unavailable. + * + * @export + */ + markAsUnavailable() { + this.status = shaka.media.SegmentReference.Status.UNAVAILABLE; + } +}; + + +/** + * Rather than using booleans to communicate what the state of the reference, + * we have this enum. + * + * @enum {number} + * @export + */ +shaka.media.SegmentReference.Status = { + AVAILABLE: 0, + UNAVAILABLE: 1, + MISSING: 2, }; diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 46b6e3b3b3..1bc5e82595 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -1424,6 +1424,105 @@ describe('HlsParser', () => { } }); + it('parse EXT-X-GAP', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', + 'CHANNELS="2",URI="audio"\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",', + 'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n', + 'video\n', + ].join(''); + + const video = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', + '#EXTINF:5,\n', + 'main.mp4\n', + '#EXTINF:5,\n', + '#EXT-X-GAP\n', + 'main.mp4\n', + '#EXTINF:5,\n', + 'main.mp4\n', + ].join(''); + + const audio = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', + '#EXTINF:5,\n', + 'main.mp4\n', + '#EXTINF:5,\n', + '#EXT-X-GAP\n', + 'main.mp4\n', + '#EXTINF:5,\n', + 'main.mp4\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', master) + .setResponseText('test:/audio', audio) + .setResponseText('test:/video', video) + .setResponseValue('test:/init.mp4', initSegmentData) + .setResponseValue('test:/main.mp4', segmentData); + + const actual = await parser.start('test:/master', playerInterface); + + expect(actual.variants.length).toBe(1); + + const variant = actual.variants[0]; + expect(variant.video).toBeTruthy(); + expect(variant.audio).toBeTruthy(); + + const available = shaka.media.SegmentReference.Status.AVAILABLE; + const missing = shaka.media.SegmentReference.Status.MISSING; + + await variant.video.createSegmentIndex(); + goog.asserts.assert(variant.video.segmentIndex != null, + 'Null segmentIndex!'); + + const firstVideoReference = variant.video.segmentIndex.get(0); + const secondVideoReference = variant.video.segmentIndex.get(1); + const thirdVideoReference = variant.video.segmentIndex.get(2); + + expect(firstVideoReference).not.toBe(null); + expect(secondVideoReference).not.toBe(null); + expect(thirdVideoReference).not.toBe(null); + + if (firstVideoReference) { + expect(firstVideoReference.getStatus()).toBe(available); + } + if (secondVideoReference) { + expect(secondVideoReference.getStatus()).toBe(missing); + } + if (thirdVideoReference) { + expect(thirdVideoReference.getStatus()).toBe(available); + } + + await variant.audio.createSegmentIndex(); + goog.asserts.assert(variant.audio.segmentIndex != null, + 'Null segmentIndex!'); + + const firstAudioReference = variant.audio.segmentIndex.get(0); + const secondAudioReference = variant.audio.segmentIndex.get(1); + const thirdAudioReference = variant.audio.segmentIndex.get(2); + + expect(firstAudioReference).not.toBe(null); + expect(secondAudioReference).not.toBe(null); + expect(thirdAudioReference).not.toBe(null); + + if (firstAudioReference) { + expect(firstAudioReference.getStatus()).toBe(available); + } + if (secondAudioReference) { + expect(secondAudioReference.getStatus()).toBe(missing); + } + if (thirdAudioReference) { + expect(thirdAudioReference.getStatus()).toBe(available); + } + }); + it('Disable audio does not create audio streams', async () => { const master = [ '#EXTM3U\n',