summaryrefslogtreecommitdiffstats
path: root/assets/js/videojs-http-streaming.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js/videojs-http-streaming.js')
-rw-r--r--assets/js/videojs-http-streaming.js28894
1 files changed, 0 insertions, 28894 deletions
diff --git a/assets/js/videojs-http-streaming.js b/assets/js/videojs-http-streaming.js
deleted file mode 100644
index 405cde92..00000000
--- a/assets/js/videojs-http-streaming.js
+++ /dev/null
@@ -1,28894 +0,0 @@
-/**
- * @videojs/http-streaming
- * @version 1.2.2
- * @copyright 2018 Brightcove, Inc
- * @license Apache-2.0
- */
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('video.js')) :
- typeof define === 'function' && define.amd ? define(['exports', 'video.js'], factory) :
- (factory((global.videojsHttpStreaming = {}),global.videojs));
-}(this, (function (exports,videojs) { 'use strict';
-
- videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
-
- var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-
- function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
- }
-
- var empty = {};
-
- var empty$1 = /*#__PURE__*/Object.freeze({
- default: empty
- });
-
- var minDoc = ( empty$1 && empty ) || empty$1;
-
- var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {};
-
- var doccy;
-
- if (typeof document !== 'undefined') {
- doccy = document;
- } else {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
-
- if (!doccy) {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
- }
- }
-
- var document_1 = doccy;
-
- var urlToolkit = createCommonjsModule(function (module, exports) {
- // see https://tools.ietf.org/html/rfc1808
-
- /* jshint ignore:start */
- (function (root) {
- /* jshint ignore:end */
-
- var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/\;?#]*)?(.*?)??(;.*?)?(\?.*?)?(#.*?)?$/;
- var FIRST_SEGMENT_REGEX = /^([^\/;?#]*)(.*)$/;
- var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
- var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g;
-
- var URLToolkit = { // jshint ignore:line
- // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
- // E.g
- // With opts.alwaysNormalize = false (default, spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
- // With opts.alwaysNormalize = true (not spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/g
- buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL, opts) {
- opts = opts || {};
- // remove any remaining space and CRLF
- baseURL = baseURL.trim();
- relativeURL = relativeURL.trim();
- if (!relativeURL) {
- // 2a) If the embedded URL is entirely empty, it inherits the
- // entire base URL (i.e., is set equal to the base URL)
- // and we are done.
- if (!opts.alwaysNormalize) {
- return baseURL;
- }
- var basePartsForNormalise = this.parseURL(baseURL);
- if (!basePartsForNormalise) {
- throw new Error('Error trying to parse base URL.');
- }
- basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
- return URLToolkit.buildURLFromParts(basePartsForNormalise);
- }
- var relativeParts = this.parseURL(relativeURL);
- if (!relativeParts) {
- throw new Error('Error trying to parse relative URL.');
- }
- if (relativeParts.scheme) {
- // 2b) If the embedded URL starts with a scheme name, it is
- // interpreted as an absolute URL and we are done.
- if (!opts.alwaysNormalize) {
- return relativeURL;
- }
- relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
- return URLToolkit.buildURLFromParts(relativeParts);
- }
- var baseParts = this.parseURL(baseURL);
- if (!baseParts) {
- throw new Error('Error trying to parse base URL.');
- }
- if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
- // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
- // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
- var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
- baseParts.netLoc = pathParts[1];
- baseParts.path = pathParts[2];
- }
- if (baseParts.netLoc && !baseParts.path) {
- baseParts.path = '/';
- }
- var builtParts = {
- // 2c) Otherwise, the embedded URL inherits the scheme of
- // the base URL.
- scheme: baseParts.scheme,
- netLoc: relativeParts.netLoc,
- path: null,
- params: relativeParts.params,
- query: relativeParts.query,
- fragment: relativeParts.fragment
- };
- if (!relativeParts.netLoc) {
- // 3) If the embedded URL's <net_loc> is non-empty, we skip to
- // Step 7. Otherwise, the embedded URL inherits the <net_loc>
- // (if any) of the base URL.
- builtParts.netLoc = baseParts.netLoc;
- // 4) If the embedded URL path is preceded by a slash "/", the
- // path is not relative and we skip to Step 7.
- if (relativeParts.path[0] !== '/') {
- if (!relativeParts.path) {
- // 5) If the embedded URL path is empty (and not preceded by a
- // slash), then the embedded URL inherits the base URL path
- builtParts.path = baseParts.path;
- // 5a) if the embedded URL's <params> is non-empty, we skip to
- // step 7; otherwise, it inherits the <params> of the base
- // URL (if any) and
- if (!relativeParts.params) {
- builtParts.params = baseParts.params;
- // 5b) if the embedded URL's <query> is non-empty, we skip to
- // step 7; otherwise, it inherits the <query> of the base
- // URL (if any) and we skip to step 7.
- if (!relativeParts.query) {
- builtParts.query = baseParts.query;
- }
- }
- } else {
- // 6) The last segment of the base URL's path (anything
- // following the rightmost slash "/", or the entire path if no
- // slash is present) is removed and the embedded URL's path is
- // appended in its place.
- var baseURLPath = baseParts.path;
- var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
- builtParts.path = URLToolkit.normalizePath(newPath);
- }
- }
- }
- if (builtParts.path === null) {
- builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
- }
- return URLToolkit.buildURLFromParts(builtParts);
- },
- parseURL: function parseURL(url) {
- var parts = URL_REGEX.exec(url);
- if (!parts) {
- return null;
- }
- return {
- scheme: parts[1] || '',
- netLoc: parts[2] || '',
- path: parts[3] || '',
- params: parts[4] || '',
- query: parts[5] || '',
- fragment: parts[6] || ''
- };
- },
- normalizePath: function normalizePath(path) {
- // The following operations are
- // then applied, in order, to the new path:
- // 6a) All occurrences of "./", where "." is a complete path
- // segment, are removed.
- // 6b) If the path ends with "." as a complete path segment,
- // that "." is removed.
- path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, '');
- // 6c) All occurrences of "<segment>/../", where <segment> is a
- // complete path segment not equal to "..", are removed.
- // Removal of these path segments is performed iteratively,
- // removing the leftmost matching pattern on each iteration,
- // until no matching pattern remains.
- // 6d) If the path ends with "<segment>/..", where <segment> is a
- // complete path segment not equal to "..", that
- // "<segment>/.." is removed.
- while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} // jshint ignore:line
- return path.split('').reverse().join('');
- },
- buildURLFromParts: function buildURLFromParts(parts) {
- return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
- }
- };
-
- /* jshint ignore:start */
- module.exports = URLToolkit;
- })(commonjsGlobal);
- /* jshint ignore:end */
- });
-
- var win;
-
- if (typeof window !== "undefined") {
- win = window;
- } else if (typeof commonjsGlobal !== "undefined") {
- win = commonjsGlobal;
- } else if (typeof self !== "undefined") {
- win = self;
- } else {
- win = {};
- }
-
- var window_1 = win;
-
- /**
- * @file resolve-url.js
- */
-
- var resolveUrl = function resolveUrl(baseURL, relativeURL) {
- // return early if we don't need to resolve
- if (/^[a-z]+:/i.test(relativeURL)) {
- return relativeURL;
- }
-
- // if the base URL is relative then combine with the current location
- if (!/\/\//i.test(baseURL)) {
- baseURL = urlToolkit.buildAbsoluteURL(window_1.location.href, baseURL);
- }
-
- return urlToolkit.buildAbsoluteURL(baseURL, relativeURL);
- };
-
- var classCallCheck = function classCallCheck(instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- var _extends = Object.assign || function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
-
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
-
- return target;
- };
-
- var inherits = function inherits(subClass, superClass) {
- if (typeof superClass !== "function" && superClass !== null) {
- throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
- }
-
- subClass.prototype = Object.create(superClass && superClass.prototype, {
- constructor: {
- value: subClass,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
- };
-
- var possibleConstructorReturn = function possibleConstructorReturn(self, call) {
- if (!self) {
- throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
- }
-
- return call && (typeof call === "object" || typeof call === "function") ? call : self;
- };
-
- /**
- * @file stream.js
- */
- /**
- * A lightweight readable stream implemention that handles event dispatching.
- *
- * @class Stream
- */
- var Stream = function () {
- function Stream() {
- classCallCheck(this, Stream);
-
- this.listeners = {};
- }
-
- /**
- * Add a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener the callback to be invoked when an event of
- * the specified type occurs
- */
-
- Stream.prototype.on = function on(type, listener) {
- if (!this.listeners[type]) {
- this.listeners[type] = [];
- }
- this.listeners[type].push(listener);
- };
-
- /**
- * Remove a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener a function previously registered for this
- * type of event through `on`
- * @return {Boolean} if we could turn it off or not
- */
-
- Stream.prototype.off = function off(type, listener) {
- if (!this.listeners[type]) {
- return false;
- }
-
- var index = this.listeners[type].indexOf(listener);
-
- this.listeners[type].splice(index, 1);
- return index > -1;
- };
-
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- *
- * @param {String} type the event name
- */
-
- Stream.prototype.trigger = function trigger(type) {
- var callbacks = this.listeners[type];
- var i = void 0;
- var length = void 0;
- var args = void 0;
-
- if (!callbacks) {
- return;
- }
- // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
- if (arguments.length === 2) {
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- args = Array.prototype.slice.call(arguments, 1);
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].apply(this, args);
- }
- }
- };
-
- /**
- * Destroys the stream and cleans up.
- */
-
- Stream.prototype.dispose = function dispose() {
- this.listeners = {};
- };
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- *
- * @param {Stream} destination the stream that will receive all `data` events
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */
-
- Stream.prototype.pipe = function pipe(destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
- };
-
- return Stream;
- }();
-
- /**
- * @file m3u8/line-stream.js
- */
- /**
- * A stream that buffers string input and generates a `data` event for each
- * line.
- *
- * @class LineStream
- * @extends Stream
- */
-
- var LineStream = function (_Stream) {
- inherits(LineStream, _Stream);
-
- function LineStream() {
- classCallCheck(this, LineStream);
-
- var _this = possibleConstructorReturn(this, _Stream.call(this));
-
- _this.buffer = '';
- return _this;
- }
-
- /**
- * Add new data to be parsed.
- *
- * @param {String} data the text to process
- */
-
- LineStream.prototype.push = function push(data) {
- var nextNewline = void 0;
-
- this.buffer += data;
- nextNewline = this.buffer.indexOf('\n');
-
- for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
- this.trigger('data', this.buffer.substring(0, nextNewline));
- this.buffer = this.buffer.substring(nextNewline + 1);
- }
- };
-
- return LineStream;
- }(Stream);
-
- /**
- * @file m3u8/parse-stream.js
- */
- /**
- * "forgiving" attribute list psuedo-grammar:
- * attributes -> keyvalue (',' keyvalue)*
- * keyvalue -> key '=' value
- * key -> [^=]*
- * value -> '"' [^"]* '"' | [^,]*
- */
- var attributeSeparator = function attributeSeparator() {
- var key = '[^=]*';
- var value = '"[^"]*"|[^,]*';
- var keyvalue = '(?:' + key + ')=(?:' + value + ')';
-
- return new RegExp('(?:^|,)(' + keyvalue + ')');
- };
-
- /**
- * Parse attributes from a line given the seperator
- *
- * @param {String} attributes the attibute line to parse
- */
- var parseAttributes = function parseAttributes(attributes) {
- // split the string using attributes as the separator
- var attrs = attributes.split(attributeSeparator());
- var result = {};
- var i = attrs.length;
- var attr = void 0;
-
- while (i--) {
- // filter out unmatched portions of the string
- if (attrs[i] === '') {
- continue;
- }
-
- // split the key and value
- attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1);
- // trim whitespace and remove optional quotes around the value
- attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
- attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
- attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
- result[attr[0]] = attr[1];
- }
- return result;
- };
-
- /**
- * A line-level M3U8 parser event stream. It expects to receive input one
- * line at a time and performs a context-free parse of its contents. A stream
- * interpretation of a manifest can be useful if the manifest is expected to
- * be too large to fit comfortably into memory or the entirety of the input
- * is not immediately available. Otherwise, it's probably much easier to work
- * with a regular `Parser` object.
- *
- * Produces `data` events with an object that captures the parser's
- * interpretation of the input. That object has a property `tag` that is one
- * of `uri`, `comment`, or `tag`. URIs only have a single additional
- * property, `line`, which captures the entirety of the input without
- * interpretation. Comments similarly have a single additional property
- * `text` which is the input without the leading `#`.
- *
- * Tags always have a property `tagType` which is the lower-cased version of
- * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
- * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
- * tags are given the tag type `unknown` and a single additional property
- * `data` with the remainder of the input.
- *
- * @class ParseStream
- * @extends Stream
- */
-
- var ParseStream = function (_Stream) {
- inherits(ParseStream, _Stream);
-
- function ParseStream() {
- classCallCheck(this, ParseStream);
-
- var _this = possibleConstructorReturn(this, _Stream.call(this));
-
- _this.customParsers = [];
- return _this;
- }
-
- /**
- * Parses an additional line of input.
- *
- * @param {String} line a single line of an M3U8 file to parse
- */
-
- ParseStream.prototype.push = function push(line) {
- var match = void 0;
- var event = void 0;
-
- // strip whitespace
- line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, '');
- if (line.length === 0) {
- // ignore empty lines
- return;
- }
-
- // URIs
- if (line[0] !== '#') {
- this.trigger('data', {
- type: 'uri',
- uri: line
- });
- return;
- }
-
- for (var i = 0; i < this.customParsers.length; i++) {
- if (this.customParsers[i].call(this, line)) {
- return;
- }
- }
-
- // Comments
- if (line.indexOf('#EXT') !== 0) {
- this.trigger('data', {
- type: 'comment',
- text: line.slice(1)
- });
- return;
- }
-
- // strip off any carriage returns here so the regex matching
- // doesn't have to account for them.
- line = line.replace('\r', '');
-
- // Tags
- match = /^#EXTM3U/.exec(line);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'm3u'
- });
- return;
- }
- match = /^#EXTINF:?([0-9\.]*)?,?(.*)?$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'inf'
- };
- if (match[1]) {
- event.duration = parseFloat(match[1]);
- }
- if (match[2]) {
- event.title = match[2];
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-TARGETDURATION:?([0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'targetduration'
- };
- if (match[1]) {
- event.duration = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#ZEN-TOTAL-DURATION:?([0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'totalduration'
- };
- if (match[1]) {
- event.duration = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-VERSION:?([0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'version'
- };
- if (match[1]) {
- event.version = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'media-sequence'
- };
- if (match[1]) {
- event.number = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'discontinuity-sequence'
- };
- if (match[1]) {
- event.number = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-PLAYLIST-TYPE:?(.*)?$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'playlist-type'
- };
- if (match[1]) {
- event.playlistType = match[1];
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'byterange'
- };
- if (match[1]) {
- event.length = parseInt(match[1], 10);
- }
- if (match[2]) {
- event.offset = parseInt(match[2], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-ALLOW-CACHE:?(YES|NO)?/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'allow-cache'
- };
- if (match[1]) {
- event.allowed = !/NO/.test(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MAP:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'map'
- };
-
- if (match[1]) {
- var attributes = parseAttributes(match[1]);
-
- if (attributes.URI) {
- event.uri = attributes.URI;
- }
- if (attributes.BYTERANGE) {
- var _attributes$BYTERANGE = attributes.BYTERANGE.split('@'),
- length = _attributes$BYTERANGE[0],
- offset = _attributes$BYTERANGE[1];
-
- event.byterange = {};
- if (length) {
- event.byterange.length = parseInt(length, 10);
- }
- if (offset) {
- event.byterange.offset = parseInt(offset, 10);
- }
- }
- }
-
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-STREAM-INF:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'stream-inf'
- };
- if (match[1]) {
- event.attributes = parseAttributes(match[1]);
-
- if (event.attributes.RESOLUTION) {
- var split = event.attributes.RESOLUTION.split('x');
- var resolution = {};
-
- if (split[0]) {
- resolution.width = parseInt(split[0], 10);
- }
- if (split[1]) {
- resolution.height = parseInt(split[1], 10);
- }
- event.attributes.RESOLUTION = resolution;
- }
- if (event.attributes.BANDWIDTH) {
- event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
- }
- if (event.attributes['PROGRAM-ID']) {
- event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
- }
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MEDIA:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'media'
- };
- if (match[1]) {
- event.attributes = parseAttributes(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-ENDLIST/.exec(line);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'endlist'
- });
- return;
- }
- match = /^#EXT-X-DISCONTINUITY/.exec(line);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'discontinuity'
- });
- return;
- }
- match = /^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'program-date-time'
- };
- if (match[1]) {
- event.dateTimeString = match[1];
- event.dateTimeObject = new Date(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-KEY:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'key'
- };
- if (match[1]) {
- event.attributes = parseAttributes(match[1]);
- // parse the IV string into a Uint32Array
- if (event.attributes.IV) {
- if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
- event.attributes.IV = event.attributes.IV.substring(2);
- }
-
- event.attributes.IV = event.attributes.IV.match(/.{8}/g);
- event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
- event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
- event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
- event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
- event.attributes.IV = new Uint32Array(event.attributes.IV);
- }
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-START:?(.*)$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'start'
- };
- if (match[1]) {
- event.attributes = parseAttributes(match[1]);
-
- event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']);
- event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-OUT-CONT:?(.*)?$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-out-cont'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-OUT:?(.*)?$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-out'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(line);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-in'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
-
- // unknown tag type
- this.trigger('data', {
- type: 'tag',
- data: line.slice(4)
- });
- };
-
- /**
- * Add a parser for custom headers
- *
- * @param {Object} options a map of options for the added parser
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {string} options.customType the custom type to register to the output
- * @param {Function} [options.dataParser] function to parse the line into an object
- * @param {boolean} [options.segment] should tag data be attached to the segment object
- */
-
- ParseStream.prototype.addParser = function addParser(_ref) {
- var _this2 = this;
-
- var expression = _ref.expression,
- customType = _ref.customType,
- dataParser = _ref.dataParser,
- segment = _ref.segment;
-
- if (typeof dataParser !== 'function') {
- dataParser = function dataParser(line) {
- return line;
- };
- }
- this.customParsers.push(function (line) {
- var match = expression.exec(line);
-
- if (match) {
- _this2.trigger('data', {
- type: 'custom',
- data: dataParser(line),
- customType: customType,
- segment: segment
- });
- return true;
- }
- });
- };
-
- return ParseStream;
- }(Stream);
-
- /**
- * @file m3u8/parser.js
- */
- /**
- * A parser for M3U8 files. The current interpretation of the input is
- * exposed as a property `manifest` on parser objects. It's just two lines to
- * create and parse a manifest once you have the contents available as a string:
- *
- * ```js
- * var parser = new m3u8.Parser();
- * parser.push(xhr.responseText);
- * ```
- *
- * New input can later be applied to update the manifest object by calling
- * `push` again.
- *
- * The parser attempts to create a usable manifest object even if the
- * underlying input is somewhat nonsensical. It emits `info` and `warning`
- * events during the parse if it encounters input that seems invalid or
- * requires some property of the manifest object to be defaulted.
- *
- * @class Parser
- * @extends Stream
- */
-
- var Parser = function (_Stream) {
- inherits(Parser, _Stream);
-
- function Parser() {
- classCallCheck(this, Parser);
-
- var _this = possibleConstructorReturn(this, _Stream.call(this));
-
- _this.lineStream = new LineStream();
- _this.parseStream = new ParseStream();
- _this.lineStream.pipe(_this.parseStream);
-
- /* eslint-disable consistent-this */
- var self = _this;
- /* eslint-enable consistent-this */
- var uris = [];
- var currentUri = {};
- // if specified, the active EXT-X-MAP definition
- var currentMap = void 0;
- // if specified, the active decryption key
- var _key = void 0;
- var noop = function noop() {};
- var defaultMediaGroups = {
- 'AUDIO': {},
- 'VIDEO': {},
- 'CLOSED-CAPTIONS': {},
- 'SUBTITLES': {}
- };
- // group segments into numbered timelines delineated by discontinuities
- var currentTimeline = 0;
-
- // the manifest is empty until the parse stream begins delivering data
- _this.manifest = {
- allowCache: true,
- discontinuityStarts: [],
- segments: []
- };
-
- // update the manifest with the m3u8 entry from the parse stream
- _this.parseStream.on('data', function (entry) {
- var mediaGroup = void 0;
- var rendition = void 0;
-
- ({
- tag: function tag() {
- // switch based on the tag type
- (({
- 'allow-cache': function allowCache() {
- this.manifest.allowCache = entry.allowed;
- if (!('allowed' in entry)) {
- this.trigger('info', {
- message: 'defaulting allowCache to YES'
- });
- this.manifest.allowCache = true;
- }
- },
- byterange: function byterange() {
- var byterange = {};
-
- if ('length' in entry) {
- currentUri.byterange = byterange;
- byterange.length = entry.length;
-
- if (!('offset' in entry)) {
- this.trigger('info', {
- message: 'defaulting offset to zero'
- });
- entry.offset = 0;
- }
- }
- if ('offset' in entry) {
- currentUri.byterange = byterange;
- byterange.offset = entry.offset;
- }
- },
- endlist: function endlist() {
- this.manifest.endList = true;
- },
- inf: function inf() {
- if (!('mediaSequence' in this.manifest)) {
- this.manifest.mediaSequence = 0;
- this.trigger('info', {
- message: 'defaulting media sequence to zero'
- });
- }
- if (!('discontinuitySequence' in this.manifest)) {
- this.manifest.discontinuitySequence = 0;
- this.trigger('info', {
- message: 'defaulting discontinuity sequence to zero'
- });
- }
- if (entry.duration > 0) {
- currentUri.duration = entry.duration;
- }
-
- if (entry.duration === 0) {
- currentUri.duration = 0.01;
- this.trigger('info', {
- message: 'updating zero segment duration to a small value'
- });
- }
-
- this.manifest.segments = uris;
- },
- key: function key() {
- if (!entry.attributes) {
- this.trigger('warn', {
- message: 'ignoring key declaration without attribute list'
- });
- return;
- }
- // clear the active encryption key
- if (entry.attributes.METHOD === 'NONE') {
- _key = null;
- return;
- }
- if (!entry.attributes.URI) {
- this.trigger('warn', {
- message: 'ignoring key declaration without URI'
- });
- return;
- }
- if (!entry.attributes.METHOD) {
- this.trigger('warn', {
- message: 'defaulting key method to AES-128'
- });
- }
-
- // setup an encryption key for upcoming segments
- _key = {
- method: entry.attributes.METHOD || 'AES-128',
- uri: entry.attributes.URI
- };
-
- if (typeof entry.attributes.IV !== 'undefined') {
- _key.iv = entry.attributes.IV;
- }
- },
- 'media-sequence': function mediaSequence() {
- if (!isFinite(entry.number)) {
- this.trigger('warn', {
- message: 'ignoring invalid media sequence: ' + entry.number
- });
- return;
- }
- this.manifest.mediaSequence = entry.number;
- },
- 'discontinuity-sequence': function discontinuitySequence() {
- if (!isFinite(entry.number)) {
- this.trigger('warn', {
- message: 'ignoring invalid discontinuity sequence: ' + entry.number
- });
- return;
- }
- this.manifest.discontinuitySequence = entry.number;
- currentTimeline = entry.number;
- },
- 'playlist-type': function playlistType() {
- if (!/VOD|EVENT/.test(entry.playlistType)) {
- this.trigger('warn', {
- message: 'ignoring unknown playlist type: ' + entry.playlist
- });
- return;
- }
- this.manifest.playlistType = entry.playlistType;
- },
- map: function map() {
- currentMap = {};
- if (entry.uri) {
- currentMap.uri = entry.uri;
- }
- if (entry.byterange) {
- currentMap.byterange = entry.byterange;
- }
- },
- 'stream-inf': function streamInf() {
- this.manifest.playlists = uris;
- this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
-
- if (!entry.attributes) {
- this.trigger('warn', {
- message: 'ignoring empty stream-inf attributes'
- });
- return;
- }
-
- if (!currentUri.attributes) {
- currentUri.attributes = {};
- }
- _extends(currentUri.attributes, entry.attributes);
- },
- media: function media() {
- this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
-
- if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
- this.trigger('warn', {
- message: 'ignoring incomplete or missing media group'
- });
- return;
- }
-
- // find the media group, creating defaults as necessary
- var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
-
- mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
- mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
-
- // collect the rendition metadata
- rendition = {
- 'default': /yes/i.test(entry.attributes.DEFAULT)
- };
- if (rendition['default']) {
- rendition.autoselect = true;
- } else {
- rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
- }
- if (entry.attributes.LANGUAGE) {
- rendition.language = entry.attributes.LANGUAGE;
- }
- if (entry.attributes.URI) {
- rendition.uri = entry.attributes.URI;
- }
- if (entry.attributes['INSTREAM-ID']) {
- rendition.instreamId = entry.attributes['INSTREAM-ID'];
- }
- if (entry.attributes.CHARACTERISTICS) {
- rendition.characteristics = entry.attributes.CHARACTERISTICS;
- }
- if (entry.attributes.FORCED) {
- rendition.forced = /yes/i.test(entry.attributes.FORCED);
- }
-
- // insert the new rendition
- mediaGroup[entry.attributes.NAME] = rendition;
- },
- discontinuity: function discontinuity() {
- currentTimeline += 1;
- currentUri.discontinuity = true;
- this.manifest.discontinuityStarts.push(uris.length);
- },
- 'program-date-time': function programDateTime() {
- if (typeof this.manifest.dateTimeString === 'undefined') {
- // PROGRAM-DATE-TIME is a media-segment tag, but for backwards
- // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
- // to the manifest object
- // TODO: Consider removing this in future major version
- this.manifest.dateTimeString = entry.dateTimeString;
- this.manifest.dateTimeObject = entry.dateTimeObject;
- }
-
- currentUri.dateTimeString = entry.dateTimeString;
- currentUri.dateTimeObject = entry.dateTimeObject;
- },
- targetduration: function targetduration() {
- if (!isFinite(entry.duration) || entry.duration < 0) {
- this.trigger('warn', {
- message: 'ignoring invalid target duration: ' + entry.duration
- });
- return;
- }
- this.manifest.targetDuration = entry.duration;
- },
- totalduration: function totalduration() {
- if (!isFinite(entry.duration) || entry.duration < 0) {
- this.trigger('warn', {
- message: 'ignoring invalid total duration: ' + entry.duration
- });
- return;
- }
- this.manifest.totalDuration = entry.duration;
- },
- start: function start() {
- if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
- this.trigger('warn', {
- message: 'ignoring start declaration without appropriate attribute list'
- });
- return;
- }
- this.manifest.start = {
- timeOffset: entry.attributes['TIME-OFFSET'],
- precise: entry.attributes.PRECISE
- };
- },
- 'cue-out': function cueOut() {
- currentUri.cueOut = entry.data;
- },
- 'cue-out-cont': function cueOutCont() {
- currentUri.cueOutCont = entry.data;
- },
- 'cue-in': function cueIn() {
- currentUri.cueIn = entry.data;
- }
- })[entry.tagType] || noop).call(self);
- },
- uri: function uri() {
- currentUri.uri = entry.uri;
- uris.push(currentUri);
-
- // if no explicit duration was declared, use the target duration
- if (this.manifest.targetDuration && !('duration' in currentUri)) {
- this.trigger('warn', {
- message: 'defaulting segment duration to the target duration'
- });
- currentUri.duration = this.manifest.targetDuration;
- }
- // annotate with encryption information, if necessary
- if (_key) {
- currentUri.key = _key;
- }
- currentUri.timeline = currentTimeline;
- // annotate with initialization segment information, if necessary
- if (currentMap) {
- currentUri.map = currentMap;
- }
-
- // prepare for the next URI
- currentUri = {};
- },
- comment: function comment() {
- // comments are not important for playback
- },
- custom: function custom() {
- // if this is segment-level data attach the output to the segment
- if (entry.segment) {
- currentUri.custom = currentUri.custom || {};
- currentUri.custom[entry.customType] = entry.data;
- // if this is manifest-level data attach to the top level manifest object
- } else {
- this.manifest.custom = this.manifest.custom || {};
- this.manifest.custom[entry.customType] = entry.data;
- }
- }
- })[entry.type].call(self);
- });
- return _this;
- }
-
- /**
- * Parse the input string and update the manifest object.
- *
- * @param {String} chunk a potentially incomplete portion of the manifest
- */
-
- Parser.prototype.push = function push(chunk) {
- this.lineStream.push(chunk);
- };
-
- /**
- * Flush any remaining input. This can be handy if the last line of an M3U8
- * manifest did not contain a trailing newline but the file has been
- * completely received.
- */
-
- Parser.prototype.end = function end() {
- // flush any buffered input
- this.lineStream.push('\n');
- };
- /**
- * Add an additional parser for non-standard tags
- *
- * @param {Object} options a map of options for the added parser
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {string} options.type the type to register to the output
- * @param {Function} [options.dataParser] function to parse the line into an object
- * @param {boolean} [options.segment] should tag data be attached to the segment object
- */
-
- Parser.prototype.addParser = function addParser(options) {
- this.parseStream.addParser(options);
- };
-
- return Parser;
- }(Stream);
-
- var classCallCheck$1 = function (instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- var createClass = function () {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
- }
- }
-
- return function (Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- };
- }();
-
- var get = function get(object, property, receiver) {
- if (object === null) object = Function.prototype;
- var desc = Object.getOwnPropertyDescriptor(object, property);
-
- if (desc === undefined) {
- var parent = Object.getPrototypeOf(object);
-
- if (parent === null) {
- return undefined;
- } else {
- return get(parent, property, receiver);
- }
- } else if ("value" in desc) {
- return desc.value;
- } else {
- var getter = desc.get;
-
- if (getter === undefined) {
- return undefined;
- }
-
- return getter.call(receiver);
- }
- };
-
- var inherits$1 = function (subClass, superClass) {
- if (typeof superClass !== "function" && superClass !== null) {
- throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
- }
-
- subClass.prototype = Object.create(superClass && superClass.prototype, {
- constructor: {
- value: subClass,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
- };
-
- var possibleConstructorReturn$1 = function (self, call) {
- if (!self) {
- throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
- }
-
- return call && (typeof call === "object" || typeof call === "function") ? call : self;
- };
-
- var slicedToArray = function () {
- function sliceIterator(arr, i) {
- var _arr = [];
- var _n = true;
- var _d = false;
- var _e = undefined;
-
- try {
- for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
- _arr.push(_s.value);
-
- if (i && _arr.length === i) break;
- }
- } catch (err) {
- _d = true;
- _e = err;
- } finally {
- try {
- if (!_n && _i["return"]) _i["return"]();
- } finally {
- if (_d) throw _e;
- }
- }
-
- return _arr;
- }
-
- return function (arr, i) {
- if (Array.isArray(arr)) {
- return arr;
- } else if (Symbol.iterator in Object(arr)) {
- return sliceIterator(arr, i);
- } else {
- throw new TypeError("Invalid attempt to destructure non-iterable instance");
- }
- };
- }();
-
- /**
- * @file playlist-loader.js
- *
- * A state machine that manages the loading, caching, and updating of
- * M3U8 playlists.
- *
- */
-
- var mergeOptions = videojs.mergeOptions,
- EventTarget = videojs.EventTarget,
- log = videojs.log;
-
- /**
- * Loops through all supported media groups in master and calls the provided
- * callback for each group
- *
- * @param {Object} master
- * The parsed master manifest object
- * @param {Function} callback
- * Callback to call for each media group
- */
-
- var forEachMediaGroup = function forEachMediaGroup(master, callback) {
- ['AUDIO', 'SUBTITLES'].forEach(function (mediaType) {
- for (var groupKey in master.mediaGroups[mediaType]) {
- for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
- var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
-
- callback(mediaProperties, mediaType, groupKey, labelKey);
- }
- }
- });
- };
-
- /**
- * Returns a new array of segments that is the result of merging
- * properties from an older list of segments onto an updated
- * list. No properties on the updated playlist will be overridden.
- *
- * @param {Array} original the outdated list of segments
- * @param {Array} update the updated list of segments
- * @param {Number=} offset the index of the first update
- * segment in the original segment list. For non-live playlists,
- * this should always be zero and does not need to be
- * specified. For live playlists, it should be the difference
- * between the media sequence numbers in the original and updated
- * playlists.
- * @return a list of merged segment objects
- */
- var updateSegments = function updateSegments(original, update, offset) {
- var result = update.slice();
-
- offset = offset || 0;
- var length = Math.min(original.length, update.length + offset);
-
- for (var i = offset; i < length; i++) {
- result[i - offset] = mergeOptions(original[i], result[i - offset]);
- }
- return result;
- };
-
- var resolveSegmentUris = function resolveSegmentUris(segment, baseUri) {
- if (!segment.resolvedUri) {
- segment.resolvedUri = resolveUrl(baseUri, segment.uri);
- }
- if (segment.key && !segment.key.resolvedUri) {
- segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri);
- }
- if (segment.map && !segment.map.resolvedUri) {
- segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
- }
- };
-
- /**
- * Returns a new master playlist that is the result of merging an
- * updated media playlist into the original version. If the
- * updated media playlist does not match any of the playlist
- * entries in the original master playlist, null is returned.
- *
- * @param {Object} master a parsed master M3U8 object
- * @param {Object} media a parsed media M3U8 object
- * @return {Object} a new object that represents the original
- * master playlist with the updated media playlist merged in, or
- * null if the merge produced no change.
- */
- var updateMaster = function updateMaster(master, media) {
- var result = mergeOptions(master, {});
- var playlist = result.playlists[media.uri];
-
- if (!playlist) {
- return null;
- }
-
- // consider the playlist unchanged if the number of segments is equal and the media
- // sequence number is unchanged
- if (playlist.segments && media.segments && playlist.segments.length === media.segments.length && playlist.mediaSequence === media.mediaSequence) {
- return null;
- }
-
- var mergedPlaylist = mergeOptions(playlist, media);
-
- // if the update could overlap existing segment information, merge the two segment lists
- if (playlist.segments) {
- mergedPlaylist.segments = updateSegments(playlist.segments, media.segments, media.mediaSequence - playlist.mediaSequence);
- }
-
- // resolve any segment URIs to prevent us from having to do it later
- mergedPlaylist.segments.forEach(function (segment) {
- resolveSegmentUris(segment, mergedPlaylist.resolvedUri);
- });
-
- // TODO Right now in the playlists array there are two references to each playlist, one
- // that is referenced by index, and one by URI. The index reference may no longer be
- // necessary.
- for (var i = 0; i < result.playlists.length; i++) {
- if (result.playlists[i].uri === media.uri) {
- result.playlists[i] = mergedPlaylist;
- }
- }
- result.playlists[media.uri] = mergedPlaylist;
-
- return result;
- };
-
- var setupMediaPlaylists = function setupMediaPlaylists(master) {
- // setup by-URI lookups and resolve media playlist URIs
- var i = master.playlists.length;
-
- while (i--) {
- var playlist = master.playlists[i];
-
- master.playlists[playlist.uri] = playlist;
- playlist.resolvedUri = resolveUrl(master.uri, playlist.uri);
- playlist.id = i;
-
- if (!playlist.attributes) {
- // Although the spec states an #EXT-X-STREAM-INF tag MUST have a
- // BANDWIDTH attribute, we can play the stream without it. This means a poorly
- // formatted master playlist may not have an attribute list. An attributes
- // property is added here to prevent undefined references when we encounter
- // this scenario.
- playlist.attributes = {};
-
- log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
- }
- }
- };
-
- var resolveMediaGroupUris = function resolveMediaGroupUris(master) {
- forEachMediaGroup(master, function (properties) {
- if (properties.uri) {
- properties.resolvedUri = resolveUrl(master.uri, properties.uri);
- }
- });
- };
-
- /**
- * Calculates the time to wait before refreshing a live playlist
- *
- * @param {Object} media
- * The current media
- * @param {Boolean} update
- * True if there were any updates from the last refresh, false otherwise
- * @return {Number}
- * The time in ms to wait before refreshing the live playlist
- */
- var refreshDelay = function refreshDelay(media, update) {
- var lastSegment = media.segments[media.segments.length - 1];
- var delay = void 0;
-
- if (update && lastSegment && lastSegment.duration) {
- delay = lastSegment.duration * 1000;
- } else {
- // if the playlist is unchanged since the last reload or last segment duration
- // cannot be determined, try again after half the target duration
- delay = (media.targetDuration || 10) * 500;
- }
- return delay;
- };
-
- /**
- * Load a playlist from a remote location
- *
- * @class PlaylistLoader
- * @extends Stream
- * @param {String} srcUrl the url to start with
- * @param {Boolean} withCredentials the withCredentials xhr option
- * @constructor
- */
-
- var PlaylistLoader = function (_EventTarget) {
- inherits$1(PlaylistLoader, _EventTarget);
-
- function PlaylistLoader(srcUrl, hls, withCredentials) {
- classCallCheck$1(this, PlaylistLoader);
-
- var _this = possibleConstructorReturn$1(this, (PlaylistLoader.__proto__ || Object.getPrototypeOf(PlaylistLoader)).call(this));
-
- _this.srcUrl = srcUrl;
- _this.hls_ = hls;
- _this.withCredentials = withCredentials;
-
- if (!_this.srcUrl) {
- throw new Error('A non-empty playlist URL is required');
- }
-
- // initialize the loader state
- _this.state = 'HAVE_NOTHING';
-
- // live playlist staleness timeout
- _this.on('mediaupdatetimeout', function () {
- if (_this.state !== 'HAVE_METADATA') {
- // only refresh the media playlist if no other activity is going on
- return;
- }
-
- _this.state = 'HAVE_CURRENT_METADATA';
-
- _this.request = _this.hls_.xhr({
- uri: resolveUrl(_this.master.uri, _this.media().uri),
- withCredentials: _this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this.request) {
- return;
- }
-
- if (error) {
- return _this.playlistRequestError(_this.request, _this.media().uri, 'HAVE_METADATA');
- }
-
- _this.haveMetadata(_this.request, _this.media().uri);
- });
- });
- return _this;
- }
-
- createClass(PlaylistLoader, [{
- key: 'playlistRequestError',
- value: function playlistRequestError(xhr, url, startingState) {
- // any in-flight request is now finished
- this.request = null;
-
- if (startingState) {
- this.state = startingState;
- }
-
- this.error = {
- playlist: this.master.playlists[url],
- status: xhr.status,
- message: 'HLS playlist request error at URL: ' + url,
- responseText: xhr.responseText,
- code: xhr.status >= 500 ? 4 : 2
- };
-
- this.trigger('error');
- }
-
- // update the playlist loader's state in response to a new or
- // updated playlist.
-
- }, {
- key: 'haveMetadata',
- value: function haveMetadata(xhr, url) {
- var _this2 = this;
-
- // any in-flight request is now finished
- this.request = null;
- this.state = 'HAVE_METADATA';
-
- var parser = new Parser();
-
- parser.push(xhr.responseText);
- parser.end();
- parser.manifest.uri = url;
- // m3u8-parser does not attach an attributes property to media playlists so make
- // sure that the property is attached to avoid undefined reference errors
- parser.manifest.attributes = parser.manifest.attributes || {};
-
- // merge this playlist into the master
- var update = updateMaster(this.master, parser.manifest);
-
- this.targetDuration = parser.manifest.targetDuration;
-
- if (update) {
- this.master = update;
- this.media_ = this.master.playlists[parser.manifest.uri];
- } else {
- this.trigger('playlistunchanged');
- }
-
- // refresh live playlists after a target duration passes
- if (!this.media().endList) {
- window_1.clearTimeout(this.mediaUpdateTimeout);
- this.mediaUpdateTimeout = window_1.setTimeout(function () {
- _this2.trigger('mediaupdatetimeout');
- }, refreshDelay(this.media(), !!update));
- }
-
- this.trigger('loadedplaylist');
- }
-
- /**
- * Abort any outstanding work and clean up.
- */
-
- }, {
- key: 'dispose',
- value: function dispose() {
- this.stopRequest();
- window_1.clearTimeout(this.mediaUpdateTimeout);
- }
- }, {
- key: 'stopRequest',
- value: function stopRequest() {
- if (this.request) {
- var oldRequest = this.request;
-
- this.request = null;
- oldRequest.onreadystatechange = null;
- oldRequest.abort();
- }
- }
-
- /**
- * When called without any arguments, returns the currently
- * active media playlist. When called with a single argument,
- * triggers the playlist loader to asynchronously switch to the
- * specified media playlist. Calling this method while the
- * loader is in the HAVE_NOTHING causes an error to be emitted
- * but otherwise has no effect.
- *
- * @param {Object=} playlist the parsed media playlist
- * object to switch to
- * @return {Playlist} the current loaded media
- */
-
- }, {
- key: 'media',
- value: function media(playlist) {
- var _this3 = this;
-
- // getter
- if (!playlist) {
- return this.media_;
- }
-
- // setter
- if (this.state === 'HAVE_NOTHING') {
- throw new Error('Cannot switch media playlist from ' + this.state);
- }
-
- var startingState = this.state;
-
- // find the playlist object if the target playlist has been
- // specified by URI
- if (typeof playlist === 'string') {
- if (!this.master.playlists[playlist]) {
- throw new Error('Unknown playlist URI: ' + playlist);
- }
- playlist = this.master.playlists[playlist];
- }
-
- var mediaChange = !this.media_ || playlist.uri !== this.media_.uri;
-
- // switch to fully loaded playlists immediately
- if (this.master.playlists[playlist.uri].endList) {
- // abort outstanding playlist requests
- if (this.request) {
- this.request.onreadystatechange = null;
- this.request.abort();
- this.request = null;
- }
- this.state = 'HAVE_METADATA';
- this.media_ = playlist;
-
- // trigger media change if the active media has been updated
- if (mediaChange) {
- this.trigger('mediachanging');
- this.trigger('mediachange');
- }
- return;
- }
-
- // switching to the active playlist is a no-op
- if (!mediaChange) {
- return;
- }
-
- this.state = 'SWITCHING_MEDIA';
-
- // there is already an outstanding playlist request
- if (this.request) {
- if (resolveUrl(this.master.uri, playlist.uri) === this.request.url) {
- // requesting to switch to the same playlist multiple times
- // has no effect after the first
- return;
- }
- this.request.onreadystatechange = null;
- this.request.abort();
- this.request = null;
- }
-
- // request the new playlist
- if (this.media_) {
- this.trigger('mediachanging');
- }
-
- this.request = this.hls_.xhr({
- uri: resolveUrl(this.master.uri, playlist.uri),
- withCredentials: this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this3.request) {
- return;
- }
-
- if (error) {
- return _this3.playlistRequestError(_this3.request, playlist.uri, startingState);
- }
-
- _this3.haveMetadata(req, playlist.uri);
-
- // fire loadedmetadata the first time a media playlist is loaded
- if (startingState === 'HAVE_MASTER') {
- _this3.trigger('loadedmetadata');
- } else {
- _this3.trigger('mediachange');
- }
- });
- }
-
- /**
- * pause loading of the playlist
- */
-
- }, {
- key: 'pause',
- value: function pause() {
- this.stopRequest();
- window_1.clearTimeout(this.mediaUpdateTimeout);
- if (this.state === 'HAVE_NOTHING') {
- // If we pause the loader before any data has been retrieved, its as if we never
- // started, so reset to an unstarted state.
- this.started = false;
- }
- // Need to restore state now that no activity is happening
- if (this.state === 'SWITCHING_MEDIA') {
- // if the loader was in the process of switching media, it should either return to
- // HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media
- // playlist yet. This is determined by the existence of loader.media_
- if (this.media_) {
- this.state = 'HAVE_METADATA';
- } else {
- this.state = 'HAVE_MASTER';
- }
- } else if (this.state === 'HAVE_CURRENT_METADATA') {
- this.state = 'HAVE_METADATA';
- }
- }
-
- /**
- * start loading of the playlist
- */
-
- }, {
- key: 'load',
- value: function load(isFinalRendition) {
- var _this4 = this;
-
- window_1.clearTimeout(this.mediaUpdateTimeout);
-
- var media = this.media();
-
- if (isFinalRendition) {
- var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000;
-
- this.mediaUpdateTimeout = window_1.setTimeout(function () {
- return _this4.load();
- }, delay);
- return;
- }
-
- if (!this.started) {
- this.start();
- return;
- }
-
- if (media && !media.endList) {
- this.trigger('mediaupdatetimeout');
- } else {
- this.trigger('loadedplaylist');
- }
- }
-
- /**
- * start loading of the playlist
- */
-
- }, {
- key: 'start',
- value: function start() {
- var _this5 = this;
-
- this.started = true;
-
- // request the specified URL
- this.request = this.hls_.xhr({
- uri: this.srcUrl,
- withCredentials: this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this5.request) {
- return;
- }
-
- // clear the loader's request reference
- _this5.request = null;
-
- if (error) {
- _this5.error = {
- status: req.status,
- message: 'HLS playlist request error at URL: ' + _this5.srcUrl,
- responseText: req.responseText,
- // MEDIA_ERR_NETWORK
- code: 2
- };
- if (_this5.state === 'HAVE_NOTHING') {
- _this5.started = false;
- }
- return _this5.trigger('error');
- }
-
- var parser = new Parser();
-
- parser.push(req.responseText);
- parser.end();
-
- _this5.state = 'HAVE_MASTER';
-
- parser.manifest.uri = _this5.srcUrl;
-
- // loaded a master playlist
- if (parser.manifest.playlists) {
- _this5.master = parser.manifest;
-
- setupMediaPlaylists(_this5.master);
- resolveMediaGroupUris(_this5.master);
-
- _this5.trigger('loadedplaylist');
- if (!_this5.request) {
- // no media playlist was specifically selected so start
- // from the first listed one
- _this5.media(parser.manifest.playlists[0]);
- }
- return;
- }
-
- // loaded a media playlist
- // infer a master playlist if none was previously requested
- _this5.master = {
- mediaGroups: {
- 'AUDIO': {},
- 'VIDEO': {},
- 'CLOSED-CAPTIONS': {},
- 'SUBTITLES': {}
- },
- uri: window_1.location.href,
- playlists: [{
- uri: _this5.srcUrl,
- id: 0
- }]
- };
- _this5.master.playlists[_this5.srcUrl] = _this5.master.playlists[0];
- _this5.master.playlists[0].resolvedUri = _this5.srcUrl;
- // m3u8-parser does not attach an attributes property to media playlists so make
- // sure that the property is attached to avoid undefined reference errors
- _this5.master.playlists[0].attributes = _this5.master.playlists[0].attributes || {};
- _this5.haveMetadata(req, _this5.srcUrl);
- return _this5.trigger('loadedmetadata');
- });
- }
- }]);
- return PlaylistLoader;
- }(EventTarget);
-
- /**
- * @file playlist.js
- *
- * Playlist related utilities.
- */
-
- var createTimeRange = videojs.createTimeRange;
-
- /**
- * walk backward until we find a duration we can use
- * or return a failure
- *
- * @param {Playlist} playlist the playlist to walk through
- * @param {Number} endSequence the mediaSequence to stop walking on
- */
-
- var backwardDuration = function backwardDuration(playlist, endSequence) {
- var result = 0;
- var i = endSequence - playlist.mediaSequence;
- // if a start time is available for segment immediately following
- // the interval, use it
- var segment = playlist.segments[i];
-
- // Walk backward until we find the latest segment with timeline
- // information that is earlier than endSequence
- if (segment) {
- if (typeof segment.start !== 'undefined') {
- return { result: segment.start, precise: true };
- }
- if (typeof segment.end !== 'undefined') {
- return {
- result: segment.end - segment.duration,
- precise: true
- };
- }
- }
- while (i--) {
- segment = playlist.segments[i];
- if (typeof segment.end !== 'undefined') {
- return { result: result + segment.end, precise: true };
- }
-
- result += segment.duration;
-
- if (typeof segment.start !== 'undefined') {
- return { result: result + segment.start, precise: true };
- }
- }
- return { result: result, precise: false };
- };
-
- /**
- * walk forward until we find a duration we can use
- * or return a failure
- *
- * @param {Playlist} playlist the playlist to walk through
- * @param {Number} endSequence the mediaSequence to stop walking on
- */
- var forwardDuration = function forwardDuration(playlist, endSequence) {
- var result = 0;
- var segment = void 0;
- var i = endSequence - playlist.mediaSequence;
- // Walk forward until we find the earliest segment with timeline
- // information
-
- for (; i < playlist.segments.length; i++) {
- segment = playlist.segments[i];
- if (typeof segment.start !== 'undefined') {
- return {
- result: segment.start - result,
- precise: true
- };
- }
-
- result += segment.duration;
-
- if (typeof segment.end !== 'undefined') {
- return {
- result: segment.end - result,
- precise: true
- };
- }
- }
- // indicate we didn't find a useful duration estimate
- return { result: -1, precise: false };
- };
-
- /**
- * Calculate the media duration from the segments associated with a
- * playlist. The duration of a subinterval of the available segments
- * may be calculated by specifying an end index.
- *
- * @param {Object} playlist a media playlist object
- * @param {Number=} endSequence an exclusive upper boundary
- * for the playlist. Defaults to playlist length.
- * @param {Number} expired the amount of time that has dropped
- * off the front of the playlist in a live scenario
- * @return {Number} the duration between the first available segment
- * and end index.
- */
- var intervalDuration = function intervalDuration(playlist, endSequence, expired) {
- var backward = void 0;
- var forward = void 0;
-
- if (typeof endSequence === 'undefined') {
- endSequence = playlist.mediaSequence + playlist.segments.length;
- }
-
- if (endSequence < playlist.mediaSequence) {
- return 0;
- }
-
- // do a backward walk to estimate the duration
- backward = backwardDuration(playlist, endSequence);
- if (backward.precise) {
- // if we were able to base our duration estimate on timing
- // information provided directly from the Media Source, return
- // it
- return backward.result;
- }
-
- // walk forward to see if a precise duration estimate can be made
- // that way
- forward = forwardDuration(playlist, endSequence);
- if (forward.precise) {
- // we found a segment that has been buffered and so it's
- // position is known precisely
- return forward.result;
- }
-
- // return the less-precise, playlist-based duration estimate
- return backward.result + expired;
- };
-
- /**
- * Calculates the duration of a playlist. If a start and end index
- * are specified, the duration will be for the subset of the media
- * timeline between those two indices. The total duration for live
- * playlists is always Infinity.
- *
- * @param {Object} playlist a media playlist object
- * @param {Number=} endSequence an exclusive upper
- * boundary for the playlist. Defaults to the playlist media
- * sequence number plus its length.
- * @param {Number=} expired the amount of time that has
- * dropped off the front of the playlist in a live scenario
- * @return {Number} the duration between the start index and end
- * index.
- */
- var duration = function duration(playlist, endSequence, expired) {
- if (!playlist) {
- return 0;
- }
-
- if (typeof expired !== 'number') {
- expired = 0;
- }
-
- // if a slice of the total duration is not requested, use
- // playlist-level duration indicators when they're present
- if (typeof endSequence === 'undefined') {
- // if present, use the duration specified in the playlist
- if (playlist.totalDuration) {
- return playlist.totalDuration;
- }
-
- // duration should be Infinity for live playlists
- if (!playlist.endList) {
- return window_1.Infinity;
- }
- }
-
- // calculate the total duration based on the segment durations
- return intervalDuration(playlist, endSequence, expired);
- };
-
- /**
- * Calculate the time between two indexes in the current playlist
- * neight the start- nor the end-index need to be within the current
- * playlist in which case, the targetDuration of the playlist is used
- * to approximate the durations of the segments
- *
- * @param {Object} playlist a media playlist object
- * @param {Number} startIndex
- * @param {Number} endIndex
- * @return {Number} the number of seconds between startIndex and endIndex
- */
- var sumDurations = function sumDurations(playlist, startIndex, endIndex) {
- var durations = 0;
-
- if (startIndex > endIndex) {
- var _ref = [endIndex, startIndex];
- startIndex = _ref[0];
- endIndex = _ref[1];
- }
-
- if (startIndex < 0) {
- for (var i = startIndex; i < Math.min(0, endIndex); i++) {
- durations += playlist.targetDuration;
- }
- startIndex = 0;
- }
-
- for (var _i = startIndex; _i < endIndex; _i++) {
- durations += playlist.segments[_i].duration;
- }
-
- return durations;
- };
-
- /**
- * Determines the media index of the segment corresponding to the safe edge of the live
- * window which is the duration of the last segment plus 2 target durations from the end
- * of the playlist.
- *
- * @param {Object} playlist
- * a media playlist object
- * @return {Number}
- * The media index of the segment at the safe live point. 0 if there is no "safe"
- * point.
- * @function safeLiveIndex
- */
- var safeLiveIndex = function safeLiveIndex(playlist) {
- if (!playlist.segments.length) {
- return 0;
- }
-
- var i = playlist.segments.length - 1;
- var distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration;
- var safeDistance = distanceFromEnd + playlist.targetDuration * 2;
-
- while (i--) {
- distanceFromEnd += playlist.segments[i].duration;
-
- if (distanceFromEnd >= safeDistance) {
- break;
- }
- }
-
- return Math.max(0, i);
- };
-
- /**
- * Calculates the playlist end time
- *
- * @param {Object} playlist a media playlist object
- * @param {Number=} expired the amount of time that has
- * dropped off the front of the playlist in a live scenario
- * @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
- * playlist end calculation should consider the safe live end
- * (truncate the playlist end by three segments). This is normally
- * used for calculating the end of the playlist's seekable range.
- * @returns {Number} the end time of playlist
- * @function playlistEnd
- */
- var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd) {
- if (!playlist || !playlist.segments) {
- return null;
- }
- if (playlist.endList) {
- return duration(playlist);
- }
-
- if (expired === null) {
- return null;
- }
-
- expired = expired || 0;
-
- var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length;
-
- return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired);
- };
-
- /**
- * Calculates the interval of time that is currently seekable in a
- * playlist. The returned time ranges are relative to the earliest
- * moment in the specified playlist that is still available. A full
- * seekable implementation for live streams would need to offset
- * these values by the duration of content that has expired from the
- * stream.
- *
- * @param {Object} playlist a media playlist object
- * dropped off the front of the playlist in a live scenario
- * @param {Number=} expired the amount of time that has
- * dropped off the front of the playlist in a live scenario
- * @return {TimeRanges} the periods of time that are valid targets
- * for seeking
- */
- var seekable = function seekable(playlist, expired) {
- var useSafeLiveEnd = true;
- var seekableStart = expired || 0;
- var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd);
-
- if (seekableEnd === null) {
- return createTimeRange();
- }
- return createTimeRange(seekableStart, seekableEnd);
- };
-
- var isWholeNumber = function isWholeNumber(num) {
- return num - Math.floor(num) === 0;
- };
-
- var roundSignificantDigit = function roundSignificantDigit(increment, num) {
- // If we have a whole number, just add 1 to it
- if (isWholeNumber(num)) {
- return num + increment * 0.1;
- }
-
- var numDecimalDigits = num.toString().split('.')[1].length;
-
- for (var i = 1; i <= numDecimalDigits; i++) {
- var scale = Math.pow(10, i);
- var temp = num * scale;
-
- if (isWholeNumber(temp) || i === numDecimalDigits) {
- return (temp + increment) / scale;
- }
- }
- };
-
- var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1);
- var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1);
-
- /**
- * Determine the index and estimated starting time of the segment that
- * contains a specified playback position in a media playlist.
- *
- * @param {Object} playlist the media playlist to query
- * @param {Number} currentTime The number of seconds since the earliest
- * possible position to determine the containing segment for
- * @param {Number} startIndex
- * @param {Number} startTime
- * @return {Object}
- */
- var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) {
- var i = void 0;
- var segment = void 0;
- var numSegments = playlist.segments.length;
-
- var time = currentTime - startTime;
-
- if (time < 0) {
- // Walk backward from startIndex in the playlist, adding durations
- // until we find a segment that contains `time` and return it
- if (startIndex > 0) {
- for (i = startIndex - 1; i >= 0; i--) {
- segment = playlist.segments[i];
- time += floorLeastSignificantDigit(segment.duration);
- if (time > 0) {
- return {
- mediaIndex: i,
- startTime: startTime - sumDurations(playlist, startIndex, i)
- };
- }
- }
- }
- // We were unable to find a good segment within the playlist
- // so select the first segment
- return {
- mediaIndex: 0,
- startTime: currentTime
- };
- }
-
- // When startIndex is negative, we first walk forward to first segment
- // adding target durations. If we "run out of time" before getting to
- // the first segment, return the first segment
- if (startIndex < 0) {
- for (i = startIndex; i < 0; i++) {
- time -= playlist.targetDuration;
- if (time < 0) {
- return {
- mediaIndex: 0,
- startTime: currentTime
- };
- }
- }
- startIndex = 0;
- }
-
- // Walk forward from startIndex in the playlist, subtracting durations
- // until we find a segment that contains `time` and return it
- for (i = startIndex; i < numSegments; i++) {
- segment = playlist.segments[i];
- time -= ceilLeastSignificantDigit(segment.duration);
- if (time < 0) {
- return {
- mediaIndex: i,
- startTime: startTime + sumDurations(playlist, startIndex, i)
- };
- }
- }
-
- // We are out of possible candidates so load the last one...
- return {
- mediaIndex: numSegments - 1,
- startTime: currentTime
- };
- };
-
- /**
- * Check whether the playlist is blacklisted or not.
- *
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist is blacklisted or not
- * @function isBlacklisted
- */
- var isBlacklisted = function isBlacklisted(playlist) {
- return playlist.excludeUntil && playlist.excludeUntil > Date.now();
- };
-
- /**
- * Check whether the playlist is compatible with current playback configuration or has
- * been blacklisted permanently for being incompatible.
- *
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist is incompatible or not
- * @function isIncompatible
- */
- var isIncompatible = function isIncompatible(playlist) {
- return playlist.excludeUntil && playlist.excludeUntil === Infinity;
- };
-
- /**
- * Check whether the playlist is enabled or not.
- *
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist is enabled or not
- * @function isEnabled
- */
- var isEnabled = function isEnabled(playlist) {
- var blacklisted = isBlacklisted(playlist);
-
- return !playlist.disabled && !blacklisted;
- };
-
- /**
- * Check whether the playlist has been manually disabled through the representations api.
- *
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist is disabled manually or not
- * @function isDisabled
- */
- var isDisabled = function isDisabled(playlist) {
- return playlist.disabled;
- };
-
- /**
- * Returns whether the current playlist is an AES encrypted HLS stream
- *
- * @return {Boolean} true if it's an AES encrypted HLS stream
- */
- var isAes = function isAes(media) {
- for (var i = 0; i < media.segments.length; i++) {
- if (media.segments[i].key) {
- return true;
- }
- }
- return false;
- };
-
- /**
- * Returns whether the current playlist contains fMP4
- *
- * @return {Boolean} true if the playlist contains fMP4
- */
- var isFmp4 = function isFmp4(media) {
- for (var i = 0; i < media.segments.length; i++) {
- if (media.segments[i].map) {
- return true;
- }
- }
- return false;
- };
-
- /**
- * Checks if the playlist has a value for the specified attribute
- *
- * @param {String} attr
- * Attribute to check for
- * @param {Object} playlist
- * The media playlist object
- * @return {Boolean}
- * Whether the playlist contains a value for the attribute or not
- * @function hasAttribute
- */
- var hasAttribute = function hasAttribute(attr, playlist) {
- return playlist.attributes && playlist.attributes[attr];
- };
-
- /**
- * Estimates the time required to complete a segment download from the specified playlist
- *
- * @param {Number} segmentDuration
- * Duration of requested segment
- * @param {Number} bandwidth
- * Current measured bandwidth of the player
- * @param {Object} playlist
- * The media playlist object
- * @param {Number=} bytesReceived
- * Number of bytes already received for the request. Defaults to 0
- * @return {Number|NaN}
- * The estimated time to request the segment. NaN if bandwidth information for
- * the given playlist is unavailable
- * @function estimateSegmentRequestTime
- */
- var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) {
- var bytesReceived = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
-
- if (!hasAttribute('BANDWIDTH', playlist)) {
- return NaN;
- }
-
- var size = segmentDuration * playlist.attributes.BANDWIDTH;
-
- return (size - bytesReceived * 8) / bandwidth;
- };
-
- /*
- * Returns whether the current playlist is the lowest rendition
- *
- * @return {Boolean} true if on lowest rendition
- */
- var isLowestEnabledRendition = function isLowestEnabledRendition(master, media) {
- if (master.playlists.length === 1) {
- return true;
- }
-
- var currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE;
-
- return master.playlists.filter(function (playlist) {
- if (!isEnabled(playlist)) {
- return false;
- }
-
- return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth;
- }).length === 0;
- };
-
- // exports
- var Playlist = {
- duration: duration,
- seekable: seekable,
- safeLiveIndex: safeLiveIndex,
- getMediaInfoForTime: getMediaInfoForTime,
- isEnabled: isEnabled,
- isDisabled: isDisabled,
- isBlacklisted: isBlacklisted,
- isIncompatible: isIncompatible,
- playlistEnd: playlistEnd,
- isAes: isAes,
- isFmp4: isFmp4,
- hasAttribute: hasAttribute,
- estimateSegmentRequestTime: estimateSegmentRequestTime,
- isLowestEnabledRendition: isLowestEnabledRendition
- };
-
- /**
- * @file xhr.js
- */
-
- var videojsXHR = videojs.xhr,
- mergeOptions$1 = videojs.mergeOptions;
-
-
- var xhrFactory = function xhrFactory() {
- var xhr = function XhrFunction(options, callback) {
- // Add a default timeout for all hls requests
- options = mergeOptions$1({
- timeout: 45e3
- }, options);
-
- // Allow an optional user-specified function to modify the option
- // object before we construct the xhr request
- var beforeRequest = XhrFunction.beforeRequest || videojs.Hls.xhr.beforeRequest;
-
- if (beforeRequest && typeof beforeRequest === 'function') {
- var newOptions = beforeRequest(options);
-
- if (newOptions) {
- options = newOptions;
- }
- }
-
- var request = videojsXHR(options, function (error, response) {
- var reqResponse = request.response;
-
- if (!error && reqResponse) {
- request.responseTime = Date.now();
- request.roundTripTime = request.responseTime - request.requestTime;
- request.bytesReceived = reqResponse.byteLength || reqResponse.length;
- if (!request.bandwidth) {
- request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000);
- }
- }
-
- if (response.headers) {
- request.responseHeaders = response.headers;
- }
-
- // videojs.xhr now uses a specific code on the error
- // object to signal that a request has timed out instead
- // of setting a boolean on the request object
- if (error && error.code === 'ETIMEDOUT') {
- request.timedout = true;
- }
-
- // videojs.xhr no longer considers status codes outside of 200 and 0
- // (for file uris) to be errors, but the old XHR did, so emulate that
- // behavior. Status 206 may be used in response to byterange requests.
- if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) {
- error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText)));
- }
-
- callback(error, request);
- });
- var originalAbort = request.abort;
-
- request.abort = function () {
- request.aborted = true;
- return originalAbort.apply(request, arguments);
- };
- request.uri = options.uri;
- request.requestTime = Date.now();
- return request;
- };
-
- return xhr;
- };
-
- /*
- * pkcs7.pad
- * https://github.com/brightcove/pkcs7
- *
- * Copyright (c) 2014 Brightcove
- * Licensed under the apache2 license.
- */
-
- /**
- * Returns the subarray of a Uint8Array without PKCS#7 padding.
- * @param padded {Uint8Array} unencrypted bytes that have been padded
- * @return {Uint8Array} the unpadded bytes
- * @see http://tools.ietf.org/html/rfc5652
- */
- function unpad(padded) {
- return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]);
- }
-
- var classCallCheck$2 = function classCallCheck(instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- var createClass$1 = function () {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
- }
- }
-
- return function (Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- };
- }();
-
- var inherits$2 = function inherits(subClass, superClass) {
- if (typeof superClass !== "function" && superClass !== null) {
- throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
- }
-
- subClass.prototype = Object.create(superClass && superClass.prototype, {
- constructor: {
- value: subClass,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
- };
-
- var possibleConstructorReturn$2 = function possibleConstructorReturn(self, call) {
- if (!self) {
- throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
- }
-
- return call && (typeof call === "object" || typeof call === "function") ? call : self;
- };
-
- /**
- * @file aes.js
- *
- * This file contains an adaptation of the AES decryption algorithm
- * from the Standford Javascript Cryptography Library. That work is
- * covered by the following copyright and permissions notice:
- *
- * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
- * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
- * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
- * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * The views and conclusions contained in the software and documentation
- * are those of the authors and should not be interpreted as representing
- * official policies, either expressed or implied, of the authors.
- */
-
- /**
- * Expand the S-box tables.
- *
- * @private
- */
- var precompute = function precompute() {
- var tables = [[[], [], [], [], []], [[], [], [], [], []]];
- var encTable = tables[0];
- var decTable = tables[1];
- var sbox = encTable[4];
- var sboxInv = decTable[4];
- var i = void 0;
- var x = void 0;
- var xInv = void 0;
- var d = [];
- var th = [];
- var x2 = void 0;
- var x4 = void 0;
- var x8 = void 0;
- var s = void 0;
- var tEnc = void 0;
- var tDec = void 0;
-
- // Compute double and third tables
- for (i = 0; i < 256; i++) {
- th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i;
- }
-
- for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
- // Compute sbox
- s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4;
- s = s >> 8 ^ s & 255 ^ 99;
- sbox[x] = s;
- sboxInv[s] = x;
-
- // Compute MixColumns
- x8 = d[x4 = d[x2 = d[x]]];
- tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100;
- tEnc = d[s] * 0x101 ^ s * 0x1010100;
-
- for (i = 0; i < 4; i++) {
- encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8;
- decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8;
- }
- }
-
- // Compactify. Considerable speedup on Firefox.
- for (i = 0; i < 5; i++) {
- encTable[i] = encTable[i].slice(0);
- decTable[i] = decTable[i].slice(0);
- }
- return tables;
- };
- var aesTables = null;
-
- /**
- * Schedule out an AES key for both encryption and decryption. This
- * is a low-level class. Use a cipher mode to do bulk encryption.
- *
- * @class AES
- * @param key {Array} The key as an array of 4, 6 or 8 words.
- */
-
- var AES = function () {
- function AES(key) {
- classCallCheck$2(this, AES);
-
- /**
- * The expanded S-box and inverse S-box tables. These will be computed
- * on the client so that we don't have to send them down the wire.
- *
- * There are two tables, _tables[0] is for encryption and
- * _tables[1] is for decryption.
- *
- * The first 4 sub-tables are the expanded S-box with MixColumns. The
- * last (_tables[01][4]) is the S-box itself.
- *
- * @private
- */
- // if we have yet to precompute the S-box tables
- // do so now
- if (!aesTables) {
- aesTables = precompute();
- }
- // then make a copy of that object for use
- this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]];
- var i = void 0;
- var j = void 0;
- var tmp = void 0;
- var encKey = void 0;
- var decKey = void 0;
- var sbox = this._tables[0][4];
- var decTable = this._tables[1];
- var keyLen = key.length;
- var rcon = 1;
-
- if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
- throw new Error('Invalid aes key size');
- }
-
- encKey = key.slice(0);
- decKey = [];
- this._key = [encKey, decKey];
-
- // schedule encryption keys
- for (i = keyLen; i < 4 * keyLen + 28; i++) {
- tmp = encKey[i - 1];
-
- // apply sbox
- if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) {
- tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255];
-
- // shift rows and add rcon
- if (i % keyLen === 0) {
- tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24;
- rcon = rcon << 1 ^ (rcon >> 7) * 283;
- }
- }
-
- encKey[i] = encKey[i - keyLen] ^ tmp;
- }
-
- // schedule decryption keys
- for (j = 0; i; j++, i--) {
- tmp = encKey[j & 3 ? i : i - 4];
- if (i <= 4 || j < 4) {
- decKey[j] = tmp;
- } else {
- decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]];
- }
- }
- }
-
- /**
- * Decrypt 16 bytes, specified as four 32-bit words.
- *
- * @param {Number} encrypted0 the first word to decrypt
- * @param {Number} encrypted1 the second word to decrypt
- * @param {Number} encrypted2 the third word to decrypt
- * @param {Number} encrypted3 the fourth word to decrypt
- * @param {Int32Array} out the array to write the decrypted words
- * into
- * @param {Number} offset the offset into the output array to start
- * writing results
- * @return {Array} The plaintext.
- */
-
- AES.prototype.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) {
- var key = this._key[1];
- // state variables a,b,c,d are loaded with pre-whitened data
- var a = encrypted0 ^ key[0];
- var b = encrypted3 ^ key[1];
- var c = encrypted2 ^ key[2];
- var d = encrypted1 ^ key[3];
- var a2 = void 0;
- var b2 = void 0;
- var c2 = void 0;
-
- // key.length === 2 ?
- var nInnerRounds = key.length / 4 - 2;
- var i = void 0;
- var kIndex = 4;
- var table = this._tables[1];
-
- // load up the tables
- var table0 = table[0];
- var table1 = table[1];
- var table2 = table[2];
- var table3 = table[3];
- var sbox = table[4];
-
- // Inner rounds. Cribbed from OpenSSL.
- for (i = 0; i < nInnerRounds; i++) {
- a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex];
- b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1];
- c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2];
- d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3];
- kIndex += 4;
- a = a2;b = b2;c = c2;
- }
-
- // Last round.
- for (i = 0; i < 4; i++) {
- out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++];
- a2 = a;a = b;b = c;c = d;d = a2;
- }
- };
-
- return AES;
- }();
-
- /**
- * @file stream.js
- */
- /**
- * A lightweight readable stream implemention that handles event dispatching.
- *
- * @class Stream
- */
- var Stream$1 = function () {
- function Stream() {
- classCallCheck$2(this, Stream);
-
- this.listeners = {};
- }
-
- /**
- * Add a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener the callback to be invoked when an event of
- * the specified type occurs
- */
-
- Stream.prototype.on = function on(type, listener) {
- if (!this.listeners[type]) {
- this.listeners[type] = [];
- }
- this.listeners[type].push(listener);
- };
-
- /**
- * Remove a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener a function previously registered for this
- * type of event through `on`
- * @return {Boolean} if we could turn it off or not
- */
-
- Stream.prototype.off = function off(type, listener) {
- if (!this.listeners[type]) {
- return false;
- }
-
- var index = this.listeners[type].indexOf(listener);
-
- this.listeners[type].splice(index, 1);
- return index > -1;
- };
-
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- *
- * @param {String} type the event name
- */
-
- Stream.prototype.trigger = function trigger(type) {
- var callbacks = this.listeners[type];
-
- if (!callbacks) {
- return;
- }
-
- // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
- if (arguments.length === 2) {
- var length = callbacks.length;
-
- for (var i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- var args = Array.prototype.slice.call(arguments, 1);
- var _length = callbacks.length;
-
- for (var _i = 0; _i < _length; ++_i) {
- callbacks[_i].apply(this, args);
- }
- }
- };
-
- /**
- * Destroys the stream and cleans up.
- */
-
- Stream.prototype.dispose = function dispose() {
- this.listeners = {};
- };
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- *
- * @param {Stream} destination the stream that will receive all `data` events
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */
-
- Stream.prototype.pipe = function pipe(destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
- };
-
- return Stream;
- }();
-
- /**
- * @file async-stream.js
- */
- /**
- * A wrapper around the Stream class to use setTiemout
- * and run stream "jobs" Asynchronously
- *
- * @class AsyncStream
- * @extends Stream
- */
-
- var AsyncStream = function (_Stream) {
- inherits$2(AsyncStream, _Stream);
-
- function AsyncStream() {
- classCallCheck$2(this, AsyncStream);
-
- var _this = possibleConstructorReturn$2(this, _Stream.call(this, Stream$1));
-
- _this.jobs = [];
- _this.delay = 1;
- _this.timeout_ = null;
- return _this;
- }
-
- /**
- * process an async job
- *
- * @private
- */
-
- AsyncStream.prototype.processJob_ = function processJob_() {
- this.jobs.shift()();
- if (this.jobs.length) {
- this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
- } else {
- this.timeout_ = null;
- }
- };
-
- /**
- * push a job into the stream
- *
- * @param {Function} job the job to push into the stream
- */
-
- AsyncStream.prototype.push = function push(job) {
- this.jobs.push(job);
- if (!this.timeout_) {
- this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
- }
- };
-
- return AsyncStream;
- }(Stream$1);
-
- /**
- * @file decrypter.js
- *
- * An asynchronous implementation of AES-128 CBC decryption with
- * PKCS#7 padding.
- */
-
- /**
- * Convert network-order (big-endian) bytes into their little-endian
- * representation.
- */
- var ntoh = function ntoh(word) {
- return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24;
- };
-
- /**
- * Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
- *
- * @param {Uint8Array} encrypted the encrypted bytes
- * @param {Uint32Array} key the bytes of the decryption key
- * @param {Uint32Array} initVector the initialization vector (IV) to
- * use for the first round of CBC.
- * @return {Uint8Array} the decrypted bytes
- *
- * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
- * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
- * @see https://tools.ietf.org/html/rfc2315
- */
- var decrypt = function decrypt(encrypted, key, initVector) {
- // word-level access to the encrypted bytes
- var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2);
-
- var decipher = new AES(Array.prototype.slice.call(key));
-
- // byte and word-level access for the decrypted output
- var decrypted = new Uint8Array(encrypted.byteLength);
- var decrypted32 = new Int32Array(decrypted.buffer);
-
- // temporary variables for working with the IV, encrypted, and
- // decrypted data
- var init0 = void 0;
- var init1 = void 0;
- var init2 = void 0;
- var init3 = void 0;
- var encrypted0 = void 0;
- var encrypted1 = void 0;
- var encrypted2 = void 0;
- var encrypted3 = void 0;
-
- // iteration variable
- var wordIx = void 0;
-
- // pull out the words of the IV to ensure we don't modify the
- // passed-in reference and easier access
- init0 = initVector[0];
- init1 = initVector[1];
- init2 = initVector[2];
- init3 = initVector[3];
-
- // decrypt four word sequences, applying cipher-block chaining (CBC)
- // to each decrypted block
- for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) {
- // convert big-endian (network order) words into little-endian
- // (javascript order)
- encrypted0 = ntoh(encrypted32[wordIx]);
- encrypted1 = ntoh(encrypted32[wordIx + 1]);
- encrypted2 = ntoh(encrypted32[wordIx + 2]);
- encrypted3 = ntoh(encrypted32[wordIx + 3]);
-
- // decrypt the block
- decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx);
-
- // XOR with the IV, and restore network byte-order to obtain the
- // plaintext
- decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0);
- decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1);
- decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2);
- decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3);
-
- // setup the IV for the next round
- init0 = encrypted0;
- init1 = encrypted1;
- init2 = encrypted2;
- init3 = encrypted3;
- }
-
- return decrypted;
- };
-
- /**
- * The `Decrypter` class that manages decryption of AES
- * data through `AsyncStream` objects and the `decrypt`
- * function
- *
- * @param {Uint8Array} encrypted the encrypted bytes
- * @param {Uint32Array} key the bytes of the decryption key
- * @param {Uint32Array} initVector the initialization vector (IV) to
- * @param {Function} done the function to run when done
- * @class Decrypter
- */
-
- var Decrypter = function () {
- function Decrypter(encrypted, key, initVector, done) {
- classCallCheck$2(this, Decrypter);
-
- var step = Decrypter.STEP;
- var encrypted32 = new Int32Array(encrypted.buffer);
- var decrypted = new Uint8Array(encrypted.byteLength);
- var i = 0;
-
- this.asyncStream_ = new AsyncStream();
-
- // split up the encryption job and do the individual chunks asynchronously
- this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
- for (i = step; i < encrypted32.length; i += step) {
- initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]);
- this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
- }
- // invoke the done() callback when everything is finished
- this.asyncStream_.push(function () {
- // remove pkcs#7 padding from the decrypted bytes
- done(null, unpad(decrypted));
- });
- }
-
- /**
- * a getter for step the maximum number of bytes to process at one time
- *
- * @return {Number} the value of step 32000
- */
-
- /**
- * @private
- */
- Decrypter.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) {
- return function () {
- var bytes = decrypt(encrypted, key, initVector);
-
- decrypted.set(bytes, encrypted.byteOffset);
- };
- };
-
- createClass$1(Decrypter, null, [{
- key: 'STEP',
- get: function get$$1() {
- // 4 * 8000;
- return 32000;
- }
- }]);
- return Decrypter;
- }();
-
- /**
- * @file bin-utils.js
- */
-
- /**
- * convert a TimeRange to text
- *
- * @param {TimeRange} range the timerange to use for conversion
- * @param {Number} i the iterator on the range to convert
- */
- var textRange = function textRange(range, i) {
- return range.start(i) + '-' + range.end(i);
- };
-
- /**
- * format a number as hex string
- *
- * @param {Number} e The number
- * @param {Number} i the iterator
- */
- var formatHexString = function formatHexString(e, i) {
- var value = e.toString(16);
-
- return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
- };
- var formatAsciiString = function formatAsciiString(e) {
- if (e >= 0x20 && e < 0x7e) {
- return String.fromCharCode(e);
- }
- return '.';
- };
-
- /**
- * Creates an object for sending to a web worker modifying properties that are TypedArrays
- * into a new object with seperated properties for the buffer, byteOffset, and byteLength.
- *
- * @param {Object} message
- * Object of properties and values to send to the web worker
- * @return {Object}
- * Modified message with TypedArray values expanded
- * @function createTransferableMessage
- */
- var createTransferableMessage = function createTransferableMessage(message) {
- var transferable = {};
-
- Object.keys(message).forEach(function (key) {
- var value = message[key];
-
- if (ArrayBuffer.isView(value)) {
- transferable[key] = {
- bytes: value.buffer,
- byteOffset: value.byteOffset,
- byteLength: value.byteLength
- };
- } else {
- transferable[key] = value;
- }
- });
-
- return transferable;
- };
-
- /**
- * Returns a unique string identifier for a media initialization
- * segment.
- */
- var initSegmentId = function initSegmentId(initSegment) {
- var byterange = initSegment.byterange || {
- length: Infinity,
- offset: 0
- };
-
- return [byterange.length, byterange.offset, initSegment.resolvedUri].join(',');
- };
-
- /**
- * utils to help dump binary data to the console
- */
- var hexDump = function hexDump(data) {
- var bytes = Array.prototype.slice.call(data);
- var step = 16;
- var result = '';
- var hex = void 0;
- var ascii = void 0;
-
- for (var j = 0; j < bytes.length / step; j++) {
- hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
- ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
- result += hex + ' ' + ascii + '\n';
- }
-
- return result;
- };
-
- var tagDump = function tagDump(_ref) {
- var bytes = _ref.bytes;
- return hexDump(bytes);
- };
-
- var textRanges = function textRanges(ranges) {
- var result = '';
- var i = void 0;
-
- for (i = 0; i < ranges.length; i++) {
- result += textRange(ranges, i) + ' ';
- }
- return result;
- };
-
- var utils = /*#__PURE__*/Object.freeze({
- createTransferableMessage: createTransferableMessage,
- initSegmentId: initSegmentId,
- hexDump: hexDump,
- tagDump: tagDump,
- textRanges: textRanges
- });
-
- /**
- * ranges
- *
- * Utilities for working with TimeRanges.
- *
- */
-
- // Fudge factor to account for TimeRanges rounding
- var TIME_FUDGE_FACTOR = 1 / 30;
- // Comparisons between time values such as current time and the end of the buffered range
- // can be misleading because of precision differences or when the current media has poorly
- // aligned audio and video, which can cause values to be slightly off from what you would
- // expect. This value is what we consider to be safe to use in such comparisons to account
- // for these scenarios.
- var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
- var filterRanges = function filterRanges(timeRanges, predicate) {
- var results = [];
- var i = void 0;
-
- if (timeRanges && timeRanges.length) {
- // Search for ranges that match the predicate
- for (i = 0; i < timeRanges.length; i++) {
- if (predicate(timeRanges.start(i), timeRanges.end(i))) {
- results.push([timeRanges.start(i), timeRanges.end(i)]);
- }
- }
- }
-
- return videojs.createTimeRanges(results);
- };
-
- /**
- * Attempts to find the buffered TimeRange that contains the specified
- * time.
- * @param {TimeRanges} buffered - the TimeRanges object to query
- * @param {number} time - the time to filter on.
- * @returns {TimeRanges} a new TimeRanges object
- */
- var findRange = function findRange(buffered, time) {
- return filterRanges(buffered, function (start, end) {
- return start - TIME_FUDGE_FACTOR <= time && end + TIME_FUDGE_FACTOR >= time;
- });
- };
-
- /**
- * Returns the TimeRanges that begin later than the specified time.
- * @param {TimeRanges} timeRanges - the TimeRanges object to query
- * @param {number} time - the time to filter on.
- * @returns {TimeRanges} a new TimeRanges object.
- */
- var findNextRange = function findNextRange(timeRanges, time) {
- return filterRanges(timeRanges, function (start) {
- return start - TIME_FUDGE_FACTOR >= time;
- });
- };
-
- /**
- * Returns gaps within a list of TimeRanges
- * @param {TimeRanges} buffered - the TimeRanges object
- * @return {TimeRanges} a TimeRanges object of gaps
- */
- var findGaps = function findGaps(buffered) {
- if (buffered.length < 2) {
- return videojs.createTimeRanges();
- }
-
- var ranges = [];
-
- for (var i = 1; i < buffered.length; i++) {
- var start = buffered.end(i - 1);
- var end = buffered.start(i);
-
- ranges.push([start, end]);
- }
-
- return videojs.createTimeRanges(ranges);
- };
-
- /**
- * Gets a human readable string for a TimeRange
- *
- * @param {TimeRange} range
- * @returns {String} a human readable string
- */
- var printableRange = function printableRange(range) {
- var strArr = [];
-
- if (!range || !range.length) {
- return '';
- }
-
- for (var i = 0; i < range.length; i++) {
- strArr.push(range.start(i) + ' => ' + range.end(i));
- }
-
- return strArr.join(', ');
- };
-
- /**
- * Calculates the amount of time left in seconds until the player hits the end of the
- * buffer and causes a rebuffer
- *
- * @param {TimeRange} buffered
- * The state of the buffer
- * @param {Numnber} currentTime
- * The current time of the player
- * @param {Number} playbackRate
- * The current playback rate of the player. Defaults to 1.
- * @return {Number}
- * Time until the player has to start rebuffering in seconds.
- * @function timeUntilRebuffer
- */
- var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) {
- var playbackRate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
-
- var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
-
- return (bufferedEnd - currentTime) / playbackRate;
- };
-
- /**
- * Converts a TimeRanges object into an array representation
- * @param {TimeRanges} timeRanges
- * @returns {Array}
- */
- var timeRangesToArray = function timeRangesToArray(timeRanges) {
- var timeRangesList = [];
-
- for (var i = 0; i < timeRanges.length; i++) {
- timeRangesList.push({
- start: timeRanges.start(i),
- end: timeRanges.end(i)
- });
- }
-
- return timeRangesList;
- };
-
- /**
- * @file create-text-tracks-if-necessary.js
- */
-
- /**
- * Create text tracks on video.js if they exist on a segment.
- *
- * @param {Object} sourceBuffer the VSB or FSB
- * @param {Object} mediaSource the HTML media source
- * @param {Object} segment the segment that may contain the text track
- * @private
- */
- var createTextTracksIfNecessary = function createTextTracksIfNecessary(sourceBuffer, mediaSource, segment) {
- var player = mediaSource.player_;
-
- // create an in-band caption track if one is present in the segment
- if (segment.captions && segment.captions.length) {
- if (!sourceBuffer.inbandTextTracks_) {
- sourceBuffer.inbandTextTracks_ = {};
- }
-
- for (var trackId in segment.captionStreams) {
- if (!sourceBuffer.inbandTextTracks_[trackId]) {
- player.tech_.trigger({ type: 'usage', name: 'hls-608' });
- var track = player.textTracks().getTrackById(trackId);
-
- if (track) {
- // Resuse an existing track with a CC# id because this was
- // very likely created by videojs-contrib-hls from information
- // in the m3u8 for us to use
- sourceBuffer.inbandTextTracks_[trackId] = track;
- } else {
- // Otherwise, create a track with the default `CC#` label and
- // without a language
- sourceBuffer.inbandTextTracks_[trackId] = player.addRemoteTextTrack({
- kind: 'captions',
- id: trackId,
- label: trackId
- }, false).track;
- }
- }
- }
- }
-
- if (segment.metadata && segment.metadata.length && !sourceBuffer.metadataTrack_) {
- sourceBuffer.metadataTrack_ = player.addRemoteTextTrack({
- kind: 'metadata',
- label: 'Timed Metadata'
- }, false).track;
- sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType = segment.metadata.dispatchType;
- }
- };
-
- /**
- * @file remove-cues-from-track.js
- */
-
- /**
- * Remove cues from a track on video.js.
- *
- * @param {Double} start start of where we should remove the cue
- * @param {Double} end end of where the we should remove the cue
- * @param {Object} track the text track to remove the cues from
- * @private
- */
- var removeCuesFromTrack = function removeCuesFromTrack(start, end, track) {
- var i = void 0;
- var cue = void 0;
-
- if (!track) {
- return;
- }
-
- if (!track.cues) {
- return;
- }
-
- i = track.cues.length;
-
- while (i--) {
- cue = track.cues[i];
-
- // Remove any overlapping cue
- if (cue.startTime <= end && cue.endTime >= start) {
- track.removeCue(cue);
- }
- }
- };
-
- /**
- * @file add-text-track-data.js
- */
- /**
- * Define properties on a cue for backwards compatability,
- * but warn the user that the way that they are using it
- * is depricated and will be removed at a later date.
- *
- * @param {Cue} cue the cue to add the properties on
- * @private
- */
- var deprecateOldCue = function deprecateOldCue(cue) {
- Object.defineProperties(cue.frame, {
- id: {
- get: function get() {
- videojs.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.');
- return cue.value.key;
- }
- },
- value: {
- get: function get() {
- videojs.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.');
- return cue.value.data;
- }
- },
- privateData: {
- get: function get() {
- videojs.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.');
- return cue.value.data;
- }
- }
- });
- };
-
- var durationOfVideo = function durationOfVideo(duration) {
- var dur = void 0;
-
- if (isNaN(duration) || Math.abs(duration) === Infinity) {
- dur = Number.MAX_VALUE;
- } else {
- dur = duration;
- }
- return dur;
- };
- /**
- * Add text track data to a source handler given the captions and
- * metadata from the buffer.
- *
- * @param {Object} sourceHandler the virtual source buffer
- * @param {Array} captionArray an array of caption data
- * @param {Array} metadataArray an array of meta data
- * @private
- */
- var addTextTrackData = function addTextTrackData(sourceHandler, captionArray, metadataArray) {
- var Cue = window_1.WebKitDataCue || window_1.VTTCue;
-
- if (captionArray) {
- captionArray.forEach(function (caption) {
- var track = caption.stream;
-
- this.inbandTextTracks_[track].addCue(new Cue(caption.startTime + this.timestampOffset, caption.endTime + this.timestampOffset, caption.text));
- }, sourceHandler);
- }
-
- if (metadataArray) {
- var videoDuration = durationOfVideo(sourceHandler.mediaSource_.duration);
-
- metadataArray.forEach(function (metadata) {
- var time = metadata.cueTime + this.timestampOffset;
-
- metadata.frames.forEach(function (frame) {
- var cue = new Cue(time, time, frame.value || frame.url || frame.data || '');
-
- cue.frame = frame;
- cue.value = frame;
- deprecateOldCue(cue);
-
- this.metadataTrack_.addCue(cue);
- }, this);
- }, sourceHandler);
-
- // Updating the metadeta cues so that
- // the endTime of each cue is the startTime of the next cue
- // the endTime of last cue is the duration of the video
- if (sourceHandler.metadataTrack_ && sourceHandler.metadataTrack_.cues && sourceHandler.metadataTrack_.cues.length) {
- var cues = sourceHandler.metadataTrack_.cues;
- var cuesArray = [];
-
- // Create a copy of the TextTrackCueList...
- // ...disregarding cues with a falsey value
- for (var i = 0; i < cues.length; i++) {
- if (cues[i]) {
- cuesArray.push(cues[i]);
- }
- }
-
- // Group cues by their startTime value
- var cuesGroupedByStartTime = cuesArray.reduce(function (obj, cue) {
- var timeSlot = obj[cue.startTime] || [];
-
- timeSlot.push(cue);
- obj[cue.startTime] = timeSlot;
-
- return obj;
- }, {});
-
- // Sort startTimes by ascending order
- var sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort(function (a, b) {
- return Number(a) - Number(b);
- });
-
- // Map each cue group's endTime to the next group's startTime
- sortedStartTimes.forEach(function (startTime, idx) {
- var cueGroup = cuesGroupedByStartTime[startTime];
- var nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration;
-
- // Map each cue's endTime the next group's startTime
- cueGroup.forEach(function (cue) {
- cue.endTime = nextTime;
- });
- });
- }
- }
- };
-
- var win$1 = typeof window !== 'undefined' ? window : {},
- TARGET = typeof Symbol === 'undefined' ? '__target' : Symbol(),
- SCRIPT_TYPE = 'application/javascript',
- BlobBuilder = win$1.BlobBuilder || win$1.WebKitBlobBuilder || win$1.MozBlobBuilder || win$1.MSBlobBuilder,
- URL = win$1.URL || win$1.webkitURL || URL && URL.msURL,
- Worker = win$1.Worker;
-
- /**
- * Returns a wrapper around Web Worker code that is constructible.
- *
- * @function shimWorker
- *
- * @param { String } filename The name of the file
- * @param { Function } fn Function wrapping the code of the worker
- */
- function shimWorker(filename, fn) {
- return function ShimWorker(forceFallback) {
- var o = this;
-
- if (!fn) {
- return new Worker(filename);
- } else if (Worker && !forceFallback) {
- // Convert the function's inner code to a string to construct the worker
- var source = fn.toString().replace(/^function.+?{/, '').slice(0, -1),
- objURL = createSourceObject(source);
-
- this[TARGET] = new Worker(objURL);
- wrapTerminate(this[TARGET], objURL);
- return this[TARGET];
- } else {
- var selfShim = {
- postMessage: function postMessage(m) {
- if (o.onmessage) {
- setTimeout(function () {
- o.onmessage({ data: m, target: selfShim });
- });
- }
- }
- };
-
- fn.call(selfShim);
- this.postMessage = function (m) {
- setTimeout(function () {
- selfShim.onmessage({ data: m, target: o });
- });
- };
- this.isThisThread = true;
- }
- };
- }
- // Test Worker capabilities
- if (Worker) {
- var testWorker,
- objURL = createSourceObject('self.onmessage = function () {}'),
- testArray = new Uint8Array(1);
-
- try {
- testWorker = new Worker(objURL);
-
- // Native browser on some Samsung devices throws for transferables, let's detect it
- testWorker.postMessage(testArray, [testArray.buffer]);
- } catch (e) {
- Worker = null;
- } finally {
- URL.revokeObjectURL(objURL);
- if (testWorker) {
- testWorker.terminate();
- }
- }
- }
-
- function createSourceObject(str) {
- try {
- return URL.createObjectURL(new Blob([str], { type: SCRIPT_TYPE }));
- } catch (e) {
- var blob = new BlobBuilder();
- blob.append(str);
- return URL.createObjectURL(blob.getBlob(type));
- }
- }
-
- function wrapTerminate(worker, objURL) {
- if (!worker || !objURL) return;
- var term = worker.terminate;
- worker.objURL = objURL;
- worker.terminate = function () {
- if (worker.objURL) URL.revokeObjectURL(worker.objURL);
- term.call(worker);
- };
- }
-
- var TransmuxWorker = new shimWorker("./transmuxer-worker.worker.js", function (window, document) {
- var self = this;
- var transmuxerWorker = function () {
-
- /**
- * mux.js
- *
- * Copyright (c) 2015 Brightcove
- * All rights reserved.
- *
- * Functions that generate fragmented MP4s suitable for use with Media
- * Source Extensions.
- */
-
- var UINT32_MAX = Math.pow(2, 32) - 1;
-
- var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS;
-
- // pre-calculate constants
- (function () {
- var i;
- types = {
- avc1: [], // codingname
- avcC: [],
- btrt: [],
- dinf: [],
- dref: [],
- esds: [],
- ftyp: [],
- hdlr: [],
- mdat: [],
- mdhd: [],
- mdia: [],
- mfhd: [],
- minf: [],
- moof: [],
- moov: [],
- mp4a: [], // codingname
- mvex: [],
- mvhd: [],
- sdtp: [],
- smhd: [],
- stbl: [],
- stco: [],
- stsc: [],
- stsd: [],
- stsz: [],
- stts: [],
- styp: [],
- tfdt: [],
- tfhd: [],
- traf: [],
- trak: [],
- trun: [],
- trex: [],
- tkhd: [],
- vmhd: []
- };
-
- // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we
- // don't throw an error
- if (typeof Uint8Array === 'undefined') {
- return;
- }
-
- for (i in types) {
- if (types.hasOwnProperty(i)) {
- types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
- }
- }
-
- MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]);
- AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]);
- MINOR_VERSION = new Uint8Array([0, 0, 0, 1]);
- VIDEO_HDLR = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
- ]);
- AUDIO_HDLR = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun'
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler'
- ]);
- HDLR_TYPES = {
- video: VIDEO_HDLR,
- audio: AUDIO_HDLR
- };
- DREF = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01, // entry_count
- 0x00, 0x00, 0x00, 0x0c, // entry_size
- 0x75, 0x72, 0x6c, 0x20, // 'url' type
- 0x00, // version 0
- 0x00, 0x00, 0x01 // entry_flags
- ]);
- SMHD = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, // balance, 0 means centered
- 0x00, 0x00 // reserved
- ]);
- STCO = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00 // entry_count
- ]);
- STSC = STCO;
- STSZ = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // sample_size
- 0x00, 0x00, 0x00, 0x00 // sample_count
- ]);
- STTS = STCO;
- VMHD = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x01, // flags
- 0x00, 0x00, // graphicsmode
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor
- ]);
- })();
-
- box = function box(type) {
- var payload = [],
- size = 0,
- i,
- result,
- view;
-
- for (i = 1; i < arguments.length; i++) {
- payload.push(arguments[i]);
- }
-
- i = payload.length;
-
- // calculate the total size we need to allocate
- while (i--) {
- size += payload[i].byteLength;
- }
- result = new Uint8Array(size + 8);
- view = new DataView(result.buffer, result.byteOffset, result.byteLength);
- view.setUint32(0, result.byteLength);
- result.set(type, 4);
-
- // copy the payload into the result
- for (i = 0, size = 8; i < payload.length; i++) {
- result.set(payload[i], size);
- size += payload[i].byteLength;
- }
- return result;
- };
-
- dinf = function dinf() {
- return box(types.dinf, box(types.dref, DREF));
- };
-
- esds = function esds(track) {
- return box(types.esds, new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
-
- // ES_Descriptor
- 0x03, // tag, ES_DescrTag
- 0x19, // length
- 0x00, 0x00, // ES_ID
- 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority
-
- // DecoderConfigDescriptor
- 0x04, // tag, DecoderConfigDescrTag
- 0x11, // length
- 0x40, // object type
- 0x15, // streamType
- 0x00, 0x06, 0x00, // bufferSizeDB
- 0x00, 0x00, 0xda, 0xc0, // maxBitrate
- 0x00, 0x00, 0xda, 0xc0, // avgBitrate
-
- // DecoderSpecificInfo
- 0x05, // tag, DecoderSpecificInfoTag
- 0x02, // length
- // ISO/IEC 14496-3, AudioSpecificConfig
- // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35
- track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig
- ]));
- };
-
- ftyp = function ftyp() {
- return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND);
- };
-
- hdlr = function hdlr(type) {
- return box(types.hdlr, HDLR_TYPES[type]);
- };
- mdat = function mdat(data) {
- return box(types.mdat, data);
- };
- mdhd = function mdhd(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x02, // creation_time
- 0x00, 0x00, 0x00, 0x03, // modification_time
- 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
-
- track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration
- 0x55, 0xc4, // 'und' language (undetermined)
- 0x00, 0x00]);
-
- // Use the sample rate from the track metadata, when it is
- // defined. The sample rate can be parsed out of an ADTS header, for
- // instance.
- if (track.samplerate) {
- result[12] = track.samplerate >>> 24 & 0xFF;
- result[13] = track.samplerate >>> 16 & 0xFF;
- result[14] = track.samplerate >>> 8 & 0xFF;
- result[15] = track.samplerate & 0xFF;
- }
-
- return box(types.mdhd, result);
- };
- mdia = function mdia(track) {
- return box(types.mdia, mdhd(track), hdlr(track.type), minf(track));
- };
- mfhd = function mfhd(sequenceNumber) {
- return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags
- (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number
- ]));
- };
- minf = function minf(track) {
- return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track));
- };
- moof = function moof(sequenceNumber, tracks) {
- var trackFragments = [],
- i = tracks.length;
- // build traf boxes for each track fragment
- while (i--) {
- trackFragments[i] = traf(tracks[i]);
- }
- return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments));
- };
- /**
- * Returns a movie box.
- * @param tracks {array} the tracks associated with this movie
- * @see ISO/IEC 14496-12:2012(E), section 8.2.1
- */
- moov = function moov(tracks) {
- var i = tracks.length,
- boxes = [];
-
- while (i--) {
- boxes[i] = trak(tracks[i]);
- }
-
- return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks)));
- };
- mvex = function mvex(tracks) {
- var i = tracks.length,
- boxes = [];
-
- while (i--) {
- boxes[i] = trex(tracks[i]);
- }
- return box.apply(null, [types.mvex].concat(boxes));
- };
- mvhd = function mvhd(duration) {
- var bytes = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01, // creation_time
- 0x00, 0x00, 0x00, 0x02, // modification_time
- 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
- (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration
- 0x00, 0x01, 0x00, 0x00, // 1.0 rate
- 0x01, 0x00, // 1.0 volume
- 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0xff, 0xff, 0xff, 0xff // next_track_ID
- ]);
- return box(types.mvhd, bytes);
- };
-
- sdtp = function sdtp(track) {
- var samples = track.samples || [],
- bytes = new Uint8Array(4 + samples.length),
- flags,
- i;
-
- // leave the full box header (4 bytes) all zero
-
- // write the sample table
- for (i = 0; i < samples.length; i++) {
- flags = samples[i].flags;
-
- bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
- }
-
- return box(types.sdtp, bytes);
- };
-
- stbl = function stbl(track) {
- return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO));
- };
-
- (function () {
- var videoSample, audioSample;
-
- stsd = function stsd(track) {
-
- return box(types.stsd, new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track));
- };
-
- videoSample = function videoSample(track) {
- var sps = track.sps || [],
- pps = track.pps || [],
- sequenceParameterSets = [],
- pictureParameterSets = [],
- i;
-
- // assemble the SPSs
- for (i = 0; i < sps.length; i++) {
- sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8);
- sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength
- sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS
- }
-
- // assemble the PPSs
- for (i = 0; i < pps.length; i++) {
- pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8);
- pictureParameterSets.push(pps[i].byteLength & 0xFF);
- pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i]));
- }
-
- return box(types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // data_reference_index
- 0x00, 0x00, // pre_defined
- 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
- (track.width & 0xff00) >> 8, track.width & 0xff, // width
- (track.height & 0xff00) >> 8, track.height & 0xff, // height
- 0x00, 0x48, 0x00, 0x00, // horizresolution
- 0x00, 0x48, 0x00, 0x00, // vertresolution
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // frame_count
- 0x13, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6a, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2d, 0x68, 0x6c, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compressorname
- 0x00, 0x18, // depth = 24
- 0x11, 0x11 // pre_defined = -1
- ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion
- track.profileIdc, // AVCProfileIndication
- track.profileCompatibility, // profile_compatibility
- track.levelIdc, // AVCLevelIndication
- 0xff // lengthSizeMinusOne, hard-coded to 4 bytes
- ].concat([sps.length // numOfSequenceParameterSets
- ]).concat(sequenceParameterSets).concat([pps.length // numOfPictureParameterSets
- ]).concat(pictureParameterSets))), // "PPS"
- box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
- 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
- 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate
- );
- };
-
- audioSample = function audioSample(track) {
- return box(types.mp4a, new Uint8Array([
-
- // SampleEntry, ISO/IEC 14496-12
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // data_reference_index
-
- // AudioSampleEntry, ISO/IEC 14496-12
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount
-
- (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize
- 0x00, 0x00, // pre_defined
- 0x00, 0x00, // reserved
-
- (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16
-
- // MP4AudioSampleEntry, ISO/IEC 14496-14
- ]), esds(track));
- };
- })();
-
- tkhd = function tkhd(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x07, // flags
- 0x00, 0x00, 0x00, 0x00, // creation_time
- 0x00, 0x00, 0x00, 0x00, // modification_time
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x00, // reserved
- (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, // layer
- 0x00, 0x00, // alternate_group
- 0x01, 0x00, // non-audio track volume
- 0x00, 0x00, // reserved
- 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
- (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width
- (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height
- ]);
-
- return box(types.tkhd, result);
- };
-
- /**
- * Generate a track fragment (traf) box. A traf box collects metadata
- * about tracks in a movie fragment (moof) box.
- */
- traf = function traf(track) {
- var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime;
-
- trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x3a, // flags
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x01, // sample_description_index
- 0x00, 0x00, 0x00, 0x00, // default_sample_duration
- 0x00, 0x00, 0x00, 0x00, // default_sample_size
- 0x00, 0x00, 0x00, 0x00 // default_sample_flags
- ]));
-
- upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / (UINT32_MAX + 1));
- lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % (UINT32_MAX + 1));
-
- trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1
- 0x00, 0x00, 0x00, // flags
- // baseMediaDecodeTime
- upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF]));
-
- // the data offset specifies the number of bytes from the start of
- // the containing moof to the first payload byte of the associated
- // mdat
- dataOffset = 32 + // tfhd
- 20 + // tfdt
- 8 + // traf header
- 16 + // mfhd
- 8 + // moof header
- 8; // mdat header
-
- // audio tracks require less metadata
- if (track.type === 'audio') {
- trackFragmentRun = trun(track, dataOffset);
- return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun);
- }
-
- // video tracks should contain an independent and disposable samples
- // box (sdtp)
- // generate one and adjust offsets to match
- sampleDependencyTable = sdtp(track);
- trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset);
- return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable);
- };
-
- /**
- * Generate a track box.
- * @param track {object} a track definition
- * @return {Uint8Array} the track box
- */
- trak = function trak(track) {
- track.duration = track.duration || 0xffffffff;
- return box(types.trak, tkhd(track), mdia(track));
- };
-
- trex = function trex(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x01, // default_sample_description_index
- 0x00, 0x00, 0x00, 0x00, // default_sample_duration
- 0x00, 0x00, 0x00, 0x00, // default_sample_size
- 0x00, 0x01, 0x00, 0x01 // default_sample_flags
- ]);
- // the last two bytes of default_sample_flags is the sample
- // degradation priority, a hint about the importance of this sample
- // relative to others. Lower the degradation priority for all sample
- // types other than video.
- if (track.type !== 'video') {
- result[result.length - 1] = 0x00;
- }
-
- return box(types.trex, result);
- };
-
- (function () {
- var audioTrun, videoTrun, trunHeader;
-
- // This method assumes all samples are uniform. That is, if a
- // duration is present for the first sample, it will be present for
- // all subsequent samples.
- // see ISO/IEC 14496-12:2012, Section 8.8.8.1
- trunHeader = function trunHeader(samples, offset) {
- var durationPresent = 0,
- sizePresent = 0,
- flagsPresent = 0,
- compositionTimeOffset = 0;
-
- // trun flag constants
- if (samples.length) {
- if (samples[0].duration !== undefined) {
- durationPresent = 0x1;
- }
- if (samples[0].size !== undefined) {
- sizePresent = 0x2;
- }
- if (samples[0].flags !== undefined) {
- flagsPresent = 0x4;
- }
- if (samples[0].compositionTimeOffset !== undefined) {
- compositionTimeOffset = 0x8;
- }
- }
-
- return [0x00, // version 0
- 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags
- (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count
- (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset
- ];
- };
-
- videoTrun = function videoTrun(track, offset) {
- var bytes, samples, sample, i;
-
- samples = track.samples || [];
- offset += 8 + 12 + 16 * samples.length;
-
- bytes = trunHeader(samples, offset);
-
- for (i = 0; i < samples.length; i++) {
- sample = samples[i];
- bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration
- (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF, // sample_size
- sample.flags.isLeading << 2 | sample.flags.dependsOn, sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample, sample.flags.degradationPriority & 0xF0 << 8, sample.flags.degradationPriority & 0x0F, // sample_flags
- (sample.compositionTimeOffset & 0xFF000000) >>> 24, (sample.compositionTimeOffset & 0xFF0000) >>> 16, (sample.compositionTimeOffset & 0xFF00) >>> 8, sample.compositionTimeOffset & 0xFF // sample_composition_time_offset
- ]);
- }
- return box(types.trun, new Uint8Array(bytes));
- };
-
- audioTrun = function audioTrun(track, offset) {
- var bytes, samples, sample, i;
-
- samples = track.samples || [];
- offset += 8 + 12 + 8 * samples.length;
-
- bytes = trunHeader(samples, offset);
-
- for (i = 0; i < samples.length; i++) {
- sample = samples[i];
- bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration
- (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF]); // sample_size
- }
-
- return box(types.trun, new Uint8Array(bytes));
- };
-
- trun = function trun(track, offset) {
- if (track.type === 'audio') {
- return audioTrun(track, offset);
- }
-
- return videoTrun(track, offset);
- };
- })();
-
- var mp4Generator = {
- ftyp: ftyp,
- mdat: mdat,
- moof: moof,
- moov: moov,
- initSegment: function initSegment(tracks) {
- var fileType = ftyp(),
- movie = moov(tracks),
- result;
-
- result = new Uint8Array(fileType.byteLength + movie.byteLength);
- result.set(fileType);
- result.set(movie, fileType.byteLength);
- return result;
- }
- };
-
- var toUnsigned = function toUnsigned(value) {
- return value >>> 0;
- };
-
- var bin = {
- toUnsigned: toUnsigned
- };
-
- var toUnsigned$1 = bin.toUnsigned;
- var _findBox, parseType, timescale, startTime, getVideoTrackIds;
-
- // Find the data for a box specified by its path
- _findBox = function findBox(data, path) {
- var results = [],
- i,
- size,
- type,
- end,
- subresults;
-
- if (!path.length) {
- // short-circuit the search for empty paths
- return null;
- }
-
- for (i = 0; i < data.byteLength;) {
- size = toUnsigned$1(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]);
-
- type = parseType(data.subarray(i + 4, i + 8));
-
- end = size > 1 ? i + size : data.byteLength;
-
- if (type === path[0]) {
- if (path.length === 1) {
- // this is the end of the path and we've found the box we were
- // looking for
- results.push(data.subarray(i + 8, end));
- } else {
- // recursively search for the next box along the path
- subresults = _findBox(data.subarray(i + 8, end), path.slice(1));
- if (subresults.length) {
- results = results.concat(subresults);
- }
- }
- }
- i = end;
- }
-
- // we've finished searching all of data
- return results;
- };
-
- /**
- * Returns the string representation of an ASCII encoded four byte buffer.
- * @param buffer {Uint8Array} a four-byte buffer to translate
- * @return {string} the corresponding string
- */
- parseType = function parseType(buffer) {
- var result = '';
- result += String.fromCharCode(buffer[0]);
- result += String.fromCharCode(buffer[1]);
- result += String.fromCharCode(buffer[2]);
- result += String.fromCharCode(buffer[3]);
- return result;
- };
-
- /**
- * Parses an MP4 initialization segment and extracts the timescale
- * values for any declared tracks. Timescale values indicate the
- * number of clock ticks per second to assume for time-based values
- * elsewhere in the MP4.
- *
- * To determine the start time of an MP4, you need two pieces of
- * information: the timescale unit and the earliest base media decode
- * time. Multiple timescales can be specified within an MP4 but the
- * base media decode time is always expressed in the timescale from
- * the media header box for the track:
- * ```
- * moov > trak > mdia > mdhd.timescale
- * ```
- * @param init {Uint8Array} the bytes of the init segment
- * @return {object} a hash of track ids to timescale values or null if
- * the init segment is malformed.
- */
- timescale = function timescale(init) {
- var result = {},
- traks = _findBox(init, ['moov', 'trak']);
-
- // mdhd timescale
- return traks.reduce(function (result, trak) {
- var tkhd, version, index, id, mdhd;
-
- tkhd = _findBox(trak, ['tkhd'])[0];
- if (!tkhd) {
- return null;
- }
- version = tkhd[0];
- index = version === 0 ? 12 : 20;
- id = toUnsigned$1(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]);
-
- mdhd = _findBox(trak, ['mdia', 'mdhd'])[0];
- if (!mdhd) {
- return null;
- }
- version = mdhd[0];
- index = version === 0 ? 12 : 20;
- result[id] = toUnsigned$1(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
- return result;
- }, result);
- };
-
- /**
- * Determine the base media decode start time, in seconds, for an MP4
- * fragment. If multiple fragments are specified, the earliest time is
- * returned.
- *
- * The base media decode time can be parsed from track fragment
- * metadata:
- * ```
- * moof > traf > tfdt.baseMediaDecodeTime
- * ```
- * It requires the timescale value from the mdhd to interpret.
- *
- * @param timescale {object} a hash of track ids to timescale values.
- * @return {number} the earliest base media decode start time for the
- * fragment, in seconds
- */
- startTime = function startTime(timescale, fragment) {
- var trafs, baseTimes, result;
-
- // we need info from two childrend of each track fragment box
- trafs = _findBox(fragment, ['moof', 'traf']);
-
- // determine the start times for each track
- baseTimes = [].concat.apply([], trafs.map(function (traf) {
- return _findBox(traf, ['tfhd']).map(function (tfhd) {
- var id, scale, baseTime;
-
- // get the track id from the tfhd
- id = toUnsigned$1(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]);
- // assume a 90kHz clock if no timescale was specified
- scale = timescale[id] || 90e3;
-
- // get the base media decode time from the tfdt
- baseTime = _findBox(traf, ['tfdt']).map(function (tfdt) {
- var version, result;
-
- version = tfdt[0];
- result = toUnsigned$1(tfdt[4] << 24 | tfdt[5] << 16 | tfdt[6] << 8 | tfdt[7]);
- if (version === 1) {
- result *= Math.pow(2, 32);
- result += toUnsigned$1(tfdt[8] << 24 | tfdt[9] << 16 | tfdt[10] << 8 | tfdt[11]);
- }
- return result;
- })[0];
- baseTime = baseTime || Infinity;
-
- // convert base time to seconds
- return baseTime / scale;
- });
- }));
-
- // return the minimum
- result = Math.min.apply(null, baseTimes);
- return isFinite(result) ? result : 0;
- };
-
- /**
- * Find the trackIds of the video tracks in this source.
- * Found by parsing the Handler Reference and Track Header Boxes:
- * moov > trak > mdia > hdlr
- * moov > trak > tkhd
- *
- * @param {Uint8Array} init - The bytes of the init segment for this source
- * @return {Number[]} A list of trackIds
- *
- * @see ISO-BMFF-12/2015, Section 8.4.3
- **/
- getVideoTrackIds = function getVideoTrackIds(init) {
- var traks = _findBox(init, ['moov', 'trak']);
- var videoTrackIds = [];
-
- traks.forEach(function (trak) {
- var hdlrs = _findBox(trak, ['mdia', 'hdlr']);
- var tkhds = _findBox(trak, ['tkhd']);
-
- hdlrs.forEach(function (hdlr, index) {
- var handlerType = parseType(hdlr.subarray(8, 12));
- var tkhd = tkhds[index];
- var view;
- var version;
- var trackId;
-
- if (handlerType === 'vide') {
- view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
- version = view.getUint8(0);
- trackId = version === 0 ? view.getUint32(12) : view.getUint32(20);
-
- videoTrackIds.push(trackId);
- }
- });
- });
-
- return videoTrackIds;
- };
-
- var probe = {
- findBox: _findBox,
- parseType: parseType,
- timescale: timescale,
- startTime: startTime,
- videoTrackIds: getVideoTrackIds
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2014 Brightcove
- * All rights reserved.
- *
- * A lightweight readable stream implemention that handles event dispatching.
- * Objects that inherit from streams should call init in their constructors.
- */
-
- var Stream = function Stream() {
- this.init = function () {
- var listeners = {};
- /**
- * Add a listener for a specified event type.
- * @param type {string} the event name
- * @param listener {function} the callback to be invoked when an event of
- * the specified type occurs
- */
- this.on = function (type, listener) {
- if (!listeners[type]) {
- listeners[type] = [];
- }
- listeners[type] = listeners[type].concat(listener);
- };
- /**
- * Remove a listener for a specified event type.
- * @param type {string} the event name
- * @param listener {function} a function previously registered for this
- * type of event through `on`
- */
- this.off = function (type, listener) {
- var index;
- if (!listeners[type]) {
- return false;
- }
- index = listeners[type].indexOf(listener);
- listeners[type] = listeners[type].slice();
- listeners[type].splice(index, 1);
- return index > -1;
- };
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- * @param type {string} the event name
- */
- this.trigger = function (type) {
- var callbacks, i, length, args;
- callbacks = listeners[type];
- if (!callbacks) {
- return;
- }
- // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
- if (arguments.length === 2) {
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- args = [];
- i = arguments.length;
- for (i = 1; i < arguments.length; ++i) {
- args.push(arguments[i]);
- }
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].apply(this, args);
- }
- }
- };
- /**
- * Destroys the stream and cleans up.
- */
- this.dispose = function () {
- listeners = {};
- };
- };
- };
-
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- * @param destination {stream} the stream that will receive all `data` events
- * @param autoFlush {boolean} if false, we will not call `flush` on the destination
- * when the current stream emits a 'done' event
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */
- Stream.prototype.pipe = function (destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
-
- this.on('done', function (flushSource) {
- destination.flush(flushSource);
- });
-
- return destination;
- };
-
- // Default stream functions that are expected to be overridden to perform
- // actual work. These are provided by the prototype as a sort of no-op
- // implementation so that we don't have to check for their existence in the
- // `pipe` function above.
- Stream.prototype.push = function (data) {
- this.trigger('data', data);
- };
-
- Stream.prototype.flush = function (flushSource) {
- this.trigger('done', flushSource);
- };
-
- var stream = Stream;
-
- // Convert an array of nal units into an array of frames with each frame being
- // composed of the nal units that make up that frame
- // Also keep track of cummulative data about the frame from the nal units such
- // as the frame duration, starting pts, etc.
- var groupNalsIntoFrames = function groupNalsIntoFrames(nalUnits) {
- var i,
- currentNal,
- currentFrame = [],
- frames = [];
-
- currentFrame.byteLength = 0;
-
- for (i = 0; i < nalUnits.length; i++) {
- currentNal = nalUnits[i];
-
- // Split on 'aud'-type nal units
- if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
- // Since the very first nal unit is expected to be an AUD
- // only push to the frames array when currentFrame is not empty
- if (currentFrame.length) {
- currentFrame.duration = currentNal.dts - currentFrame.dts;
- frames.push(currentFrame);
- }
- currentFrame = [currentNal];
- currentFrame.byteLength = currentNal.data.byteLength;
- currentFrame.pts = currentNal.pts;
- currentFrame.dts = currentNal.dts;
- } else {
- // Specifically flag key frames for ease of use later
- if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
- currentFrame.keyFrame = true;
- }
- currentFrame.duration = currentNal.dts - currentFrame.dts;
- currentFrame.byteLength += currentNal.data.byteLength;
- currentFrame.push(currentNal);
- }
- }
-
- // For the last frame, use the duration of the previous frame if we
- // have nothing better to go on
- if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) {
- currentFrame.duration = frames[frames.length - 1].duration;
- }
-
- // Push the final frame
- frames.push(currentFrame);
- return frames;
- };
-
- // Convert an array of frames into an array of Gop with each Gop being composed
- // of the frames that make up that Gop
- // Also keep track of cummulative data about the Gop from the frames such as the
- // Gop duration, starting pts, etc.
- var groupFramesIntoGops = function groupFramesIntoGops(frames) {
- var i,
- currentFrame,
- currentGop = [],
- gops = [];
-
- // We must pre-set some of the values on the Gop since we
- // keep running totals of these values
- currentGop.byteLength = 0;
- currentGop.nalCount = 0;
- currentGop.duration = 0;
- currentGop.pts = frames[0].pts;
- currentGop.dts = frames[0].dts;
-
- // store some metadata about all the Gops
- gops.byteLength = 0;
- gops.nalCount = 0;
- gops.duration = 0;
- gops.pts = frames[0].pts;
- gops.dts = frames[0].dts;
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
-
- if (currentFrame.keyFrame) {
- // Since the very first frame is expected to be an keyframe
- // only push to the gops array when currentGop is not empty
- if (currentGop.length) {
- gops.push(currentGop);
- gops.byteLength += currentGop.byteLength;
- gops.nalCount += currentGop.nalCount;
- gops.duration += currentGop.duration;
- }
-
- currentGop = [currentFrame];
- currentGop.nalCount = currentFrame.length;
- currentGop.byteLength = currentFrame.byteLength;
- currentGop.pts = currentFrame.pts;
- currentGop.dts = currentFrame.dts;
- currentGop.duration = currentFrame.duration;
- } else {
- currentGop.duration += currentFrame.duration;
- currentGop.nalCount += currentFrame.length;
- currentGop.byteLength += currentFrame.byteLength;
- currentGop.push(currentFrame);
- }
- }
-
- if (gops.length && currentGop.duration <= 0) {
- currentGop.duration = gops[gops.length - 1].duration;
- }
- gops.byteLength += currentGop.byteLength;
- gops.nalCount += currentGop.nalCount;
- gops.duration += currentGop.duration;
-
- // push the final Gop
- gops.push(currentGop);
- return gops;
- };
-
- /*
- * Search for the first keyframe in the GOPs and throw away all frames
- * until that keyframe. Then extend the duration of the pulled keyframe
- * and pull the PTS and DTS of the keyframe so that it covers the time
- * range of the frames that were disposed.
- *
- * @param {Array} gops video GOPs
- * @returns {Array} modified video GOPs
- */
- var extendFirstKeyFrame = function extendFirstKeyFrame(gops) {
- var currentGop;
-
- if (!gops[0][0].keyFrame && gops.length > 1) {
- // Remove the first GOP
- currentGop = gops.shift();
-
- gops.byteLength -= currentGop.byteLength;
- gops.nalCount -= currentGop.nalCount;
-
- // Extend the first frame of what is now the
- // first gop to cover the time period of the
- // frames we just removed
- gops[0][0].dts = currentGop.dts;
- gops[0][0].pts = currentGop.pts;
- gops[0][0].duration += currentGop.duration;
- }
-
- return gops;
- };
-
- /**
- * Default sample object
- * see ISO/IEC 14496-12:2012, section 8.6.4.3
- */
- var createDefaultSample = function createDefaultSample() {
- return {
- size: 0,
- flags: {
- isLeading: 0,
- dependsOn: 1,
- isDependedOn: 0,
- hasRedundancy: 0,
- degradationPriority: 0,
- isNonSyncSample: 1
- }
- };
- };
-
- /*
- * Collates information from a video frame into an object for eventual
- * entry into an MP4 sample table.
- *
- * @param {Object} frame the video frame
- * @param {Number} dataOffset the byte offset to position the sample
- * @return {Object} object containing sample table info for a frame
- */
- var sampleForFrame = function sampleForFrame(frame, dataOffset) {
- var sample = createDefaultSample();
-
- sample.dataOffset = dataOffset;
- sample.compositionTimeOffset = frame.pts - frame.dts;
- sample.duration = frame.duration;
- sample.size = 4 * frame.length; // Space for nal unit size
- sample.size += frame.byteLength;
-
- if (frame.keyFrame) {
- sample.flags.dependsOn = 2;
- sample.flags.isNonSyncSample = 0;
- }
-
- return sample;
- };
-
- // generate the track's sample table from an array of gops
- var generateSampleTable = function generateSampleTable(gops, baseDataOffset) {
- var h,
- i,
- sample,
- currentGop,
- currentFrame,
- dataOffset = baseDataOffset || 0,
- samples = [];
-
- for (h = 0; h < gops.length; h++) {
- currentGop = gops[h];
-
- for (i = 0; i < currentGop.length; i++) {
- currentFrame = currentGop[i];
-
- sample = sampleForFrame(currentFrame, dataOffset);
-
- dataOffset += sample.size;
-
- samples.push(sample);
- }
- }
- return samples;
- };
-
- // generate the track's raw mdat data from an array of gops
- var concatenateNalData = function concatenateNalData(gops) {
- var h,
- i,
- j,
- currentGop,
- currentFrame,
- currentNal,
- dataOffset = 0,
- nalsByteLength = gops.byteLength,
- numberOfNals = gops.nalCount,
- totalByteLength = nalsByteLength + 4 * numberOfNals,
- data = new Uint8Array(totalByteLength),
- view = new DataView(data.buffer);
-
- // For each Gop..
- for (h = 0; h < gops.length; h++) {
- currentGop = gops[h];
-
- // For each Frame..
- for (i = 0; i < currentGop.length; i++) {
- currentFrame = currentGop[i];
-
- // For each NAL..
- for (j = 0; j < currentFrame.length; j++) {
- currentNal = currentFrame[j];
-
- view.setUint32(dataOffset, currentNal.data.byteLength);
- dataOffset += 4;
- data.set(currentNal.data, dataOffset);
- dataOffset += currentNal.data.byteLength;
- }
- }
- }
- return data;
- };
-
- var frameUtils = {
- groupNalsIntoFrames: groupNalsIntoFrames,
- groupFramesIntoGops: groupFramesIntoGops,
- extendFirstKeyFrame: extendFirstKeyFrame,
- generateSampleTable: generateSampleTable,
- concatenateNalData: concatenateNalData
- };
-
- var ONE_SECOND_IN_TS = 90000; // 90kHz clock
-
- /**
- * Store information about the start and end of the track and the
- * duration for each frame/sample we process in order to calculate
- * the baseMediaDecodeTime
- */
- var collectDtsInfo = function collectDtsInfo(track, data) {
- if (typeof data.pts === 'number') {
- if (track.timelineStartInfo.pts === undefined) {
- track.timelineStartInfo.pts = data.pts;
- }
-
- if (track.minSegmentPts === undefined) {
- track.minSegmentPts = data.pts;
- } else {
- track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
- }
-
- if (track.maxSegmentPts === undefined) {
- track.maxSegmentPts = data.pts;
- } else {
- track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
- }
- }
-
- if (typeof data.dts === 'number') {
- if (track.timelineStartInfo.dts === undefined) {
- track.timelineStartInfo.dts = data.dts;
- }
-
- if (track.minSegmentDts === undefined) {
- track.minSegmentDts = data.dts;
- } else {
- track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
- }
-
- if (track.maxSegmentDts === undefined) {
- track.maxSegmentDts = data.dts;
- } else {
- track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
- }
- }
- };
-
- /**
- * Clear values used to calculate the baseMediaDecodeTime between
- * tracks
- */
- var clearDtsInfo = function clearDtsInfo(track) {
- delete track.minSegmentDts;
- delete track.maxSegmentDts;
- delete track.minSegmentPts;
- delete track.maxSegmentPts;
- };
-
- /**
- * Calculate the track's baseMediaDecodeTime based on the earliest
- * DTS the transmuxer has ever seen and the minimum DTS for the
- * current track
- * @param track {object} track metadata configuration
- * @param keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- var calculateTrackBaseMediaDecodeTime = function calculateTrackBaseMediaDecodeTime(track, keepOriginalTimestamps) {
- var baseMediaDecodeTime,
- scale,
- minSegmentDts = track.minSegmentDts;
-
- // Optionally adjust the time so the first segment starts at zero.
- if (!keepOriginalTimestamps) {
- minSegmentDts -= track.timelineStartInfo.dts;
- }
-
- // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
- // we want the start of the first segment to be placed
- baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime;
-
- // Add to that the distance this segment is from the very first
- baseMediaDecodeTime += minSegmentDts;
-
- // baseMediaDecodeTime must not become negative
- baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
-
- if (track.type === 'audio') {
- // Audio has a different clock equal to the sampling_rate so we need to
- // scale the PTS values into the clock rate of the track
- scale = track.samplerate / ONE_SECOND_IN_TS;
- baseMediaDecodeTime *= scale;
- baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
- }
-
- return baseMediaDecodeTime;
- };
-
- var trackDecodeInfo = {
- clearDtsInfo: clearDtsInfo,
- calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
- collectDtsInfo: collectDtsInfo
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2015 Brightcove
- * All rights reserved.
- *
- * Reads in-band caption information from a video elementary
- * stream. Captions must follow the CEA-708 standard for injection
- * into an MPEG-2 transport streams.
- * @see https://en.wikipedia.org/wiki/CEA-708
- * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
- */
-
- // Supplemental enhancement information (SEI) NAL units have a
- // payload type field to indicate how they are to be
- // interpreted. CEAS-708 caption content is always transmitted with
- // payload type 0x04.
-
- var USER_DATA_REGISTERED_ITU_T_T35 = 4,
- RBSP_TRAILING_BITS = 128;
-
- /**
- * Parse a supplemental enhancement information (SEI) NAL unit.
- * Stops parsing once a message of type ITU T T35 has been found.
- *
- * @param bytes {Uint8Array} the bytes of a SEI NAL unit
- * @return {object} the parsed SEI payload
- * @see Rec. ITU-T H.264, 7.3.2.3.1
- */
- var parseSei = function parseSei(bytes) {
- var i = 0,
- result = {
- payloadType: -1,
- payloadSize: 0
- },
- payloadType = 0,
- payloadSize = 0;
-
- // go through the sei_rbsp parsing each each individual sei_message
- while (i < bytes.byteLength) {
- // stop once we have hit the end of the sei_rbsp
- if (bytes[i] === RBSP_TRAILING_BITS) {
- break;
- }
-
- // Parse payload type
- while (bytes[i] === 0xFF) {
- payloadType += 255;
- i++;
- }
- payloadType += bytes[i++];
-
- // Parse payload size
- while (bytes[i] === 0xFF) {
- payloadSize += 255;
- i++;
- }
- payloadSize += bytes[i++];
-
- // this sei_message is a 608/708 caption so save it and break
- // there can only ever be one caption message in a frame's sei
- if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) {
- result.payloadType = payloadType;
- result.payloadSize = payloadSize;
- result.payload = bytes.subarray(i, i + payloadSize);
- break;
- }
-
- // skip the payload and parse the next message
- i += payloadSize;
- payloadType = 0;
- payloadSize = 0;
- }
-
- return result;
- };
-
- // see ANSI/SCTE 128-1 (2013), section 8.1
- var parseUserData = function parseUserData(sei) {
- // itu_t_t35_contry_code must be 181 (United States) for
- // captions
- if (sei.payload[0] !== 181) {
- return null;
- }
-
- // itu_t_t35_provider_code should be 49 (ATSC) for captions
- if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) {
- return null;
- }
-
- // the user_identifier should be "GA94" to indicate ATSC1 data
- if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') {
- return null;
- }
-
- // finally, user_data_type_code should be 0x03 for caption data
- if (sei.payload[7] !== 0x03) {
- return null;
- }
-
- // return the user_data_type_structure and strip the trailing
- // marker bits
- return sei.payload.subarray(8, sei.payload.length - 1);
- };
-
- // see CEA-708-D, section 4.4
- var parseCaptionPackets = function parseCaptionPackets(pts, userData) {
- var results = [],
- i,
- count,
- offset,
- data;
-
- // if this is just filler, return immediately
- if (!(userData[0] & 0x40)) {
- return results;
- }
-
- // parse out the cc_data_1 and cc_data_2 fields
- count = userData[0] & 0x1f;
- for (i = 0; i < count; i++) {
- offset = i * 3;
- data = {
- type: userData[offset + 2] & 0x03,
- pts: pts
- };
-
- // capture cc data when cc_valid is 1
- if (userData[offset + 2] & 0x04) {
- data.ccData = userData[offset + 3] << 8 | userData[offset + 4];
- results.push(data);
- }
- }
- return results;
- };
-
- var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) {
- var length = data.byteLength,
- emulationPreventionBytesPositions = [],
- i = 1,
- newLength,
- newData;
-
- // Find all `Emulation Prevention Bytes`
- while (i < length - 2) {
- if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
- emulationPreventionBytesPositions.push(i + 2);
- i += 2;
- } else {
- i++;
- }
- }
-
- // If no Emulation Prevention Bytes were found just return the original
- // array
- if (emulationPreventionBytesPositions.length === 0) {
- return data;
- }
-
- // Create a new array to hold the NAL unit data
- newLength = length - emulationPreventionBytesPositions.length;
- newData = new Uint8Array(newLength);
- var sourceIndex = 0;
-
- for (i = 0; i < newLength; sourceIndex++, i++) {
- if (sourceIndex === emulationPreventionBytesPositions[0]) {
- // Skip this byte
- sourceIndex++;
- // Remove this position index
- emulationPreventionBytesPositions.shift();
- }
- newData[i] = data[sourceIndex];
- }
-
- return newData;
- };
-
- // exports
- var captionPacketParser = {
- parseSei: parseSei,
- parseUserData: parseUserData,
- parseCaptionPackets: parseCaptionPackets,
- discardEmulationPreventionBytes: discardEmulationPreventionBytes,
- USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35
- };
-
- // -----------------
- // Link To Transport
- // -----------------
-
-
- var CaptionStream = function CaptionStream() {
-
- CaptionStream.prototype.init.call(this);
-
- this.captionPackets_ = [];
-
- this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
- ];
-
- this.reset();
-
- // forward data and done events from CCs to this CaptionStream
- this.ccStreams_.forEach(function (cc) {
- cc.on('data', this.trigger.bind(this, 'data'));
- cc.on('done', this.trigger.bind(this, 'done'));
- }, this);
- };
-
- CaptionStream.prototype = new stream();
- CaptionStream.prototype.push = function (event) {
- var sei, userData, newCaptionPackets;
-
- // only examine SEI NALs
- if (event.nalUnitType !== 'sei_rbsp') {
- return;
- }
-
- // parse the sei
- sei = captionPacketParser.parseSei(event.escapedRBSP);
-
- // ignore everything but user_data_registered_itu_t_t35
- if (sei.payloadType !== captionPacketParser.USER_DATA_REGISTERED_ITU_T_T35) {
- return;
- }
-
- // parse out the user data payload
- userData = captionPacketParser.parseUserData(sei);
-
- // ignore unrecognized userData
- if (!userData) {
- return;
- }
-
- // Sometimes, the same segment # will be downloaded twice. To stop the
- // caption data from being processed twice, we track the latest dts we've
- // received and ignore everything with a dts before that. However, since
- // data for a specific dts can be split across packets on either side of
- // a segment boundary, we need to make sure we *don't* ignore the packets
- // from the *next* segment that have dts === this.latestDts_. By constantly
- // tracking the number of packets received with dts === this.latestDts_, we
- // know how many should be ignored once we start receiving duplicates.
- if (event.dts < this.latestDts_) {
- // We've started getting older data, so set the flag.
- this.ignoreNextEqualDts_ = true;
- return;
- } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) {
- this.numSameDts_--;
- if (!this.numSameDts_) {
- // We've received the last duplicate packet, time to start processing again
- this.ignoreNextEqualDts_ = false;
- }
- return;
- }
-
- // parse out CC data packets and save them for later
- newCaptionPackets = captionPacketParser.parseCaptionPackets(event.pts, userData);
- this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets);
- if (this.latestDts_ !== event.dts) {
- this.numSameDts_ = 0;
- }
- this.numSameDts_++;
- this.latestDts_ = event.dts;
- };
-
- CaptionStream.prototype.flush = function () {
- // make sure we actually parsed captions before proceeding
- if (!this.captionPackets_.length) {
- this.ccStreams_.forEach(function (cc) {
- cc.flush();
- }, this);
- return;
- }
-
- // In Chrome, the Array#sort function is not stable so add a
- // presortIndex that we can use to ensure we get a stable-sort
- this.captionPackets_.forEach(function (elem, idx) {
- elem.presortIndex = idx;
- });
-
- // sort caption byte-pairs based on their PTS values
- this.captionPackets_.sort(function (a, b) {
- if (a.pts === b.pts) {
- return a.presortIndex - b.presortIndex;
- }
- return a.pts - b.pts;
- });
-
- this.captionPackets_.forEach(function (packet) {
- if (packet.type < 2) {
- // Dispatch packet to the right Cea608Stream
- this.dispatchCea608Packet(packet);
- }
- // this is where an 'else' would go for a dispatching packets
- // to a theoretical Cea708Stream that handles SERVICEn data
- }, this);
-
- this.captionPackets_.length = 0;
- this.ccStreams_.forEach(function (cc) {
- cc.flush();
- }, this);
- return;
- };
-
- CaptionStream.prototype.reset = function () {
- this.latestDts_ = null;
- this.ignoreNextEqualDts_ = false;
- this.numSameDts_ = 0;
- this.activeCea608Channel_ = [null, null];
- this.ccStreams_.forEach(function (ccStream) {
- ccStream.reset();
- });
- };
-
- CaptionStream.prototype.dispatchCea608Packet = function (packet) {
- // NOTE: packet.type is the CEA608 field
- if (this.setsChannel1Active(packet)) {
- this.activeCea608Channel_[packet.type] = 0;
- } else if (this.setsChannel2Active(packet)) {
- this.activeCea608Channel_[packet.type] = 1;
- }
- if (this.activeCea608Channel_[packet.type] === null) {
- // If we haven't received anything to set the active channel, discard the
- // data; we don't want jumbled captions
- return;
- }
- this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
- };
-
- CaptionStream.prototype.setsChannel1Active = function (packet) {
- return (packet.ccData & 0x7800) === 0x1000;
- };
- CaptionStream.prototype.setsChannel2Active = function (packet) {
- return (packet.ccData & 0x7800) === 0x1800;
- };
-
- // ----------------------
- // Session to Application
- // ----------------------
-
- // This hash maps non-ASCII, special, and extended character codes to their
- // proper Unicode equivalent. The first keys that are only a single byte
- // are the non-standard ASCII characters, which simply map the CEA608 byte
- // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
- // character codes, but have their MSB bitmasked with 0x03 so that a lookup
- // can be performed regardless of the field and data channel on which the
- // character code was received.
- var CHARACTER_TRANSLATION = {
- 0x2a: 0xe1, // á
- 0x5c: 0xe9, // é
- 0x5e: 0xed, // í
- 0x5f: 0xf3, // ó
- 0x60: 0xfa, // ú
- 0x7b: 0xe7, // ç
- 0x7c: 0xf7, // ÷
- 0x7d: 0xd1, // Ñ
- 0x7e: 0xf1, // ñ
- 0x7f: 0x2588, // █
- 0x0130: 0xae, // ®
- 0x0131: 0xb0, // °
- 0x0132: 0xbd, // ½
- 0x0133: 0xbf, // ¿
- 0x0134: 0x2122, // ™
- 0x0135: 0xa2, // ¢
- 0x0136: 0xa3, // £
- 0x0137: 0x266a, // ♪
- 0x0138: 0xe0, // à
- 0x0139: 0xa0, //
- 0x013a: 0xe8, // è
- 0x013b: 0xe2, // â
- 0x013c: 0xea, // ê
- 0x013d: 0xee, // î
- 0x013e: 0xf4, // ô
- 0x013f: 0xfb, // û
- 0x0220: 0xc1, // Á
- 0x0221: 0xc9, // É
- 0x0222: 0xd3, // Ó
- 0x0223: 0xda, // Ú
- 0x0224: 0xdc, // Ü
- 0x0225: 0xfc, // ü
- 0x0226: 0x2018, // ‘
- 0x0227: 0xa1, // ¡
- 0x0228: 0x2a, // *
- 0x0229: 0x27, // '
- 0x022a: 0x2014, // —
- 0x022b: 0xa9, // ©
- 0x022c: 0x2120, // ℠
- 0x022d: 0x2022, // •
- 0x022e: 0x201c, // “
- 0x022f: 0x201d, // ”
- 0x0230: 0xc0, // À
- 0x0231: 0xc2, // Â
- 0x0232: 0xc7, // Ç
- 0x0233: 0xc8, // È
- 0x0234: 0xca, // Ê
- 0x0235: 0xcb, // Ë
- 0x0236: 0xeb, // ë
- 0x0237: 0xce, // Î
- 0x0238: 0xcf, // Ï
- 0x0239: 0xef, // ï
- 0x023a: 0xd4, // Ô
- 0x023b: 0xd9, // Ù
- 0x023c: 0xf9, // ù
- 0x023d: 0xdb, // Û
- 0x023e: 0xab, // «
- 0x023f: 0xbb, // »
- 0x0320: 0xc3, // Ã
- 0x0321: 0xe3, // ã
- 0x0322: 0xcd, // Í
- 0x0323: 0xcc, // Ì
- 0x0324: 0xec, // ì
- 0x0325: 0xd2, // Ò
- 0x0326: 0xf2, // ò
- 0x0327: 0xd5, // Õ
- 0x0328: 0xf5, // õ
- 0x0329: 0x7b, // {
- 0x032a: 0x7d, // }
- 0x032b: 0x5c, // \
- 0x032c: 0x5e, // ^
- 0x032d: 0x5f, // _
- 0x032e: 0x7c, // |
- 0x032f: 0x7e, // ~
- 0x0330: 0xc4, // Ä
- 0x0331: 0xe4, // ä
- 0x0332: 0xd6, // Ö
- 0x0333: 0xf6, // ö
- 0x0334: 0xdf, // ß
- 0x0335: 0xa5, // ¥
- 0x0336: 0xa4, // ¤
- 0x0337: 0x2502, // │
- 0x0338: 0xc5, // Å
- 0x0339: 0xe5, // å
- 0x033a: 0xd8, // Ø
- 0x033b: 0xf8, // ø
- 0x033c: 0x250c, // ┌
- 0x033d: 0x2510, // ┐
- 0x033e: 0x2514, // └
- 0x033f: 0x2518 // ┘
- };
-
- var getCharFromCode = function getCharFromCode(code) {
- if (code === null) {
- return '';
- }
- code = CHARACTER_TRANSLATION[code] || code;
- return String.fromCharCode(code);
- };
-
- // the index of the last row in a CEA-608 display buffer
- var BOTTOM_ROW = 14;
-
- // This array is used for mapping PACs -> row #, since there's no way of
- // getting it through bit logic.
- var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420];
-
- // CEA-608 captions are rendered onto a 34x15 matrix of character
- // cells. The "bottom" row is the last element in the outer array.
- var createDisplayBuffer = function createDisplayBuffer() {
- var result = [],
- i = BOTTOM_ROW + 1;
- while (i--) {
- result.push('');
- }
- return result;
- };
-
- var Cea608Stream = function Cea608Stream(field, dataChannel) {
- Cea608Stream.prototype.init.call(this);
-
- this.field_ = field || 0;
- this.dataChannel_ = dataChannel || 0;
-
- this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1);
-
- this.setConstants();
- this.reset();
-
- this.push = function (packet) {
- var data, swap, char0, char1, text;
- // remove the parity bits
- data = packet.ccData & 0x7f7f;
-
- // ignore duplicate control codes; the spec demands they're sent twice
- if (data === this.lastControlCode_) {
- this.lastControlCode_ = null;
- return;
- }
-
- // Store control codes
- if ((data & 0xf000) === 0x1000) {
- this.lastControlCode_ = data;
- } else if (data !== this.PADDING_) {
- this.lastControlCode_ = null;
- }
-
- char0 = data >>> 8;
- char1 = data & 0xff;
-
- if (data === this.PADDING_) {
- return;
- } else if (data === this.RESUME_CAPTION_LOADING_) {
- this.mode_ = 'popOn';
- } else if (data === this.END_OF_CAPTION_) {
- // If an EOC is received while in paint-on mode, the displayed caption
- // text should be swapped to non-displayed memory as if it was a pop-on
- // caption. Because of that, we should explicitly switch back to pop-on
- // mode
- this.mode_ = 'popOn';
- this.clearFormatting(packet.pts);
- // if a caption was being displayed, it's gone now
- this.flushDisplayed(packet.pts);
-
- // flip memory
- swap = this.displayed_;
- this.displayed_ = this.nonDisplayed_;
- this.nonDisplayed_ = swap;
-
- // start measuring the time to display the caption
- this.startPts_ = packet.pts;
- } else if (data === this.ROLL_UP_2_ROWS_) {
- this.rollUpRows_ = 2;
- this.setRollUp(packet.pts);
- } else if (data === this.ROLL_UP_3_ROWS_) {
- this.rollUpRows_ = 3;
- this.setRollUp(packet.pts);
- } else if (data === this.ROLL_UP_4_ROWS_) {
- this.rollUpRows_ = 4;
- this.setRollUp(packet.pts);
- } else if (data === this.CARRIAGE_RETURN_) {
- this.clearFormatting(packet.pts);
- this.flushDisplayed(packet.pts);
- this.shiftRowsUp_();
- this.startPts_ = packet.pts;
- } else if (data === this.BACKSPACE_) {
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
- } else {
- this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
- }
- } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
- this.flushDisplayed(packet.pts);
- this.displayed_ = createDisplayBuffer();
- } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
- this.nonDisplayed_ = createDisplayBuffer();
- } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
- if (this.mode_ !== 'paintOn') {
- // NOTE: This should be removed when proper caption positioning is
- // implemented
- this.flushDisplayed(packet.pts);
- this.displayed_ = createDisplayBuffer();
- }
- this.mode_ = 'paintOn';
- this.startPts_ = packet.pts;
-
- // Append special characters to caption text
- } else if (this.isSpecialCharacter(char0, char1)) {
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
-
- // Append extended characters to caption text
- } else if (this.isExtCharacter(char0, char1)) {
- // Extended characters always follow their "non-extended" equivalents.
- // IE if a "è" is desired, you'll always receive "eè"; non-compliant
- // decoders are supposed to drop the "è", while compliant decoders
- // backspace the "e" and insert "è".
-
- // Delete the previous character
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
- } else {
- this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
- }
-
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
-
- // Process mid-row codes
- } else if (this.isMidRowCode(char0, char1)) {
- // Attributes are not additive, so clear all formatting
- this.clearFormatting(packet.pts);
-
- // According to the standard, mid-row codes
- // should be replaced with spaces, so add one now
- this[this.mode_](packet.pts, ' ');
- this.column_++;
-
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
-
- if ((char1 & 0x1) === 0x1) {
- this.addFormatting(packet.pts, ['u']);
- }
-
- // Detect offset control codes and adjust cursor
- } else if (this.isOffsetControlCode(char0, char1)) {
- // Cursor position is set by indent PAC (see below) in 4-column
- // increments, with an additional offset code of 1-3 to reach any
- // of the 32 columns specified by CEA-608. So all we need to do
- // here is increment the column cursor by the given offset.
- this.column_ += char1 & 0x03;
-
- // Detect PACs (Preamble Address Codes)
- } else if (this.isPAC(char0, char1)) {
-
- // There's no logic for PAC -> row mapping, so we have to just
- // find the row code in an array and use its index :(
- var row = ROWS.indexOf(data & 0x1f20);
-
- // Configure the caption window if we're in roll-up mode
- if (this.mode_ === 'rollUp') {
- this.setRollUp(packet.pts, row);
- }
-
- if (row !== this.row_) {
- // formatting is only persistent for current row
- this.clearFormatting(packet.pts);
- this.row_ = row;
- }
- // All PACs can apply underline, so detect and apply
- // (All odd-numbered second bytes set underline)
- if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) {
- this.addFormatting(packet.pts, ['u']);
- }
-
- if ((data & 0x10) === 0x10) {
- // We've got an indent level code. Each successive even number
- // increments the column cursor by 4, so we can get the desired
- // column position by bit-shifting to the right (to get n/2)
- // and multiplying by 4.
- this.column_ = ((data & 0xe) >> 1) * 4;
- }
-
- if (this.isColorPAC(char1)) {
- // it's a color code, though we only support white, which
- // can be either normal or italicized. white italics can be
- // either 0x4e or 0x6e depending on the row, so we just
- // bitwise-and with 0xe to see if italics should be turned on
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
- }
-
- // We have a normal character in char0, and possibly one in char1
- } else if (this.isNormalChar(char0)) {
- if (char1 === 0x00) {
- char1 = null;
- }
- text = getCharFromCode(char0);
- text += getCharFromCode(char1);
- this[this.mode_](packet.pts, text);
- this.column_ += text.length;
- } // finish data processing
- };
- };
- Cea608Stream.prototype = new stream();
- // Trigger a cue point that captures the current state of the
- // display buffer
- Cea608Stream.prototype.flushDisplayed = function (pts) {
- var content = this.displayed_
- // remove spaces from the start and end of the string
- .map(function (row) {
- return row.trim();
- })
- // combine all text rows to display in one cue
- .join('\n')
- // and remove blank rows from the start and end, but not the middle
- .replace(/^\n+|\n+$/g, '');
-
- if (content.length) {
- this.trigger('data', {
- startPts: this.startPts_,
- endPts: pts,
- text: content,
- stream: this.name_
- });
- }
- };
-
- /**
- * Zero out the data, used for startup and on seek
- */
- Cea608Stream.prototype.reset = function () {
- this.mode_ = 'popOn';
- // When in roll-up mode, the index of the last row that will
- // actually display captions. If a caption is shifted to a row
- // with a lower index than this, it is cleared from the display
- // buffer
- this.topRow_ = 0;
- this.startPts_ = 0;
- this.displayed_ = createDisplayBuffer();
- this.nonDisplayed_ = createDisplayBuffer();
- this.lastControlCode_ = null;
-
- // Track row and column for proper line-breaking and spacing
- this.column_ = 0;
- this.row_ = BOTTOM_ROW;
- this.rollUpRows_ = 2;
-
- // This variable holds currently-applied formatting
- this.formatting_ = [];
- };
-
- /**
- * Sets up control code and related constants for this instance
- */
- Cea608Stream.prototype.setConstants = function () {
- // The following attributes have these uses:
- // ext_ : char0 for mid-row codes, and the base for extended
- // chars (ext_+0, ext_+1, and ext_+2 are char0s for
- // extended codes)
- // control_: char0 for control codes, except byte-shifted to the
- // left so that we can do this.control_ | CONTROL_CODE
- // offset_: char0 for tab offset codes
- //
- // It's also worth noting that control codes, and _only_ control codes,
- // differ between field 1 and field2. Field 2 control codes are always
- // their field 1 value plus 1. That's why there's the "| field" on the
- // control value.
- if (this.dataChannel_ === 0) {
- this.BASE_ = 0x10;
- this.EXT_ = 0x11;
- this.CONTROL_ = (0x14 | this.field_) << 8;
- this.OFFSET_ = 0x17;
- } else if (this.dataChannel_ === 1) {
- this.BASE_ = 0x18;
- this.EXT_ = 0x19;
- this.CONTROL_ = (0x1c | this.field_) << 8;
- this.OFFSET_ = 0x1f;
- }
-
- // Constants for the LSByte command codes recognized by Cea608Stream. This
- // list is not exhaustive. For a more comprehensive listing and semantics see
- // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
- // Padding
- this.PADDING_ = 0x0000;
- // Pop-on Mode
- this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
- this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f;
- // Roll-up Mode
- this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
- this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
- this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
- this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d;
- // paint-on mode
- this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29;
- // Erasure
- this.BACKSPACE_ = this.CONTROL_ | 0x21;
- this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
- this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
- };
-
- /**
- * Detects if the 2-byte packet data is a special character
- *
- * Special characters have a second byte in the range 0x30 to 0x3f,
- * with the first byte being 0x11 (for data channel 1) or 0x19 (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an special character
- */
- Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) {
- return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f;
- };
-
- /**
- * Detects if the 2-byte packet data is an extended character
- *
- * Extended characters have a second byte in the range 0x20 to 0x3f,
- * with the first byte being 0x12 or 0x13 (for data channel 1) or
- * 0x1a or 0x1b (for data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an extended character
- */
- Cea608Stream.prototype.isExtCharacter = function (char0, char1) {
- return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f;
- };
-
- /**
- * Detects if the 2-byte packet is a mid-row code
- *
- * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
- * the first byte being 0x11 (for data channel 1) or 0x19 (for data
- * channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a mid-row code
- */
- Cea608Stream.prototype.isMidRowCode = function (char0, char1) {
- return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f;
- };
-
- /**
- * Detects if the 2-byte packet is an offset control code
- *
- * Offset control codes have a second byte in the range 0x21 to 0x23,
- * with the first byte being 0x17 (for data channel 1) or 0x1f (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an offset control code
- */
- Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) {
- return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23;
- };
-
- /**
- * Detects if the 2-byte packet is a Preamble Address Code
- *
- * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
- * or 0x18 to 0x1f (for data channel 2), with the second byte in the
- * range 0x40 to 0x7f.
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a PAC
- */
- Cea608Stream.prototype.isPAC = function (char0, char1) {
- return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f;
- };
-
- /**
- * Detects if a packet's second byte is in the range of a PAC color code
- *
- * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
- * 0x60 to 0x6f.
- *
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the byte is a color PAC
- */
- Cea608Stream.prototype.isColorPAC = function (char1) {
- return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f;
- };
-
- /**
- * Detects if a single byte is in the range of a normal character
- *
- * Normal text bytes are in the range 0x20 to 0x7f.
- *
- * @param {Integer} char The byte
- * @return {Boolean} Whether the byte is a normal character
- */
- Cea608Stream.prototype.isNormalChar = function (char) {
- return char >= 0x20 && char <= 0x7f;
- };
-
- /**
- * Configures roll-up
- *
- * @param {Integer} pts Current PTS
- * @param {Integer} newBaseRow Used by PACs to slide the current window to
- * a new position
- */
- Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) {
- // Reset the base row to the bottom row when switching modes
- if (this.mode_ !== 'rollUp') {
- this.row_ = BOTTOM_ROW;
- this.mode_ = 'rollUp';
- // Spec says to wipe memories when switching to roll-up
- this.flushDisplayed(pts);
- this.nonDisplayed_ = createDisplayBuffer();
- this.displayed_ = createDisplayBuffer();
- }
-
- if (newBaseRow !== undefined && newBaseRow !== this.row_) {
- // move currently displayed captions (up or down) to the new base row
- for (var i = 0; i < this.rollUpRows_; i++) {
- this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
- this.displayed_[this.row_ - i] = '';
- }
- }
-
- if (newBaseRow === undefined) {
- newBaseRow = this.row_;
- }
- this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
- };
-
- // Adds the opening HTML tag for the passed character to the caption text,
- // and keeps track of it for later closing
- Cea608Stream.prototype.addFormatting = function (pts, format) {
- this.formatting_ = this.formatting_.concat(format);
- var text = format.reduce(function (text, format) {
- return text + '<' + format + '>';
- }, '');
- this[this.mode_](pts, text);
- };
-
- // Adds HTML closing tags for current formatting to caption text and
- // clears remembered formatting
- Cea608Stream.prototype.clearFormatting = function (pts) {
- if (!this.formatting_.length) {
- return;
- }
- var text = this.formatting_.reverse().reduce(function (text, format) {
- return text + '</' + format + '>';
- }, '');
- this.formatting_ = [];
- this[this.mode_](pts, text);
- };
-
- // Mode Implementations
- Cea608Stream.prototype.popOn = function (pts, text) {
- var baseRow = this.nonDisplayed_[this.row_];
-
- // buffer characters
- baseRow += text;
- this.nonDisplayed_[this.row_] = baseRow;
- };
-
- Cea608Stream.prototype.rollUp = function (pts, text) {
- var baseRow = this.displayed_[this.row_];
-
- baseRow += text;
- this.displayed_[this.row_] = baseRow;
- };
-
- Cea608Stream.prototype.shiftRowsUp_ = function () {
- var i;
- // clear out inactive rows
- for (i = 0; i < this.topRow_; i++) {
- this.displayed_[i] = '';
- }
- for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
- this.displayed_[i] = '';
- }
- // shift displayed rows up
- for (i = this.topRow_; i < this.row_; i++) {
- this.displayed_[i] = this.displayed_[i + 1];
- }
- // clear out the bottom row
- this.displayed_[this.row_] = '';
- };
-
- Cea608Stream.prototype.paintOn = function (pts, text) {
- var baseRow = this.displayed_[this.row_];
-
- baseRow += text;
- this.displayed_[this.row_] = baseRow;
- };
-
- // exports
- var captionStream = {
- CaptionStream: CaptionStream,
- Cea608Stream: Cea608Stream
- };
-
- var streamTypes = {
- H264_STREAM_TYPE: 0x1B,
- ADTS_STREAM_TYPE: 0x0F,
- METADATA_STREAM_TYPE: 0x15
- };
-
- var MAX_TS = 8589934592;
-
- var RO_THRESH = 4294967296;
-
- var handleRollover = function handleRollover(value, reference) {
- var direction = 1;
-
- if (value > reference) {
- // If the current timestamp value is greater than our reference timestamp and we detect a
- // timestamp rollover, this means the roll over is happening in the opposite direction.
- // Example scenario: Enter a long stream/video just after a rollover occurred. The reference
- // point will be set to a small number, e.g. 1. The user then seeks backwards over the
- // rollover point. In loading this segment, the timestamp values will be very large,
- // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust
- // the time stamp to be `value - 2^33`.
- direction = -1;
- }
-
- // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will
- // cause an incorrect adjustment.
- while (Math.abs(reference - value) > RO_THRESH) {
- value += direction * MAX_TS;
- }
-
- return value;
- };
-
- var TimestampRolloverStream = function TimestampRolloverStream(type) {
- var lastDTS, referenceDTS;
-
- TimestampRolloverStream.prototype.init.call(this);
-
- this.type_ = type;
-
- this.push = function (data) {
- if (data.type !== this.type_) {
- return;
- }
-
- if (referenceDTS === undefined) {
- referenceDTS = data.dts;
- }
-
- data.dts = handleRollover(data.dts, referenceDTS);
- data.pts = handleRollover(data.pts, referenceDTS);
-
- lastDTS = data.dts;
-
- this.trigger('data', data);
- };
-
- this.flush = function () {
- referenceDTS = lastDTS;
- this.trigger('done');
- };
-
- this.discontinuity = function () {
- referenceDTS = void 0;
- lastDTS = void 0;
- };
- };
-
- TimestampRolloverStream.prototype = new stream();
-
- var timestampRolloverStream = {
- TimestampRolloverStream: TimestampRolloverStream,
- handleRollover: handleRollover
- };
-
- var percentEncode = function percentEncode(bytes, start, end) {
- var i,
- result = '';
- for (i = start; i < end; i++) {
- result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
- }
- return result;
- },
-
-
- // return the string representation of the specified byte range,
- // interpreted as UTf-8.
- parseUtf8 = function parseUtf8(bytes, start, end) {
- return decodeURIComponent(percentEncode(bytes, start, end));
- },
-
-
- // return the string representation of the specified byte range,
- // interpreted as ISO-8859-1.
- parseIso88591 = function parseIso88591(bytes, start, end) {
- return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
- },
- parseSyncSafeInteger = function parseSyncSafeInteger(data) {
- return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
- },
- tagParsers = {
- TXXX: function TXXX(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
-
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the text fields
- tag.description = parseUtf8(tag.data, 1, i);
- // do not include the null terminator in the tag value
- tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, '');
- break;
- }
- }
- tag.data = tag.value;
- },
- WXXX: function WXXX(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
-
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.description = parseUtf8(tag.data, 1, i);
- tag.url = parseUtf8(tag.data, i + 1, tag.data.length);
- break;
- }
- }
- },
- PRIV: function PRIV(tag) {
- var i;
-
- for (i = 0; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.owner = parseIso88591(tag.data, 0, i);
- break;
- }
- }
- tag.privateData = tag.data.subarray(i + 1);
- tag.data = tag.privateData;
- }
- },
- _MetadataStream;
-
- _MetadataStream = function MetadataStream(options) {
- var settings = {
- debug: !!(options && options.debug),
-
- // the bytes of the program-level descriptor field in MP2T
- // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
- // program element descriptors"
- descriptor: options && options.descriptor
- },
-
-
- // the total size in bytes of the ID3 tag being parsed
- tagSize = 0,
-
-
- // tag data that is not complete enough to be parsed
- buffer = [],
-
-
- // the total number of bytes currently in the buffer
- bufferSize = 0,
- i;
-
- _MetadataStream.prototype.init.call(this);
-
- // calculate the text track in-band metadata track dispatch type
- // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
- this.dispatchType = streamTypes.METADATA_STREAM_TYPE.toString(16);
- if (settings.descriptor) {
- for (i = 0; i < settings.descriptor.length; i++) {
- this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
- }
- }
-
- this.push = function (chunk) {
- var tag, frameStart, frameSize, frame, i, frameHeader;
- if (chunk.type !== 'timed-metadata') {
- return;
- }
-
- // if data_alignment_indicator is set in the PES header,
- // we must have the start of a new ID3 tag. Assume anything
- // remaining in the buffer was malformed and throw it out
- if (chunk.dataAlignmentIndicator) {
- bufferSize = 0;
- buffer.length = 0;
- }
-
- // ignore events that don't look like ID3 data
- if (buffer.length === 0 && (chunk.data.length < 10 || chunk.data[0] !== 'I'.charCodeAt(0) || chunk.data[1] !== 'D'.charCodeAt(0) || chunk.data[2] !== '3'.charCodeAt(0))) {
- if (settings.debug) {
- // eslint-disable-next-line no-console
- console.log('Skipping unrecognized metadata packet');
- }
- return;
- }
-
- // add this chunk to the data we've collected so far
-
- buffer.push(chunk);
- bufferSize += chunk.data.byteLength;
-
- // grab the size of the entire frame from the ID3 header
- if (buffer.length === 1) {
- // the frame size is transmitted as a 28-bit integer in the
- // last four bytes of the ID3 header.
- // The most significant bit of each byte is dropped and the
- // results concatenated to recover the actual value.
- tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10));
-
- // ID3 reports the tag size excluding the header but it's more
- // convenient for our comparisons to include it
- tagSize += 10;
- }
-
- // if the entire frame has not arrived, wait for more data
- if (bufferSize < tagSize) {
- return;
- }
-
- // collect the entire frame so it can be parsed
- tag = {
- data: new Uint8Array(tagSize),
- frames: [],
- pts: buffer[0].pts,
- dts: buffer[0].dts
- };
- for (i = 0; i < tagSize;) {
- tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
- i += buffer[0].data.byteLength;
- bufferSize -= buffer[0].data.byteLength;
- buffer.shift();
- }
-
- // find the start of the first frame and the end of the tag
- frameStart = 10;
- if (tag.data[5] & 0x40) {
- // advance the frame start past the extended header
- frameStart += 4; // header size field
- frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14));
-
- // clip any padding off the end
- tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20));
- }
-
- // parse one or more ID3 frames
- // http://id3.org/id3v2.3.0#ID3v2_frame_overview
- do {
- // determine the number of bytes in this frame
- frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
- if (frameSize < 1) {
- // eslint-disable-next-line no-console
- return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
- }
- frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]);
-
- frame = {
- id: frameHeader,
- data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
- };
- frame.key = frame.id;
- if (tagParsers[frame.id]) {
- tagParsers[frame.id](frame);
-
- // handle the special PRIV frame used to indicate the start
- // time for raw AAC data
- if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
- var d = frame.data,
- size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
-
- size *= 4;
- size += d[7] & 0x03;
- frame.timeStamp = size;
- // in raw AAC, all subsequent data will be timestamped based
- // on the value of this frame
- // we couldn't have known the appropriate pts and dts before
- // parsing this ID3 tag so set those values now
- if (tag.pts === undefined && tag.dts === undefined) {
- tag.pts = frame.timeStamp;
- tag.dts = frame.timeStamp;
- }
- this.trigger('timestamp', frame);
- }
- }
- tag.frames.push(frame);
-
- frameStart += 10; // advance past the frame header
- frameStart += frameSize; // advance past the frame body
- } while (frameStart < tagSize);
- this.trigger('data', tag);
- };
- };
- _MetadataStream.prototype = new stream();
-
- var metadataStream = _MetadataStream;
-
- var TimestampRolloverStream$1 = timestampRolloverStream.TimestampRolloverStream;
-
- // object types
- var _TransportPacketStream, _TransportParseStream, _ElementaryStream;
-
- // constants
- var MP2T_PACKET_LENGTH = 188,
-
- // bytes
- SYNC_BYTE = 0x47;
-
- /**
- * Splits an incoming stream of binary data into MPEG-2 Transport
- * Stream packets.
- */
- _TransportPacketStream = function TransportPacketStream() {
- var buffer = new Uint8Array(MP2T_PACKET_LENGTH),
- bytesInBuffer = 0;
-
- _TransportPacketStream.prototype.init.call(this);
-
- // Deliver new bytes to the stream.
-
- /**
- * Split a stream of data into M2TS packets
- **/
- this.push = function (bytes) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH,
- everything;
-
- // If there are bytes remaining from the last segment, prepend them to the
- // bytes that were pushed in
- if (bytesInBuffer) {
- everything = new Uint8Array(bytes.byteLength + bytesInBuffer);
- everything.set(buffer.subarray(0, bytesInBuffer));
- everything.set(bytes, bytesInBuffer);
- bytesInBuffer = 0;
- } else {
- everything = bytes;
- }
-
- // While we have enough data for a packet
- while (endIndex < everything.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) {
- // We found a packet so emit it and jump one whole packet forward in
- // the stream
- this.trigger('data', everything.subarray(startIndex, endIndex));
- startIndex += MP2T_PACKET_LENGTH;
- endIndex += MP2T_PACKET_LENGTH;
- continue;
- }
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- }
-
- // If there was some data left over at the end of the segment that couldn't
- // possibly be a whole packet, keep it because it might be the start of a packet
- // that continues in the next segment
- if (startIndex < everything.byteLength) {
- buffer.set(everything.subarray(startIndex), 0);
- bytesInBuffer = everything.byteLength - startIndex;
- }
- };
-
- /**
- * Passes identified M2TS packets to the TransportParseStream to be parsed
- **/
- this.flush = function () {
- // If the buffer contains a whole packet when we are being flushed, emit it
- // and empty the buffer. Otherwise hold onto the data because it may be
- // important for decoding the next segment
- if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) {
- this.trigger('data', buffer);
- bytesInBuffer = 0;
- }
- this.trigger('done');
- };
- };
- _TransportPacketStream.prototype = new stream();
-
- /**
- * Accepts an MP2T TransportPacketStream and emits data events with parsed
- * forms of the individual transport stream packets.
- */
- _TransportParseStream = function TransportParseStream() {
- var parsePsi, parsePat, parsePmt, self;
- _TransportParseStream.prototype.init.call(this);
- self = this;
-
- this.packetsWaitingForPmt = [];
- this.programMapTable = undefined;
-
- parsePsi = function parsePsi(payload, psi) {
- var offset = 0;
-
- // PSI packets may be split into multiple sections and those
- // sections may be split into multiple packets. If a PSI
- // section starts in this packet, the payload_unit_start_indicator
- // will be true and the first byte of the payload will indicate
- // the offset from the current position to the start of the
- // section.
- if (psi.payloadUnitStartIndicator) {
- offset += payload[offset] + 1;
- }
-
- if (psi.type === 'pat') {
- parsePat(payload.subarray(offset), psi);
- } else {
- parsePmt(payload.subarray(offset), psi);
- }
- };
-
- parsePat = function parsePat(payload, pat) {
- pat.section_number = payload[7]; // eslint-disable-line camelcase
- pat.last_section_number = payload[8]; // eslint-disable-line camelcase
-
- // skip the PSI header and parse the first PMT entry
- self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
- pat.pmtPid = self.pmtPid;
- };
-
- /**
- * Parse out the relevant fields of a Program Map Table (PMT).
- * @param payload {Uint8Array} the PMT-specific portion of an MP2T
- * packet. The first byte in this array should be the table_id
- * field.
- * @param pmt {object} the object that should be decorated with
- * fields parsed from the PMT.
- */
- parsePmt = function parsePmt(payload, pmt) {
- var sectionLength, tableEnd, programInfoLength, offset;
-
- // PMTs can be sent ahead of the time when they should actually
- // take effect. We don't believe this should ever be the case
- // for HLS but we'll ignore "forward" PMT declarations if we see
- // them. Future PMT declarations have the current_next_indicator
- // set to zero.
- if (!(payload[5] & 0x01)) {
- return;
- }
-
- // overwrite any existing program map table
- self.programMapTable = {
- video: null,
- audio: null,
- 'timed-metadata': {}
- };
-
- // the mapping table ends at the end of the current section
- sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
- tableEnd = 3 + sectionLength - 4;
-
- // to determine where the table is, we have to figure out how
- // long the program info descriptors are
- programInfoLength = (payload[10] & 0x0f) << 8 | payload[11];
-
- // advance the offset to the first entry in the mapping table
- offset = 12 + programInfoLength;
- while (offset < tableEnd) {
- var streamType = payload[offset];
- var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2];
-
- // only map a single elementary_pid for audio and video stream types
- // TODO: should this be done for metadata too? for now maintain behavior of
- // multiple metadata streams
- if (streamType === streamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) {
- self.programMapTable.video = pid;
- } else if (streamType === streamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) {
- self.programMapTable.audio = pid;
- } else if (streamType === streamTypes.METADATA_STREAM_TYPE) {
- // map pid to stream type for metadata streams
- self.programMapTable['timed-metadata'][pid] = streamType;
- }
-
- // move to the next table entry
- // skip past the elementary stream descriptors, if present
- offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
- }
-
- // record the map on the packet as well
- pmt.programMapTable = self.programMapTable;
- };
-
- /**
- * Deliver a new MP2T packet to the next stream in the pipeline.
- */
- this.push = function (packet) {
- var result = {},
- offset = 4;
-
- result.payloadUnitStartIndicator = !!(packet[1] & 0x40);
-
- // pid is a 13-bit field starting at the last bit of packet[1]
- result.pid = packet[1] & 0x1f;
- result.pid <<= 8;
- result.pid |= packet[2];
-
- // if an adaption field is present, its length is specified by the
- // fifth byte of the TS packet header. The adaptation field is
- // used to add stuffing to PES packets that don't fill a complete
- // TS packet, and to specify some forms of timing and control data
- // that we do not currently use.
- if ((packet[3] & 0x30) >>> 4 > 0x01) {
- offset += packet[offset] + 1;
- }
-
- // parse the rest of the packet based on the type
- if (result.pid === 0) {
- result.type = 'pat';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result);
- } else if (result.pid === this.pmtPid) {
- result.type = 'pmt';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result);
-
- // if there are any packets waiting for a PMT to be found, process them now
- while (this.packetsWaitingForPmt.length) {
- this.processPes_.apply(this, this.packetsWaitingForPmt.shift());
- }
- } else if (this.programMapTable === undefined) {
- // When we have not seen a PMT yet, defer further processing of
- // PES packets until one has been parsed
- this.packetsWaitingForPmt.push([packet, offset, result]);
- } else {
- this.processPes_(packet, offset, result);
- }
- };
-
- this.processPes_ = function (packet, offset, result) {
- // set the appropriate stream type
- if (result.pid === this.programMapTable.video) {
- result.streamType = streamTypes.H264_STREAM_TYPE;
- } else if (result.pid === this.programMapTable.audio) {
- result.streamType = streamTypes.ADTS_STREAM_TYPE;
- } else {
- // if not video or audio, it is timed-metadata or unknown
- // if unknown, streamType will be undefined
- result.streamType = this.programMapTable['timed-metadata'][result.pid];
- }
-
- result.type = 'pes';
- result.data = packet.subarray(offset);
-
- this.trigger('data', result);
- };
- };
- _TransportParseStream.prototype = new stream();
- _TransportParseStream.STREAM_TYPES = {
- h264: 0x1b,
- adts: 0x0f
- };
-
- /**
- * Reconsistutes program elementary stream (PES) packets from parsed
- * transport stream packets. That is, if you pipe an
- * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output
- * events will be events which capture the bytes for individual PES
- * packets plus relevant metadata that has been extracted from the
- * container.
- */
- _ElementaryStream = function ElementaryStream() {
- var self = this,
-
-
- // PES packet fragments
- video = {
- data: [],
- size: 0
- },
- audio = {
- data: [],
- size: 0
- },
- timedMetadata = {
- data: [],
- size: 0
- },
- parsePes = function parsePes(payload, pes) {
- var ptsDtsFlags;
-
- // get the packet length, this will be 0 for video
- pes.packetLength = 6 + (payload[4] << 8 | payload[5]);
-
- // find out if this packets starts a new keyframe
- pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0;
- // PES packets may be annotated with a PTS value, or a PTS value
- // and a DTS value. Determine what combination of values is
- // available to work with.
- ptsDtsFlags = payload[7];
-
- // PTS and DTS are normally stored as a 33-bit number. Javascript
- // performs all bitwise operations on 32-bit integers but javascript
- // supports a much greater range (52-bits) of integer using standard
- // mathematical operations.
- // We construct a 31-bit value using bitwise operators over the 31
- // most significant bits and then multiply by 4 (equal to a left-shift
- // of 2) before we add the final 2 least significant bits of the
- // timestamp (equal to an OR.)
- if (ptsDtsFlags & 0xC0) {
- // the PTS and DTS are not written out directly. For information
- // on how they are encoded, see
- // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
- pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3;
- pes.pts *= 4; // Left shift by 2
- pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs
- pes.dts = pes.pts;
- if (ptsDtsFlags & 0x40) {
- pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3;
- pes.dts *= 4; // Left shift by 2
- pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs
- }
- }
- // the data section starts immediately after the PES header.
- // pes_header_data_length specifies the number of header bytes
- // that follow the last byte of the field.
- pes.data = payload.subarray(9 + payload[8]);
- },
-
-
- /**
- * Pass completely parsed PES packets to the next stream in the pipeline
- **/
- flushStream = function flushStream(stream$$1, type, forceFlush) {
- var packetData = new Uint8Array(stream$$1.size),
- event = {
- type: type
- },
- i = 0,
- offset = 0,
- packetFlushable = false,
- fragment;
-
- // do nothing if there is not enough buffered data for a complete
- // PES header
- if (!stream$$1.data.length || stream$$1.size < 9) {
- return;
- }
- event.trackId = stream$$1.data[0].pid;
-
- // reassemble the packet
- for (i = 0; i < stream$$1.data.length; i++) {
- fragment = stream$$1.data[i];
-
- packetData.set(fragment.data, offset);
- offset += fragment.data.byteLength;
- }
-
- // parse assembled packet's PES header
- parsePes(packetData, event);
-
- // non-video PES packets MUST have a non-zero PES_packet_length
- // check that there is enough stream data to fill the packet
- packetFlushable = type === 'video' || event.packetLength <= stream$$1.size;
-
- // flush pending packets if the conditions are right
- if (forceFlush || packetFlushable) {
- stream$$1.size = 0;
- stream$$1.data.length = 0;
- }
-
- // only emit packets that are complete. this is to avoid assembling
- // incomplete PES packets due to poor segmentation
- if (packetFlushable) {
- self.trigger('data', event);
- }
- };
-
- _ElementaryStream.prototype.init.call(this);
-
- /**
- * Identifies M2TS packet types and parses PES packets using metadata
- * parsed from the PMT
- **/
- this.push = function (data) {
- ({
- pat: function pat() {
- // we have to wait for the PMT to arrive as well before we
- // have any meaningful metadata
- },
- pes: function pes() {
- var stream$$1, streamType;
-
- switch (data.streamType) {
- case streamTypes.H264_STREAM_TYPE:
- case streamTypes.H264_STREAM_TYPE:
- stream$$1 = video;
- streamType = 'video';
- break;
- case streamTypes.ADTS_STREAM_TYPE:
- stream$$1 = audio;
- streamType = 'audio';
- break;
- case streamTypes.METADATA_STREAM_TYPE:
- stream$$1 = timedMetadata;
- streamType = 'timed-metadata';
- break;
- default:
- // ignore unknown stream types
- return;
- }
-
- // if a new packet is starting, we can flush the completed
- // packet
- if (data.payloadUnitStartIndicator) {
- flushStream(stream$$1, streamType, true);
- }
-
- // buffer this fragment until we are sure we've received the
- // complete payload
- stream$$1.data.push(data);
- stream$$1.size += data.data.byteLength;
- },
- pmt: function pmt() {
- var event = {
- type: 'metadata',
- tracks: []
- },
- programMapTable = data.programMapTable;
-
- // translate audio and video streams to tracks
- if (programMapTable.video !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.video,
- codec: 'avc',
- type: 'video'
- });
- }
- if (programMapTable.audio !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.audio,
- codec: 'adts',
- type: 'audio'
- });
- }
-
- self.trigger('data', event);
- }
- })[data.type]();
- };
-
- /**
- * Flush any remaining input. Video PES packets may be of variable
- * length. Normally, the start of a new video packet can trigger the
- * finalization of the previous packet. That is not possible if no
- * more video is forthcoming, however. In that case, some other
- * mechanism (like the end of the file) has to be employed. When it is
- * clear that no additional data is forthcoming, calling this method
- * will flush the buffered packets.
- */
- this.flush = function () {
- // !!THIS ORDER IS IMPORTANT!!
- // video first then audio
- flushStream(video, 'video');
- flushStream(audio, 'audio');
- flushStream(timedMetadata, 'timed-metadata');
- this.trigger('done');
- };
- };
- _ElementaryStream.prototype = new stream();
-
- var m2ts = {
- PAT_PID: 0x0000,
- MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
- TransportPacketStream: _TransportPacketStream,
- TransportParseStream: _TransportParseStream,
- ElementaryStream: _ElementaryStream,
- TimestampRolloverStream: TimestampRolloverStream$1,
- CaptionStream: captionStream.CaptionStream,
- Cea608Stream: captionStream.Cea608Stream,
- MetadataStream: metadataStream
- };
-
- for (var type in streamTypes) {
- if (streamTypes.hasOwnProperty(type)) {
- m2ts[type] = streamTypes[type];
- }
- }
-
- var m2ts_1 = m2ts;
-
- var _AdtsStream;
-
- var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
-
- /*
- * Accepts a ElementaryStream and emits data events with parsed
- * AAC Audio Frames of the individual packets. Input audio in ADTS
- * format is unpacked and re-emitted as AAC frames.
- *
- * @see http://wiki.multimedia.cx/index.php?title=ADTS
- * @see http://wiki.multimedia.cx/?title=Understanding_AAC
- */
- _AdtsStream = function AdtsStream() {
- var buffer;
-
- _AdtsStream.prototype.init.call(this);
-
- this.push = function (packet) {
- var i = 0,
- frameNum = 0,
- frameLength,
- protectionSkipBytes,
- frameEnd,
- oldBuffer,
- sampleCount,
- adtsFrameDuration;
-
- if (packet.type !== 'audio') {
- // ignore non-audio data
- return;
- }
-
- // Prepend any data in the buffer to the input data so that we can parse
- // aac frames the cross a PES packet boundary
- if (buffer) {
- oldBuffer = buffer;
- buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength);
- buffer.set(oldBuffer);
- buffer.set(packet.data, oldBuffer.byteLength);
- } else {
- buffer = packet.data;
- }
-
- // unpack any ADTS frames which have been fully received
- // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS
- while (i + 5 < buffer.length) {
-
- // Loook for the start of an ADTS header..
- if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) {
- // If a valid header was not found, jump one forward and attempt to
- // find a valid ADTS header starting at the next byte
- i++;
- continue;
- }
-
- // The protection skip bit tells us if we have 2 bytes of CRC data at the
- // end of the ADTS header
- protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2;
-
- // Frame length is a 13 bit integer starting 16 bits from the
- // end of the sync sequence
- frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5;
-
- sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024;
- adtsFrameDuration = sampleCount * 90000 / ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2];
-
- frameEnd = i + frameLength;
-
- // If we don't have enough data to actually finish this ADTS frame, return
- // and wait for more data
- if (buffer.byteLength < frameEnd) {
- return;
- }
-
- // Otherwise, deliver the complete AAC frame
- this.trigger('data', {
- pts: packet.pts + frameNum * adtsFrameDuration,
- dts: packet.dts + frameNum * adtsFrameDuration,
- sampleCount: sampleCount,
- audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1,
- channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6,
- samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2],
- samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2,
- // assume ISO/IEC 14496-12 AudioSampleEntry default of 16
- samplesize: 16,
- data: buffer.subarray(i + 7 + protectionSkipBytes, frameEnd)
- });
-
- // If the buffer is empty, clear it and return
- if (buffer.byteLength === frameEnd) {
- buffer = undefined;
- return;
- }
-
- frameNum++;
-
- // Remove the finished frame from the buffer and start the process again
- buffer = buffer.subarray(frameEnd);
- }
- };
- this.flush = function () {
- this.trigger('done');
- };
- };
-
- _AdtsStream.prototype = new stream();
-
- var adts = _AdtsStream;
-
- var ExpGolomb;
-
- /**
- * Parser for exponential Golomb codes, a variable-bitwidth number encoding
- * scheme used by h264.
- */
- ExpGolomb = function ExpGolomb(workingData) {
- var
- // the number of bytes left to examine in workingData
- workingBytesAvailable = workingData.byteLength,
-
-
- // the current word being examined
- workingWord = 0,
-
- // :uint
-
- // the number of bits left to examine in the current word
- workingBitsAvailable = 0; // :uint;
-
- // ():uint
- this.length = function () {
- return 8 * workingBytesAvailable;
- };
-
- // ():uint
- this.bitsAvailable = function () {
- return 8 * workingBytesAvailable + workingBitsAvailable;
- };
-
- // ():void
- this.loadWord = function () {
- var position = workingData.byteLength - workingBytesAvailable,
- workingBytes = new Uint8Array(4),
- availableBytes = Math.min(4, workingBytesAvailable);
-
- if (availableBytes === 0) {
- throw new Error('no bytes available');
- }
-
- workingBytes.set(workingData.subarray(position, position + availableBytes));
- workingWord = new DataView(workingBytes.buffer).getUint32(0);
-
- // track the amount of workingData that has been processed
- workingBitsAvailable = availableBytes * 8;
- workingBytesAvailable -= availableBytes;
- };
-
- // (count:int):void
- this.skipBits = function (count) {
- var skipBytes; // :int
- if (workingBitsAvailable > count) {
- workingWord <<= count;
- workingBitsAvailable -= count;
- } else {
- count -= workingBitsAvailable;
- skipBytes = Math.floor(count / 8);
-
- count -= skipBytes * 8;
- workingBytesAvailable -= skipBytes;
-
- this.loadWord();
-
- workingWord <<= count;
- workingBitsAvailable -= count;
- }
- };
-
- // (size:int):uint
- this.readBits = function (size) {
- var bits = Math.min(workingBitsAvailable, size),
-
- // :uint
- valu = workingWord >>> 32 - bits; // :uint
- // if size > 31, handle error
- workingBitsAvailable -= bits;
- if (workingBitsAvailable > 0) {
- workingWord <<= bits;
- } else if (workingBytesAvailable > 0) {
- this.loadWord();
- }
-
- bits = size - bits;
- if (bits > 0) {
- return valu << bits | this.readBits(bits);
- }
- return valu;
- };
-
- // ():uint
- this.skipLeadingZeros = function () {
- var leadingZeroCount; // :uint
- for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) {
- if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) {
- // the first bit of working word is 1
- workingWord <<= leadingZeroCount;
- workingBitsAvailable -= leadingZeroCount;
- return leadingZeroCount;
- }
- }
-
- // we exhausted workingWord and still have not found a 1
- this.loadWord();
- return leadingZeroCount + this.skipLeadingZeros();
- };
-
- // ():void
- this.skipUnsignedExpGolomb = function () {
- this.skipBits(1 + this.skipLeadingZeros());
- };
-
- // ():void
- this.skipExpGolomb = function () {
- this.skipBits(1 + this.skipLeadingZeros());
- };
-
- // ():uint
- this.readUnsignedExpGolomb = function () {
- var clz = this.skipLeadingZeros(); // :uint
- return this.readBits(clz + 1) - 1;
- };
-
- // ():int
- this.readExpGolomb = function () {
- var valu = this.readUnsignedExpGolomb(); // :int
- if (0x01 & valu) {
- // the number is odd if the low order bit is set
- return 1 + valu >>> 1; // add 1 to make it even, and divide by 2
- }
- return -1 * (valu >>> 1); // divide by two then make it negative
- };
-
- // Some convenience functions
- // :Boolean
- this.readBoolean = function () {
- return this.readBits(1) === 1;
- };
-
- // ():int
- this.readUnsignedByte = function () {
- return this.readBits(8);
- };
-
- this.loadWord();
- };
-
- var expGolomb = ExpGolomb;
-
- var _H264Stream, _NalByteStream;
- var PROFILES_WITH_OPTIONAL_SPS_DATA;
-
- /**
- * Accepts a NAL unit byte stream and unpacks the embedded NAL units.
- */
- _NalByteStream = function NalByteStream() {
- var syncPoint = 0,
- i,
- buffer;
- _NalByteStream.prototype.init.call(this);
-
- /*
- * Scans a byte stream and triggers a data event with the NAL units found.
- * @param {Object} data Event received from H264Stream
- * @param {Uint8Array} data.data The h264 byte stream to be scanned
- *
- * @see H264Stream.push
- */
- this.push = function (data) {
- var swapBuffer;
-
- if (!buffer) {
- buffer = data.data;
- } else {
- swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
- swapBuffer.set(buffer);
- swapBuffer.set(data.data, buffer.byteLength);
- buffer = swapBuffer;
- }
-
- // Rec. ITU-T H.264, Annex B
- // scan for NAL unit boundaries
-
- // a match looks like this:
- // 0 0 1 .. NAL .. 0 0 1
- // ^ sync point ^ i
- // or this:
- // 0 0 1 .. NAL .. 0 0 0
- // ^ sync point ^ i
-
- // advance the sync point to a NAL start, if necessary
- for (; syncPoint < buffer.byteLength - 3; syncPoint++) {
- if (buffer[syncPoint + 2] === 1) {
- // the sync point is properly aligned
- i = syncPoint + 5;
- break;
- }
- }
-
- while (i < buffer.byteLength) {
- // look at the current byte to determine if we've hit the end of
- // a NAL unit boundary
- switch (buffer[i]) {
- case 0:
- // skip past non-sync sequences
- if (buffer[i - 1] !== 0) {
- i += 2;
- break;
- } else if (buffer[i - 2] !== 0) {
- i++;
- break;
- }
-
- // deliver the NAL unit if it isn't empty
- if (syncPoint + 3 !== i - 2) {
- this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
- }
-
- // drop trailing zeroes
- do {
- i++;
- } while (buffer[i] !== 1 && i < buffer.length);
- syncPoint = i - 2;
- i += 3;
- break;
- case 1:
- // skip past non-sync sequences
- if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) {
- i += 3;
- break;
- }
-
- // deliver the NAL unit
- this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
- syncPoint = i - 2;
- i += 3;
- break;
- default:
- // the current byte isn't a one or zero, so it cannot be part
- // of a sync sequence
- i += 3;
- break;
- }
- }
- // filter out the NAL units that were delivered
- buffer = buffer.subarray(syncPoint);
- i -= syncPoint;
- syncPoint = 0;
- };
-
- this.flush = function () {
- // deliver the last buffered NAL unit
- if (buffer && buffer.byteLength > 3) {
- this.trigger('data', buffer.subarray(syncPoint + 3));
- }
- // reset the stream state
- buffer = null;
- syncPoint = 0;
- this.trigger('done');
- };
- };
- _NalByteStream.prototype = new stream();
-
- // values of profile_idc that indicate additional fields are included in the SPS
- // see Recommendation ITU-T H.264 (4/2013),
- // 7.3.2.1.1 Sequence parameter set data syntax
- PROFILES_WITH_OPTIONAL_SPS_DATA = {
- 100: true,
- 110: true,
- 122: true,
- 244: true,
- 44: true,
- 83: true,
- 86: true,
- 118: true,
- 128: true,
- 138: true,
- 139: true,
- 134: true
- };
-
- /**
- * Accepts input from a ElementaryStream and produces H.264 NAL unit data
- * events.
- */
- _H264Stream = function H264Stream() {
- var nalByteStream = new _NalByteStream(),
- self,
- trackId,
- currentPts,
- currentDts,
- discardEmulationPreventionBytes,
- readSequenceParameterSet,
- skipScalingList;
-
- _H264Stream.prototype.init.call(this);
- self = this;
-
- /*
- * Pushes a packet from a stream onto the NalByteStream
- *
- * @param {Object} packet - A packet received from a stream
- * @param {Uint8Array} packet.data - The raw bytes of the packet
- * @param {Number} packet.dts - Decode timestamp of the packet
- * @param {Number} packet.pts - Presentation timestamp of the packet
- * @param {Number} packet.trackId - The id of the h264 track this packet came from
- * @param {('video'|'audio')} packet.type - The type of packet
- *
- */
- this.push = function (packet) {
- if (packet.type !== 'video') {
- return;
- }
- trackId = packet.trackId;
- currentPts = packet.pts;
- currentDts = packet.dts;
-
- nalByteStream.push(packet);
- };
-
- /*
- * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps
- * for the NALUs to the next stream component.
- * Also, preprocess caption and sequence parameter NALUs.
- *
- * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push`
- * @see NalByteStream.push
- */
- nalByteStream.on('data', function (data) {
- var event = {
- trackId: trackId,
- pts: currentPts,
- dts: currentDts,
- data: data
- };
-
- switch (data[0] & 0x1f) {
- case 0x05:
- event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr';
- break;
- case 0x06:
- event.nalUnitType = 'sei_rbsp';
- event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
- break;
- case 0x07:
- event.nalUnitType = 'seq_parameter_set_rbsp';
- event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
- event.config = readSequenceParameterSet(event.escapedRBSP);
- break;
- case 0x08:
- event.nalUnitType = 'pic_parameter_set_rbsp';
- break;
- case 0x09:
- event.nalUnitType = 'access_unit_delimiter_rbsp';
- break;
-
- default:
- break;
- }
- // This triggers data on the H264Stream
- self.trigger('data', event);
- });
- nalByteStream.on('done', function () {
- self.trigger('done');
- });
-
- this.flush = function () {
- nalByteStream.flush();
- };
-
- /**
- * Advance the ExpGolomb decoder past a scaling list. The scaling
- * list is optionally transmitted as part of a sequence parameter
- * set and is not relevant to transmuxing.
- * @param count {number} the number of entries in this scaling list
- * @param expGolombDecoder {object} an ExpGolomb pointed to the
- * start of a scaling list
- * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
- */
- skipScalingList = function skipScalingList(count, expGolombDecoder) {
- var lastScale = 8,
- nextScale = 8,
- j,
- deltaScale;
-
- for (j = 0; j < count; j++) {
- if (nextScale !== 0) {
- deltaScale = expGolombDecoder.readExpGolomb();
- nextScale = (lastScale + deltaScale + 256) % 256;
- }
-
- lastScale = nextScale === 0 ? lastScale : nextScale;
- }
- };
-
- /**
- * Expunge any "Emulation Prevention" bytes from a "Raw Byte
- * Sequence Payload"
- * @param data {Uint8Array} the bytes of a RBSP from a NAL
- * unit
- * @return {Uint8Array} the RBSP without any Emulation
- * Prevention Bytes
- */
- discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) {
- var length = data.byteLength,
- emulationPreventionBytesPositions = [],
- i = 1,
- newLength,
- newData;
-
- // Find all `Emulation Prevention Bytes`
- while (i < length - 2) {
- if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
- emulationPreventionBytesPositions.push(i + 2);
- i += 2;
- } else {
- i++;
- }
- }
-
- // If no Emulation Prevention Bytes were found just return the original
- // array
- if (emulationPreventionBytesPositions.length === 0) {
- return data;
- }
-
- // Create a new array to hold the NAL unit data
- newLength = length - emulationPreventionBytesPositions.length;
- newData = new Uint8Array(newLength);
- var sourceIndex = 0;
-
- for (i = 0; i < newLength; sourceIndex++, i++) {
- if (sourceIndex === emulationPreventionBytesPositions[0]) {
- // Skip this byte
- sourceIndex++;
- // Remove this position index
- emulationPreventionBytesPositions.shift();
- }
- newData[i] = data[sourceIndex];
- }
-
- return newData;
- };
-
- /**
- * Read a sequence parameter set and return some interesting video
- * properties. A sequence parameter set is the H264 metadata that
- * describes the properties of upcoming video frames.
- * @param data {Uint8Array} the bytes of a sequence parameter set
- * @return {object} an object with configuration parsed from the
- * sequence parameter set, including the dimensions of the
- * associated video frames.
- */
- readSequenceParameterSet = function readSequenceParameterSet(data) {
- var frameCropLeftOffset = 0,
- frameCropRightOffset = 0,
- frameCropTopOffset = 0,
- frameCropBottomOffset = 0,
- sarScale = 1,
- expGolombDecoder,
- profileIdc,
- levelIdc,
- profileCompatibility,
- chromaFormatIdc,
- picOrderCntType,
- numRefFramesInPicOrderCntCycle,
- picWidthInMbsMinus1,
- picHeightInMapUnitsMinus1,
- frameMbsOnlyFlag,
- scalingListCount,
- sarRatio,
- aspectRatioIdc,
- i;
-
- expGolombDecoder = new expGolomb(data);
- profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
- profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag
- levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8)
- expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
-
- // some profiles have more optional data we don't need
- if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) {
- chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
- if (chromaFormatIdc === 3) {
- expGolombDecoder.skipBits(1); // separate_colour_plane_flag
- }
- expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
- expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
- expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
- if (expGolombDecoder.readBoolean()) {
- // seq_scaling_matrix_present_flag
- scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
- for (i = 0; i < scalingListCount; i++) {
- if (expGolombDecoder.readBoolean()) {
- // seq_scaling_list_present_flag[ i ]
- if (i < 6) {
- skipScalingList(16, expGolombDecoder);
- } else {
- skipScalingList(64, expGolombDecoder);
- }
- }
- }
- }
- }
-
- expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
- picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
-
- if (picOrderCntType === 0) {
- expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4
- } else if (picOrderCntType === 1) {
- expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
- expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
- expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
- numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
- for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
- expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
- }
- }
-
- expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
- expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
-
- picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
- picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
-
- frameMbsOnlyFlag = expGolombDecoder.readBits(1);
- if (frameMbsOnlyFlag === 0) {
- expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
- }
-
- expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
- if (expGolombDecoder.readBoolean()) {
- // frame_cropping_flag
- frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
- }
- if (expGolombDecoder.readBoolean()) {
- // vui_parameters_present_flag
- if (expGolombDecoder.readBoolean()) {
- // aspect_ratio_info_present_flag
- aspectRatioIdc = expGolombDecoder.readUnsignedByte();
- switch (aspectRatioIdc) {
- case 1:
- sarRatio = [1, 1];break;
- case 2:
- sarRatio = [12, 11];break;
- case 3:
- sarRatio = [10, 11];break;
- case 4:
- sarRatio = [16, 11];break;
- case 5:
- sarRatio = [40, 33];break;
- case 6:
- sarRatio = [24, 11];break;
- case 7:
- sarRatio = [20, 11];break;
- case 8:
- sarRatio = [32, 11];break;
- case 9:
- sarRatio = [80, 33];break;
- case 10:
- sarRatio = [18, 11];break;
- case 11:
- sarRatio = [15, 11];break;
- case 12:
- sarRatio = [64, 33];break;
- case 13:
- sarRatio = [160, 99];break;
- case 14:
- sarRatio = [4, 3];break;
- case 15:
- sarRatio = [3, 2];break;
- case 16:
- sarRatio = [2, 1];break;
- case 255:
- {
- sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()];
- break;
- }
- }
- if (sarRatio) {
- sarScale = sarRatio[0] / sarRatio[1];
- }
- }
- }
- return {
- profileIdc: profileIdc,
- levelIdc: levelIdc,
- profileCompatibility: profileCompatibility,
- width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale),
- height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2
- };
- };
- };
- _H264Stream.prototype = new stream();
-
- var h264 = {
- H264Stream: _H264Stream,
- NalByteStream: _NalByteStream
- };
-
- // Constants
- var _AacStream;
-
- /**
- * Splits an incoming stream of binary data into ADTS and ID3 Frames.
- */
-
- _AacStream = function AacStream() {
- var everything = new Uint8Array(),
- timeStamp = 0;
-
- _AacStream.prototype.init.call(this);
-
- this.setTimestamp = function (timestamp) {
- timeStamp = timestamp;
- };
-
- this.parseId3TagSize = function (header, byteIndex) {
- var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9],
- flags = header[byteIndex + 5],
- footerPresent = (flags & 16) >> 4;
-
- if (footerPresent) {
- return returnSize + 20;
- }
- return returnSize + 10;
- };
-
- this.parseAdtsSize = function (header, byteIndex) {
- var lowThree = (header[byteIndex + 5] & 0xE0) >> 5,
- middle = header[byteIndex + 4] << 3,
- highTwo = header[byteIndex + 3] & 0x3 << 11;
-
- return highTwo | middle | lowThree;
- };
-
- this.push = function (bytes) {
- var frameSize = 0,
- byteIndex = 0,
- bytesLeft,
- chunk,
- packet,
- tempLength;
-
- // If there are bytes remaining from the last segment, prepend them to the
- // bytes that were pushed in
- if (everything.length) {
- tempLength = everything.length;
- everything = new Uint8Array(bytes.byteLength + tempLength);
- everything.set(everything.subarray(0, tempLength));
- everything.set(bytes, tempLength);
- } else {
- everything = bytes;
- }
-
- while (everything.length - byteIndex >= 3) {
- if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) {
-
- // Exit early because we don't have enough to parse
- // the ID3 tag header
- if (everything.length - byteIndex < 10) {
- break;
- }
-
- // check framesize
- frameSize = this.parseId3TagSize(everything, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > everything.length) {
- break;
- }
- chunk = {
- type: 'timed-metadata',
- data: everything.subarray(byteIndex, byteIndex + frameSize)
- };
- this.trigger('data', chunk);
- byteIndex += frameSize;
- continue;
- } else if (everything[byteIndex] & 0xff === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) {
-
- // Exit early because we don't have enough to parse
- // the ADTS frame header
- if (everything.length - byteIndex < 7) {
- break;
- }
-
- frameSize = this.parseAdtsSize(everything, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > everything.length) {
- break;
- }
-
- packet = {
- type: 'audio',
- data: everything.subarray(byteIndex, byteIndex + frameSize),
- pts: timeStamp,
- dts: timeStamp
- };
- this.trigger('data', packet);
- byteIndex += frameSize;
- continue;
- }
- byteIndex++;
- }
- bytesLeft = everything.length - byteIndex;
-
- if (bytesLeft > 0) {
- everything = everything.subarray(byteIndex);
- } else {
- everything = new Uint8Array();
- }
- };
- };
-
- _AacStream.prototype = new stream();
-
- var aac = _AacStream;
-
- var highPrefix = [33, 16, 5, 32, 164, 27];
- var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252];
- var zeroFill = function zeroFill(count) {
- var a = [];
- while (count--) {
- a.push(0);
- }
- return a;
- };
-
- var makeTable = function makeTable(metaTable) {
- return Object.keys(metaTable).reduce(function (obj, key) {
- obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) {
- return arr.concat(part);
- }, []));
- return obj;
- }, {});
- };
-
- // Frames-of-silence to use for filling in missing AAC frames
- var coneOfSilence = {
- 96000: [highPrefix, [227, 64], zeroFill(154), [56]],
- 88200: [highPrefix, [231], zeroFill(170), [56]],
- 64000: [highPrefix, [248, 192], zeroFill(240), [56]],
- 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]],
- 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]],
- 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]],
- 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]],
- 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]],
- 12000: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 253, 128], zeroFill(259), [56]],
- 11025: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 255, 192], zeroFill(268), [55, 175, 128], zeroFill(108), [112]],
- 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]]
- };
-
- var silence = makeTable(coneOfSilence);
-
- var ONE_SECOND_IN_TS$1 = 90000,
-
- // 90kHz clock
- secondsToVideoTs,
- secondsToAudioTs,
- videoTsToSeconds,
- audioTsToSeconds,
- audioTsToVideoTs,
- videoTsToAudioTs;
-
- secondsToVideoTs = function secondsToVideoTs(seconds) {
- return seconds * ONE_SECOND_IN_TS$1;
- };
-
- secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) {
- return seconds * sampleRate;
- };
-
- videoTsToSeconds = function videoTsToSeconds(timestamp) {
- return timestamp / ONE_SECOND_IN_TS$1;
- };
-
- audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) {
- return timestamp / sampleRate;
- };
-
- audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) {
- return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
- };
-
- videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) {
- return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
- };
-
- var clock = {
- secondsToVideoTs: secondsToVideoTs,
- secondsToAudioTs: secondsToAudioTs,
- videoTsToSeconds: videoTsToSeconds,
- audioTsToSeconds: audioTsToSeconds,
- audioTsToVideoTs: audioTsToVideoTs,
- videoTsToAudioTs: videoTsToAudioTs
- };
-
- var H264Stream = h264.H264Stream;
-
- // constants
- var AUDIO_PROPERTIES = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize'];
-
- var VIDEO_PROPERTIES = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility'];
-
- var ONE_SECOND_IN_TS$2 = 90000; // 90kHz clock
-
- // object types
- var _VideoSegmentStream, _AudioSegmentStream, _Transmuxer, _CoalesceStream;
-
- // Helper functions
- var isLikelyAacData, arrayEquals, sumFrameByteLengths;
-
- isLikelyAacData = function isLikelyAacData(data) {
- if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) {
- return true;
- }
- return false;
- };
-
- /**
- * Compare two arrays (even typed) for same-ness
- */
- arrayEquals = function arrayEquals(a, b) {
- var i;
-
- if (a.length !== b.length) {
- return false;
- }
-
- // compare the value of each element in the array
- for (i = 0; i < a.length; i++) {
- if (a[i] !== b[i]) {
- return false;
- }
- }
-
- return true;
- };
-
- /**
- * Sum the `byteLength` properties of the data in each AAC frame
- */
- sumFrameByteLengths = function sumFrameByteLengths(array) {
- var i,
- currentObj,
- sum = 0;
-
- // sum the byteLength's all each nal unit in the frame
- for (i = 0; i < array.length; i++) {
- currentObj = array[i];
- sum += currentObj.data.byteLength;
- }
-
- return sum;
- };
-
- /**
- * Constructs a single-track, ISO BMFF media segment from AAC data
- * events. The output of this stream can be fed to a SourceBuffer
- * configured with a suitable initialization segment.
- * @param track {object} track metadata configuration
- * @param options {object} transmuxer options object
- * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- _AudioSegmentStream = function AudioSegmentStream(track, options) {
- var adtsFrames = [],
- sequenceNumber = 0,
- earliestAllowedDts = 0,
- audioAppendStartTs = 0,
- videoBaseMediaDecodeTime = Infinity;
-
- options = options || {};
-
- _AudioSegmentStream.prototype.init.call(this);
-
- this.push = function (data) {
- trackDecodeInfo.collectDtsInfo(track, data);
-
- if (track) {
- AUDIO_PROPERTIES.forEach(function (prop) {
- track[prop] = data[prop];
- });
- }
-
- // buffer audio data until end() is called
- adtsFrames.push(data);
- };
-
- this.setEarliestDts = function (earliestDts) {
- earliestAllowedDts = earliestDts - track.timelineStartInfo.baseMediaDecodeTime;
- };
-
- this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) {
- videoBaseMediaDecodeTime = baseMediaDecodeTime;
- };
-
- this.setAudioAppendStart = function (timestamp) {
- audioAppendStartTs = timestamp;
- };
-
- this.flush = function () {
- var frames, moof, mdat, boxes;
-
- // return early if no audio data has been observed
- if (adtsFrames.length === 0) {
- this.trigger('done', 'AudioSegmentStream');
- return;
- }
-
- frames = this.trimAdtsFramesByEarliestDts_(adtsFrames);
- track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
-
- this.prefixWithSilence_(track, frames);
-
- // we have to build the index from byte locations to
- // samples (that is, adts frames) in the audio data
- track.samples = this.generateSampleTable_(frames);
-
- // concatenate the audio data to constuct the mdat
- mdat = mp4Generator.mdat(this.concatenateFrameData_(frames));
-
- adtsFrames = [];
-
- moof = mp4Generator.moof(sequenceNumber, [track]);
- boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
-
- // bump the sequence number for next time
- sequenceNumber++;
-
- boxes.set(moof);
- boxes.set(mdat, moof.byteLength);
-
- trackDecodeInfo.clearDtsInfo(track);
-
- this.trigger('data', { track: track, boxes: boxes });
- this.trigger('done', 'AudioSegmentStream');
- };
-
- // Possibly pad (prefix) the audio track with silence if appending this track
- // would lead to the introduction of a gap in the audio buffer
- this.prefixWithSilence_ = function (track, frames) {
- var baseMediaDecodeTimeTs,
- frameDuration = 0,
- audioGapDuration = 0,
- audioFillFrameCount = 0,
- audioFillDuration = 0,
- silentFrame,
- i;
-
- if (!frames.length) {
- return;
- }
-
- baseMediaDecodeTimeTs = clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate);
- // determine frame clock duration based on sample rate, round up to avoid overfills
- frameDuration = Math.ceil(ONE_SECOND_IN_TS$2 / (track.samplerate / 1024));
-
- if (audioAppendStartTs && videoBaseMediaDecodeTime) {
- // insert the shortest possible amount (audio gap or audio to video gap)
- audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime);
- // number of full frames in the audio gap
- audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
- audioFillDuration = audioFillFrameCount * frameDuration;
- }
-
- // don't attempt to fill gaps smaller than a single frame or larger
- // than a half second
- if (audioFillFrameCount < 1 || audioFillDuration > ONE_SECOND_IN_TS$2 / 2) {
- return;
- }
-
- silentFrame = silence[track.samplerate];
-
- if (!silentFrame) {
- // we don't have a silent frame pregenerated for the sample rate, so use a frame
- // from the content instead
- silentFrame = frames[0].data;
- }
-
- for (i = 0; i < audioFillFrameCount; i++) {
- frames.splice(i, 0, {
- data: silentFrame
- });
- }
-
- track.baseMediaDecodeTime -= Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate));
- };
-
- // If the audio segment extends before the earliest allowed dts
- // value, remove AAC frames until starts at or after the earliest
- // allowed DTS so that we don't end up with a negative baseMedia-
- // DecodeTime for the audio track
- this.trimAdtsFramesByEarliestDts_ = function (adtsFrames) {
- if (track.minSegmentDts >= earliestAllowedDts) {
- return adtsFrames;
- }
-
- // We will need to recalculate the earliest segment Dts
- track.minSegmentDts = Infinity;
-
- return adtsFrames.filter(function (currentFrame) {
- // If this is an allowed frame, keep it and record it's Dts
- if (currentFrame.dts >= earliestAllowedDts) {
- track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
- track.minSegmentPts = track.minSegmentDts;
- return true;
- }
- // Otherwise, discard it
- return false;
- });
- };
-
- // generate the track's raw mdat data from an array of frames
- this.generateSampleTable_ = function (frames) {
- var i,
- currentFrame,
- samples = [];
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
- samples.push({
- size: currentFrame.data.byteLength,
- duration: 1024 // For AAC audio, all samples contain 1024 samples
- });
- }
- return samples;
- };
-
- // generate the track's sample table from an array of frames
- this.concatenateFrameData_ = function (frames) {
- var i,
- currentFrame,
- dataOffset = 0,
- data = new Uint8Array(sumFrameByteLengths(frames));
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
-
- data.set(currentFrame.data, dataOffset);
- dataOffset += currentFrame.data.byteLength;
- }
- return data;
- };
- };
-
- _AudioSegmentStream.prototype = new stream();
-
- /**
- * Constructs a single-track, ISO BMFF media segment from H264 data
- * events. The output of this stream can be fed to a SourceBuffer
- * configured with a suitable initialization segment.
- * @param track {object} track metadata configuration
- * @param options {object} transmuxer options object
- * @param options.alignGopsAtEnd {boolean} If true, start from the end of the
- * gopsToAlignWith list when attempting to align gop pts
- * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- _VideoSegmentStream = function VideoSegmentStream(track, options) {
- var sequenceNumber = 0,
- nalUnits = [],
- gopsToAlignWith = [],
- config,
- pps;
-
- options = options || {};
-
- _VideoSegmentStream.prototype.init.call(this);
-
- delete track.minPTS;
-
- this.gopCache_ = [];
-
- /**
- * Constructs a ISO BMFF segment given H264 nalUnits
- * @param {Object} nalUnit A data event representing a nalUnit
- * @param {String} nalUnit.nalUnitType
- * @param {Object} nalUnit.config Properties for a mp4 track
- * @param {Uint8Array} nalUnit.data The nalUnit bytes
- * @see lib/codecs/h264.js
- **/
- this.push = function (nalUnit) {
- trackDecodeInfo.collectDtsInfo(track, nalUnit);
-
- // record the track config
- if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
- config = nalUnit.config;
- track.sps = [nalUnit.data];
-
- VIDEO_PROPERTIES.forEach(function (prop) {
- track[prop] = config[prop];
- }, this);
- }
-
- if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) {
- pps = nalUnit.data;
- track.pps = [nalUnit.data];
- }
-
- // buffer video until flush() is called
- nalUnits.push(nalUnit);
- };
-
- /**
- * Pass constructed ISO BMFF track and boxes on to the
- * next stream in the pipeline
- **/
- this.flush = function () {
- var frames, gopForFusion, gops, moof, mdat, boxes;
-
- // Throw away nalUnits at the start of the byte stream until
- // we find the first AUD
- while (nalUnits.length) {
- if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
- break;
- }
- nalUnits.shift();
- }
-
- // Return early if no video data has been observed
- if (nalUnits.length === 0) {
- this.resetStream_();
- this.trigger('done', 'VideoSegmentStream');
- return;
- }
-
- // Organize the raw nal-units into arrays that represent
- // higher-level constructs such as frames and gops
- // (group-of-pictures)
- frames = frameUtils.groupNalsIntoFrames(nalUnits);
- gops = frameUtils.groupFramesIntoGops(frames);
-
- // If the first frame of this fragment is not a keyframe we have
- // a problem since MSE (on Chrome) requires a leading keyframe.
- //
- // We have two approaches to repairing this situation:
- // 1) GOP-FUSION:
- // This is where we keep track of the GOPS (group-of-pictures)
- // from previous fragments and attempt to find one that we can
- // prepend to the current fragment in order to create a valid
- // fragment.
- // 2) KEYFRAME-PULLING:
- // Here we search for the first keyframe in the fragment and
- // throw away all the frames between the start of the fragment
- // and that keyframe. We then extend the duration and pull the
- // PTS of the keyframe forward so that it covers the time range
- // of the frames that were disposed of.
- //
- // #1 is far prefereable over #2 which can cause "stuttering" but
- // requires more things to be just right.
- if (!gops[0][0].keyFrame) {
- // Search for a gop for fusion from our gopCache
- gopForFusion = this.getGopForFusion_(nalUnits[0], track);
-
- if (gopForFusion) {
- gops.unshift(gopForFusion);
- // Adjust Gops' metadata to account for the inclusion of the
- // new gop at the beginning
- gops.byteLength += gopForFusion.byteLength;
- gops.nalCount += gopForFusion.nalCount;
- gops.pts = gopForFusion.pts;
- gops.dts = gopForFusion.dts;
- gops.duration += gopForFusion.duration;
- } else {
- // If we didn't find a candidate gop fall back to keyframe-pulling
- gops = frameUtils.extendFirstKeyFrame(gops);
- }
- }
-
- // Trim gops to align with gopsToAlignWith
- if (gopsToAlignWith.length) {
- var alignedGops;
-
- if (options.alignGopsAtEnd) {
- alignedGops = this.alignGopsAtEnd_(gops);
- } else {
- alignedGops = this.alignGopsAtStart_(gops);
- }
-
- if (!alignedGops) {
- // save all the nals in the last GOP into the gop cache
- this.gopCache_.unshift({
- gop: gops.pop(),
- pps: track.pps,
- sps: track.sps
- });
-
- // Keep a maximum of 6 GOPs in the cache
- this.gopCache_.length = Math.min(6, this.gopCache_.length);
-
- // Clear nalUnits
- nalUnits = [];
-
- // return early no gops can be aligned with desired gopsToAlignWith
- this.resetStream_();
- this.trigger('done', 'VideoSegmentStream');
- return;
- }
-
- // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct
- // when recalculated before sending off to CoalesceStream
- trackDecodeInfo.clearDtsInfo(track);
-
- gops = alignedGops;
- }
-
- trackDecodeInfo.collectDtsInfo(track, gops);
-
- // First, we have to build the index from byte locations to
- // samples (that is, frames) in the video data
- track.samples = frameUtils.generateSampleTable(gops);
-
- // Concatenate the video data and construct the mdat
- mdat = mp4Generator.mdat(frameUtils.concatenateNalData(gops));
-
- track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
-
- this.trigger('processedGopsInfo', gops.map(function (gop) {
- return {
- pts: gop.pts,
- dts: gop.dts,
- byteLength: gop.byteLength
- };
- }));
-
- // save all the nals in the last GOP into the gop cache
- this.gopCache_.unshift({
- gop: gops.pop(),
- pps: track.pps,
- sps: track.sps
- });
-
- // Keep a maximum of 6 GOPs in the cache
- this.gopCache_.length = Math.min(6, this.gopCache_.length);
-
- // Clear nalUnits
- nalUnits = [];
-
- this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime);
- this.trigger('timelineStartInfo', track.timelineStartInfo);
-
- moof = mp4Generator.moof(sequenceNumber, [track]);
-
- // it would be great to allocate this array up front instead of
- // throwing away hundreds of media segment fragments
- boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
-
- // Bump the sequence number for next time
- sequenceNumber++;
-
- boxes.set(moof);
- boxes.set(mdat, moof.byteLength);
-
- this.trigger('data', { track: track, boxes: boxes });
-
- this.resetStream_();
-
- // Continue with the flush process now
- this.trigger('done', 'VideoSegmentStream');
- };
-
- this.resetStream_ = function () {
- trackDecodeInfo.clearDtsInfo(track);
-
- // reset config and pps because they may differ across segments
- // for instance, when we are rendition switching
- config = undefined;
- pps = undefined;
- };
-
- // Search for a candidate Gop for gop-fusion from the gop cache and
- // return it or return null if no good candidate was found
- this.getGopForFusion_ = function (nalUnit) {
- var halfSecond = 45000,
-
- // Half-a-second in a 90khz clock
- allowableOverlap = 10000,
-
- // About 3 frames @ 30fps
- nearestDistance = Infinity,
- dtsDistance,
- nearestGopObj,
- currentGop,
- currentGopObj,
- i;
-
- // Search for the GOP nearest to the beginning of this nal unit
- for (i = 0; i < this.gopCache_.length; i++) {
- currentGopObj = this.gopCache_[i];
- currentGop = currentGopObj.gop;
-
- // Reject Gops with different SPS or PPS
- if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) {
- continue;
- }
-
- // Reject Gops that would require a negative baseMediaDecodeTime
- if (currentGop.dts < track.timelineStartInfo.dts) {
- continue;
- }
-
- // The distance between the end of the gop and the start of the nalUnit
- dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration;
-
- // Only consider GOPS that start before the nal unit and end within
- // a half-second of the nal unit
- if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) {
-
- // Always use the closest GOP we found if there is more than
- // one candidate
- if (!nearestGopObj || nearestDistance > dtsDistance) {
- nearestGopObj = currentGopObj;
- nearestDistance = dtsDistance;
- }
- }
- }
-
- if (nearestGopObj) {
- return nearestGopObj.gop;
- }
- return null;
- };
-
- // trim gop list to the first gop found that has a matching pts with a gop in the list
- // of gopsToAlignWith starting from the START of the list
- this.alignGopsAtStart_ = function (gops) {
- var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops;
-
- byteLength = gops.byteLength;
- nalCount = gops.nalCount;
- duration = gops.duration;
- alignIndex = gopIndex = 0;
-
- while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) {
- align = gopsToAlignWith[alignIndex];
- gop = gops[gopIndex];
-
- if (align.pts === gop.pts) {
- break;
- }
-
- if (gop.pts > align.pts) {
- // this current gop starts after the current gop we want to align on, so increment
- // align index
- alignIndex++;
- continue;
- }
-
- // current gop starts before the current gop we want to align on. so increment gop
- // index
- gopIndex++;
- byteLength -= gop.byteLength;
- nalCount -= gop.nalCount;
- duration -= gop.duration;
- }
-
- if (gopIndex === 0) {
- // no gops to trim
- return gops;
- }
-
- if (gopIndex === gops.length) {
- // all gops trimmed, skip appending all gops
- return null;
- }
-
- alignedGops = gops.slice(gopIndex);
- alignedGops.byteLength = byteLength;
- alignedGops.duration = duration;
- alignedGops.nalCount = nalCount;
- alignedGops.pts = alignedGops[0].pts;
- alignedGops.dts = alignedGops[0].dts;
-
- return alignedGops;
- };
-
- // trim gop list to the first gop found that has a matching pts with a gop in the list
- // of gopsToAlignWith starting from the END of the list
- this.alignGopsAtEnd_ = function (gops) {
- var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound;
-
- alignIndex = gopsToAlignWith.length - 1;
- gopIndex = gops.length - 1;
- alignEndIndex = null;
- matchFound = false;
-
- while (alignIndex >= 0 && gopIndex >= 0) {
- align = gopsToAlignWith[alignIndex];
- gop = gops[gopIndex];
-
- if (align.pts === gop.pts) {
- matchFound = true;
- break;
- }
-
- if (align.pts > gop.pts) {
- alignIndex--;
- continue;
- }
-
- if (alignIndex === gopsToAlignWith.length - 1) {
- // gop.pts is greater than the last alignment candidate. If no match is found
- // by the end of this loop, we still want to append gops that come after this
- // point
- alignEndIndex = gopIndex;
- }
-
- gopIndex--;
- }
-
- if (!matchFound && alignEndIndex === null) {
- return null;
- }
-
- var trimIndex;
-
- if (matchFound) {
- trimIndex = gopIndex;
- } else {
- trimIndex = alignEndIndex;
- }
-
- if (trimIndex === 0) {
- return gops;
- }
-
- var alignedGops = gops.slice(trimIndex);
- var metadata = alignedGops.reduce(function (total, gop) {
- total.byteLength += gop.byteLength;
- total.duration += gop.duration;
- total.nalCount += gop.nalCount;
- return total;
- }, { byteLength: 0, duration: 0, nalCount: 0 });
-
- alignedGops.byteLength = metadata.byteLength;
- alignedGops.duration = metadata.duration;
- alignedGops.nalCount = metadata.nalCount;
- alignedGops.pts = alignedGops[0].pts;
- alignedGops.dts = alignedGops[0].dts;
-
- return alignedGops;
- };
-
- this.alignGopsWith = function (newGopsToAlignWith) {
- gopsToAlignWith = newGopsToAlignWith;
- };
- };
-
- _VideoSegmentStream.prototype = new stream();
-
- /**
- * A Stream that can combine multiple streams (ie. audio & video)
- * into a single output segment for MSE. Also supports audio-only
- * and video-only streams.
- */
- _CoalesceStream = function CoalesceStream(options, metadataStream) {
- // Number of Tracks per output segment
- // If greater than 1, we combine multiple
- // tracks into a single segment
- this.numberOfTracks = 0;
- this.metadataStream = metadataStream;
-
- if (typeof options.remux !== 'undefined') {
- this.remuxTracks = !!options.remux;
- } else {
- this.remuxTracks = true;
- }
-
- this.pendingTracks = [];
- this.videoTrack = null;
- this.pendingBoxes = [];
- this.pendingCaptions = [];
- this.pendingMetadata = [];
- this.pendingBytes = 0;
- this.emittedTracks = 0;
-
- _CoalesceStream.prototype.init.call(this);
-
- // Take output from multiple
- this.push = function (output) {
- // buffer incoming captions until the associated video segment
- // finishes
- if (output.text) {
- return this.pendingCaptions.push(output);
- }
- // buffer incoming id3 tags until the final flush
- if (output.frames) {
- return this.pendingMetadata.push(output);
- }
-
- // Add this track to the list of pending tracks and store
- // important information required for the construction of
- // the final segment
- this.pendingTracks.push(output.track);
- this.pendingBoxes.push(output.boxes);
- this.pendingBytes += output.boxes.byteLength;
-
- if (output.track.type === 'video') {
- this.videoTrack = output.track;
- }
- if (output.track.type === 'audio') {
- this.audioTrack = output.track;
- }
- };
- };
-
- _CoalesceStream.prototype = new stream();
- _CoalesceStream.prototype.flush = function (flushSource) {
- var offset = 0,
- event = {
- captions: [],
- captionStreams: {},
- metadata: [],
- info: {}
- },
- caption,
- id3,
- initSegment,
- timelineStartPts = 0,
- i;
-
- if (this.pendingTracks.length < this.numberOfTracks) {
- if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') {
- // Return because we haven't received a flush from a data-generating
- // portion of the segment (meaning that we have only recieved meta-data
- // or captions.)
- return;
- } else if (this.remuxTracks) {
- // Return until we have enough tracks from the pipeline to remux (if we
- // are remuxing audio and video into a single MP4)
- return;
- } else if (this.pendingTracks.length === 0) {
- // In the case where we receive a flush without any data having been
- // received we consider it an emitted track for the purposes of coalescing
- // `done` events.
- // We do this for the case where there is an audio and video track in the
- // segment but no audio data. (seen in several playlists with alternate
- // audio tracks and no audio present in the main TS segments.)
- this.emittedTracks++;
-
- if (this.emittedTracks >= this.numberOfTracks) {
- this.trigger('done');
- this.emittedTracks = 0;
- }
- return;
- }
- }
-
- if (this.videoTrack) {
- timelineStartPts = this.videoTrack.timelineStartInfo.pts;
- VIDEO_PROPERTIES.forEach(function (prop) {
- event.info[prop] = this.videoTrack[prop];
- }, this);
- } else if (this.audioTrack) {
- timelineStartPts = this.audioTrack.timelineStartInfo.pts;
- AUDIO_PROPERTIES.forEach(function (prop) {
- event.info[prop] = this.audioTrack[prop];
- }, this);
- }
-
- if (this.pendingTracks.length === 1) {
- event.type = this.pendingTracks[0].type;
- } else {
- event.type = 'combined';
- }
-
- this.emittedTracks += this.pendingTracks.length;
-
- initSegment = mp4Generator.initSegment(this.pendingTracks);
-
- // Create a new typed array to hold the init segment
- event.initSegment = new Uint8Array(initSegment.byteLength);
-
- // Create an init segment containing a moov
- // and track definitions
- event.initSegment.set(initSegment);
-
- // Create a new typed array to hold the moof+mdats
- event.data = new Uint8Array(this.pendingBytes);
-
- // Append each moof+mdat (one per track) together
- for (i = 0; i < this.pendingBoxes.length; i++) {
- event.data.set(this.pendingBoxes[i], offset);
- offset += this.pendingBoxes[i].byteLength;
- }
-
- // Translate caption PTS times into second offsets into the
- // video timeline for the segment, and add track info
- for (i = 0; i < this.pendingCaptions.length; i++) {
- caption = this.pendingCaptions[i];
- caption.startTime = caption.startPts - timelineStartPts;
- caption.startTime /= 90e3;
- caption.endTime = caption.endPts - timelineStartPts;
- caption.endTime /= 90e3;
- event.captionStreams[caption.stream] = true;
- event.captions.push(caption);
- }
-
- // Translate ID3 frame PTS times into second offsets into the
- // video timeline for the segment
- for (i = 0; i < this.pendingMetadata.length; i++) {
- id3 = this.pendingMetadata[i];
- id3.cueTime = id3.pts - timelineStartPts;
- id3.cueTime /= 90e3;
- event.metadata.push(id3);
- }
- // We add this to every single emitted segment even though we only need
- // it for the first
- event.metadata.dispatchType = this.metadataStream.dispatchType;
-
- // Reset stream state
- this.pendingTracks.length = 0;
- this.videoTrack = null;
- this.pendingBoxes.length = 0;
- this.pendingCaptions.length = 0;
- this.pendingBytes = 0;
- this.pendingMetadata.length = 0;
-
- // Emit the built segment
- this.trigger('data', event);
-
- // Only emit `done` if all tracks have been flushed and emitted
- if (this.emittedTracks >= this.numberOfTracks) {
- this.trigger('done');
- this.emittedTracks = 0;
- }
- };
- /**
- * A Stream that expects MP2T binary data as input and produces
- * corresponding media segments, suitable for use with Media Source
- * Extension (MSE) implementations that support the ISO BMFF byte
- * stream format, like Chrome.
- */
- _Transmuxer = function Transmuxer(options) {
- var self = this,
- hasFlushed = true,
- videoTrack,
- audioTrack;
-
- _Transmuxer.prototype.init.call(this);
-
- options = options || {};
- this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0;
- this.transmuxPipeline_ = {};
-
- this.setupAacPipeline = function () {
- var pipeline = {};
- this.transmuxPipeline_ = pipeline;
-
- pipeline.type = 'aac';
- pipeline.metadataStream = new m2ts_1.MetadataStream();
-
- // set up the parsing pipeline
- pipeline.aacStream = new aac();
- pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio');
- pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata');
- pipeline.adtsStream = new adts();
- pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream);
- pipeline.headOfPipeline = pipeline.aacStream;
-
- pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
- pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
-
- pipeline.metadataStream.on('timestamp', function (frame) {
- pipeline.aacStream.setTimestamp(frame.timeStamp);
- });
-
- pipeline.aacStream.on('data', function (data) {
- if (data.type === 'timed-metadata' && !pipeline.audioSegmentStream) {
- audioTrack = audioTrack || {
- timelineStartInfo: {
- baseMediaDecodeTime: self.baseMediaDecodeTime
- },
- codec: 'adts',
- type: 'audio'
- };
- // hook up the audio segment stream to the first track with aac data
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options);
- // Set up the final part of the audio pipeline
- pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
- }
- });
-
- // Re-emit any data coming from the coalesce stream to the outside world
- pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
- // Let the consumer know we have finished flushing the entire pipeline
- pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
- };
-
- this.setupTsPipeline = function () {
- var pipeline = {};
- this.transmuxPipeline_ = pipeline;
-
- pipeline.type = 'ts';
- pipeline.metadataStream = new m2ts_1.MetadataStream();
-
- // set up the parsing pipeline
- pipeline.packetStream = new m2ts_1.TransportPacketStream();
- pipeline.parseStream = new m2ts_1.TransportParseStream();
- pipeline.elementaryStream = new m2ts_1.ElementaryStream();
- pipeline.videoTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('video');
- pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio');
- pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata');
- pipeline.adtsStream = new adts();
- pipeline.h264Stream = new H264Stream();
- pipeline.captionStream = new m2ts_1.CaptionStream();
- pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream);
- pipeline.headOfPipeline = pipeline.packetStream;
-
- // disassemble MPEG2-TS packets into elementary streams
- pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream);
-
- // !!THIS ORDER IS IMPORTANT!!
- // demux the streams
- pipeline.elementaryStream.pipe(pipeline.videoTimestampRolloverStream).pipe(pipeline.h264Stream);
- pipeline.elementaryStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
-
- pipeline.elementaryStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
-
- // Hook up CEA-608/708 caption stream
- pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream);
-
- pipeline.elementaryStream.on('data', function (data) {
- var i;
-
- if (data.type === 'metadata') {
- i = data.tracks.length;
-
- // scan the tracks listed in the metadata
- while (i--) {
- if (!videoTrack && data.tracks[i].type === 'video') {
- videoTrack = data.tracks[i];
- videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
- } else if (!audioTrack && data.tracks[i].type === 'audio') {
- audioTrack = data.tracks[i];
- audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
- }
- }
-
- // hook up the video segment stream to the first track with h264 data
- if (videoTrack && !pipeline.videoSegmentStream) {
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.videoSegmentStream = new _VideoSegmentStream(videoTrack, options);
-
- pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) {
- // When video emits timelineStartInfo data after a flush, we forward that
- // info to the AudioSegmentStream, if it exists, because video timeline
- // data takes precedence.
- if (audioTrack) {
- audioTrack.timelineStartInfo = timelineStartInfo;
- // On the first segment we trim AAC frames that exist before the
- // very earliest DTS we have seen in video because Chrome will
- // interpret any video track with a baseMediaDecodeTime that is
- // non-zero as a gap.
- pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts);
- }
- });
-
- pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo'));
-
- pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) {
- if (audioTrack) {
- pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime);
- }
- });
-
- // Set up the final part of the video pipeline
- pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream);
- }
-
- if (audioTrack && !pipeline.audioSegmentStream) {
- // hook up the audio segment stream to the first track with aac data
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options);
-
- // Set up the final part of the audio pipeline
- pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
- }
- }
- });
-
- // Re-emit any data coming from the coalesce stream to the outside world
- pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
- // Let the consumer know we have finished flushing the entire pipeline
- pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
- };
-
- // hook up the segment streams once track metadata is delivered
- this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) {
- var pipeline = this.transmuxPipeline_;
-
- this.baseMediaDecodeTime = baseMediaDecodeTime;
- if (audioTrack) {
- audioTrack.timelineStartInfo.dts = undefined;
- audioTrack.timelineStartInfo.pts = undefined;
- trackDecodeInfo.clearDtsInfo(audioTrack);
- audioTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime;
- if (pipeline.audioTimestampRolloverStream) {
- pipeline.audioTimestampRolloverStream.discontinuity();
- }
- }
- if (videoTrack) {
- if (pipeline.videoSegmentStream) {
- pipeline.videoSegmentStream.gopCache_ = [];
- pipeline.videoTimestampRolloverStream.discontinuity();
- }
- videoTrack.timelineStartInfo.dts = undefined;
- videoTrack.timelineStartInfo.pts = undefined;
- trackDecodeInfo.clearDtsInfo(videoTrack);
- pipeline.captionStream.reset();
- videoTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime;
- }
-
- if (pipeline.timedMetadataTimestampRolloverStream) {
- pipeline.timedMetadataTimestampRolloverStream.discontinuity();
- }
- };
-
- this.setAudioAppendStart = function (timestamp) {
- if (audioTrack) {
- this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp);
- }
- };
-
- this.alignGopsWith = function (gopsToAlignWith) {
- if (videoTrack && this.transmuxPipeline_.videoSegmentStream) {
- this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith);
- }
- };
-
- // feed incoming data to the front of the parsing pipeline
- this.push = function (data) {
- if (hasFlushed) {
- var isAac = isLikelyAacData(data);
-
- if (isAac && this.transmuxPipeline_.type !== 'aac') {
- this.setupAacPipeline();
- } else if (!isAac && this.transmuxPipeline_.type !== 'ts') {
- this.setupTsPipeline();
- }
- hasFlushed = false;
- }
- this.transmuxPipeline_.headOfPipeline.push(data);
- };
-
- // flush any buffered data
- this.flush = function () {
- hasFlushed = true;
- // Start at the top of the pipeline and flush all pending work
- this.transmuxPipeline_.headOfPipeline.flush();
- };
-
- // Caption data has to be reset when seeking outside buffered range
- this.resetCaptions = function () {
- if (this.transmuxPipeline_.captionStream) {
- this.transmuxPipeline_.captionStream.reset();
- }
- };
- };
- _Transmuxer.prototype = new stream();
-
- var transmuxer = {
- Transmuxer: _Transmuxer,
- VideoSegmentStream: _VideoSegmentStream,
- AudioSegmentStream: _AudioSegmentStream,
- AUDIO_PROPERTIES: AUDIO_PROPERTIES,
- VIDEO_PROPERTIES: VIDEO_PROPERTIES
- };
-
- var inspectMp4,
- _textifyMp,
- parseType$1 = probe.parseType,
- parseMp4Date = function parseMp4Date(seconds) {
- return new Date(seconds * 1000 - 2082844800000);
- },
- parseSampleFlags = function parseSampleFlags(flags) {
- return {
- isLeading: (flags[0] & 0x0c) >>> 2,
- dependsOn: flags[0] & 0x03,
- isDependedOn: (flags[1] & 0xc0) >>> 6,
- hasRedundancy: (flags[1] & 0x30) >>> 4,
- paddingValue: (flags[1] & 0x0e) >>> 1,
- isNonSyncSample: flags[1] & 0x01,
- degradationPriority: flags[2] << 8 | flags[3]
- };
- },
- nalParse = function nalParse(avcStream) {
- var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
- result = [],
- i,
- length;
- for (i = 0; i + 4 < avcStream.length; i += length) {
- length = avcView.getUint32(i);
- i += 4;
-
- // bail if this doesn't appear to be an H264 stream
- if (length <= 0) {
- result.push('<span style=\'color:red;\'>MALFORMED DATA</span>');
- continue;
- }
-
- switch (avcStream[i] & 0x1F) {
- case 0x01:
- result.push('slice_layer_without_partitioning_rbsp');
- break;
- case 0x05:
- result.push('slice_layer_without_partitioning_rbsp_idr');
- break;
- case 0x06:
- result.push('sei_rbsp');
- break;
- case 0x07:
- result.push('seq_parameter_set_rbsp');
- break;
- case 0x08:
- result.push('pic_parameter_set_rbsp');
- break;
- case 0x09:
- result.push('access_unit_delimiter_rbsp');
- break;
- default:
- result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F);
- break;
- }
- }
- return result;
- },
-
-
- // registry of handlers for individual mp4 box types
- parse = {
- // codingname, not a first-class box type. stsd entries share the
- // same format as real boxes so the parsing infrastructure can be
- // shared
- avc1: function avc1(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- dataReferenceIndex: view.getUint16(6),
- width: view.getUint16(24),
- height: view.getUint16(26),
- horizresolution: view.getUint16(28) + view.getUint16(30) / 16,
- vertresolution: view.getUint16(32) + view.getUint16(34) / 16,
- frameCount: view.getUint16(40),
- depth: view.getUint16(74),
- config: inspectMp4(data.subarray(78, data.byteLength))
- };
- },
- avcC: function avcC(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- configurationVersion: data[0],
- avcProfileIndication: data[1],
- profileCompatibility: data[2],
- avcLevelIndication: data[3],
- lengthSizeMinusOne: data[4] & 0x03,
- sps: [],
- pps: []
- },
- numOfSequenceParameterSets = data[5] & 0x1f,
- numOfPictureParameterSets,
- nalSize,
- offset,
- i;
-
- // iterate past any SPSs
- offset = 6;
- for (i = 0; i < numOfSequenceParameterSets; i++) {
- nalSize = view.getUint16(offset);
- offset += 2;
- result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
- offset += nalSize;
- }
- // iterate past any PPSs
- numOfPictureParameterSets = data[offset];
- offset++;
- for (i = 0; i < numOfPictureParameterSets; i++) {
- nalSize = view.getUint16(offset);
- offset += 2;
- result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
- offset += nalSize;
- }
- return result;
- },
- btrt: function btrt(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- bufferSizeDB: view.getUint32(0),
- maxBitrate: view.getUint32(4),
- avgBitrate: view.getUint32(8)
- };
- },
- esds: function esds(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- esId: data[6] << 8 | data[7],
- streamPriority: data[8] & 0x1f,
- decoderConfig: {
- objectProfileIndication: data[11],
- streamType: data[12] >>> 2 & 0x3f,
- bufferSize: data[13] << 16 | data[14] << 8 | data[15],
- maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19],
- avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23],
- decoderConfigDescriptor: {
- tag: data[24],
- length: data[25],
- audioObjectType: data[26] >>> 3 & 0x1f,
- samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01,
- channelConfiguration: data[27] >>> 3 & 0x0f
- }
- }
- };
- },
- ftyp: function ftyp(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- majorBrand: parseType$1(data.subarray(0, 4)),
- minorVersion: view.getUint32(4),
- compatibleBrands: []
- },
- i = 8;
- while (i < data.byteLength) {
- result.compatibleBrands.push(parseType$1(data.subarray(i, i + 4)));
- i += 4;
- }
- return result;
- },
- dinf: function dinf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- dref: function dref(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- dataReferences: inspectMp4(data.subarray(8))
- };
- },
- hdlr: function hdlr(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- handlerType: parseType$1(data.subarray(8, 12)),
- name: ''
- },
- i = 8;
-
- // parse out the name field
- for (i = 24; i < data.byteLength; i++) {
- if (data[i] === 0x00) {
- // the name field is null-terminated
- i++;
- break;
- }
- result.name += String.fromCharCode(data[i]);
- }
- // decode UTF-8 to javascript's internal representation
- // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
- result.name = decodeURIComponent(escape(result.name));
-
- return result;
- },
- mdat: function mdat(data) {
- return {
- byteLength: data.byteLength,
- nals: nalParse(data)
- };
- },
- mdhd: function mdhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- language,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- language: ''
- };
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.timescale = view.getUint32(i);
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.timescale = view.getUint32(i);
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
- // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
- // each field is the packed difference between its ASCII value and 0x60
- language = view.getUint16(i);
- result.language += String.fromCharCode((language >> 10) + 0x60);
- result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60);
- result.language += String.fromCharCode((language & 0x1f) + 0x60);
-
- return result;
- },
- mdia: function mdia(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mfhd: function mfhd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
- };
- },
- minf: function minf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- // codingname, not a first-class box type. stsd entries share the
- // same format as real boxes so the parsing infrastructure can be
- // shared
- mp4a: function mp4a(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- // 6 bytes reserved
- dataReferenceIndex: view.getUint16(6),
- // 4 + 4 bytes reserved
- channelcount: view.getUint16(16),
- samplesize: view.getUint16(18),
- // 2 bytes pre_defined
- // 2 bytes reserved
- samplerate: view.getUint16(24) + view.getUint16(26) / 65536
- };
-
- // if there are more bytes to process, assume this is an ISO/IEC
- // 14496-14 MP4AudioSampleEntry and parse the ESDBox
- if (data.byteLength > 28) {
- result.streamDescriptor = inspectMp4(data.subarray(28))[0];
- }
- return result;
- },
- moof: function moof(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- moov: function moov(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mvex: function mvex(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mvhd: function mvhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4))
- };
-
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.timescale = view.getUint32(i);
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.timescale = view.getUint32(i);
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
-
- // convert fixed-point, base 16 back to a number
- result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16;
- i += 4;
- result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
- i += 2;
- i += 2;
- i += 2 * 4;
- result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
- i += 9 * 4;
- i += 6 * 4;
- result.nextTrackId = view.getUint32(i);
- return result;
- },
- pdin: function pdin(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- rate: view.getUint32(4),
- initialDelay: view.getUint32(8)
- };
- },
- sdtp: function sdtp(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- samples: []
- },
- i;
-
- for (i = 4; i < data.byteLength; i++) {
- result.samples.push({
- dependsOn: (data[i] & 0x30) >> 4,
- isDependedOn: (data[i] & 0x0c) >> 2,
- hasRedundancy: data[i] & 0x03
- });
- }
- return result;
- },
- sidx: function sidx(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- references: [],
- referenceId: view.getUint32(4),
- timescale: view.getUint32(8),
- earliestPresentationTime: view.getUint32(12),
- firstOffset: view.getUint32(16)
- },
- referenceCount = view.getUint16(22),
- i;
-
- for (i = 24; referenceCount; i += 12, referenceCount--) {
- result.references.push({
- referenceType: (data[i] & 0x80) >>> 7,
- referencedSize: view.getUint32(i) & 0x7FFFFFFF,
- subsegmentDuration: view.getUint32(i + 4),
- startsWithSap: !!(data[i + 8] & 0x80),
- sapType: (data[i + 8] & 0x70) >>> 4,
- sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
- });
- }
-
- return result;
- },
- smhd: function smhd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- balance: data[4] + data[5] / 256
- };
- },
- stbl: function stbl(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- stco: function stco(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- chunkOffsets: []
- },
- entryCount = view.getUint32(4),
- i;
- for (i = 8; entryCount; i += 4, entryCount--) {
- result.chunkOffsets.push(view.getUint32(i));
- }
- return result;
- },
- stsc: function stsc(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- entryCount = view.getUint32(4),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleToChunks: []
- },
- i;
- for (i = 8; entryCount; i += 12, entryCount--) {
- result.sampleToChunks.push({
- firstChunk: view.getUint32(i),
- samplesPerChunk: view.getUint32(i + 4),
- sampleDescriptionIndex: view.getUint32(i + 8)
- });
- }
- return result;
- },
- stsd: function stsd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleDescriptions: inspectMp4(data.subarray(8))
- };
- },
- stsz: function stsz(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleSize: view.getUint32(4),
- entries: []
- },
- i;
- for (i = 12; i < data.byteLength; i += 4) {
- result.entries.push(view.getUint32(i));
- }
- return result;
- },
- stts: function stts(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- timeToSamples: []
- },
- entryCount = view.getUint32(4),
- i;
-
- for (i = 8; entryCount; i += 8, entryCount--) {
- result.timeToSamples.push({
- sampleCount: view.getUint32(i),
- sampleDelta: view.getUint32(i + 4)
- });
- }
- return result;
- },
- styp: function styp(data) {
- return parse.ftyp(data);
- },
- tfdt: function tfdt(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- baseMediaDecodeTime: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
- };
- if (result.version === 1) {
- result.baseMediaDecodeTime *= Math.pow(2, 32);
- result.baseMediaDecodeTime += data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11];
- }
- return result;
- },
- tfhd: function tfhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- trackId: view.getUint32(4)
- },
- baseDataOffsetPresent = result.flags[2] & 0x01,
- sampleDescriptionIndexPresent = result.flags[2] & 0x02,
- defaultSampleDurationPresent = result.flags[2] & 0x08,
- defaultSampleSizePresent = result.flags[2] & 0x10,
- defaultSampleFlagsPresent = result.flags[2] & 0x20,
- durationIsEmpty = result.flags[0] & 0x010000,
- defaultBaseIsMoof = result.flags[0] & 0x020000,
- i;
-
- i = 8;
- if (baseDataOffsetPresent) {
- i += 4; // truncate top 4 bytes
- // FIXME: should we read the full 64 bits?
- result.baseDataOffset = view.getUint32(12);
- i += 4;
- }
- if (sampleDescriptionIndexPresent) {
- result.sampleDescriptionIndex = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleDurationPresent) {
- result.defaultSampleDuration = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleSizePresent) {
- result.defaultSampleSize = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleFlagsPresent) {
- result.defaultSampleFlags = view.getUint32(i);
- }
- if (durationIsEmpty) {
- result.durationIsEmpty = true;
- }
- if (!baseDataOffsetPresent && defaultBaseIsMoof) {
- result.baseDataOffsetIsMoof = true;
- }
- return result;
- },
- tkhd: function tkhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4))
- };
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.trackId = view.getUint32(i);
- i += 4;
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.trackId = view.getUint32(i);
- i += 4;
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
- i += 2 * 4;
- result.layer = view.getUint16(i);
- i += 2;
- result.alternateGroup = view.getUint16(i);
- i += 2;
- // convert fixed-point, base 16 back to a number
- result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
- i += 2;
- i += 2;
- result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
- i += 9 * 4;
- result.width = view.getUint16(i) + view.getUint16(i + 2) / 16;
- i += 4;
- result.height = view.getUint16(i) + view.getUint16(i + 2) / 16;
- return result;
- },
- traf: function traf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- trak: function trak(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- trex: function trex(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- trackId: view.getUint32(4),
- defaultSampleDescriptionIndex: view.getUint32(8),
- defaultSampleDuration: view.getUint32(12),
- defaultSampleSize: view.getUint32(16),
- sampleDependsOn: data[20] & 0x03,
- sampleIsDependedOn: (data[21] & 0xc0) >> 6,
- sampleHasRedundancy: (data[21] & 0x30) >> 4,
- samplePaddingValue: (data[21] & 0x0e) >> 1,
- sampleIsDifferenceSample: !!(data[21] & 0x01),
- sampleDegradationPriority: view.getUint16(22)
- };
- },
- trun: function trun(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- samples: []
- },
- view = new DataView(data.buffer, data.byteOffset, data.byteLength),
-
-
- // Flag interpretation
- dataOffsetPresent = result.flags[2] & 0x01,
-
- // compare with 2nd byte of 0x1
- firstSampleFlagsPresent = result.flags[2] & 0x04,
-
- // compare with 2nd byte of 0x4
- sampleDurationPresent = result.flags[1] & 0x01,
-
- // compare with 2nd byte of 0x100
- sampleSizePresent = result.flags[1] & 0x02,
-
- // compare with 2nd byte of 0x200
- sampleFlagsPresent = result.flags[1] & 0x04,
-
- // compare with 2nd byte of 0x400
- sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
-
- // compare with 2nd byte of 0x800
- sampleCount = view.getUint32(4),
- offset = 8,
- sample;
-
- if (dataOffsetPresent) {
- // 32 bit signed integer
- result.dataOffset = view.getInt32(offset);
- offset += 4;
- }
-
- // Overrides the flags for the first sample only. The order of
- // optional values will be: duration, size, compositionTimeOffset
- if (firstSampleFlagsPresent && sampleCount) {
- sample = {
- flags: parseSampleFlags(data.subarray(offset, offset + 4))
- };
- offset += 4;
- if (sampleDurationPresent) {
- sample.duration = view.getUint32(offset);
- offset += 4;
- }
- if (sampleSizePresent) {
- sample.size = view.getUint32(offset);
- offset += 4;
- }
- if (sampleCompositionTimeOffsetPresent) {
- // Note: this should be a signed int if version is 1
- sample.compositionTimeOffset = view.getUint32(offset);
- offset += 4;
- }
- result.samples.push(sample);
- sampleCount--;
- }
-
- while (sampleCount--) {
- sample = {};
- if (sampleDurationPresent) {
- sample.duration = view.getUint32(offset);
- offset += 4;
- }
- if (sampleSizePresent) {
- sample.size = view.getUint32(offset);
- offset += 4;
- }
- if (sampleFlagsPresent) {
- sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
- offset += 4;
- }
- if (sampleCompositionTimeOffsetPresent) {
- // Note: this should be a signed int if version is 1
- sample.compositionTimeOffset = view.getUint32(offset);
- offset += 4;
- }
- result.samples.push(sample);
- }
- return result;
- },
- 'url ': function url(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4))
- };
- },
- vmhd: function vmhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- graphicsmode: view.getUint16(4),
- opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)])
- };
- }
- };
-
- /**
- * Return a javascript array of box objects parsed from an ISO base
- * media file.
- * @param data {Uint8Array} the binary data of the media to be inspected
- * @return {array} a javascript array of potentially nested box objects
- */
- inspectMp4 = function inspectMp4(data) {
- var i = 0,
- result = [],
- view,
- size,
- type,
- end,
- box;
-
- // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
- var ab = new ArrayBuffer(data.length);
- var v = new Uint8Array(ab);
- for (var z = 0; z < data.length; ++z) {
- v[z] = data[z];
- }
- view = new DataView(ab);
-
- while (i < data.byteLength) {
- // parse box data
- size = view.getUint32(i);
- type = parseType$1(data.subarray(i + 4, i + 8));
- end = size > 1 ? i + size : data.byteLength;
-
- // parse type-specific data
- box = (parse[type] || function (data) {
- return {
- data: data
- };
- })(data.subarray(i + 8, end));
- box.size = size;
- box.type = type;
-
- // store this box and move to the next
- result.push(box);
- i = end;
- }
- return result;
- };
-
- /**
- * Returns a textual representation of the javascript represtentation
- * of an MP4 file. You can use it as an alternative to
- * JSON.stringify() to compare inspected MP4s.
- * @param inspectedMp4 {array} the parsed array of boxes in an MP4
- * file
- * @param depth {number} (optional) the number of ancestor boxes of
- * the elements of inspectedMp4. Assumed to be zero if unspecified.
- * @return {string} a text representation of the parsed MP4
- */
- _textifyMp = function textifyMp4(inspectedMp4, depth) {
- var indent;
- depth = depth || 0;
- indent = new Array(depth * 2 + 1).join(' ');
-
- // iterate over all the boxes
- return inspectedMp4.map(function (box, index) {
-
- // list the box type first at the current indentation level
- return indent + box.type + '\n' +
-
- // the type is already included and handle child boxes separately
- Object.keys(box).filter(function (key) {
- return key !== 'type' && key !== 'boxes';
-
- // output all the box properties
- }).map(function (key) {
- var prefix = indent + ' ' + key + ': ',
- value = box[key];
-
- // print out raw bytes as hexademical
- if (value instanceof Uint8Array || value instanceof Uint32Array) {
- var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (byte) {
- return ' ' + ('00' + byte.toString(16)).slice(-2);
- }).join('').match(/.{1,24}/g);
- if (!bytes) {
- return prefix + '<>';
- }
- if (bytes.length === 1) {
- return prefix + '<' + bytes.join('').slice(1) + '>';
- }
- return prefix + '<\n' + bytes.map(function (line) {
- return indent + ' ' + line;
- }).join('\n') + '\n' + indent + ' >';
- }
-
- // stringify generic objects
- return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) {
- if (index === 0) {
- return line;
- }
- return indent + ' ' + line;
- }).join('\n');
- }).join('\n') + (
-
- // recursively textify the child boxes
- box.boxes ? '\n' + _textifyMp(box.boxes, depth + 1) : '');
- }).join('\n');
- };
-
- var mp4Inspector = {
- inspect: inspectMp4,
- textify: _textifyMp,
- parseTfdt: parse.tfdt,
- parseHdlr: parse.hdlr,
- parseTfhd: parse.tfhd,
- parseTrun: parse.trun
- };
-
- var discardEmulationPreventionBytes$1 = captionPacketParser.discardEmulationPreventionBytes;
- var CaptionStream$1 = captionStream.CaptionStream;
-
- /**
- * Maps an offset in the mdat to a sample based on the the size of the samples.
- * Assumes that `parseSamples` has been called first.
- *
- * @param {Number} offset - The offset into the mdat
- * @param {Object[]} samples - An array of samples, parsed using `parseSamples`
- * @return {?Object} The matching sample, or null if no match was found.
- *
- * @see ISO-BMFF-12/2015, Section 8.8.8
- **/
- var mapToSample = function mapToSample(offset, samples) {
- var approximateOffset = offset;
-
- for (var i = 0; i < samples.length; i++) {
- var sample = samples[i];
-
- if (approximateOffset < sample.size) {
- return sample;
- }
-
- approximateOffset -= sample.size;
- }
-
- return null;
- };
-
- /**
- * Finds SEI nal units contained in a Media Data Box.
- * Assumes that `parseSamples` has been called first.
- *
- * @param {Uint8Array} avcStream - The bytes of the mdat
- * @param {Object[]} samples - The samples parsed out by `parseSamples`
- * @param {Number} trackId - The trackId of this video track
- * @return {Object[]} seiNals - the parsed SEI NALUs found.
- * The contents of the seiNal should match what is expected by
- * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
- *
- * @see ISO-BMFF-12/2015, Section 8.1.1
- * @see Rec. ITU-T H.264, 7.3.2.3.1
- **/
- var findSeiNals = function findSeiNals(avcStream, samples, trackId) {
- var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
- result = [],
- seiNal,
- i,
- length,
- lastMatchedSample;
-
- for (i = 0; i + 4 < avcStream.length; i += length) {
- length = avcView.getUint32(i);
- i += 4;
-
- // Bail if this doesn't appear to be an H264 stream
- if (length <= 0) {
- continue;
- }
-
- switch (avcStream[i] & 0x1F) {
- case 0x06:
- var data = avcStream.subarray(i + 1, i + 1 + length);
- var matchingSample = mapToSample(i, samples);
-
- seiNal = {
- nalUnitType: 'sei_rbsp',
- size: length,
- data: data,
- escapedRBSP: discardEmulationPreventionBytes$1(data),
- trackId: trackId
- };
-
- if (matchingSample) {
- seiNal.pts = matchingSample.pts;
- seiNal.dts = matchingSample.dts;
- lastMatchedSample = matchingSample;
- } else {
- // If a matching sample cannot be found, use the last
- // sample's values as they should be as close as possible
- seiNal.pts = lastMatchedSample.pts;
- seiNal.dts = lastMatchedSample.dts;
- }
-
- result.push(seiNal);
- break;
- default:
- break;
- }
- }
-
- return result;
- };
-
- /**
- * Parses sample information out of Track Run Boxes and calculates
- * the absolute presentation and decode timestamps of each sample.
- *
- * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
- * @param {Number} baseMediaDecodeTime - base media decode time from tfdt
- @see ISO-BMFF-12/2015, Section 8.8.12
- * @param {Object} tfhd - The parsed Track Fragment Header
- * @see inspect.parseTfhd
- * @return {Object[]} the parsed samples
- *
- * @see ISO-BMFF-12/2015, Section 8.8.8
- **/
- var parseSamples = function parseSamples(truns, baseMediaDecodeTime, tfhd) {
- var currentDts = baseMediaDecodeTime;
- var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
- var defaultSampleSize = tfhd.defaultSampleSize || 0;
- var trackId = tfhd.trackId;
- var allSamples = [];
-
- truns.forEach(function (trun) {
- // Note: We currently do not parse the sample table as well
- // as the trun. It's possible some sources will require this.
- // moov > trak > mdia > minf > stbl
- var trackRun = mp4Inspector.parseTrun(trun);
- var samples = trackRun.samples;
-
- samples.forEach(function (sample) {
- if (sample.duration === undefined) {
- sample.duration = defaultSampleDuration;
- }
- if (sample.size === undefined) {
- sample.size = defaultSampleSize;
- }
- sample.trackId = trackId;
- sample.dts = currentDts;
- if (sample.compositionTimeOffset === undefined) {
- sample.compositionTimeOffset = 0;
- }
- sample.pts = currentDts + sample.compositionTimeOffset;
-
- currentDts += sample.duration;
- });
-
- allSamples = allSamples.concat(samples);
- });
-
- return allSamples;
- };
-
- /**
- * Parses out caption nals from an FMP4 segment's video tracks.
- *
- * @param {Uint8Array} segment - The bytes of a single segment
- * @param {Number} videoTrackId - The trackId of a video track in the segment
- * @return {Object.<Number, Object[]>} A mapping of video trackId to
- * a list of seiNals found in that track
- **/
- var parseCaptionNals = function parseCaptionNals(segment, videoTrackId) {
- // To get the samples
- var trafs = probe.findBox(segment, ['moof', 'traf']);
- // To get SEI NAL units
- var mdats = probe.findBox(segment, ['mdat']);
- var captionNals = {};
- var mdatTrafPairs = [];
-
- // Pair up each traf with a mdat as moofs and mdats are in pairs
- mdats.forEach(function (mdat, index) {
- var matchingTraf = trafs[index];
- mdatTrafPairs.push({
- mdat: mdat,
- traf: matchingTraf
- });
- });
-
- mdatTrafPairs.forEach(function (pair) {
- var mdat = pair.mdat;
- var traf = pair.traf;
- var tfhd = probe.findBox(traf, ['tfhd']);
- // Exactly 1 tfhd per traf
- var headerInfo = mp4Inspector.parseTfhd(tfhd[0]);
- var trackId = headerInfo.trackId;
- var tfdt = probe.findBox(traf, ['tfdt']);
- // Either 0 or 1 tfdt per traf
- var baseMediaDecodeTime = tfdt.length > 0 ? mp4Inspector.parseTfdt(tfdt[0]).baseMediaDecodeTime : 0;
- var truns = probe.findBox(traf, ['trun']);
- var samples;
- var seiNals;
-
- // Only parse video data for the chosen video track
- if (videoTrackId === trackId && truns.length > 0) {
- samples = parseSamples(truns, baseMediaDecodeTime, headerInfo);
-
- seiNals = findSeiNals(mdat, samples, trackId);
-
- if (!captionNals[trackId]) {
- captionNals[trackId] = [];
- }
-
- captionNals[trackId] = captionNals[trackId].concat(seiNals);
- }
- });
-
- return captionNals;
- };
-
- /**
- * Parses out inband captions from an MP4 container and returns
- * caption objects that can be used by WebVTT and the TextTrack API.
- * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
- * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
- * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
- *
- * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
- * @param {Number} trackId - The id of the video track to parse
- * @param {Number} timescale - The timescale for the video track from the init segment
- *
- * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
- * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
- * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
- * @return {String} parsedCaptions[].text - The visible content of the caption
- **/
- var parseEmbeddedCaptions = function parseEmbeddedCaptions(segment, trackId, timescale) {
- var seiNals;
-
- if (!trackId) {
- return null;
- }
-
- seiNals = parseCaptionNals(segment, trackId);
-
- return {
- seiNals: seiNals[trackId],
- timescale: timescale
- };
- };
-
- /**
- * Converts SEI NALUs into captions that can be used by video.js
- **/
- var CaptionParser = function CaptionParser() {
- var isInitialized = false;
- var captionStream$$1;
-
- // Stores segments seen before trackId and timescale are set
- var segmentCache;
- // Stores video track ID of the track being parsed
- var trackId;
- // Stores the timescale of the track being parsed
- var timescale;
- // Stores captions parsed so far
- var parsedCaptions;
-
- /**
- * A method to indicate whether a CaptionParser has been initalized
- * @returns {Boolean}
- **/
- this.isInitialized = function () {
- return isInitialized;
- };
-
- /**
- * Initializes the underlying CaptionStream, SEI NAL parsing
- * and management, and caption collection
- **/
- this.init = function () {
- captionStream$$1 = new CaptionStream$1();
- isInitialized = true;
-
- // Collect dispatched captions
- captionStream$$1.on('data', function (event) {
- // Convert to seconds in the source's timescale
- event.startTime = event.startPts / timescale;
- event.endTime = event.endPts / timescale;
-
- parsedCaptions.captions.push(event);
- parsedCaptions.captionStreams[event.stream] = true;
- });
- };
-
- /**
- * Determines if a new video track will be selected
- * or if the timescale changed
- * @return {Boolean}
- **/
- this.isNewInit = function (videoTrackIds, timescales) {
- if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) {
- return false;
- }
-
- return trackId !== videoTrackIds[0] || timescale !== timescales[trackId];
- };
-
- /**
- * Parses out SEI captions and interacts with underlying
- * CaptionStream to return dispatched captions
- *
- * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
- * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
- * @param {Object.<Number, Number>} timescales - The timescales found in the init segment
- * @see parseEmbeddedCaptions
- * @see m2ts/caption-stream.js
- **/
- this.parse = function (segment, videoTrackIds, timescales) {
- var parsedData;
-
- if (!this.isInitialized()) {
- return null;
-
- // This is not likely to be a video segment
- } else if (!videoTrackIds || !timescales) {
- return null;
- } else if (this.isNewInit(videoTrackIds, timescales)) {
- // Use the first video track only as there is no
- // mechanism to switch to other video tracks
- trackId = videoTrackIds[0];
- timescale = timescales[trackId];
-
- // If an init segment has not been seen yet, hold onto segment
- // data until we have one
- } else if (!trackId || !timescale) {
- segmentCache.push(segment);
- return null;
- }
-
- // Now that a timescale and trackId is set, parse cached segments
- while (segmentCache.length > 0) {
- var cachedSegment = segmentCache.shift();
-
- this.parse(cachedSegment, videoTrackIds, timescales);
- }
-
- parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
-
- if (parsedData === null || !parsedData.seiNals) {
- return null;
- }
-
- this.pushNals(parsedData.seiNals);
- // Force the parsed captions to be dispatched
- this.flushStream();
-
- return parsedCaptions;
- };
-
- /**
- * Pushes SEI NALUs onto CaptionStream
- * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
- * Assumes that `parseCaptionNals` has been called first
- * @see m2ts/caption-stream.js
- **/
- this.pushNals = function (nals) {
- if (!this.isInitialized() || !nals || nals.length === 0) {
- return null;
- }
-
- nals.forEach(function (nal) {
- captionStream$$1.push(nal);
- });
- };
-
- /**
- * Flushes underlying CaptionStream to dispatch processed, displayable captions
- * @see m2ts/caption-stream.js
- **/
- this.flushStream = function () {
- if (!this.isInitialized()) {
- return null;
- }
-
- captionStream$$1.flush();
- };
-
- /**
- * Reset caption buckets for new data
- **/
- this.clearParsedCaptions = function () {
- parsedCaptions.captions = [];
- parsedCaptions.captionStreams = {};
- };
-
- /**
- * Resets underlying CaptionStream
- * @see m2ts/caption-stream.js
- **/
- this.resetCaptionStream = function () {
- if (!this.isInitialized()) {
- return null;
- }
-
- captionStream$$1.reset();
- };
-
- /**
- * Convenience method to clear all captions flushed from the
- * CaptionStream and still being parsed
- * @see m2ts/caption-stream.js
- **/
- this.clearAllCaptions = function () {
- this.clearParsedCaptions();
- this.resetCaptionStream();
- };
-
- /**
- * Reset caption parser
- **/
- this.reset = function () {
- segmentCache = [];
- trackId = null;
- timescale = null;
-
- if (!parsedCaptions) {
- parsedCaptions = {
- captions: [],
- // CC1, CC2, CC3, CC4
- captionStreams: {}
- };
- } else {
- this.clearParsedCaptions();
- }
-
- this.resetCaptionStream();
- };
-
- this.reset();
- };
-
- var captionParser = CaptionParser;
-
- var mp4 = {
- generator: mp4Generator,
- probe: probe,
- Transmuxer: transmuxer.Transmuxer,
- AudioSegmentStream: transmuxer.AudioSegmentStream,
- VideoSegmentStream: transmuxer.VideoSegmentStream,
- CaptionParser: captionParser
- };
-
- var classCallCheck = function classCallCheck(instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- var createClass = function () {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
- }
- }
-
- return function (Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- };
- }();
-
- /**
- * @file transmuxer-worker.js
- */
-
- /**
- * Re-emits transmuxer events by converting them into messages to the
- * world outside the worker.
- *
- * @param {Object} transmuxer the transmuxer to wire events on
- * @private
- */
- var wireTransmuxerEvents = function wireTransmuxerEvents(self, transmuxer) {
- transmuxer.on('data', function (segment) {
- // transfer ownership of the underlying ArrayBuffer
- // instead of doing a copy to save memory
- // ArrayBuffers are transferable but generic TypedArrays are not
- // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects)
- var initArray = segment.initSegment;
-
- segment.initSegment = {
- data: initArray.buffer,
- byteOffset: initArray.byteOffset,
- byteLength: initArray.byteLength
- };
-
- var typedArray = segment.data;
-
- segment.data = typedArray.buffer;
- self.postMessage({
- action: 'data',
- segment: segment,
- byteOffset: typedArray.byteOffset,
- byteLength: typedArray.byteLength
- }, [segment.data]);
- });
-
- if (transmuxer.captionStream) {
- transmuxer.captionStream.on('data', function (caption) {
- self.postMessage({
- action: 'caption',
- data: caption
- });
- });
- }
-
- transmuxer.on('done', function (data) {
- self.postMessage({ action: 'done' });
- });
-
- transmuxer.on('gopInfo', function (gopInfo) {
- self.postMessage({
- action: 'gopInfo',
- gopInfo: gopInfo
- });
- });
- };
-
- /**
- * All incoming messages route through this hash. If no function exists
- * to handle an incoming message, then we ignore the message.
- *
- * @class MessageHandlers
- * @param {Object} options the options to initialize with
- */
-
- var MessageHandlers = function () {
- function MessageHandlers(self, options) {
- classCallCheck(this, MessageHandlers);
-
- this.options = options || {};
- this.self = self;
- this.init();
- }
-
- /**
- * initialize our web worker and wire all the events.
- */
-
- createClass(MessageHandlers, [{
- key: 'init',
- value: function init() {
- if (this.transmuxer) {
- this.transmuxer.dispose();
- }
- this.transmuxer = new mp4.Transmuxer(this.options);
- wireTransmuxerEvents(this.self, this.transmuxer);
- }
-
- /**
- * Adds data (a ts segment) to the start of the transmuxer pipeline for
- * processing.
- *
- * @param {ArrayBuffer} data data to push into the muxer
- */
-
- }, {
- key: 'push',
- value: function push(data) {
- // Cast array buffer to correct type for transmuxer
- var segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
-
- this.transmuxer.push(segment);
- }
-
- /**
- * Recreate the transmuxer so that the next segment added via `push`
- * start with a fresh transmuxer.
- */
-
- }, {
- key: 'reset',
- value: function reset() {
- this.init();
- }
-
- /**
- * Set the value that will be used as the `baseMediaDecodeTime` time for the
- * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime`
- * set relative to the first based on the PTS values.
- *
- * @param {Object} data used to set the timestamp offset in the muxer
- */
-
- }, {
- key: 'setTimestampOffset',
- value: function setTimestampOffset(data) {
- var timestampOffset = data.timestampOffset || 0;
-
- this.transmuxer.setBaseMediaDecodeTime(Math.round(timestampOffset * 90000));
- }
- }, {
- key: 'setAudioAppendStart',
- value: function setAudioAppendStart(data) {
- this.transmuxer.setAudioAppendStart(Math.ceil(data.appendStart * 90000));
- }
-
- /**
- * Forces the pipeline to finish processing the last segment and emit it's
- * results.
- *
- * @param {Object} data event data, not really used
- */
-
- }, {
- key: 'flush',
- value: function flush(data) {
- this.transmuxer.flush();
- }
- }, {
- key: 'resetCaptions',
- value: function resetCaptions() {
- this.transmuxer.resetCaptions();
- }
- }, {
- key: 'alignGopsWith',
- value: function alignGopsWith(data) {
- this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice());
- }
- }]);
- return MessageHandlers;
- }();
-
- /**
- * Our web wroker interface so that things can talk to mux.js
- * that will be running in a web worker. the scope is passed to this by
- * webworkify.
- *
- * @param {Object} self the scope for the web worker
- */
-
- var TransmuxerWorker = function TransmuxerWorker(self) {
- self.onmessage = function (event) {
- if (event.data.action === 'init' && event.data.options) {
- this.messageHandlers = new MessageHandlers(self, event.data.options);
- return;
- }
-
- if (!this.messageHandlers) {
- this.messageHandlers = new MessageHandlers(self);
- }
-
- if (event.data && event.data.action && event.data.action !== 'init') {
- if (this.messageHandlers[event.data.action]) {
- this.messageHandlers[event.data.action](event.data);
- }
- }
- };
- };
-
- var transmuxerWorker = new TransmuxerWorker(self);
-
- return transmuxerWorker;
- }();
- });
-
- /**
- * @file - codecs.js - Handles tasks regarding codec strings such as translating them to
- * codec strings, or translating codec strings into objects that can be examined.
- */
-
- // Default codec parameters if none were provided for video and/or audio
- var defaultCodecs = {
- videoCodec: 'avc1',
- videoObjectTypeIndicator: '.4d400d',
- // AAC-LC
- audioProfile: '2'
- };
-
- /**
- * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard
- * `avc1.<hhhhhh>`
- *
- * @param {Array} codecs an array of codec strings to fix
- * @return {Array} the translated codec array
- * @private
- */
- var translateLegacyCodecs = function translateLegacyCodecs(codecs) {
- return codecs.map(function (codec) {
- return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) {
- var profileHex = ('00' + Number(profile).toString(16)).slice(-2);
- var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2);
-
- return 'avc1.' + profileHex + '00' + avcLevelHex;
- });
- });
- };
-
- /**
- * Parses a codec string to retrieve the number of codecs specified,
- * the video codec and object type indicator, and the audio profile.
- */
-
- var parseCodecs = function parseCodecs() {
- var codecs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
-
- var result = {
- codecCount: 0
- };
- var parsed = void 0;
-
- result.codecCount = codecs.split(',').length;
- result.codecCount = result.codecCount || 2;
-
- // parse the video codec
- parsed = /(^|\s|,)+(avc[13])([^ ,]*)/i.exec(codecs);
- if (parsed) {
- result.videoCodec = parsed[2];
- result.videoObjectTypeIndicator = parsed[3];
- }
-
- // parse the last field of the audio codec
- result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs);
- result.audioProfile = result.audioProfile && result.audioProfile[2];
-
- return result;
- };
-
- /**
- * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
- * standard `avc1.<hhhhhh>`.
- *
- * @param codecString {String} the codec string
- * @return {String} the codec string with old apple-style codecs replaced
- *
- * @private
- */
- var mapLegacyAvcCodecs = function mapLegacyAvcCodecs(codecString) {
- return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) {
- return translateLegacyCodecs([match])[0];
- });
- };
-
- /**
- * Build a media mime-type string from a set of parameters
- * @param {String} type either 'audio' or 'video'
- * @param {String} container either 'mp2t' or 'mp4'
- * @param {Array} codecs an array of codec strings to add
- * @return {String} a valid media mime-type
- */
- var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) {
- // The codecs array is filtered so that falsey values are
- // dropped and don't cause Array#join to create spurious
- // commas
- return type + '/' + container + '; codecs="' + codecs.filter(function (c) {
- return !!c;
- }).join(', ') + '"';
- };
-
- /**
- * Returns the type container based on information in the playlist
- * @param {Playlist} media the current media playlist
- * @return {String} a valid media container type
- */
- var getContainerType = function getContainerType(media) {
- // An initialization segment means the media playlist is an iframe
- // playlist or is using the mp4 container. We don't currently
- // support iframe playlists, so assume this is signalling mp4
- // fragments.
- if (media.segments && media.segments.length && media.segments[0].map) {
- return 'mp4';
- }
- return 'mp2t';
- };
-
- /**
- * Returns a set of codec strings parsed from the playlist or the default
- * codec strings if no codecs were specified in the playlist
- * @param {Playlist} media the current media playlist
- * @return {Object} an object with the video and audio codecs
- */
- var getCodecs = function getCodecs(media) {
- // if the codecs were explicitly specified, use them instead of the
- // defaults
- var mediaAttributes = media.attributes || {};
-
- if (mediaAttributes.CODECS) {
- return parseCodecs(mediaAttributes.CODECS);
- }
- return defaultCodecs;
- };
-
- var audioProfileFromDefault = function audioProfileFromDefault(master, audioGroupId) {
- if (!master.mediaGroups.AUDIO || !audioGroupId) {
- return null;
- }
-
- var audioGroup = master.mediaGroups.AUDIO[audioGroupId];
-
- if (!audioGroup) {
- return null;
- }
-
- for (var name in audioGroup) {
- var audioType = audioGroup[name];
-
- if (audioType.default && audioType.playlists) {
- // codec should be the same for all playlists within the audio type
- return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile;
- }
- }
-
- return null;
- };
-
- /**
- * Calculates the MIME type strings for a working configuration of
- * SourceBuffers to play variant streams in a master playlist. If
- * there is no possible working configuration, an empty array will be
- * returned.
- *
- * @param master {Object} the m3u8 object for the master playlist
- * @param media {Object} the m3u8 object for the variant playlist
- * @return {Array} the MIME type strings. If the array has more than
- * one entry, the first element should be applied to the video
- * SourceBuffer and the second to the audio SourceBuffer.
- *
- * @private
- */
- var mimeTypesForPlaylist = function mimeTypesForPlaylist(master, media) {
- var containerType = getContainerType(media);
- var codecInfo = getCodecs(media);
- var mediaAttributes = media.attributes || {};
- // Default condition for a traditional HLS (no demuxed audio/video)
- var isMuxed = true;
- var isMaat = false;
-
- if (!media) {
- // Not enough information
- return [];
- }
-
- if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) {
- var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
-
- // Handle the case where we are in a multiple-audio track scenario
- if (audioGroup) {
- isMaat = true;
- // Start with the everything demuxed then...
- isMuxed = false;
- // ...check to see if any audio group tracks are muxed (ie. lacking a uri)
- for (var groupId in audioGroup) {
- // either a uri is present (if the case of HLS and an external playlist), or
- // playlists is present (in the case of DASH where we don't have external audio
- // playlists)
- if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) {
- isMuxed = true;
- break;
- }
- }
- }
- }
-
- // HLS with multiple-audio tracks must always get an audio codec.
- // Put another way, there is no way to have a video-only multiple-audio HLS!
- if (isMaat && !codecInfo.audioProfile) {
- if (!isMuxed) {
- // It is possible for codecs to be specified on the audio media group playlist but
- // not on the rendition playlist. This is mostly the case for DASH, where audio and
- // video are always separate (and separately specified).
- codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO);
- }
-
- if (!codecInfo.audioProfile) {
- videojs.log.warn('Multiple audio tracks present but no audio codec string is specified. ' + 'Attempting to use the default audio codec (mp4a.40.2)');
- codecInfo.audioProfile = defaultCodecs.audioProfile;
- }
- }
-
- // Generate the final codec strings from the codec object generated above
- var codecStrings = {};
-
- if (codecInfo.videoCodec) {
- codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator;
- }
-
- if (codecInfo.audioProfile) {
- codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile;
- }
-
- // Finally, make and return an array with proper mime-types depending on
- // the configuration
- var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]);
- var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]);
- var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]);
-
- if (isMaat) {
- if (!isMuxed && codecStrings.video) {
- return [justVideo, justAudio];
- }
-
- if (!isMuxed && !codecStrings.video) {
- // There is no muxed content and no video codec string, so this is an audio only
- // stream with alternate audio.
- return [justAudio, justAudio];
- }
-
- // There exists the possiblity that this will return a `video/container`
- // mime-type for the first entry in the array even when there is only audio.
- // This doesn't appear to be a problem and simplifies the code.
- return [bothVideoAudio, justAudio];
- }
-
- // If there is no video codec at all, always just return a single
- // audio/<container> mime-type
- if (!codecStrings.video) {
- return [justAudio];
- }
-
- // When not using separate audio media groups, audio and video is
- // *always* muxed
- return [bothVideoAudio];
- };
-
- /**
- * Parse a content type header into a type and parameters
- * object
- *
- * @param {String} type the content type header
- * @return {Object} the parsed content-type
- * @private
- */
- var parseContentType = function parseContentType(type) {
- var object = { type: '', parameters: {} };
- var parameters = type.trim().split(';');
-
- // first parameter should always be content-type
- object.type = parameters.shift().trim();
- parameters.forEach(function (parameter) {
- var pair = parameter.trim().split('=');
-
- if (pair.length > 1) {
- var name = pair[0].replace(/"/g, '').trim();
- var value = pair[1].replace(/"/g, '').trim();
-
- object.parameters[name] = value;
- }
- });
-
- return object;
- };
-
- /**
- * Check if a codec string refers to an audio codec.
- *
- * @param {String} codec codec string to check
- * @return {Boolean} if this is an audio codec
- * @private
- */
- var isAudioCodec = function isAudioCodec(codec) {
- return (/mp4a\.\d+.\d+/i.test(codec)
- );
- };
-
- /**
- * Check if a codec string refers to a video codec.
- *
- * @param {String} codec codec string to check
- * @return {Boolean} if this is a video codec
- * @private
- */
- var isVideoCodec = function isVideoCodec(codec) {
- return (/avc1\.[\da-f]+/i.test(codec)
- );
- };
-
- /**
- * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in
- * front of current time.
- *
- * @param {Array} buffer
- * The current buffer of gop information
- * @param {Number} currentTime
- * The current time
- * @param {Double} mapping
- * Offset to map display time to stream presentation time
- * @return {Array}
- * List of gops considered safe to append over
- */
- var gopsSafeToAlignWith = function gopsSafeToAlignWith(buffer, currentTime, mapping) {
- if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) {
- return [];
- }
-
- // pts value for current time + 3 seconds to give a bit more wiggle room
- var currentTimePts = Math.ceil((currentTime - mapping + 3) * 90000);
-
- var i = void 0;
-
- for (i = 0; i < buffer.length; i++) {
- if (buffer[i].pts > currentTimePts) {
- break;
- }
- }
-
- return buffer.slice(i);
- };
-
- /**
- * Appends gop information (timing and byteLength) received by the transmuxer for the
- * gops appended in the last call to appendBuffer
- *
- * @param {Array} buffer
- * The current buffer of gop information
- * @param {Array} gops
- * List of new gop information
- * @param {boolean} replace
- * If true, replace the buffer with the new gop information. If false, append the
- * new gop information to the buffer in the right location of time.
- * @return {Array}
- * Updated list of gop information
- */
- var updateGopBuffer = function updateGopBuffer(buffer, gops, replace) {
- if (!gops.length) {
- return buffer;
- }
-
- if (replace) {
- // If we are in safe append mode, then completely overwrite the gop buffer
- // with the most recent appeneded data. This will make sure that when appending
- // future segments, we only try to align with gops that are both ahead of current
- // time and in the last segment appended.
- return gops.slice();
- }
-
- var start = gops[0].pts;
-
- var i = 0;
-
- for (i; i < buffer.length; i++) {
- if (buffer[i].pts >= start) {
- break;
- }
- }
-
- return buffer.slice(0, i).concat(gops);
- };
-
- /**
- * Removes gop information in buffer that overlaps with provided start and end
- *
- * @param {Array} buffer
- * The current buffer of gop information
- * @param {Double} start
- * position to start the remove at
- * @param {Double} end
- * position to end the remove at
- * @param {Double} mapping
- * Offset to map display time to stream presentation time
- */
- var removeGopBuffer = function removeGopBuffer(buffer, start, end, mapping) {
- var startPts = Math.ceil((start - mapping) * 90000);
- var endPts = Math.ceil((end - mapping) * 90000);
- var updatedBuffer = buffer.slice();
-
- var i = buffer.length;
-
- while (i--) {
- if (buffer[i].pts <= endPts) {
- break;
- }
- }
-
- if (i === -1) {
- // no removal because end of remove range is before start of buffer
- return updatedBuffer;
- }
-
- var j = i + 1;
-
- while (j--) {
- if (buffer[j].pts <= startPts) {
- break;
- }
- }
-
- // clamp remove range start to 0 index
- j = Math.max(j, 0);
-
- updatedBuffer.splice(j, i - j + 1);
-
- return updatedBuffer;
- };
-
- var buffered = function buffered(videoBuffer, audioBuffer, audioDisabled) {
- var start = null;
- var end = null;
- var arity = 0;
- var extents = [];
- var ranges = [];
-
- // neither buffer has been created yet
- if (!videoBuffer && !audioBuffer) {
- return videojs.createTimeRange();
- }
-
- // only one buffer is configured
- if (!videoBuffer) {
- return audioBuffer.buffered;
- }
- if (!audioBuffer) {
- return videoBuffer.buffered;
- }
-
- // both buffers are configured
- if (audioDisabled) {
- return videoBuffer.buffered;
- }
-
- // both buffers are empty
- if (videoBuffer.buffered.length === 0 && audioBuffer.buffered.length === 0) {
- return videojs.createTimeRange();
- }
-
- // Handle the case where we have both buffers and create an
- // intersection of the two
- var videoBuffered = videoBuffer.buffered;
- var audioBuffered = audioBuffer.buffered;
- var count = videoBuffered.length;
-
- // A) Gather up all start and end times
- while (count--) {
- extents.push({ time: videoBuffered.start(count), type: 'start' });
- extents.push({ time: videoBuffered.end(count), type: 'end' });
- }
- count = audioBuffered.length;
- while (count--) {
- extents.push({ time: audioBuffered.start(count), type: 'start' });
- extents.push({ time: audioBuffered.end(count), type: 'end' });
- }
- // B) Sort them by time
- extents.sort(function (a, b) {
- return a.time - b.time;
- });
-
- // C) Go along one by one incrementing arity for start and decrementing
- // arity for ends
- for (count = 0; count < extents.length; count++) {
- if (extents[count].type === 'start') {
- arity++;
-
- // D) If arity is ever incremented to 2 we are entering an
- // overlapping range
- if (arity === 2) {
- start = extents[count].time;
- }
- } else if (extents[count].type === 'end') {
- arity--;
-
- // E) If arity is ever decremented to 1 we leaving an
- // overlapping range
- if (arity === 1) {
- end = extents[count].time;
- }
- }
-
- // F) Record overlapping ranges
- if (start !== null && end !== null) {
- ranges.push([start, end]);
- start = null;
- end = null;
- }
- }
-
- return videojs.createTimeRanges(ranges);
- };
-
- /**
- * @file virtual-source-buffer.js
- */
-
- // We create a wrapper around the SourceBuffer so that we can manage the
- // state of the `updating` property manually. We have to do this because
- // Firefox changes `updating` to false long before triggering `updateend`
- // events and that was causing strange problems in videojs-contrib-hls
- var makeWrappedSourceBuffer = function makeWrappedSourceBuffer(mediaSource, mimeType) {
- var sourceBuffer = mediaSource.addSourceBuffer(mimeType);
- var wrapper = Object.create(null);
-
- wrapper.updating = false;
- wrapper.realBuffer_ = sourceBuffer;
-
- var _loop = function _loop(key) {
- if (typeof sourceBuffer[key] === 'function') {
- wrapper[key] = function () {
- return sourceBuffer[key].apply(sourceBuffer, arguments);
- };
- } else if (typeof wrapper[key] === 'undefined') {
- Object.defineProperty(wrapper, key, {
- get: function get$$1() {
- return sourceBuffer[key];
- },
- set: function set$$1(v) {
- return sourceBuffer[key] = v;
- }
- });
- }
- };
-
- for (var key in sourceBuffer) {
- _loop(key);
- }
-
- return wrapper;
- };
-
- /**
- * VirtualSourceBuffers exist so that we can transmux non native formats
- * into a native format, but keep the same api as a native source buffer.
- * It creates a transmuxer, that works in its own thread (a web worker) and
- * that transmuxer muxes the data into a native format. VirtualSourceBuffer will
- * then send all of that data to the naive sourcebuffer so that it is
- * indestinguishable from a natively supported format.
- *
- * @param {HtmlMediaSource} mediaSource the parent mediaSource
- * @param {Array} codecs array of codecs that we will be dealing with
- * @class VirtualSourceBuffer
- * @extends video.js.EventTarget
- */
-
- var VirtualSourceBuffer = function (_videojs$EventTarget) {
- inherits$1(VirtualSourceBuffer, _videojs$EventTarget);
-
- function VirtualSourceBuffer(mediaSource, codecs) {
- classCallCheck$1(this, VirtualSourceBuffer);
-
- var _this = possibleConstructorReturn$1(this, (VirtualSourceBuffer.__proto__ || Object.getPrototypeOf(VirtualSourceBuffer)).call(this, videojs.EventTarget));
-
- _this.timestampOffset_ = 0;
- _this.pendingBuffers_ = [];
- _this.bufferUpdating_ = false;
-
- _this.mediaSource_ = mediaSource;
- _this.codecs_ = codecs;
- _this.audioCodec_ = null;
- _this.videoCodec_ = null;
- _this.audioDisabled_ = false;
- _this.appendAudioInitSegment_ = true;
- _this.gopBuffer_ = [];
- _this.timeMapping_ = 0;
- _this.safeAppend_ = videojs.browser.IE_VERSION >= 11;
-
- var options = {
- remux: false,
- alignGopsAtEnd: _this.safeAppend_
- };
-
- _this.codecs_.forEach(function (codec) {
- if (isAudioCodec(codec)) {
- _this.audioCodec_ = codec;
- } else if (isVideoCodec(codec)) {
- _this.videoCodec_ = codec;
- }
- });
-
- // append muxed segments to their respective native buffers as
- // soon as they are available
- _this.transmuxer_ = new TransmuxWorker();
- _this.transmuxer_.postMessage({ action: 'init', options: options });
-
- _this.transmuxer_.onmessage = function (event) {
- if (event.data.action === 'data') {
- return _this.data_(event);
- }
-
- if (event.data.action === 'done') {
- return _this.done_(event);
- }
-
- if (event.data.action === 'gopInfo') {
- return _this.appendGopInfo_(event);
- }
- };
-
- // this timestampOffset is a property with the side-effect of resetting
- // baseMediaDecodeTime in the transmuxer on the setter
- Object.defineProperty(_this, 'timestampOffset', {
- get: function get$$1() {
- return this.timestampOffset_;
- },
- set: function set$$1(val) {
- if (typeof val === 'number' && val >= 0) {
- this.timestampOffset_ = val;
- this.appendAudioInitSegment_ = true;
-
- // reset gop buffer on timestampoffset as this signals a change in timeline
- this.gopBuffer_.length = 0;
- this.timeMapping_ = 0;
-
- // We have to tell the transmuxer to set the baseMediaDecodeTime to
- // the desired timestampOffset for the next segment
- this.transmuxer_.postMessage({
- action: 'setTimestampOffset',
- timestampOffset: val
- });
- }
- }
- });
-
- // setting the append window affects both source buffers
- Object.defineProperty(_this, 'appendWindowStart', {
- get: function get$$1() {
- return (this.videoBuffer_ || this.audioBuffer_).appendWindowStart;
- },
- set: function set$$1(start) {
- if (this.videoBuffer_) {
- this.videoBuffer_.appendWindowStart = start;
- }
- if (this.audioBuffer_) {
- this.audioBuffer_.appendWindowStart = start;
- }
- }
- });
-
- // this buffer is "updating" if either of its native buffers are
- Object.defineProperty(_this, 'updating', {
- get: function get$$1() {
- return !!(this.bufferUpdating_ || !this.audioDisabled_ && this.audioBuffer_ && this.audioBuffer_.updating || this.videoBuffer_ && this.videoBuffer_.updating);
- }
- });
-
- // the buffered property is the intersection of the buffered
- // ranges of the native source buffers
- Object.defineProperty(_this, 'buffered', {
- get: function get$$1() {
- return buffered(this.videoBuffer_, this.audioBuffer_, this.audioDisabled_);
- }
- });
- return _this;
- }
-
- /**
- * When we get a data event from the transmuxer
- * we call this function and handle the data that
- * was sent to us
- *
- * @private
- * @param {Event} event the data event from the transmuxer
- */
-
-
- createClass(VirtualSourceBuffer, [{
- key: 'data_',
- value: function data_(event) {
- var segment = event.data.segment;
-
- // Cast ArrayBuffer to TypedArray
- segment.data = new Uint8Array(segment.data, event.data.byteOffset, event.data.byteLength);
-
- segment.initSegment = new Uint8Array(segment.initSegment.data, segment.initSegment.byteOffset, segment.initSegment.byteLength);
-
- createTextTracksIfNecessary(this, this.mediaSource_, segment);
-
- // Add the segments to the pendingBuffers array
- this.pendingBuffers_.push(segment);
- return;
- }
-
- /**
- * When we get a done event from the transmuxer
- * we call this function and we process all
- * of the pending data that we have been saving in the
- * data_ function
- *
- * @private
- * @param {Event} event the done event from the transmuxer
- */
-
- }, {
- key: 'done_',
- value: function done_(event) {
- // Don't process and append data if the mediaSource is closed
- if (this.mediaSource_.readyState === 'closed') {
- this.pendingBuffers_.length = 0;
- return;
- }
-
- // All buffers should have been flushed from the muxer
- // start processing anything we have received
- this.processPendingSegments_();
- return;
- }
-
- /**
- * Create our internal native audio/video source buffers and add
- * event handlers to them with the following conditions:
- * 1. they do not already exist on the mediaSource
- * 2. this VSB has a codec for them
- *
- * @private
- */
-
- }, {
- key: 'createRealSourceBuffers_',
- value: function createRealSourceBuffers_() {
- var _this2 = this;
-
- var types = ['audio', 'video'];
-
- types.forEach(function (type) {
- // Don't create a SourceBuffer of this type if we don't have a
- // codec for it
- if (!_this2[type + 'Codec_']) {
- return;
- }
-
- // Do nothing if a SourceBuffer of this type already exists
- if (_this2[type + 'Buffer_']) {
- return;
- }
-
- var buffer = null;
-
- // If the mediasource already has a SourceBuffer for the codec
- // use that
- if (_this2.mediaSource_[type + 'Buffer_']) {
- buffer = _this2.mediaSource_[type + 'Buffer_'];
- // In multiple audio track cases, the audio source buffer is disabled
- // on the main VirtualSourceBuffer by the HTMLMediaSource much earlier
- // than createRealSourceBuffers_ is called to create the second
- // VirtualSourceBuffer because that happens as a side-effect of
- // videojs-contrib-hls starting the audioSegmentLoader. As a result,
- // the audioBuffer is essentially "ownerless" and no one will toggle
- // the `updating` state back to false once the `updateend` event is received
- //
- // Setting `updating` to false manually will work around this
- // situation and allow work to continue
- buffer.updating = false;
- } else {
- var codecProperty = type + 'Codec_';
- var mimeType = type + '/mp4;codecs="' + _this2[codecProperty] + '"';
-
- buffer = makeWrappedSourceBuffer(_this2.mediaSource_.nativeMediaSource_, mimeType);
-
- _this2.mediaSource_[type + 'Buffer_'] = buffer;
- }
-
- _this2[type + 'Buffer_'] = buffer;
-
- // Wire up the events to the SourceBuffer
- ['update', 'updatestart', 'updateend'].forEach(function (event) {
- buffer.addEventListener(event, function () {
- // if audio is disabled
- if (type === 'audio' && _this2.audioDisabled_) {
- return;
- }
-
- if (event === 'updateend') {
- _this2[type + 'Buffer_'].updating = false;
- }
-
- var shouldTrigger = types.every(function (t) {
- // skip checking audio's updating status if audio
- // is not enabled
- if (t === 'audio' && _this2.audioDisabled_) {
- return true;
- }
- // if the other type if updating we don't trigger
- if (type !== t && _this2[t + 'Buffer_'] && _this2[t + 'Buffer_'].updating) {
- return false;
- }
- return true;
- });
-
- if (shouldTrigger) {
- return _this2.trigger(event);
- }
- });
- });
- });
- }
-
- /**
- * Emulate the native mediasource function, but our function will
- * send all of the proposed segments to the transmuxer so that we
- * can transmux them before we append them to our internal
- * native source buffers in the correct format.
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
- * @param {Uint8Array} segment the segment to append to the buffer
- */
-
- }, {
- key: 'appendBuffer',
- value: function appendBuffer(segment) {
- // Start the internal "updating" state
- this.bufferUpdating_ = true;
-
- if (this.audioBuffer_ && this.audioBuffer_.buffered.length) {
- var audioBuffered = this.audioBuffer_.buffered;
-
- this.transmuxer_.postMessage({
- action: 'setAudioAppendStart',
- appendStart: audioBuffered.end(audioBuffered.length - 1)
- });
- }
-
- if (this.videoBuffer_) {
- this.transmuxer_.postMessage({
- action: 'alignGopsWith',
- gopsToAlignWith: gopsSafeToAlignWith(this.gopBuffer_, this.mediaSource_.player_ ? this.mediaSource_.player_.currentTime() : null, this.timeMapping_)
- });
- }
-
- this.transmuxer_.postMessage({
- action: 'push',
- // Send the typed-array of data as an ArrayBuffer so that
- // it can be sent as a "Transferable" and avoid the costly
- // memory copy
- data: segment.buffer,
-
- // To recreate the original typed-array, we need information
- // about what portion of the ArrayBuffer it was a view into
- byteOffset: segment.byteOffset,
- byteLength: segment.byteLength
- }, [segment.buffer]);
- this.transmuxer_.postMessage({ action: 'flush' });
- }
-
- /**
- * Appends gop information (timing and byteLength) received by the transmuxer for the
- * gops appended in the last call to appendBuffer
- *
- * @param {Event} event
- * The gopInfo event from the transmuxer
- * @param {Array} event.data.gopInfo
- * List of gop info to append
- */
-
- }, {
- key: 'appendGopInfo_',
- value: function appendGopInfo_(event) {
- this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, event.data.gopInfo, this.safeAppend_);
- }
-
- /**
- * Emulate the native mediasource function and remove parts
- * of the buffer from any of our internal buffers that exist
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove
- * @param {Double} start position to start the remove at
- * @param {Double} end position to end the remove at
- */
-
- }, {
- key: 'remove',
- value: function remove(start, end) {
- if (this.videoBuffer_) {
- this.videoBuffer_.updating = true;
- this.videoBuffer_.remove(start, end);
- this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_);
- }
- if (!this.audioDisabled_ && this.audioBuffer_) {
- this.audioBuffer_.updating = true;
- this.audioBuffer_.remove(start, end);
- }
-
- // Remove Metadata Cues (id3)
- removeCuesFromTrack(start, end, this.metadataTrack_);
-
- // Remove Any Captions
- if (this.inbandTextTracks_) {
- for (var track in this.inbandTextTracks_) {
- removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
- }
- }
- }
-
- /**
- * Process any segments that the muxer has output
- * Concatenate segments together based on type and append them into
- * their respective sourceBuffers
- *
- * @private
- */
-
- }, {
- key: 'processPendingSegments_',
- value: function processPendingSegments_() {
- var sortedSegments = {
- video: {
- segments: [],
- bytes: 0
- },
- audio: {
- segments: [],
- bytes: 0
- },
- captions: [],
- metadata: []
- };
-
- // Sort segments into separate video/audio arrays and
- // keep track of their total byte lengths
- sortedSegments = this.pendingBuffers_.reduce(function (segmentObj, segment) {
- var type = segment.type;
- var data = segment.data;
- var initSegment = segment.initSegment;
-
- segmentObj[type].segments.push(data);
- segmentObj[type].bytes += data.byteLength;
-
- segmentObj[type].initSegment = initSegment;
-
- // Gather any captions into a single array
- if (segment.captions) {
- segmentObj.captions = segmentObj.captions.concat(segment.captions);
- }
-
- if (segment.info) {
- segmentObj[type].info = segment.info;
- }
-
- // Gather any metadata into a single array
- if (segment.metadata) {
- segmentObj.metadata = segmentObj.metadata.concat(segment.metadata);
- }
-
- return segmentObj;
- }, sortedSegments);
-
- // Create the real source buffers if they don't exist by now since we
- // finally are sure what tracks are contained in the source
- if (!this.videoBuffer_ && !this.audioBuffer_) {
- // Remove any codecs that may have been specified by default but
- // are no longer applicable now
- if (sortedSegments.video.bytes === 0) {
- this.videoCodec_ = null;
- }
- if (sortedSegments.audio.bytes === 0) {
- this.audioCodec_ = null;
- }
-
- this.createRealSourceBuffers_();
- }
-
- if (sortedSegments.audio.info) {
- this.mediaSource_.trigger({ type: 'audioinfo', info: sortedSegments.audio.info });
- }
- if (sortedSegments.video.info) {
- this.mediaSource_.trigger({ type: 'videoinfo', info: sortedSegments.video.info });
- }
-
- if (this.appendAudioInitSegment_) {
- if (!this.audioDisabled_ && this.audioBuffer_) {
- sortedSegments.audio.segments.unshift(sortedSegments.audio.initSegment);
- sortedSegments.audio.bytes += sortedSegments.audio.initSegment.byteLength;
- }
- this.appendAudioInitSegment_ = false;
- }
-
- var triggerUpdateend = false;
-
- // Merge multiple video and audio segments into one and append
- if (this.videoBuffer_ && sortedSegments.video.bytes) {
- sortedSegments.video.segments.unshift(sortedSegments.video.initSegment);
- sortedSegments.video.bytes += sortedSegments.video.initSegment.byteLength;
- this.concatAndAppendSegments_(sortedSegments.video, this.videoBuffer_);
- // TODO: are video tracks the only ones with text tracks?
- addTextTrackData(this, sortedSegments.captions, sortedSegments.metadata);
- } else if (this.videoBuffer_ && (this.audioDisabled_ || !this.audioBuffer_)) {
- // The transmuxer did not return any bytes of video, meaning it was all trimmed
- // for gop alignment. Since we have a video buffer and audio is disabled, updateend
- // will never be triggered by this source buffer, which will cause contrib-hls
- // to be stuck forever waiting for updateend. If audio is not disabled, updateend
- // will be triggered by the audio buffer, which will be sent upwards since the video
- // buffer will not be in an updating state.
- triggerUpdateend = true;
- }
-
- if (!this.audioDisabled_ && this.audioBuffer_) {
- this.concatAndAppendSegments_(sortedSegments.audio, this.audioBuffer_);
- }
-
- this.pendingBuffers_.length = 0;
-
- if (triggerUpdateend) {
- this.trigger('updateend');
- }
-
- // We are no longer in the internal "updating" state
- this.bufferUpdating_ = false;
- }
-
- /**
- * Combine all segments into a single Uint8Array and then append them
- * to the destination buffer
- *
- * @param {Object} segmentObj
- * @param {SourceBuffer} destinationBuffer native source buffer to append data to
- * @private
- */
-
- }, {
- key: 'concatAndAppendSegments_',
- value: function concatAndAppendSegments_(segmentObj, destinationBuffer) {
- var offset = 0;
- var tempBuffer = void 0;
-
- if (segmentObj.bytes) {
- tempBuffer = new Uint8Array(segmentObj.bytes);
-
- // Combine the individual segments into one large typed-array
- segmentObj.segments.forEach(function (segment) {
- tempBuffer.set(segment, offset);
- offset += segment.byteLength;
- });
-
- try {
- destinationBuffer.updating = true;
- destinationBuffer.appendBuffer(tempBuffer);
- } catch (error) {
- if (this.mediaSource_.player_) {
- this.mediaSource_.player_.error({
- code: -3,
- type: 'APPEND_BUFFER_ERR',
- message: error.message,
- originalError: error
- });
- }
- }
- }
- }
-
- /**
- * Emulate the native mediasource function. abort any soureBuffer
- * actions and throw out any un-appended data.
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort
- */
-
- }, {
- key: 'abort',
- value: function abort() {
- if (this.videoBuffer_) {
- this.videoBuffer_.abort();
- }
- if (!this.audioDisabled_ && this.audioBuffer_) {
- this.audioBuffer_.abort();
- }
- if (this.transmuxer_) {
- this.transmuxer_.postMessage({ action: 'reset' });
- }
- this.pendingBuffers_.length = 0;
- this.bufferUpdating_ = false;
- }
- }]);
- return VirtualSourceBuffer;
- }(videojs.EventTarget);
-
- /**
- * @file html-media-source.js
- */
-
- /**
- * Our MediaSource implementation in HTML, mimics native
- * MediaSource where/if possible.
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
- * @class HtmlMediaSource
- * @extends videojs.EventTarget
- */
-
- var HtmlMediaSource = function (_videojs$EventTarget) {
- inherits$1(HtmlMediaSource, _videojs$EventTarget);
-
- function HtmlMediaSource() {
- classCallCheck$1(this, HtmlMediaSource);
-
- var _this = possibleConstructorReturn$1(this, (HtmlMediaSource.__proto__ || Object.getPrototypeOf(HtmlMediaSource)).call(this));
-
- var property = void 0;
-
- _this.nativeMediaSource_ = new window_1.MediaSource();
- // delegate to the native MediaSource's methods by default
- for (property in _this.nativeMediaSource_) {
- if (!(property in HtmlMediaSource.prototype) && typeof _this.nativeMediaSource_[property] === 'function') {
- _this[property] = _this.nativeMediaSource_[property].bind(_this.nativeMediaSource_);
- }
- }
-
- // emulate `duration` and `seekable` until seeking can be
- // handled uniformly for live streams
- // see https://github.com/w3c/media-source/issues/5
- _this.duration_ = NaN;
- Object.defineProperty(_this, 'duration', {
- get: function get$$1() {
- if (this.duration_ === Infinity) {
- return this.duration_;
- }
- return this.nativeMediaSource_.duration;
- },
- set: function set$$1(duration) {
- this.duration_ = duration;
- if (duration !== Infinity) {
- this.nativeMediaSource_.duration = duration;
- return;
- }
- }
- });
- Object.defineProperty(_this, 'seekable', {
- get: function get$$1() {
- if (this.duration_ === Infinity) {
- return videojs.createTimeRanges([[0, this.nativeMediaSource_.duration]]);
- }
- return this.nativeMediaSource_.seekable;
- }
- });
-
- Object.defineProperty(_this, 'readyState', {
- get: function get$$1() {
- return this.nativeMediaSource_.readyState;
- }
- });
-
- Object.defineProperty(_this, 'activeSourceBuffers', {
- get: function get$$1() {
- return this.activeSourceBuffers_;
- }
- });
-
- // the list of virtual and native SourceBuffers created by this
- // MediaSource
- _this.sourceBuffers = [];
-
- _this.activeSourceBuffers_ = [];
-
- /**
- * update the list of active source buffers based upon various
- * imformation from HLS and video.js
- *
- * @private
- */
- _this.updateActiveSourceBuffers_ = function () {
- // Retain the reference but empty the array
- _this.activeSourceBuffers_.length = 0;
-
- // If there is only one source buffer, then it will always be active and audio will
- // be disabled based on the codec of the source buffer
- if (_this.sourceBuffers.length === 1) {
- var sourceBuffer = _this.sourceBuffers[0];
-
- sourceBuffer.appendAudioInitSegment_ = true;
- sourceBuffer.audioDisabled_ = !sourceBuffer.audioCodec_;
- _this.activeSourceBuffers_.push(sourceBuffer);
- return;
- }
-
- // There are 2 source buffers, a combined (possibly video only) source buffer and
- // and an audio only source buffer.
- // By default, the audio in the combined virtual source buffer is enabled
- // and the audio-only source buffer (if it exists) is disabled.
- var disableCombined = false;
- var disableAudioOnly = true;
-
- // TODO: maybe we can store the sourcebuffers on the track objects?
- // safari may do something like this
- for (var i = 0; i < _this.player_.audioTracks().length; i++) {
- var track = _this.player_.audioTracks()[i];
-
- if (track.enabled && track.kind !== 'main') {
- // The enabled track is an alternate audio track so disable the audio in
- // the combined source buffer and enable the audio-only source buffer.
- disableCombined = true;
- disableAudioOnly = false;
- break;
- }
- }
-
- _this.sourceBuffers.forEach(function (sourceBuffer, index) {
- /* eslinst-disable */
- // TODO once codecs are required, we can switch to using the codecs to determine
- // what stream is the video stream, rather than relying on videoTracks
- /* eslinst-enable */
-
- sourceBuffer.appendAudioInitSegment_ = true;
-
- if (sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) {
- // combined
- sourceBuffer.audioDisabled_ = disableCombined;
- } else if (sourceBuffer.videoCodec_ && !sourceBuffer.audioCodec_) {
- // If the "combined" source buffer is video only, then we do not want
- // disable the audio-only source buffer (this is mostly for demuxed
- // audio and video hls)
- sourceBuffer.audioDisabled_ = true;
- disableAudioOnly = false;
- } else if (!sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) {
- // audio only
- // In the case of audio only with alternate audio and disableAudioOnly is true
- // this means we want to disable the audio on the alternate audio sourcebuffer
- // but not the main "combined" source buffer. The "combined" source buffer is
- // always at index 0, so this ensures audio won't be disabled in both source
- // buffers.
- sourceBuffer.audioDisabled_ = index ? disableAudioOnly : !disableAudioOnly;
- if (sourceBuffer.audioDisabled_) {
- return;
- }
- }
-
- _this.activeSourceBuffers_.push(sourceBuffer);
- });
- };
-
- _this.onPlayerMediachange_ = function () {
- _this.sourceBuffers.forEach(function (sourceBuffer) {
- sourceBuffer.appendAudioInitSegment_ = true;
- });
- };
-
- _this.onHlsReset_ = function () {
- _this.sourceBuffers.forEach(function (sourceBuffer) {
- if (sourceBuffer.transmuxer_) {
- sourceBuffer.transmuxer_.postMessage({ action: 'resetCaptions' });
- }
- });
- };
-
- _this.onHlsSegmentTimeMapping_ = function (event) {
- _this.sourceBuffers.forEach(function (buffer) {
- return buffer.timeMapping_ = event.mapping;
- });
- };
-
- // Re-emit MediaSource events on the polyfill
- ['sourceopen', 'sourceclose', 'sourceended'].forEach(function (eventName) {
- this.nativeMediaSource_.addEventListener(eventName, this.trigger.bind(this));
- }, _this);
-
- // capture the associated player when the MediaSource is
- // successfully attached
- _this.on('sourceopen', function (event) {
- // Get the player this MediaSource is attached to
- var video = document_1.querySelector('[src="' + _this.url_ + '"]');
-
- if (!video) {
- return;
- }
-
- _this.player_ = videojs(video.parentNode);
-
- // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader
- // resets its state and flushes the buffer
- _this.player_.tech_.on('hls-reset', _this.onHlsReset_);
- // hls-segment-time-mapping is fired by videojs.Hls on to the tech after the main
- // SegmentLoader inspects an MTS segment and has an accurate stream to display
- // time mapping
- _this.player_.tech_.on('hls-segment-time-mapping', _this.onHlsSegmentTimeMapping_);
-
- if (_this.player_.audioTracks && _this.player_.audioTracks()) {
- _this.player_.audioTracks().on('change', _this.updateActiveSourceBuffers_);
- _this.player_.audioTracks().on('addtrack', _this.updateActiveSourceBuffers_);
- _this.player_.audioTracks().on('removetrack', _this.updateActiveSourceBuffers_);
- }
-
- _this.player_.on('mediachange', _this.onPlayerMediachange_);
- });
-
- _this.on('sourceended', function (event) {
- var duration = durationOfVideo(_this.duration);
-
- for (var i = 0; i < _this.sourceBuffers.length; i++) {
- var sourcebuffer = _this.sourceBuffers[i];
- var cues = sourcebuffer.metadataTrack_ && sourcebuffer.metadataTrack_.cues;
-
- if (cues && cues.length) {
- cues[cues.length - 1].endTime = duration;
- }
- }
- });
-
- // explicitly terminate any WebWorkers that were created
- // by SourceHandlers
- _this.on('sourceclose', function (event) {
- this.sourceBuffers.forEach(function (sourceBuffer) {
- if (sourceBuffer.transmuxer_) {
- sourceBuffer.transmuxer_.terminate();
- }
- });
-
- this.sourceBuffers.length = 0;
- if (!this.player_) {
- return;
- }
-
- if (this.player_.audioTracks && this.player_.audioTracks()) {
- this.player_.audioTracks().off('change', this.updateActiveSourceBuffers_);
- this.player_.audioTracks().off('addtrack', this.updateActiveSourceBuffers_);
- this.player_.audioTracks().off('removetrack', this.updateActiveSourceBuffers_);
- }
-
- // We can only change this if the player hasn't been disposed of yet
- // because `off` eventually tries to use the el_ property. If it has
- // been disposed of, then don't worry about it because there are no
- // event handlers left to unbind anyway
- if (this.player_.el_) {
- this.player_.off('mediachange', this.onPlayerMediachange_);
- this.player_.tech_.off('hls-reset', this.onHlsReset_);
- this.player_.tech_.off('hls-segment-time-mapping', this.onHlsSegmentTimeMapping_);
- }
- });
- return _this;
- }
-
- /**
- * Add a range that that can now be seeked to.
- *
- * @param {Double} start where to start the addition
- * @param {Double} end where to end the addition
- * @private
- */
-
-
- createClass(HtmlMediaSource, [{
- key: 'addSeekableRange_',
- value: function addSeekableRange_(start, end) {
- var error = void 0;
-
- if (this.duration !== Infinity) {
- error = new Error('MediaSource.addSeekableRange() can only be invoked ' + 'when the duration is Infinity');
- error.name = 'InvalidStateError';
- error.code = 11;
- throw error;
- }
-
- if (end > this.nativeMediaSource_.duration || isNaN(this.nativeMediaSource_.duration)) {
- this.nativeMediaSource_.duration = end;
- }
- }
-
- /**
- * Add a source buffer to the media source.
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer
- * @param {String} type the content-type of the content
- * @return {Object} the created source buffer
- */
-
- }, {
- key: 'addSourceBuffer',
- value: function addSourceBuffer(type) {
- var buffer = void 0;
- var parsedType = parseContentType(type);
-
- // Create a VirtualSourceBuffer to transmux MPEG-2 transport
- // stream segments into fragmented MP4s
- if (/^(video|audio)\/mp2t$/i.test(parsedType.type)) {
- var codecs = [];
-
- if (parsedType.parameters && parsedType.parameters.codecs) {
- codecs = parsedType.parameters.codecs.split(',');
- codecs = translateLegacyCodecs(codecs);
- codecs = codecs.filter(function (codec) {
- return isAudioCodec(codec) || isVideoCodec(codec);
- });
- }
-
- if (codecs.length === 0) {
- codecs = ['avc1.4d400d', 'mp4a.40.2'];
- }
-
- buffer = new VirtualSourceBuffer(this, codecs);
-
- if (this.sourceBuffers.length !== 0) {
- // If another VirtualSourceBuffer already exists, then we are creating a
- // SourceBuffer for an alternate audio track and therefore we know that
- // the source has both an audio and video track.
- // That means we should trigger the manual creation of the real
- // SourceBuffers instead of waiting for the transmuxer to return data
- this.sourceBuffers[0].createRealSourceBuffers_();
- buffer.createRealSourceBuffers_();
-
- // Automatically disable the audio on the first source buffer if
- // a second source buffer is ever created
- this.sourceBuffers[0].audioDisabled_ = true;
- }
- } else {
- // delegate to the native implementation
- buffer = this.nativeMediaSource_.addSourceBuffer(type);
- }
-
- this.sourceBuffers.push(buffer);
- return buffer;
- }
- }]);
- return HtmlMediaSource;
- }(videojs.EventTarget);
-
- /**
- * @file videojs-contrib-media-sources.js
- */
- var urlCount = 0;
-
- // ------------
- // Media Source
- // ------------
-
- // store references to the media sources so they can be connected
- // to a video element (a swf object)
- // TODO: can we store this somewhere local to this module?
- videojs.mediaSources = {};
-
- /**
- * Provide a method for a swf object to notify JS that a
- * media source is now open.
- *
- * @param {String} msObjectURL string referencing the MSE Object URL
- * @param {String} swfId the swf id
- */
- var open = function open(msObjectURL, swfId) {
- var mediaSource = videojs.mediaSources[msObjectURL];
-
- if (mediaSource) {
- mediaSource.trigger({ type: 'sourceopen', swfId: swfId });
- } else {
- throw new Error('Media Source not found (Video.js)');
- }
- };
-
- /**
- * Check to see if the native MediaSource object exists and supports
- * an MP4 container with both H.264 video and AAC-LC audio.
- *
- * @return {Boolean} if native media sources are supported
- */
- var supportsNativeMediaSources = function supportsNativeMediaSources() {
- return !!window_1.MediaSource && !!window_1.MediaSource.isTypeSupported && window_1.MediaSource.isTypeSupported('video/mp4;codecs="avc1.4d400d,mp4a.40.2"');
- };
-
- /**
- * An emulation of the MediaSource API so that we can support
- * native and non-native functionality. returns an instance of
- * HtmlMediaSource.
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/MediaSource
- */
- var MediaSource = function MediaSource() {
- this.MediaSource = {
- open: open,
- supportsNativeMediaSources: supportsNativeMediaSources
- };
-
- if (supportsNativeMediaSources()) {
- return new HtmlMediaSource();
- }
-
- throw new Error('Cannot use create a virtual MediaSource for this video');
- };
-
- MediaSource.open = open;
- MediaSource.supportsNativeMediaSources = supportsNativeMediaSources;
-
- /**
- * A wrapper around the native URL for our MSE object
- * implementation, this object is exposed under videojs.URL
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
- */
- var URL$1 = {
- /**
- * A wrapper around the native createObjectURL for our objects.
- * This function maps a native or emulated mediaSource to a blob
- * url so that it can be loaded into video.js
- *
- * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
- * @param {MediaSource} object the object to create a blob url to
- */
- createObjectURL: function createObjectURL(object) {
- var objectUrlPrefix = 'blob:vjs-media-source/';
- var url = void 0;
-
- // use the native MediaSource to generate an object URL
- if (object instanceof HtmlMediaSource) {
- url = window_1.URL.createObjectURL(object.nativeMediaSource_);
- object.url_ = url;
- return url;
- }
- // if the object isn't an emulated MediaSource, delegate to the
- // native implementation
- if (!(object instanceof HtmlMediaSource)) {
- url = window_1.URL.createObjectURL(object);
- object.url_ = url;
- return url;
- }
-
- // build a URL that can be used to map back to the emulated
- // MediaSource
- url = objectUrlPrefix + urlCount;
-
- urlCount++;
-
- // setup the mapping back to object
- videojs.mediaSources[url] = object;
-
- return url;
- }
- };
-
- videojs.MediaSource = MediaSource;
- videojs.URL = URL$1;
-
- /**
- * mpd-parser
- * @version 0.6.1
- * @copyright 2018 Brightcove, Inc
- * @license Apache-2.0
- */
-
- var formatAudioPlaylist = function formatAudioPlaylist(_ref) {
- var _attributes;
-
- var attributes = _ref.attributes,
- segments = _ref.segments;
-
- var playlist = {
- attributes: (_attributes = {
- NAME: attributes.id,
- BANDWIDTH: attributes.bandwidth,
- CODECS: attributes.codecs
- }, _attributes['PROGRAM-ID'] = 1, _attributes),
- uri: '',
- endList: (attributes.type || 'static') === 'static',
- timeline: attributes.periodIndex,
- resolvedUri: '',
- targetDuration: attributes.duration,
- segments: segments,
- mediaSequence: segments.length ? segments[0].number : 1
- };
-
- if (attributes.contentProtection) {
- playlist.contentProtection = attributes.contentProtection;
- }
-
- return playlist;
- };
-
- var formatVttPlaylist = function formatVttPlaylist(_ref2) {
- var _attributes2;
-
- var attributes = _ref2.attributes,
- segments = _ref2.segments;
-
- if (typeof segments === 'undefined') {
- // vtt tracks may use single file in BaseURL
- segments = [{
- uri: attributes.baseUrl,
- timeline: attributes.periodIndex,
- resolvedUri: attributes.baseUrl || '',
- duration: attributes.sourceDuration,
- number: 0
- }];
- // targetDuration should be the same duration as the only segment
- attributes.duration = attributes.sourceDuration;
- }
- return {
- attributes: (_attributes2 = {
- NAME: attributes.id,
- BANDWIDTH: attributes.bandwidth
- }, _attributes2['PROGRAM-ID'] = 1, _attributes2),
- uri: '',
- endList: (attributes.type || 'static') === 'static',
- timeline: attributes.periodIndex,
- resolvedUri: attributes.baseUrl || '',
- targetDuration: attributes.duration,
- segments: segments,
- mediaSequence: segments.length ? segments[0].number : 1
- };
- };
-
- var organizeAudioPlaylists = function organizeAudioPlaylists(playlists) {
- return playlists.reduce(function (a, playlist) {
- var role = playlist.attributes.role && playlist.attributes.role.value || 'main';
- var language = playlist.attributes.lang || '';
-
- var label = 'main';
-
- if (language) {
- label = playlist.attributes.lang + ' (' + role + ')';
- }
-
- // skip if we already have the highest quality audio for a language
- if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) {
- return a;
- }
-
- a[label] = {
- language: language,
- autoselect: true,
- 'default': role === 'main',
- playlists: [formatAudioPlaylist(playlist)],
- uri: ''
- };
-
- return a;
- }, {});
- };
-
- var organizeVttPlaylists = function organizeVttPlaylists(playlists) {
- return playlists.reduce(function (a, playlist) {
- var label = playlist.attributes.lang || 'text';
-
- // skip if we already have subtitles
- if (a[label]) {
- return a;
- }
-
- a[label] = {
- language: label,
- 'default': false,
- autoselect: false,
- playlists: [formatVttPlaylist(playlist)],
- uri: ''
- };
-
- return a;
- }, {});
- };
-
- var formatVideoPlaylist = function formatVideoPlaylist(_ref3) {
- var _attributes3;
-
- var attributes = _ref3.attributes,
- segments = _ref3.segments;
-
- var playlist = {
- attributes: (_attributes3 = {
- NAME: attributes.id,
- AUDIO: 'audio',
- SUBTITLES: 'subs',
- RESOLUTION: {
- width: attributes.width,
- height: attributes.height
- },
- CODECS: attributes.codecs,
- BANDWIDTH: attributes.bandwidth
- }, _attributes3['PROGRAM-ID'] = 1, _attributes3),
- uri: '',
- endList: (attributes.type || 'static') === 'static',
- timeline: attributes.periodIndex,
- resolvedUri: '',
- targetDuration: attributes.duration,
- segments: segments,
- mediaSequence: segments.length ? segments[0].number : 1
- };
-
- if (attributes.contentProtection) {
- playlist.contentProtection = attributes.contentProtection;
- }
-
- return playlist;
- };
-
- var toM3u8 = function toM3u8(dashPlaylists) {
- var _mediaGroups;
-
- if (!dashPlaylists.length) {
- return {};
- }
-
- // grab all master attributes
- var _dashPlaylists$0$attr = dashPlaylists[0].attributes,
- duration = _dashPlaylists$0$attr.sourceDuration,
- _dashPlaylists$0$attr2 = _dashPlaylists$0$attr.minimumUpdatePeriod,
- minimumUpdatePeriod = _dashPlaylists$0$attr2 === undefined ? 0 : _dashPlaylists$0$attr2;
-
- var videoOnly = function videoOnly(_ref4) {
- var attributes = _ref4.attributes;
- return attributes.mimeType === 'video/mp4' || attributes.contentType === 'video';
- };
- var audioOnly = function audioOnly(_ref5) {
- var attributes = _ref5.attributes;
- return attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio';
- };
- var vttOnly = function vttOnly(_ref6) {
- var attributes = _ref6.attributes;
- return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
- };
-
- var videoPlaylists = dashPlaylists.filter(videoOnly).map(formatVideoPlaylist);
- var audioPlaylists = dashPlaylists.filter(audioOnly);
- var vttPlaylists = dashPlaylists.filter(vttOnly);
-
- var master = {
- allowCache: true,
- discontinuityStarts: [],
- segments: [],
- endList: true,
- mediaGroups: (_mediaGroups = {
- AUDIO: {},
- VIDEO: {}
- }, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups),
- uri: '',
- duration: duration,
- playlists: videoPlaylists,
- minimumUpdatePeriod: minimumUpdatePeriod * 1000
- };
-
- if (audioPlaylists.length) {
- master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists);
- }
-
- if (vttPlaylists.length) {
- master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists);
- }
-
- return master;
- };
-
- var _typeof$1 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
- return typeof obj;
- } : function (obj) {
- return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
- };
-
- var isObject = function isObject(obj) {
- return !!obj && (typeof obj === 'undefined' ? 'undefined' : _typeof$1(obj)) === 'object';
- };
-
- var merge = function merge() {
- for (var _len = arguments.length, objects = Array(_len), _key = 0; _key < _len; _key++) {
- objects[_key] = arguments[_key];
- }
-
- return objects.reduce(function (result, source) {
-
- Object.keys(source).forEach(function (key) {
-
- if (Array.isArray(result[key]) && Array.isArray(source[key])) {
- result[key] = result[key].concat(source[key]);
- } else if (isObject(result[key]) && isObject(source[key])) {
- result[key] = merge(result[key], source[key]);
- } else {
- result[key] = source[key];
- }
- });
- return result;
- }, {});
- };
-
- var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) {
- // return early if we don't need to resolve
- if (/^[a-z]+:/i.test(relativeUrl)) {
- return relativeUrl;
- }
-
- // if the base URL is relative then combine with the current location
- if (!/\/\//i.test(baseUrl)) {
- baseUrl = urlToolkit.buildAbsoluteURL(window_1.location.href, baseUrl);
- }
-
- return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
- };
-
- /**
- * @typedef {Object} SingleUri
- * @property {string} uri - relative location of segment
- * @property {string} resolvedUri - resolved location of segment
- * @property {Object} byterange - Object containing information on how to make byte range
- * requests following byte-range-spec per RFC2616.
- * @property {String} byterange.length - length of range request
- * @property {String} byterange.offset - byte offset of range request
- *
- * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
- */
-
- /**
- * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
- * that conforms to how m3u8-parser is structured
- *
- * @see https://github.com/videojs/m3u8-parser
- *
- * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
- * @param {string} source - source url for segment
- * @param {string} range - optional range used for range calls, follows
- * @return {SingleUri} full segment information transformed into a format similar
- * to m3u8-parser
- */
- var urlTypeToSegment = function urlTypeToSegment(_ref) {
- var _ref$baseUrl = _ref.baseUrl,
- baseUrl = _ref$baseUrl === undefined ? '' : _ref$baseUrl,
- _ref$source = _ref.source,
- source = _ref$source === undefined ? '' : _ref$source,
- _ref$range = _ref.range,
- range = _ref$range === undefined ? '' : _ref$range;
-
- var init = {
- uri: source,
- resolvedUri: resolveUrl$1(baseUrl || '', source)
- };
-
- if (range) {
- var ranges = range.split('-');
- var startRange = parseInt(ranges[0], 10);
- var endRange = parseInt(ranges[1], 10);
-
- init.byterange = {
- length: endRange - startRange,
- offset: startRange
- };
- }
-
- return init;
- };
-
- /**
- * Calculates the R (repetition) value for a live stream (for the final segment
- * in a manifest where the r value is negative 1)
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {number} time
- * current time (typically the total time up until the final segment)
- * @param {number} duration
- * duration property for the given <S />
- *
- * @return {number}
- * R value to reach the end of the given period
- */
- var getLiveRValue = function getLiveRValue(attributes, time, duration) {
- var NOW = attributes.NOW,
- clientOffset = attributes.clientOffset,
- availabilityStartTime = attributes.availabilityStartTime,
- _attributes$timescale = attributes.timescale,
- timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale,
- _attributes$start = attributes.start,
- start = _attributes$start === undefined ? 0 : _attributes$start,
- _attributes$minimumUp = attributes.minimumUpdatePeriod,
- minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp;
-
- var now = (NOW + clientOffset) / 1000;
- var periodStartWC = availabilityStartTime + start;
- var periodEndWC = now + minimumUpdatePeriod;
- var periodDuration = periodEndWC - periodStartWC;
-
- return Math.ceil((periodDuration * timescale - time) / duration);
- };
-
- /**
- * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
- * timing and duration
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {Object[]} segmentTimeline
- * List of objects representing the attributes of each S element contained within
- *
- * @return {{number: number, duration: number, time: number, timeline: number}[]}
- * List of Objects with segment timing and duration info
- */
- var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) {
- var _attributes$type = attributes.type,
- type = _attributes$type === undefined ? 'static' : _attributes$type,
- _attributes$minimumUp2 = attributes.minimumUpdatePeriod,
- minimumUpdatePeriod = _attributes$minimumUp2 === undefined ? 0 : _attributes$minimumUp2,
- _attributes$media = attributes.media,
- media = _attributes$media === undefined ? '' : _attributes$media,
- sourceDuration = attributes.sourceDuration,
- _attributes$timescale2 = attributes.timescale,
- timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2,
- _attributes$startNumb = attributes.startNumber,
- startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb,
- timeline = attributes.periodIndex;
-
- var segments = [];
- var time = -1;
-
- for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
- var S = segmentTimeline[sIndex];
- var duration = S.d;
- var repeat = S.r || 0;
- var segmentTime = S.t || 0;
-
- if (time < 0) {
- // first segment
- time = segmentTime;
- }
-
- if (segmentTime && segmentTime > time) {
- // discontinuity
-
- // TODO: How to handle this type of discontinuity
- // timeline++ here would treat it like HLS discontuity and content would
- // get appended without gap
- // E.G.
- // <S t="0" d="1" />
- // <S d="1" />
- // <S d="1" />
- // <S t="5" d="1" />
- // would have $Time$ values of [0, 1, 2, 5]
- // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
- // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
- // does the value of sourceDuration consider this when calculating arbitrary
- // negative @r repeat value?
- // E.G. Same elements as above with this added at the end
- // <S d="1" r="-1" />
- // with a sourceDuration of 10
- // Would the 2 gaps be included in the time duration calculations resulting in
- // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
- // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
-
- time = segmentTime;
- }
-
- var count = void 0;
-
- if (repeat < 0) {
- var nextS = sIndex + 1;
-
- if (nextS === segmentTimeline.length) {
- // last segment
- if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
- count = getLiveRValue(attributes, time, duration);
- } else {
- // TODO: This may be incorrect depending on conclusion of TODO above
- count = (sourceDuration * timescale - time) / duration;
- }
- } else {
- count = (segmentTimeline[nextS].t - time) / duration;
- }
- } else {
- count = repeat + 1;
- }
-
- var end = startNumber + segments.length + count;
- var number = startNumber + segments.length;
-
- while (number < end) {
- segments.push({ number: number, duration: duration / timescale, time: time, timeline: timeline });
- time += duration;
- number++;
- }
- }
-
- return segments;
- };
-
- var range = function range(start, end) {
- var result = [];
-
- for (var i = start; i < end; i++) {
- result.push(i);
- }
-
- return result;
- };
-
- var flatten = function flatten(lists) {
- return lists.reduce(function (x, y) {
- return x.concat(y);
- }, []);
- };
-
- var from = function from(list) {
- if (!list.length) {
- return [];
- }
-
- var result = [];
-
- for (var i = 0; i < list.length; i++) {
- result.push(list[i]);
- }
-
- return result;
- };
-
- /**
- * Functions for calculating the range of available segments in static and dynamic
- * manifests.
- */
- var segmentRange = {
- /**
- * Returns the entire range of available segments for a static MPD
- *
- * @param {Object} attributes
- * Inheritied MPD attributes
- * @return {{ start: number, end: number }}
- * The start and end numbers for available segments
- */
- 'static': function _static(attributes) {
- var duration = attributes.duration,
- _attributes$timescale = attributes.timescale,
- timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale,
- sourceDuration = attributes.sourceDuration;
-
- return {
- start: 0,
- end: Math.ceil(sourceDuration / (duration / timescale))
- };
- },
-
- /**
- * Returns the current live window range of available segments for a dynamic MPD
- *
- * @param {Object} attributes
- * Inheritied MPD attributes
- * @return {{ start: number, end: number }}
- * The start and end numbers for available segments
- */
- dynamic: function dynamic(attributes) {
- var NOW = attributes.NOW,
- clientOffset = attributes.clientOffset,
- availabilityStartTime = attributes.availabilityStartTime,
- _attributes$timescale2 = attributes.timescale,
- timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2,
- duration = attributes.duration,
- _attributes$start = attributes.start,
- start = _attributes$start === undefined ? 0 : _attributes$start,
- _attributes$minimumUp = attributes.minimumUpdatePeriod,
- minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp,
- _attributes$timeShift = attributes.timeShiftBufferDepth,
- timeShiftBufferDepth = _attributes$timeShift === undefined ? Infinity : _attributes$timeShift;
-
- var now = (NOW + clientOffset) / 1000;
- var periodStartWC = availabilityStartTime + start;
- var periodEndWC = now + minimumUpdatePeriod;
- var periodDuration = periodEndWC - periodStartWC;
- var segmentCount = Math.ceil(periodDuration * timescale / duration);
- var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
- var availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
-
- return {
- start: Math.max(0, availableStart),
- end: Math.min(segmentCount, availableEnd)
- };
- }
- };
-
- /**
- * Maps a range of numbers to objects with information needed to build the corresponding
- * segment list
- *
- * @name toSegmentsCallback
- * @function
- * @param {number} number
- * Number of the segment
- * @param {number} index
- * Index of the number in the range list
- * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
- * Object with segment timing and duration info
- */
-
- /**
- * Returns a callback for Array.prototype.map for mapping a range of numbers to
- * information needed to build the segment list.
- *
- * @param {Object} attributes
- * Inherited MPD attributes
- * @return {toSegmentsCallback}
- * Callback map function
- */
- var toSegments = function toSegments(attributes) {
- return function (number, index) {
- var duration = attributes.duration,
- _attributes$timescale3 = attributes.timescale,
- timescale = _attributes$timescale3 === undefined ? 1 : _attributes$timescale3,
- periodIndex = attributes.periodIndex,
- _attributes$startNumb = attributes.startNumber,
- startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb;
-
- return {
- number: startNumber + number,
- duration: duration / timescale,
- timeline: periodIndex,
- time: index * duration
- };
- };
- };
-
- /**
- * Returns a list of objects containing segment timing and duration info used for
- * building the list of segments. This uses the @duration attribute specified
- * in the MPD manifest to derive the range of segments.
- *
- * @param {Object} attributes
- * Inherited MPD attributes
- * @return {{number: number, duration: number, time: number, timeline: number}[]}
- * List of Objects with segment timing and duration info
- */
- var parseByDuration = function parseByDuration(attributes) {
- var _attributes$type = attributes.type,
- type = _attributes$type === undefined ? 'static' : _attributes$type,
- duration = attributes.duration,
- _attributes$timescale4 = attributes.timescale,
- timescale = _attributes$timescale4 === undefined ? 1 : _attributes$timescale4,
- sourceDuration = attributes.sourceDuration;
-
- var _segmentRange$type = segmentRange[type](attributes),
- start = _segmentRange$type.start,
- end = _segmentRange$type.end;
-
- var segments = range(start, end).map(toSegments(attributes));
-
- if (type === 'static') {
- var index = segments.length - 1;
-
- // final segment may be less than full segment duration
- segments[index].duration = sourceDuration - duration / timescale * index;
- }
-
- return segments;
- };
-
- var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
-
- /**
- * Replaces template identifiers with corresponding values. To be used as the callback
- * for String.prototype.replace
- *
- * @name replaceCallback
- * @function
- * @param {string} match
- * Entire match of identifier
- * @param {string} identifier
- * Name of matched identifier
- * @param {string} format
- * Format tag string. Its presence indicates that padding is expected
- * @param {string} width
- * Desired length of the replaced value. Values less than this width shall be left
- * zero padded
- * @return {string}
- * Replacement for the matched identifier
- */
-
- /**
- * Returns a function to be used as a callback for String.prototype.replace to replace
- * template identifiers
- *
- * @param {Obect} values
- * Object containing values that shall be used to replace known identifiers
- * @param {number} values.RepresentationID
- * Value of the Representation@id attribute
- * @param {number} values.Number
- * Number of the corresponding segment
- * @param {number} values.Bandwidth
- * Value of the Representation@bandwidth attribute.
- * @param {number} values.Time
- * Timestamp value of the corresponding segment
- * @return {replaceCallback}
- * Callback to be used with String.prototype.replace to replace identifiers
- */
- var identifierReplacement = function identifierReplacement(values) {
- return function (match, identifier, format, width) {
- if (match === '$$') {
- // escape sequence
- return '$';
- }
-
- if (typeof values[identifier] === 'undefined') {
- return match;
- }
-
- var value = '' + values[identifier];
-
- if (identifier === 'RepresentationID') {
- // Format tag shall not be present with RepresentationID
- return value;
- }
-
- if (!format) {
- width = 1;
- } else {
- width = parseInt(width, 10);
- }
-
- if (value.length >= width) {
- return value;
- }
-
- return '' + new Array(width - value.length + 1).join('0') + value;
- };
- };
-
- /**
- * Constructs a segment url from a template string
- *
- * @param {string} url
- * Template string to construct url from
- * @param {Obect} values
- * Object containing values that shall be used to replace known identifiers
- * @param {number} values.RepresentationID
- * Value of the Representation@id attribute
- * @param {number} values.Number
- * Number of the corresponding segment
- * @param {number} values.Bandwidth
- * Value of the Representation@bandwidth attribute.
- * @param {number} values.Time
- * Timestamp value of the corresponding segment
- * @return {string}
- * Segment url with identifiers replaced
- */
- var constructTemplateUrl = function constructTemplateUrl(url, values) {
- return url.replace(identifierPattern, identifierReplacement(values));
- };
-
- /**
- * Generates a list of objects containing timing and duration information about each
- * segment needed to generate segment uris and the complete segment object
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {Object[]|undefined} segmentTimeline
- * List of objects representing the attributes of each S element contained within
- * the SegmentTimeline element
- * @return {{number: number, duration: number, time: number, timeline: number}[]}
- * List of Objects with segment timing and duration info
- */
- var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) {
- if (!attributes.duration && !segmentTimeline) {
- // if neither @duration or SegmentTimeline are present, then there shall be exactly
- // one media segment
- return [{
- number: attributes.startNumber || 1,
- duration: attributes.sourceDuration,
- time: 0,
- timeline: attributes.periodIndex
- }];
- }
-
- if (attributes.duration) {
- return parseByDuration(attributes);
- }
-
- return parseByTimeline(attributes, segmentTimeline);
- };
-
- /**
- * Generates a list of segments using information provided by the SegmentTemplate element
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {Object[]|undefined} segmentTimeline
- * List of objects representing the attributes of each S element contained within
- * the SegmentTimeline element
- * @return {Object[]}
- * List of segment objects
- */
- var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) {
- var templateValues = {
- RepresentationID: attributes.id,
- Bandwidth: attributes.bandwidth || 0
- };
-
- var _attributes$initializ = attributes.initialization,
- initialization = _attributes$initializ === undefined ? { sourceURL: '', range: '' } : _attributes$initializ;
-
- var mapSegment = urlTypeToSegment({
- baseUrl: attributes.baseUrl,
- source: constructTemplateUrl(initialization.sourceURL, templateValues),
- range: initialization.range
- });
-
- var segments = parseTemplateInfo(attributes, segmentTimeline);
-
- return segments.map(function (segment) {
- templateValues.Number = segment.number;
- templateValues.Time = segment.time;
-
- var uri = constructTemplateUrl(attributes.media || '', templateValues);
-
- return {
- uri: uri,
- timeline: segment.timeline,
- duration: segment.duration,
- resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri),
- map: mapSegment,
- number: segment.number
- };
- });
- };
-
- var errors = {
- INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
- DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
- DASH_INVALID_XML: 'DASH_INVALID_XML',
- NO_BASE_URL: 'NO_BASE_URL',
- MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
- SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
- UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
- };
-
- /**
- * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
- * to an object that matches the output of a segment in videojs/mpd-parser
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {Object} segmentUrl
- * <SegmentURL> node to translate into a segment object
- * @return {Object} translated segment object
- */
- var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) {
- var baseUrl = attributes.baseUrl,
- _attributes$initializ = attributes.initialization,
- initialization = _attributes$initializ === undefined ? {} : _attributes$initializ;
-
- var initSegment = urlTypeToSegment({
- baseUrl: baseUrl,
- source: initialization.sourceURL,
- range: initialization.range
- });
-
- var segment = urlTypeToSegment({
- baseUrl: baseUrl,
- source: segmentUrl.media,
- range: segmentUrl.mediaRange
- });
-
- segment.map = initSegment;
-
- return segment;
- };
-
- /**
- * Generates a list of segments using information provided by the SegmentList element
- * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
- * node should be translated into segment.
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @param {Object[]|undefined} segmentTimeline
- * List of objects representing the attributes of each S element contained within
- * the SegmentTimeline element
- * @return {Object.<Array>} list of segments
- */
- var segmentsFromList = function segmentsFromList(attributes, segmentTimeline) {
- var duration = attributes.duration,
- _attributes$segmentUr = attributes.segmentUrls,
- segmentUrls = _attributes$segmentUr === undefined ? [] : _attributes$segmentUr;
-
- // Per spec (5.3.9.2.1) no way to determine segment duration OR
- // if both SegmentTimeline and @duration are defined, it is outside of spec.
-
- if (!duration && !segmentTimeline || duration && segmentTimeline) {
- throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
- }
-
- var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) {
- return SegmentURLToSegmentObject(attributes, segmentUrlObject);
- });
- var segmentTimeInfo = void 0;
-
- if (duration) {
- segmentTimeInfo = parseByDuration(attributes);
- }
-
- if (segmentTimeline) {
- segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
- }
-
- var segments = segmentTimeInfo.map(function (segmentTime, index) {
- if (segmentUrlMap[index]) {
- var segment = segmentUrlMap[index];
-
- segment.timeline = segmentTime.timeline;
- segment.duration = segmentTime.duration;
- segment.number = segmentTime.number;
- return segment;
- }
- // Since we're mapping we should get rid of any blank segments (in case
- // the given SegmentTimeline is handling for more elements than we have
- // SegmentURLs for).
- }).filter(function (segment) {
- return segment;
- });
-
- return segments;
- };
-
- /**
- * Translates SegmentBase into a set of segments.
- * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
- * node should be translated into segment.
- *
- * @param {Object} attributes
- * Object containing all inherited attributes from parent elements with attribute
- * names as keys
- * @return {Object.<Array>} list of segments
- */
- var segmentsFromBase = function segmentsFromBase(attributes) {
- var baseUrl = attributes.baseUrl,
- _attributes$initializ = attributes.initialization,
- initialization = _attributes$initializ === undefined ? {} : _attributes$initializ,
- sourceDuration = attributes.sourceDuration,
- _attributes$timescale = attributes.timescale,
- timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale,
- _attributes$indexRang = attributes.indexRange,
- indexRange = _attributes$indexRang === undefined ? '' : _attributes$indexRang,
- duration = attributes.duration;
-
- // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
-
- if (!baseUrl) {
- throw new Error(errors.NO_BASE_URL);
- }
-
- var initSegment = urlTypeToSegment({
- baseUrl: baseUrl,
- source: initialization.sourceURL,
- range: initialization.range
- });
- var segment = urlTypeToSegment({ baseUrl: baseUrl, source: baseUrl, range: indexRange });
-
- segment.map = initSegment;
-
- // If there is a duration, use it, otherwise use the given duration of the source
- // (since SegmentBase is only for one total segment)
- if (duration) {
- var segmentTimeInfo = parseByDuration(attributes);
-
- if (segmentTimeInfo.length) {
- segment.duration = segmentTimeInfo[0].duration;
- segment.timeline = segmentTimeInfo[0].timeline;
- }
- } else if (sourceDuration) {
- segment.duration = sourceDuration / timescale;
- segment.timeline = 0;
- }
-
- // This is used for mediaSequence
- segment.number = 0;
-
- return [segment];
- };
-
- var generateSegments = function generateSegments(_ref) {
- var attributes = _ref.attributes,
- segmentInfo = _ref.segmentInfo;
-
- var segmentAttributes = void 0;
- var segmentsFn = void 0;
-
- if (segmentInfo.template) {
- segmentsFn = segmentsFromTemplate;
- segmentAttributes = merge(attributes, segmentInfo.template);
- } else if (segmentInfo.base) {
- segmentsFn = segmentsFromBase;
- segmentAttributes = merge(attributes, segmentInfo.base);
- } else if (segmentInfo.list) {
- segmentsFn = segmentsFromList;
- segmentAttributes = merge(attributes, segmentInfo.list);
- }
-
- if (!segmentsFn) {
- return { attributes: attributes };
- }
-
- var segments = segmentsFn(segmentAttributes, segmentInfo.timeline);
-
- // The @duration attribute will be used to determin the playlist's targetDuration which
- // must be in seconds. Since we've generated the segment list, we no longer need
- // @duration to be in @timescale units, so we can convert it here.
- if (segmentAttributes.duration) {
- var _segmentAttributes = segmentAttributes,
- duration = _segmentAttributes.duration,
- _segmentAttributes$ti = _segmentAttributes.timescale,
- timescale = _segmentAttributes$ti === undefined ? 1 : _segmentAttributes$ti;
-
- segmentAttributes.duration = duration / timescale;
- } else if (segments.length) {
- // if there is no @duration attribute, use the largest segment duration as
- // as target duration
- segmentAttributes.duration = segments.reduce(function (max, segment) {
- return Math.max(max, Math.ceil(segment.duration));
- }, 0);
- } else {
- segmentAttributes.duration = 0;
- }
-
- return {
- attributes: segmentAttributes,
- segments: segments
- };
- };
-
- var toPlaylists = function toPlaylists(representations) {
- return representations.map(generateSegments);
- };
-
- var findChildren = function findChildren(element, name) {
- return from(element.childNodes).filter(function (_ref) {
- var tagName = _ref.tagName;
- return tagName === name;
- });
- };
-
- var getContent = function getContent(element) {
- return element.textContent.trim();
- };
-
- var parseDuration = function parseDuration(str) {
- var SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
- var SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
- var SECONDS_IN_DAY = 24 * 60 * 60;
- var SECONDS_IN_HOUR = 60 * 60;
- var SECONDS_IN_MIN = 60;
-
- // P10Y10M10DT10H10M10.1S
- var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
- var match = durationRegex.exec(str);
-
- if (!match) {
- return 0;
- }
-
- var _match$slice = match.slice(1),
- year = _match$slice[0],
- month = _match$slice[1],
- day = _match$slice[2],
- hour = _match$slice[3],
- minute = _match$slice[4],
- second = _match$slice[5];
-
- return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0);
- };
-
- var parseDate = function parseDate(str) {
- // Date format without timezone according to ISO 8601
- // YYY-MM-DDThh:mm:ss.ssssss
- var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/;
-
- // If the date string does not specifiy a timezone, we must specifiy UTC. This is
- // expressed by ending with 'Z'
- if (dateRegex.test(str)) {
- str += 'Z';
- }
-
- return Date.parse(str);
- };
-
- // TODO: maybe order these in some way that makes it easy to find specific attributes
- var parsers = {
- /**
- * Specifies the duration of the entire Media Presentation. Format is a duration string
- * as specified in ISO 8601
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The duration in seconds
- */
- mediaPresentationDuration: function mediaPresentationDuration(value) {
- return parseDuration(value);
- },
-
- /**
- * Specifies the Segment availability start time for all Segments referred to in this
- * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
- * time. Format is a date string as specified in ISO 8601
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The date as seconds from unix epoch
- */
- availabilityStartTime: function availabilityStartTime(value) {
- return parseDate(value) / 1000;
- },
-
- /**
- * Specifies the smallest period between potential changes to the MPD. Format is a
- * duration string as specified in ISO 8601
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The duration in seconds
- */
- minimumUpdatePeriod: function minimumUpdatePeriod(value) {
- return parseDuration(value);
- },
-
- /**
- * Specifies the duration of the smallest time shifting buffer for any Representation
- * in the MPD. Format is a duration string as specified in ISO 8601
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The duration in seconds
- */
- timeShiftBufferDepth: function timeShiftBufferDepth(value) {
- return parseDuration(value);
- },
-
- /**
- * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
- * Format is a duration string as specified in ISO 8601
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The duration in seconds
- */
- start: function start(value) {
- return parseDuration(value);
- },
-
- /**
- * Specifies the width of the visual presentation
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed width
- */
- width: function width(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the height of the visual presentation
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed height
- */
- height: function height(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the bitrate of the representation
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed bandwidth
- */
- bandwidth: function bandwidth(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the number of the first Media Segment in this Representation in the Period
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed number
- */
- startNumber: function startNumber(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the timescale in units per seconds
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The aprsed timescale
- */
- timescale: function timescale(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the constant approximate Segment duration
- * NOTE: The <Period> element also contains an @duration attribute. This duration
- * specifies the duration of the Period. This attribute is currently not
- * supported by the rest of the parser, however we still check for it to prevent
- * errors.
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed duration
- */
- duration: function duration(value) {
- var parsedValue = parseInt(value, 10);
-
- if (isNaN(parsedValue)) {
- return parseDuration(value);
- }
-
- return parsedValue;
- },
-
- /**
- * Specifies the Segment duration, in units of the value of the @timescale.
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed duration
- */
- d: function d(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the MPD start time, in @timescale units, the first Segment in the series
- * starts relative to the beginning of the Period
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed time
- */
- t: function t(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Specifies the repeat count of the number of following contiguous Segments with the
- * same duration expressed by the value of @d
- *
- * @param {string} value
- * value of attribute as a string
- * @return {number}
- * The parsed number
- */
- r: function r(value) {
- return parseInt(value, 10);
- },
-
- /**
- * Default parser for all other attributes. Acts as a no-op and just returns the value
- * as a string
- *
- * @param {string} value
- * value of attribute as a string
- * @return {string}
- * Unparsed value
- */
- DEFAULT: function DEFAULT(value) {
- return value;
- }
- };
-
- /**
- * Gets all the attributes and values of the provided node, parses attributes with known
- * types, and returns an object with attribute names mapped to values.
- *
- * @param {Node} el
- * The node to parse attributes from
- * @return {Object}
- * Object with all attributes of el parsed
- */
- var parseAttributes$1 = function parseAttributes(el) {
- if (!(el && el.attributes)) {
- return {};
- }
-
- return from(el.attributes).reduce(function (a, e) {
- var parseFn = parsers[e.name] || parsers.DEFAULT;
-
- a[e.name] = parseFn(e.value);
-
- return a;
- }, {});
- };
-
- function decodeB64ToUint8Array(b64Text) {
- var decodedString = window_1.atob(b64Text);
- var array = new Uint8Array(decodedString.length);
-
- for (var i = 0; i < decodedString.length; i++) {
- array[i] = decodedString.charCodeAt(i);
- }
- return array;
- }
-
- var keySystemsMap = {
- 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
- 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
- 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
- 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
- };
-
- /**
- * Builds a list of urls that is the product of the reference urls and BaseURL values
- *
- * @param {string[]} referenceUrls
- * List of reference urls to resolve to
- * @param {Node[]} baseUrlElements
- * List of BaseURL nodes from the mpd
- * @return {string[]}
- * List of resolved urls
- */
- var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) {
- if (!baseUrlElements.length) {
- return referenceUrls;
- }
-
- return flatten(referenceUrls.map(function (reference) {
- return baseUrlElements.map(function (baseUrlElement) {
- return resolveUrl$1(reference, getContent(baseUrlElement));
- });
- }));
- };
-
- /**
- * Contains all Segment information for its containing AdaptationSet
- *
- * @typedef {Object} SegmentInformation
- * @property {Object|undefined} template
- * Contains the attributes for the SegmentTemplate node
- * @property {Object[]|undefined} timeline
- * Contains a list of atrributes for each S node within the SegmentTimeline node
- * @property {Object|undefined} list
- * Contains the attributes for the SegmentList node
- * @property {Object|undefined} base
- * Contains the attributes for the SegmentBase node
- */
-
- /**
- * Returns all available Segment information contained within the AdaptationSet node
- *
- * @param {Node} adaptationSet
- * The AdaptationSet node to get Segment information from
- * @return {SegmentInformation}
- * The Segment information contained within the provided AdaptationSet
- */
- var getSegmentInformation = function getSegmentInformation(adaptationSet) {
- var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
- var segmentList = findChildren(adaptationSet, 'SegmentList')[0];
- var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) {
- return merge({ tag: 'SegmentURL' }, parseAttributes$1(s));
- });
- var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
- var segmentTimelineParentNode = segmentList || segmentTemplate;
- var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
- var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
- var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0];
-
- // SegmentTemplate is handled slightly differently, since it can have both
- // @initialization and an <Initialization> node. @initialization can be templated,
- // while the node can have a url and range specified. If the <SegmentTemplate> has
- // both @initialization and an <Initialization> subelement we opt to override with
- // the node, as this interaction is not defined in the spec.
- var template = segmentTemplate && parseAttributes$1(segmentTemplate);
-
- if (template && segmentInitialization) {
- template.initialization = segmentInitialization && parseAttributes$1(segmentInitialization);
- } else if (template && template.initialization) {
- // If it is @initialization we convert it to an object since this is the format that
- // later functions will rely on for the initialization segment. This is only valid
- // for <SegmentTemplate>
- template.initialization = { sourceURL: template.initialization };
- }
-
- var segmentInfo = {
- template: template,
- timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) {
- return parseAttributes$1(s);
- }),
- list: segmentList && merge(parseAttributes$1(segmentList), {
- segmentUrls: segmentUrls,
- initialization: parseAttributes$1(segmentInitialization)
- }),
- base: segmentBase && merge(parseAttributes$1(segmentBase), {
- initialization: parseAttributes$1(segmentInitialization)
- })
- };
-
- Object.keys(segmentInfo).forEach(function (key) {
- if (!segmentInfo[key]) {
- delete segmentInfo[key];
- }
- });
-
- return segmentInfo;
- };
-
- /**
- * Contains Segment information and attributes needed to construct a Playlist object
- * from a Representation
- *
- * @typedef {Object} RepresentationInformation
- * @property {SegmentInformation} segmentInfo
- * Segment information for this Representation
- * @property {Object} attributes
- * Inherited attributes for this Representation
- */
-
- /**
- * Maps a Representation node to an object containing Segment information and attributes
- *
- * @name inheritBaseUrlsCallback
- * @function
- * @param {Node} representation
- * Representation node from the mpd
- * @return {RepresentationInformation}
- * Representation information needed to construct a Playlist object
- */
-
- /**
- * Returns a callback for Array.prototype.map for mapping Representation nodes to
- * Segment information and attributes using inherited BaseURL nodes.
- *
- * @param {Object} adaptationSetAttributes
- * Contains attributes inherited by the AdaptationSet
- * @param {string[]} adaptationSetBaseUrls
- * Contains list of resolved base urls inherited by the AdaptationSet
- * @param {SegmentInformation} adaptationSetSegmentInfo
- * Contains Segment information for the AdaptationSet
- * @return {inheritBaseUrlsCallback}
- * Callback map function
- */
- var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) {
- return function (representation) {
- var repBaseUrlElements = findChildren(representation, 'BaseURL');
- var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
- var attributes = merge(adaptationSetAttributes, parseAttributes$1(representation));
- var representationSegmentInfo = getSegmentInformation(representation);
-
- return repBaseUrls.map(function (baseUrl) {
- return {
- segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
- attributes: merge(attributes, { baseUrl: baseUrl })
- };
- });
- };
- };
-
- /**
- * Tranforms a series of content protection nodes to
- * an object containing pssh data by key system
- *
- * @param {Node[]} contentProtectionNodes
- * Content protection nodes
- * @return {Object}
- * Object containing pssh data by key system
- */
- var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) {
- return contentProtectionNodes.reduce(function (acc, node) {
- var attributes = parseAttributes$1(node);
- var keySystem = keySystemsMap[attributes.schemeIdUri];
-
- if (keySystem) {
- acc[keySystem] = { attributes: attributes };
-
- var psshNode = findChildren(node, 'cenc:pssh')[0];
-
- if (psshNode) {
- var pssh = getContent(psshNode);
- var psshBuffer = pssh && decodeB64ToUint8Array(pssh);
-
- acc[keySystem].pssh = psshBuffer;
- }
- }
-
- return acc;
- }, {});
- };
-
- /**
- * Maps an AdaptationSet node to a list of Representation information objects
- *
- * @name toRepresentationsCallback
- * @function
- * @param {Node} adaptationSet
- * AdaptationSet node from the mpd
- * @return {RepresentationInformation[]}
- * List of objects containing Representaion information
- */
-
- /**
- * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
- * Representation information objects
- *
- * @param {Object} periodAttributes
- * Contains attributes inherited by the Period
- * @param {string[]} periodBaseUrls
- * Contains list of resolved base urls inherited by the Period
- * @param {string[]} periodSegmentInfo
- * Contains Segment Information at the period level
- * @return {toRepresentationsCallback}
- * Callback map function
- */
- var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) {
- return function (adaptationSet) {
- var adaptationSetAttributes = parseAttributes$1(adaptationSet);
- var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
- var role = findChildren(adaptationSet, 'Role')[0];
- var roleAttributes = { role: parseAttributes$1(role) };
-
- var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes);
-
- var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
-
- if (Object.keys(contentProtection).length) {
- attrs = merge(attrs, { contentProtection: contentProtection });
- }
-
- var segmentInfo = getSegmentInformation(adaptationSet);
- var representations = findChildren(adaptationSet, 'Representation');
- var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
-
- return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
- };
- };
-
- /**
- * Maps an Period node to a list of Representation inforamtion objects for all
- * AdaptationSet nodes contained within the Period
- *
- * @name toAdaptationSetsCallback
- * @function
- * @param {Node} period
- * Period node from the mpd
- * @param {number} periodIndex
- * Index of the Period within the mpd
- * @return {RepresentationInformation[]}
- * List of objects containing Representaion information
- */
-
- /**
- * Returns a callback for Array.prototype.map for mapping Period nodes to a list of
- * Representation information objects
- *
- * @param {Object} mpdAttributes
- * Contains attributes inherited by the mpd
- * @param {string[]} mpdBaseUrls
- * Contains list of resolved base urls inherited by the mpd
- * @return {toAdaptationSetsCallback}
- * Callback map function
- */
- var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) {
- return function (period, periodIndex) {
- var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL'));
- var periodAtt = parseAttributes$1(period);
- var periodAttributes = merge(mpdAttributes, periodAtt, { periodIndex: periodIndex });
- var adaptationSets = findChildren(period, 'AdaptationSet');
- var periodSegmentInfo = getSegmentInformation(period);
-
- return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
- };
- };
-
- /**
- * Traverses the mpd xml tree to generate a list of Representation information objects
- * that have inherited attributes from parent nodes
- *
- * @param {Node} mpd
- * The root node of the mpd
- * @param {Object} options
- * Available options for inheritAttributes
- * @param {string} options.manifestUri
- * The uri source of the mpd
- * @param {number} options.NOW
- * Current time per DASH IOP. Default is current time in ms since epoch
- * @param {number} options.clientOffset
- * Client time difference from NOW (in milliseconds)
- * @return {RepresentationInformation[]}
- * List of objects containing Representation information
- */
- var inheritAttributes = function inheritAttributes(mpd) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- var _options$manifestUri = options.manifestUri,
- manifestUri = _options$manifestUri === undefined ? '' : _options$manifestUri,
- _options$NOW = options.NOW,
- NOW = _options$NOW === undefined ? Date.now() : _options$NOW,
- _options$clientOffset = options.clientOffset,
- clientOffset = _options$clientOffset === undefined ? 0 : _options$clientOffset;
-
- var periods = findChildren(mpd, 'Period');
-
- if (periods.length !== 1) {
- // TODO add support for multiperiod
- throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
- }
-
- var mpdAttributes = parseAttributes$1(mpd);
- var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL'));
-
- mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
- mpdAttributes.NOW = NOW;
- mpdAttributes.clientOffset = clientOffset;
-
- return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls)));
- };
-
- var stringToMpdXml = function stringToMpdXml(manifestString) {
- if (manifestString === '') {
- throw new Error(errors.DASH_EMPTY_MANIFEST);
- }
-
- var parser = new window_1.DOMParser();
- var xml = parser.parseFromString(manifestString, 'application/xml');
- var mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
-
- if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
- throw new Error(errors.DASH_INVALID_XML);
- }
-
- return mpd;
- };
-
- /**
- * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
- *
- * @param {string} mpd
- * XML string of the MPD manifest
- * @return {Object|null}
- * Attributes of UTCTiming node specified in the manifest. Null if none found
- */
- var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) {
- var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
-
- if (!UTCTimingNode) {
- return null;
- }
-
- var attributes = parseAttributes$1(UTCTimingNode);
-
- switch (attributes.schemeIdUri) {
- case 'urn:mpeg:dash:utc:http-head:2014':
- case 'urn:mpeg:dash:utc:http-head:2012':
- attributes.method = 'HEAD';
- break;
- case 'urn:mpeg:dash:utc:http-xsdate:2014':
- case 'urn:mpeg:dash:utc:http-iso:2014':
- case 'urn:mpeg:dash:utc:http-xsdate:2012':
- case 'urn:mpeg:dash:utc:http-iso:2012':
- attributes.method = 'GET';
- break;
- case 'urn:mpeg:dash:utc:direct:2014':
- case 'urn:mpeg:dash:utc:direct:2012':
- attributes.method = 'DIRECT';
- attributes.value = Date.parse(attributes.value);
- break;
- case 'urn:mpeg:dash:utc:http-ntp:2014':
- case 'urn:mpeg:dash:utc:ntp:2014':
- case 'urn:mpeg:dash:utc:sntp:2014':
- default:
- throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
- }
-
- return attributes;
- };
-
- var parse = function parse(manifestString, options) {
- return toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)));
- };
-
- /**
- * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
- *
- * @param {string} manifestString
- * XML string of the MPD manifest
- * @return {Object|null}
- * Attributes of UTCTiming node specified in the manifest. Null if none found
- */
- var parseUTCTiming = function parseUTCTiming(manifestString) {
- return parseUTCTimingScheme(stringToMpdXml(manifestString));
- };
-
- var EventTarget$1 = videojs.EventTarget,
- mergeOptions$2 = videojs.mergeOptions;
-
- /**
- * Returns a new master manifest that is the result of merging an updated master manifest
- * into the original version.
- *
- * @param {Object} oldMaster
- * The old parsed mpd object
- * @param {Object} newMaster
- * The updated parsed mpd object
- * @return {Object}
- * A new object representing the original master manifest with the updated media
- * playlists merged in
- */
-
- var updateMaster$1 = function updateMaster$$1(oldMaster, newMaster) {
- var update = mergeOptions$2(oldMaster, {
- // These are top level properties that can be updated
- duration: newMaster.duration,
- minimumUpdatePeriod: newMaster.minimumUpdatePeriod
- });
-
- // First update the playlists in playlist list
- for (var i = 0; i < newMaster.playlists.length; i++) {
- var playlistUpdate = updateMaster(update, newMaster.playlists[i]);
-
- if (playlistUpdate) {
- update = playlistUpdate;
- }
- }
-
- // Then update media group playlists
- forEachMediaGroup(newMaster, function (properties, type, group, label) {
- if (properties.playlists && properties.playlists.length) {
- var uri = properties.playlists[0].uri;
- var _playlistUpdate = updateMaster(update, properties.playlists[0]);
-
- if (_playlistUpdate) {
- update = _playlistUpdate;
- // update the playlist reference within media groups
- update.mediaGroups[type][group][label].playlists[0] = update.playlists[uri];
- }
- }
- });
-
- return update;
- };
-
- var DashPlaylistLoader = function (_EventTarget) {
- inherits$1(DashPlaylistLoader, _EventTarget);
-
- // DashPlaylistLoader must accept either a src url or a playlist because subsequent
- // playlist loader setups from media groups will expect to be able to pass a playlist
- // (since there aren't external URLs to media playlists with DASH)
- function DashPlaylistLoader(srcUrlOrPlaylist, hls, withCredentials, masterPlaylistLoader) {
- classCallCheck$1(this, DashPlaylistLoader);
-
- var _this = possibleConstructorReturn$1(this, (DashPlaylistLoader.__proto__ || Object.getPrototypeOf(DashPlaylistLoader)).call(this));
-
- _this.hls_ = hls;
- _this.withCredentials = withCredentials;
-
- if (!srcUrlOrPlaylist) {
- throw new Error('A non-empty playlist URL or playlist is required');
- }
-
- // event naming?
- _this.on('minimumUpdatePeriod', function () {
- _this.refreshXml_();
- });
-
- // live playlist staleness timeout
- _this.on('mediaupdatetimeout', function () {
- _this.refreshMedia_();
- });
-
- // initialize the loader state
- if (typeof srcUrlOrPlaylist === 'string') {
- _this.srcUrl = srcUrlOrPlaylist;
- _this.state = 'HAVE_NOTHING';
- return possibleConstructorReturn$1(_this);
- }
-
- _this.masterPlaylistLoader_ = masterPlaylistLoader;
-
- _this.state = 'HAVE_METADATA';
- _this.started = true;
- // we only should have one playlist so select it
- _this.media(srcUrlOrPlaylist);
- // trigger async to mimic behavior of HLS, where it must request a playlist
- window_1.setTimeout(function () {
- _this.trigger('loadedmetadata');
- }, 0);
- return _this;
- }
-
- createClass(DashPlaylistLoader, [{
- key: 'dispose',
- value: function dispose() {
- this.stopRequest();
- window_1.clearTimeout(this.mediaUpdateTimeout);
- }
- }, {
- key: 'stopRequest',
- value: function stopRequest() {
- if (this.request) {
- var oldRequest = this.request;
-
- this.request = null;
- oldRequest.onreadystatechange = null;
- oldRequest.abort();
- }
- }
- }, {
- key: 'media',
- value: function media(playlist) {
- // getter
- if (!playlist) {
- return this.media_;
- }
-
- // setter
- if (this.state === 'HAVE_NOTHING') {
- throw new Error('Cannot switch media playlist from ' + this.state);
- }
-
- var startingState = this.state;
-
- // find the playlist object if the target playlist has been specified by URI
- if (typeof playlist === 'string') {
- if (!this.master.playlists[playlist]) {
- throw new Error('Unknown playlist URI: ' + playlist);
- }
- playlist = this.master.playlists[playlist];
- }
-
- var mediaChange = !this.media_ || playlist.uri !== this.media_.uri;
-
- this.state = 'HAVE_METADATA';
-
- // switching to the active playlist is a no-op
- if (!mediaChange) {
- return;
- }
-
- // switching from an already loaded playlist
- if (this.media_) {
- this.trigger('mediachanging');
- }
-
- this.media_ = playlist;
-
- this.refreshMedia_();
-
- // trigger media change if the active media has been updated
- if (startingState !== 'HAVE_MASTER') {
- this.trigger('mediachange');
- }
- }
- }, {
- key: 'pause',
- value: function pause() {
- this.stopRequest();
- if (this.state === 'HAVE_NOTHING') {
- // If we pause the loader before any data has been retrieved, its as if we never
- // started, so reset to an unstarted state.
- this.started = false;
- }
- }
- }, {
- key: 'load',
- value: function load() {
- // because the playlists are internal to the manifest, load should either load the
- // main manifest, or do nothing but trigger an event
- if (!this.started) {
- this.start();
- return;
- }
-
- this.trigger('loadedplaylist');
- }
-
- /**
- * Parses the master xml string and updates playlist uri references
- *
- * @return {Object}
- * The parsed mpd manifest object
- */
-
- }, {
- key: 'parseMasterXml',
- value: function parseMasterXml() {
- var master = parse(this.masterXml_, {
- manifestUri: this.srcUrl,
- clientOffset: this.clientOffset_
- });
-
- master.uri = this.srcUrl;
-
- // Set up phony URIs for the playlists since we won't have external URIs for DASH
- // but reference playlists by their URI throughout the project
- // TODO: Should we create the dummy uris in mpd-parser as well (leaning towards yes).
- for (var i = 0; i < master.playlists.length; i++) {
- var phonyUri = 'placeholder-uri-' + i;
-
- master.playlists[i].uri = phonyUri;
- // set up by URI references
- master.playlists[phonyUri] = master.playlists[i];
- }
-
- // set up phony URIs for the media group playlists since we won't have external
- // URIs for DASH but reference playlists by their URI throughout the project
- forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) {
- if (properties.playlists && properties.playlists.length) {
- var _phonyUri = 'placeholder-uri-' + mediaType + '-' + groupKey + '-' + labelKey;
-
- properties.playlists[0].uri = _phonyUri;
- // setup URI references
- master.playlists[_phonyUri] = properties.playlists[0];
- }
- });
-
- setupMediaPlaylists(master);
- resolveMediaGroupUris(master);
-
- return master;
- }
- }, {
- key: 'start',
- value: function start() {
- var _this2 = this;
-
- this.started = true;
-
- // request the specified URL
- this.request = this.hls_.xhr({
- uri: this.srcUrl,
- withCredentials: this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this2.request) {
- return;
- }
-
- // clear the loader's request reference
- _this2.request = null;
-
- if (error) {
- _this2.error = {
- status: req.status,
- message: 'DASH playlist request error at URL: ' + _this2.srcUrl,
- responseText: req.responseText,
- // MEDIA_ERR_NETWORK
- code: 2
- };
- if (_this2.state === 'HAVE_NOTHING') {
- _this2.started = false;
- }
- return _this2.trigger('error');
- }
-
- _this2.masterXml_ = req.responseText;
-
- if (req.responseHeaders && req.responseHeaders.date) {
- _this2.masterLoaded_ = Date.parse(req.responseHeaders.date);
- } else {
- _this2.masterLoaded_ = Date.now();
- }
-
- _this2.syncClientServerClock_(_this2.onClientServerClockSync_.bind(_this2));
- });
- }
-
- /**
- * Parses the master xml for UTCTiming node to sync the client clock to the server
- * clock. If the UTCTiming node requires a HEAD or GET request, that request is made.
- *
- * @param {Function} done
- * Function to call when clock sync has completed
- */
-
- }, {
- key: 'syncClientServerClock_',
- value: function syncClientServerClock_(done) {
- var _this3 = this;
-
- var utcTiming = parseUTCTiming(this.masterXml_);
-
- // No UTCTiming element found in the mpd. Use Date header from mpd request as the
- // server clock
- if (utcTiming === null) {
- this.clientOffset_ = this.masterLoaded_ - Date.now();
- return done();
- }
-
- if (utcTiming.method === 'DIRECT') {
- this.clientOffset_ = utcTiming.value - Date.now();
- return done();
- }
-
- this.request = this.hls_.xhr({
- uri: resolveUrl(this.srcUrl, utcTiming.value),
- method: utcTiming.method,
- withCredentials: this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this3.request) {
- return;
- }
-
- if (error) {
- // sync request failed, fall back to using date header from mpd
- // TODO: log warning
- _this3.clientOffset_ = _this3.masterLoaded_ - Date.now();
- return done();
- }
-
- var serverTime = void 0;
-
- if (utcTiming.method === 'HEAD') {
- if (!req.responseHeaders || !req.responseHeaders.date) {
- // expected date header not preset, fall back to using date header from mpd
- // TODO: log warning
- serverTime = _this3.masterLoaded_;
- } else {
- serverTime = Date.parse(req.responseHeaders.date);
- }
- } else {
- serverTime = Date.parse(req.responseText);
- }
-
- _this3.clientOffset_ = serverTime - Date.now();
-
- done();
- });
- }
-
- /**
- * Handler for after client/server clock synchronization has happened. Sets up
- * xml refresh timer if specificed by the manifest.
- */
-
- }, {
- key: 'onClientServerClockSync_',
- value: function onClientServerClockSync_() {
- var _this4 = this;
-
- this.master = this.parseMasterXml();
-
- this.state = 'HAVE_MASTER';
-
- this.trigger('loadedplaylist');
-
- if (!this.media_) {
- // no media playlist was specifically selected so start
- // from the first listed one
- this.media(this.master.playlists[0]);
- }
- // trigger loadedmetadata to resolve setup of media groups
- // trigger async to mimic behavior of HLS, where it must request a playlist
- window_1.setTimeout(function () {
- _this4.trigger('loadedmetadata');
- }, 0);
-
- // TODO: minimumUpdatePeriod can have a value of 0. Currently the manifest will not
- // be refreshed when this is the case. The inter-op guide says that when the
- // minimumUpdatePeriod is 0, the manifest should outline all currently available
- // segments, but future segments may require an update. I think a good solution
- // would be to update the manifest at the same rate that the media playlists
- // are "refreshed", i.e. every targetDuration.
- if (this.master.minimumUpdatePeriod) {
- window_1.setTimeout(function () {
- _this4.trigger('minimumUpdatePeriod');
- }, this.master.minimumUpdatePeriod);
- }
- }
-
- /**
- * Sends request to refresh the master xml and updates the parsed master manifest
- * TODO: Does the client offset need to be recalculated when the xml is refreshed?
- */
-
- }, {
- key: 'refreshXml_',
- value: function refreshXml_() {
- var _this5 = this;
-
- this.request = this.hls_.xhr({
- uri: this.srcUrl,
- withCredentials: this.withCredentials
- }, function (error, req) {
- // disposed
- if (!_this5.request) {
- return;
- }
-
- // clear the loader's request reference
- _this5.request = null;
-
- if (error) {
- _this5.error = {
- status: req.status,
- message: 'DASH playlist request error at URL: ' + _this5.srcUrl,
- responseText: req.responseText,
- // MEDIA_ERR_NETWORK
- code: 2
- };
- if (_this5.state === 'HAVE_NOTHING') {
- _this5.started = false;
- }
- return _this5.trigger('error');
- }
-
- _this5.masterXml_ = req.responseText;
-
- var newMaster = _this5.parseMasterXml();
-
- _this5.master = updateMaster$1(_this5.master, newMaster);
-
- window_1.setTimeout(function () {
- _this5.trigger('minimumUpdatePeriod');
- }, _this5.master.minimumUpdatePeriod);
- });
- }
-
- /**
- * Refreshes the media playlist by re-parsing the master xml and updating playlist
- * references. If this is an alternate loader, the updated parsed manifest is retrieved
- * from the master loader.
- */
-
- }, {
- key: 'refreshMedia_',
- value: function refreshMedia_() {
- var _this6 = this;
-
- var oldMaster = void 0;
- var newMaster = void 0;
-
- if (this.masterPlaylistLoader_) {
- oldMaster = this.masterPlaylistLoader_.master;
- newMaster = this.masterPlaylistLoader_.parseMasterXml();
- } else {
- oldMaster = this.master;
- newMaster = this.parseMasterXml();
- }
-
- var updatedMaster = updateMaster$1(oldMaster, newMaster);
-
- if (updatedMaster) {
- if (this.masterPlaylistLoader_) {
- this.masterPlaylistLoader_.master = updatedMaster;
- } else {
- this.master = updatedMaster;
- }
- this.media_ = updatedMaster.playlists[this.media_.uri];
- } else {
- this.trigger('playlistunchanged');
- }
-
- if (!this.media().endList) {
- this.mediaUpdateTimeout = window_1.setTimeout(function () {
- _this6.trigger('mediaupdatetimeout');
- }, refreshDelay(this.media(), !!updatedMaster));
- }
-
- this.trigger('loadedplaylist');
- }
- }]);
- return DashPlaylistLoader;
- }(EventTarget$1);
-
- var logger = function logger(source) {
- if (videojs.log.debug) {
- return videojs.log.debug.bind(videojs, 'VHS:', source + ' >');
- }
-
- return function () {};
- };
-
- function noop() {}
-
- /**
- * @file source-updater.js
- */
-
- /**
- * A queue of callbacks to be serialized and applied when a
- * MediaSource and its associated SourceBuffers are not in the
- * updating state. It is used by the segment loader to update the
- * underlying SourceBuffers when new data is loaded, for instance.
- *
- * @class SourceUpdater
- * @param {MediaSource} mediaSource the MediaSource to create the
- * SourceBuffer from
- * @param {String} mimeType the desired MIME type of the underlying
- * SourceBuffer
- * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer is
- * added to the media source
- */
-
- var SourceUpdater = function () {
- function SourceUpdater(mediaSource, mimeType, type, sourceBufferEmitter) {
- classCallCheck$1(this, SourceUpdater);
-
- this.callbacks_ = [];
- this.pendingCallback_ = null;
- this.timestampOffset_ = 0;
- this.mediaSource = mediaSource;
- this.processedAppend_ = false;
- this.type_ = type;
- this.mimeType_ = mimeType;
- this.logger_ = logger('SourceUpdater[' + type + '][' + mimeType + ']');
-
- if (mediaSource.readyState === 'closed') {
- mediaSource.addEventListener('sourceopen', this.createSourceBuffer_.bind(this, mimeType, sourceBufferEmitter));
- } else {
- this.createSourceBuffer_(mimeType, sourceBufferEmitter);
- }
- }
-
- createClass(SourceUpdater, [{
- key: 'createSourceBuffer_',
- value: function createSourceBuffer_(mimeType, sourceBufferEmitter) {
- var _this = this;
-
- this.sourceBuffer_ = this.mediaSource.addSourceBuffer(mimeType);
-
- this.logger_('created SourceBuffer');
-
- if (sourceBufferEmitter) {
- sourceBufferEmitter.trigger('sourcebufferadded');
-
- if (this.mediaSource.sourceBuffers.length < 2) {
- // There's another source buffer we must wait for before we can start updating
- // our own (or else we can get into a bad state, i.e., appending video/audio data
- // before the other video/audio source buffer is available and leading to a video
- // or audio only buffer).
- sourceBufferEmitter.on('sourcebufferadded', function () {
- _this.start_();
- });
- return;
- }
- }
-
- this.start_();
- }
- }, {
- key: 'start_',
- value: function start_() {
- var _this2 = this;
-
- this.started_ = true;
-
- // run completion handlers and process callbacks as updateend
- // events fire
- this.onUpdateendCallback_ = function () {
- var pendingCallback = _this2.pendingCallback_;
-
- _this2.pendingCallback_ = null;
-
- _this2.logger_('buffered [' + printableRange(_this2.buffered()) + ']');
-
- if (pendingCallback) {
- pendingCallback();
- }
-
- _this2.runCallback_();
- };
-
- this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_);
-
- this.runCallback_();
- }
-
- /**
- * Aborts the current segment and resets the segment parser.
- *
- * @param {Function} done function to call when done
- * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void
- */
-
- }, {
- key: 'abort',
- value: function abort(done) {
- var _this3 = this;
-
- if (this.processedAppend_) {
- this.queueCallback_(function () {
- _this3.sourceBuffer_.abort();
- }, done);
- }
- }
-
- /**
- * Queue an update to append an ArrayBuffer.
- *
- * @param {ArrayBuffer} bytes
- * @param {Function} done the function to call when done
- * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
- */
-
- }, {
- key: 'appendBuffer',
- value: function appendBuffer(bytes, done) {
- var _this4 = this;
-
- this.processedAppend_ = true;
- this.queueCallback_(function () {
- _this4.sourceBuffer_.appendBuffer(bytes);
- }, done);
- }
-
- /**
- * Indicates what TimeRanges are buffered in the managed SourceBuffer.
- *
- * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered
- */
-
- }, {
- key: 'buffered',
- value: function buffered() {
- if (!this.sourceBuffer_) {
- return videojs.createTimeRanges();
- }
- return this.sourceBuffer_.buffered;
- }
-
- /**
- * Queue an update to remove a time range from the buffer.
- *
- * @param {Number} start where to start the removal
- * @param {Number} end where to end the removal
- * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
- */
-
- }, {
- key: 'remove',
- value: function remove(start, end) {
- var _this5 = this;
-
- if (this.processedAppend_) {
- this.queueCallback_(function () {
- _this5.logger_('remove [' + start + ' => ' + end + ']');
- _this5.sourceBuffer_.remove(start, end);
- }, noop);
- }
- }
-
- /**
- * Whether the underlying sourceBuffer is updating or not
- *
- * @return {Boolean} the updating status of the SourceBuffer
- */
-
- }, {
- key: 'updating',
- value: function updating() {
- return !this.sourceBuffer_ || this.sourceBuffer_.updating || this.pendingCallback_;
- }
-
- /**
- * Set/get the timestampoffset on the SourceBuffer
- *
- * @return {Number} the timestamp offset
- */
-
- }, {
- key: 'timestampOffset',
- value: function timestampOffset(offset) {
- var _this6 = this;
-
- if (typeof offset !== 'undefined') {
- this.queueCallback_(function () {
- _this6.sourceBuffer_.timestampOffset = offset;
- });
- this.timestampOffset_ = offset;
- }
- return this.timestampOffset_;
- }
-
- /**
- * Queue a callback to run
- */
-
- }, {
- key: 'queueCallback_',
- value: function queueCallback_(callback, done) {
- this.callbacks_.push([callback.bind(this), done]);
- this.runCallback_();
- }
-
- /**
- * Run a queued callback
- */
-
- }, {
- key: 'runCallback_',
- value: function runCallback_() {
- var callbacks = void 0;
-
- if (!this.updating() && this.callbacks_.length && this.started_) {
- callbacks = this.callbacks_.shift();
- this.pendingCallback_ = callbacks[1];
- callbacks[0]();
- }
- }
-
- /**
- * dispose of the source updater and the underlying sourceBuffer
- */
-
- }, {
- key: 'dispose',
- value: function dispose() {
- this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_);
- if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') {
- this.sourceBuffer_.abort();
- }
- }
- }]);
- return SourceUpdater;
- }();
-
- var Config = {
- GOAL_BUFFER_LENGTH: 30,
- MAX_GOAL_BUFFER_LENGTH: 60,
- GOAL_BUFFER_LENGTH_RATE: 1,
- // A fudge factor to apply to advertised playlist bitrates to account for
- // temporary flucations in client bandwidth
- BANDWIDTH_VARIANCE: 1.2,
- // How much of the buffer must be filled before we consider upswitching
- BUFFER_LOW_WATER_LINE: 0,
- MAX_BUFFER_LOW_WATER_LINE: 30,
- BUFFER_LOW_WATER_LINE_RATE: 1
- };
-
- var toUnsigned = function toUnsigned(value) {
- return value >>> 0;
- };
-
- var bin = {
- toUnsigned: toUnsigned
- };
-
- var toUnsigned$1 = bin.toUnsigned;
- var _findBox, parseType, timescale, startTime, getVideoTrackIds;
-
- // Find the data for a box specified by its path
- _findBox = function findBox(data, path) {
- var results = [],
- i,
- size,
- type,
- end,
- subresults;
-
- if (!path.length) {
- // short-circuit the search for empty paths
- return null;
- }
-
- for (i = 0; i < data.byteLength;) {
- size = toUnsigned$1(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]);
-
- type = parseType(data.subarray(i + 4, i + 8));
-
- end = size > 1 ? i + size : data.byteLength;
-
- if (type === path[0]) {
- if (path.length === 1) {
- // this is the end of the path and we've found the box we were
- // looking for
- results.push(data.subarray(i + 8, end));
- } else {
- // recursively search for the next box along the path
- subresults = _findBox(data.subarray(i + 8, end), path.slice(1));
- if (subresults.length) {
- results = results.concat(subresults);
- }
- }
- }
- i = end;
- }
-
- // we've finished searching all of data
- return results;
- };
-
- /**
- * Returns the string representation of an ASCII encoded four byte buffer.
- * @param buffer {Uint8Array} a four-byte buffer to translate
- * @return {string} the corresponding string
- */
- parseType = function parseType(buffer) {
- var result = '';
- result += String.fromCharCode(buffer[0]);
- result += String.fromCharCode(buffer[1]);
- result += String.fromCharCode(buffer[2]);
- result += String.fromCharCode(buffer[3]);
- return result;
- };
-
- /**
- * Parses an MP4 initialization segment and extracts the timescale
- * values for any declared tracks. Timescale values indicate the
- * number of clock ticks per second to assume for time-based values
- * elsewhere in the MP4.
- *
- * To determine the start time of an MP4, you need two pieces of
- * information: the timescale unit and the earliest base media decode
- * time. Multiple timescales can be specified within an MP4 but the
- * base media decode time is always expressed in the timescale from
- * the media header box for the track:
- * ```
- * moov > trak > mdia > mdhd.timescale
- * ```
- * @param init {Uint8Array} the bytes of the init segment
- * @return {object} a hash of track ids to timescale values or null if
- * the init segment is malformed.
- */
- timescale = function timescale(init) {
- var result = {},
- traks = _findBox(init, ['moov', 'trak']);
-
- // mdhd timescale
- return traks.reduce(function (result, trak) {
- var tkhd, version, index, id, mdhd;
-
- tkhd = _findBox(trak, ['tkhd'])[0];
- if (!tkhd) {
- return null;
- }
- version = tkhd[0];
- index = version === 0 ? 12 : 20;
- id = toUnsigned$1(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]);
-
- mdhd = _findBox(trak, ['mdia', 'mdhd'])[0];
- if (!mdhd) {
- return null;
- }
- version = mdhd[0];
- index = version === 0 ? 12 : 20;
- result[id] = toUnsigned$1(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
- return result;
- }, result);
- };
-
- /**
- * Determine the base media decode start time, in seconds, for an MP4
- * fragment. If multiple fragments are specified, the earliest time is
- * returned.
- *
- * The base media decode time can be parsed from track fragment
- * metadata:
- * ```
- * moof > traf > tfdt.baseMediaDecodeTime
- * ```
- * It requires the timescale value from the mdhd to interpret.
- *
- * @param timescale {object} a hash of track ids to timescale values.
- * @return {number} the earliest base media decode start time for the
- * fragment, in seconds
- */
- startTime = function startTime(timescale, fragment) {
- var trafs, baseTimes, result;
-
- // we need info from two childrend of each track fragment box
- trafs = _findBox(fragment, ['moof', 'traf']);
-
- // determine the start times for each track
- baseTimes = [].concat.apply([], trafs.map(function (traf) {
- return _findBox(traf, ['tfhd']).map(function (tfhd) {
- var id, scale, baseTime;
-
- // get the track id from the tfhd
- id = toUnsigned$1(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]);
- // assume a 90kHz clock if no timescale was specified
- scale = timescale[id] || 90e3;
-
- // get the base media decode time from the tfdt
- baseTime = _findBox(traf, ['tfdt']).map(function (tfdt) {
- var version, result;
-
- version = tfdt[0];
- result = toUnsigned$1(tfdt[4] << 24 | tfdt[5] << 16 | tfdt[6] << 8 | tfdt[7]);
- if (version === 1) {
- result *= Math.pow(2, 32);
- result += toUnsigned$1(tfdt[8] << 24 | tfdt[9] << 16 | tfdt[10] << 8 | tfdt[11]);
- }
- return result;
- })[0];
- baseTime = baseTime || Infinity;
-
- // convert base time to seconds
- return baseTime / scale;
- });
- }));
-
- // return the minimum
- result = Math.min.apply(null, baseTimes);
- return isFinite(result) ? result : 0;
- };
-
- /**
- * Find the trackIds of the video tracks in this source.
- * Found by parsing the Handler Reference and Track Header Boxes:
- * moov > trak > mdia > hdlr
- * moov > trak > tkhd
- *
- * @param {Uint8Array} init - The bytes of the init segment for this source
- * @return {Number[]} A list of trackIds
- *
- * @see ISO-BMFF-12/2015, Section 8.4.3
- **/
- getVideoTrackIds = function getVideoTrackIds(init) {
- var traks = _findBox(init, ['moov', 'trak']);
- var videoTrackIds = [];
-
- traks.forEach(function (trak) {
- var hdlrs = _findBox(trak, ['mdia', 'hdlr']);
- var tkhds = _findBox(trak, ['tkhd']);
-
- hdlrs.forEach(function (hdlr, index) {
- var handlerType = parseType(hdlr.subarray(8, 12));
- var tkhd = tkhds[index];
- var view;
- var version;
- var trackId;
-
- if (handlerType === 'vide') {
- view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
- version = view.getUint8(0);
- trackId = version === 0 ? view.getUint32(12) : view.getUint32(20);
-
- videoTrackIds.push(trackId);
- }
- });
- });
-
- return videoTrackIds;
- };
-
- var probe = {
- findBox: _findBox,
- parseType: parseType,
- timescale: timescale,
- startTime: startTime,
- videoTrackIds: getVideoTrackIds
- };
-
- var REQUEST_ERRORS = {
- FAILURE: 2,
- TIMEOUT: -101,
- ABORTED: -102
- };
-
- /**
- * Turns segment byterange into a string suitable for use in
- * HTTP Range requests
- *
- * @param {Object} byterange - an object with two values defining the start and end
- * of a byte-range
- */
- var byterangeStr = function byterangeStr(byterange) {
- var byterangeStart = void 0;
- var byterangeEnd = void 0;
-
- // `byterangeEnd` is one less than `offset + length` because the HTTP range
- // header uses inclusive ranges
- byterangeEnd = byterange.offset + byterange.length - 1;
- byterangeStart = byterange.offset;
- return 'bytes=' + byterangeStart + '-' + byterangeEnd;
- };
-
- /**
- * Defines headers for use in the xhr request for a particular segment.
- *
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- */
- var segmentXhrHeaders = function segmentXhrHeaders(segment) {
- var headers = {};
-
- if (segment.byterange) {
- headers.Range = byterangeStr(segment.byterange);
- }
- return headers;
- };
-
- /**
- * Abort all requests
- *
- * @param {Object} activeXhrs - an object that tracks all XHR requests
- */
- var abortAll = function abortAll(activeXhrs) {
- activeXhrs.forEach(function (xhr) {
- xhr.abort();
- });
- };
-
- /**
- * Gather important bandwidth stats once a request has completed
- *
- * @param {Object} request - the XHR request from which to gather stats
- */
- var getRequestStats = function getRequestStats(request) {
- return {
- bandwidth: request.bandwidth,
- bytesReceived: request.bytesReceived || 0,
- roundTripTime: request.roundTripTime || 0
- };
- };
-
- /**
- * If possible gather bandwidth stats as a request is in
- * progress
- *
- * @param {Event} progressEvent - an event object from an XHR's progress event
- */
- var getProgressStats = function getProgressStats(progressEvent) {
- var request = progressEvent.target;
- var roundTripTime = Date.now() - request.requestTime;
- var stats = {
- bandwidth: Infinity,
- bytesReceived: 0,
- roundTripTime: roundTripTime || 0
- };
-
- stats.bytesReceived = progressEvent.loaded;
- // This can result in Infinity if stats.roundTripTime is 0 but that is ok
- // because we should only use bandwidth stats on progress to determine when
- // abort a request early due to insufficient bandwidth
- stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000);
-
- return stats;
- };
-
- /**
- * Handle all error conditions in one place and return an object
- * with all the information
- *
- * @param {Error|null} error - if non-null signals an error occured with the XHR
- * @param {Object} request - the XHR request that possibly generated the error
- */
- var handleErrors = function handleErrors(error, request) {
- if (request.timedout) {
- return {
- status: request.status,
- message: 'HLS request timed-out at URL: ' + request.uri,
- code: REQUEST_ERRORS.TIMEOUT,
- xhr: request
- };
- }
-
- if (request.aborted) {
- return {
- status: request.status,
- message: 'HLS request aborted at URL: ' + request.uri,
- code: REQUEST_ERRORS.ABORTED,
- xhr: request
- };
- }
-
- if (error) {
- return {
- status: request.status,
- message: 'HLS request errored at URL: ' + request.uri,
- code: REQUEST_ERRORS.FAILURE,
- xhr: request
- };
- }
-
- return null;
- };
-
- /**
- * Handle responses for key data and convert the key data to the correct format
- * for the decryption step later
- *
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} finishProcessingFn - a callback to execute to continue processing
- * this request
- */
- var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) {
- return function (error, request) {
- var response = request.response;
- var errorObj = handleErrors(error, request);
-
- if (errorObj) {
- return finishProcessingFn(errorObj, segment);
- }
-
- if (response.byteLength !== 16) {
- return finishProcessingFn({
- status: request.status,
- message: 'Invalid HLS key at URL: ' + request.uri,
- code: REQUEST_ERRORS.FAILURE,
- xhr: request
- }, segment);
- }
-
- var view = new DataView(response);
-
- segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
- return finishProcessingFn(null, segment);
- };
- };
-
- /**
- * Handle init-segment responses
- *
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} finishProcessingFn - a callback to execute to continue processing
- * this request
- */
- var handleInitSegmentResponse = function handleInitSegmentResponse(segment, captionParser, finishProcessingFn) {
- return function (error, request) {
- var response = request.response;
- var errorObj = handleErrors(error, request);
-
- if (errorObj) {
- return finishProcessingFn(errorObj, segment);
- }
-
- // stop processing if received empty content
- if (response.byteLength === 0) {
- return finishProcessingFn({
- status: request.status,
- message: 'Empty HLS segment content at URL: ' + request.uri,
- code: REQUEST_ERRORS.FAILURE,
- xhr: request
- }, segment);
- }
-
- segment.map.bytes = new Uint8Array(request.response);
-
- // Initialize CaptionParser if it hasn't been yet
- if (!captionParser.isInitialized()) {
- captionParser.init();
- }
-
- segment.map.timescales = probe.timescale(segment.map.bytes);
- segment.map.videoTrackIds = probe.videoTrackIds(segment.map.bytes);
-
- return finishProcessingFn(null, segment);
- };
- };
-
- /**
- * Response handler for segment-requests being sure to set the correct
- * property depending on whether the segment is encryped or not
- * Also records and keeps track of stats that are used for ABR purposes
- *
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} finishProcessingFn - a callback to execute to continue processing
- * this request
- */
- var handleSegmentResponse = function handleSegmentResponse(segment, captionParser, finishProcessingFn) {
- return function (error, request) {
- var response = request.response;
- var errorObj = handleErrors(error, request);
- var parsed = void 0;
-
- if (errorObj) {
- return finishProcessingFn(errorObj, segment);
- }
-
- // stop processing if received empty content
- if (response.byteLength === 0) {
- return finishProcessingFn({
- status: request.status,
- message: 'Empty HLS segment content at URL: ' + request.uri,
- code: REQUEST_ERRORS.FAILURE,
- xhr: request
- }, segment);
- }
-
- segment.stats = getRequestStats(request);
-
- if (segment.key) {
- segment.encryptedBytes = new Uint8Array(request.response);
- } else {
- segment.bytes = new Uint8Array(request.response);
- }
-
- // This is likely an FMP4 and has the init segment.
- // Run through the CaptionParser in case there are captions.
- if (segment.map && segment.map.bytes) {
- // Initialize CaptionParser if it hasn't been yet
- if (!captionParser.isInitialized()) {
- captionParser.init();
- }
-
- parsed = captionParser.parse(segment.bytes, segment.map.videoTrackIds, segment.map.timescales);
-
- if (parsed && parsed.captions) {
- segment.captionStreams = parsed.captionStreams;
- segment.fmp4Captions = parsed.captions;
- }
- }
-
- return finishProcessingFn(null, segment);
- };
- };
-
- /**
- * Decrypt the segment via the decryption web worker
- *
- * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} doneFn - a callback that is executed after decryption has completed
- */
- var decryptSegment = function decryptSegment(decrypter, segment, doneFn) {
- var decryptionHandler = function decryptionHandler(event) {
- if (event.data.source === segment.requestId) {
- decrypter.removeEventListener('message', decryptionHandler);
- var decrypted = event.data.decrypted;
-
- segment.bytes = new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength);
- return doneFn(null, segment);
- }
- };
-
- decrypter.addEventListener('message', decryptionHandler);
-
- // this is an encrypted segment
- // incrementally decrypt the segment
- decrypter.postMessage(createTransferableMessage({
- source: segment.requestId,
- encrypted: segment.encryptedBytes,
- key: segment.key.bytes,
- iv: segment.key.iv
- }), [segment.encryptedBytes.buffer, segment.key.bytes.buffer]);
- };
-
- /**
- * The purpose of this function is to get the most pertinent error from the
- * array of errors.
- * For instance if a timeout and two aborts occur, then the aborts were
- * likely triggered by the timeout so return that error object.
- */
- var getMostImportantError = function getMostImportantError(errors) {
- return errors.reduce(function (prev, err) {
- return err.code > prev.code ? err : prev;
- });
- };
-
- /**
- * This function waits for all XHRs to finish (with either success or failure)
- * before continueing processing via it's callback. The function gathers errors
- * from each request into a single errors array so that the error status for
- * each request can be examined later.
- *
- * @param {Object} activeXhrs - an object that tracks all XHR requests
- * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines
- * @param {Function} doneFn - a callback that is executed after all resources have been
- * downloaded and any decryption completed
- */
- var waitForCompletion = function waitForCompletion(activeXhrs, decrypter, doneFn) {
- var errors = [];
- var count = 0;
-
- return function (error, segment) {
- if (error) {
- // If there are errors, we have to abort any outstanding requests
- abortAll(activeXhrs);
- errors.push(error);
- }
- count += 1;
-
- if (count === activeXhrs.length) {
- // Keep track of when *all* of the requests have completed
- segment.endOfAllRequests = Date.now();
-
- if (errors.length > 0) {
- var worstError = getMostImportantError(errors);
-
- return doneFn(worstError, segment);
- }
- if (segment.encryptedBytes) {
- return decryptSegment(decrypter, segment, doneFn);
- }
- // Otherwise, everything is ready just continue
- return doneFn(null, segment);
- }
- };
- };
-
- /**
- * Simple progress event callback handler that gathers some stats before
- * executing a provided callback with the `segment` object
- *
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} progressFn - a callback that is executed each time a progress event
- * is received
- * @param {Event} event - the progress event object from XMLHttpRequest
- */
- var handleProgress = function handleProgress(segment, progressFn) {
- return function (event) {
- segment.stats = videojs.mergeOptions(segment.stats, getProgressStats(event));
-
- // record the time that we receive the first byte of data
- if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) {
- segment.stats.firstBytesReceivedAt = Date.now();
- }
-
- return progressFn(event, segment);
- };
- };
-
- /**
- * Load all resources and does any processing necessary for a media-segment
- *
- * Features:
- * decrypts the media-segment if it has a key uri and an iv
- * aborts *all* requests if *any* one request fails
- *
- * The segment object, at minimum, has the following format:
- * {
- * resolvedUri: String,
- * [byterange]: {
- * offset: Number,
- * length: Number
- * },
- * [key]: {
- * resolvedUri: String
- * [byterange]: {
- * offset: Number,
- * length: Number
- * },
- * iv: {
- * bytes: Uint32Array
- * }
- * },
- * [map]: {
- * resolvedUri: String,
- * [byterange]: {
- * offset: Number,
- * length: Number
- * },
- * [bytes]: Uint8Array
- * }
- * }
- * ...where [name] denotes optional properties
- *
- * @param {Function} xhr - an instance of the xhr wrapper in xhr.js
- * @param {Object} xhrOptions - the base options to provide to all xhr requests
- * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128
- * decryption routines
- * @param {Object} segment - a simplified copy of the segmentInfo object
- * from SegmentLoader
- * @param {Function} progressFn - a callback that receives progress events from the main
- * segment's xhr request
- * @param {Function} doneFn - a callback that is executed only once all requests have
- * succeeded or failed
- * @returns {Function} a function that, when invoked, immediately aborts all
- * outstanding requests
- */
- var mediaSegmentRequest = function mediaSegmentRequest(xhr, xhrOptions, decryptionWorker, captionParser, segment, progressFn, doneFn) {
- var activeXhrs = [];
- var finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn);
-
- // optionally, request the decryption key
- if (segment.key) {
- var keyRequestOptions = videojs.mergeOptions(xhrOptions, {
- uri: segment.key.resolvedUri,
- responseType: 'arraybuffer'
- });
- var keyRequestCallback = handleKeyResponse(segment, finishProcessingFn);
- var keyXhr = xhr(keyRequestOptions, keyRequestCallback);
-
- activeXhrs.push(keyXhr);
- }
-
- // optionally, request the associated media init segment
- if (segment.map && !segment.map.bytes) {
- var initSegmentOptions = videojs.mergeOptions(xhrOptions, {
- uri: segment.map.resolvedUri,
- responseType: 'arraybuffer',
- headers: segmentXhrHeaders(segment.map)
- });
- var initSegmentRequestCallback = handleInitSegmentResponse(segment, captionParser, finishProcessingFn);
- var initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
-
- activeXhrs.push(initSegmentXhr);
- }
-
- var segmentRequestOptions = videojs.mergeOptions(xhrOptions, {
- uri: segment.resolvedUri,
- responseType: 'arraybuffer',
- headers: segmentXhrHeaders(segment)
- });
- var segmentRequestCallback = handleSegmentResponse(segment, captionParser, finishProcessingFn);
- var segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);
-
- segmentXhr.addEventListener('progress', handleProgress(segment, progressFn));
- activeXhrs.push(segmentXhr);
-
- return function () {
- return abortAll(activeXhrs);
- };
- };
-
- // Utilities
-
- /**
- * Returns the CSS value for the specified property on an element
- * using `getComputedStyle`. Firefox has a long-standing issue where
- * getComputedStyle() may return null when running in an iframe with
- * `display: none`.
- *
- * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
- * @param {HTMLElement} el the htmlelement to work on
- * @param {string} the proprety to get the style for
- */
- var safeGetComputedStyle = function safeGetComputedStyle(el, property) {
- var result = void 0;
-
- if (!el) {
- return '';
- }
-
- result = window_1.getComputedStyle(el);
- if (!result) {
- return '';
- }
-
- return result[property];
- };
-
- /**
- * Resuable stable sort function
- *
- * @param {Playlists} array
- * @param {Function} sortFn Different comparators
- * @function stableSort
- */
- var stableSort = function stableSort(array, sortFn) {
- var newArray = array.slice();
-
- array.sort(function (left, right) {
- var cmp = sortFn(left, right);
-
- if (cmp === 0) {
- return newArray.indexOf(left) - newArray.indexOf(right);
- }
- return cmp;
- });
- };
-
- /**
- * A comparator function to sort two playlist object by bandwidth.
- *
- * @param {Object} left a media playlist object
- * @param {Object} right a media playlist object
- * @return {Number} Greater than zero if the bandwidth attribute of
- * left is greater than the corresponding attribute of right. Less
- * than zero if the bandwidth of right is greater than left and
- * exactly zero if the two are equal.
- */
- var comparePlaylistBandwidth = function comparePlaylistBandwidth(left, right) {
- var leftBandwidth = void 0;
- var rightBandwidth = void 0;
-
- if (left.attributes.BANDWIDTH) {
- leftBandwidth = left.attributes.BANDWIDTH;
- }
- leftBandwidth = leftBandwidth || window_1.Number.MAX_VALUE;
- if (right.attributes.BANDWIDTH) {
- rightBandwidth = right.attributes.BANDWIDTH;
- }
- rightBandwidth = rightBandwidth || window_1.Number.MAX_VALUE;
-
- return leftBandwidth - rightBandwidth;
- };
-
- /**
- * A comparator function to sort two playlist object by resolution (width).
- * @param {Object} left a media playlist object
- * @param {Object} right a media playlist object
- * @return {Number} Greater than zero if the resolution.width attribute of
- * left is greater than the corresponding attribute of right. Less
- * than zero if the resolution.width of right is greater than left and
- * exactly zero if the two are equal.
- */
- var comparePlaylistResolution = function comparePlaylistResolution(left, right) {
- var leftWidth = void 0;
- var rightWidth = void 0;
-
- if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
- leftWidth = left.attributes.RESOLUTION.width;
- }
-
- leftWidth = leftWidth || window_1.Number.MAX_VALUE;
-
- if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
- rightWidth = right.attributes.RESOLUTION.width;
- }
-
- rightWidth = rightWidth || window_1.Number.MAX_VALUE;
-
- // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
- // have the same media dimensions/ resolution
- if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
- return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
- }
- return leftWidth - rightWidth;
- };
-
- /**
- * Chooses the appropriate media playlist based on bandwidth and player size
- *
- * @param {Object} master
- * Object representation of the master manifest
- * @param {Number} playerBandwidth
- * Current calculated bandwidth of the player
- * @param {Number} playerWidth
- * Current width of the player element
- * @param {Number} playerHeight
- * Current height of the player element
- * @return {Playlist} the highest bitrate playlist less than the
- * currently detected bandwidth, accounting for some amount of
- * bandwidth variance
- */
- var simpleSelector = function simpleSelector(master, playerBandwidth, playerWidth, playerHeight) {
- // convert the playlists to an intermediary representation to make comparisons easier
- var sortedPlaylistReps = master.playlists.map(function (playlist) {
- var width = void 0;
- var height = void 0;
- var bandwidth = void 0;
-
- width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
- height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
- bandwidth = playlist.attributes.BANDWIDTH;
-
- bandwidth = bandwidth || window_1.Number.MAX_VALUE;
-
- return {
- bandwidth: bandwidth,
- width: width,
- height: height,
- playlist: playlist
- };
- });
-
- stableSort(sortedPlaylistReps, function (left, right) {
- return left.bandwidth - right.bandwidth;
- });
-
- // filter out any playlists that have been excluded due to
- // incompatible configurations
- sortedPlaylistReps = sortedPlaylistReps.filter(function (rep) {
- return !Playlist.isIncompatible(rep.playlist);
- });
-
- // filter out any playlists that have been disabled manually through the representations
- // api or blacklisted temporarily due to playback errors.
- var enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) {
- return Playlist.isEnabled(rep.playlist);
- });
-
- if (!enabledPlaylistReps.length) {
- // if there are no enabled playlists, then they have all been blacklisted or disabled
- // by the user through the representations api. In this case, ignore blacklisting and
- // fallback to what the user wants by using playlists the user has not disabled.
- enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) {
- return !Playlist.isDisabled(rep.playlist);
- });
- }
-
- // filter out any variant that has greater effective bitrate
- // than the current estimated bandwidth
- var bandwidthPlaylistReps = enabledPlaylistReps.filter(function (rep) {
- return rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth;
- });
-
- var highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
-
- // get all of the renditions with the same (highest) bandwidth
- // and then taking the very first element
- var bandwidthBestRep = bandwidthPlaylistReps.filter(function (rep) {
- return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
- })[0];
-
- // filter out playlists without resolution information
- var haveResolution = bandwidthPlaylistReps.filter(function (rep) {
- return rep.width && rep.height;
- });
-
- // sort variants by resolution
- stableSort(haveResolution, function (left, right) {
- return left.width - right.width;
- });
-
- // if we have the exact resolution as the player use it
- var resolutionBestRepList = haveResolution.filter(function (rep) {
- return rep.width === playerWidth && rep.height === playerHeight;
- });
-
- highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
- // ensure that we pick the highest bandwidth variant that have exact resolution
- var resolutionBestRep = resolutionBestRepList.filter(function (rep) {
- return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
- })[0];
-
- var resolutionPlusOneList = void 0;
- var resolutionPlusOneSmallest = void 0;
- var resolutionPlusOneRep = void 0;
-
- // find the smallest variant that is larger than the player
- // if there is no match of exact resolution
- if (!resolutionBestRep) {
- resolutionPlusOneList = haveResolution.filter(function (rep) {
- return rep.width > playerWidth || rep.height > playerHeight;
- });
-
- // find all the variants have the same smallest resolution
- resolutionPlusOneSmallest = resolutionPlusOneList.filter(function (rep) {
- return rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height;
- });
-
- // ensure that we also pick the highest bandwidth variant that
- // is just-larger-than the video player
- highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
- resolutionPlusOneRep = resolutionPlusOneSmallest.filter(function (rep) {
- return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
- })[0];
- }
-
- // fallback chain of variants
- var chosenRep = resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
-
- return chosenRep ? chosenRep.playlist : null;
- };
-
- // Playlist Selectors
-
- /**
- * Chooses the appropriate media playlist based on the most recent
- * bandwidth estimate and the player size.
- *
- * Expects to be called within the context of an instance of HlsHandler
- *
- * @return {Playlist} the highest bitrate playlist less than the
- * currently detected bandwidth, accounting for some amount of
- * bandwidth variance
- */
- var lastBandwidthSelector = function lastBandwidthSelector() {
- return simpleSelector(this.playlists.master, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
- };
-
- /**
- * Chooses the appropriate media playlist based on the potential to rebuffer
- *
- * @param {Object} settings
- * Object of information required to use this selector
- * @param {Object} settings.master
- * Object representation of the master manifest
- * @param {Number} settings.currentTime
- * The current time of the player
- * @param {Number} settings.bandwidth
- * Current measured bandwidth
- * @param {Number} settings.duration
- * Duration of the media
- * @param {Number} settings.segmentDuration
- * Segment duration to be used in round trip time calculations
- * @param {Number} settings.timeUntilRebuffer
- * Time left in seconds until the player has to rebuffer
- * @param {Number} settings.currentTimeline
- * The current timeline segments are being loaded from
- * @param {SyncController} settings.syncController
- * SyncController for determining if we have a sync point for a given playlist
- * @return {Object|null}
- * {Object} return.playlist
- * The highest bandwidth playlist with the least amount of rebuffering
- * {Number} return.rebufferingImpact
- * The amount of time in seconds switching to this playlist will rebuffer. A
- * negative value means that switching will cause zero rebuffering.
- */
- var minRebufferMaxBandwidthSelector = function minRebufferMaxBandwidthSelector(settings) {
- var master = settings.master,
- currentTime = settings.currentTime,
- bandwidth = settings.bandwidth,
- duration$$1 = settings.duration,
- segmentDuration = settings.segmentDuration,
- timeUntilRebuffer = settings.timeUntilRebuffer,
- currentTimeline = settings.currentTimeline,
- syncController = settings.syncController;
-
- // filter out any playlists that have been excluded due to
- // incompatible configurations
-
- var compatiblePlaylists = master.playlists.filter(function (playlist) {
- return !Playlist.isIncompatible(playlist);
- });
-
- // filter out any playlists that have been disabled manually through the representations
- // api or blacklisted temporarily due to playback errors.
- var enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
-
- if (!enabledPlaylists.length) {
- // if there are no enabled playlists, then they have all been blacklisted or disabled
- // by the user through the representations api. In this case, ignore blacklisting and
- // fallback to what the user wants by using playlists the user has not disabled.
- enabledPlaylists = compatiblePlaylists.filter(function (playlist) {
- return !Playlist.isDisabled(playlist);
- });
- }
-
- var bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));
-
- var rebufferingEstimates = bandwidthPlaylists.map(function (playlist) {
- var syncPoint = syncController.getSyncPoint(playlist, duration$$1, currentTimeline, currentTime);
- // If there is no sync point for this playlist, switching to it will require a
- // sync request first. This will double the request time
- var numRequests = syncPoint ? 1 : 2;
- var requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist);
- var rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer;
-
- return {
- playlist: playlist,
- rebufferingImpact: rebufferingImpact
- };
- });
-
- var noRebufferingPlaylists = rebufferingEstimates.filter(function (estimate) {
- return estimate.rebufferingImpact <= 0;
- });
-
- // Sort by bandwidth DESC
- stableSort(noRebufferingPlaylists, function (a, b) {
- return comparePlaylistBandwidth(b.playlist, a.playlist);
- });
-
- if (noRebufferingPlaylists.length) {
- return noRebufferingPlaylists[0];
- }
-
- stableSort(rebufferingEstimates, function (a, b) {
- return a.rebufferingImpact - b.rebufferingImpact;
- });
-
- return rebufferingEstimates[0] || null;
- };
-
- /**
- * Chooses the appropriate media playlist, which in this case is the lowest bitrate
- * one with video. If no renditions with video exist, return the lowest audio rendition.
- *
- * Expects to be called within the context of an instance of HlsHandler
- *
- * @return {Object|null}
- * {Object} return.playlist
- * The lowest bitrate playlist that contains a video codec. If no such rendition
- * exists pick the lowest audio rendition.
- */
- var lowestBitrateCompatibleVariantSelector = function lowestBitrateCompatibleVariantSelector() {
- // filter out any playlists that have been excluded due to
- // incompatible configurations or playback errors
- var playlists = this.playlists.master.playlists.filter(Playlist.isEnabled);
-
- // Sort ascending by bitrate
- stableSort(playlists, function (a, b) {
- return comparePlaylistBandwidth(a, b);
- });
-
- // Parse and assume that playlists with no video codec have no video
- // (this is not necessarily true, although it is generally true).
- //
- // If an entire manifest has no valid videos everything will get filtered
- // out.
- var playlistsWithVideo = playlists.filter(function (playlist) {
- return parseCodecs(playlist.attributes.CODECS).videoCodec;
- });
-
- return playlistsWithVideo[0] || null;
- };
-
- /**
- * Create captions text tracks on video.js if they do not exist
- *
- * @param {Object} inbandTextTracks a reference to current inbandTextTracks
- * @param {Object} tech the video.js tech
- * @param {Object} captionStreams the caption streams to create
- * @private
- */
- var createCaptionsTrackIfNotExists = function createCaptionsTrackIfNotExists(inbandTextTracks, tech, captionStreams) {
- for (var trackId in captionStreams) {
- if (!inbandTextTracks[trackId]) {
- tech.trigger({ type: 'usage', name: 'hls-608' });
- var track = tech.textTracks().getTrackById(trackId);
-
- if (track) {
- // Resuse an existing track with a CC# id because this was
- // very likely created by videojs-contrib-hls from information
- // in the m3u8 for us to use
- inbandTextTracks[trackId] = track;
- } else {
- // Otherwise, create a track with the default `CC#` label and
- // without a language
- inbandTextTracks[trackId] = tech.addRemoteTextTrack({
- kind: 'captions',
- id: trackId,
- label: trackId
- }, false).track;
- }
- }
- }
- };
-
- var addCaptionData = function addCaptionData(_ref) {
- var inbandTextTracks = _ref.inbandTextTracks,
- captionArray = _ref.captionArray,
- timestampOffset = _ref.timestampOffset;
-
- if (!captionArray) {
- return;
- }
-
- var Cue = window.WebKitDataCue || window.VTTCue;
-
- captionArray.forEach(function (caption) {
- var track = caption.stream;
- var startTime = caption.startTime;
- var endTime = caption.endTime;
-
- if (!inbandTextTracks[track]) {
- return;
- }
-
- startTime += timestampOffset;
- endTime += timestampOffset;
-
- inbandTextTracks[track].addCue(new Cue(startTime, endTime, caption.text));
- });
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2015 Brightcove
- * All rights reserved.
- *
- * Functions that generate fragmented MP4s suitable for use with Media
- * Source Extensions.
- */
-
- var UINT32_MAX = Math.pow(2, 32) - 1;
-
- var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS;
-
- // pre-calculate constants
- (function () {
- var i;
- types = {
- avc1: [], // codingname
- avcC: [],
- btrt: [],
- dinf: [],
- dref: [],
- esds: [],
- ftyp: [],
- hdlr: [],
- mdat: [],
- mdhd: [],
- mdia: [],
- mfhd: [],
- minf: [],
- moof: [],
- moov: [],
- mp4a: [], // codingname
- mvex: [],
- mvhd: [],
- sdtp: [],
- smhd: [],
- stbl: [],
- stco: [],
- stsc: [],
- stsd: [],
- stsz: [],
- stts: [],
- styp: [],
- tfdt: [],
- tfhd: [],
- traf: [],
- trak: [],
- trun: [],
- trex: [],
- tkhd: [],
- vmhd: []
- };
-
- // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we
- // don't throw an error
- if (typeof Uint8Array === 'undefined') {
- return;
- }
-
- for (i in types) {
- if (types.hasOwnProperty(i)) {
- types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
- }
- }
-
- MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]);
- AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]);
- MINOR_VERSION = new Uint8Array([0, 0, 0, 1]);
- VIDEO_HDLR = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
- ]);
- AUDIO_HDLR = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun'
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler'
- ]);
- HDLR_TYPES = {
- video: VIDEO_HDLR,
- audio: AUDIO_HDLR
- };
- DREF = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01, // entry_count
- 0x00, 0x00, 0x00, 0x0c, // entry_size
- 0x75, 0x72, 0x6c, 0x20, // 'url' type
- 0x00, // version 0
- 0x00, 0x00, 0x01 // entry_flags
- ]);
- SMHD = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, // balance, 0 means centered
- 0x00, 0x00 // reserved
- ]);
- STCO = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00 // entry_count
- ]);
- STSC = STCO;
- STSZ = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x00, // sample_size
- 0x00, 0x00, 0x00, 0x00 // sample_count
- ]);
- STTS = STCO;
- VMHD = new Uint8Array([0x00, // version
- 0x00, 0x00, 0x01, // flags
- 0x00, 0x00, // graphicsmode
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor
- ]);
- })();
-
- box = function box(type) {
- var payload = [],
- size = 0,
- i,
- result,
- view;
-
- for (i = 1; i < arguments.length; i++) {
- payload.push(arguments[i]);
- }
-
- i = payload.length;
-
- // calculate the total size we need to allocate
- while (i--) {
- size += payload[i].byteLength;
- }
- result = new Uint8Array(size + 8);
- view = new DataView(result.buffer, result.byteOffset, result.byteLength);
- view.setUint32(0, result.byteLength);
- result.set(type, 4);
-
- // copy the payload into the result
- for (i = 0, size = 8; i < payload.length; i++) {
- result.set(payload[i], size);
- size += payload[i].byteLength;
- }
- return result;
- };
-
- dinf = function dinf() {
- return box(types.dinf, box(types.dref, DREF));
- };
-
- esds = function esds(track) {
- return box(types.esds, new Uint8Array([0x00, // version
- 0x00, 0x00, 0x00, // flags
-
- // ES_Descriptor
- 0x03, // tag, ES_DescrTag
- 0x19, // length
- 0x00, 0x00, // ES_ID
- 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority
-
- // DecoderConfigDescriptor
- 0x04, // tag, DecoderConfigDescrTag
- 0x11, // length
- 0x40, // object type
- 0x15, // streamType
- 0x00, 0x06, 0x00, // bufferSizeDB
- 0x00, 0x00, 0xda, 0xc0, // maxBitrate
- 0x00, 0x00, 0xda, 0xc0, // avgBitrate
-
- // DecoderSpecificInfo
- 0x05, // tag, DecoderSpecificInfoTag
- 0x02, // length
- // ISO/IEC 14496-3, AudioSpecificConfig
- // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35
- track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig
- ]));
- };
-
- ftyp = function ftyp() {
- return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND);
- };
-
- hdlr = function hdlr(type) {
- return box(types.hdlr, HDLR_TYPES[type]);
- };
- mdat = function mdat(data) {
- return box(types.mdat, data);
- };
- mdhd = function mdhd(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x02, // creation_time
- 0x00, 0x00, 0x00, 0x03, // modification_time
- 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
-
- track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration
- 0x55, 0xc4, // 'und' language (undetermined)
- 0x00, 0x00]);
-
- // Use the sample rate from the track metadata, when it is
- // defined. The sample rate can be parsed out of an ADTS header, for
- // instance.
- if (track.samplerate) {
- result[12] = track.samplerate >>> 24 & 0xFF;
- result[13] = track.samplerate >>> 16 & 0xFF;
- result[14] = track.samplerate >>> 8 & 0xFF;
- result[15] = track.samplerate & 0xFF;
- }
-
- return box(types.mdhd, result);
- };
- mdia = function mdia(track) {
- return box(types.mdia, mdhd(track), hdlr(track.type), minf(track));
- };
- mfhd = function mfhd(sequenceNumber) {
- return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags
- (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number
- ]));
- };
- minf = function minf(track) {
- return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track));
- };
- moof = function moof(sequenceNumber, tracks) {
- var trackFragments = [],
- i = tracks.length;
- // build traf boxes for each track fragment
- while (i--) {
- trackFragments[i] = traf(tracks[i]);
- }
- return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments));
- };
- /**
- * Returns a movie box.
- * @param tracks {array} the tracks associated with this movie
- * @see ISO/IEC 14496-12:2012(E), section 8.2.1
- */
- moov = function moov(tracks) {
- var i = tracks.length,
- boxes = [];
-
- while (i--) {
- boxes[i] = trak(tracks[i]);
- }
-
- return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks)));
- };
- mvex = function mvex(tracks) {
- var i = tracks.length,
- boxes = [];
-
- while (i--) {
- boxes[i] = trex(tracks[i]);
- }
- return box.apply(null, [types.mvex].concat(boxes));
- };
- mvhd = function mvhd(duration) {
- var bytes = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01, // creation_time
- 0x00, 0x00, 0x00, 0x02, // modification_time
- 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
- (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration
- 0x00, 0x01, 0x00, 0x00, // 1.0 rate
- 0x01, 0x00, // 1.0 volume
- 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
- 0xff, 0xff, 0xff, 0xff // next_track_ID
- ]);
- return box(types.mvhd, bytes);
- };
-
- sdtp = function sdtp(track) {
- var samples = track.samples || [],
- bytes = new Uint8Array(4 + samples.length),
- flags,
- i;
-
- // leave the full box header (4 bytes) all zero
-
- // write the sample table
- for (i = 0; i < samples.length; i++) {
- flags = samples[i].flags;
-
- bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
- }
-
- return box(types.sdtp, bytes);
- };
-
- stbl = function stbl(track) {
- return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO));
- };
-
- (function () {
- var videoSample, audioSample;
-
- stsd = function stsd(track) {
-
- return box(types.stsd, new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track));
- };
-
- videoSample = function videoSample(track) {
- var sps = track.sps || [],
- pps = track.pps || [],
- sequenceParameterSets = [],
- pictureParameterSets = [],
- i;
-
- // assemble the SPSs
- for (i = 0; i < sps.length; i++) {
- sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8);
- sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength
- sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS
- }
-
- // assemble the PPSs
- for (i = 0; i < pps.length; i++) {
- pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8);
- pictureParameterSets.push(pps[i].byteLength & 0xFF);
- pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i]));
- }
-
- return box(types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // data_reference_index
- 0x00, 0x00, // pre_defined
- 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
- (track.width & 0xff00) >> 8, track.width & 0xff, // width
- (track.height & 0xff00) >> 8, track.height & 0xff, // height
- 0x00, 0x48, 0x00, 0x00, // horizresolution
- 0x00, 0x48, 0x00, 0x00, // vertresolution
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // frame_count
- 0x13, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6a, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2d, 0x68, 0x6c, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compressorname
- 0x00, 0x18, // depth = 24
- 0x11, 0x11 // pre_defined = -1
- ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion
- track.profileIdc, // AVCProfileIndication
- track.profileCompatibility, // profile_compatibility
- track.levelIdc, // AVCLevelIndication
- 0xff // lengthSizeMinusOne, hard-coded to 4 bytes
- ].concat([sps.length // numOfSequenceParameterSets
- ]).concat(sequenceParameterSets).concat([pps.length // numOfPictureParameterSets
- ]).concat(pictureParameterSets))), // "PPS"
- box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
- 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
- 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate
- );
- };
-
- audioSample = function audioSample(track) {
- return box(types.mp4a, new Uint8Array([
-
- // SampleEntry, ISO/IEC 14496-12
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x01, // data_reference_index
-
- // AudioSampleEntry, ISO/IEC 14496-12
- 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, 0x00, 0x00, // reserved
- (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount
-
- (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize
- 0x00, 0x00, // pre_defined
- 0x00, 0x00, // reserved
-
- (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16
-
- // MP4AudioSampleEntry, ISO/IEC 14496-14
- ]), esds(track));
- };
- })();
-
- tkhd = function tkhd(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x07, // flags
- 0x00, 0x00, 0x00, 0x00, // creation_time
- 0x00, 0x00, 0x00, 0x00, // modification_time
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x00, // reserved
- (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
- 0x00, 0x00, // layer
- 0x00, 0x00, // alternate_group
- 0x01, 0x00, // non-audio track volume
- 0x00, 0x00, // reserved
- 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
- (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width
- (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height
- ]);
-
- return box(types.tkhd, result);
- };
-
- /**
- * Generate a track fragment (traf) box. A traf box collects metadata
- * about tracks in a movie fragment (moof) box.
- */
- traf = function traf(track) {
- var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime;
-
- trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x3a, // flags
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x01, // sample_description_index
- 0x00, 0x00, 0x00, 0x00, // default_sample_duration
- 0x00, 0x00, 0x00, 0x00, // default_sample_size
- 0x00, 0x00, 0x00, 0x00 // default_sample_flags
- ]));
-
- upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / (UINT32_MAX + 1));
- lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % (UINT32_MAX + 1));
-
- trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1
- 0x00, 0x00, 0x00, // flags
- // baseMediaDecodeTime
- upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF]));
-
- // the data offset specifies the number of bytes from the start of
- // the containing moof to the first payload byte of the associated
- // mdat
- dataOffset = 32 + // tfhd
- 20 + // tfdt
- 8 + // traf header
- 16 + // mfhd
- 8 + // moof header
- 8; // mdat header
-
- // audio tracks require less metadata
- if (track.type === 'audio') {
- trackFragmentRun = trun(track, dataOffset);
- return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun);
- }
-
- // video tracks should contain an independent and disposable samples
- // box (sdtp)
- // generate one and adjust offsets to match
- sampleDependencyTable = sdtp(track);
- trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset);
- return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable);
- };
-
- /**
- * Generate a track box.
- * @param track {object} a track definition
- * @return {Uint8Array} the track box
- */
- trak = function trak(track) {
- track.duration = track.duration || 0xffffffff;
- return box(types.trak, tkhd(track), mdia(track));
- };
-
- trex = function trex(track) {
- var result = new Uint8Array([0x00, // version 0
- 0x00, 0x00, 0x00, // flags
- (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
- 0x00, 0x00, 0x00, 0x01, // default_sample_description_index
- 0x00, 0x00, 0x00, 0x00, // default_sample_duration
- 0x00, 0x00, 0x00, 0x00, // default_sample_size
- 0x00, 0x01, 0x00, 0x01 // default_sample_flags
- ]);
- // the last two bytes of default_sample_flags is the sample
- // degradation priority, a hint about the importance of this sample
- // relative to others. Lower the degradation priority for all sample
- // types other than video.
- if (track.type !== 'video') {
- result[result.length - 1] = 0x00;
- }
-
- return box(types.trex, result);
- };
-
- (function () {
- var audioTrun, videoTrun, trunHeader;
-
- // This method assumes all samples are uniform. That is, if a
- // duration is present for the first sample, it will be present for
- // all subsequent samples.
- // see ISO/IEC 14496-12:2012, Section 8.8.8.1
- trunHeader = function trunHeader(samples, offset) {
- var durationPresent = 0,
- sizePresent = 0,
- flagsPresent = 0,
- compositionTimeOffset = 0;
-
- // trun flag constants
- if (samples.length) {
- if (samples[0].duration !== undefined) {
- durationPresent = 0x1;
- }
- if (samples[0].size !== undefined) {
- sizePresent = 0x2;
- }
- if (samples[0].flags !== undefined) {
- flagsPresent = 0x4;
- }
- if (samples[0].compositionTimeOffset !== undefined) {
- compositionTimeOffset = 0x8;
- }
- }
-
- return [0x00, // version 0
- 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags
- (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count
- (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset
- ];
- };
-
- videoTrun = function videoTrun(track, offset) {
- var bytes, samples, sample, i;
-
- samples = track.samples || [];
- offset += 8 + 12 + 16 * samples.length;
-
- bytes = trunHeader(samples, offset);
-
- for (i = 0; i < samples.length; i++) {
- sample = samples[i];
- bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration
- (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF, // sample_size
- sample.flags.isLeading << 2 | sample.flags.dependsOn, sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample, sample.flags.degradationPriority & 0xF0 << 8, sample.flags.degradationPriority & 0x0F, // sample_flags
- (sample.compositionTimeOffset & 0xFF000000) >>> 24, (sample.compositionTimeOffset & 0xFF0000) >>> 16, (sample.compositionTimeOffset & 0xFF00) >>> 8, sample.compositionTimeOffset & 0xFF // sample_composition_time_offset
- ]);
- }
- return box(types.trun, new Uint8Array(bytes));
- };
-
- audioTrun = function audioTrun(track, offset) {
- var bytes, samples, sample, i;
-
- samples = track.samples || [];
- offset += 8 + 12 + 8 * samples.length;
-
- bytes = trunHeader(samples, offset);
-
- for (i = 0; i < samples.length; i++) {
- sample = samples[i];
- bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration
- (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF]); // sample_size
- }
-
- return box(types.trun, new Uint8Array(bytes));
- };
-
- trun = function trun(track, offset) {
- if (track.type === 'audio') {
- return audioTrun(track, offset);
- }
-
- return videoTrun(track, offset);
- };
- })();
-
- var mp4Generator = {
- ftyp: ftyp,
- mdat: mdat,
- moof: moof,
- moov: moov,
- initSegment: function initSegment(tracks) {
- var fileType = ftyp(),
- movie = moov(tracks),
- result;
-
- result = new Uint8Array(fileType.byteLength + movie.byteLength);
- result.set(fileType);
- result.set(movie, fileType.byteLength);
- return result;
- }
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2014 Brightcove
- * All rights reserved.
- *
- * A lightweight readable stream implemention that handles event dispatching.
- * Objects that inherit from streams should call init in their constructors.
- */
-
- var Stream$2 = function Stream() {
- this.init = function () {
- var listeners = {};
- /**
- * Add a listener for a specified event type.
- * @param type {string} the event name
- * @param listener {function} the callback to be invoked when an event of
- * the specified type occurs
- */
- this.on = function (type, listener) {
- if (!listeners[type]) {
- listeners[type] = [];
- }
- listeners[type] = listeners[type].concat(listener);
- };
- /**
- * Remove a listener for a specified event type.
- * @param type {string} the event name
- * @param listener {function} a function previously registered for this
- * type of event through `on`
- */
- this.off = function (type, listener) {
- var index;
- if (!listeners[type]) {
- return false;
- }
- index = listeners[type].indexOf(listener);
- listeners[type] = listeners[type].slice();
- listeners[type].splice(index, 1);
- return index > -1;
- };
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- * @param type {string} the event name
- */
- this.trigger = function (type) {
- var callbacks, i, length, args;
- callbacks = listeners[type];
- if (!callbacks) {
- return;
- }
- // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
- if (arguments.length === 2) {
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- args = [];
- i = arguments.length;
- for (i = 1; i < arguments.length; ++i) {
- args.push(arguments[i]);
- }
- length = callbacks.length;
- for (i = 0; i < length; ++i) {
- callbacks[i].apply(this, args);
- }
- }
- };
- /**
- * Destroys the stream and cleans up.
- */
- this.dispose = function () {
- listeners = {};
- };
- };
- };
-
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- * @param destination {stream} the stream that will receive all `data` events
- * @param autoFlush {boolean} if false, we will not call `flush` on the destination
- * when the current stream emits a 'done' event
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */
- Stream$2.prototype.pipe = function (destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
-
- this.on('done', function (flushSource) {
- destination.flush(flushSource);
- });
-
- return destination;
- };
-
- // Default stream functions that are expected to be overridden to perform
- // actual work. These are provided by the prototype as a sort of no-op
- // implementation so that we don't have to check for their existence in the
- // `pipe` function above.
- Stream$2.prototype.push = function (data) {
- this.trigger('data', data);
- };
-
- Stream$2.prototype.flush = function (flushSource) {
- this.trigger('done', flushSource);
- };
-
- var stream = Stream$2;
-
- // Convert an array of nal units into an array of frames with each frame being
- // composed of the nal units that make up that frame
- // Also keep track of cummulative data about the frame from the nal units such
- // as the frame duration, starting pts, etc.
- var groupNalsIntoFrames = function groupNalsIntoFrames(nalUnits) {
- var i,
- currentNal,
- currentFrame = [],
- frames = [];
-
- currentFrame.byteLength = 0;
-
- for (i = 0; i < nalUnits.length; i++) {
- currentNal = nalUnits[i];
-
- // Split on 'aud'-type nal units
- if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
- // Since the very first nal unit is expected to be an AUD
- // only push to the frames array when currentFrame is not empty
- if (currentFrame.length) {
- currentFrame.duration = currentNal.dts - currentFrame.dts;
- frames.push(currentFrame);
- }
- currentFrame = [currentNal];
- currentFrame.byteLength = currentNal.data.byteLength;
- currentFrame.pts = currentNal.pts;
- currentFrame.dts = currentNal.dts;
- } else {
- // Specifically flag key frames for ease of use later
- if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
- currentFrame.keyFrame = true;
- }
- currentFrame.duration = currentNal.dts - currentFrame.dts;
- currentFrame.byteLength += currentNal.data.byteLength;
- currentFrame.push(currentNal);
- }
- }
-
- // For the last frame, use the duration of the previous frame if we
- // have nothing better to go on
- if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) {
- currentFrame.duration = frames[frames.length - 1].duration;
- }
-
- // Push the final frame
- frames.push(currentFrame);
- return frames;
- };
-
- // Convert an array of frames into an array of Gop with each Gop being composed
- // of the frames that make up that Gop
- // Also keep track of cummulative data about the Gop from the frames such as the
- // Gop duration, starting pts, etc.
- var groupFramesIntoGops = function groupFramesIntoGops(frames) {
- var i,
- currentFrame,
- currentGop = [],
- gops = [];
-
- // We must pre-set some of the values on the Gop since we
- // keep running totals of these values
- currentGop.byteLength = 0;
- currentGop.nalCount = 0;
- currentGop.duration = 0;
- currentGop.pts = frames[0].pts;
- currentGop.dts = frames[0].dts;
-
- // store some metadata about all the Gops
- gops.byteLength = 0;
- gops.nalCount = 0;
- gops.duration = 0;
- gops.pts = frames[0].pts;
- gops.dts = frames[0].dts;
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
-
- if (currentFrame.keyFrame) {
- // Since the very first frame is expected to be an keyframe
- // only push to the gops array when currentGop is not empty
- if (currentGop.length) {
- gops.push(currentGop);
- gops.byteLength += currentGop.byteLength;
- gops.nalCount += currentGop.nalCount;
- gops.duration += currentGop.duration;
- }
-
- currentGop = [currentFrame];
- currentGop.nalCount = currentFrame.length;
- currentGop.byteLength = currentFrame.byteLength;
- currentGop.pts = currentFrame.pts;
- currentGop.dts = currentFrame.dts;
- currentGop.duration = currentFrame.duration;
- } else {
- currentGop.duration += currentFrame.duration;
- currentGop.nalCount += currentFrame.length;
- currentGop.byteLength += currentFrame.byteLength;
- currentGop.push(currentFrame);
- }
- }
-
- if (gops.length && currentGop.duration <= 0) {
- currentGop.duration = gops[gops.length - 1].duration;
- }
- gops.byteLength += currentGop.byteLength;
- gops.nalCount += currentGop.nalCount;
- gops.duration += currentGop.duration;
-
- // push the final Gop
- gops.push(currentGop);
- return gops;
- };
-
- /*
- * Search for the first keyframe in the GOPs and throw away all frames
- * until that keyframe. Then extend the duration of the pulled keyframe
- * and pull the PTS and DTS of the keyframe so that it covers the time
- * range of the frames that were disposed.
- *
- * @param {Array} gops video GOPs
- * @returns {Array} modified video GOPs
- */
- var extendFirstKeyFrame = function extendFirstKeyFrame(gops) {
- var currentGop;
-
- if (!gops[0][0].keyFrame && gops.length > 1) {
- // Remove the first GOP
- currentGop = gops.shift();
-
- gops.byteLength -= currentGop.byteLength;
- gops.nalCount -= currentGop.nalCount;
-
- // Extend the first frame of what is now the
- // first gop to cover the time period of the
- // frames we just removed
- gops[0][0].dts = currentGop.dts;
- gops[0][0].pts = currentGop.pts;
- gops[0][0].duration += currentGop.duration;
- }
-
- return gops;
- };
-
- /**
- * Default sample object
- * see ISO/IEC 14496-12:2012, section 8.6.4.3
- */
- var createDefaultSample = function createDefaultSample() {
- return {
- size: 0,
- flags: {
- isLeading: 0,
- dependsOn: 1,
- isDependedOn: 0,
- hasRedundancy: 0,
- degradationPriority: 0,
- isNonSyncSample: 1
- }
- };
- };
-
- /*
- * Collates information from a video frame into an object for eventual
- * entry into an MP4 sample table.
- *
- * @param {Object} frame the video frame
- * @param {Number} dataOffset the byte offset to position the sample
- * @return {Object} object containing sample table info for a frame
- */
- var sampleForFrame = function sampleForFrame(frame, dataOffset) {
- var sample = createDefaultSample();
-
- sample.dataOffset = dataOffset;
- sample.compositionTimeOffset = frame.pts - frame.dts;
- sample.duration = frame.duration;
- sample.size = 4 * frame.length; // Space for nal unit size
- sample.size += frame.byteLength;
-
- if (frame.keyFrame) {
- sample.flags.dependsOn = 2;
- sample.flags.isNonSyncSample = 0;
- }
-
- return sample;
- };
-
- // generate the track's sample table from an array of gops
- var generateSampleTable = function generateSampleTable(gops, baseDataOffset) {
- var h,
- i,
- sample,
- currentGop,
- currentFrame,
- dataOffset = baseDataOffset || 0,
- samples = [];
-
- for (h = 0; h < gops.length; h++) {
- currentGop = gops[h];
-
- for (i = 0; i < currentGop.length; i++) {
- currentFrame = currentGop[i];
-
- sample = sampleForFrame(currentFrame, dataOffset);
-
- dataOffset += sample.size;
-
- samples.push(sample);
- }
- }
- return samples;
- };
-
- // generate the track's raw mdat data from an array of gops
- var concatenateNalData = function concatenateNalData(gops) {
- var h,
- i,
- j,
- currentGop,
- currentFrame,
- currentNal,
- dataOffset = 0,
- nalsByteLength = gops.byteLength,
- numberOfNals = gops.nalCount,
- totalByteLength = nalsByteLength + 4 * numberOfNals,
- data = new Uint8Array(totalByteLength),
- view = new DataView(data.buffer);
-
- // For each Gop..
- for (h = 0; h < gops.length; h++) {
- currentGop = gops[h];
-
- // For each Frame..
- for (i = 0; i < currentGop.length; i++) {
- currentFrame = currentGop[i];
-
- // For each NAL..
- for (j = 0; j < currentFrame.length; j++) {
- currentNal = currentFrame[j];
-
- view.setUint32(dataOffset, currentNal.data.byteLength);
- dataOffset += 4;
- data.set(currentNal.data, dataOffset);
- dataOffset += currentNal.data.byteLength;
- }
- }
- }
- return data;
- };
-
- var frameUtils = {
- groupNalsIntoFrames: groupNalsIntoFrames,
- groupFramesIntoGops: groupFramesIntoGops,
- extendFirstKeyFrame: extendFirstKeyFrame,
- generateSampleTable: generateSampleTable,
- concatenateNalData: concatenateNalData
- };
-
- var ONE_SECOND_IN_TS = 90000; // 90kHz clock
-
- /**
- * Store information about the start and end of the track and the
- * duration for each frame/sample we process in order to calculate
- * the baseMediaDecodeTime
- */
- var collectDtsInfo = function collectDtsInfo(track, data) {
- if (typeof data.pts === 'number') {
- if (track.timelineStartInfo.pts === undefined) {
- track.timelineStartInfo.pts = data.pts;
- }
-
- if (track.minSegmentPts === undefined) {
- track.minSegmentPts = data.pts;
- } else {
- track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
- }
-
- if (track.maxSegmentPts === undefined) {
- track.maxSegmentPts = data.pts;
- } else {
- track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
- }
- }
-
- if (typeof data.dts === 'number') {
- if (track.timelineStartInfo.dts === undefined) {
- track.timelineStartInfo.dts = data.dts;
- }
-
- if (track.minSegmentDts === undefined) {
- track.minSegmentDts = data.dts;
- } else {
- track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
- }
-
- if (track.maxSegmentDts === undefined) {
- track.maxSegmentDts = data.dts;
- } else {
- track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
- }
- }
- };
-
- /**
- * Clear values used to calculate the baseMediaDecodeTime between
- * tracks
- */
- var clearDtsInfo = function clearDtsInfo(track) {
- delete track.minSegmentDts;
- delete track.maxSegmentDts;
- delete track.minSegmentPts;
- delete track.maxSegmentPts;
- };
-
- /**
- * Calculate the track's baseMediaDecodeTime based on the earliest
- * DTS the transmuxer has ever seen and the minimum DTS for the
- * current track
- * @param track {object} track metadata configuration
- * @param keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- var calculateTrackBaseMediaDecodeTime = function calculateTrackBaseMediaDecodeTime(track, keepOriginalTimestamps) {
- var baseMediaDecodeTime,
- scale,
- minSegmentDts = track.minSegmentDts;
-
- // Optionally adjust the time so the first segment starts at zero.
- if (!keepOriginalTimestamps) {
- minSegmentDts -= track.timelineStartInfo.dts;
- }
-
- // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
- // we want the start of the first segment to be placed
- baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime;
-
- // Add to that the distance this segment is from the very first
- baseMediaDecodeTime += minSegmentDts;
-
- // baseMediaDecodeTime must not become negative
- baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
-
- if (track.type === 'audio') {
- // Audio has a different clock equal to the sampling_rate so we need to
- // scale the PTS values into the clock rate of the track
- scale = track.samplerate / ONE_SECOND_IN_TS;
- baseMediaDecodeTime *= scale;
- baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
- }
-
- return baseMediaDecodeTime;
- };
-
- var trackDecodeInfo = {
- clearDtsInfo: clearDtsInfo,
- calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
- collectDtsInfo: collectDtsInfo
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2015 Brightcove
- * All rights reserved.
- *
- * Reads in-band caption information from a video elementary
- * stream. Captions must follow the CEA-708 standard for injection
- * into an MPEG-2 transport streams.
- * @see https://en.wikipedia.org/wiki/CEA-708
- * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
- */
-
- // Supplemental enhancement information (SEI) NAL units have a
- // payload type field to indicate how they are to be
- // interpreted. CEAS-708 caption content is always transmitted with
- // payload type 0x04.
-
- var USER_DATA_REGISTERED_ITU_T_T35 = 4,
- RBSP_TRAILING_BITS = 128;
-
- /**
- * Parse a supplemental enhancement information (SEI) NAL unit.
- * Stops parsing once a message of type ITU T T35 has been found.
- *
- * @param bytes {Uint8Array} the bytes of a SEI NAL unit
- * @return {object} the parsed SEI payload
- * @see Rec. ITU-T H.264, 7.3.2.3.1
- */
- var parseSei = function parseSei(bytes) {
- var i = 0,
- result = {
- payloadType: -1,
- payloadSize: 0
- },
- payloadType = 0,
- payloadSize = 0;
-
- // go through the sei_rbsp parsing each each individual sei_message
- while (i < bytes.byteLength) {
- // stop once we have hit the end of the sei_rbsp
- if (bytes[i] === RBSP_TRAILING_BITS) {
- break;
- }
-
- // Parse payload type
- while (bytes[i] === 0xFF) {
- payloadType += 255;
- i++;
- }
- payloadType += bytes[i++];
-
- // Parse payload size
- while (bytes[i] === 0xFF) {
- payloadSize += 255;
- i++;
- }
- payloadSize += bytes[i++];
-
- // this sei_message is a 608/708 caption so save it and break
- // there can only ever be one caption message in a frame's sei
- if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) {
- result.payloadType = payloadType;
- result.payloadSize = payloadSize;
- result.payload = bytes.subarray(i, i + payloadSize);
- break;
- }
-
- // skip the payload and parse the next message
- i += payloadSize;
- payloadType = 0;
- payloadSize = 0;
- }
-
- return result;
- };
-
- // see ANSI/SCTE 128-1 (2013), section 8.1
- var parseUserData = function parseUserData(sei) {
- // itu_t_t35_contry_code must be 181 (United States) for
- // captions
- if (sei.payload[0] !== 181) {
- return null;
- }
-
- // itu_t_t35_provider_code should be 49 (ATSC) for captions
- if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) {
- return null;
- }
-
- // the user_identifier should be "GA94" to indicate ATSC1 data
- if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') {
- return null;
- }
-
- // finally, user_data_type_code should be 0x03 for caption data
- if (sei.payload[7] !== 0x03) {
- return null;
- }
-
- // return the user_data_type_structure and strip the trailing
- // marker bits
- return sei.payload.subarray(8, sei.payload.length - 1);
- };
-
- // see CEA-708-D, section 4.4
- var parseCaptionPackets = function parseCaptionPackets(pts, userData) {
- var results = [],
- i,
- count,
- offset,
- data;
-
- // if this is just filler, return immediately
- if (!(userData[0] & 0x40)) {
- return results;
- }
-
- // parse out the cc_data_1 and cc_data_2 fields
- count = userData[0] & 0x1f;
- for (i = 0; i < count; i++) {
- offset = i * 3;
- data = {
- type: userData[offset + 2] & 0x03,
- pts: pts
- };
-
- // capture cc data when cc_valid is 1
- if (userData[offset + 2] & 0x04) {
- data.ccData = userData[offset + 3] << 8 | userData[offset + 4];
- results.push(data);
- }
- }
- return results;
- };
-
- var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) {
- var length = data.byteLength,
- emulationPreventionBytesPositions = [],
- i = 1,
- newLength,
- newData;
-
- // Find all `Emulation Prevention Bytes`
- while (i < length - 2) {
- if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
- emulationPreventionBytesPositions.push(i + 2);
- i += 2;
- } else {
- i++;
- }
- }
-
- // If no Emulation Prevention Bytes were found just return the original
- // array
- if (emulationPreventionBytesPositions.length === 0) {
- return data;
- }
-
- // Create a new array to hold the NAL unit data
- newLength = length - emulationPreventionBytesPositions.length;
- newData = new Uint8Array(newLength);
- var sourceIndex = 0;
-
- for (i = 0; i < newLength; sourceIndex++, i++) {
- if (sourceIndex === emulationPreventionBytesPositions[0]) {
- // Skip this byte
- sourceIndex++;
- // Remove this position index
- emulationPreventionBytesPositions.shift();
- }
- newData[i] = data[sourceIndex];
- }
-
- return newData;
- };
-
- // exports
- var captionPacketParser = {
- parseSei: parseSei,
- parseUserData: parseUserData,
- parseCaptionPackets: parseCaptionPackets,
- discardEmulationPreventionBytes: discardEmulationPreventionBytes,
- USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35
- };
-
- // -----------------
- // Link To Transport
- // -----------------
-
-
- var CaptionStream = function CaptionStream() {
-
- CaptionStream.prototype.init.call(this);
-
- this.captionPackets_ = [];
-
- this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
- ];
-
- this.reset();
-
- // forward data and done events from CCs to this CaptionStream
- this.ccStreams_.forEach(function (cc) {
- cc.on('data', this.trigger.bind(this, 'data'));
- cc.on('done', this.trigger.bind(this, 'done'));
- }, this);
- };
-
- CaptionStream.prototype = new stream();
- CaptionStream.prototype.push = function (event) {
- var sei, userData, newCaptionPackets;
-
- // only examine SEI NALs
- if (event.nalUnitType !== 'sei_rbsp') {
- return;
- }
-
- // parse the sei
- sei = captionPacketParser.parseSei(event.escapedRBSP);
-
- // ignore everything but user_data_registered_itu_t_t35
- if (sei.payloadType !== captionPacketParser.USER_DATA_REGISTERED_ITU_T_T35) {
- return;
- }
-
- // parse out the user data payload
- userData = captionPacketParser.parseUserData(sei);
-
- // ignore unrecognized userData
- if (!userData) {
- return;
- }
-
- // Sometimes, the same segment # will be downloaded twice. To stop the
- // caption data from being processed twice, we track the latest dts we've
- // received and ignore everything with a dts before that. However, since
- // data for a specific dts can be split across packets on either side of
- // a segment boundary, we need to make sure we *don't* ignore the packets
- // from the *next* segment that have dts === this.latestDts_. By constantly
- // tracking the number of packets received with dts === this.latestDts_, we
- // know how many should be ignored once we start receiving duplicates.
- if (event.dts < this.latestDts_) {
- // We've started getting older data, so set the flag.
- this.ignoreNextEqualDts_ = true;
- return;
- } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) {
- this.numSameDts_--;
- if (!this.numSameDts_) {
- // We've received the last duplicate packet, time to start processing again
- this.ignoreNextEqualDts_ = false;
- }
- return;
- }
-
- // parse out CC data packets and save them for later
- newCaptionPackets = captionPacketParser.parseCaptionPackets(event.pts, userData);
- this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets);
- if (this.latestDts_ !== event.dts) {
- this.numSameDts_ = 0;
- }
- this.numSameDts_++;
- this.latestDts_ = event.dts;
- };
-
- CaptionStream.prototype.flush = function () {
- // make sure we actually parsed captions before proceeding
- if (!this.captionPackets_.length) {
- this.ccStreams_.forEach(function (cc) {
- cc.flush();
- }, this);
- return;
- }
-
- // In Chrome, the Array#sort function is not stable so add a
- // presortIndex that we can use to ensure we get a stable-sort
- this.captionPackets_.forEach(function (elem, idx) {
- elem.presortIndex = idx;
- });
-
- // sort caption byte-pairs based on their PTS values
- this.captionPackets_.sort(function (a, b) {
- if (a.pts === b.pts) {
- return a.presortIndex - b.presortIndex;
- }
- return a.pts - b.pts;
- });
-
- this.captionPackets_.forEach(function (packet) {
- if (packet.type < 2) {
- // Dispatch packet to the right Cea608Stream
- this.dispatchCea608Packet(packet);
- }
- // this is where an 'else' would go for a dispatching packets
- // to a theoretical Cea708Stream that handles SERVICEn data
- }, this);
-
- this.captionPackets_.length = 0;
- this.ccStreams_.forEach(function (cc) {
- cc.flush();
- }, this);
- return;
- };
-
- CaptionStream.prototype.reset = function () {
- this.latestDts_ = null;
- this.ignoreNextEqualDts_ = false;
- this.numSameDts_ = 0;
- this.activeCea608Channel_ = [null, null];
- this.ccStreams_.forEach(function (ccStream) {
- ccStream.reset();
- });
- };
-
- CaptionStream.prototype.dispatchCea608Packet = function (packet) {
- // NOTE: packet.type is the CEA608 field
- if (this.setsChannel1Active(packet)) {
- this.activeCea608Channel_[packet.type] = 0;
- } else if (this.setsChannel2Active(packet)) {
- this.activeCea608Channel_[packet.type] = 1;
- }
- if (this.activeCea608Channel_[packet.type] === null) {
- // If we haven't received anything to set the active channel, discard the
- // data; we don't want jumbled captions
- return;
- }
- this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
- };
-
- CaptionStream.prototype.setsChannel1Active = function (packet) {
- return (packet.ccData & 0x7800) === 0x1000;
- };
- CaptionStream.prototype.setsChannel2Active = function (packet) {
- return (packet.ccData & 0x7800) === 0x1800;
- };
-
- // ----------------------
- // Session to Application
- // ----------------------
-
- // This hash maps non-ASCII, special, and extended character codes to their
- // proper Unicode equivalent. The first keys that are only a single byte
- // are the non-standard ASCII characters, which simply map the CEA608 byte
- // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
- // character codes, but have their MSB bitmasked with 0x03 so that a lookup
- // can be performed regardless of the field and data channel on which the
- // character code was received.
- var CHARACTER_TRANSLATION = {
- 0x2a: 0xe1, // á
- 0x5c: 0xe9, // é
- 0x5e: 0xed, // í
- 0x5f: 0xf3, // ó
- 0x60: 0xfa, // ú
- 0x7b: 0xe7, // ç
- 0x7c: 0xf7, // ÷
- 0x7d: 0xd1, // Ñ
- 0x7e: 0xf1, // ñ
- 0x7f: 0x2588, // █
- 0x0130: 0xae, // ®
- 0x0131: 0xb0, // °
- 0x0132: 0xbd, // ½
- 0x0133: 0xbf, // ¿
- 0x0134: 0x2122, // ™
- 0x0135: 0xa2, // ¢
- 0x0136: 0xa3, // £
- 0x0137: 0x266a, // ♪
- 0x0138: 0xe0, // à
- 0x0139: 0xa0, //
- 0x013a: 0xe8, // è
- 0x013b: 0xe2, // â
- 0x013c: 0xea, // ê
- 0x013d: 0xee, // î
- 0x013e: 0xf4, // ô
- 0x013f: 0xfb, // û
- 0x0220: 0xc1, // Á
- 0x0221: 0xc9, // É
- 0x0222: 0xd3, // Ó
- 0x0223: 0xda, // Ú
- 0x0224: 0xdc, // Ü
- 0x0225: 0xfc, // ü
- 0x0226: 0x2018, // ‘
- 0x0227: 0xa1, // ¡
- 0x0228: 0x2a, // *
- 0x0229: 0x27, // '
- 0x022a: 0x2014, // —
- 0x022b: 0xa9, // ©
- 0x022c: 0x2120, // ℠
- 0x022d: 0x2022, // •
- 0x022e: 0x201c, // “
- 0x022f: 0x201d, // ”
- 0x0230: 0xc0, // À
- 0x0231: 0xc2, // Â
- 0x0232: 0xc7, // Ç
- 0x0233: 0xc8, // È
- 0x0234: 0xca, // Ê
- 0x0235: 0xcb, // Ë
- 0x0236: 0xeb, // ë
- 0x0237: 0xce, // Î
- 0x0238: 0xcf, // Ï
- 0x0239: 0xef, // ï
- 0x023a: 0xd4, // Ô
- 0x023b: 0xd9, // Ù
- 0x023c: 0xf9, // ù
- 0x023d: 0xdb, // Û
- 0x023e: 0xab, // «
- 0x023f: 0xbb, // »
- 0x0320: 0xc3, // Ã
- 0x0321: 0xe3, // ã
- 0x0322: 0xcd, // Í
- 0x0323: 0xcc, // Ì
- 0x0324: 0xec, // ì
- 0x0325: 0xd2, // Ò
- 0x0326: 0xf2, // ò
- 0x0327: 0xd5, // Õ
- 0x0328: 0xf5, // õ
- 0x0329: 0x7b, // {
- 0x032a: 0x7d, // }
- 0x032b: 0x5c, // \
- 0x032c: 0x5e, // ^
- 0x032d: 0x5f, // _
- 0x032e: 0x7c, // |
- 0x032f: 0x7e, // ~
- 0x0330: 0xc4, // Ä
- 0x0331: 0xe4, // ä
- 0x0332: 0xd6, // Ö
- 0x0333: 0xf6, // ö
- 0x0334: 0xdf, // ß
- 0x0335: 0xa5, // ¥
- 0x0336: 0xa4, // ¤
- 0x0337: 0x2502, // │
- 0x0338: 0xc5, // Å
- 0x0339: 0xe5, // å
- 0x033a: 0xd8, // Ø
- 0x033b: 0xf8, // ø
- 0x033c: 0x250c, // ┌
- 0x033d: 0x2510, // ┐
- 0x033e: 0x2514, // └
- 0x033f: 0x2518 // ┘
- };
-
- var getCharFromCode = function getCharFromCode(code) {
- if (code === null) {
- return '';
- }
- code = CHARACTER_TRANSLATION[code] || code;
- return String.fromCharCode(code);
- };
-
- // the index of the last row in a CEA-608 display buffer
- var BOTTOM_ROW = 14;
-
- // This array is used for mapping PACs -> row #, since there's no way of
- // getting it through bit logic.
- var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420];
-
- // CEA-608 captions are rendered onto a 34x15 matrix of character
- // cells. The "bottom" row is the last element in the outer array.
- var createDisplayBuffer = function createDisplayBuffer() {
- var result = [],
- i = BOTTOM_ROW + 1;
- while (i--) {
- result.push('');
- }
- return result;
- };
-
- var Cea608Stream = function Cea608Stream(field, dataChannel) {
- Cea608Stream.prototype.init.call(this);
-
- this.field_ = field || 0;
- this.dataChannel_ = dataChannel || 0;
-
- this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1);
-
- this.setConstants();
- this.reset();
-
- this.push = function (packet) {
- var data, swap, char0, char1, text;
- // remove the parity bits
- data = packet.ccData & 0x7f7f;
-
- // ignore duplicate control codes; the spec demands they're sent twice
- if (data === this.lastControlCode_) {
- this.lastControlCode_ = null;
- return;
- }
-
- // Store control codes
- if ((data & 0xf000) === 0x1000) {
- this.lastControlCode_ = data;
- } else if (data !== this.PADDING_) {
- this.lastControlCode_ = null;
- }
-
- char0 = data >>> 8;
- char1 = data & 0xff;
-
- if (data === this.PADDING_) {
- return;
- } else if (data === this.RESUME_CAPTION_LOADING_) {
- this.mode_ = 'popOn';
- } else if (data === this.END_OF_CAPTION_) {
- // If an EOC is received while in paint-on mode, the displayed caption
- // text should be swapped to non-displayed memory as if it was a pop-on
- // caption. Because of that, we should explicitly switch back to pop-on
- // mode
- this.mode_ = 'popOn';
- this.clearFormatting(packet.pts);
- // if a caption was being displayed, it's gone now
- this.flushDisplayed(packet.pts);
-
- // flip memory
- swap = this.displayed_;
- this.displayed_ = this.nonDisplayed_;
- this.nonDisplayed_ = swap;
-
- // start measuring the time to display the caption
- this.startPts_ = packet.pts;
- } else if (data === this.ROLL_UP_2_ROWS_) {
- this.rollUpRows_ = 2;
- this.setRollUp(packet.pts);
- } else if (data === this.ROLL_UP_3_ROWS_) {
- this.rollUpRows_ = 3;
- this.setRollUp(packet.pts);
- } else if (data === this.ROLL_UP_4_ROWS_) {
- this.rollUpRows_ = 4;
- this.setRollUp(packet.pts);
- } else if (data === this.CARRIAGE_RETURN_) {
- this.clearFormatting(packet.pts);
- this.flushDisplayed(packet.pts);
- this.shiftRowsUp_();
- this.startPts_ = packet.pts;
- } else if (data === this.BACKSPACE_) {
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
- } else {
- this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
- }
- } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
- this.flushDisplayed(packet.pts);
- this.displayed_ = createDisplayBuffer();
- } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
- this.nonDisplayed_ = createDisplayBuffer();
- } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
- if (this.mode_ !== 'paintOn') {
- // NOTE: This should be removed when proper caption positioning is
- // implemented
- this.flushDisplayed(packet.pts);
- this.displayed_ = createDisplayBuffer();
- }
- this.mode_ = 'paintOn';
- this.startPts_ = packet.pts;
-
- // Append special characters to caption text
- } else if (this.isSpecialCharacter(char0, char1)) {
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
-
- // Append extended characters to caption text
- } else if (this.isExtCharacter(char0, char1)) {
- // Extended characters always follow their "non-extended" equivalents.
- // IE if a "è" is desired, you'll always receive "eè"; non-compliant
- // decoders are supposed to drop the "è", while compliant decoders
- // backspace the "e" and insert "è".
-
- // Delete the previous character
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
- } else {
- this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
- }
-
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
-
- // Process mid-row codes
- } else if (this.isMidRowCode(char0, char1)) {
- // Attributes are not additive, so clear all formatting
- this.clearFormatting(packet.pts);
-
- // According to the standard, mid-row codes
- // should be replaced with spaces, so add one now
- this[this.mode_](packet.pts, ' ');
- this.column_++;
-
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
-
- if ((char1 & 0x1) === 0x1) {
- this.addFormatting(packet.pts, ['u']);
- }
-
- // Detect offset control codes and adjust cursor
- } else if (this.isOffsetControlCode(char0, char1)) {
- // Cursor position is set by indent PAC (see below) in 4-column
- // increments, with an additional offset code of 1-3 to reach any
- // of the 32 columns specified by CEA-608. So all we need to do
- // here is increment the column cursor by the given offset.
- this.column_ += char1 & 0x03;
-
- // Detect PACs (Preamble Address Codes)
- } else if (this.isPAC(char0, char1)) {
-
- // There's no logic for PAC -> row mapping, so we have to just
- // find the row code in an array and use its index :(
- var row = ROWS.indexOf(data & 0x1f20);
-
- // Configure the caption window if we're in roll-up mode
- if (this.mode_ === 'rollUp') {
- this.setRollUp(packet.pts, row);
- }
-
- if (row !== this.row_) {
- // formatting is only persistent for current row
- this.clearFormatting(packet.pts);
- this.row_ = row;
- }
- // All PACs can apply underline, so detect and apply
- // (All odd-numbered second bytes set underline)
- if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) {
- this.addFormatting(packet.pts, ['u']);
- }
-
- if ((data & 0x10) === 0x10) {
- // We've got an indent level code. Each successive even number
- // increments the column cursor by 4, so we can get the desired
- // column position by bit-shifting to the right (to get n/2)
- // and multiplying by 4.
- this.column_ = ((data & 0xe) >> 1) * 4;
- }
-
- if (this.isColorPAC(char1)) {
- // it's a color code, though we only support white, which
- // can be either normal or italicized. white italics can be
- // either 0x4e or 0x6e depending on the row, so we just
- // bitwise-and with 0xe to see if italics should be turned on
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
- }
-
- // We have a normal character in char0, and possibly one in char1
- } else if (this.isNormalChar(char0)) {
- if (char1 === 0x00) {
- char1 = null;
- }
- text = getCharFromCode(char0);
- text += getCharFromCode(char1);
- this[this.mode_](packet.pts, text);
- this.column_ += text.length;
- } // finish data processing
- };
- };
- Cea608Stream.prototype = new stream();
- // Trigger a cue point that captures the current state of the
- // display buffer
- Cea608Stream.prototype.flushDisplayed = function (pts) {
- var content = this.displayed_
- // remove spaces from the start and end of the string
- .map(function (row) {
- return row.trim();
- })
- // combine all text rows to display in one cue
- .join('\n')
- // and remove blank rows from the start and end, but not the middle
- .replace(/^\n+|\n+$/g, '');
-
- if (content.length) {
- this.trigger('data', {
- startPts: this.startPts_,
- endPts: pts,
- text: content,
- stream: this.name_
- });
- }
- };
-
- /**
- * Zero out the data, used for startup and on seek
- */
- Cea608Stream.prototype.reset = function () {
- this.mode_ = 'popOn';
- // When in roll-up mode, the index of the last row that will
- // actually display captions. If a caption is shifted to a row
- // with a lower index than this, it is cleared from the display
- // buffer
- this.topRow_ = 0;
- this.startPts_ = 0;
- this.displayed_ = createDisplayBuffer();
- this.nonDisplayed_ = createDisplayBuffer();
- this.lastControlCode_ = null;
-
- // Track row and column for proper line-breaking and spacing
- this.column_ = 0;
- this.row_ = BOTTOM_ROW;
- this.rollUpRows_ = 2;
-
- // This variable holds currently-applied formatting
- this.formatting_ = [];
- };
-
- /**
- * Sets up control code and related constants for this instance
- */
- Cea608Stream.prototype.setConstants = function () {
- // The following attributes have these uses:
- // ext_ : char0 for mid-row codes, and the base for extended
- // chars (ext_+0, ext_+1, and ext_+2 are char0s for
- // extended codes)
- // control_: char0 for control codes, except byte-shifted to the
- // left so that we can do this.control_ | CONTROL_CODE
- // offset_: char0 for tab offset codes
- //
- // It's also worth noting that control codes, and _only_ control codes,
- // differ between field 1 and field2. Field 2 control codes are always
- // their field 1 value plus 1. That's why there's the "| field" on the
- // control value.
- if (this.dataChannel_ === 0) {
- this.BASE_ = 0x10;
- this.EXT_ = 0x11;
- this.CONTROL_ = (0x14 | this.field_) << 8;
- this.OFFSET_ = 0x17;
- } else if (this.dataChannel_ === 1) {
- this.BASE_ = 0x18;
- this.EXT_ = 0x19;
- this.CONTROL_ = (0x1c | this.field_) << 8;
- this.OFFSET_ = 0x1f;
- }
-
- // Constants for the LSByte command codes recognized by Cea608Stream. This
- // list is not exhaustive. For a more comprehensive listing and semantics see
- // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
- // Padding
- this.PADDING_ = 0x0000;
- // Pop-on Mode
- this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
- this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f;
- // Roll-up Mode
- this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
- this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
- this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
- this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d;
- // paint-on mode
- this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29;
- // Erasure
- this.BACKSPACE_ = this.CONTROL_ | 0x21;
- this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
- this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
- };
-
- /**
- * Detects if the 2-byte packet data is a special character
- *
- * Special characters have a second byte in the range 0x30 to 0x3f,
- * with the first byte being 0x11 (for data channel 1) or 0x19 (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an special character
- */
- Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) {
- return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f;
- };
-
- /**
- * Detects if the 2-byte packet data is an extended character
- *
- * Extended characters have a second byte in the range 0x20 to 0x3f,
- * with the first byte being 0x12 or 0x13 (for data channel 1) or
- * 0x1a or 0x1b (for data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an extended character
- */
- Cea608Stream.prototype.isExtCharacter = function (char0, char1) {
- return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f;
- };
-
- /**
- * Detects if the 2-byte packet is a mid-row code
- *
- * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
- * the first byte being 0x11 (for data channel 1) or 0x19 (for data
- * channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a mid-row code
- */
- Cea608Stream.prototype.isMidRowCode = function (char0, char1) {
- return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f;
- };
-
- /**
- * Detects if the 2-byte packet is an offset control code
- *
- * Offset control codes have a second byte in the range 0x21 to 0x23,
- * with the first byte being 0x17 (for data channel 1) or 0x1f (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an offset control code
- */
- Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) {
- return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23;
- };
-
- /**
- * Detects if the 2-byte packet is a Preamble Address Code
- *
- * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
- * or 0x18 to 0x1f (for data channel 2), with the second byte in the
- * range 0x40 to 0x7f.
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a PAC
- */
- Cea608Stream.prototype.isPAC = function (char0, char1) {
- return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f;
- };
-
- /**
- * Detects if a packet's second byte is in the range of a PAC color code
- *
- * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
- * 0x60 to 0x6f.
- *
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the byte is a color PAC
- */
- Cea608Stream.prototype.isColorPAC = function (char1) {
- return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f;
- };
-
- /**
- * Detects if a single byte is in the range of a normal character
- *
- * Normal text bytes are in the range 0x20 to 0x7f.
- *
- * @param {Integer} char The byte
- * @return {Boolean} Whether the byte is a normal character
- */
- Cea608Stream.prototype.isNormalChar = function (char) {
- return char >= 0x20 && char <= 0x7f;
- };
-
- /**
- * Configures roll-up
- *
- * @param {Integer} pts Current PTS
- * @param {Integer} newBaseRow Used by PACs to slide the current window to
- * a new position
- */
- Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) {
- // Reset the base row to the bottom row when switching modes
- if (this.mode_ !== 'rollUp') {
- this.row_ = BOTTOM_ROW;
- this.mode_ = 'rollUp';
- // Spec says to wipe memories when switching to roll-up
- this.flushDisplayed(pts);
- this.nonDisplayed_ = createDisplayBuffer();
- this.displayed_ = createDisplayBuffer();
- }
-
- if (newBaseRow !== undefined && newBaseRow !== this.row_) {
- // move currently displayed captions (up or down) to the new base row
- for (var i = 0; i < this.rollUpRows_; i++) {
- this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
- this.displayed_[this.row_ - i] = '';
- }
- }
-
- if (newBaseRow === undefined) {
- newBaseRow = this.row_;
- }
- this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
- };
-
- // Adds the opening HTML tag for the passed character to the caption text,
- // and keeps track of it for later closing
- Cea608Stream.prototype.addFormatting = function (pts, format) {
- this.formatting_ = this.formatting_.concat(format);
- var text = format.reduce(function (text, format) {
- return text + '<' + format + '>';
- }, '');
- this[this.mode_](pts, text);
- };
-
- // Adds HTML closing tags for current formatting to caption text and
- // clears remembered formatting
- Cea608Stream.prototype.clearFormatting = function (pts) {
- if (!this.formatting_.length) {
- return;
- }
- var text = this.formatting_.reverse().reduce(function (text, format) {
- return text + '</' + format + '>';
- }, '');
- this.formatting_ = [];
- this[this.mode_](pts, text);
- };
-
- // Mode Implementations
- Cea608Stream.prototype.popOn = function (pts, text) {
- var baseRow = this.nonDisplayed_[this.row_];
-
- // buffer characters
- baseRow += text;
- this.nonDisplayed_[this.row_] = baseRow;
- };
-
- Cea608Stream.prototype.rollUp = function (pts, text) {
- var baseRow = this.displayed_[this.row_];
-
- baseRow += text;
- this.displayed_[this.row_] = baseRow;
- };
-
- Cea608Stream.prototype.shiftRowsUp_ = function () {
- var i;
- // clear out inactive rows
- for (i = 0; i < this.topRow_; i++) {
- this.displayed_[i] = '';
- }
- for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
- this.displayed_[i] = '';
- }
- // shift displayed rows up
- for (i = this.topRow_; i < this.row_; i++) {
- this.displayed_[i] = this.displayed_[i + 1];
- }
- // clear out the bottom row
- this.displayed_[this.row_] = '';
- };
-
- Cea608Stream.prototype.paintOn = function (pts, text) {
- var baseRow = this.displayed_[this.row_];
-
- baseRow += text;
- this.displayed_[this.row_] = baseRow;
- };
-
- // exports
- var captionStream = {
- CaptionStream: CaptionStream,
- Cea608Stream: Cea608Stream
- };
-
- var streamTypes = {
- H264_STREAM_TYPE: 0x1B,
- ADTS_STREAM_TYPE: 0x0F,
- METADATA_STREAM_TYPE: 0x15
- };
-
- var MAX_TS = 8589934592;
-
- var RO_THRESH = 4294967296;
-
- var handleRollover = function handleRollover(value, reference) {
- var direction = 1;
-
- if (value > reference) {
- // If the current timestamp value is greater than our reference timestamp and we detect a
- // timestamp rollover, this means the roll over is happening in the opposite direction.
- // Example scenario: Enter a long stream/video just after a rollover occurred. The reference
- // point will be set to a small number, e.g. 1. The user then seeks backwards over the
- // rollover point. In loading this segment, the timestamp values will be very large,
- // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust
- // the time stamp to be `value - 2^33`.
- direction = -1;
- }
-
- // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will
- // cause an incorrect adjustment.
- while (Math.abs(reference - value) > RO_THRESH) {
- value += direction * MAX_TS;
- }
-
- return value;
- };
-
- var TimestampRolloverStream = function TimestampRolloverStream(type) {
- var lastDTS, referenceDTS;
-
- TimestampRolloverStream.prototype.init.call(this);
-
- this.type_ = type;
-
- this.push = function (data) {
- if (data.type !== this.type_) {
- return;
- }
-
- if (referenceDTS === undefined) {
- referenceDTS = data.dts;
- }
-
- data.dts = handleRollover(data.dts, referenceDTS);
- data.pts = handleRollover(data.pts, referenceDTS);
-
- lastDTS = data.dts;
-
- this.trigger('data', data);
- };
-
- this.flush = function () {
- referenceDTS = lastDTS;
- this.trigger('done');
- };
-
- this.discontinuity = function () {
- referenceDTS = void 0;
- lastDTS = void 0;
- };
- };
-
- TimestampRolloverStream.prototype = new stream();
-
- var timestampRolloverStream = {
- TimestampRolloverStream: TimestampRolloverStream,
- handleRollover: handleRollover
- };
-
- var percentEncode = function percentEncode(bytes, start, end) {
- var i,
- result = '';
- for (i = start; i < end; i++) {
- result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
- }
- return result;
- },
-
- // return the string representation of the specified byte range,
- // interpreted as UTf-8.
- parseUtf8 = function parseUtf8(bytes, start, end) {
- return decodeURIComponent(percentEncode(bytes, start, end));
- },
-
- // return the string representation of the specified byte range,
- // interpreted as ISO-8859-1.
- parseIso88591 = function parseIso88591(bytes, start, end) {
- return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
- },
- parseSyncSafeInteger = function parseSyncSafeInteger(data) {
- return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
- },
- tagParsers = {
- TXXX: function TXXX(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
-
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the text fields
- tag.description = parseUtf8(tag.data, 1, i);
- // do not include the null terminator in the tag value
- tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, '');
- break;
- }
- }
- tag.data = tag.value;
- },
- WXXX: function WXXX(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
-
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.description = parseUtf8(tag.data, 1, i);
- tag.url = parseUtf8(tag.data, i + 1, tag.data.length);
- break;
- }
- }
- },
- PRIV: function PRIV(tag) {
- var i;
-
- for (i = 0; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.owner = parseIso88591(tag.data, 0, i);
- break;
- }
- }
- tag.privateData = tag.data.subarray(i + 1);
- tag.data = tag.privateData;
- }
- },
- _MetadataStream;
-
- _MetadataStream = function MetadataStream(options) {
- var settings = {
- debug: !!(options && options.debug),
-
- // the bytes of the program-level descriptor field in MP2T
- // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
- // program element descriptors"
- descriptor: options && options.descriptor
- },
-
- // the total size in bytes of the ID3 tag being parsed
- tagSize = 0,
-
- // tag data that is not complete enough to be parsed
- buffer = [],
-
- // the total number of bytes currently in the buffer
- bufferSize = 0,
- i;
-
- _MetadataStream.prototype.init.call(this);
-
- // calculate the text track in-band metadata track dispatch type
- // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
- this.dispatchType = streamTypes.METADATA_STREAM_TYPE.toString(16);
- if (settings.descriptor) {
- for (i = 0; i < settings.descriptor.length; i++) {
- this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
- }
- }
-
- this.push = function (chunk) {
- var tag, frameStart, frameSize, frame, i, frameHeader;
- if (chunk.type !== 'timed-metadata') {
- return;
- }
-
- // if data_alignment_indicator is set in the PES header,
- // we must have the start of a new ID3 tag. Assume anything
- // remaining in the buffer was malformed and throw it out
- if (chunk.dataAlignmentIndicator) {
- bufferSize = 0;
- buffer.length = 0;
- }
-
- // ignore events that don't look like ID3 data
- if (buffer.length === 0 && (chunk.data.length < 10 || chunk.data[0] !== 'I'.charCodeAt(0) || chunk.data[1] !== 'D'.charCodeAt(0) || chunk.data[2] !== '3'.charCodeAt(0))) {
- if (settings.debug) {
- // eslint-disable-next-line no-console
- console.log('Skipping unrecognized metadata packet');
- }
- return;
- }
-
- // add this chunk to the data we've collected so far
-
- buffer.push(chunk);
- bufferSize += chunk.data.byteLength;
-
- // grab the size of the entire frame from the ID3 header
- if (buffer.length === 1) {
- // the frame size is transmitted as a 28-bit integer in the
- // last four bytes of the ID3 header.
- // The most significant bit of each byte is dropped and the
- // results concatenated to recover the actual value.
- tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10));
-
- // ID3 reports the tag size excluding the header but it's more
- // convenient for our comparisons to include it
- tagSize += 10;
- }
-
- // if the entire frame has not arrived, wait for more data
- if (bufferSize < tagSize) {
- return;
- }
-
- // collect the entire frame so it can be parsed
- tag = {
- data: new Uint8Array(tagSize),
- frames: [],
- pts: buffer[0].pts,
- dts: buffer[0].dts
- };
- for (i = 0; i < tagSize;) {
- tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
- i += buffer[0].data.byteLength;
- bufferSize -= buffer[0].data.byteLength;
- buffer.shift();
- }
-
- // find the start of the first frame and the end of the tag
- frameStart = 10;
- if (tag.data[5] & 0x40) {
- // advance the frame start past the extended header
- frameStart += 4; // header size field
- frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14));
-
- // clip any padding off the end
- tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20));
- }
-
- // parse one or more ID3 frames
- // http://id3.org/id3v2.3.0#ID3v2_frame_overview
- do {
- // determine the number of bytes in this frame
- frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
- if (frameSize < 1) {
- // eslint-disable-next-line no-console
- return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
- }
- frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]);
-
- frame = {
- id: frameHeader,
- data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
- };
- frame.key = frame.id;
- if (tagParsers[frame.id]) {
- tagParsers[frame.id](frame);
-
- // handle the special PRIV frame used to indicate the start
- // time for raw AAC data
- if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
- var d = frame.data,
- size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
-
- size *= 4;
- size += d[7] & 0x03;
- frame.timeStamp = size;
- // in raw AAC, all subsequent data will be timestamped based
- // on the value of this frame
- // we couldn't have known the appropriate pts and dts before
- // parsing this ID3 tag so set those values now
- if (tag.pts === undefined && tag.dts === undefined) {
- tag.pts = frame.timeStamp;
- tag.dts = frame.timeStamp;
- }
- this.trigger('timestamp', frame);
- }
- }
- tag.frames.push(frame);
-
- frameStart += 10; // advance past the frame header
- frameStart += frameSize; // advance past the frame body
- } while (frameStart < tagSize);
- this.trigger('data', tag);
- };
- };
- _MetadataStream.prototype = new stream();
-
- var metadataStream = _MetadataStream;
-
- var TimestampRolloverStream$1 = timestampRolloverStream.TimestampRolloverStream;
-
- // object types
- var _TransportPacketStream, _TransportParseStream, _ElementaryStream;
-
- // constants
- var MP2T_PACKET_LENGTH = 188,
- // bytes
- SYNC_BYTE = 0x47;
-
- /**
- * Splits an incoming stream of binary data into MPEG-2 Transport
- * Stream packets.
- */
- _TransportPacketStream = function TransportPacketStream() {
- var buffer = new Uint8Array(MP2T_PACKET_LENGTH),
- bytesInBuffer = 0;
-
- _TransportPacketStream.prototype.init.call(this);
-
- // Deliver new bytes to the stream.
-
- /**
- * Split a stream of data into M2TS packets
- **/
- this.push = function (bytes) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH,
- everything;
-
- // If there are bytes remaining from the last segment, prepend them to the
- // bytes that were pushed in
- if (bytesInBuffer) {
- everything = new Uint8Array(bytes.byteLength + bytesInBuffer);
- everything.set(buffer.subarray(0, bytesInBuffer));
- everything.set(bytes, bytesInBuffer);
- bytesInBuffer = 0;
- } else {
- everything = bytes;
- }
-
- // While we have enough data for a packet
- while (endIndex < everything.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) {
- // We found a packet so emit it and jump one whole packet forward in
- // the stream
- this.trigger('data', everything.subarray(startIndex, endIndex));
- startIndex += MP2T_PACKET_LENGTH;
- endIndex += MP2T_PACKET_LENGTH;
- continue;
- }
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- }
-
- // If there was some data left over at the end of the segment that couldn't
- // possibly be a whole packet, keep it because it might be the start of a packet
- // that continues in the next segment
- if (startIndex < everything.byteLength) {
- buffer.set(everything.subarray(startIndex), 0);
- bytesInBuffer = everything.byteLength - startIndex;
- }
- };
-
- /**
- * Passes identified M2TS packets to the TransportParseStream to be parsed
- **/
- this.flush = function () {
- // If the buffer contains a whole packet when we are being flushed, emit it
- // and empty the buffer. Otherwise hold onto the data because it may be
- // important for decoding the next segment
- if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) {
- this.trigger('data', buffer);
- bytesInBuffer = 0;
- }
- this.trigger('done');
- };
- };
- _TransportPacketStream.prototype = new stream();
-
- /**
- * Accepts an MP2T TransportPacketStream and emits data events with parsed
- * forms of the individual transport stream packets.
- */
- _TransportParseStream = function TransportParseStream() {
- var parsePsi, parsePat, parsePmt, self;
- _TransportParseStream.prototype.init.call(this);
- self = this;
-
- this.packetsWaitingForPmt = [];
- this.programMapTable = undefined;
-
- parsePsi = function parsePsi(payload, psi) {
- var offset = 0;
-
- // PSI packets may be split into multiple sections and those
- // sections may be split into multiple packets. If a PSI
- // section starts in this packet, the payload_unit_start_indicator
- // will be true and the first byte of the payload will indicate
- // the offset from the current position to the start of the
- // section.
- if (psi.payloadUnitStartIndicator) {
- offset += payload[offset] + 1;
- }
-
- if (psi.type === 'pat') {
- parsePat(payload.subarray(offset), psi);
- } else {
- parsePmt(payload.subarray(offset), psi);
- }
- };
-
- parsePat = function parsePat(payload, pat) {
- pat.section_number = payload[7]; // eslint-disable-line camelcase
- pat.last_section_number = payload[8]; // eslint-disable-line camelcase
-
- // skip the PSI header and parse the first PMT entry
- self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
- pat.pmtPid = self.pmtPid;
- };
-
- /**
- * Parse out the relevant fields of a Program Map Table (PMT).
- * @param payload {Uint8Array} the PMT-specific portion of an MP2T
- * packet. The first byte in this array should be the table_id
- * field.
- * @param pmt {object} the object that should be decorated with
- * fields parsed from the PMT.
- */
- parsePmt = function parsePmt(payload, pmt) {
- var sectionLength, tableEnd, programInfoLength, offset;
-
- // PMTs can be sent ahead of the time when they should actually
- // take effect. We don't believe this should ever be the case
- // for HLS but we'll ignore "forward" PMT declarations if we see
- // them. Future PMT declarations have the current_next_indicator
- // set to zero.
- if (!(payload[5] & 0x01)) {
- return;
- }
-
- // overwrite any existing program map table
- self.programMapTable = {
- video: null,
- audio: null,
- 'timed-metadata': {}
- };
-
- // the mapping table ends at the end of the current section
- sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
- tableEnd = 3 + sectionLength - 4;
-
- // to determine where the table is, we have to figure out how
- // long the program info descriptors are
- programInfoLength = (payload[10] & 0x0f) << 8 | payload[11];
-
- // advance the offset to the first entry in the mapping table
- offset = 12 + programInfoLength;
- while (offset < tableEnd) {
- var streamType = payload[offset];
- var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2];
-
- // only map a single elementary_pid for audio and video stream types
- // TODO: should this be done for metadata too? for now maintain behavior of
- // multiple metadata streams
- if (streamType === streamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) {
- self.programMapTable.video = pid;
- } else if (streamType === streamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) {
- self.programMapTable.audio = pid;
- } else if (streamType === streamTypes.METADATA_STREAM_TYPE) {
- // map pid to stream type for metadata streams
- self.programMapTable['timed-metadata'][pid] = streamType;
- }
-
- // move to the next table entry
- // skip past the elementary stream descriptors, if present
- offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
- }
-
- // record the map on the packet as well
- pmt.programMapTable = self.programMapTable;
- };
-
- /**
- * Deliver a new MP2T packet to the next stream in the pipeline.
- */
- this.push = function (packet) {
- var result = {},
- offset = 4;
-
- result.payloadUnitStartIndicator = !!(packet[1] & 0x40);
-
- // pid is a 13-bit field starting at the last bit of packet[1]
- result.pid = packet[1] & 0x1f;
- result.pid <<= 8;
- result.pid |= packet[2];
-
- // if an adaption field is present, its length is specified by the
- // fifth byte of the TS packet header. The adaptation field is
- // used to add stuffing to PES packets that don't fill a complete
- // TS packet, and to specify some forms of timing and control data
- // that we do not currently use.
- if ((packet[3] & 0x30) >>> 4 > 0x01) {
- offset += packet[offset] + 1;
- }
-
- // parse the rest of the packet based on the type
- if (result.pid === 0) {
- result.type = 'pat';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result);
- } else if (result.pid === this.pmtPid) {
- result.type = 'pmt';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result);
-
- // if there are any packets waiting for a PMT to be found, process them now
- while (this.packetsWaitingForPmt.length) {
- this.processPes_.apply(this, this.packetsWaitingForPmt.shift());
- }
- } else if (this.programMapTable === undefined) {
- // When we have not seen a PMT yet, defer further processing of
- // PES packets until one has been parsed
- this.packetsWaitingForPmt.push([packet, offset, result]);
- } else {
- this.processPes_(packet, offset, result);
- }
- };
-
- this.processPes_ = function (packet, offset, result) {
- // set the appropriate stream type
- if (result.pid === this.programMapTable.video) {
- result.streamType = streamTypes.H264_STREAM_TYPE;
- } else if (result.pid === this.programMapTable.audio) {
- result.streamType = streamTypes.ADTS_STREAM_TYPE;
- } else {
- // if not video or audio, it is timed-metadata or unknown
- // if unknown, streamType will be undefined
- result.streamType = this.programMapTable['timed-metadata'][result.pid];
- }
-
- result.type = 'pes';
- result.data = packet.subarray(offset);
-
- this.trigger('data', result);
- };
- };
- _TransportParseStream.prototype = new stream();
- _TransportParseStream.STREAM_TYPES = {
- h264: 0x1b,
- adts: 0x0f
- };
-
- /**
- * Reconsistutes program elementary stream (PES) packets from parsed
- * transport stream packets. That is, if you pipe an
- * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output
- * events will be events which capture the bytes for individual PES
- * packets plus relevant metadata that has been extracted from the
- * container.
- */
- _ElementaryStream = function ElementaryStream() {
- var self = this,
-
- // PES packet fragments
- video = {
- data: [],
- size: 0
- },
- audio = {
- data: [],
- size: 0
- },
- timedMetadata = {
- data: [],
- size: 0
- },
- parsePes = function parsePes(payload, pes) {
- var ptsDtsFlags;
-
- // get the packet length, this will be 0 for video
- pes.packetLength = 6 + (payload[4] << 8 | payload[5]);
-
- // find out if this packets starts a new keyframe
- pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0;
- // PES packets may be annotated with a PTS value, or a PTS value
- // and a DTS value. Determine what combination of values is
- // available to work with.
- ptsDtsFlags = payload[7];
-
- // PTS and DTS are normally stored as a 33-bit number. Javascript
- // performs all bitwise operations on 32-bit integers but javascript
- // supports a much greater range (52-bits) of integer using standard
- // mathematical operations.
- // We construct a 31-bit value using bitwise operators over the 31
- // most significant bits and then multiply by 4 (equal to a left-shift
- // of 2) before we add the final 2 least significant bits of the
- // timestamp (equal to an OR.)
- if (ptsDtsFlags & 0xC0) {
- // the PTS and DTS are not written out directly. For information
- // on how they are encoded, see
- // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
- pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3;
- pes.pts *= 4; // Left shift by 2
- pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs
- pes.dts = pes.pts;
- if (ptsDtsFlags & 0x40) {
- pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3;
- pes.dts *= 4; // Left shift by 2
- pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs
- }
- }
- // the data section starts immediately after the PES header.
- // pes_header_data_length specifies the number of header bytes
- // that follow the last byte of the field.
- pes.data = payload.subarray(9 + payload[8]);
- },
-
- /**
- * Pass completely parsed PES packets to the next stream in the pipeline
- **/
- flushStream = function flushStream(stream$$1, type, forceFlush) {
- var packetData = new Uint8Array(stream$$1.size),
- event = {
- type: type
- },
- i = 0,
- offset = 0,
- packetFlushable = false,
- fragment;
-
- // do nothing if there is not enough buffered data for a complete
- // PES header
- if (!stream$$1.data.length || stream$$1.size < 9) {
- return;
- }
- event.trackId = stream$$1.data[0].pid;
-
- // reassemble the packet
- for (i = 0; i < stream$$1.data.length; i++) {
- fragment = stream$$1.data[i];
-
- packetData.set(fragment.data, offset);
- offset += fragment.data.byteLength;
- }
-
- // parse assembled packet's PES header
- parsePes(packetData, event);
-
- // non-video PES packets MUST have a non-zero PES_packet_length
- // check that there is enough stream data to fill the packet
- packetFlushable = type === 'video' || event.packetLength <= stream$$1.size;
-
- // flush pending packets if the conditions are right
- if (forceFlush || packetFlushable) {
- stream$$1.size = 0;
- stream$$1.data.length = 0;
- }
-
- // only emit packets that are complete. this is to avoid assembling
- // incomplete PES packets due to poor segmentation
- if (packetFlushable) {
- self.trigger('data', event);
- }
- };
-
- _ElementaryStream.prototype.init.call(this);
-
- /**
- * Identifies M2TS packet types and parses PES packets using metadata
- * parsed from the PMT
- **/
- this.push = function (data) {
- ({
- pat: function pat() {
- // we have to wait for the PMT to arrive as well before we
- // have any meaningful metadata
- },
- pes: function pes() {
- var stream$$1, streamType;
-
- switch (data.streamType) {
- case streamTypes.H264_STREAM_TYPE:
- case streamTypes.H264_STREAM_TYPE:
- stream$$1 = video;
- streamType = 'video';
- break;
- case streamTypes.ADTS_STREAM_TYPE:
- stream$$1 = audio;
- streamType = 'audio';
- break;
- case streamTypes.METADATA_STREAM_TYPE:
- stream$$1 = timedMetadata;
- streamType = 'timed-metadata';
- break;
- default:
- // ignore unknown stream types
- return;
- }
-
- // if a new packet is starting, we can flush the completed
- // packet
- if (data.payloadUnitStartIndicator) {
- flushStream(stream$$1, streamType, true);
- }
-
- // buffer this fragment until we are sure we've received the
- // complete payload
- stream$$1.data.push(data);
- stream$$1.size += data.data.byteLength;
- },
- pmt: function pmt() {
- var event = {
- type: 'metadata',
- tracks: []
- },
- programMapTable = data.programMapTable;
-
- // translate audio and video streams to tracks
- if (programMapTable.video !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.video,
- codec: 'avc',
- type: 'video'
- });
- }
- if (programMapTable.audio !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.audio,
- codec: 'adts',
- type: 'audio'
- });
- }
-
- self.trigger('data', event);
- }
- })[data.type]();
- };
-
- /**
- * Flush any remaining input. Video PES packets may be of variable
- * length. Normally, the start of a new video packet can trigger the
- * finalization of the previous packet. That is not possible if no
- * more video is forthcoming, however. In that case, some other
- * mechanism (like the end of the file) has to be employed. When it is
- * clear that no additional data is forthcoming, calling this method
- * will flush the buffered packets.
- */
- this.flush = function () {
- // !!THIS ORDER IS IMPORTANT!!
- // video first then audio
- flushStream(video, 'video');
- flushStream(audio, 'audio');
- flushStream(timedMetadata, 'timed-metadata');
- this.trigger('done');
- };
- };
- _ElementaryStream.prototype = new stream();
-
- var m2ts = {
- PAT_PID: 0x0000,
- MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
- TransportPacketStream: _TransportPacketStream,
- TransportParseStream: _TransportParseStream,
- ElementaryStream: _ElementaryStream,
- TimestampRolloverStream: TimestampRolloverStream$1,
- CaptionStream: captionStream.CaptionStream,
- Cea608Stream: captionStream.Cea608Stream,
- MetadataStream: metadataStream
- };
-
- for (var type$1 in streamTypes) {
- if (streamTypes.hasOwnProperty(type$1)) {
- m2ts[type$1] = streamTypes[type$1];
- }
- }
-
- var m2ts_1 = m2ts;
-
- var _AdtsStream;
-
- var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
-
- /*
- * Accepts a ElementaryStream and emits data events with parsed
- * AAC Audio Frames of the individual packets. Input audio in ADTS
- * format is unpacked and re-emitted as AAC frames.
- *
- * @see http://wiki.multimedia.cx/index.php?title=ADTS
- * @see http://wiki.multimedia.cx/?title=Understanding_AAC
- */
- _AdtsStream = function AdtsStream() {
- var buffer;
-
- _AdtsStream.prototype.init.call(this);
-
- this.push = function (packet) {
- var i = 0,
- frameNum = 0,
- frameLength,
- protectionSkipBytes,
- frameEnd,
- oldBuffer,
- sampleCount,
- adtsFrameDuration;
-
- if (packet.type !== 'audio') {
- // ignore non-audio data
- return;
- }
-
- // Prepend any data in the buffer to the input data so that we can parse
- // aac frames the cross a PES packet boundary
- if (buffer) {
- oldBuffer = buffer;
- buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength);
- buffer.set(oldBuffer);
- buffer.set(packet.data, oldBuffer.byteLength);
- } else {
- buffer = packet.data;
- }
-
- // unpack any ADTS frames which have been fully received
- // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS
- while (i + 5 < buffer.length) {
-
- // Loook for the start of an ADTS header..
- if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) {
- // If a valid header was not found, jump one forward and attempt to
- // find a valid ADTS header starting at the next byte
- i++;
- continue;
- }
-
- // The protection skip bit tells us if we have 2 bytes of CRC data at the
- // end of the ADTS header
- protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2;
-
- // Frame length is a 13 bit integer starting 16 bits from the
- // end of the sync sequence
- frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5;
-
- sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024;
- adtsFrameDuration = sampleCount * 90000 / ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2];
-
- frameEnd = i + frameLength;
-
- // If we don't have enough data to actually finish this ADTS frame, return
- // and wait for more data
- if (buffer.byteLength < frameEnd) {
- return;
- }
-
- // Otherwise, deliver the complete AAC frame
- this.trigger('data', {
- pts: packet.pts + frameNum * adtsFrameDuration,
- dts: packet.dts + frameNum * adtsFrameDuration,
- sampleCount: sampleCount,
- audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1,
- channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6,
- samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2],
- samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2,
- // assume ISO/IEC 14496-12 AudioSampleEntry default of 16
- samplesize: 16,
- data: buffer.subarray(i + 7 + protectionSkipBytes, frameEnd)
- });
-
- // If the buffer is empty, clear it and return
- if (buffer.byteLength === frameEnd) {
- buffer = undefined;
- return;
- }
-
- frameNum++;
-
- // Remove the finished frame from the buffer and start the process again
- buffer = buffer.subarray(frameEnd);
- }
- };
- this.flush = function () {
- this.trigger('done');
- };
- };
-
- _AdtsStream.prototype = new stream();
-
- var adts = _AdtsStream;
-
- var ExpGolomb;
-
- /**
- * Parser for exponential Golomb codes, a variable-bitwidth number encoding
- * scheme used by h264.
- */
- ExpGolomb = function ExpGolomb(workingData) {
- var
- // the number of bytes left to examine in workingData
- workingBytesAvailable = workingData.byteLength,
-
-
- // the current word being examined
- workingWord = 0,
- // :uint
-
- // the number of bits left to examine in the current word
- workingBitsAvailable = 0; // :uint;
-
- // ():uint
- this.length = function () {
- return 8 * workingBytesAvailable;
- };
-
- // ():uint
- this.bitsAvailable = function () {
- return 8 * workingBytesAvailable + workingBitsAvailable;
- };
-
- // ():void
- this.loadWord = function () {
- var position = workingData.byteLength - workingBytesAvailable,
- workingBytes = new Uint8Array(4),
- availableBytes = Math.min(4, workingBytesAvailable);
-
- if (availableBytes === 0) {
- throw new Error('no bytes available');
- }
-
- workingBytes.set(workingData.subarray(position, position + availableBytes));
- workingWord = new DataView(workingBytes.buffer).getUint32(0);
-
- // track the amount of workingData that has been processed
- workingBitsAvailable = availableBytes * 8;
- workingBytesAvailable -= availableBytes;
- };
-
- // (count:int):void
- this.skipBits = function (count) {
- var skipBytes; // :int
- if (workingBitsAvailable > count) {
- workingWord <<= count;
- workingBitsAvailable -= count;
- } else {
- count -= workingBitsAvailable;
- skipBytes = Math.floor(count / 8);
-
- count -= skipBytes * 8;
- workingBytesAvailable -= skipBytes;
-
- this.loadWord();
-
- workingWord <<= count;
- workingBitsAvailable -= count;
- }
- };
-
- // (size:int):uint
- this.readBits = function (size) {
- var bits = Math.min(workingBitsAvailable, size),
- // :uint
- valu = workingWord >>> 32 - bits; // :uint
- // if size > 31, handle error
- workingBitsAvailable -= bits;
- if (workingBitsAvailable > 0) {
- workingWord <<= bits;
- } else if (workingBytesAvailable > 0) {
- this.loadWord();
- }
-
- bits = size - bits;
- if (bits > 0) {
- return valu << bits | this.readBits(bits);
- }
- return valu;
- };
-
- // ():uint
- this.skipLeadingZeros = function () {
- var leadingZeroCount; // :uint
- for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) {
- if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) {
- // the first bit of working word is 1
- workingWord <<= leadingZeroCount;
- workingBitsAvailable -= leadingZeroCount;
- return leadingZeroCount;
- }
- }
-
- // we exhausted workingWord and still have not found a 1
- this.loadWord();
- return leadingZeroCount + this.skipLeadingZeros();
- };
-
- // ():void
- this.skipUnsignedExpGolomb = function () {
- this.skipBits(1 + this.skipLeadingZeros());
- };
-
- // ():void
- this.skipExpGolomb = function () {
- this.skipBits(1 + this.skipLeadingZeros());
- };
-
- // ():uint
- this.readUnsignedExpGolomb = function () {
- var clz = this.skipLeadingZeros(); // :uint
- return this.readBits(clz + 1) - 1;
- };
-
- // ():int
- this.readExpGolomb = function () {
- var valu = this.readUnsignedExpGolomb(); // :int
- if (0x01 & valu) {
- // the number is odd if the low order bit is set
- return 1 + valu >>> 1; // add 1 to make it even, and divide by 2
- }
- return -1 * (valu >>> 1); // divide by two then make it negative
- };
-
- // Some convenience functions
- // :Boolean
- this.readBoolean = function () {
- return this.readBits(1) === 1;
- };
-
- // ():int
- this.readUnsignedByte = function () {
- return this.readBits(8);
- };
-
- this.loadWord();
- };
-
- var expGolomb = ExpGolomb;
-
- var _H264Stream, _NalByteStream;
- var PROFILES_WITH_OPTIONAL_SPS_DATA;
-
- /**
- * Accepts a NAL unit byte stream and unpacks the embedded NAL units.
- */
- _NalByteStream = function NalByteStream() {
- var syncPoint = 0,
- i,
- buffer;
- _NalByteStream.prototype.init.call(this);
-
- /*
- * Scans a byte stream and triggers a data event with the NAL units found.
- * @param {Object} data Event received from H264Stream
- * @param {Uint8Array} data.data The h264 byte stream to be scanned
- *
- * @see H264Stream.push
- */
- this.push = function (data) {
- var swapBuffer;
-
- if (!buffer) {
- buffer = data.data;
- } else {
- swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
- swapBuffer.set(buffer);
- swapBuffer.set(data.data, buffer.byteLength);
- buffer = swapBuffer;
- }
-
- // Rec. ITU-T H.264, Annex B
- // scan for NAL unit boundaries
-
- // a match looks like this:
- // 0 0 1 .. NAL .. 0 0 1
- // ^ sync point ^ i
- // or this:
- // 0 0 1 .. NAL .. 0 0 0
- // ^ sync point ^ i
-
- // advance the sync point to a NAL start, if necessary
- for (; syncPoint < buffer.byteLength - 3; syncPoint++) {
- if (buffer[syncPoint + 2] === 1) {
- // the sync point is properly aligned
- i = syncPoint + 5;
- break;
- }
- }
-
- while (i < buffer.byteLength) {
- // look at the current byte to determine if we've hit the end of
- // a NAL unit boundary
- switch (buffer[i]) {
- case 0:
- // skip past non-sync sequences
- if (buffer[i - 1] !== 0) {
- i += 2;
- break;
- } else if (buffer[i - 2] !== 0) {
- i++;
- break;
- }
-
- // deliver the NAL unit if it isn't empty
- if (syncPoint + 3 !== i - 2) {
- this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
- }
-
- // drop trailing zeroes
- do {
- i++;
- } while (buffer[i] !== 1 && i < buffer.length);
- syncPoint = i - 2;
- i += 3;
- break;
- case 1:
- // skip past non-sync sequences
- if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) {
- i += 3;
- break;
- }
-
- // deliver the NAL unit
- this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
- syncPoint = i - 2;
- i += 3;
- break;
- default:
- // the current byte isn't a one or zero, so it cannot be part
- // of a sync sequence
- i += 3;
- break;
- }
- }
- // filter out the NAL units that were delivered
- buffer = buffer.subarray(syncPoint);
- i -= syncPoint;
- syncPoint = 0;
- };
-
- this.flush = function () {
- // deliver the last buffered NAL unit
- if (buffer && buffer.byteLength > 3) {
- this.trigger('data', buffer.subarray(syncPoint + 3));
- }
- // reset the stream state
- buffer = null;
- syncPoint = 0;
- this.trigger('done');
- };
- };
- _NalByteStream.prototype = new stream();
-
- // values of profile_idc that indicate additional fields are included in the SPS
- // see Recommendation ITU-T H.264 (4/2013),
- // 7.3.2.1.1 Sequence parameter set data syntax
- PROFILES_WITH_OPTIONAL_SPS_DATA = {
- 100: true,
- 110: true,
- 122: true,
- 244: true,
- 44: true,
- 83: true,
- 86: true,
- 118: true,
- 128: true,
- 138: true,
- 139: true,
- 134: true
- };
-
- /**
- * Accepts input from a ElementaryStream and produces H.264 NAL unit data
- * events.
- */
- _H264Stream = function H264Stream() {
- var nalByteStream = new _NalByteStream(),
- self,
- trackId,
- currentPts,
- currentDts,
- discardEmulationPreventionBytes,
- readSequenceParameterSet,
- skipScalingList;
-
- _H264Stream.prototype.init.call(this);
- self = this;
-
- /*
- * Pushes a packet from a stream onto the NalByteStream
- *
- * @param {Object} packet - A packet received from a stream
- * @param {Uint8Array} packet.data - The raw bytes of the packet
- * @param {Number} packet.dts - Decode timestamp of the packet
- * @param {Number} packet.pts - Presentation timestamp of the packet
- * @param {Number} packet.trackId - The id of the h264 track this packet came from
- * @param {('video'|'audio')} packet.type - The type of packet
- *
- */
- this.push = function (packet) {
- if (packet.type !== 'video') {
- return;
- }
- trackId = packet.trackId;
- currentPts = packet.pts;
- currentDts = packet.dts;
-
- nalByteStream.push(packet);
- };
-
- /*
- * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps
- * for the NALUs to the next stream component.
- * Also, preprocess caption and sequence parameter NALUs.
- *
- * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push`
- * @see NalByteStream.push
- */
- nalByteStream.on('data', function (data) {
- var event = {
- trackId: trackId,
- pts: currentPts,
- dts: currentDts,
- data: data
- };
-
- switch (data[0] & 0x1f) {
- case 0x05:
- event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr';
- break;
- case 0x06:
- event.nalUnitType = 'sei_rbsp';
- event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
- break;
- case 0x07:
- event.nalUnitType = 'seq_parameter_set_rbsp';
- event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
- event.config = readSequenceParameterSet(event.escapedRBSP);
- break;
- case 0x08:
- event.nalUnitType = 'pic_parameter_set_rbsp';
- break;
- case 0x09:
- event.nalUnitType = 'access_unit_delimiter_rbsp';
- break;
-
- default:
- break;
- }
- // This triggers data on the H264Stream
- self.trigger('data', event);
- });
- nalByteStream.on('done', function () {
- self.trigger('done');
- });
-
- this.flush = function () {
- nalByteStream.flush();
- };
-
- /**
- * Advance the ExpGolomb decoder past a scaling list. The scaling
- * list is optionally transmitted as part of a sequence parameter
- * set and is not relevant to transmuxing.
- * @param count {number} the number of entries in this scaling list
- * @param expGolombDecoder {object} an ExpGolomb pointed to the
- * start of a scaling list
- * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
- */
- skipScalingList = function skipScalingList(count, expGolombDecoder) {
- var lastScale = 8,
- nextScale = 8,
- j,
- deltaScale;
-
- for (j = 0; j < count; j++) {
- if (nextScale !== 0) {
- deltaScale = expGolombDecoder.readExpGolomb();
- nextScale = (lastScale + deltaScale + 256) % 256;
- }
-
- lastScale = nextScale === 0 ? lastScale : nextScale;
- }
- };
-
- /**
- * Expunge any "Emulation Prevention" bytes from a "Raw Byte
- * Sequence Payload"
- * @param data {Uint8Array} the bytes of a RBSP from a NAL
- * unit
- * @return {Uint8Array} the RBSP without any Emulation
- * Prevention Bytes
- */
- discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) {
- var length = data.byteLength,
- emulationPreventionBytesPositions = [],
- i = 1,
- newLength,
- newData;
-
- // Find all `Emulation Prevention Bytes`
- while (i < length - 2) {
- if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
- emulationPreventionBytesPositions.push(i + 2);
- i += 2;
- } else {
- i++;
- }
- }
-
- // If no Emulation Prevention Bytes were found just return the original
- // array
- if (emulationPreventionBytesPositions.length === 0) {
- return data;
- }
-
- // Create a new array to hold the NAL unit data
- newLength = length - emulationPreventionBytesPositions.length;
- newData = new Uint8Array(newLength);
- var sourceIndex = 0;
-
- for (i = 0; i < newLength; sourceIndex++, i++) {
- if (sourceIndex === emulationPreventionBytesPositions[0]) {
- // Skip this byte
- sourceIndex++;
- // Remove this position index
- emulationPreventionBytesPositions.shift();
- }
- newData[i] = data[sourceIndex];
- }
-
- return newData;
- };
-
- /**
- * Read a sequence parameter set and return some interesting video
- * properties. A sequence parameter set is the H264 metadata that
- * describes the properties of upcoming video frames.
- * @param data {Uint8Array} the bytes of a sequence parameter set
- * @return {object} an object with configuration parsed from the
- * sequence parameter set, including the dimensions of the
- * associated video frames.
- */
- readSequenceParameterSet = function readSequenceParameterSet(data) {
- var frameCropLeftOffset = 0,
- frameCropRightOffset = 0,
- frameCropTopOffset = 0,
- frameCropBottomOffset = 0,
- sarScale = 1,
- expGolombDecoder,
- profileIdc,
- levelIdc,
- profileCompatibility,
- chromaFormatIdc,
- picOrderCntType,
- numRefFramesInPicOrderCntCycle,
- picWidthInMbsMinus1,
- picHeightInMapUnitsMinus1,
- frameMbsOnlyFlag,
- scalingListCount,
- sarRatio,
- aspectRatioIdc,
- i;
-
- expGolombDecoder = new expGolomb(data);
- profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
- profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag
- levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8)
- expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
-
- // some profiles have more optional data we don't need
- if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) {
- chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
- if (chromaFormatIdc === 3) {
- expGolombDecoder.skipBits(1); // separate_colour_plane_flag
- }
- expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
- expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
- expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
- if (expGolombDecoder.readBoolean()) {
- // seq_scaling_matrix_present_flag
- scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
- for (i = 0; i < scalingListCount; i++) {
- if (expGolombDecoder.readBoolean()) {
- // seq_scaling_list_present_flag[ i ]
- if (i < 6) {
- skipScalingList(16, expGolombDecoder);
- } else {
- skipScalingList(64, expGolombDecoder);
- }
- }
- }
- }
- }
-
- expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
- picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
-
- if (picOrderCntType === 0) {
- expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4
- } else if (picOrderCntType === 1) {
- expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
- expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
- expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
- numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
- for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
- expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
- }
- }
-
- expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
- expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
-
- picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
- picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
-
- frameMbsOnlyFlag = expGolombDecoder.readBits(1);
- if (frameMbsOnlyFlag === 0) {
- expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
- }
-
- expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
- if (expGolombDecoder.readBoolean()) {
- // frame_cropping_flag
- frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
- frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
- }
- if (expGolombDecoder.readBoolean()) {
- // vui_parameters_present_flag
- if (expGolombDecoder.readBoolean()) {
- // aspect_ratio_info_present_flag
- aspectRatioIdc = expGolombDecoder.readUnsignedByte();
- switch (aspectRatioIdc) {
- case 1:
- sarRatio = [1, 1];break;
- case 2:
- sarRatio = [12, 11];break;
- case 3:
- sarRatio = [10, 11];break;
- case 4:
- sarRatio = [16, 11];break;
- case 5:
- sarRatio = [40, 33];break;
- case 6:
- sarRatio = [24, 11];break;
- case 7:
- sarRatio = [20, 11];break;
- case 8:
- sarRatio = [32, 11];break;
- case 9:
- sarRatio = [80, 33];break;
- case 10:
- sarRatio = [18, 11];break;
- case 11:
- sarRatio = [15, 11];break;
- case 12:
- sarRatio = [64, 33];break;
- case 13:
- sarRatio = [160, 99];break;
- case 14:
- sarRatio = [4, 3];break;
- case 15:
- sarRatio = [3, 2];break;
- case 16:
- sarRatio = [2, 1];break;
- case 255:
- {
- sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()];
- break;
- }
- }
- if (sarRatio) {
- sarScale = sarRatio[0] / sarRatio[1];
- }
- }
- }
- return {
- profileIdc: profileIdc,
- levelIdc: levelIdc,
- profileCompatibility: profileCompatibility,
- width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale),
- height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2
- };
- };
- };
- _H264Stream.prototype = new stream();
-
- var h264 = {
- H264Stream: _H264Stream,
- NalByteStream: _NalByteStream
- };
-
- // Constants
- var _AacStream;
-
- /**
- * Splits an incoming stream of binary data into ADTS and ID3 Frames.
- */
-
- _AacStream = function AacStream() {
- var everything = new Uint8Array(),
- timeStamp = 0;
-
- _AacStream.prototype.init.call(this);
-
- this.setTimestamp = function (timestamp) {
- timeStamp = timestamp;
- };
-
- this.parseId3TagSize = function (header, byteIndex) {
- var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9],
- flags = header[byteIndex + 5],
- footerPresent = (flags & 16) >> 4;
-
- if (footerPresent) {
- return returnSize + 20;
- }
- return returnSize + 10;
- };
-
- this.parseAdtsSize = function (header, byteIndex) {
- var lowThree = (header[byteIndex + 5] & 0xE0) >> 5,
- middle = header[byteIndex + 4] << 3,
- highTwo = header[byteIndex + 3] & 0x3 << 11;
-
- return highTwo | middle | lowThree;
- };
-
- this.push = function (bytes) {
- var frameSize = 0,
- byteIndex = 0,
- bytesLeft,
- chunk,
- packet,
- tempLength;
-
- // If there are bytes remaining from the last segment, prepend them to the
- // bytes that were pushed in
- if (everything.length) {
- tempLength = everything.length;
- everything = new Uint8Array(bytes.byteLength + tempLength);
- everything.set(everything.subarray(0, tempLength));
- everything.set(bytes, tempLength);
- } else {
- everything = bytes;
- }
-
- while (everything.length - byteIndex >= 3) {
- if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) {
-
- // Exit early because we don't have enough to parse
- // the ID3 tag header
- if (everything.length - byteIndex < 10) {
- break;
- }
-
- // check framesize
- frameSize = this.parseId3TagSize(everything, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > everything.length) {
- break;
- }
- chunk = {
- type: 'timed-metadata',
- data: everything.subarray(byteIndex, byteIndex + frameSize)
- };
- this.trigger('data', chunk);
- byteIndex += frameSize;
- continue;
- } else if (everything[byteIndex] & 0xff === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) {
-
- // Exit early because we don't have enough to parse
- // the ADTS frame header
- if (everything.length - byteIndex < 7) {
- break;
- }
-
- frameSize = this.parseAdtsSize(everything, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > everything.length) {
- break;
- }
-
- packet = {
- type: 'audio',
- data: everything.subarray(byteIndex, byteIndex + frameSize),
- pts: timeStamp,
- dts: timeStamp
- };
- this.trigger('data', packet);
- byteIndex += frameSize;
- continue;
- }
- byteIndex++;
- }
- bytesLeft = everything.length - byteIndex;
-
- if (bytesLeft > 0) {
- everything = everything.subarray(byteIndex);
- } else {
- everything = new Uint8Array();
- }
- };
- };
-
- _AacStream.prototype = new stream();
-
- var aac = _AacStream;
-
- var highPrefix = [33, 16, 5, 32, 164, 27];
- var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252];
- var zeroFill = function zeroFill(count) {
- var a = [];
- while (count--) {
- a.push(0);
- }
- return a;
- };
-
- var makeTable = function makeTable(metaTable) {
- return Object.keys(metaTable).reduce(function (obj, key) {
- obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) {
- return arr.concat(part);
- }, []));
- return obj;
- }, {});
- };
-
- // Frames-of-silence to use for filling in missing AAC frames
- var coneOfSilence = {
- 96000: [highPrefix, [227, 64], zeroFill(154), [56]],
- 88200: [highPrefix, [231], zeroFill(170), [56]],
- 64000: [highPrefix, [248, 192], zeroFill(240), [56]],
- 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]],
- 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]],
- 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]],
- 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]],
- 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]],
- 12000: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 253, 128], zeroFill(259), [56]],
- 11025: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 255, 192], zeroFill(268), [55, 175, 128], zeroFill(108), [112]],
- 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]]
- };
-
- var silence = makeTable(coneOfSilence);
-
- var ONE_SECOND_IN_TS$1 = 90000,
- // 90kHz clock
- secondsToVideoTs,
- secondsToAudioTs,
- videoTsToSeconds,
- audioTsToSeconds,
- audioTsToVideoTs,
- videoTsToAudioTs;
-
- secondsToVideoTs = function secondsToVideoTs(seconds) {
- return seconds * ONE_SECOND_IN_TS$1;
- };
-
- secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) {
- return seconds * sampleRate;
- };
-
- videoTsToSeconds = function videoTsToSeconds(timestamp) {
- return timestamp / ONE_SECOND_IN_TS$1;
- };
-
- audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) {
- return timestamp / sampleRate;
- };
-
- audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) {
- return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
- };
-
- videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) {
- return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
- };
-
- var clock = {
- secondsToVideoTs: secondsToVideoTs,
- secondsToAudioTs: secondsToAudioTs,
- videoTsToSeconds: videoTsToSeconds,
- audioTsToSeconds: audioTsToSeconds,
- audioTsToVideoTs: audioTsToVideoTs,
- videoTsToAudioTs: videoTsToAudioTs
- };
-
- var H264Stream = h264.H264Stream;
-
- // constants
- var AUDIO_PROPERTIES = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize'];
-
- var VIDEO_PROPERTIES = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility'];
-
- var ONE_SECOND_IN_TS$2 = 90000; // 90kHz clock
-
- // object types
- var _VideoSegmentStream, _AudioSegmentStream, _Transmuxer, _CoalesceStream;
-
- // Helper functions
- var isLikelyAacData, arrayEquals, sumFrameByteLengths;
-
- isLikelyAacData = function isLikelyAacData(data) {
- if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) {
- return true;
- }
- return false;
- };
-
- /**
- * Compare two arrays (even typed) for same-ness
- */
- arrayEquals = function arrayEquals(a, b) {
- var i;
-
- if (a.length !== b.length) {
- return false;
- }
-
- // compare the value of each element in the array
- for (i = 0; i < a.length; i++) {
- if (a[i] !== b[i]) {
- return false;
- }
- }
-
- return true;
- };
-
- /**
- * Sum the `byteLength` properties of the data in each AAC frame
- */
- sumFrameByteLengths = function sumFrameByteLengths(array) {
- var i,
- currentObj,
- sum = 0;
-
- // sum the byteLength's all each nal unit in the frame
- for (i = 0; i < array.length; i++) {
- currentObj = array[i];
- sum += currentObj.data.byteLength;
- }
-
- return sum;
- };
-
- /**
- * Constructs a single-track, ISO BMFF media segment from AAC data
- * events. The output of this stream can be fed to a SourceBuffer
- * configured with a suitable initialization segment.
- * @param track {object} track metadata configuration
- * @param options {object} transmuxer options object
- * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- _AudioSegmentStream = function AudioSegmentStream(track, options) {
- var adtsFrames = [],
- sequenceNumber = 0,
- earliestAllowedDts = 0,
- audioAppendStartTs = 0,
- videoBaseMediaDecodeTime = Infinity;
-
- options = options || {};
-
- _AudioSegmentStream.prototype.init.call(this);
-
- this.push = function (data) {
- trackDecodeInfo.collectDtsInfo(track, data);
-
- if (track) {
- AUDIO_PROPERTIES.forEach(function (prop) {
- track[prop] = data[prop];
- });
- }
-
- // buffer audio data until end() is called
- adtsFrames.push(data);
- };
-
- this.setEarliestDts = function (earliestDts) {
- earliestAllowedDts = earliestDts - track.timelineStartInfo.baseMediaDecodeTime;
- };
-
- this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) {
- videoBaseMediaDecodeTime = baseMediaDecodeTime;
- };
-
- this.setAudioAppendStart = function (timestamp) {
- audioAppendStartTs = timestamp;
- };
-
- this.flush = function () {
- var frames, moof, mdat, boxes;
-
- // return early if no audio data has been observed
- if (adtsFrames.length === 0) {
- this.trigger('done', 'AudioSegmentStream');
- return;
- }
-
- frames = this.trimAdtsFramesByEarliestDts_(adtsFrames);
- track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
-
- this.prefixWithSilence_(track, frames);
-
- // we have to build the index from byte locations to
- // samples (that is, adts frames) in the audio data
- track.samples = this.generateSampleTable_(frames);
-
- // concatenate the audio data to constuct the mdat
- mdat = mp4Generator.mdat(this.concatenateFrameData_(frames));
-
- adtsFrames = [];
-
- moof = mp4Generator.moof(sequenceNumber, [track]);
- boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
-
- // bump the sequence number for next time
- sequenceNumber++;
-
- boxes.set(moof);
- boxes.set(mdat, moof.byteLength);
-
- trackDecodeInfo.clearDtsInfo(track);
-
- this.trigger('data', { track: track, boxes: boxes });
- this.trigger('done', 'AudioSegmentStream');
- };
-
- // Possibly pad (prefix) the audio track with silence if appending this track
- // would lead to the introduction of a gap in the audio buffer
- this.prefixWithSilence_ = function (track, frames) {
- var baseMediaDecodeTimeTs,
- frameDuration = 0,
- audioGapDuration = 0,
- audioFillFrameCount = 0,
- audioFillDuration = 0,
- silentFrame,
- i;
-
- if (!frames.length) {
- return;
- }
-
- baseMediaDecodeTimeTs = clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate);
- // determine frame clock duration based on sample rate, round up to avoid overfills
- frameDuration = Math.ceil(ONE_SECOND_IN_TS$2 / (track.samplerate / 1024));
-
- if (audioAppendStartTs && videoBaseMediaDecodeTime) {
- // insert the shortest possible amount (audio gap or audio to video gap)
- audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime);
- // number of full frames in the audio gap
- audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
- audioFillDuration = audioFillFrameCount * frameDuration;
- }
-
- // don't attempt to fill gaps smaller than a single frame or larger
- // than a half second
- if (audioFillFrameCount < 1 || audioFillDuration > ONE_SECOND_IN_TS$2 / 2) {
- return;
- }
-
- silentFrame = silence[track.samplerate];
-
- if (!silentFrame) {
- // we don't have a silent frame pregenerated for the sample rate, so use a frame
- // from the content instead
- silentFrame = frames[0].data;
- }
-
- for (i = 0; i < audioFillFrameCount; i++) {
- frames.splice(i, 0, {
- data: silentFrame
- });
- }
-
- track.baseMediaDecodeTime -= Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate));
- };
-
- // If the audio segment extends before the earliest allowed dts
- // value, remove AAC frames until starts at or after the earliest
- // allowed DTS so that we don't end up with a negative baseMedia-
- // DecodeTime for the audio track
- this.trimAdtsFramesByEarliestDts_ = function (adtsFrames) {
- if (track.minSegmentDts >= earliestAllowedDts) {
- return adtsFrames;
- }
-
- // We will need to recalculate the earliest segment Dts
- track.minSegmentDts = Infinity;
-
- return adtsFrames.filter(function (currentFrame) {
- // If this is an allowed frame, keep it and record it's Dts
- if (currentFrame.dts >= earliestAllowedDts) {
- track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
- track.minSegmentPts = track.minSegmentDts;
- return true;
- }
- // Otherwise, discard it
- return false;
- });
- };
-
- // generate the track's raw mdat data from an array of frames
- this.generateSampleTable_ = function (frames) {
- var i,
- currentFrame,
- samples = [];
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
- samples.push({
- size: currentFrame.data.byteLength,
- duration: 1024 // For AAC audio, all samples contain 1024 samples
- });
- }
- return samples;
- };
-
- // generate the track's sample table from an array of frames
- this.concatenateFrameData_ = function (frames) {
- var i,
- currentFrame,
- dataOffset = 0,
- data = new Uint8Array(sumFrameByteLengths(frames));
-
- for (i = 0; i < frames.length; i++) {
- currentFrame = frames[i];
-
- data.set(currentFrame.data, dataOffset);
- dataOffset += currentFrame.data.byteLength;
- }
- return data;
- };
- };
-
- _AudioSegmentStream.prototype = new stream();
-
- /**
- * Constructs a single-track, ISO BMFF media segment from H264 data
- * events. The output of this stream can be fed to a SourceBuffer
- * configured with a suitable initialization segment.
- * @param track {object} track metadata configuration
- * @param options {object} transmuxer options object
- * @param options.alignGopsAtEnd {boolean} If true, start from the end of the
- * gopsToAlignWith list when attempting to align gop pts
- * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
- * in the source; false to adjust the first segment to start at 0.
- */
- _VideoSegmentStream = function VideoSegmentStream(track, options) {
- var sequenceNumber = 0,
- nalUnits = [],
- gopsToAlignWith = [],
- config,
- pps;
-
- options = options || {};
-
- _VideoSegmentStream.prototype.init.call(this);
-
- delete track.minPTS;
-
- this.gopCache_ = [];
-
- /**
- * Constructs a ISO BMFF segment given H264 nalUnits
- * @param {Object} nalUnit A data event representing a nalUnit
- * @param {String} nalUnit.nalUnitType
- * @param {Object} nalUnit.config Properties for a mp4 track
- * @param {Uint8Array} nalUnit.data The nalUnit bytes
- * @see lib/codecs/h264.js
- **/
- this.push = function (nalUnit) {
- trackDecodeInfo.collectDtsInfo(track, nalUnit);
-
- // record the track config
- if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
- config = nalUnit.config;
- track.sps = [nalUnit.data];
-
- VIDEO_PROPERTIES.forEach(function (prop) {
- track[prop] = config[prop];
- }, this);
- }
-
- if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) {
- pps = nalUnit.data;
- track.pps = [nalUnit.data];
- }
-
- // buffer video until flush() is called
- nalUnits.push(nalUnit);
- };
-
- /**
- * Pass constructed ISO BMFF track and boxes on to the
- * next stream in the pipeline
- **/
- this.flush = function () {
- var frames, gopForFusion, gops, moof, mdat, boxes;
-
- // Throw away nalUnits at the start of the byte stream until
- // we find the first AUD
- while (nalUnits.length) {
- if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
- break;
- }
- nalUnits.shift();
- }
-
- // Return early if no video data has been observed
- if (nalUnits.length === 0) {
- this.resetStream_();
- this.trigger('done', 'VideoSegmentStream');
- return;
- }
-
- // Organize the raw nal-units into arrays that represent
- // higher-level constructs such as frames and gops
- // (group-of-pictures)
- frames = frameUtils.groupNalsIntoFrames(nalUnits);
- gops = frameUtils.groupFramesIntoGops(frames);
-
- // If the first frame of this fragment is not a keyframe we have
- // a problem since MSE (on Chrome) requires a leading keyframe.
- //
- // We have two approaches to repairing this situation:
- // 1) GOP-FUSION:
- // This is where we keep track of the GOPS (group-of-pictures)
- // from previous fragments and attempt to find one that we can
- // prepend to the current fragment in order to create a valid
- // fragment.
- // 2) KEYFRAME-PULLING:
- // Here we search for the first keyframe in the fragment and
- // throw away all the frames between the start of the fragment
- // and that keyframe. We then extend the duration and pull the
- // PTS of the keyframe forward so that it covers the time range
- // of the frames that were disposed of.
- //
- // #1 is far prefereable over #2 which can cause "stuttering" but
- // requires more things to be just right.
- if (!gops[0][0].keyFrame) {
- // Search for a gop for fusion from our gopCache
- gopForFusion = this.getGopForFusion_(nalUnits[0], track);
-
- if (gopForFusion) {
- gops.unshift(gopForFusion);
- // Adjust Gops' metadata to account for the inclusion of the
- // new gop at the beginning
- gops.byteLength += gopForFusion.byteLength;
- gops.nalCount += gopForFusion.nalCount;
- gops.pts = gopForFusion.pts;
- gops.dts = gopForFusion.dts;
- gops.duration += gopForFusion.duration;
- } else {
- // If we didn't find a candidate gop fall back to keyframe-pulling
- gops = frameUtils.extendFirstKeyFrame(gops);
- }
- }
-
- // Trim gops to align with gopsToAlignWith
- if (gopsToAlignWith.length) {
- var alignedGops;
-
- if (options.alignGopsAtEnd) {
- alignedGops = this.alignGopsAtEnd_(gops);
- } else {
- alignedGops = this.alignGopsAtStart_(gops);
- }
-
- if (!alignedGops) {
- // save all the nals in the last GOP into the gop cache
- this.gopCache_.unshift({
- gop: gops.pop(),
- pps: track.pps,
- sps: track.sps
- });
-
- // Keep a maximum of 6 GOPs in the cache
- this.gopCache_.length = Math.min(6, this.gopCache_.length);
-
- // Clear nalUnits
- nalUnits = [];
-
- // return early no gops can be aligned with desired gopsToAlignWith
- this.resetStream_();
- this.trigger('done', 'VideoSegmentStream');
- return;
- }
-
- // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct
- // when recalculated before sending off to CoalesceStream
- trackDecodeInfo.clearDtsInfo(track);
-
- gops = alignedGops;
- }
-
- trackDecodeInfo.collectDtsInfo(track, gops);
-
- // First, we have to build the index from byte locations to
- // samples (that is, frames) in the video data
- track.samples = frameUtils.generateSampleTable(gops);
-
- // Concatenate the video data and construct the mdat
- mdat = mp4Generator.mdat(frameUtils.concatenateNalData(gops));
-
- track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
-
- this.trigger('processedGopsInfo', gops.map(function (gop) {
- return {
- pts: gop.pts,
- dts: gop.dts,
- byteLength: gop.byteLength
- };
- }));
-
- // save all the nals in the last GOP into the gop cache
- this.gopCache_.unshift({
- gop: gops.pop(),
- pps: track.pps,
- sps: track.sps
- });
-
- // Keep a maximum of 6 GOPs in the cache
- this.gopCache_.length = Math.min(6, this.gopCache_.length);
-
- // Clear nalUnits
- nalUnits = [];
-
- this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime);
- this.trigger('timelineStartInfo', track.timelineStartInfo);
-
- moof = mp4Generator.moof(sequenceNumber, [track]);
-
- // it would be great to allocate this array up front instead of
- // throwing away hundreds of media segment fragments
- boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
-
- // Bump the sequence number for next time
- sequenceNumber++;
-
- boxes.set(moof);
- boxes.set(mdat, moof.byteLength);
-
- this.trigger('data', { track: track, boxes: boxes });
-
- this.resetStream_();
-
- // Continue with the flush process now
- this.trigger('done', 'VideoSegmentStream');
- };
-
- this.resetStream_ = function () {
- trackDecodeInfo.clearDtsInfo(track);
-
- // reset config and pps because they may differ across segments
- // for instance, when we are rendition switching
- config = undefined;
- pps = undefined;
- };
-
- // Search for a candidate Gop for gop-fusion from the gop cache and
- // return it or return null if no good candidate was found
- this.getGopForFusion_ = function (nalUnit) {
- var halfSecond = 45000,
- // Half-a-second in a 90khz clock
- allowableOverlap = 10000,
- // About 3 frames @ 30fps
- nearestDistance = Infinity,
- dtsDistance,
- nearestGopObj,
- currentGop,
- currentGopObj,
- i;
-
- // Search for the GOP nearest to the beginning of this nal unit
- for (i = 0; i < this.gopCache_.length; i++) {
- currentGopObj = this.gopCache_[i];
- currentGop = currentGopObj.gop;
-
- // Reject Gops with different SPS or PPS
- if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) {
- continue;
- }
-
- // Reject Gops that would require a negative baseMediaDecodeTime
- if (currentGop.dts < track.timelineStartInfo.dts) {
- continue;
- }
-
- // The distance between the end of the gop and the start of the nalUnit
- dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration;
-
- // Only consider GOPS that start before the nal unit and end within
- // a half-second of the nal unit
- if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) {
-
- // Always use the closest GOP we found if there is more than
- // one candidate
- if (!nearestGopObj || nearestDistance > dtsDistance) {
- nearestGopObj = currentGopObj;
- nearestDistance = dtsDistance;
- }
- }
- }
-
- if (nearestGopObj) {
- return nearestGopObj.gop;
- }
- return null;
- };
-
- // trim gop list to the first gop found that has a matching pts with a gop in the list
- // of gopsToAlignWith starting from the START of the list
- this.alignGopsAtStart_ = function (gops) {
- var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops;
-
- byteLength = gops.byteLength;
- nalCount = gops.nalCount;
- duration = gops.duration;
- alignIndex = gopIndex = 0;
-
- while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) {
- align = gopsToAlignWith[alignIndex];
- gop = gops[gopIndex];
-
- if (align.pts === gop.pts) {
- break;
- }
-
- if (gop.pts > align.pts) {
- // this current gop starts after the current gop we want to align on, so increment
- // align index
- alignIndex++;
- continue;
- }
-
- // current gop starts before the current gop we want to align on. so increment gop
- // index
- gopIndex++;
- byteLength -= gop.byteLength;
- nalCount -= gop.nalCount;
- duration -= gop.duration;
- }
-
- if (gopIndex === 0) {
- // no gops to trim
- return gops;
- }
-
- if (gopIndex === gops.length) {
- // all gops trimmed, skip appending all gops
- return null;
- }
-
- alignedGops = gops.slice(gopIndex);
- alignedGops.byteLength = byteLength;
- alignedGops.duration = duration;
- alignedGops.nalCount = nalCount;
- alignedGops.pts = alignedGops[0].pts;
- alignedGops.dts = alignedGops[0].dts;
-
- return alignedGops;
- };
-
- // trim gop list to the first gop found that has a matching pts with a gop in the list
- // of gopsToAlignWith starting from the END of the list
- this.alignGopsAtEnd_ = function (gops) {
- var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound;
-
- alignIndex = gopsToAlignWith.length - 1;
- gopIndex = gops.length - 1;
- alignEndIndex = null;
- matchFound = false;
-
- while (alignIndex >= 0 && gopIndex >= 0) {
- align = gopsToAlignWith[alignIndex];
- gop = gops[gopIndex];
-
- if (align.pts === gop.pts) {
- matchFound = true;
- break;
- }
-
- if (align.pts > gop.pts) {
- alignIndex--;
- continue;
- }
-
- if (alignIndex === gopsToAlignWith.length - 1) {
- // gop.pts is greater than the last alignment candidate. If no match is found
- // by the end of this loop, we still want to append gops that come after this
- // point
- alignEndIndex = gopIndex;
- }
-
- gopIndex--;
- }
-
- if (!matchFound && alignEndIndex === null) {
- return null;
- }
-
- var trimIndex;
-
- if (matchFound) {
- trimIndex = gopIndex;
- } else {
- trimIndex = alignEndIndex;
- }
-
- if (trimIndex === 0) {
- return gops;
- }
-
- var alignedGops = gops.slice(trimIndex);
- var metadata = alignedGops.reduce(function (total, gop) {
- total.byteLength += gop.byteLength;
- total.duration += gop.duration;
- total.nalCount += gop.nalCount;
- return total;
- }, { byteLength: 0, duration: 0, nalCount: 0 });
-
- alignedGops.byteLength = metadata.byteLength;
- alignedGops.duration = metadata.duration;
- alignedGops.nalCount = metadata.nalCount;
- alignedGops.pts = alignedGops[0].pts;
- alignedGops.dts = alignedGops[0].dts;
-
- return alignedGops;
- };
-
- this.alignGopsWith = function (newGopsToAlignWith) {
- gopsToAlignWith = newGopsToAlignWith;
- };
- };
-
- _VideoSegmentStream.prototype = new stream();
-
- /**
- * A Stream that can combine multiple streams (ie. audio & video)
- * into a single output segment for MSE. Also supports audio-only
- * and video-only streams.
- */
- _CoalesceStream = function CoalesceStream(options, metadataStream) {
- // Number of Tracks per output segment
- // If greater than 1, we combine multiple
- // tracks into a single segment
- this.numberOfTracks = 0;
- this.metadataStream = metadataStream;
-
- if (typeof options.remux !== 'undefined') {
- this.remuxTracks = !!options.remux;
- } else {
- this.remuxTracks = true;
- }
-
- this.pendingTracks = [];
- this.videoTrack = null;
- this.pendingBoxes = [];
- this.pendingCaptions = [];
- this.pendingMetadata = [];
- this.pendingBytes = 0;
- this.emittedTracks = 0;
-
- _CoalesceStream.prototype.init.call(this);
-
- // Take output from multiple
- this.push = function (output) {
- // buffer incoming captions until the associated video segment
- // finishes
- if (output.text) {
- return this.pendingCaptions.push(output);
- }
- // buffer incoming id3 tags until the final flush
- if (output.frames) {
- return this.pendingMetadata.push(output);
- }
-
- // Add this track to the list of pending tracks and store
- // important information required for the construction of
- // the final segment
- this.pendingTracks.push(output.track);
- this.pendingBoxes.push(output.boxes);
- this.pendingBytes += output.boxes.byteLength;
-
- if (output.track.type === 'video') {
- this.videoTrack = output.track;
- }
- if (output.track.type === 'audio') {
- this.audioTrack = output.track;
- }
- };
- };
-
- _CoalesceStream.prototype = new stream();
- _CoalesceStream.prototype.flush = function (flushSource) {
- var offset = 0,
- event = {
- captions: [],
- captionStreams: {},
- metadata: [],
- info: {}
- },
- caption,
- id3,
- initSegment,
- timelineStartPts = 0,
- i;
-
- if (this.pendingTracks.length < this.numberOfTracks) {
- if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') {
- // Return because we haven't received a flush from a data-generating
- // portion of the segment (meaning that we have only recieved meta-data
- // or captions.)
- return;
- } else if (this.remuxTracks) {
- // Return until we have enough tracks from the pipeline to remux (if we
- // are remuxing audio and video into a single MP4)
- return;
- } else if (this.pendingTracks.length === 0) {
- // In the case where we receive a flush without any data having been
- // received we consider it an emitted track for the purposes of coalescing
- // `done` events.
- // We do this for the case where there is an audio and video track in the
- // segment but no audio data. (seen in several playlists with alternate
- // audio tracks and no audio present in the main TS segments.)
- this.emittedTracks++;
-
- if (this.emittedTracks >= this.numberOfTracks) {
- this.trigger('done');
- this.emittedTracks = 0;
- }
- return;
- }
- }
-
- if (this.videoTrack) {
- timelineStartPts = this.videoTrack.timelineStartInfo.pts;
- VIDEO_PROPERTIES.forEach(function (prop) {
- event.info[prop] = this.videoTrack[prop];
- }, this);
- } else if (this.audioTrack) {
- timelineStartPts = this.audioTrack.timelineStartInfo.pts;
- AUDIO_PROPERTIES.forEach(function (prop) {
- event.info[prop] = this.audioTrack[prop];
- }, this);
- }
-
- if (this.pendingTracks.length === 1) {
- event.type = this.pendingTracks[0].type;
- } else {
- event.type = 'combined';
- }
-
- this.emittedTracks += this.pendingTracks.length;
-
- initSegment = mp4Generator.initSegment(this.pendingTracks);
-
- // Create a new typed array to hold the init segment
- event.initSegment = new Uint8Array(initSegment.byteLength);
-
- // Create an init segment containing a moov
- // and track definitions
- event.initSegment.set(initSegment);
-
- // Create a new typed array to hold the moof+mdats
- event.data = new Uint8Array(this.pendingBytes);
-
- // Append each moof+mdat (one per track) together
- for (i = 0; i < this.pendingBoxes.length; i++) {
- event.data.set(this.pendingBoxes[i], offset);
- offset += this.pendingBoxes[i].byteLength;
- }
-
- // Translate caption PTS times into second offsets into the
- // video timeline for the segment, and add track info
- for (i = 0; i < this.pendingCaptions.length; i++) {
- caption = this.pendingCaptions[i];
- caption.startTime = caption.startPts - timelineStartPts;
- caption.startTime /= 90e3;
- caption.endTime = caption.endPts - timelineStartPts;
- caption.endTime /= 90e3;
- event.captionStreams[caption.stream] = true;
- event.captions.push(caption);
- }
-
- // Translate ID3 frame PTS times into second offsets into the
- // video timeline for the segment
- for (i = 0; i < this.pendingMetadata.length; i++) {
- id3 = this.pendingMetadata[i];
- id3.cueTime = id3.pts - timelineStartPts;
- id3.cueTime /= 90e3;
- event.metadata.push(id3);
- }
- // We add this to every single emitted segment even though we only need
- // it for the first
- event.metadata.dispatchType = this.metadataStream.dispatchType;
-
- // Reset stream state
- this.pendingTracks.length = 0;
- this.videoTrack = null;
- this.pendingBoxes.length = 0;
- this.pendingCaptions.length = 0;
- this.pendingBytes = 0;
- this.pendingMetadata.length = 0;
-
- // Emit the built segment
- this.trigger('data', event);
-
- // Only emit `done` if all tracks have been flushed and emitted
- if (this.emittedTracks >= this.numberOfTracks) {
- this.trigger('done');
- this.emittedTracks = 0;
- }
- };
- /**
- * A Stream that expects MP2T binary data as input and produces
- * corresponding media segments, suitable for use with Media Source
- * Extension (MSE) implementations that support the ISO BMFF byte
- * stream format, like Chrome.
- */
- _Transmuxer = function Transmuxer(options) {
- var self = this,
- hasFlushed = true,
- videoTrack,
- audioTrack;
-
- _Transmuxer.prototype.init.call(this);
-
- options = options || {};
- this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0;
- this.transmuxPipeline_ = {};
-
- this.setupAacPipeline = function () {
- var pipeline = {};
- this.transmuxPipeline_ = pipeline;
-
- pipeline.type = 'aac';
- pipeline.metadataStream = new m2ts_1.MetadataStream();
-
- // set up the parsing pipeline
- pipeline.aacStream = new aac();
- pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio');
- pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata');
- pipeline.adtsStream = new adts();
- pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream);
- pipeline.headOfPipeline = pipeline.aacStream;
-
- pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
- pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
-
- pipeline.metadataStream.on('timestamp', function (frame) {
- pipeline.aacStream.setTimestamp(frame.timeStamp);
- });
-
- pipeline.aacStream.on('data', function (data) {
- if (data.type === 'timed-metadata' && !pipeline.audioSegmentStream) {
- audioTrack = audioTrack || {
- timelineStartInfo: {
- baseMediaDecodeTime: self.baseMediaDecodeTime
- },
- codec: 'adts',
- type: 'audio'
- };
- // hook up the audio segment stream to the first track with aac data
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options);
- // Set up the final part of the audio pipeline
- pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
- }
- });
-
- // Re-emit any data coming from the coalesce stream to the outside world
- pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
- // Let the consumer know we have finished flushing the entire pipeline
- pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
- };
-
- this.setupTsPipeline = function () {
- var pipeline = {};
- this.transmuxPipeline_ = pipeline;
-
- pipeline.type = 'ts';
- pipeline.metadataStream = new m2ts_1.MetadataStream();
-
- // set up the parsing pipeline
- pipeline.packetStream = new m2ts_1.TransportPacketStream();
- pipeline.parseStream = new m2ts_1.TransportParseStream();
- pipeline.elementaryStream = new m2ts_1.ElementaryStream();
- pipeline.videoTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('video');
- pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio');
- pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata');
- pipeline.adtsStream = new adts();
- pipeline.h264Stream = new H264Stream();
- pipeline.captionStream = new m2ts_1.CaptionStream();
- pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream);
- pipeline.headOfPipeline = pipeline.packetStream;
-
- // disassemble MPEG2-TS packets into elementary streams
- pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream);
-
- // !!THIS ORDER IS IMPORTANT!!
- // demux the streams
- pipeline.elementaryStream.pipe(pipeline.videoTimestampRolloverStream).pipe(pipeline.h264Stream);
- pipeline.elementaryStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
-
- pipeline.elementaryStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
-
- // Hook up CEA-608/708 caption stream
- pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream);
-
- pipeline.elementaryStream.on('data', function (data) {
- var i;
-
- if (data.type === 'metadata') {
- i = data.tracks.length;
-
- // scan the tracks listed in the metadata
- while (i--) {
- if (!videoTrack && data.tracks[i].type === 'video') {
- videoTrack = data.tracks[i];
- videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
- } else if (!audioTrack && data.tracks[i].type === 'audio') {
- audioTrack = data.tracks[i];
- audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
- }
- }
-
- // hook up the video segment stream to the first track with h264 data
- if (videoTrack && !pipeline.videoSegmentStream) {
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.videoSegmentStream = new _VideoSegmentStream(videoTrack, options);
-
- pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) {
- // When video emits timelineStartInfo data after a flush, we forward that
- // info to the AudioSegmentStream, if it exists, because video timeline
- // data takes precedence.
- if (audioTrack) {
- audioTrack.timelineStartInfo = timelineStartInfo;
- // On the first segment we trim AAC frames that exist before the
- // very earliest DTS we have seen in video because Chrome will
- // interpret any video track with a baseMediaDecodeTime that is
- // non-zero as a gap.
- pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts);
- }
- });
-
- pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo'));
-
- pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) {
- if (audioTrack) {
- pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime);
- }
- });
-
- // Set up the final part of the video pipeline
- pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream);
- }
-
- if (audioTrack && !pipeline.audioSegmentStream) {
- // hook up the audio segment stream to the first track with aac data
- pipeline.coalesceStream.numberOfTracks++;
- pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options);
-
- // Set up the final part of the audio pipeline
- pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
- }
- }
- });
-
- // Re-emit any data coming from the coalesce stream to the outside world
- pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
- // Let the consumer know we have finished flushing the entire pipeline
- pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
- };
-
- // hook up the segment streams once track metadata is delivered
- this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) {
- var pipeline = this.transmuxPipeline_;
-
- this.baseMediaDecodeTime = baseMediaDecodeTime;
- if (audioTrack) {
- audioTrack.timelineStartInfo.dts = undefined;
- audioTrack.timelineStartInfo.pts = undefined;
- trackDecodeInfo.clearDtsInfo(audioTrack);
- audioTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime;
- if (pipeline.audioTimestampRolloverStream) {
- pipeline.audioTimestampRolloverStream.discontinuity();
- }
- }
- if (videoTrack) {
- if (pipeline.videoSegmentStream) {
- pipeline.videoSegmentStream.gopCache_ = [];
- pipeline.videoTimestampRolloverStream.discontinuity();
- }
- videoTrack.timelineStartInfo.dts = undefined;
- videoTrack.timelineStartInfo.pts = undefined;
- trackDecodeInfo.clearDtsInfo(videoTrack);
- pipeline.captionStream.reset();
- videoTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime;
- }
-
- if (pipeline.timedMetadataTimestampRolloverStream) {
- pipeline.timedMetadataTimestampRolloverStream.discontinuity();
- }
- };
-
- this.setAudioAppendStart = function (timestamp) {
- if (audioTrack) {
- this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp);
- }
- };
-
- this.alignGopsWith = function (gopsToAlignWith) {
- if (videoTrack && this.transmuxPipeline_.videoSegmentStream) {
- this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith);
- }
- };
-
- // feed incoming data to the front of the parsing pipeline
- this.push = function (data) {
- if (hasFlushed) {
- var isAac = isLikelyAacData(data);
-
- if (isAac && this.transmuxPipeline_.type !== 'aac') {
- this.setupAacPipeline();
- } else if (!isAac && this.transmuxPipeline_.type !== 'ts') {
- this.setupTsPipeline();
- }
- hasFlushed = false;
- }
- this.transmuxPipeline_.headOfPipeline.push(data);
- };
-
- // flush any buffered data
- this.flush = function () {
- hasFlushed = true;
- // Start at the top of the pipeline and flush all pending work
- this.transmuxPipeline_.headOfPipeline.flush();
- };
-
- // Caption data has to be reset when seeking outside buffered range
- this.resetCaptions = function () {
- if (this.transmuxPipeline_.captionStream) {
- this.transmuxPipeline_.captionStream.reset();
- }
- };
- };
- _Transmuxer.prototype = new stream();
-
- var transmuxer = {
- Transmuxer: _Transmuxer,
- VideoSegmentStream: _VideoSegmentStream,
- AudioSegmentStream: _AudioSegmentStream,
- AUDIO_PROPERTIES: AUDIO_PROPERTIES,
- VIDEO_PROPERTIES: VIDEO_PROPERTIES
- };
-
- var inspectMp4,
- _textifyMp,
- parseType$1 = probe.parseType,
- parseMp4Date = function parseMp4Date(seconds) {
- return new Date(seconds * 1000 - 2082844800000);
- },
- parseSampleFlags = function parseSampleFlags(flags) {
- return {
- isLeading: (flags[0] & 0x0c) >>> 2,
- dependsOn: flags[0] & 0x03,
- isDependedOn: (flags[1] & 0xc0) >>> 6,
- hasRedundancy: (flags[1] & 0x30) >>> 4,
- paddingValue: (flags[1] & 0x0e) >>> 1,
- isNonSyncSample: flags[1] & 0x01,
- degradationPriority: flags[2] << 8 | flags[3]
- };
- },
- nalParse = function nalParse(avcStream) {
- var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
- result = [],
- i,
- length;
- for (i = 0; i + 4 < avcStream.length; i += length) {
- length = avcView.getUint32(i);
- i += 4;
-
- // bail if this doesn't appear to be an H264 stream
- if (length <= 0) {
- result.push('<span style=\'color:red;\'>MALFORMED DATA</span>');
- continue;
- }
-
- switch (avcStream[i] & 0x1F) {
- case 0x01:
- result.push('slice_layer_without_partitioning_rbsp');
- break;
- case 0x05:
- result.push('slice_layer_without_partitioning_rbsp_idr');
- break;
- case 0x06:
- result.push('sei_rbsp');
- break;
- case 0x07:
- result.push('seq_parameter_set_rbsp');
- break;
- case 0x08:
- result.push('pic_parameter_set_rbsp');
- break;
- case 0x09:
- result.push('access_unit_delimiter_rbsp');
- break;
- default:
- result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F);
- break;
- }
- }
- return result;
- },
-
-
- // registry of handlers for individual mp4 box types
- parse$1 = {
- // codingname, not a first-class box type. stsd entries share the
- // same format as real boxes so the parsing infrastructure can be
- // shared
- avc1: function avc1(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- dataReferenceIndex: view.getUint16(6),
- width: view.getUint16(24),
- height: view.getUint16(26),
- horizresolution: view.getUint16(28) + view.getUint16(30) / 16,
- vertresolution: view.getUint16(32) + view.getUint16(34) / 16,
- frameCount: view.getUint16(40),
- depth: view.getUint16(74),
- config: inspectMp4(data.subarray(78, data.byteLength))
- };
- },
- avcC: function avcC(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- configurationVersion: data[0],
- avcProfileIndication: data[1],
- profileCompatibility: data[2],
- avcLevelIndication: data[3],
- lengthSizeMinusOne: data[4] & 0x03,
- sps: [],
- pps: []
- },
- numOfSequenceParameterSets = data[5] & 0x1f,
- numOfPictureParameterSets,
- nalSize,
- offset,
- i;
-
- // iterate past any SPSs
- offset = 6;
- for (i = 0; i < numOfSequenceParameterSets; i++) {
- nalSize = view.getUint16(offset);
- offset += 2;
- result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
- offset += nalSize;
- }
- // iterate past any PPSs
- numOfPictureParameterSets = data[offset];
- offset++;
- for (i = 0; i < numOfPictureParameterSets; i++) {
- nalSize = view.getUint16(offset);
- offset += 2;
- result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
- offset += nalSize;
- }
- return result;
- },
- btrt: function btrt(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- bufferSizeDB: view.getUint32(0),
- maxBitrate: view.getUint32(4),
- avgBitrate: view.getUint32(8)
- };
- },
- esds: function esds(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- esId: data[6] << 8 | data[7],
- streamPriority: data[8] & 0x1f,
- decoderConfig: {
- objectProfileIndication: data[11],
- streamType: data[12] >>> 2 & 0x3f,
- bufferSize: data[13] << 16 | data[14] << 8 | data[15],
- maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19],
- avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23],
- decoderConfigDescriptor: {
- tag: data[24],
- length: data[25],
- audioObjectType: data[26] >>> 3 & 0x1f,
- samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01,
- channelConfiguration: data[27] >>> 3 & 0x0f
- }
- }
- };
- },
- ftyp: function ftyp(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- majorBrand: parseType$1(data.subarray(0, 4)),
- minorVersion: view.getUint32(4),
- compatibleBrands: []
- },
- i = 8;
- while (i < data.byteLength) {
- result.compatibleBrands.push(parseType$1(data.subarray(i, i + 4)));
- i += 4;
- }
- return result;
- },
- dinf: function dinf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- dref: function dref(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- dataReferences: inspectMp4(data.subarray(8))
- };
- },
- hdlr: function hdlr(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- handlerType: parseType$1(data.subarray(8, 12)),
- name: ''
- },
- i = 8;
-
- // parse out the name field
- for (i = 24; i < data.byteLength; i++) {
- if (data[i] === 0x00) {
- // the name field is null-terminated
- i++;
- break;
- }
- result.name += String.fromCharCode(data[i]);
- }
- // decode UTF-8 to javascript's internal representation
- // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
- result.name = decodeURIComponent(escape(result.name));
-
- return result;
- },
- mdat: function mdat(data) {
- return {
- byteLength: data.byteLength,
- nals: nalParse(data)
- };
- },
- mdhd: function mdhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- language,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- language: ''
- };
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.timescale = view.getUint32(i);
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.timescale = view.getUint32(i);
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
- // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
- // each field is the packed difference between its ASCII value and 0x60
- language = view.getUint16(i);
- result.language += String.fromCharCode((language >> 10) + 0x60);
- result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60);
- result.language += String.fromCharCode((language & 0x1f) + 0x60);
-
- return result;
- },
- mdia: function mdia(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mfhd: function mfhd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
- };
- },
- minf: function minf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- // codingname, not a first-class box type. stsd entries share the
- // same format as real boxes so the parsing infrastructure can be
- // shared
- mp4a: function mp4a(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- // 6 bytes reserved
- dataReferenceIndex: view.getUint16(6),
- // 4 + 4 bytes reserved
- channelcount: view.getUint16(16),
- samplesize: view.getUint16(18),
- // 2 bytes pre_defined
- // 2 bytes reserved
- samplerate: view.getUint16(24) + view.getUint16(26) / 65536
- };
-
- // if there are more bytes to process, assume this is an ISO/IEC
- // 14496-14 MP4AudioSampleEntry and parse the ESDBox
- if (data.byteLength > 28) {
- result.streamDescriptor = inspectMp4(data.subarray(28))[0];
- }
- return result;
- },
- moof: function moof(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- moov: function moov(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mvex: function mvex(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- mvhd: function mvhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4))
- };
-
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.timescale = view.getUint32(i);
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.timescale = view.getUint32(i);
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
-
- // convert fixed-point, base 16 back to a number
- result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16;
- i += 4;
- result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
- i += 2;
- i += 2;
- i += 2 * 4;
- result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
- i += 9 * 4;
- i += 6 * 4;
- result.nextTrackId = view.getUint32(i);
- return result;
- },
- pdin: function pdin(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4)),
- rate: view.getUint32(4),
- initialDelay: view.getUint32(8)
- };
- },
- sdtp: function sdtp(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- samples: []
- },
- i;
-
- for (i = 4; i < data.byteLength; i++) {
- result.samples.push({
- dependsOn: (data[i] & 0x30) >> 4,
- isDependedOn: (data[i] & 0x0c) >> 2,
- hasRedundancy: data[i] & 0x03
- });
- }
- return result;
- },
- sidx: function sidx(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- references: [],
- referenceId: view.getUint32(4),
- timescale: view.getUint32(8),
- earliestPresentationTime: view.getUint32(12),
- firstOffset: view.getUint32(16)
- },
- referenceCount = view.getUint16(22),
- i;
-
- for (i = 24; referenceCount; i += 12, referenceCount--) {
- result.references.push({
- referenceType: (data[i] & 0x80) >>> 7,
- referencedSize: view.getUint32(i) & 0x7FFFFFFF,
- subsegmentDuration: view.getUint32(i + 4),
- startsWithSap: !!(data[i + 8] & 0x80),
- sapType: (data[i + 8] & 0x70) >>> 4,
- sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
- });
- }
-
- return result;
- },
- smhd: function smhd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- balance: data[4] + data[5] / 256
- };
- },
- stbl: function stbl(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- stco: function stco(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- chunkOffsets: []
- },
- entryCount = view.getUint32(4),
- i;
- for (i = 8; entryCount; i += 4, entryCount--) {
- result.chunkOffsets.push(view.getUint32(i));
- }
- return result;
- },
- stsc: function stsc(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- entryCount = view.getUint32(4),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleToChunks: []
- },
- i;
- for (i = 8; entryCount; i += 12, entryCount--) {
- result.sampleToChunks.push({
- firstChunk: view.getUint32(i),
- samplesPerChunk: view.getUint32(i + 4),
- sampleDescriptionIndex: view.getUint32(i + 8)
- });
- }
- return result;
- },
- stsd: function stsd(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleDescriptions: inspectMp4(data.subarray(8))
- };
- },
- stsz: function stsz(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- sampleSize: view.getUint32(4),
- entries: []
- },
- i;
- for (i = 12; i < data.byteLength; i += 4) {
- result.entries.push(view.getUint32(i));
- }
- return result;
- },
- stts: function stts(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- timeToSamples: []
- },
- entryCount = view.getUint32(4),
- i;
-
- for (i = 8; entryCount; i += 8, entryCount--) {
- result.timeToSamples.push({
- sampleCount: view.getUint32(i),
- sampleDelta: view.getUint32(i + 4)
- });
- }
- return result;
- },
- styp: function styp(data) {
- return parse$1.ftyp(data);
- },
- tfdt: function tfdt(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- baseMediaDecodeTime: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
- };
- if (result.version === 1) {
- result.baseMediaDecodeTime *= Math.pow(2, 32);
- result.baseMediaDecodeTime += data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11];
- }
- return result;
- },
- tfhd: function tfhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- trackId: view.getUint32(4)
- },
- baseDataOffsetPresent = result.flags[2] & 0x01,
- sampleDescriptionIndexPresent = result.flags[2] & 0x02,
- defaultSampleDurationPresent = result.flags[2] & 0x08,
- defaultSampleSizePresent = result.flags[2] & 0x10,
- defaultSampleFlagsPresent = result.flags[2] & 0x20,
- durationIsEmpty = result.flags[0] & 0x010000,
- defaultBaseIsMoof = result.flags[0] & 0x020000,
- i;
-
- i = 8;
- if (baseDataOffsetPresent) {
- i += 4; // truncate top 4 bytes
- // FIXME: should we read the full 64 bits?
- result.baseDataOffset = view.getUint32(12);
- i += 4;
- }
- if (sampleDescriptionIndexPresent) {
- result.sampleDescriptionIndex = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleDurationPresent) {
- result.defaultSampleDuration = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleSizePresent) {
- result.defaultSampleSize = view.getUint32(i);
- i += 4;
- }
- if (defaultSampleFlagsPresent) {
- result.defaultSampleFlags = view.getUint32(i);
- }
- if (durationIsEmpty) {
- result.durationIsEmpty = true;
- }
- if (!baseDataOffsetPresent && defaultBaseIsMoof) {
- result.baseDataOffsetIsMoof = true;
- }
- return result;
- },
- tkhd: function tkhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
- i = 4,
- result = {
- version: view.getUint8(0),
- flags: new Uint8Array(data.subarray(1, 4))
- };
- if (result.version === 1) {
- i += 4;
- result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 8;
- result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
- i += 4;
- result.trackId = view.getUint32(i);
- i += 4;
- i += 8;
- result.duration = view.getUint32(i); // truncating top 4 bytes
- } else {
- result.creationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.modificationTime = parseMp4Date(view.getUint32(i));
- i += 4;
- result.trackId = view.getUint32(i);
- i += 4;
- i += 4;
- result.duration = view.getUint32(i);
- }
- i += 4;
- i += 2 * 4;
- result.layer = view.getUint16(i);
- i += 2;
- result.alternateGroup = view.getUint16(i);
- i += 2;
- // convert fixed-point, base 16 back to a number
- result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
- i += 2;
- i += 2;
- result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
- i += 9 * 4;
- result.width = view.getUint16(i) + view.getUint16(i + 2) / 16;
- i += 4;
- result.height = view.getUint16(i) + view.getUint16(i + 2) / 16;
- return result;
- },
- traf: function traf(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- trak: function trak(data) {
- return {
- boxes: inspectMp4(data)
- };
- },
- trex: function trex(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- trackId: view.getUint32(4),
- defaultSampleDescriptionIndex: view.getUint32(8),
- defaultSampleDuration: view.getUint32(12),
- defaultSampleSize: view.getUint32(16),
- sampleDependsOn: data[20] & 0x03,
- sampleIsDependedOn: (data[21] & 0xc0) >> 6,
- sampleHasRedundancy: (data[21] & 0x30) >> 4,
- samplePaddingValue: (data[21] & 0x0e) >> 1,
- sampleIsDifferenceSample: !!(data[21] & 0x01),
- sampleDegradationPriority: view.getUint16(22)
- };
- },
- trun: function trun(data) {
- var result = {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- samples: []
- },
- view = new DataView(data.buffer, data.byteOffset, data.byteLength),
-
- // Flag interpretation
- dataOffsetPresent = result.flags[2] & 0x01,
- // compare with 2nd byte of 0x1
- firstSampleFlagsPresent = result.flags[2] & 0x04,
- // compare with 2nd byte of 0x4
- sampleDurationPresent = result.flags[1] & 0x01,
- // compare with 2nd byte of 0x100
- sampleSizePresent = result.flags[1] & 0x02,
- // compare with 2nd byte of 0x200
- sampleFlagsPresent = result.flags[1] & 0x04,
- // compare with 2nd byte of 0x400
- sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
- // compare with 2nd byte of 0x800
- sampleCount = view.getUint32(4),
- offset = 8,
- sample;
-
- if (dataOffsetPresent) {
- // 32 bit signed integer
- result.dataOffset = view.getInt32(offset);
- offset += 4;
- }
-
- // Overrides the flags for the first sample only. The order of
- // optional values will be: duration, size, compositionTimeOffset
- if (firstSampleFlagsPresent && sampleCount) {
- sample = {
- flags: parseSampleFlags(data.subarray(offset, offset + 4))
- };
- offset += 4;
- if (sampleDurationPresent) {
- sample.duration = view.getUint32(offset);
- offset += 4;
- }
- if (sampleSizePresent) {
- sample.size = view.getUint32(offset);
- offset += 4;
- }
- if (sampleCompositionTimeOffsetPresent) {
- // Note: this should be a signed int if version is 1
- sample.compositionTimeOffset = view.getUint32(offset);
- offset += 4;
- }
- result.samples.push(sample);
- sampleCount--;
- }
-
- while (sampleCount--) {
- sample = {};
- if (sampleDurationPresent) {
- sample.duration = view.getUint32(offset);
- offset += 4;
- }
- if (sampleSizePresent) {
- sample.size = view.getUint32(offset);
- offset += 4;
- }
- if (sampleFlagsPresent) {
- sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
- offset += 4;
- }
- if (sampleCompositionTimeOffsetPresent) {
- // Note: this should be a signed int if version is 1
- sample.compositionTimeOffset = view.getUint32(offset);
- offset += 4;
- }
- result.samples.push(sample);
- }
- return result;
- },
- 'url ': function url(data) {
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4))
- };
- },
- vmhd: function vmhd(data) {
- var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
- return {
- version: data[0],
- flags: new Uint8Array(data.subarray(1, 4)),
- graphicsmode: view.getUint16(4),
- opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)])
- };
- }
- };
-
- /**
- * Return a javascript array of box objects parsed from an ISO base
- * media file.
- * @param data {Uint8Array} the binary data of the media to be inspected
- * @return {array} a javascript array of potentially nested box objects
- */
- inspectMp4 = function inspectMp4(data) {
- var i = 0,
- result = [],
- view,
- size,
- type,
- end,
- box;
-
- // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
- var ab = new ArrayBuffer(data.length);
- var v = new Uint8Array(ab);
- for (var z = 0; z < data.length; ++z) {
- v[z] = data[z];
- }
- view = new DataView(ab);
-
- while (i < data.byteLength) {
- // parse box data
- size = view.getUint32(i);
- type = parseType$1(data.subarray(i + 4, i + 8));
- end = size > 1 ? i + size : data.byteLength;
-
- // parse type-specific data
- box = (parse$1[type] || function (data) {
- return {
- data: data
- };
- })(data.subarray(i + 8, end));
- box.size = size;
- box.type = type;
-
- // store this box and move to the next
- result.push(box);
- i = end;
- }
- return result;
- };
-
- /**
- * Returns a textual representation of the javascript represtentation
- * of an MP4 file. You can use it as an alternative to
- * JSON.stringify() to compare inspected MP4s.
- * @param inspectedMp4 {array} the parsed array of boxes in an MP4
- * file
- * @param depth {number} (optional) the number of ancestor boxes of
- * the elements of inspectedMp4. Assumed to be zero if unspecified.
- * @return {string} a text representation of the parsed MP4
- */
- _textifyMp = function textifyMp4(inspectedMp4, depth) {
- var indent;
- depth = depth || 0;
- indent = new Array(depth * 2 + 1).join(' ');
-
- // iterate over all the boxes
- return inspectedMp4.map(function (box, index) {
-
- // list the box type first at the current indentation level
- return indent + box.type + '\n' +
-
- // the type is already included and handle child boxes separately
- Object.keys(box).filter(function (key) {
- return key !== 'type' && key !== 'boxes';
-
- // output all the box properties
- }).map(function (key) {
- var prefix = indent + ' ' + key + ': ',
- value = box[key];
-
- // print out raw bytes as hexademical
- if (value instanceof Uint8Array || value instanceof Uint32Array) {
- var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (byte) {
- return ' ' + ('00' + byte.toString(16)).slice(-2);
- }).join('').match(/.{1,24}/g);
- if (!bytes) {
- return prefix + '<>';
- }
- if (bytes.length === 1) {
- return prefix + '<' + bytes.join('').slice(1) + '>';
- }
- return prefix + '<\n' + bytes.map(function (line) {
- return indent + ' ' + line;
- }).join('\n') + '\n' + indent + ' >';
- }
-
- // stringify generic objects
- return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) {
- if (index === 0) {
- return line;
- }
- return indent + ' ' + line;
- }).join('\n');
- }).join('\n') + (
-
- // recursively textify the child boxes
- box.boxes ? '\n' + _textifyMp(box.boxes, depth + 1) : '');
- }).join('\n');
- };
-
- var mp4Inspector = {
- inspect: inspectMp4,
- textify: _textifyMp,
- parseTfdt: parse$1.tfdt,
- parseHdlr: parse$1.hdlr,
- parseTfhd: parse$1.tfhd,
- parseTrun: parse$1.trun
- };
-
- var discardEmulationPreventionBytes$1 = captionPacketParser.discardEmulationPreventionBytes;
- var CaptionStream$1 = captionStream.CaptionStream;
-
- /**
- * Maps an offset in the mdat to a sample based on the the size of the samples.
- * Assumes that `parseSamples` has been called first.
- *
- * @param {Number} offset - The offset into the mdat
- * @param {Object[]} samples - An array of samples, parsed using `parseSamples`
- * @return {?Object} The matching sample, or null if no match was found.
- *
- * @see ISO-BMFF-12/2015, Section 8.8.8
- **/
- var mapToSample = function mapToSample(offset, samples) {
- var approximateOffset = offset;
-
- for (var i = 0; i < samples.length; i++) {
- var sample = samples[i];
-
- if (approximateOffset < sample.size) {
- return sample;
- }
-
- approximateOffset -= sample.size;
- }
-
- return null;
- };
-
- /**
- * Finds SEI nal units contained in a Media Data Box.
- * Assumes that `parseSamples` has been called first.
- *
- * @param {Uint8Array} avcStream - The bytes of the mdat
- * @param {Object[]} samples - The samples parsed out by `parseSamples`
- * @param {Number} trackId - The trackId of this video track
- * @return {Object[]} seiNals - the parsed SEI NALUs found.
- * The contents of the seiNal should match what is expected by
- * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
- *
- * @see ISO-BMFF-12/2015, Section 8.1.1
- * @see Rec. ITU-T H.264, 7.3.2.3.1
- **/
- var findSeiNals = function findSeiNals(avcStream, samples, trackId) {
- var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
- result = [],
- seiNal,
- i,
- length,
- lastMatchedSample;
-
- for (i = 0; i + 4 < avcStream.length; i += length) {
- length = avcView.getUint32(i);
- i += 4;
-
- // Bail if this doesn't appear to be an H264 stream
- if (length <= 0) {
- continue;
- }
-
- switch (avcStream[i] & 0x1F) {
- case 0x06:
- var data = avcStream.subarray(i + 1, i + 1 + length);
- var matchingSample = mapToSample(i, samples);
-
- seiNal = {
- nalUnitType: 'sei_rbsp',
- size: length,
- data: data,
- escapedRBSP: discardEmulationPreventionBytes$1(data),
- trackId: trackId
- };
-
- if (matchingSample) {
- seiNal.pts = matchingSample.pts;
- seiNal.dts = matchingSample.dts;
- lastMatchedSample = matchingSample;
- } else {
- // If a matching sample cannot be found, use the last
- // sample's values as they should be as close as possible
- seiNal.pts = lastMatchedSample.pts;
- seiNal.dts = lastMatchedSample.dts;
- }
-
- result.push(seiNal);
- break;
- default:
- break;
- }
- }
-
- return result;
- };
-
- /**
- * Parses sample information out of Track Run Boxes and calculates
- * the absolute presentation and decode timestamps of each sample.
- *
- * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
- * @param {Number} baseMediaDecodeTime - base media decode time from tfdt
- @see ISO-BMFF-12/2015, Section 8.8.12
- * @param {Object} tfhd - The parsed Track Fragment Header
- * @see inspect.parseTfhd
- * @return {Object[]} the parsed samples
- *
- * @see ISO-BMFF-12/2015, Section 8.8.8
- **/
- var parseSamples = function parseSamples(truns, baseMediaDecodeTime, tfhd) {
- var currentDts = baseMediaDecodeTime;
- var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
- var defaultSampleSize = tfhd.defaultSampleSize || 0;
- var trackId = tfhd.trackId;
- var allSamples = [];
-
- truns.forEach(function (trun) {
- // Note: We currently do not parse the sample table as well
- // as the trun. It's possible some sources will require this.
- // moov > trak > mdia > minf > stbl
- var trackRun = mp4Inspector.parseTrun(trun);
- var samples = trackRun.samples;
-
- samples.forEach(function (sample) {
- if (sample.duration === undefined) {
- sample.duration = defaultSampleDuration;
- }
- if (sample.size === undefined) {
- sample.size = defaultSampleSize;
- }
- sample.trackId = trackId;
- sample.dts = currentDts;
- if (sample.compositionTimeOffset === undefined) {
- sample.compositionTimeOffset = 0;
- }
- sample.pts = currentDts + sample.compositionTimeOffset;
-
- currentDts += sample.duration;
- });
-
- allSamples = allSamples.concat(samples);
- });
-
- return allSamples;
- };
-
- /**
- * Parses out caption nals from an FMP4 segment's video tracks.
- *
- * @param {Uint8Array} segment - The bytes of a single segment
- * @param {Number} videoTrackId - The trackId of a video track in the segment
- * @return {Object.<Number, Object[]>} A mapping of video trackId to
- * a list of seiNals found in that track
- **/
- var parseCaptionNals = function parseCaptionNals(segment, videoTrackId) {
- // To get the samples
- var trafs = probe.findBox(segment, ['moof', 'traf']);
- // To get SEI NAL units
- var mdats = probe.findBox(segment, ['mdat']);
- var captionNals = {};
- var mdatTrafPairs = [];
-
- // Pair up each traf with a mdat as moofs and mdats are in pairs
- mdats.forEach(function (mdat, index) {
- var matchingTraf = trafs[index];
- mdatTrafPairs.push({
- mdat: mdat,
- traf: matchingTraf
- });
- });
-
- mdatTrafPairs.forEach(function (pair) {
- var mdat = pair.mdat;
- var traf = pair.traf;
- var tfhd = probe.findBox(traf, ['tfhd']);
- // Exactly 1 tfhd per traf
- var headerInfo = mp4Inspector.parseTfhd(tfhd[0]);
- var trackId = headerInfo.trackId;
- var tfdt = probe.findBox(traf, ['tfdt']);
- // Either 0 or 1 tfdt per traf
- var baseMediaDecodeTime = tfdt.length > 0 ? mp4Inspector.parseTfdt(tfdt[0]).baseMediaDecodeTime : 0;
- var truns = probe.findBox(traf, ['trun']);
- var samples;
- var seiNals;
-
- // Only parse video data for the chosen video track
- if (videoTrackId === trackId && truns.length > 0) {
- samples = parseSamples(truns, baseMediaDecodeTime, headerInfo);
-
- seiNals = findSeiNals(mdat, samples, trackId);
-
- if (!captionNals[trackId]) {
- captionNals[trackId] = [];
- }
-
- captionNals[trackId] = captionNals[trackId].concat(seiNals);
- }
- });
-
- return captionNals;
- };
-
- /**
- * Parses out inband captions from an MP4 container and returns
- * caption objects that can be used by WebVTT and the TextTrack API.
- * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
- * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
- * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
- *
- * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
- * @param {Number} trackId - The id of the video track to parse
- * @param {Number} timescale - The timescale for the video track from the init segment
- *
- * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
- * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
- * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
- * @return {String} parsedCaptions[].text - The visible content of the caption
- **/
- var parseEmbeddedCaptions = function parseEmbeddedCaptions(segment, trackId, timescale) {
- var seiNals;
-
- if (!trackId) {
- return null;
- }
-
- seiNals = parseCaptionNals(segment, trackId);
-
- return {
- seiNals: seiNals[trackId],
- timescale: timescale
- };
- };
-
- /**
- * Converts SEI NALUs into captions that can be used by video.js
- **/
- var CaptionParser = function CaptionParser() {
- var isInitialized = false;
- var captionStream$$1;
-
- // Stores segments seen before trackId and timescale are set
- var segmentCache;
- // Stores video track ID of the track being parsed
- var trackId;
- // Stores the timescale of the track being parsed
- var timescale;
- // Stores captions parsed so far
- var parsedCaptions;
-
- /**
- * A method to indicate whether a CaptionParser has been initalized
- * @returns {Boolean}
- **/
- this.isInitialized = function () {
- return isInitialized;
- };
-
- /**
- * Initializes the underlying CaptionStream, SEI NAL parsing
- * and management, and caption collection
- **/
- this.init = function () {
- captionStream$$1 = new CaptionStream$1();
- isInitialized = true;
-
- // Collect dispatched captions
- captionStream$$1.on('data', function (event) {
- // Convert to seconds in the source's timescale
- event.startTime = event.startPts / timescale;
- event.endTime = event.endPts / timescale;
-
- parsedCaptions.captions.push(event);
- parsedCaptions.captionStreams[event.stream] = true;
- });
- };
-
- /**
- * Determines if a new video track will be selected
- * or if the timescale changed
- * @return {Boolean}
- **/
- this.isNewInit = function (videoTrackIds, timescales) {
- if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) {
- return false;
- }
-
- return trackId !== videoTrackIds[0] || timescale !== timescales[trackId];
- };
-
- /**
- * Parses out SEI captions and interacts with underlying
- * CaptionStream to return dispatched captions
- *
- * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
- * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
- * @param {Object.<Number, Number>} timescales - The timescales found in the init segment
- * @see parseEmbeddedCaptions
- * @see m2ts/caption-stream.js
- **/
- this.parse = function (segment, videoTrackIds, timescales) {
- var parsedData;
-
- if (!this.isInitialized()) {
- return null;
-
- // This is not likely to be a video segment
- } else if (!videoTrackIds || !timescales) {
- return null;
- } else if (this.isNewInit(videoTrackIds, timescales)) {
- // Use the first video track only as there is no
- // mechanism to switch to other video tracks
- trackId = videoTrackIds[0];
- timescale = timescales[trackId];
-
- // If an init segment has not been seen yet, hold onto segment
- // data until we have one
- } else if (!trackId || !timescale) {
- segmentCache.push(segment);
- return null;
- }
-
- // Now that a timescale and trackId is set, parse cached segments
- while (segmentCache.length > 0) {
- var cachedSegment = segmentCache.shift();
-
- this.parse(cachedSegment, videoTrackIds, timescales);
- }
-
- parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
-
- if (parsedData === null || !parsedData.seiNals) {
- return null;
- }
-
- this.pushNals(parsedData.seiNals);
- // Force the parsed captions to be dispatched
- this.flushStream();
-
- return parsedCaptions;
- };
-
- /**
- * Pushes SEI NALUs onto CaptionStream
- * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
- * Assumes that `parseCaptionNals` has been called first
- * @see m2ts/caption-stream.js
- **/
- this.pushNals = function (nals) {
- if (!this.isInitialized() || !nals || nals.length === 0) {
- return null;
- }
-
- nals.forEach(function (nal) {
- captionStream$$1.push(nal);
- });
- };
-
- /**
- * Flushes underlying CaptionStream to dispatch processed, displayable captions
- * @see m2ts/caption-stream.js
- **/
- this.flushStream = function () {
- if (!this.isInitialized()) {
- return null;
- }
-
- captionStream$$1.flush();
- };
-
- /**
- * Reset caption buckets for new data
- **/
- this.clearParsedCaptions = function () {
- parsedCaptions.captions = [];
- parsedCaptions.captionStreams = {};
- };
-
- /**
- * Resets underlying CaptionStream
- * @see m2ts/caption-stream.js
- **/
- this.resetCaptionStream = function () {
- if (!this.isInitialized()) {
- return null;
- }
-
- captionStream$$1.reset();
- };
-
- /**
- * Convenience method to clear all captions flushed from the
- * CaptionStream and still being parsed
- * @see m2ts/caption-stream.js
- **/
- this.clearAllCaptions = function () {
- this.clearParsedCaptions();
- this.resetCaptionStream();
- };
-
- /**
- * Reset caption parser
- **/
- this.reset = function () {
- segmentCache = [];
- trackId = null;
- timescale = null;
-
- if (!parsedCaptions) {
- parsedCaptions = {
- captions: [],
- // CC1, CC2, CC3, CC4
- captionStreams: {}
- };
- } else {
- this.clearParsedCaptions();
- }
-
- this.resetCaptionStream();
- };
-
- this.reset();
- };
-
- var captionParser = CaptionParser;
-
- var mp4 = {
- generator: mp4Generator,
- probe: probe,
- Transmuxer: transmuxer.Transmuxer,
- AudioSegmentStream: transmuxer.AudioSegmentStream,
- VideoSegmentStream: transmuxer.VideoSegmentStream,
- CaptionParser: captionParser
- };
- var mp4_6 = mp4.CaptionParser;
-
- /**
- * @file segment-loader.js
- */
-
- // in ms
- var CHECK_BUFFER_DELAY = 500;
-
- /**
- * Determines if we should call endOfStream on the media source based
- * on the state of the buffer or if appened segment was the final
- * segment in the playlist.
- *
- * @param {Object} playlist a media playlist object
- * @param {Object} mediaSource the MediaSource object
- * @param {Number} segmentIndex the index of segment we last appended
- * @returns {Boolean} do we need to call endOfStream on the MediaSource
- */
- var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) {
- if (!playlist || !mediaSource) {
- return false;
- }
-
- var segments = playlist.segments;
-
- // determine a few boolean values to help make the branch below easier
- // to read
- var appendedLastSegment = segmentIndex === segments.length;
-
- // if we've buffered to the end of the video, we need to call endOfStream
- // so that MediaSources can trigger the `ended` event when it runs out of
- // buffered data instead of waiting for me
- return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment;
- };
-
- var finite = function finite(num) {
- return typeof num === 'number' && isFinite(num);
- };
-
- var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) {
- // Although these checks should most likely cover non 'main' types, for now it narrows
- // the scope of our checks.
- if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) {
- return null;
- }
-
- if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) {
- return 'Neither audio nor video found in segment.';
- }
-
- if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) {
- return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
- }
-
- if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) {
- return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
- }
-
- return null;
- };
-
- /**
- * Calculates a time value that is safe to remove from the back buffer without interupting
- * playback.
- *
- * @param {TimeRange} seekable
- * The current seekable range
- * @param {Number} currentTime
- * The current time of the player
- * @param {Number} targetDuration
- * The target duration of the current playlist
- * @return {Number}
- * Time that is safe to remove from the back buffer without interupting playback
- */
- var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable$$1, currentTime, targetDuration) {
- var removeToTime = void 0;
-
- if (seekable$$1.length && seekable$$1.start(0) > 0 && seekable$$1.start(0) < currentTime) {
- // If we have a seekable range use that as the limit for what can be removed safely
- removeToTime = seekable$$1.start(0);
- } else {
- // otherwise remove anything older than 30 seconds before the current play head
- removeToTime = currentTime - 30;
- }
-
- // Don't allow removing from the buffer within target duration of current time
- // to avoid the possibility of removing the GOP currently being played which could
- // cause playback stalls.
- return Math.min(removeToTime, currentTime - targetDuration);
- };
-
- var segmentInfoString = function segmentInfoString(segmentInfo) {
- var _segmentInfo$segment = segmentInfo.segment,
- start = _segmentInfo$segment.start,
- end = _segmentInfo$segment.end,
- _segmentInfo$playlist = segmentInfo.playlist,
- seq = _segmentInfo$playlist.mediaSequence,
- id = _segmentInfo$playlist.id,
- _segmentInfo$playlist2 = _segmentInfo$playlist.segments,
- segments = _segmentInfo$playlist2 === undefined ? [] : _segmentInfo$playlist2,
- index = segmentInfo.mediaIndex,
- timeline = segmentInfo.timeline;
-
-
- return ['appending [' + index + '] of [' + seq + ', ' + (seq + segments.length) + '] from playlist [' + id + ']', '[' + start + ' => ' + end + '] in timeline [' + timeline + ']'].join(' ');
- };
-
- /**
- * An object that manages segment loading and appending.
- *
- * @class SegmentLoader
- * @param {Object} options required and optional options
- * @extends videojs.EventTarget
- */
-
- var SegmentLoader = function (_videojs$EventTarget) {
- inherits$1(SegmentLoader, _videojs$EventTarget);
-
- function SegmentLoader(settings) {
- classCallCheck$1(this, SegmentLoader);
-
- // check pre-conditions
- var _this = possibleConstructorReturn$1(this, (SegmentLoader.__proto__ || Object.getPrototypeOf(SegmentLoader)).call(this));
-
- if (!settings) {
- throw new TypeError('Initialization settings are required');
- }
- if (typeof settings.currentTime !== 'function') {
- throw new TypeError('No currentTime getter specified');
- }
- if (!settings.mediaSource) {
- throw new TypeError('No MediaSource specified');
- }
- // public properties
- _this.bandwidth = settings.bandwidth;
- _this.throughput = { rate: 0, count: 0 };
- _this.roundTrip = NaN;
- _this.resetStats_();
- _this.mediaIndex = null;
-
- // private settings
- _this.hasPlayed_ = settings.hasPlayed;
- _this.currentTime_ = settings.currentTime;
- _this.seekable_ = settings.seekable;
- _this.seeking_ = settings.seeking;
- _this.duration_ = settings.duration;
- _this.mediaSource_ = settings.mediaSource;
- _this.hls_ = settings.hls;
- _this.loaderType_ = settings.loaderType;
- _this.startingMedia_ = void 0;
- _this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
- _this.goalBufferLength_ = settings.goalBufferLength;
- _this.sourceType_ = settings.sourceType;
- _this.inbandTextTracks_ = settings.inbandTextTracks;
- _this.state_ = 'INIT';
-
- // private instance variables
- _this.checkBufferTimeout_ = null;
- _this.error_ = void 0;
- _this.currentTimeline_ = -1;
- _this.pendingSegment_ = null;
- _this.mimeType_ = null;
- _this.sourceUpdater_ = null;
- _this.xhrOptions_ = null;
-
- // Fragmented mp4 playback
- _this.activeInitSegmentId_ = null;
- _this.initSegments_ = {};
- // Fmp4 CaptionParser
- _this.captionParser_ = new mp4_6();
-
- _this.decrypter_ = settings.decrypter;
-
- // Manages the tracking and generation of sync-points, mappings
- // between a time in the display time and a segment index within
- // a playlist
- _this.syncController_ = settings.syncController;
- _this.syncPoint_ = {
- segmentIndex: 0,
- time: 0
- };
-
- _this.syncController_.on('syncinfoupdate', function () {
- return _this.trigger('syncinfoupdate');
- });
-
- _this.mediaSource_.addEventListener('sourceopen', function () {
- return _this.ended_ = false;
- });
-
- // ...for determining the fetch location
- _this.fetchAtBuffer_ = false;
-
- _this.logger_ = logger('SegmentLoader[' + _this.loaderType_ + ']');
-
- Object.defineProperty(_this, 'state', {
- get: function get$$1() {
- return this.state_;
- },
- set: function set$$1(newState) {
- if (newState !== this.state_) {
- this.logger_(this.state_ + ' -> ' + newState);
- this.state_ = newState;
- }
- }
- });
- return _this;
- }
-
- /**
- * reset all of our media stats
- *
- * @private
- */
-
-
- createClass(SegmentLoader, [{
- key: 'resetStats_',
- value: function resetStats_() {
- this.mediaBytesTransferred = 0;
- this.mediaRequests = 0;
- this.mediaRequestsAborted = 0;
- this.mediaRequestsTimedout = 0;
- this.mediaRequestsErrored = 0;
- this.mediaTransferDuration = 0;
- this.mediaSecondsLoaded = 0;
- }
-
- /**
- * dispose of the SegmentLoader and reset to the default state
- */
-
- }, {
- key: 'dispose',
- value: function dispose() {
- this.state = 'DISPOSED';
- this.pause();
- this.abort_();
- if (this.sourceUpdater_) {
- this.sourceUpdater_.dispose();
- }
- this.resetStats_();
- this.captionParser_.reset();
- }
-
- /**
- * abort anything that is currently doing on with the SegmentLoader
- * and reset to a default state
- */
-
- }, {
- key: 'abort',
- value: function abort() {
- if (this.state !== 'WAITING') {
- if (this.pendingSegment_) {
- this.pendingSegment_ = null;
- }
- return;
- }
-
- this.abort_();
-
- // We aborted the requests we were waiting on, so reset the loader's state to READY
- // since we are no longer "waiting" on any requests. XHR callback is not always run
- // when the request is aborted. This will prevent the loader from being stuck in the
- // WAITING state indefinitely.
- this.state = 'READY';
-
- // don't wait for buffer check timeouts to begin fetching the
- // next segment
- if (!this.paused()) {
- this.monitorBuffer_();
- }
- }
-
- /**
- * abort all pending xhr requests and null any pending segements
- *
- * @private
- */
-
- }, {
- key: 'abort_',
- value: function abort_() {
- if (this.pendingSegment_) {
- this.pendingSegment_.abortRequests();
- }
-
- // clear out the segment being processed
- this.pendingSegment_ = null;
- }
-
- /**
- * set an error on the segment loader and null out any pending segements
- *
- * @param {Error} error the error to set on the SegmentLoader
- * @return {Error} the error that was set or that is currently set
- */
-
- }, {
- key: 'error',
- value: function error(_error) {
- if (typeof _error !== 'undefined') {
- this.error_ = _error;
- }
-
- this.pendingSegment_ = null;
- return this.error_;
- }
- }, {
- key: 'endOfStream',
- value: function endOfStream() {
- this.ended_ = true;
- this.pause();
- this.trigger('ended');
- }
-
- /**
- * Indicates which time ranges are buffered
- *
- * @return {TimeRange}
- * TimeRange object representing the current buffered ranges
- */
-
- }, {
- key: 'buffered_',
- value: function buffered_() {
- if (!this.sourceUpdater_) {
- return videojs.createTimeRanges();
- }
-
- return this.sourceUpdater_.buffered();
- }
-
- /**
- * Gets and sets init segment for the provided map
- *
- * @param {Object} map
- * The map object representing the init segment to get or set
- * @param {Boolean=} set
- * If true, the init segment for the provided map should be saved
- * @return {Object}
- * map object for desired init segment
- */
-
- }, {
- key: 'initSegment',
- value: function initSegment(map) {
- var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-
- if (!map) {
- return null;
- }
-
- var id = initSegmentId(map);
- var storedMap = this.initSegments_[id];
-
- if (set$$1 && !storedMap && map.bytes) {
- this.initSegments_[id] = storedMap = {
- resolvedUri: map.resolvedUri,
- byterange: map.byterange,
- bytes: map.bytes,
- timescales: map.timescales,
- videoTrackIds: map.videoTrackIds
- };
- }
-
- return storedMap || map;
- }
-
- /**
- * Returns true if all configuration required for loading is present, otherwise false.
- *
- * @return {Boolean} True if the all configuration is ready for loading
- * @private
- */
-
- }, {
- key: 'couldBeginLoading_',
- value: function couldBeginLoading_() {
- return this.playlist_ && (
- // the source updater is created when init_ is called, so either having a
- // source updater or being in the INIT state with a mimeType is enough
- // to say we have all the needed configuration to start loading.
- this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused();
- }
-
- /**
- * load a playlist and start to fill the buffer
- */
-
- }, {
- key: 'load',
- value: function load() {
- // un-pause
- this.monitorBuffer_();
-
- // if we don't have a playlist yet, keep waiting for one to be
- // specified
- if (!this.playlist_) {
- return;
- }
-
- // not sure if this is the best place for this
- this.syncController_.setDateTimeMapping(this.playlist_);
-
- // if all the configuration is ready, initialize and begin loading
- if (this.state === 'INIT' && this.couldBeginLoading_()) {
- return this.init_();
- }
-
- // if we're in the middle of processing a segment already, don't
- // kick off an additional segment request
- if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
- return;
- }
-
- this.state = 'READY';
- }
-
- /**
- * Once all the starting parameters have been specified, begin
- * operation. This method should only be invoked from the INIT
- * state.
- *
- * @private
- */
-
- }, {
- key: 'init_',
- value: function init_() {
- this.state = 'READY';
- this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_, this.loaderType_, this.sourceBufferEmitter_);
- this.resetEverything();
- return this.monitorBuffer_();
- }
-
- /**
- * set a playlist on the segment loader
- *
- * @param {PlaylistLoader} media the playlist to set on the segment loader
- */
-
- }, {
- key: 'playlist',
- value: function playlist(newPlaylist) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- if (!newPlaylist) {
- return;
- }
-
- var oldPlaylist = this.playlist_;
- var segmentInfo = this.pendingSegment_;
-
- this.playlist_ = newPlaylist;
- this.xhrOptions_ = options;
-
- // when we haven't started playing yet, the start of a live playlist
- // is always our zero-time so force a sync update each time the playlist
- // is refreshed from the server
- if (!this.hasPlayed_()) {
- newPlaylist.syncInfo = {
- mediaSequence: newPlaylist.mediaSequence,
- time: 0
- };
- }
-
- var oldId = oldPlaylist ? oldPlaylist.id : null;
-
- this.logger_('playlist update [' + oldId + ' => ' + newPlaylist.id + ']');
-
- // in VOD, this is always a rendition switch (or we updated our syncInfo above)
- // in LIVE, we always want to update with new playlists (including refreshes)
- this.trigger('syncinfoupdate');
-
- // if we were unpaused but waiting for a playlist, start
- // buffering now
- if (this.state === 'INIT' && this.couldBeginLoading_()) {
- return this.init_();
- }
-
- if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
- if (this.mediaIndex !== null) {
- // we must "resync" the segment loader when we switch renditions and
- // the segment loader is already synced to the previous rendition
- this.resyncLoader();
- }
-
- // the rest of this function depends on `oldPlaylist` being defined
- return;
- }
-
- // we reloaded the same playlist so we are in a live scenario
- // and we will likely need to adjust the mediaIndex
- var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
-
- this.logger_('live window shift [' + mediaSequenceDiff + ']');
-
- // update the mediaIndex on the SegmentLoader
- // this is important because we can abort a request and this value must be
- // equal to the last appended mediaIndex
- if (this.mediaIndex !== null) {
- this.mediaIndex -= mediaSequenceDiff;
- }
-
- // update the mediaIndex on the SegmentInfo object
- // this is important because we will update this.mediaIndex with this value
- // in `handleUpdateEnd_` after the segment has been successfully appended
- if (segmentInfo) {
- segmentInfo.mediaIndex -= mediaSequenceDiff;
-
- // we need to update the referenced segment so that timing information is
- // saved for the new playlist's segment, however, if the segment fell off the
- // playlist, we can leave the old reference and just lose the timing info
- if (segmentInfo.mediaIndex >= 0) {
- segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
- }
- }
-
- this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
- }
-
- /**
- * Prevent the loader from fetching additional segments. If there
- * is a segment request outstanding, it will finish processing
- * before the loader halts. A segment loader can be unpaused by
- * calling load().
- */
-
- }, {
- key: 'pause',
- value: function pause() {
- if (this.checkBufferTimeout_) {
- window_1.clearTimeout(this.checkBufferTimeout_);
-
- this.checkBufferTimeout_ = null;
- }
- }
-
- /**
- * Returns whether the segment loader is fetching additional
- * segments when given the opportunity. This property can be
- * modified through calls to pause() and load().
- */
-
- }, {
- key: 'paused',
- value: function paused() {
- return this.checkBufferTimeout_ === null;
- }
-
- /**
- * create/set the following mimetype on the SourceBuffer through a
- * SourceUpdater
- *
- * @param {String} mimeType the mime type string to use
- * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer
- * is added to the media source
- */
-
- }, {
- key: 'mimeType',
- value: function mimeType(_mimeType, sourceBufferEmitter) {
- if (this.mimeType_) {
- return;
- }
-
- this.mimeType_ = _mimeType;
- this.sourceBufferEmitter_ = sourceBufferEmitter;
- // if we were unpaused but waiting for a sourceUpdater, start
- // buffering now
- if (this.state === 'INIT' && this.couldBeginLoading_()) {
- this.init_();
- }
- }
-
- /**
- * Delete all the buffered data and reset the SegmentLoader
- */
-
- }, {
- key: 'resetEverything',
- value: function resetEverything() {
- this.ended_ = false;
- this.resetLoader();
- this.remove(0, this.duration_());
- // clears fmp4 captions
- this.captionParser_.clearAllCaptions();
- this.trigger('reseteverything');
- }
-
- /**
- * Force the SegmentLoader to resync and start loading around the currentTime instead
- * of starting at the end of the buffer
- *
- * Useful for fast quality changes
- */
-
- }, {
- key: 'resetLoader',
- value: function resetLoader() {
- this.fetchAtBuffer_ = false;
- this.resyncLoader();
- }
-
- /**
- * Force the SegmentLoader to restart synchronization and make a conservative guess
- * before returning to the simple walk-forward method
- */
-
- }, {
- key: 'resyncLoader',
- value: function resyncLoader() {
- this.mediaIndex = null;
- this.syncPoint_ = null;
- this.abort();
- }
-
- /**
- * Remove any data in the source buffer between start and end times
- * @param {Number} start - the start time of the region to remove from the buffer
- * @param {Number} end - the end time of the region to remove from the buffer
- */
-
- }, {
- key: 'remove',
- value: function remove(start, end) {
- if (this.sourceUpdater_) {
- this.sourceUpdater_.remove(start, end);
- }
- removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
-
- if (this.inbandTextTracks_) {
- for (var id in this.inbandTextTracks_) {
- removeCuesFromTrack(start, end, this.inbandTextTracks_[id]);
- }
- }
- }
-
- /**
- * (re-)schedule monitorBufferTick_ to run as soon as possible
- *
- * @private
- */
-
- }, {
- key: 'monitorBuffer_',
- value: function monitorBuffer_() {
- if (this.checkBufferTimeout_) {
- window_1.clearTimeout(this.checkBufferTimeout_);
- }
-
- this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), 1);
- }
-
- /**
- * As long as the SegmentLoader is in the READY state, periodically
- * invoke fillBuffer_().
- *
- * @private
- */
-
- }, {
- key: 'monitorBufferTick_',
- value: function monitorBufferTick_() {
- if (this.state === 'READY') {
- this.fillBuffer_();
- }
-
- if (this.checkBufferTimeout_) {
- window_1.clearTimeout(this.checkBufferTimeout_);
- }
-
- this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
- }
-
- /**
- * fill the buffer with segements unless the sourceBuffers are
- * currently updating
- *
- * Note: this function should only ever be called by monitorBuffer_
- * and never directly
- *
- * @private
- */
-
- }, {
- key: 'fillBuffer_',
- value: function fillBuffer_() {
- if (this.sourceUpdater_.updating()) {
- return;
- }
-
- if (!this.syncPoint_) {
- this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
- }
-
- // see if we need to begin loading immediately
- var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
-
- if (!segmentInfo) {
- return;
- }
-
- var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex);
-
- if (isEndOfStream) {
- this.endOfStream();
- return;
- }
-
- if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) {
- return;
- }
-
- // We will need to change timestampOffset of the sourceBuffer if either of
- // the following conditions are true:
- // - The segment.timeline !== this.currentTimeline
- // (we are crossing a discontinuity somehow)
- // - The "timestampOffset" for the start of this segment is less than
- // the currently set timestampOffset
- // Also, clear captions if we are crossing a discontinuity boundary
- if (segmentInfo.timeline !== this.currentTimeline_ || segmentInfo.startOfSegment !== null && segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset()) {
- this.syncController_.reset();
- segmentInfo.timestampOffset = segmentInfo.startOfSegment;
- this.captionParser_.clearAllCaptions();
- }
-
- this.loadSegment_(segmentInfo);
- }
-
- /**
- * Determines what segment request should be made, given current playback
- * state.
- *
- * @param {TimeRanges} buffered - the state of the buffer
- * @param {Object} playlist - the playlist object to fetch segments from
- * @param {Number} mediaIndex - the previous mediaIndex fetched or null
- * @param {Boolean} hasPlayed - a flag indicating whether we have played or not
- * @param {Number} currentTime - the playback position in seconds
- * @param {Object} syncPoint - a segment info object that describes the
- * @returns {Object} a segment request object that describes the segment to load
- */
-
- }, {
- key: 'checkBuffer_',
- value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) {
- var lastBufferedEnd = 0;
- var startOfSegment = void 0;
-
- if (buffered.length) {
- lastBufferedEnd = buffered.end(buffered.length - 1);
- }
-
- var bufferedTime = Math.max(0, lastBufferedEnd - currentTime);
-
- if (!playlist.segments.length) {
- return null;
- }
-
- // if there is plenty of content buffered, and the video has
- // been played before relax for awhile
- if (bufferedTime >= this.goalBufferLength_()) {
- return null;
- }
-
- // if the video has not yet played once, and we already have
- // one segment downloaded do nothing
- if (!hasPlayed && bufferedTime >= 1) {
- return null;
- }
-
- // When the syncPoint is null, there is no way of determining a good
- // conservative segment index to fetch from
- // The best thing to do here is to get the kind of sync-point data by
- // making a request
- if (syncPoint === null) {
- mediaIndex = this.getSyncSegmentCandidate_(playlist);
- return this.generateSegmentInfo_(playlist, mediaIndex, null, true);
- }
-
- // Under normal playback conditions fetching is a simple walk forward
- if (mediaIndex !== null) {
- var segment = playlist.segments[mediaIndex];
-
- if (segment && segment.end) {
- startOfSegment = segment.end;
- } else {
- startOfSegment = lastBufferedEnd;
- }
- return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false);
- }
-
- // There is a sync-point but the lack of a mediaIndex indicates that
- // we need to make a good conservative guess about which segment to
- // fetch
- if (this.fetchAtBuffer_) {
- // Find the segment containing the end of the buffer
- var mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time);
-
- mediaIndex = mediaSourceInfo.mediaIndex;
- startOfSegment = mediaSourceInfo.startTime;
- } else {
- // Find the segment containing currentTime
- var _mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time);
-
- mediaIndex = _mediaSourceInfo.mediaIndex;
- startOfSegment = _mediaSourceInfo.startTime;
- }
-
- return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false);
- }
-
- /**
- * The segment loader has no recourse except to fetch a segment in the
- * current playlist and use the internal timestamps in that segment to
- * generate a syncPoint. This function returns a good candidate index
- * for that process.
- *
- * @param {Object} playlist - the playlist object to look for a
- * @returns {Number} An index of a segment from the playlist to load
- */
-
- }, {
- key: 'getSyncSegmentCandidate_',
- value: function getSyncSegmentCandidate_(playlist) {
- var _this2 = this;
-
- if (this.currentTimeline_ === -1) {
- return 0;
- }
-
- var segmentIndexArray = playlist.segments.map(function (s, i) {
- return {
- timeline: s.timeline,
- segmentIndex: i
- };
- }).filter(function (s) {
- return s.timeline === _this2.currentTimeline_;
- });
-
- if (segmentIndexArray.length) {
- return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex;
- }
-
- return Math.max(playlist.segments.length - 1, 0);
- }
- }, {
- key: 'generateSegmentInfo_',
- value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) {
- if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) {
- return null;
- }
-
- var segment = playlist.segments[mediaIndex];
-
- return {
- requestId: 'segment-loader-' + Math.random(),
- // resolve the segment URL relative to the playlist
- uri: segment.resolvedUri,
- // the segment's mediaIndex at the time it was requested
- mediaIndex: mediaIndex,
- // whether or not to update the SegmentLoader's state with this
- // segment's mediaIndex
- isSyncRequest: isSyncRequest,
- startOfSegment: startOfSegment,
- // the segment's playlist
- playlist: playlist,
- // unencrypted bytes of the segment
- bytes: null,
- // when a key is defined for this segment, the encrypted bytes
- encryptedBytes: null,
- // The target timestampOffset for this segment when we append it
- // to the source buffer
- timestampOffset: null,
- // The timeline that the segment is in
- timeline: segment.timeline,
- // The expected duration of the segment in seconds
- duration: segment.duration,
- // retain the segment in case the playlist updates while doing an async process
- segment: segment
- };
- }
-
- /**
- * Determines if the network has enough bandwidth to complete the current segment
- * request in a timely manner. If not, the request will be aborted early and bandwidth
- * updated to trigger a playlist switch.
- *
- * @param {Object} stats
- * Object containing stats about the request timing and size
- * @return {Boolean} True if the request was aborted, false otherwise
- * @private
- */
-
- }, {
- key: 'abortRequestEarly_',
- value: function abortRequestEarly_(stats) {
- if (this.hls_.tech_.paused() ||
- // Don't abort if the current playlist is on the lowestEnabledRendition
- // TODO: Replace using timeout with a boolean indicating whether this playlist is
- // the lowestEnabledRendition.
- !this.xhrOptions_.timeout ||
- // Don't abort if we have no bandwidth information to estimate segment sizes
- !this.playlist_.attributes.BANDWIDTH) {
- return false;
- }
-
- // Wait at least 1 second since the first byte of data has been received before
- // using the calculated bandwidth from the progress event to allow the bitrate
- // to stabilize
- if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
- return false;
- }
-
- var currentTime = this.currentTime_();
- var measuredBandwidth = stats.bandwidth;
- var segmentDuration = this.pendingSegment_.duration;
-
- var requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived);
-
- // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
- // if we are only left with less than 1 second when the request completes.
- // A negative timeUntilRebuffering indicates we are already rebuffering
- var timeUntilRebuffer$$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1;
-
- // Only consider aborting early if the estimated time to finish the download
- // is larger than the estimated time until the player runs out of forward buffer
- if (requestTimeRemaining <= timeUntilRebuffer$$1) {
- return false;
- }
-
- var switchCandidate = minRebufferMaxBandwidthSelector({
- master: this.hls_.playlists.master,
- currentTime: currentTime,
- bandwidth: measuredBandwidth,
- duration: this.duration_(),
- segmentDuration: segmentDuration,
- timeUntilRebuffer: timeUntilRebuffer$$1,
- currentTimeline: this.currentTimeline_,
- syncController: this.syncController_
- });
-
- if (!switchCandidate) {
- return;
- }
-
- var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$$1;
-
- var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
-
- var minimumTimeSaving = 0.5;
-
- // If we are already rebuffering, increase the amount of variance we add to the
- // potential round trip time of the new request so that we are not too aggressive
- // with switching to a playlist that might save us a fraction of a second.
- if (timeUntilRebuffer$$1 <= TIME_FUDGE_FACTOR) {
- minimumTimeSaving = 1;
- }
-
- if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
- return false;
- }
-
- // set the bandwidth to that of the desired playlist being sure to scale by
- // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
- // don't trigger a bandwidthupdate as the bandwidth is artifial
- this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1;
- this.abort();
- this.trigger('earlyabort');
- return true;
- }
-
- /**
- * XHR `progress` event handler
- *
- * @param {Event}
- * The XHR `progress` event
- * @param {Object} simpleSegment
- * A simplified segment object copy
- * @private
- */
-
- }, {
- key: 'handleProgress_',
- value: function handleProgress_(event, simpleSegment) {
- if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) {
- return;
- }
-
- this.trigger('progress');
- }
-
- /**
- * load a specific segment from a request into the buffer
- *
- * @private
- */
-
- }, {
- key: 'loadSegment_',
- value: function loadSegment_(segmentInfo) {
- this.state = 'WAITING';
- this.pendingSegment_ = segmentInfo;
- this.trimBackBuffer_(segmentInfo);
-
- segmentInfo.abortRequests = mediaSegmentRequest(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.captionParser_, this.createSimplifiedSegmentObj_(segmentInfo),
- // progress callback
- this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this));
- }
-
- /**
- * trim the back buffer so that we don't have too much data
- * in the source buffer
- *
- * @private
- *
- * @param {Object} segmentInfo - the current segment
- */
-
- }, {
- key: 'trimBackBuffer_',
- value: function trimBackBuffer_(segmentInfo) {
- var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10);
-
- // Chrome has a hard limit of 150MB of
- // buffer and a very conservative "garbage collector"
- // We manually clear out the old buffer to ensure
- // we don't trigger the QuotaExceeded error
- // on the source buffer during subsequent appends
-
- if (removeToTime > 0) {
- this.remove(0, removeToTime);
- }
- }
-
- /**
- * created a simplified copy of the segment object with just the
- * information necessary to perform the XHR and decryption
- *
- * @private
- *
- * @param {Object} segmentInfo - the current segment
- * @returns {Object} a simplified segment object copy
- */
-
- }, {
- key: 'createSimplifiedSegmentObj_',
- value: function createSimplifiedSegmentObj_(segmentInfo) {
- var segment = segmentInfo.segment;
- var simpleSegment = {
- resolvedUri: segment.resolvedUri,
- byterange: segment.byterange,
- requestId: segmentInfo.requestId
- };
-
- if (segment.key) {
- // if the media sequence is greater than 2^32, the IV will be incorrect
- // assuming 10s segments, that would be about 1300 years
- var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);
-
- simpleSegment.key = {
- resolvedUri: segment.key.resolvedUri,
- iv: iv
- };
- }
-
- if (segment.map) {
- simpleSegment.map = this.initSegment(segment.map);
- }
-
- return simpleSegment;
- }
-
- /**
- * Handle the callback from the segmentRequest function and set the
- * associated SegmentLoader state and errors if necessary
- *
- * @private
- */
-
- }, {
- key: 'segmentRequestFinished_',
- value: function segmentRequestFinished_(error, simpleSegment) {
- // every request counts as a media request even if it has been aborted
- // or canceled due to a timeout
- this.mediaRequests += 1;
-
- if (simpleSegment.stats) {
- this.mediaBytesTransferred += simpleSegment.stats.bytesReceived;
- this.mediaTransferDuration += simpleSegment.stats.roundTripTime;
- }
-
- // The request was aborted and the SegmentLoader has already been reset
- if (!this.pendingSegment_) {
- this.mediaRequestsAborted += 1;
- return;
- }
-
- // the request was aborted and the SegmentLoader has already started
- // another request. this can happen when the timeout for an aborted
- // request triggers due to a limitation in the XHR library
- // do not count this as any sort of request or we risk double-counting
- if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
- return;
- }
-
- // an error occurred from the active pendingSegment_ so reset everything
- if (error) {
- this.pendingSegment_ = null;
- this.state = 'READY';
-
- // the requests were aborted just record the aborted stat and exit
- // this is not a true error condition and nothing corrective needs
- // to be done
- if (error.code === REQUEST_ERRORS.ABORTED) {
- this.mediaRequestsAborted += 1;
- return;
- }
-
- this.pause();
-
- // the error is really just that at least one of the requests timed-out
- // set the bandwidth to a very low value and trigger an ABR switch to
- // take emergency action
- if (error.code === REQUEST_ERRORS.TIMEOUT) {
- this.mediaRequestsTimedout += 1;
- this.bandwidth = 1;
- this.roundTrip = NaN;
- this.trigger('bandwidthupdate');
- return;
- }
-
- // if control-flow has arrived here, then the error is real
- // emit an error event to blacklist the current playlist
- this.mediaRequestsErrored += 1;
- this.error(error);
- this.trigger('error');
- return;
- }
-
- // the response was a success so set any bandwidth stats the request
- // generated for ABR purposes
- this.bandwidth = simpleSegment.stats.bandwidth;
- this.roundTrip = simpleSegment.stats.roundTripTime;
-
- // if this request included an initialization segment, save that data
- // to the initSegment cache
- if (simpleSegment.map) {
- simpleSegment.map = this.initSegment(simpleSegment.map, true);
- }
-
- this.processSegmentResponse_(simpleSegment);
- }
-
- /**
- * Move any important data from the simplified segment object
- * back to the real segment object for future phases
- *
- * @private
- */
-
- }, {
- key: 'processSegmentResponse_',
- value: function processSegmentResponse_(simpleSegment) {
- var segmentInfo = this.pendingSegment_;
-
- segmentInfo.bytes = simpleSegment.bytes;
- if (simpleSegment.map) {
- segmentInfo.segment.map.bytes = simpleSegment.map.bytes;
- }
-
- segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
-
- // This has fmp4 captions, add them to text tracks
- if (simpleSegment.fmp4Captions) {
- createCaptionsTrackIfNotExists(this.inbandTextTracks_, this.hls_.tech_, simpleSegment.captionStreams);
- addCaptionData({
- inbandTextTracks: this.inbandTextTracks_,
- captionArray: simpleSegment.fmp4Captions,
- // fmp4s will not have a timestamp offset
- timestampOffset: 0
- });
- // Reset stored captions since we added parsed
- // captions to a text track at this point
- this.captionParser_.clearParsedCaptions();
- }
-
- this.handleSegment_();
- }
-
- /**
- * append a decrypted segement to the SourceBuffer through a SourceUpdater
- *
- * @private
- */
-
- }, {
- key: 'handleSegment_',
- value: function handleSegment_() {
- var _this3 = this;
-
- if (!this.pendingSegment_) {
- this.state = 'READY';
- return;
- }
-
- var segmentInfo = this.pendingSegment_;
- var segment = segmentInfo.segment;
- var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo);
-
- // When we have our first timing info, determine what media types this loader is
- // dealing with. Although we're maintaining extra state, it helps to preserve the
- // separation of segment loader from the actual source buffers.
- if (typeof this.startingMedia_ === 'undefined' && timingInfo && (
- // Guard against cases where we're not getting timing info at all until we are
- // certain that all streams will provide it.
- timingInfo.containsAudio || timingInfo.containsVideo)) {
- this.startingMedia_ = {
- containsAudio: timingInfo.containsAudio,
- containsVideo: timingInfo.containsVideo
- };
- }
-
- var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo);
-
- if (illegalMediaSwitchError) {
- this.error({
- message: illegalMediaSwitchError,
- blacklistDuration: Infinity
- });
- this.trigger('error');
- return;
- }
-
- if (segmentInfo.isSyncRequest) {
- this.trigger('syncinfoupdate');
- this.pendingSegment_ = null;
- this.state = 'READY';
- return;
- }
-
- if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
- this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
- // fired when a timestamp offset is set in HLS (can also identify discontinuities)
- this.trigger('timestampoffset');
- }
-
- var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline);
-
- if (timelineMapping !== null) {
- this.trigger({
- type: 'segmenttimemapping',
- mapping: timelineMapping
- });
- }
-
- this.state = 'APPENDING';
-
- // if the media initialization segment is changing, append it
- // before the content segment
- if (segment.map) {
- var initId = initSegmentId(segment.map);
-
- if (!this.activeInitSegmentId_ || this.activeInitSegmentId_ !== initId) {
- var initSegment = this.initSegment(segment.map);
-
- this.sourceUpdater_.appendBuffer(initSegment.bytes, function () {
- _this3.activeInitSegmentId_ = initId;
- });
- }
- }
-
- segmentInfo.byteLength = segmentInfo.bytes.byteLength;
- if (typeof segment.start === 'number' && typeof segment.end === 'number') {
- this.mediaSecondsLoaded += segment.end - segment.start;
- } else {
- this.mediaSecondsLoaded += segment.duration;
- }
-
- this.logger_(segmentInfoString(segmentInfo));
-
- this.sourceUpdater_.appendBuffer(segmentInfo.bytes, this.handleUpdateEnd_.bind(this));
- }
-
- /**
- * callback to run when appendBuffer is finished. detects if we are
- * in a good state to do things with the data we got, or if we need
- * to wait for more
- *
- * @private
- */
-
- }, {
- key: 'handleUpdateEnd_',
- value: function handleUpdateEnd_() {
- if (!this.pendingSegment_) {
- this.state = 'READY';
- if (!this.paused()) {
- this.monitorBuffer_();
- }
- return;
- }
-
- var segmentInfo = this.pendingSegment_;
- var segment = segmentInfo.segment;
- var isWalkingForward = this.mediaIndex !== null;
-
- this.pendingSegment_ = null;
- this.recordThroughput_(segmentInfo);
- this.addSegmentMetadataCue_(segmentInfo);
-
- this.state = 'READY';
-
- this.mediaIndex = segmentInfo.mediaIndex;
- this.fetchAtBuffer_ = true;
- this.currentTimeline_ = segmentInfo.timeline;
-
- // We must update the syncinfo to recalculate the seekable range before
- // the following conditional otherwise it may consider this a bad "guess"
- // and attempt to resync when the post-update seekable window and live
- // point would mean that this was the perfect segment to fetch
- this.trigger('syncinfoupdate');
-
- // If we previously appended a segment that ends more than 3 targetDurations before
- // the currentTime_ that means that our conservative guess was too conservative.
- // In that case, reset the loader state so that we try to use any information gained
- // from the previous request to create a new, more accurate, sync-point.
- if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) {
- this.resetEverything();
- return;
- }
-
- // Don't do a rendition switch unless we have enough time to get a sync segment
- // and conservatively guess
- if (isWalkingForward) {
- this.trigger('bandwidthupdate');
- }
- this.trigger('progress');
-
- // any time an update finishes and the last segment is in the
- // buffer, end the stream. this ensures the "ended" event will
- // fire if playback reaches that point.
- var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1);
-
- if (isEndOfStream) {
- this.endOfStream();
- }
-
- if (!this.paused()) {
- this.monitorBuffer_();
- }
- }
-
- /**
- * Records the current throughput of the decrypt, transmux, and append
- * portion of the semgment pipeline. `throughput.rate` is a the cumulative
- * moving average of the throughput. `throughput.count` is the number of
- * data points in the average.
- *
- * @private
- * @param {Object} segmentInfo the object returned by loadSegment
- */
-
- }, {
- key: 'recordThroughput_',
- value: function recordThroughput_(segmentInfo) {
- var rate = this.throughput.rate;
- // Add one to the time to ensure that we don't accidentally attempt to divide
- // by zero in the case where the throughput is ridiculously high
- var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1;
- // Multiply by 8000 to convert from bytes/millisecond to bits/second
- var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000);
-
- // This is just a cumulative moving average calculation:
- // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
- this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
- }
-
- /**
- * Adds a cue to the segment-metadata track with some metadata information about the
- * segment
- *
- * @private
- * @param {Object} segmentInfo
- * the object returned by loadSegment
- * @method addSegmentMetadataCue_
- */
-
- }, {
- key: 'addSegmentMetadataCue_',
- value: function addSegmentMetadataCue_(segmentInfo) {
- if (!this.segmentMetadataTrack_) {
- return;
- }
-
- var segment = segmentInfo.segment;
- var start = segment.start;
- var end = segment.end;
-
- // Do not try adding the cue if the start and end times are invalid.
- if (!finite(start) || !finite(end)) {
- return;
- }
-
- removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
-
- var Cue = window_1.WebKitDataCue || window_1.VTTCue;
- var value = {
- bandwidth: segmentInfo.playlist.attributes.BANDWIDTH,
- resolution: segmentInfo.playlist.attributes.RESOLUTION,
- codecs: segmentInfo.playlist.attributes.CODECS,
- byteLength: segmentInfo.byteLength,
- uri: segmentInfo.uri,
- timeline: segmentInfo.timeline,
- playlist: segmentInfo.playlist.uri,
- start: start,
- end: end
- };
- var data = JSON.stringify(value);
- var cue = new Cue(start, end, data);
-
- // Attach the metadata to the value property of the cue to keep consistency between
- // the differences of WebKitDataCue in safari and VTTCue in other browsers
- cue.value = value;
-
- this.segmentMetadataTrack_.addCue(cue);
- }
- }]);
- return SegmentLoader;
- }(videojs.EventTarget);
-
- var uint8ToUtf8 = function uint8ToUtf8(uintArray) {
- return decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray)));
- };
-
- /**
- * @file vtt-segment-loader.js
- */
-
- var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (char) {
- return char.charCodeAt(0);
- }));
-
- /**
- * An object that manages segment loading and appending.
- *
- * @class VTTSegmentLoader
- * @param {Object} options required and optional options
- * @extends videojs.EventTarget
- */
-
- var VTTSegmentLoader = function (_SegmentLoader) {
- inherits$1(VTTSegmentLoader, _SegmentLoader);
-
- function VTTSegmentLoader(settings) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- classCallCheck$1(this, VTTSegmentLoader);
-
- // SegmentLoader requires a MediaSource be specified or it will throw an error;
- // however, VTTSegmentLoader has no need of a media source, so delete the reference
- var _this = possibleConstructorReturn$1(this, (VTTSegmentLoader.__proto__ || Object.getPrototypeOf(VTTSegmentLoader)).call(this, settings, options));
-
- _this.mediaSource_ = null;
-
- _this.subtitlesTrack_ = null;
- return _this;
- }
-
- /**
- * Indicates which time ranges are buffered
- *
- * @return {TimeRange}
- * TimeRange object representing the current buffered ranges
- */
-
-
- createClass(VTTSegmentLoader, [{
- key: 'buffered_',
- value: function buffered_() {
- if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) {
- return videojs.createTimeRanges();
- }
-
- var cues = this.subtitlesTrack_.cues;
- var start = cues[0].startTime;
- var end = cues[cues.length - 1].startTime;
-
- return videojs.createTimeRanges([[start, end]]);
- }
-
- /**
- * Gets and sets init segment for the provided map
- *
- * @param {Object} map
- * The map object representing the init segment to get or set
- * @param {Boolean=} set
- * If true, the init segment for the provided map should be saved
- * @return {Object}
- * map object for desired init segment
- */
-
- }, {
- key: 'initSegment',
- value: function initSegment(map) {
- var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-
- if (!map) {
- return null;
- }
-
- var id = initSegmentId(map);
- var storedMap = this.initSegments_[id];
-
- if (set$$1 && !storedMap && map.bytes) {
- // append WebVTT line terminators to the media initialization segment if it exists
- // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that
- // requires two or more WebVTT line terminators between the WebVTT header and the
- // rest of the file
- var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
- var combinedSegment = new Uint8Array(combinedByteLength);
-
- combinedSegment.set(map.bytes);
- combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
-
- this.initSegments_[id] = storedMap = {
- resolvedUri: map.resolvedUri,
- byterange: map.byterange,
- bytes: combinedSegment
- };
- }
-
- return storedMap || map;
- }
-
- /**
- * Returns true if all configuration required for loading is present, otherwise false.
- *
- * @return {Boolean} True if the all configuration is ready for loading
- * @private
- */
-
- }, {
- key: 'couldBeginLoading_',
- value: function couldBeginLoading_() {
- return this.playlist_ && this.subtitlesTrack_ && !this.paused();
- }
-
- /**
- * Once all the starting parameters have been specified, begin
- * operation. This method should only be invoked from the INIT
- * state.
- *
- * @private
- */
-
- }, {
- key: 'init_',
- value: function init_() {
- this.state = 'READY';
- this.resetEverything();
- return this.monitorBuffer_();
- }
-
- /**
- * Set a subtitle track on the segment loader to add subtitles to
- *
- * @param {TextTrack=} track
- * The text track to add loaded subtitles to
- * @return {TextTrack}
- * Returns the subtitles track
- */
-
- }, {
- key: 'track',
- value: function track(_track) {
- if (typeof _track === 'undefined') {
- return this.subtitlesTrack_;
- }
-
- this.subtitlesTrack_ = _track;
-
- // if we were unpaused but waiting for a sourceUpdater, start
- // buffering now
- if (this.state === 'INIT' && this.couldBeginLoading_()) {
- this.init_();
- }
-
- return this.subtitlesTrack_;
- }
-
- /**
- * Remove any data in the source buffer between start and end times
- * @param {Number} start - the start time of the region to remove from the buffer
- * @param {Number} end - the end time of the region to remove from the buffer
- */
-
- }, {
- key: 'remove',
- value: function remove(start, end) {
- removeCuesFromTrack(start, end, this.subtitlesTrack_);
- }
-
- /**
- * fill the buffer with segements unless the sourceBuffers are
- * currently updating
- *
- * Note: this function should only ever be called by monitorBuffer_
- * and never directly
- *
- * @private
- */
-
- }, {
- key: 'fillBuffer_',
- value: function fillBuffer_() {
- var _this2 = this;
-
- if (!this.syncPoint_) {
- this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
- }
-
- // see if we need to begin loading immediately
- var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
-
- segmentInfo = this.skipEmptySegments_(segmentInfo);
-
- if (!segmentInfo) {
- return;
- }
-
- if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
- // We don't have the timestamp offset that we need to sync subtitles.
- // Rerun on a timestamp offset or user interaction.
- var checkTimestampOffset = function checkTimestampOffset() {
- _this2.state = 'READY';
- if (!_this2.paused()) {
- // if not paused, queue a buffer check as soon as possible
- _this2.monitorBuffer_();
- }
- };
-
- this.syncController_.one('timestampoffset', checkTimestampOffset);
- this.state = 'WAITING_ON_TIMELINE';
- return;
- }
-
- this.loadSegment_(segmentInfo);
- }
-
- /**
- * Prevents the segment loader from requesting segments we know contain no subtitles
- * by walking forward until we find the next segment that we don't know whether it is
- * empty or not.
- *
- * @param {Object} segmentInfo
- * a segment info object that describes the current segment
- * @return {Object}
- * a segment info object that describes the current segment
- */
-
- }, {
- key: 'skipEmptySegments_',
- value: function skipEmptySegments_(segmentInfo) {
- while (segmentInfo && segmentInfo.segment.empty) {
- segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest);
- }
- return segmentInfo;
- }
-
- /**
- * append a decrypted segement to the SourceBuffer through a SourceUpdater
- *
- * @private
- */
-
- }, {
- key: 'handleSegment_',
- value: function handleSegment_() {
- var _this3 = this;
-
- if (!this.pendingSegment_ || !this.subtitlesTrack_) {
- this.state = 'READY';
- return;
- }
-
- this.state = 'APPENDING';
-
- var segmentInfo = this.pendingSegment_;
- var segment = segmentInfo.segment;
-
- // Make sure that vttjs has loaded, otherwise, wait till it finished loading
- if (typeof window_1.WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) {
-
- var loadHandler = function loadHandler() {
- _this3.handleSegment_();
- };
-
- this.state = 'WAITING_ON_VTTJS';
- this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler);
- this.subtitlesTrack_.tech_.one('vttjserror', function () {
- _this3.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler);
- _this3.error({
- message: 'Error loading vtt.js'
- });
- _this3.state = 'READY';
- _this3.pause();
- _this3.trigger('error');
- });
-
- return;
- }
-
- segment.requested = true;
-
- try {
- this.parseVTTCues_(segmentInfo);
- } catch (e) {
- this.error({
- message: e.message
- });
- this.state = 'READY';
- this.pause();
- return this.trigger('error');
- }
-
- this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_);
-
- if (segmentInfo.isSyncRequest) {
- this.trigger('syncinfoupdate');
- this.pendingSegment_ = null;
- this.state = 'READY';
- return;
- }
-
- segmentInfo.byteLength = segmentInfo.bytes.byteLength;
-
- this.mediaSecondsLoaded += segment.duration;
-
- if (segmentInfo.cues.length) {
- // remove any overlapping cues to prevent doubling
- this.remove(segmentInfo.cues[0].endTime, segmentInfo.cues[segmentInfo.cues.length - 1].endTime);
- }
-
- segmentInfo.cues.forEach(function (cue) {
- _this3.subtitlesTrack_.addCue(cue);
- });
-
- this.handleUpdateEnd_();
- }
-
- /**
- * Uses the WebVTT parser to parse the segment response
- *
- * @param {Object} segmentInfo
- * a segment info object that describes the current segment
- * @private
- */
-
- }, {
- key: 'parseVTTCues_',
- value: function parseVTTCues_(segmentInfo) {
- var decoder = void 0;
- var decodeBytesToString = false;
-
- if (typeof window_1.TextDecoder === 'function') {
- decoder = new window_1.TextDecoder('utf8');
- } else {
- decoder = window_1.WebVTT.StringDecoder();
- decodeBytesToString = true;
- }
-
- var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, decoder);
-
- segmentInfo.cues = [];
- segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
-
- parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
- parser.ontimestampmap = function (map) {
- return segmentInfo.timestampmap = map;
- };
- parser.onparsingerror = function (error) {
- videojs.log.warn('Error encountered when parsing cues: ' + error.message);
- };
-
- if (segmentInfo.segment.map) {
- var mapData = segmentInfo.segment.map.bytes;
-
- if (decodeBytesToString) {
- mapData = uint8ToUtf8(mapData);
- }
-
- parser.parse(mapData);
- }
-
- var segmentData = segmentInfo.bytes;
-
- if (decodeBytesToString) {
- segmentData = uint8ToUtf8(segmentData);
- }
-
- parser.parse(segmentData);
- parser.flush();
- }
-
- /**
- * Updates the start and end times of any cues parsed by the WebVTT parser using
- * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping
- * from the SyncController
- *
- * @param {Object} segmentInfo
- * a segment info object that describes the current segment
- * @param {Object} mappingObj
- * object containing a mapping from TS to media time
- * @param {Object} playlist
- * the playlist object containing the segment
- * @private
- */
-
- }, {
- key: 'updateTimeMapping_',
- value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) {
- var segment = segmentInfo.segment;
-
- if (!mappingObj) {
- // If the sync controller does not have a mapping of TS to Media Time for the
- // timeline, then we don't have enough information to update the cue
- // start/end times
- return;
- }
-
- if (!segmentInfo.cues.length) {
- // If there are no cues, we also do not have enough information to figure out
- // segment timing. Mark that the segment contains no cues so we don't re-request
- // an empty segment.
- segment.empty = true;
- return;
- }
-
- var timestampmap = segmentInfo.timestampmap;
- var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping;
-
- segmentInfo.cues.forEach(function (cue) {
- // First convert cue time to TS time using the timestamp-map provided within the vtt
- cue.startTime += diff;
- cue.endTime += diff;
- });
-
- if (!playlist.syncInfo) {
- var firstStart = segmentInfo.cues[0].startTime;
- var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
-
- playlist.syncInfo = {
- mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
- time: Math.min(firstStart, lastStart - segment.duration)
- };
- }
- }
- }]);
- return VTTSegmentLoader;
- }(SegmentLoader);
-
- /**
- * @file ad-cue-tags.js
- */
-
- /**
- * Searches for an ad cue that overlaps with the given mediaTime
- */
- var findAdCue = function findAdCue(track, mediaTime) {
- var cues = track.cues;
-
- for (var i = 0; i < cues.length; i++) {
- var cue = cues[i];
-
- if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) {
- return cue;
- }
- }
- return null;
- };
-
- var updateAdCues = function updateAdCues(media, track) {
- var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
-
- if (!media.segments) {
- return;
- }
-
- var mediaTime = offset;
- var cue = void 0;
-
- for (var i = 0; i < media.segments.length; i++) {
- var segment = media.segments[i];
-
- if (!cue) {
- // Since the cues will span for at least the segment duration, adding a fudge
- // factor of half segment duration will prevent duplicate cues from being
- // created when timing info is not exact (e.g. cue start time initialized
- // at 10.006677, but next call mediaTime is 10.003332 )
- cue = findAdCue(track, mediaTime + segment.duration / 2);
- }
-
- if (cue) {
- if ('cueIn' in segment) {
- // Found a CUE-IN so end the cue
- cue.endTime = mediaTime;
- cue.adEndTime = mediaTime;
- mediaTime += segment.duration;
- cue = null;
- continue;
- }
-
- if (mediaTime < cue.endTime) {
- // Already processed this mediaTime for this cue
- mediaTime += segment.duration;
- continue;
- }
-
- // otherwise extend cue until a CUE-IN is found
- cue.endTime += segment.duration;
- } else {
- if ('cueOut' in segment) {
- cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut);
- cue.adStartTime = mediaTime;
- // Assumes tag format to be
- // #EXT-X-CUE-OUT:30
- cue.adEndTime = mediaTime + parseFloat(segment.cueOut);
- track.addCue(cue);
- }
-
- if ('cueOutCont' in segment) {
- // Entered into the middle of an ad cue
- var adOffset = void 0;
- var adTotal = void 0;
-
- // Assumes tag formate to be
- // #EXT-X-CUE-OUT-CONT:10/30
-
- var _segment$cueOutCont$s = segment.cueOutCont.split('/').map(parseFloat);
-
- var _segment$cueOutCont$s2 = slicedToArray(_segment$cueOutCont$s, 2);
-
- adOffset = _segment$cueOutCont$s2[0];
- adTotal = _segment$cueOutCont$s2[1];
-
-
- cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, '');
- cue.adStartTime = mediaTime - adOffset;
- cue.adEndTime = cue.adStartTime + adTotal;
- track.addCue(cue);
- }
- }
- mediaTime += segment.duration;
- }
- };
-
- var parsePid = function parsePid(packet) {
- var pid = packet[1] & 0x1f;
- pid <<= 8;
- pid |= packet[2];
- return pid;
- };
-
- var parsePayloadUnitStartIndicator = function parsePayloadUnitStartIndicator(packet) {
- return !!(packet[1] & 0x40);
- };
-
- var parseAdaptionField = function parseAdaptionField(packet) {
- var offset = 0;
- // if an adaption field is present, its length is specified by the
- // fifth byte of the TS packet header. The adaptation field is
- // used to add stuffing to PES packets that don't fill a complete
- // TS packet, and to specify some forms of timing and control data
- // that we do not currently use.
- if ((packet[3] & 0x30) >>> 4 > 0x01) {
- offset += packet[4] + 1;
- }
- return offset;
- };
-
- var parseType$2 = function parseType(packet, pmtPid) {
- var pid = parsePid(packet);
- if (pid === 0) {
- return 'pat';
- } else if (pid === pmtPid) {
- return 'pmt';
- } else if (pmtPid) {
- return 'pes';
- }
- return null;
- };
-
- var parsePat = function parsePat(packet) {
- var pusi = parsePayloadUnitStartIndicator(packet);
- var offset = 4 + parseAdaptionField(packet);
-
- if (pusi) {
- offset += packet[offset] + 1;
- }
-
- return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11];
- };
-
- var parsePmt = function parsePmt(packet) {
- var programMapTable = {};
- var pusi = parsePayloadUnitStartIndicator(packet);
- var payloadOffset = 4 + parseAdaptionField(packet);
-
- if (pusi) {
- payloadOffset += packet[payloadOffset] + 1;
- }
-
- // PMTs can be sent ahead of the time when they should actually
- // take effect. We don't believe this should ever be the case
- // for HLS but we'll ignore "forward" PMT declarations if we see
- // them. Future PMT declarations have the current_next_indicator
- // set to zero.
- if (!(packet[payloadOffset + 5] & 0x01)) {
- return;
- }
-
- var sectionLength, tableEnd, programInfoLength;
- // the mapping table ends at the end of the current section
- sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2];
- tableEnd = 3 + sectionLength - 4;
-
- // to determine where the table is, we have to figure out how
- // long the program info descriptors are
- programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11];
-
- // advance the offset to the first entry in the mapping table
- var offset = 12 + programInfoLength;
- while (offset < tableEnd) {
- var i = payloadOffset + offset;
- // add an entry that maps the elementary_pid to the stream_type
- programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i];
-
- // move to the next table entry
- // skip past the elementary stream descriptors, if present
- offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5;
- }
- return programMapTable;
- };
-
- var parsePesType = function parsePesType(packet, programMapTable) {
- var pid = parsePid(packet);
- var type = programMapTable[pid];
- switch (type) {
- case streamTypes.H264_STREAM_TYPE:
- return 'video';
- case streamTypes.ADTS_STREAM_TYPE:
- return 'audio';
- case streamTypes.METADATA_STREAM_TYPE:
- return 'timed-metadata';
- default:
- return null;
- }
- };
-
- var parsePesTime = function parsePesTime(packet) {
- var pusi = parsePayloadUnitStartIndicator(packet);
- if (!pusi) {
- return null;
- }
-
- var offset = 4 + parseAdaptionField(packet);
-
- if (offset >= packet.byteLength) {
- // From the H 222.0 MPEG-TS spec
- // "For transport stream packets carrying PES packets, stuffing is needed when there
- // is insufficient PES packet data to completely fill the transport stream packet
- // payload bytes. Stuffing is accomplished by defining an adaptation field longer than
- // the sum of the lengths of the data elements in it, so that the payload bytes
- // remaining after the adaptation field exactly accommodates the available PES packet
- // data."
- //
- // If the offset is >= the length of the packet, then the packet contains no data
- // and instead is just adaption field stuffing bytes
- return null;
- }
-
- var pes = null;
- var ptsDtsFlags;
-
- // PES packets may be annotated with a PTS value, or a PTS value
- // and a DTS value. Determine what combination of values is
- // available to work with.
- ptsDtsFlags = packet[offset + 7];
-
- // PTS and DTS are normally stored as a 33-bit number. Javascript
- // performs all bitwise operations on 32-bit integers but javascript
- // supports a much greater range (52-bits) of integer using standard
- // mathematical operations.
- // We construct a 31-bit value using bitwise operators over the 31
- // most significant bits and then multiply by 4 (equal to a left-shift
- // of 2) before we add the final 2 least significant bits of the
- // timestamp (equal to an OR.)
- if (ptsDtsFlags & 0xC0) {
- pes = {};
- // the PTS and DTS are not written out directly. For information
- // on how they are encoded, see
- // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
- pes.pts = (packet[offset + 9] & 0x0E) << 27 | (packet[offset + 10] & 0xFF) << 20 | (packet[offset + 11] & 0xFE) << 12 | (packet[offset + 12] & 0xFF) << 5 | (packet[offset + 13] & 0xFE) >>> 3;
- pes.pts *= 4; // Left shift by 2
- pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs
- pes.dts = pes.pts;
- if (ptsDtsFlags & 0x40) {
- pes.dts = (packet[offset + 14] & 0x0E) << 27 | (packet[offset + 15] & 0xFF) << 20 | (packet[offset + 16] & 0xFE) << 12 | (packet[offset + 17] & 0xFF) << 5 | (packet[offset + 18] & 0xFE) >>> 3;
- pes.dts *= 4; // Left shift by 2
- pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs
- }
- }
- return pes;
- };
-
- var parseNalUnitType = function parseNalUnitType(type) {
- switch (type) {
- case 0x05:
- return 'slice_layer_without_partitioning_rbsp_idr';
- case 0x06:
- return 'sei_rbsp';
- case 0x07:
- return 'seq_parameter_set_rbsp';
- case 0x08:
- return 'pic_parameter_set_rbsp';
- case 0x09:
- return 'access_unit_delimiter_rbsp';
- default:
- return null;
- }
- };
-
- var videoPacketContainsKeyFrame = function videoPacketContainsKeyFrame(packet) {
- var offset = 4 + parseAdaptionField(packet);
- var frameBuffer = packet.subarray(offset);
- var frameI = 0;
- var frameSyncPoint = 0;
- var foundKeyFrame = false;
- var nalType;
-
- // advance the sync point to a NAL start, if necessary
- for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) {
- if (frameBuffer[frameSyncPoint + 2] === 1) {
- // the sync point is properly aligned
- frameI = frameSyncPoint + 5;
- break;
- }
- }
-
- while (frameI < frameBuffer.byteLength) {
- // look at the current byte to determine if we've hit the end of
- // a NAL unit boundary
- switch (frameBuffer[frameI]) {
- case 0:
- // skip past non-sync sequences
- if (frameBuffer[frameI - 1] !== 0) {
- frameI += 2;
- break;
- } else if (frameBuffer[frameI - 2] !== 0) {
- frameI++;
- break;
- }
-
- if (frameSyncPoint + 3 !== frameI - 2) {
- nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
- if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
- foundKeyFrame = true;
- }
- }
-
- // drop trailing zeroes
- do {
- frameI++;
- } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length);
- frameSyncPoint = frameI - 2;
- frameI += 3;
- break;
- case 1:
- // skip past non-sync sequences
- if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) {
- frameI += 3;
- break;
- }
-
- nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
- if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
- foundKeyFrame = true;
- }
- frameSyncPoint = frameI - 2;
- frameI += 3;
- break;
- default:
- // the current byte isn't a one or zero, so it cannot be part
- // of a sync sequence
- frameI += 3;
- break;
- }
- }
- frameBuffer = frameBuffer.subarray(frameSyncPoint);
- frameI -= frameSyncPoint;
- frameSyncPoint = 0;
- // parse the final nal
- if (frameBuffer && frameBuffer.byteLength > 3) {
- nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
- if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
- foundKeyFrame = true;
- }
- }
-
- return foundKeyFrame;
- };
-
- var probe$1 = {
- parseType: parseType$2,
- parsePat: parsePat,
- parsePmt: parsePmt,
- parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator,
- parsePesType: parsePesType,
- parsePesTime: parsePesTime,
- videoPacketContainsKeyFrame: videoPacketContainsKeyFrame
- };
-
- /**
- * mux.js
- *
- * Copyright (c) 2016 Brightcove
- * All rights reserved.
- *
- * Utilities to detect basic properties and metadata about Aac data.
- */
-
- var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
-
- var parseSyncSafeInteger$1 = function parseSyncSafeInteger(data) {
- return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
- };
-
- // return a percent-encoded representation of the specified byte range
- // @see http://en.wikipedia.org/wiki/Percent-encoding
- var percentEncode$1 = function percentEncode(bytes, start, end) {
- var i,
- result = '';
- for (i = start; i < end; i++) {
- result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
- }
- return result;
- };
-
- // return the string representation of the specified byte range,
- // interpreted as ISO-8859-1.
- var parseIso88591$1 = function parseIso88591(bytes, start, end) {
- return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line
- };
-
- var parseId3TagSize = function parseId3TagSize(header, byteIndex) {
- var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9],
- flags = header[byteIndex + 5],
- footerPresent = (flags & 16) >> 4;
-
- if (footerPresent) {
- return returnSize + 20;
- }
- return returnSize + 10;
- };
-
- var parseAdtsSize = function parseAdtsSize(header, byteIndex) {
- var lowThree = (header[byteIndex + 5] & 0xE0) >> 5,
- middle = header[byteIndex + 4] << 3,
- highTwo = header[byteIndex + 3] & 0x3 << 11;
-
- return highTwo | middle | lowThree;
- };
-
- var parseType$3 = function parseType(header, byteIndex) {
- if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) {
- return 'timed-metadata';
- } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) {
- return 'audio';
- }
- return null;
- };
-
- var parseSampleRate = function parseSampleRate(packet) {
- var i = 0;
-
- while (i + 5 < packet.length) {
- if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) {
- // If a valid header was not found, jump one forward and attempt to
- // find a valid ADTS header starting at the next byte
- i++;
- continue;
- }
- return ADTS_SAMPLING_FREQUENCIES$1[(packet[i + 2] & 0x3c) >>> 2];
- }
-
- return null;
- };
-
- var parseAacTimestamp = function parseAacTimestamp(packet) {
- var frameStart, frameSize, frame, frameHeader;
-
- // find the start of the first frame and the end of the tag
- frameStart = 10;
- if (packet[5] & 0x40) {
- // advance the frame start past the extended header
- frameStart += 4; // header size field
- frameStart += parseSyncSafeInteger$1(packet.subarray(10, 14));
- }
-
- // parse one or more ID3 frames
- // http://id3.org/id3v2.3.0#ID3v2_frame_overview
- do {
- // determine the number of bytes in this frame
- frameSize = parseSyncSafeInteger$1(packet.subarray(frameStart + 4, frameStart + 8));
- if (frameSize < 1) {
- return null;
- }
- frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]);
-
- if (frameHeader === 'PRIV') {
- frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10);
-
- for (var i = 0; i < frame.byteLength; i++) {
- if (frame[i] === 0) {
- var owner = parseIso88591$1(frame, 0, i);
- if (owner === 'com.apple.streaming.transportStreamTimestamp') {
- var d = frame.subarray(i + 1);
- var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
- size *= 4;
- size += d[7] & 0x03;
-
- return size;
- }
- break;
- }
- }
- }
-
- frameStart += 10; // advance past the frame header
- frameStart += frameSize; // advance past the frame body
- } while (frameStart < packet.byteLength);
- return null;
- };
-
- var probe$2 = {
- parseId3TagSize: parseId3TagSize,
- parseAdtsSize: parseAdtsSize,
- parseType: parseType$3,
- parseSampleRate: parseSampleRate,
- parseAacTimestamp: parseAacTimestamp
- };
-
- var handleRollover$1 = timestampRolloverStream.handleRollover;
- var probe$3 = {};
- probe$3.ts = probe$1;
- probe$3.aac = probe$2;
-
- var PES_TIMESCALE = 90000,
- MP2T_PACKET_LENGTH$1 = 188,
- // bytes
- SYNC_BYTE$1 = 0x47;
-
- var isLikelyAacData$1 = function isLikelyAacData(data) {
- if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) {
- return true;
- }
- return false;
- };
-
- /**
- * walks through segment data looking for pat and pmt packets to parse out
- * program map table information
- */
- var parsePsi_ = function parsePsi_(bytes, pmt) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH$1,
- packet,
- type;
-
- while (endIndex < bytes.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (bytes[startIndex] === SYNC_BYTE$1 && bytes[endIndex] === SYNC_BYTE$1) {
- // We found a packet
- packet = bytes.subarray(startIndex, endIndex);
- type = probe$3.ts.parseType(packet, pmt.pid);
-
- switch (type) {
- case 'pat':
- if (!pmt.pid) {
- pmt.pid = probe$3.ts.parsePat(packet);
- }
- break;
- case 'pmt':
- if (!pmt.table) {
- pmt.table = probe$3.ts.parsePmt(packet);
- }
- break;
- default:
- break;
- }
-
- // Found the pat and pmt, we can stop walking the segment
- if (pmt.pid && pmt.table) {
- return;
- }
-
- startIndex += MP2T_PACKET_LENGTH$1;
- endIndex += MP2T_PACKET_LENGTH$1;
- continue;
- }
-
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- }
- };
-
- /**
- * walks through the segment data from the start and end to get timing information
- * for the first and last audio pes packets
- */
- var parseAudioPes_ = function parseAudioPes_(bytes, pmt, result) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH$1,
- packet,
- type,
- pesType,
- pusi,
- parsed;
-
- var endLoop = false;
-
- // Start walking from start of segment to get first audio packet
- while (endIndex < bytes.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (bytes[startIndex] === SYNC_BYTE$1 && bytes[endIndex] === SYNC_BYTE$1) {
- // We found a packet
- packet = bytes.subarray(startIndex, endIndex);
- type = probe$3.ts.parseType(packet, pmt.pid);
-
- switch (type) {
- case 'pes':
- pesType = probe$3.ts.parsePesType(packet, pmt.table);
- pusi = probe$3.ts.parsePayloadUnitStartIndicator(packet);
- if (pesType === 'audio' && pusi) {
- parsed = probe$3.ts.parsePesTime(packet);
- if (parsed) {
- parsed.type = 'audio';
- result.audio.push(parsed);
- endLoop = true;
- }
- }
- break;
- default:
- break;
- }
-
- if (endLoop) {
- break;
- }
-
- startIndex += MP2T_PACKET_LENGTH$1;
- endIndex += MP2T_PACKET_LENGTH$1;
- continue;
- }
-
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- }
-
- // Start walking from end of segment to get last audio packet
- endIndex = bytes.byteLength;
- startIndex = endIndex - MP2T_PACKET_LENGTH$1;
- endLoop = false;
- while (startIndex >= 0) {
- // Look for a pair of start and end sync bytes in the data..
- if (bytes[startIndex] === SYNC_BYTE$1 && bytes[endIndex] === SYNC_BYTE$1) {
- // We found a packet
- packet = bytes.subarray(startIndex, endIndex);
- type = probe$3.ts.parseType(packet, pmt.pid);
-
- switch (type) {
- case 'pes':
- pesType = probe$3.ts.parsePesType(packet, pmt.table);
- pusi = probe$3.ts.parsePayloadUnitStartIndicator(packet);
- if (pesType === 'audio' && pusi) {
- parsed = probe$3.ts.parsePesTime(packet);
- if (parsed) {
- parsed.type = 'audio';
- result.audio.push(parsed);
- endLoop = true;
- }
- }
- break;
- default:
- break;
- }
-
- if (endLoop) {
- break;
- }
-
- startIndex -= MP2T_PACKET_LENGTH$1;
- endIndex -= MP2T_PACKET_LENGTH$1;
- continue;
- }
-
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex--;
- endIndex--;
- }
- };
-
- /**
- * walks through the segment data from the start and end to get timing information
- * for the first and last video pes packets as well as timing information for the first
- * key frame.
- */
- var parseVideoPes_ = function parseVideoPes_(bytes, pmt, result) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH$1,
- packet,
- type,
- pesType,
- pusi,
- parsed,
- frame,
- i,
- pes;
-
- var endLoop = false;
-
- var currentFrame = {
- data: [],
- size: 0
- };
-
- // Start walking from start of segment to get first video packet
- while (endIndex < bytes.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (bytes[startIndex] === SYNC_BYTE$1 && bytes[endIndex] === SYNC_BYTE$1) {
- // We found a packet
- packet = bytes.subarray(startIndex, endIndex);
- type = probe$3.ts.parseType(packet, pmt.pid);
-
- switch (type) {
- case 'pes':
- pesType = probe$3.ts.parsePesType(packet, pmt.table);
- pusi = probe$3.ts.parsePayloadUnitStartIndicator(packet);
- if (pesType === 'video') {
- if (pusi && !endLoop) {
- parsed = probe$3.ts.parsePesTime(packet);
- if (parsed) {
- parsed.type = 'video';
- result.video.push(parsed);
- endLoop = true;
- }
- }
- if (!result.firstKeyFrame) {
- if (pusi) {
- if (currentFrame.size !== 0) {
- frame = new Uint8Array(currentFrame.size);
- i = 0;
- while (currentFrame.data.length) {
- pes = currentFrame.data.shift();
- frame.set(pes, i);
- i += pes.byteLength;
- }
- if (probe$3.ts.videoPacketContainsKeyFrame(frame)) {
- result.firstKeyFrame = probe$3.ts.parsePesTime(frame);
- result.firstKeyFrame.type = 'video';
- }
- currentFrame.size = 0;
- }
- }
- currentFrame.data.push(packet);
- currentFrame.size += packet.byteLength;
- }
- }
- break;
- default:
- break;
- }
-
- if (endLoop && result.firstKeyFrame) {
- break;
- }
-
- startIndex += MP2T_PACKET_LENGTH$1;
- endIndex += MP2T_PACKET_LENGTH$1;
- continue;
- }
-
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- }
-
- // Start walking from end of segment to get last video packet
- endIndex = bytes.byteLength;
- startIndex = endIndex - MP2T_PACKET_LENGTH$1;
- endLoop = false;
- while (startIndex >= 0) {
- // Look for a pair of start and end sync bytes in the data..
- if (bytes[startIndex] === SYNC_BYTE$1 && bytes[endIndex] === SYNC_BYTE$1) {
- // We found a packet
- packet = bytes.subarray(startIndex, endIndex);
- type = probe$3.ts.parseType(packet, pmt.pid);
-
- switch (type) {
- case 'pes':
- pesType = probe$3.ts.parsePesType(packet, pmt.table);
- pusi = probe$3.ts.parsePayloadUnitStartIndicator(packet);
- if (pesType === 'video' && pusi) {
- parsed = probe$3.ts.parsePesTime(packet);
- if (parsed) {
- parsed.type = 'video';
- result.video.push(parsed);
- endLoop = true;
- }
- }
- break;
- default:
- break;
- }
-
- if (endLoop) {
- break;
- }
-
- startIndex -= MP2T_PACKET_LENGTH$1;
- endIndex -= MP2T_PACKET_LENGTH$1;
- continue;
- }
-
- // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex--;
- endIndex--;
- }
- };
-
- /**
- * Adjusts the timestamp information for the segment to account for
- * rollover and convert to seconds based on pes packet timescale (90khz clock)
- */
- var adjustTimestamp_ = function adjustTimestamp_(segmentInfo, baseTimestamp) {
- if (segmentInfo.audio && segmentInfo.audio.length) {
- var audioBaseTimestamp = baseTimestamp;
- if (typeof audioBaseTimestamp === 'undefined') {
- audioBaseTimestamp = segmentInfo.audio[0].dts;
- }
- segmentInfo.audio.forEach(function (info) {
- info.dts = handleRollover$1(info.dts, audioBaseTimestamp);
- info.pts = handleRollover$1(info.pts, audioBaseTimestamp);
- // time in seconds
- info.dtsTime = info.dts / PES_TIMESCALE;
- info.ptsTime = info.pts / PES_TIMESCALE;
- });
- }
-
- if (segmentInfo.video && segmentInfo.video.length) {
- var videoBaseTimestamp = baseTimestamp;
- if (typeof videoBaseTimestamp === 'undefined') {
- videoBaseTimestamp = segmentInfo.video[0].dts;
- }
- segmentInfo.video.forEach(function (info) {
- info.dts = handleRollover$1(info.dts, videoBaseTimestamp);
- info.pts = handleRollover$1(info.pts, videoBaseTimestamp);
- // time in seconds
- info.dtsTime = info.dts / PES_TIMESCALE;
- info.ptsTime = info.pts / PES_TIMESCALE;
- });
- if (segmentInfo.firstKeyFrame) {
- var frame = segmentInfo.firstKeyFrame;
- frame.dts = handleRollover$1(frame.dts, videoBaseTimestamp);
- frame.pts = handleRollover$1(frame.pts, videoBaseTimestamp);
- // time in seconds
- frame.dtsTime = frame.dts / PES_TIMESCALE;
- frame.ptsTime = frame.dts / PES_TIMESCALE;
- }
- }
- };
-
- /**
- * inspects the aac data stream for start and end time information
- */
- var inspectAac_ = function inspectAac_(bytes) {
- var endLoop = false,
- audioCount = 0,
- sampleRate = null,
- timestamp = null,
- frameSize = 0,
- byteIndex = 0,
- packet;
-
- while (bytes.length - byteIndex >= 3) {
- var type = probe$3.aac.parseType(bytes, byteIndex);
- switch (type) {
- case 'timed-metadata':
- // Exit early because we don't have enough to parse
- // the ID3 tag header
- if (bytes.length - byteIndex < 10) {
- endLoop = true;
- break;
- }
-
- frameSize = probe$3.aac.parseId3TagSize(bytes, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > bytes.length) {
- endLoop = true;
- break;
- }
- if (timestamp === null) {
- packet = bytes.subarray(byteIndex, byteIndex + frameSize);
- timestamp = probe$3.aac.parseAacTimestamp(packet);
- }
- byteIndex += frameSize;
- break;
- case 'audio':
- // Exit early because we don't have enough to parse
- // the ADTS frame header
- if (bytes.length - byteIndex < 7) {
- endLoop = true;
- break;
- }
-
- frameSize = probe$3.aac.parseAdtsSize(bytes, byteIndex);
-
- // Exit early if we don't have enough in the buffer
- // to emit a full packet
- if (frameSize > bytes.length) {
- endLoop = true;
- break;
- }
- if (sampleRate === null) {
- packet = bytes.subarray(byteIndex, byteIndex + frameSize);
- sampleRate = probe$3.aac.parseSampleRate(packet);
- }
- audioCount++;
- byteIndex += frameSize;
- break;
- default:
- byteIndex++;
- break;
- }
- if (endLoop) {
- return null;
- }
- }
- if (sampleRate === null || timestamp === null) {
- return null;
- }
-
- var audioTimescale = PES_TIMESCALE / sampleRate;
-
- var result = {
- audio: [{
- type: 'audio',
- dts: timestamp,
- pts: timestamp
- }, {
- type: 'audio',
- dts: timestamp + audioCount * 1024 * audioTimescale,
- pts: timestamp + audioCount * 1024 * audioTimescale
- }]
- };
-
- return result;
- };
-
- /**
- * inspects the transport stream segment data for start and end time information
- * of the audio and video tracks (when present) as well as the first key frame's
- * start time.
- */
- var inspectTs_ = function inspectTs_(bytes) {
- var pmt = {
- pid: null,
- table: null
- };
-
- var result = {};
-
- parsePsi_(bytes, pmt);
-
- for (var pid in pmt.table) {
- if (pmt.table.hasOwnProperty(pid)) {
- var type = pmt.table[pid];
- switch (type) {
- case streamTypes.H264_STREAM_TYPE:
- result.video = [];
- parseVideoPes_(bytes, pmt, result);
- if (result.video.length === 0) {
- delete result.video;
- }
- break;
- case streamTypes.ADTS_STREAM_TYPE:
- result.audio = [];
- parseAudioPes_(bytes, pmt, result);
- if (result.audio.length === 0) {
- delete result.audio;
- }
- break;
- default:
- break;
- }
- }
- }
- return result;
- };
-
- /**
- * Inspects segment byte data and returns an object with start and end timing information
- *
- * @param {Uint8Array} bytes The segment byte data
- * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame
- * timestamps for rollover. This value must be in 90khz clock.
- * @return {Object} Object containing start and end frame timing info of segment.
- */
- var inspect = function inspect(bytes, baseTimestamp) {
- var isAacData = isLikelyAacData$1(bytes);
-
- var result;
-
- if (isAacData) {
- result = inspectAac_(bytes);
- } else {
- result = inspectTs_(bytes);
- }
-
- if (!result || !result.audio && !result.video) {
- return null;
- }
-
- adjustTimestamp_(result, baseTimestamp);
-
- return result;
- };
-
- var tsInspector = {
- inspect: inspect
- };
-
- /**
- * @file sync-controller.js
- */
-
- var tsprobe = tsInspector.inspect;
-
- var syncPointStrategies = [
- // Stategy "VOD": Handle the VOD-case where the sync-point is *always*
- // the equivalence display-time 0 === segment-index 0
- {
- name: 'VOD',
- run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) {
- if (duration$$1 !== Infinity) {
- var syncPoint = {
- time: 0,
- segmentIndex: 0
- };
-
- return syncPoint;
- }
- return null;
- }
- },
- // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
- {
- name: 'ProgramDateTime',
- run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) {
- if (!syncController.datetimeToDisplayTime) {
- return null;
- }
-
- var segments = playlist.segments || [];
- var syncPoint = null;
- var lastDistance = null;
-
- currentTime = currentTime || 0;
-
- for (var i = 0; i < segments.length; i++) {
- var segment = segments[i];
-
- if (segment.dateTimeObject) {
- var segmentTime = segment.dateTimeObject.getTime() / 1000;
- var segmentStart = segmentTime + syncController.datetimeToDisplayTime;
- var distance = Math.abs(currentTime - segmentStart);
-
- // Once the distance begins to increase, we have passed
- // currentTime and can stop looking for better candidates
- if (lastDistance !== null && lastDistance < distance) {
- break;
- }
-
- lastDistance = distance;
- syncPoint = {
- time: segmentStart,
- segmentIndex: i
- };
- }
- }
- return syncPoint;
- }
- },
- // Stategy "Segment": We have a known time mapping for a timeline and a
- // segment in the current timeline with timing data
- {
- name: 'Segment',
- run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) {
- var segments = playlist.segments || [];
- var syncPoint = null;
- var lastDistance = null;
-
- currentTime = currentTime || 0;
-
- for (var i = 0; i < segments.length; i++) {
- var segment = segments[i];
-
- if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') {
- var distance = Math.abs(currentTime - segment.start);
-
- // Once the distance begins to increase, we have passed
- // currentTime and can stop looking for better candidates
- if (lastDistance !== null && lastDistance < distance) {
- break;
- }
-
- if (!syncPoint || lastDistance === null || lastDistance >= distance) {
- lastDistance = distance;
- syncPoint = {
- time: segment.start,
- segmentIndex: i
- };
- }
- }
- }
- return syncPoint;
- }
- },
- // Stategy "Discontinuity": We have a discontinuity with a known
- // display-time
- {
- name: 'Discontinuity',
- run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) {
- var syncPoint = null;
-
- currentTime = currentTime || 0;
-
- if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
- var lastDistance = null;
-
- for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
- var segmentIndex = playlist.discontinuityStarts[i];
- var discontinuity = playlist.discontinuitySequence + i + 1;
- var discontinuitySync = syncController.discontinuities[discontinuity];
-
- if (discontinuitySync) {
- var distance = Math.abs(currentTime - discontinuitySync.time);
-
- // Once the distance begins to increase, we have passed
- // currentTime and can stop looking for better candidates
- if (lastDistance !== null && lastDistance < distance) {
- break;
- }
-
- if (!syncPoint || lastDistance === null || lastDistance >= distance) {
- lastDistance = distance;
- syncPoint = {
- time: discontinuitySync.time,
- segmentIndex: segmentIndex
- };
- }
- }
- }
- }
- return syncPoint;
- }
- },
- // Stategy "Playlist": We have a playlist with a known mapping of
- // segment index to display time
- {
- name: 'Playlist',
- run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) {
- if (playlist.syncInfo) {
- var syncPoint = {
- time: playlist.syncInfo.time,
- segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence
- };
-
- return syncPoint;
- }
- return null;
- }
- }];
-
- var SyncController = function (_videojs$EventTarget) {
- inherits$1(SyncController, _videojs$EventTarget);
-
- function SyncController() {
- classCallCheck$1(this, SyncController);
-
- // Segment Loader state variables...
- // ...for synching across variants
- var _this = possibleConstructorReturn$1(this, (SyncController.__proto__ || Object.getPrototypeOf(SyncController)).call(this));
-
- _this.inspectCache_ = undefined;
-
- // ...for synching across variants
- _this.timelines = [];
- _this.discontinuities = [];
- _this.datetimeToDisplayTime = null;
-
- _this.logger_ = logger('SyncController');
- return _this;
- }
-
- /**
- * Find a sync-point for the playlist specified
- *
- * A sync-point is defined as a known mapping from display-time to
- * a segment-index in the current playlist.
- *
- * @param {Playlist} playlist
- * The playlist that needs a sync-point
- * @param {Number} duration
- * Duration of the MediaSource (Infinite if playing a live source)
- * @param {Number} currentTimeline
- * The last timeline from which a segment was loaded
- * @returns {Object}
- * A sync-point object
- */
-
-
- createClass(SyncController, [{
- key: 'getSyncPoint',
- value: function getSyncPoint(playlist, duration$$1, currentTimeline, currentTime) {
- var syncPoints = this.runStrategies_(playlist, duration$$1, currentTimeline, currentTime);
-
- if (!syncPoints.length) {
- // Signal that we need to attempt to get a sync-point manually
- // by fetching a segment in the playlist and constructing
- // a sync-point from that information
- return null;
- }
-
- // Now find the sync-point that is closest to the currentTime because
- // that should result in the most accurate guess about which segment
- // to fetch
- return this.selectSyncPoint_(syncPoints, { key: 'time', value: currentTime });
- }
-
- /**
- * Calculate the amount of time that has expired off the playlist during playback
- *
- * @param {Playlist} playlist
- * Playlist object to calculate expired from
- * @param {Number} duration
- * Duration of the MediaSource (Infinity if playling a live source)
- * @returns {Number|null}
- * The amount of time that has expired off the playlist during playback. Null
- * if no sync-points for the playlist can be found.
- */
-
- }, {
- key: 'getExpiredTime',
- value: function getExpiredTime(playlist, duration$$1) {
- if (!playlist || !playlist.segments) {
- return null;
- }
-
- var syncPoints = this.runStrategies_(playlist, duration$$1, playlist.discontinuitySequence, 0);
-
- // Without sync-points, there is not enough information to determine the expired time
- if (!syncPoints.length) {
- return null;
- }
-
- var syncPoint = this.selectSyncPoint_(syncPoints, {
- key: 'segmentIndex',
- value: 0
- });
-
- // If the sync-point is beyond the start of the playlist, we want to subtract the
- // duration from index 0 to syncPoint.segmentIndex instead of adding.
- if (syncPoint.segmentIndex > 0) {
- syncPoint.time *= -1;
- }
-
- return Math.abs(syncPoint.time + sumDurations(playlist, syncPoint.segmentIndex, 0));
- }
-
- /**
- * Runs each sync-point strategy and returns a list of sync-points returned by the
- * strategies
- *
- * @private
- * @param {Playlist} playlist
- * The playlist that needs a sync-point
- * @param {Number} duration
- * Duration of the MediaSource (Infinity if playing a live source)
- * @param {Number} currentTimeline
- * The last timeline from which a segment was loaded
- * @returns {Array}
- * A list of sync-point objects
- */
-
- }, {
- key: 'runStrategies_',
- value: function runStrategies_(playlist, duration$$1, currentTimeline, currentTime) {
- var syncPoints = [];
-
- // Try to find a sync-point in by utilizing various strategies...
- for (var i = 0; i < syncPointStrategies.length; i++) {
- var strategy = syncPointStrategies[i];
- var syncPoint = strategy.run(this, playlist, duration$$1, currentTimeline, currentTime);
-
- if (syncPoint) {
- syncPoint.strategy = strategy.name;
- syncPoints.push({
- strategy: strategy.name,
- syncPoint: syncPoint
- });
- }
- }
-
- return syncPoints;
- }
-
- /**
- * Selects the sync-point nearest the specified target
- *
- * @private
- * @param {Array} syncPoints
- * List of sync-points to select from
- * @param {Object} target
- * Object specifying the property and value we are targeting
- * @param {String} target.key
- * Specifies the property to target. Must be either 'time' or 'segmentIndex'
- * @param {Number} target.value
- * The value to target for the specified key.
- * @returns {Object}
- * The sync-point nearest the target
- */
-
- }, {
- key: 'selectSyncPoint_',
- value: function selectSyncPoint_(syncPoints, target) {
- var bestSyncPoint = syncPoints[0].syncPoint;
- var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value);
- var bestStrategy = syncPoints[0].strategy;
-
- for (var i = 1; i < syncPoints.length; i++) {
- var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value);
-
- if (newDistance < bestDistance) {
- bestDistance = newDistance;
- bestSyncPoint = syncPoints[i].syncPoint;
- bestStrategy = syncPoints[i].strategy;
- }
- }
-
- this.logger_('syncPoint for [' + target.key + ': ' + target.value + '] chosen with strategy' + (' [' + bestStrategy + ']: [time:' + bestSyncPoint.time + ',') + (' segmentIndex:' + bestSyncPoint.segmentIndex + ']'));
-
- return bestSyncPoint;
- }
-
- /**
- * Save any meta-data present on the segments when segments leave
- * the live window to the playlist to allow for synchronization at the
- * playlist level later.
- *
- * @param {Playlist} oldPlaylist - The previous active playlist
- * @param {Playlist} newPlaylist - The updated and most current playlist
- */
-
- }, {
- key: 'saveExpiredSegmentInfo',
- value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
- var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
-
- // When a segment expires from the playlist and it has a start time
- // save that information as a possible sync-point reference in future
- for (var i = mediaSequenceDiff - 1; i >= 0; i--) {
- var lastRemovedSegment = oldPlaylist.segments[i];
-
- if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
- newPlaylist.syncInfo = {
- mediaSequence: oldPlaylist.mediaSequence + i,
- time: lastRemovedSegment.start
- };
- this.logger_('playlist refresh sync: [time:' + newPlaylist.syncInfo.time + ',' + (' mediaSequence: ' + newPlaylist.syncInfo.mediaSequence + ']'));
- this.trigger('syncinfoupdate');
- break;
- }
- }
- }
-
- /**
- * Save the mapping from playlist's ProgramDateTime to display. This should
- * only ever happen once at the start of playback.
- *
- * @param {Playlist} playlist - The currently active playlist
- */
-
- }, {
- key: 'setDateTimeMapping',
- value: function setDateTimeMapping(playlist) {
- if (!this.datetimeToDisplayTime && playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) {
- var playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000;
-
- this.datetimeToDisplayTime = -playlistTimestamp;
- }
- }
-
- /**
- * Reset the state of the inspection cache when we do a rendition
- * switch
- */
-
- }, {
- key: 'reset',
- value: function reset() {
- this.inspectCache_ = undefined;
- }
-
- /**
- * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start
- * and end of the segment in it's internal "media time". Used to generate
- * mappings from that internal "media time" to the display time that is
- * shown on the player.
- *
- * @param {SegmentInfo} segmentInfo - The current active request information
- */
-
- }, {
- key: 'probeSegmentInfo',
- value: function probeSegmentInfo(segmentInfo) {
- var segment = segmentInfo.segment;
- var playlist = segmentInfo.playlist;
- var timingInfo = void 0;
-
- if (segment.map) {
- timingInfo = this.probeMp4Segment_(segmentInfo);
- } else {
- timingInfo = this.probeTsSegment_(segmentInfo);
- }
-
- if (timingInfo) {
- if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) {
- this.saveDiscontinuitySyncInfo_(segmentInfo);
-
- // If the playlist does not have sync information yet, record that information
- // now with segment timing information
- if (!playlist.syncInfo) {
- playlist.syncInfo = {
- mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
- time: segment.start
- };
- }
- }
- }
-
- return timingInfo;
- }
-
- /**
- * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment
- * in it's internal "media time".
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- * @return {object} The start and end time of the current segment in "media time"
- */
-
- }, {
- key: 'probeMp4Segment_',
- value: function probeMp4Segment_(segmentInfo) {
- var segment = segmentInfo.segment;
- var timescales = probe.timescale(segment.map.bytes);
- var startTime = probe.startTime(timescales, segmentInfo.bytes);
-
- if (segmentInfo.timestampOffset !== null) {
- segmentInfo.timestampOffset -= startTime;
- }
-
- return {
- start: startTime,
- end: startTime + segment.duration
- };
- }
-
- /**
- * Probe an mpeg2-ts segment to determine the start and end of the segment
- * in it's internal "media time".
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- * @return {object} The start and end time of the current segment in "media time"
- */
-
- }, {
- key: 'probeTsSegment_',
- value: function probeTsSegment_(segmentInfo) {
- var timeInfo = tsprobe(segmentInfo.bytes, this.inspectCache_);
- var segmentStartTime = void 0;
- var segmentEndTime = void 0;
-
- if (!timeInfo) {
- return null;
- }
-
- if (timeInfo.video && timeInfo.video.length === 2) {
- this.inspectCache_ = timeInfo.video[1].dts;
- segmentStartTime = timeInfo.video[0].dtsTime;
- segmentEndTime = timeInfo.video[1].dtsTime;
- } else if (timeInfo.audio && timeInfo.audio.length === 2) {
- this.inspectCache_ = timeInfo.audio[1].dts;
- segmentStartTime = timeInfo.audio[0].dtsTime;
- segmentEndTime = timeInfo.audio[1].dtsTime;
- }
-
- return {
- start: segmentStartTime,
- end: segmentEndTime,
- containsVideo: timeInfo.video && timeInfo.video.length === 2,
- containsAudio: timeInfo.audio && timeInfo.audio.length === 2
- };
- }
- }, {
- key: 'timestampOffsetForTimeline',
- value: function timestampOffsetForTimeline(timeline) {
- if (typeof this.timelines[timeline] === 'undefined') {
- return null;
- }
- return this.timelines[timeline].time;
- }
- }, {
- key: 'mappingForTimeline',
- value: function mappingForTimeline(timeline) {
- if (typeof this.timelines[timeline] === 'undefined') {
- return null;
- }
- return this.timelines[timeline].mapping;
- }
-
- /**
- * Use the "media time" for a segment to generate a mapping to "display time" and
- * save that display time to the segment.
- *
- * @private
- * @param {SegmentInfo} segmentInfo
- * The current active request information
- * @param {object} timingInfo
- * The start and end time of the current segment in "media time"
- * @returns {Boolean}
- * Returns false if segment time mapping could not be calculated
- */
-
- }, {
- key: 'calculateSegmentTimeMapping_',
- value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) {
- var segment = segmentInfo.segment;
- var mappingObj = this.timelines[segmentInfo.timeline];
-
- if (segmentInfo.timestampOffset !== null) {
- mappingObj = {
- time: segmentInfo.startOfSegment,
- mapping: segmentInfo.startOfSegment - timingInfo.start
- };
- this.timelines[segmentInfo.timeline] = mappingObj;
- this.trigger('timestampoffset');
-
- this.logger_('time mapping for timeline ' + segmentInfo.timeline + ': ' + ('[time: ' + mappingObj.time + '] [mapping: ' + mappingObj.mapping + ']'));
-
- segment.start = segmentInfo.startOfSegment;
- segment.end = timingInfo.end + mappingObj.mapping;
- } else if (mappingObj) {
- segment.start = timingInfo.start + mappingObj.mapping;
- segment.end = timingInfo.end + mappingObj.mapping;
- } else {
- return false;
- }
-
- return true;
- }
-
- /**
- * Each time we have discontinuity in the playlist, attempt to calculate the location
- * in display of the start of the discontinuity and save that. We also save an accuracy
- * value so that we save values with the most accuracy (closest to 0.)
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- */
-
- }, {
- key: 'saveDiscontinuitySyncInfo_',
- value: function saveDiscontinuitySyncInfo_(segmentInfo) {
- var playlist = segmentInfo.playlist;
- var segment = segmentInfo.segment;
-
- // If the current segment is a discontinuity then we know exactly where
- // the start of the range and it's accuracy is 0 (greater accuracy values
- // mean more approximation)
- if (segment.discontinuity) {
- this.discontinuities[segment.timeline] = {
- time: segment.start,
- accuracy: 0
- };
- } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
- // Search for future discontinuities that we can provide better timing
- // information for and save that information for sync purposes
- for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
- var segmentIndex = playlist.discontinuityStarts[i];
- var discontinuity = playlist.discontinuitySequence + i + 1;
- var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex;
- var accuracy = Math.abs(mediaIndexDiff);
-
- if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) {
- var time = void 0;
-
- if (mediaIndexDiff < 0) {
- time = segment.start - sumDurations(playlist, segmentInfo.mediaIndex, segmentIndex);
- } else {
- time = segment.end + sumDurations(playlist, segmentInfo.mediaIndex + 1, segmentIndex);
- }
-
- this.discontinuities[discontinuity] = {
- time: time,
- accuracy: accuracy
- };
- }
- }
- }
- }
- }]);
- return SyncController;
- }(videojs.EventTarget);
-
- var Decrypter$1 = new shimWorker("./decrypter-worker.worker.js", function (window, document) {
- var self = this;
- var decrypterWorker = function () {
-
- /*
- * pkcs7.pad
- * https://github.com/brightcove/pkcs7
- *
- * Copyright (c) 2014 Brightcove
- * Licensed under the apache2 license.
- */
-
- /**
- * Returns the subarray of a Uint8Array without PKCS#7 padding.
- * @param padded {Uint8Array} unencrypted bytes that have been padded
- * @return {Uint8Array} the unpadded bytes
- * @see http://tools.ietf.org/html/rfc5652
- */
-
- function unpad(padded) {
- return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]);
- }
-
- var classCallCheck = function classCallCheck(instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- var createClass = function () {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
- }
- }
-
- return function (Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- };
- }();
-
- var inherits = function inherits(subClass, superClass) {
- if (typeof superClass !== "function" && superClass !== null) {
- throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
- }
-
- subClass.prototype = Object.create(superClass && superClass.prototype, {
- constructor: {
- value: subClass,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
- };
-
- var possibleConstructorReturn = function possibleConstructorReturn(self, call) {
- if (!self) {
- throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
- }
-
- return call && (typeof call === "object" || typeof call === "function") ? call : self;
- };
-
- /**
- * @file aes.js
- *
- * This file contains an adaptation of the AES decryption algorithm
- * from the Standford Javascript Cryptography Library. That work is
- * covered by the following copyright and permissions notice:
- *
- * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
- * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
- * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
- * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * The views and conclusions contained in the software and documentation
- * are those of the authors and should not be interpreted as representing
- * official policies, either expressed or implied, of the authors.
- */
-
- /**
- * Expand the S-box tables.
- *
- * @private
- */
- var precompute = function precompute() {
- var tables = [[[], [], [], [], []], [[], [], [], [], []]];
- var encTable = tables[0];
- var decTable = tables[1];
- var sbox = encTable[4];
- var sboxInv = decTable[4];
- var i = void 0;
- var x = void 0;
- var xInv = void 0;
- var d = [];
- var th = [];
- var x2 = void 0;
- var x4 = void 0;
- var x8 = void 0;
- var s = void 0;
- var tEnc = void 0;
- var tDec = void 0;
-
- // Compute double and third tables
- for (i = 0; i < 256; i++) {
- th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i;
- }
-
- for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
- // Compute sbox
- s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4;
- s = s >> 8 ^ s & 255 ^ 99;
- sbox[x] = s;
- sboxInv[s] = x;
-
- // Compute MixColumns
- x8 = d[x4 = d[x2 = d[x]]];
- tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100;
- tEnc = d[s] * 0x101 ^ s * 0x1010100;
-
- for (i = 0; i < 4; i++) {
- encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8;
- decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8;
- }
- }
-
- // Compactify. Considerable speedup on Firefox.
- for (i = 0; i < 5; i++) {
- encTable[i] = encTable[i].slice(0);
- decTable[i] = decTable[i].slice(0);
- }
- return tables;
- };
- var aesTables = null;
-
- /**
- * Schedule out an AES key for both encryption and decryption. This
- * is a low-level class. Use a cipher mode to do bulk encryption.
- *
- * @class AES
- * @param key {Array} The key as an array of 4, 6 or 8 words.
- */
-
- var AES = function () {
- function AES(key) {
- classCallCheck(this, AES);
-
- /**
- * The expanded S-box and inverse S-box tables. These will be computed
- * on the client so that we don't have to send them down the wire.
- *
- * There are two tables, _tables[0] is for encryption and
- * _tables[1] is for decryption.
- *
- * The first 4 sub-tables are the expanded S-box with MixColumns. The
- * last (_tables[01][4]) is the S-box itself.
- *
- * @private
- */
- // if we have yet to precompute the S-box tables
- // do so now
- if (!aesTables) {
- aesTables = precompute();
- }
- // then make a copy of that object for use
- this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]];
- var i = void 0;
- var j = void 0;
- var tmp = void 0;
- var encKey = void 0;
- var decKey = void 0;
- var sbox = this._tables[0][4];
- var decTable = this._tables[1];
- var keyLen = key.length;
- var rcon = 1;
-
- if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
- throw new Error('Invalid aes key size');
- }
-
- encKey = key.slice(0);
- decKey = [];
- this._key = [encKey, decKey];
-
- // schedule encryption keys
- for (i = keyLen; i < 4 * keyLen + 28; i++) {
- tmp = encKey[i - 1];
-
- // apply sbox
- if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) {
- tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255];
-
- // shift rows and add rcon
- if (i % keyLen === 0) {
- tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24;
- rcon = rcon << 1 ^ (rcon >> 7) * 283;
- }
- }
-
- encKey[i] = encKey[i - keyLen] ^ tmp;
- }
-
- // schedule decryption keys
- for (j = 0; i; j++, i--) {
- tmp = encKey[j & 3 ? i : i - 4];
- if (i <= 4 || j < 4) {
- decKey[j] = tmp;
- } else {
- decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]];
- }
- }
- }
-
- /**
- * Decrypt 16 bytes, specified as four 32-bit words.
- *
- * @param {Number} encrypted0 the first word to decrypt
- * @param {Number} encrypted1 the second word to decrypt
- * @param {Number} encrypted2 the third word to decrypt
- * @param {Number} encrypted3 the fourth word to decrypt
- * @param {Int32Array} out the array to write the decrypted words
- * into
- * @param {Number} offset the offset into the output array to start
- * writing results
- * @return {Array} The plaintext.
- */
-
- AES.prototype.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) {
- var key = this._key[1];
- // state variables a,b,c,d are loaded with pre-whitened data
- var a = encrypted0 ^ key[0];
- var b = encrypted3 ^ key[1];
- var c = encrypted2 ^ key[2];
- var d = encrypted1 ^ key[3];
- var a2 = void 0;
- var b2 = void 0;
- var c2 = void 0;
-
- // key.length === 2 ?
- var nInnerRounds = key.length / 4 - 2;
- var i = void 0;
- var kIndex = 4;
- var table = this._tables[1];
-
- // load up the tables
- var table0 = table[0];
- var table1 = table[1];
- var table2 = table[2];
- var table3 = table[3];
- var sbox = table[4];
-
- // Inner rounds. Cribbed from OpenSSL.
- for (i = 0; i < nInnerRounds; i++) {
- a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex];
- b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1];
- c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2];
- d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3];
- kIndex += 4;
- a = a2;b = b2;c = c2;
- }
-
- // Last round.
- for (i = 0; i < 4; i++) {
- out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++];
- a2 = a;a = b;b = c;c = d;d = a2;
- }
- };
-
- return AES;
- }();
-
- /**
- * @file stream.js
- */
- /**
- * A lightweight readable stream implemention that handles event dispatching.
- *
- * @class Stream
- */
- var Stream = function () {
- function Stream() {
- classCallCheck(this, Stream);
-
- this.listeners = {};
- }
-
- /**
- * Add a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener the callback to be invoked when an event of
- * the specified type occurs
- */
-
- Stream.prototype.on = function on(type, listener) {
- if (!this.listeners[type]) {
- this.listeners[type] = [];
- }
- this.listeners[type].push(listener);
- };
-
- /**
- * Remove a listener for a specified event type.
- *
- * @param {String} type the event name
- * @param {Function} listener a function previously registered for this
- * type of event through `on`
- * @return {Boolean} if we could turn it off or not
- */
-
- Stream.prototype.off = function off(type, listener) {
- if (!this.listeners[type]) {
- return false;
- }
-
- var index = this.listeners[type].indexOf(listener);
-
- this.listeners[type].splice(index, 1);
- return index > -1;
- };
-
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- *
- * @param {String} type the event name
- */
-
- Stream.prototype.trigger = function trigger(type) {
- var callbacks = this.listeners[type];
-
- if (!callbacks) {
- return;
- }
-
- // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
- if (arguments.length === 2) {
- var length = callbacks.length;
-
- for (var i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- var args = Array.prototype.slice.call(arguments, 1);
- var _length = callbacks.length;
-
- for (var _i = 0; _i < _length; ++_i) {
- callbacks[_i].apply(this, args);
- }
- }
- };
-
- /**
- * Destroys the stream and cleans up.
- */
-
- Stream.prototype.dispose = function dispose() {
- this.listeners = {};
- };
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- *
- * @param {Stream} destination the stream that will receive all `data` events
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */
-
- Stream.prototype.pipe = function pipe(destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
- };
-
- return Stream;
- }();
-
- /**
- * @file async-stream.js
- */
- /**
- * A wrapper around the Stream class to use setTiemout
- * and run stream "jobs" Asynchronously
- *
- * @class AsyncStream
- * @extends Stream
- */
-
- var AsyncStream = function (_Stream) {
- inherits(AsyncStream, _Stream);
-
- function AsyncStream() {
- classCallCheck(this, AsyncStream);
-
- var _this = possibleConstructorReturn(this, _Stream.call(this, Stream));
-
- _this.jobs = [];
- _this.delay = 1;
- _this.timeout_ = null;
- return _this;
- }
-
- /**
- * process an async job
- *
- * @private
- */
-
- AsyncStream.prototype.processJob_ = function processJob_() {
- this.jobs.shift()();
- if (this.jobs.length) {
- this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
- } else {
- this.timeout_ = null;
- }
- };
-
- /**
- * push a job into the stream
- *
- * @param {Function} job the job to push into the stream
- */
-
- AsyncStream.prototype.push = function push(job) {
- this.jobs.push(job);
- if (!this.timeout_) {
- this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
- }
- };
-
- return AsyncStream;
- }(Stream);
-
- /**
- * @file decrypter.js
- *
- * An asynchronous implementation of AES-128 CBC decryption with
- * PKCS#7 padding.
- */
-
- /**
- * Convert network-order (big-endian) bytes into their little-endian
- * representation.
- */
- var ntoh = function ntoh(word) {
- return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24;
- };
-
- /**
- * Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
- *
- * @param {Uint8Array} encrypted the encrypted bytes
- * @param {Uint32Array} key the bytes of the decryption key
- * @param {Uint32Array} initVector the initialization vector (IV) to
- * use for the first round of CBC.
- * @return {Uint8Array} the decrypted bytes
- *
- * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
- * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
- * @see https://tools.ietf.org/html/rfc2315
- */
- var decrypt = function decrypt(encrypted, key, initVector) {
- // word-level access to the encrypted bytes
- var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2);
-
- var decipher = new AES(Array.prototype.slice.call(key));
-
- // byte and word-level access for the decrypted output
- var decrypted = new Uint8Array(encrypted.byteLength);
- var decrypted32 = new Int32Array(decrypted.buffer);
-
- // temporary variables for working with the IV, encrypted, and
- // decrypted data
- var init0 = void 0;
- var init1 = void 0;
- var init2 = void 0;
- var init3 = void 0;
- var encrypted0 = void 0;
- var encrypted1 = void 0;
- var encrypted2 = void 0;
- var encrypted3 = void 0;
-
- // iteration variable
- var wordIx = void 0;
-
- // pull out the words of the IV to ensure we don't modify the
- // passed-in reference and easier access
- init0 = initVector[0];
- init1 = initVector[1];
- init2 = initVector[2];
- init3 = initVector[3];
-
- // decrypt four word sequences, applying cipher-block chaining (CBC)
- // to each decrypted block
- for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) {
- // convert big-endian (network order) words into little-endian
- // (javascript order)
- encrypted0 = ntoh(encrypted32[wordIx]);
- encrypted1 = ntoh(encrypted32[wordIx + 1]);
- encrypted2 = ntoh(encrypted32[wordIx + 2]);
- encrypted3 = ntoh(encrypted32[wordIx + 3]);
-
- // decrypt the block
- decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx);
-
- // XOR with the IV, and restore network byte-order to obtain the
- // plaintext
- decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0);
- decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1);
- decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2);
- decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3);
-
- // setup the IV for the next round
- init0 = encrypted0;
- init1 = encrypted1;
- init2 = encrypted2;
- init3 = encrypted3;
- }
-
- return decrypted;
- };
-
- /**
- * The `Decrypter` class that manages decryption of AES
- * data through `AsyncStream` objects and the `decrypt`
- * function
- *
- * @param {Uint8Array} encrypted the encrypted bytes
- * @param {Uint32Array} key the bytes of the decryption key
- * @param {Uint32Array} initVector the initialization vector (IV) to
- * @param {Function} done the function to run when done
- * @class Decrypter
- */
-
- var Decrypter = function () {
- function Decrypter(encrypted, key, initVector, done) {
- classCallCheck(this, Decrypter);
-
- var step = Decrypter.STEP;
- var encrypted32 = new Int32Array(encrypted.buffer);
- var decrypted = new Uint8Array(encrypted.byteLength);
- var i = 0;
-
- this.asyncStream_ = new AsyncStream();
-
- // split up the encryption job and do the individual chunks asynchronously
- this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
- for (i = step; i < encrypted32.length; i += step) {
- initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]);
- this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
- }
- // invoke the done() callback when everything is finished
- this.asyncStream_.push(function () {
- // remove pkcs#7 padding from the decrypted bytes
- done(null, unpad(decrypted));
- });
- }
-
- /**
- * a getter for step the maximum number of bytes to process at one time
- *
- * @return {Number} the value of step 32000
- */
-
- /**
- * @private
- */
- Decrypter.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) {
- return function () {
- var bytes = decrypt(encrypted, key, initVector);
-
- decrypted.set(bytes, encrypted.byteOffset);
- };
- };
-
- createClass(Decrypter, null, [{
- key: 'STEP',
- get: function get$$1() {
- // 4 * 8000;
- return 32000;
- }
- }]);
- return Decrypter;
- }();
-
- /**
- * @file bin-utils.js
- */
-
- /**
- * Creates an object for sending to a web worker modifying properties that are TypedArrays
- * into a new object with seperated properties for the buffer, byteOffset, and byteLength.
- *
- * @param {Object} message
- * Object of properties and values to send to the web worker
- * @return {Object}
- * Modified message with TypedArray values expanded
- * @function createTransferableMessage
- */
- var createTransferableMessage = function createTransferableMessage(message) {
- var transferable = {};
-
- Object.keys(message).forEach(function (key) {
- var value = message[key];
-
- if (ArrayBuffer.isView(value)) {
- transferable[key] = {
- bytes: value.buffer,
- byteOffset: value.byteOffset,
- byteLength: value.byteLength
- };
- } else {
- transferable[key] = value;
- }
- });
-
- return transferable;
- };
-
- /**
- * Our web worker interface so that things can talk to aes-decrypter
- * that will be running in a web worker. the scope is passed to this by
- * webworkify.
- *
- * @param {Object} self
- * the scope for the web worker
- */
- var DecrypterWorker = function DecrypterWorker(self) {
- self.onmessage = function (event) {
- var data = event.data;
- var encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength);
- var key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4);
- var iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4);
-
- /* eslint-disable no-new, handle-callback-err */
- new Decrypter(encrypted, key, iv, function (err, bytes) {
- self.postMessage(createTransferableMessage({
- source: data.source,
- decrypted: bytes
- }), [bytes.buffer]);
- });
- /* eslint-enable */
- };
- };
-
- var decrypterWorker = new DecrypterWorker(self);
-
- return decrypterWorker;
- }();
- });
-
- /**
- * Convert the properties of an HLS track into an audioTrackKind.
- *
- * @private
- */
- var audioTrackKind_ = function audioTrackKind_(properties) {
- var kind = properties.default ? 'main' : 'alternative';
-
- if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
- kind = 'main-desc';
- }
-
- return kind;
- };
-
- /**
- * Pause provided segment loader and playlist loader if active
- *
- * @param {SegmentLoader} segmentLoader
- * SegmentLoader to pause
- * @param {Object} mediaType
- * Active media type
- * @function stopLoaders
- */
- var stopLoaders = function stopLoaders(segmentLoader, mediaType) {
- segmentLoader.abort();
- segmentLoader.pause();
-
- if (mediaType && mediaType.activePlaylistLoader) {
- mediaType.activePlaylistLoader.pause();
- mediaType.activePlaylistLoader = null;
- }
- };
-
- /**
- * Start loading provided segment loader and playlist loader
- *
- * @param {PlaylistLoader} playlistLoader
- * PlaylistLoader to start loading
- * @param {Object} mediaType
- * Active media type
- * @function startLoaders
- */
- var startLoaders = function startLoaders(playlistLoader, mediaType) {
- // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
- // playlist loader
- mediaType.activePlaylistLoader = playlistLoader;
- playlistLoader.load();
- };
-
- /**
- * Returns a function to be called when the media group changes. It performs a
- * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
- * change of group is merely a rendition switch of the same content at another encoding,
- * rather than a change of content, such as switching audio from English to Spanish.
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Handler for a non-destructive resync of SegmentLoader when the active media
- * group changes.
- * @function onGroupChanged
- */
- var onGroupChanged = function onGroupChanged(type, settings) {
- return function () {
- var _settings$segmentLoad = settings.segmentLoaders,
- segmentLoader = _settings$segmentLoad[type],
- mainSegmentLoader = _settings$segmentLoad.main,
- mediaType = settings.mediaTypes[type];
-
- var activeTrack = mediaType.activeTrack();
- var activeGroup = mediaType.activeGroup(activeTrack);
- var previousActiveLoader = mediaType.activePlaylistLoader;
-
- stopLoaders(segmentLoader, mediaType);
-
- if (!activeGroup) {
- // there is no group active
- return;
- }
-
- if (!activeGroup.playlistLoader) {
- if (previousActiveLoader) {
- // The previous group had a playlist loader but the new active group does not
- // this means we are switching from demuxed to muxed audio. In this case we want to
- // do a destructive reset of the main segment loader and not restart the audio
- // loaders.
- mainSegmentLoader.resetEverything();
- }
- return;
- }
-
- // Non-destructive resync
- segmentLoader.resyncLoader();
-
- startLoaders(activeGroup.playlistLoader, mediaType);
- };
- };
-
- /**
- * Returns a function to be called when the media track changes. It performs a
- * destructive reset of the SegmentLoader to ensure we start loading as close to
- * currentTime as possible.
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Handler for a destructive reset of SegmentLoader when the active media
- * track changes.
- * @function onTrackChanged
- */
- var onTrackChanged = function onTrackChanged(type, settings) {
- return function () {
- var _settings$segmentLoad2 = settings.segmentLoaders,
- segmentLoader = _settings$segmentLoad2[type],
- mainSegmentLoader = _settings$segmentLoad2.main,
- mediaType = settings.mediaTypes[type];
-
- var activeTrack = mediaType.activeTrack();
- var activeGroup = mediaType.activeGroup(activeTrack);
- var previousActiveLoader = mediaType.activePlaylistLoader;
-
- stopLoaders(segmentLoader, mediaType);
-
- if (!activeGroup) {
- // there is no group active so we do not want to restart loaders
- return;
- }
-
- if (!activeGroup.playlistLoader) {
- // when switching from demuxed audio/video to muxed audio/video (noted by no playlist
- // loader for the audio group), we want to do a destructive reset of the main segment
- // loader and not restart the audio loaders
- mainSegmentLoader.resetEverything();
- return;
- }
-
- if (previousActiveLoader === activeGroup.playlistLoader) {
- // Nothing has actually changed. This can happen because track change events can fire
- // multiple times for a "single" change. One for enabling the new active track, and
- // one for disabling the track that was active
- startLoaders(activeGroup.playlistLoader, mediaType);
- return;
- }
-
- if (segmentLoader.track) {
- // For WebVTT, set the new text track in the segmentloader
- segmentLoader.track(activeTrack);
- }
-
- // destructive reset
- segmentLoader.resetEverything();
-
- startLoaders(activeGroup.playlistLoader, mediaType);
- };
- };
-
- var onError = {
- /**
- * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
- * an error.
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Error handler. Logs warning (or error if the playlist is blacklisted) to
- * console and switches back to default audio track.
- * @function onError.AUDIO
- */
- AUDIO: function AUDIO(type, settings) {
- return function () {
- var segmentLoader = settings.segmentLoaders[type],
- mediaType = settings.mediaTypes[type],
- blacklistCurrentPlaylist = settings.blacklistCurrentPlaylist;
-
-
- stopLoaders(segmentLoader, mediaType);
-
- // switch back to default audio track
- var activeTrack = mediaType.activeTrack();
- var activeGroup = mediaType.activeGroup();
- var id = (activeGroup.filter(function (group) {
- return group.default;
- })[0] || activeGroup[0]).id;
- var defaultTrack = mediaType.tracks[id];
-
- if (activeTrack === defaultTrack) {
- // Default track encountered an error. All we can do now is blacklist the current
- // rendition and hope another will switch audio groups
- blacklistCurrentPlaylist({
- message: 'Problem encountered loading the default audio track.'
- });
- return;
- }
-
- videojs.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.');
-
- for (var trackId in mediaType.tracks) {
- mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
- }
-
- mediaType.onTrackChanged();
- };
- },
- /**
- * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
- * an error.
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Error handler. Logs warning to console and disables the active subtitle track
- * @function onError.SUBTITLES
- */
- SUBTITLES: function SUBTITLES(type, settings) {
- return function () {
- var segmentLoader = settings.segmentLoaders[type],
- mediaType = settings.mediaTypes[type];
-
-
- videojs.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.');
-
- stopLoaders(segmentLoader, mediaType);
-
- var track = mediaType.activeTrack();
-
- if (track) {
- track.mode = 'disabled';
- }
-
- mediaType.onTrackChanged();
- };
- }
- };
-
- var setupListeners = {
- /**
- * Setup event listeners for audio playlist loader
- *
- * @param {String} type
- * MediaGroup type
- * @param {PlaylistLoader|null} playlistLoader
- * PlaylistLoader to register listeners on
- * @param {Object} settings
- * Object containing required information for media groups
- * @function setupListeners.AUDIO
- */
- AUDIO: function AUDIO(type, playlistLoader, settings) {
- if (!playlistLoader) {
- // no playlist loader means audio will be muxed with the video
- return;
- }
-
- var tech = settings.tech,
- requestOptions = settings.requestOptions,
- segmentLoader = settings.segmentLoaders[type];
-
-
- playlistLoader.on('loadedmetadata', function () {
- var media = playlistLoader.media();
-
- segmentLoader.playlist(media, requestOptions);
-
- // if the video is already playing, or if this isn't a live video and preload
- // permits, start downloading segments
- if (!tech.paused() || media.endList && tech.preload() !== 'none') {
- segmentLoader.load();
- }
- });
-
- playlistLoader.on('loadedplaylist', function () {
- segmentLoader.playlist(playlistLoader.media(), requestOptions);
-
- // If the player isn't paused, ensure that the segment loader is running
- if (!tech.paused()) {
- segmentLoader.load();
- }
- });
-
- playlistLoader.on('error', onError[type](type, settings));
- },
- /**
- * Setup event listeners for subtitle playlist loader
- *
- * @param {String} type
- * MediaGroup type
- * @param {PlaylistLoader|null} playlistLoader
- * PlaylistLoader to register listeners on
- * @param {Object} settings
- * Object containing required information for media groups
- * @function setupListeners.SUBTITLES
- */
- SUBTITLES: function SUBTITLES(type, playlistLoader, settings) {
- var tech = settings.tech,
- requestOptions = settings.requestOptions,
- segmentLoader = settings.segmentLoaders[type],
- mediaType = settings.mediaTypes[type];
-
-
- playlistLoader.on('loadedmetadata', function () {
- var media = playlistLoader.media();
-
- segmentLoader.playlist(media, requestOptions);
- segmentLoader.track(mediaType.activeTrack());
-
- // if the video is already playing, or if this isn't a live video and preload
- // permits, start downloading segments
- if (!tech.paused() || media.endList && tech.preload() !== 'none') {
- segmentLoader.load();
- }
- });
-
- playlistLoader.on('loadedplaylist', function () {
- segmentLoader.playlist(playlistLoader.media(), requestOptions);
-
- // If the player isn't paused, ensure that the segment loader is running
- if (!tech.paused()) {
- segmentLoader.load();
- }
- });
-
- playlistLoader.on('error', onError[type](type, settings));
- }
- };
-
- var byGroupId = function byGroupId(type, groupId) {
- return function (playlist) {
- return playlist.attributes[type] === groupId;
- };
- };
-
- var byResolvedUri = function byResolvedUri(resolvedUri) {
- return function (playlist) {
- return playlist.resolvedUri === resolvedUri;
- };
- };
-
- var initialize = {
- /**
- * Setup PlaylistLoaders and AudioTracks for the audio groups
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @function initialize.AUDIO
- */
- 'AUDIO': function AUDIO(type, settings) {
- var hls = settings.hls,
- sourceType = settings.sourceType,
- segmentLoader = settings.segmentLoaders[type],
- withCredentials = settings.requestOptions.withCredentials,
- _settings$master = settings.master,
- mediaGroups = _settings$master.mediaGroups,
- playlists = _settings$master.playlists,
- _settings$mediaTypes$ = settings.mediaTypes[type],
- groups = _settings$mediaTypes$.groups,
- tracks = _settings$mediaTypes$.tracks,
- masterPlaylistLoader = settings.masterPlaylistLoader;
-
- // force a default if we have none
-
- if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) {
- mediaGroups[type] = { main: { default: { default: true } } };
- }
-
- for (var groupId in mediaGroups[type]) {
- if (!groups[groupId]) {
- groups[groupId] = [];
- }
-
- // List of playlists that have an AUDIO attribute value matching the current
- // group ID
- var groupPlaylists = playlists.filter(byGroupId(type, groupId));
-
- for (var variantLabel in mediaGroups[type][groupId]) {
- var properties = mediaGroups[type][groupId][variantLabel];
-
- // List of playlists for the current group ID that have a matching uri with
- // this alternate audio variant
- var matchingPlaylists = groupPlaylists.filter(byResolvedUri(properties.resolvedUri));
-
- if (matchingPlaylists.length) {
- // If there is a playlist that has the same uri as this audio variant, assume
- // that the playlist is audio only. We delete the resolvedUri property here
- // to prevent a playlist loader from being created so that we don't have
- // both the main and audio segment loaders loading the same audio segments
- // from the same playlist.
- delete properties.resolvedUri;
- }
-
- var playlistLoader = void 0;
-
- if (properties.resolvedUri) {
- playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials);
- } else if (properties.playlists && sourceType === 'dash') {
- playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader);
- } else {
- // no resolvedUri means the audio is muxed with the video when using this
- // audio track
- playlistLoader = null;
- }
-
- properties = videojs.mergeOptions({ id: variantLabel, playlistLoader: playlistLoader }, properties);
-
- setupListeners[type](type, properties.playlistLoader, settings);
-
- groups[groupId].push(properties);
-
- if (typeof tracks[variantLabel] === 'undefined') {
- var track = new videojs.AudioTrack({
- id: variantLabel,
- kind: audioTrackKind_(properties),
- enabled: false,
- language: properties.language,
- default: properties.default,
- label: variantLabel
- });
-
- tracks[variantLabel] = track;
- }
- }
- }
-
- // setup single error event handler for the segment loader
- segmentLoader.on('error', onError[type](type, settings));
- },
- /**
- * Setup PlaylistLoaders and TextTracks for the subtitle groups
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @function initialize.SUBTITLES
- */
- 'SUBTITLES': function SUBTITLES(type, settings) {
- var tech = settings.tech,
- hls = settings.hls,
- sourceType = settings.sourceType,
- segmentLoader = settings.segmentLoaders[type],
- withCredentials = settings.requestOptions.withCredentials,
- mediaGroups = settings.master.mediaGroups,
- _settings$mediaTypes$2 = settings.mediaTypes[type],
- groups = _settings$mediaTypes$2.groups,
- tracks = _settings$mediaTypes$2.tracks,
- masterPlaylistLoader = settings.masterPlaylistLoader;
-
-
- for (var groupId in mediaGroups[type]) {
- if (!groups[groupId]) {
- groups[groupId] = [];
- }
-
- for (var variantLabel in mediaGroups[type][groupId]) {
- if (mediaGroups[type][groupId][variantLabel].forced) {
- // Subtitle playlists with the forced attribute are not selectable in Safari.
- // According to Apple's HLS Authoring Specification:
- // If content has forced subtitles and regular subtitles in a given language,
- // the regular subtitles track in that language MUST contain both the forced
- // subtitles and the regular subtitles for that language.
- // Because of this requirement and that Safari does not add forced subtitles,
- // forced subtitles are skipped here to maintain consistent experience across
- // all platforms
- continue;
- }
-
- var properties = mediaGroups[type][groupId][variantLabel];
-
- var playlistLoader = void 0;
-
- if (sourceType === 'hls') {
- playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials);
- } else if (sourceType === 'dash') {
- playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader);
- }
-
- properties = videojs.mergeOptions({
- id: variantLabel,
- playlistLoader: playlistLoader
- }, properties);
-
- setupListeners[type](type, properties.playlistLoader, settings);
-
- groups[groupId].push(properties);
-
- if (typeof tracks[variantLabel] === 'undefined') {
- var track = tech.addRemoteTextTrack({
- id: variantLabel,
- kind: 'subtitles',
- enabled: false,
- language: properties.language,
- label: variantLabel
- }, false).track;
-
- tracks[variantLabel] = track;
- }
- }
- }
-
- // setup single error event handler for the segment loader
- segmentLoader.on('error', onError[type](type, settings));
- },
- /**
- * Setup TextTracks for the closed-caption groups
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @function initialize['CLOSED-CAPTIONS']
- */
- 'CLOSED-CAPTIONS': function CLOSEDCAPTIONS(type, settings) {
- var tech = settings.tech,
- mediaGroups = settings.master.mediaGroups,
- _settings$mediaTypes$3 = settings.mediaTypes[type],
- groups = _settings$mediaTypes$3.groups,
- tracks = _settings$mediaTypes$3.tracks;
-
-
- for (var groupId in mediaGroups[type]) {
- if (!groups[groupId]) {
- groups[groupId] = [];
- }
-
- for (var variantLabel in mediaGroups[type][groupId]) {
- var properties = mediaGroups[type][groupId][variantLabel];
-
- // We only support CEA608 captions for now, so ignore anything that
- // doesn't use a CCx INSTREAM-ID
- if (!properties.instreamId.match(/CC\d/)) {
- continue;
- }
-
- // No PlaylistLoader is required for Closed-Captions because the captions are
- // embedded within the video stream
- groups[groupId].push(videojs.mergeOptions({ id: variantLabel }, properties));
-
- if (typeof tracks[variantLabel] === 'undefined') {
- var track = tech.addRemoteTextTrack({
- id: properties.instreamId,
- kind: 'captions',
- enabled: false,
- language: properties.language,
- label: variantLabel
- }, false).track;
-
- tracks[variantLabel] = track;
- }
- }
- }
- }
- };
-
- /**
- * Returns a function used to get the active group of the provided type
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Function that returns the active media group for the provided type. Takes an
- * optional parameter {TextTrack} track. If no track is provided, a list of all
- * variants in the group, otherwise the variant corresponding to the provided
- * track is returned.
- * @function activeGroup
- */
- var activeGroup = function activeGroup(type, settings) {
- return function (track) {
- var masterPlaylistLoader = settings.masterPlaylistLoader,
- groups = settings.mediaTypes[type].groups;
-
-
- var media = masterPlaylistLoader.media();
-
- if (!media) {
- return null;
- }
-
- var variants = null;
-
- if (media.attributes[type]) {
- variants = groups[media.attributes[type]];
- }
-
- variants = variants || groups.main;
-
- if (typeof track === 'undefined') {
- return variants;
- }
-
- if (track === null) {
- // An active track was specified so a corresponding group is expected. track === null
- // means no track is currently active so there is no corresponding group
- return null;
- }
-
- return variants.filter(function (props) {
- return props.id === track.id;
- })[0] || null;
- };
- };
-
- var activeTrack = {
- /**
- * Returns a function used to get the active track of type provided
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Function that returns the active media track for the provided type. Returns
- * null if no track is active
- * @function activeTrack.AUDIO
- */
- AUDIO: function AUDIO(type, settings) {
- return function () {
- var tracks = settings.mediaTypes[type].tracks;
-
-
- for (var id in tracks) {
- if (tracks[id].enabled) {
- return tracks[id];
- }
- }
-
- return null;
- };
- },
- /**
- * Returns a function used to get the active track of type provided
- *
- * @param {String} type
- * MediaGroup type
- * @param {Object} settings
- * Object containing required information for media groups
- * @return {Function}
- * Function that returns the active media track for the provided type. Returns
- * null if no track is active
- * @function activeTrack.SUBTITLES
- */
- SUBTITLES: function SUBTITLES(type, settings) {
- return function () {
- var tracks = settings.mediaTypes[type].tracks;
-
-
- for (var id in tracks) {
- if (tracks[id].mode === 'showing') {
- return tracks[id];
- }
- }
-
- return null;
- };
- }
- };
-
- /**
- * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
- * Closed-Captions) specified in the master manifest.
- *
- * @param {Object} settings
- * Object containing required information for setting up the media groups
- * @param {SegmentLoader} settings.segmentLoaders.AUDIO
- * Audio segment loader
- * @param {SegmentLoader} settings.segmentLoaders.SUBTITLES
- * Subtitle segment loader
- * @param {SegmentLoader} settings.segmentLoaders.main
- * Main segment loader
- * @param {Tech} settings.tech
- * The tech of the player
- * @param {Object} settings.requestOptions
- * XHR request options used by the segment loaders
- * @param {PlaylistLoader} settings.masterPlaylistLoader
- * PlaylistLoader for the master source
- * @param {HlsHandler} settings.hls
- * HLS SourceHandler
- * @param {Object} settings.master
- * The parsed master manifest
- * @param {Object} settings.mediaTypes
- * Object to store the loaders, tracks, and utility methods for each media type
- * @param {Function} settings.blacklistCurrentPlaylist
- * Blacklists the current rendition and forces a rendition switch.
- * @function setupMediaGroups
- */
- var setupMediaGroups = function setupMediaGroups(settings) {
- ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) {
- initialize[type](type, settings);
- });
-
- var mediaTypes = settings.mediaTypes,
- masterPlaylistLoader = settings.masterPlaylistLoader,
- tech = settings.tech,
- hls = settings.hls;
-
- // setup active group and track getters and change event handlers
-
- ['AUDIO', 'SUBTITLES'].forEach(function (type) {
- mediaTypes[type].activeGroup = activeGroup(type, settings);
- mediaTypes[type].activeTrack = activeTrack[type](type, settings);
- mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
- mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
- });
-
- // DO NOT enable the default subtitle or caption track.
- // DO enable the default audio track
- var audioGroup = mediaTypes.AUDIO.activeGroup();
- var groupId = (audioGroup.filter(function (group) {
- return group.default;
- })[0] || audioGroup[0]).id;
-
- mediaTypes.AUDIO.tracks[groupId].enabled = true;
- mediaTypes.AUDIO.onTrackChanged();
-
- masterPlaylistLoader.on('mediachange', function () {
- ['AUDIO', 'SUBTITLES'].forEach(function (type) {
- return mediaTypes[type].onGroupChanged();
- });
- });
-
- // custom audio track change event handler for usage event
- var onAudioTrackChanged = function onAudioTrackChanged() {
- mediaTypes.AUDIO.onTrackChanged();
- tech.trigger({ type: 'usage', name: 'hls-audio-change' });
- };
-
- tech.audioTracks().addEventListener('change', onAudioTrackChanged);
- tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
-
- hls.on('dispose', function () {
- tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
- tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
- });
-
- // clear existing audio tracks and add the ones we just created
- tech.clearTracks('audio');
-
- for (var id in mediaTypes.AUDIO.tracks) {
- tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
- }
- };
-
- /**
- * Creates skeleton object used to store the loaders, tracks, and utility methods for each
- * media type
- *
- * @return {Object}
- * Object to store the loaders, tracks, and utility methods for each media type
- * @function createMediaTypes
- */
- var createMediaTypes = function createMediaTypes() {
- var mediaTypes = {};
-
- ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) {
- mediaTypes[type] = {
- groups: {},
- tracks: {},
- activePlaylistLoader: null,
- activeGroup: noop,
- activeTrack: noop,
- onGroupChanged: noop,
- onTrackChanged: noop
- };
- });
-
- return mediaTypes;
- };
-
- /**
- * @file master-playlist-controller.js
- */
-
- var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2;
-
- var Hls = void 0;
-
- // SegmentLoader stats that need to have each loader's
- // values summed to calculate the final value
- var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred'];
- var sumLoaderStat = function sumLoaderStat(stat) {
- return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
- };
-
- /**
- * the master playlist controller controller all interactons
- * between playlists and segmentloaders. At this time this mainly
- * involves a master playlist and a series of audio playlists
- * if they are available
- *
- * @class MasterPlaylistController
- * @extends videojs.EventTarget
- */
- var MasterPlaylistController = function (_videojs$EventTarget) {
- inherits$1(MasterPlaylistController, _videojs$EventTarget);
-
- function MasterPlaylistController(options) {
- classCallCheck$1(this, MasterPlaylistController);
-
- var _this = possibleConstructorReturn$1(this, (MasterPlaylistController.__proto__ || Object.getPrototypeOf(MasterPlaylistController)).call(this));
-
- var url = options.url,
- withCredentials = options.withCredentials,
- tech = options.tech,
- bandwidth = options.bandwidth,
- externHls = options.externHls,
- useCueTags = options.useCueTags,
- blacklistDuration = options.blacklistDuration,
- enableLowInitialPlaylist = options.enableLowInitialPlaylist,
- sourceType = options.sourceType,
- seekTo = options.seekTo;
-
-
- if (!url) {
- throw new Error('A non-empty playlist URL is required');
- }
-
- Hls = externHls;
-
- _this.withCredentials = withCredentials;
- _this.tech_ = tech;
- _this.hls_ = tech.hls;
- _this.seekTo_ = seekTo;
- _this.sourceType_ = sourceType;
- _this.useCueTags_ = useCueTags;
- _this.blacklistDuration = blacklistDuration;
- _this.enableLowInitialPlaylist = enableLowInitialPlaylist;
- if (_this.useCueTags_) {
- _this.cueTagsTrack_ = _this.tech_.addTextTrack('metadata', 'ad-cues');
- _this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
- }
-
- _this.requestOptions_ = {
- withCredentials: _this.withCredentials,
- timeout: null
- };
-
- _this.mediaTypes_ = createMediaTypes();
-
- _this.mediaSource = new videojs.MediaSource();
-
- // load the media source into the player
- _this.mediaSource.addEventListener('sourceopen', _this.handleSourceOpen_.bind(_this));
-
- _this.seekable_ = videojs.createTimeRanges();
- _this.hasPlayed_ = function () {
- return false;
- };
-
- _this.syncController_ = new SyncController(options);
- _this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
- kind: 'metadata',
- label: 'segment-metadata'
- }, false).track;
-
- _this.decrypter_ = new Decrypter$1();
- _this.inbandTextTracks_ = {};
-
- var segmentLoaderSettings = {
- hls: _this.hls_,
- mediaSource: _this.mediaSource,
- currentTime: _this.tech_.currentTime.bind(_this.tech_),
- seekable: function seekable$$1() {
- return _this.seekable();
- },
- seeking: function seeking() {
- return _this.tech_.seeking();
- },
- duration: function duration$$1() {
- return _this.mediaSource.duration;
- },
- hasPlayed: function hasPlayed() {
- return _this.hasPlayed_();
- },
- goalBufferLength: function goalBufferLength() {
- return _this.goalBufferLength();
- },
- bandwidth: bandwidth,
- syncController: _this.syncController_,
- decrypter: _this.decrypter_,
- sourceType: _this.sourceType_,
- inbandTextTracks: _this.inbandTextTracks_
- };
-
- _this.masterPlaylistLoader_ = _this.sourceType_ === 'dash' ? new DashPlaylistLoader(url, _this.hls_, _this.withCredentials) : new PlaylistLoader(url, _this.hls_, _this.withCredentials);
- _this.setupMasterPlaylistLoaderListeners_();
-
- // setup segment loaders
- // combined audio/video or just video when alternate audio track is selected
- _this.mainSegmentLoader_ = new SegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
- segmentMetadataTrack: _this.segmentMetadataTrack_,
- loaderType: 'main'
- }), options);
-
- // alternate audio track
- _this.audioSegmentLoader_ = new SegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
- loaderType: 'audio'
- }), options);
-
- _this.subtitleSegmentLoader_ = new VTTSegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
- loaderType: 'vtt'
- }), options);
-
- _this.setupSegmentLoaderListeners_();
-
- // Create SegmentLoader stat-getters
- loaderStats.forEach(function (stat) {
- _this[stat + '_'] = sumLoaderStat.bind(_this, stat);
- });
-
- _this.logger_ = logger('MPC');
-
- _this.masterPlaylistLoader_.load();
- return _this;
- }
-
- /**
- * Register event handlers on the master playlist loader. A helper
- * function for construction time.
- *
- * @private
- */
-
-
- createClass(MasterPlaylistController, [{
- key: 'setupMasterPlaylistLoaderListeners_',
- value: function setupMasterPlaylistLoaderListeners_() {
- var _this2 = this;
-
- this.masterPlaylistLoader_.on('loadedmetadata', function () {
- var media = _this2.masterPlaylistLoader_.media();
- var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
-
- // If we don't have any more available playlists, we don't want to
- // timeout the request.
- if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) {
- _this2.requestOptions_.timeout = 0;
- } else {
- _this2.requestOptions_.timeout = requestTimeout;
- }
-
- // if this isn't a live video and preload permits, start
- // downloading segments
- if (media.endList && _this2.tech_.preload() !== 'none') {
- _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
- _this2.mainSegmentLoader_.load();
- }
-
- setupMediaGroups({
- sourceType: _this2.sourceType_,
- segmentLoaders: {
- AUDIO: _this2.audioSegmentLoader_,
- SUBTITLES: _this2.subtitleSegmentLoader_,
- main: _this2.mainSegmentLoader_
- },
- tech: _this2.tech_,
- requestOptions: _this2.requestOptions_,
- masterPlaylistLoader: _this2.masterPlaylistLoader_,
- hls: _this2.hls_,
- master: _this2.master(),
- mediaTypes: _this2.mediaTypes_,
- blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2)
- });
-
- _this2.triggerPresenceUsage_(_this2.master(), media);
-
- try {
- _this2.setupSourceBuffers_();
- } catch (e) {
- videojs.log.warn('Failed to create SourceBuffers', e);
- return _this2.mediaSource.endOfStream('decode');
- }
- _this2.setupFirstPlay();
-
- _this2.trigger('selectedinitialmedia');
- });
-
- this.masterPlaylistLoader_.on('loadedplaylist', function () {
- var updatedPlaylist = _this2.masterPlaylistLoader_.media();
-
- if (!updatedPlaylist) {
- // blacklist any variants that are not supported by the browser before selecting
- // an initial media as the playlist selectors do not consider browser support
- _this2.excludeUnsupportedVariants_();
-
- var selectedMedia = void 0;
-
- if (_this2.enableLowInitialPlaylist) {
- selectedMedia = _this2.selectInitialPlaylist();
- }
-
- if (!selectedMedia) {
- selectedMedia = _this2.selectPlaylist();
- }
-
- _this2.initialMedia_ = selectedMedia;
- _this2.masterPlaylistLoader_.media(_this2.initialMedia_);
- return;
- }
-
- if (_this2.useCueTags_) {
- _this2.updateAdCues_(updatedPlaylist);
- }
-
- // TODO: Create a new event on the PlaylistLoader that signals
- // that the segments have changed in some way and use that to
- // update the SegmentLoader instead of doing it twice here and
- // on `mediachange`
- _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_);
- _this2.updateDuration();
-
- // If the player isn't paused, ensure that the segment loader is running,
- // as it is possible that it was temporarily stopped while waiting for
- // a playlist (e.g., in case the playlist errored and we re-requested it).
- if (!_this2.tech_.paused()) {
- _this2.mainSegmentLoader_.load();
- }
-
- if (!updatedPlaylist.endList) {
- var addSeekableRange = function addSeekableRange() {
- var seekable$$1 = _this2.seekable();
-
- if (seekable$$1.length !== 0) {
- _this2.mediaSource.addSeekableRange_(seekable$$1.start(0), seekable$$1.end(0));
- }
- };
-
- if (_this2.duration() !== Infinity) {
- var onDurationchange = function onDurationchange() {
- if (_this2.duration() === Infinity) {
- addSeekableRange();
- } else {
- _this2.tech_.one('durationchange', onDurationchange);
- }
- };
-
- _this2.tech_.one('durationchange', onDurationchange);
- } else {
- addSeekableRange();
- }
- }
- });
-
- this.masterPlaylistLoader_.on('error', function () {
- _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error);
- });
-
- this.masterPlaylistLoader_.on('mediachanging', function () {
- _this2.mainSegmentLoader_.abort();
- _this2.mainSegmentLoader_.pause();
- });
-
- this.masterPlaylistLoader_.on('mediachange', function () {
- var media = _this2.masterPlaylistLoader_.media();
- var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
-
- // If we don't have any more available playlists, we don't want to
- // timeout the request.
- if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) {
- _this2.requestOptions_.timeout = 0;
- } else {
- _this2.requestOptions_.timeout = requestTimeout;
- }
-
- // TODO: Create a new event on the PlaylistLoader that signals
- // that the segments have changed in some way and use that to
- // update the SegmentLoader instead of doing it twice here and
- // on `loadedplaylist`
- _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
- _this2.mainSegmentLoader_.load();
-
- _this2.tech_.trigger({
- type: 'mediachange',
- bubbles: true
- });
- });
-
- this.masterPlaylistLoader_.on('playlistunchanged', function () {
- var updatedPlaylist = _this2.masterPlaylistLoader_.media();
- var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist);
-
- if (playlistOutdated) {
- // Playlist has stopped updating and we're stuck at its end. Try to
- // blacklist it and switch to another playlist in the hope that that
- // one is updating (and give the player a chance to re-adjust to the
- // safe live point).
- _this2.blacklistCurrentPlaylist({
- message: 'Playlist no longer updating.'
- });
- // useful for monitoring QoS
- _this2.tech_.trigger('playliststuck');
- }
- });
-
- this.masterPlaylistLoader_.on('renditiondisabled', function () {
- _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' });
- });
- this.masterPlaylistLoader_.on('renditionenabled', function () {
- _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' });
- });
- }
-
- /**
- * A helper function for triggerring presence usage events once per source
- *
- * @private
- */
-
- }, {
- key: 'triggerPresenceUsage_',
- value: function triggerPresenceUsage_(master, media) {
- var mediaGroups = master.mediaGroups || {};
- var defaultDemuxed = true;
- var audioGroupKeys = Object.keys(mediaGroups.AUDIO);
-
- for (var mediaGroup in mediaGroups.AUDIO) {
- for (var label in mediaGroups.AUDIO[mediaGroup]) {
- var properties = mediaGroups.AUDIO[mediaGroup][label];
-
- if (!properties.uri) {
- defaultDemuxed = false;
- }
- }
- }
-
- if (defaultDemuxed) {
- this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' });
- }
-
- if (Object.keys(mediaGroups.SUBTITLES).length) {
- this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' });
- }
-
- if (Hls.Playlist.isAes(media)) {
- this.tech_.trigger({ type: 'usage', name: 'hls-aes' });
- }
-
- if (Hls.Playlist.isFmp4(media)) {
- this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' });
- }
-
- if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
- this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' });
- }
-
- if (this.useCueTags_) {
- this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' });
- }
- }
- /**
- * Register event handlers on the segment loaders. A helper function
- * for construction time.
- *
- * @private
- */
-
- }, {
- key: 'setupSegmentLoaderListeners_',
- value: function setupSegmentLoaderListeners_() {
- var _this3 = this;
-
- this.mainSegmentLoader_.on('bandwidthupdate', function () {
- var nextPlaylist = _this3.selectPlaylist();
- var currentPlaylist = _this3.masterPlaylistLoader_.media();
- var buffered = _this3.tech_.buffered();
- var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0;
-
- var bufferLowWaterLine = _this3.bufferLowWaterLine();
-
- // If the playlist is live, then we want to not take low water line into account.
- // This is because in LIVE, the player plays 3 segments from the end of the
- // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
- // in those segments, a viewer will never experience a rendition upswitch.
- if (!currentPlaylist.endList ||
- // For the same reason as LIVE, we ignore the low water line when the VOD
- // duration is below the max potential low water line
- _this3.duration() < Config.MAX_BUFFER_LOW_WATER_LINE ||
- // we want to switch down to lower resolutions quickly to continue playback, but
- nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH ||
- // ensure we have some buffer before we switch up to prevent us running out of
- // buffer while loading a higher rendition.
- forwardBuffer >= bufferLowWaterLine) {
- _this3.masterPlaylistLoader_.media(nextPlaylist);
- }
-
- _this3.tech_.trigger('bandwidthupdate');
- });
- this.mainSegmentLoader_.on('progress', function () {
- _this3.trigger('progress');
- });
-
- this.mainSegmentLoader_.on('error', function () {
- _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error());
- });
-
- this.mainSegmentLoader_.on('syncinfoupdate', function () {
- _this3.onSyncInfoUpdate_();
- });
-
- this.mainSegmentLoader_.on('timestampoffset', function () {
- _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' });
- });
- this.audioSegmentLoader_.on('syncinfoupdate', function () {
- _this3.onSyncInfoUpdate_();
- });
-
- this.mainSegmentLoader_.on('ended', function () {
- _this3.onEndOfStream();
- });
-
- this.mainSegmentLoader_.on('earlyabort', function () {
- _this3.blacklistCurrentPlaylist({
- message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.'
- }, ABORT_EARLY_BLACKLIST_SECONDS);
- });
-
- this.mainSegmentLoader_.on('reseteverything', function () {
- // If playing an MTS stream, a videojs.MediaSource is listening for
- // hls-reset to reset caption parsing state in the transmuxer
- _this3.tech_.trigger('hls-reset');
- });
-
- this.mainSegmentLoader_.on('segmenttimemapping', function (event) {
- // If playing an MTS stream in html, a videojs.MediaSource is listening for
- // hls-segment-time-mapping update its internal mapping of stream to display time
- _this3.tech_.trigger({
- type: 'hls-segment-time-mapping',
- mapping: event.mapping
- });
- });
-
- this.audioSegmentLoader_.on('ended', function () {
- _this3.onEndOfStream();
- });
- }
- }, {
- key: 'mediaSecondsLoaded_',
- value: function mediaSecondsLoaded_() {
- return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
- }
-
- /**
- * Call load on our SegmentLoaders
- */
-
- }, {
- key: 'load',
- value: function load() {
- this.mainSegmentLoader_.load();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.load();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.load();
- }
- }
-
- /**
- * Re-tune playback quality level for the current player
- * conditions. This method may perform destructive actions, like
- * removing already buffered content, to readjust the currently
- * active playlist quickly.
- *
- * @private
- */
-
- }, {
- key: 'fastQualityChange_',
- value: function fastQualityChange_() {
- var media = this.selectPlaylist();
-
- if (media !== this.masterPlaylistLoader_.media()) {
- this.masterPlaylistLoader_.media(media);
-
- this.mainSegmentLoader_.resetLoader();
- // don't need to reset audio as it is reset when media changes
- }
- }
-
- /**
- * Begin playback.
- */
-
- }, {
- key: 'play',
- value: function play() {
- if (this.setupFirstPlay()) {
- return;
- }
-
- if (this.tech_.ended()) {
- this.seekTo_(0);
- }
-
- if (this.hasPlayed_()) {
- this.load();
- }
-
- var seekable$$1 = this.tech_.seekable();
-
- // if the viewer has paused and we fell out of the live window,
- // seek forward to the live point
- if (this.tech_.duration() === Infinity) {
- if (this.tech_.currentTime() < seekable$$1.start(0)) {
- return this.seekTo_(seekable$$1.end(seekable$$1.length - 1));
- }
- }
- }
-
- /**
- * Seek to the latest media position if this is a live video and the
- * player and video are loaded and initialized.
- */
-
- }, {
- key: 'setupFirstPlay',
- value: function setupFirstPlay() {
- var _this4 = this;
-
- var media = this.masterPlaylistLoader_.media();
-
- // Check that everything is ready to begin buffering for the first call to play
- // If 1) there is no active media
- // 2) the player is paused
- // 3) the first play has already been setup
- // then exit early
- if (!media || this.tech_.paused() || this.hasPlayed_()) {
- return false;
- }
-
- // when the video is a live stream
- if (!media.endList) {
- var seekable$$1 = this.seekable();
-
- if (!seekable$$1.length) {
- // without a seekable range, the player cannot seek to begin buffering at the live
- // point
- return false;
- }
-
- if (videojs.browser.IE_VERSION && this.tech_.readyState() === 0) {
- // IE11 throws an InvalidStateError if you try to set currentTime while the
- // readyState is 0, so it must be delayed until the tech fires loadedmetadata.
- this.tech_.one('loadedmetadata', function () {
- _this4.trigger('firstplay');
- _this4.seekTo_(seekable$$1.end(0));
- _this4.hasPlayed_ = function () {
- return true;
- };
- });
-
- return false;
- }
-
- // trigger firstplay to inform the source handler to ignore the next seek event
- this.trigger('firstplay');
- // seek to the live point
- this.seekTo_(seekable$$1.end(0));
- }
-
- this.hasPlayed_ = function () {
- return true;
- };
- // we can begin loading now that everything is ready
- this.load();
- return true;
- }
-
- /**
- * handle the sourceopen event on the MediaSource
- *
- * @private
- */
-
- }, {
- key: 'handleSourceOpen_',
- value: function handleSourceOpen_() {
- // Only attempt to create the source buffer if none already exist.
- // handleSourceOpen is also called when we are "re-opening" a source buffer
- // after `endOfStream` has been called (in response to a seek for instance)
- try {
- this.setupSourceBuffers_();
- } catch (e) {
- videojs.log.warn('Failed to create Source Buffers', e);
- return this.mediaSource.endOfStream('decode');
- }
-
- // if autoplay is enabled, begin playback. This is duplicative of
- // code in video.js but is required because play() must be invoked
- // *after* the media source has opened.
- if (this.tech_.autoplay()) {
- var playPromise = this.tech_.play();
-
- // Catch/silence error when a pause interrupts a play request
- // on browsers which return a promise
- if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
- playPromise.then(null, function (e) {});
- }
- }
-
- this.trigger('sourceopen');
- }
-
- /**
- * Calls endOfStream on the media source when all active stream types have called
- * endOfStream
- *
- * @param {string} streamType
- * Stream type of the segment loader that called endOfStream
- * @private
- */
-
- }, {
- key: 'onEndOfStream',
- value: function onEndOfStream() {
- var isEndOfStream = this.mainSegmentLoader_.ended_;
-
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- // if the audio playlist loader exists, then alternate audio is active
- if (!this.mainSegmentLoader_.startingMedia_ || this.mainSegmentLoader_.startingMedia_.containsVideo) {
- // if we do not know if the main segment loader contains video yet or if we
- // definitively know the main segment loader contains video, then we need to wait
- // for both main and audio segment loaders to call endOfStream
- isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
- } else {
- // otherwise just rely on the audio loader
- isEndOfStream = this.audioSegmentLoader_.ended_;
- }
- }
-
- if (isEndOfStream) {
- this.mediaSource.endOfStream();
- }
- }
-
- /**
- * Check if a playlist has stopped being updated
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist has stopped being updated or not
- */
-
- }, {
- key: 'stuckAtPlaylistEnd_',
- value: function stuckAtPlaylistEnd_(playlist) {
- var seekable$$1 = this.seekable();
-
- if (!seekable$$1.length) {
- // playlist doesn't have enough information to determine whether we are stuck
- return false;
- }
-
- var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration);
-
- if (expired === null) {
- return false;
- }
-
- // does not use the safe live end to calculate playlist end, since we
- // don't want to say we are stuck while there is still content
- var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired);
- var currentTime = this.tech_.currentTime();
- var buffered = this.tech_.buffered();
-
- if (!buffered.length) {
- // return true if the playhead reached the absolute end of the playlist
- return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA;
- }
- var bufferedEnd = buffered.end(buffered.length - 1);
-
- // return true if there is too little buffer left and buffer has reached absolute
- // end of playlist
- return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA;
- }
-
- /**
- * Blacklists a playlist when an error occurs for a set amount of time
- * making it unavailable for selection by the rendition selection algorithm
- * and then forces a new playlist (rendition) selection.
- *
- * @param {Object=} error an optional error that may include the playlist
- * to blacklist
- * @param {Number=} blacklistDuration an optional number of seconds to blacklist the
- * playlist
- */
-
- }, {
- key: 'blacklistCurrentPlaylist',
- value: function blacklistCurrentPlaylist() {
- var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
- var blacklistDuration = arguments[1];
-
- var currentPlaylist = void 0;
- var nextPlaylist = void 0;
-
- // If the `error` was generated by the playlist loader, it will contain
- // the playlist we were trying to load (but failed) and that should be
- // blacklisted instead of the currently selected playlist which is likely
- // out-of-date in this scenario
- currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
-
- blacklistDuration = blacklistDuration || error.blacklistDuration || this.blacklistDuration;
-
- // If there is no current playlist, then an error occurred while we were
- // trying to load the master OR while we were disposing of the tech
- if (!currentPlaylist) {
- this.error = error;
-
- try {
- return this.mediaSource.endOfStream('network');
- } catch (e) {
- return this.trigger('error');
- }
- }
-
- var isFinalRendition = this.masterPlaylistLoader_.master.playlists.filter(isEnabled).length === 1;
-
- if (isFinalRendition) {
- // Never blacklisting this playlist because it's final rendition
- videojs.log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.');
-
- this.tech_.trigger('retryplaylist');
- return this.masterPlaylistLoader_.load(isFinalRendition);
- }
- // Blacklist this playlist
- currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000;
- this.tech_.trigger('blacklistplaylist');
- this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' });
-
- // Select a new playlist
- nextPlaylist = this.selectPlaylist();
- videojs.log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.');
-
- return this.masterPlaylistLoader_.media(nextPlaylist);
- }
-
- /**
- * Pause all segment loaders
- */
-
- }, {
- key: 'pauseLoading',
- value: function pauseLoading() {
- this.mainSegmentLoader_.pause();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.pause();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.pause();
- }
- }
-
- /**
- * set the current time on all segment loaders
- *
- * @param {TimeRange} currentTime the current time to set
- * @return {TimeRange} the current time
- */
-
- }, {
- key: 'setCurrentTime',
- value: function setCurrentTime(currentTime) {
- var buffered = findRange(this.tech_.buffered(), currentTime);
-
- if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
- // return immediately if the metadata is not ready yet
- return 0;
- }
-
- // it's clearly an edge-case but don't thrown an error if asked to
- // seek within an empty playlist
- if (!this.masterPlaylistLoader_.media().segments) {
- return 0;
- }
-
- // In flash playback, the segment loaders should be reset on every seek, even
- // in buffer seeks. If the seek location is already buffered, continue buffering as
- // usual
- // TODO: redo this comment
- if (buffered && buffered.length) {
- return currentTime;
- }
-
- // cancel outstanding requests so we begin buffering at the new
- // location
- this.mainSegmentLoader_.resetEverything();
- this.mainSegmentLoader_.abort();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.resetEverything();
- this.audioSegmentLoader_.abort();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.resetEverything();
- this.subtitleSegmentLoader_.abort();
- }
-
- // start segment loader loading in case they are paused
- this.load();
- }
-
- /**
- * get the current duration
- *
- * @return {TimeRange} the duration
- */
-
- }, {
- key: 'duration',
- value: function duration$$1() {
- if (!this.masterPlaylistLoader_) {
- return 0;
- }
-
- if (this.mediaSource) {
- return this.mediaSource.duration;
- }
-
- return Hls.Playlist.duration(this.masterPlaylistLoader_.media());
- }
-
- /**
- * check the seekable range
- *
- * @return {TimeRange} the seekable range
- */
-
- }, {
- key: 'seekable',
- value: function seekable$$1() {
- return this.seekable_;
- }
- }, {
- key: 'onSyncInfoUpdate_',
- value: function onSyncInfoUpdate_() {
- var mainSeekable = void 0;
- var audioSeekable = void 0;
-
- if (!this.masterPlaylistLoader_) {
- return;
- }
-
- var media = this.masterPlaylistLoader_.media();
-
- if (!media) {
- return;
- }
-
- var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
-
- if (expired === null) {
- // not enough information to update seekable
- return;
- }
-
- mainSeekable = Hls.Playlist.seekable(media, expired);
-
- if (mainSeekable.length === 0) {
- return;
- }
-
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
- expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
-
- if (expired === null) {
- return;
- }
-
- audioSeekable = Hls.Playlist.seekable(media, expired);
-
- if (audioSeekable.length === 0) {
- return;
- }
- }
-
- if (!audioSeekable) {
- // seekable has been calculated based on buffering video data so it
- // can be returned directly
- this.seekable_ = mainSeekable;
- } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) {
- // seekables are pretty far off, rely on main
- this.seekable_ = mainSeekable;
- } else {
- this.seekable_ = videojs.createTimeRanges([[audioSeekable.start(0) > mainSeekable.start(0) ? audioSeekable.start(0) : mainSeekable.start(0), audioSeekable.end(0) < mainSeekable.end(0) ? audioSeekable.end(0) : mainSeekable.end(0)]]);
- }
-
- this.logger_('seekable updated [' + printableRange(this.seekable_) + ']');
-
- this.tech_.trigger('seekablechanged');
- }
-
- /**
- * Update the player duration
- */
-
- }, {
- key: 'updateDuration',
- value: function updateDuration() {
- var _this5 = this;
-
- var oldDuration = this.mediaSource.duration;
- var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media());
- var buffered = this.tech_.buffered();
- var setDuration = function setDuration() {
- _this5.mediaSource.duration = newDuration;
- _this5.tech_.trigger('durationchange');
-
- _this5.mediaSource.removeEventListener('sourceopen', setDuration);
- };
-
- if (buffered.length > 0) {
- newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
- }
-
- // if the duration has changed, invalidate the cached value
- if (oldDuration !== newDuration) {
- // update the duration
- if (this.mediaSource.readyState !== 'open') {
- this.mediaSource.addEventListener('sourceopen', setDuration);
- } else {
- setDuration();
- }
- }
- }
-
- /**
- * dispose of the MasterPlaylistController and everything
- * that it controls
- */
-
- }, {
- key: 'dispose',
- value: function dispose() {
- var _this6 = this;
-
- this.decrypter_.terminate();
- this.masterPlaylistLoader_.dispose();
- this.mainSegmentLoader_.dispose();
-
- ['AUDIO', 'SUBTITLES'].forEach(function (type) {
- var groups = _this6.mediaTypes_[type].groups;
-
- for (var id in groups) {
- groups[id].forEach(function (group) {
- if (group.playlistLoader) {
- group.playlistLoader.dispose();
- }
- });
- }
- });
-
- this.audioSegmentLoader_.dispose();
- this.subtitleSegmentLoader_.dispose();
- }
-
- /**
- * return the master playlist object if we have one
- *
- * @return {Object} the master playlist object that we parsed
- */
-
- }, {
- key: 'master',
- value: function master() {
- return this.masterPlaylistLoader_.master;
- }
-
- /**
- * return the currently selected playlist
- *
- * @return {Object} the currently selected playlist object that we parsed
- */
-
- }, {
- key: 'media',
- value: function media() {
- // playlist loader will not return media if it has not been fully loaded
- return this.masterPlaylistLoader_.media() || this.initialMedia_;
- }
-
- /**
- * setup our internal source buffers on our segment Loaders
- *
- * @private
- */
-
- }, {
- key: 'setupSourceBuffers_',
- value: function setupSourceBuffers_() {
- var media = this.masterPlaylistLoader_.media();
- var mimeTypes = void 0;
-
- // wait until a media playlist is available and the Media Source is
- // attached
- if (!media || this.mediaSource.readyState !== 'open') {
- return;
- }
-
- mimeTypes = mimeTypesForPlaylist(this.masterPlaylistLoader_.master, media);
- if (mimeTypes.length < 1) {
- this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri;
- return this.mediaSource.endOfStream('decode');
- }
-
- this.configureLoaderMimeTypes_(mimeTypes);
- // exclude any incompatible variant streams from future playlist
- // selection
- this.excludeIncompatibleVariants_(media);
- }
- }, {
- key: 'configureLoaderMimeTypes_',
- value: function configureLoaderMimeTypes_(mimeTypes) {
- // If the content is demuxed, we can't start appending segments to a source buffer
- // until both source buffers are set up, or else the browser may not let us add the
- // second source buffer (it will assume we are playing either audio only or video
- // only).
- var sourceBufferEmitter =
- // If there is more than one mime type
- mimeTypes.length > 1 &&
- // and the first mime type does not have muxed video and audio
- mimeTypes[0].indexOf(',') === -1 &&
- // and the two mime types are different (they can be the same in the case of audio
- // only with alternate audio)
- mimeTypes[0] !== mimeTypes[1] ?
- // then we want to wait on the second source buffer
- new videojs.EventTarget() :
- // otherwise there is no need to wait as the content is either audio only,
- // video only, or muxed content.
- null;
-
- this.mainSegmentLoader_.mimeType(mimeTypes[0], sourceBufferEmitter);
- if (mimeTypes[1]) {
- this.audioSegmentLoader_.mimeType(mimeTypes[1], sourceBufferEmitter);
- }
- }
-
- /**
- * Blacklists playlists with codecs that are unsupported by the browser.
- */
-
- }, {
- key: 'excludeUnsupportedVariants_',
- value: function excludeUnsupportedVariants_() {
- this.master().playlists.forEach(function (variant) {
- if (variant.attributes.CODECS && window_1.MediaSource && window_1.MediaSource.isTypeSupported && !window_1.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs(variant.attributes.CODECS) + '"')) {
- variant.excludeUntil = Infinity;
- }
- });
- }
-
- /**
- * Blacklist playlists that are known to be codec or
- * stream-incompatible with the SourceBuffer configuration. For
- * instance, Media Source Extensions would cause the video element to
- * stall waiting for video data if you switched from a variant with
- * video and audio to an audio-only one.
- *
- * @param {Object} media a media playlist compatible with the current
- * set of SourceBuffers. Variants in the current master playlist that
- * do not appear to have compatible codec or stream configurations
- * will be excluded from the default playlist selection algorithm
- * indefinitely.
- * @private
- */
-
- }, {
- key: 'excludeIncompatibleVariants_',
- value: function excludeIncompatibleVariants_(media) {
- var codecCount = 2;
- var videoCodec = null;
- var codecs = void 0;
-
- if (media.attributes.CODECS) {
- codecs = parseCodecs(media.attributes.CODECS);
- videoCodec = codecs.videoCodec;
- codecCount = codecs.codecCount;
- }
-
- this.master().playlists.forEach(function (variant) {
- var variantCodecs = {
- codecCount: 2,
- videoCodec: null
- };
-
- if (variant.attributes.CODECS) {
- variantCodecs = parseCodecs(variant.attributes.CODECS);
- }
-
- // if the streams differ in the presence or absence of audio or
- // video, they are incompatible
- if (variantCodecs.codecCount !== codecCount) {
- variant.excludeUntil = Infinity;
- }
-
- // if h.264 is specified on the current playlist, some flavor of
- // it must be specified on all compatible variants
- if (variantCodecs.videoCodec !== videoCodec) {
- variant.excludeUntil = Infinity;
- }
- });
- }
- }, {
- key: 'updateAdCues_',
- value: function updateAdCues_(media) {
- var offset = 0;
- var seekable$$1 = this.seekable();
-
- if (seekable$$1.length) {
- offset = seekable$$1.start(0);
- }
-
- updateAdCues(media, this.cueTagsTrack_, offset);
- }
-
- /**
- * Calculates the desired forward buffer length based on current time
- *
- * @return {Number} Desired forward buffer length in seconds
- */
-
- }, {
- key: 'goalBufferLength',
- value: function goalBufferLength() {
- var currentTime = this.tech_.currentTime();
- var initial = Config.GOAL_BUFFER_LENGTH;
- var rate = Config.GOAL_BUFFER_LENGTH_RATE;
- var max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH);
-
- return Math.min(initial + currentTime * rate, max);
- }
-
- /**
- * Calculates the desired buffer low water line based on current time
- *
- * @return {Number} Desired buffer low water line in seconds
- */
-
- }, {
- key: 'bufferLowWaterLine',
- value: function bufferLowWaterLine() {
- var currentTime = this.tech_.currentTime();
- var initial = Config.BUFFER_LOW_WATER_LINE;
- var rate = Config.BUFFER_LOW_WATER_LINE_RATE;
- var max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
-
- return Math.min(initial + currentTime * rate, max);
- }
- }]);
- return MasterPlaylistController;
- }(videojs.EventTarget);
-
- /**
- * Returns a function that acts as the Enable/disable playlist function.
- *
- * @param {PlaylistLoader} loader - The master playlist loader
- * @param {String} playlistUri - uri of the playlist
- * @param {Function} changePlaylistFn - A function to be called after a
- * playlist's enabled-state has been changed. Will NOT be called if a
- * playlist's enabled-state is unchanged
- * @param {Boolean=} enable - Value to set the playlist enabled-state to
- * or if undefined returns the current enabled-state for the playlist
- * @return {Function} Function for setting/getting enabled
- */
- var enableFunction = function enableFunction(loader, playlistUri, changePlaylistFn) {
- return function (enable) {
- var playlist = loader.master.playlists[playlistUri];
- var incompatible = isIncompatible(playlist);
- var currentlyEnabled = isEnabled(playlist);
-
- if (typeof enable === 'undefined') {
- return currentlyEnabled;
- }
-
- if (enable) {
- delete playlist.disabled;
- } else {
- playlist.disabled = true;
- }
-
- if (enable !== currentlyEnabled && !incompatible) {
- // Ensure the outside world knows about our changes
- changePlaylistFn();
- if (enable) {
- loader.trigger('renditionenabled');
- } else {
- loader.trigger('renditiondisabled');
- }
- }
- return enable;
- };
- };
-
- /**
- * The representation object encapsulates the publicly visible information
- * in a media playlist along with a setter/getter-type function (enabled)
- * for changing the enabled-state of a particular playlist entry
- *
- * @class Representation
- */
-
- var Representation = function Representation(hlsHandler, playlist, id) {
- classCallCheck$1(this, Representation);
-
- // Get a reference to a bound version of fastQualityChange_
- var fastChangeFunction = hlsHandler.masterPlaylistController_.fastQualityChange_.bind(hlsHandler.masterPlaylistController_);
-
- // some playlist attributes are optional
- if (playlist.attributes.RESOLUTION) {
- var resolution = playlist.attributes.RESOLUTION;
-
- this.width = resolution.width;
- this.height = resolution.height;
- }
-
- this.bandwidth = playlist.attributes.BANDWIDTH;
-
- // The id is simply the ordinality of the media playlist
- // within the master playlist
- this.id = id;
-
- // Partially-apply the enableFunction to create a playlist-
- // specific variant
- this.enabled = enableFunction(hlsHandler.playlists, playlist.uri, fastChangeFunction);
- };
-
- /**
- * A mixin function that adds the `representations` api to an instance
- * of the HlsHandler class
- * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the
- * representation API into
- */
-
-
- var renditionSelectionMixin = function renditionSelectionMixin(hlsHandler) {
- var playlists = hlsHandler.playlists;
-
- // Add a single API-specific function to the HlsHandler instance
- hlsHandler.representations = function () {
- return playlists.master.playlists.filter(function (media) {
- return !isIncompatible(media);
- }).map(function (e, i) {
- return new Representation(hlsHandler, e, e.uri);
- });
- };
- };
-
- /**
- * @file playback-watcher.js
- *
- * Playback starts, and now my watch begins. It shall not end until my death. I shall
- * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns
- * and win no glory. I shall live and die at my post. I am the corrector of the underflow.
- * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge
- * my life and honor to the Playback Watch, for this Player and all the Players to come.
- */
-
- // Set of events that reset the playback-watcher time check logic and clear the timeout
- var timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error'];
-
- /**
- * @class PlaybackWatcher
- */
-
- var PlaybackWatcher = function () {
- /**
- * Represents an PlaybackWatcher object.
- * @constructor
- * @param {object} options an object that includes the tech and settings
- */
- function PlaybackWatcher(options) {
- var _this = this;
-
- classCallCheck$1(this, PlaybackWatcher);
-
- this.tech_ = options.tech;
- this.seekable = options.seekable;
- this.seekTo = options.seekTo;
-
- this.consecutiveUpdates = 0;
- this.lastRecordedTime = null;
- this.timer_ = null;
- this.checkCurrentTimeTimeout_ = null;
- this.logger_ = logger('PlaybackWatcher');
-
- this.logger_('initialize');
-
- var canPlayHandler = function canPlayHandler() {
- return _this.monitorCurrentTime_();
- };
- var waitingHandler = function waitingHandler() {
- return _this.techWaiting_();
- };
- var cancelTimerHandler = function cancelTimerHandler() {
- return _this.cancelTimer_();
- };
- var fixesBadSeeksHandler = function fixesBadSeeksHandler() {
- return _this.fixesBadSeeks_();
- };
-
- this.tech_.on('seekablechanged', fixesBadSeeksHandler);
- this.tech_.on('waiting', waitingHandler);
- this.tech_.on(timerCancelEvents, cancelTimerHandler);
- this.tech_.on('canplay', canPlayHandler);
-
- // Define the dispose function to clean up our events
- this.dispose = function () {
- _this.logger_('dispose');
- _this.tech_.off('seekablechanged', fixesBadSeeksHandler);
- _this.tech_.off('waiting', waitingHandler);
- _this.tech_.off(timerCancelEvents, cancelTimerHandler);
- _this.tech_.off('canplay', canPlayHandler);
- if (_this.checkCurrentTimeTimeout_) {
- window_1.clearTimeout(_this.checkCurrentTimeTimeout_);
- }
- _this.cancelTimer_();
- };
- }
-
- /**
- * Periodically check current time to see if playback stopped
- *
- * @private
- */
-
-
- createClass(PlaybackWatcher, [{
- key: 'monitorCurrentTime_',
- value: function monitorCurrentTime_() {
- this.checkCurrentTime_();
-
- if (this.checkCurrentTimeTimeout_) {
- window_1.clearTimeout(this.checkCurrentTimeTimeout_);
- }
-
- // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
- this.checkCurrentTimeTimeout_ = window_1.setTimeout(this.monitorCurrentTime_.bind(this), 250);
- }
-
- /**
- * The purpose of this function is to emulate the "waiting" event on
- * browsers that do not emit it when they are waiting for more
- * data to continue playback
- *
- * @private
- */
-
- }, {
- key: 'checkCurrentTime_',
- value: function checkCurrentTime_() {
- if (this.tech_.seeking() && this.fixesBadSeeks_()) {
- this.consecutiveUpdates = 0;
- this.lastRecordedTime = this.tech_.currentTime();
- return;
- }
-
- if (this.tech_.paused() || this.tech_.seeking()) {
- return;
- }
-
- var currentTime = this.tech_.currentTime();
- var buffered = this.tech_.buffered();
-
- if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) {
- // If current time is at the end of the final buffered region, then any playback
- // stall is most likely caused by buffering in a low bandwidth environment. The tech
- // should fire a `waiting` event in this scenario, but due to browser and tech
- // inconsistencies. Calling `techWaiting_` here allows us to simulate
- // responding to a native `waiting` event when the tech fails to emit one.
- return this.techWaiting_();
- }
-
- if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) {
- this.consecutiveUpdates++;
- this.waiting_();
- } else if (currentTime === this.lastRecordedTime) {
- this.consecutiveUpdates++;
- } else {
- this.consecutiveUpdates = 0;
- this.lastRecordedTime = currentTime;
- }
- }
-
- /**
- * Cancels any pending timers and resets the 'timeupdate' mechanism
- * designed to detect that we are stalled
- *
- * @private
- */
-
- }, {
- key: 'cancelTimer_',
- value: function cancelTimer_() {
- this.consecutiveUpdates = 0;
-
- if (this.timer_) {
- this.logger_('cancelTimer_');
- clearTimeout(this.timer_);
- }
-
- this.timer_ = null;
- }
-
- /**
- * Fixes situations where there's a bad seek
- *
- * @return {Boolean} whether an action was taken to fix the seek
- * @private
- */
-
- }, {
- key: 'fixesBadSeeks_',
- value: function fixesBadSeeks_() {
- var seeking = this.tech_.seeking();
- var seekable = this.seekable();
- var currentTime = this.tech_.currentTime();
- var seekTo = void 0;
-
- if (seeking && this.afterSeekableWindow_(seekable, currentTime)) {
- var seekableEnd = seekable.end(seekable.length - 1);
-
- // sync to live point (if VOD, our seekable was updated and we're simply adjusting)
- seekTo = seekableEnd;
- }
-
- if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) {
- var seekableStart = seekable.start(0);
-
- // sync to the beginning of the live window
- // provide a buffer of .1 seconds to handle rounding/imprecise numbers
- seekTo = seekableStart + SAFE_TIME_DELTA;
- }
-
- if (typeof seekTo !== 'undefined') {
- this.logger_('Trying to seek outside of seekable at time ' + currentTime + ' with ' + ('seekable range ' + printableRange(seekable) + '. Seeking to ') + (seekTo + '.'));
-
- this.seekTo(seekTo);
- return true;
- }
-
- return false;
- }
-
- /**
- * Handler for situations when we determine the player is waiting.
- *
- * @private
- */
-
- }, {
- key: 'waiting_',
- value: function waiting_() {
- if (this.techWaiting_()) {
- return;
- }
-
- // All tech waiting checks failed. Use last resort correction
- var currentTime = this.tech_.currentTime();
- var buffered = this.tech_.buffered();
- var currentRange = findRange(buffered, currentTime);
-
- // Sometimes the player can stall for unknown reasons within a contiguous buffered
- // region with no indication that anything is amiss (seen in Firefox). Seeking to
- // currentTime is usually enough to kickstart the player. This checks that the player
- // is currently within a buffered region before attempting a corrective seek.
- // Chrome does not appear to continue `timeupdate` events after a `waiting` event
- // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also
- // make sure there is ~3 seconds of forward buffer before taking any corrective action
- // to avoid triggering an `unknownwaiting` event when the network is slow.
- if (currentRange.length && currentTime + 3 <= currentRange.end(0)) {
- this.cancelTimer_();
- this.seekTo(currentTime);
-
- this.logger_('Stopped at ' + currentTime + ' while inside a buffered region ' + ('[' + currentRange.start(0) + ' -> ' + currentRange.end(0) + ']. Attempting to resume ') + 'playback by seeking to the current time.');
-
- // unknown waiting corrections may be useful for monitoring QoS
- this.tech_.trigger({ type: 'usage', name: 'hls-unknown-waiting' });
- return;
- }
- }
-
- /**
- * Handler for situations when the tech fires a `waiting` event
- *
- * @return {Boolean}
- * True if an action (or none) was needed to correct the waiting. False if no
- * checks passed
- * @private
- */
-
- }, {
- key: 'techWaiting_',
- value: function techWaiting_() {
- var seekable = this.seekable();
- var currentTime = this.tech_.currentTime();
-
- if (this.tech_.seeking() && this.fixesBadSeeks_()) {
- // Tech is seeking or bad seek fixed, no action needed
- return true;
- }
-
- if (this.tech_.seeking() || this.timer_ !== null) {
- // Tech is seeking or already waiting on another action, no action needed
- return true;
- }
-
- if (this.beforeSeekableWindow_(seekable, currentTime)) {
- var livePoint = seekable.end(seekable.length - 1);
-
- this.logger_('Fell out of live window at time ' + currentTime + '. Seeking to ' + ('live point (seekable end) ' + livePoint));
- this.cancelTimer_();
- this.seekTo(livePoint);
-
- // live window resyncs may be useful for monitoring QoS
- this.tech_.trigger({ type: 'usage', name: 'hls-live-resync' });
- return true;
- }
-
- var buffered = this.tech_.buffered();
- var nextRange = findNextRange(buffered, currentTime);
-
- if (this.videoUnderflow_(nextRange, buffered, currentTime)) {
- // Even though the video underflowed and was stuck in a gap, the audio overplayed
- // the gap, leading currentTime into a buffered range. Seeking to currentTime
- // allows the video to catch up to the audio position without losing any audio
- // (only suffering ~3 seconds of frozen video and a pause in audio playback).
- this.cancelTimer_();
- this.seekTo(currentTime);
-
- // video underflow may be useful for monitoring QoS
- this.tech_.trigger({ type: 'usage', name: 'hls-video-underflow' });
- return true;
- }
-
- // check for gap
- if (nextRange.length > 0) {
- var difference = nextRange.start(0) - currentTime;
-
- this.logger_('Stopped at ' + currentTime + ', setting timer for ' + difference + ', seeking ' + ('to ' + nextRange.start(0)));
-
- this.timer_ = setTimeout(this.skipTheGap_.bind(this), difference * 1000, currentTime);
- return true;
- }
-
- // All checks failed. Returning false to indicate failure to correct waiting
- return false;
- }
- }, {
- key: 'afterSeekableWindow_',
- value: function afterSeekableWindow_(seekable, currentTime) {
- if (!seekable.length) {
- // we can't make a solid case if there's no seekable, default to false
- return false;
- }
-
- if (currentTime > seekable.end(seekable.length - 1) + SAFE_TIME_DELTA) {
- return true;
- }
-
- return false;
- }
- }, {
- key: 'beforeSeekableWindow_',
- value: function beforeSeekableWindow_(seekable, currentTime) {
- if (seekable.length &&
- // can't fall before 0 and 0 seekable start identifies VOD stream
- seekable.start(0) > 0 && currentTime < seekable.start(0) - SAFE_TIME_DELTA) {
- return true;
- }
-
- return false;
- }
- }, {
- key: 'videoUnderflow_',
- value: function videoUnderflow_(nextRange, buffered, currentTime) {
- if (nextRange.length === 0) {
- // Even if there is no available next range, there is still a possibility we are
- // stuck in a gap due to video underflow.
- var gap = this.gapFromVideoUnderflow_(buffered, currentTime);
-
- if (gap) {
- this.logger_('Encountered a gap in video from ' + gap.start + ' to ' + gap.end + '. ' + ('Seeking to current time ' + currentTime));
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Timer callback. If playback still has not proceeded, then we seek
- * to the start of the next buffered region.
- *
- * @private
- */
-
- }, {
- key: 'skipTheGap_',
- value: function skipTheGap_(scheduledCurrentTime) {
- var buffered = this.tech_.buffered();
- var currentTime = this.tech_.currentTime();
- var nextRange = findNextRange(buffered, currentTime);
-
- this.cancelTimer_();
-
- if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) {
- return;
- }
-
- this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0));
-
- // only seek if we still have not played
- this.seekTo(nextRange.start(0) + TIME_FUDGE_FACTOR);
-
- this.tech_.trigger({ type: 'usage', name: 'hls-gap-skip' });
- }
- }, {
- key: 'gapFromVideoUnderflow_',
- value: function gapFromVideoUnderflow_(buffered, currentTime) {
- // At least in Chrome, if there is a gap in the video buffer, the audio will continue
- // playing for ~3 seconds after the video gap starts. This is done to account for
- // video buffer underflow/underrun (note that this is not done when there is audio
- // buffer underflow/underrun -- in that case the video will stop as soon as it
- // encounters the gap, as audio stalls are more noticeable/jarring to a user than
- // video stalls). The player's time will reflect the playthrough of audio, so the
- // time will appear as if we are in a buffered region, even if we are stuck in a
- // "gap."
- //
- // Example:
- // video buffer: 0 => 10.1, 10.2 => 20
- // audio buffer: 0 => 20
- // overall buffer: 0 => 10.1, 10.2 => 20
- // current time: 13
- //
- // Chrome's video froze at 10 seconds, where the video buffer encountered the gap,
- // however, the audio continued playing until it reached ~3 seconds past the gap
- // (13 seconds), at which point it stops as well. Since current time is past the
- // gap, findNextRange will return no ranges.
- //
- // To check for this issue, we see if there is a gap that starts somewhere within
- // a 3 second range (3 seconds +/- 1 second) back from our current time.
- var gaps = findGaps(buffered);
-
- for (var i = 0; i < gaps.length; i++) {
- var start = gaps.start(i);
- var end = gaps.end(i);
-
- // gap is starts no more than 4 seconds back
- if (currentTime - start < 4 && currentTime - start > 2) {
- return {
- start: start,
- end: end
- };
- }
- }
-
- return null;
- }
- }]);
- return PlaybackWatcher;
- }();
-
- var defaultOptions = {
- errorInterval: 30,
- getSource: function getSource(next) {
- var tech = this.tech({ IWillNotUseThisInPlugins: true });
- var sourceObj = tech.currentSource_;
-
- return next(sourceObj);
- }
- };
-
- /**
- * Main entry point for the plugin
- *
- * @param {Player} player a reference to a videojs Player instance
- * @param {Object} [options] an object with plugin options
- * @private
- */
- var initPlugin = function initPlugin(player, options) {
- var lastCalled = 0;
- var seekTo = 0;
- var localOptions = videojs.mergeOptions(defaultOptions, options);
-
- player.ready(function () {
- player.trigger({ type: 'usage', name: 'hls-error-reload-initialized' });
- });
-
- /**
- * Player modifications to perform that must wait until `loadedmetadata`
- * has been triggered
- *
- * @private
- */
- var loadedMetadataHandler = function loadedMetadataHandler() {
- if (seekTo) {
- player.currentTime(seekTo);
- }
- };
-
- /**
- * Set the source on the player element, play, and seek if necessary
- *
- * @param {Object} sourceObj An object specifying the source url and mime-type to play
- * @private
- */
- var setSource = function setSource(sourceObj) {
- if (sourceObj === null || sourceObj === undefined) {
- return;
- }
- seekTo = player.duration() !== Infinity && player.currentTime() || 0;
-
- player.one('loadedmetadata', loadedMetadataHandler);
-
- player.src(sourceObj);
- player.trigger({ type: 'usage', name: 'hls-error-reload' });
- player.play();
- };
-
- /**
- * Attempt to get a source from either the built-in getSource function
- * or a custom function provided via the options
- *
- * @private
- */
- var errorHandler = function errorHandler() {
- // Do not attempt to reload the source if a source-reload occurred before
- // 'errorInterval' time has elapsed since the last source-reload
- if (Date.now() - lastCalled < localOptions.errorInterval * 1000) {
- player.trigger({ type: 'usage', name: 'hls-error-reload-canceled' });
- return;
- }
-
- if (!localOptions.getSource || typeof localOptions.getSource !== 'function') {
- videojs.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!');
- return;
- }
- lastCalled = Date.now();
-
- return localOptions.getSource.call(player, setSource);
- };
-
- /**
- * Unbind any event handlers that were bound by the plugin
- *
- * @private
- */
- var cleanupEvents = function cleanupEvents() {
- player.off('loadedmetadata', loadedMetadataHandler);
- player.off('error', errorHandler);
- player.off('dispose', cleanupEvents);
- };
-
- /**
- * Cleanup before re-initializing the plugin
- *
- * @param {Object} [newOptions] an object with plugin options
- * @private
- */
- var reinitPlugin = function reinitPlugin(newOptions) {
- cleanupEvents();
- initPlugin(player, newOptions);
- };
-
- player.on('error', errorHandler);
- player.on('dispose', cleanupEvents);
-
- // Overwrite the plugin function so that we can correctly cleanup before
- // initializing the plugin
- player.reloadSourceOnError = reinitPlugin;
- };
-
- /**
- * Reload the source when an error is detected as long as there
- * wasn't an error previously within the last 30 seconds
- *
- * @param {Object} [options] an object with plugin options
- */
- var reloadSourceOnError = function reloadSourceOnError(options) {
- initPlugin(this, options);
- };
-
- var version$2 = "1.2.2";
-
- // since VHS handles HLS and DASH (and in the future, more types), use * to capture all
- videojs.use('*', function (player) {
- return {
- setSource: function setSource(srcObj, next) {
- // pass null as the first argument to indicate that the source is not rejected
- next(null, srcObj);
- },
-
- // VHS needs to know when seeks happen. For external seeks (generated at the player
- // level), this middleware will capture the action. For internal seeks (generated at
- // the tech level), we use a wrapped function so that we can handle it on our own
- // (specified elsewhere).
- setCurrentTime: function setCurrentTime(time) {
- if (player.vhs && player.currentSource().src === player.vhs.source_.src) {
- player.vhs.setCurrentTime(time);
- }
-
- return time;
- }
- };
- });
-
- /**
- * @file videojs-http-streaming.js
- *
- * The main file for the HLS project.
- * License: https://github.com/videojs/videojs-http-streaming/blob/master/LICENSE
- */
-
- var Hls$1 = {
- PlaylistLoader: PlaylistLoader,
- Playlist: Playlist,
- Decrypter: Decrypter,
- AsyncStream: AsyncStream,
- decrypt: decrypt,
- utils: utils,
-
- STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
- INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
- comparePlaylistBandwidth: comparePlaylistBandwidth,
- comparePlaylistResolution: comparePlaylistResolution,
-
- xhr: xhrFactory()
- };
-
- // 0.5 MB/s
- var INITIAL_BANDWIDTH = 4194304;
-
- // Define getter/setters for config properites
- ['GOAL_BUFFER_LENGTH', 'MAX_GOAL_BUFFER_LENGTH', 'GOAL_BUFFER_LENGTH_RATE', 'BUFFER_LOW_WATER_LINE', 'MAX_BUFFER_LOW_WATER_LINE', 'BUFFER_LOW_WATER_LINE_RATE', 'BANDWIDTH_VARIANCE'].forEach(function (prop) {
- Object.defineProperty(Hls$1, prop, {
- get: function get$$1() {
- videojs.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing');
- return Config[prop];
- },
- set: function set$$1(value) {
- videojs.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing');
-
- if (typeof value !== 'number' || value < 0) {
- videojs.log.warn('value of Hls.' + prop + ' must be greater than or equal to 0');
- return;
- }
-
- Config[prop] = value;
- }
- });
- });
-
- var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) {
- var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
-
- if (mpegurlRE.test(type)) {
- return 'hls';
- }
-
- var dashRE = /^application\/dash\+xml/i;
-
- if (dashRE.test(type)) {
- return 'dash';
- }
-
- return null;
- };
-
- /**
- * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls.
- *
- * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
- * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
- * @function handleHlsMediaChange
- */
- var handleHlsMediaChange = function handleHlsMediaChange(qualityLevels, playlistLoader) {
- var newPlaylist = playlistLoader.media();
- var selectedIndex = -1;
-
- for (var i = 0; i < qualityLevels.length; i++) {
- if (qualityLevels[i].id === newPlaylist.uri) {
- selectedIndex = i;
- break;
- }
- }
-
- qualityLevels.selectedIndex_ = selectedIndex;
- qualityLevels.trigger({
- selectedIndex: selectedIndex,
- type: 'change'
- });
- };
-
- /**
- * Adds quality levels to list once playlist metadata is available
- *
- * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
- * @param {Object} hls Hls object to listen to for media events.
- * @function handleHlsLoadedMetadata
- */
- var handleHlsLoadedMetadata = function handleHlsLoadedMetadata(qualityLevels, hls) {
- hls.representations().forEach(function (rep) {
- qualityLevels.addQualityLevel(rep);
- });
- handleHlsMediaChange(qualityLevels, hls.playlists);
- };
-
- // HLS is a source handler, not a tech. Make sure attempts to use it
- // as one do not cause exceptions.
- Hls$1.canPlaySource = function () {
- return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
- };
-
- var emeKeySystems = function emeKeySystems(keySystemOptions, videoPlaylist, audioPlaylist) {
- if (!keySystemOptions) {
- return keySystemOptions;
- }
-
- // upsert the content types based on the selected playlist
- var keySystemContentTypes = {};
-
- for (var keySystem in keySystemOptions) {
- keySystemContentTypes[keySystem] = {
- audioContentType: 'audio/mp4; codecs="' + audioPlaylist.attributes.CODECS + '"',
- videoContentType: 'video/mp4; codecs="' + videoPlaylist.attributes.CODECS + '"'
- };
-
- if (videoPlaylist.contentProtection && videoPlaylist.contentProtection[keySystem] && videoPlaylist.contentProtection[keySystem].pssh) {
- keySystemContentTypes[keySystem].pssh = videoPlaylist.contentProtection[keySystem].pssh;
- }
-
- // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
- // so we need to prevent overwriting the URL entirely
- if (typeof keySystemOptions[keySystem] === 'string') {
- keySystemContentTypes[keySystem].url = keySystemOptions[keySystem];
- }
- }
-
- return videojs.mergeOptions(keySystemOptions, keySystemContentTypes);
- };
-
- var setupEmeOptions = function setupEmeOptions(hlsHandler) {
- if (hlsHandler.options_.sourceType !== 'dash') {
- return;
- }
- var player = videojs.players[hlsHandler.tech_.options_.playerId];
-
- if (player.eme) {
- var sourceOptions = emeKeySystems(hlsHandler.source_.keySystems, hlsHandler.playlists.media(), hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media());
-
- if (sourceOptions) {
- player.currentSource().keySystems = sourceOptions;
- }
- }
- };
-
- /**
- * Whether the browser has built-in HLS support.
- */
- Hls$1.supportsNativeHls = function () {
- var video = document_1.createElement('video');
-
- // native HLS is definitely not supported if HTML5 video isn't
- if (!videojs.getTech('Html5').isSupported()) {
- return false;
- }
-
- // HLS manifests can go by many mime-types
- var canPlay = [
- // Apple santioned
- 'application/vnd.apple.mpegurl',
- // Apple sanctioned for backwards compatibility
- 'audio/mpegurl',
- // Very common
- 'audio/x-mpegurl',
- // Very common
- 'application/x-mpegurl',
- // Included for completeness
- 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl'];
-
- return canPlay.some(function (canItPlay) {
- return (/maybe|probably/i.test(video.canPlayType(canItPlay))
- );
- });
- }();
-
- Hls$1.supportsNativeDash = function () {
- if (!videojs.getTech('Html5').isSupported()) {
- return false;
- }
-
- return (/maybe|probably/i.test(document_1.createElement('video').canPlayType('application/dash+xml'))
- );
- }();
-
- Hls$1.supportsTypeNatively = function (type) {
- if (type === 'hls') {
- return Hls$1.supportsNativeHls;
- }
-
- if (type === 'dash') {
- return Hls$1.supportsNativeDash;
- }
-
- return false;
- };
-
- /**
- * HLS is a source handler, not a tech. Make sure attempts to use it
- * as one do not cause exceptions.
- */
- Hls$1.isSupported = function () {
- return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
- };
-
- var Component = videojs.getComponent('Component');
-
- /**
- * The Hls Handler object, where we orchestrate all of the parts
- * of HLS to interact with video.js
- *
- * @class HlsHandler
- * @extends videojs.Component
- * @param {Object} source the soruce object
- * @param {Tech} tech the parent tech object
- * @param {Object} options optional and required options
- */
-
- var HlsHandler = function (_Component) {
- inherits$1(HlsHandler, _Component);
-
- function HlsHandler(source, tech, options) {
- classCallCheck$1(this, HlsHandler);
-
- // tech.player() is deprecated but setup a reference to HLS for
- // backwards-compatibility
- var _this = possibleConstructorReturn$1(this, (HlsHandler.__proto__ || Object.getPrototypeOf(HlsHandler)).call(this, tech, options.hls));
-
- if (tech.options_ && tech.options_.playerId) {
- var _player = videojs(tech.options_.playerId);
-
- if (!_player.hasOwnProperty('hls')) {
- Object.defineProperty(_player, 'hls', {
- get: function get$$1() {
- videojs.log.warn('player.hls is deprecated. Use player.tech().hls instead.');
- tech.trigger({ type: 'usage', name: 'hls-player-access' });
- return _this;
- }
- });
- }
-
- // Set up a reference to the HlsHandler from player.vhs. This allows users to start
- // migrating from player.tech_.hls... to player.vhs... for API access. Although this
- // isn't the most appropriate form of reference for video.js (since all APIs should
- // be provided through core video.js), it is a common pattern for plugins, and vhs
- // will act accordingly.
- _player.vhs = _this;
- // deprecated, for backwards compatibility
- _player.dash = _this;
- }
-
- _this.tech_ = tech;
- _this.source_ = source;
- _this.stats = {};
- _this.setOptions_();
-
- if (_this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) {
- tech.overrideNativeAudioTracks(true);
- tech.overrideNativeVideoTracks(true);
- } else if (_this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
- // overriding native HLS only works if audio tracks have been emulated
- // error early if we're misconfigured
- throw new Error('Overriding native HLS requires emulated tracks. ' + 'See https://git.io/vMpjB');
- }
-
- // listen for fullscreenchange events for this player so that we
- // can adjust our quality selection quickly
- _this.on(document_1, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], function (event) {
- var fullscreenElement = document_1.fullscreenElement || document_1.webkitFullscreenElement || document_1.mozFullScreenElement || document_1.msFullscreenElement;
-
- if (fullscreenElement && fullscreenElement.contains(_this.tech_.el())) {
- _this.masterPlaylistController_.fastQualityChange_();
- }
- });
- _this.on(_this.tech_, 'error', function () {
- if (this.masterPlaylistController_) {
- this.masterPlaylistController_.pauseLoading();
- }
- });
-
- _this.on(_this.tech_, 'play', _this.play);
- return _this;
- }
-
- createClass(HlsHandler, [{
- key: 'setOptions_',
- value: function setOptions_() {
- var _this2 = this;
-
- // defaults
- this.options_.withCredentials = this.options_.withCredentials || false;
-
- if (typeof this.options_.blacklistDuration !== 'number') {
- this.options_.blacklistDuration = 5 * 60;
- }
-
- // start playlist selection at a reasonable bandwidth for
- // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s)
- if (typeof this.options_.bandwidth !== 'number') {
- this.options_.bandwidth = INITIAL_BANDWIDTH;
- }
-
- // If the bandwidth number is unchanged from the initial setting
- // then this takes precedence over the enableLowInitialPlaylist option
- this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === INITIAL_BANDWIDTH;
-
- // grab options passed to player.src
- ['withCredentials', 'bandwidth'].forEach(function (option) {
- if (typeof _this2.source_[option] !== 'undefined') {
- _this2.options_[option] = _this2.source_[option];
- }
- });
-
- this.bandwidth = this.options_.bandwidth;
- }
- /**
- * called when player.src gets called, handle a new source
- *
- * @param {Object} src the source object to handle
- */
-
- }, {
- key: 'src',
- value: function src(_src, type) {
- var _this3 = this;
-
- // do nothing if the src is falsey
- if (!_src) {
- return;
- }
- this.setOptions_();
- // add master playlist controller options
- this.options_.url = this.source_.src;
- this.options_.tech = this.tech_;
- this.options_.externHls = Hls$1;
- this.options_.sourceType = simpleTypeFromSourceType(type);
- // Whenever we seek internally, we should update both the tech and call our own
- // setCurrentTime function. This is needed because "seeking" events aren't always
- // reliable. External seeks (via the player object) are handled via middleware.
- this.options_.seekTo = function (time) {
- _this3.tech_.setCurrentTime(time);
- _this3.setCurrentTime(time);
- };
-
- this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
- this.playbackWatcher_ = new PlaybackWatcher(videojs.mergeOptions(this.options_, {
- seekable: function seekable$$1() {
- return _this3.seekable();
- }
- }));
-
- this.masterPlaylistController_.on('error', function () {
- var player = videojs.players[_this3.tech_.options_.playerId];
-
- player.error(_this3.masterPlaylistController_.error);
- });
-
- // `this` in selectPlaylist should be the HlsHandler for backwards
- // compatibility with < v2
- this.masterPlaylistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls$1.STANDARD_PLAYLIST_SELECTOR.bind(this);
-
- this.masterPlaylistController_.selectInitialPlaylist = Hls$1.INITIAL_PLAYLIST_SELECTOR.bind(this);
-
- // re-expose some internal objects for backwards compatibility with < v2
- this.playlists = this.masterPlaylistController_.masterPlaylistLoader_;
- this.mediaSource = this.masterPlaylistController_.mediaSource;
-
- // Proxy assignment of some properties to the master playlist
- // controller. Using a custom property for backwards compatibility
- // with < v2
- Object.defineProperties(this, {
- selectPlaylist: {
- get: function get$$1() {
- return this.masterPlaylistController_.selectPlaylist;
- },
- set: function set$$1(selectPlaylist) {
- this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
- }
- },
- throughput: {
- get: function get$$1() {
- return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate;
- },
- set: function set$$1(throughput) {
- this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput;
- // By setting `count` to 1 the throughput value becomes the starting value
- // for the cumulative average
- this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1;
- }
- },
- bandwidth: {
- get: function get$$1() {
- return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
- },
- set: function set$$1(bandwidth) {
- this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
- // setting the bandwidth manually resets the throughput counter
- // `count` is set to zero that current value of `rate` isn't included
- // in the cumulative average
- this.masterPlaylistController_.mainSegmentLoader_.throughput = {
- rate: 0,
- count: 0
- };
- }
- },
- /**
- * `systemBandwidth` is a combination of two serial processes bit-rates. The first
- * is the network bitrate provided by `bandwidth` and the second is the bitrate of
- * the entire process after that - decryption, transmuxing, and appending - provided
- * by `throughput`.
- *
- * Since the two process are serial, the overall system bandwidth is given by:
- * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
- */
- systemBandwidth: {
- get: function get$$1() {
- var invBandwidth = 1 / (this.bandwidth || 1);
- var invThroughput = void 0;
-
- if (this.throughput > 0) {
- invThroughput = 1 / this.throughput;
- } else {
- invThroughput = 0;
- }
-
- var systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
-
- return systemBitrate;
- },
- set: function set$$1() {
- videojs.log.error('The "systemBandwidth" property is read-only');
- }
- }
- });
-
- Object.defineProperties(this.stats, {
- bandwidth: {
- get: function get$$1() {
- return _this3.bandwidth || 0;
- },
- enumerable: true
- },
- mediaRequests: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaRequests_() || 0;
- },
- enumerable: true
- },
- mediaRequestsAborted: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaRequestsAborted_() || 0;
- },
- enumerable: true
- },
- mediaRequestsTimedout: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaRequestsTimedout_() || 0;
- },
- enumerable: true
- },
- mediaRequestsErrored: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaRequestsErrored_() || 0;
- },
- enumerable: true
- },
- mediaTransferDuration: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaTransferDuration_() || 0;
- },
- enumerable: true
- },
- mediaBytesTransferred: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaBytesTransferred_() || 0;
- },
- enumerable: true
- },
- mediaSecondsLoaded: {
- get: function get$$1() {
- return _this3.masterPlaylistController_.mediaSecondsLoaded_() || 0;
- },
- enumerable: true
- },
- buffered: {
- get: function get$$1() {
- return timeRangesToArray(_this3.tech_.buffered());
- },
- enumerable: true
- },
- currentTime: {
- get: function get$$1() {
- return _this3.tech_.currentTime();
- },
- enumerable: true
- },
- currentSource: {
- get: function get$$1() {
- return _this3.tech_.currentSource_;
- },
- enumerable: true
- },
- currentTech: {
- get: function get$$1() {
- return _this3.tech_.name_;
- },
- enumerable: true
- },
- duration: {
- get: function get$$1() {
- return _this3.tech_.duration();
- },
- enumerable: true
- },
- master: {
- get: function get$$1() {
- return _this3.playlists.master;
- },
- enumerable: true
- },
- playerDimensions: {
- get: function get$$1() {
- return _this3.tech_.currentDimensions();
- },
- enumerable: true
- },
- seekable: {
- get: function get$$1() {
- return timeRangesToArray(_this3.tech_.seekable());
- },
- enumerable: true
- },
- timestamp: {
- get: function get$$1() {
- return Date.now();
- },
- enumerable: true
- },
- videoPlaybackQuality: {
- get: function get$$1() {
- return _this3.tech_.getVideoPlaybackQuality();
- },
- enumerable: true
- }
- });
-
- this.tech_.one('canplay', this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
-
- this.masterPlaylistController_.on('selectedinitialmedia', function () {
- // Add the manual rendition mix-in to HlsHandler
- renditionSelectionMixin(_this3);
- setupEmeOptions(_this3);
- });
-
- // the bandwidth of the primary segment loader is our best
- // estimate of overall bandwidth
- this.on(this.masterPlaylistController_, 'progress', function () {
- this.tech_.trigger('progress');
- });
-
- this.tech_.ready(function () {
- return _this3.setupQualityLevels_();
- });
-
- // do nothing if the tech has been disposed already
- // this can occur if someone sets the src in player.ready(), for instance
- if (!this.tech_.el()) {
- return;
- }
-
- this.tech_.src(videojs.URL.createObjectURL(this.masterPlaylistController_.mediaSource));
- }
-
- /**
- * Initializes the quality levels and sets listeners to update them.
- *
- * @method setupQualityLevels_
- * @private
- */
-
- }, {
- key: 'setupQualityLevels_',
- value: function setupQualityLevels_() {
- var _this4 = this;
-
- var player = videojs.players[this.tech_.options_.playerId];
-
- if (player && player.qualityLevels) {
- this.qualityLevels_ = player.qualityLevels();
-
- this.masterPlaylistController_.on('selectedinitialmedia', function () {
- handleHlsLoadedMetadata(_this4.qualityLevels_, _this4);
- });
-
- this.playlists.on('mediachange', function () {
- handleHlsMediaChange(_this4.qualityLevels_, _this4.playlists);
- });
- }
- }
-
- /**
- * Begin playing the video.
- */
-
- }, {
- key: 'play',
- value: function play() {
- this.masterPlaylistController_.play();
- }
-
- /**
- * a wrapper around the function in MasterPlaylistController
- */
-
- }, {
- key: 'setCurrentTime',
- value: function setCurrentTime(currentTime) {
- this.masterPlaylistController_.setCurrentTime(currentTime);
- }
-
- /**
- * a wrapper around the function in MasterPlaylistController
- */
-
- }, {
- key: 'duration',
- value: function duration$$1() {
- return this.masterPlaylistController_.duration();
- }
-
- /**
- * a wrapper around the function in MasterPlaylistController
- */
-
- }, {
- key: 'seekable',
- value: function seekable$$1() {
- return this.masterPlaylistController_.seekable();
- }
-
- /**
- * Abort all outstanding work and cleanup.
- */
-
- }, {
- key: 'dispose',
- value: function dispose() {
- if (this.playbackWatcher_) {
- this.playbackWatcher_.dispose();
- }
- if (this.masterPlaylistController_) {
- this.masterPlaylistController_.dispose();
- }
- if (this.qualityLevels_) {
- this.qualityLevels_.dispose();
- }
- get(HlsHandler.prototype.__proto__ || Object.getPrototypeOf(HlsHandler.prototype), 'dispose', this).call(this);
- }
- }]);
- return HlsHandler;
- }(Component);
-
- /**
- * The Source Handler object, which informs video.js what additional
- * MIME types are supported and sets up playback. It is registered
- * automatically to the appropriate tech based on the capabilities of
- * the browser it is running in. It is not necessary to use or modify
- * this object in normal usage.
- */
-
-
- var HlsSourceHandler = {
- name: 'videojs-http-streaming',
- VERSION: version$2,
- canHandleSource: function canHandleSource(srcObj) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- var localOptions = videojs.mergeOptions(videojs.options, options);
-
- return HlsSourceHandler.canPlayType(srcObj.type, localOptions);
- },
- handleSource: function handleSource(source, tech) {
- var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
-
- var localOptions = videojs.mergeOptions(videojs.options, options);
-
- tech.hls = new HlsHandler(source, tech, localOptions);
- tech.hls.xhr = xhrFactory();
-
- tech.hls.src(source.src, source.type);
- return tech.hls;
- },
- canPlayType: function canPlayType(type) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- var _videojs$mergeOptions = videojs.mergeOptions(videojs.options, options),
- overrideNative = _videojs$mergeOptions.hls.overrideNative;
-
- var supportedType = simpleTypeFromSourceType(type);
- var canUseMsePlayback = supportedType && (!Hls$1.supportsTypeNatively(supportedType) || overrideNative);
-
- return canUseMsePlayback ? 'maybe' : '';
- }
- };
-
- if (typeof videojs.MediaSource === 'undefined' || typeof videojs.URL === 'undefined') {
- videojs.MediaSource = MediaSource;
- videojs.URL = URL$1;
- }
-
- // register source handlers with the appropriate techs
- if (MediaSource.supportsNativeMediaSources()) {
- videojs.getTech('Html5').registerSourceHandler(HlsSourceHandler, 0);
- }
-
- videojs.HlsHandler = HlsHandler;
- videojs.HlsSourceHandler = HlsSourceHandler;
- videojs.Hls = Hls$1;
- if (!videojs.use) {
- videojs.registerComponent('Hls', Hls$1);
- }
- videojs.options.hls = videojs.options.hls || {};
-
- if (videojs.registerPlugin) {
- videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
- } else {
- videojs.plugin('reloadSourceOnError', reloadSourceOnError);
- }
-
- exports.Hls = Hls$1;
- exports.HlsHandler = HlsHandler;
- exports.HlsSourceHandler = HlsSourceHandler;
- exports.emeKeySystems = emeKeySystems;
- exports.simpleTypeFromSourceType = simpleTypeFromSourceType;
-
- Object.defineProperty(exports, '__esModule', { value: true });
-
-})));