diff --git a/build/conformance.textproto b/build/conformance.textproto index 1698220e32..2ccb4dd038 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -378,8 +378,8 @@ requirement: { value: "DOMParser.prototype.parseFromString" error_message: "Using \"DOMParser.parseFromString\" directly is not allowed; " - "use shaka.util.XmlUtils.parseXmlString instead." - whitelist_regexp: "lib/util/xml_utils.js" + "use shaka.util.TXml.parseXmlString instead." + whitelist_regexp: "lib/util/tXml.js" whitelist_regexp: "test/" } diff --git a/build/types/core b/build/types/core index ebfd9b1c99..cb1b111383 100644 --- a/build/types/core +++ b/build/types/core @@ -117,7 +117,7 @@ +../../lib/util/timer.js +../../lib/util/ts_parser.js +../../lib/util/uint8array_utils.js -+../../lib/util/xml_utils.js ++../../lib/util/tXml.js +../../third_party/closure-uri/uri.js +../../third_party/closure-uri/utils.js diff --git a/docs/tutorials/upgrade.md b/docs/tutorials/upgrade.md index d63f29d8f2..2505d8e162 100644 --- a/docs/tutorials/upgrade.md +++ b/docs/tutorials/upgrade.md @@ -96,9 +96,12 @@ application: - Configuration changes: - `streaming.forceTransmuxTS` has been renamed to `streaming.forceTransmux` (deprecated in v4.3.0) + - `manifest.dash.manifestPreprocessor` callback now receives a type of `shaka.externs.xml.Node` instead of `Element`. + - `manifest.mss.manifestPreprocessor` callback now receives a type of `shaka.externs.xml.Node` instead of `Element`. - Plugin changes: - `Transmuxer` plugins now has three new parameters in `transmux()` method. - Player API Changes: - The constructor no longer takes `mediaElement` as a parameter; use the `attach` method to attach to a media element instead. (Deprecated in v4.6) + - The `TimelineRegionInfo.eventElement` property is now a type of `shaka.externs.xml.Node` instead of `Element` diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 62000d4ab7..58e82a5b5e 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -521,7 +521,7 @@ shaka.extern.MetadataFrame; * startTime: number, * endTime: number, * id: string, - * eventElement: Element + * eventElement: ?shaka.extern.xml.Node * }} * * @description @@ -539,7 +539,7 @@ shaka.extern.MetadataFrame; * The presentation time (in seconds) that the region should end. * @property {string} id * Specifies an identifier for this instance of the region. - * @property {Element} eventElement + * @property {?shaka.extern.xml.Node} eventElement * The XML element that defines the Event. * @exportDoc */ @@ -840,6 +840,28 @@ shaka.extern.DrmConfiguration; shaka.extern.InitDataTransform; +/** + * @typedef {{ + * tagName: !string, + * attributes: !Object, + * children: !Array., + * parent: ?shaka.extern.xml.Node + * }} + * + * @description + * Data structure for xml nodes as simple objects + * + * @property {!string} tagName + * The name of the element + * @property {!object} attributes + * The attributes of the element + * @property {!Array.} children + * The child nodes or string body of the element + * @property {?shaka.extern.xml.Node} parent + * The parent of the current element + */ +shaka.extern.xml.Node; + /** * @typedef {{ * clockSyncUri: string, @@ -853,7 +875,7 @@ shaka.extern.InitDataTransform; * ignoreEmptyAdaptationSet: boolean, * ignoreMaxSegmentDuration: boolean, * keySystemsByURI: !Object., - * manifestPreprocessor: function(!Element), + * manifestPreprocessor: function(!shaka.extern.xml.Node), * sequenceMode: boolean, * enableAudioGroups: boolean, * multiTypeVariantsAllowed: boolean @@ -906,7 +928,7 @@ shaka.extern.InitDataTransform; * @property {Object.} keySystemsByURI * A map of scheme URI to key system name. Defaults to default key systems * mapping handled by Shaka. - * @property {function(!Element)} manifestPreprocessor + * @property {function(!shaka.extern.xml.Node)} manifestPreprocessor * Called immediately after the DASH manifest has been parsed into an * XMLDocument. Provides a way for applications to perform efficient * preprocessing of the manifest. @@ -1018,12 +1040,12 @@ shaka.extern.HlsManifestConfiguration; /** * @typedef {{ - * manifestPreprocessor: function(!Element), + * manifestPreprocessor: function(!shaka.extern.xml.Node), * sequenceMode: boolean, * keySystemsBySystemId: !Object. * }} * - * @property {function(!Element)} manifestPreprocessor + * @property {function(!shaka.extern.xml.Node)} manifestPreprocessor * Called immediately after the MSS manifest has been parsed into an * XMLDocument. Provides a way for applications to perform efficient * preprocessing of the manifest. diff --git a/lib/ads/ad_manager.js b/lib/ads/ad_manager.js index c6a82c3d03..732f00ea84 100644 --- a/lib/ads/ad_manager.js +++ b/lib/ads/ad_manager.js @@ -706,7 +706,7 @@ shaka.ads.AdManager = class extends shaka.util.FakeEventTarget { if (this.ssAdManager_ && region.schemeIdUri == 'urn:google:dai:2018') { const type = region.schemeIdUri; const data = region.eventElement ? - region.eventElement.getAttribute('messageData') : null; + region.eventElement.attributes['messageData'] : null; const timestamp = region.startTime; this.ssAdManager_.onTimedMetadata(type, data, timestamp); } diff --git a/lib/dash/content_protection.js b/lib/dash/content_protection.js index 017474d96a..a63f30597d 100644 --- a/lib/dash/content_protection.js +++ b/lib/dash/content_protection.js @@ -13,8 +13,8 @@ goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @@ -25,7 +25,7 @@ shaka.dash.ContentProtection = class { /** * Parses info from the ContentProtection elements at the AdaptationSet level. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {boolean} ignoreDrmInfo * @param {!Object.} keySystemsByURI * @return {shaka.dash.ContentProtection.Context} @@ -137,7 +137,7 @@ shaka.dash.ContentProtection = class { * Parses the given ContentProtection elements found at the Representation * level. This may update the |context|. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {shaka.dash.ContentProtection.Context} context * @param {boolean} ignoreDrmInfo * @param {!Object.} keySystemsByURI @@ -191,17 +191,20 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getWidevineLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const mslaurlNode = shaka.util.XmlUtils.findChildNS( + const mslaurlNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft', 'laurl'); if (mslaurlNode) { - return mslaurlNode.getAttribute('licenseUrl') || ''; + return mslaurlNode.attributes['licenseUrl'] || ''; } return ''; } @@ -214,21 +217,27 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getClearKeyLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const clearKeyLaurlNode = shaka.util.XmlUtils.findChildNS( + const clearKeyLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.ClearKeyNamespaceUri_, 'Laurl', ); if (clearKeyLaurlNode && - clearKeyLaurlNode.getAttribute('Lic_type') === 'EME-1.0') { - if (clearKeyLaurlNode.textContent) { - return clearKeyLaurlNode.textContent; + clearKeyLaurlNode.attributes['Lic_type'] === 'EME-1.0') { + if (clearKeyLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(clearKeyLaurlNode); + if (textContents) { + return textContents; + } } } return ''; @@ -312,18 +321,21 @@ shaka.dash.ContentProtection = class { /** * PlayReady Header format: https://goo.gl/dBzxNA * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {string} * @private */ static getLaurl_(xml) { + const TXml = shaka.util.TXml; // LA_URL element is optional and no more than one is // allowed inside the DATA element. Only absolute URLs are allowed. // If the LA_URL element exists, it must not be empty. - for (const elem of xml.getElementsByTagName('DATA')) { - for (const child of elem.childNodes) { - if (child instanceof Element && child.tagName == 'LA_URL') { - return child.textContent; + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + if (elem.children) { + for (const child of elem.children) { + if (child.tagName == 'LA_URL') { + return /** @type{string} */(shaka.util.TXml.getTextContents(child)); + } } } } @@ -340,25 +352,30 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getPlayReadyLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const proNode = shaka.util.XmlUtils.findChildNS( + const proNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft:playready', 'pro'); - if (!proNode) { + if (!proNode || !shaka.util.TXml.getTextContents(proNode)) { return ''; } const ContentProtection = shaka.dash.ContentProtection; const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES; - const bytes = shaka.util.Uint8ArrayUtils.fromBase64(proNode.textContent); + const textContent = + /** @type {string} */ (shaka.util.TXml.getTextContents(proNode)); + const bytes = shaka.util.Uint8ArrayUtils.fromBase64(textContent); const records = ContentProtection.parseMsPro_(bytes); const record = records.filter((record) => { return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT; @@ -369,7 +386,7 @@ shaka.dash.ContentProtection = class { } const xml = shaka.util.StringUtils.fromUTF16(record.value, true); - const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER'); + const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER'); if (!rootElement) { return ''; } @@ -386,13 +403,16 @@ shaka.dash.ContentProtection = class { * @private */ static getInitDataFromPro_(element) { - const proNode = shaka.util.XmlUtils.findChildNS( + const proNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft:playready', 'pro'); - if (!proNode) { + if (!proNode || !shaka.util.TXml.getTextContents(proNode)) { return null; } + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - const data = Uint8ArrayUtils.fromBase64(proNode.textContent); + const textContent = + /** @type{string} */ (shaka.util.TXml.getTextContents(proNode)); + const data = Uint8ArrayUtils.fromBase64(textContent); const systemId = new Uint8Array([ 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95, @@ -492,7 +512,7 @@ shaka.dash.ContentProtection = class { * Parses the given ContentProtection elements. If there is an error, it * removes those elements. * - * @param {!Array.} elems + * @param {!Array.} elems * @return {!Array.} * @private */ @@ -513,20 +533,21 @@ shaka.dash.ContentProtection = class { /** * Parses the given ContentProtection element. * - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @return {?shaka.dash.ContentProtection.Element} * @private */ static parseElement_(elem) { const NS = shaka.dash.ContentProtection.CencNamespaceUri_; + const TXml = shaka.util.TXml; /** @type {?string} */ - let schemeUri = elem.getAttribute('schemeIdUri'); + let schemeUri = elem.attributes['schemeIdUri']; /** @type {?string} */ - let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID'); + let keyId = TXml.getAttributeNS(elem, NS, 'default_KID'); /** @type {!Array.} */ - const psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh') - .map(shaka.util.XmlUtils.getContents); + const psshs = TXml.findChildrenNS(elem, NS, 'pssh') + .map(TXml.getContents); if (!schemeUri) { shaka.log.error('Missing required schemeIdUri attribute on', @@ -590,7 +611,7 @@ shaka.dash.ContentProtection = class { } const namespace = 'urn:mpeg:dash:schema:sea:2012'; - const segmentEncryption = shaka.util.XmlUtils.findChildNS( + const segmentEncryption = shaka.util.TXml.findChildNS( element.node, namespace, 'SegmentEncryption'); if (!segmentEncryption) { @@ -602,7 +623,7 @@ shaka.dash.ContentProtection = class { const aesSchemeIdUri = 'urn:mpeg:dash:sea:aes128-cbc:2013'; const segmentEncryptionSchemeIdUri = - segmentEncryption.getAttribute('schemeIdUri'); + segmentEncryption.attributes['schemeIdUri']; if (segmentEncryptionSchemeIdUri != aesSchemeIdUri) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -610,7 +631,7 @@ shaka.dash.ContentProtection = class { shaka.util.Error.Code.DASH_UNSUPPORTED_AES_128); } - const cryptoPeriod = shaka.util.XmlUtils.findChildNS( + const cryptoPeriod = shaka.util.TXml.findChildNS( element.node, namespace, 'CryptoPeriod'); if (!cryptoPeriod) { @@ -620,8 +641,8 @@ shaka.dash.ContentProtection = class { shaka.util.Error.Code.DASH_UNSUPPORTED_AES_128); } - const ivHex = cryptoPeriod.getAttribute('IV'); - const keyUri = cryptoPeriod.getAttribute('keyUriTemplate'); + const ivHex = cryptoPeriod.attributes['IV']; + const keyUri = cryptoPeriod.attributes['keyUriTemplate']; if (!ivHex || !keyUri) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -721,7 +742,7 @@ shaka.dash.ContentProtection.Aes128Info; /** * @typedef {{ - * node: !Element, + * node: !shaka.extern.xml.Node, * schemeUri: string, * keyId: ?string, * init: Array. @@ -730,7 +751,7 @@ shaka.dash.ContentProtection.Aes128Info; * @description * The parsed result of a single ContentProtection element. * - * @property {!Element} node + * @property {!shaka.extern.xml.Node} node * The ContentProtection XML element. * @property {string} schemeUri * The scheme URI. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index af5063a055..44c9ee185a 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -31,7 +31,7 @@ goog.require('shaka.util.OperationManager'); goog.require('shaka.util.PeriodCombiner'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -329,13 +329,16 @@ shaka.dash.DashParser = class { async parseManifest_(data, finalManifestUri) { const Error = shaka.util.Error; const MpdUtils = shaka.dash.MpdUtils; + const TXml = shaka.util.TXml; + + const mpd = TXml.parseXml(data, 'MPD'); - const mpd = shaka.util.XmlUtils.parseXml(data, 'MPD'); if (!mpd) { throw new Error( Error.Severity.CRITICAL, Error.Category.MANIFEST, Error.Code.DASH_INVALID_XML, finalManifestUri); } + const disableXlinkProcessing = this.config_.dash.disableXlinkProcessing; if (disableXlinkProcessing) { return this.processManifest_(mpd, finalManifestUri); @@ -355,14 +358,14 @@ shaka.dash.DashParser = class { /** * Takes a formatted MPD and converts it into a manifest. * - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @param {string} finalManifestUri The final manifest URI, which may * differ from this.manifestUri_ if there has been a redirect. * @return {!Promise} * @private */ async processManifest_(mpd, finalManifestUri) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const manifestPreprocessor = this.config_.dash.manifestPreprocessor; if (manifestPreprocessor) { @@ -381,10 +384,10 @@ shaka.dash.DashParser = class { const locations = []; /** @type {!Map.} */ const locationsMapping = new Map(); - const locationsObjs = XmlUtils.findChildren(mpd, 'Location'); + const locationsObjs = TXml.findChildren(mpd, 'Location'); for (const locationsObj of locationsObjs) { - const serviceLocation = locationsObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(locationsObj); + const serviceLocation = locationsObj.attributes['serviceLocation']; + const uri = TXml.getContents(locationsObj); if (!uri) { continue; } @@ -414,10 +417,10 @@ shaka.dash.DashParser = class { let contentSteeringPromise = Promise.resolve(); - const contentSteering = XmlUtils.findChild(mpd, 'ContentSteering'); + const contentSteering = TXml.findChild(mpd, 'ContentSteering'); if (contentSteering && this.playerInterface_) { const defaultPathwayId = - contentSteering.getAttribute('defaultServiceLocation'); + contentSteering.attributes['defaultServiceLocation']; if (!this.contentSteeringManager_) { this.contentSteeringManager_ = new shaka.util.ContentSteeringManager(this.playerInterface_); @@ -426,11 +429,11 @@ shaka.dash.DashParser = class { shaka.media.ManifestParser.DASH); this.contentSteeringManager_.setBaseUris(manifestBaseUris); this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId); - const uri = XmlUtils.getContents(contentSteering); + const uri = TXml.getContents(contentSteering); if (uri) { const queryBeforeStart = - XmlUtils.parseAttr(contentSteering, 'queryBeforeStart', - XmlUtils.parseBoolean, /* defaultValue= */ false); + TXml.parseAttr(contentSteering, 'queryBeforeStart', + TXml.parseBoolean, /* defaultValue= */ false); if (queryBeforeStart) { contentSteeringPromise = this.contentSteeringManager_.requestInfo(uri); @@ -449,13 +452,13 @@ shaka.dash.DashParser = class { } } - const uriObjs = XmlUtils.findChildren(mpd, 'BaseURL'); + const uriObjs = TXml.findChildren(mpd, 'BaseURL'); let calculatedBaseUris; let someLocationValid = false; if (this.contentSteeringManager_) { for (const uriObj of uriObjs) { - const serviceLocation = uriObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(uriObj); + const serviceLocation = uriObj.attributes['serviceLocation']; + const uri = TXml.getContents(uriObj); if (serviceLocation && uri) { this.contentSteeringManager_.addLocation( 'BaseURL', serviceLocation, uri); @@ -464,7 +467,7 @@ shaka.dash.DashParser = class { } } if (!someLocationValid || !this.contentSteeringManager_) { - const uris = uriObjs.map(XmlUtils.getContents); + const uris = uriObjs.map(TXml.getContents); calculatedBaseUris = shaka.util.ManifestParserUtils.resolveUris( manifestBaseUris, uris); } @@ -481,41 +484,41 @@ shaka.dash.DashParser = class { let availabilityTimeOffset = 0; if (uriObjs && uriObjs.length) { - availabilityTimeOffset = XmlUtils.parseAttr( - uriObjs[0], 'availabilityTimeOffset', XmlUtils.parseFloat) || 0; + availabilityTimeOffset = TXml.parseAttr(uriObjs[0], + 'availabilityTimeOffset', TXml.parseFloat) || 0; } const ignoreMinBufferTime = this.config_.dash.ignoreMinBufferTime; let minBufferTime = 0; if (!ignoreMinBufferTime) { minBufferTime = - XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration) || 0; + TXml.parseAttr(mpd, 'minBufferTime', TXml.parseDuration) || 0; } - this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr( - mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1)); + this.updatePeriod_ = /** @type {number} */ (TXml.parseAttr( + mpd, 'minimumUpdatePeriod', TXml.parseDuration, -1)); - const presentationStartTime = XmlUtils.parseAttr( - mpd, 'availabilityStartTime', XmlUtils.parseDate); - let segmentAvailabilityDuration = XmlUtils.parseAttr( - mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration); + const presentationStartTime = TXml.parseAttr( + mpd, 'availabilityStartTime', TXml.parseDate); + let segmentAvailabilityDuration = TXml.parseAttr( + mpd, 'timeShiftBufferDepth', TXml.parseDuration); const ignoreSuggestedPresentationDelay = this.config_.dash.ignoreSuggestedPresentationDelay; let suggestedPresentationDelay = null; if (!ignoreSuggestedPresentationDelay) { - suggestedPresentationDelay = XmlUtils.parseAttr( - mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration); + suggestedPresentationDelay = TXml.parseAttr( + mpd, 'suggestedPresentationDelay', TXml.parseDuration); } const ignoreMaxSegmentDuration = this.config_.dash.ignoreMaxSegmentDuration; let maxSegmentDuration = null; if (!ignoreMaxSegmentDuration) { - maxSegmentDuration = XmlUtils.parseAttr( - mpd, 'maxSegmentDuration', XmlUtils.parseDuration); + maxSegmentDuration = TXml.parseAttr( + mpd, 'maxSegmentDuration', TXml.parseDuration); } - const mpdType = mpd.getAttribute('type') || 'static'; + const mpdType = mpd.attributes['type'] || 'static'; /** @type {!shaka.media.PresentationTimeline} */ let presentationTimeline; @@ -575,7 +578,7 @@ shaka.dash.DashParser = class { presentationTimeline.setSegmentAvailabilityDuration( segmentAvailabilityDuration); - const profiles = mpd.getAttribute('profiles') || ''; + const profiles = mpd.attributes['profiles'] || ''; /** @type {shaka.dash.DashParser.Context} */ const context = { @@ -663,8 +666,8 @@ shaka.dash.DashParser = class { // We only need to do clock sync when we're using presentation start // time. This condition also excludes VOD streams. if (presentationTimeline.usingPresentationStartTime()) { - const XmlUtils = shaka.util.XmlUtils; - const timingElements = XmlUtils.findChildren(mpd, 'UTCTiming'); + const TXml = shaka.util.TXml; + const timingElements = TXml.findChildren(mpd, 'UTCTiming'); const offset = await this.parseUtcTiming_(getBaseUris, timingElements); // Detect calls to stop(). if (!this.playerInterface_) { @@ -703,33 +706,33 @@ shaka.dash.DashParser = class { * Reads maxLatency and maxPlaybackRate properties from service * description element. * - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @return {?shaka.extern.ServiceDescription} * @private */ parseServiceDescription_(mpd) { - const XmlUtils = shaka.util.XmlUtils; - const elem = XmlUtils.findChild(mpd, 'ServiceDescription'); + const TXml = shaka.util.TXml; + const elem = TXml.findChild(mpd, 'ServiceDescription'); if (!elem ) { return null; } - const latencyNode = XmlUtils.findChild(elem, 'Latency'); - const playbackRateNode = XmlUtils.findChild(elem, 'PlaybackRate'); + const latencyNode = TXml.findChild(elem, 'Latency'); + const playbackRateNode = TXml.findChild(elem, 'PlaybackRate'); - if ((latencyNode && latencyNode.getAttribute('max')) || playbackRateNode) { - const maxLatency = latencyNode && latencyNode.getAttribute('max') ? - parseInt(latencyNode.getAttribute('max'), 10) / 1000 : + if ((latencyNode && latencyNode.attributes['max']) || playbackRateNode) { + const maxLatency = latencyNode && latencyNode.attributes['max'] ? + parseInt(latencyNode.attributes['max'], 10) / 1000 : null; const maxPlaybackRate = playbackRateNode ? - parseFloat(playbackRateNode.getAttribute('max')) : + parseFloat(playbackRateNode.attributes['max']) : null; - const minLatency = latencyNode && latencyNode.getAttribute('min') ? - parseInt(latencyNode.getAttribute('min'), 10) / 1000 : + const minLatency = latencyNode && latencyNode.attributes['min'] ? + parseInt(latencyNode.attributes['min'], 10) / 1000 : null; const minPlaybackRate = playbackRateNode ? - parseFloat(playbackRateNode.getAttribute('min')) : + parseFloat(playbackRateNode.attributes['min']) : null; return { @@ -750,7 +753,7 @@ shaka.dash.DashParser = class { * * @param {shaka.dash.DashParser.Context} context * @param {function():!Array.} getBaseUris - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @return {{ * periods: !Array., * duration: ?number, @@ -759,21 +762,21 @@ shaka.dash.DashParser = class { * @private */ parsePeriods_(context, getBaseUris, mpd) { - const XmlUtils = shaka.util.XmlUtils; - const presentationDuration = XmlUtils.parseAttr( - mpd, 'mediaPresentationDuration', XmlUtils.parseDuration); + const TXml = shaka.util.TXml; + const presentationDuration = TXml.parseAttr( + mpd, 'mediaPresentationDuration', TXml.parseDuration); const periods = []; let prevEnd = 0; - const periodNodes = XmlUtils.findChildren(mpd, 'Period'); + const periodNodes = TXml.findChildren(mpd, 'Period'); for (let i = 0; i < periodNodes.length; i++) { const elem = periodNodes[i]; const next = periodNodes[i + 1]; const start = /** @type {number} */ ( - XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd)); - const periodId = elem.id; + TXml.parseAttr(elem, 'start', TXml.parseDuration, prevEnd)); + const periodId = elem.attributes['id']; const givenDuration = - XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration); + TXml.parseAttr(elem, 'duration', TXml.parseDuration); let periodDuration = null; if (next) { @@ -781,7 +784,7 @@ shaka.dash.DashParser = class { // of the following Period is the duration of the media content // represented by this Period." const nextStart = - XmlUtils.parseAttr(next, 'start', XmlUtils.parseDuration); + TXml.parseAttr(next, 'start', TXml.parseDuration); if (nextStart != null) { periodDuration = nextStart - start; } @@ -909,7 +912,7 @@ shaka.dash.DashParser = class { */ parsePeriod_(context, getBaseUris, periodInfo) { const Functional = shaka.util.Functional; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; context.period = this.createFrame_(periodInfo.node, null, getBaseUris); @@ -925,7 +928,7 @@ shaka.dash.DashParser = class { } const eventStreamNodes = - XmlUtils.findChildren(periodInfo.node, 'EventStream'); + TXml.findChildren(periodInfo.node, 'EventStream'); const availabilityStart = context.presentationTimeline.getSegmentAvailabilityStart(); @@ -935,7 +938,7 @@ shaka.dash.DashParser = class { } const adaptationSetNodes = - XmlUtils.findChildren(periodInfo.node, 'AdaptationSet'); + TXml.findChildren(periodInfo.node, 'AdaptationSet'); const adaptationSets = adaptationSetNodes .map((node) => this.parseAdaptationSet_(context, node)) .filter(Functional.isNotNull); @@ -1044,12 +1047,12 @@ shaka.dash.DashParser = class { * Parses an AdaptationSet XML element. * * @param {shaka.dash.DashParser.Context} context - * @param {!Element} elem The AdaptationSet element. + * @param {!shaka.extern.xml.Node} elem The AdaptationSet element. * @return {?shaka.dash.DashParser.AdaptationInfo} * @private */ parseAdaptationSet_(context, elem) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const Functional = shaka.util.Functional; const ManifestParserUtils = shaka.util.ManifestParserUtils; const ContentType = ManifestParserUtils.ContentType; @@ -1058,9 +1061,9 @@ shaka.dash.DashParser = class { context.adaptationSet = this.createFrame_(elem, context.period, null); let main = false; - const roleElements = XmlUtils.findChildren(elem, 'Role'); + const roleElements = TXml.findChildren(elem, 'Role'); const roleValues = roleElements.map((role) => { - return role.getAttribute('value'); + return role.attributes['value']; }).filter(Functional.isNotNull); // Default kind for text streams is 'subtitle' if unspecified in the @@ -1072,12 +1075,12 @@ shaka.dash.DashParser = class { } for (const roleElement of roleElements) { - const scheme = roleElement.getAttribute('schemeIdUri'); + const scheme = roleElement.attributes['schemeIdUri']; if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') { // These only apply for the given scheme, but allow them to be specified // if there is no scheme specified. // See: DASH section 5.8.5.5 - const value = roleElement.getAttribute('value'); + const value = roleElement.attributes['value']; switch (value) { case 'main': main = true; @@ -1122,18 +1125,18 @@ shaka.dash.DashParser = class { }; const essentialProperties = - XmlUtils.findChildren(elem, 'EssentialProperty'); + TXml.findChildren(elem, 'EssentialProperty'); // ID of real AdaptationSet if this is a trick mode set: let trickModeFor = null; let isFastSwitching = false; let unrecognizedEssentialProperty = false; for (const prop of essentialProperties) { - const schemeId = prop.getAttribute('schemeIdUri'); + const schemeId = prop.attributes['schemeIdUri']; if (schemeId == 'http://dashif.org/guidelines/trickmode') { - trickModeFor = prop.getAttribute('value'); + trickModeFor = prop.attributes['value']; } else if (schemeId == transferCharacteristicsScheme) { videoRange = getVideoRangeFromTransferCharacteristicCICP( - parseInt(prop.getAttribute('value'), 10), + parseInt(prop.attributes['value'], 10), ); } else if (schemeId == colourPrimariesScheme || schemeId == matrixCoefficientsScheme) { @@ -1146,24 +1149,24 @@ shaka.dash.DashParser = class { } const supplementalProperties = - XmlUtils.findChildren(elem, 'SupplementalProperty'); + TXml.findChildren(elem, 'SupplementalProperty'); for (const prop of supplementalProperties) { - const schemeId = prop.getAttribute('schemeIdUri'); + const schemeId = prop.attributes['schemeIdUri']; if (schemeId == transferCharacteristicsScheme) { videoRange = getVideoRangeFromTransferCharacteristicCICP( - parseInt(prop.getAttribute('value'), 10), + parseInt(prop.attributes['value'], 10), ); } } - const accessibilities = XmlUtils.findChildren(elem, 'Accessibility'); + const accessibilities = TXml.findChildren(elem, 'Accessibility'); const LanguageUtils = shaka.util.LanguageUtils; const closedCaptions = new Map(); /** @type {?shaka.media.ManifestParser.AccessibilityPurpose} */ let accessibilityPurpose; for (const prop of accessibilities) { - const schemeId = prop.getAttribute('schemeIdUri'); - const value = prop.getAttribute('value'); + const schemeId = prop.attributes['schemeIdUri']; + const value = prop.attributes['value']; if (schemeId == 'urn:scte:dash:cc:cea-608:2015' ) { let channelId = 1; if (value != null) { @@ -1267,7 +1270,7 @@ shaka.dash.DashParser = class { } const contentProtectionElems = - XmlUtils.findChildren(elem, 'ContentProtection'); + TXml.findChildren(elem, 'ContentProtection'); const contentProtection = ContentProtection.parseFromAdaptationSet( contentProtectionElems, this.config_.dash.ignoreDrmInfo, @@ -1277,20 +1280,21 @@ shaka.dash.DashParser = class { context.adaptationSet.language || 'und'); // This attribute is currently non-standard, but it is supported by Kaltura. - let label = elem.getAttribute('label'); + let label = elem.attributes['label']; // See DASH IOP 4.3 here https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf (page 35) - const labelElements = XmlUtils.findChildren(elem, 'Label'); + const labelElements = TXml.findChildren(elem, 'Label'); if (labelElements && labelElements.length) { // NOTE: Right now only one label field is supported. const firstLabelElement = labelElements[0]; - if (firstLabelElement.textContent) { - label = firstLabelElement.textContent; + const textContent = shaka.util.TXml.getTextContents(firstLabelElement); + if (textContent) { + label = textContent; } } // Parse Representations into Streams. - const representations = XmlUtils.findChildren(elem, 'Representation'); + const representations = TXml.findChildren(elem, 'Representation'); const streams = representations.map((representation) => { const parsedRepresentation = this.parseRepresentation_(context, contentProtection, kind, language, label, main, roleValues, @@ -1350,7 +1354,7 @@ shaka.dash.DashParser = class { } const repIds = representations - .map((node) => { return node.getAttribute('id'); }) + .map((node) => { return node.attributes['id']; }) .filter(shaka.util.Functional.isNotNull); return { @@ -1376,7 +1380,7 @@ shaka.dash.DashParser = class { * @param {boolean} isPrimary * @param {!Array.} roles * @param {Map.} closedCaptions - * @param {!Element} node + * @param {!shaka.extern.xml.Node} node * @param {?shaka.media.ManifestParser.AccessibilityPurpose} * accessibilityPurpose * @@ -1386,7 +1390,7 @@ shaka.dash.DashParser = class { */ parseRepresentation_(context, contentProtection, kind, language, label, isPrimary, roles, closedCaptions, node, accessibilityPurpose) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; context.representation = @@ -1408,7 +1412,7 @@ shaka.dash.DashParser = class { // To avoid NaN at the variant level on broken content, fall back to zero. // https://github.com/shaka-project/shaka-player/issues/938#issuecomment-317278180 context.bandwidth = - XmlUtils.parseAttr(node, 'bandwidth', XmlUtils.parsePositiveInt) || 0; + TXml.parseAttr(node, 'bandwidth', TXml.parsePositiveInt) || 0; /** @type {?shaka.dash.DashParser.StreamInfo} */ let streamInfo; @@ -1501,7 +1505,7 @@ shaka.dash.DashParser = class { } const contentProtectionElems = - XmlUtils.findChildren(node, 'ContentProtection'); + TXml.findChildren(node, 'ContentProtection'); const keyId = shaka.dash.ContentProtection.parseFromRepresentation( contentProtectionElems, contentProtection, this.config_.dash.ignoreDrmInfo, @@ -1511,12 +1515,12 @@ shaka.dash.DashParser = class { // Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling. // See: ETSI TS 103 420 V1.2.1 (2018-10) const supplementalPropertyElems = - XmlUtils.findChildren(node, 'SupplementalProperty'); + TXml.findChildren(node, 'SupplementalProperty'); const hasJoc = supplementalPropertyElems.some((element) => { const expectedUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018'; const expectedValue = 'JOC'; - return element.getAttribute('schemeIdUri') == expectedUri && - element.getAttribute('value') == expectedValue; + return element.attributes['schemeIdUri'] == expectedUri && + element.attributes['value'] == expectedValue; }); let spatialAudio = false; if (hasJoc) { @@ -1534,16 +1538,16 @@ shaka.dash.DashParser = class { let tilesLayout; if (isImage) { const essentialPropertyElems = - XmlUtils.findChildren(node, 'EssentialProperty'); + TXml.findChildren(node, 'EssentialProperty'); const thumbnailTileElem = essentialPropertyElems.find((element) => { const expectedUris = [ 'http://dashif.org/thumbnail_tile', 'http://dashif.org/guidelines/thumbnail_tile', ]; - return expectedUris.includes(element.getAttribute('schemeIdUri')); + return expectedUris.includes(element.attributes['schemeIdUri']); }); if (thumbnailTileElem) { - tilesLayout = thumbnailTileElem.getAttribute('value'); + tilesLayout = thumbnailTileElem.attributes['value']; } // Filter image adaptation sets that has no tilesLayout. if (!tilesLayout) { @@ -1702,7 +1706,7 @@ shaka.dash.DashParser = class { /** * Creates a new inheritance frame for the given element. * - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @param {?shaka.dash.DashParser.InheritanceFrame} parent * @param {?function():!Array.} getBaseUris * @return {shaka.dash.DashParser.InheritanceFrame} @@ -1712,7 +1716,7 @@ shaka.dash.DashParser = class { goog.asserts.assert(parent || getBaseUris, 'Must provide either parent or getBaseUris'); const ManifestParserUtils = shaka.util.ManifestParserUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({ contentType: '', mimeType: '', @@ -1727,17 +1731,17 @@ shaka.dash.DashParser = class { }); getBaseUris = getBaseUris || parent.getBaseUris; - const parseNumber = XmlUtils.parseNonNegativeInt; - const evalDivision = XmlUtils.evalDivision; + const parseNumber = TXml.parseNonNegativeInt; + const evalDivision = TXml.evalDivision; - const id = elem.getAttribute('id'); - const uriObjs = XmlUtils.findChildren(elem, 'BaseURL'); + const id = elem.attributes['id']; + const uriObjs = TXml.findChildren(elem, 'BaseURL'); let calculatedBaseUris; let someLocationValid = false; if (this.contentSteeringManager_) { for (const uriObj of uriObjs) { - const serviceLocation = uriObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(uriObj); + const serviceLocation = uriObj.attributes['serviceLocation']; + const uri = TXml.getContents(uriObj); if (serviceLocation && uri) { this.contentSteeringManager_.addLocation( id, serviceLocation, uri); @@ -1746,7 +1750,7 @@ shaka.dash.DashParser = class { } } if (!someLocationValid || !this.contentSteeringManager_) { - calculatedBaseUris = uriObjs.map(XmlUtils.getContents); + calculatedBaseUris = uriObjs.map(TXml.getContents); } const getFrameUris = () => { @@ -1762,55 +1766,55 @@ shaka.dash.DashParser = class { return []; }; - let contentType = elem.getAttribute('contentType') || parent.contentType; - const mimeType = elem.getAttribute('mimeType') || parent.mimeType; - const codecs = elem.getAttribute('codecs') || parent.codecs; + let contentType = elem.attributes['contentType'] || parent.contentType; + const mimeType = elem.attributes['mimeType'] || parent.mimeType; + const codecs = elem.attributes['codecs'] || parent.codecs; const frameRate = - XmlUtils.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate; + TXml.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate; const pixelAspectRatio = - elem.getAttribute('sar') || parent.pixelAspectRatio; + elem.attributes['sar'] || parent.pixelAspectRatio; const emsgSchemeIdUris = this.emsgSchemeIdUris_( - XmlUtils.findChildren(elem, 'InbandEventStream'), + TXml.findChildren(elem, 'InbandEventStream'), parent.emsgSchemeIdUris); const audioChannelConfigs = - XmlUtils.findChildren(elem, 'AudioChannelConfiguration'); + TXml.findChildren(elem, 'AudioChannelConfiguration'); const numChannels = this.parseAudioChannels_(audioChannelConfigs) || parent.numChannels; const audioSamplingRate = - XmlUtils.parseAttr(elem, 'audioSamplingRate', parseNumber) || + TXml.parseAttr(elem, 'audioSamplingRate', parseNumber) || parent.audioSamplingRate; if (!contentType) { contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs); } - const segmentBase = XmlUtils.findChild(elem, 'SegmentBase'); - const segmentTemplate = XmlUtils.findChild(elem, 'SegmentTemplate'); + const segmentBase = TXml.findChild(elem, 'SegmentBase'); + const segmentTemplate = TXml.findChild(elem, 'SegmentTemplate'); // The availabilityTimeOffset is the sum of all @availabilityTimeOffset // values that apply to the adaptation set, via BaseURL, SegmentBase, // or SegmentTemplate elements. const segmentBaseAto = segmentBase ? - (XmlUtils.parseAttr(segmentBase, 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(segmentBase, 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const segmentTemplateAto = segmentTemplate ? - (XmlUtils.parseAttr(segmentTemplate, 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(segmentTemplate, 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const baseUriAto = uriObjs && uriObjs.length ? - (XmlUtils.parseAttr(uriObjs[0], 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(uriObjs[0], 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const availabilityTimeOffset = parent.availabilityTimeOffset + baseUriAto + segmentBaseAto + segmentTemplateAto; let segmentSequenceCadence = null; const segmentSequenceProperties = - XmlUtils.findChild(elem, 'SegmentSequenceProperties'); + TXml.findChild(elem, 'SegmentSequenceProperties'); if (segmentSequenceProperties) { - const sap = XmlUtils.findChild(segmentSequenceProperties, 'SAP'); + const sap = TXml.findChild(segmentSequenceProperties, 'SAP'); if (sap) { - segmentSequenceCadence = XmlUtils.parseAttr(sap, 'cadence', - XmlUtils.parseInt); + segmentSequenceCadence = TXml.parseAttr(sap, 'cadence', + TXml.parseInt); } } @@ -1819,10 +1823,10 @@ shaka.dash.DashParser = class { () => ManifestParserUtils.resolveUris(getBaseUris(), getFrameUris()), segmentBase: segmentBase || parent.segmentBase, segmentList: - XmlUtils.findChild(elem, 'SegmentList') || parent.segmentList, + TXml.findChild(elem, 'SegmentList') || parent.segmentList, segmentTemplate: segmentTemplate || parent.segmentTemplate, - width: XmlUtils.parseAttr(elem, 'width', parseNumber) || parent.width, - height: XmlUtils.parseAttr(elem, 'height', parseNumber) || parent.height, + width: TXml.parseAttr(elem, 'width', parseNumber) || parent.width, + height: TXml.parseAttr(elem, 'height', parseNumber) || parent.height, contentType: contentType, mimeType: mimeType, codecs: codecs, @@ -1830,7 +1834,7 @@ shaka.dash.DashParser = class { pixelAspectRatio: pixelAspectRatio, emsgSchemeIdUris: emsgSchemeIdUris, id: id, - language: elem.getAttribute('lang'), + language: elem.attributes['lang'], numChannels: numChannels, audioSamplingRate: audioSamplingRate, availabilityTimeOffset: availabilityTimeOffset, @@ -1844,7 +1848,8 @@ shaka.dash.DashParser = class { * of the ones parsed from inBandEventStreams and the ones provided in * emsgSchemeIdUris. * - * @param {!Array.} inBandEventStreams Array of InbandEventStream + * @param {!Array.} inBandEventStreams + * Array of InbandEventStream * elements to parse and add to the returned array. * @param {!Array.} emsgSchemeIdUris Array of parsed * InbandEventStream schemeIdUri attributes to add to the returned array. @@ -1855,7 +1860,7 @@ shaka.dash.DashParser = class { emsgSchemeIdUris_(inBandEventStreams, emsgSchemeIdUris) { const schemeIdUris = emsgSchemeIdUris.slice(); for (const event of inBandEventStreams) { - const schemeIdUri = event.getAttribute('schemeIdUri'); + const schemeIdUri = event.attributes['schemeIdUri']; if (!schemeIdUris.includes(schemeIdUri)) { schemeIdUris.push(schemeIdUri); } @@ -1864,19 +1869,19 @@ shaka.dash.DashParser = class { } /** - * @param {!Array.} audioChannelConfigs An array of + * @param {!Array.} audioChannelConfigs An array of * AudioChannelConfiguration elements. * @return {?number} The number of audio channels, or null if unknown. * @private */ parseAudioChannels_(audioChannelConfigs) { for (const elem of audioChannelConfigs) { - const scheme = elem.getAttribute('schemeIdUri'); + const scheme = elem.attributes['schemeIdUri']; if (!scheme) { continue; } - const value = elem.getAttribute('value'); + const value = elem.attributes['value']; if (!value) { continue; } @@ -2047,15 +2052,15 @@ shaka.dash.DashParser = class { * Parses an array of UTCTiming elements. * * @param {function():!Array.} getBaseUris - * @param {!Array.} elems + * @param {!Array.} elems * @return {!Promise.} * @private */ async parseUtcTiming_(getBaseUris, elems) { const schemesAndValues = elems.map((elem) => { return { - scheme: elem.getAttribute('schemeIdUri'), - value: elem.getAttribute('value'), + scheme: elem.attributes['schemeIdUri'], + value: elem.attributes['value'], }; }); @@ -2119,23 +2124,23 @@ shaka.dash.DashParser = class { * * @param {number} periodStart * @param {?number} periodDuration - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @param {number} availabilityStart * @private */ parseEventStream_(periodStart, periodDuration, elem, availabilityStart) { - const XmlUtils = shaka.util.XmlUtils; - const parseNumber = XmlUtils.parseNonNegativeInt; + const TXml = shaka.util.TXml; + const parseNumber = shaka.util.TXml.parseNonNegativeInt; - const schemeIdUri = elem.getAttribute('schemeIdUri') || ''; - const value = elem.getAttribute('value') || ''; - const timescale = XmlUtils.parseAttr(elem, 'timescale', parseNumber) || 1; + const schemeIdUri = elem.attributes['schemeIdUri'] || ''; + const value = elem.attributes['value'] || ''; + const timescale = TXml.parseAttr(elem, 'timescale', parseNumber) || 1; - for (const eventNode of XmlUtils.findChildren(elem, 'Event')) { + for (const eventNode of TXml.findChildren(elem, 'Event')) { const presentationTime = - XmlUtils.parseAttr(eventNode, 'presentationTime', parseNumber) || 0; + TXml.parseAttr(eventNode, 'presentationTime', parseNumber) || 0; const duration = - XmlUtils.parseAttr(eventNode, 'duration', parseNumber) || 0; + TXml.parseAttr(eventNode, 'duration', parseNumber) || 0; let startTime = presentationTime / timescale + periodStart; let endTime = startTime + (duration / timescale); @@ -2157,7 +2162,7 @@ shaka.dash.DashParser = class { value: value, startTime: startTime, endTime: endTime, - id: eventNode.getAttribute('id') || '', + id: eventNode.attributes['id'] || '', eventElement: eventNode, }; @@ -2246,9 +2251,9 @@ shaka.dash.DashParser.RequestSegmentCallback; /** * @typedef {{ - * segmentBase: Element, - * segmentList: Element, - * segmentTemplate: Element, + * segmentBase: ?shaka.extern.xml.Node, + * segmentList: ?shaka.extern.xml.Node, + * segmentTemplate: ?shaka.extern.xml.Node, * getBaseUris: function():!Array., * width: (number|undefined), * height: (number|undefined), @@ -2270,11 +2275,11 @@ shaka.dash.DashParser.RequestSegmentCallback; * A collection of elements and properties which are inherited across levels * of a DASH manifest. * - * @property {Element} segmentBase + * @property {?shaka.extern.xml.Node} segmentBase * The XML node for SegmentBase. - * @property {Element} segmentList + * @property {?shaka.extern.xml.Node} segmentList * The XML node for SegmentList. - * @property {Element} segmentTemplate + * @property {?shaka.extern.xml.Node} segmentTemplate * The XML node for SegmentTemplate. * @property {function():!Array.} getBaseUris * Function than returns an array of absolute base URIs for the frame. @@ -2359,7 +2364,7 @@ shaka.dash.DashParser.Context; * @typedef {{ * start: number, * duration: ?number, - * node: !Element, + * node: !shaka.extern.xml.Node, * isLastPeriod: boolean * }} * @@ -2371,7 +2376,7 @@ shaka.dash.DashParser.Context; * @property {?number} duration * The duration of the period; or null if the duration is not given. This * will be non-null for all periods except the last. - * @property {!Element} node + * @property {!shaka.extern.xml.Node} node * The XML Node for the Period. * @property {boolean} isLastPeriod * Whether this Period is the last one in the manifest. diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 4a31dc8c46..14f00e2d52 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -13,7 +13,7 @@ goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); @@ -123,7 +123,7 @@ shaka.dash.MpdUtils = class { * Expands a SegmentTimeline into an array-based timeline. The results are in * seconds. * - * @param {!Element} segmentTimeline + * @param {!shaka.extern.xml.Node} segmentTimeline * @param {number} timescale * @param {number} unscaledPresentationTimeOffset * @param {number} periodDuration The Period's duration in seconds. @@ -141,9 +141,9 @@ shaka.dash.MpdUtils = class { periodDuration > 0, 'period duration must be a positive integer'); // Alias. - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - const timePoints = XmlUtils.findChildren(segmentTimeline, 'S'); + const timePoints = TXml.findChildren(segmentTimeline, 'S'); /** @type {!Array.} */ const timeline = []; @@ -152,12 +152,12 @@ shaka.dash.MpdUtils = class { for (let i = 0; i < timePoints.length; ++i) { const timePoint = timePoints[i]; const next = timePoints[i + 1]; - let t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt); + let t = TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt); const d = - XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt); - const r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt); + TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt); + const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt); - const k = XmlUtils.parseAttr(timePoint, 'k', XmlUtils.parseInt); + const k = TXml.parseAttr(timePoint, 'k', TXml.parseInt); const partialSegments = k || 0; @@ -179,7 +179,7 @@ shaka.dash.MpdUtils = class { if (repeat < 0) { if (next) { const nextStartTime = - XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(next, 't', TXml.parseNonNegativeInt); if (nextStartTime == null) { shaka.log.warning( 'An "S" element cannot have a negative repeat', @@ -260,7 +260,8 @@ shaka.dash.MpdUtils = class { * Parses common segment info for SegmentList and SegmentTemplate. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the element that contains the segment info. * @return {shaka.dash.MpdUtils.SegmentInfo} */ @@ -269,23 +270,23 @@ shaka.dash.MpdUtils = class { callback(context.representation), 'There must be at least one element of the given type.'); const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale'); let timescale = 1; if (timescaleStr) { - timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + timescale = TXml.parsePositiveInt(timescaleStr) || 1; } const durationStr = MpdUtils.inheritAttribute(context, callback, 'duration'); - let segmentDuration = XmlUtils.parsePositiveInt(durationStr || ''); + let segmentDuration = TXml.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 = TXml.parseFloat(durationStr || ''); } if (segmentDuration) { segmentDuration /= timescale; @@ -296,7 +297,7 @@ shaka.dash.MpdUtils = class { const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(context, callback, 'presentationTimeOffset')) || 0; - let startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || ''); + let startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); if (startNumberStr == null || startNumber == null) { startNumber = 1; } @@ -326,8 +327,9 @@ shaka.dash.MpdUtils = class { /** * Parses common attributes for Representation, AdaptationSet, and Period. * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback - * @return {!Array.} + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback + * @return {!Array.} */ static getNodes(context, callback) { const Functional = shaka.util.Functional; @@ -347,7 +349,8 @@ shaka.dash.MpdUtils = class { * Searches the inheritance for a Segment* with the given attribute. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the Element that contains the attribute to inherit. * @param {string} attribute * @return {?string} @@ -358,7 +361,7 @@ shaka.dash.MpdUtils = class { let result = null; for (const node of nodes) { - result = node.getAttribute(attribute); + result = node.attributes[attribute]; if (result) { break; } @@ -370,19 +373,20 @@ shaka.dash.MpdUtils = class { * Searches the inheritance for a Segment* with the given child. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the Element that contains the child to inherit. * @param {string} child - * @return {Element} + * @return {?shaka.extern.xml.Node} */ static inheritChild(context, callback, child) { const MpdUtils = shaka.dash.MpdUtils; const nodes = MpdUtils.getNodes(context, callback); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; let result = null; for (const node of nodes) { - result = XmlUtils.findChild(node, child); + result = TXml.findChild(node, child); if (result) { break; } @@ -395,33 +399,35 @@ shaka.dash.MpdUtils = class { * It also strips the xlink properties off of the element, * even if the process fails. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.extern.RetryParameters} retryParameters * @param {boolean} failGracefully * @param {string} baseUri * @param {!shaka.net.NetworkingEngine} networkingEngine * @param {number} linkDepth - * @return {!shaka.util.AbortableOperation.} + * @return {!shaka.util.AbortableOperation.} * @private */ static handleXlinkInElement_( element, retryParameters, failGracefully, baseUri, networkingEngine, linkDepth) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const Error = shaka.util.Error; const ManifestParserUtils = shaka.util.ManifestParserUtils; const NS = MpdUtils.XlinkNamespaceUri_; - const xlinkHref = XmlUtils.getAttributeNS(element, NS, 'href'); + const xlinkHref = TXml.getAttributeNS(element, NS, 'href'); const xlinkActuate = - XmlUtils.getAttributeNS(element, NS, 'actuate') || 'onRequest'; + TXml.getAttributeNS(element, NS, 'actuate') || 'onRequest'; // Remove the xlink properties, so it won't download again // when re-processed. - for (const attribute of Array.from(element.attributes)) { - if (attribute.namespaceURI == NS) { - element.removeAttributeNS(attribute.namespaceURI, attribute.localName); + for (const key of Object.keys(element.attributes)) { + const segs = key.split(':'); + const namespace = shaka.util.TXml.getKnownNameSpace(NS); + if (segs[0] == namespace) { + delete element.attributes[key]; } } @@ -465,7 +471,7 @@ shaka.dash.MpdUtils = class { // top-level element. If there are multiple roots, it will be // rejected. const rootElem = - shaka.util.XmlUtils.parseXml(response.data, element.tagName); + TXml.parseXml(response.data, element.tagName); if (!rootElem) { // It was not valid XML. return shaka.util.AbortableOperation.failed(new Error( @@ -477,20 +483,20 @@ shaka.dash.MpdUtils = class { // the element can be changed further. // Remove the current contents of the node. - while (element.childNodes.length) { - element.removeChild(element.childNodes[0]); - } + element.children = []; // Move the children of the loaded xml into the current element. - while (rootElem.childNodes.length) { - const child = rootElem.childNodes[0]; - rootElem.removeChild(child); - element.appendChild(child); + while (rootElem.children.length) { + const child = rootElem.children.shift(); + if (TXml.isNode(child)) { + child.parent = element; + } + element.children.push(child); } // Move the attributes of the loaded xml into the current element. - for (const attribute of Array.from(rootElem.attributes)) { - element.setAttributeNode(attribute.cloneNode(/* deep= */ false)); + for (const key of Object.keys(rootElem.attributes)) { + element.attributes[key] = rootElem.attributes[key]; } return shaka.dash.MpdUtils.processXlinks( @@ -503,25 +509,26 @@ shaka.dash.MpdUtils = class { * Filter the contents of a node recursively, replacing xlink links * with their associated online data. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.extern.RetryParameters} retryParameters * @param {boolean} failGracefully * @param {string} baseUri * @param {!shaka.net.NetworkingEngine} networkingEngine * @param {number=} linkDepth, default set to 0 - * @return {!shaka.util.AbortableOperation.} + * @return {!shaka.util.AbortableOperation.} */ static processXlinks( - element, retryParameters, failGracefully, baseUri, networkingEngine, + element, retryParameters, + failGracefully, baseUri, networkingEngine, linkDepth = 0) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const NS = MpdUtils.XlinkNamespaceUri_; - if (XmlUtils.getAttributeNS(element, NS, 'href')) { + if (TXml.getAttributeNS(element, NS, 'href')) { let handled = MpdUtils.handleXlinkInElement_( - element, retryParameters, failGracefully, baseUri, networkingEngine, - linkDepth); + element, retryParameters, failGracefully, + baseUri, networkingEngine, linkDepth); if (failGracefully) { // Catch any error and go on. handled = handled.chain(undefined, (error) => { @@ -537,23 +544,23 @@ shaka.dash.MpdUtils = class { } const childOperations = []; - for (const child of Array.from(element.childNodes)) { - if (child instanceof Element) { - const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013'; - if (XmlUtils.getAttributeNS(child, NS, 'href') == resolveToZeroString) { - // This is a 'resolve to zero' code; it means the element should - // be removed, as specified by the mpeg-dash rules for xlink. - element.removeChild(child); - } else if (child.tagName != 'SegmentTimeline') { - // Don't recurse into a SegmentTimeline since xlink attributes - // aren't valid in there and looking at each segment can take a long - // time with larger manifests. - - // Replace the child with its processed form. - childOperations.push(shaka.dash.MpdUtils.processXlinks( - /** @type {!Element} */ (child), retryParameters, failGracefully, - baseUri, networkingEngine, linkDepth)); - } + for (const child of shaka.util.TXml.getChildNodes(element)) { + const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013'; + if (TXml.getAttributeNS(child, NS, 'href') == resolveToZeroString) { + // This is a 'resolve to zero' code; it means the element should + // be removed, as specified by the mpeg-dash rules for xlink. + element.children = element.children.filter( + (elem) => elem !== child); + } else if (child.tagName != 'SegmentTimeline') { + // Don't recurse into a SegmentTimeline since xlink attributes + // aren't valid in there and looking at each segment can take a long + // time with larger manifests. + + // Replace the child with its processed form. + childOperations.push(shaka.dash.MpdUtils.processXlinks( + /** @type {!shaka.extern.xml.Node} */ (child), + retryParameters, failGracefully, + baseUri, networkingEngine, linkDepth)); } } diff --git a/lib/dash/segment_base.js b/lib/dash/segment_base.js index a5f53d13e2..eaee5006af 100644 --- a/lib/dash/segment_base.js +++ b/lib/dash/segment_base.js @@ -16,7 +16,7 @@ goog.require('shaka.media.WebmSegmentIndexParser'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.ObjectUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); goog.requireType('shaka.media.SegmentReference'); @@ -30,13 +30,14 @@ shaka.dash.SegmentBase = class { * Creates an init segment reference from a Context object. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * @param {shaka.extern.aesKey|undefined} aesKey * @return {shaka.media.InitSegmentReference} */ static createInitSegment(context, callback, aesKey) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ManifestParserUtils = shaka.util.ManifestParserUtils; const initialization = @@ -46,15 +47,14 @@ shaka.dash.SegmentBase = class { } let resolvedUris = context.representation.getBaseUris(); - const uri = initialization.getAttribute('sourceURL'); + const uri = initialization.attributes['sourceURL']; if (uri) { resolvedUris = ManifestParserUtils.resolveUris(resolvedUris, [uri]); } let startByte = 0; let endByte = null; - const range = - XmlUtils.parseAttr(initialization, 'range', XmlUtils.parseRange); + const range = TXml.parseAttr(initialization, 'range', TXml.parseRange); if (range) { startByte = range.start; endByte = range.end; @@ -87,7 +87,7 @@ shaka.dash.SegmentBase = class { // the initial parse. const MpdUtils = shaka.dash.MpdUtils; const SegmentBase = shaka.dash.SegmentBase; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute( context, SegmentBase.fromInheritance_, 'presentationTimeOffset')) || 0; @@ -96,7 +96,7 @@ shaka.dash.SegmentBase = class { context, SegmentBase.fromInheritance_, 'timescale'); let timescale = 1; if (timescaleStr) { - timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + timescale = TXml.parsePositiveInt(timescaleStr) || 1; } const scaledPresentationTimeOffset = @@ -201,7 +201,7 @@ shaka.dash.SegmentBase = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { @@ -218,17 +218,17 @@ shaka.dash.SegmentBase = class { static computeIndexRange_(context) { const MpdUtils = shaka.dash.MpdUtils; const SegmentBase = shaka.dash.SegmentBase; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const representationIndex = MpdUtils.inheritChild( context, SegmentBase.fromInheritance_, 'RepresentationIndex'); const indexRangeElem = MpdUtils.inheritAttribute( context, SegmentBase.fromInheritance_, 'indexRange'); - let indexRange = XmlUtils.parseRange(indexRangeElem || ''); + let indexRange = TXml.parseRange(indexRangeElem || ''); if (representationIndex) { - indexRange = XmlUtils.parseAttr( - representationIndex, 'range', XmlUtils.parseRange, indexRange); + indexRange = TXml.parseAttr( + representationIndex, 'range', TXml.parseRange, indexRange); } return indexRange; } @@ -250,7 +250,7 @@ shaka.dash.SegmentBase = class { let indexUris = context.representation.getBaseUris(); if (representationIndex) { - const representationUri = representationIndex.getAttribute('sourceURL'); + const representationUri = representationIndex.attributes['sourceURL']; if (representationUri) { indexUris = ManifestParserUtils.resolveUris( indexUris, [representationUri]); diff --git a/lib/dash/segment_list.js b/lib/dash/segment_list.js index 5b3fb4153f..7a4dbcfa52 100644 --- a/lib/dash/segment_list.js +++ b/lib/dash/segment_list.js @@ -16,7 +16,7 @@ goog.require('shaka.media.SegmentReference'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); @@ -94,7 +94,7 @@ shaka.dash.SegmentList = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { @@ -282,21 +282,21 @@ shaka.dash.SegmentList = class { */ static parseMediaSegments_(context) { const Functional = shaka.util.Functional; - /** @type {!Array.} */ + /** @type {!Array.} */ const segmentLists = [ context.representation.segmentList, context.adaptationSet.segmentList, context.period.segmentList, ].filter(Functional.isNotNull); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; // Search each SegmentList for one with at least one SegmentURL element, // select the first one, and convert each SegmentURL element to a tuple. return segmentLists - .map((node) => { return XmlUtils.findChildren(node, 'SegmentURL'); }) + .map((node) => { return TXml.findChildren(node, 'SegmentURL'); }) .reduce((all, part) => { return all.length > 0 ? all : part; }) .map((urlNode) => { - if (urlNode.getAttribute('indexRange') && + if (urlNode.attributes['indexRange'] && !context.indexRangeWarningGiven) { context.indexRangeWarningGiven = true; shaka.log.warning( @@ -305,9 +305,9 @@ shaka.dash.SegmentList = class { 'attribute or SegmentTimeline, which must be accurate.'); } - const uri = urlNode.getAttribute('media'); - const range = XmlUtils.parseAttr( - urlNode, 'mediaRange', XmlUtils.parseRange, + const uri = urlNode.attributes['media']; + const range = TXml.parseAttr( + urlNode, 'mediaRange', TXml.parseRange, {start: 0, end: null}); return {mediaUri: uri, start: range.start, end: range.end}; }); diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 7fd3d18dad..fb66516cde 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -178,7 +178,7 @@ shaka.dash.SegmentTemplate = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 9fbd167af4..2135e0573c 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -36,9 +36,9 @@ goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.media.SegmentUtils'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); goog.requireType('shaka.hls.Segment'); @@ -3161,7 +3161,7 @@ shaka.hls.HlsParser = class { const dateTimeTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME'); if (dateTimeTag && dateTimeTag.value) { - syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value); + syncTime = shaka.util.TXml.parseDate(dateTimeTag.value); goog.asserts.assert(syncTime != null, 'EXT-X-PROGRAM-DATE-TIME format not valid'); } diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index c3320c11f9..fbe56fd4d5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -27,8 +27,8 @@ goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StreamUtils'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @implements {shaka.util.IDestroyable} */ @@ -1625,6 +1625,7 @@ shaka.media.DrmEngine = class { // // // + const TXml = shaka.util.TXml; const xml = shaka.util.StringUtils.fromUTF16( request.body, /* littleEndian= */ true, /* noThrow= */ true); @@ -1638,24 +1639,28 @@ shaka.media.DrmEngine = class { return; } shaka.log.debug('Unwrapping PlayReady request.'); - const dom = shaka.util.XmlUtils.parseXmlString(xml, 'PlayReadyKeyMessage'); + const dom = TXml.parseXmlString(xml, 'PlayReadyKeyMessage'); goog.asserts.assert(dom, 'Failed to parse PlayReady XML!'); // Set request headers. - const headers = dom.getElementsByTagName('HttpHeader'); + const headers = TXml.getElementsByTagName(dom, 'HttpHeader'); for (const header of headers) { - const name = header.getElementsByTagName('name')[0]; - const value = header.getElementsByTagName('value')[0]; + const name = TXml.getElementsByTagName(header, 'name')[0]; + const value = TXml.getElementsByTagName(header, 'value')[0]; goog.asserts.assert(name && value, 'Malformed PlayReady headers!'); - request.headers[name.textContent] = value.textContent; + request.headers[ + /** @type {string} */(shaka.util.TXml.getTextContents(name))] = + /** @type {string} */(shaka.util.TXml.getTextContents(value)); } // Unpack the base64-encoded challenge. - const challenge = dom.getElementsByTagName('Challenge')[0]; - goog.asserts.assert(challenge, 'Malformed PlayReady challenge!'); - goog.asserts.assert(challenge.getAttribute('encoding') == 'base64encoded', + const challenge = TXml.getElementsByTagName(dom, 'Challenge')[0]; + goog.asserts.assert(challenge, + 'Malformed PlayReady challenge!'); + goog.asserts.assert(challenge.attributes['encoding'] == 'base64encoded', 'Unexpected PlayReady challenge encoding!'); - request.body = shaka.util.Uint8ArrayUtils.fromBase64(challenge.textContent); + request.body = shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */(shaka.util.TXml.getTextContents(challenge))); } /** diff --git a/lib/mss/content_protection.js b/lib/mss/content_protection.js index 90cb5a5371..a7e3edd67b 100644 --- a/lib/mss/content_protection.js +++ b/lib/mss/content_protection.js @@ -11,8 +11,8 @@ goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @@ -23,19 +23,19 @@ shaka.mss.ContentProtection = class { /** * Parses info from the Protection elements. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {!Object.} keySystemsBySystemId * @return {!Array.} */ static parseFromProtection(elems, keySystemsBySystemId) { const ContentProtection = shaka.mss.ContentProtection; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - /** @type {!Array.} */ + /** @type {!Array.} */ let protectionHeader = []; for (const elem of elems) { protectionHeader = protectionHeader.concat( - XmlUtils.findChildren(elem, 'ProtectionHeader')); + TXml.findChildren(elem, 'ProtectionHeader')); } if (!protectionHeader.length) { return []; @@ -123,18 +123,19 @@ shaka.mss.ContentProtection = class { * Parse a PlayReady Header format: https://goo.gl/dBzxNA * a try to find the LA_URL value. * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {string} * @private */ static getLaurl_(xml) { + const TXml = shaka.util.TXml; // LA_URL element is optional and no more than one is // allowed inside the DATA element. Only absolute URLs are allowed. // If the LA_URL element exists, it must not be empty. - for (const elem of xml.getElementsByTagName('DATA')) { - const laUrl = shaka.util.XmlUtils.findChild(elem, 'LA_URL'); + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + const laUrl = TXml.findChild(elem, 'LA_URL'); if (laUrl) { - return laUrl.textContent; + return /** @type {string} */ (shaka.util.TXml.getTextContents(laUrl)); } } @@ -148,7 +149,7 @@ shaka.mss.ContentProtection = class { * Gets a PlayReady license URL from a protection element * containing a PlayReady Header Object * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @return {string} */ static getPlayReadyLicenseUrl(element) { @@ -165,19 +166,21 @@ shaka.mss.ContentProtection = class { * Parse a PlayReady Header format: https://goo.gl/dBzxNA * a try to find the KID value. * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {?string} * @private */ static getKID_(xml) { + const TXml = shaka.util.TXml; // KID element is optional and no more than one is // allowed inside the DATA element. - for (const elem of xml.getElementsByTagName('DATA')) { - const kid = shaka.util.XmlUtils.findChild(elem, 'KID'); + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + const kid = TXml.findChild(elem, 'KID'); if (kid) { // GUID: [DWORD, WORD, WORD, 8-BYTE] const guidBytes = - shaka.util.Uint8ArrayUtils.fromBase64(kid.textContent); + shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(kid))); // Reverse byte order from little-endian to big-endian const kidBytes = new Uint8Array([ guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0], @@ -197,7 +200,7 @@ shaka.mss.ContentProtection = class { * Gets a PlayReady KID from a protection element * containing a PlayReady Header Object * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @return {?string} * @private */ @@ -214,15 +217,16 @@ shaka.mss.ContentProtection = class { /** * Gets a PlayReady Header Object from a protection element * - * @param {!Element} element - * @return {?Element} + * @param {!shaka.extern.xml.Node} element + * @return {?shaka.extern.xml.Node} * @private */ static getPlayReadyHeaderObject_(element) { const ContentProtection = shaka.mss.ContentProtection; const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES; - const bytes = shaka.util.Uint8ArrayUtils.fromBase64(element.textContent); + const bytes = shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(element))); const records = ContentProtection.parseMsPro_(bytes); const record = records.filter((record) => { return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT; @@ -233,7 +237,7 @@ shaka.mss.ContentProtection = class { } const xml = shaka.util.StringUtils.fromUTF16(record.value, true); - const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER'); + const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER'); if (!rootElement) { return null; } @@ -243,7 +247,7 @@ shaka.mss.ContentProtection = class { /** * Gets a initData from a protection element. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {string} systemID * @param {?string} keyId * @return {?Array.} @@ -251,7 +255,8 @@ shaka.mss.ContentProtection = class { */ static getInitDataFromPro_(element, systemID, keyId) { const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - const data = Uint8ArrayUtils.fromBase64(element.textContent); + const data = Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(element))); const systemId = Uint8ArrayUtils.fromHex(systemID.replace(/-/g, '')); const keyIds = new Set(); const psshVersion = 0; @@ -269,7 +274,7 @@ shaka.mss.ContentProtection = class { /** * Creates DrmInfo objects from an array of elements. * - * @param {!Array.} elements + * @param {!Array.} elements * @param {!Object.} keySystemsBySystemId * @return {!Array.} * @private @@ -284,7 +289,7 @@ shaka.mss.ContentProtection = class { for (let i = 0; i < elements.length; i++) { const element = elements[i]; - const systemID = element.getAttribute('SystemID').toLowerCase(); + const systemID = element.attributes['SystemID'].toLowerCase(); const keySystem = keySystemsBySystemId[systemID]; if (keySystem) { const KID = ContentProtection.getPlayReadyKID_(element); @@ -338,7 +343,7 @@ shaka.mss.ContentProtection.PLAYREADY_RECORD_TYPES = { /** * A map of key system name to license server url parser. * - * @const {!Map.} + * @const {!Map.} * @private */ shaka.mss.ContentProtection.licenseUrlParsers_ = new Map() diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index fb16df7a22..87e75832eb 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -22,7 +22,7 @@ goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Timer'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -277,7 +277,7 @@ shaka.mss.MssParser = class { * @private */ parseManifest_(data, finalManifestUri) { - const mss = shaka.util.XmlUtils.parseXml(data, 'SmoothStreamingMedia'); + const mss = shaka.util.TXml.parseXml(data, 'SmoothStreamingMedia'); if (!mss) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -293,13 +293,13 @@ shaka.mss.MssParser = class { /** * Takes a formatted MSS and converts it into a manifest. * - * @param {!Element} mss + * @param {!shaka.extern.xml.Node} mss * @param {string} finalManifestUri The final manifest URI, which may * differ from this.manifestUri_ if there has been a redirect. * @private */ processManifest_(mss, finalManifestUri) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const manifestPreprocessor = this.config_.mss.manifestPreprocessor; if (manifestPreprocessor) { @@ -311,8 +311,8 @@ shaka.mss.MssParser = class { /* presentationStartTime= */ null, /* delay= */ 0); } - const isLive = XmlUtils.parseAttr(mss, 'IsLive', - XmlUtils.parseBoolean, /* defaultValue= */ false); + const isLive = TXml.parseAttr(mss, 'IsLive', + TXml.parseBoolean, /* defaultValue= */ false); if (isLive) { throw new shaka.util.Error( @@ -323,21 +323,21 @@ shaka.mss.MssParser = class { this.presentationTimeline_.setStatic(!isLive); - const timescale = XmlUtils.parseAttr(mss, 'TimeScale', - XmlUtils.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_); + const timescale = TXml.parseAttr(mss, 'TimeScale', + TXml.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_); goog.asserts.assert(timescale && timescale >= 0, 'Timescale must be defined!'); - let dvrWindowLength = XmlUtils.parseAttr(mss, 'DVRWindowLength', - XmlUtils.parseNonNegativeInt); + let dvrWindowLength = TXml.parseAttr(mss, 'DVRWindowLength', + TXml.parseNonNegativeInt); // If the DVRWindowLength field is omitted for a live presentation or set // to 0, the DVR window is effectively infinite if (isLive && (dvrWindowLength === 0 || isNaN(dvrWindowLength))) { dvrWindowLength = Infinity; } // Start-over - const canSeek = XmlUtils.parseAttr(mss, 'CanSeek', - XmlUtils.parseBoolean, /* defaultValue= */ false); + const canSeek = TXml.parseAttr(mss, 'CanSeek', + TXml.parseBoolean, /* defaultValue= */ false); if (dvrWindowLength === 0 && canSeek) { dvrWindowLength = Infinity; } @@ -362,8 +362,8 @@ shaka.mss.MssParser = class { segmentAvailabilityDuration); // Duration in timescale units. - const duration = XmlUtils.parseAttr(mss, 'Duration', - XmlUtils.parseNonNegativeInt, Infinity); + const duration = TXml.parseAttr(mss, 'Duration', + TXml.parseNonNegativeInt, Infinity); goog.asserts.assert(duration && duration >= 0, 'Duration must be defined!'); @@ -413,25 +413,25 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} mss + * @param {!shaka.extern.xml.Node} mss * @param {!shaka.mss.MssParser.Context} context * @private */ parseStreamIndexes_(mss, context) { const ContentProtection = shaka.mss.ContentProtection; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; - const protectionElems = XmlUtils.findChildren(mss, 'Protection'); + const protectionElems = TXml.findChildren(mss, 'Protection'); const drmInfos = ContentProtection.parseFromProtection( protectionElems, this.config_.mss.keySystemsBySystemId); const audioStreams = []; const videoStreams = []; const textStreams = []; - const streamIndexes = XmlUtils.findChildren(mss, 'StreamIndex'); + const streamIndexes = TXml.findChildren(mss, 'StreamIndex'); for (const streamIndex of streamIndexes) { - const qualityLevels = XmlUtils.findChildren(streamIndex, 'QualityLevel'); + const qualityLevels = TXml.findChildren(streamIndex, 'QualityLevel'); const timeline = this.createTimeline_( streamIndex, context.timescale, context.duration); // For each QualityLevel node, create a stream element @@ -466,8 +466,8 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} streamIndex - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} streamIndex + * @param {!shaka.extern.xml.Node} qualityLevel * @param {!Array.} timeline * @param {!Array.} drmInfos * @param {!shaka.mss.MssParser.Context} context @@ -475,11 +475,11 @@ shaka.mss.MssParser = class { * @private */ createStream_(streamIndex, qualityLevel, timeline, drmInfos, context) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; const MssParser = shaka.mss.MssParser; - const type = streamIndex.getAttribute('Type'); + const type = streamIndex.attributes['Type']; const isValidType = type === 'audio' || type === 'video' || type === 'text'; if (!isValidType) { @@ -487,19 +487,19 @@ shaka.mss.MssParser = class { return null; } - const lang = streamIndex.getAttribute('Language'); + const lang = streamIndex.attributes['Language']; const id = this.globalId_++; - const bandwidth = XmlUtils.parseAttr( - qualityLevel, 'Bitrate', XmlUtils.parsePositiveInt); - const width = XmlUtils.parseAttr( - qualityLevel, 'MaxWidth', XmlUtils.parsePositiveInt); - const height = XmlUtils.parseAttr( - qualityLevel, 'MaxHeight', XmlUtils.parsePositiveInt); - const channelsCount = XmlUtils.parseAttr( - qualityLevel, 'Channels', XmlUtils.parsePositiveInt); - const audioSamplingRate = XmlUtils.parseAttr( - qualityLevel, 'SamplingRate', XmlUtils.parsePositiveInt); + const bandwidth = TXml.parseAttr( + qualityLevel, 'Bitrate', TXml.parsePositiveInt); + const width = TXml.parseAttr( + qualityLevel, 'MaxWidth', TXml.parsePositiveInt); + const height = TXml.parseAttr( + qualityLevel, 'MaxHeight', TXml.parsePositiveInt); + const channelsCount = TXml.parseAttr( + qualityLevel, 'Channels', TXml.parsePositiveInt); + const audioSamplingRate = TXml.parseAttr( + qualityLevel, 'SamplingRate', TXml.parsePositiveInt); let duration = context.duration; if (timeline.length) { @@ -515,7 +515,7 @@ shaka.mss.MssParser = class { /** @type {!shaka.extern.Stream} */ const stream = { id: id, - originalId: streamIndex.getAttribute('Name') || String(id), + originalId: streamIndex.attributes['Name'] || String(id), groupId: null, createSegmentIndex: () => Promise.resolve(), closeSegmentIndex: () => Promise.resolve(), @@ -559,7 +559,7 @@ shaka.mss.MssParser = class { }; // This is specifically for text tracks. - const subType = streamIndex.getAttribute('Subtype'); + const subType = streamIndex.attributes['Subtype']; if (subType) { const role = MssParser.ROLE_MAPPING_[subType]; if (role) { @@ -570,12 +570,12 @@ shaka.mss.MssParser = class { } } - let fourCCValue = qualityLevel.getAttribute('FourCC'); + let fourCCValue = qualityLevel.attributes['FourCC']; // If FourCC not defined at QualityLevel level, // then get it from StreamIndex level if (fourCCValue === null || fourCCValue === '') { - fourCCValue = streamIndex.getAttribute('FourCC'); + fourCCValue = streamIndex.attributes['FourCC']; } // If still not defined (optional for audio stream, @@ -694,7 +694,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {string} type * @param {string} fourCCValue * @param {!shaka.extern.Stream} stream @@ -702,7 +702,7 @@ shaka.mss.MssParser = class { * @private */ getCodecPrivateData_(qualityLevel, type, fourCCValue, stream) { - const codecPrivateData = qualityLevel.getAttribute('CodecPrivateData'); + const codecPrivateData = qualityLevel.attributes['CodecPrivateData']; if (codecPrivateData) { return codecPrivateData; } @@ -780,7 +780,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {string} fourCCValue * @param {?string} codecPrivateData * @return {string} @@ -809,7 +809,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {?string} codecPrivateData * @return {string} * @private @@ -835,14 +835,14 @@ shaka.mss.MssParser = class { /** * @param {!shaka.media.InitSegmentReference} initSegmentRef * @param {!shaka.extern.Stream} stream - * @param {!Element} streamIndex + * @param {!shaka.extern.xml.Node} streamIndex * @param {!Array.} timeline * @return {!Array.} * @private */ createSegments_(initSegmentRef, stream, streamIndex, timeline) { const ManifestParserUtils = shaka.util.ManifestParserUtils; - const url = streamIndex.getAttribute('Url'); + const url = streamIndex.attributes['Url']; goog.asserts.assert(url, 'Missing URL for segments'); const mediaUrl = url.replace('{bitrate}', String(stream.bandwidth)); @@ -871,7 +871,7 @@ shaka.mss.MssParser = class { * Expands a streamIndex into an array-based timeline. The results are in * seconds. * - * @param {!Element} streamIndex + * @param {!shaka.extern.xml.Node} streamIndex * @param {number} timescale * @param {number} duration The duration in seconds. * @return {!Array.} @@ -884,9 +884,9 @@ shaka.mss.MssParser = class { goog.asserts.assert( duration > 0, 'duration must be a positive integer'); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - const timePoints = XmlUtils.findChildren(streamIndex, 'c'); + const timePoints = TXml.findChildren(streamIndex, 'c'); /** @type {!Array.} */ const timeline = []; @@ -896,10 +896,10 @@ shaka.mss.MssParser = class { const timePoint = timePoints[i]; const next = timePoints[i + 1]; const t = - XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt); const d = - XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt); - const r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt); + TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt); + const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt); if (!d) { shaka.log.warning( @@ -914,7 +914,7 @@ shaka.mss.MssParser = class { if (repeat < 0) { if (next) { const nextStartTime = - XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(next, 't', TXml.parseNonNegativeInt); if (nextStartTime == null) { shaka.log.warning( 'An "c" element cannot have a negative repeat', diff --git a/lib/text/ttml_text_parser.js b/lib/text/ttml_text_parser.js index 10760fcb25..3fd9bce5c9 100644 --- a/lib/text/ttml_text_parser.js +++ b/lib/text/ttml_text_parser.js @@ -15,7 +15,7 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -53,7 +53,7 @@ shaka.text.TtmlTextParser = class { */ parseMedia(data, time, uri) { const TtmlTextParser = shaka.text.TtmlTextParser; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttpNs = TtmlTextParser.parameterNs_; const ttsNs = TtmlTextParser.styleNs_; const str = shaka.util.StringUtils.fromUTF8(data); @@ -65,7 +65,7 @@ shaka.text.TtmlTextParser = class { return cues; } - const tt = XmlUtils.parseXmlString(str, 'tt'); + const tt = TXml.parseXmlString(str, 'tt'); if (!tt) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -74,23 +74,23 @@ shaka.text.TtmlTextParser = class { 'Failed to parse TTML.'); } - const body = tt.getElementsByTagName('body')[0]; + const body = TXml.getElementsByTagName(tt, 'body')[0]; if (!body) { return []; } // Get the framerate, subFrameRate and frameRateMultiplier if applicable. - const frameRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRate'); - const subFrameRate = XmlUtils.getAttributeNSList( + const frameRate = TXml.getAttributeNSList(tt, ttpNs, 'frameRate'); + const subFrameRate = TXml.getAttributeNSList( tt, ttpNs, 'subFrameRate'); const frameRateMultiplier = - XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier'); - const tickRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'tickRate'); + TXml.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier'); + const tickRate = TXml.getAttributeNSList(tt, ttpNs, 'tickRate'); - const cellResolution = XmlUtils.getAttributeNSList( + const cellResolution = TXml.getAttributeNSList( tt, ttpNs, 'cellResolution'); - const spaceStyle = tt.getAttribute('xml:space') || 'default'; - const extent = XmlUtils.getAttributeNSList(tt, ttsNs, 'extent'); + const spaceStyle = tt.attributes['xml:space'] || 'default'; + const extent = TXml.getAttributeNSList(tt, ttsNs, 'extent'); if (spaceStyle != 'default' && spaceStyle != 'preserve') { throw new shaka.util.Error( @@ -107,10 +107,10 @@ shaka.text.TtmlTextParser = class { const cellResolutionInfo = TtmlTextParser.getCellResolution_(cellResolution); - const metadata = tt.getElementsByTagName('metadata')[0]; - const metadataElements = metadata ? XmlUtils.getChildren(metadata) : []; - const styles = Array.from(tt.getElementsByTagName('style')); - const regionElements = Array.from(tt.getElementsByTagName('region')); + const metadata = TXml.getElementsByTagName(tt, 'metadata')[0]; + const metadataElements = metadata ? metadata.children : []; + const styles = TXml.getElementsByTagName(tt, 'style'); + const regionElements = TXml.getElementsByTagName(tt, 'region'); const cueRegions = []; for (const region of regionElements) { @@ -125,7 +125,7 @@ shaka.text.TtmlTextParser = class { // elements. We used to allow this, but it is non-compliant, and the // loose nature of our previous parser made it difficult to implement TTML // nesting more fully. - if (XmlUtils.findChildren(body, 'p').length) { + if (TXml.findChildren(body, 'p').length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, @@ -133,9 +133,9 @@ shaka.text.TtmlTextParser = class { '

can only be inside

in TTML'); } - for (const div of XmlUtils.findChildren(body, 'div')) { + for (const div of TXml.findChildren(body, 'div')) { // A
element should only contain

, not . - if (XmlUtils.findChildren(div, 'span').length) { + if (TXml.findChildren(div, 'span').length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, @@ -165,16 +165,16 @@ shaka.text.TtmlTextParser = class { /** * Parses a TTML node into a Cue. * - * @param {!Node} cueNode + * @param {!shaka.extern.xml.Node} cueNode * @param {shaka.extern.TextParser.TimeContext} timeContext * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo - * @param {!Array.} metadataElements - * @param {!Array.} styles - * @param {!Array.} regionElements + * @param {!Array.} metadataElements + * @param {!Array.} styles + * @param {!Array.} regionElements * @param {!Array.} cueRegions * @param {boolean} whitespaceTrim * @param {?{columns: number, rows: number}} cellResolution - * @param {?Element} parentCueElement + * @param {?shaka.extern.xml.Node} parentCueElement * @param {boolean} isContent * @param {?(string|undefined)} uri * @return {shaka.text.Cue} @@ -184,17 +184,14 @@ shaka.text.TtmlTextParser = class { cueNode, timeContext, rateInfo, metadataElements, styles, regionElements, cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent, uri) { - /** @type {Element} */ + const TXml = shaka.util.TXml; + const StringUtils = shaka.util.StringUtils; + /** @type {shaka.extern.xml.Node} */ let cueElement; - /** @type {Element} */ - let parentElement = /** @type {Element} */ (cueNode.parentNode); + /** @type {?shaka.extern.xml.Node} */ + let parentElement = parentCueElement; - if (cueNode.nodeType == Node.COMMENT_NODE) { - // The comments do not contain information that interests us here. - return null; - } - - if (cueNode.nodeType == Node.TEXT_NODE) { + if (TXml.isText(cueNode)) { if (!isContent) { // Ignore text elements outside the content. For example, whitespace // on the same lexical level as the

elements, in a document with @@ -205,13 +202,16 @@ shaka.text.TtmlTextParser = class { // So pretend the element was a . parentElement was set above, so // we should still be able to correctly traverse up for timing // information later. - const span = document.createElement('span'); - span.textContent = cueNode.textContent; + /** @type {shaka.extern.xml.Node} */ + const span = { + tagName: 'span', + children: [TXml.getTextContents(cueNode)], + attributes: {}, + parent: null, + }; cueElement = span; } else { - goog.asserts.assert(cueNode.nodeType == Node.ELEMENT_NODE, - 'nodeType should be ELEMENT_NODE!'); - cueElement = /** @type {!Element} */(cueNode); + cueElement = cueNode; } goog.asserts.assert(cueElement, 'cueElement should be non-null!'); @@ -226,7 +226,7 @@ shaka.text.TtmlTextParser = class { } let imageUri = null; - const backgroundImage = shaka.util.XmlUtils.getAttributeNSList( + const backgroundImage = TXml.getAttributeNSList( cueElement, shaka.text.TtmlTextParser.smpteNsList_, 'backgroundImage'); @@ -239,27 +239,24 @@ shaka.text.TtmlTextParser = class { } } - if (cueNode.nodeName == 'p' || imageElement || imageUri) { + if (cueNode.tagName == 'p' || imageElement || imageUri) { isContent = true; } const parentIsContent = isContent; - const spaceStyle = cueElement.getAttribute('xml:space') || + const spaceStyle = cueElement.attributes['xml:space'] || (whitespaceTrim ? 'default' : 'preserve'); const localWhitespaceTrim = spaceStyle == 'default'; // Parse any nested cues first. - const isTextNode = (node) => { - return node.nodeType == Node.TEXT_NODE; - }; - const isLeafNode = Array.from(cueElement.childNodes).every(isTextNode); + const isLeafNode = cueElement.children.every(TXml.isText); const nestedCues = []; if (!isLeafNode) { // Otherwise, recurse into the children. Text nodes will convert into // anonymous spans, which will then be leaf nodes. - for (const childNode of cueElement.childNodes) { + for (const childNode of cueElement.children) { const nestedCue = shaka.text.TtmlTextParser.parseCue_( childNode, timeContext, @@ -284,12 +281,16 @@ shaka.text.TtmlTextParser = class { const isNested = /** @type {boolean} */ (parentCueElement != null); + const textContent = TXml.getTextContents(cueElement); // In this regex, "\S" means "non-whitespace character". - const hasTextContent = /\S/.test(cueElement.textContent); + const hasTextContent = cueElement.children.length && + textContent && + /\S/.test(textContent); + const hasTimeAttributes = - cueElement.hasAttribute('begin') || - cueElement.hasAttribute('end') || - cueElement.hasAttribute('dur'); + cueElement.attributes['begin'] || + cueElement.attributes['end'] || + cueElement.attributes['dur']; if (!hasTimeAttributes && !hasTextContent && cueElement.tagName != 'br' && nestedCues.length == 0) { @@ -310,11 +311,12 @@ shaka.text.TtmlTextParser = class { cueElement, rateInfo); // Resolve local time relative to parent elements. Time elements can appear // all the way up to 'body', but not 'tt'. - while (parentElement && parentElement.nodeType == Node.ELEMENT_NODE && + while (parentElement && TXml.isNode(parentElement) && parentElement.tagName != 'tt') { ({start, end} = shaka.text.TtmlTextParser.resolveTime_( parentElement, rateInfo, start, end)); - parentElement = /** @type {Element} */(parentElement.parentNode); + parentElement = + /** @type {shaka.extern.xml.Node} */ (parentElement.parent); } if (start == null) { @@ -357,7 +359,8 @@ shaka.text.TtmlTextParser = class { let payload = ''; if (isLeafNode) { // If the childNodes are all text, this is a leaf node. Get the payload. - payload = cueElement.textContent; + payload = StringUtils.htmlUnescape( + shaka.util.TXml.getTextContents(cueElement) || ''); if (localWhitespaceTrim) { // Trim leading and trailing whitespace. payload = payload.trim(); @@ -386,16 +389,16 @@ shaka.text.TtmlTextParser = class { // Do not actually apply that region unless it is non-inherited, though. // This makes it so that, if a parent element has a region, the children // don't also all independently apply the positioning of that region. - if (cueElement.hasAttribute('region')) { - if (regionElement && regionElement.getAttribute('xml:id')) { - const regionId = regionElement.getAttribute('xml:id'); + if (cueElement.attributes['region']) { + if (regionElement && regionElement.attributes['xml:id']) { + const regionId = regionElement.attributes['xml:id']; cue.region = cueRegions.filter((region) => region.id == regionId)[0]; } } let regionElementForStyle = regionElement; - if (parentCueElement && isNested && !cueElement.getAttribute('region') && - !cueElement.getAttribute('style')) { + if (parentCueElement && isNested && !cueElement.attributes['region'] && + !cueElement.attributes['style']) { regionElementForStyle = shaka.text.TtmlTextParser.getElementsFromCollection_( parentCueElement, 'region', regionElements, /* prefix= */ '')[0]; @@ -405,7 +408,7 @@ shaka.text.TtmlTextParser = class { cue, cueElement, regionElementForStyle, - imageElement, + /** @type {!shaka.extern.xml.Node} */(imageElement), imageUri, styles, /** isNested= */ parentIsContent, // "nested in a

" doesn't count. @@ -417,9 +420,9 @@ shaka.text.TtmlTextParser = class { /** * Parses an Element into a TextTrackCue or VTTCue. * - * @param {!Element} regionElement - * @param {!Array.} styles Defined in the top of tt element and - * used principally for images. + * @param {!shaka.extern.xml.Node} regionElement + * @param {!Array.} styles + * Defined in the top of tt element and used principally for images. * @param {?string} globalExtent * @return {shaka.text.CueRegion} * @private @@ -427,7 +430,7 @@ shaka.text.TtmlTextParser = class { static parseCueRegion_(regionElement, styles, globalExtent) { const TtmlTextParser = shaka.text.TtmlTextParser; const region = new shaka.text.CueRegion(); - const id = regionElement.getAttribute('xml:id'); + const id = regionElement.attributes['xml:id']; if (!id) { shaka.log.warning('TtmlTextParser parser encountered a region with ' + 'no id. Region will be ignored.'); @@ -506,11 +509,11 @@ shaka.text.TtmlTextParser = class { * Adds applicable style properties to a cue. * * @param {!shaka.text.Cue} cue - * @param {!Element} cueElement - * @param {Element} region - * @param {Element} imageElement + * @param {!shaka.extern.xml.Node} cueElement + * @param {shaka.extern.xml.Node} region + * @param {shaka.extern.xml.Node} imageElement * @param {?string} imageUri - * @param {!Array.} styles + * @param {!Array.} styles * @param {boolean} isNested * @param {boolean} isLeaf * @private @@ -519,6 +522,7 @@ shaka.text.TtmlTextParser = class { cue, cueElement, region, imageElement, imageUri, styles, isNested, isLeaf) { const TtmlTextParser = shaka.text.TtmlTextParser; + const TXml = shaka.util.TXml; const Cue = shaka.text.Cue; // Styles should be inherited from regions, if a style property is not @@ -676,10 +680,10 @@ shaka.text.TtmlTextParser = class { // in PR #1859, in April 2019, and first released in v2.5.0. // Now we check for both, although only imageType (camelCase) is to spec. const backgroundImageType = - imageElement.getAttribute('imageType') || - imageElement.getAttribute('imagetype'); - const backgroundImageEncoding = imageElement.getAttribute('encoding'); - const backgroundImageData = imageElement.textContent.trim(); + imageElement.attributes['imageType'] || + imageElement.attributes['imagetype']; + const backgroundImageEncoding = imageElement.attributes['encoding']; + const backgroundImageData = (TXml.getTextContents(imageElement)).trim(); if (backgroundImageType == 'PNG' && backgroundImageEncoding == 'Base64' && backgroundImageData) { @@ -818,9 +822,9 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on either the original cue element or its * associated region and returns the value if the attribute was found. * - * @param {!Element} cueElement - * @param {Element} region - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} cueElement + * @param {shaka.extern.xml.Node} region + * @param {!Array.} styles * @param {string} attribute * @param {boolean=} shouldInheritRegionStyles * @return {?string} @@ -848,21 +852,21 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on the element's associated region * and returns the value if the attribute was found. * - * @param {Element} region - * @param {!Array.} styles + * @param {shaka.extern.xml.Node} region + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getStyleAttributeFromRegion_(region, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; if (!region) { return null; } - const attr = XmlUtils.getAttributeNSList(region, ttsNs, attribute); + const attr = TXml.getAttributeNSList(region, ttsNs, attribute); if (attr) { return attr; } @@ -875,19 +879,19 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on the cue element and returns the value * if the attribute was found. * - * @param {!Element} cueElement - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} cueElement + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getStyleAttributeFromElement_(cueElement, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; // Styling on elements should take precedence // over the main styling attributes - const elementAttribute = XmlUtils.getAttributeNSList( + const elementAttribute = TXml.getAttributeNSList( cueElement, ttsNs, attribute); @@ -903,14 +907,14 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on an element's styles and the styles those * styles inherit from. * - * @param {!Element} element - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} element + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getInheritedStyleAttribute_(element, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; const ebuttsNs = shaka.text.TtmlTextParser.styleEbuttsNs_; @@ -923,14 +927,14 @@ shaka.text.TtmlTextParser = class { // The last value in our styles stack takes the precedence over the others for (let i = 0; i < inheritedStyles.length; i++) { // Check ebu namespace first. - let styleAttributeValue = XmlUtils.getAttributeNS( + let styleAttributeValue = TXml.getAttributeNS( inheritedStyles[i], ebuttsNs, attribute); if (!styleAttributeValue) { // Fall back to tts namespace. - styleAttributeValue = XmlUtils.getAttributeNSList( + styleAttributeValue = TXml.getAttributeNSList( inheritedStyles[i], ttsNs, attribute); @@ -957,12 +961,12 @@ shaka.text.TtmlTextParser = class { * Selects items from |collection| whose id matches |attributeName| * from |element|. * - * @param {Element} element + * @param {shaka.extern.xml.Node} element * @param {string} attributeName - * @param {!Array.} collection + * @param {!Array.} collection * @param {string} prefixName * @param {string=} nsName - * @return {!Array.} + * @return {!Array.} * @private */ static getElementsFromCollection_( @@ -983,7 +987,7 @@ shaka.text.TtmlTextParser = class { for (const name of itemNames) { for (const item of collection) { - if ((prefixName + item.getAttribute('xml:id')) == name) { + if ((prefixName + item.attributes['xml:id']) == name) { items.push(item); break; } @@ -998,7 +1002,7 @@ shaka.text.TtmlTextParser = class { /** * Traverses upwards from a given node until a given attribute is found. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {string} attributeName * @param {string=} nsName * @return {?string} @@ -1006,19 +1010,19 @@ shaka.text.TtmlTextParser = class { */ static getInheritedAttribute_(element, attributeName, nsName) { let ret = null; - const XmlUtils = shaka.util.XmlUtils; - while (element) { + const TXml = shaka.util.TXml; + while (!ret) { ret = nsName ? - XmlUtils.getAttributeNS(element, nsName, attributeName) : - element.getAttribute(attributeName); + TXml.getAttributeNS(element, nsName, attributeName) : + element.attributes[attributeName]; if (ret) { break; } // Element.parentNode can lead to XMLDocument, which is not an Element and // has no getAttribute(). - const parentNode = element.parentNode; - if (parentNode instanceof Element) { + const parentNode = element.parent; + if (parentNode) { element = parentNode; } else { break; @@ -1031,7 +1035,7 @@ shaka.text.TtmlTextParser = class { * Factor parent/ancestor time attributes into the parsed time of a * child/descendent. * - * @param {!Element} parentElement + * @param {!shaka.extern.xml.Node} parentElement * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo * @param {?number} start The child's start time * @param {?number} end The child's end time @@ -1069,18 +1073,18 @@ shaka.text.TtmlTextParser = class { /** * Parse TTML time attributes from the given element. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo * @return {{start: ?number, end: ?number}} * @private */ static parseTime_(element, rateInfo) { const start = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('begin'), rateInfo); + element.attributes['begin'], rateInfo); let end = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('end'), rateInfo); + element.attributes['end'], rateInfo); const duration = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('dur'), rateInfo); + element.attributes['dur'], rateInfo); if (end == null && duration != null) { end = start + duration; } diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index 54379b8018..fc6b586856 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -15,7 +15,7 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.TextParser'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -451,9 +451,11 @@ shaka.text.VttTextParser = class { */ static parseCueStyles(payload, rootCue, styles) { const VttTextParser = shaka.text.VttTextParser; + const StringUtils = shaka.util.StringUtils; + const TXml = shaka.util.TXml; // Optimization for unstyled payloads. if (!payload.includes('<')) { - rootCue.payload = VttTextParser.htmlUnescape_(payload); + rootCue.payload = StringUtils.htmlUnescape(payload); return; } if (styles.size === 0) { @@ -463,14 +465,19 @@ shaka.text.VttTextParser = class { payload = VttTextParser.replaceKaraokeStylePayload_(payload); payload = VttTextParser.replaceVoiceStylePayload_(payload); const xmlPayload = '' + payload + ''; - const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span'); + let element; + try { + element = TXml.parseXmlString(xmlPayload, 'span'); + } catch (e) { + shaka.log.warning('cue parse fail: ', e); + } + if (element) { - const childNodes = element.childNodes; + const childNodes = element.children; if (childNodes.length == 1) { const childNode = childNodes[0]; - if (childNode.nodeType == Node.TEXT_NODE || - childNode.nodeType == Node.CDATA_SECTION_NODE) { - rootCue.payload = VttTextParser.htmlUnescape_(payload); + if (!TXml.isNode(childNode)) { + rootCue.payload = StringUtils.htmlUnescape(payload); return; } } @@ -479,7 +486,7 @@ shaka.text.VttTextParser = class { } } else { shaka.log.warning('The cue\'s markup could not be parsed: ', payload); - rootCue.payload = VttTextParser.htmlUnescape_(payload); + rootCue.payload = StringUtils.htmlUnescape(payload); } } @@ -700,13 +707,14 @@ shaka.text.VttTextParser = class { } /** - * @param {!Node} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.text.Cue} rootCue * @param {!Map.} styles * @private */ static generateCueFromElement_(element, rootCue, styles) { const VttTextParser = shaka.text.VttTextParser; + const TXml = shaka.util.TXml; const nestedCue = rootCue.clone(); // We don't want propagate some properties. nestedCue.nestedCues = []; @@ -717,11 +725,12 @@ shaka.text.VttTextParser = class { nestedCue.region = new shaka.text.CueRegion(); nestedCue.position = null; nestedCue.size = 0; - if (element.nodeType === Node.ELEMENT_NODE && element.nodeName) { + + if (shaka.util.TXml.isNode(element)) { const bold = shaka.text.Cue.fontWeight.BOLD; const italic = shaka.text.Cue.fontStyle.ITALIC; const underline = shaka.text.Cue.textDecoration.UNDERLINE; - const tags = element.nodeName.split(/(?=[ .])+/g); + const tags = element.tagName.split(/(?=[ .])+/g); for (const tag of tags) { let styleTag = tag; // White blanks at start indicate that the style is a voice @@ -754,15 +763,14 @@ shaka.text.VttTextParser = class { nestedCue.textDecoration.push(underline); break; case 'font': { - const color = - /** @type {!Element} */(element).getAttribute('color'); + const color = element.attributes['color']; if (color) { nestedCue.color = color; } break; } case 'div': { - const time = /** @type {!Element} */(element).getAttribute('time'); + const time = element.attributes['time']; if (!time) { break; } @@ -784,13 +792,13 @@ shaka.text.VttTextParser = class { } } - const isTextNode = (item) => shaka.util.XmlUtils.isText(item); - const childNodes = element.childNodes; + const isTextNode = (item) => shaka.util.TXml.isText(item); + const childNodes = element.children; if (isTextNode(element) || (childNodes.length == 1 && isTextNode(childNodes[0]))) { // Trailing line breaks may lost when convert cue to HTML tag // Need to insert line break cue to preserve line breaks - const textArr = element.textContent.split('\n'); + const textArr = TXml.getTextContents(element).split('\n'); let isFirst = true; for (const text of textArr) { if (!isFirst) { @@ -800,7 +808,7 @@ shaka.text.VttTextParser = class { } if (text.length > 0) { const textCue = nestedCue.clone(); - textCue.payload = VttTextParser.htmlUnescape_(text); + textCue.payload = shaka.util.StringUtils.htmlUnescape(text); rootCue.nestedCues.push(textCue); } isFirst = false; @@ -1016,42 +1024,6 @@ shaka.text.VttTextParser = class { return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600); } - - /** - * This method converts the HTML entities &, <, >, ", ', - *  , ‎ and ‏ in string to their corresponding characters. - * - * @param {!string} input - * @return {string} - * @private - */ - static htmlUnescape_(input) { - // Used to map HTML entities to characters. - const htmlUnescapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': '\'', - ' ': '\u{a0}', - '‎': '\u{200e}', - '‏': '\u{200f}', - }; - - // Used to match HTML entities and HTML characters. - const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39|nbsp|lrm|rlm);/g; - const reHasEscapedHtml = RegExp(reEscapedHtml.source); - // This check is an optimization, since replace always makes a copy - if (input && reHasEscapedHtml.test(input)) { - return input.replace(reEscapedHtml, (entity) => { - // The only thing that might not match the dictionary above is the - // single quote, which can be matched by many strings in the regex, but - // only has a single entry in the dictionary. - return htmlUnescapes[entity] || '\''; - }); - } - return input || ''; - } }; /** diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index 40753123fc..c4b5404c32 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -276,6 +276,42 @@ shaka.util.StringUtils = class { static resetFromCharCode() { shaka.util.StringUtils.fromCharCodeImpl_.reset(); } + + /** + * This method converts the HTML entities &, <, >, ", ', + *  , ‎ and ‏ in string to their corresponding characters. + * + * @param {!string} input + * @return {string} + */ + static htmlUnescape(input) { + // Used to map HTML entities to characters. + const htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', + ''': '\'', + ' ': '\u{a0}', + '‎': '\u{200e}', + '‏': '\u{200f}', + }; + + // Used to match HTML entities and HTML characters. + const reEscapedHtml = /&(?:amp|lt|gt|quot|apos|#(0+)?39|nbsp|lrm|rlm);/g; + const reHasEscapedHtml = RegExp(reEscapedHtml.source); + // This check is an optimization, since replace always makes a copy + if (input && reHasEscapedHtml.test(input)) { + return input.replace(reEscapedHtml, (entity) => { + // The only thing that might not match the dictionary above is the + // single quote, which can be matched by many strings in the regex, but + // only has a single entry in the dictionary. + return htmlUnescapes[entity] || '\''; + }); + } + return input || ''; + } }; diff --git a/lib/util/tXml.js b/lib/util/tXml.js new file mode 100644 index 0000000000..8eb7634f17 --- /dev/null +++ b/lib/util/tXml.js @@ -0,0 +1,724 @@ +goog.provide('shaka.util.TXml'); + +goog.require('shaka.util.StringUtils'); +goog.require('shaka.log'); + +/** + * This code is a modified version of the tXml library. + * + * @author: Tobias Nickel + * created: 06.04.2015 + * https://github.com/TobiasNickel/tXml + */ + +/** + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +shaka.util.TXml = class { + /** + * Parse some data + * @param {BufferSource} data + * @param {string=} expectedRootElemName + * @return {shaka.extern.xml.Node | null} + */ + static parseXml(data, expectedRootElemName) { + const xmlString = shaka.util.StringUtils.fromBytesAutoDetect(data); + return shaka.util.TXml.parseXmlString(xmlString, expectedRootElemName); + } + + /** + * Parse some data + * @param {string} xmlString + * @param {string=} expectedRootElemName + * @return {shaka.extern.xml.Node | null} + */ + static parseXmlString(xmlString, expectedRootElemName) { + const result = shaka.util.TXml.parse(xmlString); + if (!expectedRootElemName && result.length) { + return result[0]; + } + const rootNode = result.find((n) => n.tagName === expectedRootElemName); + if (rootNode) { + return rootNode; + } + + shaka.log.error('parseXml root element not found!'); + return null; + } + + /** + * Parse some data + * @param {string} schema + * @return {string} + */ + static getKnownNameSpace(schema) { + if (shaka.util.TXml.knownNameSpaces_.has(schema)) { + return shaka.util.TXml.knownNameSpaces_.get(schema); + } + return ''; + } + + /** + * Parse some data + * @param {string} schema + * @param {string} NS + */ + static setKnownNameSpace(schema, NS) { + shaka.util.TXml.knownNameSpaces_.set(schema, NS); + } + + /** + * parseXML / html into a DOM Object, + * with no validation and some failure tolerance + * @param {string} S your XML to parse + * @return {Array.} + */ + static parse(S) { + let pos = 0; + + const openBracket = '<'; + const openBracketCC = '<'.charCodeAt(0); + const closeBracket = '>'; + const closeBracketCC = '>'.charCodeAt(0); + const minusCC = '-'.charCodeAt(0); + const slashCC = '/'.charCodeAt(0); + const exclamationCC = '!'.charCodeAt(0); + const singleQuoteCC = '\''.charCodeAt(0); + const doubleQuoteCC = '"'.charCodeAt(0); + const openCornerBracketCC = '['.charCodeAt(0); + + /** + * parsing a list of entries + */ + function parseChildren(tagName, preserveSpace = false) { + /** @type {Array.} */ + const children = []; + while (S[pos]) { + if (S.charCodeAt(pos) == openBracketCC) { + if (S.charCodeAt(pos + 1) === slashCC) { + const closeStart = pos + 2; + pos = S.indexOf(closeBracket, pos); + + const closeTag = S.substring(closeStart, pos); + let indexOfCloseTag = closeTag.indexOf(tagName); + if (indexOfCloseTag == -1) { + // handle VTT closing tags like + const indexOfPeriod = tagName.indexOf('.'); + if (indexOfPeriod > 0) { + const shortTag = tagName.substring(0, indexOfPeriod); + indexOfCloseTag = closeTag.indexOf(shortTag); + } + } + // eslint-disable-next-line no-restricted-syntax + if (indexOfCloseTag == -1) { + const parsedText = S.substring(0, pos).split('\n'); + throw new Error( + 'Unexpected close tag\nLine: ' + (parsedText.length - 1) + + '\nColumn: ' + + (parsedText[parsedText.length - 1].length + 1) + + '\nChar: ' + S[pos], + ); + } + + if (pos + 1) { + pos += 1; + } + + return children; + } else if (S.charCodeAt(pos + 1) === exclamationCC) { + if (S.charCodeAt(pos + 2) == minusCC) { + while (pos !== -1 && !(S.charCodeAt(pos) === closeBracketCC && + S.charCodeAt(pos - 1) == minusCC && + S.charCodeAt(pos - 2) == minusCC && + pos != -1)) { + pos = S.indexOf(closeBracket, pos + 1); + } + if (pos === -1) { + pos = S.length; + } + } else if ( + S.charCodeAt(pos + 2) === openCornerBracketCC && + S.charCodeAt(pos + 8) === openCornerBracketCC && + S.substr(pos + 3, 5).toLowerCase() === 'cdata' + ) { + // cdata + const cdataEndIndex = S.indexOf(']]>', pos); + if (cdataEndIndex == -1) { + children.push(S.substr(pos + 9)); + pos = S.length; + } else { + children.push(S.substring(pos + 9, cdataEndIndex)); + pos = cdataEndIndex + 3; + } + continue; + } + pos++; + continue; + } + const node = parseNode(preserveSpace); + children.push(node); + if (typeof node === 'string') { + return children; + } + if (node.tagName[0] === '?' && node.children) { + children.push(...node.children); + node.children = []; + } + } else { + const text = parseText(); + if (preserveSpace) { + if (text.length > 0) { + children.push(text); + } + } else { + const trimmed = text.trim(); + if (trimmed.length > 0) { + children.push(text); + } + } + pos++; + } + } + return children; + } + + /** + * returns the text outside of texts until the first '<' + */ + function parseText() { + const start = pos; + pos = S.indexOf(openBracket, pos) - 1; + if (pos === -2) { + pos = S.length; + } + return S.slice(start, pos + 1); + } + /** + * returns text until the first nonAlphabetic letter + */ + const nameSpacer = '\r\n\t>/= '; + + /** + * Parse text in current context + * @return {string} + */ + function parseName() { + const start = pos; + while (nameSpacer.indexOf(S[pos]) === -1 && S[pos]) { + pos++; + } + return S.slice(start, pos); + } + + /** + * Parse text in current context + * @param {boolean} preserveSpace Preserve the space between nodes + * @return {shaka.extern.xml.Node | string} + */ + function parseNode(preserveSpace) { + pos++; + const tagName = parseName(); + const attributes = {}; + let children = []; + + // parsing attributes + while (S.charCodeAt(pos) !== closeBracketCC && S[pos]) { + const c = S.charCodeAt(pos); + // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + if ((c > 64 && c < 91) || (c > 96 && c < 123)) { + const name = parseName(); + // search beginning of the string + let code = S.charCodeAt(pos); + while (code && code !== singleQuoteCC && code !== doubleQuoteCC && + !((code > 64 && code < 91) || (code > 96 && code < 123)) && + code !== closeBracketCC) { + pos++; + code = S.charCodeAt(pos); + } + let value = parseString(); + if (code === singleQuoteCC || code === doubleQuoteCC) { + if (pos === -1) { + /** @type {shaka.extern.xml.Node} */ + const node = { + tagName, + attributes, + children, + parent: null, + }; + for (let i = 0; i < children.length; i++) { + if (typeof children[i] !== 'string') { + children[i].parent = node; + } + } + return node; + } + } else { + value = null; + pos--; + } + if (name.startsWith('xmlns:')) { + const segs = name.split(':'); + shaka.util.TXml.setKnownNameSpace( + /** @type {string} */ (value), segs[1]); + } + if (tagName === 'tt' && + name === 'xml:space' && + value === 'preserve') { + preserveSpace = true; + } + attributes[name] = value; + } + pos++; + } + + if (S.charCodeAt(pos - 1) !== slashCC) { + pos++; + const contents = parseChildren(tagName, preserveSpace); + children = contents; + } else { + pos++; + } + /** @type {shaka.extern.xml.Node} */ + const node = { + tagName, + attributes, + children, + parent: null, + }; + for (let i = 0; i < children.length; i++) { + if (typeof children[i] !== 'string') { + children[i].parent = node; + } + } + return node; + } + + /** + * Parse string in current context + * @return {string} + */ + function parseString() { + const startChar = S[pos]; + const startpos = pos + 1; + pos = S.indexOf(startChar, startpos); + return S.slice(startpos, pos); + } + + return parseChildren(''); + } + + /** + * Verifies if the element is a TXml node. + * @param {!shaka.extern.xml.Node} elem The XML element. + * @return {!boolean} Is the element a TXml node + */ + static isNode(elem) { + return !!(elem.tagName); + } + + /** + * Checks if a node is of type text. + * @param {!shaka.extern.xml.Node | string} elem The XML element. + * @return {boolean} True if it is a text node. + */ + static isText(elem) { + return typeof elem === 'string'; + } + + /** + * gets child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @return {!Array.} The child XML elements. + */ + static getChildNodes(elem) { + const found = []; + if (!elem.children) { + return []; + } + for (const child of elem.children) { + if (typeof child !== 'string') { + found.push(child); + } + } + return found; + } + + /** + * Finds child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @return {!Array.} The child XML elements. + */ + static findChildren(elem, name) { + const found = []; + if (!elem.children) { + return []; + } + for (const child of elem.children) { + if (child.tagName === name) { + found.push(child); + } + } + return found; + } + + /** + * Gets inner text. + * @param {!shaka.extern.xml.Node | string} node The XML element. + * @return {?string} The text contents, or null if there are none. + */ + static getTextContents(node) { + if (typeof node === 'string') { + return node; + } + const textContent = node.children.reduce( + (acc, curr) => (typeof curr === 'string' ? acc + curr : acc), + '', + ); + if (textContent === '') { + return null; + } + return textContent; + } + + /** + * Gets the text contents of a node. + * @param {!shaka.extern.xml.Node} node The XML element. + * @return {?string} The text contents, or null if there are none. + */ + static getContents(node) { + if (!Array.from(node.children).every( + (n) => typeof n === 'string' )) { + return null; + } + + // Read merged text content from all text nodes. + let text = shaka.util.TXml.getTextContents(node); + if (text) { + text = text.trim(); + } + return text; + } + + /** + * Finds child XML elements recursively. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @param {!Array.} found accumulator for found nodes + * @return {!Array.} The child XML elements. + */ + static getElementsByTagName(elem, name, found = []) { + if (elem.tagName === name) { + found.push(elem); + } + if (elem.children) { + for (const child of elem.children) { + shaka.util.TXml.getElementsByTagName(child, name, found); + } + } + return found; + } + + /** + * Finds a child XML element. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @return {shaka.extern.xml.Node | null} The child XML element, + * or null if a child XML element + * does not exist with the given tag name OR if there exists more than one + * child XML element with the given tag name. + */ + static findChild(elem, name) { + const children = shaka.util.TXml.findChildren(elem, name); + if (children.length != 1) { + return null; + } + return children[0]; + } + + /** + * Finds a namespace-qualified child XML element. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} ns The child XML element's namespace URI. + * @param {string} name The child XML element's local name. + * @return {shaka.extern.xml.Node | null} The child XML element, or null + * if a child XML element + * does not exist with the given tag name OR if there exists more than one + * child XML element with the given tag name. + */ + static findChildNS(elem, ns, name) { + const children = shaka.util.TXml.findChildrenNS(elem, ns, name); + if (children.length != 1) { + return null; + } + return children[0]; + } + + /** + * Parses an attribute by its name. + * @param {!shaka.extern.xml.Node} elem The XML element. + * @param {string} name The attribute name. + * @param {function(string): (T|null)} parseFunction A function that parses + * the attribute. + * @param {(T|null)=} defaultValue The attribute's default value, if not + * specified, the attibute's default value is null. + * @return {(T|null)} The parsed attribute on success, or the attribute's + * default value if the attribute does not exist or could not be parsed. + * @template T + */ + static parseAttr(elem, name, parseFunction, defaultValue = null) { + let parsedValue = null; + + const value = elem.attributes[name]; + if (value != null) { + parsedValue = parseFunction(value); + } + return parsedValue == null ? defaultValue : parsedValue; + } + + /** + * Gets a namespace-qualified attribute. + * @param {!shaka.extern.xml.Node} elem The element to get from. + * @param {string} ns The namespace URI. + * @param {string} name The local name of the attribute. + * @return {?string} The attribute's value, or null if not present. + */ + static getAttributeNS(elem, ns, name) { + const schemaNS = shaka.util.TXml.getKnownNameSpace(ns); + // Think this is equivalent + const attribute = elem.attributes[`${schemaNS}:${name}`]; + return attribute || null; + } + + /** + * Finds namespace-qualified child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} ns The child XML element's namespace URI. + * @param {string} name The child XML element's local name. + * @return {!Array.} The child XML elements. + */ + static findChildrenNS(elem, ns, name) { + const schemaNS = shaka.util.TXml.getKnownNameSpace(ns); + const found = []; + if (elem.children) { + for (const child of elem.children) { + if (child && child.tagName === `${schemaNS}:${name}`) { + found.push(child); + } + } + } + return found; + } + + /** + * Gets a namespace-qualified attribute. + * @param {!shaka.extern.xml.Node} elem The element to get from. + * @param {!Array.} nsList The lis of namespace URIs. + * @param {string} name The local name of the attribute. + * @return {?string} The attribute's value, or null if not present. + */ + static getAttributeNSList(elem, nsList, name) { + for (const ns of nsList) { + const attr = shaka.util.TXml.getAttributeNS( + elem, ns, name, + ); + if (attr) { + return attr; + } + } + return null; + } + + + /** + * Parses an XML date string. + * @param {string} dateString + * @return {?number} The parsed date in seconds on success; otherwise, return + * null. + */ + static parseDate(dateString) { + if (!dateString) { + return null; + } + + // Times in the manifest should be in UTC. If they don't specify a timezone, + // Date.parse() will use the local timezone instead of UTC. So manually add + // the timezone if missing ('Z' indicates the UTC timezone). + // Format: YYYY-MM-DDThh:mm:ss.ssssss + if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) { + dateString += 'Z'; + } + + const result = Date.parse(dateString); + return isNaN(result) ? null : (result / 1000.0); + } + + + /** + * Parses an XML duration string. + * Negative values are not supported. Years and months are treated as exactly + * 365 and 30 days respectively. + * @param {string} durationString The duration string, e.g., "PT1H3M43.2S", + * which means 1 hour, 3 minutes, and 43.2 seconds. + * @return {?number} The parsed duration in seconds on success; otherwise, + * return null. + * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html} + */ + static parseDuration(durationString) { + if (!durationString) { + return null; + } + + const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + + '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$'; + const matches = new RegExp(re).exec(durationString); + + if (!matches) { + shaka.log.warning('Invalid duration string:', durationString); + return null; + } + + // Note: Number(null) == 0 but Number(undefined) == NaN. + const years = Number(matches[1] || null); + const months = Number(matches[2] || null); + const days = Number(matches[3] || null); + const hours = Number(matches[4] || null); + const minutes = Number(matches[5] || null); + const seconds = Number(matches[6] || null); + + // Assume a year always has 365 days and a month always has 30 days. + const d = (60 * 60 * 24 * 365) * years + + (60 * 60 * 24 * 30) * months + + (60 * 60 * 24) * days + + (60 * 60) * hours + + 60 * minutes + + seconds; + return isFinite(d) ? d : null; + } + + + /** + * Parses a range string. + * @param {string} rangeString The range string, e.g., "101-9213". + * @return {?{start: number, end: number}} The parsed range on success; + * otherwise, return null. + */ + static parseRange(rangeString) { + const matches = /([0-9]+)-([0-9]+)/.exec(rangeString); + + if (!matches) { + return null; + } + + const start = Number(matches[1]); + if (!isFinite(start)) { + return null; + } + + const end = Number(matches[2]); + if (!isFinite(end)) { + return null; + } + + return {start: start, end: end}; + } + + + /** + * Parses an integer. + * @param {string} intString The integer string. + * @return {?number} The parsed integer on success; otherwise, return null. + */ + static parseInt(intString) { + const n = Number(intString); + return (n % 1 === 0) ? n : null; + } + + + /** + * Parses a positive integer. + * @param {string} intString The integer string. + * @return {?number} The parsed positive integer on success; otherwise, + * return null. + */ + static parsePositiveInt(intString) { + const n = Number(intString); + return (n % 1 === 0) && (n > 0) ? n : null; + } + + + /** + * Parses a non-negative integer. + * @param {string} intString The integer string. + * @return {?number} The parsed non-negative integer on success; otherwise, + * return null. + */ + static parseNonNegativeInt(intString) { + const n = Number(intString); + return (n % 1 === 0) && (n >= 0) ? n : null; + } + + + /** + * Parses a floating point number. + * @param {string} floatString The floating point number string. + * @return {?number} The parsed floating point number on success; otherwise, + * return null. May return -Infinity or Infinity. + */ + static parseFloat(floatString) { + const n = Number(floatString); + return !isNaN(n) ? n : null; + } + + + /** + * Parses a boolean. + * @param {string} booleanString The boolean string. + * @return {boolean} The boolean + */ + static parseBoolean(booleanString) { + if (!booleanString) { + return false; + } + return booleanString.toLowerCase() === 'true'; + } + + + /** + * Evaluate a division expressed as a string. + * @param {string} exprString + * The expression to evaluate, e.g. "200/2". Can also be a single number. + * @return {?number} The evaluated expression as floating point number on + * success; otherwise return null. + */ + static evalDivision(exprString) { + let res; + let n; + if ((res = exprString.match(/^(\d+)\/(\d+)$/))) { + n = Number(res[1]) / Number(res[2]); + } else { + n = Number(exprString); + } + return !isNaN(n) ? n : null; + } +}; + +shaka.util.TXml.knownNameSpaces_ = new Map([]); diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js deleted file mode 100644 index abe74880d6..0000000000 --- a/lib/util/xml_utils.js +++ /dev/null @@ -1,461 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shaka.util.XmlUtils'); - -goog.require('goog.asserts'); -goog.require('shaka.log'); -goog.require('shaka.util.Lazy'); -goog.require('shaka.util.StringUtils'); - - -/** - * @summary A set of XML utility functions. - */ -shaka.util.XmlUtils = class { - /** - * Finds a child XML element. - * @param {!Node} elem The parent XML element. - * @param {string} name The child XML element's tag name. - * @return {Element} The child XML element, or null if a child XML element - * does not exist with the given tag name OR if there exists more than one - * child XML element with the given tag name. - */ - static findChild(elem, name) { - const children = shaka.util.XmlUtils.findChildren(elem, name); - if (children.length != 1) { - return null; - } - return children[0]; - } - - - /** - * Finds a namespace-qualified child XML element. - * @param {!Node} elem The parent XML element. - * @param {string} ns The child XML element's namespace URI. - * @param {string} name The child XML element's local name. - * @return {Element} The child XML element, or null if a child XML element - * does not exist with the given tag name OR if there exists more than one - * child XML element with the given tag name. - */ - static findChildNS(elem, ns, name) { - const children = shaka.util.XmlUtils.findChildrenNS(elem, ns, name); - if (children.length != 1) { - return null; - } - return children[0]; - } - - - /** - * 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. - */ - static findChildren(elem, name) { - const found = []; - for (const child of elem.childNodes) { - if (child instanceof Element && child.tagName == name) { - found.push(child); - } - } - return found; - } - - - /** - * @param {!Node} elem the parent XML element. - * @return {!Array.} The child XML elements. - */ - static getChildren(elem) { - return Array.from(elem.childNodes).filter((child) => { - return child instanceof Element; - }); - } - - - /** - * Finds namespace-qualified child XML elements. - * @param {!Node} elem The parent XML element. - * @param {string} ns The child XML element's namespace URI. - * @param {string} name The child XML element's local name. - * @return {!Array.} The child XML elements. - */ - static findChildrenNS(elem, ns, name) { - const found = []; - for (const child of elem.childNodes) { - if (child instanceof Element && child.localName == name && - child.namespaceURI == ns) { - found.push(child); - } - } - return found; - } - - - /** - * Gets a namespace-qualified attribute. - * @param {!Element} elem The element to get from. - * @param {string} ns The namespace URI. - * @param {string} name The local name of the attribute. - * @return {?string} The attribute's value, or null if not present. - */ - static getAttributeNS(elem, ns, name) { - // Some browsers return the empty string when the attribute is missing, - // so check if it exists first. See: https://mzl.la/2L7F0UK - return elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null; - } - - - /** - * Gets a namespace-qualified attribute. - * @param {!Element} elem The element to get from. - * @param {!Array.} nsList The lis of namespace URIs. - * @param {string} name The local name of the attribute. - * @return {?string} The attribute's value, or null if not present. - */ - static getAttributeNSList(elem, nsList, name) { - // Some browsers return the empty string when the attribute is missing, - // so check if it exists first. See: https://mzl.la/2L7F0UK - for (const ns of nsList) { - if (elem.hasAttributeNS(ns, name)) { - return elem.getAttributeNS(ns, name); - } - } - return null; - } - - - /** - * Gets the text contents of a node. - * @param {!Node} elem The XML element. - * @return {?string} The text contents, or null if there are none. - */ - static getContents(elem) { - const XmlUtils = shaka.util.XmlUtils; - if (!Array.from(elem.childNodes).every(XmlUtils.isText)) { - return null; - } - - // Read merged text content from all text nodes. - return elem.textContent.trim(); - } - - /** - * Checks if a node is of type text. - * @param {!Node} elem The XML element. - * @return {boolean} True if it is a text node. - */ - static isText(elem) { - return elem.nodeType == Node.TEXT_NODE || - elem.nodeType == Node.CDATA_SECTION_NODE; - } - - /** - * Parses an attribute by its name. - * @param {!Element} elem The XML element. - * @param {string} name The attribute name. - * @param {function(string): (T|null)} parseFunction A function that parses - * the attribute. - * @param {(T|null)=} defaultValue The attribute's default value, if not - * specified, the attibute's default value is null. - * @return {(T|null)} The parsed attribute on success, or the attribute's - * default value if the attribute does not exist or could not be parsed. - * @template T - */ - static parseAttr( - elem, name, parseFunction, defaultValue = null) { - let parsedValue = null; - - const value = elem.getAttribute(name); - if (value != null) { - parsedValue = parseFunction(value); - } - return parsedValue == null ? defaultValue : parsedValue; - } - - - /** - * Parses an XML date string. - * @param {string} dateString - * @return {?number} The parsed date in seconds on success; otherwise, return - * null. - */ - static parseDate(dateString) { - if (!dateString) { - return null; - } - - // Times in the manifest should be in UTC. If they don't specify a timezone, - // Date.parse() will use the local timezone instead of UTC. So manually add - // the timezone if missing ('Z' indicates the UTC timezone). - // Format: YYYY-MM-DDThh:mm:ss.ssssss - if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) { - dateString += 'Z'; - } - - const result = Date.parse(dateString); - return isNaN(result) ? null : (result / 1000.0); - } - - - /** - * Parses an XML duration string. - * Negative values are not supported. Years and months are treated as exactly - * 365 and 30 days respectively. - * @param {string} durationString The duration string, e.g., "PT1H3M43.2S", - * which means 1 hour, 3 minutes, and 43.2 seconds. - * @return {?number} The parsed duration in seconds on success; otherwise, - * return null. - * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html} - */ - static parseDuration(durationString) { - if (!durationString) { - return null; - } - - const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + - '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$'; - const matches = new RegExp(re).exec(durationString); - - if (!matches) { - shaka.log.warning('Invalid duration string:', durationString); - return null; - } - - // Note: Number(null) == 0 but Number(undefined) == NaN. - const years = Number(matches[1] || null); - const months = Number(matches[2] || null); - const days = Number(matches[3] || null); - const hours = Number(matches[4] || null); - const minutes = Number(matches[5] || null); - const seconds = Number(matches[6] || null); - - // Assume a year always has 365 days and a month always has 30 days. - const d = (60 * 60 * 24 * 365) * years + - (60 * 60 * 24 * 30) * months + - (60 * 60 * 24) * days + - (60 * 60) * hours + - 60 * minutes + - seconds; - return isFinite(d) ? d : null; - } - - - /** - * Parses a range string. - * @param {string} rangeString The range string, e.g., "101-9213". - * @return {?{start: number, end: number}} The parsed range on success; - * otherwise, return null. - */ - static parseRange(rangeString) { - const matches = /([0-9]+)-([0-9]+)/.exec(rangeString); - - if (!matches) { - return null; - } - - const start = Number(matches[1]); - if (!isFinite(start)) { - return null; - } - - const end = Number(matches[2]); - if (!isFinite(end)) { - return null; - } - - return {start: start, end: end}; - } - - - /** - * Parses an integer. - * @param {string} intString The integer string. - * @return {?number} The parsed integer on success; otherwise, return null. - */ - static parseInt(intString) { - const n = Number(intString); - return (n % 1 === 0) ? n : null; - } - - - /** - * Parses a positive integer. - * @param {string} intString The integer string. - * @return {?number} The parsed positive integer on success; otherwise, - * return null. - */ - static parsePositiveInt(intString) { - const n = Number(intString); - return (n % 1 === 0) && (n > 0) ? n : null; - } - - - /** - * Parses a non-negative integer. - * @param {string} intString The integer string. - * @return {?number} The parsed non-negative integer on success; otherwise, - * return null. - */ - static parseNonNegativeInt(intString) { - const n = Number(intString); - return (n % 1 === 0) && (n >= 0) ? n : null; - } - - - /** - * Parses a floating point number. - * @param {string} floatString The floating point number string. - * @return {?number} The parsed floating point number on success; otherwise, - * return null. May return -Infinity or Infinity. - */ - static parseFloat(floatString) { - const n = Number(floatString); - return !isNaN(n) ? n : null; - } - - - /** - * Parses a boolean. - * @param {string} booleanString The boolean string. - * @return {boolean} The boolean - */ - static parseBoolean(booleanString) { - if (!booleanString) { - return false; - } - return booleanString.toLowerCase() === 'true'; - } - - - /** - * Evaluate a division expressed as a string. - * @param {string} exprString - * The expression to evaluate, e.g. "200/2". Can also be a single number. - * @return {?number} The evaluated expression as floating point number on - * success; otherwise return null. - */ - static evalDivision(exprString) { - let res; - let n; - if ((res = exprString.match(/^(\d+)\/(\d+)$/))) { - n = Number(res[1]) / Number(res[2]); - } else { - n = Number(exprString); - } - return !isNaN(n) ? n : null; - } - - - /** - * Parse a string and return the resulting root element if it was valid XML. - * - * @param {string} xmlString - * @param {string} expectedRootElemName - * @return {Element} - */ - static parseXmlString(xmlString, expectedRootElemName) { - const parser = new DOMParser(); - const unsafeXmlString = - shaka.util.XmlUtils.trustedHTMLFromString_.value()(xmlString); - let unsafeXml = null; - try { - unsafeXml = parser.parseFromString(unsafeXmlString, 'text/xml'); - } catch (exception) { - shaka.log.error('XML parsing exception:', exception); - return null; - } - - // According to MDN, parseFromString never returns null. - goog.asserts.assert(unsafeXml, 'Parsed XML document cannot be null!'); - - // Check for empty documents. - const rootElem = unsafeXml.documentElement; - if (!rootElem) { - shaka.log.error('XML document was empty!'); - return null; - } - - // Check for parser errors. - const parserErrorElements = rootElem.getElementsByTagName('parsererror'); - if (parserErrorElements.length) { - shaka.log.error('XML parser error found:', parserErrorElements[0]); - return null; - } - - // The top-level element in the loaded XML should have the name we expect. - if (rootElem.tagName != expectedRootElemName) { - shaka.log.error( - `XML tag name does not match expected "${expectedRootElemName}":`, - rootElem.tagName); - return null; - } - - // Cobalt browser doesn't support document.createNodeIterator. - if (!('createNodeIterator' in document)) { - return rootElem; - } - - // SECURITY: Verify that the document does not contain elements from the - // HTML or SVG namespaces, which could trigger script execution and XSS. - const iterator = document.createNodeIterator( - unsafeXml, - NodeFilter.SHOW_ALL, - ); - let currentNode; - while (currentNode = iterator.nextNode()) { - if (currentNode instanceof HTMLElement || - currentNode instanceof SVGElement) { - shaka.log.error('XML document embeds unsafe content!'); - return null; - } - } - - return rootElem; - } - - - /** - * Parse some data (auto-detecting the encoding) and return the resulting - * root element if it was valid XML. - * @param {BufferSource} data - * @param {string} expectedRootElemName - * @return {Element} - */ - static parseXml(data, expectedRootElemName) { - try { - const string = shaka.util.StringUtils.fromBytesAutoDetect(data); - return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName); - } catch (exception) { - shaka.log.error('parseXmlString threw!', exception); - return null; - } - } -}; - -/** - * Promote a string to TrustedHTML. This function is security-sensitive and - * should only be used with security approval where the string is guaranteed not - * to cause an XSS vulnerability. - * - * @private {!shaka.util.Lazy.} - */ -shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => { - if (typeof trustedTypes !== 'undefined') { - // Create a Trusted Types policy for promoting the string to TrustedHTML. - // The Lazy wrapper ensures this policy is only created once. - const policy = trustedTypes.createPolicy('shaka-player#xml', { - createHTML: (s) => s, - }); - return (s) => policy.createHTML(s); - } - // Fall back to strings in environments that don't support Trusted Types. - return (s) => s; -}); - diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 8785c6b15c..5a9fb51a6c 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -9,8 +9,7 @@ describe('DashParser ContentProtection', () => { const Dash = shaka.test.Dash; const ContentProtection = shaka.dash.ContentProtection; const strToXml = (str) => { - const parser = new DOMParser(); - return parser.parseFromString(str, 'application/xml').documentElement; + return shaka.util.TXml.parseXmlString(str); }; /** diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index dd0673cd41..90d78f0ef9 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1300,7 +1300,7 @@ describe('DashParser Live', () => { startTime: 10, endTime: 60, id: '', - eventElement: jasmine.any(Element), + eventElement: jasmine.any(Object), }); expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({ schemeIdUri: 'http://example.com', @@ -1308,7 +1308,7 @@ describe('DashParser Live', () => { startTime: 13, endTime: 23, id: 'abc', - eventElement: jasmine.any(Element), + eventElement: jasmine.any(Object), }); }); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index be4ed43dc5..9ed899d3af 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -910,36 +910,6 @@ describe('DashParser Manifest', () => { }); describe('fails for', () => { - it('invalid XML', async () => { - const source = ' { - const source = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join('\n'); - const error = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.DASH_INVALID_XML, - 'dummy://foo'); - await Dash.testFails(source, error); - }); - it('xlink problems when xlinkFailGracefully is false', async () => { const source = [ ' { fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.manifestPreprocessor = (mpd) => { - const selector = 'AdaptationSet[mimeType="text/vtt"'; - const vttElements = mpd.querySelectorAll(selector); - for (const element of vttElements) { - element.parentNode.removeChild(element); - } + /** @type {shaka.extern.xml.Node} */ + const manifest = /** @type {shaka.extern.xml.Node} */ ( + /** @type {shaka.extern.xml.Node} */(mpd).children[0]); + manifest.children = [ + manifest.children[0], + manifest.children[1], + ]; }; parser.configure(config); diff --git a/test/dash/mpd_utils_unit.js b/test/dash/mpd_utils_unit.js index 9ce1ec892d..6cbab088bd 100644 --- a/test/dash/mpd_utils_unit.js +++ b/test/dash/mpd_utils_unit.js @@ -452,11 +452,9 @@ describe('MpdUtils', () => { ' />'); } xmlLines.push(''); - const parser = new DOMParser(); - const xml = - parser.parseFromString(xmlLines.join('\n'), 'application/xml'); - const segmentTimeline = xml.documentElement; - console.assert(segmentTimeline); + const segmentTimeline = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(xmlLines.join('\n'), + 'SegmentTimeline')); const timeline = MpdUtils.createTimeline( segmentTimeline, timescale, presentationTimeOffset, @@ -473,8 +471,6 @@ describe('MpdUtils', () => { let fakeNetEngine; /** @type {shaka.extern.RetryParameters} */ let retry; - /** @type {!DOMParser} */ - let parser; /** @type {boolean} */ let failGracefully; @@ -482,7 +478,6 @@ describe('MpdUtils', () => { failGracefully = false; retry = shaka.net.NetworkingEngine.defaultRetryParameters(); fakeNetEngine = new shaka.test.FakeNetworkingEngine(); - parser = new DOMParser(); }); it('will replace elements and children', async () => { @@ -541,18 +536,6 @@ describe('MpdUtils', () => { await testSucceeds(baseXMLString, desiredXMLString, 3); }); - it('fails if loaded file is invalid xml', async () => { - const baseXMLString = inBaseContainer( - ''); - // Note this does not have a close angle bracket. - const xlinkXMLString = ' { const baseXMLString = inBaseContainer( @@ -691,8 +674,8 @@ describe('MpdUtils', () => { /** @type {!shaka.util.PublicPromise} */ const continuePromise = fakeNetEngine.delayNextRequest(); - const xml = parser.parseFromString(baseXMLString, 'text/xml') - .documentElement; + const xml = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(baseXMLString)); /** @type {!shaka.extern.IAbortableOperation} */ const operation = MpdUtils.processXlinks( xml, retry, failGracefully, 'https://base', fakeNetEngine); @@ -730,11 +713,11 @@ describe('MpdUtils', () => { async function testSucceeds( baseXMLString, desiredXMLString, desiredNetCalls) { - const desiredXML = parser.parseFromString(desiredXMLString, 'text/xml') - .documentElement; + const desiredXML = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(desiredXMLString)); const finalXML = await testRequest(baseXMLString); expect(fakeNetEngine.request).toHaveBeenCalledTimes(desiredNetCalls); - expect(finalXML).toEqualElement(desiredXML); + expect(finalXML).toEqual(desiredXML); } async function testFails(baseXMLString, desiredError, desiredNetCalls) { @@ -785,8 +768,8 @@ describe('MpdUtils', () => { } function testRequest(baseXMLString) { - const xml = parser.parseFromString(baseXMLString, 'text/xml') - .documentElement; + const xml = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(baseXMLString)); return MpdUtils.processXlinks(xml, retry, failGracefully, 'https://base', fakeNetEngine).promise; } diff --git a/test/mss/mss_parser_content_protection_unit.js b/test/mss/mss_parser_content_protection_unit.js index cc160c0f10..10983f2989 100644 --- a/test/mss/mss_parser_content_protection_unit.js +++ b/test/mss/mss_parser_content_protection_unit.js @@ -9,8 +9,7 @@ describe('MssParser ContentProtection', () => { const ContentProtection = shaka.mss.ContentProtection; const strToXml = (str) => { - const parser = new DOMParser(); - return parser.parseFromString(str, 'application/xml').documentElement; + return shaka.util.TXml.parseXmlString(str); }; it('getPlayReadyLicenseURL', () => { diff --git a/test/mss/mss_parser_unit.js b/test/mss/mss_parser_unit.js index 41df61695b..3f52ad8d9e 100644 --- a/test/mss/mss_parser_unit.js +++ b/test/mss/mss_parser_unit.js @@ -85,35 +85,6 @@ describe('MssParser Manifest', () => { }); describe('fails for', () => { - it('invalid XML', async () => { - const source = ' { - const source = [ - '', - ' ', - ' ', - ' ', - ' ', - ].join('\n'); - const error = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.MSS_INVALID_XML, - 'dummy://foo'); - await Mss.testFails(source, error); - }); - it('failed network requests', async () => { const expectedError = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -273,11 +244,7 @@ describe('MssParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.mss.manifestPreprocessor = (mss) => { - const selector = 'StreamIndex[Name="text"'; - const vttElements = mss.querySelectorAll(selector); - for (const element of vttElements) { - element.parentNode.removeChild(element); - } + /** @type{shaka.extern.xml.Node} */ (mss).children.pop(); }; parser.configure(config); diff --git a/test/test/util/util.js b/test/test/util/util.js index d20b134e93..99b923ce85 100644 --- a/test/test/util/util.js +++ b/test/test/util/util.js @@ -154,7 +154,7 @@ shaka.test.Util = class { (expected['outerHTML'] || expected.textContent) + ': '; const getAttr = (obj, attr) => { if (attr.namespaceURI) { - return shaka.util.XmlUtils.getAttributeNS( + return shaka.util.TXml.getAttributeNS( obj, attr.namespaceURI, attr.localName); } else { return obj.getAttribute(attr.localName); diff --git a/test/util/tXml_unit.js b/test/util/tXml_unit.js new file mode 100644 index 0000000000..46f8576800 --- /dev/null +++ b/test/util/tXml_unit.js @@ -0,0 +1,391 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('tXml', () => { + // A number that cannot be represented as a Javascript number. + const HUGE_NUMBER_STRING = new Array(500).join('7'); + + const TXml = shaka.util.TXml; + + describe('findChild', () => { + it('finds a child node', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.findChild(root, 'Child')).toBeTruthy(); + expect(TXml.findChild(root, 'DoesNotExist')).toBeNull(); + }); + + it('handles duplicate child nodes', () => { + const xmlString = [ + '', + '', + ' ', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.findChild(root, 'Child')).toBeNull(); + }); + }); + + it('findChildren', () => { + const xmlString = [ + '', + '', + ' ', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(root).toBeTruthy(); + + let children = TXml.findChildren(root, 'Child'); + expect(children.length).toBe(2); + + children = TXml.findChildren(root, 'DoesNotExist'); + expect(children.length).toBe(0); + }); + + describe('getContents', () => { + it('returns node contents', () => { + const xmlString = [ + '', + '', + ' foo bar', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBe('foo bar'); + }); + + it('handles empty node contents', () => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBeNull(); + }); + + it('handles null node contents', () => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + const xml = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(xml, 'parseFromString should succeed'); + + expect(TXml.getContents(xml)).toBeNull(); + }); + + it('handles CDATA sections', () => { + const xmlString = [ + '', + '', + ' Bar]]>', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBe(' Bar'); + }); + }); + + describe('parseAttr', () => { + /** @type {!shaka.extern.xml.Node} */ + let xml; + + beforeEach(() => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + xml = /** @type {!shaka.extern.xml.Node} */ ( + TXml.parseXmlString(xmlString, 'Root')); + }); + + it('delegates to parser function', () => { + const root = xml; + expect(TXml.parseAttr(root, 'a', TXml.parseRange)).toEqual( + {start: 2, end: 7}); + expect(TXml.parseAttr(root, 'b', TXml.parseInt)).toBe(-5); + expect(TXml.parseAttr(root, 'c', TXml.parseInt)).toBe(0); + expect(TXml.parseAttr(root, 'd', TXml.parseInt)).toBeNull(); + }); + + it('supports default values', () => { + const root = xml; + goog.asserts.assert(root, 'findChild should find element'); + expect(TXml.parseAttr(root, 'd', TXml.parseInt, 9)).toBe(9); + }); + }); + + describe('parseXmlString', () => { + it('parses a simple XML document', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(root.tagName).toBe('Root'); + }); + + it('returns null on an empty XML document', () => { + const xmlString = ''; + const doc = TXml.parseXmlString(xmlString, 'Root'); + expect(doc).toBeNull(); + }); + + it('returns null on root element mismatch', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const doc = TXml.parseXmlString(xmlString, 'Document'); + expect(doc).toBeNull(); + }); + }); + + it('parseDate', () => { + // Should be parsed as UTC independent of local timezone. + expect(TXml.parseDate('2015-11-30T12:46:33')).toBe(1448887593); + // Should be parsed using the given timezone, not the local timezone. + expect(TXml.parseDate('2015-11-30T12:46:33+06:00')).toBe(1448865993); + + expect(TXml.parseDate('November 30, 2015')).toBeTruthy(); + expect(TXml.parseDate('Apple')).toBeNull(); + expect(TXml.parseDate('')).toBeNull(); + }); + + it('parseDuration', () => { + // No time. + expect(TXml.parseDuration('P')).toBe(0); + expect(TXml.parseDuration('PT')).toBe(0); + + // Years only. 1 year has 365 or 366 days. + expect(TXml.parseDuration('P3Y')).toBeLessThan( + 3 * (60 * 60 * 24 * 366) + 1); + expect(TXml.parseDuration('P3Y')).toBeGreaterThan( + 3 * (60 * 60 * 24 * 365) - 1); + + // Months only. 1 month has 28 to 31 days. + expect(TXml.parseDuration('P2M')).toBeLessThan( + 2 * (60 * 60 * 24 * 31) + 1); + expect(TXml.parseDuration('P2M')).toBeGreaterThan( + 2 * (60 * 60 * 24 * 28) - 1); + + // Days only. + expect(TXml.parseDuration('P7D')).toBe(604800); + + // Hours only. + expect(TXml.parseDuration('PT1H')).toBe(3600); + + // Minutes only. + expect(TXml.parseDuration('PT1M')).toBe(60); + + // Seconds only (with no fractional part). + expect(TXml.parseDuration('PT1S')).toBe(1); + + // Seconds only (with no whole part). + expect(TXml.parseDuration('PT0.1S')).toBe(0.1); + expect(TXml.parseDuration('PT.1S')).toBe(0.1); + + // Seconds only (with whole part and fractional part). + expect(TXml.parseDuration('PT1.1S')).toBe(1.1); + + // Hours, and minutes. + expect(TXml.parseDuration('PT1H2M')).toBe(3720); + + // Hours, and seconds. + expect(TXml.parseDuration('PT1H2S')).toBe(3602); + expect(TXml.parseDuration('PT1H2.2S')).toBe(3602.2); + + // Minutes, and seconds. + expect(TXml.parseDuration('PT1M2S')).toBe(62); + expect(TXml.parseDuration('PT1M2.2S')).toBe(62.2); + + // Hours, minutes, and seconds. + expect(TXml.parseDuration('PT1H2M3S')).toBe(3723); + expect(TXml.parseDuration('PT1H2M3.3S')).toBe(3723.3); + + // Days, hours, minutes, and seconds. + expect(TXml.parseDuration('P1DT1H2M3S')).toBe(90123); + expect(TXml.parseDuration('P1DT1H2M3.3S')).toBe(90123.3); + + // Months, hours, minutes, and seconds. + expect(TXml.parseDuration('P1M1DT1H2M3S')).toBeLessThan( + (60 * 60 * 24 * 31) + 90123 + 1); + expect(TXml.parseDuration('P1M1DT1H2M3S')).toBeGreaterThan( + (60 * 60 * 24 * 28) + 90123 - 1); + + // Years, Months, hours, minutes, and seconds. + expect(TXml.parseDuration('P1Y1M1DT1H2M3S')).toBeLessThan( + (60 * 60 * 24 * 366) + (60 * 60 * 24 * 31) + 90123 + 1); + expect(TXml.parseDuration('P1Y1M1DT1H2M3S')).toBeGreaterThan( + (60 * 60 * 24 * 365) + (60 * 60 * 24 * 28) + 90123 - 1); + + expect(TXml.parseDuration('PT')).toBe(0); + expect(TXml.parseDuration('P')).toBe(0); + + // Error cases. + expect(TXml.parseDuration('-PT3S')).toBeNull(); + expect(TXml.parseDuration('PT-3S')).toBeNull(); + expect(TXml.parseDuration('P1Sasdf')).toBeNull(); + expect(TXml.parseDuration('1H2M3S')).toBeNull(); + expect(TXml.parseDuration('123')).toBeNull(); + expect(TXml.parseDuration('abc')).toBeNull(); + expect(TXml.parseDuration('')).toBeNull(); + + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'Y')).toBeNull(); + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'M')).toBeNull(); + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'D')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'H')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'M')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'S')).toBeNull(); + }); + + it('parseRange', () => { + expect(TXml.parseRange('0-0')).toEqual({start: 0, end: 0}); + expect(TXml.parseRange('1-1')).toEqual({start: 1, end: 1}); + expect(TXml.parseRange('1-50')).toEqual({start: 1, end: 50}); + expect(TXml.parseRange('50-1')).toEqual({start: 50, end: 1}); + + expect(TXml.parseRange('-1')).toBeNull(); + expect(TXml.parseRange('1-')).toBeNull(); + expect(TXml.parseRange('1')).toBeNull(); + expect(TXml.parseRange('-')).toBeNull(); + expect(TXml.parseRange('')).toBeNull(); + + expect(TXml.parseRange('abc')).toBeNull(); + expect(TXml.parseRange('a-')).toBeNull(); + expect(TXml.parseRange('-b')).toBeNull(); + expect(TXml.parseRange('a-b')).toBeNull(); + + expect(TXml.parseRange(HUGE_NUMBER_STRING + '-1')).toBeNull(); + expect(TXml.parseRange('1-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseInt', () => { + expect(TXml.parseInt('0')).toBe(0); + expect(TXml.parseInt('1')).toBe(1); + expect(TXml.parseInt('191')).toBe(191); + + expect(TXml.parseInt('-0')).toBe(0); + expect(TXml.parseInt('-1')).toBe(-1); + expect(TXml.parseInt('-191')).toBe(-191); + + expect(TXml.parseInt('abc')).toBeNull(); + expect(TXml.parseInt('1abc')).toBeNull(); + expect(TXml.parseInt('abc1')).toBeNull(); + + expect(TXml.parseInt('0.0')).toBe(0); + expect(TXml.parseInt('-0.0')).toBe(0); + + expect(TXml.parseInt('0.1')).toBeNull(); + expect(TXml.parseInt('1.1')).toBeNull(); + + expect(TXml.parseInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parseInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parsePositiveInt', () => { + expect(TXml.parsePositiveInt('0')).toBeNull(); + expect(TXml.parsePositiveInt('1')).toBe(1); + expect(TXml.parsePositiveInt('191')).toBe(191); + + expect(TXml.parsePositiveInt('-0')).toBeNull(); + expect(TXml.parsePositiveInt('-1')).toBeNull(); + expect(TXml.parsePositiveInt('-191')).toBeNull(); + + expect(TXml.parsePositiveInt('abc')).toBeNull(); + expect(TXml.parsePositiveInt('1abc')).toBeNull(); + expect(TXml.parsePositiveInt('abc1')).toBeNull(); + + expect(TXml.parsePositiveInt('0.0')).toBeNull(); + expect(TXml.parsePositiveInt('-0.0')).toBeNull(); + + expect(TXml.parsePositiveInt('0.1')).toBeNull(); + expect(TXml.parsePositiveInt('1.1')).toBeNull(); + + expect(TXml.parsePositiveInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parsePositiveInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseNonNegativeInt', () => { + expect(TXml.parseNonNegativeInt('0')).toBe(0); + expect(TXml.parseNonNegativeInt('1')).toBe(1); + expect(TXml.parseNonNegativeInt('191')).toBe(191); + + expect(TXml.parseNonNegativeInt('-0')).toBe(0); + expect(TXml.parseNonNegativeInt('-1')).toBeNull(); + expect(TXml.parseNonNegativeInt('-191')).toBeNull(); + + expect(TXml.parseNonNegativeInt('abc')).toBeNull(); + expect(TXml.parseNonNegativeInt('1abc')).toBeNull(); + expect(TXml.parseNonNegativeInt('abc1')).toBeNull(); + + expect(TXml.parseNonNegativeInt('0.0')).toBe(0); + expect(TXml.parseNonNegativeInt('-0.0')).toBe(0); + + expect(TXml.parseNonNegativeInt('0.1')).toBeNull(); + expect(TXml.parseNonNegativeInt('1.1')).toBeNull(); + + expect(TXml.parseNonNegativeInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parseNonNegativeInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseFloat', () => { + expect(TXml.parseFloat('0')).toBe(0); + expect(TXml.parseFloat('1')).toBe(1); + expect(TXml.parseFloat('191')).toBe(191); + + expect(TXml.parseFloat('-0')).toBe(0); + expect(TXml.parseFloat('-1')).toBe(-1); + expect(TXml.parseFloat('-191')).toBe(-191); + + expect(TXml.parseFloat('abc')).toBeNull(); + expect(TXml.parseFloat('1abc')).toBeNull(); + expect(TXml.parseFloat('abc1')).toBeNull(); + + expect(TXml.parseFloat('0.0')).toBe(0); + expect(TXml.parseFloat('-0.0')).toBe(0); + + expect(TXml.parseFloat('0.1')).toBeCloseTo(0.1); + expect(TXml.parseFloat('1.1')).toBeCloseTo(1.1); + + expect(TXml.parseFloat('19.1134')).toBeCloseTo(19.1134); + expect(TXml.parseFloat('4e2')).toBeCloseTo(4e2); + expect(TXml.parseFloat('4e-2')).toBeCloseTo(4e-2); + + expect(TXml.parseFloat(HUGE_NUMBER_STRING)).toBe(Infinity); + expect(TXml.parseFloat('-' + HUGE_NUMBER_STRING)).toBe(-Infinity); + }); +}); diff --git a/test/util/xml_utils_unit.js b/test/util/xml_utils_unit.js deleted file mode 100644 index d89275b543..0000000000 --- a/test/util/xml_utils_unit.js +++ /dev/null @@ -1,444 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -describe('XmlUtils', () => { - // A number that cannot be represented as a Javascript number. - const HUGE_NUMBER_STRING = new Array(500).join('7'); - - const XmlUtils = shaka.util.XmlUtils; - - describe('findChild', () => { - it('finds a child node', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - - expect(XmlUtils.findChild(root, 'Child')).toBeTruthy(); - expect(XmlUtils.findChild(root, 'DoesNotExist')).toBeNull(); - }); - - it('handles duplicate child nodes', () => { - const xmlString = [ - '', - '', - ' ', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - - expect(XmlUtils.findChild(root, 'Child')).toBeNull(); - }); - }); - - it('findChildren', () => { - const xmlString = [ - '', - '', - ' ', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const roots = XmlUtils.findChildren(xml, 'Root'); - expect(roots).toBeTruthy(); - expect(roots.length).toBe(1); - - let children = XmlUtils.findChildren(roots[0], 'Child'); - expect(children.length).toBe(2); - - children = XmlUtils.findChildren(roots[0], 'DoesNotExist'); - expect(children.length).toBe(0); - }); - - describe('getContents', () => { - it('returns node contents', () => { - const xmlString = [ - '', - '', - ' foo bar', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe('foo bar'); - }); - - it('handles empty node contents', () => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe(''); - }); - - it('handles null node contents', () => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - expect(XmlUtils.getContents(xml)).toBeNull(); - }); - - it('handles CDATA sections', () => { - const xmlString = [ - '', - '', - ' Bar]]>', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe(' Bar'); - }); - }); - - describe('parseAttr', () => { - /** @type {!Document} */ - let xml; - - beforeEach(() => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - xml = /** @type {!Document} */ ( - new DOMParser().parseFromString(xmlString, 'application/xml')); - }); - - it('delegates to parser function', () => { - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.parseAttr(root, 'a', XmlUtils.parseRange)).toEqual( - {start: 2, end: 7}); - expect(XmlUtils.parseAttr(root, 'b', XmlUtils.parseInt)).toBe(-5); - expect(XmlUtils.parseAttr(root, 'c', XmlUtils.parseInt)).toBe(0); - expect(XmlUtils.parseAttr(root, 'd', XmlUtils.parseInt)).toBeNull(); - }); - - it('supports default values', () => { - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.parseAttr(root, 'd', XmlUtils.parseInt, 9)).toBe(9); - }); - }); - - it('parseDate', () => { - // Should be parsed as UTC independent of local timezone. - expect(XmlUtils.parseDate('2015-11-30T12:46:33')).toBe(1448887593); - // Should be parsed using the given timezone, not the local timezone. - expect(XmlUtils.parseDate('2015-11-30T12:46:33+06:00')).toBe(1448865993); - - expect(XmlUtils.parseDate('November 30, 2015')).toBeTruthy(); - expect(XmlUtils.parseDate('Apple')).toBeNull(); - expect(XmlUtils.parseDate('')).toBeNull(); - }); - - it('parseDuration', () => { - // No time. - expect(XmlUtils.parseDuration('P')).toBe(0); - expect(XmlUtils.parseDuration('PT')).toBe(0); - - // Years only. 1 year has 365 or 366 days. - expect(XmlUtils.parseDuration('P3Y')).toBeLessThan( - 3 * (60 * 60 * 24 * 366) + 1); - expect(XmlUtils.parseDuration('P3Y')).toBeGreaterThan( - 3 * (60 * 60 * 24 * 365) - 1); - - // Months only. 1 month has 28 to 31 days. - expect(XmlUtils.parseDuration('P2M')).toBeLessThan( - 2 * (60 * 60 * 24 * 31) + 1); - expect(XmlUtils.parseDuration('P2M')).toBeGreaterThan( - 2 * (60 * 60 * 24 * 28) - 1); - - // Days only. - expect(XmlUtils.parseDuration('P7D')).toBe(604800); - - // Hours only. - expect(XmlUtils.parseDuration('PT1H')).toBe(3600); - - // Minutes only. - expect(XmlUtils.parseDuration('PT1M')).toBe(60); - - // Seconds only (with no fractional part). - expect(XmlUtils.parseDuration('PT1S')).toBe(1); - - // Seconds only (with no whole part). - expect(XmlUtils.parseDuration('PT0.1S')).toBe(0.1); - expect(XmlUtils.parseDuration('PT.1S')).toBe(0.1); - - // Seconds only (with whole part and fractional part). - expect(XmlUtils.parseDuration('PT1.1S')).toBe(1.1); - - // Hours, and minutes. - expect(XmlUtils.parseDuration('PT1H2M')).toBe(3720); - - // Hours, and seconds. - expect(XmlUtils.parseDuration('PT1H2S')).toBe(3602); - expect(XmlUtils.parseDuration('PT1H2.2S')).toBe(3602.2); - - // Minutes, and seconds. - expect(XmlUtils.parseDuration('PT1M2S')).toBe(62); - expect(XmlUtils.parseDuration('PT1M2.2S')).toBe(62.2); - - // Hours, minutes, and seconds. - expect(XmlUtils.parseDuration('PT1H2M3S')).toBe(3723); - expect(XmlUtils.parseDuration('PT1H2M3.3S')).toBe(3723.3); - - // Days, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1DT1H2M3S')).toBe(90123); - expect(XmlUtils.parseDuration('P1DT1H2M3.3S')).toBe(90123.3); - - // Months, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1M1DT1H2M3S')).toBeLessThan( - (60 * 60 * 24 * 31) + 90123 + 1); - expect(XmlUtils.parseDuration('P1M1DT1H2M3S')).toBeGreaterThan( - (60 * 60 * 24 * 28) + 90123 - 1); - - // Years, Months, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1Y1M1DT1H2M3S')).toBeLessThan( - (60 * 60 * 24 * 366) + (60 * 60 * 24 * 31) + 90123 + 1); - expect(XmlUtils.parseDuration('P1Y1M1DT1H2M3S')).toBeGreaterThan( - (60 * 60 * 24 * 365) + (60 * 60 * 24 * 28) + 90123 - 1); - - expect(XmlUtils.parseDuration('PT')).toBe(0); - expect(XmlUtils.parseDuration('P')).toBe(0); - - // Error cases. - expect(XmlUtils.parseDuration('-PT3S')).toBeNull(); - expect(XmlUtils.parseDuration('PT-3S')).toBeNull(); - expect(XmlUtils.parseDuration('P1Sasdf')).toBeNull(); - expect(XmlUtils.parseDuration('1H2M3S')).toBeNull(); - expect(XmlUtils.parseDuration('123')).toBeNull(); - expect(XmlUtils.parseDuration('abc')).toBeNull(); - expect(XmlUtils.parseDuration('')).toBeNull(); - - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'Y')).toBeNull(); - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'M')).toBeNull(); - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'D')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'H')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'M')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'S')).toBeNull(); - }); - - it('parseRange', () => { - expect(XmlUtils.parseRange('0-0')).toEqual({start: 0, end: 0}); - expect(XmlUtils.parseRange('1-1')).toEqual({start: 1, end: 1}); - expect(XmlUtils.parseRange('1-50')).toEqual({start: 1, end: 50}); - expect(XmlUtils.parseRange('50-1')).toEqual({start: 50, end: 1}); - - expect(XmlUtils.parseRange('-1')).toBeNull(); - expect(XmlUtils.parseRange('1-')).toBeNull(); - expect(XmlUtils.parseRange('1')).toBeNull(); - expect(XmlUtils.parseRange('-')).toBeNull(); - expect(XmlUtils.parseRange('')).toBeNull(); - - expect(XmlUtils.parseRange('abc')).toBeNull(); - expect(XmlUtils.parseRange('a-')).toBeNull(); - expect(XmlUtils.parseRange('-b')).toBeNull(); - expect(XmlUtils.parseRange('a-b')).toBeNull(); - - expect(XmlUtils.parseRange(HUGE_NUMBER_STRING + '-1')).toBeNull(); - expect(XmlUtils.parseRange('1-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseInt', () => { - expect(XmlUtils.parseInt('0')).toBe(0); - expect(XmlUtils.parseInt('1')).toBe(1); - expect(XmlUtils.parseInt('191')).toBe(191); - - expect(XmlUtils.parseInt('-0')).toBe(0); - expect(XmlUtils.parseInt('-1')).toBe(-1); - expect(XmlUtils.parseInt('-191')).toBe(-191); - - expect(XmlUtils.parseInt('abc')).toBeNull(); - expect(XmlUtils.parseInt('1abc')).toBeNull(); - expect(XmlUtils.parseInt('abc1')).toBeNull(); - - expect(XmlUtils.parseInt('0.0')).toBe(0); - expect(XmlUtils.parseInt('-0.0')).toBe(0); - - expect(XmlUtils.parseInt('0.1')).toBeNull(); - expect(XmlUtils.parseInt('1.1')).toBeNull(); - - expect(XmlUtils.parseInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parseInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parsePositiveInt', () => { - expect(XmlUtils.parsePositiveInt('0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1')).toBe(1); - expect(XmlUtils.parsePositiveInt('191')).toBe(191); - - expect(XmlUtils.parsePositiveInt('-0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-1')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-191')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('abc')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1abc')).toBeNull(); - expect(XmlUtils.parsePositiveInt('abc1')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('0.0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-0.0')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('0.1')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1.1')).toBeNull(); - - expect(XmlUtils.parsePositiveInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parsePositiveInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseNonNegativeInt', () => { - expect(XmlUtils.parseNonNegativeInt('0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('1')).toBe(1); - expect(XmlUtils.parseNonNegativeInt('191')).toBe(191); - - expect(XmlUtils.parseNonNegativeInt('-0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('-1')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('-191')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt('abc')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('1abc')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('abc1')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt('0.0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('-0.0')).toBe(0); - - expect(XmlUtils.parseNonNegativeInt('0.1')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('1.1')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseFloat', () => { - expect(XmlUtils.parseFloat('0')).toBe(0); - expect(XmlUtils.parseFloat('1')).toBe(1); - expect(XmlUtils.parseFloat('191')).toBe(191); - - expect(XmlUtils.parseFloat('-0')).toBe(0); - expect(XmlUtils.parseFloat('-1')).toBe(-1); - expect(XmlUtils.parseFloat('-191')).toBe(-191); - - expect(XmlUtils.parseFloat('abc')).toBeNull(); - expect(XmlUtils.parseFloat('1abc')).toBeNull(); - expect(XmlUtils.parseFloat('abc1')).toBeNull(); - - expect(XmlUtils.parseFloat('0.0')).toBe(0); - expect(XmlUtils.parseFloat('-0.0')).toBe(0); - - expect(XmlUtils.parseFloat('0.1')).toBeCloseTo(0.1); - expect(XmlUtils.parseFloat('1.1')).toBeCloseTo(1.1); - - expect(XmlUtils.parseFloat('19.1134')).toBeCloseTo(19.1134); - expect(XmlUtils.parseFloat('4e2')).toBeCloseTo(4e2); - expect(XmlUtils.parseFloat('4e-2')).toBeCloseTo(4e-2); - - expect(XmlUtils.parseFloat(HUGE_NUMBER_STRING)).toBe(Infinity); - expect(XmlUtils.parseFloat('-' + HUGE_NUMBER_STRING)).toBe(-Infinity); - }); - - describe('parseXmlString', () => { - it('parses a simple XML document', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).not.toBeNull(); - expect(doc.tagName).toBe('Root'); - }); - - it('returns null on an empty XML document', () => { - const xmlString = ''; - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on malformed XML', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on root element mismatch', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Document'); - expect(doc).toBeNull(); - }); - - it('returns null on XML that embeds HTML', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on XML that embeds SVG', () => { - // Some platforms, such as Xbox One, don't recognize elements as SVG - // based on namespace alone. So the SVG element below needs to be a real - // SVG element. - const xmlString = [ - '', - '', - ' ', - ' ', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - }); -}); -