Skip to content

Commit

Permalink
feat(HLS): Add support for QUERYPARAM variable type in #EXT-X-DEFINE (#…
Browse files Browse the repository at this point in the history
…5801)

Closes #5333
  • Loading branch information
avelad committed Oct 24, 2023
1 parent 51870e9 commit fda3189
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 21 deletions.
29 changes: 25 additions & 4 deletions lib/hls/hls_parser.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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.<!shaka.hls.Tag>} tags Variant tags from the playlist.
* @param {string} uri Media playlist URI.
* @return {!Map.<string, string>}
* @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);
Expand Down Expand Up @@ -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!');
Expand Down
39 changes: 22 additions & 17 deletions test/hls/hls_parser_unit.js
Expand Up @@ -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 = [
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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 () => {
Expand Down
12 changes: 12 additions & 0 deletions third_party/closure-uri/uri.js
Expand Up @@ -830,6 +830,18 @@ goog.Uri.QueryData.prototype.add = function(key, value) {
};


/**
* Get the values from a key.
*
* @param {string} key Name.
* @return {Array.<string>}
*/
goog.Uri.QueryData.prototype.get = function(key) {
this.ensureKeyMapInitialized_();
return this.keyMap_[key] || [];
};


/**
* @return {string} Encoded query string.
* @override
Expand Down

0 comments on commit fda3189

Please sign in to comment.