diff --git a/lib/hls/hls_classes.js b/lib/hls/hls_classes.js index d6f5d3d5dd..8d003ba951 100644 --- a/lib/hls/hls_classes.js +++ b/lib/hls/hls_classes.js @@ -11,7 +11,6 @@ goog.provide('shaka.hls.Segment'); goog.provide('shaka.hls.Tag'); goog.require('goog.asserts'); -goog.require('shaka.hls.Utils'); goog.require('shaka.util.Error'); @@ -203,48 +202,20 @@ shaka.hls.Segment = class { /** * Creates an HLS segment object. * - * @param {string} absoluteMediaPlaylistUri An absolute Media Playlist URI. * @param {string} verbatimSegmentUri verbatim segment URI. * @param {!Array.} tags * @param {!Array.=} partialSegments */ - constructor(absoluteMediaPlaylistUri, verbatimSegmentUri, tags, - partialSegments=[]) { + constructor(verbatimSegmentUri, tags, partialSegments=[]) { /** @const {!Array.} */ this.tags = tags; - /** @type {?string} */ - this.absoluteUri_ = null; - - /** @type {?string} */ - this.absoluteMediaPlaylistUri_ = absoluteMediaPlaylistUri; - - /** @type {?string} */ - this.verbatimSegmentUri_ = verbatimSegmentUri; + /** @const {?string} */ + this.verbatimSegmentUri = verbatimSegmentUri; /** @type {!Array.} */ this.partialSegments = partialSegments; } - - /** - * Returns an absolute URI. - * - * @return {string} - */ - get absoluteUri() { - if (!this.absoluteUri_ && - this.absoluteMediaPlaylistUri_ && this.verbatimSegmentUri_) { - goog.asserts.assert(this.absoluteMediaPlaylistUri_, - 'An absolute Media Playlist URI should be defined!'); - goog.asserts.assert(this.verbatimSegmentUri_, - 'An verbatim segment URI should be defined!'); - this.absoluteUri_ = shaka.hls.Utils.constructAbsoluteUri( - this.absoluteMediaPlaylistUri_, this.verbatimSegmentUri_); - this.absoluteMediaPlaylistUri_ = null; - this.verbatimSegmentUri_ = null; - } - return this.absoluteUri_ || ''; - } }; diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 1b61a20a45..745c21bd49 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -235,7 +235,7 @@ shaka.hls.HlsParser = class { this.playerInterface_ = playerInterface; this.lowLatencyMode_ = playerInterface.isLowLatencyMode(); - const response = await this.requestManifest_(uri); + const response = await this.requestManifest_([uri]); // Record the master playlist URI after redirects. this.masterPlaylistUri_ = response.uri; @@ -346,32 +346,35 @@ shaka.hls.HlsParser = class { * @private */ async updateStream_(streamInfo) { - const manifestUri = streamInfo.absoluteMediaPlaylistUri; - const uriObj = new goog.Uri(manifestUri); - const queryData = new goog.Uri.QueryData(); - if (streamInfo.canBlockReload) { - if (streamInfo.nextMediaSequence >= 0) { - // Indicates that the server must hold the request until a Playlist - // contains a Media Segment with Media Sequence - queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence)); + const manifestUris = []; + for (const uri of streamInfo.absoluteMediaPlaylistUris) { + const uriObj = new goog.Uri(uri); + const queryData = uriObj.getQueryData(); + if (streamInfo.canBlockReload) { + if (streamInfo.nextMediaSequence >= 0) { + // Indicates that the server must hold the request until a Playlist + // contains a Media Segment with Media Sequence + queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence)); + } + if (streamInfo.nextPart >= 0) { + // Indicates, in combination with _HLS_msn, that the server must hold + // the request until a Playlist contains Partial Segment N of Media + // Sequence Number M or later. + queryData.add('_HLS_part', String(streamInfo.nextPart)); + } } - if (streamInfo.nextPart >= 0) { - // Indicates, in combination with _HLS_msn, that the server must hold - // the request until a Playlist contains Partial Segment N of Media - // Sequence Number M or later. - queryData.add('_HLS_part', String(streamInfo.nextPart)); + if (streamInfo.canSkipSegments) { + // Enable delta updates. This will replace older segments with + // 'EXT-X-SKIP' tag in the media playlist. + queryData.add('_HLS_skip', 'YES'); } - } - if (streamInfo.canSkipSegments) { - // Enable delta updates. This will replace older segments with - // 'EXT-X-SKIP' tag in the media playlist. - queryData.add('_HLS_skip', 'YES'); - } - if (queryData.getCount()) { - uriObj.setQueryData(queryData); + if (queryData.getCount()) { + uriObj.setQueryData(queryData); + } + manifestUris.push(uriObj.toString()); } const response = - await this.requestManifest_(uriObj.toString(), /* isPlaylist= */ true); + await this.requestManifest_(manifestUris, /* isPlaylist= */ true); if (!streamInfo.stream.segmentIndex) { // The stream was closed since the update was first requested. return; @@ -410,8 +413,7 @@ shaka.hls.HlsParser = class { } const {segments, bandwidth} = this.createSegments_( - streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, - stream.mimeType, mediaSequenceToStartTime, mediaVariables); + playlist, stream, mediaSequenceToStartTime, mediaVariables); stream.bandwidth = bandwidth; @@ -655,8 +657,6 @@ shaka.hls.HlsParser = class { /** @type {!Array.} */ const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE'); - this.parseMasterVariables_(variablesTags); - /** @type {!Array.} */ let variants = []; /** @type {!Array.} */ @@ -715,6 +715,8 @@ shaka.hls.HlsParser = class { decodingInfos: [], }); } else { + this.parseMasterVariables_(variablesTags); + /** @type {!Array.} */ const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA'); /** @type {!Array.} */ @@ -739,8 +741,8 @@ shaka.hls.HlsParser = class { const value = tag.getAttributeValue('VALUE'); const data = (new Map()).set('id', id); if (uri) { - data.set('uri', shaka.hls.Utils.constructAbsoluteUri( - this.masterPlaylistUri_, uri)); + data.set('uri', shaka.hls.Utils.constructUris( + [this.masterPlaylistUri_], uri, this.globalVariables_)[0]); } if (language) { data.set('language', language); @@ -838,7 +840,10 @@ shaka.hls.HlsParser = class { return defaultBasicInfo; } const firstSegment = playlist.segments[0]; - const parsedUri = new goog.Uri(firstSegment.absoluteUri); + const firstSegmentUri = shaka.hls.Utils.constructUris( + [playlist.absoluteUri], + playlist.segments[0].verbatimSegmentUri)[0]; + const parsedUri = new goog.Uri(firstSegmentUri); const extension = parsedUri.getPath().split('.').pop(); const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension]; if (rawMimeType) { @@ -854,9 +859,9 @@ shaka.hls.HlsParser = class { }; } - let segmentUris = [firstSegment.absoluteUri]; + let segmentUris = [firstSegmentUri]; const initSegmentRef = this.getInitSegmentReference_( - playlist, firstSegment.tags, new Map()); + playlist, firstSegment.tags); if (initSegmentRef) { segmentUris = initSegmentRef.getUris(); } @@ -1826,8 +1831,7 @@ shaka.hls.HlsParser = class { codecs = this.groupIdToCodecsMap_.get(groupId); } - const verbatimMediaPlaylistUri = this.variableSubstitution_( - tag.getRequiredAttrValue('URI'), this.globalVariables_); + const verbatimMediaPlaylistUri = tag.getRequiredAttrValue('URI'); // Check if the stream has already been created as part of another Variant // and return it if it has. @@ -1888,8 +1892,7 @@ shaka.hls.HlsParser = class { /** @type {string} */ const type = shaka.util.ManifestParserUtils.ContentType.IMAGE; - const verbatimImagePlaylistUri = this.variableSubstitution_( - tag.getRequiredAttrValue('URI'), this.globalVariables_); + const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI'); const codecs = tag.getAttributeValue('CODECS', 'jpeg') || ''; // Check if the stream has already been created as part of another Variant @@ -1958,8 +1961,7 @@ shaka.hls.HlsParser = class { createStreamInfoFromVariantTag_(tag, allCodecs, type) { goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF', 'Should only be called on variant tags!'); - const verbatimMediaPlaylistUri = this.variableSubstitution_( - tag.getRequiredAttrValue('URI'), this.globalVariables_); + const verbatimMediaPlaylistUri = tag.getRequiredAttrValue('URI'); if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) { return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri); @@ -2003,8 +2005,9 @@ shaka.hls.HlsParser = class { primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio) { // TODO: Refactor, too many parameters - const initialMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri( - this.masterPlaylistUri_, verbatimMediaPlaylistUri); + const initialMediaPlaylistUris = shaka.hls.Utils.constructUris( + [this.masterPlaylistUri_], verbatimMediaPlaylistUri, + this.globalVariables_); // This stream is lazy-loaded inside the createSegmentIndex function. // So we start out with a stream object that does not contain the actual @@ -2017,7 +2020,7 @@ shaka.hls.HlsParser = class { type, verbatimMediaPlaylistUri, // These values are filled out or updated after lazy-loading: - absoluteMediaPlaylistUri: initialMediaPlaylistUri, + absoluteMediaPlaylistUris: initialMediaPlaylistUris, minTimestamp: 0, maxTimestamp: 0, mediaSequenceToStartTime: new Map(), @@ -2034,7 +2037,7 @@ shaka.hls.HlsParser = class { const downloadSegmentIndex = async (abortSignal) => { // Download the actual manifest. const response = await this.requestManifest_( - streamInfo.absoluteMediaPlaylistUri, /* isPlaylist= */ true); + streamInfo.absoluteMediaPlaylistUris, /* isPlaylist= */ true); if (abortSignal.aborted) { return; } @@ -2345,11 +2348,20 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED); } + const stream = this.makeStreamObject_(codecs, type, languageValue, primary, + name, channelsCount, closedCaptions, characteristics, forced, + sampleRate, spatialAudio); + stream.encrypted = encrypted; + stream.drmInfos = drmInfos; + stream.keyIds = keyIds; + stream.mimeType = mimeType; + const mediaSequenceToStartTime = this.isLive_() ? this.mediaSequenceToStartTimeByType_.get(type) : new Map(); + const {segments, bandwidth} = this.createSegments_( - verbatimMediaPlaylistUri, playlist, type, mimeType, - mediaSequenceToStartTime, mediaVariables); + playlist, stream, mediaSequenceToStartTime, mediaVariables); + stream.bandwidth = bandwidth; // This new calculation is necessary for Low Latency streams. if (this.isLive_()) { @@ -2361,6 +2373,7 @@ shaka.hls.HlsParser = class { const lastEndTime = lastSegment.endTime; /** @type {!shaka.media.SegmentIndex} */ const segmentIndex = new shaka.media.SegmentIndex(segments); + stream.segmentIndex = segmentIndex; const serverControlTag = shaka.hls.Utils.getFirstTagWithName( playlist.tags, 'EXT-X-SERVER-CONTROL'); @@ -2375,21 +2388,11 @@ shaka.hls.HlsParser = class { const {nextMediaSequence, nextPart} = this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments); - const stream = this.makeStreamObject_(codecs, type, languageValue, primary, - name, channelsCount, closedCaptions, characteristics, forced, - sampleRate, spatialAudio); - stream.segmentIndex = segmentIndex; - stream.encrypted = encrypted; - stream.drmInfos = drmInfos; - stream.keyIds = keyIds; - stream.mimeType = mimeType; - stream.bandwidth = bandwidth; - return { stream, type, verbatimMediaPlaylistUri, - absoluteMediaPlaylistUri, + absoluteMediaPlaylistUris: [absoluteMediaPlaylistUri], minTimestamp: firstStartTime, maxTimestamp: lastEndTime, canSkipSegments, @@ -2595,10 +2598,11 @@ shaka.hls.HlsParser = class { /** * @param {!shaka.hls.Tag} drmTag * @param {!shaka.hls.Playlist} playlist + * @param {?Map.=} variables * @return {!shaka.extern.HlsAes128Key} * @private */ - parseAES128DrmTag_(drmTag, playlist) { + parseAES128DrmTag_(drmTag, playlist, variables) { // Check if the Web Crypto API is available. if (!window.crypto || !window.crypto.subtle) { shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' + @@ -2633,8 +2637,8 @@ shaka.hls.HlsParser = class { } } - const keyUri = shaka.hls.Utils.constructAbsoluteUri( - playlist.absoluteUri, drmTag.getRequiredAttrValue('URI')); + const keyUri = shaka.hls.Utils.constructUris( + [playlist.absoluteUri], drmTag.getRequiredAttrValue('URI'), variables); const requestType = shaka.net.NetworkingEngine.RequestType.KEY; const request = shaka.net.NetworkingEngine.makeRequest( @@ -2821,7 +2825,7 @@ shaka.hls.HlsParser = class { * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag. * @param {!shaka.hls.Playlist} playlist * @param {!Array.} tags Segment tags - * @param {!Map.} variables + * @param {?Map.=} variables * @return {shaka.media.InitSegmentReference} * @private */ @@ -2834,13 +2838,11 @@ shaka.hls.HlsParser = class { } // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0" const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI'); - const absoluteInitSegmentUri = this.variableSubstitution_( - shaka.hls.Utils.constructAbsoluteUri( - playlist.absoluteUri, verbatimInitSegmentUri), - variables); + const absoluteInitSegmentUris = shaka.hls.Utils.constructUris( + [playlist.absoluteUri], verbatimInitSegmentUri, variables); const mapTagKey = [ - absoluteInitSegmentUri, + absoluteInitSegmentUris.toString(), mapTag.getAttributeValue('BYTERANGE', ''), ].join('-'); if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) { @@ -2851,14 +2853,14 @@ shaka.hls.HlsParser = class { if (tag.name == 'EXT-X-KEY') { if (tag.getRequiredAttrValue('METHOD') == 'AES-128' && tag.id < mapTag.id) { - aes128Key = this.parseAES128DrmTag_(tag, playlist); + aes128Key = this.parseAES128DrmTag_(tag, playlist, variables); } } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) { byteRangeTag = tag; } } const initSegmentRef = this.createInitSegmentReference_( - absoluteInitSegmentUri, mapTag, byteRangeTag, aes128Key); + absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key); this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef); } return this.mapTagToInitSegmentRefMap_.get(mapTagKey); @@ -2867,14 +2869,14 @@ shaka.hls.HlsParser = class { /** * Create an InitSegmentReference object for the EXT-X-MAP tag in the media * playlist. - * @param {string} absoluteInitSegmentUri + * @param {!Array.} absoluteInitSegmentUris * @param {!shaka.hls.Tag} mapTag EXT-X-MAP * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE * @param {shaka.extern.HlsAes128Key=} aes128Key * @return {!shaka.media.InitSegmentReference} * @private */ - createInitSegmentReference_(absoluteInitSegmentUri, mapTag, byteRangeTag, + createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag, aes128Key) { let startByte = 0; let endByte = null; @@ -2902,7 +2904,7 @@ shaka.hls.HlsParser = class { } const initSegmentRef = new shaka.media.InitSegmentReference( - () => [absoluteInitSegmentUri], + () => absoluteInitSegmentUris, startByte, endByte, /* mediaQuality= */ null, @@ -2920,16 +2922,15 @@ shaka.hls.HlsParser = class { * @param {!shaka.hls.Segment} hlsSegment * @param {number} startTime * @param {!Map.} variables - * @param {string} absoluteMediaPlaylistUri - * @param {string} type - * @param {string} mimeType + * @param {!shaka.hls.Playlist} playlist + * @param {shaka.extern.Stream} stream * @param {shaka.extern.HlsAes128Key=} hlsAes128Key * @return {shaka.media.SegmentReference} * @private */ createSegmentReference_( initSegmentReference, previousReference, hlsSegment, startTime, - variables, absoluteMediaPlaylistUri, type, mimeType, hlsAes128Key) { + variables, playlist, stream, hlsAes128Key) { const tags = hlsSegment.tags; const extinfTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF'); @@ -2986,8 +2987,8 @@ shaka.hls.HlsParser = class { let isPreloadSegment = false; if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) { - const byterangeOptimizationSupport = (mimeType == 'video/mp4' || - mimeType == 'audio/mp4') && window.ReadableStream; + const byterangeOptimizationSupport = (stream.mimeType == 'video/mp4' || + stream.mimeType == 'audio/mp4') && window.ReadableStream; let partialSyncTime = syncTime; for (let i = 0; i < hlsSegment.partialSegments.length; i++) { @@ -3043,14 +3044,14 @@ shaka.hls.HlsParser = class { somePartialSegmentWithGap = true; } - let pAbsoluteUri = null; + let uris = null; const getPartialUris = () => { - if (pAbsoluteUri == null) { + if (uris == null) { goog.asserts.assert(pUri, 'Partial uri should be defined!'); - pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri( - absoluteMediaPlaylistUri, pUri); + uris = shaka.hls.Utils.constructUris( + [playlist.absoluteUri], pUri, variables); } - return [pAbsoluteUri]; + return uris; }; if (byterangeOptimizationSupport && @@ -3145,7 +3146,7 @@ shaka.hls.HlsParser = class { let tilesLayout = ''; let tileDuration = null; - if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) { + if (stream.type == shaka.util.ManifestParserUtils.ContentType.IMAGE) { // By default in HLS the tilesLayout is 1x1 tilesLayout = '1x1'; const tilesTag = @@ -3159,16 +3160,16 @@ shaka.hls.HlsParser = class { } } - let absoluteSegmentUri = null; + let uris = null; const getUris = () => { if (getUrisOptimization) { return getUrisOptimization(); } - if (absoluteSegmentUri == null) { - absoluteSegmentUri = this.variableSubstitution_( - hlsSegment.absoluteUri, variables); + if (uris == null) { + uris = shaka.hls.Utils.constructUris([playlist.absoluteUri], + hlsSegment.verbatimSegmentUri, variables); } - return absoluteSegmentUri.length ? [absoluteSegmentUri] : []; + return uris || []; }; const reference = new shaka.media.SegmentReference( @@ -3234,18 +3235,15 @@ shaka.hls.HlsParser = class { * get the bandwidth necessary for this segments If it's defined in the * playlist. * - * @param {string} verbatimMediaPlaylistUri * @param {!shaka.hls.Playlist} playlist - * @param {string} type - * @param {string} mimeType + * @param {shaka.extern.Stream} stream * @param {!Map.} mediaSequenceToStartTime * @param {!Map.} variables * @return {{segments: !Array., * bandwidth: (number|undefined)}} * @private */ - createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType, - mediaSequenceToStartTime, variables) { + createSegments_(playlist, stream, mediaSequenceToStartTime, variables) { /** @type {Array.} */ const hlsSegments = playlist.segments; goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!'); @@ -3297,7 +3295,7 @@ shaka.hls.HlsParser = class { for (const drmTag of item.tags) { if (drmTag.name == 'EXT-X-KEY') { if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') { - hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist); + hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist, variables); } else { hlsAes128Key = undefined; } @@ -3327,9 +3325,8 @@ shaka.hls.HlsParser = class { item, startTime, variables, - playlist.absoluteUri, - type, - mimeType, + playlist, + stream, hlsAes128Key); if (reference) { @@ -3463,42 +3460,6 @@ shaka.hls.HlsParser = class { }; } - /** - * Replaces the variables of a given URI. - * - * @param {string} uri - * @param {!Map.} variables - * @return {string} - * @private - */ - variableSubstitution_(uri, variables) { - if (!variables.size) { - return uri; - } - let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}'); - - const uriVariables = newUri.match(/{\$\w*}/g); - if (uriVariables) { - for (const variable of uriVariables) { - // Note: All variables have the structure {$...} - const variableName = variable.slice(2, variable.length - 1); - const replaceValue = variables.get(variableName); - if (replaceValue) { - newUri = newUri.replace(variable, replaceValue); - } else { - shaka.log.error('A variable has been found that is not declared', - variableName); - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND, - variableName); - } - } - } - return newUri; - } - /** * Attempts to guess stream's mime type based on content type and URI. * @@ -3577,10 +3538,13 @@ shaka.hls.HlsParser = class { goog.asserts.assert(playlist.segments.length, 'Playlist should have segments!'); const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2); - const middleSegmentUri = this.variableSubstitution_( - playlist.segments[middleSegmentIdx].absoluteUri, variables); - const parsedUri = new goog.Uri(middleSegmentUri); + const middleSegmentUris = shaka.hls.Utils.constructUris( + [playlist.absoluteUri], + playlist.segments[middleSegmentIdx].verbatimSegmentUri, + variables); + + const parsedUri = new goog.Uri(middleSegmentUris[0]); const extension = parsedUri.getPath().split('.').pop(); const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType]; @@ -3603,7 +3567,7 @@ shaka.hls.HlsParser = class { // If unable to guess mime type, request a segment and try getting it // from the response. const headRequest = shaka.net.NetworkingEngine.makeRequest( - [middleSegmentUri], this.config_.retryParameters); + middleSegmentUris, this.config_.retryParameters); headRequest.method = 'HEAD'; const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT; const response = await this.makeNetworkRequest_( @@ -3660,16 +3624,16 @@ shaka.hls.HlsParser = class { * Makes a network request for the manifest and returns a Promise * with the resulting data. * - * @param {string} absoluteUri + * @param {!Array.} uris * @param {boolean=} isPlaylist * @return {!Promise.} * @private */ - requestManifest_(absoluteUri, isPlaylist) { + requestManifest_(uris, isPlaylist) { const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; const request = shaka.net.NetworkingEngine.makeRequest( - [absoluteUri], this.config_.retryParameters); + uris, this.config_.retryParameters); const type = isPlaylist ? shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST : @@ -3941,7 +3905,7 @@ shaka.hls.HlsParser = class { * stream: !shaka.extern.Stream, * type: string, * verbatimMediaPlaylistUri: string, - * absoluteMediaPlaylistUri: string, + * absoluteMediaPlaylistUris: !Array., * minTimestamp: number, * maxTimestamp: number, * mediaSequenceToStartTime: !Map., @@ -3966,8 +3930,8 @@ shaka.hls.HlsParser = class { * This has not been canonicalized into an absolute URI. This gives us a * consistent key for this playlist, even if redirects cause us to update * from different origins each time. - * @property {string} absoluteMediaPlaylistUri - * The absolute media playlist URI, resolved relative to the master playlist + * @property {!Array.} absoluteMediaPlaylistUris + * The absolute media playlist URIs, resolved relative to the master playlist * and updated to reflect any redirects. * @property {number} minTimestamp * The minimum timestamp found in the stream. diff --git a/lib/hls/hls_utils.js b/lib/hls/hls_utils.js index 374519baef..b73d6e6918 100644 --- a/lib/hls/hls_utils.js +++ b/lib/hls/hls_utils.js @@ -6,6 +6,8 @@ goog.provide('shaka.hls.Utils'); +goog.require('shaka.log'); +goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.requireType('shaka.hls.Tag'); @@ -70,15 +72,59 @@ shaka.hls.Utils = class { /** - * @param {string} parentAbsoluteUri + * @param {!Array.} baseUris + * @param {?string} relativeUri + * @param {?Map.=} variables + * @return {!Array.} + */ + static constructUris(baseUris, relativeUri, variables) { + if (!relativeUri) { + return []; + } + const resolvedUris = shaka.util.ManifestParserUtils.resolveUris( + baseUris, [relativeUri]); + + const uris = []; + for (const uri of resolvedUris) { + uris.push(shaka.hls.Utils.variableSubstitution(uri, variables)); + } + + return uris; + } + + /** + * Replaces the variables of a given URI. + * * @param {string} uri + * @param {?Map.=} variables * @return {string} */ - static constructAbsoluteUri(parentAbsoluteUri, uri) { - const uris = shaka.util.ManifestParserUtils.resolveUris( - [parentAbsoluteUri], [uri]); + static variableSubstitution(uri, variables) { + if (!variables || !variables.size) { + return uri; + } + let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}'); - return uris[0]; + const uriVariables = newUri.match(/{\$\w*}/g); + if (uriVariables) { + for (const variable of uriVariables) { + // Note: All variables have the structure {$...} + const variableName = variable.slice(2, variable.length - 1); + const replaceValue = variables.get(variableName); + if (replaceValue) { + newUri = newUri.replace(variable, replaceValue); + } else { + shaka.log.error('A variable has been found that is not declared', + variableName); + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND, + variableName); + } + } + } + return newUri; } diff --git a/lib/hls/manifest_text_parser.js b/lib/hls/manifest_text_parser.js index c413c414cc..28085f23af 100644 --- a/lib/hls/manifest_text_parser.js +++ b/lib/hls/manifest_text_parser.js @@ -177,7 +177,7 @@ shaka.hls.ManifestTextParser = class { segmentTags.push(currentMapTag); } // The URI appears after all of the tags describing the segment. - const segment = new shaka.hls.Segment(absoluteMediaPlaylistUri, + const segment = new shaka.hls.Segment( verbatimSegmentUri, segmentTags, partialSegmentTags); segments.push(segment); segmentTags = []; @@ -193,7 +193,7 @@ shaka.hls.ManifestTextParser = class { if (currentMapTag) { segmentTags.push(currentMapTag); } - const segment = new shaka.hls.Segment('', '', segmentTags, + const segment = new shaka.hls.Segment('', segmentTags, partialSegmentTags); segments.push(segment); } diff --git a/test/hls/manifest_text_parser_unit.js b/test/hls/manifest_text_parser_unit.js index 248ea2ad48..ddf10b07a8 100644 --- a/test/hls/manifest_text_parser_unit.js +++ b/test/hls/manifest_text_parser_unit.js @@ -307,7 +307,7 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', + new shaka.hls.Segment('https://test/test.mp4', [ new shaka.hls.Tag(/* id= */ 2, 'EXTINF', [], '5.99467'), ]), @@ -332,7 +332,7 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', [ + new shaka.hls.Segment('https://test/test.mp4', [ new shaka.hls.Tag( /* id= */ 2, 'EXTINF', @@ -360,7 +360,7 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 2, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'https://test/test.mp4', + new shaka.hls.Segment('https://test/test.mp4', [ new shaka.hls.Tag(/* id= */ 1, 'EXT-X-KEY', [ @@ -393,7 +393,7 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 0, 'EXT-X-MEDIA-SEQUENCE', [], '1'), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'test.mp4', + new shaka.hls.Segment('test.mp4', [ new shaka.hls.Tag(/* id= */ 2, 'EXTINF', [], '5.99467'), ]), @@ -427,9 +427,9 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 0, 'EXT-X-TARGETDURATION', [], '6'), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'uri', + new shaka.hls.Segment('uri', [new shaka.hls.Tag(2, 'EXTINF', [], '5')]), - new shaka.hls.Segment('https://test/manifest.m3u8', 'uri2', + new shaka.hls.Segment('uri2', [new shaka.hls.Tag(3, 'EXTINF', [], '4')]), ], }, @@ -449,9 +449,9 @@ describe('ManifestTextParser', () => { new shaka.hls.Tag(/* id= */ 4, 'EXT-X-ENDLIST', []), ], segments: [ - new shaka.hls.Segment('https://test/manifest.m3u8', 'uri', + new shaka.hls.Segment('uri', [new shaka.hls.Tag(2, 'EXTINF', [], '5')]), - new shaka.hls.Segment('https://test/manifest.m3u8', 'uri2', + new shaka.hls.Segment('uri2', [new shaka.hls.Tag(3, 'EXTINF', [], '4')]), ], }, @@ -508,14 +508,12 @@ describe('ManifestTextParser', () => { ], segments: [ new shaka.hls.Segment( - /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri', /* tags= */ [ new shaka.hls.Tag(3, 'EXTINF', [], '5'), mapTag, ]), new shaka.hls.Segment( - /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri2', /* tags= */ [ new shaka.hls.Tag(6, 'EXTINF', [], '2'), @@ -523,7 +521,6 @@ describe('ManifestTextParser', () => { ], /* partialSegments= */ partialSegments1), new shaka.hls.Segment( - /* absoluteMediaPlaylistUri= */ '', /* verbatimSegmentUri= */ '', /* tags= */ [mapTag], /* partialSegments= */ partialSegments2), @@ -577,14 +574,12 @@ describe('ManifestTextParser', () => { ], segments: [ new shaka.hls.Segment( - /* absoluteMediaPlaylistUri= */ 'https://test/manifest.m3u8', /* verbatimSegmentUri= */ 'uri', /* tags= */ [ new shaka.hls.Tag(3, 'EXTINF', [], '5'), mapTag, ]), new shaka.hls.Segment( - /* absoluteMediaPlaylistUri= */ '', /* verbatimSegmentUri= */ '', /* tags= */ [preloadMapTag], /* partialSegments= */ preloadSegment),