From ebdf9d781729da20b506ecbf06cdddea6898133d Mon Sep 17 00:00:00 2001 From: Michelle Zhuo Date: Thu, 23 May 2019 16:42:33 -0700 Subject: [PATCH] Update Util files to ES6 Issue #1157 Change-Id: Ib81d198e46bc57745f60af328d1160064e253ba3 --- lib/hls/hls_parser.js | 3 +- lib/util/event_manager.js | 241 +++--- lib/util/functional.js | 90 +- lib/util/manifest_parser_utils.js | 83 +- lib/util/map_utils.js | 53 +- lib/util/mp4_parser.js | 484 +++++------ lib/util/multi_map.js | 148 ++-- lib/util/pssh.js | 133 +-- lib/util/public_promise.js | 65 +- lib/util/stream_utils.js | 1320 ++++++++++++++--------------- lib/util/string_utils.js | 382 ++++----- lib/util/text_parser.js | 217 ++--- lib/util/uint8array_utils.js | 254 +++--- lib/util/xml_utils.js | 589 +++++++------ 14 files changed, 2033 insertions(+), 2029 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 732e5df49..3d8318f12 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -484,7 +484,6 @@ shaka.hls.HlsParser = class { * @private */ async createPeriod_(playlist) { - const Functional = shaka.util.Functional; const tags = playlist.tags; const mediaTags = @@ -515,7 +514,7 @@ shaka.hls.HlsParser = class { }); const allVariants = await Promise.all(variantsPromises); - let variants = allVariants.reduce(Functional.collapseArrays, []); + let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []); // Filter out null variants. variants = variants.filter((variant) => variant != null); diff --git a/lib/util/event_manager.js b/lib/util/event_manager.js index f81040475..c6ebe9071 100644 --- a/lib/util/event_manager.js +++ b/lib/util/event_manager.js @@ -23,19 +23,108 @@ goog.require('shaka.util.MultiMap'); /** - * Creates a new EventManager. An EventManager maintains a collection of "event + * @summary + * An EventManager maintains a collection of "event * bindings" between event targets and event listeners. * - * @struct - * @constructor * @implements {shaka.util.IReleasable} */ -shaka.util.EventManager = function() { +shaka.util.EventManager = class { + constructor() { + /** + * Maps an event type to an array of event bindings. + * @private {shaka.util.MultiMap.} + */ + this.bindingMap_ = new shaka.util.MultiMap(); + } + + /** - * Maps an event type to an array of event bindings. - * @private {shaka.util.MultiMap.} + * Detaches all event listeners. + * @override */ - this.bindingMap_ = new shaka.util.MultiMap(); + release() { + this.removeAll(); + this.bindingMap_ = null; + } + + + /** + * Attaches an event listener to an event target. + * @param {EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType} listener The event listener. + */ + listen(target, type, listener) { + if (!this.bindingMap_) { + return; + } + + const binding = new shaka.util.EventManager.Binding_(target, type, + listener); + this.bindingMap_.push(type, binding); + } + + + /** + * Attaches an event listener to an event target. The listener will be + * removed when the first instance of the event is fired. + * @param {EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType} listener The event listener. + */ + listenOnce(target, type, listener) { + // Install a shim listener that will stop listening after the first event. + const shim = (event) => { + // Stop listening to this event. + this.unlisten(target, type, shim); + // Call the original listener. + listener(event); + }; + this.listen(target, type, shim); + } + + + /** + * Detaches an event listener from an event target. + * @param {EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType=} listener The event listener. + */ + unlisten(target, type, listener) { + if (!this.bindingMap_) { + return; + } + + const list = this.bindingMap_.get(type) || []; + + for (const binding of list) { + if (binding.target == target) { + if (listener == binding.listener || !listener) { + binding.unlisten(); + this.bindingMap_.remove(type, binding); + } + } + } + } + + + /** + * Detaches all event listeners from all targets. + */ + removeAll() { + if (!this.bindingMap_) { + return; + } + + const list = this.bindingMap_.getAll(); + + for (const binding of list) { + binding.unlisten(); + } + + this.bindingMap_.clear(); + } }; @@ -45,127 +134,41 @@ shaka.util.EventManager = function() { shaka.util.EventManager.ListenerType; -/** - * Detaches all event listeners. - * @override - */ -shaka.util.EventManager.prototype.release = function() { - this.removeAll(); - this.bindingMap_ = null; -}; - - -/** - * Attaches an event listener to an event target. - * @param {EventTarget} target The event target. - * @param {string} type The event type. - * @param {shaka.util.EventManager.ListenerType} listener The event listener. - */ -shaka.util.EventManager.prototype.listen = function(target, type, listener) { - if (!this.bindingMap_) { - return; - } - - const binding = new shaka.util.EventManager.Binding_(target, type, listener); - this.bindingMap_.push(type, binding); -}; - - -/** - * Attaches an event listener to an event target. The listener will be removed - * when the first instance of the event is fired. - * @param {EventTarget} target The event target. - * @param {string} type The event type. - * @param {shaka.util.EventManager.ListenerType} listener The event listener. - */ -shaka.util.EventManager.prototype.listenOnce = - function(target, type, listener) { - // Install a shim listener that will stop listening after the first event. - const shim = (event) => { - // Stop listening to this event. - this.unlisten(target, type, shim); - // Call the original listener. - listener(event); - }; - this.listen(target, type, shim); - }; - - -/** - * Detaches an event listener from an event target. - * @param {EventTarget} target The event target. - * @param {string} type The event type. - * @param {shaka.util.EventManager.ListenerType=} listener The event listener. - */ -shaka.util.EventManager.prototype.unlisten = function(target, type, listener) { - if (!this.bindingMap_) { - return; - } - - const list = this.bindingMap_.get(type) || []; - - for (let i = 0; i < list.length; ++i) { - const binding = list[i]; - - if (binding.target == target) { - if (listener == binding.listener || !listener) { - binding.unlisten(); - this.bindingMap_.remove(type, binding); - } - } - } -}; - - -/** - * Detaches all event listeners from all targets. - */ -shaka.util.EventManager.prototype.removeAll = function() { - if (!this.bindingMap_) { - return; - } - - const list = this.bindingMap_.getAll(); - - for (let i = 0; i < list.length; ++i) { - list[i].unlisten(); - } - - this.bindingMap_.clear(); -}; - - /** * Creates a new Binding_ and attaches the event listener to the event target. - * @param {EventTarget} target The event target. - * @param {string} type The event type. - * @param {shaka.util.EventManager.ListenerType} listener The event listener. - * @constructor + * * @private */ -shaka.util.EventManager.Binding_ = function(target, type, listener) { - /** @type {EventTarget} */ - this.target = target; +shaka.util.EventManager.Binding_ = class { + /** + * @param {EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType} listener The event listener. + */ + constructor(target, type, listener) { + /** @type {EventTarget} */ + this.target = target; - /** @type {string} */ - this.type = type; + /** @type {string} */ + this.type = type; - /** @type {?shaka.util.EventManager.ListenerType} */ - this.listener = listener; + /** @type {?shaka.util.EventManager.ListenerType} */ + this.listener = listener; - this.target.addEventListener(type, listener, false); -}; + this.target.addEventListener(type, listener, false); + } -/** - * Detaches the event listener from the event target. This does nothing if the - * event listener is already detached. - */ -shaka.util.EventManager.Binding_.prototype.unlisten = function() { - goog.asserts.assert(this.target, 'Missing target'); - this.target.removeEventListener(this.type, this.listener, false); + /** + * Detaches the event listener from the event target. This does nothing if + * the event listener is already detached. + */ + unlisten() { + goog.asserts.assert(this.target, 'Missing target'); + this.target.removeEventListener(this.type, this.listener, false); - this.target = null; - this.listener = null; + this.target = null; + this.listener = null; + } }; diff --git a/lib/util/functional.js b/lib/util/functional.js index a7e9c9fad..faf7cf813 100644 --- a/lib/util/functional.js +++ b/lib/util/functional.js @@ -19,57 +19,57 @@ goog.provide('shaka.util.Functional'); /** - * @namespace shaka.util.Functional * @summary A set of functional utility functions. */ +shaka.util.Functional = class { + /** + * Creates a promise chain that calls the given callback for each element in + * the array in a catch of a promise. + * + * e.g.: + * Promise.reject().catch(callback(array[0])).catch(callback(array[1])); + * + * @param {!Array.} array + * @param {function(ELEM):!Promise.} callback + * @return {!Promise.} + * @template ELEM,RESULT + */ + static createFallbackPromiseChain(array, callback) { + return array.reduce(((callback, promise, elem) => { + return promise.catch(callback.bind(null, elem)); + }).bind(null, callback), Promise.reject()); + } -/** - * Creates a promise chain that calls the given callback for each element in - * the array in a catch of a promise. - * - * e.g.: - * Promise.reject().catch(callback(array[0])).catch(callback(array[1])); - * - * @param {!Array.} array - * @param {function(ELEM):!Promise.} callback - * @return {!Promise.} - * @template ELEM,RESULT - */ -shaka.util.Functional.createFallbackPromiseChain = function(array, callback) { - return array.reduce(((callback, promise, elem) => { - return promise.catch(callback.bind(null, elem)); - }).bind(null, callback), Promise.reject()); -}; + /** + * Returns the first array concatenated to the second; used to collapse an + * array of arrays into a single array. + * + * @param {!Array.} all + * @param {!Array.} part + * @return {!Array.} + * @template T + */ + static collapseArrays(all, part) { + return all.concat(part); + } -/** - * Returns the first array concatenated to the second; used to collapse an - * array of arrays into a single array. - * - * @param {!Array.} all - * @param {!Array.} part - * @return {!Array.} - * @template T - */ -shaka.util.Functional.collapseArrays = function(all, part) { - return all.concat(part); -}; + /** + * A no-op function. Useful in promise chains. + */ + static noop() {} -/** - * A no-op function. Useful in promise chains. - */ -shaka.util.Functional.noop = function() {}; - - -/** - * Returns if the given value is not null; useful for filtering out null values. - * - * @param {T} value - * @return {boolean} - * @template T - */ -shaka.util.Functional.isNotNull = function(value) { - return value != null; + /** + * Returns if the given value is not null; useful for filtering out null + * values. + * + * @param {T} value + * @return {boolean} + * @template T + */ + static isNotNull(value) { + return value != null; + } }; diff --git a/lib/util/manifest_parser_utils.js b/lib/util/manifest_parser_utils.js index 84b4c8074..cd5958145 100644 --- a/lib/util/manifest_parser_utils.js +++ b/lib/util/manifest_parser_utils.js @@ -22,54 +22,53 @@ goog.require('shaka.util.Functional'); /** - * @namespace shaka.util.ManifestParserUtils * @summary Utility functions for manifest parsing. */ +shaka.util.ManifestParserUtils = class { + /** + * Resolves an array of relative URIs to the given base URIs. This will result + * in M*N number of URIs. + * + * @param {!Array.} baseUris + * @param {!Array.} relativeUris + * @return {!Array.} + */ + static resolveUris(baseUris, relativeUris) { + const Functional = shaka.util.Functional; + if (relativeUris.length == 0) { + return baseUris; + } - -/** - * Resolves an array of relative URIs to the given base URIs. This will result - * in M*N number of URIs. - * - * @param {!Array.} baseUris - * @param {!Array.} relativeUris - * @return {!Array.} - */ -shaka.util.ManifestParserUtils.resolveUris = function(baseUris, relativeUris) { - const Functional = shaka.util.Functional; - if (relativeUris.length == 0) { - return baseUris; + const relativeAsGoog = relativeUris.map((uri) => new goog.Uri(uri)); + // Resolve each URI relative to each base URI, creating an Array of Arrays. + // Then flatten the Arrays into a single Array. + return baseUris.map((uri) => new goog.Uri(uri)) + .map((base) => relativeAsGoog.map(base.resolve.bind(base))) + .reduce(Functional.collapseArrays, []) + .map((uri) => uri.toString()); } - const relativeAsGoog = relativeUris.map((uri) => new goog.Uri(uri)); - // Resolve each URI relative to each base URI, creating an Array of Arrays. - // Then flatten the Arrays into a single Array. - return baseUris.map((uri) => new goog.Uri(uri)) - .map((base) => relativeAsGoog.map(base.resolve.bind(base))) - .reduce(Functional.collapseArrays, []) - .map((uri) => uri.toString()); -}; - -/** - * Creates a DrmInfo object from the given info. - * - * @param {string} keySystem - * @param {Array.} initData - * @return {shaka.extern.DrmInfo} - */ -shaka.util.ManifestParserUtils.createDrmInfo = function(keySystem, initData) { - return { - keySystem: keySystem, - licenseServerUri: '', - distinctiveIdentifierRequired: false, - persistentStateRequired: false, - audioRobustness: '', - videoRobustness: '', - serverCertificate: null, - initData: initData || [], - keyIds: [], - }; + /** + * Creates a DrmInfo object from the given info. + * + * @param {string} keySystem + * @param {Array.} initData + * @return {shaka.extern.DrmInfo} + */ + static createDrmInfo(keySystem, initData) { + return { + keySystem: keySystem, + licenseServerUri: '', + distinctiveIdentifierRequired: false, + persistentStateRequired: false, + audioRobustness: '', + videoRobustness: '', + serverCertificate: null, + initData: initData || [], + keyIds: [], + }; + } }; diff --git a/lib/util/map_utils.js b/lib/util/map_utils.js index bfb0a3e89..5d7293acc 100644 --- a/lib/util/map_utils.js +++ b/lib/util/map_utils.js @@ -19,36 +19,35 @@ goog.provide('shaka.util.MapUtils'); /** - * @namespace shaka.util.MapUtils * @summary A set of map/object utility functions. */ +shaka.util.MapUtils = class { + /** + * @param {!Object.} object + * @return {!Map.} + * @template KEY,VALUE + */ + static asMap(object) { + const map = new Map(); + Object.keys(object).forEach((key) => { + map.set(key, object[key]); + }); + + return map; + } -/** - * @param {!Object.} object - * @return {!Map.} - * @template KEY,VALUE - */ -shaka.util.MapUtils.asMap = function(object) { - const map = new Map(); - Object.keys(object).forEach((key) => { - map.set(key, object[key]); - }); + /** + * @param {!Map.} map + * @return {!Object.} + * @template KEY,VALUE + */ + static asObject(map) { + const obj = {}; + map.forEach((value, key) => { + obj[key] = value; + }); - return map; -}; - - -/** - * @param {!Map.} map - * @return {!Object.} - * @template KEY,VALUE - */ -shaka.util.MapUtils.asObject = function(map) { - const obj = {}; - map.forEach((value, key) => { - obj[key] = value; - }); - - return obj; + return obj; + } }; diff --git a/lib/util/mp4_parser.js b/lib/util/mp4_parser.js index f8adfd020..6129a6bb3 100644 --- a/lib/util/mp4_parser.js +++ b/lib/util/mp4_parser.js @@ -23,20 +23,252 @@ goog.require('shaka.util.DataViewReader'); /** - * Create a new MP4 Parser - * @struct - * @constructor * @export */ -shaka.util.Mp4Parser = function() { - /** @private {!Object.} */ - this.headers_ = []; +shaka.util.Mp4Parser = class { + constructor() { + /** @private {!Object.} */ + this.headers_ = []; - /** @private {!Object.} */ - this.boxDefinitions_ = []; + /** @private {!Object.} */ + this.boxDefinitions_ = []; - /** @private {boolean} */ - this.done_ = false; + /** @private {boolean} */ + this.done_ = false; + } + + + /** + * Declare a box type as a Box. + * + * @param {string} type + * @param {!shaka.util.Mp4Parser.CallbackType} definition + * @return {!shaka.util.Mp4Parser} + * @export + */ + box(type, definition) { + const typeCode = shaka.util.Mp4Parser.typeFromString_(type); + this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.BASIC_BOX; + this.boxDefinitions_[typeCode] = definition; + return this; + } + + + /** + * Declare a box type as a Full Box. + * + * @param {string} type + * @param {!shaka.util.Mp4Parser.CallbackType} definition + * @return {!shaka.util.Mp4Parser} + * @export + */ + fullBox(type, definition) { + const typeCode = shaka.util.Mp4Parser.typeFromString_(type); + this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.FULL_BOX; + this.boxDefinitions_[typeCode] = definition; + return this; + } + + + /** + * Stop parsing. Useful for extracting information from partial segments and + * avoiding an out-of-bounds error once you find what you are looking for. + * + * @export + */ + stop() { + this.done_ = true; + } + + + /** + * Parse the given data using the added callbacks. + * + * @param {!BufferSource} data + * @param {boolean=} partialOkay If true, allow reading partial payloads + * from some boxes. If the goal is a child box, we can sometimes find it + * without enough data to find all child boxes. + * @export + */ + parse(data, partialOkay) { + const wrapped = new Uint8Array(data); + const reader = new shaka.util.DataViewReader( + new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength), + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + + this.done_ = false; + while (reader.hasMoreData() && !this.done_) { + this.parseNext(0, reader, partialOkay); + } + } + + + /** + * Parse the next box on the current level. + * + * @param {number} absStart The absolute start position in the original + * byte array. + * @param {!shaka.util.DataViewReader} reader + * @param {boolean=} partialOkay If true, allow reading partial payloads + * from some boxes. If the goal is a child box, we can sometimes find it + * without enough data to find all child boxes. + * @export + */ + parseNext(absStart, reader, partialOkay) { + const start = reader.getPosition(); + + let size = reader.readUint32(); + const type = reader.readUint32(); + const name = shaka.util.Mp4Parser.typeToString(type); + shaka.log.v2('Parsing MP4 box', name); + + switch (size) { + case 0: + size = reader.getLength() - start; + break; + case 1: + size = reader.readUint64(); + break; + } + + const boxDefinition = this.boxDefinitions_[type]; + + if (boxDefinition) { + let version = null; + let flags = null; + + if (this.headers_[type] == shaka.util.Mp4Parser.BoxType_.FULL_BOX) { + const versionAndFlags = reader.readUint32(); + version = versionAndFlags >>> 24; + flags = versionAndFlags & 0xFFFFFF; + } + + // Read the whole payload so that the current level can be safely read + // regardless of how the payload is parsed. + let end = start + size; + if (partialOkay && end > reader.getLength()) { + // For partial reads, truncate the payload if we must. + end = reader.getLength(); + } + const payloadSize = end - reader.getPosition(); + const payload = + (payloadSize > 0) ? reader.readBytes(payloadSize) : new Uint8Array(0); + + const payloadReader = new shaka.util.DataViewReader( + new DataView( + payload.buffer, payload.byteOffset, payload.byteLength), + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + + /** @type {shaka.extern.ParsedBox} */ + const box = { + parser: this, + partialOkay: partialOkay || false, + version: version, + flags: flags, + reader: payloadReader, + size: size, + start: start + absStart, + }; + + boxDefinition(box); + } else { + // Move the read head to be at the end of the box. + // If the box is longer than the remaining parts of the file, e.g. the + // mp4 is improperly formatted, or this was a partial range request that + // ended in the middle of a box, just skip to the end. + const skipLength = Math.min( + start + size - reader.getPosition(), + reader.getLength() - reader.getPosition()); + reader.skip(skipLength); + } + } + + + /** + * A callback that tells the Mp4 parser to treat the body of a box as a series + * of boxes. The number of boxes is limited by the size of the parent box. + * + * @param {!shaka.extern.ParsedBox} box + * @export + */ + static children(box) { + while (box.reader.hasMoreData() && !box.parser.done_) { + box.parser.parseNext(box.start, box.reader, box.partialOkay); + } + } + + + /** + * A callback that tells the Mp4 parser to treat the body of a box as a sample + * description. A sample description box has a fixed number of children. The + * number of children is represented by a 4 byte unsigned integer. Each child + * is a box. + * + * @param {!shaka.extern.ParsedBox} box + * @export + */ + static sampleDescription(box) { + for (let count = box.reader.readUint32(); + count > 0 && !box.parser.done_; + count -= 1) { + box.parser.parseNext(box.start, box.reader, box.partialOkay); + } + } + + + /** + * Create a callback that tells the Mp4 parser to treat the body of a box as a + * binary blob and to parse the body's contents using the provided callback. + * + * @param {function(!Uint8Array)} callback + * @return {!shaka.util.Mp4Parser.CallbackType} + * @export + */ + static allData(callback) { + return function(box) { + const all = box.reader.getLength() - box.reader.getPosition(); + callback(box.reader.readBytes(all)); + }; + } + + + /** + * Convert an ascii string name to the integer type for a box. + * + * @param {string} name The name of the box. The name must be four + * characters long. + * @return {number} + * @private + */ + static typeFromString_(name) { + goog.asserts.assert( + name.length == 4, + 'Mp4 box names must be 4 characters long'); + + let code = 0; + for (let i = 0; i < name.length; i++) { + code = (code << 8) | name.charCodeAt(i); + } + return code; + } + + + /** + * Convert an integer type from a box into an ascii string name. + * Useful for debugging. + * + * @param {number} type The type of the box, a uint32. + * @return {string} + * @export + */ + static typeToString(type) { + const name = String.fromCharCode( + (type >> 24) & 0xff, + (type >> 16) & 0xff, + (type >> 8) & 0xff, + type & 0xff); + return name; + } }; @@ -60,235 +292,3 @@ shaka.util.Mp4Parser.BoxType_ = { }; -/** - * Declare a box type as a Box. - * - * @param {string} type - * @param {!shaka.util.Mp4Parser.CallbackType} definition - * @return {!shaka.util.Mp4Parser} - * @export - */ -shaka.util.Mp4Parser.prototype.box = function(type, definition) { - const typeCode = shaka.util.Mp4Parser.typeFromString_(type); - this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.BASIC_BOX; - this.boxDefinitions_[typeCode] = definition; - return this; -}; - - -/** - * Declare a box type as a Full Box. - * - * @param {string} type - * @param {!shaka.util.Mp4Parser.CallbackType} definition - * @return {!shaka.util.Mp4Parser} - * @export - */ -shaka.util.Mp4Parser.prototype.fullBox = function(type, definition) { - const typeCode = shaka.util.Mp4Parser.typeFromString_(type); - this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.FULL_BOX; - this.boxDefinitions_[typeCode] = definition; - return this; -}; - - -/** - * Stop parsing. Useful for extracting information from partial segments and - * avoiding an out-of-bounds error once you find what you are looking for. - * - * @export - */ -shaka.util.Mp4Parser.prototype.stop = function() { - this.done_ = true; -}; - - -/** - * Parse the given data using the added callbacks. - * - * @param {!BufferSource} data - * @param {boolean=} partialOkay If true, allow reading partial payloads - * from some boxes. If the goal is a child box, we can sometimes find it - * without enough data to find all child boxes. - * @export - */ -shaka.util.Mp4Parser.prototype.parse = function(data, partialOkay) { - const wrapped = new Uint8Array(data); - const reader = new shaka.util.DataViewReader( - new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength), - shaka.util.DataViewReader.Endianness.BIG_ENDIAN); - - this.done_ = false; - while (reader.hasMoreData() && !this.done_) { - this.parseNext(0, reader, partialOkay); - } -}; - - -/** - * Parse the next box on the current level. - * - * @param {number} absStart The absolute start position in the original - * byte array. - * @param {!shaka.util.DataViewReader} reader - * @param {boolean=} partialOkay If true, allow reading partial payloads - * from some boxes. If the goal is a child box, we can sometimes find it - * without enough data to find all child boxes. - * @export - */ -shaka.util.Mp4Parser.prototype.parseNext = - function(absStart, reader, partialOkay) { - const start = reader.getPosition(); - - let size = reader.readUint32(); - const type = reader.readUint32(); - const name = shaka.util.Mp4Parser.typeToString(type); - shaka.log.v2('Parsing MP4 box', name); - - switch (size) { - case 0: - size = reader.getLength() - start; - break; - case 1: - size = reader.readUint64(); - break; - } - - const boxDefinition = this.boxDefinitions_[type]; - - if (boxDefinition) { - let version = null; - let flags = null; - - if (this.headers_[type] == shaka.util.Mp4Parser.BoxType_.FULL_BOX) { - const versionAndFlags = reader.readUint32(); - version = versionAndFlags >>> 24; - flags = versionAndFlags & 0xFFFFFF; - } - - // Read the whole payload so that the current level can be safely read - // regardless of how the payload is parsed. - let end = start + size; - if (partialOkay && end > reader.getLength()) { - // For partial reads, truncate the payload if we must. - end = reader.getLength(); - } - const payloadSize = end - reader.getPosition(); - const payload = - (payloadSize > 0) ? reader.readBytes(payloadSize) : new Uint8Array(0); - - const payloadReader = new shaka.util.DataViewReader( - new DataView( - payload.buffer, payload.byteOffset, payload.byteLength), - shaka.util.DataViewReader.Endianness.BIG_ENDIAN); - - /** @type {shaka.extern.ParsedBox} */ - const box = { - parser: this, - partialOkay: partialOkay || false, - version: version, - flags: flags, - reader: payloadReader, - size: size, - start: start + absStart, - }; - - boxDefinition(box); - } else { - // Move the read head to be at the end of the box. - // If the box is longer than the remaining parts of the file, e.g. the - // mp4 is improperly formatted, or this was a partial range request that - // ended in the middle of a box, just skip to the end. - const skipLength = Math.min( - start + size - reader.getPosition(), - reader.getLength() - reader.getPosition()); - reader.skip(skipLength); - } - }; - - -/** - * A callback that tells the Mp4 parser to treat the body of a box as a series - * of boxes. The number of boxes is limited by the size of the parent box. - * - * @param {!shaka.extern.ParsedBox} box - * @export - */ -shaka.util.Mp4Parser.children = function(box) { - while (box.reader.hasMoreData() && !box.parser.done_) { - box.parser.parseNext(box.start, box.reader, box.partialOkay); - } -}; - - -/** - * A callback that tells the Mp4 parser to treat the body of a box as a sample - * description. A sample description box has a fixed number of children. The - * number of children is represented by a 4 byte unsigned integer. Each child - * is a box. - * - * @param {!shaka.extern.ParsedBox} box - * @export - */ -shaka.util.Mp4Parser.sampleDescription = function(box) { - for (let count = box.reader.readUint32(); - count > 0 && !box.parser.done_; - count -= 1) { - box.parser.parseNext(box.start, box.reader, box.partialOkay); - } -}; - - -/** - * Create a callback that tells the Mp4 parser to treat the body of a box as a - * binary blob and to parse the body's contents using the provided callback. - * - * @param {function(!Uint8Array)} callback - * @return {!shaka.util.Mp4Parser.CallbackType} - * @export - */ -shaka.util.Mp4Parser.allData = function(callback) { - return function(box) { - const all = box.reader.getLength() - box.reader.getPosition(); - callback(box.reader.readBytes(all)); - }; -}; - - -/** - * Convert an ascii string name to the integer type for a box. - * - * @param {string} name The name of the box. The name must be four - * characters long. - * @return {number} - * @private - */ -shaka.util.Mp4Parser.typeFromString_ = function(name) { - goog.asserts.assert( - name.length == 4, - 'Mp4 box names must be 4 characters long'); - - let code = 0; - for (let i = 0; i < name.length; i++) { - code = (code << 8) | name.charCodeAt(i); - } - return code; -}; - - -/** - * Convert an integer type from a box into an ascii string name. - * Useful for debugging. - * - * @param {number} type The type of the box, a uint32. - * @return {string} - * @export - */ -shaka.util.Mp4Parser.typeToString = function(type) { - const name = String.fromCharCode( - (type >> 24) & 0xff, - (type >> 16) & 0xff, - (type >> 8) & 0xff, - type & 0xff); - return name; -}; diff --git a/lib/util/multi_map.js b/lib/util/multi_map.js index 9a3f8d378..10cb4b667 100644 --- a/lib/util/multi_map.js +++ b/lib/util/multi_map.js @@ -19,89 +19,89 @@ goog.provide('shaka.util.MultiMap'); /** - * A simple multimap template. - * @constructor - * @struct + * @summary A simple multimap template. * @template T */ -shaka.util.MultiMap = function() { - /** @private {!Object.>} */ - this.map_ = {}; -}; - - -/** - * Add a key, value pair to the map. - * @param {string} key - * @param {T} value - */ -shaka.util.MultiMap.prototype.push = function(key, value) { - if (this.map_.hasOwnProperty(key)) { - this.map_[key].push(value); - } else { - this.map_[key] = [value]; +shaka.util.MultiMap = class { + constructor() { + /** @private {!Object.>} */ + this.map_ = {}; } -}; -/** - * Get a list of values by key. - * @param {string} key - * @return {Array.} or null if no such key exists. - */ -shaka.util.MultiMap.prototype.get = function(key) { - const list = this.map_[key]; - // slice() clones the list so that it and the map can each be modified - // without affecting the other. - return list ? list.slice() : null; -}; - - -/** - * Get a list of all values. - * @return {!Array.} - */ -shaka.util.MultiMap.prototype.getAll = function() { - const list = []; - for (const key in this.map_) { - list.push.apply(list, this.map_[key]); + /** + * Add a key, value pair to the map. + * @param {string} key + * @param {T} value + */ + push(key, value) { + if (this.map_.hasOwnProperty(key)) { + this.map_[key].push(value); + } else { + this.map_[key] = [value]; + } } - return list; -}; -/** - * Remove a specific value, if it exists. - * @param {string} key - * @param {T} value - */ -shaka.util.MultiMap.prototype.remove = function(key, value) { - const list = this.map_[key]; - if (!list) { - return; + /** + * Get a list of values by key. + * @param {string} key + * @return {Array.} or null if no such key exists. + */ + get(key) { + const list = this.map_[key]; + // slice() clones the list so that it and the map can each be modified + // without affecting the other. + return list ? list.slice() : null; } - for (let i = 0; i < list.length; ++i) { - if (list[i] == value) { - list.splice(i, 1); - --i; + + + /** + * Get a list of all values. + * @return {!Array.} + */ + getAll() { + const list = []; + for (const key in this.map_) { + list.push.apply(list, this.map_[key]); + } + return list; + } + + + /** + * Remove a specific value, if it exists. + * @param {string} key + * @param {T} value + */ + remove(key, value) { + const list = this.map_[key]; + if (!list) { + return; + } + for (let i = 0; i < list.length; ++i) { + if (list[i] == value) { + list.splice(i, 1); + --i; + } + } + } + + + /** + * Clear all keys and values from the multimap. + */ + clear() { + this.map_ = {}; + } + + + /** + * @param {function(string, !Array.)} callback + */ + forEach(callback) { + for (const key in this.map_) { + callback(key, this.map_[key]); } } }; - - -/** - * Clear all keys and values from the multimap. - */ -shaka.util.MultiMap.prototype.clear = function() { - this.map_ = {}; -}; - - -/** - * @param {function(string, !Array.)} callback - */ -shaka.util.MultiMap.prototype.forEach = function(callback) { - for (const key in this.map_) { - callback(key, this.map_[key]); - } -}; diff --git a/lib/util/pssh.js b/lib/util/pssh.js index 2da708564..23c57413c 100644 --- a/lib/util/pssh.js +++ b/lib/util/pssh.js @@ -24,83 +24,86 @@ goog.require('shaka.util.Uint8ArrayUtils'); /** + * @summary * Parse a PSSH box and extract the system IDs. - * - * @param {!Uint8Array} psshBox - * @constructor - * @struct - * @throws {shaka.util.Error} if a PSSH box is truncated or contains a size - * field over 53 bits. */ -shaka.util.Pssh = function(psshBox) { +shaka.util.Pssh = class { /** - * In hex. - * @type {!Array.} + * @param {!Uint8Array} psshBox + * @throws {shaka.util.Error} if a PSSH box is truncated or contains a size + * field over 53 bits. */ - this.systemIds = []; + constructor(psshBox) { + /** + * In hex. + * @type {!Array.} + */ + this.systemIds = []; - /** - * In hex. - * @type {!Array.} - */ - this.cencKeyIds = []; + /** + * In hex. + * @type {!Array.} + */ + this.cencKeyIds = []; - /* - * Array of tuples that define the startIndex + size for each - * discrete pssh within |psshBox| - * */ - this.dataBoundaries = []; + /* + * Array of tuples that define the startIndex + size for each + * discrete pssh within |psshBox| + * */ + this.dataBoundaries = []; - new shaka.util.Mp4Parser() - .fullBox('pssh', this.parseBox_.bind(this)).parse(psshBox.buffer); + new shaka.util.Mp4Parser() + .fullBox('pssh', this.parseBox_.bind(this)).parse(psshBox.buffer); - if (this.dataBoundaries.length == 0) { - shaka.log.warning('No pssh box found!'); - } -}; - - -/** - * @param {!shaka.extern.ParsedBox} box - * @private - */ -shaka.util.Pssh.prototype.parseBox_ = function(box) { - goog.asserts.assert( - box.version != null, - 'PSSH boxes are full boxes and must have a valid version'); - - goog.asserts.assert( - box.flags != null, - 'PSSH boxes are full boxes and must have a valid flag'); - - if (box.version > 1) { - shaka.log.warning('Unrecognized PSSH version found!'); - return; - } - - const systemId = shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16)); - const keyIds = []; - if (box.version > 0) { - const numKeyIds = box.reader.readUint32(); - for (let i = 0; i < numKeyIds; ++i) { - const keyId = shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16)); - keyIds.push(keyId); + if (this.dataBoundaries.length == 0) { + shaka.log.warning('No pssh box found!'); } } - const dataSize = box.reader.readUint32(); - box.reader.skip(dataSize); // Ignore the data section. - // Now that everything has been succesfully parsed from this box, - // update member variables. - this.cencKeyIds.push.apply(this.cencKeyIds, keyIds); - this.systemIds.push(systemId); - this.dataBoundaries.push({ - start: box.start, - end: box.start + box.size - 1, - }); + /** + * @param {!shaka.extern.ParsedBox} box + * @private + */ + parseBox_(box) { + goog.asserts.assert( + box.version != null, + 'PSSH boxes are full boxes and must have a valid version'); - if (box.reader.getPosition() != box.reader.getLength()) { - shaka.log.warning('Mismatch between box size and data size!'); + goog.asserts.assert( + box.flags != null, + 'PSSH boxes are full boxes and must have a valid flag'); + + if (box.version > 1) { + shaka.log.warning('Unrecognized PSSH version found!'); + return; + } + + const systemId = shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16)); + const keyIds = []; + if (box.version > 0) { + const numKeyIds = box.reader.readUint32(); + for (let i = 0; i < numKeyIds; ++i) { + const keyId = + shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16)); + keyIds.push(keyId); + } + } + + const dataSize = box.reader.readUint32(); + box.reader.skip(dataSize); // Ignore the data section. + + // Now that everything has been succesfully parsed from this box, + // update member variables. + this.cencKeyIds.push.apply(this.cencKeyIds, keyIds); + this.systemIds.push(systemId); + this.dataBoundaries.push({ + start: box.start, + end: box.start + box.size - 1, + }); + + if (box.reader.getPosition() != box.reader.getLength()) { + shaka.log.warning('Mismatch between box size and data size!'); + } } }; diff --git a/lib/util/public_promise.js b/lib/util/public_promise.js index fb8b380d0..8c1d66d57 100644 --- a/lib/util/public_promise.js +++ b/lib/util/public_promise.js @@ -19,46 +19,49 @@ goog.provide('shaka.util.PublicPromise'); /** + * @summary * A utility to create Promises with convenient public resolve and reject * methods. * - * @constructor - * @struct * @extends {Promise.} - * @return {Promise.} * @template T */ -shaka.util.PublicPromise = function() { - let resolvePromise; - let rejectPromise; +shaka.util.PublicPromise = class { + /** + * @return {Promise.} + */ + constructor() { + let resolvePromise; + let rejectPromise; - // Promise.call causes an error. It seems that inheriting from a native - // Promise is not permitted by JavaScript interpreters. + // Promise.call causes an error. It seems that inheriting from a native + // Promise is not permitted by JavaScript interpreters. - // The work-around is to construct a Promise object, modify it to look like - // the compiler's picture of PublicPromise, then return it. The caller of - // new PublicPromise will receive |promise| instead of |this|, and the - // compiler will be aware of the additional properties |resolve| and - // |reject|. + // The work-around is to construct a Promise object, modify it to look like + // the compiler's picture of PublicPromise, then return it. The caller of + // new PublicPromise will receive |promise| instead of |this|, and the + // compiler will be aware of the additional properties |resolve| and + // |reject|. - const promise = new Promise(((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - })); + const promise = new Promise(((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + })); - // Now cast the Promise object to our subclass PublicPromise so that the - // compiler will permit us to attach resolve() and reject() to it. - const publicPromise = /** @type {shaka.util.PublicPromise} */(promise); - publicPromise.resolve = resolvePromise; - publicPromise.reject = rejectPromise; + // Now cast the Promise object to our subclass PublicPromise so that the + // compiler will permit us to attach resolve() and reject() to it. + const publicPromise = /** @type {shaka.util.PublicPromise} */(promise); + publicPromise.resolve = resolvePromise; + publicPromise.reject = rejectPromise; - return publicPromise; + return publicPromise; + } + + + /** @param {T=} value */ + resolve(value) {} + + + /** @param {*=} reason */ + reject(reason) {} }; - - -/** @param {T=} value */ -shaka.util.PublicPromise.prototype.resolve = function(value) {}; - - -/** @param {*=} reason */ -shaka.util.PublicPromise.prototype.reject = function(reason) {}; diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 06c9859b7..705ac4c46 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -29,715 +29,711 @@ goog.require('shaka.util.MimeUtils'); /** - * @namespace shaka.util.StreamUtils * @summary A set of utility functions for dealing with Streams and Manifests. */ - - -/** - * @param {shaka.extern.Variant} variant - * @param {shaka.extern.Restrictions} restrictions - * Configured restrictions from the user. - * @param {{width: number, height: number}} maxHwRes - * The maximum resolution the hardware can handle. - * This is applied separately from user restrictions because the setting - * should not be easily replaced by the user's configuration. - * @return {boolean} - */ -shaka.util.StreamUtils.meetsRestrictions = function( - variant, restrictions, maxHwRes) { - /** @type {function(number, number, number):boolean} */ - const inRange = (x, min, max) => { - return x >= min && x <= max; - }; - - const video = variant.video; - - // |video.width| and |video.height| can be undefined, which breaks - // the math, so make sure they are there first. - if (video && video.width && video.height) { - if (!inRange(video.width, - restrictions.minWidth, - Math.min(restrictions.maxWidth, maxHwRes.width))) { - return false; - } - - if (!inRange(video.height, - restrictions.minHeight, - Math.min(restrictions.maxHeight, maxHwRes.height))) { - return false; - } - - if (!inRange(video.width * video.height, - restrictions.minPixels, - restrictions.maxPixels)) { - return false; - } - } - - if (!inRange(variant.bandwidth, - restrictions.minBandwidth, - restrictions.maxBandwidth)) { - return false; - } - - return true; -}; - - -/** - * @param {!Array.} variants - * @param {shaka.extern.Restrictions} restrictions - * @param {{width: number, height: number}} maxHwRes - * @return {boolean} Whether the tracks changed. - */ -shaka.util.StreamUtils.applyRestrictions = - function(variants, restrictions, maxHwRes) { - let tracksChanged = false; - - variants.forEach((variant) => { - const originalAllowed = variant.allowedByApplication; - variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions( - variant, restrictions, maxHwRes); - - if (originalAllowed != variant.allowedByApplication) { - tracksChanged = true; - } - }); - - return tracksChanged; +shaka.util.StreamUtils = class { + /** + * @param {shaka.extern.Variant} variant + * @param {shaka.extern.Restrictions} restrictions + * Configured restrictions from the user. + * @param {{width: number, height: number}} maxHwRes + * The maximum resolution the hardware can handle. + * This is applied separately from user restrictions because the setting + * should not be easily replaced by the user's configuration. + * @return {boolean} + */ + static meetsRestrictions(variant, restrictions, maxHwRes) { + /** @type {function(number, number, number):boolean} */ + const inRange = (x, min, max) => { + return x >= min && x <= max; }; - -/** - * Alters the given Period to filter out any unplayable streams. - * - * @param {shaka.media.DrmEngine} drmEngine - * @param {?shaka.extern.Stream} activeAudio - * @param {?shaka.extern.Stream} activeVideo - * @param {shaka.extern.Period} period - */ -shaka.util.StreamUtils.filterNewPeriod = function( - drmEngine, activeAudio, activeVideo, period) { - const StreamUtils = shaka.util.StreamUtils; - - if (activeAudio) { - goog.asserts.assert(StreamUtils.isAudio(activeAudio), - 'Audio streams must have the audio type.'); - } - - if (activeVideo) { - goog.asserts.assert(StreamUtils.isVideo(activeVideo), - 'Video streams must have the video type.'); - } - - // Filter variants. - period.variants = period.variants.filter((variant) => { - if (drmEngine && drmEngine.initialized()) { - if (!drmEngine.supportsVariant(variant)) { - shaka.log.debug('Dropping variant - not compatible with key system', - variant); - return false; - } - } - - const audio = variant.audio; const video = variant.video; - if (audio && !shaka.media.MediaSourceEngine.isStreamSupported(audio)) { - shaka.log.debug('Dropping variant - audio not compatible with platform', - StreamUtils.getStreamSummaryString_(audio)); - return false; - } + // |video.width| and |video.height| can be undefined, which breaks + // the math, so make sure they are there first. + if (video && video.width && video.height) { + if (!inRange(video.width, + restrictions.minWidth, + Math.min(restrictions.maxWidth, maxHwRes.width))) { + return false; + } - if (video && !shaka.media.MediaSourceEngine.isStreamSupported(video)) { - shaka.log.debug('Dropping variant - video not compatible with platform', - StreamUtils.getStreamSummaryString_(video)); - return false; - } + if (!inRange(video.height, + restrictions.minHeight, + Math.min(restrictions.maxHeight, maxHwRes.height))) { + return false; + } - if (audio && activeAudio) { - if (!StreamUtils.areStreamsCompatible_(audio, activeAudio)) { - shaka.log.debug('Droping variant - not compatible with active audio', - 'active audio', - StreamUtils.getStreamSummaryString_(activeAudio), - 'variant.audio', - StreamUtils.getStreamSummaryString_(audio)); + if (!inRange(video.width * video.height, + restrictions.minPixels, + restrictions.maxPixels)) { return false; } } - if (video && activeVideo) { - if (!StreamUtils.areStreamsCompatible_(video, activeVideo)) { - shaka.log.debug('Droping variant - not compatible with active video', - 'active video', - StreamUtils.getStreamSummaryString_(activeVideo), - 'variant.video', - StreamUtils.getStreamSummaryString_(video)); - return false; - } + if (!inRange(variant.bandwidth, + restrictions.minBandwidth, + restrictions.maxBandwidth)) { + return false; } return true; - }); + } - // Filter text streams. - period.textStreams = period.textStreams.filter((stream) => { - const fullMimeType = shaka.util.MimeUtils.getFullType( - stream.mimeType, stream.codecs); - const keep = shaka.text.TextEngine.isTypeSupported(fullMimeType); - if (!keep) { - shaka.log.debug('Dropping text stream. Is not supported by the ' + - 'platform.', stream); + /** + * @param {!Array.} variants + * @param {shaka.extern.Restrictions} restrictions + * @param {{width: number, height: number}} maxHwRes + * @return {boolean} Whether the tracks changed. + */ + static applyRestrictions(variants, restrictions, maxHwRes) { + let tracksChanged = false; + + variants.forEach((variant) => { + const originalAllowed = variant.allowedByApplication; + variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions( + variant, restrictions, maxHwRes); + + if (originalAllowed != variant.allowedByApplication) { + tracksChanged = true; + } + }); + + return tracksChanged; + } + + + /** + * Alters the given Period to filter out any unplayable streams. + * + * @param {shaka.media.DrmEngine} drmEngine + * @param {?shaka.extern.Stream} activeAudio + * @param {?shaka.extern.Stream} activeVideo + * @param {shaka.extern.Period} period + */ + static filterNewPeriod(drmEngine, activeAudio, activeVideo, period) { + const StreamUtils = shaka.util.StreamUtils; + + if (activeAudio) { + goog.asserts.assert(StreamUtils.isAudio(activeAudio), + 'Audio streams must have the audio type.'); } - return keep; - }); -}; + if (activeVideo) { + goog.asserts.assert(StreamUtils.isVideo(activeVideo), + 'Video streams must have the video type.'); + } + // Filter variants. + period.variants = period.variants.filter((variant) => { + if (drmEngine && drmEngine.initialized()) { + if (!drmEngine.supportsVariant(variant)) { + shaka.log.debug('Dropping variant - not compatible with key system', + variant); + return false; + } + } -/** - * @param {shaka.extern.Stream} s0 - * @param {shaka.extern.Stream} s1 - * @return {boolean} - * @private - */ -shaka.util.StreamUtils.areStreamsCompatible_ = function(s0, s1) { - // Basic mime types and basic codecs need to match. - // For example, we can't adapt between WebM and MP4, - // nor can we adapt between mp4a.* to ec-3. - // We can switch between text types on the fly, - // so don't run this check on text. - if (s0.mimeType != s1.mimeType) { - return false; + const audio = variant.audio; + const video = variant.video; + + if (audio && !shaka.media.MediaSourceEngine.isStreamSupported(audio)) { + shaka.log.debug('Dropping variant - audio not compatible with platform', + StreamUtils.getStreamSummaryString_(audio)); + return false; + } + + if (video && !shaka.media.MediaSourceEngine.isStreamSupported(video)) { + shaka.log.debug('Dropping variant - video not compatible with platform', + StreamUtils.getStreamSummaryString_(video)); + return false; + } + + if (audio && activeAudio) { + if (!StreamUtils.areStreamsCompatible_(audio, activeAudio)) { + shaka.log.debug('Droping variant - not compatible with active audio', + 'active audio', + StreamUtils.getStreamSummaryString_(activeAudio), + 'variant.audio', + StreamUtils.getStreamSummaryString_(audio)); + return false; + } + } + + if (video && activeVideo) { + if (!StreamUtils.areStreamsCompatible_(video, activeVideo)) { + shaka.log.debug('Droping variant - not compatible with active video', + 'active video', + StreamUtils.getStreamSummaryString_(activeVideo), + 'variant.video', + StreamUtils.getStreamSummaryString_(video)); + return false; + } + } + + return true; + }); + + // Filter text streams. + period.textStreams = period.textStreams.filter((stream) => { + const fullMimeType = shaka.util.MimeUtils.getFullType( + stream.mimeType, stream.codecs); + const keep = shaka.text.TextEngine.isTypeSupported(fullMimeType); + + if (!keep) { + shaka.log.debug('Dropping text stream. Is not supported by the ' + + 'platform.', stream); + } + + return keep; + }); } - if (s0.codecs.split('.')[0] != s1.codecs.split('.')[0]) { - return false; + + /** + * @param {shaka.extern.Stream} s0 + * @param {shaka.extern.Stream} s1 + * @return {boolean} + * @private + */ + static areStreamsCompatible_(s0, s1) { + // Basic mime types and basic codecs need to match. + // For example, we can't adapt between WebM and MP4, + // nor can we adapt between mp4a.* to ec-3. + // We can switch between text types on the fly, + // so don't run this check on text. + if (s0.mimeType != s1.mimeType) { + return false; + } + + if (s0.codecs.split('.')[0] != s1.codecs.split('.')[0]) { + return false; + } + + return true; } - return true; -}; + /** + * @param {shaka.extern.Variant} variant + * @return {shaka.extern.Track} + */ + static variantToTrack(variant) { + /** @type {?shaka.extern.Stream} */ + const audio = variant.audio; + /** @type {?shaka.extern.Stream} */ + const video = variant.video; -/** - * @param {shaka.extern.Variant} variant - * @return {shaka.extern.Track} - */ -shaka.util.StreamUtils.variantToTrack = function(variant) { - /** @type {?shaka.extern.Stream} */ - const audio = variant.audio; - /** @type {?shaka.extern.Stream} */ - const video = variant.video; + /** @type {?string} */ + const audioCodec = audio ? audio.codecs : null; + /** @type {?string} */ + const videoCodec = video ? video.codecs : null; - /** @type {?string} */ - const audioCodec = audio ? audio.codecs : null; - /** @type {?string} */ - const videoCodec = video ? video.codecs : null; + /** @type {!Array.} */ + const codecs = []; + if (videoCodec) { + codecs.push(videoCodec); + } + if (audioCodec) { + codecs.push(audioCodec); + } - /** @type {!Array.} */ - const codecs = []; - if (videoCodec) { - codecs.push(videoCodec); - } - if (audioCodec) { - codecs.push(audioCodec); + /** @type {!Array.} */ + const mimeTypes = []; + if (video) { + mimeTypes.push(video.mimeType); + } + if (audio) { + mimeTypes.push(audio.mimeType); + } + /** @type {?string} */ + const mimeType = mimeTypes[0] || null; + + /** @type {!Array.} */ + const kinds = []; + if (audio) { + kinds.push(audio.kind); + } + if (video) { + kinds.push(video.kind); + } + /** @type {?string} */ + const kind = kinds[0] || null; + + /** @type {!Set.} */ + const roles = new Set(); + if (audio) { + audio.roles.forEach((role) => roles.add(role)); + } + if (video) { + video.roles.forEach((role) => roles.add(role)); + } + + /** @type {shaka.extern.Track} */ + const track = { + id: variant.id, + active: false, + type: 'variant', + bandwidth: variant.bandwidth, + language: variant.language, + label: null, + kind: kind, + width: null, + height: null, + frameRate: null, + mimeType: mimeType, + codecs: codecs.join(', '), + audioCodec: audioCodec, + videoCodec: videoCodec, + primary: variant.primary, + roles: Array.from(roles), + audioRoles: null, + videoId: null, + audioId: null, + channelsCount: null, + audioBandwidth: null, + videoBandwidth: null, + originalVideoId: null, + originalAudioId: null, + originalTextId: null, + }; + + if (video) { + track.videoId = video.id; + track.originalVideoId = video.originalId; + track.width = video.width || null; + track.height = video.height || null; + track.frameRate = video.frameRate || null; + track.videoBandwidth = video.bandwidth || null; + } + + if (audio) { + track.audioId = audio.id; + track.originalAudioId = audio.originalId; + track.channelsCount = audio.channelsCount; + track.audioBandwidth = audio.bandwidth || null; + track.label = audio.label; + track.audioRoles = audio.roles; + } + + return track; } - /** @type {!Array.} */ - const mimeTypes = []; - if (video) { - mimeTypes.push(video.mimeType); - } - if (audio) { - mimeTypes.push(audio.mimeType); - } - /** @type {?string} */ - const mimeType = mimeTypes[0] || null; - /** @type {!Array.} */ - const kinds = []; - if (audio) { - kinds.push(audio.kind); - } - if (video) { - kinds.push(video.kind); - } - /** @type {?string} */ - const kind = kinds[0] || null; + /** + * @param {shaka.extern.Stream} stream + * @return {shaka.extern.Track} + */ + static textStreamToTrack(stream) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; - /** @type {!Set.} */ - const roles = new Set(); - if (audio) { - audio.roles.forEach((role) => roles.add(role)); - } - if (video) { - video.roles.forEach((role) => roles.add(role)); + /** @type {shaka.extern.Track} */ + const track = { + id: stream.id, + active: false, + type: ContentType.TEXT, + bandwidth: 0, + language: stream.language, + label: stream.label, + kind: stream.kind || null, + width: null, + height: null, + frameRate: null, + mimeType: stream.mimeType, + codecs: stream.codecs || null, + audioCodec: null, + videoCodec: null, + primary: stream.primary, + roles: stream.roles, + audioRoles: null, + videoId: null, + audioId: null, + channelsCount: null, + audioBandwidth: null, + videoBandwidth: null, + originalVideoId: null, + originalAudioId: null, + originalTextId: stream.originalId, + }; + + return track; } - /** @type {shaka.extern.Track} */ - const track = { - id: variant.id, - active: false, - type: 'variant', - bandwidth: variant.bandwidth, - language: variant.language, - label: null, - kind: kind, - width: null, - height: null, - frameRate: null, - mimeType: mimeType, - codecs: codecs.join(', '), - audioCodec: audioCodec, - videoCodec: videoCodec, - primary: variant.primary, - roles: Array.from(roles), - audioRoles: null, - videoId: null, - audioId: null, - channelsCount: null, - audioBandwidth: null, - videoBandwidth: null, - originalVideoId: null, - originalAudioId: null, - originalTextId: null, - }; - if (video) { - track.videoId = video.id; - track.originalVideoId = video.originalId; - track.width = video.width || null; - track.height = video.height || null; - track.frameRate = video.frameRate || null; - track.videoBandwidth = video.bandwidth || null; + /** + * Generate and return an ID for this track, since the ID field is optional. + * + * @param {TextTrack|AudioTrack} html5Track + * @return {number} The generated ID. + */ + static html5TrackId(html5Track) { + if (!html5Track['__shaka_id']) { + html5Track['__shaka_id'] = shaka.util.StreamUtils.nextTrackId_++; + } + return html5Track['__shaka_id']; } - if (audio) { - track.audioId = audio.id; - track.originalAudioId = audio.originalId; - track.channelsCount = audio.channelsCount; - track.audioBandwidth = audio.bandwidth || null; - track.label = audio.label; - track.audioRoles = audio.roles; + + /** + * @param {TextTrack} textTrack + * @return {shaka.extern.Track} + */ + static html5TextTrackToTrack(textTrack) { + const CLOSED_CAPTION_MIMETYPE = + shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; + const StreamUtils = shaka.util.StreamUtils; + + /** @type {shaka.extern.Track} */ + const track = StreamUtils.html5TrackToGenericShakaTrack_(textTrack); + track.active = textTrack.mode != 'disabled'; + track.type = 'text'; + track.originalTextId = textTrack.id; + if (textTrack.kind == 'captions') { + track.mimeType = CLOSED_CAPTION_MIMETYPE; + } + + return track; } - return track; -}; + /** + * @param {AudioTrack} audioTrack + * @return {shaka.extern.Track} + */ + static html5AudioTrackToTrack(audioTrack) { + const StreamUtils = shaka.util.StreamUtils; -/** - * @param {shaka.extern.Stream} stream - * @return {shaka.extern.Track} - */ -shaka.util.StreamUtils.textStreamToTrack = function(stream) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; + /** @type {shaka.extern.Track} */ + const track = StreamUtils.html5TrackToGenericShakaTrack_(audioTrack); + track.active = audioTrack.enabled; + track.type = 'variant'; + track.originalAudioId = audioTrack.id; - /** @type {shaka.extern.Track} */ - const track = { - id: stream.id, - active: false, - type: ContentType.TEXT, - bandwidth: 0, - language: stream.language, - label: stream.label, - kind: stream.kind || null, - width: null, - height: null, - frameRate: null, - mimeType: stream.mimeType, - codecs: stream.codecs || null, - audioCodec: null, - videoCodec: null, - primary: stream.primary, - roles: stream.roles, - audioRoles: null, - videoId: null, - audioId: null, - channelsCount: null, - audioBandwidth: null, - videoBandwidth: null, - originalVideoId: null, - originalAudioId: null, - originalTextId: stream.originalId, - }; + if (audioTrack.kind == 'main') { + track.primary = true; + track.roles = ['main']; + track.audioRoles = ['main']; + } else { + track.audioRoles = []; + } - return track; -}; - - -/** - * Generate and return an ID for this track, since the ID field is optional. - * - * @param {TextTrack|AudioTrack} html5Track - * @return {number} The generated ID. - */ -shaka.util.StreamUtils.html5TrackId = function(html5Track) { - if (!html5Track['__shaka_id']) { - html5Track['__shaka_id'] = shaka.util.StreamUtils.nextTrackId_++; + return track; + } + + + /** + * Creates a Track object with non-type specific fields filled out. The + * caller is responsible for completing the Track object with any + * type-specific information (audio or text). + * + * @param {TextTrack|AudioTrack} html5Track + * @return {shaka.extern.Track} + * @private + */ + static html5TrackToGenericShakaTrack_(html5Track) { + /** @type {shaka.extern.Track} */ + const track = { + id: shaka.util.StreamUtils.html5TrackId(html5Track), + active: false, + type: '', + bandwidth: 0, + language: shaka.util.LanguageUtils.normalize(html5Track.language), + label: html5Track.label, + kind: html5Track.kind, + width: null, + height: null, + frameRate: null, + mimeType: null, + codecs: null, + audioCodec: null, + videoCodec: null, + primary: false, + roles: [], + audioRoles: null, + videoId: null, + audioId: null, + channelsCount: null, + audioBandwidth: null, + videoBandwidth: null, + originalVideoId: null, + originalAudioId: null, + originalTextId: null, + }; + + return track; + } + + + /** + * Determines if the given variant is playable. + * @param {!shaka.extern.Variant} variant + * @return {boolean} + */ + static isPlayable(variant) { + return variant.allowedByApplication && variant.allowedByKeySystem; + } + + + /** + * Filters out unplayable variants. + * @param {!Array.} variants + * @return {!Array.} + */ + static getPlayableVariants(variants) { + return variants.filter((variant) => { + return shaka.util.StreamUtils.isPlayable(variant); + }); + } + + + /** + * Filters variants according to the given audio channel count config. + * + * @param {!Array.} variants + * @param {number} preferredAudioChannelCount + * @return {!Array.} + */ + static filterVariantsByAudioChannelCount( + variants, preferredAudioChannelCount) { + // Group variants by their audio channel counts. + const variantsWithChannelCounts = + variants.filter((v) => v.audio && v.audio.channelsCount); + + /** @type {!Map.>} */ + const variantsByChannelCount = new Map(); + for (const variant of variantsWithChannelCounts) { + const count = variant.audio.channelsCount; + goog.asserts.assert(count != null, 'Must have count after filtering!'); + if (!variantsByChannelCount.has(count)) { + variantsByChannelCount.set(count, []); + } + variantsByChannelCount.get(count).push(variant); + } + + /** @type {!Array.} */ + const channelCounts = Array.from(variantsByChannelCount.keys()); + + // If no variant has audio channel count info, return the original variants. + if (channelCounts.length == 0) { + return variants; + } + + // Choose the variants with the largest number of audio channels less than + // or equal to the configured number of audio channels. + const countLessThanOrEqualtoConfig = + channelCounts.filter((count) => count <= preferredAudioChannelCount); + if (countLessThanOrEqualtoConfig.length) { + return variantsByChannelCount.get(Math.max.apply(null, + countLessThanOrEqualtoConfig)); + } + + // If all variants have more audio channels than the config, choose the + // variants with the fewest audio channels. + return variantsByChannelCount.get(Math.min.apply(null, channelCounts)); + } + + /** + * Chooses streams according to the given config. + * + * @param {!Array.} streams + * @param {string} preferredLanguage + * @param {string} preferredRole + * @return {!Array.} + */ + static filterStreamsByLanguageAndRole( + streams, preferredLanguage, preferredRole) { + const LanguageUtils = shaka.util.LanguageUtils; + + /** @type {!Array.} */ + let chosen = streams; + + // Start with the set of primary streams. + /** @type {!Array.} */ + const primary = streams.filter((stream) => { + return stream.primary; + }); + + if (primary.length) { + chosen = primary; + } + + // Now reduce the set to one language. This covers both arbitrary language + // choice and the reduction of the "primary" stream set to one language. + const firstLanguage = chosen.length ? chosen[0].language : ''; + chosen = chosen.filter((stream) => { + return stream.language == firstLanguage; + }); + + // Find the streams that best match our language preference. This will + // override previous selections. + if (preferredLanguage) { + const closestLocale = LanguageUtils.findClosestLocale( + LanguageUtils.normalize(preferredLanguage), + streams.map((stream) => stream.language)); + + // Only replace |chosen| if we found a locale that is close to our + // preference. + if (closestLocale) { + chosen = streams.filter((stream) => { + const locale = LanguageUtils.normalize(stream.language); + return locale == closestLocale; + }); + } + } + + // Now refine the choice based on role preference. + if (preferredRole) { + const roleMatches = shaka.util.StreamUtils.filterTextStreamsByRole_( + chosen, preferredRole); + if (roleMatches.length) { + return roleMatches; + } else { + shaka.log.warning('No exact match for the text role could be found.'); + } + } else { + // Prefer text streams with no roles, if they exist. + const noRoleMatches = chosen.filter((stream) => { + return stream.roles.length == 0; + }); + if (noRoleMatches.length) { + return noRoleMatches; + } + } + + // Either there was no role preference, or it could not be satisfied. + // Choose an arbitrary role, if there are any, and filter out any other + // roles. This ensures we never adapt between roles. + + const allRoles = chosen.map((stream) => { + return stream.roles; + }).reduce(shaka.util.Functional.collapseArrays, []); + + if (!allRoles.length) { + return chosen; + } + return shaka.util.StreamUtils.filterTextStreamsByRole_(chosen, allRoles[0]); + } + + + /** + * Filter text Streams by role. + * + * @param {!Array.} textStreams + * @param {string} preferredRole + * @return {!Array.} + * @private + */ + static filterTextStreamsByRole_(textStreams, preferredRole) { + return textStreams.filter((stream) => { + return stream.roles.includes(preferredRole); + }); + } + + + /** + * Finds a Variant with given audio and video streams. + * Returns null if no such Variant was found. + * + * @param {?shaka.extern.Stream} audio + * @param {?shaka.extern.Stream} video + * @param {!Array.} variants + * @return {?shaka.extern.Variant} + */ + static getVariantByStreams(audio, video, variants) { + if (audio) { + goog.asserts.assert( + shaka.util.StreamUtils.isAudio(audio), + 'Audio streams must have the audio type.'); + } + + if (video) { + goog.asserts.assert( + shaka.util.StreamUtils.isVideo(video), + 'Video streams must have the video type.'); + } + + for (let i = 0; i < variants.length; i++) { + if (variants[i].audio == audio && variants[i].video == video) { + return variants[i]; + } + } + + return null; + } + + + /** + * Checks if the given stream is an audio stream. + * + * @param {shaka.extern.Stream} stream + * @return {boolean} + */ + static isAudio(stream) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + return stream.type == ContentType.AUDIO; + } + + + /** + * Checks if the given stream is a video stream. + * + * @param {shaka.extern.Stream} stream + * @return {boolean} + */ + static isVideo(stream) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + return stream.type == ContentType.VIDEO; + } + + + /** + * Get all non-null streams in the variant as an array. + * + * @param {shaka.extern.Variant} variant + * @return {!Array.} + */ + static getVariantStreams(variant) { + const streams = []; + + if (variant.audio) { + streams.push(variant.audio); + } + if (variant.video) { + streams.push(variant.video); + } + + return streams; + } + + + /** + * @param {shaka.extern.Stream} stream + * @return {string} + * @private + */ + static getStreamSummaryString_(stream) { + if (shaka.util.StreamUtils.isAudio(stream)) { + return 'type=audio' + + ' codecs=' + stream.codecs + + ' bandwidth='+ stream.bandwidth + + ' channelsCount=' + stream.channelsCount; + } + + if (shaka.util.StreamUtils.isVideo(stream)) { + return 'type=video' + + ' codecs=' + stream.codecs + + ' bandwidth=' + stream.bandwidth + + ' frameRate=' + stream.frameRate + + ' width=' + stream.width + + ' height=' + stream.height; + } + + return 'unexpected stream type'; } - return html5Track['__shaka_id']; }; /** @private {number} */ shaka.util.StreamUtils.nextTrackId_ = 0; - - -/** - * @param {TextTrack} textTrack - * @return {shaka.extern.Track} - */ -shaka.util.StreamUtils.html5TextTrackToTrack = function(textTrack) { - const CLOSED_CAPTION_MIMETYPE = shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; - const StreamUtils = shaka.util.StreamUtils; - - /** @type {shaka.extern.Track} */ - const track = StreamUtils.html5TrackToGenericShakaTrack_(textTrack); - track.active = textTrack.mode != 'disabled'; - track.type = 'text'; - track.originalTextId = textTrack.id; - if (textTrack.kind == 'captions') { - track.mimeType = CLOSED_CAPTION_MIMETYPE; - } - - return track; -}; - - -/** - * @param {AudioTrack} audioTrack - * @return {shaka.extern.Track} - */ -shaka.util.StreamUtils.html5AudioTrackToTrack = function(audioTrack) { - const StreamUtils = shaka.util.StreamUtils; - - /** @type {shaka.extern.Track} */ - const track = StreamUtils.html5TrackToGenericShakaTrack_(audioTrack); - track.active = audioTrack.enabled; - track.type = 'variant'; - track.originalAudioId = audioTrack.id; - - if (audioTrack.kind == 'main') { - track.primary = true; - track.roles = ['main']; - track.audioRoles = ['main']; - } else { - track.audioRoles = []; - } - - return track; -}; - - -/** - * Creates a Track object with non-type specific fields filled out. The caller - * is responsible for completing the Track object with any type-specific - * information (audio or text). - * - * @param {TextTrack|AudioTrack} html5Track - * @return {shaka.extern.Track} - * @private - */ -shaka.util.StreamUtils.html5TrackToGenericShakaTrack_ = function(html5Track) { - /** @type {shaka.extern.Track} */ - const track = { - id: shaka.util.StreamUtils.html5TrackId(html5Track), - active: false, - type: '', - bandwidth: 0, - language: shaka.util.LanguageUtils.normalize(html5Track.language), - label: html5Track.label, - kind: html5Track.kind, - width: null, - height: null, - frameRate: null, - mimeType: null, - codecs: null, - audioCodec: null, - videoCodec: null, - primary: false, - roles: [], - audioRoles: null, - videoId: null, - audioId: null, - channelsCount: null, - audioBandwidth: null, - videoBandwidth: null, - originalVideoId: null, - originalAudioId: null, - originalTextId: null, - }; - - return track; -}; - - -/** - * Determines if the given variant is playable. - * @param {!shaka.extern.Variant} variant - * @return {boolean} - */ -shaka.util.StreamUtils.isPlayable = function(variant) { - return variant.allowedByApplication && variant.allowedByKeySystem; -}; - - -/** - * Filters out unplayable variants. - * @param {!Array.} variants - * @return {!Array.} - */ -shaka.util.StreamUtils.getPlayableVariants = function(variants) { - return variants.filter((variant) => { - return shaka.util.StreamUtils.isPlayable(variant); - }); -}; - - -/** - * Filters variants according to the given audio channel count config. - * - * @param {!Array.} variants - * @param {number} preferredAudioChannelCount - * @return {!Array.} - */ -shaka.util.StreamUtils.filterVariantsByAudioChannelCount = function( - variants, preferredAudioChannelCount) { - // Group variants by their audio channel counts. - const variantsWithChannelCounts = - variants.filter((v) => v.audio && v.audio.channelsCount); - - /** @type {!Map.>} */ - const variantsByChannelCount = new Map(); - for (const variant of variantsWithChannelCounts) { - const count = variant.audio.channelsCount; - goog.asserts.assert(count != null, 'Must have count after filtering!'); - if (!variantsByChannelCount.has(count)) { - variantsByChannelCount.set(count, []); - } - variantsByChannelCount.get(count).push(variant); - } - - /** @type {!Array.} */ - const channelCounts = Array.from(variantsByChannelCount.keys()); - - // If no variant has audio channel count info, return the original variants. - if (channelCounts.length == 0) { - return variants; - } - - // Choose the variants with the largest number of audio channels less than or - // equal to the configured number of audio channels. - const countLessThanOrEqualtoConfig = - channelCounts.filter((count) => count <= preferredAudioChannelCount); - if (countLessThanOrEqualtoConfig.length) { - return variantsByChannelCount.get(Math.max.apply(null, - countLessThanOrEqualtoConfig)); - } - - // If all variants have more audio channels than the config, choose the - // variants with the fewest audio channels. - return variantsByChannelCount.get(Math.min.apply(null, channelCounts)); -}; - -/** - * Chooses streams according to the given config. - * - * @param {!Array.} streams - * @param {string} preferredLanguage - * @param {string} preferredRole - * @return {!Array.} - */ -shaka.util.StreamUtils.filterStreamsByLanguageAndRole = function( - streams, preferredLanguage, preferredRole) { - const LanguageUtils = shaka.util.LanguageUtils; - - /** @type {!Array.} */ - let chosen = streams; - - // Start with the set of primary streams. - /** @type {!Array.} */ - const primary = streams.filter((stream) => { - return stream.primary; - }); - - if (primary.length) { - chosen = primary; - } - - // Now reduce the set to one language. This covers both arbitrary language - // choice and the reduction of the "primary" stream set to one language. - const firstLanguage = chosen.length ? chosen[0].language : ''; - chosen = chosen.filter((stream) => { - return stream.language == firstLanguage; - }); - - // Find the streams that best match our language preference. This will - // override previous selections. - if (preferredLanguage) { - const closestLocale = LanguageUtils.findClosestLocale( - LanguageUtils.normalize(preferredLanguage), - streams.map((stream) => stream.language)); - - // Only replace |chosen| if we found a locale that is close to our - // preference. - if (closestLocale) { - chosen = streams.filter((stream) => { - const locale = LanguageUtils.normalize(stream.language); - return locale == closestLocale; - }); - } - } - - // Now refine the choice based on role preference. - if (preferredRole) { - const roleMatches = shaka.util.StreamUtils.filterTextStreamsByRole_( - chosen, preferredRole); - if (roleMatches.length) { - return roleMatches; - } else { - shaka.log.warning('No exact match for the text role could be found.'); - } - } else { - // Prefer text streams with no roles, if they exist. - const noRoleMatches = chosen.filter((stream) => { - return stream.roles.length == 0; - }); - if (noRoleMatches.length) { - return noRoleMatches; - } - } - - // Either there was no role preference, or it could not be satisfied. - // Choose an arbitrary role, if there are any, and filter out any other roles. - // This ensures we never adapt between roles. - - const allRoles = chosen.map((stream) => { - return stream.roles; - }).reduce(shaka.util.Functional.collapseArrays, []); - - if (!allRoles.length) { - return chosen; - } - return shaka.util.StreamUtils.filterTextStreamsByRole_(chosen, allRoles[0]); -}; - - -/** - * Filter text Streams by role. - * - * @param {!Array.} textStreams - * @param {string} preferredRole - * @return {!Array.} - * @private - */ -shaka.util.StreamUtils.filterTextStreamsByRole_ = - function(textStreams, preferredRole) { - return textStreams.filter((stream) => { - return stream.roles.includes(preferredRole); - }); - }; - - -/** - * Finds a Variant with given audio and video streams. - * Returns null if no such Variant was found. - * - * @param {?shaka.extern.Stream} audio - * @param {?shaka.extern.Stream} video - * @param {!Array.} variants - * @return {?shaka.extern.Variant} - */ -shaka.util.StreamUtils.getVariantByStreams = function(audio, video, variants) { - if (audio) { - goog.asserts.assert( - shaka.util.StreamUtils.isAudio(audio), - 'Audio streams must have the audio type.'); - } - - if (video) { - goog.asserts.assert( - shaka.util.StreamUtils.isVideo(video), - 'Video streams must have the video type.'); - } - - for (let i = 0; i < variants.length; i++) { - if (variants[i].audio == audio && variants[i].video == video) { - return variants[i]; - } - } - - return null; -}; - - -/** - * Checks if the given stream is an audio stream. - * - * @param {shaka.extern.Stream} stream - * @return {boolean} - */ -shaka.util.StreamUtils.isAudio = function(stream) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - return stream.type == ContentType.AUDIO; -}; - - -/** - * Checks if the given stream is a video stream. - * - * @param {shaka.extern.Stream} stream - * @return {boolean} - */ -shaka.util.StreamUtils.isVideo = function(stream) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - return stream.type == ContentType.VIDEO; -}; - - -/** - * Get all non-null streams in the variant as an array. - * - * @param {shaka.extern.Variant} variant - * @return {!Array.} - */ -shaka.util.StreamUtils.getVariantStreams = function(variant) { - const streams = []; - - if (variant.audio) { - streams.push(variant.audio); - } - if (variant.video) { - streams.push(variant.video); - } - - return streams; -}; - - -/** - * @param {shaka.extern.Stream} stream - * @return {string} - * @private - */ -shaka.util.StreamUtils.getStreamSummaryString_ = function(stream) { - if (shaka.util.StreamUtils.isAudio(stream)) { - return 'type=audio' + - ' codecs=' + stream.codecs + - ' bandwidth='+ stream.bandwidth + - ' channelsCount=' + stream.channelsCount; - } - - if (shaka.util.StreamUtils.isVideo(stream)) { - return 'type=video' + - ' codecs=' + stream.codecs + - ' bandwidth=' + stream.bandwidth + - ' frameRate=' + stream.frameRate + - ' width=' + stream.width + - ' height=' + stream.height; - } - - return 'unexpected stream type'; -}; diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index cf3837f76..2cb884a71 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -26,202 +26,204 @@ goog.require('shaka.util.Error'); * @summary A set of string utility functions. * @exportDoc */ +shaka.util.StringUtils = class { + /** + * Creates a string from the given buffer as UTF-8 encoding. + * + * @param {?BufferSource} data + * @return {string} + * @throws {shaka.util.Error} + * @export + */ + static fromUTF8(data) { + if (!data) { + return ''; + } + let uint8 = new Uint8Array(data); + // If present, strip off the UTF-8 BOM. + if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) { + uint8 = uint8.subarray(3); + } -/** - * Creates a string from the given buffer as UTF-8 encoding. - * - * @param {?BufferSource} data - * @return {string} - * @throws {shaka.util.Error} - * @export - */ -shaka.util.StringUtils.fromUTF8 = function(data) { - if (!data) { - return ''; + // http://stackoverflow.com/a/13691499 + const utf8 = shaka.util.StringUtils.fromCharCode(uint8); + // This converts each character in the string to an escape sequence. If the + // character is in the ASCII range, it is not converted; otherwise it is + // converted to a URI escape sequence. + // Example: '\x67\x35\xe3\x82\xac' -> 'g#%E3%82%AC' + const escaped = escape(utf8); + // Decode the escaped sequence. This will interpret UTF-8 sequences into + // the correct character. + // Example: 'g#%E3%82%AC' -> 'g#€' + try { + return decodeURIComponent(escaped); + } catch (e) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.BAD_ENCODING); + } } - let uint8 = new Uint8Array(data); - // If present, strip off the UTF-8 BOM. - if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) { - uint8 = uint8.subarray(3); + + /** + * Creates a string from the given buffer as UTF-16 encoding. + * + * @param {?BufferSource} data + * @param {boolean} littleEndian + true to read little endian, false to read big. + * @param {boolean=} noThrow true to avoid throwing in cases where we may + * expect invalid input. If noThrow is true and the data has an odd + * length,it will be truncated. + * @return {string} + * @throws {shaka.util.Error} + * @export + */ + static fromUTF16(data, littleEndian, noThrow) { + if (!data) { + return ''; + } + + if (!noThrow && data.byteLength % 2 != 0) { + shaka.log.error('Data has an incorrect length, must be even.'); + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.BAD_ENCODING); + } + + /** @type {ArrayBuffer} */ + let buffer; + if (data instanceof ArrayBuffer) { + buffer = data; + } else { + // Have to create a new buffer because the argument may be a smaller + // view on a larger ArrayBuffer. We cannot use an ArrayBufferView in + // a DataView. + const temp = new Uint8Array(data.byteLength); + temp.set(new Uint8Array(data)); + buffer = temp.buffer; + } + + // Use a DataView to ensure correct endianness. + const length = Math.floor(data.byteLength / 2); + const arr = new Uint16Array(length); + const dataView = new DataView(buffer); + for (let i = 0; i < length; i++) { + arr[i] = dataView.getUint16(i * 2, littleEndian); + } + return shaka.util.StringUtils.fromCharCode(arr); } - // http://stackoverflow.com/a/13691499 - const utf8 = shaka.util.StringUtils.fromCharCode(uint8); - // This converts each character in the string to an escape sequence. If the - // character is in the ASCII range, it is not converted; otherwise it is - // converted to a URI escape sequence. - // Example: '\x67\x35\xe3\x82\xac' -> 'g#%E3%82%AC' - const escaped = escape(utf8); - // Decode the escaped sequence. This will interpret UTF-8 sequences into the - // correct character. - // Example: 'g#%E3%82%AC' -> 'g#€' - try { - return decodeURIComponent(escaped); - } catch (e) { + + /** + * Creates a string from the given buffer, auto-detecting the encoding that is + * being used. If it cannot detect the encoding, it will throw an exception. + * + * @param {?BufferSource} data + * @return {string} + * @throws {shaka.util.Error} + * @export + */ + static fromBytesAutoDetect(data) { + const StringUtils = shaka.util.StringUtils; + + const uint8 = new Uint8Array(data); + if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) { + return StringUtils.fromUTF8(uint8); + } else if (uint8[0] == 0xfe && uint8[1] == 0xff) { + return StringUtils.fromUTF16(uint8.subarray(2), false /* littleEndian */); + } else if (uint8[0] == 0xff && uint8[1] == 0xfe) { + return StringUtils.fromUTF16(uint8.subarray(2), true /* littleEndian */); + } + + const isAscii = (function(arr, i) { + // arr[i] >= ' ' && arr[i] <= '~'; + return arr.byteLength <= i || (arr[i] >= 0x20 && arr[i] <= 0x7e); + }.bind(null, uint8)); + + shaka.log.debug( + 'Unable to find byte-order-mark, making an educated guess.'); + if (uint8[0] == 0 && uint8[2] == 0) { + return StringUtils.fromUTF16(data, false /* littleEndian */); + } else if (uint8[1] == 0 && uint8[3] == 0) { + return StringUtils.fromUTF16(data, true /* littleEndian */); + } else if (isAscii(0) && isAscii(1) && isAscii(2) && isAscii(3)) { + return StringUtils.fromUTF8(data); + } + throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, - shaka.util.Error.Code.BAD_ENCODING); + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.UNABLE_TO_DETECT_ENCODING); + } + + + /** + * Creates a ArrayBuffer from the given string, converting to UTF-8 encoding. + * + * @param {string} str + * @return {!ArrayBuffer} + * @export + */ + static toUTF8(str) { + // http://stackoverflow.com/a/13691499 + // Converts the given string to a URI encoded string. If a character falls + // in the ASCII range, it is not converted; otherwise it will be converted + // to a series of URI escape sequences according to UTF-8. + // Example: 'g#€' -> 'g#%E3%82%AC' + const encoded = encodeURIComponent(str); + // Convert each escape sequence individually into a character. Each escape + // sequence is interpreted as a code-point, so if an escape sequence happens + // to be part of a multi-byte sequence, each byte will be converted to a + // single character. + // Example: 'g#%E3%82%AC' -> '\x67\x35\xe3\x82\xac' + const utf8 = unescape(encoded); + + const result = new Uint8Array(utf8.length); + for (let i = 0; i < utf8.length; ++i) { + result[i] = utf8.charCodeAt(i); + } + return result.buffer; + } + + + /** + * Creates a ArrayBuffer from the given string, converting to UTF-16 encoding. + * + * @param {string} str + * @param {boolean} littleEndian + * @return {!ArrayBuffer} + * @export + */ + static toUTF16(str, littleEndian) { + const result = new Uint8Array(str.length * 2); + const view = new DataView(result.buffer); + for (let i = 0; i < str.length; ++i) { + const value = str.charCodeAt(i); + view.setUint16(/* position= */ i * 2, value, littleEndian); + } + return result.buffer; + } + + + /** + * Creates a new string from the given array of char codes. + * + * Using String.fromCharCode.apply is risky because you can trigger stack + * errors on very large arrays. This breaks up the array into several pieces + * to avoid this. + * + * @param {!TypedArray} array + * @return {string} + */ + static fromCharCode(array) { + const max = 16000; + let ret = ''; + for (let i = 0; i < array.length; i += max) { + const subArray = array.subarray(i, i + max); + ret += String.fromCharCode.apply(null, subArray); + } + + return ret; } }; - - -/** - * Creates a string from the given buffer as UTF-16 encoding. - * - * @param {?BufferSource} data - * @param {boolean} littleEndian true to read little endian, false to read big. - * @param {boolean=} noThrow true to avoid throwing in cases where we may - * expect invalid input. If noThrow is true and the data has an odd length, - * it will be truncated. - * @return {string} - * @throws {shaka.util.Error} - * @export - */ -shaka.util.StringUtils.fromUTF16 = function(data, littleEndian, noThrow) { - if (!data) { - return ''; - } - - if (!noThrow && data.byteLength % 2 != 0) { - shaka.log.error('Data has an incorrect length, must be even.'); - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, - shaka.util.Error.Code.BAD_ENCODING); - } - - /** @type {ArrayBuffer} */ - let buffer; - if (data instanceof ArrayBuffer) { - buffer = data; - } else { - // Have to create a new buffer because the argument may be a smaller - // view on a larger ArrayBuffer. We cannot use an ArrayBufferView in - // a DataView. - const temp = new Uint8Array(data.byteLength); - temp.set(new Uint8Array(data)); - buffer = temp.buffer; - } - - // Use a DataView to ensure correct endianness. - const length = Math.floor(data.byteLength / 2); - const arr = new Uint16Array(length); - const dataView = new DataView(buffer); - for (let i = 0; i < length; i++) { - arr[i] = dataView.getUint16(i * 2, littleEndian); - } - return shaka.util.StringUtils.fromCharCode(arr); -}; - - -/** - * Creates a string from the given buffer, auto-detecting the encoding that is - * being used. If it cannot detect the encoding, it will throw an exception. - * - * @param {?BufferSource} data - * @return {string} - * @throws {shaka.util.Error} - * @export - */ -shaka.util.StringUtils.fromBytesAutoDetect = function(data) { - const StringUtils = shaka.util.StringUtils; - - const uint8 = new Uint8Array(data); - if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) { - return StringUtils.fromUTF8(uint8); - } else if (uint8[0] == 0xfe && uint8[1] == 0xff) { - return StringUtils.fromUTF16(uint8.subarray(2), false /* littleEndian */); - } else if (uint8[0] == 0xff && uint8[1] == 0xfe) { - return StringUtils.fromUTF16(uint8.subarray(2), true /* littleEndian */); - } - - const isAscii = (function(arr, i) { - // arr[i] >= ' ' && arr[i] <= '~'; - return arr.byteLength <= i || (arr[i] >= 0x20 && arr[i] <= 0x7e); - }.bind(null, uint8)); - - shaka.log.debug('Unable to find byte-order-mark, making an educated guess.'); - if (uint8[0] == 0 && uint8[2] == 0) { - return StringUtils.fromUTF16(data, false /* littleEndian */); - } else if (uint8[1] == 0 && uint8[3] == 0) { - return StringUtils.fromUTF16(data, true /* littleEndian */); - } else if (isAscii(0) && isAscii(1) && isAscii(2) && isAscii(3)) { - return StringUtils.fromUTF8(data); - } - - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.TEXT, - shaka.util.Error.Code.UNABLE_TO_DETECT_ENCODING); -}; - - -/** - * Creates a ArrayBuffer from the given string, converting to UTF-8 encoding. - * - * @param {string} str - * @return {!ArrayBuffer} - * @export - */ -shaka.util.StringUtils.toUTF8 = function(str) { - // http://stackoverflow.com/a/13691499 - // Converts the given string to a URI encoded string. If a character falls - // in the ASCII range, it is not converted; otherwise it will be converted to - // a series of URI escape sequences according to UTF-8. - // Example: 'g#€' -> 'g#%E3%82%AC' - const encoded = encodeURIComponent(str); - // Convert each escape sequence individually into a character. Each escape - // sequence is interpreted as a code-point, so if an escape sequence happens - // to be part of a multi-byte sequence, each byte will be converted to a - // single character. - // Example: 'g#%E3%82%AC' -> '\x67\x35\xe3\x82\xac' - const utf8 = unescape(encoded); - - const result = new Uint8Array(utf8.length); - for (let i = 0; i < utf8.length; ++i) { - result[i] = utf8.charCodeAt(i); - } - return result.buffer; -}; - - -/** - * Creates a ArrayBuffer from the given string, converting to UTF-16 encoding. - * - * @param {string} str - * @param {boolean} littleEndian - * @return {!ArrayBuffer} - * @export - */ -shaka.util.StringUtils.toUTF16 = function(str, littleEndian) { - const result = new Uint8Array(str.length * 2); - const view = new DataView(result.buffer); - for (let i = 0; i < str.length; ++i) { - const value = str.charCodeAt(i); - view.setUint16(/* position= */ i * 2, value, littleEndian); - } - return result.buffer; -}; - - -/** - * Creates a new string from the given array of char codes. - * - * Using String.fromCharCode.apply is risky because you can trigger stack errors - * on very large arrays. This breaks up the array into several pieces to avoid - * this. - * - * @param {!TypedArray} array - * @return {string} - */ -shaka.util.StringUtils.fromCharCode = function(array) { - const max = 16000; - let ret = ''; - for (let i = 0; i < array.length; i += max) { - const subArray = array.subarray(i, i + max); - ret += String.fromCharCode.apply(null, subArray); - } - - return ret; -}; diff --git a/lib/util/text_parser.js b/lib/util/text_parser.js index e762e92d4..fad5091c4 100644 --- a/lib/util/text_parser.js +++ b/lib/util/text_parser.js @@ -22,123 +22,124 @@ goog.require('goog.asserts'); /** * Reads elements from strings. - * - * @param {string} data - * @constructor - * @struct */ -shaka.util.TextParser = function(data) { +shaka.util.TextParser = class { /** - * @const - * @private {string} + * @param {string} data */ - this.data_ = data; + constructor(data) { + /** + * @const + * @private {string} + */ + this.data_ = data; - /** @private {number} */ - this.position_ = 0; -}; - - -/** @return {boolean} Whether it is at the end of the string. */ -shaka.util.TextParser.prototype.atEnd = function() { - return this.position_ == this.data_.length; -}; - - -/** - * Reads a line from the parser. This will read but not return the newline. - * Returns null at the end. - * - * @return {?string} - */ -shaka.util.TextParser.prototype.readLine = function() { - return this.readRegexReturnCapture_(/(.*?)(\n|$)/gm, 1); -}; - - -/** - * Reads a word from the parser. This will not read or return any whitespace - * before or after the word (including newlines). Returns null at the end. - * - * @return {?string} - */ -shaka.util.TextParser.prototype.readWord = function() { - return this.readRegexReturnCapture_(/[^ \t\n]*/gm, 0); -}; - - -/** - * Skips any continuous whitespace from the parser. Returns null at the end. - */ -shaka.util.TextParser.prototype.skipWhitespace = function() { - this.readRegex(/[ \t]+/gm); -}; - - -/** - * Reads the given regular expression from the parser. This requires the match - * to be at the current position; there is no need to include a head anchor. - * This requires that the regex have the global flag to be set so that it can - * set lastIndex to start the search at the current position. Returns null at - * the end or if the regex does not match the current position. - * - * @param {!RegExp} regex - * @return {Array.} - */ -shaka.util.TextParser.prototype.readRegex = function(regex) { - const index = this.indexOf_(regex); - if (this.atEnd() || index == null || index.position != this.position_) { - return null; + /** @private {number} */ + this.position_ = 0; } - this.position_ += index.length; - return index.results; -}; - -/** - * Reads a regex from the parser and returns the given capture. - * - * @param {!RegExp} regex - * @param {number} index - * @return {?string} - * @private - */ -shaka.util.TextParser.prototype.readRegexReturnCapture_ = function( - regex, index) { - if (this.atEnd()) { - return null; + /** @return {boolean} Whether it is at the end of the string. */ + atEnd() { + return this.position_ == this.data_.length; } - const ret = this.readRegex(regex); - if (!ret) { - return null; - } else { - return ret[index]; + + /** + * Reads a line from the parser. This will read but not return the newline. + * Returns null at the end. + * + * @return {?string} + */ + readLine() { + return this.readRegexReturnCapture_(/(.*?)(\n|$)/gm, 1); + } + + + /** + * Reads a word from the parser. This will not read or return any whitespace + * before or after the word (including newlines). Returns null at the end. + * + * @return {?string} + */ + readWord() { + return this.readRegexReturnCapture_(/[^ \t\n]*/gm, 0); + } + + + /** + * Skips any continuous whitespace from the parser. Returns null at the end. + */ + skipWhitespace() { + this.readRegex(/[ \t]+/gm); + } + + + /** + * Reads the given regular expression from the parser. This requires the + * match to be at the current position; there is no need to include a head + * anchor. + * This requires that the regex have the global flag to be set so that it can + * set lastIndex to start the search at the current position. Returns null at + * the end or if the regex does not match the current position. + * + * @param {!RegExp} regex + * @return {Array.} + */ + readRegex(regex) { + const index = this.indexOf_(regex); + if (this.atEnd() || index == null || index.position != this.position_) { + return null; + } + + this.position_ += index.length; + return index.results; } -}; - - -/** - * Returns the index info about a regular expression match. - * - * @param {!RegExp} regex - * @return {?{position: number, length: number, results: !Array.}} - * @private - */ -shaka.util.TextParser.prototype.indexOf_ = function(regex) { - // The global flag is required to use lastIndex. - goog.asserts.assert(regex.global, 'global flag should be set'); - - regex.lastIndex = this.position_; - const results = regex.exec(this.data_); - if (results == null) { - return null; - } else { - return { - position: results.index, - length: results[0].length, - results: results, - }; + + + /** + * Reads a regex from the parser and returns the given capture. + * + * @param {!RegExp} regex + * @param {number} index + * @return {?string} + * @private + */ + readRegexReturnCapture_(regex, index) { + if (this.atEnd()) { + return null; + } + + const ret = this.readRegex(regex); + if (!ret) { + return null; + } else { + return ret[index]; + } + } + + + /** + * Returns the index info about a regular expression match. + * + * @param {!RegExp} regex + * @return {?{position: number, length: number, results: !Array.}} + * @private + */ + indexOf_(regex) { + // The global flag is required to use lastIndex. + goog.asserts.assert(regex.global, 'global flag should be set'); + + regex.lastIndex = this.position_; + const results = regex.exec(this.data_); + if (results == null) { + return null; + } else { + return { + position: results.index, + length: results[0].length, + results: results, + }; + } } }; diff --git a/lib/util/uint8array_utils.js b/lib/util/uint8array_utils.js index 713ca9679..e99624900 100644 --- a/lib/util/uint8array_utils.js +++ b/lib/util/uint8array_utils.js @@ -21,152 +21,152 @@ goog.require('shaka.util.StringUtils'); /** - * @namespace shaka.util.Uint8ArrayUtils * @summary A set of Uint8Array utility functions. * @exportDoc */ - -/** - * Convert a Uint8Array to a base64 string. The output will be standard alphabet - * as opposed to base64url safe alphabet. - * @param {!Uint8Array} u8Arr - * @return {string} - * @export - */ - -shaka.util.Uint8ArrayUtils.toStandardBase64 = function(u8Arr) { - const bytes = shaka.util.StringUtils.fromCharCode(u8Arr); - return btoa(bytes); -}; - -/** - * Convert a Uint8Array to a base64 string. The output will always use the - * alternate encoding/alphabet also known as "base64url". - * @param {!Uint8Array} arr - * @param {boolean=} padding If true, pad the output with equals signs. - * Defaults to true. - * @return {string} - * @export - */ -shaka.util.Uint8ArrayUtils.toBase64 = function(arr, padding) { - padding = (padding == undefined) ? true : padding; - const base64 = shaka.util.Uint8ArrayUtils.toStandardBase64(arr) - .replace(/\+/g, '-').replace(/\//g, '_'); - return padding ? base64 : base64.replace(/[=]*$/, ''); -}; - -/** - * Convert a base64 string to a Uint8Array. Accepts either the standard - * alphabet or the alternate "base64url" alphabet. - * @param {string} str - * @return {!Uint8Array} - * @export - */ -shaka.util.Uint8ArrayUtils.fromBase64 = function(str) { - // atob creates a "raw string" where each character is interpreted as a byte. - const bytes = window.atob(str.replace(/-/g, '+').replace(/_/g, '/')); - const result = new Uint8Array(bytes.length); - for (let i = 0; i < bytes.length; ++i) { - result[i] = bytes.charCodeAt(i); +shaka.util.Uint8ArrayUtils = class { + /** + * Convert a Uint8Array to a base64 string. The output will be standard + * alphabet as opposed to base64url safe alphabet. + * @param {!Uint8Array} u8Arr + * @return {string} + * @export + */ + static toStandardBase64(u8Arr) { + const bytes = shaka.util.StringUtils.fromCharCode(u8Arr); + return btoa(bytes); } - return result; -}; - -/** - * Convert a hex string to a Uint8Array. - * @param {string} str - * @return {!Uint8Array} - * @export - */ -shaka.util.Uint8ArrayUtils.fromHex = function(str) { - const arr = new Uint8Array(str.length / 2); - for (let i = 0; i < str.length; i += 2) { - arr[i / 2] = window.parseInt(str.substr(i, 2), 16); + /** + * Convert a Uint8Array to a base64 string. The output will always use the + * alternate encoding/alphabet also known as "base64url". + * @param {!Uint8Array} arr + * @param {boolean=} padding If true, pad the output with equals signs. + * Defaults to true. + * @return {string} + * @export + */ + static toBase64(arr, padding) { + padding = (padding == undefined) ? true : padding; + const base64 = shaka.util.Uint8ArrayUtils.toStandardBase64(arr) + .replace(/\+/g, '-').replace(/\//g, '_'); + return padding ? base64 : base64.replace(/[=]*$/, ''); } - return arr; -}; - -/** - * Convert a Uint8Array to a hex string. - * @param {!Uint8Array} arr - * @return {string} - * @export - */ -shaka.util.Uint8ArrayUtils.toHex = function(arr) { - let hex = ''; - for (let i = 0; i < arr.length; ++i) { - let value = arr[i].toString(16); - if (value.length == 1) { - value = '0' + value; + /** + * Convert a base64 string to a Uint8Array. Accepts either the standard + * alphabet or the alternate "base64url" alphabet. + * @param {string} str + * @return {!Uint8Array} + * @export + */ + static fromBase64(str) { + // atob creates a "raw string" where each character is interpreted as a + // byte. + const bytes = window.atob(str.replace(/-/g, '+').replace(/_/g, '/')); + const result = new Uint8Array(bytes.length); + for (let i = 0; i < bytes.length; ++i) { + result[i] = bytes.charCodeAt(i); } - hex += value; + return result; } - return hex; -}; -/** - * Compare two Uint8Arrays for equality. - * For convenience, this also accepts Arrays, so that one can trivially compare - * a Uint8Array to an Array of numbers. - * - * @param {(Uint8Array|Array.)} arr1 - * @param {(Uint8Array|Array.)} arr2 - * @return {boolean} - * @export - */ -shaka.util.Uint8ArrayUtils.equal = function(arr1, arr2) { - if (!arr1 && !arr2) { - return true; + /** + * Convert a hex string to a Uint8Array. + * @param {string} str + * @return {!Uint8Array} + * @export + */ + static fromHex(str) { + const arr = new Uint8Array(str.length / 2); + for (let i = 0; i < str.length; i += 2) { + arr[i / 2] = window.parseInt(str.substr(i, 2), 16); + } + return arr; } - if (!arr1 || !arr2) { - return false; + + + /** + * Convert a Uint8Array to a hex string. + * @param {!Uint8Array} arr + * @return {string} + * @export + */ + static toHex(arr) { + let hex = ''; + for (let i = 0; i < arr.length; ++i) { + let value = arr[i].toString(16); + if (value.length == 1) { + value = '0' + value; + } + hex += value; + } + return hex; } - if (arr1.length != arr2.length) { - return false; - } - for (let i = 0; i < arr1.length; ++i) { - if (arr1[i] != arr2[i]) { + + + /** + * Compare two Uint8Arrays for equality. + * For convenience, this also accepts Arrays, so that one can trivially + * compare a Uint8Array to an Array of numbers. + * + * @param {(Uint8Array|Array.)} arr1 + * @param {(Uint8Array|Array.)} arr2 + * @return {boolean} + * @export + */ + static equal(arr1, arr2) { + if (!arr1 && !arr2) { + return true; + } + if (!arr1 || !arr2) { return false; } - } - return true; -}; - - -/** - * Concatenate Uint8Arrays. - * @param {...!Uint8Array} varArgs - * @return {!Uint8Array} - * @export - */ -shaka.util.Uint8ArrayUtils.concat = function(...varArgs) { - let totalLength = 0; - for (let i = 0; i < varArgs.length; ++i) { - totalLength += varArgs[i].length; + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i < arr1.length; ++i) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; } - const result = new Uint8Array(totalLength); - let offset = 0; - for (let i = 0; i < varArgs.length; ++i) { - result.set(varArgs[i], offset); - offset += varArgs[i].length; + + /** + * Concatenate Uint8Arrays. + * @param {...!Uint8Array} varArgs + * @return {!Uint8Array} + * @export + */ + static concat(...varArgs) { + let totalLength = 0; + for (let i = 0; i < varArgs.length; ++i) { + totalLength += varArgs[i].length; + } + + const result = new Uint8Array(totalLength); + let offset = 0; + for (let i = 0; i < varArgs.length; ++i) { + result.set(varArgs[i], offset); + offset += varArgs[i].length; + } + return result; } - return result; -}; -/** - * Creates a DataView over the given buffer. - * @param {!BufferSource} buffer - * @return {!DataView} - */ -shaka.util.Uint8ArrayUtils.toDataView = function(buffer) { - if (buffer instanceof ArrayBuffer) { - return new DataView(buffer); - } else { - return new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + /** + * Creates a DataView over the given buffer. + * @param {!BufferSource} buffer + * @return {!DataView} + */ + static toDataView(buffer) { + if (buffer instanceof ArrayBuffer) { + return new DataView(buffer); + } else { + return new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + } } }; diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js index 22e868ee7..b3a5c1479 100644 --- a/lib/util/xml_utils.js +++ b/lib/util/xml_utils.js @@ -22,332 +22,331 @@ goog.require('shaka.util.StringUtils'); /** - * @namespace shaka.util.XmlUtils * @summary A set of XML utility functions. */ - - -/** - * 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. - */ -shaka.util.XmlUtils.findChild = function(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. - */ -shaka.util.XmlUtils.findChildNS = function(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. - */ -shaka.util.XmlUtils.findChildren = function(elem, name) { - return Array.prototype.filter.call(elem.childNodes, (child) => { - return child instanceof Element && child.tagName == name; - }); -}; - - -/** - * 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. - */ -shaka.util.XmlUtils.findChildrenNS = function(elem, ns, name) { - return Array.prototype.filter.call(elem.childNodes, (child) => { - return child instanceof Element && child.localName == name && - child.namespaceURI == ns; - }); -}; - - -/** - * 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. - */ -shaka.util.XmlUtils.getAttributeNS = function(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 the text contents of a node. - * @param {!Node} elem The XML element. - * @return {?string} The text contents, or null if there are none. - */ -shaka.util.XmlUtils.getContents = function(elem) { - const isText = (child) => { - return child.nodeType == Node.TEXT_NODE || - child.nodeType == Node.CDATA_SECTION_NODE; - }; - if (!Array.prototype.every.call(elem.childNodes, isText)) { - return null; +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]; } - // Read merged text content from all text nodes. - return elem.textContent.trim(); -}; - -/** - * 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 - */ -shaka.util.XmlUtils.parseAttr = function( - 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. - */ -shaka.util.XmlUtils.parseDate = function(dateString) { - if (!dateString) { - return null; + /** + * 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]; } - // 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'; + + /** + * 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) { + return Array.prototype.filter.call(elem.childNodes, (child) => { + return child instanceof Element && child.tagName == name; + }); } - const result = Date.parse(dateString); - return (!isNaN(result) ? Math.floor(result / 1000.0) : null); -}; - -/** - * 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} - */ -shaka.util.XmlUtils.parseDuration = function(durationString) { - if (!durationString) { - return null; + /** + * 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) { + return Array.prototype.filter.call(elem.childNodes, (child) => { + return child instanceof Element && child.localName == name && + child.namespaceURI == ns; + }); } - 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; + /** + * 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; } - // 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; -}; + /** + * 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 isText = (child) => { + return child.nodeType == Node.TEXT_NODE || + child.nodeType == Node.CDATA_SECTION_NODE; + }; + if (!Array.prototype.every.call(elem.childNodes, isText)) { + return 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. - */ -shaka.util.XmlUtils.parseRange = function(rangeString) { - const matches = /([0-9]+)-([0-9]+)/.exec(rangeString); - - if (!matches) { - return null; + // Read merged text content from all text nodes. + return elem.textContent.trim(); } - const start = Number(matches[1]); - if (!isFinite(start)) { - return null; + + /** + * 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; } - const end = Number(matches[2]); - if (!isFinite(end)) { - 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) ? Math.floor(result / 1000.0) : null); } - return {start: start, end: end}; -}; + /** + * 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; + } -/** - * Parses an integer. - * @param {string} intString The integer string. - * @return {?number} The parsed integer on success; otherwise, return null. - */ -shaka.util.XmlUtils.parseInt = function(intString) { - const n = Number(intString); - return (n % 1 === 0) ? n : 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; + } -/** - * Parses a positive integer. - * @param {string} intString The integer string. - * @return {?number} The parsed positive integer on success; otherwise, - * return null. - */ -shaka.util.XmlUtils.parsePositiveInt = function(intString) { - const n = Number(intString); - return (n % 1 === 0) && (n > 0) ? n : 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); - -/** - * Parses a non-negative integer. - * @param {string} intString The integer string. - * @return {?number} The parsed non-negative integer on success; otherwise, - * return null. - */ -shaka.util.XmlUtils.parseNonNegativeInt = function(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. - */ -shaka.util.XmlUtils.parseFloat = function(floatString) { - const n = Number(floatString); - return !isNaN(n) ? n : null; -}; - - -/** - * 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. - */ -shaka.util.XmlUtils.evalDivision = function(exprString) { - let res; - let n; - if ((res = exprString.match(/^(\d+)\/(\d+)$/))) { - n = Number(res[1]) / Number(res[2]); - } else { - n = Number(exprString); + // 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; } - 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} - */ -shaka.util.XmlUtils.parseXmlString = function(xmlString, expectedRootElemName) { - const parser = new DOMParser(); - let rootElem = null; - let xml = null; - try { - xml = parser.parseFromString(xmlString, 'text/xml'); - } catch (exception) {} - if (xml) { - // The top-level element in the loaded xml should have the - // same type as the element linking. - if (xml.documentElement.tagName == expectedRootElemName) { - rootElem = xml.documentElement; + /** + * 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; + } + + + /** + * 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(); + let rootElem = null; + let xml = null; + try { + xml = parser.parseFromString(xmlString, 'text/xml'); + } catch (exception) {} + if (xml) { + // The top-level element in the loaded xml should have the + // same type as the element linking. + if (xml.documentElement.tagName == expectedRootElemName) { + rootElem = xml.documentElement; + } + } + if (rootElem && rootElem.getElementsByTagName('parsererror').length > 0) { + return null; + } // It had a parser error in it. + + return rootElem; + } + + + /** + * Parse some UTF8 data and return the resulting root element if + * it was valid XML. + * @param {ArrayBuffer} data + * @param {string} expectedRootElemName + * @return {Element} + */ + static parseXml(data, expectedRootElemName) { + try { + const string = shaka.util.StringUtils.fromUTF8(data); + return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName); + } catch (exception) { + return null; } } - if (rootElem && rootElem.getElementsByTagName('parsererror').length > 0) { - return null; - } // It had a parser error in it. - - return rootElem; -}; - - -/** - * Parse some UTF8 data and return the resulting root element if - * it was valid XML. - * @param {ArrayBuffer} data - * @param {string} expectedRootElemName - * @return {Element} - */ -shaka.util.XmlUtils.parseXml = function(data, expectedRootElemName) { - try { - const string = shaka.util.StringUtils.fromUTF8(data); - return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName); - } catch (exception) { - return null; - } };