Update Util files to ES6

Issue #1157

Change-Id: Ib81d198e46bc57745f60af328d1160064e253ba3
This commit is contained in:
Michelle Zhuo
2019-05-23 16:42:33 -07:00
parent 92ef26cf56
commit ebdf9d7817
14 changed files with 2033 additions and 2029 deletions
+1 -2
View File
@@ -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);
+122 -119
View File
@@ -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.<!shaka.util.EventManager.Binding_>}
*/
this.bindingMap_ = new shaka.util.MultiMap();
}
/**
* Maps an event type to an array of event bindings.
* @private {shaka.util.MultiMap.<!shaka.util.EventManager.Binding_>}
* 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;
}
};
+45 -45
View File
@@ -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.<ELEM>} array
* @param {function(ELEM):!Promise.<RESULT>} callback
* @return {!Promise.<RESULT>}
* @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.<ELEM>} array
* @param {function(ELEM):!Promise.<RESULT>} callback
* @return {!Promise.<RESULT>}
* @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.<T>} all
* @param {!Array.<T>} part
* @return {!Array.<T>}
* @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.<T>} all
* @param {!Array.<T>} part
* @return {!Array.<T>}
* @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;
}
};
+41 -42
View File
@@ -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.<string>} baseUris
* @param {!Array.<string>} relativeUris
* @return {!Array.<string>}
*/
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.<string>} baseUris
* @param {!Array.<string>} relativeUris
* @return {!Array.<string>}
*/
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.<shaka.extern.InitDataOverride>} 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.<shaka.extern.InitDataOverride>} initData
* @return {shaka.extern.DrmInfo}
*/
static createDrmInfo(keySystem, initData) {
return {
keySystem: keySystem,
licenseServerUri: '',
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
audioRobustness: '',
videoRobustness: '',
serverCertificate: null,
initData: initData || [],
keyIds: [],
};
}
};
+26 -27
View File
@@ -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.<KEY, VALUE>} object
* @return {!Map.<KEY, VALUE>}
* @template KEY,VALUE
*/
static asMap(object) {
const map = new Map();
Object.keys(object).forEach((key) => {
map.set(key, object[key]);
});
return map;
}
/**
* @param {!Object.<KEY, VALUE>} object
* @return {!Map.<KEY, VALUE>}
* @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.<KEY, VALUE>} map
* @return {!Object.<KEY, VALUE>}
* @template KEY,VALUE
*/
static asObject(map) {
const obj = {};
map.forEach((value, key) => {
obj[key] = value;
});
return map;
};
/**
* @param {!Map.<KEY, VALUE>} map
* @return {!Object.<KEY, VALUE>}
* @template KEY,VALUE
*/
shaka.util.MapUtils.asObject = function(map) {
const obj = {};
map.forEach((value, key) => {
obj[key] = value;
});
return obj;
return obj;
}
};
+242 -242
View File
@@ -23,20 +23,252 @@ goog.require('shaka.util.DataViewReader');
/**
* Create a new MP4 Parser
* @struct
* @constructor
* @export
*/
shaka.util.Mp4Parser = function() {
/** @private {!Object.<number, shaka.util.Mp4Parser.BoxType_>} */
this.headers_ = [];
shaka.util.Mp4Parser = class {
constructor() {
/** @private {!Object.<number, shaka.util.Mp4Parser.BoxType_>} */
this.headers_ = [];
/** @private {!Object.<number, !shaka.util.Mp4Parser.CallbackType>} */
this.boxDefinitions_ = [];
/** @private {!Object.<number, !shaka.util.Mp4Parser.CallbackType>} */
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;
};
+74 -74
View File
@@ -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.<string, !Array.<T>>} */
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.<string, !Array.<T>>} */
this.map_ = {};
}
};
/**
* Get a list of values by key.
* @param {string} key
* @return {Array.<T>} 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.<T>}
*/
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.<T>} 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.<T>}
*/
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.<T>)} 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.<T>)} callback
*/
shaka.util.MultiMap.prototype.forEach = function(callback) {
for (const key in this.map_) {
callback(key, this.map_[key]);
}
};
+68 -65
View File
@@ -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.<string>}
* @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.<string>}
*/
this.systemIds = [];
/**
* In hex.
* @type {!Array.<string>}
*/
this.cencKeyIds = [];
/**
* In hex.
* @type {!Array.<string>}
*/
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!');
}
}
};
+34 -31
View File
@@ -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.<T>}
* @return {Promise.<T>}
* @template T
*/
shaka.util.PublicPromise = function() {
let resolvePromise;
let rejectPromise;
shaka.util.PublicPromise = class {
/**
* @return {Promise.<T>}
*/
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) {};
+658 -662
View File
File diff suppressed because it is too large Load Diff
+192 -190
View File
@@ -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;
};
+109 -108
View File
@@ -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.<string>}
*/
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.<string>}
*/
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.<string>}}
* @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.<string>}}
* @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,
};
}
}
};
+127 -127
View File
@@ -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.<number>)} arr1
* @param {(Uint8Array|Array.<number>)} 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.<number>)} arr1
* @param {(Uint8Array|Array.<number>)} 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);
}
}
};
+294 -295
View File
@@ -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.<!Element>} 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.<!Element>} 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.<!Element>} 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.<!Element>} 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;
}
};