From 5af34add689ee5075b94eedf59125eec2f8a45c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Thu, 29 Jun 2023 18:00:35 +0200 Subject: [PATCH] feat(HLS): Parse SAMPLE-RATE attribute (#5375) --- lib/hls/hls_parser.js | 50 ++++++++++++++++++++++++++----------- test/hls/hls_parser_unit.js | 3 ++- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 87d543a302..869bcd0456 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -686,15 +686,13 @@ shaka.hls.HlsParser = class { // Make the stream info, with those values. const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_( playlist, uri, uri, codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, spatialAudio, - mimeType); + channelsCount, closedCaptions, characteristics, forced, sampleRate, + spatialAudio, mimeType); this.uriToStreamInfosMap_.set(uri, streamInfo); if (type == 'video') { this.addVideoAttributes_(streamInfo.stream, width, height, /* frameRate= */ null, /* videoRange= */ null); - } else if (type == 'audio') { - streamInfo.stream.audioSamplingRate = sampleRate; } // Wrap the stream from that stream info with a variant. @@ -1547,6 +1545,21 @@ shaka.hls.HlsParser = class { return count; } + /** + * Get the sample rate information for an HLS audio track. + * + * @param {!shaka.hls.Tag} tag + * @return {?number} + * @private + */ + getSampleRate_(tag) { + const sampleRate = tag.getAttributeValue('SAMPLE-RATE'); + if (!sampleRate) { + return null; + } + return parseInt(sampleRate, 10); + } + /** * Get the spatial audio information for an HLS audio track. * In HLS the channels field indicates the number of audio channels that the @@ -1801,12 +1814,13 @@ shaka.hls.HlsParser = class { const forcedAttrValue = tag.getAttributeValue('FORCED'); const forced = forcedAttrValue == 'YES'; + const sampleRate = type == 'audio' ? this.getSampleRate_(tag) : null; // TODO: Should we take into account some of the currently ignored // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj const streamInfo = this.createStreamInfo_( verbatimMediaPlaylistUri, codecs, type, language, primary, name, channelsCount, /* closedCaptions= */ null, characteristics, forced, - spatialAudio); + sampleRate, spatialAudio); if (this.groupIdToStreamInfosMap_.has(groupId)) { this.groupIdToStreamInfosMap_.get(groupId).push(streamInfo); } else { @@ -1853,7 +1867,8 @@ shaka.hls.HlsParser = class { const streamInfo = this.createStreamInfo_( verbatimImagePlaylistUri, codecs, type, language, /* primary= */ false, name, /* channelsCount= */ null, /* closedCaptions= */ null, - characteristics, /* forced= */ false, /* spatialAudio= */ false); + characteristics, /* forced= */ false, /* sampleRate= */ null, + /* spatialAudio= */ false); // TODO: This check is necessary because of the possibility of multiple // calls to createStreamInfoFromImageTag_ before either has resolved. @@ -1917,7 +1932,7 @@ shaka.hls.HlsParser = class { codecs, type, /* language= */ 'und', /* primary= */ false, /* name= */ null, /* channelcount= */ null, closedCaptions, /* characteristics= */ null, /* forced= */ false, - /* spatialAudio= */ false); + /* sampleRate= */ null, /* spatialAudio= */ false); // TODO: This check is necessary because of the possibility of multiple // calls to createStreamInfoFromVariantTag_ before either has resolved. if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) { @@ -1940,13 +1955,14 @@ shaka.hls.HlsParser = class { * @param {Map.} closedCaptions * @param {?string} characteristics * @param {boolean} forced + * @param {?number} sampleRate * @param {boolean} spatialAudio * @return {!shaka.hls.HlsParser.StreamInfo} * @private */ createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language, primary, name, channelsCount, closedCaptions, characteristics, forced, - spatialAudio) { + sampleRate, spatialAudio) { // TODO: Refactor, too many parameters const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri( this.masterPlaylistUri_, verbatimMediaPlaylistUri); @@ -1955,7 +1971,8 @@ shaka.hls.HlsParser = class { // So we start out with a stream object that does not contain the actual // segment index, then download when createSegmentIndex is called. const stream = this.makeStreamObject_(codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, spatialAudio); + channelsCount, closedCaptions, characteristics, forced, sampleRate, + spatialAudio); const streamInfo = { stream, type, @@ -1996,7 +2013,7 @@ shaka.hls.HlsParser = class { const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_( playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type, language, primary, name, channelsCount, closedCaptions, - characteristics, forced, spatialAudio); + characteristics, forced, sampleRate, spatialAudio); if (abortSignal.aborted) { return; } @@ -2239,6 +2256,7 @@ shaka.hls.HlsParser = class { * @param {Map.} closedCaptions * @param {?string} characteristics * @param {boolean} forced + * @param {?number} sampleRate * @param {boolean} spatialAudio * @param {(string|undefined)} mimeType * @return {!Promise.} @@ -2246,8 +2264,8 @@ shaka.hls.HlsParser = class { */ async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, spatialAudio, - mimeType = undefined) { + channelsCount, closedCaptions, characteristics, forced, sampleRate, + spatialAudio, mimeType = undefined) { if (playlist.type != shaka.hls.PlaylistType.MEDIA) { // EXT-X-MEDIA and EXT-X-IMAGE-STREAM-INF tags should point to media // playlists. @@ -2317,7 +2335,8 @@ shaka.hls.HlsParser = class { this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments); const stream = this.makeStreamObject_(codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, spatialAudio); + channelsCount, closedCaptions, characteristics, forced, sampleRate, + spatialAudio); stream.segmentIndex = segmentIndex; stream.encrypted = encrypted; stream.drmInfos = drmInfos; @@ -2401,12 +2420,13 @@ shaka.hls.HlsParser = class { * @param {Map.} closedCaptions * @param {?string} characteristics * @param {boolean} forced + * @param {?number} sampleRate * @param {boolean} spatialAudio * @return {!shaka.extern.Stream} * @private */ makeStreamObject_(codecs, type, language, primary, name, channelsCount, - closedCaptions, characteristics, forced, spatialAudio) { + closedCaptions, characteristics, forced, sampleRate, spatialAudio) { // Fill out a "best-guess" mimeType, for now. It will be replaced once the // stream is lazy-loaded. const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) || @@ -2439,7 +2459,7 @@ shaka.hls.HlsParser = class { roles: characteristics ? characteristics.split(',') : [], forced, channelsCount, - audioSamplingRate: null, + audioSamplingRate: sampleRate, spatialAudio, closedCaptions, hdr: undefined, diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index a417f8c229..20bb088400 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -150,7 +150,7 @@ describe('HlsParser', () => { const master = [ '#EXTM3U\n', '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', - 'CHANNELS="16/JOC",URI="audio"\n', + 'CHANNELS="16/JOC",SAMPLE-RATE="48000",URI="audio"\n', '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",', 'URI="text"\n', '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",LANGUAGE="es",', @@ -192,6 +192,7 @@ describe('HlsParser', () => { variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; stream.channelsCount = 16; + stream.audioSamplingRate = 48000; stream.spatialAudio = true; stream.mime('audio/mp4', 'mp4a'); });