From fda3189fd2fb8a89946cdb2d8fd0c57bcbed3ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Tue, 24 Oct 2023 19:55:42 +0200 Subject: [PATCH] feat(HLS): Add support for QUERYPARAM variable type in #EXT-X-DEFINE (#5801) Closes https://github.com/shaka-project/shaka-player/issues/5333 --- lib/hls/hls_parser.js | 29 +++++++++++++++++++++---- test/hls/hls_parser_unit.js | 39 +++++++++++++++++++--------------- third_party/closure-uri/uri.js | 12 +++++++++++ 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 7b33051da..3cbd6f1f0 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -408,7 +408,8 @@ shaka.hls.HlsParser = class { const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE'); - const mediaVariables = this.parseMediaVariables_(variablesTags); + const mediaVariables = this.parseMediaVariables_( + variablesTags, response.uri); const stream = streamInfo.stream; @@ -949,31 +950,50 @@ shaka.hls.HlsParser = class { * @private */ parseMasterVariables_(tags) { + const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData(); for (const variableTag of tags) { const name = variableTag.getAttributeValue('NAME'); const value = variableTag.getAttributeValue('VALUE'); + const queryParam = variableTag.getAttributeValue('QUERYPARAM'); if (name && value) { if (!this.globalVariables_.has(name)) { this.globalVariables_.set(name, value); } } + if (queryParam) { + const queryParamValue = queryParams.get(queryParam)[0]; + if (queryParamValue && !this.globalVariables_.has(queryParamValue)) { + this.globalVariables_.set(queryParam, queryParamValue); + } + } } } /** * Get the variables of each variant tag, and store in a map. * @param {!Array.} tags Variant tags from the playlist. + * @param {string} uri Media playlist URI. * @return {!Map.} * @private */ - parseMediaVariables_(tags) { + parseMediaVariables_(tags, uri) { + const queryParams = new goog.Uri(uri).getQueryData(); const mediaVariables = new Map(); for (const variableTag of tags) { const name = variableTag.getAttributeValue('NAME'); const value = variableTag.getAttributeValue('VALUE'); + const queryParam = variableTag.getAttributeValue('QUERYPARAM'); const mediaImport = variableTag.getAttributeValue('IMPORT'); if (name && value) { - mediaVariables.set(name, value); + if (!mediaVariables.has(name)) { + mediaVariables.set(name, value); + } + } + if (queryParam) { + const queryParamValue = queryParams.get(queryParam)[0]; + if (queryParamValue && !mediaVariables.has(queryParamValue)) { + mediaVariables.set(queryParam, queryParamValue); + } } if (mediaImport) { const globalValue = this.globalVariables_.get(mediaImport); @@ -2151,7 +2171,8 @@ shaka.hls.HlsParser = class { const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE'); - const mediaVariables = this.parseMediaVariables_(variablesTags); + const mediaVariables = this.parseMediaVariables_( + variablesTags, absoluteMediaPlaylistUri); goog.asserts.assert(playlist.segments != null, 'Media playlist should have segments!'); diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 293f59785..b4a681306 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -4292,11 +4292,14 @@ describe('HlsParser', () => { it('parse variables master playlist', async () => { const master = [ '#EXTM3U\n', - '#EXT-X-DEFINE:NAME="auth",VALUE="?token=1"\n', + '#EXT-X-DEFINE:NAME="auth",VALUE="token=1"\n', + '#EXT-X-DEFINE:QUERYPARAM="a"\n', + '#EXT-X-DEFINE:QUERYPARAM="b"\n', '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",', 'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n', - 'audio.m3u8{$auth}\n', - '#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video.m3u8{$auth}"', + 'audio.m3u8?{$auth}&a={$a}&b={$b}\n', + '#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",', + 'URI="video.m3u8?{$auth}&a={$a}&b={$b}"', ].join(''); const media = [ @@ -4308,14 +4311,14 @@ describe('HlsParser', () => { ].join(''); fakeNetEngine - .setResponseText('test:/host/master.m3u8', master) - .setResponseText('test:/host/audio.m3u8?token=1', media) - .setResponseText('test:/host/video.m3u8?token=1', media) + .setResponseText('test:/host/master.m3u8?a=1&b=2', master) + .setResponseText('test:/host/audio.m3u8?token=1&a=1&b=2', media) + .setResponseText('test:/host/video.m3u8?token=1&a=1&b=2', media) .setResponseValue('test:/host/init.mp4', initSegmentData) .setResponseValue('test:/host/segment.mp4', segmentData); - const actual = - await parser.start('test:/host/master.m3u8', playerInterface); + const actual = await parser.start( + 'test:/host/master.m3u8?a=1&b=2', playerInterface); await loadAllStreamsFor(actual); const video = actual.variants[0].video; const audio = actual.variants[0].audio; @@ -4341,26 +4344,28 @@ describe('HlsParser', () => { '#EXTM3U\n', '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",', 'RESOLUTION=960x540,FRAME-RATE=60,VIDEO="vid"\n', - 'audio.m3u8\n', - '#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video.m3u8"', + 'audio.m3u8?fooParam=1\n', + '#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="vid",URI="video.m3u8?fooParam=1"', ].join(''); const media = [ '#EXTM3U\n', - '#EXT-X-DEFINE:NAME="auth",VALUE="?token=1"\n', + '#EXT-X-DEFINE:NAME="auth",VALUE="token=1"\n', '#EXT-X-DEFINE:NAME="path",VALUE="test/"\n', + '#EXT-X-DEFINE:QUERYPARAM="fooParam"\n', '#EXT-X-PLAYLIST-TYPE:VOD\n', '#EXT-X-MAP:URI="{$path}init.mp4"\n', '#EXTINF:5,\n', - '{$path}segment.mp4{$auth}', + '{$path}segment.mp4?{$auth}&fooParam={$fooParam}', ].join(''); fakeNetEngine .setResponseText('test:/host/master.m3u8', master) - .setResponseText('test:/host/audio.m3u8', media) - .setResponseText('test:/host/video.m3u8', media) + .setResponseText('test:/host/audio.m3u8?fooParam=1', media) + .setResponseText('test:/host/video.m3u8?fooParam=1', media) .setResponseValue('test:/host/test/init.mp4', initSegmentData) - .setResponseValue('test:/host/test/segment.mp4?token=1', segmentData); + .setResponseValue('test:/host/test/segment.mp4?token=1&fooParam=1', + segmentData); const actual = await parser.start('test:/host/master.m3u8', playerInterface); @@ -4377,11 +4382,11 @@ describe('HlsParser', () => { // flow has gone well. const videoReference = Array.from(video.segmentIndex)[0]; expect(videoReference.getUris()) - .toEqual(['test:/host/test/segment.mp4?token=1']); + .toEqual(['test:/host/test/segment.mp4?token=1&fooParam=1']); const audioReference = Array.from(audio.segmentIndex)[0]; expect(audioReference.getUris()) - .toEqual(['test:/host/test/segment.mp4?token=1']); + .toEqual(['test:/host/test/segment.mp4?token=1&fooParam=1']); }); it('import variables in media from master playlist', async () => { diff --git a/third_party/closure-uri/uri.js b/third_party/closure-uri/uri.js index 1bd34bb14..0e2e877a9 100644 --- a/third_party/closure-uri/uri.js +++ b/third_party/closure-uri/uri.js @@ -830,6 +830,18 @@ goog.Uri.QueryData.prototype.add = function(key, value) { }; +/** + * Get the values from a key. + * + * @param {string} key Name. + * @return {Array.} + */ + goog.Uri.QueryData.prototype.get = function(key) { + this.ensureKeyMapInitialized_(); + return this.keyMap_[key] || []; +}; + + /** * @return {string} Encoded query string. * @override