From 33a87890b131cfa0c11f40da9ba3163d23758a73 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 9 May 2023 08:14:06 +0100 Subject: [PATCH 01/48] initial buildable patch changes --- demo/common/message_ids.js | 1 + demo/config.js | 2 + demo/locales/source.json | 4 + externs/shaka/player.js | 7 +- lib/dash/dash_parser.js | 412 +++++++++++++++++++++++++++++-- lib/dash/mpd_utils.js | 84 ++++--- lib/dash/segment_template.js | 61 ++++- lib/media/drm_engine.js | 19 ++ lib/player.js | 11 + lib/util/player_configuration.js | 1 + lib/util/xml_utils.js | 34 +++ test/dash/mpd_utils_unit.js | 4 +- 12 files changed, 574 insertions(+), 66 deletions(-) diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index d9adce6065..c03b7c60b7 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -194,6 +194,7 @@ shakaDemo.MessageIds = { DRM_SESSION_TYPE: 'DEMO_DRM_SESSION_TYPE', DURATION_BACKOFF: 'DEMO_DURATION_BACKOFF', ENABLED: 'DEMO_ENABLED', + ENABLE_PATCH_MPD_SUPPORT: 'DEMO_ENABLE_PATCH_MPD_SUPPORT', FAST_HALF_LIFE: 'DEMO_FAST_HALF_LIFE', FORCE_HTTPS: 'DEMO_FORCE_HTTPS', FORCE_TRANSMUX: 'DEMO_FORCE_TRANSMUX', diff --git a/demo/config.js b/demo/config.js index 6fd343ce85..566cd94b43 100644 --- a/demo/config.js +++ b/demo/config.js @@ -236,6 +236,8 @@ shakaDemo.Config = class { .addTextInput_(MessageIds.CLOCK_SYNC_URI, 'manifest.dash.clockSyncUri') .addNumberInput_(MessageIds.DEFAULT_PRESENTATION_DELAY, 'manifest.defaultPresentationDelay') + .addBoolInput_(MessageIds.ENABLE_PATCH_MPD_SUPPORT, + 'manifest.dash.enablePatchMPDSupport') .addBoolInput_(MessageIds.IGNORE_MIN_BUFFER_TIME, 'manifest.dash.ignoreMinBufferTime') .addNumberInput_(MessageIds.INITIAL_SEGMENT_LIMIT, diff --git a/demo/locales/source.json b/demo/locales/source.json index 4ea488cca0..2ddc88926a 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -1074,5 +1074,9 @@ "DEMO_SEGMENT_PREFETCH_LIMIT": { "description": "Max number of segments to be prefetched ahead of current time position.", "message": "Segment Prefetch Limit." + }, + "DEMO_ENABLE_PATCH_MPD_SUPPORT": { + "description": "The name of a configuration value.", + "message": "Enable Patch MPD support" } } diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 39947751df..e04df2adf1 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -829,7 +829,8 @@ shaka.extern.InitDataTransform; * ignoreMaxSegmentDuration: boolean, * keySystemsByURI: !Object., * manifestPreprocessor: function(!Element), - * sequenceMode: boolean + * sequenceMode: boolean, + * enablePatchMPDSupport: boolean * }} * * @property {string} clockSyncUri @@ -887,6 +888,10 @@ shaka.extern.InitDataTransform; * If true, the media segments are appended to the SourceBuffer in * "sequence mode" (ignoring their internal timestamps). * Defaults to false. + * @property {boolean} enablePatchMPDSupport + * Enables DASH Patch manifest support. + * This feature is experimental. + * This value defaults to false. * @exportDoc */ shaka.extern.DashManifestConfiguration; diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index fc2b005472..83e49cd15f 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -56,6 +56,26 @@ shaka.dash.DashParser = class { /** @private {number} */ this.globalId_ = 1; + /** @private {?string} */ + this.patchLocationUrl_ = null; + + /** + * A context of the living manifest used for processing + * Patch MPD's + * @private {!shaka.dash.DashParser.PatchContext} + */ + this.manifestPatchContext_ = { + type: '', + profiles: [], + mediaPresentationDuration: null, + availabilityTimeOffset: 0, + baseUris: [], + }; + + /** @private {!Map} */ + this.contextCache_ = new Map(); + + /** * A map of IDs to Stream objects. * ID: Period@id,AdaptationSet@id,@Representation@id @@ -178,6 +198,14 @@ shaka.dash.DashParser = class { this.manifestUris_ = []; this.manifest_ = null; this.streamMap_ = {}; + this.contextCache_.clear(); + this.manifestPatchContext_ = { + type: '', + profiles: [], + mediaPresentationDuration: null, + availabilityTimeOffset: 0, + baseUris: [], + }; this.periodCombiner_ = null; if (this.updateTimer_ != null) { @@ -222,9 +250,13 @@ shaka.dash.DashParser = class { async requestManifest_() { const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; const type = shaka.net.NetworkingEngine.AdvancedRequestType.MPD; + const manifestUris = this.patchLocationUrl_ ? + [this.patchLocationUrl_] : this.manifestUris_; + const request = shaka.net.NetworkingEngine.makeRequest( - this.manifestUris_, this.config_.retryParameters); + manifestUris, this.config_.retryParameters); const networkingEngine = this.playerInterface_.networkingEngine; + const startTime = Date.now(); const operation = networkingEngine.request(requestType, request, {type}); this.operationManager_.manage(operation); @@ -267,12 +299,19 @@ shaka.dash.DashParser = class { const Error = shaka.util.Error; const MpdUtils = shaka.dash.MpdUtils; - const mpd = shaka.util.XmlUtils.parseXml(data, 'MPD'); + const rootElement = this.patchLocationUrl_ ? 'Patch' : 'MPD'; + + const mpd = shaka.util.XmlUtils.parseXml(data, rootElement); if (!mpd) { throw new Error( Error.Severity.CRITICAL, Error.Category.MANIFEST, Error.Code.DASH_INVALID_XML, finalManifestUri); } + + if (this.patchLocationUrl_) { + return this.processPatchManifest_(mpd); + } + const disableXlinkProcessing = this.config_.dash.disableXlinkProcessing; if (disableXlinkProcessing) { return this.processManifest_(mpd, finalManifestUri); @@ -322,16 +361,30 @@ shaka.dash.DashParser = class { manifestBaseUris = absoluteLocations; } + if (this.config_.dash.enablePatchMPDSupport) { + // Get patch location element + const patchLocation = XmlUtils.findChildren(mpd, 'PatchLocation') + .map(XmlUtils.getContents) + .filter(Functional.isNotNull); + if (patchLocation.length > 0) { + // we are patching + this.patchLocationUrl_ = patchLocation[0]; + } + } + const uriObjs = XmlUtils.findChildren(mpd, 'BaseURL'); const uris = uriObjs.map(XmlUtils.getContents); const baseUris = shaka.util.ManifestParserUtils.resolveUris( manifestBaseUris, uris); + this.manifestPatchContext_.baseUris = baseUris; + let availabilityTimeOffset = 0; if (uriObjs && uriObjs.length) { availabilityTimeOffset = XmlUtils.parseAttr( uriObjs[0], 'availabilityTimeOffset', XmlUtils.parseFloat) || 0; } + this.manifestPatchContext_.availabilityTimeOffset = availabilityTimeOffset; const ignoreMinBufferTime = this.config_.dash.ignoreMinBufferTime; let minBufferTime = 0; @@ -365,6 +418,8 @@ shaka.dash.DashParser = class { } const mpdType = mpd.getAttribute('type') || 'static'; + this.manifestPatchContext_.type = mpdType; + /** @type {!shaka.media.PresentationTimeline} */ let presentationTimeline; if (this.manifest_) { @@ -424,6 +479,7 @@ shaka.dash.DashParser = class { segmentAvailabilityDuration); const profiles = mpd.getAttribute('profiles') || ''; + this.manifestPatchContext_.profiles = profiles.split(','); /** @type {shaka.dash.DashParser.Context} */ const context = { @@ -438,6 +494,8 @@ shaka.dash.DashParser = class { bandwidth: 0, indexRangeWarningGiven: false, availabilityTimeOffset: availabilityTimeOffset, + segmentInfo: null, + mediaPresentationDuration: null, profiles: profiles.split(','), }; @@ -483,10 +541,10 @@ shaka.dash.DashParser = class { presentationTimeline.assertIsValid(); } - await this.periodCombiner_.combinePeriods(periods, context.dynamic); - // These steps are not done on manifest update. if (!this.manifest_) { + await this.periodCombiner_.combinePeriods(periods, context.dynamic); + this.manifest_ = { presentationTimeline: presentationTimeline, variants: this.periodCombiner_.getVariants(), @@ -517,22 +575,232 @@ shaka.dash.DashParser = class { // maintain consistency from here on. presentationTimeline.lockStartTime(); } else { - // Just update the variants and text streams, which may change as periods - // are added or removed. - this.manifest_.variants = this.periodCombiner_.getVariants(); - this.manifest_.textStreams = this.periodCombiner_.getTextStreams(); - this.manifest_.imageStreams = this.periodCombiner_.getImageStreams(); - - // Re-filter the manifest. This will check any configured restrictions on - // new variants, and will pass any new init data to DrmEngine to ensure - // that key rotation works correctly. - this.playerInterface_.filter(this.manifest_); - } - - // Add text streams to correspond to closed captions. This happens right - // after period combining, while we still have a direct reference, so that - // any new streams will appear in the period combiner. - this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_); + await this.postPeriodProcessing_(periodsAndDuration.periods); + } + } + + /** + * Handles common procedures after processing new periods. + * + * @param {!Array} periods to be appended + * @private + */ + async postPeriodProcessing_(periods) { + await this.periodCombiner_.combinePeriods(periods, true); + + // Just update the variants and text streams, which may change as periods + // are added or removed. + this.manifest_.variants = this.periodCombiner_.getVariants(); + this.manifest_.textStreams = this.periodCombiner_.getTextStreams(); + this.manifest_.imageStreams = this.periodCombiner_.getImageStreams(); + + // Re-filter the manifest. This will check any configured restrictions on + // new variants, and will pass any new init data to DrmEngine to ensure + // that key rotation works correctly. + this.playerInterface_.filter(this.manifest_); + } + + /** + * Takes a formatted Patch MPD and converts it into a manifest. + * + * @param {!Element} mpd + * @return {!Promise} + * @private + */ + async processPatchManifest_(mpd) { + const XmlUtils = shaka.util.XmlUtils; + + const additions = XmlUtils.findChildren(mpd, 'p:add'); + + /** @type {!Array.} */ + const newPeriods = []; + + for (const addition of additions) { + const selector = addition.getAttribute('sel'); + + const paths = XmlUtils.parseXpath(selector); + + switch (paths[paths.length - 1].name) { + case 'MPD': + if (selector == '/MPD/@mediaPresentationDuration') { + const content = XmlUtils.getContents(addition) || ''; + this.parsePatchMediaPresentationDurationChange_(content); + } else { + newPeriods.push(...this.parsePatchPeriod_(addition)); + } + break; + case 'SegmentTimeline': + this.parsePatchSegment_(paths, + XmlUtils.findChildren(addition, 'S')); + + break; + // TODO handle SegmentList + // TODO handle SegmentBase + } + } + + const replaces = XmlUtils.findChildren(mpd, 'p:replace'); + for (const replace of replaces) { + const selector = replace.getAttribute('sel'); + const content = XmlUtils.getContents(replace) || ''; + + if (selector == '/MPD/@type') { + this.parsePatchMpdTypeChange_(content); + } else if (selector == '/MPD/@mediaPresentationDuration') { + this.parsePatchMediaPresentationDurationChange_(content); + } + } + + if (newPeriods.length) { + await this.postPeriodProcessing_(newPeriods); + } + if (this.manifestPatchContext_.type == 'static') { + const duration = this.manifestPatchContext_.mediaPresentationDuration; + this.manifest_.presentationTimeline.setDuration(duration || Infinity); + } + } + + /** + * Handles manifest type changes, this transition is expected to be + * "dyanmic" to "static". + * + * @param {!string} mpdType + * @private + */ + parsePatchMpdTypeChange_(mpdType) { + this.manifest_.presentationTimeline.setStatic(mpdType == 'static'); + this.manifestPatchContext_.type = mpdType; + for (const context of this.contextCache_.values()) { + context.dynamic = mpdType == 'dynamic'; + } + } + + /** + * @param {string} durationString + * @private + */ + parsePatchMediaPresentationDurationChange_(durationString) { + const duration = shaka.util.XmlUtils.parseDuration(durationString); + if (duration == null) { + return; + } + this.manifestPatchContext_.mediaPresentationDuration = duration; + for (const context of this.contextCache_.values()) { + context.mediaPresentationDuration = duration; + } + } + + /** + * Ingests a full MPD period element from a patch update + * + * @param {!Element} periods + * @private + */ + parsePatchPeriod_(periods) { + this.contextCache_.clear(); + + /** @type {shaka.dash.DashParser.Context} */ + const context = { + dynamic: this.manifestPatchContext_.type == 'dynamic', + presentationTimeline: this.manifest_.presentationTimeline, + period: null, + periodInfo: null, + adaptationSet: null, + representation: null, + bandwidth: 0, + indexRangeWarningGiven: false, + availabilityTimeOffset: this.manifestPatchContext_.availabilityTimeOffset, + profiles: this.manifestPatchContext_.profiles, + segmentInfo: null, + mediaPresentationDuration: + this.manifestPatchContext_.mediaPresentationDuration, + timelineCache: new Map(), + }; + + const periodsAndDuration = this.parsePeriods_(context, + this.manifestPatchContext_.baseUris, periods); + + return periodsAndDuration.periods; + } + + /** + * Ingests Path MPD segments. + * + * @param {!Array} paths + * @param {Array} segments + * @private + */ + parsePatchSegment_(paths, segments) { + let periodId = ''; + let adaptationSetId = ''; + let representationId = ''; + for (const node of paths) { + if (node.name == 'Period') { + periodId = node.id; + } else if (node.name == 'AdaptationSet') { + adaptationSetId = node.id; + } else if (node.name == 'Representation') { + representationId = node.id; + } + } + + /** @type {!Array} */ + const representationIds = []; + + if (representationId != '') { + representationIds.push(representationId); + } else { + for (const context of this.contextCache_.values()) { + if (context.adaptationSet.id == adaptationSetId && + context.representation.id) { + representationIds.push(context.representation.id); + } + } + } + + for (const repId of representationIds) { + const contextId = periodId + ',' + repId; + + /** @type {shaka.dash.DashParser.Context} */ + const context = this.contextCache_.get(contextId); + + context.segmentInfo.timepoints = segments; + + const currentStream = this.streamMap_[contextId]; + goog.asserts.assert(currentStream, 'stream should exist'); + + if (currentStream.segmentIndex) { + currentStream.segmentIndex.evict( + this.manifest_.presentationTimeline.getSegmentAvailabilityStart()); + } + + try { + const requestSegment = (uris, startByte, endByte, isInit) => { + return this.requestSegment_(uris, startByte, endByte, isInit); + }; + const streamInfo = shaka.dash.SegmentTemplate.createStreamInfo( + context, requestSegment, this.streamMap_, /* isUpdate= */ true, + this.config_.dash.initialSegmentLimit, this.periodDurations_, + /* isPatchUpdate= */ true); + currentStream.createSegmentIndex = async () => { + if (!currentStream.segmentIndex) { + currentStream.segmentIndex = + await streamInfo.generateSegmentIndex(); + } + }; + } catch (error) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const contentType = context.representation.contentType; + const isText = contentType == ContentType.TEXT || + contentType == ContentType.APPLICATION; + const isImage = contentType == ContentType.IMAGE; + if (!(isText || isImage) || + error.code != shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) { + // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image + throw error; + } + } + } } /** @@ -552,8 +820,14 @@ shaka.dash.DashParser = class { */ parsePeriods_(context, baseUris, mpd) { const XmlUtils = shaka.util.XmlUtils; - const presentationDuration = XmlUtils.parseAttr( - mpd, 'mediaPresentationDuration', XmlUtils.parseDuration); + let presentationDuration = context.mediaPresentationDuration; + + if (!presentationDuration) { + presentationDuration = XmlUtils.parseAttr( + mpd, 'mediaPresentationDuration', XmlUtils.parseDuration); + this.manifestPatchContext_.mediaPresentationDuration = + presentationDuration; + } const periods = []; let prevEnd = 0; @@ -1145,6 +1419,8 @@ shaka.dash.DashParser = class { context.representation = this.createFrame_(node, context.adaptationSet, null); + const representationId = context.representation.id; + this.minTotalAvailabilityTimeOffset_ = Math.min(this.minTotalAvailabilityTimeOffset_, context.representation.availabilityTimeOffset); @@ -1186,7 +1462,8 @@ shaka.dash.DashParser = class { streamInfo = shaka.dash.SegmentTemplate.createStreamInfo( context, requestSegment, this.streamMap_, hasManifest, - this.config_.dash.initialSegmentLimit, this.periodDurations_); + this.config_.dash.initialSegmentLimit, this.periodDurations_, + /* isPatchUpdate= */ false); } else { goog.asserts.assert(isText, 'Must have Segment* with non-text streams.'); @@ -1276,6 +1553,13 @@ shaka.dash.DashParser = class { const contextId = context.representation.id ? context.period.id + ',' + context.representation.id : ''; + if (this.patchLocationUrl_ && context.periodInfo.isLastPeriod && + representationId) { + this.contextCache_.set(`${context.period.id},${representationId}`, + this.cloneContext_(context)); + } + context.segmentInfo = null; + /** @type {shaka.extern.Stream} */ let stream; @@ -1336,6 +1620,53 @@ shaka.dash.DashParser = class { return stream; } + /** + * Clone context and remove xml document references. + * + * @param {!shaka.dash.DashParser.Context} context + * @return {!shaka.dash.DashParser.Context} + * @private + */ + cloneContext_(context) { + const contextClone = /** @type {!shaka.dash.DashParser.Context} */({}); + + for (const k of Object.keys(context)) { + if (['period', 'adaptationSet', 'representation'].includes(k)) { + contextClone[k] = { + segmentBase: null, + segmentList: null, + segmentTemplate: null, + baseUris: context[k].baseUris, + width: context[k].width, + height: context[k].height, + contentType: context[k].contentType, + mimeType: context[k].mimeType, + lang: context[k].lang, + codecs: context[k].codecs, + frameRate: context[k].frameRate, + pixelAspectRatio: context[k].pixelAspectRatio, + emsgSchemeIdUris: context[k].emsgSchemeIdUris, + id: context[k].id, + numChannels: context[k].numChannels, + audioSamplingRate: context[k].audioSamplingRate, + availabilityTimeOffset: context[k].availabilityTimeOffset, + initialization: context[k].initialization, + }; + } else if (k == 'periodInfo') { + contextClone[k] = { + start: context[k].start, + duration: context[k].duration, + node: null, + isLastPeriod: context[k].isLastPeriod, + }; + } else { + contextClone[k] = context[k]; + } + } + + return contextClone; + } + /** * Called when the update timer ticks. * @@ -1497,6 +1828,7 @@ shaka.dash.DashParser = class { numChannels: numChannels, audioSamplingRate: audioSamplingRate, availabilityTimeOffset: availabilityTimeOffset, + initialization: null, }; } @@ -1879,6 +2211,31 @@ shaka.dash.DashParser = class { } }; +/** +* @typedef {{ + * type: string, + * mediaPresentationDuration: ?number, + * profiles: !Array., + * availabilityTimeOffset: number, + * baseUris: !Array. + * }} + */ +shaka.dash.DashParser.PatchContext; + + +/** + * @typedef {{ + * timescale: number, + * duration: number, + * startNumber: number, + * presentationTimeOffset: number, + * media: ?string, + * index: ?string, + * timepoints: Array, + * timeline: Array + * }} + */ +shaka.dash.DashParser.SegmentInfo; /** * Contains the minimum amount of time, in seconds, between manifest update @@ -1916,7 +2273,8 @@ shaka.dash.DashParser.RequestSegmentCallback; * id: ?string, * numChannels: ?number, * audioSamplingRate: ?number, - * availabilityTimeOffset: number + * availabilityTimeOffset: number, + * initialization: ?string * }} * * @description @@ -1970,6 +2328,8 @@ shaka.dash.DashParser.InheritanceFrame; * bandwidth: number, * indexRangeWarningGiven: boolean, * availabilityTimeOffset: number, + * segmentInfo: ?shaka.dash.DashParser.SegmentInfo, + * mediaPresentationDuration: ?number, * profiles: !Array. * }} * @@ -1999,6 +2359,10 @@ shaka.dash.DashParser.InheritanceFrame; * @property {!Array.} profiles * Profiles of DASH are defined to enable interoperability and the signaling * of the use of features. + * @property {?shaka.dash.DashParser.SegmentInfo} segmentInfo + * The segment info for current representation. + * @property {?number} mediaPresentationDuration + * Media presentation duration, or null if unknown. */ shaka.dash.DashParser.Context; diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 1fd3ca9e92..4f34739bca 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -116,7 +116,7 @@ shaka.dash.MpdUtils = class { * Expands a SegmentTimeline into an array-based timeline. The results are in * seconds. * - * @param {!Element} segmentTimeline + * @param {Array} timePoints * @param {number} timescale * @param {number} unscaledPresentationTimeOffset * @param {number} periodDuration The Period's duration in seconds. @@ -124,7 +124,7 @@ shaka.dash.MpdUtils = class { * @return {!Array.} */ static createTimeline( - segmentTimeline, timescale, unscaledPresentationTimeOffset, + timePoints, timescale, unscaledPresentationTimeOffset, periodDuration) { goog.asserts.assert( timescale > 0 && timescale < Infinity, @@ -135,8 +135,6 @@ shaka.dash.MpdUtils = class { // Alias. const XmlUtils = shaka.util.XmlUtils; - const timePoints = XmlUtils.findChildren(segmentTimeline, 'S'); - /** @type {!Array.} */ const timeline = []; let lastEndTime = -unscaledPresentationTimeOffset; @@ -252,49 +250,77 @@ shaka.dash.MpdUtils = class { */ static parseSegmentInfo(context, callback) { goog.asserts.assert( - callback(context.representation), - 'There must be at least one element of the given type.'); + callback(context.representation) || context.segmentInfo, + 'There must be at least one element of the given type ' + + 'or segment info defined.'); const MpdUtils = shaka.dash.MpdUtils; const XmlUtils = shaka.util.XmlUtils; - const timescaleStr = - MpdUtils.inheritAttribute(context, callback, 'timescale'); let timescale = 1; - if (timescaleStr) { - timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + if (context.segmentInfo) { + timescale = context.segmentInfo.timescale; + } else { + const timescaleStr = + MpdUtils.inheritAttribute(context, callback, 'timescale'); + + if (timescaleStr) { + timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + } } - - const durationStr = + let segmentDuration = 0; + if (context.segmentInfo) { + segmentDuration = context.segmentInfo.duration; + } else { + const durationStr = MpdUtils.inheritAttribute(context, callback, 'duration'); - let segmentDuration = XmlUtils.parsePositiveInt(durationStr || ''); - const ContentType = shaka.util.ManifestParserUtils.ContentType; - // TODO: The specification is not clear, check this once it is resolved: - // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/404 - if (context.representation.contentType == ContentType.IMAGE) { - segmentDuration = XmlUtils.parseFloat(durationStr || ''); + segmentDuration = XmlUtils.parsePositiveInt(durationStr || ''); + + if (segmentDuration) { + segmentDuration /= timescale; + } } - if (segmentDuration) { - segmentDuration /= timescale; + + let startNumber = 1; + if (context.segmentInfo) { + startNumber = context.segmentInfo.startNumber; + } else { + const startNumberStr = + MpdUtils.inheritAttribute(context, callback, 'startNumber'); + startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || ''); + if (startNumberStr == null || startNumber == null) { + startNumber = 1; + } } - const startNumberStr = - MpdUtils.inheritAttribute(context, callback, 'startNumber'); - const unscaledPresentationTimeOffset = + let unscaledPresentationTimeOffset; + if (context.segmentInfo) { + unscaledPresentationTimeOffset = + context.segmentInfo.presentationTimeOffset; + } else { + unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(context, callback, 'presentationTimeOffset')) || 0; - let startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || ''); - if (startNumberStr == null || startNumber == null) { - startNumber = 1; } const timelineNode = MpdUtils.inheritChild(context, callback, 'SegmentTimeline'); /** @type {Array.} */ let timeline = null; - if (timelineNode) { - timeline = MpdUtils.createTimeline( - timelineNode, timescale, unscaledPresentationTimeOffset, + if (context.segmentInfo && context.segmentInfo.timepoints) { + timeline = context.segmentInfo.timeline; + goog.asserts.assert(timeline, 'timeline should exist!'); + const timePoints = context.segmentInfo.timepoints; + const partialTimeline = MpdUtils.createTimeline( + timePoints, timescale, unscaledPresentationTimeOffset, context.periodInfo.duration || Infinity); + timeline.push(...partialTimeline); + } else { + if (timelineNode) { + const timePoints = XmlUtils.findChildren(timelineNode, 'S'); + timeline = MpdUtils.createTimeline( + timePoints, timescale, unscaledPresentationTimeOffset, + context.periodInfo.duration || Infinity); + } } const scaledPresentationTimeOffset = diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 63375a88ef..7bdef283f8 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -35,22 +35,45 @@ shaka.dash.SegmentTemplate = class { * @param {number} segmentLimit The maximum number of segments to generate for * a SegmentTemplate with fixed duration. * @param {!Object.} periodDurationMap + * @param {boolean} isPatchUpdate * @return {shaka.dash.DashParser.StreamInfo} */ static createStreamInfo( context, requestSegment, streamMap, isUpdate, segmentLimit, - periodDurationMap) { - goog.asserts.assert(context.representation.segmentTemplate, - 'Should only be called with SegmentTemplate'); + periodDurationMap, isPatchUpdate) { + goog.asserts.assert(context.representation.segmentTemplate || + context.segmentInfo, 'Should only be called with SegmentTemplate ' + + 'or segment info defined'); + const MpdUtils = shaka.dash.MpdUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; const TimelineSegmentIndex = shaka.dash.TimelineSegmentIndex; - const initSegmentReference = SegmentTemplate.createInitSegment_(context); + if (!isPatchUpdate && !context.representation.initialization) { + context.representation.initialization = + MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'initialization'); + } + + const initSegmentReference = context.representation.initialization ? + SegmentTemplate.createInitSegment_(context) : null; /** @type {shaka.dash.SegmentTemplate.SegmentTemplateInfo} */ const info = SegmentTemplate.parseSegmentTemplateInfo_(context); - SegmentTemplate.checkSegmentTemplateInfo_(context, info); + if (!context.segmentInfo) { + context.segmentInfo = { + timescale: info.timescale, + media: info.mediaTemplate, + index: info.indexTemplate, + startNumber: info.startNumber, + nextStartNumber: 0, + duration: info.segmentDuration || 0, + presentationTimeOffset: info.unscaledPresentationTimeOffset, + timepoints: null, + timeline: info.timeline, + }; + SegmentTemplate.checkSegmentTemplateInfo_(context, info); + } // Direct fields of context will be reassigned by the parser before // generateSegmentIndex is called. So we must make a shallow copy first, @@ -192,10 +215,21 @@ shaka.dash.SegmentTemplate = class { const segmentInfo = MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_); - const media = MpdUtils.inheritAttribute( - context, SegmentTemplate.fromInheritance_, 'media'); - const index = MpdUtils.inheritAttribute( - context, SegmentTemplate.fromInheritance_, 'index'); + let media = null; + if (context.segmentInfo) { + media = context.segmentInfo.media; + } else { + media = MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'media'); + } + + let index = null; + if (context.segmentInfo) { + index = context.segmentInfo.index; + } else { + index = MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'index'); + } return { segmentDuration: segmentInfo.segmentDuration, @@ -534,6 +568,8 @@ shaka.dash.SegmentTemplate = class { // (See section 5.3.9.5.3 of the DASH spec.) const segmentReplacement = i + info.startNumber; + context.segmentInfo.startNumber = segmentReplacement + 1; + // Consider the presentation time offset in segment uri computation const timeReplacement = unscaledStart + info.unscaledPresentationTimeOffset; @@ -589,8 +625,11 @@ shaka.dash.SegmentTemplate = class { const ManifestParserUtils = shaka.util.ManifestParserUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; - const initialization = MpdUtils.inheritAttribute( - context, SegmentTemplate.fromInheritance_, 'initialization'); + let initialization = context.representation.initialization; + if (!initialization) { + initialization = MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'initialization'); + } if (!initialization) { return null; } diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b8bf2687ee..1874ef1655 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -144,6 +144,25 @@ shaka.media.DrmEngine = class { this.manifestInitData_ = null; } + /** HACK FOR TESTING */ + /** + * Get the MediaKeys and active Media Key Sessions + * + * @return {*} + */ + getMediaKeysData() { + /** @type {Map} */ + const adaptedSessions = new Map(); + const activeSessions = Array.from(this.activeSessions_.entries()); + for (const [session, metadata] of activeSessions) { + adaptedSessions.set(session, metadata.initData); + } + return { + mediaKeysInstance: this.mediaKeys_, + activeSessions: adaptedSessions, + }; + } + /** @override */ destroy() { return this.destroyer_.destroy(); diff --git a/lib/player.js b/lib/player.js index 37b11bff88..683aaa662b 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1935,6 +1935,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget { }); } + /** HACK FOR TESTING */ + /** + * Get the MediaKeys and active Media Key Sessions + * + * @return {*} + * @export + */ + getMediaKeysData() { + return this.drmEngine_ ? this.drmEngine_.getMediaKeysData() : null; + } + /** * This should only be called by the load graph when it is time to initialize * drmEngine. The only time this may be called is when we are attached a diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 9290a5d1a8..322dba9d31 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -141,6 +141,7 @@ shaka.util.PlayerConfiguration = class { element); }, sequenceMode: false, + enablePatchMPDSupport: false, }, hls: { ignoreTextStreamFailures: false, diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js index 543e986eb3..c46c8495e9 100644 --- a/lib/util/xml_utils.js +++ b/lib/util/xml_utils.js @@ -352,6 +352,32 @@ shaka.util.XmlUtils = class { return !isNaN(n) ? n : null; } + /** + * Parse xPath strings for segments and id targets. + * @param {string} exprString + * @return {Array} + */ + static parseXpath(exprString) { + const returnPaths = []; + // Split string by paths but ignore '/' in quotes + const paths = exprString + .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/); + for (const path of paths) { + const nodeName = path.match(/\b([A-Z])\w+/); + + // We only want the id attribute in which case + // /'(.*?)'/ will suffice to get it. + const idAttr = path.match(/(@id='(.*?)')/); + if (nodeName) { + returnPaths.push({ + name: nodeName[0], + id: idAttr ? + idAttr[0].match(/'(.*?)'/)[0].replaceAll('\'', '') : null, + }); + } + } + return returnPaths; + } /** * Parse a string and return the resulting root element if it was valid XML. @@ -454,3 +480,11 @@ shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => { return (s) => s; }); +/** + * @typedef {{ +* name: !string, +* id: string +* }} +* @export +*/ +shaka.util.XmlUtils.PathNode; diff --git a/test/dash/mpd_utils_unit.js b/test/dash/mpd_utils_unit.js index 0a99b28fa8..e27c9d3094 100644 --- a/test/dash/mpd_utils_unit.js +++ b/test/dash/mpd_utils_unit.js @@ -428,8 +428,10 @@ describe('MpdUtils', () => { const segmentTimeline = xml.documentElement; console.assert(segmentTimeline); + const timePoints = shaka.util.XmlUtils.findChildren(segmentTimeline, 'S'); + const timeline = MpdUtils.createTimeline( - segmentTimeline, timescale, presentationTimeOffset, periodDuration); + timePoints, timescale, presentationTimeOffset, periodDuration); expect(timeline).toEqual( expected.map((c) => jasmine.objectContaining(c))); } From 713f56e9a63eb2709af34b3a1ccf90383e064183 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Thu, 11 May 2023 15:33:38 +0100 Subject: [PATCH 02/48] add mpd patch adapter --- build/conformance.textproto | 2 ++ demo/common/assets.js | 20 +++++++++++++++++--- demo/common/message_ids.js | 1 + demo/main.js | 27 +++++++++++++++++++++++++++ lib/dash/dash_parser.js | 3 +++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/build/conformance.textproto b/build/conformance.textproto index a2ffb71ef2..5b909e979b 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -320,6 +320,7 @@ requirement: { "use shaka.test.Util.fetch or NetworkingEngine.request " "instead." whitelist_regexp: "lib/net/http_fetch_plugin.js" + whitelist_regexp: "demo/common/patch_mpd_adapter.js" } # Disallow the use of generators, which are a major performance issue. See @@ -375,5 +376,6 @@ requirement: { "use shaka.util.XmlUtils.parseXmlString instead." whitelist_regexp: "lib/util/xml_utils.js" whitelist_regexp: "test/" + whitelist_regexp: "demo/common/patch_mpd_adapter.js" } diff --git a/demo/common/assets.js b/demo/common/assets.js index 35537d7372..c6b600ac85 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -139,6 +139,8 @@ shakaAssets.Feature = { // Set if the asset is a MPEG-DASH manifest. DASH: shakaDemo.MessageIds.DASH, + // Enable MPD patch for live DASH + ENABLE_DASH_PATCH: shakaDemo.MessageIds.ENABLE_DASH_PATCH, // Set if the asset is an HLS manifest. HLS: shakaDemo.MessageIds.HLS, // Set if the asset is an MSS manifest. @@ -544,15 +546,27 @@ shakaAssets.testAssets = [ .setIMAContentSourceId('2528370') .setIMAVideoId('tears-of-steel') .setIMAManifestType('HLS'), - new ShakaDemoAssetInfo( - /* name= */ 'Tears of Steel (live, DASH, Server Side ads)', + // new ShakaDemoAssetInfo( + // /* name= */ 'Tears of Steel (live, DASH, Server Side ads)', + // /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + // /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', + // /* source= */ shakaAssets.Source.SHAKA) + // .addFeature(shakaAssets.Feature.DASH) + // .addFeature(shakaAssets.Feature.MP4) + // .addFeature(shakaAssets.Feature.SUBTITLES) + // .addFeature(shakaAssets.Feature.LIVE) + // .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') + // .setIMAManifestType('DASH'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (live, DASH, MPD PATCH)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd?foo=1', /* source= */ shakaAssets.Source.SHAKA) .addFeature(shakaAssets.Feature.DASH) .addFeature(shakaAssets.Feature.MP4) .addFeature(shakaAssets.Feature.SUBTITLES) .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.ENABLE_DASH_PATCH) .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') .setIMAManifestType('DASH'), new ShakaDemoAssetInfo( diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index c03b7c60b7..e4c8ff2f5f 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -15,6 +15,7 @@ shakaDemo.MessageIds = { CAPTIONS: 'DEMO_CAPTIONS', CONTAINERLESS: 'DEMO_CONTAINERLESS', DASH: 'DEMO_DASH', + ENABLE_DASH_PATCH: 'ENABLE_DASH_PATCH', HIGH_DEFINITION: 'DEMO_HIGH_DEFINITION', HLS: 'DEMO_HLS', LCEVC: 'DEMO_LCEVC', diff --git a/demo/main.js b/demo/main.js index 79f69ccff2..18a768b62d 100644 --- a/demo/main.js +++ b/demo/main.js @@ -14,6 +14,7 @@ goog.require('shakaDemo.MessageIds'); goog.require('shakaDemo.Utils'); goog.require('shakaDemo.Visualizer'); goog.require('shakaDemo.VisualizerButton'); +goog.require('shakaDemo.MPDPatchAdapter'); /** * Shaka Player demo, main section. @@ -72,6 +73,9 @@ shakaDemo.Main = class { /** @private {boolean} */ this.noInput_ = false; + /** @private {shakaDemo.MPDPatchAdapter} */ + this.mpdPatchAdapter_ = null; + /** @private {!HTMLAnchorElement} */ this.errorDisplayLink_ = /** @type {!HTMLAnchorElement} */( document.getElementById('error-display-link')); @@ -1308,6 +1312,7 @@ shakaDemo.Main = class { } await this.drmConfiguration_(asset); + this.controls_.getCastProxy().setAppData({'asset': asset}); // Finally, the asset can be loaded. @@ -1322,6 +1327,28 @@ shakaDemo.Main = class { if (asset.imaAssetKey || (asset.imaContentSrcId && asset.imaVideoId)) { manifestUri = await this.getManifestUriFromAdManager_(asset); } + + if (asset.features.includes( + shakaAssets.Feature.ENABLE_DASH_PATCH)) { + console.log('**** doing the thing'); + this.mpdPatchAdapter_ = + new shakaDemo.MPDPatchAdapter(manifestUri); + const patchConfig = { + manifest: { + dash: { + enablePatchMPDSupport: true, + manifestPreprocessor: + // eslint-disable-next-line + (e) => this.mpdPatchAdapter_.patchMpdPreProcessor(e), + }, + }, + }; + + shaka.net.NetworkingEngine.registerScheme('patch', + (url) => this.mpdPatchAdapter_.customPatchHandler(url)); + this.player_.configure(patchConfig); + } + await this.player_.load( manifestUri, /* startTime= */ null, diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 83e49cd15f..f86985ba51 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -361,6 +361,9 @@ shaka.dash.DashParser = class { manifestBaseUris = absoluteLocations; } + console.log('***** dashparser', this.config_.dash.enablePatchMPDSupport, + this.config_.dash.manifestPreprocessor); + if (this.config_.dash.enablePatchMPDSupport) { // Get patch location element const patchLocation = XmlUtils.findChildren(mpd, 'PatchLocation') From 44001b21f514249e1cf07a1ed741b1004d23c191 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Thu, 11 May 2023 15:34:03 +0100 Subject: [PATCH 03/48] add mpd patch adapter.. --- demo/common/patch_mpd_adapter.js | 506 +++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 demo/common/patch_mpd_adapter.js diff --git a/demo/common/patch_mpd_adapter.js b/demo/common/patch_mpd_adapter.js new file mode 100644 index 0000000000..dc3ccb24a8 --- /dev/null +++ b/demo/common/patch_mpd_adapter.js @@ -0,0 +1,506 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shakaDemo.MPDPatchAdapter'); + +const PatchType = { + ADD: 'add', + REPLACE: 'replace', + DELETE: 'delete', +}; + +const ParserMode = { + INITIAL_FULL_MPD: 'INITIAL_FULL_MPD', + PATCH_MPD: 'PATCH_MPD', +}; + +/** +* @typedef {{ +* type: string, +* xpathLocation: string, +* element: Node +* }} +*/ +shakaDemo.MPDPatchAdapter.Patch; + +/** + * Finds child XML elements. + * @param {Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @return {!Array.} The child XML elements. + */ +function findChildren(elem, name) { + const found = []; + if (!elem) { + return []; + } + for (const child of elem.childNodes) { + if (child instanceof Element && child.tagName == name) { + found.push(child); + } + } + return found; +} + +shakaDemo.MPDPatchAdapter = class { + /** + * @param {string} url + */ + constructor(url) { + /** @type {string} */ + this.manifestUrl_ = url; + /** @type {Set} */ + this.periodContext_ = new Set(); + /** @type {Map} */ + this.segmentContext_ = new Map(); + /** @type {string|undefined} */ + this.manifestType_ = undefined; + /** @type {?string} */ + this.mediaPresentationDuration_ = null; + /** @type {string} */ + this.parserMode_ = ParserMode.INITIAL_FULL_MPD; + } + + /** + * @param {Element} manifest + */ + patchMpdPreProcessor(manifest) { + console.log('****** manifest preprocessor called'); + // createElementNS is being used here over createElement to preserve + // the capitalisation of the first letter. + const patch = document.createElementNS('', 'PatchLocation'); + // eslint-disable-next-line no-useless-concat + patch['inner'+'HTML'] = this.getPatchManifestUrl_(); + manifest.appendChild(patch); + this.processManifest_(manifest); + this.parserMode_ = ParserMode.PATCH_MPD; + } + + /** + * + * @param {Element} manifest + * @return {Array} + * @private + */ + processManifest_(manifest) { + /** @type {Array} */ + const additions = []; + + const mediaPresentationDuration = + manifest.getAttribute('mediaPresentationDuration'); + if (this.mediaPresentationDuration_ != mediaPresentationDuration) { + if (this.parserMode_ == ParserMode.PATCH_MPD) { + additions.push({ + xpathLocation: '/MPD/@mediaPresentationDuration', + type: this.mediaPresentationDuration_ ? + PatchType.REPLACE : PatchType.ADD, + element: document.createTextNode(mediaPresentationDuration), + }); + } + this.mediaPresentationDuration_ = mediaPresentationDuration; + } + + const manifestType = manifest.getAttribute('type') || ''; + if (this.manifestType_ != manifestType) { + this.manifestType_ = manifestType; + if (this.parserMode_ == ParserMode.PATCH_MPD) { + additions.push({ + xpathLocation: '/MPD/@type', + type: PatchType.REPLACE, + element: document.createTextNode(manifestType), + }); + } + } + + const periods = this.getPeriods_(manifest); + const lastPeriod = periods[periods.length - 1]; + + // We're doing backward loop as new periods and segments should appear + // at the end of manifest. + for (let i = periods.length - 1; i >= 0; i--) { + const period = periods[i]; + + // On the initial pass we will only look at the last period + if (this.parserMode_ === ParserMode.INITIAL_FULL_MPD && + period !== lastPeriod) { + const periodCacheKey = this.getCacheKeyForPeriod_(period); + this.periodContext_.add(periodCacheKey); + continue; + } + + const periodCacheKey = this.getCacheKeyForPeriod_(period); + const addingCompletePeriod = !this.periodContext_.has(periodCacheKey); + if (addingCompletePeriod) { + this.periodContext_.add(periodCacheKey); + + if (this.parserMode_ === ParserMode.PATCH_MPD) { + additions.unshift({ + xpathLocation: '/MPD/', + element: period, + type: PatchType.ADD, + }); + } + } + + for (const adaptationSet of this.getAdaptationSets_(period) || []) { + const representations = this.getRepresentations_(adaptationSet); + + if (!adaptationSet.hasAttribute('id')) { + const adaptationSetId = this.generateAdaptationSetId_( + adaptationSet, representations[representations.length - 1]); + adaptationSet.setAttribute('id', adaptationSetId); + } + + // This is a peacock optimisation as we assume if there + // is a segment timeline this will be identical in all + // child representations. + const segmentTemplate = this.getSegmentTemplate_(adaptationSet); + if (segmentTemplate) { + this.processSegmentTemplate_(additions, segmentTemplate, + addingCompletePeriod, period, adaptationSet); + } else { + for (const representation of representations || []) { + const segmentTemplate = this.getSegmentTemplate_(representation); + if (segmentTemplate) { + this.processSegmentTemplate_(additions, segmentTemplate, + addingCompletePeriod, period, adaptationSet, representation); + } + } + } + } + + // If we're not adding complete period in this iteration, it's clear + // we already added all new periods and new segments to the last + // existing period. So, we can easily break parsing here. + if (!addingCompletePeriod) { + break; + } + } + + console.log('***** processed manifest', manifest); + + return additions; + } + + /** + * + * @param {Array} additions + * @param {Element} segmentTemplate + * @param {boolean} addingCompletePeriod + * @param {Element} period + * @param {Element} adaptationSet + * @param {Element=} representation + * @private + */ + processSegmentTemplate_( + additions, + segmentTemplate, + addingCompletePeriod, + period, + adaptationSet, + representation, + ) { + const cacheKey = this.getCacheKeyForSegmentTimeline_( + period, adaptationSet, representation); + /** @type {Array} */ + const segmentPatches = []; + + const segmentTimeline = this.getSegmentTimeline_(segmentTemplate); + if (segmentTimeline) { + const segmentTags = this.getSegment_(segmentTimeline); + const lastSegmentSeen = this.segmentContext_.get(cacheKey) || 0; + let lastEndTime = 0; + + for (const segmentTag of segmentTags || []) { + let additionalSegments = 0; + /** @type {number} */ + let firstNewSegmentStartTime; + const t = Number(segmentTag.getAttribute('t') || lastEndTime); + const d = Number(segmentTag.getAttribute('d')); + const r = Number(segmentTag.getAttribute('r') || 0); + + let startTime = t; + + for (let j = 0; j <= r; ++j) { + const endTime = startTime + d; + + if (endTime > lastSegmentSeen) { + this.segmentContext_.set(cacheKey, endTime); + additionalSegments++; + if (!firstNewSegmentStartTime) { + firstNewSegmentStartTime = startTime; + } + } + startTime = endTime; + lastEndTime = endTime; + } + if (additionalSegments > 0 && !addingCompletePeriod) { + // createElementNS is being used here over createElement to preserve + // the capitalisation of the first letter. + const newSegment = document.createElementNS('', 'S'); + newSegment.setAttribute('d', d.toString()); + newSegment.setAttribute('t', firstNewSegmentStartTime.toString()); + if (additionalSegments > 1) { + // minus one repeat for the original + newSegment.setAttribute('r', (additionalSegments - 1).toString()); + } + + if (this.parserMode_ === ParserMode.PATCH_MPD) { + segmentPatches.push({ + xpathLocation: this.getSegmentXpathLocation_( + period, adaptationSet, representation), + element: newSegment, + type: PatchType.ADD, + }); + } + } + } + } + additions.unshift(...segmentPatches.values()); + } + + + /** + * @param {string} url + */ + customPatchHandler(url) { + console.log('**** handling custom manifest'); + const manifestUrl = this.getManifestUrlFromPatch_(url); + const start = (performance && performance.now()) || Date.now(); + const fetchPromise = fetch(manifestUrl).then((response) => { + const end = (performance && performance.now()) || Date.now(); + const parser = new DOMParser(); + return response.text().then((body) => { + const manifest = parser.parseFromString(body, 'text/xml'); + const additions = this.processManifest_(manifest.documentElement); + const patchManifest = this.generatePatch_( + manifest.documentElement, additions); + + const buffer = shaka.util.StringUtils.toUTF8( + // eslint-disable-next-line no-useless-concat + patchManifest['outer'+'HTML']); + + const data = { + timeMs: end - start, + fromCache: false, + data: buffer, + }; + return data; + }); + }); + + + /** @type {!shaka.util.AbortableOperation} */ + const op = new shaka.util.AbortableOperation( + fetchPromise, () => { + return Promise.resolve(); + }); + + return op; + } + + /** + * + * @param {Element} manifest + * @param {Array} additions + * @return {Element} + * @private + */ + generatePatch_(manifest, additions) { + const patch = document.createElementNS('', 'Patch'); + patch.setAttribute('mpdId', 'channel'); + patch.setAttribute('publishTime', + manifest.getAttribute('publishTime') || ''); + this.appendXmlNamespaces_(manifest, patch); + this.generateAdditions_(patch, additions); + return patch; + } + + /** + * + * @param {Element} originalManifest + * @param {Element} patchManifest + * @private + */ + appendXmlNamespaces_(originalManifest, patchManifest) { + for (const node of originalManifest.attributes) { + if (node.name.includes('xmlns')) { + if (node.name === 'xmlns') { + const namespaces = node.value.split(':'); + namespaces.push('mpdpatch', '2020'); + patchManifest.setAttribute('xmlns', namespaces.join(':')); + } else { + patchManifest.setAttribute(node.name, + originalManifest.getAttribute(node.name)); + } + } + } + patchManifest.setAttribute('xmlns:p', + 'urn:ietf:params:xml:schema:patchops'); + } + + /** + * + * @param {Element} patchElement + * @param {Array} additions + * @private + */ + generateAdditions_(patchElement, additions) { + for (const patch of additions.values()) { + const patchChange = document.createElementNS('p', `p:${patch.type}`); + patchChange.setAttribute('sel', patch.xpathLocation); + if (patch.element) { + patchChange.appendChild(patch.element); + } + patchElement.appendChild(patchChange); + } + } + + /** @private */ + getPatchManifestUrl_() { + // This method replaces the protocol 'https' with 'patch' + // so it is handled with via the patch network scheme + return this.manifestUrl_.replace('https', 'patch'); + } + + /** + * + * @param {string} url + * @return {string} + * @private + */ + getManifestUrlFromPatch_(url) { + // This is intentionally different from the method above + // as we want to request the smaller 2 minute window manifest + // and not the full window. + // https://github.com/sky-uk/core-video-sdk-js/pull/5428#discussion_r1038259178 + return url.replace('patch', 'https'); + } + + /** + * + * @param {Element} adaptationSet + * @param {Element} representation + * @return {string} + * @private + */ + generateAdaptationSetId_(adaptationSet, representation) { + const mimeType = adaptationSet.getAttribute('mimeType'); + const lang = adaptationSet.getAttribute('lang'); + let adaptationSetId = `${mimeType}#${lang}`; + if (mimeType === 'audio/mp4') { + const representationId = representation.getAttribute('id'); + adaptationSetId += `#${representationId}`; + } + return adaptationSetId; + } + + /** + * + * @param {Element} element + * @return {?Array} + * @private + */ + getPeriods_(element) { + return findChildren(element, 'Period'); + } + + /** + * + * @param {Element} element + * @return {Array} + * @private + */ + getRepresentations_(element) { + return findChildren(element, 'Representation'); + } + + /** + * + * @param {Element} element + * @return {Array} + * @private + */ + getAdaptationSets_(element) { + return findChildren(element, 'AdaptationSet'); + } + + /** + * + * @param {Element} element + * @return {Element} + * @private + */ + getSegmentTemplate_(element) { + const segmentTemplates = findChildren( + element, 'SegmentTemplate'); + return segmentTemplates.length ? segmentTemplates[0] : null; + } + + /** + * + * @param {Element} element + * @return {Element} + * @private + */ + getSegmentTimeline_(element) { + const segmentTimeline = findChildren( + element, 'SegmentTimeline'); + return segmentTimeline.length ? segmentTimeline[0] : null; + } + + /** + * + * @param {Element} element + * @return {Array} + * @private + */ + getSegment_(element) { + return findChildren(element, 'S'); + } + + /** + * + * @param {Element} period + * @return {string} + * @private + */ + getCacheKeyForPeriod_(period) { + return `P_${period.getAttribute('id')}`; + } + + /** + * + * @param {Element} period + * @param {Element} adaptationSet + * @param {Element=} representation + * @return {string} + * @private + */ + getCacheKeyForSegmentTimeline_(period, adaptationSet, representation) { + return `${this.getCacheKeyForPeriod_(period)}_AS_${ + adaptationSet.getAttribute('id')}_R_${ + (representation && representation.getAttribute('id')) || 'xx'}`; + } + + /** + * + * @param {Element} period + * @param {Element} adaptationSet + * @param {Element=} representation + * @return {string} + * @private + */ + getSegmentXpathLocation_(period, adaptationSet, representation) { + const representationCriteria = representation ? + `/Representation[@id='${representation.getAttribute('id')}']` : ''; + + return `/MPD/Period[@id='${period.getAttribute('id') + }']/AdaptationSet[@id='${adaptationSet.getAttribute( + 'id', + )}']${representationCriteria}/SegmentTemplate/SegmentTimeline`; + } +}; From 3711a6a9bc31c324e5237eac26d6e0caf06cbff1 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Fri, 12 May 2023 16:08:52 +0100 Subject: [PATCH 04/48] port over more improvements --- lib/dash/dash_parser.js | 138 ++++++++++++++++++++-------------------- lib/dash/mpd_utils.js | 4 +- lib/util/periods.js | 54 +++++++++++----- 3 files changed, 111 insertions(+), 85 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index f86985ba51..566cba2a9a 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -60,10 +60,10 @@ shaka.dash.DashParser = class { this.patchLocationUrl_ = null; /** - * A context of the living manifest used for processing - * Patch MPD's - * @private {!shaka.dash.DashParser.PatchContext} - */ + * A context of the living manifest used for processing + * Patch MPD's + * @private {!shaka.dash.DashParser.PatchContext} + */ this.manifestPatchContext_ = { type: '', profiles: [], @@ -77,33 +77,33 @@ shaka.dash.DashParser = class { /** - * A map of IDs to Stream objects. - * ID: Period@id,AdaptationSet@id,@Representation@id - * e.g.: '1,5,23' - * @private {!Object.} - */ + * A map of IDs to Stream objects. + * ID: Period@id,AdaptationSet@id,@Representation@id + * e.g.: '1,5,23' + * @private {!Object.} + */ this.streamMap_ = {}; /** - * A map of period ids to their durations - * @private {!Object.} - */ + * A map of period ids to their durations + * @private {!Object.} + */ this.periodDurations_ = {}; /** @private {shaka.util.PeriodCombiner} */ this.periodCombiner_ = new shaka.util.PeriodCombiner(); /** - * The update period in seconds, or 0 for no updates. - * @private {number} - */ + * The update period in seconds, or 0 for no updates. + * @private {number} + */ this.updatePeriod_ = 0; /** - * An ewma that tracks how long updates take. - * This is to mitigate issues caused by slow parsing on embedded devices. - * @private {!shaka.abr.Ewma} - */ + * An ewma that tracks how long updates take. + * This is to mitigate issues caused by slow parsing on embedded devices. + * @private {!shaka.abr.Ewma} + */ this.averageUpdateDuration_ = new shaka.abr.Ewma(5); /** @private {shaka.util.Timer} */ @@ -115,22 +115,22 @@ shaka.dash.DashParser = class { this.operationManager_ = new shaka.util.OperationManager(); /** - * Largest period start time seen. - * @private {?number} - */ + * Largest period start time seen. + * @private {?number} + */ this.largestPeriodStartTime_ = null; /** - * Period IDs seen in previous manifest. - * @private {!Array.} - */ + * Period IDs seen in previous manifest. + * @private {!Array.} + */ this.lastManifestUpdatePeriodIds_ = []; /** - * The minimum of the availabilityTimeOffset values among the adaptation - * sets. - * @private {number} - */ + * The minimum of the availabilityTimeOffset values among the adaptation + * sets. + * @private {number} + */ this.minTotalAvailabilityTimeOffset_ = Infinity; /** @private {boolean} */ @@ -309,6 +309,7 @@ shaka.dash.DashParser = class { } if (this.patchLocationUrl_) { + console.log('********* patch', mpd); return this.processPatchManifest_(mpd); } @@ -578,7 +579,7 @@ shaka.dash.DashParser = class { // maintain consistency from here on. presentationTimeline.lockStartTime(); } else { - await this.postPeriodProcessing_(periodsAndDuration.periods); + await this.postPeriodProcessing_(periodsAndDuration.periods, false); } } @@ -586,10 +587,11 @@ shaka.dash.DashParser = class { * Handles common procedures after processing new periods. * * @param {!Array} periods to be appended + * @param {boolean} isPatchUpdate does call comes from mpd patch update * @private */ - async postPeriodProcessing_(periods) { - await this.periodCombiner_.combinePeriods(periods, true); + async postPeriodProcessing_(periods, isPatchUpdate) { + await this.periodCombiner_.combinePeriods(periods, true, isPatchUpdate); // Just update the variants and text streams, which may change as periods // are added or removed. @@ -655,7 +657,7 @@ shaka.dash.DashParser = class { } if (newPeriods.length) { - await this.postPeriodProcessing_(newPeriods); + await this.postPeriodProcessing_(newPeriods, true); } if (this.manifestPatchContext_.type == 'static') { const duration = this.manifestPatchContext_.mediaPresentationDuration; @@ -664,12 +666,12 @@ shaka.dash.DashParser = class { } /** - * Handles manifest type changes, this transition is expected to be - * "dyanmic" to "static". - * - * @param {!string} mpdType - * @private - */ + * Handles manifest type changes, this transition is expected to be + * "dyanmic" to "static". + * + * @param {!string} mpdType + * @private + */ parsePatchMpdTypeChange_(mpdType) { this.manifest_.presentationTimeline.setStatic(mpdType == 'static'); this.manifestPatchContext_.type = mpdType; @@ -679,9 +681,9 @@ shaka.dash.DashParser = class { } /** - * @param {string} durationString - * @private - */ + * @param {string} durationString + * @private + */ parsePatchMediaPresentationDurationChange_(durationString) { const duration = shaka.util.XmlUtils.parseDuration(durationString); if (duration == null) { @@ -694,11 +696,11 @@ shaka.dash.DashParser = class { } /** - * Ingests a full MPD period element from a patch update - * - * @param {!Element} periods - * @private - */ + * Ingests a full MPD period element from a patch update + * + * @param {!Element} periods + * @private + */ parsePatchPeriod_(periods) { this.contextCache_.clear(); @@ -716,7 +718,7 @@ shaka.dash.DashParser = class { profiles: this.manifestPatchContext_.profiles, segmentInfo: null, mediaPresentationDuration: - this.manifestPatchContext_.mediaPresentationDuration, + this.manifestPatchContext_.mediaPresentationDuration, timelineCache: new Map(), }; @@ -727,12 +729,12 @@ shaka.dash.DashParser = class { } /** - * Ingests Path MPD segments. - * - * @param {!Array} paths - * @param {Array} segments - * @private - */ + * Ingests Path MPD segments. + * + * @param {!Array} paths + * @param {Array} segments + * @private + */ parsePatchSegment_(paths, segments) { let periodId = ''; let adaptationSetId = ''; @@ -875,20 +877,20 @@ shaka.dash.DashParser = class { } /** - * This is to improve robustness when the player observes manifest with - * past periods that are inconsistent to previous ones. - * - * This may happen when a CDN or proxy server switches its upstream from - * one encoder to another redundant encoder. - * - * Skip periods that match all of the following criteria: - * - Start time is earlier than latest period start time ever seen - * - Period ID is never seen in the previous manifest - * - Not the last period in the manifest - * - * Periods that meet the aforementioned criteria are considered invalid - * and should be safe to discard. - */ + * This is to improve robustness when the player observes manifest with + * past periods that are inconsistent to previous ones. + * + * This may happen when a CDN or proxy server switches its upstream from + * one encoder to another redundant encoder. + * + * Skip periods that match all of the following criteria: + * - Start time is earlier than latest period start time ever seen + * - Period ID is never seen in the previous manifest + * - Not the last period in the manifest + * + * Periods that meet the aforementioned criteria are considered invalid + * and should be safe to discard. + */ if (this.largestPeriodStartTime_ !== null && periodId !== null && start !== null && diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 4f34739bca..03d8cb9c4c 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -302,8 +302,6 @@ shaka.dash.MpdUtils = class { 'presentationTimeOffset')) || 0; } - const timelineNode = - MpdUtils.inheritChild(context, callback, 'SegmentTimeline'); /** @type {Array.} */ let timeline = null; if (context.segmentInfo && context.segmentInfo.timepoints) { @@ -315,6 +313,8 @@ shaka.dash.MpdUtils = class { context.periodInfo.duration || Infinity); timeline.push(...partialTimeline); } else { + const timelineNode = + MpdUtils.inheritChild(context, callback, 'SegmentTimeline'); if (timelineNode) { const timePoints = XmlUtils.findChildren(timelineNode, 'S'); timeline = MpdUtils.createTimeline( diff --git a/lib/util/periods.js b/lib/util/periods.js index b80b1b6cb5..767de7c3e5 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -93,9 +93,10 @@ shaka.util.PeriodCombiner = class { /** * @param {!Array.} periods * @param {boolean} isDynamic + * @param {boolean=} isPatchUpdate * @return {!Promise} */ - async combinePeriods(periods, isDynamic) { + async combinePeriods(periods, isDynamic, isPatchUpdate = false) { const ContentType = shaka.util.ManifestParserUtils.ContentType; shaka.util.PeriodCombiner.filterOutAudioStreamDuplicates_(periods); @@ -113,6 +114,8 @@ shaka.util.PeriodCombiner = class { this.textStreams_ = firstPeriod.textStreams; this.imageStreams_ = firstPeriod.imageStreams; } else { + // How many periods we've seen before which are not included in this call. + const periodsMissing = isPatchUpdate ? this.usedPeriodIds_.size : 0; // Find the first period we haven't seen before. Tag all the periods we // see now as "used". let firstNewPeriodIndex = -1; @@ -164,28 +167,32 @@ shaka.util.PeriodCombiner = class { audioStreamsPerPeriod, firstNewPeriodIndex, shaka.util.PeriodCombiner.cloneStream_, - shaka.util.PeriodCombiner.concatenateStreams_); + shaka.util.PeriodCombiner.concatenateStreams_, + periodsMissing); await shaka.util.PeriodCombiner.combine_( this.videoStreams_, videoStreamsPerPeriod, firstNewPeriodIndex, shaka.util.PeriodCombiner.cloneStream_, - shaka.util.PeriodCombiner.concatenateStreams_); + shaka.util.PeriodCombiner.concatenateStreams_, + periodsMissing); await shaka.util.PeriodCombiner.combine_( this.textStreams_, textStreamsPerPeriod, firstNewPeriodIndex, shaka.util.PeriodCombiner.cloneStream_, - shaka.util.PeriodCombiner.concatenateStreams_); + shaka.util.PeriodCombiner.concatenateStreams_, + periodsMissing); await shaka.util.PeriodCombiner.combine_( this.imageStreams_, imageStreamsPerPeriod, firstNewPeriodIndex, shaka.util.PeriodCombiner.cloneStream_, - shaka.util.PeriodCombiner.concatenateStreams_); + shaka.util.PeriodCombiner.concatenateStreams_, + periodsMissing); } // Create variants for all audio/video combinations. @@ -432,28 +439,32 @@ shaka.util.PeriodCombiner = class { audioStreamDbsPerPeriod, /* firstNewPeriodIndex= */ 0, shaka.util.PeriodCombiner.cloneStreamDB_, - shaka.util.PeriodCombiner.concatenateStreamDBs_); + shaka.util.PeriodCombiner.concatenateStreamDBs_, + /* periodsMissing= */ 0); const combinedVideoStreamDbs = await shaka.util.PeriodCombiner.combine_( /* outputStreams= */ [], videoStreamDbsPerPeriod, /* firstNewPeriodIndex= */ 0, shaka.util.PeriodCombiner.cloneStreamDB_, - shaka.util.PeriodCombiner.concatenateStreamDBs_); + shaka.util.PeriodCombiner.concatenateStreamDBs_, + /* periodsMissing= */ 0); const combinedTextStreamDbs = await shaka.util.PeriodCombiner.combine_( /* outputStreams= */ [], textStreamDbsPerPeriod, /* firstNewPeriodIndex= */ 0, shaka.util.PeriodCombiner.cloneStreamDB_, - shaka.util.PeriodCombiner.concatenateStreamDBs_); + shaka.util.PeriodCombiner.concatenateStreamDBs_, + /* periodsMissing= */ 0); const combinedImageStreamDbs = await shaka.util.PeriodCombiner.combine_( /* outputStreams= */ [], imageStreamDbsPerPeriod, /* firstNewPeriodIndex= */ 0, shaka.util.PeriodCombiner.cloneStreamDB_, - shaka.util.PeriodCombiner.concatenateStreamDBs_); + shaka.util.PeriodCombiner.concatenateStreamDBs_, + /* periodsMissing= */ 0); // Recreate variantIds from scratch in the output. // HLS content is always single-period, so the early return at the top of @@ -500,6 +511,7 @@ shaka.util.PeriodCombiner = class { * @param {function(T):T} clone Make a clone of an input stream. * @param {function(T, T)} concat Concatenate the second stream onto the end * of the first. + * @param {number} periodsMissing How many periods are missing in this update. * * @return {!Promise.>} The same array passed to outputStreams, * modified to include any newly-created streams. @@ -510,10 +522,14 @@ shaka.util.PeriodCombiner = class { * @private */ static async combine_( - outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat) { + outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat, + periodsMissing) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const unusedStreamsPerPeriod = []; + for (let i = 0; i < periodsMissing; i++) { + unusedStreamsPerPeriod.push(new Set()); + } for (let i = 0; i < streamsPerPeriod.length; i++) { if (i >= firstNewPeriodIndex) { // This periods streams are all new. @@ -529,7 +545,7 @@ shaka.util.PeriodCombiner = class { // eslint-disable-next-line no-await-in-loop const ok = await shaka.util.PeriodCombiner.extendExistingOutputStream_( outputStream, streamsPerPeriod, firstNewPeriodIndex, concat, - unusedStreamsPerPeriod); + unusedStreamsPerPeriod, periodsMissing > 0); if (!ok) { // This output Stream was not properly extended to include streams from // the new period. This is likely a bug in our algorithm, so throw an @@ -561,6 +577,11 @@ shaka.util.PeriodCombiner = class { } // for (const unusedStreams of unusedStreamsPerPeriod) for (const unusedStreams of unusedStreamsPerPeriod) { + // eslint-disable-next-line no-restricted-syntax + if (unusedStreamsPerPeriod.indexOf(unusedStreams) < + periodsMissing && unusedStreams.size == 0) { + continue; + } for (const stream of unusedStreams) { const isDummyText = stream.type == ContentType.TEXT && !stream.language; const isDummyImage = stream.type == ContentType.IMAGE && @@ -607,6 +628,7 @@ shaka.util.PeriodCombiner = class { * of the first. * @param {!Array.>} unusedStreamsPerPeriod An array of sets of * unused streams from each period. + * @param {boolean} shouldAppend shall extend existing matching streams. * * @return {!Promise.} * @@ -618,9 +640,9 @@ shaka.util.PeriodCombiner = class { */ static async extendExistingOutputStream_( outputStream, streamsPerPeriod, firstNewPeriodIndex, concat, - unusedStreamsPerPeriod) { + unusedStreamsPerPeriod, shouldAppend) { shaka.util.PeriodCombiner.findMatchesInAllPeriods_(streamsPerPeriod, - outputStream); + outputStream, shouldAppend); // This only exists where T == Stream, and this should only ever be called // on Stream types. StreamDB should not have pre-existing output streams. @@ -979,14 +1001,16 @@ shaka.util.PeriodCombiner = class { * * @param {!Array.>} streamsPerPeriod * @param {T} outputStream + * @param {boolean=} shouldAppend * * @template T * Accepts either a StreamDB or Stream type. * * @private */ - static findMatchesInAllPeriods_(streamsPerPeriod, outputStream) { - const matches = []; + static findMatchesInAllPeriods_(streamsPerPeriod, outputStream, + shouldAppend = false) { + const matches = shouldAppend ? outputStream.matchedStreams : []; for (const streams of streamsPerPeriod) { const match = shaka.util.PeriodCombiner.findBestMatchInPeriod_( streams, outputStream); From d2d91a8e904fefd077ce2cacf0409a53def64734 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 16 May 2023 09:18:00 +0100 Subject: [PATCH 05/48] remove testing hacks --- demo/common/assets.js | 28 ++++++++++++++-------------- lib/dash/mpd_utils.js | 2 +- lib/media/drm_engine.js | 19 ------------------- lib/player.js | 11 ----------- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/demo/common/assets.js b/demo/common/assets.js index c6b600ac85..48da52ec51 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -546,21 +546,21 @@ shakaAssets.testAssets = [ .setIMAContentSourceId('2528370') .setIMAVideoId('tears-of-steel') .setIMAManifestType('HLS'), - // new ShakaDemoAssetInfo( - // /* name= */ 'Tears of Steel (live, DASH, Server Side ads)', - // /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - // /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', - // /* source= */ shakaAssets.Source.SHAKA) - // .addFeature(shakaAssets.Feature.DASH) - // .addFeature(shakaAssets.Feature.MP4) - // .addFeature(shakaAssets.Feature.SUBTITLES) - // .addFeature(shakaAssets.Feature.LIVE) - // .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') - // .setIMAManifestType('DASH'), - new ShakaDemoAssetInfo( - /* name= */ 'Tears of Steel (live, DASH, MPD PATCH)', + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (live, DASH, Server Side ads)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.LIVE) + .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') + .setIMAManifestType('DASH'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (MPD PATCH)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd?foo=1', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd?patch=1', /* source= */ shakaAssets.Source.SHAKA) .addFeature(shakaAssets.Feature.DASH) .addFeature(shakaAssets.Feature.MP4) diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 03d8cb9c4c..2240791302 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -252,7 +252,7 @@ shaka.dash.MpdUtils = class { goog.asserts.assert( callback(context.representation) || context.segmentInfo, 'There must be at least one element of the given type ' + - 'or segment info defined.'); + 'or segment info defined.'); const MpdUtils = shaka.dash.MpdUtils; const XmlUtils = shaka.util.XmlUtils; diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 1874ef1655..b8bf2687ee 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -144,25 +144,6 @@ shaka.media.DrmEngine = class { this.manifestInitData_ = null; } - /** HACK FOR TESTING */ - /** - * Get the MediaKeys and active Media Key Sessions - * - * @return {*} - */ - getMediaKeysData() { - /** @type {Map} */ - const adaptedSessions = new Map(); - const activeSessions = Array.from(this.activeSessions_.entries()); - for (const [session, metadata] of activeSessions) { - adaptedSessions.set(session, metadata.initData); - } - return { - mediaKeysInstance: this.mediaKeys_, - activeSessions: adaptedSessions, - }; - } - /** @override */ destroy() { return this.destroyer_.destroy(); diff --git a/lib/player.js b/lib/player.js index 683aaa662b..37b11bff88 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1935,17 +1935,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { }); } - /** HACK FOR TESTING */ - /** - * Get the MediaKeys and active Media Key Sessions - * - * @return {*} - * @export - */ - getMediaKeysData() { - return this.drmEngine_ ? this.drmEngine_.getMediaKeysData() : null; - } - /** * This should only be called by the load graph when it is time to initialize * drmEngine. The only time this may be called is when we are attached a From b72f3df57f12db5495a6d8ecf27bbef3e8b20c9c Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 16 May 2023 15:55:43 +0100 Subject: [PATCH 06/48] remove comments --- demo/common/patch_mpd_adapter.js | 4 ---- demo/main.js | 1 - lib/dash/dash_parser.js | 4 ---- test/dash/dash_parser_manifest_unit.js | 2 +- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/demo/common/patch_mpd_adapter.js b/demo/common/patch_mpd_adapter.js index dc3ccb24a8..5178dc4a2b 100644 --- a/demo/common/patch_mpd_adapter.js +++ b/demo/common/patch_mpd_adapter.js @@ -68,7 +68,6 @@ shakaDemo.MPDPatchAdapter = class { * @param {Element} manifest */ patchMpdPreProcessor(manifest) { - console.log('****** manifest preprocessor called'); // createElementNS is being used here over createElement to preserve // the capitalisation of the first letter. const patch = document.createElementNS('', 'PatchLocation'); @@ -180,8 +179,6 @@ shakaDemo.MPDPatchAdapter = class { } } - console.log('***** processed manifest', manifest); - return additions; } @@ -267,7 +264,6 @@ shakaDemo.MPDPatchAdapter = class { * @param {string} url */ customPatchHandler(url) { - console.log('**** handling custom manifest'); const manifestUrl = this.getManifestUrlFromPatch_(url); const start = (performance && performance.now()) || Date.now(); const fetchPromise = fetch(manifestUrl).then((response) => { diff --git a/demo/main.js b/demo/main.js index 18a768b62d..b07b7d8869 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1330,7 +1330,6 @@ shakaDemo.Main = class { if (asset.features.includes( shakaAssets.Feature.ENABLE_DASH_PATCH)) { - console.log('**** doing the thing'); this.mpdPatchAdapter_ = new shakaDemo.MPDPatchAdapter(manifestUri); const patchConfig = { diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 566cba2a9a..6cb80fd34f 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -309,7 +309,6 @@ shaka.dash.DashParser = class { } if (this.patchLocationUrl_) { - console.log('********* patch', mpd); return this.processPatchManifest_(mpd); } @@ -362,9 +361,6 @@ shaka.dash.DashParser = class { manifestBaseUris = absoluteLocations; } - console.log('***** dashparser', this.config_.dash.enablePatchMPDSupport, - this.config_.dash.manifestPreprocessor); - if (this.config_.dash.enablePatchMPDSupport) { // Get patch location element const patchLocation = XmlUtils.findChildren(mpd, 'PatchLocation') diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index b261e2bdc2..43f02e6706 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -1451,7 +1451,7 @@ describe('DashParser Manifest', () => { }); }); - it('does not fail on AdaptationSets without segment info', async () => { + fit('does not fail on AdaptationSets without segment info', async () => { const manifestText = [ '', ' ', From 2fab40c4ce9dbcbb7e726a335455af302c5c49f6 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Wed, 17 May 2023 16:24:15 +0100 Subject: [PATCH 07/48] add unit tests --- demo/common/assets.js | 2 +- demo/common/message_ids.js | 3 +- demo/config.js | 2 - demo/locales/en.json | 1 + demo/locales/source.json | 2 +- lib/dash/segment_template.js | 3 +- test/dash/dash_parser_live_unit.js | 156 +++++++++++++++++++++++++ test/dash/dash_parser_manifest_unit.js | 2 +- test/demo/demo_unit.js | 3 +- test/util/xml_utils_unit.js | 21 ++++ 10 files changed, 186 insertions(+), 9 deletions(-) diff --git a/demo/common/assets.js b/demo/common/assets.js index 48da52ec51..82b9dade18 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -140,7 +140,7 @@ shakaAssets.Feature = { // Set if the asset is a MPEG-DASH manifest. DASH: shakaDemo.MessageIds.DASH, // Enable MPD patch for live DASH - ENABLE_DASH_PATCH: shakaDemo.MessageIds.ENABLE_DASH_PATCH, + DEMO_ENABLE_DASH_PATCH: shakaDemo.MessageIds.DEMO_ENABLE_DASH_PATCH, // Set if the asset is an HLS manifest. HLS: shakaDemo.MessageIds.HLS, // Set if the asset is an MSS manifest. diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index e4c8ff2f5f..9943cfa203 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -15,7 +15,7 @@ shakaDemo.MessageIds = { CAPTIONS: 'DEMO_CAPTIONS', CONTAINERLESS: 'DEMO_CONTAINERLESS', DASH: 'DEMO_DASH', - ENABLE_DASH_PATCH: 'ENABLE_DASH_PATCH', + ENABLE_DASH_PATCH: 'DEMO_ENABLE_DASH_PATCH', HIGH_DEFINITION: 'DEMO_HIGH_DEFINITION', HLS: 'DEMO_HLS', LCEVC: 'DEMO_LCEVC', @@ -195,7 +195,6 @@ shakaDemo.MessageIds = { DRM_SESSION_TYPE: 'DEMO_DRM_SESSION_TYPE', DURATION_BACKOFF: 'DEMO_DURATION_BACKOFF', ENABLED: 'DEMO_ENABLED', - ENABLE_PATCH_MPD_SUPPORT: 'DEMO_ENABLE_PATCH_MPD_SUPPORT', FAST_HALF_LIFE: 'DEMO_FAST_HALF_LIFE', FORCE_HTTPS: 'DEMO_FORCE_HTTPS', FORCE_TRANSMUX: 'DEMO_FORCE_TRANSMUX', diff --git a/demo/config.js b/demo/config.js index 566cd94b43..6fd343ce85 100644 --- a/demo/config.js +++ b/demo/config.js @@ -236,8 +236,6 @@ shakaDemo.Config = class { .addTextInput_(MessageIds.CLOCK_SYNC_URI, 'manifest.dash.clockSyncUri') .addNumberInput_(MessageIds.DEFAULT_PRESENTATION_DELAY, 'manifest.defaultPresentationDelay') - .addBoolInput_(MessageIds.ENABLE_PATCH_MPD_SUPPORT, - 'manifest.dash.enablePatchMPDSupport') .addBoolInput_(MessageIds.IGNORE_MIN_BUFFER_TIME, 'manifest.dash.ignoreMinBufferTime') .addNumberInput_(MessageIds.INITIAL_SEGMENT_LIMIT, diff --git a/demo/locales/en.json b/demo/locales/en.json index 667a14462d..472b1e8565 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -70,6 +70,7 @@ "DEMO_DISABLE_XLINK_PROCESSING": "Disable Xlink processing", "DEMO_DISPATCH_ALL_EMSG_BOXES": "Dispatch all emsg boxes", "DEMO_DOCUMENTATION": "Documentation", + "DEMO_ENABLE_DASH_PATCH": "Enable mpd patch manifest support", "DEMO_DRM_RETRY_SECTION_HEADER": "DRM Retry Parameters", "DEMO_DRM_SEARCH": "DRM", "DEMO_DRM_SECTION_HEADER": "DRM", diff --git a/demo/locales/source.json b/demo/locales/source.json index 2ddc88926a..b51910ea92 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -1075,7 +1075,7 @@ "description": "Max number of segments to be prefetched ahead of current time position.", "message": "Segment Prefetch Limit." }, - "DEMO_ENABLE_PATCH_MPD_SUPPORT": { + "DEMO_ENABLE_DASH_PATCH": { "description": "The name of a configuration value.", "message": "Enable Patch MPD support" } diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 88b8db79bb..ca26ead5df 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -72,9 +72,10 @@ shaka.dash.SegmentTemplate = class { timepoints: null, timeline: info.timeline, }; - SegmentTemplate.checkSegmentTemplateInfo_(context, info); } + SegmentTemplate.checkSegmentTemplateInfo_(context, info); + // Direct fields of context will be reassigned by the parser before // generateSegmentIndex is called. So we must make a shallow copy first, // and use that in the generateSegmentIndex callbacks. diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 93da3225c5..7393adc25c 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1558,4 +1558,160 @@ describe('DashParser Live', () => { shaka.test.ManifestParser.makeReference('s5.mp4', 8, 10, originalUri), ]); }); + describe('Patch MPD', () => { + const manifestRequest = shaka.net.NetworkingEngine.RequestType.MANIFEST; + const manifestContext = { + type: shaka.net.NetworkingEngine.AdvancedRequestType.MPD, + }; + const manifestText = [ + '', + ' dummy://bar', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + + beforeEach(() => { + const config = shaka.util.PlayerConfiguration.createDefault().manifest; + config.dash.enablePatchMPDSupport = true; + parser.configure(config); + + fakeNetEngine.setResponseText('dummy://foo', manifestText); + }); + + it('does not use Mpd.PatchLocation if not configured', async () => { + const config = shaka.util.PlayerConfiguration.createDefault().manifest; + config.dash.enablePatchMPDSupport = false; + parser.configure(config); + + const patchText = [ + '', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + }); + + it('uses Mpd.PatchLocation', async () => { + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + const patchText = [ + '', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); + }); + + it('transforms from dynamic to static', async () => { + const patchText = [ + '', + ' ', + ' static', + ' ', + ' ', + ' PT28462.033599998S', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + expect(manifest.presentationTimeline.isLive()).toBe(true); + expect(manifest.presentationTimeline.getDuration()).toBe(Infinity); + + await updateManifest(); + expect(manifest.presentationTimeline.isLive()).toBe(false); + expect(manifest.presentationTimeline.getDuration()).not.toBe(Infinity); + }); + + it('adds new period', async () => { + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + const patchText = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(1); + + await updateManifest(); + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(2); + }); + + it('adds new segments to the existing period', async () => { + const xPath = '/' + [ + 'MPD', + 'Period[@id=\'1\']', + 'AdaptationSet[@id=\'1\']', + 'Representation[@id=\'3\']', + 'SegmentTemplate', + 'SegmentTimeline', + ].join('/'); + const patchText = [ + '', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + expect(stream).toBeTruthy(); + await stream.createSegmentIndex(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ]); + + await updateManifest(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ManifestParser.makeReference('s2.mp4', 1, 2, originalUri), + ]); + }); + }); }); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 02145bf476..ef7f78c392 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -1451,7 +1451,7 @@ describe('DashParser Manifest', () => { }); }); - fit('does not fail on AdaptationSets without segment info', async () => { + it('does not fail on AdaptationSets without segment info', async () => { const manifestText = [ '', ' ', diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js index 6f078ee0fe..cf538a3c90 100644 --- a/test/demo/demo_unit.js +++ b/test/demo/demo_unit.js @@ -100,7 +100,8 @@ describe('Demo', () => { .add('streaming.parsePrftBox') .add('manifest.raiseFatalErrorOnManifestUpdateRequestFailure') .add('drm.persistentSessionOnlinePlayback') - .add('drm.persistentSessionsMetadata'); + .add('drm.persistentSessionsMetadata') + .add('manifest.dash.enablePatchMPDSupport'); /** * @param {!Object} section diff --git a/test/util/xml_utils_unit.js b/test/util/xml_utils_unit.js index d89275b543..f78d4834f7 100644 --- a/test/util/xml_utils_unit.js +++ b/test/util/xml_utils_unit.js @@ -439,6 +439,27 @@ describe('XmlUtils', () => { const doc = XmlUtils.parseXmlString(xmlString, 'Root'); expect(doc).toBeNull(); }); + + it('parseXpath', () => { + expect(XmlUtils.parseXpath('/MPD')).toEqual([{name: 'MPD', id: null}]); + expect(XmlUtils.parseXpath('/MPD/@type')) + .toEqual([{name: 'MPD', id: null}]); + + const timelinePath = '/' + [ + 'MPD', + 'Period[@id=\'6469\']', + 'AdaptationSet[@id=\'7\']', + 'SegmentTemplate', + 'SegmentTimeline', + ].join('/'); + expect(XmlUtils.parseXpath(timelinePath)).toEqual([ + {name: 'MPD', id: null}, + {name: 'Period', id: '6469'}, + {name: 'AdaptationSet', id: '7'}, + {name: 'SegmentTemplate', id: null}, + {name: 'SegmentTimeline', id: null}, + ]); + }); }); }); From 0d6e19975844c033d1e074abc37b1d60c53195c0 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Fri, 19 May 2023 10:26:49 +0100 Subject: [PATCH 08/48] rename even --- demo/common/assets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/common/assets.js b/demo/common/assets.js index 82b9dade18..48da52ec51 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -140,7 +140,7 @@ shakaAssets.Feature = { // Set if the asset is a MPEG-DASH manifest. DASH: shakaDemo.MessageIds.DASH, // Enable MPD patch for live DASH - DEMO_ENABLE_DASH_PATCH: shakaDemo.MessageIds.DEMO_ENABLE_DASH_PATCH, + ENABLE_DASH_PATCH: shakaDemo.MessageIds.ENABLE_DASH_PATCH, // Set if the asset is an HLS manifest. HLS: shakaDemo.MessageIds.HLS, // Set if the asset is an MSS manifest. From 19978059b14eea260084d3b5c87e2a76a0dd70c0 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Fri, 19 May 2023 15:15:31 +0100 Subject: [PATCH 09/48] wipe segmentinfo when not patch --- lib/dash/segment_template.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index ca26ead5df..29a8325145 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -44,6 +44,9 @@ shaka.dash.SegmentTemplate = class { goog.asserts.assert(context.representation.segmentTemplate || context.segmentInfo, 'Should only be called with SegmentTemplate ' + 'or segment info defined'); + if (!isPatchUpdate) { + context.segmentInfo = null; + } const MpdUtils = shaka.dash.MpdUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; const TimelineSegmentIndex = shaka.dash.TimelineSegmentIndex; From 2d4518a94bf01b7c7083fc9094c64982993db951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 29 May 2023 10:15:07 +0200 Subject: [PATCH 10/48] fix jsdoc formatting --- demo/common/patch_mpd_adapter.js | 28 ++---- demo/main.js | 1 - lib/dash/dash_parser.js | 164 +++++++++++++++---------------- lib/util/xml_utils.js | 10 +- 4 files changed, 93 insertions(+), 110 deletions(-) diff --git a/demo/common/patch_mpd_adapter.js b/demo/common/patch_mpd_adapter.js index 5178dc4a2b..2f4524e80f 100644 --- a/demo/common/patch_mpd_adapter.js +++ b/demo/common/patch_mpd_adapter.js @@ -18,12 +18,12 @@ const ParserMode = { }; /** -* @typedef {{ -* type: string, -* xpathLocation: string, -* element: Node -* }} -*/ + * @typedef {{ + * type: string, + * xpathLocation: string, + * element: Node + * }} + */ shakaDemo.MPDPatchAdapter.Patch; /** @@ -79,7 +79,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} manifest * @return {Array} * @private @@ -183,7 +182,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Array} additions * @param {Element} segmentTemplate * @param {boolean} addingCompletePeriod @@ -299,7 +297,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} manifest * @param {Array} additions * @return {Element} @@ -316,7 +313,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} originalManifest * @param {Element} patchManifest * @private @@ -339,7 +335,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} patchElement * @param {Array} additions * @private @@ -363,7 +358,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {string} url * @return {string} * @private @@ -377,7 +371,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} adaptationSet * @param {Element} representation * @return {string} @@ -395,7 +388,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {?Array} * @private @@ -405,7 +397,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {Array} * @private @@ -415,7 +406,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {Array} * @private @@ -425,7 +415,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {Element} * @private @@ -437,7 +426,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {Element} * @private @@ -449,7 +437,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} element * @return {Array} * @private @@ -459,7 +446,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} period * @return {string} * @private @@ -469,7 +455,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} period * @param {Element} adaptationSet * @param {Element=} representation @@ -483,7 +468,6 @@ shakaDemo.MPDPatchAdapter = class { } /** - * * @param {Element} period * @param {Element} adaptationSet * @param {Element=} representation diff --git a/demo/main.js b/demo/main.js index b07b7d8869..6c186439fd 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1312,7 +1312,6 @@ shakaDemo.Main = class { } await this.drmConfiguration_(asset); - this.controls_.getCastProxy().setAppData({'asset': asset}); // Finally, the asset can be loaded. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 6a0c7f3e99..d7f806ca98 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -60,10 +60,10 @@ shaka.dash.DashParser = class { this.patchLocationUrl_ = null; /** - * A context of the living manifest used for processing - * Patch MPD's - * @private {!shaka.dash.DashParser.PatchContext} - */ + * A context of the living manifest used for processing + * Patch MPD's + * @private {!shaka.dash.DashParser.PatchContext} + */ this.manifestPatchContext_ = { type: '', profiles: [], @@ -77,33 +77,33 @@ shaka.dash.DashParser = class { /** - * A map of IDs to Stream objects. - * ID: Period@id,AdaptationSet@id,@Representation@id - * e.g.: '1,5,23' - * @private {!Object.} - */ + * A map of IDs to Stream objects. + * ID: Period@id,AdaptationSet@id,@Representation@id + * e.g.: '1,5,23' + * @private {!Object.} + */ this.streamMap_ = {}; /** - * A map of period ids to their durations - * @private {!Object.} - */ + * A map of period ids to their durations + * @private {!Object.} + */ this.periodDurations_ = {}; /** @private {shaka.util.PeriodCombiner} */ this.periodCombiner_ = new shaka.util.PeriodCombiner(); /** - * The update period in seconds, or 0 for no updates. - * @private {number} - */ + * The update period in seconds, or 0 for no updates. + * @private {number} + */ this.updatePeriod_ = 0; /** - * An ewma that tracks how long updates take. - * This is to mitigate issues caused by slow parsing on embedded devices. - * @private {!shaka.abr.Ewma} - */ + * An ewma that tracks how long updates take. + * This is to mitigate issues caused by slow parsing on embedded devices. + * @private {!shaka.abr.Ewma} + */ this.averageUpdateDuration_ = new shaka.abr.Ewma(5); /** @private {shaka.util.Timer} */ @@ -115,22 +115,22 @@ shaka.dash.DashParser = class { this.operationManager_ = new shaka.util.OperationManager(); /** - * Largest period start time seen. - * @private {?number} - */ + * Largest period start time seen. + * @private {?number} + */ this.largestPeriodStartTime_ = null; /** - * Period IDs seen in previous manifest. - * @private {!Array.} - */ + * Period IDs seen in previous manifest. + * @private {!Array.} + */ this.lastManifestUpdatePeriodIds_ = []; /** - * The minimum of the availabilityTimeOffset values among the adaptation - * sets. - * @private {number} - */ + * The minimum of the availabilityTimeOffset values among the adaptation + * sets. + * @private {number} + */ this.minTotalAvailabilityTimeOffset_ = Infinity; /** @private {boolean} */ @@ -662,12 +662,12 @@ shaka.dash.DashParser = class { } /** - * Handles manifest type changes, this transition is expected to be - * "dyanmic" to "static". - * - * @param {!string} mpdType - * @private - */ + * Handles manifest type changes, this transition is expected to be + * "dyanmic" to "static". + * + * @param {!string} mpdType + * @private + */ parsePatchMpdTypeChange_(mpdType) { this.manifest_.presentationTimeline.setStatic(mpdType == 'static'); this.manifestPatchContext_.type = mpdType; @@ -677,9 +677,9 @@ shaka.dash.DashParser = class { } /** - * @param {string} durationString - * @private - */ + * @param {string} durationString + * @private + */ parsePatchMediaPresentationDurationChange_(durationString) { const duration = shaka.util.XmlUtils.parseDuration(durationString); if (duration == null) { @@ -692,11 +692,11 @@ shaka.dash.DashParser = class { } /** - * Ingests a full MPD period element from a patch update - * - * @param {!Element} periods - * @private - */ + * Ingests a full MPD period element from a patch update + * + * @param {!Element} periods + * @private + */ parsePatchPeriod_(periods) { this.contextCache_.clear(); @@ -725,12 +725,12 @@ shaka.dash.DashParser = class { } /** - * Ingests Path MPD segments. - * - * @param {!Array} paths - * @param {Array} segments - * @private - */ + * Ingests Path MPD segments. + * + * @param {!Array} paths + * @param {Array} segments + * @private + */ parsePatchSegment_(paths, segments) { let periodId = ''; let adaptationSetId = ''; @@ -873,20 +873,20 @@ shaka.dash.DashParser = class { } /** - * This is to improve robustness when the player observes manifest with - * past periods that are inconsistent to previous ones. - * - * This may happen when a CDN or proxy server switches its upstream from - * one encoder to another redundant encoder. - * - * Skip periods that match all of the following criteria: - * - Start time is earlier than latest period start time ever seen - * - Period ID is never seen in the previous manifest - * - Not the last period in the manifest - * - * Periods that meet the aforementioned criteria are considered invalid - * and should be safe to discard. - */ + * This is to improve robustness when the player observes manifest with + * past periods that are inconsistent to previous ones. + * + * This may happen when a CDN or proxy server switches its upstream from + * one encoder to another redundant encoder. + * + * Skip periods that match all of the following criteria: + * - Start time is earlier than latest period start time ever seen + * - Period ID is never seen in the previous manifest + * - Not the last period in the manifest + * + * Periods that meet the aforementioned criteria are considered invalid + * and should be safe to discard. + */ if (this.largestPeriodStartTime_ !== null && periodId !== null && start !== null && @@ -2227,29 +2227,29 @@ shaka.dash.DashParser = class { }; /** -* @typedef {{ - * type: string, - * mediaPresentationDuration: ?number, - * profiles: !Array., - * availabilityTimeOffset: number, - * baseUris: !Array. - * }} - */ + * @typedef {{ + * type: string, + * mediaPresentationDuration: ?number, + * profiles: !Array., + * availabilityTimeOffset: number, + * baseUris: !Array. + * }} + */ shaka.dash.DashParser.PatchContext; /** - * @typedef {{ - * timescale: number, - * duration: number, - * startNumber: number, - * presentationTimeOffset: number, - * media: ?string, - * index: ?string, - * timepoints: Array, - * timeline: Array - * }} - */ + * @typedef {{ + * timescale: number, + * duration: number, + * startNumber: number, + * presentationTimeOffset: number, + * media: ?string, + * index: ?string, + * timepoints: Array, + * timeline: Array + * }} + */ shaka.dash.DashParser.SegmentInfo; /** diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js index c46c8495e9..0181c2ab67 100644 --- a/lib/util/xml_utils.js +++ b/lib/util/xml_utils.js @@ -482,9 +482,9 @@ shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => { /** * @typedef {{ -* name: !string, -* id: string -* }} -* @export -*/ + * name: !string, + * id: string + * }} + * @export + */ shaka.util.XmlUtils.PathNode; From 4c93fbcac2b0b22030c6f050179283b727f05658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 29 May 2023 10:17:21 +0200 Subject: [PATCH 11/48] fix typedef formatting --- demo/common/patch_mpd_adapter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/common/patch_mpd_adapter.js b/demo/common/patch_mpd_adapter.js index 2f4524e80f..62fa4da8e8 100644 --- a/demo/common/patch_mpd_adapter.js +++ b/demo/common/patch_mpd_adapter.js @@ -19,9 +19,9 @@ const ParserMode = { /** * @typedef {{ - * type: string, - * xpathLocation: string, - * element: Node + * type: string, + * xpathLocation: string, + * element: Node * }} */ shakaDemo.MPDPatchAdapter.Patch; From cff1c1a79e685a75aa07eff7686c2e8908b42f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 29 May 2023 10:18:44 +0200 Subject: [PATCH 12/48] fix typedef formatting 2 --- lib/util/xml_utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js index 0181c2ab67..b23f68b454 100644 --- a/lib/util/xml_utils.js +++ b/lib/util/xml_utils.js @@ -482,8 +482,8 @@ shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => { /** * @typedef {{ - * name: !string, - * id: string + * name: !string, + * id: string * }} * @export */ From ce92c01f3a18393c3cef0dfe2062a64ff236da4d Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 20 Jun 2023 18:24:19 +0100 Subject: [PATCH 13/48] remove patch manifest adapter --- build/conformance.textproto | 2 - demo/common/assets.js | 14 - demo/common/message_ids.js | 1 - demo/common/patch_mpd_adapter.js | 486 ------------------------------- demo/locales/en.json | 1 - demo/locales/source.json | 4 - demo/main.js | 24 -- 7 files changed, 532 deletions(-) delete mode 100644 demo/common/patch_mpd_adapter.js diff --git a/build/conformance.textproto b/build/conformance.textproto index 5b909e979b..a2ffb71ef2 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -320,7 +320,6 @@ requirement: { "use shaka.test.Util.fetch or NetworkingEngine.request " "instead." whitelist_regexp: "lib/net/http_fetch_plugin.js" - whitelist_regexp: "demo/common/patch_mpd_adapter.js" } # Disallow the use of generators, which are a major performance issue. See @@ -376,6 +375,5 @@ requirement: { "use shaka.util.XmlUtils.parseXmlString instead." whitelist_regexp: "lib/util/xml_utils.js" whitelist_regexp: "test/" - whitelist_regexp: "demo/common/patch_mpd_adapter.js" } diff --git a/demo/common/assets.js b/demo/common/assets.js index 48da52ec51..35537d7372 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -139,8 +139,6 @@ shakaAssets.Feature = { // Set if the asset is a MPEG-DASH manifest. DASH: shakaDemo.MessageIds.DASH, - // Enable MPD patch for live DASH - ENABLE_DASH_PATCH: shakaDemo.MessageIds.ENABLE_DASH_PATCH, // Set if the asset is an HLS manifest. HLS: shakaDemo.MessageIds.HLS, // Set if the asset is an MSS manifest. @@ -557,18 +555,6 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.LIVE) .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') .setIMAManifestType('DASH'), - new ShakaDemoAssetInfo( - /* name= */ 'Tears of Steel (MPD PATCH)', - /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd?patch=1', - /* source= */ shakaAssets.Source.SHAKA) - .addFeature(shakaAssets.Feature.DASH) - .addFeature(shakaAssets.Feature.MP4) - .addFeature(shakaAssets.Feature.SUBTITLES) - .addFeature(shakaAssets.Feature.LIVE) - .addFeature(shakaAssets.Feature.ENABLE_DASH_PATCH) - .setIMAAssetKey('PSzZMzAkSXCmlJOWDmRj8Q') - .setIMAManifestType('DASH'), new ShakaDemoAssetInfo( /* name= */ 'Tears of Steel (multicodec, surround + stereo)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index 9943cfa203..d9adce6065 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -15,7 +15,6 @@ shakaDemo.MessageIds = { CAPTIONS: 'DEMO_CAPTIONS', CONTAINERLESS: 'DEMO_CONTAINERLESS', DASH: 'DEMO_DASH', - ENABLE_DASH_PATCH: 'DEMO_ENABLE_DASH_PATCH', HIGH_DEFINITION: 'DEMO_HIGH_DEFINITION', HLS: 'DEMO_HLS', LCEVC: 'DEMO_LCEVC', diff --git a/demo/common/patch_mpd_adapter.js b/demo/common/patch_mpd_adapter.js deleted file mode 100644 index 62fa4da8e8..0000000000 --- a/demo/common/patch_mpd_adapter.js +++ /dev/null @@ -1,486 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shakaDemo.MPDPatchAdapter'); - -const PatchType = { - ADD: 'add', - REPLACE: 'replace', - DELETE: 'delete', -}; - -const ParserMode = { - INITIAL_FULL_MPD: 'INITIAL_FULL_MPD', - PATCH_MPD: 'PATCH_MPD', -}; - -/** - * @typedef {{ - * type: string, - * xpathLocation: string, - * element: Node - * }} - */ -shakaDemo.MPDPatchAdapter.Patch; - -/** - * Finds child XML elements. - * @param {Node} elem The parent XML element. - * @param {string} name The child XML element's tag name. - * @return {!Array.} The child XML elements. - */ -function findChildren(elem, name) { - const found = []; - if (!elem) { - return []; - } - for (const child of elem.childNodes) { - if (child instanceof Element && child.tagName == name) { - found.push(child); - } - } - return found; -} - -shakaDemo.MPDPatchAdapter = class { - /** - * @param {string} url - */ - constructor(url) { - /** @type {string} */ - this.manifestUrl_ = url; - /** @type {Set} */ - this.periodContext_ = new Set(); - /** @type {Map} */ - this.segmentContext_ = new Map(); - /** @type {string|undefined} */ - this.manifestType_ = undefined; - /** @type {?string} */ - this.mediaPresentationDuration_ = null; - /** @type {string} */ - this.parserMode_ = ParserMode.INITIAL_FULL_MPD; - } - - /** - * @param {Element} manifest - */ - patchMpdPreProcessor(manifest) { - // createElementNS is being used here over createElement to preserve - // the capitalisation of the first letter. - const patch = document.createElementNS('', 'PatchLocation'); - // eslint-disable-next-line no-useless-concat - patch['inner'+'HTML'] = this.getPatchManifestUrl_(); - manifest.appendChild(patch); - this.processManifest_(manifest); - this.parserMode_ = ParserMode.PATCH_MPD; - } - - /** - * @param {Element} manifest - * @return {Array} - * @private - */ - processManifest_(manifest) { - /** @type {Array} */ - const additions = []; - - const mediaPresentationDuration = - manifest.getAttribute('mediaPresentationDuration'); - if (this.mediaPresentationDuration_ != mediaPresentationDuration) { - if (this.parserMode_ == ParserMode.PATCH_MPD) { - additions.push({ - xpathLocation: '/MPD/@mediaPresentationDuration', - type: this.mediaPresentationDuration_ ? - PatchType.REPLACE : PatchType.ADD, - element: document.createTextNode(mediaPresentationDuration), - }); - } - this.mediaPresentationDuration_ = mediaPresentationDuration; - } - - const manifestType = manifest.getAttribute('type') || ''; - if (this.manifestType_ != manifestType) { - this.manifestType_ = manifestType; - if (this.parserMode_ == ParserMode.PATCH_MPD) { - additions.push({ - xpathLocation: '/MPD/@type', - type: PatchType.REPLACE, - element: document.createTextNode(manifestType), - }); - } - } - - const periods = this.getPeriods_(manifest); - const lastPeriod = periods[periods.length - 1]; - - // We're doing backward loop as new periods and segments should appear - // at the end of manifest. - for (let i = periods.length - 1; i >= 0; i--) { - const period = periods[i]; - - // On the initial pass we will only look at the last period - if (this.parserMode_ === ParserMode.INITIAL_FULL_MPD && - period !== lastPeriod) { - const periodCacheKey = this.getCacheKeyForPeriod_(period); - this.periodContext_.add(periodCacheKey); - continue; - } - - const periodCacheKey = this.getCacheKeyForPeriod_(period); - const addingCompletePeriod = !this.periodContext_.has(periodCacheKey); - if (addingCompletePeriod) { - this.periodContext_.add(periodCacheKey); - - if (this.parserMode_ === ParserMode.PATCH_MPD) { - additions.unshift({ - xpathLocation: '/MPD/', - element: period, - type: PatchType.ADD, - }); - } - } - - for (const adaptationSet of this.getAdaptationSets_(period) || []) { - const representations = this.getRepresentations_(adaptationSet); - - if (!adaptationSet.hasAttribute('id')) { - const adaptationSetId = this.generateAdaptationSetId_( - adaptationSet, representations[representations.length - 1]); - adaptationSet.setAttribute('id', adaptationSetId); - } - - // This is a peacock optimisation as we assume if there - // is a segment timeline this will be identical in all - // child representations. - const segmentTemplate = this.getSegmentTemplate_(adaptationSet); - if (segmentTemplate) { - this.processSegmentTemplate_(additions, segmentTemplate, - addingCompletePeriod, period, adaptationSet); - } else { - for (const representation of representations || []) { - const segmentTemplate = this.getSegmentTemplate_(representation); - if (segmentTemplate) { - this.processSegmentTemplate_(additions, segmentTemplate, - addingCompletePeriod, period, adaptationSet, representation); - } - } - } - } - - // If we're not adding complete period in this iteration, it's clear - // we already added all new periods and new segments to the last - // existing period. So, we can easily break parsing here. - if (!addingCompletePeriod) { - break; - } - } - - return additions; - } - - /** - * @param {Array} additions - * @param {Element} segmentTemplate - * @param {boolean} addingCompletePeriod - * @param {Element} period - * @param {Element} adaptationSet - * @param {Element=} representation - * @private - */ - processSegmentTemplate_( - additions, - segmentTemplate, - addingCompletePeriod, - period, - adaptationSet, - representation, - ) { - const cacheKey = this.getCacheKeyForSegmentTimeline_( - period, adaptationSet, representation); - /** @type {Array} */ - const segmentPatches = []; - - const segmentTimeline = this.getSegmentTimeline_(segmentTemplate); - if (segmentTimeline) { - const segmentTags = this.getSegment_(segmentTimeline); - const lastSegmentSeen = this.segmentContext_.get(cacheKey) || 0; - let lastEndTime = 0; - - for (const segmentTag of segmentTags || []) { - let additionalSegments = 0; - /** @type {number} */ - let firstNewSegmentStartTime; - const t = Number(segmentTag.getAttribute('t') || lastEndTime); - const d = Number(segmentTag.getAttribute('d')); - const r = Number(segmentTag.getAttribute('r') || 0); - - let startTime = t; - - for (let j = 0; j <= r; ++j) { - const endTime = startTime + d; - - if (endTime > lastSegmentSeen) { - this.segmentContext_.set(cacheKey, endTime); - additionalSegments++; - if (!firstNewSegmentStartTime) { - firstNewSegmentStartTime = startTime; - } - } - startTime = endTime; - lastEndTime = endTime; - } - if (additionalSegments > 0 && !addingCompletePeriod) { - // createElementNS is being used here over createElement to preserve - // the capitalisation of the first letter. - const newSegment = document.createElementNS('', 'S'); - newSegment.setAttribute('d', d.toString()); - newSegment.setAttribute('t', firstNewSegmentStartTime.toString()); - if (additionalSegments > 1) { - // minus one repeat for the original - newSegment.setAttribute('r', (additionalSegments - 1).toString()); - } - - if (this.parserMode_ === ParserMode.PATCH_MPD) { - segmentPatches.push({ - xpathLocation: this.getSegmentXpathLocation_( - period, adaptationSet, representation), - element: newSegment, - type: PatchType.ADD, - }); - } - } - } - } - additions.unshift(...segmentPatches.values()); - } - - - /** - * @param {string} url - */ - customPatchHandler(url) { - const manifestUrl = this.getManifestUrlFromPatch_(url); - const start = (performance && performance.now()) || Date.now(); - const fetchPromise = fetch(manifestUrl).then((response) => { - const end = (performance && performance.now()) || Date.now(); - const parser = new DOMParser(); - return response.text().then((body) => { - const manifest = parser.parseFromString(body, 'text/xml'); - const additions = this.processManifest_(manifest.documentElement); - const patchManifest = this.generatePatch_( - manifest.documentElement, additions); - - const buffer = shaka.util.StringUtils.toUTF8( - // eslint-disable-next-line no-useless-concat - patchManifest['outer'+'HTML']); - - const data = { - timeMs: end - start, - fromCache: false, - data: buffer, - }; - return data; - }); - }); - - - /** @type {!shaka.util.AbortableOperation} */ - const op = new shaka.util.AbortableOperation( - fetchPromise, () => { - return Promise.resolve(); - }); - - return op; - } - - /** - * @param {Element} manifest - * @param {Array} additions - * @return {Element} - * @private - */ - generatePatch_(manifest, additions) { - const patch = document.createElementNS('', 'Patch'); - patch.setAttribute('mpdId', 'channel'); - patch.setAttribute('publishTime', - manifest.getAttribute('publishTime') || ''); - this.appendXmlNamespaces_(manifest, patch); - this.generateAdditions_(patch, additions); - return patch; - } - - /** - * @param {Element} originalManifest - * @param {Element} patchManifest - * @private - */ - appendXmlNamespaces_(originalManifest, patchManifest) { - for (const node of originalManifest.attributes) { - if (node.name.includes('xmlns')) { - if (node.name === 'xmlns') { - const namespaces = node.value.split(':'); - namespaces.push('mpdpatch', '2020'); - patchManifest.setAttribute('xmlns', namespaces.join(':')); - } else { - patchManifest.setAttribute(node.name, - originalManifest.getAttribute(node.name)); - } - } - } - patchManifest.setAttribute('xmlns:p', - 'urn:ietf:params:xml:schema:patchops'); - } - - /** - * @param {Element} patchElement - * @param {Array} additions - * @private - */ - generateAdditions_(patchElement, additions) { - for (const patch of additions.values()) { - const patchChange = document.createElementNS('p', `p:${patch.type}`); - patchChange.setAttribute('sel', patch.xpathLocation); - if (patch.element) { - patchChange.appendChild(patch.element); - } - patchElement.appendChild(patchChange); - } - } - - /** @private */ - getPatchManifestUrl_() { - // This method replaces the protocol 'https' with 'patch' - // so it is handled with via the patch network scheme - return this.manifestUrl_.replace('https', 'patch'); - } - - /** - * @param {string} url - * @return {string} - * @private - */ - getManifestUrlFromPatch_(url) { - // This is intentionally different from the method above - // as we want to request the smaller 2 minute window manifest - // and not the full window. - // https://github.com/sky-uk/core-video-sdk-js/pull/5428#discussion_r1038259178 - return url.replace('patch', 'https'); - } - - /** - * @param {Element} adaptationSet - * @param {Element} representation - * @return {string} - * @private - */ - generateAdaptationSetId_(adaptationSet, representation) { - const mimeType = adaptationSet.getAttribute('mimeType'); - const lang = adaptationSet.getAttribute('lang'); - let adaptationSetId = `${mimeType}#${lang}`; - if (mimeType === 'audio/mp4') { - const representationId = representation.getAttribute('id'); - adaptationSetId += `#${representationId}`; - } - return adaptationSetId; - } - - /** - * @param {Element} element - * @return {?Array} - * @private - */ - getPeriods_(element) { - return findChildren(element, 'Period'); - } - - /** - * @param {Element} element - * @return {Array} - * @private - */ - getRepresentations_(element) { - return findChildren(element, 'Representation'); - } - - /** - * @param {Element} element - * @return {Array} - * @private - */ - getAdaptationSets_(element) { - return findChildren(element, 'AdaptationSet'); - } - - /** - * @param {Element} element - * @return {Element} - * @private - */ - getSegmentTemplate_(element) { - const segmentTemplates = findChildren( - element, 'SegmentTemplate'); - return segmentTemplates.length ? segmentTemplates[0] : null; - } - - /** - * @param {Element} element - * @return {Element} - * @private - */ - getSegmentTimeline_(element) { - const segmentTimeline = findChildren( - element, 'SegmentTimeline'); - return segmentTimeline.length ? segmentTimeline[0] : null; - } - - /** - * @param {Element} element - * @return {Array} - * @private - */ - getSegment_(element) { - return findChildren(element, 'S'); - } - - /** - * @param {Element} period - * @return {string} - * @private - */ - getCacheKeyForPeriod_(period) { - return `P_${period.getAttribute('id')}`; - } - - /** - * @param {Element} period - * @param {Element} adaptationSet - * @param {Element=} representation - * @return {string} - * @private - */ - getCacheKeyForSegmentTimeline_(period, adaptationSet, representation) { - return `${this.getCacheKeyForPeriod_(period)}_AS_${ - adaptationSet.getAttribute('id')}_R_${ - (representation && representation.getAttribute('id')) || 'xx'}`; - } - - /** - * @param {Element} period - * @param {Element} adaptationSet - * @param {Element=} representation - * @return {string} - * @private - */ - getSegmentXpathLocation_(period, adaptationSet, representation) { - const representationCriteria = representation ? - `/Representation[@id='${representation.getAttribute('id')}']` : ''; - - return `/MPD/Period[@id='${period.getAttribute('id') - }']/AdaptationSet[@id='${adaptationSet.getAttribute( - 'id', - )}']${representationCriteria}/SegmentTemplate/SegmentTimeline`; - } -}; diff --git a/demo/locales/en.json b/demo/locales/en.json index 472b1e8565..667a14462d 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -70,7 +70,6 @@ "DEMO_DISABLE_XLINK_PROCESSING": "Disable Xlink processing", "DEMO_DISPATCH_ALL_EMSG_BOXES": "Dispatch all emsg boxes", "DEMO_DOCUMENTATION": "Documentation", - "DEMO_ENABLE_DASH_PATCH": "Enable mpd patch manifest support", "DEMO_DRM_RETRY_SECTION_HEADER": "DRM Retry Parameters", "DEMO_DRM_SEARCH": "DRM", "DEMO_DRM_SECTION_HEADER": "DRM", diff --git a/demo/locales/source.json b/demo/locales/source.json index b51910ea92..4ea488cca0 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -1074,9 +1074,5 @@ "DEMO_SEGMENT_PREFETCH_LIMIT": { "description": "Max number of segments to be prefetched ahead of current time position.", "message": "Segment Prefetch Limit." - }, - "DEMO_ENABLE_DASH_PATCH": { - "description": "The name of a configuration value.", - "message": "Enable Patch MPD support" } } diff --git a/demo/main.js b/demo/main.js index b414a4513a..5e902abcc6 100644 --- a/demo/main.js +++ b/demo/main.js @@ -14,7 +14,6 @@ goog.require('shakaDemo.MessageIds'); goog.require('shakaDemo.Utils'); goog.require('shakaDemo.Visualizer'); goog.require('shakaDemo.VisualizerButton'); -goog.require('shakaDemo.MPDPatchAdapter'); /** * Shaka Player demo, main section. @@ -73,9 +72,6 @@ shakaDemo.Main = class { /** @private {boolean} */ this.noInput_ = false; - /** @private {shakaDemo.MPDPatchAdapter} */ - this.mpdPatchAdapter_ = null; - /** @private {!HTMLAnchorElement} */ this.errorDisplayLink_ = /** @type {!HTMLAnchorElement} */( document.getElementById('error-display-link')); @@ -1326,26 +1322,6 @@ shakaDemo.Main = class { manifestUri = await this.getManifestUriFromAdManager_(asset); } - if (asset.features.includes( - shakaAssets.Feature.ENABLE_DASH_PATCH)) { - this.mpdPatchAdapter_ = - new shakaDemo.MPDPatchAdapter(manifestUri); - const patchConfig = { - manifest: { - dash: { - enablePatchMPDSupport: true, - manifestPreprocessor: - // eslint-disable-next-line - (e) => this.mpdPatchAdapter_.patchMpdPreProcessor(e), - }, - }, - }; - - shaka.net.NetworkingEngine.registerScheme('patch', - (url) => this.mpdPatchAdapter_.customPatchHandler(url)); - this.player_.configure(patchConfig); - } - await this.player_.load( manifestUri, /* startTime= */ null, From 7ed2820830c2e0901b2a0d24770fd4bf4c5bee17 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 20 Jun 2023 18:25:42 +0100 Subject: [PATCH 14/48] remove whitespace --- demo/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/main.js b/demo/main.js index 5e902abcc6..6db7f232f7 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1321,7 +1321,6 @@ shakaDemo.Main = class { if (asset.imaAssetKey || (asset.imaContentSrcId && asset.imaVideoId)) { manifestUri = await this.getManifestUriFromAdManager_(asset); } - await this.player_.load( manifestUri, /* startTime= */ null, From 5ec3acffcbf157e2d8cce2abbf007f7e4d62232b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 25 Jul 2023 09:25:05 +0200 Subject: [PATCH 15/48] mpd patch: stop updates after transition to static --- lib/dash/dash_parser.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 85d5171ac3..9e1e1787e0 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -688,6 +688,10 @@ shaka.dash.DashParser = class { for (const context of this.contextCache_.values()) { context.dynamic = mpdType == 'dynamic'; } + if (mpdType == 'static') { + // Manifest is no longer dynamic, so stop live updates. + this.updatePeriod_ = -1; + } } /** From fa32efd1542db84840c987f6ad5d7233994a6558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 2 Aug 2023 09:05:26 +0200 Subject: [PATCH 16/48] enhance dynamic->static test --- test/dash/dash_parser_live_unit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 8e0ff07842..6acc27206f 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1646,9 +1646,13 @@ describe('DashParser Live', () => { expect(manifest.presentationTimeline.isLive()).toBe(true); expect(manifest.presentationTimeline.getDuration()).toBe(Infinity); + /** @type {!jasmine.Spy} */ + const tickAfter = updateTickSpy(); await updateManifest(); expect(manifest.presentationTimeline.isLive()).toBe(false); expect(manifest.presentationTimeline.getDuration()).not.toBe(Infinity); + // should stop updates after transition to static + expect(tickAfter).not.toHaveBeenCalled(); }); it('adds new period', async () => { From ec4c73362443a058de9c41d9246af6a43bf672fc Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Wed, 4 Oct 2023 17:34:48 +0100 Subject: [PATCH 17/48] addressing pr comments --- lib/dash/dash_parser.js | 100 ++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 36e4e7f7ed..8e9ba2e265 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -72,10 +72,16 @@ shaka.dash.DashParser = class { baseUris: [], }; - /** @private {!Map} */ + /** + * This is a cache is used the store a snapshot of the context + * object which is built up throughout node traversal to maintain + * a current state. This data needs to be preserved for parsing + * patches. + * The key is a combination period and representation id's. + * @private {!Map} + */ this.contextCache_ = new Map(); - /** * A map of IDs to Stream objects. * ID: Period@id,AdaptationSet@id,@Representation@id @@ -539,8 +545,6 @@ shaka.dash.DashParser = class { presentationTimeline.assertIsValid(); } - await this.periodCombiner_.combinePeriods(periods, context.dynamic); - // Set minBufferTime to 0 for low-latency DASH live stream to achieve the // best latency if (this.lowLatencyMode_) { @@ -588,8 +592,14 @@ shaka.dash.DashParser = class { // maintain consistency from here on. presentationTimeline.lockStartTime(); } else { - await this.postPeriodProcessing_(periodsAndDuration.periods, false); + await this.postPeriodProcessing_( + periodsAndDuration.periods, /* isPatchUpdate= */ false); } + + // Add text streams to correspond to closed captions. This happens right + // after period combining, while we still have a direct reference, so that + // any new streams will appear in the period combiner. + this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_); } /** @@ -669,7 +679,7 @@ shaka.dash.DashParser = class { } if (newPeriods.length) { - await this.postPeriodProcessing_(newPeriods, true); + await this.postPeriodProcessing_(newPeriods, /* isPatchUpdate= */ true); } if (this.manifestPatchContext_.type == 'static') { const duration = this.manifestPatchContext_.mediaPresentationDuration; @@ -1766,32 +1776,36 @@ shaka.dash.DashParser = class { for (const k of Object.keys(context)) { if (['period', 'adaptationSet', 'representation'].includes(k)) { + /** @type {shaka.dash.DashParser.InheritanceFrame} */ + const frameRef = context[k]; contextClone[k] = { segmentBase: null, segmentList: null, segmentTemplate: null, - baseUris: context[k].baseUris, - width: context[k].width, - height: context[k].height, - contentType: context[k].contentType, - mimeType: context[k].mimeType, - lang: context[k].lang, - codecs: context[k].codecs, - frameRate: context[k].frameRate, - pixelAspectRatio: context[k].pixelAspectRatio, - emsgSchemeIdUris: context[k].emsgSchemeIdUris, - id: context[k].id, - numChannels: context[k].numChannels, - audioSamplingRate: context[k].audioSamplingRate, - availabilityTimeOffset: context[k].availabilityTimeOffset, - initialization: context[k].initialization, + baseUris: frameRef.baseUris, + width: frameRef.width, + height: frameRef.height, + contentType: frameRef.contentType, + mimeType: frameRef.mimeType, + language: frameRef.language, + codecs: frameRef.codecs, + frameRate: frameRef.frameRate, + pixelAspectRatio: frameRef.pixelAspectRatio, + emsgSchemeIdUris: frameRef.emsgSchemeIdUris, + id: frameRef.id, + numChannels: frameRef.numChannels, + audioSamplingRate: frameRef.audioSamplingRate, + availabilityTimeOffset: frameRef.availabilityTimeOffset, + initialization: frameRef.initialization, }; } else if (k == 'periodInfo') { + /** @type {shaka.dash.DashParser.PeriodInfo} */ + const frameRef = context[k]; contextClone[k] = { - start: context[k].start, - duration: context[k].duration, + start: frameRef.start, + duration: frameRef.duration, node: null, - isLastPeriod: context[k].isLastPeriod, + isLastPeriod: frameRef.isLastPeriod, }; } else { contextClone[k] = context[k]; @@ -2371,6 +2385,18 @@ shaka.dash.DashParser = class { * availabilityTimeOffset: number, * baseUris: !Array. * }} + * + * @property {string} type + * Specifies the type of the dash manifest i.e. "static" + * @property {?number} mediaPresentationDuration + * Media presentation duration, or null if unknown. + * @property {!Array.} profiles + * Profiles of DASH are defined to enable interoperability and the + * signaling of the use of features. + * @property {number} availabilityTimeOffset + * Specifies the total availabilityTimeOffset of the segment. + * @property {!Array.} baseUris + * An array of absolute base URIs. */ shaka.dash.DashParser.PatchContext; @@ -2386,9 +2412,33 @@ shaka.dash.DashParser.PatchContext; * timepoints: Array, * timeline: Array * }} + * + * @description + * A collection of elements and properties which are inherited across levels + * of a DASH manifest. + * + * @property {number} timescale + * Specifies timescale for this Representation. + * @property {number} duration + * Specifies the duration of each Segment in units of a time. + * @property {number} startNumber + * Specifies the number of the first segment in the Period assigned + * to a Representation. + * @property {number} presentationTimeOffset + * Specifies the presentation time offset of the Representation relative + * to the start of the Period. + * @property {?string} media + * The template for the Media Segment assigned to a Representation. + * @property {?string} index + * The index template for the Media Segment assigned to a Representation. + * @property {Array} timepoints + * Segments of the segment template used to construct the timeline. + * @property {Array} timeline + * The timeline of the representation. */ shaka.dash.DashParser.SegmentInfo; + /** * @typedef { * function(!Array., ?number, ?number, boolean): @@ -2459,6 +2509,8 @@ shaka.dash.DashParser.RequestSegmentCallback; * Specifies the maximum sampling rate of the content, or null if unknown. * @property {number} availabilityTimeOffset * Specifies the total availabilityTimeOffset of the segment, or 0 if unknown. + * @property {?string} initialization + * Specifies the file where the init segment is located, or null. * @property {(shaka.extern.aes128Key|undefined)} aes128Key * AES-128 Content protection key */ From a7071c10fb19bd2356e39b1157a2c734453a97b9 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Tue, 23 Jan 2024 13:27:48 +0000 Subject: [PATCH 18/48] merge in master --- lib/util/periods.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index e814f67a49..0b9dfc9dee 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -460,9 +460,7 @@ shaka.util.PeriodCombiner = class { outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat, periodsMissing) { const unusedStreamsPerPeriod = []; - for (let i = 0; i < periodsMissing; i++) { - unusedStreamsPerPeriod.push(new Set()); - } + for (let i = 0; i < streamsPerPeriod.length; i++) { if (i >= firstNewPeriodIndex) { // This periods streams are all new. @@ -478,7 +476,7 @@ shaka.util.PeriodCombiner = class { // eslint-disable-next-line no-await-in-loop const ok = await this.extendExistingOutputStream_( outputStream, streamsPerPeriod, firstNewPeriodIndex, concat, - unusedStreamsPerPeriod, periodsMissing > 0); + unusedStreamsPerPeriod, periodsMissing); if (!ok) { // This output Stream was not properly extended to include streams from // the new period. This is likely a bug in our algorithm, so throw an @@ -509,11 +507,6 @@ shaka.util.PeriodCombiner = class { } // for (const unusedStreams of unusedStreamsPerPeriod) for (const unusedStreams of unusedStreamsPerPeriod) { - // eslint-disable-next-line no-restricted-syntax - if (unusedStreamsPerPeriod.indexOf(unusedStreams) < - periodsMissing && unusedStreams.size == 0) { - continue; - } for (const stream of unusedStreams) { if (shaka.util.PeriodCombiner.isDummy_(stream)) { // This is one of our dummy streams, so ignore it. We may not use @@ -554,7 +547,7 @@ shaka.util.PeriodCombiner = class { * of the first. * @param {!Array.>} unusedStreamsPerPeriod An array of sets of * unused streams from each period. - * @param {boolean} shouldAppend shall extend existing matching streams. + * @param {number} periodsMissing How many periods are missing in this update. * * @return {!Promise.} * @@ -566,9 +559,9 @@ shaka.util.PeriodCombiner = class { */ async extendExistingOutputStream_( outputStream, streamsPerPeriod, firstNewPeriodIndex, concat, - unusedStreamsPerPeriod, shouldAppend) { + unusedStreamsPerPeriod, periodsMissing) { this.findMatchesInAllPeriods_(streamsPerPeriod, - outputStream, shouldAppend); + outputStream, periodsMissing > 0); // This only exists where T == Stream, and this should only ever be called // on Stream types. StreamDB should not have pre-existing output streams. @@ -589,7 +582,7 @@ shaka.util.PeriodCombiner = class { } shaka.util.PeriodCombiner.extendOutputStream_(outputStream, - firstNewPeriodIndex, concat, unusedStreamsPerPeriod); + firstNewPeriodIndex, concat, unusedStreamsPerPeriod, periodsMissing); return true; } @@ -696,7 +689,8 @@ shaka.util.PeriodCombiner = class { return null; } shaka.util.PeriodCombiner.extendOutputStream_(outputStream, - /* firstNewPeriodIndex= */ 0, concat, unusedStreamsPerPeriod); + /* firstNewPeriodIndex= */ 0, concat, unusedStreamsPerPeriod, + /* periodsMissing= */ 0); return outputStream; } @@ -710,6 +704,7 @@ shaka.util.PeriodCombiner = class { * of the first. * @param {!Array.>} unusedStreamsPerPeriod An array of sets of * unused streams from each period. + * @param {number} periodsMissing How many periods are missing in this update * * @template T * Accepts either a StreamDB or Stream type. @@ -717,7 +712,8 @@ shaka.util.PeriodCombiner = class { * @private */ static extendOutputStream_( - outputStream, firstNewPeriodIndex, concat, unusedStreamsPerPeriod) { + outputStream, firstNewPeriodIndex, concat, unusedStreamsPerPeriod, + periodsMissing) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const LanguageUtils = shaka.util.LanguageUtils; const matches = outputStream.matchedStreams; @@ -729,7 +725,8 @@ shaka.util.PeriodCombiner = class { // Concatenate the new matches onto the stream, starting at the first new // period. - for (let i = firstNewPeriodIndex; i < matches.length; i++) { + const start = firstNewPeriodIndex + periodsMissing; + for (let i = start; i < matches.length; i++) { const match = matches[i]; concat(outputStream, match); @@ -747,7 +744,7 @@ shaka.util.PeriodCombiner = class { } if (used) { - unusedStreamsPerPeriod[i].delete(match); + unusedStreamsPerPeriod[i - periodsMissing].delete(match); // Add the codec of this stream to the output stream's codecs. const codecs = new Set(outputStream.codecs.split(',')); for (const codec of match.codecs.split(',')) { From b2196b1a4c88b87cf20451b568b0fabe1a491a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 12 Mar 2024 13:05:01 +0100 Subject: [PATCH 19/48] Fix MPD Patch --- lib/dash/dash_parser.js | 15 +++++++++++---- lib/util/periods.js | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 7db177b156..ee65f03161 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -790,8 +790,10 @@ shaka.dash.DashParser = class { const additions = TXml.findChildren(mpd, 'p:add'); - /** @type {!Array.} */ + /** @type {!Array} */ const newPeriods = []; + /** @type {!Array} */ + const periodAdditions = []; for (const addition of additions) { const selector = addition.attributes['sel']; @@ -804,12 +806,11 @@ shaka.dash.DashParser = class { const content = TXml.getContents(addition) || ''; this.parsePatchMediaPresentationDurationChange_(content); } else { - newPeriods.push(...this.parsePatchPeriod_(addition)); + periodAdditions.push(addition); } break; case 'SegmentTimeline': - this.parsePatchSegment_(paths, - TXml.findChildren(addition, 'S')); + this.parsePatchSegment_(paths, TXml.findChildren(addition, 'S')); break; // TODO handle SegmentList @@ -817,6 +818,12 @@ shaka.dash.DashParser = class { } } + // Add new periods after extending timelines, as new periods + // remove context cache of previous periods. + for (const addition of periodAdditions) { + newPeriods.push(...this.parsePatchPeriod_(addition)); + } + const replaces = TXml.findChildren(mpd, 'p:replace'); for (const replace of replaces) { const selector = replace.attributes['sel']; diff --git a/lib/util/periods.js b/lib/util/periods.js index b9341920da..56481a89d2 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -578,7 +578,7 @@ shaka.util.PeriodCombiner = class { // the output's MetaSegmentIndex. if (outputStream.segmentIndex) { await shaka.util.PeriodCombiner.extendOutputSegmentIndex_(outputStream, - firstNewPeriodIndex); + firstNewPeriodIndex + periodsMissing); } shaka.util.PeriodCombiner.extendOutputStream_(outputStream, From 6432ea51e81b1010122b4c6c099aa5b34c2964fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 23 Apr 2024 16:26:10 +0200 Subject: [PATCH 20/48] remove mpd patch config --- externs/shaka/player.js | 5 ----- lib/dash/dash_parser.js | 16 +++++++--------- lib/util/player_configuration.js | 1 - test/dash/dash_parser_live_unit.js | 26 -------------------------- test/demo/demo_unit.js | 1 - 5 files changed, 7 insertions(+), 42 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index df84168945..341184d86d 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -895,7 +895,6 @@ shaka.extern.xml.Node; * manifestPreprocessor: function(!Element), * manifestPreprocessorTXml: function(!shaka.extern.xml.Node), * sequenceMode: boolean, - * enablePatchMPDSupport: boolean, * enableAudioGroups: boolean, * multiTypeVariantsAllowed: boolean, * useStreamOnceInPeriodFlattening: boolean, @@ -962,10 +961,6 @@ shaka.extern.xml.Node; * If true, the media segments are appended to the SourceBuffer in * "sequence mode" (ignoring their internal timestamps). * Defaults to false. - * @property {boolean} enablePatchMPDSupport - * Enables DASH Patch manifest support. - * This feature is experimental. - * This value defaults to false. * @property {boolean} enableAudioGroups * If set, audio streams will be grouped and filtered by their parent * adaptation set ID. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index e57edf6f84..244946b2aa 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -482,15 +482,13 @@ shaka.dash.DashParser = class { manifestBaseUris = locations; } - if (this.config_.dash.enablePatchMPDSupport) { - // Get patch location element - const patchLocation = TXml.findChildren(mpd, 'PatchLocation') - .map(TXml.getContents) - .filter(shaka.util.Functional.isNotNull); - if (patchLocation.length > 0) { - // we are patching - this.patchLocationUrl_ = patchLocation[0]; - } + // Get patch location element + const patchLocation = TXml.findChildren(mpd, 'PatchLocation') + .map(TXml.getContents) + .filter(shaka.util.Functional.isNotNull); + if (patchLocation.length > 0) { + // we are patching + this.patchLocationUrl_ = patchLocation[0]; } let contentSteeringPromise = Promise.resolve(); diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 52219fd049..fa250005a7 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -145,7 +145,6 @@ shaka.util.PlayerConfiguration = class { element); }, sequenceMode: false, - enablePatchMPDSupport: false, enableAudioGroups: false, multiTypeVariantsAllowed, useStreamOnceInPeriodFlattening: false, diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 99e5cc1589..278331632a 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1648,35 +1648,9 @@ describe('DashParser Live', () => { ].join('\n'); beforeEach(() => { - const config = shaka.util.PlayerConfiguration.createDefault().manifest; - config.dash.enablePatchMPDSupport = true; - parser.configure(config); - fakeNetEngine.setResponseText('dummy://foo', manifestText); }); - it('does not use Mpd.PatchLocation if not configured', async () => { - const config = shaka.util.PlayerConfiguration.createDefault().manifest; - config.dash.enablePatchMPDSupport = false; - parser.configure(config); - - const patchText = [ - '', - '', - ].join('\n'); - fakeNetEngine.setResponseText('dummy://bar', patchText); - - await parser.start('dummy://foo', playerInterface); - - expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); - fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); - fakeNetEngine.request.calls.reset(); - - await updateManifest(); - expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); - fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); - }); - it('uses Mpd.PatchLocation', async () => { await parser.start('dummy://foo', playerInterface); diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js index 262f6d9614..de07f43461 100644 --- a/test/demo/demo_unit.js +++ b/test/demo/demo_unit.js @@ -83,7 +83,6 @@ describe('Demo', () => { .add('manifest.raiseFatalErrorOnManifestUpdateRequestFailure') .add('drm.persistentSessionOnlinePlayback') .add('drm.persistentSessionsMetadata') - .add('manifest.dash.enablePatchMPDSupport') .add('drm.persistentSessionsMetadata') .add('mediaSource.modifyCueCallback'); From c1d638e2f17130ad1a870c72384c483ced8dfbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 23 Apr 2024 16:27:24 +0200 Subject: [PATCH 21/48] remove dup --- test/demo/demo_unit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js index de07f43461..c000cca1a8 100644 --- a/test/demo/demo_unit.js +++ b/test/demo/demo_unit.js @@ -83,7 +83,6 @@ describe('Demo', () => { .add('manifest.raiseFatalErrorOnManifestUpdateRequestFailure') .add('drm.persistentSessionOnlinePlayback') .add('drm.persistentSessionsMetadata') - .add('drm.persistentSessionsMetadata') .add('mediaSource.modifyCueCallback'); /** From 8f4517b30f3ecdd17a665242e3d001b5bc284bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 24 Apr 2024 10:40:51 +0200 Subject: [PATCH 22/48] fix compiler issue --- lib/dash/dash_parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 244946b2aa..260e259f52 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -963,7 +963,7 @@ shaka.dash.DashParser = class { /** @type {!Array} */ const representationIds = []; - if (representationId != '') { + if (representationId) { representationIds.push(representationId); } else { for (const context of this.contextCache_.values()) { From 0d147e6cfbdc39d74a1bd63192ee385975c8953e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 24 Apr 2024 10:57:46 +0200 Subject: [PATCH 23/48] resolve uris correctly --- lib/dash/dash_parser.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 260e259f52..ddd5797c33 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -60,8 +60,8 @@ shaka.dash.DashParser = class { /** @private {number} */ this.globalId_ = 1; - /** @private {?string} */ - this.patchLocationUrl_ = null; + /** @private {!Array} */ + this.patchLocationUris_ = []; /** * A context of the living manifest used for processing @@ -321,8 +321,8 @@ shaka.dash.DashParser = class { async requestManifest_() { const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; const type = shaka.net.NetworkingEngine.AdvancedRequestType.MPD; - let manifestUris = this.patchLocationUrl_ ? - [this.patchLocationUrl_] : this.manifestUris_; + let manifestUris = this.patchLocationUris_.length ? + this.patchLocationUris_ : this.manifestUris_; if (this.manifestUris_.length > 1 && this.contentSteeringManager_) { const locations = this.contentSteeringManager_.getLocations( 'Location', /* ignoreBaseUrls= */ true); @@ -372,7 +372,7 @@ shaka.dash.DashParser = class { */ async parseManifest_(data, finalManifestUri) { let manifestData = data; - const rootElement = this.patchLocationUrl_ ? 'Patch' : 'MPD'; + const rootElement = this.patchLocationUris_.length ? 'Patch' : 'MPD'; const manifestPreprocessor = this.config_.dash.manifestPreprocessor; if (manifestPreprocessor) { shaka.Deprecate.deprecateFeature(5, @@ -404,7 +404,7 @@ shaka.dash.DashParser = class { manifestPreprocessorTXml(mpd); } - if (this.patchLocationUrl_) { + if (this.patchLocationUris_.length) { return this.processPatchManifest_(mpd); } @@ -488,7 +488,8 @@ shaka.dash.DashParser = class { .filter(shaka.util.Functional.isNotNull); if (patchLocation.length > 0) { // we are patching - this.patchLocationUrl_ = patchLocation[0]; + this.patchLocationUris_ = shaka.util.ManifestParserUtils.resolveUris( + manifestBaseUris, patchLocation); } let contentSteeringPromise = Promise.resolve(); @@ -1909,7 +1910,7 @@ shaka.dash.DashParser = class { const contextId = context.representation.id ? context.period.id + ',' + context.representation.id : ''; - if (this.patchLocationUrl_ && context.periodInfo.isLastPeriod && + if (this.patchLocationUris_.length && context.periodInfo.isLastPeriod && representationId) { this.contextCache_.set(`${context.period.id},${representationId}`, this.cloneContext_(context)); From d887cb08f46643f08526449ca1fcdf808cf8cbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 24 Apr 2024 10:58:08 +0200 Subject: [PATCH 24/48] add demo asset --- demo/common/assets.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/demo/common/assets.js b/demo/common/assets.js index c658a3850a..521d26bb49 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -165,6 +165,9 @@ shakaAssets.Feature = { // Set if the asset has Content Steering. CONTENT_STEERING: 'Content Steering', + + // Set if the asset supports MPD Patch. + MPD_PATCH: 'MPD Patch', }; @@ -989,6 +992,15 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.MP4) .addFeature(shakaAssets.Feature.LIVE) .addFeature(shakaAssets.Feature.THUMBNAILS), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - Live stream w/ SegmentTimeline (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), // End DASH-IF Assets }}} // bitcodin assets {{{ From b59e24c4772c5347091783c65cc38241190e186f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 7 May 2024 12:50:33 +0200 Subject: [PATCH 25/48] remove p: prefixes & update PatchLocation uris --- lib/dash/dash_parser.js | 31 ++++++++++++++++++++---------- test/dash/dash_parser_live_unit.js | 16 +++++++-------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 27c1dc3d52..4a928f4d74 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -488,14 +488,7 @@ shaka.dash.DashParser = class { } // Get patch location element - const patchLocation = TXml.findChildren(mpd, 'PatchLocation') - .map(TXml.getContents) - .filter(shaka.util.Functional.isNotNull); - if (patchLocation.length > 0) { - // we are patching - this.patchLocationUris_ = shaka.util.ManifestParserUtils.resolveUris( - manifestBaseUris, patchLocation); - } + this.updatePatchLocationUris_(mpd); let contentSteeringPromise = Promise.resolve(); @@ -819,7 +812,7 @@ shaka.dash.DashParser = class { async processPatchManifest_(mpd) { const TXml = shaka.util.TXml; - const additions = TXml.findChildren(mpd, 'p:add'); + const additions = TXml.findChildren(mpd, 'add'); /** @type {!Array} */ const newPeriods = []; @@ -855,7 +848,7 @@ shaka.dash.DashParser = class { newPeriods.push(...this.parsePatchPeriod_(addition)); } - const replaces = TXml.findChildren(mpd, 'p:replace'); + const replaces = TXml.findChildren(mpd, 'replace'); for (const replace of replaces) { const selector = replace.attributes['sel']; const content = TXml.getContents(replace) || ''; @@ -864,6 +857,8 @@ shaka.dash.DashParser = class { this.parsePatchMpdTypeChange_(content); } else if (selector == '/MPD/@mediaPresentationDuration') { this.parsePatchMediaPresentationDurationChange_(content); + } else if (selector.startsWith('/MPD/PatchLocation')) { + this.updatePatchLocationUris_(replace); } } @@ -2661,6 +2656,22 @@ shaka.dash.DashParser = class { this.operationManager_.manage(op); return op.promise; } + + /** + * @param {shaka.extern.xml.Node} node + * @private + */ + updatePatchLocationUris_(node) { + const TXml = shaka.util.TXml; + const patchLocation = TXml.findChildren(node, 'PatchLocation') + .map(TXml.getContents) + .filter(shaka.util.Functional.isNotNull); + if (patchLocation.length > 0) { + // we are patching + this.patchLocationUris_ = shaka.util.ManifestParserUtils.resolveUris( + this.manifestUris_, patchLocation); + } + } }; /** diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 278331632a..594a2c1932 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1672,12 +1672,12 @@ describe('DashParser Live', () => { it('transforms from dynamic to static', async () => { const patchText = [ '', - ' ', + ' ', ' static', - ' ', - ' ', + ' ', + ' ', ' PT28462.033599998S', - ' ', + ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://bar', patchText); @@ -1700,7 +1700,7 @@ describe('DashParser Live', () => { const stream = manifest.variants[0].video; const patchText = [ '', - ' ', + ' ', ' ', ' ', ' ', @@ -1708,7 +1708,7 @@ describe('DashParser Live', () => { ' ', ' ', ' ', - ' ', + ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://bar', patchText); @@ -1734,9 +1734,9 @@ describe('DashParser Live', () => { ].join('/'); const patchText = [ '', - ' ', + ' ', ' ', - ' ', + ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://bar', patchText); From 743d76f7a8f2a46c9d931c051feb25765caa8051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 7 May 2024 13:03:12 +0200 Subject: [PATCH 26/48] unescape html in xpath parse --- lib/util/tXml.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 01d24bcbd5..9962d8695b 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -741,6 +741,9 @@ shaka.util.TXml = class { * @return {!Array} */ static parseXpath(exprString) { + const StringUtils = shaka.util.StringUtils; + exprString = StringUtils.htmlUnescape(exprString); + const returnPaths = []; // Split string by paths but ignore '/' in quotes const paths = exprString From 588e3155b781fb1f906a3d5b1e0a63dd7f741d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 7 May 2024 13:04:14 +0200 Subject: [PATCH 27/48] reformat --- lib/util/tXml.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 9962d8695b..ba2bed9c39 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -742,11 +742,10 @@ shaka.util.TXml = class { */ static parseXpath(exprString) { const StringUtils = shaka.util.StringUtils; - exprString = StringUtils.htmlUnescape(exprString); const returnPaths = []; // Split string by paths but ignore '/' in quotes - const paths = exprString + const paths = StringUtils.htmlUnescape(exprString) .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/); for (const path of paths) { const nodeName = path.match(/\b([A-Z])\w+/); From 3d0f30497a6bd92288cbeef94a1586c66c89371e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 7 May 2024 13:48:45 +0200 Subject: [PATCH 28/48] log unhandled patch portions --- lib/dash/dash_parser.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 4a928f4d74..52f6858729 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -835,10 +835,9 @@ shaka.dash.DashParser = class { break; case 'SegmentTimeline': this.parsePatchSegment_(paths, TXml.findChildren(addition, 'S')); - break; - // TODO handle SegmentList - // TODO handle SegmentBase + default: + shaka.log.warning('Unhandled add', selector); } } @@ -859,9 +858,17 @@ shaka.dash.DashParser = class { this.parsePatchMediaPresentationDurationChange_(content); } else if (selector.startsWith('/MPD/PatchLocation')) { this.updatePatchLocationUris_(replace); + } else { + shaka.log.warning('Unhandled replace', selector); } } + const removes = TXml.findChildren(mpd, 'remove'); + for (const remove of removes) { + const selector = remove.attributes['sel']; + shaka.log.warning('Unhandled remove', selector); + } + if (newPeriods.length) { await this.postPeriodProcessing_(newPeriods, /* isPatchUpdate= */ true); } From aeb8e198ee02c13f6577e0d7953a01409450da3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 20 May 2024 12:24:34 +0200 Subject: [PATCH 29/48] demo: add filter for MPD Patch --- demo/search.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demo/search.js b/demo/search.js index 9d8d59f3d1..3cac88ace9 100644 --- a/demo/search.js +++ b/demo/search.js @@ -397,6 +397,8 @@ shakaDemo.Search = class { 'Filters for assets that have an LCEVC enhancement layer.'); this.makeBooleanInput_(specialContainer, Feature.CONTENT_STEERING, FEATURE, 'Filters for assets that use Content Steering.'); + this.makeBooleanInput_(specialContainer, Feature.MPD_PATCH, FEATURE, + 'Filters for assets that use MPD Patch.'); this.makeBooleanInput_(specialContainer, Feature.VR, FEATURE, 'Filters for assets that are VR.'); From 090d3b08ba69dc54421f498d795904b3e4becdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 20 May 2024 12:59:57 +0200 Subject: [PATCH 30/48] simplify processPatchManifest_ --- lib/dash/dash_parser.js | 79 ++++++++++++++++++++--------------------- lib/util/tXml.js | 4 +-- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 68b0382f5e..a411472a8f 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -811,34 +811,53 @@ shaka.dash.DashParser = class { * @private */ async processPatchManifest_(mpd) { + const StringUtils = shaka.util.StringUtils; const TXml = shaka.util.TXml; - const additions = TXml.findChildren(mpd, 'add'); - /** @type {!Array} */ const newPeriods = []; /** @type {!Array} */ const periodAdditions = []; - for (const addition of additions) { - const selector = addition.attributes['sel']; - + for (const child of TXml.getChildNodes(mpd)) { + let handled = true; + const selector = StringUtils.htmlUnescape(child.attributes['sel']); const paths = TXml.parseXpath(selector); - switch (paths[paths.length - 1].name) { - case 'MPD': - if (selector == '/MPD/@mediaPresentationDuration') { - const content = TXml.getContents(addition) || ''; - this.parsePatchMediaPresentationDurationChange_(content); - } else { - periodAdditions.push(addition); - } - break; - case 'SegmentTimeline': - this.parsePatchSegment_(paths, TXml.findChildren(addition, 'S')); - break; - default: - shaka.log.warning('Unhandled add', selector); + if (child.name === 'add') { + switch (paths[paths.length - 1].name) { + case 'MPD': + if (selector == '/MPD/@mediaPresentationDuration') { + const content = TXml.getContents(addition) || ''; + this.parsePatchMediaPresentationDurationChange_(content); + } else { + periodAdditions.push(addition); + } + break; + case 'SegmentTimeline': + this.parsePatchSegment_(paths, TXml.findChildren(addition, 'S')); + break; + default: + handled = false; + } + } else if (child.name === 'replace') { + const content = TXml.getContents(replace) || ''; + + if (selector == '/MPD/@type') { + this.parsePatchMpdTypeChange_(content); + } else if (selector == '/MPD/@mediaPresentationDuration') { + this.parsePatchMediaPresentationDurationChange_(content); + } else if (selector.startsWith('/MPD/PatchLocation')) { + this.updatePatchLocationUris_(replace); + } else { + handled = false; + } + } else if (child.name === 'remove') { + handled = false; + } + + if (!handled) { + shaka.log.warning('Unhandled ' + child.name + ' operation', selector); } } @@ -848,28 +867,6 @@ shaka.dash.DashParser = class { newPeriods.push(...this.parsePatchPeriod_(addition)); } - const replaces = TXml.findChildren(mpd, 'replace'); - for (const replace of replaces) { - const selector = replace.attributes['sel']; - const content = TXml.getContents(replace) || ''; - - if (selector == '/MPD/@type') { - this.parsePatchMpdTypeChange_(content); - } else if (selector == '/MPD/@mediaPresentationDuration') { - this.parsePatchMediaPresentationDurationChange_(content); - } else if (selector.startsWith('/MPD/PatchLocation')) { - this.updatePatchLocationUris_(replace); - } else { - shaka.log.warning('Unhandled replace', selector); - } - } - - const removes = TXml.findChildren(mpd, 'remove'); - for (const remove of removes) { - const selector = remove.attributes['sel']; - shaka.log.warning('Unhandled remove', selector); - } - if (newPeriods.length) { await this.postPeriodProcessing_(newPeriods, /* isPatchUpdate= */ true); } diff --git a/lib/util/tXml.js b/lib/util/tXml.js index ba2bed9c39..01d24bcbd5 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -741,11 +741,9 @@ shaka.util.TXml = class { * @return {!Array} */ static parseXpath(exprString) { - const StringUtils = shaka.util.StringUtils; - const returnPaths = []; // Split string by paths but ignore '/' in quotes - const paths = StringUtils.htmlUnescape(exprString) + const paths = exprString .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/); for (const path of paths) { const nodeName = path.match(/\b([A-Z])\w+/); From 7fc3501816cbbf6c2ae9967efdb0d859efa0ed74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 20 May 2024 14:49:34 +0200 Subject: [PATCH 31/48] get more details during xPath parse --- lib/dash/dash_parser.js | 52 ++++++++++++++++++++++++++--------------- lib/util/tXml.js | 14 +++++++---- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index a411472a8f..5dc618f58c 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -823,41 +823,55 @@ shaka.dash.DashParser = class { let handled = true; const selector = StringUtils.htmlUnescape(child.attributes['sel']); const paths = TXml.parseXpath(selector); + const node = paths[paths.length - 1]; - if (child.name === 'add') { - switch (paths[paths.length - 1].name) { + if (child.tagName === 'add') { + switch (node.name) { case 'MPD': - if (selector == '/MPD/@mediaPresentationDuration') { - const content = TXml.getContents(addition) || ''; + if (node.attribute === 'mediaPresentationDuration') { + const content = TXml.getContents(child) || ''; this.parsePatchMediaPresentationDurationChange_(content); + } else if (node.attribute === null) { + periodAdditions.push(child); } else { - periodAdditions.push(addition); + handled = false; } break; case 'SegmentTimeline': - this.parsePatchSegment_(paths, TXml.findChildren(addition, 'S')); + this.parsePatchSegment_(paths, TXml.findChildren(child, 'S')); break; default: handled = false; } - } else if (child.name === 'replace') { - const content = TXml.getContents(replace) || ''; - - if (selector == '/MPD/@type') { - this.parsePatchMpdTypeChange_(content); - } else if (selector == '/MPD/@mediaPresentationDuration') { - this.parsePatchMediaPresentationDurationChange_(content); - } else if (selector.startsWith('/MPD/PatchLocation')) { - this.updatePatchLocationUris_(replace); - } else { - handled = false; + } else if (child.tagName === 'replace') { + const content = TXml.getContents(child) || ''; + + switch (node.name) { + case 'MPD': + switch (node.attribute) { + case 'type': + this.parsePatchMpdTypeChange_(content); + break; + case 'mediaPresentationDuration': + this.parsePatchMediaPresentationDurationChange_(content); + break; + default: + handled = false; + } + break; + case 'PatchLocation': + this.updatePatchLocationUris_(child); + break; + default: + handled = false; } - } else if (child.name === 'remove') { + } else if (child.tagName === 'remove') { handled = false; } if (!handled) { - shaka.log.warning('Unhandled ' + child.name + ' operation', selector); + shaka.log.warning('Unhandled ' + child.tagName + ' operation', + selector); } } diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 01d24bcbd5..205d3788f9 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -748,14 +748,18 @@ shaka.util.TXml = class { for (const path of paths) { const nodeName = path.match(/\b([A-Z])\w+/); - // We only want the id attribute in which case - // /'(.*?)'/ will suffice to get it. - const idAttr = path.match(/(@id='(.*?)')/); if (nodeName) { + // We only want the id attribute in which case + // /'(.*?)'/ will suffice to get it. + const idAttr = path.match(/(@id='(.*?)')/); + const position = path.match(/\[(\d+)\]/); returnPaths.push({ name: nodeName[0], id: idAttr ? idAttr[0].match(/'(.*?)'/)[0].replace(/'/gm, '') : null, + // position is counted from 1, so make it readable for devs + position: position ? Number(position[1]) - 1 : null, + attribute: path.split('/@')[1] || null, }); } } @@ -808,7 +812,9 @@ shaka.util.TXml.knownNameSpaces_ = new Map([]); /** * @typedef {{ * name: string, - * id: ?string + * id: ?string, + * position: ?number, + * attribute: ?string * }} */ shaka.util.TXml.PathNode; From 01a464f33d72f5a891e66ab6e88bc8d04e3c20dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 21 May 2024 11:33:41 +0200 Subject: [PATCH 32/48] Add more assets to test --- demo/common/assets.js | 47 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/demo/common/assets.js b/demo/common/assets.js index cb0e2e50d7..0fff227fc9 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -998,7 +998,43 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.LIVE) .addFeature(shakaAssets.Feature.THUMBNAILS), new ShakaDemoAssetInfo( - /* name= */ 'DASH-IF MPD Patch - Live stream w/ SegmentTimeline (livesim)', + /* name= */ 'DASH-IF MPD Patch - SegmentTemplate with $Number$ (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - SegmentTemplate with $Number$, multiperiod (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/periods_60/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - SegmentTimeline with $Number$ (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/segtimelinenr_1/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - SegmentTimeline with $Number$, multiperiod (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/segtimelinenr_1/periods_60/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - SegmentTimeline with $Time$ (livesim)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpd', /* source= */ shakaAssets.Source.DASH_IF) @@ -1006,6 +1042,15 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.MP4) .addFeature(shakaAssets.Feature.LIVE) .addFeature(shakaAssets.Feature.MPD_PATCH), + new ShakaDemoAssetInfo( + /* name= */ 'DASH-IF MPD Patch - SegmentTimeline with $Time$, multiperiod (livesim)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim2.dashif.org/livesim2/patch_60/segtimeline_1/periods_60/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MPD_PATCH), // End DASH-IF Assets }}} // bitcodin assets {{{ From 9f4c6dd7e753673208cab743d03107943d1600a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 22 May 2024 12:18:06 +0200 Subject: [PATCH 33/48] Parse SegmentTimeline with $Time$ --- lib/dash/dash_parser.js | 180 ++++++++++++++++++----------------- lib/dash/mpd_utils.js | 21 +--- lib/dash/segment_template.js | 43 ++++++++- lib/util/tXml.js | 2 +- 4 files changed, 139 insertions(+), 107 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 5dc618f58c..65ffeb7454 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -818,67 +818,53 @@ shaka.dash.DashParser = class { const newPeriods = []; /** @type {!Array} */ const periodAdditions = []; + /** @type {!Set} */ + const modifiedTimelines = new Set(); for (const child of TXml.getChildNodes(mpd)) { let handled = true; const selector = StringUtils.htmlUnescape(child.attributes['sel']); const paths = TXml.parseXpath(selector); const node = paths[paths.length - 1]; - - if (child.tagName === 'add') { - switch (node.name) { - case 'MPD': - if (node.attribute === 'mediaPresentationDuration') { - const content = TXml.getContents(child) || ''; - this.parsePatchMediaPresentationDurationChange_(content); - } else if (node.attribute === null) { - periodAdditions.push(child); - } else { - handled = false; - } - break; - case 'SegmentTimeline': - this.parsePatchSegment_(paths, TXml.findChildren(child, 'S')); - break; - default: - handled = false; + const position = child.attributes['pos'] || null; + const content = TXml.getContents(child) || ''; + + if (node.name === 'MPD') { + if (node.attribute === 'mediaPresentationDuration') { + const content = TXml.getContents(child) || ''; + this.parsePatchMediaPresentationDurationChange_(content); + } else if (node.attribute === 'type') { + this.parsePatchMpdTypeChange_(content); + } else if (node.attribute === null && child.tagName === 'add') { + periodAdditions.push(child); + } else { + handled = false; } - } else if (child.tagName === 'replace') { - const content = TXml.getContents(child) || ''; - - switch (node.name) { - case 'MPD': - switch (node.attribute) { - case 'type': - this.parsePatchMpdTypeChange_(content); - break; - case 'mediaPresentationDuration': - this.parsePatchMediaPresentationDurationChange_(content); - break; - default: - handled = false; - } - break; - case 'PatchLocation': - this.updatePatchLocationUris_(child); - break; - default: - handled = false; + } else if (node.name === 'PatchLocation') { + this.updatePatchLocationUris_(child); + } else if (node.name === 'SegmentTimeline' || node.name === 'S') { + const timelines = this.modifyTimepoints_(paths, child.tagName, position, + TXml.findChildren(child, 'S')); + for (const timeline of timelines) { + modifiedTimelines.add(timeline); } - } else if (child.tagName === 'remove') { + } else { handled = false; } - if (!handled) { shaka.log.warning('Unhandled ' + child.tagName + ' operation', selector); } } + for (const timeline of modifiedTimelines) { + this.parsePatchSegment_(timeline); + } + // Add new periods after extending timelines, as new periods // remove context cache of previous periods. - for (const addition of periodAdditions) { - newPeriods.push(...this.parsePatchPeriod_(addition)); + for (const periodAddition of periodAdditions) { + newPeriods.push(...this.parsePatchPeriod_(periodAddition)); } if (newPeriods.length) { @@ -960,23 +946,31 @@ shaka.dash.DashParser = class { } /** - * Ingests Path MPD segments. + * Ingests Patch MPD segments into timeline. * * @param {!Array} paths - * @param {Array} segments + * @param {string} action add, replace or remove + * @param {?string} position + * @param {!Array} segments + * @return {!Array} context ids with updated timeline * @private */ - parsePatchSegment_(paths, segments) { + modifyTimepoints_(paths, action, position, segments) { + const SegmentTemplate = shaka.dash.SegmentTemplate; + let periodId = ''; let adaptationSetId = ''; let representationId = ''; + let nodePosition = null; for (const node of paths) { - if (node.name == 'Period') { + if (node.name === 'Period') { periodId = node.id; - } else if (node.name == 'AdaptationSet') { + } else if (node.name === 'AdaptationSet') { adaptationSetId = node.id; - } else if (node.name == 'Representation') { + } else if (node.name === 'Representation') { representationId = node.id; + } else if (node.name === 'S') { + nodePosition = node.position; } } @@ -987,56 +981,69 @@ shaka.dash.DashParser = class { representationIds.push(representationId); } else { for (const context of this.contextCache_.values()) { - if (context.adaptationSet.id == adaptationSetId && + if (context.adaptationSet.id === adaptationSetId && context.representation.id) { representationIds.push(context.representation.id); } } } + const modifiedContextIds = []; for (const repId of representationIds) { const contextId = periodId + ',' + repId; - /** @type {shaka.dash.DashParser.Context} */ const context = this.contextCache_.get(contextId); + SegmentTemplate.modifyTimepoints( + context, action, position, nodePosition, segments); + modifiedContextIds.push(contextId); + } + return modifiedContextIds; + } - context.segmentInfo.timepoints = segments; + /** + * Parses modified segments. + * + * @param {string} contextId + * @private + */ + parsePatchSegment_(contextId) { + /** @type {shaka.dash.DashParser.Context} */ + const context = this.contextCache_.get(contextId); - const currentStream = this.streamMap_[contextId]; - goog.asserts.assert(currentStream, 'stream should exist'); + const currentStream = this.streamMap_[contextId]; + goog.asserts.assert(currentStream, 'stream should exist'); - if (currentStream.segmentIndex) { - currentStream.segmentIndex.evict( - this.manifest_.presentationTimeline.getSegmentAvailabilityStart()); - } + if (currentStream.segmentIndex) { + currentStream.segmentIndex.evict( + this.manifest_.presentationTimeline.getSegmentAvailabilityStart()); + } - try { - const requestSegment = (uris, startByte, endByte, isInit) => { - return this.requestSegment_(uris, startByte, endByte, isInit); - }; - // TODO we should obtain lastSegmentNumber if possible - const streamInfo = shaka.dash.SegmentTemplate.createStreamInfo( - context, requestSegment, this.streamMap_, /* isUpdate= */ true, - this.config_.dash.initialSegmentLimit, this.periodDurations_, - context.representation.aesKey, /* lastSegmentNumber= */ null, - /* isPatchUpdate= */ true); - currentStream.createSegmentIndex = async () => { - if (!currentStream.segmentIndex) { - currentStream.segmentIndex = - await streamInfo.generateSegmentIndex(); - } - }; - } catch (error) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - const contentType = context.representation.contentType; - const isText = contentType == ContentType.TEXT || - contentType == ContentType.APPLICATION; - const isImage = contentType == ContentType.IMAGE; - if (!(isText || isImage) || - error.code != shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) { - // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image - throw error; + try { + const requestSegment = (uris, startByte, endByte, isInit) => { + return this.requestSegment_(uris, startByte, endByte, isInit); + }; + // TODO we should obtain lastSegmentNumber if possible + const streamInfo = shaka.dash.SegmentTemplate.createStreamInfo( + context, requestSegment, this.streamMap_, /* isUpdate= */ true, + this.config_.dash.initialSegmentLimit, this.periodDurations_, + context.representation.aesKey, /* lastSegmentNumber= */ null, + /* isPatchUpdate= */ true); + currentStream.createSegmentIndex = async () => { + if (!currentStream.segmentIndex) { + currentStream.segmentIndex = + await streamInfo.generateSegmentIndex(); } + }; + } catch (error) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const contentType = context.representation.contentType; + const isText = contentType == ContentType.TEXT || + contentType == ContentType.APPLICATION; + const isImage = contentType == ContentType.IMAGE; + if (!(isText || isImage) || + error.code != shaka.util.Error.Code.DASH_NO_SEGMENT_INFO) { + // We will ignore any DASH_NO_SEGMENT_INFO errors for text/image + throw error; } } } @@ -2021,7 +2028,7 @@ shaka.dash.DashParser = class { contextClone[k] = { segmentBase: null, segmentList: null, - segmentTemplate: null, + segmentTemplate: frameRef.segmentTemplate, getBaseUris: frameRef.getBaseUris, width: frameRef.width, height: frameRef.height, @@ -2735,7 +2742,6 @@ shaka.dash.DashParser.PatchContext; * presentationTimeOffset: number, * media: ?string, * index: ?string, - * timepoints: Array, * timeline: Array * }} * @@ -2757,8 +2763,6 @@ shaka.dash.DashParser.PatchContext; * The template for the Media Segment assigned to a Representation. * @property {?string} index * The index template for the Media Segment assigned to a Representation. - * @property {Array} timepoints - * Segments of the segment template used to construct the timeline. * @property {Array} timeline * The timeline of the representation. */ diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index c22cd8fa22..4908409d78 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -306,24 +306,13 @@ shaka.dash.MpdUtils = class { /** @type {Array.} */ let timeline = null; - if (context.segmentInfo && context.segmentInfo.timepoints) { - timeline = context.segmentInfo.timeline; - goog.asserts.assert(timeline, 'timeline should exist!'); - startNumber = timeline[timeline.length-1].segmentPosition+1; - const timePoints = context.segmentInfo.timepoints; - const partialTimeline = MpdUtils.createTimeline( + const timelineNode = + MpdUtils.inheritChild(context, callback, 'SegmentTimeline'); + if (timelineNode) { + const timePoints = TXml.findChildren(timelineNode, 'S'); + timeline = MpdUtils.createTimeline( timePoints, timescale, unscaledPresentationTimeOffset, context.periodInfo.duration || Infinity, startNumber); - timeline.push(...partialTimeline); - } else { - const timelineNode = - MpdUtils.inheritChild(context, callback, 'SegmentTimeline'); - if (timelineNode) { - const timePoints = TXml.findChildren(timelineNode, 'S'); - timeline = MpdUtils.createTimeline( - timePoints, timescale, unscaledPresentationTimeOffset, - context.periodInfo.duration || Infinity, startNumber); - } } const scaledPresentationTimeOffset = diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index b8ee83c748..818a00ff74 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -18,6 +18,7 @@ goog.require('shaka.util.IReleasable'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.ObjectUtils'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); @@ -73,10 +74,8 @@ shaka.dash.SegmentTemplate = class { media: info.mediaTemplate, index: info.indexTemplate, startNumber: info.startNumber, - nextStartNumber: 0, duration: info.segmentDuration || 0, presentationTimeOffset: info.unscaledPresentationTimeOffset, - timepoints: null, timeline: info.timeline, }; } @@ -204,6 +203,46 @@ shaka.dash.SegmentTemplate = class { } } + /** + * Ingests Patch MPD segments into timeline. + * + * @param {!shaka.dash.DashParser.Context} context + * @param {string} action add, replace or remove + * @param {?string} position + * @param {?number} nodePosition + * @param {!Array} segments + */ + static modifyTimepoints(context, action, position, nodePosition, segments) { + const MpdUtils = shaka.dash.MpdUtils; + const SegmentTemplate = shaka.dash.SegmentTemplate; + const TXml = shaka.util.TXml; + + const timelineNode = MpdUtils.inheritChild(context, + SegmentTemplate.fromInheritance_, 'SegmentTimeline'); + goog.asserts.assert(timelineNode, 'timeline node not found'); + const timepoints = TXml.findChildren(timelineNode, 'S'); + + goog.asserts.assert(timepoints, 'timepoints should exist'); + let index; + if (nodePosition === null) { + index = position === 'prepend' ? 0 : timepoints.length; + } else { + index = nodePosition; + if (position === 'prepend') { + --index; + } else if (position === 'after') { + ++index; + } + } + if (action === 'remove' || action === 'replace') { + timepoints.splice(index, 1); + } + if (action === 'add' || action === 'replace') { + timepoints.splice(index, 0, ...segments); + } + timelineNode.children = timepoints; + } + /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame * @return {?shaka.extern.xml.Node} diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 205d3788f9..ba8c93cc89 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -746,7 +746,7 @@ shaka.util.TXml = class { const paths = exprString .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/); for (const path of paths) { - const nodeName = path.match(/\b([A-Z])\w+/); + const nodeName = path.match(/^([\w]+)/); if (nodeName) { // We only want the id attribute in which case From 5b72afef23f1ee0d5362137b647e80f5c46df9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 22 May 2024 13:52:21 +0200 Subject: [PATCH 34/48] Parse SegmentTimeline with $Number$ --- lib/dash/dash_parser.js | 90 +++++++++++++++++++++++++++++++++-------- lib/dash/mpd_utils.js | 21 ++++++---- lib/util/tXml.js | 2 + 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 5d1f2e174c..0edbdc16a8 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -843,6 +843,11 @@ shaka.dash.DashParser = class { } } else if (node.name === 'PatchLocation') { this.updatePatchLocationUris_(child); + } else if (node.name === 'SegmentTemplate' && node.attribute !== null) { + const timelines = this.modifySegmentTemplateAttribute_(paths, content); + for (const timeline of timelines) { + modifiedTimelines.add(timeline); + } } else if (node.name === 'SegmentTimeline' || node.name === 'S') { const timelines = this.modifyTimepoints_(paths, child.tagName, position, TXml.findChildren(child, 'S')); @@ -947,22 +952,14 @@ shaka.dash.DashParser = class { } /** - * Ingests Patch MPD segments into timeline. - * * @param {!Array} paths - * @param {string} action add, replace or remove - * @param {?string} position - * @param {!Array} segments - * @return {!Array} context ids with updated timeline + * @return {!Array} * @private */ - modifyTimepoints_(paths, action, position, segments) { - const SegmentTemplate = shaka.dash.SegmentTemplate; - + getContextIdsFromPath_(paths) { let periodId = ''; let adaptationSetId = ''; let representationId = ''; - let nodePosition = null; for (const node of paths) { if (node.name === 'Period') { periodId = node.id; @@ -970,8 +967,6 @@ shaka.dash.DashParser = class { adaptationSetId = node.id; } else if (node.name === 'Representation') { representationId = node.id; - } else if (node.name === 'S') { - nodePosition = node.position; } } @@ -988,17 +983,78 @@ shaka.dash.DashParser = class { } } } + return representationIds.map((repId) => periodId + ',' + repId); + } + + /** + * Modifeis SegmentTemplate attribute based on MPD patch + * + * @param {!Array} paths + * @param {string} content + * @return {!Array} context ids with updated timeline + * @private + */ + modifySegmentTemplateAttribute_(paths, content) { + const contextIds = this.getContextIdsFromPath_(paths); + const lastPath = paths[paths.length - 1]; + const attribute = lastPath.attribute; + const contentAsNumber = parseInt(content, 10); + for (const contextId of contextIds) { + /** @type {shaka.dash.DashParser.Context} */ + const context = this.contextCache_.get(contextId); + const segmentInfo = context.segmentInfo; + switch (attribute) { + case 'timescale': + segmentInfo.timescale = contentAsNumber; + break; + case 'duration': + segmentInfo.duration = contentAsNumber; + break; + case 'startNumber': + segmentInfo.startNumber = contentAsNumber; + break; + case 'presentationTimeOffset': + segmentInfo.presentationTimeOffset = contentAsNumber; + break; + case 'media': + segmentInfo.media = content; + break; + case 'index': + segmentInfo.index = content; + break; + } + } + return contextIds; + } + + /** + * Ingests Patch MPD segments into timeline. + * + * @param {!Array} paths + * @param {string} action add, replace or remove + * @param {?string} position + * @param {!Array} segments + * @return {!Array} context ids with updated timeline + * @private + */ + modifyTimepoints_(paths, action, position, segments) { + const SegmentTemplate = shaka.dash.SegmentTemplate; + + let nodePosition = null; + const lastPath = paths[paths.length - 1]; + if (lastPath.name === 'S') { + nodePosition = lastPath.position; + } + + const contextIds = this.getContextIdsFromPath_(paths); - const modifiedContextIds = []; - for (const repId of representationIds) { - const contextId = periodId + ',' + repId; + for (const contextId of contextIds) { /** @type {shaka.dash.DashParser.Context} */ const context = this.contextCache_.get(contextId); SegmentTemplate.modifyTimepoints( context, action, position, nodePosition, segments); - modifiedContextIds.push(contextId); } - return modifiedContextIds; + return contextIds; } /** diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 4908409d78..8c44412b18 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -293,16 +293,21 @@ shaka.dash.MpdUtils = class { segmentDuration /= timescale; } } + let startNumber = 1; + if (context.segmentInfo) { + startNumber = context.segmentInfo.startNumber; + } else { + const startNumberStr = + MpdUtils.inheritAttribute(context, callback, 'startNumber'); + startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); + if (startNumberStr == null || startNumber == null) { + startNumber = 1; + } + } - const startNumberStr = - MpdUtils.inheritAttribute(context, callback, 'startNumber'); const unscaledPresentationTimeOffset = - Number(MpdUtils.inheritAttribute(context, callback, - 'presentationTimeOffset')) || 0; - let startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); - if (startNumberStr == null || startNumber == null) { - startNumber = 1; - } + Number(MpdUtils.inheritAttribute(context, callback, + 'presentationTimeOffset')) || 0; /** @type {Array.} */ let timeline = null; diff --git a/lib/util/tXml.js b/lib/util/tXml.js index ba8c93cc89..14d9a6e396 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -761,6 +761,8 @@ shaka.util.TXml = class { position: position ? Number(position[1]) - 1 : null, attribute: path.split('/@')[1] || null, }); + } else if (path.startsWith('@') && returnPaths.length) { + returnPaths[returnPaths.length - 1].attribute = path.slice(1); } } return returnPaths; From ee4150c3f244049539b4f802a115fc410a77fcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 22 May 2024 16:14:17 +0200 Subject: [PATCH 35/48] handle ttl --- lib/dash/dash_parser.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 0edbdc16a8..cd5192d82b 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -76,6 +76,7 @@ shaka.dash.DashParser = class { mediaPresentationDuration: null, availabilityTimeOffset: 0, getBaseUris: null, + publishTime: 0, }; /** @@ -237,6 +238,7 @@ shaka.dash.DashParser = class { mediaPresentationDuration: null, availabilityTimeOffset: 0, getBaseUris: null, + publishTime: 0, }; this.periodCombiner_ = null; @@ -559,6 +561,8 @@ shaka.dash.DashParser = class { }; this.manifestPatchContext_.getBaseUris = getBaseUris; + this.manifestPatchContext_.publishTime = + TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0; let availabilityTimeOffset = 0; if (uriObjs && uriObjs.length) { @@ -836,6 +840,8 @@ shaka.dash.DashParser = class { this.parsePatchMediaPresentationDurationChange_(content); } else if (node.attribute === 'type') { this.parsePatchMpdTypeChange_(content); + } else if (node.attribute === 'publishTime') { + this.manifestPatchContext_.publishTime = TXml.parseDate(content) || 0; } else if (node.attribute === null && child.tagName === 'add') { periodAdditions.push(child); } else { @@ -2782,13 +2788,22 @@ shaka.dash.DashParser = class { */ updatePatchLocationUris_(node) { const TXml = shaka.util.TXml; - const patchLocation = TXml.findChildren(node, 'PatchLocation') + this.patchLocationUris_ = []; + const publishTime = this.manifestPatchContext_.publishTime; + let patchLocations = TXml.findChildren(node, 'PatchLocation'); + if (!publishTime || !patchLocations.length) { + return; + } + patchLocations = patchLocations.filter((patchLocation) => { + const ttl = TXml.parseNonNegativeInt(patchLocation.attributes['ttl']); + return !ttl || publishTime + ttl > Date.now(); + }) .map(TXml.getContents) .filter(shaka.util.Functional.isNotNull); - if (patchLocation.length > 0) { + if (patchLocations.length > 0) { // we are patching this.patchLocationUris_ = shaka.util.ManifestParserUtils.resolveUris( - this.manifestUris_, patchLocation); + this.manifestUris_, patchLocations); } } }; @@ -2799,7 +2814,8 @@ shaka.dash.DashParser = class { * mediaPresentationDuration: ?number, * profiles: !Array., * availabilityTimeOffset: number, - * getBaseUris: ?function():!Array. + * getBaseUris: ?function():!Array., + * publishTime: number * }} * * @property {string} type @@ -2813,6 +2829,8 @@ shaka.dash.DashParser = class { * Specifies the total availabilityTimeOffset of the segment. * @property {?function():!Array.} getBaseUris * An array of absolute base URIs. + * @property {number} publishTime + * Time when manifest has been published. */ shaka.dash.DashParser.PatchContext; From 4015edd311463d696de5cae88416911b488f933d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 22 May 2024 16:30:16 +0200 Subject: [PATCH 36/48] get publishTime before parsing PatchLocation --- lib/dash/dash_parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index cd5192d82b..174df59d83 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -490,6 +490,8 @@ shaka.dash.DashParser = class { manifestBaseUris = locations; } + this.manifestPatchContext_.publishTime = + TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0; // Get patch location element this.updatePatchLocationUris_(mpd); @@ -561,8 +563,6 @@ shaka.dash.DashParser = class { }; this.manifestPatchContext_.getBaseUris = getBaseUris; - this.manifestPatchContext_.publishTime = - TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0; let availabilityTimeOffset = 0; if (uriObjs && uriObjs.length) { From 4290b87748beee21bc5ed2f5b926e3dd451c6109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Wed, 22 May 2024 17:01:55 +0200 Subject: [PATCH 37/48] ttl units fixes --- lib/dash/dash_parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 174df59d83..4667f973c0 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -2796,7 +2796,7 @@ shaka.dash.DashParser = class { } patchLocations = patchLocations.filter((patchLocation) => { const ttl = TXml.parseNonNegativeInt(patchLocation.attributes['ttl']); - return !ttl || publishTime + ttl > Date.now(); + return !ttl || publishTime + ttl > Date.now() / 1000; }) .map(TXml.getContents) .filter(shaka.util.Functional.isNotNull); @@ -2830,7 +2830,7 @@ shaka.dash.DashParser = class { * @property {?function():!Array.} getBaseUris * An array of absolute base URIs. * @property {number} publishTime - * Time when manifest has been published. + * Time when manifest has been published, in seconds. */ shaka.dash.DashParser.PatchContext; From 1c8078c82d8d1eebf9a769e50e82608d428d54e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Thu, 23 May 2024 12:10:58 +0200 Subject: [PATCH 38/48] support multiperiod --- lib/dash/dash_parser.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 4667f973c0..20c7cd4c2c 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -25,6 +25,7 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.ContentSteeringManager'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); @@ -849,6 +850,12 @@ shaka.dash.DashParser = class { } } else if (node.name === 'PatchLocation') { this.updatePatchLocationUris_(child); + } else if (node.name === 'Period') { + if (child.tagName === 'add') { + periodAdditions.push(child); + } else if (child.tagName === 'remove' && node.id) { + this.removePatchPeriod_(node.id); + } } else if (node.name === 'SegmentTemplate' && node.attribute !== null) { const timelines = this.modifySegmentTemplateAttribute_(paths, content); for (const timeline of timelines) { @@ -931,7 +938,6 @@ shaka.dash.DashParser = class { parsePatchPeriod_(periods) { goog.asserts.assert(this.manifestPatchContext_.getBaseUris, 'Must provide getBaseUris on manifestPatchContext_'); - this.contextCache_.clear(); /** @type {shaka.dash.DashParser.Context} */ const context = { @@ -948,7 +954,6 @@ shaka.dash.DashParser = class { segmentInfo: null, mediaPresentationDuration: this.manifestPatchContext_.mediaPresentationDuration, - timelineCache: new Map(), }; const periodsAndDuration = this.parsePeriods_(context, @@ -957,6 +962,20 @@ shaka.dash.DashParser = class { return periodsAndDuration.periods; } + /** + * @param {string} periodId + * @private + */ + removePatchPeriod_(periodId) { + const Iterables = shaka.util.Iterables; + const contextIds = Iterables.filter(this.contextCache_.keys(), (key) => { + return key.split(',')[0] === periodId; + }); + for (const contextId of contextIds) { + this.contextCache_.delete(contextId); + } + } + /** * @param {!Array} paths * @return {!Array} @@ -983,8 +1002,9 @@ shaka.dash.DashParser = class { representationIds.push(representationId); } else { for (const context of this.contextCache_.values()) { - if (context.adaptationSet.id === adaptationSetId && - context.representation.id) { + if (context.period.id === periodId && + context.adaptationSet.id === adaptationSetId && + context.representation.id) { representationIds.push(context.representation.id); } } From 70201201a2d3052b0877f6ee5f06e477a41c03d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Thu, 23 May 2024 13:00:18 +0200 Subject: [PATCH 39/48] fix xpath tests --- test/util/tXml_unit.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/util/tXml_unit.js b/test/util/tXml_unit.js index 1a580b4052..7977e92cc8 100644 --- a/test/util/tXml_unit.js +++ b/test/util/tXml_unit.js @@ -418,9 +418,10 @@ describe('tXml', () => { }); it('parseXpath', () => { - expect(TXml.parseXpath('/MPD')).toEqual([{name: 'MPD', id: null}]); + expect(TXml.parseXpath('/MPD')) + .toEqual([{name: 'MPD', id: null, position: null, attribute: null}]); expect(TXml.parseXpath('/MPD/@type')) - .toEqual([{name: 'MPD', id: null}]); + .toEqual([{name: 'MPD', id: null, position: null, attribute: 'type'}]); const timelinePath = '/' + [ 'MPD', @@ -428,13 +429,15 @@ describe('tXml', () => { 'AdaptationSet[@id=\'7\']', 'SegmentTemplate', 'SegmentTimeline', + 'S[2]', ].join('/'); expect(TXml.parseXpath(timelinePath)).toEqual([ - {name: 'MPD', id: null}, - {name: 'Period', id: '6469'}, - {name: 'AdaptationSet', id: '7'}, - {name: 'SegmentTemplate', id: null}, - {name: 'SegmentTimeline', id: null}, + {name: 'MPD', id: null, position: null, attribute: null}, + {name: 'Period', id: '6469', position: null, attribute: null}, + {name: 'AdaptationSet', id: '7', position: null, attribute: null}, + {name: 'SegmentTemplate', id: null, position: null, attribute: null}, + {name: 'SegmentTimeline', id: null, position: null, attribute: null}, + {name: 'S', id: null, position: 1, attribute: null}, ]); }); }); From c95ccd6cb1fcb4fec298389870a2acbf9ecfb492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 24 May 2024 11:51:05 +0200 Subject: [PATCH 40/48] Unit tests + code improvements --- lib/dash/dash_parser.js | 124 +++++---- lib/dash/segment_template.js | 25 +- lib/util/tXml.js | 50 +++- test/dash/dash_parser_live_unit.js | 134 ---------- test/dash/dash_parser_patch_unit.js | 385 ++++++++++++++++++++++++++++ 5 files changed, 497 insertions(+), 221 deletions(-) create mode 100644 test/dash/dash_parser_patch_unit.js diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 20c7cd4c2c..37026c7c6a 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -25,7 +25,6 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.ContentSteeringManager'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); -goog.require('shaka.util.Iterables'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); @@ -63,8 +62,8 @@ shaka.dash.DashParser = class { /** @private {number} */ this.globalId_ = 1; - /** @private {!Array} */ - this.patchLocationUris_ = []; + /** @private {!Array} */ + this.patchLocationNodes_ = []; /** * A context of the living manifest used for processing @@ -326,9 +325,13 @@ shaka.dash.DashParser = class { async requestManifest_() { const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; const type = shaka.net.NetworkingEngine.AdvancedRequestType.MPD; - let manifestUris = this.patchLocationUris_.length ? - this.patchLocationUris_ : this.manifestUris_; - if (this.manifestUris_.length > 1 && this.contentSteeringManager_) { + let rootElement = 'MPD'; + const patchLocationUris = this.getPatchLocationUris_(); + let manifestUris = this.manifestUris_; + if (patchLocationUris.length) { + manifestUris = patchLocationUris; + rootElement = 'Patch'; + } else if (this.manifestUris_.length > 1 && this.contentSteeringManager_) { const locations = this.contentSteeringManager_.getLocations( 'Location', /* ignoreBaseUrls= */ true); if (locations.length) { @@ -355,7 +358,7 @@ shaka.dash.DashParser = class { } // This may throw, but it will result in a failed promise. - await this.parseManifest_(response.data, response.uri); + await this.parseManifest_(response.data, response.uri, rootElement); // Keep track of how long the longest manifest update took. const endTime = Date.now(); const updateDuration = (endTime - startTime) / 1000.0; @@ -372,12 +375,12 @@ shaka.dash.DashParser = class { * @param {BufferSource} data * @param {string} finalManifestUri The final manifest URI, which may * differ from this.manifestUri_ if there has been a redirect. + * @param {string} rootElement MPD or Patch, depending on context * @return {!Promise} * @private */ - async parseManifest_(data, finalManifestUri) { + async parseManifest_(data, finalManifestUri, rootElement) { let manifestData = data; - const rootElement = this.patchLocationUris_.length ? 'Patch' : 'MPD'; const manifestPreprocessor = this.config_.dash.manifestPreprocessor; const defaultManifestPreprocessor = shaka.util.PlayerConfiguration.defaultManifestPreprocessor; @@ -413,7 +416,7 @@ shaka.dash.DashParser = class { manifestPreprocessorTXml(mpd); } - if (this.patchLocationUris_.length) { + if (rootElement === 'Patch') { return this.processPatchManifest_(mpd); } @@ -493,8 +496,7 @@ shaka.dash.DashParser = class { this.manifestPatchContext_.publishTime = TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0; - // Get patch location element - this.updatePatchLocationUris_(mpd); + this.patchLocationNodes_ = TXml.findChildren(mpd, 'PatchLocation'); let contentSteeringPromise = Promise.resolve(); @@ -817,7 +819,6 @@ shaka.dash.DashParser = class { * @private */ async processPatchManifest_(mpd) { - const StringUtils = shaka.util.StringUtils; const TXml = shaka.util.TXml; /** @type {!Array} */ @@ -827,33 +828,31 @@ shaka.dash.DashParser = class { /** @type {!Set} */ const modifiedTimelines = new Set(); - for (const child of TXml.getChildNodes(mpd)) { + for (const patchNode of TXml.getChildNodes(mpd)) { let handled = true; - const selector = StringUtils.htmlUnescape(child.attributes['sel']); - const paths = TXml.parseXpath(selector); + const paths = TXml.parseXpath(patchNode.attributes['sel'] || ''); const node = paths[paths.length - 1]; - const position = child.attributes['pos'] || null; - const content = TXml.getContents(child) || ''; + const content = TXml.getContents(patchNode) || ''; if (node.name === 'MPD') { if (node.attribute === 'mediaPresentationDuration') { - const content = TXml.getContents(child) || ''; + const content = TXml.getContents(patchNode) || ''; this.parsePatchMediaPresentationDurationChange_(content); } else if (node.attribute === 'type') { this.parsePatchMpdTypeChange_(content); } else if (node.attribute === 'publishTime') { this.manifestPatchContext_.publishTime = TXml.parseDate(content) || 0; - } else if (node.attribute === null && child.tagName === 'add') { - periodAdditions.push(child); + } else if (node.attribute === null && patchNode.tagName === 'add') { + periodAdditions.push(patchNode); } else { handled = false; } } else if (node.name === 'PatchLocation') { - this.updatePatchLocationUris_(child); + this.updatePatchLocationNodes_(patchNode); } else if (node.name === 'Period') { - if (child.tagName === 'add') { - periodAdditions.push(child); - } else if (child.tagName === 'remove' && node.id) { + if (patchNode.tagName === 'add') { + periodAdditions.push(patchNode); + } else if (patchNode.tagName === 'remove' && node.id) { this.removePatchPeriod_(node.id); } } else if (node.name === 'SegmentTemplate' && node.attribute !== null) { @@ -862,8 +861,7 @@ shaka.dash.DashParser = class { modifiedTimelines.add(timeline); } } else if (node.name === 'SegmentTimeline' || node.name === 'S') { - const timelines = this.modifyTimepoints_(paths, child.tagName, position, - TXml.findChildren(child, 'S')); + const timelines = this.modifyTimepoints_(patchNode); for (const timeline of timelines) { modifiedTimelines.add(timeline); } @@ -871,8 +869,8 @@ shaka.dash.DashParser = class { handled = false; } if (!handled) { - shaka.log.warning('Unhandled ' + child.tagName + ' operation', - selector); + shaka.log.warning('Unhandled ' + patchNode.tagName + ' operation', + patchNode.attributes['sel']); } } @@ -967,12 +965,10 @@ shaka.dash.DashParser = class { * @private */ removePatchPeriod_(periodId) { - const Iterables = shaka.util.Iterables; - const contextIds = Iterables.filter(this.contextCache_.keys(), (key) => { - return key.split(',')[0] === periodId; - }); - for (const contextId of contextIds) { - this.contextCache_.delete(contextId); + for (const contextId of this.contextCache_.keys()) { + if (contextId.startsWith(periodId)) { + this.contextCache_.delete(contextId); + } } } @@ -996,20 +992,20 @@ shaka.dash.DashParser = class { } /** @type {!Array} */ - const representationIds = []; + const contextIds = []; if (representationId) { - representationIds.push(representationId); + contextIds.push(periodId + ',' + representationId); } else { for (const context of this.contextCache_.values()) { if (context.period.id === periodId && context.adaptationSet.id === adaptationSetId && context.representation.id) { - representationIds.push(context.representation.id); + contextIds.push(periodId + ',' + context.representation.id); } } } - return representationIds.map((repId) => periodId + ',' + repId); + return contextIds; } /** @@ -1056,29 +1052,21 @@ shaka.dash.DashParser = class { /** * Ingests Patch MPD segments into timeline. * - * @param {!Array} paths - * @param {string} action add, replace or remove - * @param {?string} position - * @param {!Array} segments + * @param {!shaka.extern.xml.Node} patchNode * @return {!Array} context ids with updated timeline * @private */ - modifyTimepoints_(paths, action, position, segments) { + modifyTimepoints_(patchNode) { + const TXml = shaka.util.TXml; const SegmentTemplate = shaka.dash.SegmentTemplate; - let nodePosition = null; - const lastPath = paths[paths.length - 1]; - if (lastPath.name === 'S') { - nodePosition = lastPath.position; - } - + const paths = TXml.parseXpath(patchNode.attributes['sel'] || ''); const contextIds = this.getContextIdsFromPath_(paths); for (const contextId of contextIds) { /** @type {shaka.dash.DashParser.Context} */ const context = this.contextCache_.get(contextId); - SegmentTemplate.modifyTimepoints( - context, action, position, nodePosition, segments); + SegmentTemplate.modifyTimepoints(context, patchNode); } return contextIds; } @@ -2046,7 +2034,7 @@ shaka.dash.DashParser = class { const contextId = context.representation.id ? context.period.id + ',' + context.representation.id : ''; - if (this.patchLocationUris_.length && context.periodInfo.isLastPeriod && + if (this.patchLocationNodes_.length && context.periodInfo.isLastPeriod && representationId) { this.contextCache_.set(`${context.period.id},${representationId}`, this.cloneContext_(context)); @@ -2803,28 +2791,36 @@ shaka.dash.DashParser = class { } /** - * @param {shaka.extern.xml.Node} node + * @param {!shaka.extern.xml.Node} patchNode + * @private + */ + updatePatchLocationNodes_(patchNode) { + const TXml = shaka.util.TXml; + TXml.modifyNodes(this.patchLocationNodes_, patchNode); + } + + /** + * @return {!Array} * @private */ - updatePatchLocationUris_(node) { + getPatchLocationUris_() { const TXml = shaka.util.TXml; - this.patchLocationUris_ = []; const publishTime = this.manifestPatchContext_.publishTime; - let patchLocations = TXml.findChildren(node, 'PatchLocation'); - if (!publishTime || !patchLocations.length) { - return; + if (!publishTime || !this.patchLocationNodes_.length) { + return []; } - patchLocations = patchLocations.filter((patchLocation) => { + const patchLocations = this.patchLocationNodes_.filter((patchLocation) => { const ttl = TXml.parseNonNegativeInt(patchLocation.attributes['ttl']); return !ttl || publishTime + ttl > Date.now() / 1000; }) .map(TXml.getContents) .filter(shaka.util.Functional.isNotNull); - if (patchLocations.length > 0) { - // we are patching - this.patchLocationUris_ = shaka.util.ManifestParserUtils.resolveUris( - this.manifestUris_, patchLocations); + + if (!patchLocations.length) { + return []; } + return shaka.util.ManifestParserUtils.resolveUris( + this.manifestUris_, patchLocations); } }; diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 818a00ff74..1e34a48ace 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -207,12 +207,9 @@ shaka.dash.SegmentTemplate = class { * Ingests Patch MPD segments into timeline. * * @param {!shaka.dash.DashParser.Context} context - * @param {string} action add, replace or remove - * @param {?string} position - * @param {?number} nodePosition - * @param {!Array} segments + * @param {shaka.extern.xml.Node} patchNode */ - static modifyTimepoints(context, action, position, nodePosition, segments) { + static modifyTimepoints(context, patchNode) { const MpdUtils = shaka.dash.MpdUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; const TXml = shaka.util.TXml; @@ -223,23 +220,7 @@ shaka.dash.SegmentTemplate = class { const timepoints = TXml.findChildren(timelineNode, 'S'); goog.asserts.assert(timepoints, 'timepoints should exist'); - let index; - if (nodePosition === null) { - index = position === 'prepend' ? 0 : timepoints.length; - } else { - index = nodePosition; - if (position === 'prepend') { - --index; - } else if (position === 'after') { - ++index; - } - } - if (action === 'remove' || action === 'replace') { - timepoints.splice(index, 1); - } - if (action === 'add' || action === 'replace') { - timepoints.splice(index, 0, ...segments); - } + TXml.modifyNodes(timepoints, patchNode); timelineNode.children = timepoints; } diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 14d9a6e396..e32007247d 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -741,9 +741,10 @@ shaka.util.TXml = class { * @return {!Array} */ static parseXpath(exprString) { + const StringUtils = shaka.util.StringUtils; const returnPaths = []; // Split string by paths but ignore '/' in quotes - const paths = exprString + const paths = StringUtils.htmlUnescape(exprString) .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/); for (const path of paths) { const nodeName = path.match(/^([\w]+)/); @@ -769,6 +770,53 @@ shaka.util.TXml = class { } + /** + * Modifies nodes in specified array by adding or removing nodes + * and updating attributes. + * @param {!Array} nodes + * @param {!shaka.extern.xml.Node} patchNode + */ + static modifyNodes(nodes, patchNode) { + const TXml = shaka.util.TXml; + + const paths = TXml.parseXpath(patchNode.attributes['sel'] || ''); + if (!paths.length) { + return; + } + const lastNode = paths[paths.length - 1]; + const position = patchNode.attributes['pos'] || null; + + let index = lastNode.position; + if (index === null) { + index = position === 'prepend' ? 0 : nodes.length; + } else if (position === 'prepend') { + --index; + } else if (position === 'after') { + ++index; + } + const action = patchNode.tagName; + const attribute = lastNode.attribute; + + // Modify attribute + if (attribute) { + if (action === 'remove') { + delete nodes[index].attributes[attribute]; + } else if (action === 'add' || action === 'replace') { + nodes[index].attributes[attribute] = TXml.getContents(patchNode) || ''; + } + // Rearrange nodes + } else { + if (action === 'remove' || action === 'replace') { + nodes.splice(index, 1); + } + if (action === 'add' || action === 'replace') { + const newNodes = patchNode.children; + nodes.splice(index, 0, ...newNodes); + } + } + } + + /** * Converts a tXml node to DOM element. * @param {shaka.extern.xml.Node} node diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 594a2c1932..13cb10951f 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1622,138 +1622,4 @@ describe('DashParser Live', () => { await updateManifest(); expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); }); - describe('Patch MPD', () => { - const manifestRequest = shaka.net.NetworkingEngine.RequestType.MANIFEST; - const manifestContext = { - type: shaka.net.NetworkingEngine.AdvancedRequestType.MPD, - }; - const manifestText = [ - '', - ' dummy://bar', - ' ', - ' ', - ' ', - ' http://example.com', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join('\n'); - - beforeEach(() => { - fakeNetEngine.setResponseText('dummy://foo', manifestText); - }); - - it('uses Mpd.PatchLocation', async () => { - await parser.start('dummy://foo', playerInterface); - - expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); - fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); - fakeNetEngine.request.calls.reset(); - - const patchText = [ - '', - '', - ].join('\n'); - fakeNetEngine.setResponseText('dummy://bar', patchText); - - await updateManifest(); - expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); - fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); - }); - - it('transforms from dynamic to static', async () => { - const patchText = [ - '', - ' ', - ' static', - ' ', - ' ', - ' PT28462.033599998S', - ' ', - '', - ].join('\n'); - fakeNetEngine.setResponseText('dummy://bar', patchText); - - const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.presentationTimeline.isLive()).toBe(true); - expect(manifest.presentationTimeline.getDuration()).toBe(Infinity); - - /** @type {!jasmine.Spy} */ - const tickAfter = updateTickSpy(); - await updateManifest(); - expect(manifest.presentationTimeline.isLive()).toBe(false); - expect(manifest.presentationTimeline.getDuration()).not.toBe(Infinity); - // should stop updates after transition to static - expect(tickAfter).not.toHaveBeenCalled(); - }); - - it('adds new period', async () => { - const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.variants[0].video; - const patchText = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join('\n'); - fakeNetEngine.setResponseText('dummy://bar', patchText); - - await stream.createSegmentIndex(); - - expect(stream.matchedStreams.length).toBe(1); - - await updateManifest(); - await stream.createSegmentIndex(); - - expect(stream.matchedStreams.length).toBe(2); - }); - - it('adds new segments to the existing period', async () => { - const xPath = '/' + [ - 'MPD', - 'Period[@id=\'1\']', - 'AdaptationSet[@id=\'1\']', - 'Representation[@id=\'3\']', - 'SegmentTemplate', - 'SegmentTimeline', - ].join('/'); - const patchText = [ - '', - ' ', - ' ', - ' ', - '', - ].join('\n'); - fakeNetEngine.setResponseText('dummy://bar', patchText); - - const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.variants[0].video; - expect(stream).toBeTruthy(); - await stream.createSegmentIndex(); - ManifestParser.verifySegmentIndex(stream, [ - ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), - ]); - - await updateManifest(); - ManifestParser.verifySegmentIndex(stream, [ - ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), - ManifestParser.makeReference('s2.mp4', 1, 2, originalUri), - ]); - }); - }); }); diff --git a/test/dash/dash_parser_patch_unit.js b/test/dash/dash_parser_patch_unit.js new file mode 100644 index 0000000000..a51bc58206 --- /dev/null +++ b/test/dash/dash_parser_patch_unit.js @@ -0,0 +1,385 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('DashParser Patch', () => { + const Util = shaka.test.Util; + const ManifestParser = shaka.test.ManifestParser; + + const oldNow = Date.now; + const updateTime = 5; + const ttl = 60; + const originalUri = 'http://example.com/'; + const manifestRequest = shaka.net.NetworkingEngine.RequestType.MANIFEST; + const manifestContext = { + type: shaka.net.NetworkingEngine.AdvancedRequestType.MPD, + }; + + /** @type {!shaka.test.FakeNetworkingEngine} */ + let fakeNetEngine; + /** @type {!shaka.dash.DashParser} */ + let parser; + /** @type {shaka.extern.ManifestParser.PlayerInterface} */ + let playerInterface; + /** @type {!Date} */ + let publishTime; + + beforeEach(() => { + publishTime = new Date(2024, 0, 1); + fakeNetEngine = new shaka.test.FakeNetworkingEngine(); + parser = new shaka.dash.DashParser(); + parser.configure(shaka.util.PlayerConfiguration.createDefault().manifest); + playerInterface = { + networkingEngine: fakeNetEngine, + filter: (manifest) => Promise.resolve(), + makeTextStreamsForClosedCaptions: (manifest) => {}, + onTimelineRegionAdded: fail, // Should not have any EventStream elements. + onEvent: fail, + onError: fail, + isLowLatencyMode: () => false, + isAutoLowLatencyMode: () => false, + enableLowLatencyMode: () => {}, + updateDuration: () => {}, + newDrmInfo: (stream) => {}, + onManifestUpdated: () => {}, + getBandwidthEstimate: () => 1e6, + }; + Date.now = () => publishTime.getTime() + 10; + + const manifestText = [ + '', + ' dummy://bar', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://foo', manifestText); + }); + + afterEach(() => { + // Dash parser stop is synchronous. + parser.stop(); + Date.now = oldNow; + }); + + /** + * Trigger a manifest update. + * @suppress {accessControls} + */ + async function updateManifest() { + if (parser.updateTimer_) { + parser.updateTimer_.tickNow(); + } + await Util.shortDelay(); // Allow update to complete. + } + + /** + * Gets a spy on the function that sets the update period. + * @return {!jasmine.Spy} + * @suppress {accessControls} + */ + function updateTickSpy() { + return spyOn(parser.updateTimer_, 'tickAfter'); + } + + describe('MPD', () => { + it('transforms from dynamic to static', async () => { + const patchText = [ + '', + ' ', + ' static', + ' ', + ' ', + ' PT28462.033599998S', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + expect(manifest.presentationTimeline.isLive()).toBe(true); + expect(manifest.presentationTimeline.getDuration()).toBe(Infinity); + + /** @type {!jasmine.Spy} */ + const tickAfter = updateTickSpy(); + await updateManifest(); + expect(manifest.presentationTimeline.isLive()).toBe(false); + expect(manifest.presentationTimeline.getDuration()).not.toBe(Infinity); + // should stop updates after transition to static + expect(tickAfter).not.toHaveBeenCalled(); + }); + }); + + describe('PatchLocation', () => { + beforeEach(() => { + const patchText = ''; + fakeNetEngine.setResponseText('dummy://bar', patchText); + }); + + it('uses PatchLocation', async () => { + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); + }); + + it('does not use PatchLocation if publishTime is not defined', async () => { + const manifestText = [ + '', + ' dummy://bar', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://foo', manifestText); + + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.expectNoRequest('dummy://bar', manifestRequest, manifestContext); + }); + + it('does not use PatchLocation if it expired', async () => { + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + // Make current time exceed Patch's TTL. + Date.now = () => publishTime.getTime() + (ttl * 2) * 1000; + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.expectNoRequest('dummy://bar', manifestRequest, manifestContext); + }); + + it('replaces PatchLocation with new URL', async () => { + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + const patchText = [ + '', + ' ', + ' dummy://bar2', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + fakeNetEngine.setResponseText('dummy://bar2', patchText); + // Another request should be made to new URI. + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar2', manifestRequest, manifestContext); + }); + }); + + describe('Period', () => { + it('adds new period as an MPD child', async () => { + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + const patchText = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(1); + + await updateManifest(); + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(2); + }); + + it('adds new period as a Period successor', async () => { + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + const patchText = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(1); + + await updateManifest(); + await stream.createSegmentIndex(); + + expect(stream.matchedStreams.length).toBe(2); + }); + }); + + describe('SegmentTimeline', () => { + it('adds new S elements as SegmentTimeline children', async () => { + const xPath = '/' + [ + 'MPD', + 'Period[@id=\'1\']', + 'AdaptationSet[@id=\'1\']', + 'Representation[@id=\'3\']', + 'SegmentTemplate', + 'SegmentTimeline', + ].join('/'); + const patchText = [ + '', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + expect(stream).toBeTruthy(); + await stream.createSegmentIndex(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ]); + + await updateManifest(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ManifestParser.makeReference('s2.mp4', 1, 2, originalUri), + ]); + }); + + it('adds new S elements as S successor', async () => { + const xPath = '/' + [ + 'MPD', + 'Period[@id=\'1\']', + 'AdaptationSet[@id=\'1\']', + 'Representation[@id=\'3\']', + 'SegmentTemplate', + 'SegmentTimeline', + 'S', + ].join('/'); + const patchText = [ + '', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + expect(stream).toBeTruthy(); + await stream.createSegmentIndex(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ]); + + await updateManifest(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ManifestParser.makeReference('s2.mp4', 1, 2, originalUri), + ]); + }); + + it('modify @r attribute of an S element', async () => { + const xPath = '/' + [ + 'MPD', + 'Period[@id=\'1\']', + 'AdaptationSet[@id=\'1\']', + 'Representation[@id=\'3\']', + 'SegmentTemplate', + 'SegmentTimeline', + 'S[1]/@r', + ].join('/'); + const patchText = [ + '', + ' ', + ' 2', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + const manifest = await parser.start('dummy://foo', playerInterface); + const stream = manifest.variants[0].video; + expect(stream).toBeTruthy(); + await stream.createSegmentIndex(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ]); + + await updateManifest(); + ManifestParser.verifySegmentIndex(stream, [ + ManifestParser.makeReference('s1.mp4', 0, 1, originalUri), + ManifestParser.makeReference('s2.mp4', 1, 2, originalUri), + ManifestParser.makeReference('s3.mp4', 2, 3, originalUri), + ]); + }); + }); +}); From 97329c490d9c88bbf8322155f5aff0007198b7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 24 May 2024 14:58:22 +0200 Subject: [PATCH 41/48] remove SegmentInfo cache --- lib/dash/dash_parser.js | 83 +++++------------------------------- lib/dash/mpd_utils.js | 46 ++++++++------------ lib/dash/segment_template.js | 39 +++-------------- lib/util/tXml.js | 2 +- 4 files changed, 36 insertions(+), 134 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 3e286da0ef..df5c334932 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -682,7 +682,6 @@ shaka.dash.DashParser = class { bandwidth: 0, indexRangeWarningGiven: false, availabilityTimeOffset: availabilityTimeOffset, - segmentInfo: null, mediaPresentationDuration: null, profiles: profiles.split(','), }; @@ -855,8 +854,8 @@ shaka.dash.DashParser = class { } else if (patchNode.tagName === 'remove' && node.id) { this.removePatchPeriod_(node.id); } - } else if (node.name === 'SegmentTemplate' && node.attribute !== null) { - const timelines = this.modifySegmentTemplateAttribute_(paths, content); + } else if (node.name === 'SegmentTemplate') { + const timelines = this.modifySegmentTemplate_(patchNode); for (const timeline of timelines) { modifiedTimelines.add(timeline); } @@ -949,7 +948,6 @@ shaka.dash.DashParser = class { indexRangeWarningGiven: false, availabilityTimeOffset: this.manifestPatchContext_.availabilityTimeOffset, profiles: this.manifestPatchContext_.profiles, - segmentInfo: null, mediaPresentationDuration: this.manifestPatchContext_.mediaPresentationDuration, }; @@ -1009,42 +1007,21 @@ shaka.dash.DashParser = class { } /** - * Modifeis SegmentTemplate attribute based on MPD patch + * Modifies SegmentTemplate based on MPD patch * - * @param {!Array} paths - * @param {string} content + * @param {!shaka.extern.xml.Node} patchNode * @return {!Array} context ids with updated timeline * @private */ - modifySegmentTemplateAttribute_(paths, content) { + modifySegmentTemplate_(patchNode) { + const TXml = shaka.util.TXml; + const paths = TXml.parseXpath(patchNode.attributes['sel'] || ''); const contextIds = this.getContextIdsFromPath_(paths); - const lastPath = paths[paths.length - 1]; - const attribute = lastPath.attribute; - const contentAsNumber = parseInt(content, 10); + for (const contextId of contextIds) { /** @type {shaka.dash.DashParser.Context} */ const context = this.contextCache_.get(contextId); - const segmentInfo = context.segmentInfo; - switch (attribute) { - case 'timescale': - segmentInfo.timescale = contentAsNumber; - break; - case 'duration': - segmentInfo.duration = contentAsNumber; - break; - case 'startNumber': - segmentInfo.startNumber = contentAsNumber; - break; - case 'presentationTimeOffset': - segmentInfo.presentationTimeOffset = contentAsNumber; - break; - case 'media': - segmentInfo.media = content; - break; - case 'index': - segmentInfo.index = content; - break; - } + TXml.modifyNodes([context.representation.segmentTemplate], patchNode); } return contextIds; } @@ -2066,7 +2043,6 @@ shaka.dash.DashParser = class { this.contextCache_.set(`${context.period.id},${representationId}`, this.cloneContext_(context)); } - context.segmentInfo = null; /** @type {shaka.extern.Stream} */ let stream; @@ -2837,9 +2813,10 @@ shaka.dash.DashParser = class { if (!publishTime || !this.patchLocationNodes_.length) { return []; } + const now = Date.now() / 1000; const patchLocations = this.patchLocationNodes_.filter((patchLocation) => { const ttl = TXml.parseNonNegativeInt(patchLocation.attributes['ttl']); - return !ttl || publishTime + ttl > Date.now() / 1000; + return !ttl || publishTime + ttl > now; }) .map(TXml.getContents) .filter(shaka.util.Functional.isNotNull); @@ -2879,41 +2856,6 @@ shaka.dash.DashParser = class { shaka.dash.DashParser.PatchContext; -/** - * @typedef {{ - * timescale: number, - * duration: number, - * startNumber: number, - * presentationTimeOffset: number, - * media: ?string, - * index: ?string, - * timeline: Array - * }} - * - * @description - * A collection of elements and properties which are inherited across levels - * of a DASH manifest. - * - * @property {number} timescale - * Specifies timescale for this Representation. - * @property {number} duration - * Specifies the duration of each Segment in units of a time. - * @property {number} startNumber - * Specifies the number of the first segment in the Period assigned - * to a Representation. - * @property {number} presentationTimeOffset - * Specifies the presentation time offset of the Representation relative - * to the start of the Period. - * @property {?string} media - * The template for the Media Segment assigned to a Representation. - * @property {?string} index - * The index template for the Media Segment assigned to a Representation. - * @property {Array} timeline - * The timeline of the representation. - */ -shaka.dash.DashParser.SegmentInfo; - - /** * @const {string} * @private @@ -3014,7 +2956,6 @@ shaka.dash.DashParser.InheritanceFrame; * bandwidth: number, * indexRangeWarningGiven: boolean, * availabilityTimeOffset: number, - * segmentInfo: ?shaka.dash.DashParser.SegmentInfo, * mediaPresentationDuration: ?number, * profiles: !Array. * }} @@ -3045,8 +2986,6 @@ shaka.dash.DashParser.InheritanceFrame; * @property {!Array.} profiles * Profiles of DASH are defined to enable interoperability and the signaling * of the use of features. - * @property {?shaka.dash.DashParser.SegmentInfo} segmentInfo - * The segment info for current representation. * @property {?number} mediaPresentationDuration * Media presentation duration, or null if unknown. */ diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 8c44412b18..22e14e7c45 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -265,44 +265,32 @@ shaka.dash.MpdUtils = class { */ static parseSegmentInfo(context, callback) { goog.asserts.assert( - callback(context.representation) || context.segmentInfo, + callback(context.representation), 'There must be at least one element of the given type ' + 'or segment info defined.'); const MpdUtils = shaka.dash.MpdUtils; const TXml = shaka.util.TXml; let timescale = 1; - if (context.segmentInfo) { - timescale = context.segmentInfo.timescale; - } else { - const timescaleStr = - MpdUtils.inheritAttribute(context, callback, 'timescale'); - - if (timescaleStr) { - timescale = TXml.parsePositiveInt(timescaleStr) || 1; - } + const timescaleStr = + MpdUtils.inheritAttribute(context, callback, 'timescale'); + + if (timescaleStr) { + timescale = TXml.parsePositiveInt(timescaleStr) || 1; } let segmentDuration = 0; - if (context.segmentInfo) { - segmentDuration = context.segmentInfo.duration; - } else { - const durationStr = - MpdUtils.inheritAttribute(context, callback, 'duration'); - segmentDuration = TXml.parsePositiveInt(durationStr || ''); - if (segmentDuration) { - segmentDuration /= timescale; - } + const durationStr = + MpdUtils.inheritAttribute(context, callback, 'duration'); + segmentDuration = TXml.parsePositiveInt(durationStr || ''); + if (segmentDuration) { + segmentDuration /= timescale; } let startNumber = 1; - if (context.segmentInfo) { - startNumber = context.segmentInfo.startNumber; - } else { - const startNumberStr = - MpdUtils.inheritAttribute(context, callback, 'startNumber'); - startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); - if (startNumberStr == null || startNumber == null) { - startNumber = 1; - } + const startNumberStr = + MpdUtils.inheritAttribute(context, callback, 'startNumber'); + startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); + if (startNumberStr == null || startNumber == null) { + startNumber = 1; } const unscaledPresentationTimeOffset = @@ -342,7 +330,7 @@ shaka.dash.MpdUtils = class { static getNodes(context, callback) { const Functional = shaka.util.Functional; goog.asserts.assert( - callback(context.representation) || context.segmentInfo, + callback(context.representation), 'There must be at least one element of the given type ' + 'or segment info defined.', ); diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 1e34a48ace..de5b3a8418 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -46,12 +46,9 @@ shaka.dash.SegmentTemplate = class { static createStreamInfo( context, requestSegment, streamMap, isUpdate, segmentLimit, periodDurationMap, aesKey, lastSegmentNumber, isPatchUpdate) { - if (!isPatchUpdate) { - context.segmentInfo = null; - } - goog.asserts.assert(context.representation.segmentTemplate || - context.segmentInfo, 'Should only be called with SegmentTemplate ' + - 'or segment info defined'); + goog.asserts.assert(context.representation.segmentTemplate, + 'Should only be called with SegmentTemplate ' + + 'or segment info defined'); const MpdUtils = shaka.dash.MpdUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; const TimelineSegmentIndex = shaka.dash.TimelineSegmentIndex; @@ -68,18 +65,6 @@ shaka.dash.SegmentTemplate = class { /** @type {shaka.dash.SegmentTemplate.SegmentTemplateInfo} */ const info = SegmentTemplate.parseSegmentTemplateInfo_(context); - if (!context.segmentInfo) { - context.segmentInfo = { - timescale: info.timescale, - media: info.mediaTemplate, - index: info.indexTemplate, - startNumber: info.startNumber, - duration: info.segmentDuration || 0, - presentationTimeOffset: info.unscaledPresentationTimeOffset, - timeline: info.timeline, - }; - } - SegmentTemplate.checkSegmentTemplateInfo_(context, info); // Direct fields of context will be reassigned by the parser before @@ -247,21 +232,11 @@ shaka.dash.SegmentTemplate = class { const segmentInfo = MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_); - let media = null; - if (context.segmentInfo) { - media = context.segmentInfo.media; - } else { - media = MpdUtils.inheritAttribute( - context, SegmentTemplate.fromInheritance_, 'media'); - } + const media = MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'media'); - let index = null; - if (context.segmentInfo) { - index = context.segmentInfo.index; - } else { - index = MpdUtils.inheritAttribute( - context, SegmentTemplate.fromInheritance_, 'index'); - } + const index = MpdUtils.inheritAttribute( + context, SegmentTemplate.fromInheritance_, 'index'); return { segmentDuration: segmentInfo.segmentDuration, diff --git a/lib/util/tXml.js b/lib/util/tXml.js index e32007247d..93c9256e38 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -788,7 +788,7 @@ shaka.util.TXml = class { let index = lastNode.position; if (index === null) { - index = position === 'prepend' ? 0 : nodes.length; + index = position === 'after' ? nodes.length : 0; } else if (position === 'prepend') { --index; } else if (position === 'after') { From c2f13ab2e6cf776c9c709078b3fb724198f1ac13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 08:15:38 +0200 Subject: [PATCH 42/48] fix SegmentTemplate modifications --- lib/dash/dash_parser.js | 12 ++++++++++-- lib/util/tXml.js | 24 ++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index df5c334932..c39be82a45 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -1007,7 +1007,7 @@ shaka.dash.DashParser = class { } /** - * Modifies SegmentTemplate based on MPD patch + * Modifies SegmentTemplate based on MPD patch. * * @param {!shaka.extern.xml.Node} patchNode * @return {!Array} context ids with updated timeline @@ -1016,12 +1016,20 @@ shaka.dash.DashParser = class { modifySegmentTemplate_(patchNode) { const TXml = shaka.util.TXml; const paths = TXml.parseXpath(patchNode.attributes['sel'] || ''); + const lastPath = paths[paths.length - 1]; + if (!lastPath.attribute) { + return []; + } const contextIds = this.getContextIdsFromPath_(paths); + const content = TXml.getContents(patchNode) || ''; for (const contextId of contextIds) { /** @type {shaka.dash.DashParser.Context} */ const context = this.contextCache_.get(contextId); - TXml.modifyNodes([context.representation.segmentTemplate], patchNode); + goog.asserts.assert(context && context.representation.segmentTemplate, + 'cannot modify segment template'); + TXml.modifyNodeAttribute(context.representation.segmentTemplate, + patchNode.tagName, lastPath.attribute, content); } return contextIds; } diff --git a/lib/util/tXml.js b/lib/util/tXml.js index 93c9256e38..2d5e01e427 100644 --- a/lib/util/tXml.js +++ b/lib/util/tXml.js @@ -788,7 +788,7 @@ shaka.util.TXml = class { let index = lastNode.position; if (index === null) { - index = position === 'after' ? nodes.length : 0; + index = position === 'prepend' ? 0 : nodes.length; } else if (position === 'prepend') { --index; } else if (position === 'after') { @@ -799,11 +799,8 @@ shaka.util.TXml = class { // Modify attribute if (attribute) { - if (action === 'remove') { - delete nodes[index].attributes[attribute]; - } else if (action === 'add' || action === 'replace') { - nodes[index].attributes[attribute] = TXml.getContents(patchNode) || ''; - } + TXml.modifyNodeAttribute(nodes[index], action, attribute, + TXml.getContents(patchNode) || ''); // Rearrange nodes } else { if (action === 'remove' || action === 'replace') { @@ -817,6 +814,21 @@ shaka.util.TXml = class { } + /** + * @param {!shaka.extern.xml.Node} node + * @param {string} action + * @param {string} attribute + * @param {string} value + */ + static modifyNodeAttribute(node, action, attribute, value) { + if (action === 'remove') { + delete node.attributes[attribute]; + } else if (action === 'add' || action === 'replace') { + node.attributes[attribute] = value; + } + } + + /** * Converts a tXml node to DOM element. * @param {shaka.extern.xml.Node} node From ad7d852f646859beb0696efe1e59dc0c87a670a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 09:16:11 +0200 Subject: [PATCH 43/48] validate MPD ID & publish time --- lib/dash/dash_parser.js | 22 ++++- lib/util/error.js | 5 ++ test/dash/dash_parser_patch_unit.js | 119 +++++++++++++++++++++++----- 3 files changed, 126 insertions(+), 20 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index c39be82a45..69e65f5d24 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -71,6 +71,7 @@ shaka.dash.DashParser = class { * @private {!shaka.dash.DashParser.PatchContext} */ this.manifestPatchContext_ = { + mpdId: '', type: '', profiles: [], mediaPresentationDuration: null, @@ -233,6 +234,7 @@ shaka.dash.DashParser = class { this.streamMap_ = {}; this.contextCache_.clear(); this.manifestPatchContext_ = { + mpdId: '', type: '', profiles: [], mediaPresentationDuration: null, @@ -494,6 +496,7 @@ shaka.dash.DashParser = class { manifestBaseUris = locations; } + this.manifestPatchContext_.mpdId = mpd.attributes['id'] || ''; this.manifestPatchContext_.publishTime = TXml.parseAttr(mpd, 'publishTime', TXml.parseDate) || 0; this.patchLocationNodes_ = TXml.findChildren(mpd, 'PatchLocation'); @@ -820,6 +823,19 @@ shaka.dash.DashParser = class { async processPatchManifest_(mpd) { const TXml = shaka.util.TXml; + const mpdId = mpd.attributes['mpdId']; + const originalPublishTime = TXml.parseAttr(mpd, 'originalPublishTime', + TXml.parseDate); + if (!mpdId || mpdId !== this.manifestPatchContext_.mpdId || + originalPublishTime !== this.manifestPatchContext_.publishTime) { + // Clean patch location nodes, so it will force full MPD update. + this.patchLocationNodes_ = []; + throw new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.DASH_INVALID_PATCH); + } + /** @type {!Array} */ const newPeriods = []; /** @type {!Array} */ @@ -2817,8 +2833,9 @@ shaka.dash.DashParser = class { */ getPatchLocationUris_() { const TXml = shaka.util.TXml; + const mpdId = this.manifestPatchContext_.mpdId; const publishTime = this.manifestPatchContext_.publishTime; - if (!publishTime || !this.patchLocationNodes_.length) { + if (!mpdId || !publishTime || !this.patchLocationNodes_.length) { return []; } const now = Date.now() / 1000; @@ -2839,6 +2856,7 @@ shaka.dash.DashParser = class { /** * @typedef {{ + * mpdId: string, * type: string, * mediaPresentationDuration: ?number, * profiles: !Array., @@ -2847,6 +2865,8 @@ shaka.dash.DashParser = class { * publishTime: number * }} * + * @property {string} mpdId + * ID of the original MPD file. * @property {string} type * Specifies the type of the dash manifest i.e. "static" * @property {?number} mediaPresentationDuration diff --git a/lib/util/error.js b/lib/util/error.js index 38d369567d..d5953d6e30 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -772,6 +772,11 @@ shaka.util.Error.Code = { */ 'DASH_UNSUPPORTED_AES_128': 4051, + /** + * Patch requested during an update did not match original manifest. + */ + 'DASH_INVALID_PATCH': 4052, + // RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000, // RETIRED: 'INVALID_SEGMENT_INDEX': 5001, diff --git a/test/dash/dash_parser_patch_unit.js b/test/dash/dash_parser_patch_unit.js index a51bc58206..c2ab91e70a 100644 --- a/test/dash/dash_parser_patch_unit.js +++ b/test/dash/dash_parser_patch_unit.js @@ -9,6 +9,7 @@ describe('DashParser Patch', () => { const ManifestParser = shaka.test.ManifestParser; const oldNow = Date.now; + const mpdId = 'foo'; const updateTime = 5; const ttl = 60; const originalUri = 'http://example.com/'; @@ -49,11 +50,12 @@ describe('DashParser Patch', () => { Date.now = () => publishTime.getTime() + 10; const manifestText = [ - '', - ' dummy://bar', + ` minimumUpdatePeriod="PT${updateTime}S">`, + ` dummy://bar`, ' ', ' ', ' ', @@ -98,9 +100,77 @@ describe('DashParser Patch', () => { } describe('MPD', () => { + it('rolls back to regular update if id mismatches', async () => { + const patchText = [ + '', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + /** @type {!jasmine.Spy} */ + const onError = jasmine.createSpy('onError'); + playerInterface.onError = Util.spyFunc(onError); + + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); + expect(onError).toHaveBeenCalledOnceWith(new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.DASH_INVALID_PATCH)); + + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + }); + + it('rolls back to regular update if publishTime mismatches', async () => { + const publishTime = new Date(1992, 5, 2); + const patchText = [ + `', + ].join('\n'); + fakeNetEngine.setResponseText('dummy://bar', patchText); + + /** @type {!jasmine.Spy} */ + const onError = jasmine.createSpy('onError'); + playerInterface.onError = Util.spyFunc(onError); + + await parser.start('dummy://foo', playerInterface); + + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); + expect(onError).toHaveBeenCalledOnceWith(new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.DASH_INVALID_PATCH)); + + fakeNetEngine.request.calls.reset(); + + await updateManifest(); + expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); + fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext); + }); + it('transforms from dynamic to static', async () => { const patchText = [ - '', + ``, ' ', ' static', ' ', @@ -127,7 +197,11 @@ describe('DashParser Patch', () => { describe('PatchLocation', () => { beforeEach(() => { - const patchText = ''; + const patchText = [ + `', + ].join('\n'); fakeNetEngine.setResponseText('dummy://bar', patchText); }); @@ -145,10 +219,11 @@ describe('DashParser Patch', () => { it('does not use PatchLocation if publishTime is not defined', async () => { const manifestText = [ - '', - ' dummy://bar', + ` minimumUpdatePeriod="PT${updateTime}S">`, + ` dummy://bar`, ' ', ' ', ' ', @@ -200,9 +275,10 @@ describe('DashParser Patch', () => { fakeNetEngine.request.calls.reset(); const patchText = [ - '', + ``, ' ', - ' dummy://bar2', + ` dummy://bar2`, ' ', '', ].join('\n'); @@ -226,7 +302,8 @@ describe('DashParser Patch', () => { const manifest = await parser.start('dummy://foo', playerInterface); const stream = manifest.variants[0].video; const patchText = [ - '', + ``, ' ', ' ', ' ', @@ -254,7 +331,8 @@ describe('DashParser Patch', () => { const manifest = await parser.start('dummy://foo', playerInterface); const stream = manifest.variants[0].video; const patchText = [ - '', + ``, ' ', ' ', ' ', @@ -290,8 +368,9 @@ describe('DashParser Patch', () => { 'SegmentTimeline', ].join('/'); const patchText = [ - '', - ' ', + ``, + ` `, ' ', ' ', '', @@ -324,8 +403,9 @@ describe('DashParser Patch', () => { 'S', ].join('/'); const patchText = [ - '', - ' ', + ``, + ` `, ' ', ' ', '', @@ -358,8 +438,9 @@ describe('DashParser Patch', () => { 'S[1]/@r', ].join('/'); const patchText = [ - '', - ' ', + ``, + ` `, ' 2', ' ', '', From e0a56022fc7027a95018a733651177320fe8f7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 09:20:33 +0200 Subject: [PATCH 44/48] fix tests --- test/dash/dash_parser_patch_unit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dash/dash_parser_patch_unit.js b/test/dash/dash_parser_patch_unit.js index c2ab91e70a..d4a5c75e3d 100644 --- a/test/dash/dash_parser_patch_unit.js +++ b/test/dash/dash_parser_patch_unit.js @@ -121,7 +121,7 @@ describe('DashParser Patch', () => { await updateManifest(); expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); - expect(onError).toHaveBeenCalledOnceWith(new shaka.util.Error( + expect(onError).toHaveBeenCalledWith(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_INVALID_PATCH)); @@ -155,7 +155,7 @@ describe('DashParser Patch', () => { await updateManifest(); expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); fakeNetEngine.expectRequest('dummy://bar', manifestRequest, manifestContext); - expect(onError).toHaveBeenCalledOnceWith(new shaka.util.Error( + expect(onError).toHaveBeenCalledWith(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_INVALID_PATCH)); From 6438ce5a6611358a4dadb36f71fc74e9054b06ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 09:32:42 +0200 Subject: [PATCH 45/48] Revert formatting --- lib/dash/mpd_utils.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 22e14e7c45..605f5029dd 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -266,37 +266,34 @@ shaka.dash.MpdUtils = class { static parseSegmentInfo(context, callback) { goog.asserts.assert( callback(context.representation), - 'There must be at least one element of the given type ' + - 'or segment info defined.'); + 'There must be at least one element of the given type.'); const MpdUtils = shaka.dash.MpdUtils; const TXml = shaka.util.TXml; - let timescale = 1; const timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale'); - + let timescale = 1; if (timescaleStr) { timescale = TXml.parsePositiveInt(timescaleStr) || 1; } - let segmentDuration = 0; + const durationStr = MpdUtils.inheritAttribute(context, callback, 'duration'); - segmentDuration = TXml.parsePositiveInt(durationStr || ''); + let segmentDuration = TXml.parsePositiveInt(durationStr || ''); if (segmentDuration) { segmentDuration /= timescale; } - let startNumber = 1; + const startNumberStr = MpdUtils.inheritAttribute(context, callback, 'startNumber'); - startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); + const unscaledPresentationTimeOffset = + Number(MpdUtils.inheritAttribute(context, callback, + 'presentationTimeOffset')) || 0; + let startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); if (startNumberStr == null || startNumber == null) { startNumber = 1; } - const unscaledPresentationTimeOffset = - Number(MpdUtils.inheritAttribute(context, callback, - 'presentationTimeOffset')) || 0; - /** @type {Array.} */ let timeline = null; const timelineNode = @@ -331,8 +328,7 @@ shaka.dash.MpdUtils = class { const Functional = shaka.util.Functional; goog.asserts.assert( callback(context.representation), - 'There must be at least one element of the given type ' + - 'or segment info defined.', + 'There must be at least one element of the given type.', ); return [ From 7bf7b08b309c97b26ba67d82eed72e922cab19ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 09:50:21 +0200 Subject: [PATCH 46/48] update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 51eae93984..f7d4720580 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ DASH features supported: - CEA-608/708 captions - Multi-codec variants (on platforms with changeType support) - MPD chaining + - MPD Patch updates for SegmentTemplate with $Number$, SegmentTimeline with + $Number$ and SegmentTimeline with $Time$ DASH features **not** supported: - Xlink with actuate=onRequest @@ -116,6 +118,7 @@ DASH features **not** supported: bitrates - Timescales so large that timestamps cannot be represented as integers in JavaScript (2^53): https://github.com/shaka-project/shaka-player/issues/1667 + - Modifying elements with an @schemeIdUri attribute via MPD Patch ## HLS features From 682654c6d4c26ee18483ee41b93a646f6da0e6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 27 May 2024 10:56:37 +0200 Subject: [PATCH 47/48] update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f7d4720580..05b4454df2 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ DASH features **not** supported: - Timescales so large that timestamps cannot be represented as integers in JavaScript (2^53): https://github.com/shaka-project/shaka-player/issues/1667 - Modifying elements with an @schemeIdUri attribute via MPD Patch + - Xlink dereferencing with MPD Patch ## HLS features From 9985e6658e59823a8589b6db5cf8bd8b4bc32698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 28 May 2024 15:10:08 +0200 Subject: [PATCH 48/48] multiperiod fixes --- lib/dash/dash_parser.js | 7 +++-- lib/dash/segment_template.js | 28 ++++++++++++++++--- .../dash/dash_parser_segment_template_unit.js | 21 ++++++++++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 69e65f5d24..1a406ca7d6 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -979,8 +979,12 @@ shaka.dash.DashParser = class { * @private */ removePatchPeriod_(periodId) { + const SegmentTemplate = shaka.dash.SegmentTemplate; for (const contextId of this.contextCache_.keys()) { if (contextId.startsWith(periodId)) { + const context = this.contextCache_.get(contextId); + SegmentTemplate.removeTimepoints(context); + this.parsePatchSegment_(contextId); this.contextCache_.delete(contextId); } } @@ -2062,8 +2066,7 @@ shaka.dash.DashParser = class { const contextId = context.representation.id ? context.period.id + ',' + context.representation.id : ''; - if (this.patchLocationNodes_.length && context.periodInfo.isLastPeriod && - representationId) { + if (this.patchLocationNodes_.length && representationId) { this.contextCache_.set(`${context.period.id},${representationId}`, this.cloneContext_(context)); } diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index de5b3a8418..d654095edc 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -209,6 +209,21 @@ shaka.dash.SegmentTemplate = class { timelineNode.children = timepoints; } + /** + * Removes all segments from timeline. + * + * @param {!shaka.dash.DashParser.Context} context + */ + static removeTimepoints(context) { + const MpdUtils = shaka.dash.MpdUtils; + const SegmentTemplate = shaka.dash.SegmentTemplate; + + const timelineNode = MpdUtils.inheritChild(context, + SegmentTemplate.fromInheritance_, 'SegmentTimeline'); + goog.asserts.assert(timelineNode, 'timeline node not found'); + timelineNode.children = []; + } + /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame * @return {?shaka.extern.xml.Node} @@ -752,10 +767,15 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex { this.templateInfo_.mediaTemplate = info.mediaTemplate; // Append timeline - const lastCurrentEntry = currentTimeline[currentTimeline.length - 1]; - const newEntries = info.timeline.filter((entry) => { - return entry.start >= lastCurrentEntry.end; - }); + let newEntries; + if (currentTimeline.length) { + const lastCurrentEntry = currentTimeline[currentTimeline.length - 1]; + newEntries = info.timeline.filter((entry) => { + return entry.start >= lastCurrentEntry.end; + }); + } else { + newEntries = info.timeline.slice(); + } if (newEntries.length > 0) { shaka.log.debug(`Appending ${newEntries.length} entries`); diff --git a/test/dash/dash_parser_segment_template_unit.js b/test/dash/dash_parser_segment_template_unit.js index fa0a3c08dc..14ab181fd8 100644 --- a/test/dash/dash_parser_segment_template_unit.js +++ b/test/dash/dash_parser_segment_template_unit.js @@ -790,6 +790,19 @@ describe('DashParser SegmentTemplate', () => { expect(index.find(newStart)).toBe(10); expect(index.find(newEnd - 1.0)).toBe(19); }); + + it('appends new timeline to empty one', async () => { + const info = makeTemplateInfo([]); + const index = await makeTimelineSegmentIndex(info, false); + + const newRanges = makeRanges(0, 2.0, 10); + const newTemplateInfo = makeTemplateInfo(newRanges); + + const newEnd = newRanges[newRanges.length - 1].end; + index.appendTemplateInfo(newTemplateInfo, /* periodStart= */ 0, newEnd); + expect(index.find(0)).toBe(0); + expect(index.find(newEnd - 1.0)).toBe(9); + }); }); describe('evict', () => { @@ -814,8 +827,10 @@ describe('DashParser SegmentTemplate', () => { */ async function makeTimelineSegmentIndex(info, delayPeriodEnd = true, shouldFit = false) { + const isTimeline = info.timeline.length > 0; // Period end may be a bit after the last timeline entry - let periodEnd = info.timeline[info.timeline.length - 1].end; + let periodEnd = isTimeline ? + info.timeline[info.timeline.length - 1].end : 0; if (delayPeriodEnd) { periodEnd += 1.0; } @@ -824,7 +839,7 @@ describe('DashParser SegmentTemplate', () => { '', ' ', - ' ', + isTimeline ? ' ' : '', ' ', '', ], /* duration= */ 45); @@ -841,7 +856,7 @@ describe('DashParser SegmentTemplate', () => { /** @type {?} */ const index = stream.segmentIndex; index.release(); - index.appendTemplateInfo(info, info.timeline[0].start, + index.appendTemplateInfo(info, isTimeline ? info.timeline[0].start : 0, periodEnd, shouldFit); return index;