mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
8233 lines
347 KiB
JavaScript
8233 lines
347 KiB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Hls = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
function EventEmitter() {
|
|
this._events = this._events || {};
|
|
this._maxListeners = this._maxListeners || undefined;
|
|
}
|
|
module.exports = EventEmitter;
|
|
|
|
// Backwards-compat with node 0.10.x
|
|
EventEmitter.EventEmitter = EventEmitter;
|
|
|
|
EventEmitter.prototype._events = undefined;
|
|
EventEmitter.prototype._maxListeners = undefined;
|
|
|
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
// added to it. This is a useful default which helps finding memory leaks.
|
|
EventEmitter.defaultMaxListeners = 10;
|
|
|
|
// Obviously not all Emitters should be limited to 10. This function allows
|
|
// that to be increased. Set to zero for unlimited.
|
|
EventEmitter.prototype.setMaxListeners = function(n) {
|
|
if (!isNumber(n) || n < 0 || isNaN(n))
|
|
throw TypeError('n must be a positive number');
|
|
this._maxListeners = n;
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.emit = function(type) {
|
|
var er, handler, len, args, i, listeners;
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// If there is no 'error' event listener then throw.
|
|
if (type === 'error') {
|
|
if (!this._events.error ||
|
|
(isObject(this._events.error) && !this._events.error.length)) {
|
|
er = arguments[1];
|
|
if (er instanceof Error) {
|
|
throw er; // Unhandled 'error' event
|
|
}
|
|
throw TypeError('Uncaught, unspecified "error" event.');
|
|
}
|
|
}
|
|
|
|
handler = this._events[type];
|
|
|
|
if (isUndefined(handler))
|
|
return false;
|
|
|
|
if (isFunction(handler)) {
|
|
switch (arguments.length) {
|
|
// fast cases
|
|
case 1:
|
|
handler.call(this);
|
|
break;
|
|
case 2:
|
|
handler.call(this, arguments[1]);
|
|
break;
|
|
case 3:
|
|
handler.call(this, arguments[1], arguments[2]);
|
|
break;
|
|
// slower
|
|
default:
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
handler.apply(this, args);
|
|
}
|
|
} else if (isObject(handler)) {
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
listeners = handler.slice();
|
|
len = listeners.length;
|
|
for (i = 0; i < len; i++)
|
|
listeners[i].apply(this, args);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
EventEmitter.prototype.addListener = function(type, listener) {
|
|
var m;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// To avoid recursion in the case that type === "newListener"! Before
|
|
// adding it to the listeners, first emit "newListener".
|
|
if (this._events.newListener)
|
|
this.emit('newListener', type,
|
|
isFunction(listener.listener) ?
|
|
listener.listener : listener);
|
|
|
|
if (!this._events[type])
|
|
// Optimize the case of one listener. Don't need the extra array object.
|
|
this._events[type] = listener;
|
|
else if (isObject(this._events[type]))
|
|
// If we've already got an array, just append.
|
|
this._events[type].push(listener);
|
|
else
|
|
// Adding the second element, need to change to array.
|
|
this._events[type] = [this._events[type], listener];
|
|
|
|
// Check for listener leak
|
|
if (isObject(this._events[type]) && !this._events[type].warned) {
|
|
if (!isUndefined(this._maxListeners)) {
|
|
m = this._maxListeners;
|
|
} else {
|
|
m = EventEmitter.defaultMaxListeners;
|
|
}
|
|
|
|
if (m && m > 0 && this._events[type].length > m) {
|
|
this._events[type].warned = true;
|
|
console.error('(node) warning: possible EventEmitter memory ' +
|
|
'leak detected. %d listeners added. ' +
|
|
'Use emitter.setMaxListeners() to increase limit.',
|
|
this._events[type].length);
|
|
if (typeof console.trace === 'function') {
|
|
// not supported in IE 10
|
|
console.trace();
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
|
|
EventEmitter.prototype.once = function(type, listener) {
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
var fired = false;
|
|
|
|
function g() {
|
|
this.removeListener(type, g);
|
|
|
|
if (!fired) {
|
|
fired = true;
|
|
listener.apply(this, arguments);
|
|
}
|
|
}
|
|
|
|
g.listener = listener;
|
|
this.on(type, g);
|
|
|
|
return this;
|
|
};
|
|
|
|
// emits a 'removeListener' event iff the listener was removed
|
|
EventEmitter.prototype.removeListener = function(type, listener) {
|
|
var list, position, length, i;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events || !this._events[type])
|
|
return this;
|
|
|
|
list = this._events[type];
|
|
length = list.length;
|
|
position = -1;
|
|
|
|
if (list === listener ||
|
|
(isFunction(list.listener) && list.listener === listener)) {
|
|
delete this._events[type];
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
|
|
} else if (isObject(list)) {
|
|
for (i = length; i-- > 0;) {
|
|
if (list[i] === listener ||
|
|
(list[i].listener && list[i].listener === listener)) {
|
|
position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (position < 0)
|
|
return this;
|
|
|
|
if (list.length === 1) {
|
|
list.length = 0;
|
|
delete this._events[type];
|
|
} else {
|
|
list.splice(position, 1);
|
|
}
|
|
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
var key, listeners;
|
|
|
|
if (!this._events)
|
|
return this;
|
|
|
|
// not listening for removeListener, no need to emit
|
|
if (!this._events.removeListener) {
|
|
if (arguments.length === 0)
|
|
this._events = {};
|
|
else if (this._events[type])
|
|
delete this._events[type];
|
|
return this;
|
|
}
|
|
|
|
// emit removeListener for all listeners on all events
|
|
if (arguments.length === 0) {
|
|
for (key in this._events) {
|
|
if (key === 'removeListener') continue;
|
|
this.removeAllListeners(key);
|
|
}
|
|
this.removeAllListeners('removeListener');
|
|
this._events = {};
|
|
return this;
|
|
}
|
|
|
|
listeners = this._events[type];
|
|
|
|
if (isFunction(listeners)) {
|
|
this.removeListener(type, listeners);
|
|
} else if (listeners) {
|
|
// LIFO order
|
|
while (listeners.length)
|
|
this.removeListener(type, listeners[listeners.length - 1]);
|
|
}
|
|
delete this._events[type];
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.listeners = function(type) {
|
|
var ret;
|
|
if (!this._events || !this._events[type])
|
|
ret = [];
|
|
else if (isFunction(this._events[type]))
|
|
ret = [this._events[type]];
|
|
else
|
|
ret = this._events[type].slice();
|
|
return ret;
|
|
};
|
|
|
|
EventEmitter.prototype.listenerCount = function(type) {
|
|
if (this._events) {
|
|
var evlistener = this._events[type];
|
|
|
|
if (isFunction(evlistener))
|
|
return 1;
|
|
else if (evlistener)
|
|
return evlistener.length;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
EventEmitter.listenerCount = function(emitter, type) {
|
|
return emitter.listenerCount(type);
|
|
};
|
|
|
|
function isFunction(arg) {
|
|
return typeof arg === 'function';
|
|
}
|
|
|
|
function isNumber(arg) {
|
|
return typeof arg === 'number';
|
|
}
|
|
|
|
function isObject(arg) {
|
|
return typeof arg === 'object' && arg !== null;
|
|
}
|
|
|
|
function isUndefined(arg) {
|
|
return arg === void 0;
|
|
}
|
|
|
|
},{}],2:[function(require,module,exports){
|
|
var bundleFn = arguments[3];
|
|
var sources = arguments[4];
|
|
var cache = arguments[5];
|
|
|
|
var stringify = JSON.stringify;
|
|
|
|
module.exports = function (fn) {
|
|
var keys = [];
|
|
var wkey;
|
|
var cacheKeys = Object.keys(cache);
|
|
|
|
for (var i = 0, l = cacheKeys.length; i < l; i++) {
|
|
var key = cacheKeys[i];
|
|
var exp = cache[key].exports;
|
|
// Using babel as a transpiler to use esmodule, the export will always
|
|
// be an object with the default export as a property of it. To ensure
|
|
// the existing api and babel esmodule exports are both supported we
|
|
// check for both
|
|
if (exp === fn || exp.default === fn) {
|
|
wkey = key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!wkey) {
|
|
wkey = Math.floor(Math.pow(16, 8) * Math.random()).toString(16);
|
|
var wcache = {};
|
|
for (var i = 0, l = cacheKeys.length; i < l; i++) {
|
|
var key = cacheKeys[i];
|
|
wcache[key] = key;
|
|
}
|
|
sources[wkey] = [
|
|
Function(['require','module','exports'], '(' + fn + ')(self)'),
|
|
wcache
|
|
];
|
|
}
|
|
var skey = Math.floor(Math.pow(16, 8) * Math.random()).toString(16);
|
|
|
|
var scache = {}; scache[wkey] = wkey;
|
|
sources[skey] = [
|
|
Function(['require'], (
|
|
// try to call default if defined to also support babel esmodule
|
|
// exports
|
|
'var f = require(' + stringify(wkey) + ');' +
|
|
'(f.default ? f.default : f)(self);'
|
|
)),
|
|
scache
|
|
];
|
|
|
|
var src = '(' + bundleFn + ')({'
|
|
+ Object.keys(sources).map(function (key) {
|
|
return stringify(key) + ':['
|
|
+ sources[key][0]
|
|
+ ',' + stringify(sources[key][1]) + ']'
|
|
;
|
|
}).join(',')
|
|
+ '},{},[' + stringify(skey) + '])'
|
|
;
|
|
|
|
var URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
|
|
|
|
return new Worker(URL.createObjectURL(
|
|
new Blob([src], { type: 'text/javascript' })
|
|
));
|
|
};
|
|
|
|
},{}],3:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* simple ABR Controller
|
|
*/
|
|
|
|
var AbrController = function (_EventHandler) {
|
|
_inherits(AbrController, _EventHandler);
|
|
|
|
function AbrController(hls) {
|
|
_classCallCheck(this, AbrController);
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(AbrController).call(this, hls, _events2.default.FRAG_LOAD_PROGRESS));
|
|
|
|
_this.lastfetchlevel = 0;
|
|
_this._autoLevelCapping = -1;
|
|
_this._nextAutoLevel = -1;
|
|
return _this;
|
|
}
|
|
|
|
_createClass(AbrController, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onFragLoadProgress',
|
|
value: function onFragLoadProgress(data) {
|
|
var stats = data.stats;
|
|
// only update stats if first frag loading
|
|
// if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
|
|
// and leading to wrong bw estimation
|
|
if (stats.aborted === undefined && data.frag.loadCounter === 1) {
|
|
this.lastfetchduration = (performance.now() - stats.trequest) / 1000;
|
|
this.lastfetchlevel = data.frag.level;
|
|
this.lastbw = stats.loaded * 8 / this.lastfetchduration;
|
|
//console.log(`fetchDuration:${this.lastfetchduration},bw:${(this.lastbw/1000).toFixed(0)}/${stats.aborted}`);
|
|
}
|
|
}
|
|
|
|
/** Return the capping/max level value that could be used by automatic level selection algorithm **/
|
|
|
|
}, {
|
|
key: 'autoLevelCapping',
|
|
get: function get() {
|
|
return this._autoLevelCapping;
|
|
}
|
|
|
|
/** set the capping/max level value that could be used by automatic level selection algorithm **/
|
|
,
|
|
set: function set(newLevel) {
|
|
this._autoLevelCapping = newLevel;
|
|
}
|
|
}, {
|
|
key: 'nextAutoLevel',
|
|
get: function get() {
|
|
var lastbw = this.lastbw,
|
|
hls = this.hls,
|
|
adjustedbw,
|
|
i,
|
|
maxAutoLevel;
|
|
if (this._autoLevelCapping === -1) {
|
|
maxAutoLevel = hls.levels.length - 1;
|
|
} else {
|
|
maxAutoLevel = this._autoLevelCapping;
|
|
}
|
|
|
|
if (this._nextAutoLevel !== -1) {
|
|
var nextLevel = Math.min(this._nextAutoLevel, maxAutoLevel);
|
|
if (nextLevel === this.lastfetchlevel) {
|
|
this._nextAutoLevel = -1;
|
|
} else {
|
|
return nextLevel;
|
|
}
|
|
}
|
|
|
|
// follow algorithm captured from stagefright :
|
|
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
|
|
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
|
|
for (i = 0; i <= maxAutoLevel; i++) {
|
|
// consider only 80% of the available bandwidth, but if we are switching up,
|
|
// be even more conservative (70%) to avoid overestimating and immediately
|
|
// switching back.
|
|
if (i <= this.lastfetchlevel) {
|
|
adjustedbw = 0.8 * lastbw;
|
|
} else {
|
|
adjustedbw = 0.7 * lastbw;
|
|
}
|
|
if (adjustedbw < hls.levels[i].bitrate) {
|
|
return Math.max(0, i - 1);
|
|
}
|
|
}
|
|
return i - 1;
|
|
},
|
|
set: function set(nextLevel) {
|
|
this._nextAutoLevel = nextLevel;
|
|
}
|
|
}]);
|
|
|
|
return AbrController;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = AbrController;
|
|
|
|
},{"../event-handler":20,"../events":21}],4:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Buffer Controller
|
|
*/
|
|
|
|
var BufferController = function (_EventHandler) {
|
|
_inherits(BufferController, _EventHandler);
|
|
|
|
function BufferController(hls) {
|
|
_classCallCheck(this, BufferController);
|
|
|
|
// Source Buffer listeners
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(BufferController).call(this, hls, _events2.default.MEDIA_ATTACHING, _events2.default.MEDIA_DETACHING, _events2.default.BUFFER_RESET, _events2.default.BUFFER_APPENDING, _events2.default.BUFFER_CODECS, _events2.default.BUFFER_EOS, _events2.default.BUFFER_FLUSHING));
|
|
|
|
_this.onsbue = _this.onSBUpdateEnd.bind(_this);
|
|
_this.onsbe = _this.onSBUpdateError.bind(_this);
|
|
return _this;
|
|
}
|
|
|
|
_createClass(BufferController, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onMediaAttaching',
|
|
value: function onMediaAttaching(data) {
|
|
var media = this.media = data.media;
|
|
// setup the media source
|
|
var ms = this.mediaSource = new MediaSource();
|
|
//Media Source listeners
|
|
this.onmso = this.onMediaSourceOpen.bind(this);
|
|
this.onmse = this.onMediaSourceEnded.bind(this);
|
|
this.onmsc = this.onMediaSourceClose.bind(this);
|
|
ms.addEventListener('sourceopen', this.onmso);
|
|
ms.addEventListener('sourceended', this.onmse);
|
|
ms.addEventListener('sourceclose', this.onmsc);
|
|
// link video and media Source
|
|
media.src = URL.createObjectURL(ms);
|
|
}
|
|
}, {
|
|
key: 'onMediaDetaching',
|
|
value: function onMediaDetaching() {
|
|
var ms = this.mediaSource;
|
|
if (ms) {
|
|
if (ms.readyState === 'open') {
|
|
try {
|
|
// endOfStream could trigger exception if any sourcebuffer is in updating state
|
|
// we don't really care about checking sourcebuffer state here,
|
|
// as we are anyway detaching the MediaSource
|
|
// let's just avoid this exception to propagate
|
|
ms.endOfStream();
|
|
} catch (err) {
|
|
_logger.logger.warn('onMediaDetaching:' + err.message + ' while calling endOfStream');
|
|
}
|
|
}
|
|
ms.removeEventListener('sourceopen', this.onmso);
|
|
ms.removeEventListener('sourceended', this.onmse);
|
|
ms.removeEventListener('sourceclose', this.onmsc);
|
|
// unlink MediaSource from video tag
|
|
this.media.src = '';
|
|
this.media.removeAttribute('src');
|
|
this.mediaSource = null;
|
|
this.media = null;
|
|
this.pendingTracks = null;
|
|
}
|
|
this.onmso = this.onmse = this.onmsc = null;
|
|
this.hls.trigger(_events2.default.MEDIA_DETACHED);
|
|
}
|
|
}, {
|
|
key: 'onMediaSourceOpen',
|
|
value: function onMediaSourceOpen() {
|
|
_logger.logger.log('media source opened');
|
|
this.hls.trigger(_events2.default.MEDIA_ATTACHED, { media: this.media });
|
|
// once received, don't listen anymore to sourceopen event
|
|
this.mediaSource.removeEventListener('sourceopen', this.onmso);
|
|
// if any buffer codecs pending, treat it here.
|
|
var pendingTracks = this.pendingTracks;
|
|
if (pendingTracks) {
|
|
this.onBufferCodecs(pendingTracks);
|
|
this.pendingTracks = null;
|
|
this.doAppending();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onMediaSourceClose',
|
|
value: function onMediaSourceClose() {
|
|
_logger.logger.log('media source closed');
|
|
}
|
|
}, {
|
|
key: 'onMediaSourceEnded',
|
|
value: function onMediaSourceEnded() {
|
|
_logger.logger.log('media source ended');
|
|
}
|
|
}, {
|
|
key: 'onSBUpdateEnd',
|
|
value: function onSBUpdateEnd() {
|
|
|
|
if (this._needsFlush) {
|
|
this.doFlush();
|
|
}
|
|
|
|
if (this._needsEos) {
|
|
this.onBufferEos();
|
|
}
|
|
|
|
this.hls.trigger(_events2.default.BUFFER_APPENDED);
|
|
|
|
this.doAppending();
|
|
}
|
|
}, {
|
|
key: 'onSBUpdateError',
|
|
value: function onSBUpdateError(event) {
|
|
_logger.logger.error('sourceBuffer error:' + event);
|
|
// according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
|
|
// this error might not always be fatal (it is fatal if decode error is set, in that case
|
|
// it will be followed by a mediaElement error ...)
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.BUFFER_APPENDING_ERROR, fatal: false });
|
|
// we don't need to do more than that, as accordin to the spec, updateend will be fired just after
|
|
}
|
|
}, {
|
|
key: 'onBufferReset',
|
|
value: function onBufferReset() {
|
|
var sourceBuffer = this.sourceBuffer;
|
|
if (sourceBuffer) {
|
|
for (var type in sourceBuffer) {
|
|
var sb = sourceBuffer[type];
|
|
try {
|
|
this.mediaSource.removeSourceBuffer(sb);
|
|
sb.removeEventListener('updateend', this.onsbue);
|
|
sb.removeEventListener('error', this.onsbe);
|
|
} catch (err) {}
|
|
}
|
|
this.sourceBuffer = null;
|
|
}
|
|
this.flushRange = [];
|
|
this.appended = 0;
|
|
}
|
|
}, {
|
|
key: 'onBufferCodecs',
|
|
value: function onBufferCodecs(tracks) {
|
|
var sb, trackName, track, codec, mimeType;
|
|
|
|
if (!this.media) {
|
|
this.pendingTracks = tracks;
|
|
return;
|
|
}
|
|
|
|
if (!this.sourceBuffer) {
|
|
var sourceBuffer = {},
|
|
mediaSource = this.mediaSource;
|
|
for (trackName in tracks) {
|
|
track = tracks[trackName];
|
|
// use levelCodec as first priority
|
|
codec = track.levelCodec || track.codec;
|
|
mimeType = track.container + ';codecs=' + codec;
|
|
_logger.logger.log('creating sourceBuffer with mimeType:' + mimeType);
|
|
sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType);
|
|
sb.addEventListener('updateend', this.onsbue);
|
|
sb.addEventListener('error', this.onsbe);
|
|
}
|
|
this.sourceBuffer = sourceBuffer;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onBufferAppending',
|
|
value: function onBufferAppending(data) {
|
|
if (!this.segments) {
|
|
this.segments = [data];
|
|
} else {
|
|
this.segments.push(data);
|
|
}
|
|
this.doAppending();
|
|
}
|
|
}, {
|
|
key: 'onBufferAppendFail',
|
|
value: function onBufferAppendFail(data) {
|
|
_logger.logger.error('sourceBuffer error:' + data.event);
|
|
// according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
|
|
// this error might not always be fatal (it is fatal if decode error is set, in that case
|
|
// it will be followed by a mediaElement error ...)
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.BUFFER_APPENDING_ERROR, fatal: false, frag: this.fragCurrent });
|
|
}
|
|
}, {
|
|
key: 'onBufferEos',
|
|
value: function onBufferEos() {
|
|
var sb = this.sourceBuffer,
|
|
mediaSource = this.mediaSource;
|
|
if (!mediaSource || mediaSource.readyState !== 'open') {
|
|
return;
|
|
}
|
|
if (!(sb.audio && sb.audio.updating || sb.video && sb.video.updating)) {
|
|
_logger.logger.log('all media data available, signal endOfStream() to MediaSource and stop loading fragment');
|
|
//Notify the media element that it now has all of the media data
|
|
mediaSource.endOfStream();
|
|
this._needsEos = false;
|
|
} else {
|
|
this._needsEos = true;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onBufferFlushing',
|
|
value: function onBufferFlushing(data) {
|
|
this.flushRange.push({ start: data.startOffset, end: data.endOffset });
|
|
// attempt flush immediatly
|
|
this.flushBufferCounter = 0;
|
|
this.doFlush();
|
|
}
|
|
}, {
|
|
key: 'doFlush',
|
|
value: function doFlush() {
|
|
// loop through all buffer ranges to flush
|
|
while (this.flushRange.length) {
|
|
var range = this.flushRange[0];
|
|
// flushBuffer will abort any buffer append in progress and flush Audio/Video Buffer
|
|
if (this.flushBuffer(range.start, range.end)) {
|
|
// range flushed, remove from flush array
|
|
this.flushRange.shift();
|
|
this.flushBufferCounter = 0;
|
|
} else {
|
|
this._needsFlush = true;
|
|
// avoid looping, wait for SB update end to retrigger a flush
|
|
return;
|
|
}
|
|
}
|
|
if (this.flushRange.length === 0) {
|
|
// everything flushed
|
|
this._needsFlush = false;
|
|
|
|
// let's recompute this.appended, which is used to avoid flush looping
|
|
var appended = 0;
|
|
var sourceBuffer = this.sourceBuffer;
|
|
if (sourceBuffer) {
|
|
for (var type in sourceBuffer) {
|
|
appended += sourceBuffer[type].buffered.length;
|
|
}
|
|
}
|
|
this.appended = appended;
|
|
this.hls.trigger(_events2.default.BUFFER_FLUSHED);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'doAppending',
|
|
value: function doAppending() {
|
|
var hls = this.hls,
|
|
sourceBuffer = this.sourceBuffer,
|
|
segments = this.segments;
|
|
if (sourceBuffer) {
|
|
if (this.media.error) {
|
|
segments = [];
|
|
_logger.logger.error('trying to append although a media error occured, flush segment and abort');
|
|
return;
|
|
}
|
|
for (var type in sourceBuffer) {
|
|
if (sourceBuffer[type].updating) {
|
|
//logger.log('sb update in progress');
|
|
return;
|
|
}
|
|
}
|
|
if (segments.length) {
|
|
var segment = segments.shift();
|
|
try {
|
|
//logger.log(`appending ${segment.type} SB, size:${segment.data.length});
|
|
sourceBuffer[segment.type].appendBuffer(segment.data);
|
|
this.appendError = 0;
|
|
this.appended++;
|
|
} catch (err) {
|
|
// in case any error occured while appending, put back segment in segments table
|
|
_logger.logger.error('error while trying to append buffer:' + err.message);
|
|
segments.unshift(segment);
|
|
var event = { type: _errors.ErrorTypes.MEDIA_ERROR };
|
|
if (err.code !== 22) {
|
|
if (this.appendError) {
|
|
this.appendError++;
|
|
} else {
|
|
this.appendError = 1;
|
|
}
|
|
event.details = _errors.ErrorDetails.BUFFER_APPEND_ERROR;
|
|
event.frag = this.fragCurrent;
|
|
/* with UHD content, we could get loop of quota exceeded error until
|
|
browser is able to evict some data from sourcebuffer. retrying help recovering this
|
|
*/
|
|
if (this.appendError > hls.config.appendErrorMaxRetry) {
|
|
_logger.logger.log('fail ' + hls.config.appendErrorMaxRetry + ' times to append segment in sourceBuffer');
|
|
segments = [];
|
|
event.fatal = true;
|
|
hls.trigger(_events2.default.ERROR, event);
|
|
return;
|
|
} else {
|
|
event.fatal = false;
|
|
hls.trigger(_events2.default.ERROR, event);
|
|
}
|
|
} else {
|
|
// QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
|
|
// let's stop appending any segments, and report BUFFER_FULL_ERROR error
|
|
segments = [];
|
|
event.details = _errors.ErrorDetails.BUFFER_FULL_ERROR;
|
|
hls.trigger(_events2.default.ERROR, event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
flush specified buffered range,
|
|
return true once range has been flushed.
|
|
as sourceBuffer.remove() is asynchronous, flushBuffer will be retriggered on sourceBuffer update end
|
|
*/
|
|
|
|
}, {
|
|
key: 'flushBuffer',
|
|
value: function flushBuffer(startOffset, endOffset) {
|
|
var sb, i, bufStart, bufEnd, flushStart, flushEnd;
|
|
//logger.log('flushBuffer,pos/start/end: ' + this.media.currentTime + '/' + startOffset + '/' + endOffset);
|
|
// safeguard to avoid infinite looping : don't try to flush more than the nb of appended segments
|
|
if (this.flushBufferCounter < this.appended && this.sourceBuffer) {
|
|
for (var type in this.sourceBuffer) {
|
|
sb = this.sourceBuffer[type];
|
|
if (!sb.updating) {
|
|
for (i = 0; i < sb.buffered.length; i++) {
|
|
bufStart = sb.buffered.start(i);
|
|
bufEnd = sb.buffered.end(i);
|
|
// workaround firefox not able to properly flush multiple buffered range.
|
|
if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1 && endOffset === Number.POSITIVE_INFINITY) {
|
|
flushStart = startOffset;
|
|
flushEnd = endOffset;
|
|
} else {
|
|
flushStart = Math.max(bufStart, startOffset);
|
|
flushEnd = Math.min(bufEnd, endOffset);
|
|
}
|
|
/* sometimes sourcebuffer.remove() does not flush
|
|
the exact expected time range.
|
|
to avoid rounding issues/infinite loop,
|
|
only flush buffer range of length greater than 500ms.
|
|
*/
|
|
if (Math.min(flushEnd, bufEnd) - flushStart > 0.5) {
|
|
this.flushBufferCounter++;
|
|
_logger.logger.log('flush ' + type + ' [' + flushStart + ',' + flushEnd + '], of [' + bufStart + ',' + bufEnd + '], pos:' + this.media.currentTime);
|
|
sb.remove(flushStart, flushEnd);
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
//logger.log('abort ' + type + ' append in progress');
|
|
// this will abort any appending in progress
|
|
//sb.abort();
|
|
_logger.logger.warn('cannot flush, sb updating in progress');
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
_logger.logger.warn('abort flushing too many retries');
|
|
}
|
|
_logger.logger.log('buffer flushed');
|
|
// everything flushed !
|
|
return true;
|
|
}
|
|
}]);
|
|
|
|
return BufferController;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = BufferController;
|
|
|
|
},{"../errors":19,"../event-handler":20,"../events":21,"../utils/logger":34}],5:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Level Controller
|
|
*/
|
|
|
|
var LevelController = function (_EventHandler) {
|
|
_inherits(LevelController, _EventHandler);
|
|
|
|
function LevelController(hls) {
|
|
_classCallCheck(this, LevelController);
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(LevelController).call(this, hls, _events2.default.MANIFEST_LOADED, _events2.default.LEVEL_LOADED, _events2.default.ERROR));
|
|
|
|
_this.ontick = _this.tick.bind(_this);
|
|
_this._manualLevel = _this._autoLevelCapping = -1;
|
|
return _this;
|
|
}
|
|
|
|
_createClass(LevelController, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
}
|
|
this._manualLevel = -1;
|
|
}
|
|
}, {
|
|
key: 'onManifestLoaded',
|
|
value: function onManifestLoaded(data) {
|
|
var levels0 = [],
|
|
levels = [],
|
|
bitrateStart,
|
|
i,
|
|
bitrateSet = {},
|
|
videoCodecFound = false,
|
|
audioCodecFound = false,
|
|
hls = this.hls;
|
|
|
|
// regroup redundant level together
|
|
data.levels.forEach(function (level) {
|
|
if (level.videoCodec) {
|
|
videoCodecFound = true;
|
|
}
|
|
if (level.audioCodec) {
|
|
audioCodecFound = true;
|
|
}
|
|
var redundantLevelId = bitrateSet[level.bitrate];
|
|
if (redundantLevelId === undefined) {
|
|
bitrateSet[level.bitrate] = levels0.length;
|
|
level.url = [level.url];
|
|
level.urlId = 0;
|
|
levels0.push(level);
|
|
} else {
|
|
levels0[redundantLevelId].url.push(level.url);
|
|
}
|
|
});
|
|
|
|
// remove audio-only level if we also have levels with audio+video codecs signalled
|
|
if (videoCodecFound && audioCodecFound) {
|
|
levels0.forEach(function (level) {
|
|
if (level.videoCodec) {
|
|
levels.push(level);
|
|
}
|
|
});
|
|
} else {
|
|
levels = levels0;
|
|
}
|
|
|
|
// only keep level with supported audio/video codecs
|
|
levels = levels.filter(function (level) {
|
|
var checkSupportedAudio = function checkSupportedAudio(codec) {
|
|
return MediaSource.isTypeSupported('audio/mp4;codecs=' + codec);
|
|
};
|
|
var checkSupportedVideo = function checkSupportedVideo(codec) {
|
|
return MediaSource.isTypeSupported('video/mp4;codecs=' + codec);
|
|
};
|
|
var audioCodec = level.audioCodec,
|
|
videoCodec = level.videoCodec;
|
|
|
|
return (!audioCodec || checkSupportedAudio(audioCodec)) && (!videoCodec || checkSupportedVideo(videoCodec));
|
|
});
|
|
|
|
if (levels.length) {
|
|
// start bitrate is the first bitrate of the manifest
|
|
bitrateStart = levels[0].bitrate;
|
|
// sort level on bitrate
|
|
levels.sort(function (a, b) {
|
|
return a.bitrate - b.bitrate;
|
|
});
|
|
this._levels = levels;
|
|
// find index of first level in sorted levels
|
|
for (i = 0; i < levels.length; i++) {
|
|
if (levels[i].bitrate === bitrateStart) {
|
|
this._firstLevel = i;
|
|
_logger.logger.log('manifest loaded,' + levels.length + ' level(s) found, first bitrate:' + bitrateStart);
|
|
break;
|
|
}
|
|
}
|
|
hls.trigger(_events2.default.MANIFEST_PARSED, { levels: this._levels, firstLevel: this._firstLevel, stats: data.stats });
|
|
} else {
|
|
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, fatal: true, url: hls.url, reason: 'no level with compatible codecs found in manifest' });
|
|
}
|
|
return;
|
|
}
|
|
}, {
|
|
key: 'setLevelInternal',
|
|
value: function setLevelInternal(newLevel) {
|
|
// check if level idx is valid
|
|
if (newLevel >= 0 && newLevel < this._levels.length) {
|
|
// stopping live reloading timer if any
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
this._level = newLevel;
|
|
_logger.logger.log('switching to level ' + newLevel);
|
|
this.hls.trigger(_events2.default.LEVEL_SWITCH, { level: newLevel });
|
|
var level = this._levels[newLevel];
|
|
// check if we need to load playlist for this level
|
|
if (level.details === undefined || level.details.live === true) {
|
|
// level not retrieved yet, or live playlist we need to (re)load it
|
|
_logger.logger.log('(re)loading playlist for level ' + newLevel);
|
|
var urlId = level.urlId;
|
|
this.hls.trigger(_events2.default.LEVEL_LOADING, { url: level.url[urlId], level: newLevel, id: urlId });
|
|
}
|
|
} else {
|
|
// invalid level id given, trigger error
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.OTHER_ERROR, details: _errors.ErrorDetails.LEVEL_SWITCH_ERROR, level: newLevel, fatal: false, reason: 'invalid level idx' });
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onError',
|
|
value: function onError(data) {
|
|
if (data.fatal) {
|
|
return;
|
|
}
|
|
|
|
var details = data.details,
|
|
hls = this.hls,
|
|
levelId,
|
|
level;
|
|
// try to recover not fatal errors
|
|
switch (details) {
|
|
case _errors.ErrorDetails.FRAG_LOAD_ERROR:
|
|
case _errors.ErrorDetails.FRAG_LOAD_TIMEOUT:
|
|
case _errors.ErrorDetails.FRAG_LOOP_LOADING_ERROR:
|
|
case _errors.ErrorDetails.KEY_LOAD_ERROR:
|
|
case _errors.ErrorDetails.KEY_LOAD_TIMEOUT:
|
|
levelId = data.frag.level;
|
|
break;
|
|
case _errors.ErrorDetails.LEVEL_LOAD_ERROR:
|
|
case _errors.ErrorDetails.LEVEL_LOAD_TIMEOUT:
|
|
levelId = data.level;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
/* try to switch to a redundant stream if any available.
|
|
* if no redundant stream available, emergency switch down (if in auto mode and current level not 0)
|
|
* otherwise, we cannot recover this network error ...
|
|
* don't raise FRAG_LOAD_ERROR and FRAG_LOAD_TIMEOUT as fatal, as it is handled by mediaController
|
|
*/
|
|
if (levelId !== undefined) {
|
|
level = this._levels[levelId];
|
|
if (level.urlId < level.url.length - 1) {
|
|
level.urlId++;
|
|
level.details = undefined;
|
|
_logger.logger.warn('level controller,' + details + ' for level ' + levelId + ': switching to redundant stream id ' + level.urlId);
|
|
} else {
|
|
// we could try to recover if in auto mode and current level not lowest level (0)
|
|
var recoverable = this._manualLevel === -1 && levelId;
|
|
if (recoverable) {
|
|
_logger.logger.warn('level controller,' + details + ': emergency switch-down for next fragment');
|
|
hls.abrController.nextAutoLevel = 0;
|
|
} else if (level && level.details && level.details.live) {
|
|
_logger.logger.warn('level controller,' + details + ' on live stream, discard');
|
|
// FRAG_LOAD_ERROR and FRAG_LOAD_TIMEOUT are handled by mediaController
|
|
} else if (details !== _errors.ErrorDetails.FRAG_LOAD_ERROR && details !== _errors.ErrorDetails.FRAG_LOAD_TIMEOUT) {
|
|
_logger.logger.error('cannot recover ' + details + ' error');
|
|
this._level = undefined;
|
|
// stopping live reloading timer if any
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
// redispatch same error but with fatal set to true
|
|
data.fatal = true;
|
|
hls.trigger(event, data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onLevelLoaded',
|
|
value: function onLevelLoaded(data) {
|
|
// check if current playlist is a live playlist
|
|
if (data.details.live && !this.timer) {
|
|
// if live playlist we will have to reload it periodically
|
|
// set reload period to playlist target duration
|
|
this.timer = setInterval(this.ontick, 1000 * data.details.targetduration);
|
|
}
|
|
if (!data.details.live && this.timer) {
|
|
// playlist is not live and timer is armed : stopping it
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'tick',
|
|
value: function tick() {
|
|
var levelId = this._level;
|
|
if (levelId !== undefined) {
|
|
var level = this._levels[levelId],
|
|
urlId = level.urlId;
|
|
this.hls.trigger(_events2.default.LEVEL_LOADING, { url: level.url[urlId], level: levelId, id: urlId });
|
|
}
|
|
}
|
|
}, {
|
|
key: 'levels',
|
|
get: function get() {
|
|
return this._levels;
|
|
}
|
|
}, {
|
|
key: 'level',
|
|
get: function get() {
|
|
return this._level;
|
|
},
|
|
set: function set(newLevel) {
|
|
if (this._level !== newLevel || this._levels[newLevel].details === undefined) {
|
|
this.setLevelInternal(newLevel);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'manualLevel',
|
|
get: function get() {
|
|
return this._manualLevel;
|
|
},
|
|
set: function set(newLevel) {
|
|
this._manualLevel = newLevel;
|
|
if (newLevel !== -1) {
|
|
this.level = newLevel;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'firstLevel',
|
|
get: function get() {
|
|
return this._firstLevel;
|
|
},
|
|
set: function set(newLevel) {
|
|
this._firstLevel = newLevel;
|
|
}
|
|
}, {
|
|
key: 'startLevel',
|
|
get: function get() {
|
|
if (this._startLevel === undefined) {
|
|
return this._firstLevel;
|
|
} else {
|
|
return this._startLevel;
|
|
}
|
|
},
|
|
set: function set(newLevel) {
|
|
this._startLevel = newLevel;
|
|
}
|
|
}, {
|
|
key: 'nextLoadLevel',
|
|
get: function get() {
|
|
if (this._manualLevel !== -1) {
|
|
return this._manualLevel;
|
|
} else {
|
|
return this.hls.abrController.nextAutoLevel;
|
|
}
|
|
},
|
|
set: function set(nextLevel) {
|
|
this.level = nextLevel;
|
|
if (this._manualLevel === -1) {
|
|
this.hls.abrController.nextAutoLevel = nextLevel;
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return LevelController;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = LevelController;
|
|
|
|
},{"../errors":19,"../event-handler":20,"../events":21,"../utils/logger":34}],6:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _demuxer = require('../demux/demuxer');
|
|
|
|
var _demuxer2 = _interopRequireDefault(_demuxer);
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _binarySearch = require('../utils/binary-search');
|
|
|
|
var _binarySearch2 = _interopRequireDefault(_binarySearch);
|
|
|
|
var _levelHelper = require('../helper/level-helper');
|
|
|
|
var _levelHelper2 = _interopRequireDefault(_levelHelper);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Stream Controller
|
|
*/
|
|
|
|
var State = {
|
|
ERROR: 'ERROR',
|
|
STARTING: 'STARTING',
|
|
IDLE: 'IDLE',
|
|
PAUSED: 'PAUSED',
|
|
KEY_LOADING: 'KEY_LOADING',
|
|
FRAG_LOADING: 'FRAG_LOADING',
|
|
FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
|
|
WAITING_LEVEL: 'WAITING_LEVEL',
|
|
PARSING: 'PARSING',
|
|
PARSED: 'PARSED',
|
|
ENDED: 'ENDED'
|
|
};
|
|
|
|
var StreamController = function (_EventHandler) {
|
|
_inherits(StreamController, _EventHandler);
|
|
|
|
function StreamController(hls) {
|
|
_classCallCheck(this, StreamController);
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(StreamController).call(this, hls, _events2.default.MEDIA_ATTACHED, _events2.default.MEDIA_DETACHING, _events2.default.MANIFEST_PARSED, _events2.default.LEVEL_LOADED, _events2.default.KEY_LOADED, _events2.default.FRAG_LOADED, _events2.default.FRAG_PARSING_INIT_SEGMENT, _events2.default.FRAG_PARSING_DATA, _events2.default.FRAG_PARSED, _events2.default.ERROR, _events2.default.BUFFER_APPENDED, _events2.default.BUFFER_FLUSHED));
|
|
|
|
_this.config = hls.config;
|
|
_this.audioCodecSwap = false;
|
|
_this.ticks = 0;
|
|
_this.ontick = _this.tick.bind(_this);
|
|
return _this;
|
|
}
|
|
|
|
_createClass(StreamController, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
this.stop();
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
this.state = State.IDLE;
|
|
}
|
|
}, {
|
|
key: 'startLoad',
|
|
value: function startLoad() {
|
|
if (this.levels) {
|
|
var media = this.media,
|
|
lastCurrentTime = this.lastCurrentTime;
|
|
this.stop();
|
|
this.demuxer = new _demuxer2.default(this.hls);
|
|
this.timer = setInterval(this.ontick, 100);
|
|
this.level = -1;
|
|
this.fragLoadError = 0;
|
|
if (media && lastCurrentTime) {
|
|
_logger.logger.log('configure startPosition @' + lastCurrentTime);
|
|
if (!this.lastPaused) {
|
|
_logger.logger.log('resuming video');
|
|
media.play();
|
|
}
|
|
this.state = State.IDLE;
|
|
} else {
|
|
this.lastCurrentTime = this.startPosition ? this.startPosition : 0;
|
|
this.state = State.STARTING;
|
|
}
|
|
this.nextLoadPosition = this.startPosition = this.lastCurrentTime;
|
|
this.tick();
|
|
} else {
|
|
_logger.logger.warn('cannot start loading as manifest not parsed yet');
|
|
}
|
|
}
|
|
}, {
|
|
key: 'stop',
|
|
value: function stop() {
|
|
this.bufferRange = [];
|
|
this.stalled = false;
|
|
var frag = this.fragCurrent;
|
|
if (frag) {
|
|
if (frag.loader) {
|
|
frag.loader.abort();
|
|
}
|
|
this.fragCurrent = null;
|
|
}
|
|
this.fragPrevious = null;
|
|
_logger.logger.log('trigger BUFFER_RESET');
|
|
this.hls.trigger(_events2.default.BUFFER_RESET);
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
if (this.demuxer) {
|
|
this.demuxer.destroy();
|
|
this.demuxer = null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'tick',
|
|
value: function tick() {
|
|
this.ticks++;
|
|
if (this.ticks === 1) {
|
|
this.doTick();
|
|
if (this.ticks > 1) {
|
|
setTimeout(this.tick, 1);
|
|
}
|
|
this.ticks = 0;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'doTick',
|
|
value: function doTick() {
|
|
var _this2 = this;
|
|
|
|
var pos,
|
|
level,
|
|
levelDetails,
|
|
hls = this.hls,
|
|
config = hls.config;
|
|
//logger.log(this.state);
|
|
switch (this.state) {
|
|
case State.ERROR:
|
|
//don't do anything in error state to avoid breaking further ...
|
|
case State.PAUSED:
|
|
//don't do anything in paused state either ...
|
|
break;
|
|
case State.STARTING:
|
|
// determine load level
|
|
this.startLevel = hls.startLevel;
|
|
if (this.startLevel === -1) {
|
|
// -1 : guess start Level by doing a bitrate test by loading first fragment of lowest quality level
|
|
this.startLevel = 0;
|
|
this.fragBitrateTest = true;
|
|
}
|
|
// set new level to playlist loader : this will trigger start level load
|
|
this.level = hls.nextLoadLevel = this.startLevel;
|
|
this.state = State.WAITING_LEVEL;
|
|
this.loadedmetadata = false;
|
|
break;
|
|
case State.IDLE:
|
|
// if video not attached AND
|
|
// start fragment already requested OR start frag prefetch disable
|
|
// exit loop
|
|
// => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
|
|
if (!this.media && (this.startFragRequested || !config.startFragPrefetch)) {
|
|
break;
|
|
}
|
|
// determine next candidate fragment to be loaded, based on current position and
|
|
// end of buffer position
|
|
// ensure 60s of buffer upfront
|
|
// if we have not yet loaded any fragment, start loading from start position
|
|
if (this.loadedmetadata) {
|
|
pos = this.media.currentTime;
|
|
} else {
|
|
pos = this.nextLoadPosition;
|
|
}
|
|
// determine next load level
|
|
if (this.startFragRequested === false) {
|
|
level = this.startLevel;
|
|
} else {
|
|
// we are not at playback start, get next load level from level Controller
|
|
level = hls.nextLoadLevel;
|
|
}
|
|
var bufferInfo = this.bufferInfo(pos, config.maxBufferHole),
|
|
bufferLen = bufferInfo.len,
|
|
bufferEnd = bufferInfo.end,
|
|
fragPrevious = this.fragPrevious,
|
|
maxBufLen;
|
|
// compute max Buffer Length that we could get from this load level, based on level bitrate. don't buffer more than 60 MB and more than 30s
|
|
if (this.levels[level].hasOwnProperty('bitrate')) {
|
|
maxBufLen = Math.max(8 * config.maxBufferSize / this.levels[level].bitrate, config.maxBufferLength);
|
|
maxBufLen = Math.min(maxBufLen, config.maxMaxBufferLength);
|
|
} else {
|
|
maxBufLen = config.maxBufferLength;
|
|
}
|
|
// if buffer length is less than maxBufLen try to load a new fragment
|
|
if (bufferLen < maxBufLen) {
|
|
// set next load level : this will trigger a playlist load if needed
|
|
hls.nextLoadLevel = level;
|
|
this.level = level;
|
|
levelDetails = this.levels[level].details;
|
|
// if level info not retrieved yet, switch state and wait for level retrieval
|
|
// if live playlist, ensure that new playlist has been refreshed to avoid loading/try to load
|
|
// a useless and outdated fragment (that might even introduce load error if it is already out of the live playlist)
|
|
if (typeof levelDetails === 'undefined' || levelDetails.live && this.levelLastLoaded !== level) {
|
|
this.state = State.WAITING_LEVEL;
|
|
break;
|
|
}
|
|
// find fragment index, contiguous with end of buffer position
|
|
var fragments = levelDetails.fragments,
|
|
fragLen = fragments.length,
|
|
start = fragments[0].start,
|
|
end = fragments[fragLen - 1].start + fragments[fragLen - 1].duration,
|
|
_frag = undefined;
|
|
|
|
// in case of live playlist we need to ensure that requested position is not located before playlist start
|
|
if (levelDetails.live) {
|
|
// check if requested position is within seekable boundaries :
|
|
//logger.log(`start/pos/bufEnd/seeking:${start.toFixed(3)}/${pos.toFixed(3)}/${bufferEnd.toFixed(3)}/${this.media.seeking}`);
|
|
var maxLatency = config.liveMaxLatencyDuration !== undefined ? config.liveMaxLatencyDuration : config.liveMaxLatencyDurationCount * levelDetails.targetduration;
|
|
|
|
if (bufferEnd < Math.max(start, end - maxLatency)) {
|
|
var targetLatency = config.liveSyncDuration !== undefined ? config.liveSyncDuration : config.liveSyncDurationCount * levelDetails.targetduration;
|
|
this.seekAfterBuffered = start + Math.max(0, levelDetails.totalduration - targetLatency);
|
|
_logger.logger.log('buffer end: ' + bufferEnd + ' is located too far from the end of live sliding playlist, media position will be reseted to: ' + this.seekAfterBuffered.toFixed(3));
|
|
bufferEnd = this.seekAfterBuffered;
|
|
}
|
|
if (this.startFragRequested && !levelDetails.PTSKnown) {
|
|
/* we are switching level on live playlist, but we don't have any PTS info for that quality level ...
|
|
try to load frag matching with next SN.
|
|
even if SN are not synchronized between playlists, loading this frag will help us
|
|
compute playlist sliding and find the right one after in case it was not the right consecutive one */
|
|
if (fragPrevious) {
|
|
var targetSN = fragPrevious.sn + 1;
|
|
if (targetSN >= levelDetails.startSN && targetSN <= levelDetails.endSN) {
|
|
_frag = fragments[targetSN - levelDetails.startSN];
|
|
_logger.logger.log('live playlist, switching playlist, load frag with next SN: ' + _frag.sn);
|
|
}
|
|
}
|
|
if (!_frag) {
|
|
/* we have no idea about which fragment should be loaded.
|
|
so let's load mid fragment. it will help computing playlist sliding and find the right one
|
|
*/
|
|
_frag = fragments[Math.min(fragLen - 1, Math.round(fragLen / 2))];
|
|
_logger.logger.log('live playlist, switching playlist, unknown, load middle frag : ' + _frag.sn);
|
|
}
|
|
}
|
|
} else {
|
|
// VoD playlist: if bufferEnd before start of playlist, load first fragment
|
|
if (bufferEnd < start) {
|
|
_frag = fragments[0];
|
|
}
|
|
}
|
|
if (!_frag) {
|
|
(function () {
|
|
var foundFrag = undefined;
|
|
var maxFragLookUpTolerance = config.maxFragLookUpTolerance;
|
|
if (bufferEnd < end) {
|
|
if (bufferEnd > end - maxFragLookUpTolerance) {
|
|
maxFragLookUpTolerance = 0;
|
|
}
|
|
foundFrag = _binarySearch2.default.search(fragments, function (candidate) {
|
|
// offset should be within fragment boundary - config.maxFragLookUpTolerance
|
|
// this is to cope with situations like
|
|
// bufferEnd = 9.991
|
|
// frag[Ø] : [0,10]
|
|
// frag[1] : [10,20]
|
|
// bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here
|
|
// frag start frag start+duration
|
|
// |-----------------------------|
|
|
// <---> <--->
|
|
// ...--------><-----------------------------><---------....
|
|
// previous frag matching fragment next frag
|
|
// return -1 return 0 return 1
|
|
//logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`);
|
|
if (candidate.start + candidate.duration - maxFragLookUpTolerance <= bufferEnd) {
|
|
return 1;
|
|
} else if (candidate.start - maxFragLookUpTolerance > bufferEnd) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
});
|
|
} else {
|
|
// reach end of playlist
|
|
foundFrag = fragments[fragLen - 1];
|
|
}
|
|
if (foundFrag) {
|
|
_frag = foundFrag;
|
|
start = foundFrag.start;
|
|
//logger.log('find SN matching with pos:' + bufferEnd + ':' + frag.sn);
|
|
if (fragPrevious && _frag.level === fragPrevious.level && _frag.sn === fragPrevious.sn) {
|
|
if (_frag.sn < levelDetails.endSN) {
|
|
_frag = fragments[_frag.sn + 1 - levelDetails.startSN];
|
|
_logger.logger.log('SN just loaded, load next one: ' + _frag.sn);
|
|
} else {
|
|
// have we reached end of VOD playlist ?
|
|
if (!levelDetails.live) {
|
|
_this2.hls.trigger(_events2.default.BUFFER_EOS);
|
|
_this2.state = State.ENDED;
|
|
}
|
|
_frag = null;
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
if (_frag) {
|
|
//logger.log(' loading frag ' + i +',pos/bufEnd:' + pos.toFixed(3) + '/' + bufferEnd.toFixed(3));
|
|
if (_frag.decryptdata.uri != null && _frag.decryptdata.key == null) {
|
|
_logger.logger.log('Loading key for ' + _frag.sn + ' of [' + levelDetails.startSN + ' ,' + levelDetails.endSN + '],level ' + level);
|
|
this.state = State.KEY_LOADING;
|
|
hls.trigger(_events2.default.KEY_LOADING, { frag: _frag });
|
|
} else {
|
|
_logger.logger.log('Loading ' + _frag.sn + ' of [' + levelDetails.startSN + ' ,' + levelDetails.endSN + '],level ' + level + ', currentTime:' + pos + ',bufferEnd:' + bufferEnd.toFixed(3));
|
|
_frag.autoLevel = hls.autoLevelEnabled;
|
|
if (this.levels.length > 1) {
|
|
_frag.expectedLen = Math.round(_frag.duration * this.levels[level].bitrate / 8);
|
|
_frag.trequest = performance.now();
|
|
}
|
|
// ensure that we are not reloading the same fragments in loop ...
|
|
if (this.fragLoadIdx !== undefined) {
|
|
this.fragLoadIdx++;
|
|
} else {
|
|
this.fragLoadIdx = 0;
|
|
}
|
|
if (_frag.loadCounter) {
|
|
_frag.loadCounter++;
|
|
var maxThreshold = config.fragLoadingLoopThreshold;
|
|
// if this frag has already been loaded 3 times, and if it has been reloaded recently
|
|
if (_frag.loadCounter > maxThreshold && Math.abs(this.fragLoadIdx - _frag.loadIdx) < maxThreshold) {
|
|
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_LOOP_LOADING_ERROR, fatal: false, frag: _frag });
|
|
return;
|
|
}
|
|
} else {
|
|
_frag.loadCounter = 1;
|
|
}
|
|
_frag.loadIdx = this.fragLoadIdx;
|
|
this.fragCurrent = _frag;
|
|
this.startFragRequested = true;
|
|
hls.trigger(_events2.default.FRAG_LOADING, { frag: _frag });
|
|
this.state = State.FRAG_LOADING;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case State.WAITING_LEVEL:
|
|
level = this.levels[this.level];
|
|
// check if playlist is already loaded
|
|
if (level && level.details) {
|
|
this.state = State.IDLE;
|
|
}
|
|
break;
|
|
case State.FRAG_LOADING:
|
|
/*
|
|
monitor fragment retrieval time...
|
|
we compute expected time of arrival of the complete fragment.
|
|
we compare it to expected time of buffer starvation
|
|
*/
|
|
var v = this.media,
|
|
frag = this.fragCurrent;
|
|
/* only monitor frag retrieval time if
|
|
(video not paused OR first fragment being loaded) AND autoswitching enabled AND not lowest level AND multiple levels */
|
|
if (v && (!v.paused || this.loadedmetadata === false) && frag.autoLevel && this.level && this.levels.length > 1) {
|
|
var requestDelay = performance.now() - frag.trequest;
|
|
// monitor fragment load progress after half of expected fragment duration,to stabilize bitrate
|
|
if (requestDelay > 500 * frag.duration) {
|
|
var loadRate = Math.max(1, frag.loaded * 1000 / requestDelay); // byte/s; at least 1 byte/s to avoid division by zero
|
|
if (frag.expectedLen < frag.loaded) {
|
|
frag.expectedLen = frag.loaded;
|
|
}
|
|
pos = v.currentTime;
|
|
var fragLoadedDelay = (frag.expectedLen - frag.loaded) / loadRate;
|
|
var bufferStarvationDelay = this.bufferInfo(pos, config.maxBufferHole).end - pos;
|
|
// consider emergency switch down only if we have less than 2 frag buffered AND
|
|
// time to finish loading current fragment is bigger than buffer starvation delay
|
|
// ie if we risk buffer starvation if bw does not increase quickly
|
|
if (bufferStarvationDelay < 2 * frag.duration && fragLoadedDelay > bufferStarvationDelay) {
|
|
var fragLevelNextLoadedDelay = undefined,
|
|
nextLoadLevel = undefined;
|
|
// lets iterate through lower level and try to find the biggest one that could avoid rebuffering
|
|
// we start from current level - 1 and we step down , until we find a matching level
|
|
for (nextLoadLevel = this.level - 1; nextLoadLevel >= 0; nextLoadLevel--) {
|
|
// compute time to load next fragment at lower level
|
|
// 0.8 : consider only 80% of current bw to be conservative
|
|
// 8 = bits per byte (bps/Bps)
|
|
fragLevelNextLoadedDelay = frag.duration * this.levels[nextLoadLevel].bitrate / (8 * 0.8 * loadRate);
|
|
_logger.logger.log('fragLoadedDelay/bufferStarvationDelay/fragLevelNextLoadedDelay[' + nextLoadLevel + '] :' + fragLoadedDelay.toFixed(1) + '/' + bufferStarvationDelay.toFixed(1) + '/' + fragLevelNextLoadedDelay.toFixed(1));
|
|
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
|
|
// we found a lower level that be rebuffering free with current estimated bw !
|
|
break;
|
|
}
|
|
}
|
|
// only emergency switch down if it takes less time to load new fragment at lowest level instead
|
|
// of finishing loading current one ...
|
|
if (fragLevelNextLoadedDelay < fragLoadedDelay) {
|
|
// ensure nextLoadLevel is not negative
|
|
nextLoadLevel = Math.max(0, nextLoadLevel);
|
|
// force next load level in auto mode
|
|
hls.nextLoadLevel = nextLoadLevel;
|
|
// abort fragment loading ...
|
|
_logger.logger.warn('loading too slow, abort fragment loading and switch to level ' + nextLoadLevel);
|
|
//abort fragment loading
|
|
frag.loader.abort();
|
|
hls.trigger(_events2.default.FRAG_LOAD_EMERGENCY_ABORTED, { frag: frag });
|
|
// switch back to IDLE state to request new fragment at lower level
|
|
this.state = State.IDLE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case State.FRAG_LOADING_WAITING_RETRY:
|
|
var now = performance.now();
|
|
var retryDate = this.retryDate;
|
|
var media = this.media;
|
|
var isSeeking = media && media.seeking;
|
|
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
|
|
if (!retryDate || now >= retryDate || isSeeking) {
|
|
_logger.logger.log('mediaController: retryDate reached, switch back to IDLE state');
|
|
this.state = State.IDLE;
|
|
}
|
|
break;
|
|
case State.PARSING:
|
|
// nothing to do, wait for fragment being parsed
|
|
break;
|
|
case State.PARSED:
|
|
// nothing to do, wait for all buffers to be appended
|
|
break;
|
|
case State.ENDED:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// check buffer
|
|
this._checkBuffer();
|
|
// check/update current fragment
|
|
this._checkFragmentChanged();
|
|
}
|
|
}, {
|
|
key: 'bufferInfo',
|
|
value: function bufferInfo(pos, maxHoleDuration) {
|
|
var media = this.media;
|
|
if (media) {
|
|
var vbuffered = media.buffered,
|
|
buffered = [],
|
|
i;
|
|
for (i = 0; i < vbuffered.length; i++) {
|
|
buffered.push({ start: vbuffered.start(i), end: vbuffered.end(i) });
|
|
}
|
|
return this.bufferedInfo(buffered, pos, maxHoleDuration);
|
|
} else {
|
|
return { len: 0, start: 0, end: 0, nextStart: undefined };
|
|
}
|
|
}
|
|
}, {
|
|
key: 'bufferedInfo',
|
|
value: function bufferedInfo(buffered, pos, maxHoleDuration) {
|
|
var buffered2 = [],
|
|
|
|
// bufferStart and bufferEnd are buffer boundaries around current video position
|
|
bufferLen,
|
|
bufferStart,
|
|
bufferEnd,
|
|
bufferStartNext,
|
|
i;
|
|
// sort on buffer.start/smaller end (IE does not always return sorted buffered range)
|
|
buffered.sort(function (a, b) {
|
|
var diff = a.start - b.start;
|
|
if (diff) {
|
|
return diff;
|
|
} else {
|
|
return b.end - a.end;
|
|
}
|
|
});
|
|
// there might be some small holes between buffer time range
|
|
// consider that holes smaller than maxHoleDuration are irrelevant and build another
|
|
// buffer time range representations that discards those holes
|
|
for (i = 0; i < buffered.length; i++) {
|
|
var buf2len = buffered2.length;
|
|
if (buf2len) {
|
|
var buf2end = buffered2[buf2len - 1].end;
|
|
// if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
|
|
if (buffered[i].start - buf2end < maxHoleDuration) {
|
|
// merge overlapping time ranges
|
|
// update lastRange.end only if smaller than item.end
|
|
// e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
|
|
// whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
|
|
if (buffered[i].end > buf2end) {
|
|
buffered2[buf2len - 1].end = buffered[i].end;
|
|
}
|
|
} else {
|
|
// big hole
|
|
buffered2.push(buffered[i]);
|
|
}
|
|
} else {
|
|
// first value
|
|
buffered2.push(buffered[i]);
|
|
}
|
|
}
|
|
for (i = 0, bufferLen = 0, bufferStart = bufferEnd = pos; i < buffered2.length; i++) {
|
|
var start = buffered2[i].start,
|
|
end = buffered2[i].end;
|
|
//logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
|
|
if (pos + maxHoleDuration >= start && pos < end) {
|
|
// play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
|
|
bufferStart = start;
|
|
bufferEnd = end;
|
|
bufferLen = bufferEnd - pos;
|
|
} else if (pos + maxHoleDuration < start) {
|
|
bufferStartNext = start;
|
|
break;
|
|
}
|
|
}
|
|
return { len: bufferLen, start: bufferStart, end: bufferEnd, nextStart: bufferStartNext };
|
|
}
|
|
}, {
|
|
key: 'getBufferRange',
|
|
value: function getBufferRange(position) {
|
|
var i,
|
|
range,
|
|
bufferRange = this.bufferRange;
|
|
if (bufferRange) {
|
|
for (i = bufferRange.length - 1; i >= 0; i--) {
|
|
range = bufferRange[i];
|
|
if (position >= range.start && position <= range.end) {
|
|
return range;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}, {
|
|
key: 'followingBufferRange',
|
|
value: function followingBufferRange(range) {
|
|
if (range) {
|
|
// try to get range of next fragment (500ms after this range)
|
|
return this.getBufferRange(range.end + 0.5);
|
|
}
|
|
return null;
|
|
}
|
|
}, {
|
|
key: 'isBuffered',
|
|
value: function isBuffered(position) {
|
|
var v = this.media,
|
|
buffered = v.buffered;
|
|
for (var i = 0; i < buffered.length; i++) {
|
|
if (position >= buffered.start(i) && position <= buffered.end(i)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}, {
|
|
key: '_checkFragmentChanged',
|
|
value: function _checkFragmentChanged() {
|
|
var rangeCurrent,
|
|
currentTime,
|
|
video = this.media;
|
|
if (video && video.seeking === false) {
|
|
currentTime = video.currentTime;
|
|
/* if video element is in seeked state, currentTime can only increase.
|
|
(assuming that playback rate is positive ...)
|
|
As sometimes currentTime jumps back to zero after a
|
|
media decode error, check this, to avoid seeking back to
|
|
wrong position after a media decode error
|
|
*/
|
|
if (currentTime > video.playbackRate * this.lastCurrentTime) {
|
|
this.lastCurrentTime = currentTime;
|
|
}
|
|
if (this.isBuffered(currentTime)) {
|
|
rangeCurrent = this.getBufferRange(currentTime);
|
|
} else if (this.isBuffered(currentTime + 0.1)) {
|
|
/* ensure that FRAG_CHANGED event is triggered at startup,
|
|
when first video frame is displayed and playback is paused.
|
|
add a tolerance of 100ms, in case current position is not buffered,
|
|
check if current pos+100ms is buffered and use that buffer range
|
|
for FRAG_CHANGED event reporting */
|
|
rangeCurrent = this.getBufferRange(currentTime + 0.1);
|
|
}
|
|
if (rangeCurrent) {
|
|
var fragPlaying = rangeCurrent.frag;
|
|
if (fragPlaying !== this.fragPlaying) {
|
|
this.fragPlaying = fragPlaying;
|
|
this.hls.trigger(_events2.default.FRAG_CHANGED, { frag: fragPlaying });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
on immediate level switch :
|
|
- pause playback if playing
|
|
- cancel any pending load request
|
|
- and trigger a buffer flush
|
|
*/
|
|
|
|
}, {
|
|
key: 'immediateLevelSwitch',
|
|
value: function immediateLevelSwitch() {
|
|
_logger.logger.log('immediateLevelSwitch');
|
|
if (!this.immediateSwitch) {
|
|
this.immediateSwitch = true;
|
|
this.previouslyPaused = this.media.paused;
|
|
this.media.pause();
|
|
}
|
|
var fragCurrent = this.fragCurrent;
|
|
if (fragCurrent && fragCurrent.loader) {
|
|
fragCurrent.loader.abort();
|
|
}
|
|
this.fragCurrent = null;
|
|
// flush everything
|
|
this.hls.trigger(_events2.default.BUFFER_FLUSHING, { startOffset: 0, endOffset: Number.POSITIVE_INFINITY });
|
|
this.state = State.PAUSED;
|
|
// increase fragment load Index to avoid frag loop loading error after buffer flush
|
|
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold;
|
|
// speed up switching, trigger timer function
|
|
this.tick();
|
|
}
|
|
|
|
/*
|
|
on immediate level switch end, after new fragment has been buffered :
|
|
- nudge video decoder by slightly adjusting video currentTime
|
|
- resume the playback if needed
|
|
*/
|
|
|
|
}, {
|
|
key: 'immediateLevelSwitchEnd',
|
|
value: function immediateLevelSwitchEnd() {
|
|
this.immediateSwitch = false;
|
|
this.media.currentTime -= 0.0001;
|
|
if (!this.previouslyPaused) {
|
|
this.media.play();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'nextLevelSwitch',
|
|
value: function nextLevelSwitch() {
|
|
/* try to switch ASAP without breaking video playback :
|
|
in order to ensure smooth but quick level switching,
|
|
we need to find the next flushable buffer range
|
|
we should take into account new segment fetch time
|
|
*/
|
|
var fetchdelay, currentRange, nextRange;
|
|
currentRange = this.getBufferRange(this.media.currentTime);
|
|
if (currentRange && currentRange.start > 1) {
|
|
// flush buffer preceding current fragment (flush until current fragment start offset)
|
|
// minus 1s to avoid video freezing, that could happen if we flush keyframe of current video ...
|
|
this.hls.trigger(_events2.default.BUFFER_FLUSHING, { startOffset: 0, endOffset: currentRange.start - 1 });
|
|
this.state = State.PAUSED;
|
|
}
|
|
if (!this.media.paused) {
|
|
// add a safety delay of 1s
|
|
var nextLevelId = this.hls.nextLoadLevel,
|
|
nextLevel = this.levels[nextLevelId],
|
|
fragLastKbps = this.fragLastKbps;
|
|
if (fragLastKbps && this.fragCurrent) {
|
|
fetchdelay = this.fragCurrent.duration * nextLevel.bitrate / (1000 * fragLastKbps) + 1;
|
|
} else {
|
|
fetchdelay = 0;
|
|
}
|
|
} else {
|
|
fetchdelay = 0;
|
|
}
|
|
//logger.log('fetchdelay:'+fetchdelay);
|
|
// find buffer range that will be reached once new fragment will be fetched
|
|
nextRange = this.getBufferRange(this.media.currentTime + fetchdelay);
|
|
if (nextRange) {
|
|
// we can flush buffer range following this one without stalling playback
|
|
nextRange = this.followingBufferRange(nextRange);
|
|
if (nextRange) {
|
|
// flush position is the start position of this new buffer
|
|
this.hls.trigger(_events2.default.BUFFER_FLUSHING, { startOffset: nextRange.start, endOffset: Number.POSITIVE_INFINITY });
|
|
this.state = State.PAUSED;
|
|
// if we are here, we can also cancel any loading/demuxing in progress, as they are useless
|
|
var fragCurrent = this.fragCurrent;
|
|
if (fragCurrent && fragCurrent.loader) {
|
|
fragCurrent.loader.abort();
|
|
}
|
|
this.fragCurrent = null;
|
|
// increase fragment load Index to avoid frag loop loading error after buffer flush
|
|
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold;
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onMediaAttached',
|
|
value: function onMediaAttached(data) {
|
|
var media = this.media = data.media;
|
|
this.onvseeking = this.onMediaSeeking.bind(this);
|
|
this.onvseeked = this.onMediaSeeked.bind(this);
|
|
this.onvended = this.onMediaEnded.bind(this);
|
|
media.addEventListener('seeking', this.onvseeking);
|
|
media.addEventListener('seeked', this.onvseeked);
|
|
media.addEventListener('ended', this.onvended);
|
|
if (this.levels && this.config.autoStartLoad) {
|
|
this.startLoad();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onMediaDetaching',
|
|
value: function onMediaDetaching() {
|
|
var media = this.media;
|
|
if (media && media.ended) {
|
|
_logger.logger.log('MSE detaching and video ended, reset startPosition');
|
|
this.startPosition = this.lastCurrentTime = 0;
|
|
}
|
|
|
|
// reset fragment loading counter on MSE detaching to avoid reporting FRAG_LOOP_LOADING_ERROR after error recovery
|
|
var levels = this.levels;
|
|
if (levels) {
|
|
// reset fragment load counter
|
|
levels.forEach(function (level) {
|
|
if (level.details) {
|
|
level.details.fragments.forEach(function (fragment) {
|
|
fragment.loadCounter = undefined;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
// remove video listeners
|
|
if (media) {
|
|
media.removeEventListener('seeking', this.onvseeking);
|
|
media.removeEventListener('seeked', this.onvseeked);
|
|
media.removeEventListener('ended', this.onvended);
|
|
this.onvseeking = this.onvseeked = this.onvended = null;
|
|
}
|
|
this.media = null;
|
|
this.loadedmetadata = false;
|
|
this.stop();
|
|
}
|
|
}, {
|
|
key: 'onMediaSeeking',
|
|
value: function onMediaSeeking() {
|
|
if (this.state === State.FRAG_LOADING) {
|
|
// check if currently loaded fragment is inside buffer.
|
|
//if outside, cancel fragment loading, otherwise do nothing
|
|
if (this.bufferInfo(this.media.currentTime, this.config.maxBufferHole).len === 0) {
|
|
_logger.logger.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
|
|
var fragCurrent = this.fragCurrent;
|
|
if (fragCurrent) {
|
|
if (fragCurrent.loader) {
|
|
fragCurrent.loader.abort();
|
|
}
|
|
this.fragCurrent = null;
|
|
}
|
|
this.fragPrevious = null;
|
|
// switch to IDLE state to load new fragment
|
|
this.state = State.IDLE;
|
|
}
|
|
} else if (this.state === State.ENDED) {
|
|
// switch to IDLE state to check for potential new fragment
|
|
this.state = State.IDLE;
|
|
}
|
|
if (this.media) {
|
|
this.lastCurrentTime = this.media.currentTime;
|
|
}
|
|
// avoid reporting fragment loop loading error in case user is seeking several times on same position
|
|
if (this.fragLoadIdx !== undefined) {
|
|
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold;
|
|
}
|
|
// tick to speed up processing
|
|
this.tick();
|
|
}
|
|
}, {
|
|
key: 'onMediaSeeked',
|
|
value: function onMediaSeeked() {
|
|
// tick to speed up FRAGMENT_PLAYING triggering
|
|
this.tick();
|
|
}
|
|
}, {
|
|
key: 'onMediaEnded',
|
|
value: function onMediaEnded() {
|
|
_logger.logger.log('media ended');
|
|
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
|
|
this.startPosition = this.lastCurrentTime = 0;
|
|
}
|
|
}, {
|
|
key: 'onManifestParsed',
|
|
value: function onManifestParsed(data) {
|
|
var aac = false,
|
|
heaac = false,
|
|
codec;
|
|
data.levels.forEach(function (level) {
|
|
// detect if we have different kind of audio codecs used amongst playlists
|
|
codec = level.audioCodec;
|
|
if (codec) {
|
|
if (codec.indexOf('mp4a.40.2') !== -1) {
|
|
aac = true;
|
|
}
|
|
if (codec.indexOf('mp4a.40.5') !== -1) {
|
|
heaac = true;
|
|
}
|
|
}
|
|
});
|
|
this.audioCodecSwitch = aac && heaac;
|
|
if (this.audioCodecSwitch) {
|
|
_logger.logger.log('both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC');
|
|
}
|
|
this.levels = data.levels;
|
|
this.startLevelLoaded = false;
|
|
this.startFragRequested = false;
|
|
if (this.config.autoStartLoad) {
|
|
this.startLoad();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onLevelLoaded',
|
|
value: function onLevelLoaded(data) {
|
|
var newDetails = data.details,
|
|
newLevelId = data.level,
|
|
curLevel = this.levels[newLevelId],
|
|
duration = newDetails.totalduration,
|
|
sliding = 0;
|
|
|
|
_logger.logger.log('level ' + newLevelId + ' loaded [' + newDetails.startSN + ',' + newDetails.endSN + '],duration:' + duration);
|
|
this.levelLastLoaded = newLevelId;
|
|
|
|
if (newDetails.live) {
|
|
var curDetails = curLevel.details;
|
|
if (curDetails) {
|
|
// we already have details for that level, merge them
|
|
_levelHelper2.default.mergeDetails(curDetails, newDetails);
|
|
sliding = newDetails.fragments[0].start;
|
|
if (newDetails.PTSKnown) {
|
|
_logger.logger.log('live playlist sliding:' + sliding.toFixed(3));
|
|
} else {
|
|
_logger.logger.log('live playlist - outdated PTS, unknown sliding');
|
|
}
|
|
} else {
|
|
newDetails.PTSKnown = false;
|
|
_logger.logger.log('live playlist - first load, unknown sliding');
|
|
}
|
|
} else {
|
|
newDetails.PTSKnown = false;
|
|
}
|
|
// override level info
|
|
curLevel.details = newDetails;
|
|
this.hls.trigger(_events2.default.LEVEL_UPDATED, { details: newDetails, level: newLevelId });
|
|
|
|
// compute start position
|
|
if (this.startFragRequested === false) {
|
|
// if live playlist, set start position to be fragment N-this.config.liveSyncDurationCount (usually 3)
|
|
if (newDetails.live) {
|
|
var targetLatency = this.config.liveSyncDuration !== undefined ? this.config.liveSyncDuration : this.config.liveSyncDurationCount * newDetails.targetduration;
|
|
this.startPosition = Math.max(0, sliding + duration - targetLatency);
|
|
}
|
|
this.nextLoadPosition = this.startPosition;
|
|
}
|
|
// only switch batck to IDLE state if we were waiting for level to start downloading a new fragment
|
|
if (this.state === State.WAITING_LEVEL) {
|
|
this.state = State.IDLE;
|
|
}
|
|
//trigger handler right now
|
|
this.tick();
|
|
}
|
|
}, {
|
|
key: 'onKeyLoaded',
|
|
value: function onKeyLoaded() {
|
|
if (this.state === State.KEY_LOADING) {
|
|
this.state = State.IDLE;
|
|
this.tick();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onFragLoaded',
|
|
value: function onFragLoaded(data) {
|
|
var fragCurrent = this.fragCurrent;
|
|
if (this.state === State.FRAG_LOADING && fragCurrent && data.frag.level === fragCurrent.level && data.frag.sn === fragCurrent.sn) {
|
|
if (this.fragBitrateTest === true) {
|
|
// switch back to IDLE state ... we just loaded a fragment to determine adequate start bitrate and initialize autoswitch algo
|
|
this.state = State.IDLE;
|
|
this.fragBitrateTest = false;
|
|
data.stats.tparsed = data.stats.tbuffered = performance.now();
|
|
this.hls.trigger(_events2.default.FRAG_BUFFERED, { stats: data.stats, frag: fragCurrent });
|
|
} else {
|
|
this.state = State.PARSING;
|
|
// transmux the MPEG-TS data to ISO-BMFF segments
|
|
this.stats = data.stats;
|
|
var currentLevel = this.levels[this.level],
|
|
details = currentLevel.details,
|
|
duration = details.totalduration,
|
|
start = fragCurrent.start,
|
|
level = fragCurrent.level,
|
|
sn = fragCurrent.sn,
|
|
audioCodec = currentLevel.audioCodec || this.config.defaultAudioCodec;
|
|
if (this.audioCodecSwap) {
|
|
_logger.logger.log('swapping playlist audio codec');
|
|
if (audioCodec === undefined) {
|
|
audioCodec = this.lastAudioCodec;
|
|
}
|
|
if (audioCodec) {
|
|
if (audioCodec.indexOf('mp4a.40.5') !== -1) {
|
|
audioCodec = 'mp4a.40.2';
|
|
} else {
|
|
audioCodec = 'mp4a.40.5';
|
|
}
|
|
}
|
|
}
|
|
this.pendingAppending = 0;
|
|
_logger.logger.log('Demuxing ' + sn + ' of [' + details.startSN + ' ,' + details.endSN + '],level ' + level);
|
|
this.demuxer.push(data.payload, audioCodec, currentLevel.videoCodec, start, fragCurrent.cc, level, sn, duration, fragCurrent.decryptdata);
|
|
}
|
|
}
|
|
this.fragLoadError = 0;
|
|
}
|
|
}, {
|
|
key: 'onFragParsingInitSegment',
|
|
value: function onFragParsingInitSegment(data) {
|
|
if (this.state === State.PARSING) {
|
|
var tracks = data.tracks,
|
|
trackName,
|
|
track;
|
|
|
|
// include levelCodec in audio and video tracks
|
|
track = tracks.audio;
|
|
if (track) {
|
|
var audioCodec = this.levels[this.level].audioCodec,
|
|
ua = navigator.userAgent.toLowerCase();
|
|
if (audioCodec && this.audioCodecSwap) {
|
|
_logger.logger.log('swapping playlist audio codec');
|
|
if (audioCodec.indexOf('mp4a.40.5') !== -1) {
|
|
audioCodec = 'mp4a.40.2';
|
|
} else {
|
|
audioCodec = 'mp4a.40.5';
|
|
}
|
|
}
|
|
// in case AAC and HE-AAC audio codecs are signalled in manifest
|
|
// force HE-AAC , as it seems that most browsers prefers that way,
|
|
// except for mono streams OR on FF
|
|
// these conditions might need to be reviewed ...
|
|
if (this.audioCodecSwitch) {
|
|
// don't force HE-AAC if mono stream
|
|
if (track.metadata.channelCount !== 1 &&
|
|
// don't force HE-AAC if firefox
|
|
ua.indexOf('firefox') === -1) {
|
|
audioCodec = 'mp4a.40.5';
|
|
}
|
|
}
|
|
// HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise
|
|
if (ua.indexOf('android') !== -1) {
|
|
audioCodec = 'mp4a.40.2';
|
|
_logger.logger.log('Android: force audio codec to' + audioCodec);
|
|
}
|
|
track.levelCodec = audioCodec;
|
|
}
|
|
track = tracks.video;
|
|
if (track) {
|
|
track.levelCodec = this.levels[this.level].videoCodec;
|
|
}
|
|
|
|
// if remuxer specify that a unique track needs to generated,
|
|
// let's merge all tracks together
|
|
if (data.unique) {
|
|
var mergedTrack = {
|
|
codec: '',
|
|
levelCodec: ''
|
|
};
|
|
for (trackName in data.tracks) {
|
|
track = tracks[trackName];
|
|
mergedTrack.container = track.container;
|
|
if (mergedTrack.codec) {
|
|
mergedTrack.codec += ',';
|
|
mergedTrack.levelCodec += ',';
|
|
}
|
|
if (track.codec) {
|
|
mergedTrack.codec += track.codec;
|
|
}
|
|
if (track.levelCodec) {
|
|
mergedTrack.levelCodec += track.levelCodec;
|
|
}
|
|
}
|
|
tracks = { audiovideo: mergedTrack };
|
|
}
|
|
this.hls.trigger(_events2.default.BUFFER_CODECS, tracks);
|
|
// loop through tracks that are going to be provided to bufferController
|
|
for (trackName in tracks) {
|
|
track = tracks[trackName];
|
|
_logger.logger.log('track:' + trackName + ',container:' + track.container + ',codecs[level/parsed]=[' + track.levelCodec + '/' + track.codec + ']');
|
|
var initSegment = track.initSegment;
|
|
if (initSegment) {
|
|
this.pendingAppending++;
|
|
this.hls.trigger(_events2.default.BUFFER_APPENDING, { type: trackName, data: initSegment });
|
|
}
|
|
}
|
|
//trigger handler right now
|
|
this.tick();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onFragParsingData',
|
|
value: function onFragParsingData(data) {
|
|
var _this3 = this;
|
|
|
|
if (this.state === State.PARSING) {
|
|
this.tparse2 = Date.now();
|
|
var level = this.levels[this.level],
|
|
frag = this.fragCurrent;
|
|
|
|
_logger.logger.log('parsed ' + data.type + ',PTS:[' + data.startPTS.toFixed(3) + ',' + data.endPTS.toFixed(3) + '],DTS:[' + data.startDTS.toFixed(3) + '/' + data.endDTS.toFixed(3) + '],nb:' + data.nb);
|
|
|
|
var drift = _levelHelper2.default.updateFragPTS(level.details, frag.sn, data.startPTS, data.endPTS),
|
|
hls = this.hls;
|
|
hls.trigger(_events2.default.LEVEL_PTS_UPDATED, { details: level.details, level: this.level, drift: drift });
|
|
|
|
[data.data1, data.data2].forEach(function (buffer) {
|
|
if (buffer) {
|
|
_this3.pendingAppending++;
|
|
hls.trigger(_events2.default.BUFFER_APPENDING, { type: data.type, data: buffer });
|
|
}
|
|
});
|
|
|
|
this.nextLoadPosition = data.endPTS;
|
|
this.bufferRange.push({ type: data.type, start: data.startPTS, end: data.endPTS, frag: frag });
|
|
|
|
//trigger handler right now
|
|
this.tick();
|
|
} else {
|
|
_logger.logger.warn('not in PARSING state but ' + this.state + ', ignoring FRAG_PARSING_DATA event');
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onFragParsed',
|
|
value: function onFragParsed() {
|
|
if (this.state === State.PARSING) {
|
|
this.stats.tparsed = performance.now();
|
|
this.state = State.PARSED;
|
|
this._checkAppendedParsed();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onBufferAppended',
|
|
value: function onBufferAppended() {
|
|
switch (this.state) {
|
|
case State.PARSING:
|
|
case State.PARSED:
|
|
this.pendingAppending--;
|
|
this._checkAppendedParsed();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}, {
|
|
key: '_checkAppendedParsed',
|
|
value: function _checkAppendedParsed() {
|
|
//trigger handler right now
|
|
if (this.state === State.PARSED && this.pendingAppending === 0) {
|
|
var frag = this.fragCurrent,
|
|
stats = this.stats;
|
|
if (frag) {
|
|
this.fragPrevious = frag;
|
|
stats.tbuffered = performance.now();
|
|
this.fragLastKbps = Math.round(8 * stats.length / (stats.tbuffered - stats.tfirst));
|
|
this.hls.trigger(_events2.default.FRAG_BUFFERED, { stats: stats, frag: frag });
|
|
_logger.logger.log('media buffered : ' + this.timeRangesToString(this.media.buffered));
|
|
this.state = State.IDLE;
|
|
}
|
|
this.tick();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onError',
|
|
value: function onError(data) {
|
|
switch (data.details) {
|
|
case _errors.ErrorDetails.FRAG_LOAD_ERROR:
|
|
case _errors.ErrorDetails.FRAG_LOAD_TIMEOUT:
|
|
if (!data.fatal) {
|
|
var loadError = this.fragLoadError;
|
|
if (loadError) {
|
|
loadError++;
|
|
} else {
|
|
loadError = 1;
|
|
}
|
|
if (loadError <= this.config.fragLoadingMaxRetry) {
|
|
this.fragLoadError = loadError;
|
|
// reset load counter to avoid frag loop loading error
|
|
data.frag.loadCounter = 0;
|
|
// exponential backoff capped to 64s
|
|
var delay = Math.min(Math.pow(2, loadError - 1) * this.config.fragLoadingRetryDelay, 64000);
|
|
_logger.logger.warn('mediaController: frag loading failed, retry in ' + delay + ' ms');
|
|
this.retryDate = performance.now() + delay;
|
|
// retry loading state
|
|
this.state = State.FRAG_LOADING_WAITING_RETRY;
|
|
} else {
|
|
_logger.logger.error('mediaController: ' + data.details + ' reaches max retry, redispatch as fatal ...');
|
|
// redispatch same error but with fatal set to true
|
|
data.fatal = true;
|
|
this.hls.trigger(_events2.default.ERROR, data);
|
|
this.state = State.ERROR;
|
|
}
|
|
}
|
|
break;
|
|
case _errors.ErrorDetails.FRAG_LOOP_LOADING_ERROR:
|
|
case _errors.ErrorDetails.LEVEL_LOAD_ERROR:
|
|
case _errors.ErrorDetails.LEVEL_LOAD_TIMEOUT:
|
|
case _errors.ErrorDetails.KEY_LOAD_ERROR:
|
|
case _errors.ErrorDetails.KEY_LOAD_TIMEOUT:
|
|
// if fatal error, stop processing, otherwise move to IDLE to retry loading
|
|
_logger.logger.warn('mediaController: ' + data.details + ' while loading frag,switch to ' + (data.fatal ? 'ERROR' : 'IDLE') + ' state ...');
|
|
this.state = data.fatal ? State.ERROR : State.IDLE;
|
|
break;
|
|
case _errors.ErrorDetails.BUFFER_FULL_ERROR:
|
|
// trigger a smooth level switch to empty buffers
|
|
// also reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
|
|
this.config.maxMaxBufferLength /= 2;
|
|
_logger.logger.warn('reduce max buffer length to ' + this.config.maxMaxBufferLength + 's and trigger a nextLevelSwitch to flush old buffer and fix QuotaExceededError');
|
|
this.nextLevelSwitch();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}, {
|
|
key: '_checkBuffer',
|
|
value: function _checkBuffer() {
|
|
var media = this.media;
|
|
if (media) {
|
|
// compare readyState
|
|
var readyState = media.readyState;
|
|
// if ready state different from HAVE_NOTHING (numeric value 0), we are allowed to seek
|
|
if (readyState) {
|
|
var targetSeekPosition, currentTime;
|
|
// if seek after buffered defined, let's seek if within acceptable range
|
|
var seekAfterBuffered = this.seekAfterBuffered;
|
|
if (seekAfterBuffered) {
|
|
if (media.duration >= seekAfterBuffered) {
|
|
targetSeekPosition = seekAfterBuffered;
|
|
this.seekAfterBuffered = undefined;
|
|
}
|
|
} else {
|
|
currentTime = media.currentTime;
|
|
var loadedmetadata = this.loadedmetadata;
|
|
|
|
// adjust currentTime to start position on loaded metadata
|
|
if (!loadedmetadata && media.buffered.length) {
|
|
this.loadedmetadata = true;
|
|
// only adjust currentTime if not equal to 0
|
|
if (!currentTime && currentTime !== this.startPosition) {
|
|
targetSeekPosition = this.startPosition;
|
|
}
|
|
}
|
|
}
|
|
if (targetSeekPosition) {
|
|
currentTime = targetSeekPosition;
|
|
_logger.logger.log('target seek position:' + targetSeekPosition);
|
|
}
|
|
var bufferInfo = this.bufferInfo(currentTime, 0),
|
|
expectedPlaying = !(media.paused || media.ended || media.seeking || readyState < 2),
|
|
jumpThreshold = 0.4,
|
|
// tolerance needed as some browsers stalls playback before reaching buffered range end
|
|
playheadMoving = currentTime > media.playbackRate * this.lastCurrentTime;
|
|
|
|
if (this.stalled && playheadMoving) {
|
|
this.stalled = false;
|
|
}
|
|
// check buffer upfront
|
|
// if less than 200ms is buffered, and media is expected to play but playhead is not moving,
|
|
// and we have a new buffer range available upfront, let's seek to that one
|
|
if (bufferInfo.len <= jumpThreshold) {
|
|
if (playheadMoving || !expectedPlaying) {
|
|
// playhead moving or media not playing
|
|
jumpThreshold = 0;
|
|
} else {
|
|
// playhead not moving AND media expected to play
|
|
_logger.logger.log('playback seems stuck @' + currentTime);
|
|
if (!this.stalled) {
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.BUFFER_STALLED_ERROR, fatal: false });
|
|
this.stalled = true;
|
|
}
|
|
}
|
|
// if we are below threshold, try to jump if next buffer range is close
|
|
if (bufferInfo.len <= jumpThreshold) {
|
|
// no buffer available @ currentTime, check if next buffer is close (within a config.maxSeekHole second range)
|
|
var nextBufferStart = bufferInfo.nextStart,
|
|
delta = nextBufferStart - currentTime;
|
|
if (nextBufferStart && delta < this.config.maxSeekHole && delta > 0 && !media.seeking) {
|
|
// next buffer is close ! adjust currentTime to nextBufferStart
|
|
// this will ensure effective video decoding
|
|
_logger.logger.log('adjust currentTime from ' + media.currentTime + ' to next buffered @ ' + nextBufferStart);
|
|
media.currentTime = nextBufferStart;
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.BUFFER_SEEK_OVER_HOLE, fatal: false });
|
|
}
|
|
}
|
|
} else {
|
|
if (targetSeekPosition && media.currentTime !== targetSeekPosition) {
|
|
_logger.logger.log('adjust currentTime from ' + media.currentTime + ' to ' + targetSeekPosition);
|
|
media.currentTime = targetSeekPosition;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onBufferFlushed',
|
|
value: function onBufferFlushed() {
|
|
/* after successful buffer flushing, rebuild buffer Range array
|
|
loop through existing buffer range and check if
|
|
corresponding range is still buffered. only push to new array already buffered range
|
|
*/
|
|
var newRange = [],
|
|
range,
|
|
i;
|
|
for (i = 0; i < this.bufferRange.length; i++) {
|
|
range = this.bufferRange[i];
|
|
if (this.isBuffered((range.start + range.end) / 2)) {
|
|
newRange.push(range);
|
|
}
|
|
}
|
|
this.bufferRange = newRange;
|
|
|
|
// handle end of immediate switching if needed
|
|
if (this.immediateSwitch) {
|
|
this.immediateLevelSwitchEnd();
|
|
}
|
|
// move to IDLE once flush complete. this should trigger new fragment loading
|
|
this.state = State.IDLE;
|
|
// reset reference to frag
|
|
this.fragPrevious = null;
|
|
}
|
|
}, {
|
|
key: 'swapAudioCodec',
|
|
value: function swapAudioCodec() {
|
|
this.audioCodecSwap = !this.audioCodecSwap;
|
|
}
|
|
}, {
|
|
key: 'timeRangesToString',
|
|
value: function timeRangesToString(r) {
|
|
var log = '',
|
|
len = r.length;
|
|
for (var i = 0; i < len; i++) {
|
|
log += '[' + r.start(i) + ',' + r.end(i) + ']';
|
|
}
|
|
return log;
|
|
}
|
|
}, {
|
|
key: 'currentLevel',
|
|
get: function get() {
|
|
if (this.media) {
|
|
var range = this.getBufferRange(this.media.currentTime);
|
|
if (range) {
|
|
return range.frag.level;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}, {
|
|
key: 'nextBufferRange',
|
|
get: function get() {
|
|
if (this.media) {
|
|
// first get end range of current fragment
|
|
return this.followingBufferRange(this.getBufferRange(this.media.currentTime));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'nextLevel',
|
|
get: function get() {
|
|
var range = this.nextBufferRange;
|
|
if (range) {
|
|
return range.frag.level;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return StreamController;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = StreamController;
|
|
|
|
},{"../demux/demuxer":15,"../errors":19,"../event-handler":20,"../events":21,"../helper/level-helper":22,"../utils/binary-search":32,"../utils/logger":34}],7:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _cea708Interpreter = require('../utils/cea-708-interpreter');
|
|
|
|
var _cea708Interpreter2 = _interopRequireDefault(_cea708Interpreter);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Timeline Controller
|
|
*/
|
|
|
|
var TimelineController = function (_EventHandler) {
|
|
_inherits(TimelineController, _EventHandler);
|
|
|
|
function TimelineController(hls) {
|
|
_classCallCheck(this, TimelineController);
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(TimelineController).call(this, hls, _events2.default.MEDIA_ATTACHING, _events2.default.MEDIA_DETACHING, _events2.default.FRAG_PARSING_USERDATA, _events2.default.MANIFEST_LOADING, _events2.default.FRAG_LOADED));
|
|
|
|
_this.hls = hls;
|
|
_this.config = hls.config;
|
|
|
|
if (_this.config.enableCEA708Captions) {
|
|
_this.cea708Interpreter = new _cea708Interpreter2.default();
|
|
}
|
|
return _this;
|
|
}
|
|
|
|
_createClass(TimelineController, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onMediaAttaching',
|
|
value: function onMediaAttaching(data) {
|
|
var media = this.media = data.media;
|
|
this.cea708Interpreter.attach(media);
|
|
}
|
|
}, {
|
|
key: 'onMediaDetaching',
|
|
value: function onMediaDetaching() {
|
|
this.cea708Interpreter.detach();
|
|
}
|
|
}, {
|
|
key: 'onManifestLoading',
|
|
value: function onManifestLoading() {
|
|
this.lastPts = Number.POSITIVE_INFINITY;
|
|
}
|
|
}, {
|
|
key: 'onFragLoaded',
|
|
value: function onFragLoaded(data) {
|
|
var pts = data.frag.start; //Number.POSITIVE_INFINITY;
|
|
|
|
// if this is a frag for a previously loaded timerange, remove all captions
|
|
// TODO: consider just removing captions for the timerange
|
|
if (pts <= this.lastPts) {
|
|
this.cea708Interpreter.clear();
|
|
}
|
|
|
|
this.lastPts = pts;
|
|
}
|
|
}, {
|
|
key: 'onFragParsingUserdata',
|
|
value: function onFragParsingUserdata(data) {
|
|
// push all of the CEA-708 messages into the interpreter
|
|
// immediately. It will create the proper timestamps based on our PTS value
|
|
for (var i = 0; i < data.samples.length; i++) {
|
|
this.cea708Interpreter.push(data.samples[i].pts, data.samples[i].bytes);
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return TimelineController;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = TimelineController;
|
|
|
|
},{"../event-handler":20,"../events":21,"../utils/cea-708-interpreter":33}],8:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
/*
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
var AES = function () {
|
|
|
|
/**
|
|
* Schedule out an AES key for both encryption and decryption. This
|
|
* is a low-level class. Use a cipher mode to do bulk encryption.
|
|
*
|
|
* @constructor
|
|
* @param key {Array} The key as an array of 4, 6 or 8 words.
|
|
*/
|
|
|
|
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
|
|
*/
|
|
this._tables = [[[], [], [], [], []], [[], [], [], [], []]];
|
|
|
|
this._precompute();
|
|
|
|
var i,
|
|
j,
|
|
tmp,
|
|
encKey,
|
|
decKey,
|
|
sbox = this._tables[0][4],
|
|
decTable = this._tables[1],
|
|
keyLen = key.length,
|
|
rcon = 1;
|
|
|
|
if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
|
|
throw new Error('Invalid aes key size=' + keyLen);
|
|
}
|
|
|
|
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]];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expand the S-box tables.
|
|
*
|
|
* @private
|
|
*/
|
|
|
|
_createClass(AES, [{
|
|
key: '_precompute',
|
|
value: function _precompute() {
|
|
var encTable = this._tables[0],
|
|
decTable = this._tables[1],
|
|
sbox = encTable[4],
|
|
sboxInv = decTable[4],
|
|
i,
|
|
x,
|
|
xInv,
|
|
d = [],
|
|
th = [],
|
|
x2,
|
|
x4,
|
|
x8,
|
|
s,
|
|
tEnc,
|
|
tDec;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt 16 bytes, specified as four 32-bit words.
|
|
* @param encrypted0 {number} the first word to decrypt
|
|
* @param encrypted1 {number} the second word to decrypt
|
|
* @param encrypted2 {number} the third word to decrypt
|
|
* @param encrypted3 {number} the fourth word to decrypt
|
|
* @param out {Int32Array} the array to write the decrypted words
|
|
* into
|
|
* @param offset {number} the offset into the output array to start
|
|
* writing results
|
|
* @return {Array} The plaintext.
|
|
*/
|
|
|
|
}, {
|
|
key: 'decrypt',
|
|
value: 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
|
|
a = encrypted0 ^ key[0],
|
|
b = encrypted3 ^ key[1],
|
|
c = encrypted2 ^ key[2],
|
|
d = encrypted1 ^ key[3],
|
|
a2,
|
|
b2,
|
|
c2,
|
|
nInnerRounds = key.length / 4 - 2,
|
|
// key.length === 2 ?
|
|
i,
|
|
kIndex = 4,
|
|
table = this._tables[1],
|
|
|
|
// load up the tables
|
|
table0 = table[0],
|
|
table1 = table[1],
|
|
table2 = table[2],
|
|
table3 = table[3],
|
|
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;
|
|
}();
|
|
|
|
exports.default = AES;
|
|
|
|
},{}],9:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /*
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _aes = require('./aes');
|
|
|
|
var _aes2 = _interopRequireDefault(_aes);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var AES128Decrypter = function () {
|
|
function AES128Decrypter(key, initVector) {
|
|
_classCallCheck(this, AES128Decrypter);
|
|
|
|
this.key = key;
|
|
this.iv = initVector;
|
|
}
|
|
|
|
/**
|
|
* Convert network-order (big-endian) bytes into their little-endian
|
|
* representation.
|
|
*/
|
|
|
|
_createClass(AES128Decrypter, [{
|
|
key: 'ntoh',
|
|
value: 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 encrypted {Uint8Array} the encrypted bytes
|
|
* @param key {Uint32Array} the bytes of the decryption key
|
|
* @param initVector {Uint32Array} 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
|
|
*/
|
|
|
|
}, {
|
|
key: 'doDecrypt',
|
|
value: function doDecrypt(encrypted, key, initVector) {
|
|
var
|
|
// word-level access to the encrypted bytes
|
|
encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2),
|
|
decipher = new _aes2.default(Array.prototype.slice.call(key)),
|
|
|
|
// byte and word-level access for the decrypted output
|
|
decrypted = new Uint8Array(encrypted.byteLength),
|
|
decrypted32 = new Int32Array(decrypted.buffer),
|
|
|
|
// temporary variables for working with the IV, encrypted, and
|
|
// decrypted data
|
|
init0,
|
|
init1,
|
|
init2,
|
|
init3,
|
|
encrypted0,
|
|
encrypted1,
|
|
encrypted2,
|
|
encrypted3,
|
|
|
|
// iteration variable
|
|
wordIx;
|
|
|
|
// 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 = ~ ~this.ntoh(encrypted32[wordIx]);
|
|
encrypted1 = ~ ~this.ntoh(encrypted32[wordIx + 1]);
|
|
encrypted2 = ~ ~this.ntoh(encrypted32[wordIx + 2]);
|
|
encrypted3 = ~ ~this.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] = this.ntoh(decrypted32[wordIx] ^ init0);
|
|
decrypted32[wordIx + 1] = this.ntoh(decrypted32[wordIx + 1] ^ init1);
|
|
decrypted32[wordIx + 2] = this.ntoh(decrypted32[wordIx + 2] ^ init2);
|
|
decrypted32[wordIx + 3] = this.ntoh(decrypted32[wordIx + 3] ^ init3);
|
|
|
|
// setup the IV for the next round
|
|
init0 = encrypted0;
|
|
init1 = encrypted1;
|
|
init2 = encrypted2;
|
|
init3 = encrypted3;
|
|
}
|
|
|
|
return decrypted;
|
|
}
|
|
}, {
|
|
key: 'localDecrypt',
|
|
value: function localDecrypt(encrypted, key, initVector, decrypted) {
|
|
var bytes = this.doDecrypt(encrypted, key, initVector);
|
|
decrypted.set(bytes, encrypted.byteOffset);
|
|
}
|
|
}, {
|
|
key: 'decrypt',
|
|
value: function decrypt(encrypted) {
|
|
var step = 4 * 8000,
|
|
|
|
//encrypted32 = new Int32Array(encrypted.buffer),
|
|
encrypted32 = new Int32Array(encrypted),
|
|
decrypted = new Uint8Array(encrypted.byteLength),
|
|
i = 0;
|
|
|
|
// split up the encryption job and do the individual chunks asynchronously
|
|
var key = this.key;
|
|
var initVector = this.iv;
|
|
this.localDecrypt(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
|
|
|
for (i = step; i < encrypted32.length; i += step) {
|
|
initVector = new Uint32Array([this.ntoh(encrypted32[i - 4]), this.ntoh(encrypted32[i - 3]), this.ntoh(encrypted32[i - 2]), this.ntoh(encrypted32[i - 1])]);
|
|
this.localDecrypt(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
|
}
|
|
|
|
return decrypted;
|
|
}
|
|
}]);
|
|
|
|
return AES128Decrypter;
|
|
}();
|
|
|
|
exports.default = AES128Decrypter;
|
|
|
|
},{"./aes":8}],10:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /*
|
|
* AES128 decryption.
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _aes128Decrypter = require('./aes128-decrypter');
|
|
|
|
var _aes128Decrypter2 = _interopRequireDefault(_aes128Decrypter);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var Decrypter = function () {
|
|
function Decrypter(hls) {
|
|
_classCallCheck(this, Decrypter);
|
|
|
|
this.hls = hls;
|
|
try {
|
|
var browserCrypto = window ? window.crypto : crypto;
|
|
this.subtle = browserCrypto.subtle || browserCrypto.webkitSubtle;
|
|
this.disableWebCrypto = !this.subtle;
|
|
} catch (e) {
|
|
this.disableWebCrypto = true;
|
|
}
|
|
}
|
|
|
|
_createClass(Decrypter, [{
|
|
key: 'destroy',
|
|
value: function destroy() {}
|
|
}, {
|
|
key: 'decrypt',
|
|
value: function decrypt(data, key, iv, callback) {
|
|
if (this.disableWebCrypto && this.hls.config.enableSoftwareAES) {
|
|
this.decryptBySoftware(data, key, iv, callback);
|
|
} else {
|
|
this.decryptByWebCrypto(data, key, iv, callback);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'decryptByWebCrypto',
|
|
value: function decryptByWebCrypto(data, key, iv, callback) {
|
|
var _this = this;
|
|
|
|
_logger.logger.log('decrypting by WebCrypto API');
|
|
|
|
this.subtle.importKey('raw', key, { name: 'AES-CBC', length: 128 }, false, ['decrypt']).then(function (importedKey) {
|
|
_this.subtle.decrypt({ name: 'AES-CBC', iv: iv.buffer }, importedKey, data).then(callback).catch(function (err) {
|
|
_this.onWebCryptoError(err, data, key, iv, callback);
|
|
});
|
|
}).catch(function (err) {
|
|
_this.onWebCryptoError(err, data, key, iv, callback);
|
|
});
|
|
}
|
|
}, {
|
|
key: 'decryptBySoftware',
|
|
value: function decryptBySoftware(data, key8, iv8, callback) {
|
|
_logger.logger.log('decrypting by JavaScript Implementation');
|
|
|
|
var view = new DataView(key8.buffer);
|
|
var key = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
|
|
|
|
view = new DataView(iv8.buffer);
|
|
var iv = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
|
|
|
|
var decrypter = new _aes128Decrypter2.default(key, iv);
|
|
callback(decrypter.decrypt(data).buffer);
|
|
}
|
|
}, {
|
|
key: 'onWebCryptoError',
|
|
value: function onWebCryptoError(err, data, key, iv, callback) {
|
|
if (this.hls.config.enableSoftwareAES) {
|
|
_logger.logger.log('disabling to use WebCrypto API');
|
|
this.disableWebCrypto = true;
|
|
this.decryptBySoftware(data, key, iv, callback);
|
|
} else {
|
|
_logger.logger.error('decrypting error : ' + err.message);
|
|
this.hls.trigger(Event.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_DECRYPT_ERROR, fatal: true, reason: err.message });
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return Decrypter;
|
|
}();
|
|
|
|
exports.default = Decrypter;
|
|
|
|
},{"../errors":19,"../utils/logger":34,"./aes128-decrypter":9}],11:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* AAC demuxer
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _adts = require('./adts');
|
|
|
|
var _adts2 = _interopRequireDefault(_adts);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _id = require('../demux/id3');
|
|
|
|
var _id2 = _interopRequireDefault(_id);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var AACDemuxer = function () {
|
|
function AACDemuxer(observer, remuxerClass) {
|
|
_classCallCheck(this, AACDemuxer);
|
|
|
|
this.observer = observer;
|
|
this.remuxerClass = remuxerClass;
|
|
this.remuxer = new this.remuxerClass(observer);
|
|
this._aacTrack = { container: 'audio/adts', type: 'audio', id: -1, sequenceNumber: 0, samples: [], len: 0 };
|
|
}
|
|
|
|
_createClass(AACDemuxer, [{
|
|
key: 'push',
|
|
|
|
// feed incoming data to the front of the parsing pipeline
|
|
value: function push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration) {
|
|
var track = this._aacTrack,
|
|
id3 = new _id2.default(data),
|
|
pts = 90 * id3.timeStamp,
|
|
config,
|
|
frameLength,
|
|
frameDuration,
|
|
frameIndex,
|
|
offset,
|
|
headerLength,
|
|
stamp,
|
|
len,
|
|
aacSample;
|
|
// look for ADTS header (0xFFFx)
|
|
for (offset = id3.length, len = data.length; offset < len - 1; offset++) {
|
|
if (data[offset] === 0xff && (data[offset + 1] & 0xf0) === 0xf0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!track.audiosamplerate) {
|
|
config = _adts2.default.getAudioConfig(this.observer, data, offset, audioCodec);
|
|
track.config = config.config;
|
|
track.audiosamplerate = config.samplerate;
|
|
track.channelCount = config.channelCount;
|
|
track.codec = config.codec;
|
|
track.timescale = config.samplerate;
|
|
track.duration = config.samplerate * duration;
|
|
_logger.logger.log('parsed codec:' + track.codec + ',rate:' + config.samplerate + ',nb channel:' + config.channelCount);
|
|
}
|
|
frameIndex = 0;
|
|
frameDuration = 1024 * 90000 / track.audiosamplerate;
|
|
while (offset + 5 < len) {
|
|
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
|
|
headerLength = !!(data[offset + 1] & 0x01) ? 7 : 9;
|
|
// retrieve frame size
|
|
frameLength = (data[offset + 3] & 0x03) << 11 | data[offset + 4] << 3 | (data[offset + 5] & 0xE0) >>> 5;
|
|
frameLength -= headerLength;
|
|
//stamp = pes.pts;
|
|
|
|
if (frameLength > 0 && offset + headerLength + frameLength <= len) {
|
|
stamp = pts + frameIndex * frameDuration;
|
|
//logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}/${(stamp/90).toFixed(0)}`);
|
|
aacSample = { unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp };
|
|
track.samples.push(aacSample);
|
|
track.len += frameLength;
|
|
offset += frameLength + headerLength;
|
|
frameIndex++;
|
|
// look for ADTS header (0xFFFx)
|
|
for (; offset < len - 1; offset++) {
|
|
if (data[offset] === 0xff && (data[offset + 1] & 0xf0) === 0xf0) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
this.remuxer.remux(this._aacTrack, { samples: [] }, { samples: [{ pts: pts, dts: pts, unit: id3.payload }] }, { samples: [] }, timeOffset);
|
|
}
|
|
}, {
|
|
key: 'destroy',
|
|
value: function destroy() {}
|
|
}], [{
|
|
key: 'probe',
|
|
value: function probe(data) {
|
|
// check if data contains ID3 timestamp and ADTS sync worc
|
|
var id3 = new _id2.default(data),
|
|
offset,
|
|
len;
|
|
if (id3.hasTimeStamp) {
|
|
// look for ADTS header (0xFFFx)
|
|
for (offset = id3.length, len = data.length; offset < len - 1; offset++) {
|
|
if (data[offset] === 0xff && (data[offset + 1] & 0xf0) === 0xf0) {
|
|
//logger.log('ADTS sync word found !');
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}]);
|
|
|
|
return AACDemuxer;
|
|
}();
|
|
|
|
exports.default = AACDemuxer;
|
|
|
|
},{"../demux/id3":17,"../utils/logger":34,"./adts":12}],12:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* ADTS parser helper
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var ADTS = function () {
|
|
function ADTS() {
|
|
_classCallCheck(this, ADTS);
|
|
}
|
|
|
|
_createClass(ADTS, null, [{
|
|
key: 'getAudioConfig',
|
|
value: function getAudioConfig(observer, data, offset, audioCodec) {
|
|
var adtsObjectType,
|
|
// :int
|
|
adtsSampleingIndex,
|
|
// :int
|
|
adtsExtensionSampleingIndex,
|
|
// :int
|
|
adtsChanelConfig,
|
|
// :int
|
|
config,
|
|
userAgent = navigator.userAgent.toLowerCase(),
|
|
adtsSampleingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
|
// byte 2
|
|
adtsObjectType = ((data[offset + 2] & 0xC0) >>> 6) + 1;
|
|
adtsSampleingIndex = (data[offset + 2] & 0x3C) >>> 2;
|
|
if (adtsSampleingIndex > adtsSampleingRates.length - 1) {
|
|
observer.trigger(Event.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: 'invalid ADTS sampling index:' + adtsSampleingIndex });
|
|
return;
|
|
}
|
|
adtsChanelConfig = (data[offset + 2] & 0x01) << 2;
|
|
// byte 3
|
|
adtsChanelConfig |= (data[offset + 3] & 0xC0) >>> 6;
|
|
_logger.logger.log('manifest codec:' + audioCodec + ',ADTS data:type:' + adtsObjectType + ',sampleingIndex:' + adtsSampleingIndex + '[' + adtsSampleingRates[adtsSampleingIndex] + 'Hz],channelConfig:' + adtsChanelConfig);
|
|
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
|
|
if (userAgent.indexOf('firefox') !== -1) {
|
|
if (adtsSampleingIndex >= 6) {
|
|
adtsObjectType = 5;
|
|
config = new Array(4);
|
|
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
|
// there is a factor 2 between frame sample rate and output sample rate
|
|
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
|
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
|
} else {
|
|
adtsObjectType = 2;
|
|
config = new Array(2);
|
|
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
|
}
|
|
// Android : always use AAC
|
|
} else if (userAgent.indexOf('android') !== -1) {
|
|
adtsObjectType = 2;
|
|
config = new Array(2);
|
|
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
|
} else {
|
|
/* for other browsers (chrome ...)
|
|
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
|
|
*/
|
|
adtsObjectType = 5;
|
|
config = new Array(4);
|
|
// if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz)
|
|
if (audioCodec && (audioCodec.indexOf('mp4a.40.29') !== -1 || audioCodec.indexOf('mp4a.40.5') !== -1) || !audioCodec && adtsSampleingIndex >= 6) {
|
|
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
|
// there is a factor 2 between frame sample rate and output sample rate
|
|
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
|
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
|
} else {
|
|
// if (manifest codec is AAC) AND (frequency less than 24kHz OR nb channel is 1) OR (manifest codec not specified and mono audio)
|
|
// Chrome fails to play back with AAC LC mono when initialized with HE-AAC. This is not a problem with stereo.
|
|
if (audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && (adtsSampleingIndex >= 6 || adtsChanelConfig === 1) || !audioCodec && adtsChanelConfig === 1) {
|
|
adtsObjectType = 2;
|
|
config = new Array(2);
|
|
}
|
|
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
|
}
|
|
}
|
|
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
|
|
ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig()
|
|
Audio Profile / Audio Object Type
|
|
0: Null
|
|
1: AAC Main
|
|
2: AAC LC (Low Complexity)
|
|
3: AAC SSR (Scalable Sample Rate)
|
|
4: AAC LTP (Long Term Prediction)
|
|
5: SBR (Spectral Band Replication)
|
|
6: AAC Scalable
|
|
sampling freq
|
|
0: 96000 Hz
|
|
1: 88200 Hz
|
|
2: 64000 Hz
|
|
3: 48000 Hz
|
|
4: 44100 Hz
|
|
5: 32000 Hz
|
|
6: 24000 Hz
|
|
7: 22050 Hz
|
|
8: 16000 Hz
|
|
9: 12000 Hz
|
|
10: 11025 Hz
|
|
11: 8000 Hz
|
|
12: 7350 Hz
|
|
13: Reserved
|
|
14: Reserved
|
|
15: frequency is written explictly
|
|
Channel Configurations
|
|
These are the channel configurations:
|
|
0: Defined in AOT Specifc Config
|
|
1: 1 channel: front-center
|
|
2: 2 channels: front-left, front-right
|
|
*/
|
|
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
|
|
config[0] = adtsObjectType << 3;
|
|
// samplingFrequencyIndex
|
|
config[0] |= (adtsSampleingIndex & 0x0E) >> 1;
|
|
config[1] |= (adtsSampleingIndex & 0x01) << 7;
|
|
// channelConfiguration
|
|
config[1] |= adtsChanelConfig << 3;
|
|
if (adtsObjectType === 5) {
|
|
// adtsExtensionSampleingIndex
|
|
config[1] |= (adtsExtensionSampleingIndex & 0x0E) >> 1;
|
|
config[2] = (adtsExtensionSampleingIndex & 0x01) << 7;
|
|
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
|
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
|
|
config[2] |= 2 << 2;
|
|
config[3] = 0;
|
|
}
|
|
return { config: config, samplerate: adtsSampleingRates[adtsSampleingIndex], channelCount: adtsChanelConfig, codec: 'mp4a.40.' + adtsObjectType };
|
|
}
|
|
}]);
|
|
|
|
return ADTS;
|
|
}();
|
|
|
|
exports.default = ADTS;
|
|
|
|
},{"../errors":19,"../utils/logger":34}],13:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /* inline demuxer.
|
|
* probe fragments and instantiate appropriate demuxer depending on content type (TSDemuxer, AACDemuxer, ...)
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
var _aacdemuxer = require('../demux/aacdemuxer');
|
|
|
|
var _aacdemuxer2 = _interopRequireDefault(_aacdemuxer);
|
|
|
|
var _tsdemuxer = require('../demux/tsdemuxer');
|
|
|
|
var _tsdemuxer2 = _interopRequireDefault(_tsdemuxer);
|
|
|
|
var _mp4Remuxer = require('../remux/mp4-remuxer');
|
|
|
|
var _mp4Remuxer2 = _interopRequireDefault(_mp4Remuxer);
|
|
|
|
var _passthroughRemuxer = require('../remux/passthrough-remuxer');
|
|
|
|
var _passthroughRemuxer2 = _interopRequireDefault(_passthroughRemuxer);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var DemuxerInline = function () {
|
|
function DemuxerInline(hls, typeSupported) {
|
|
_classCallCheck(this, DemuxerInline);
|
|
|
|
this.hls = hls;
|
|
this.typeSupported = typeSupported;
|
|
}
|
|
|
|
_createClass(DemuxerInline, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
var demuxer = this.demuxer;
|
|
if (demuxer) {
|
|
demuxer.destroy();
|
|
}
|
|
}
|
|
}, {
|
|
key: 'push',
|
|
value: function push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration) {
|
|
var demuxer = this.demuxer;
|
|
if (!demuxer) {
|
|
var hls = this.hls;
|
|
// probe for content type
|
|
if (_tsdemuxer2.default.probe(data)) {
|
|
if (this.typeSupported.mp2t === true) {
|
|
demuxer = new _tsdemuxer2.default(hls, _passthroughRemuxer2.default);
|
|
} else {
|
|
demuxer = new _tsdemuxer2.default(hls, _mp4Remuxer2.default);
|
|
}
|
|
} else if (_aacdemuxer2.default.probe(data)) {
|
|
demuxer = new _aacdemuxer2.default(hls, _mp4Remuxer2.default);
|
|
} else {
|
|
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: 'no demux matching with content found' });
|
|
return;
|
|
}
|
|
this.demuxer = demuxer;
|
|
}
|
|
demuxer.push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration);
|
|
}
|
|
}]);
|
|
|
|
return DemuxerInline;
|
|
}();
|
|
|
|
exports.default = DemuxerInline;
|
|
|
|
},{"../demux/aacdemuxer":11,"../demux/tsdemuxer":18,"../errors":19,"../events":21,"../remux/mp4-remuxer":29,"../remux/passthrough-remuxer":30}],14:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _demuxerInline = require('../demux/demuxer-inline');
|
|
|
|
var _demuxerInline2 = _interopRequireDefault(_demuxerInline);
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _events3 = require('events');
|
|
|
|
var _events4 = _interopRequireDefault(_events3);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
var DemuxerWorker = function DemuxerWorker(self) {
|
|
// observer setup
|
|
var observer = new _events4.default();
|
|
observer.trigger = function trigger(event) {
|
|
for (var _len = arguments.length, data = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
data[_key - 1] = arguments[_key];
|
|
}
|
|
|
|
observer.emit.apply(observer, [event, event].concat(data));
|
|
};
|
|
|
|
observer.off = function off(event) {
|
|
for (var _len2 = arguments.length, data = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
data[_key2 - 1] = arguments[_key2];
|
|
}
|
|
|
|
observer.removeListener.apply(observer, [event].concat(data));
|
|
};
|
|
self.addEventListener('message', function (ev) {
|
|
var data = ev.data;
|
|
//console.log('demuxer cmd:' + data.cmd);
|
|
switch (data.cmd) {
|
|
case 'init':
|
|
self.demuxer = new _demuxerInline2.default(observer, data.typeSupported);
|
|
break;
|
|
case 'demux':
|
|
self.demuxer.push(new Uint8Array(data.data), data.audioCodec, data.videoCodec, data.timeOffset, data.cc, data.level, data.sn, data.duration);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
|
|
// listen to events triggered by Demuxer
|
|
observer.on(_events2.default.FRAG_PARSING_INIT_SEGMENT, function (ev, data) {
|
|
self.postMessage({ event: ev, tracks: data.tracks, unique: data.unique });
|
|
});
|
|
|
|
observer.on(_events2.default.FRAG_PARSING_DATA, function (ev, data) {
|
|
var objData = { event: ev, type: data.type, startPTS: data.startPTS, endPTS: data.endPTS, startDTS: data.startDTS, endDTS: data.endDTS, data1: data.data1.buffer, data2: data.data2.buffer, nb: data.nb };
|
|
// pass data1/data2 as transferable object (no copy)
|
|
self.postMessage(objData, [objData.data1, objData.data2]);
|
|
});
|
|
|
|
observer.on(_events2.default.FRAG_PARSED, function (event) {
|
|
self.postMessage({ event: event });
|
|
});
|
|
|
|
observer.on(_events2.default.ERROR, function (event, data) {
|
|
self.postMessage({ event: event, data: data });
|
|
});
|
|
|
|
observer.on(_events2.default.FRAG_PARSING_METADATA, function (event, data) {
|
|
var objData = { event: event, samples: data.samples };
|
|
self.postMessage(objData);
|
|
});
|
|
|
|
observer.on(_events2.default.FRAG_PARSING_USERDATA, function (event, data) {
|
|
var objData = { event: event, samples: data.samples };
|
|
self.postMessage(objData);
|
|
});
|
|
}; /* demuxer web worker.
|
|
* - listen to worker message, and trigger DemuxerInline upon reception of Fragments.
|
|
* - provides MP4 Boxes back to main thread using [transferable objects](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast) in order to minimize message passing overhead.
|
|
*/
|
|
|
|
exports.default = DemuxerWorker;
|
|
|
|
},{"../demux/demuxer-inline":13,"../events":21,"events":1}],15:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _demuxerInline = require('../demux/demuxer-inline');
|
|
|
|
var _demuxerInline2 = _interopRequireDefault(_demuxerInline);
|
|
|
|
var _demuxerWorker = require('../demux/demuxer-worker');
|
|
|
|
var _demuxerWorker2 = _interopRequireDefault(_demuxerWorker);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _decrypter = require('../crypt/decrypter');
|
|
|
|
var _decrypter2 = _interopRequireDefault(_decrypter);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var Demuxer = function () {
|
|
function Demuxer(hls) {
|
|
_classCallCheck(this, Demuxer);
|
|
|
|
this.hls = hls;
|
|
var typeSupported = {
|
|
mp4: MediaSource.isTypeSupported('video/mp4'),
|
|
mp2t: hls.config.enableMP2TPassThrough && MediaSource.isTypeSupported('video/mp2t')
|
|
};
|
|
if (hls.config.enableWorker && typeof Worker !== 'undefined') {
|
|
_logger.logger.log('demuxing in webworker');
|
|
try {
|
|
var work = require('webworkify');
|
|
this.w = work(_demuxerWorker2.default);
|
|
this.onwmsg = this.onWorkerMessage.bind(this);
|
|
this.w.addEventListener('message', this.onwmsg);
|
|
this.w.postMessage({ cmd: 'init', typeSupported: typeSupported });
|
|
} catch (err) {
|
|
_logger.logger.error('error while initializing DemuxerWorker, fallback on DemuxerInline');
|
|
this.demuxer = new _demuxerInline2.default(hls, typeSupported);
|
|
}
|
|
} else {
|
|
this.demuxer = new _demuxerInline2.default(hls, typeSupported);
|
|
}
|
|
this.demuxInitialized = true;
|
|
}
|
|
|
|
_createClass(Demuxer, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
if (this.w) {
|
|
this.w.removeEventListener('message', this.onwmsg);
|
|
this.w.terminate();
|
|
this.w = null;
|
|
} else {
|
|
this.demuxer.destroy();
|
|
this.demuxer = null;
|
|
}
|
|
if (this.decrypter) {
|
|
this.decrypter.destroy();
|
|
this.decrypter = null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'pushDecrypted',
|
|
value: function pushDecrypted(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration) {
|
|
if (this.w) {
|
|
// post fragment payload as transferable objects (no copy)
|
|
this.w.postMessage({ cmd: 'demux', data: data, audioCodec: audioCodec, videoCodec: videoCodec, timeOffset: timeOffset, cc: cc, level: level, sn: sn, duration: duration }, [data]);
|
|
} else {
|
|
this.demuxer.push(new Uint8Array(data), audioCodec, videoCodec, timeOffset, cc, level, sn, duration);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'push',
|
|
value: function push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration, decryptdata) {
|
|
if (data.byteLength > 0 && decryptdata != null && decryptdata.key != null && decryptdata.method === 'AES-128') {
|
|
if (this.decrypter == null) {
|
|
this.decrypter = new _decrypter2.default(this.hls);
|
|
}
|
|
|
|
var localthis = this;
|
|
this.decrypter.decrypt(data, decryptdata.key, decryptdata.iv, function (decryptedData) {
|
|
localthis.pushDecrypted(decryptedData, audioCodec, videoCodec, timeOffset, cc, level, sn, duration);
|
|
});
|
|
} else {
|
|
this.pushDecrypted(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'onWorkerMessage',
|
|
value: function onWorkerMessage(ev) {
|
|
var data = ev.data;
|
|
//console.log('onWorkerMessage:' + data.event);
|
|
switch (data.event) {
|
|
case _events2.default.FRAG_PARSING_INIT_SEGMENT:
|
|
var obj = {};
|
|
obj.tracks = data.tracks;
|
|
obj.unique = data.unique;
|
|
this.hls.trigger(_events2.default.FRAG_PARSING_INIT_SEGMENT, obj);
|
|
break;
|
|
case _events2.default.FRAG_PARSING_DATA:
|
|
this.hls.trigger(_events2.default.FRAG_PARSING_DATA, {
|
|
data1: new Uint8Array(data.data1),
|
|
data2: new Uint8Array(data.data2),
|
|
startPTS: data.startPTS,
|
|
endPTS: data.endPTS,
|
|
startDTS: data.startDTS,
|
|
endDTS: data.endDTS,
|
|
type: data.type,
|
|
nb: data.nb
|
|
});
|
|
break;
|
|
case _events2.default.FRAG_PARSING_METADATA:
|
|
this.hls.trigger(_events2.default.FRAG_PARSING_METADATA, {
|
|
samples: data.samples
|
|
});
|
|
break;
|
|
case _events2.default.FRAG_PARSING_USERDATA:
|
|
this.hls.trigger(_events2.default.FRAG_PARSING_USERDATA, {
|
|
samples: data.samples
|
|
});
|
|
break;
|
|
default:
|
|
this.hls.trigger(data.event, data.data);
|
|
break;
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return Demuxer;
|
|
}();
|
|
|
|
exports.default = Demuxer;
|
|
|
|
},{"../crypt/decrypter":10,"../demux/demuxer-inline":13,"../demux/demuxer-worker":14,"../events":21,"../utils/logger":34,"webworkify":2}],16:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264.
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var ExpGolomb = function () {
|
|
function ExpGolomb(data) {
|
|
_classCallCheck(this, ExpGolomb);
|
|
|
|
this.data = data;
|
|
// the number of bytes left to examine in this.data
|
|
this.bytesAvailable = this.data.byteLength;
|
|
// the current word being examined
|
|
this.word = 0; // :uint
|
|
// the number of bits left to examine in the current word
|
|
this.bitsAvailable = 0; // :uint
|
|
}
|
|
|
|
// ():void
|
|
|
|
_createClass(ExpGolomb, [{
|
|
key: 'loadWord',
|
|
value: function loadWord() {
|
|
var position = this.data.byteLength - this.bytesAvailable,
|
|
workingBytes = new Uint8Array(4),
|
|
availableBytes = Math.min(4, this.bytesAvailable);
|
|
if (availableBytes === 0) {
|
|
throw new Error('no bytes available');
|
|
}
|
|
workingBytes.set(this.data.subarray(position, position + availableBytes));
|
|
this.word = new DataView(workingBytes.buffer).getUint32(0);
|
|
// track the amount of this.data that has been processed
|
|
this.bitsAvailable = availableBytes * 8;
|
|
this.bytesAvailable -= availableBytes;
|
|
}
|
|
|
|
// (count:int):void
|
|
|
|
}, {
|
|
key: 'skipBits',
|
|
value: function skipBits(count) {
|
|
var skipBytes; // :int
|
|
if (this.bitsAvailable > count) {
|
|
this.word <<= count;
|
|
this.bitsAvailable -= count;
|
|
} else {
|
|
count -= this.bitsAvailable;
|
|
skipBytes = count >> 3;
|
|
count -= skipBytes >> 3;
|
|
this.bytesAvailable -= skipBytes;
|
|
this.loadWord();
|
|
this.word <<= count;
|
|
this.bitsAvailable -= count;
|
|
}
|
|
}
|
|
|
|
// (size:int):uint
|
|
|
|
}, {
|
|
key: 'readBits',
|
|
value: function readBits(size) {
|
|
var bits = Math.min(this.bitsAvailable, size),
|
|
// :uint
|
|
valu = this.word >>> 32 - bits; // :uint
|
|
if (size > 32) {
|
|
_logger.logger.error('Cannot read more than 32 bits at a time');
|
|
}
|
|
this.bitsAvailable -= bits;
|
|
if (this.bitsAvailable > 0) {
|
|
this.word <<= bits;
|
|
} else if (this.bytesAvailable > 0) {
|
|
this.loadWord();
|
|
}
|
|
bits = size - bits;
|
|
if (bits > 0) {
|
|
return valu << bits | this.readBits(bits);
|
|
} else {
|
|
return valu;
|
|
}
|
|
}
|
|
|
|
// ():uint
|
|
|
|
}, {
|
|
key: 'skipLZ',
|
|
value: function skipLZ() {
|
|
var leadingZeroCount; // :uint
|
|
for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) {
|
|
if (0 !== (this.word & 0x80000000 >>> leadingZeroCount)) {
|
|
// the first bit of working word is 1
|
|
this.word <<= leadingZeroCount;
|
|
this.bitsAvailable -= leadingZeroCount;
|
|
return leadingZeroCount;
|
|
}
|
|
}
|
|
// we exhausted word and still have not found a 1
|
|
this.loadWord();
|
|
return leadingZeroCount + this.skipLZ();
|
|
}
|
|
|
|
// ():void
|
|
|
|
}, {
|
|
key: 'skipUEG',
|
|
value: function skipUEG() {
|
|
this.skipBits(1 + this.skipLZ());
|
|
}
|
|
|
|
// ():void
|
|
|
|
}, {
|
|
key: 'skipEG',
|
|
value: function skipEG() {
|
|
this.skipBits(1 + this.skipLZ());
|
|
}
|
|
|
|
// ():uint
|
|
|
|
}, {
|
|
key: 'readUEG',
|
|
value: function readUEG() {
|
|
var clz = this.skipLZ(); // :uint
|
|
return this.readBits(clz + 1) - 1;
|
|
}
|
|
|
|
// ():int
|
|
|
|
}, {
|
|
key: 'readEG',
|
|
value: function readEG() {
|
|
var valu = this.readUEG(); // :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
|
|
} else {
|
|
return -1 * (valu >>> 1); // divide by two then make it negative
|
|
}
|
|
}
|
|
|
|
// Some convenience functions
|
|
// :Boolean
|
|
|
|
}, {
|
|
key: 'readBoolean',
|
|
value: function readBoolean() {
|
|
return 1 === this.readBits(1);
|
|
}
|
|
|
|
// ():int
|
|
|
|
}, {
|
|
key: 'readUByte',
|
|
value: function readUByte() {
|
|
return this.readBits(8);
|
|
}
|
|
|
|
// ():int
|
|
|
|
}, {
|
|
key: 'readUShort',
|
|
value: function readUShort() {
|
|
return this.readBits(16);
|
|
}
|
|
// ():int
|
|
|
|
}, {
|
|
key: 'readUInt',
|
|
value: function readUInt() {
|
|
return this.readBits(32);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
|
|
*/
|
|
|
|
}, {
|
|
key: 'skipScalingList',
|
|
value: function skipScalingList(count) {
|
|
var lastScale = 8,
|
|
nextScale = 8,
|
|
j,
|
|
deltaScale;
|
|
for (j = 0; j < count; j++) {
|
|
if (nextScale !== 0) {
|
|
deltaScale = this.readEG();
|
|
nextScale = (lastScale + deltaScale + 256) % 256;
|
|
}
|
|
lastScale = nextScale === 0 ? lastScale : nextScale;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
|
|
}, {
|
|
key: 'readSPS',
|
|
value: function readSPS() {
|
|
var frameCropLeftOffset = 0,
|
|
frameCropRightOffset = 0,
|
|
frameCropTopOffset = 0,
|
|
frameCropBottomOffset = 0,
|
|
sarScale = 1,
|
|
profileIdc,
|
|
profileCompat,
|
|
levelIdc,
|
|
numRefFramesInPicOrderCntCycle,
|
|
picWidthInMbsMinus1,
|
|
picHeightInMapUnitsMinus1,
|
|
frameMbsOnlyFlag,
|
|
scalingListCount,
|
|
i;
|
|
this.readUByte();
|
|
profileIdc = this.readUByte(); // profile_idc
|
|
profileCompat = this.readBits(5); // constraint_set[0-4]_flag, u(5)
|
|
this.skipBits(3); // reserved_zero_3bits u(3),
|
|
levelIdc = this.readUByte(); //level_idc u(8)
|
|
this.skipUEG(); // seq_parameter_set_id
|
|
// some profiles have more optional data we don't need
|
|
if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) {
|
|
var chromaFormatIdc = this.readUEG();
|
|
if (chromaFormatIdc === 3) {
|
|
this.skipBits(1); // separate_colour_plane_flag
|
|
}
|
|
this.skipUEG(); // bit_depth_luma_minus8
|
|
this.skipUEG(); // bit_depth_chroma_minus8
|
|
this.skipBits(1); // qpprime_y_zero_transform_bypass_flag
|
|
if (this.readBoolean()) {
|
|
// seq_scaling_matrix_present_flag
|
|
scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
|
|
for (i = 0; i < scalingListCount; i++) {
|
|
if (this.readBoolean()) {
|
|
// seq_scaling_list_present_flag[ i ]
|
|
if (i < 6) {
|
|
this.skipScalingList(16);
|
|
} else {
|
|
this.skipScalingList(64);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.skipUEG(); // log2_max_frame_num_minus4
|
|
var picOrderCntType = this.readUEG();
|
|
if (picOrderCntType === 0) {
|
|
this.readUEG(); //log2_max_pic_order_cnt_lsb_minus4
|
|
} else if (picOrderCntType === 1) {
|
|
this.skipBits(1); // delta_pic_order_always_zero_flag
|
|
this.skipEG(); // offset_for_non_ref_pic
|
|
this.skipEG(); // offset_for_top_to_bottom_field
|
|
numRefFramesInPicOrderCntCycle = this.readUEG();
|
|
for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
|
this.skipEG(); // offset_for_ref_frame[ i ]
|
|
}
|
|
}
|
|
this.skipUEG(); // max_num_ref_frames
|
|
this.skipBits(1); // gaps_in_frame_num_value_allowed_flag
|
|
picWidthInMbsMinus1 = this.readUEG();
|
|
picHeightInMapUnitsMinus1 = this.readUEG();
|
|
frameMbsOnlyFlag = this.readBits(1);
|
|
if (frameMbsOnlyFlag === 0) {
|
|
this.skipBits(1); // mb_adaptive_frame_field_flag
|
|
}
|
|
this.skipBits(1); // direct_8x8_inference_flag
|
|
if (this.readBoolean()) {
|
|
// frame_cropping_flag
|
|
frameCropLeftOffset = this.readUEG();
|
|
frameCropRightOffset = this.readUEG();
|
|
frameCropTopOffset = this.readUEG();
|
|
frameCropBottomOffset = this.readUEG();
|
|
}
|
|
if (this.readBoolean()) {
|
|
// vui_parameters_present_flag
|
|
if (this.readBoolean()) {
|
|
// aspect_ratio_info_present_flag
|
|
var sarRatio = undefined;
|
|
var aspectRatioIdc = this.readUByte();
|
|
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 = [this.readUByte() << 8 | this.readUByte(), this.readUByte() << 8 | this.readUByte()];
|
|
break;
|
|
}
|
|
}
|
|
if (sarRatio) {
|
|
sarScale = sarRatio[0] / sarRatio[1];
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale),
|
|
height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)
|
|
};
|
|
}
|
|
}, {
|
|
key: 'readSliceType',
|
|
value: function readSliceType() {
|
|
// skip NALu type
|
|
this.readUByte();
|
|
// discard first_mb_in_slice
|
|
this.readUEG();
|
|
// return slice_type
|
|
return this.readUEG();
|
|
}
|
|
}]);
|
|
|
|
return ExpGolomb;
|
|
}();
|
|
|
|
exports.default = ExpGolomb;
|
|
|
|
},{"../utils/logger":34}],17:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* ID3 parser
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
//import Hex from '../utils/hex';
|
|
|
|
var ID3 = function () {
|
|
function ID3(data) {
|
|
_classCallCheck(this, ID3);
|
|
|
|
this._hasTimeStamp = false;
|
|
var offset = 0,
|
|
byte1,
|
|
byte2,
|
|
byte3,
|
|
byte4,
|
|
tagSize,
|
|
endPos,
|
|
header,
|
|
len;
|
|
do {
|
|
header = this.readUTF(data, offset, 3);
|
|
offset += 3;
|
|
// first check for ID3 header
|
|
if (header === 'ID3') {
|
|
// skip 24 bits
|
|
offset += 3;
|
|
// retrieve tag(s) length
|
|
byte1 = data[offset++] & 0x7f;
|
|
byte2 = data[offset++] & 0x7f;
|
|
byte3 = data[offset++] & 0x7f;
|
|
byte4 = data[offset++] & 0x7f;
|
|
tagSize = (byte1 << 21) + (byte2 << 14) + (byte3 << 7) + byte4;
|
|
endPos = offset + tagSize;
|
|
//logger.log(`ID3 tag found, size/end: ${tagSize}/${endPos}`);
|
|
|
|
// read ID3 tags
|
|
this._parseID3Frames(data, offset, endPos);
|
|
offset = endPos;
|
|
} else if (header === '3DI') {
|
|
// http://id3.org/id3v2.4.0-structure chapter 3.4. ID3v2 footer
|
|
offset += 7;
|
|
_logger.logger.log('3DI footer found, end: ' + offset);
|
|
} else {
|
|
offset -= 3;
|
|
len = offset;
|
|
if (len) {
|
|
//logger.log(`ID3 len: ${len}`);
|
|
if (!this.hasTimeStamp) {
|
|
_logger.logger.warn('ID3 tag found, but no timestamp');
|
|
}
|
|
this._length = len;
|
|
this._payload = data.subarray(0, len);
|
|
}
|
|
return;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
_createClass(ID3, [{
|
|
key: 'readUTF',
|
|
value: function readUTF(data, start, len) {
|
|
|
|
var result = '',
|
|
offset = start,
|
|
end = start + len;
|
|
do {
|
|
result += String.fromCharCode(data[offset++]);
|
|
} while (offset < end);
|
|
return result;
|
|
}
|
|
}, {
|
|
key: '_parseID3Frames',
|
|
value: function _parseID3Frames(data, offset, endPos) {
|
|
var tagId, tagLen, tagStart, tagFlags, timestamp;
|
|
while (offset + 8 <= endPos) {
|
|
tagId = this.readUTF(data, offset, 4);
|
|
offset += 4;
|
|
|
|
tagLen = data[offset++] << 24 + data[offset++] << 16 + data[offset++] << 8 + data[offset++];
|
|
|
|
tagFlags = data[offset++] << 8 + data[offset++];
|
|
|
|
tagStart = offset;
|
|
//logger.log("ID3 tag id:" + tagId);
|
|
switch (tagId) {
|
|
case 'PRIV':
|
|
//logger.log('parse frame:' + Hex.hexDump(data.subarray(offset,endPos)));
|
|
// owner should be "com.apple.streaming.transportStreamTimestamp"
|
|
if (this.readUTF(data, offset, 44) === 'com.apple.streaming.transportStreamTimestamp') {
|
|
offset += 44;
|
|
// smelling even better ! we found the right descriptor
|
|
// skip null character (string end) + 3 first bytes
|
|
offset += 4;
|
|
|
|
// timestamp is 33 bit expressed as a big-endian eight-octet number, with the upper 31 bits set to zero.
|
|
var pts33Bit = data[offset++] & 0x1;
|
|
this._hasTimeStamp = true;
|
|
|
|
timestamp = ((data[offset++] << 23) + (data[offset++] << 15) + (data[offset++] << 7) + data[offset++]) / 45;
|
|
|
|
if (pts33Bit) {
|
|
timestamp += 47721858.84; // 2^32 / 90
|
|
}
|
|
timestamp = Math.round(timestamp);
|
|
_logger.logger.trace('ID3 timestamp found: ' + timestamp);
|
|
this._timeStamp = timestamp;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'hasTimeStamp',
|
|
get: function get() {
|
|
return this._hasTimeStamp;
|
|
}
|
|
}, {
|
|
key: 'timeStamp',
|
|
get: function get() {
|
|
return this._timeStamp;
|
|
}
|
|
}, {
|
|
key: 'length',
|
|
get: function get() {
|
|
return this._length;
|
|
}
|
|
}, {
|
|
key: 'payload',
|
|
get: function get() {
|
|
return this._payload;
|
|
}
|
|
}]);
|
|
|
|
return ID3;
|
|
}();
|
|
|
|
exports.default = ID3;
|
|
|
|
},{"../utils/logger":34}],18:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* highly optimized TS demuxer:
|
|
* parse PAT, PMT
|
|
* extract PES packet from audio and video PIDs
|
|
* extract AVC/H264 NAL units and AAC/ADTS samples from PES packet
|
|
* trigger the remuxer upon parsing completion
|
|
* it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
|
|
* it also controls the remuxing process :
|
|
* upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
|
|
*/
|
|
|
|
// import Hex from '../utils/hex';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _adts = require('./adts');
|
|
|
|
var _adts2 = _interopRequireDefault(_adts);
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _expGolomb = require('./exp-golomb');
|
|
|
|
var _expGolomb2 = _interopRequireDefault(_expGolomb);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var TSDemuxer = function () {
|
|
function TSDemuxer(observer, remuxerClass) {
|
|
_classCallCheck(this, TSDemuxer);
|
|
|
|
this.observer = observer;
|
|
this.remuxerClass = remuxerClass;
|
|
this.lastCC = 0;
|
|
this.remuxer = new this.remuxerClass(observer);
|
|
}
|
|
|
|
_createClass(TSDemuxer, [{
|
|
key: 'switchLevel',
|
|
value: function switchLevel() {
|
|
this.pmtParsed = false;
|
|
this._pmtId = -1;
|
|
this.lastAacPTS = null;
|
|
this.aacOverFlow = null;
|
|
this._avcTrack = { container: 'video/mp2t', type: 'video', id: -1, sequenceNumber: 0, samples: [], len: 0, nbNalu: 0 };
|
|
this._aacTrack = { container: 'video/mp2t', type: 'audio', id: -1, sequenceNumber: 0, samples: [], len: 0 };
|
|
this._id3Track = { type: 'id3', id: -1, sequenceNumber: 0, samples: [], len: 0 };
|
|
this._txtTrack = { type: 'text', id: -1, sequenceNumber: 0, samples: [], len: 0 };
|
|
this.remuxer.switchLevel();
|
|
}
|
|
}, {
|
|
key: 'insertDiscontinuity',
|
|
value: function insertDiscontinuity() {
|
|
this.switchLevel();
|
|
this.remuxer.insertDiscontinuity();
|
|
}
|
|
|
|
// feed incoming data to the front of the parsing pipeline
|
|
|
|
}, {
|
|
key: 'push',
|
|
value: function push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration) {
|
|
var avcData,
|
|
aacData,
|
|
id3Data,
|
|
start,
|
|
len = data.length,
|
|
stt,
|
|
pid,
|
|
atf,
|
|
offset,
|
|
codecsOnly = this.remuxer.passthrough;
|
|
|
|
this.audioCodec = audioCodec;
|
|
this.videoCodec = videoCodec;
|
|
this.timeOffset = timeOffset;
|
|
this._duration = duration;
|
|
this.contiguous = false;
|
|
if (cc !== this.lastCC) {
|
|
_logger.logger.log('discontinuity detected');
|
|
this.insertDiscontinuity();
|
|
this.lastCC = cc;
|
|
} else if (level !== this.lastLevel) {
|
|
_logger.logger.log('level switch detected');
|
|
this.switchLevel();
|
|
this.lastLevel = level;
|
|
} else if (sn === this.lastSN + 1) {
|
|
this.contiguous = true;
|
|
}
|
|
this.lastSN = sn;
|
|
|
|
if (!this.contiguous) {
|
|
// flush any partial content
|
|
this.aacOverFlow = null;
|
|
}
|
|
|
|
var pmtParsed = this.pmtParsed,
|
|
avcId = this._avcTrack.id,
|
|
aacId = this._aacTrack.id,
|
|
id3Id = this._id3Track.id;
|
|
|
|
// don't parse last TS packet if incomplete
|
|
len -= len % 188;
|
|
// loop through TS packets
|
|
for (start = 0; start < len; start += 188) {
|
|
if (data[start] === 0x47) {
|
|
stt = !!(data[start + 1] & 0x40);
|
|
// pid is a 13-bit field starting at the last bit of TS[1]
|
|
pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
|
|
atf = (data[start + 3] & 0x30) >> 4;
|
|
// if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
|
|
if (atf > 1) {
|
|
offset = start + 5 + data[start + 4];
|
|
// continue if there is only adaptation field
|
|
if (offset === start + 188) {
|
|
continue;
|
|
}
|
|
} else {
|
|
offset = start + 4;
|
|
}
|
|
if (pmtParsed) {
|
|
if (pid === avcId) {
|
|
if (stt) {
|
|
if (avcData) {
|
|
this._parseAVCPES(this._parsePES(avcData));
|
|
if (codecsOnly) {
|
|
// if we have video codec info AND
|
|
// if audio PID is undefined OR if we have audio codec info,
|
|
// we have all codec info !
|
|
if (this._avcTrack.codec && (aacId === -1 || this._aacTrack.codec)) {
|
|
this.remux(data);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
avcData = { data: [], size: 0 };
|
|
}
|
|
if (avcData) {
|
|
avcData.data.push(data.subarray(offset, start + 188));
|
|
avcData.size += start + 188 - offset;
|
|
}
|
|
} else if (pid === aacId) {
|
|
if (stt) {
|
|
if (aacData) {
|
|
this._parseAACPES(this._parsePES(aacData));
|
|
if (codecsOnly) {
|
|
// here we now that we have audio codec info
|
|
// if video PID is undefined OR if we have video codec info,
|
|
// we have all codec infos !
|
|
if (this._aacTrack.codec && (avcId === -1 || this._avcTrack.codec)) {
|
|
this.remux(data);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
aacData = { data: [], size: 0 };
|
|
}
|
|
if (aacData) {
|
|
aacData.data.push(data.subarray(offset, start + 188));
|
|
aacData.size += start + 188 - offset;
|
|
}
|
|
} else if (pid === id3Id) {
|
|
if (stt) {
|
|
if (id3Data) {
|
|
this._parseID3PES(this._parsePES(id3Data));
|
|
}
|
|
id3Data = { data: [], size: 0 };
|
|
}
|
|
if (id3Data) {
|
|
id3Data.data.push(data.subarray(offset, start + 188));
|
|
id3Data.size += start + 188 - offset;
|
|
}
|
|
}
|
|
} else {
|
|
if (stt) {
|
|
offset += data[offset] + 1;
|
|
}
|
|
if (pid === 0) {
|
|
this._parsePAT(data, offset);
|
|
} else if (pid === this._pmtId) {
|
|
this._parsePMT(data, offset);
|
|
pmtParsed = this.pmtParsed = true;
|
|
avcId = this._avcTrack.id;
|
|
aacId = this._aacTrack.id;
|
|
id3Id = this._id3Track.id;
|
|
}
|
|
}
|
|
} else {
|
|
this.observer.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: false, reason: 'TS packet did not start with 0x47' });
|
|
}
|
|
}
|
|
// parse last PES packet
|
|
if (avcData) {
|
|
this._parseAVCPES(this._parsePES(avcData));
|
|
}
|
|
if (aacData) {
|
|
this._parseAACPES(this._parsePES(aacData));
|
|
}
|
|
if (id3Data) {
|
|
this._parseID3PES(this._parsePES(id3Data));
|
|
}
|
|
this.remux(null);
|
|
}
|
|
}, {
|
|
key: 'remux',
|
|
value: function remux(data) {
|
|
this.remuxer.remux(this._aacTrack, this._avcTrack, this._id3Track, this._txtTrack, this.timeOffset, this.contiguous, data);
|
|
}
|
|
}, {
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
this.switchLevel();
|
|
this._initPTS = this._initDTS = undefined;
|
|
this._duration = 0;
|
|
}
|
|
}, {
|
|
key: '_parsePAT',
|
|
value: function _parsePAT(data, offset) {
|
|
// skip the PSI header and parse the first PMT entry
|
|
this._pmtId = (data[offset + 10] & 0x1F) << 8 | data[offset + 11];
|
|
//logger.log('PMT PID:' + this._pmtId);
|
|
}
|
|
}, {
|
|
key: '_parsePMT',
|
|
value: function _parsePMT(data, offset) {
|
|
var sectionLength, tableEnd, programInfoLength, pid;
|
|
sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
|
|
tableEnd = offset + 3 + sectionLength - 4;
|
|
// to determine where the table is, we have to figure out how
|
|
// long the program info descriptors are
|
|
programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
|
|
// advance the offset to the first entry in the mapping table
|
|
offset += 12 + programInfoLength;
|
|
while (offset < tableEnd) {
|
|
pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
|
|
switch (data[offset]) {
|
|
// ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
|
|
case 0x0f:
|
|
//logger.log('AAC PID:' + pid);
|
|
this._aacTrack.id = pid;
|
|
break;
|
|
// Packetized metadata (ID3)
|
|
case 0x15:
|
|
//logger.log('ID3 PID:' + pid);
|
|
this._id3Track.id = pid;
|
|
break;
|
|
// ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
|
|
case 0x1b:
|
|
//logger.log('AVC PID:' + pid);
|
|
this._avcTrack.id = pid;
|
|
break;
|
|
default:
|
|
_logger.logger.log('unkown stream type:' + data[offset]);
|
|
break;
|
|
}
|
|
// move to the next table entry
|
|
// skip past the elementary stream descriptors, if present
|
|
offset += ((data[offset + 3] & 0x0F) << 8 | data[offset + 4]) + 5;
|
|
}
|
|
}
|
|
}, {
|
|
key: '_parsePES',
|
|
value: function _parsePES(stream) {
|
|
var i = 0,
|
|
frag,
|
|
pesFlags,
|
|
pesPrefix,
|
|
pesLen,
|
|
pesHdrLen,
|
|
pesData,
|
|
pesPts,
|
|
pesDts,
|
|
payloadStartOffset,
|
|
data = stream.data;
|
|
//retrieve PTS/DTS from first fragment
|
|
frag = data[0];
|
|
pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2];
|
|
if (pesPrefix === 1) {
|
|
pesLen = (frag[4] << 8) + frag[5];
|
|
pesFlags = frag[7];
|
|
if (pesFlags & 0xC0) {
|
|
/* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
|
as PTS / DTS is 33 bit we cannot use bitwise operator in JS,
|
|
as Bitwise operators treat their operands as a sequence of 32 bits */
|
|
pesPts = (frag[9] & 0x0E) * 536870912 + // 1 << 29
|
|
(frag[10] & 0xFF) * 4194304 + // 1 << 22
|
|
(frag[11] & 0xFE) * 16384 + // 1 << 14
|
|
(frag[12] & 0xFF) * 128 + // 1 << 7
|
|
(frag[13] & 0xFE) / 2;
|
|
// check if greater than 2^32 -1
|
|
if (pesPts > 4294967295) {
|
|
// decrement 2^33
|
|
pesPts -= 8589934592;
|
|
}
|
|
if (pesFlags & 0x40) {
|
|
pesDts = (frag[14] & 0x0E) * 536870912 + // 1 << 29
|
|
(frag[15] & 0xFF) * 4194304 + // 1 << 22
|
|
(frag[16] & 0xFE) * 16384 + // 1 << 14
|
|
(frag[17] & 0xFF) * 128 + // 1 << 7
|
|
(frag[18] & 0xFE) / 2;
|
|
// check if greater than 2^32 -1
|
|
if (pesDts > 4294967295) {
|
|
// decrement 2^33
|
|
pesDts -= 8589934592;
|
|
}
|
|
} else {
|
|
pesDts = pesPts;
|
|
}
|
|
}
|
|
pesHdrLen = frag[8];
|
|
payloadStartOffset = pesHdrLen + 9;
|
|
|
|
stream.size -= payloadStartOffset;
|
|
//reassemble PES packet
|
|
pesData = new Uint8Array(stream.size);
|
|
while (data.length) {
|
|
frag = data.shift();
|
|
var len = frag.byteLength;
|
|
if (payloadStartOffset) {
|
|
if (payloadStartOffset > len) {
|
|
// trim full frag if PES header bigger than frag
|
|
payloadStartOffset -= len;
|
|
continue;
|
|
} else {
|
|
// trim partial frag if PES header smaller than frag
|
|
frag = frag.subarray(payloadStartOffset);
|
|
len -= payloadStartOffset;
|
|
payloadStartOffset = 0;
|
|
}
|
|
}
|
|
pesData.set(frag, i);
|
|
i += len;
|
|
}
|
|
return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}, {
|
|
key: '_parseAVCPES',
|
|
value: function _parseAVCPES(pes) {
|
|
var _this = this;
|
|
|
|
var track = this._avcTrack,
|
|
samples = track.samples,
|
|
units = this._parseAVCNALu(pes.data),
|
|
units2 = [],
|
|
debug = false,
|
|
key = false,
|
|
length = 0,
|
|
expGolombDecoder,
|
|
avcSample,
|
|
push,
|
|
i;
|
|
// no NALu found
|
|
if (units.length === 0 && samples.length > 0) {
|
|
// append pes.data to previous NAL unit
|
|
var lastavcSample = samples[samples.length - 1];
|
|
var lastUnit = lastavcSample.units.units[lastavcSample.units.units.length - 1];
|
|
var tmp = new Uint8Array(lastUnit.data.byteLength + pes.data.byteLength);
|
|
tmp.set(lastUnit.data, 0);
|
|
tmp.set(pes.data, lastUnit.data.byteLength);
|
|
lastUnit.data = tmp;
|
|
lastavcSample.units.length += pes.data.byteLength;
|
|
track.len += pes.data.byteLength;
|
|
}
|
|
//free pes.data to save up some memory
|
|
pes.data = null;
|
|
var debugString = '';
|
|
|
|
units.forEach(function (unit) {
|
|
switch (unit.type) {
|
|
//NDR
|
|
case 1:
|
|
push = true;
|
|
if (debug) {
|
|
debugString += 'NDR ';
|
|
}
|
|
break;
|
|
//IDR
|
|
case 5:
|
|
push = true;
|
|
if (debug) {
|
|
debugString += 'IDR ';
|
|
}
|
|
key = true;
|
|
break;
|
|
//SEI
|
|
case 6:
|
|
push = true;
|
|
if (debug) {
|
|
debugString += 'SEI ';
|
|
}
|
|
expGolombDecoder = new _expGolomb2.default(unit.data);
|
|
|
|
// skip frameType
|
|
expGolombDecoder.readUByte();
|
|
|
|
var payloadType = expGolombDecoder.readUByte();
|
|
|
|
// TODO: there can be more than one payload in an SEI packet...
|
|
// TODO: need to read type and size in a while loop to get them all
|
|
if (payloadType === 4) {
|
|
var payloadSize = 0;
|
|
|
|
do {
|
|
payloadSize = expGolombDecoder.readUByte();
|
|
} while (payloadSize === 255);
|
|
|
|
var countryCode = expGolombDecoder.readUByte();
|
|
|
|
if (countryCode === 181) {
|
|
var providerCode = expGolombDecoder.readUShort();
|
|
|
|
if (providerCode === 49) {
|
|
var userStructure = expGolombDecoder.readUInt();
|
|
|
|
if (userStructure === 0x47413934) {
|
|
var userDataType = expGolombDecoder.readUByte();
|
|
|
|
// Raw CEA-608 bytes wrapped in CEA-708 packet
|
|
if (userDataType === 3) {
|
|
var firstByte = expGolombDecoder.readUByte();
|
|
var secondByte = expGolombDecoder.readUByte();
|
|
|
|
var totalCCs = 31 & firstByte;
|
|
var byteArray = [firstByte, secondByte];
|
|
|
|
for (i = 0; i < totalCCs; i++) {
|
|
// 3 bytes per CC
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
}
|
|
|
|
_this._txtTrack.samples.push({ type: 3, pts: pes.pts, bytes: byteArray });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
//SPS
|
|
case 7:
|
|
push = true;
|
|
if (debug) {
|
|
debugString += 'SPS ';
|
|
}
|
|
if (!track.sps) {
|
|
expGolombDecoder = new _expGolomb2.default(unit.data);
|
|
var config = expGolombDecoder.readSPS();
|
|
track.width = config.width;
|
|
track.height = config.height;
|
|
track.sps = [unit.data];
|
|
track.timescale = _this.remuxer.timescale;
|
|
track.duration = _this.remuxer.timescale * _this._duration;
|
|
var codecarray = unit.data.subarray(1, 4);
|
|
var codecstring = 'avc1.';
|
|
for (i = 0; i < 3; i++) {
|
|
var h = codecarray[i].toString(16);
|
|
if (h.length < 2) {
|
|
h = '0' + h;
|
|
}
|
|
codecstring += h;
|
|
}
|
|
track.codec = codecstring;
|
|
}
|
|
break;
|
|
//PPS
|
|
case 8:
|
|
push = true;
|
|
if (debug) {
|
|
debugString += 'PPS ';
|
|
}
|
|
if (!track.pps) {
|
|
track.pps = [unit.data];
|
|
}
|
|
break;
|
|
case 9:
|
|
push = false;
|
|
if (debug) {
|
|
debugString += 'AUD ';
|
|
}
|
|
break;
|
|
default:
|
|
push = false;
|
|
debugString += 'unknown NAL ' + unit.type + ' ';
|
|
break;
|
|
}
|
|
if (push) {
|
|
units2.push(unit);
|
|
length += unit.data.byteLength;
|
|
}
|
|
});
|
|
if (debug || debugString.length) {
|
|
_logger.logger.log(debugString);
|
|
}
|
|
//build sample from PES
|
|
// Annex B to MP4 conversion to be done
|
|
if (units2.length) {
|
|
// only push AVC sample if keyframe already found. browsers expect a keyframe at first to start decoding
|
|
if (key === true || track.sps) {
|
|
avcSample = { units: { units: units2, length: length }, pts: pes.pts, dts: pes.dts, key: key };
|
|
samples.push(avcSample);
|
|
track.len += length;
|
|
track.nbNalu += units2.length;
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: '_parseAVCNALu',
|
|
value: function _parseAVCNALu(array) {
|
|
var i = 0,
|
|
len = array.byteLength,
|
|
value,
|
|
overflow,
|
|
state = 0;
|
|
var units = [],
|
|
unit,
|
|
unitType,
|
|
lastUnitStart,
|
|
lastUnitType;
|
|
//logger.log('PES:' + Hex.hexDump(array));
|
|
while (i < len) {
|
|
value = array[i++];
|
|
// finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01)
|
|
switch (state) {
|
|
case 0:
|
|
if (value === 0) {
|
|
state = 1;
|
|
}
|
|
break;
|
|
case 1:
|
|
if (value === 0) {
|
|
state = 2;
|
|
} else {
|
|
state = 0;
|
|
}
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
if (value === 0) {
|
|
state = 3;
|
|
} else if (value === 1 && i < len) {
|
|
unitType = array[i] & 0x1f;
|
|
//logger.log('find NALU @ offset:' + i + ',type:' + unitType);
|
|
if (lastUnitStart) {
|
|
unit = { data: array.subarray(lastUnitStart, i - state - 1), type: lastUnitType };
|
|
//logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
|
|
units.push(unit);
|
|
} else {
|
|
// If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
|
|
overflow = i - state - 1;
|
|
if (overflow) {
|
|
var track = this._avcTrack,
|
|
samples = track.samples;
|
|
//logger.log('first NALU found with overflow:' + overflow);
|
|
if (samples.length) {
|
|
var lastavcSample = samples[samples.length - 1],
|
|
lastUnits = lastavcSample.units.units,
|
|
lastUnit = lastUnits[lastUnits.length - 1],
|
|
tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
|
|
tmp.set(lastUnit.data, 0);
|
|
tmp.set(array.subarray(0, overflow), lastUnit.data.byteLength);
|
|
lastUnit.data = tmp;
|
|
lastavcSample.units.length += overflow;
|
|
track.len += overflow;
|
|
}
|
|
}
|
|
}
|
|
lastUnitStart = i;
|
|
lastUnitType = unitType;
|
|
state = 0;
|
|
} else {
|
|
state = 0;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (lastUnitStart) {
|
|
unit = { data: array.subarray(lastUnitStart, len), type: lastUnitType };
|
|
units.push(unit);
|
|
//logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
|
|
}
|
|
return units;
|
|
}
|
|
}, {
|
|
key: '_parseAACPES',
|
|
value: function _parseAACPES(pes) {
|
|
var track = this._aacTrack,
|
|
data = pes.data,
|
|
pts = pes.pts,
|
|
startOffset = 0,
|
|
duration = this._duration,
|
|
audioCodec = this.audioCodec,
|
|
aacOverFlow = this.aacOverFlow,
|
|
lastAacPTS = this.lastAacPTS,
|
|
config,
|
|
frameLength,
|
|
frameDuration,
|
|
frameIndex,
|
|
offset,
|
|
headerLength,
|
|
stamp,
|
|
len,
|
|
aacSample;
|
|
if (aacOverFlow) {
|
|
var tmp = new Uint8Array(aacOverFlow.byteLength + data.byteLength);
|
|
tmp.set(aacOverFlow, 0);
|
|
tmp.set(data, aacOverFlow.byteLength);
|
|
//logger.log(`AAC: append overflowing ${aacOverFlow.byteLength} bytes to beginning of new PES`);
|
|
data = tmp;
|
|
}
|
|
// look for ADTS header (0xFFFx)
|
|
for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
|
|
if (data[offset] === 0xff && (data[offset + 1] & 0xf0) === 0xf0) {
|
|
break;
|
|
}
|
|
}
|
|
// if ADTS header does not start straight from the beginning of the PES payload, raise an error
|
|
if (offset) {
|
|
var reason, fatal;
|
|
if (offset < len - 1) {
|
|
reason = 'AAC PES did not start with ADTS header,offset:' + offset;
|
|
fatal = false;
|
|
} else {
|
|
reason = 'no ADTS header found in AAC PES';
|
|
fatal = true;
|
|
}
|
|
this.observer.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: fatal, reason: reason });
|
|
if (fatal) {
|
|
return;
|
|
}
|
|
}
|
|
if (!track.audiosamplerate) {
|
|
config = _adts2.default.getAudioConfig(this.observer, data, offset, audioCodec);
|
|
track.config = config.config;
|
|
track.audiosamplerate = config.samplerate;
|
|
track.channelCount = config.channelCount;
|
|
track.codec = config.codec;
|
|
track.timescale = config.samplerate;
|
|
track.duration = config.samplerate * duration;
|
|
_logger.logger.log('parsed codec:' + track.codec + ',rate:' + config.samplerate + ',nb channel:' + config.channelCount);
|
|
}
|
|
frameIndex = 0;
|
|
frameDuration = 1024 * 90000 / track.audiosamplerate;
|
|
|
|
// if last AAC frame is overflowing, we should ensure timestamps are contiguous:
|
|
// first sample PTS should be equal to last sample PTS + frameDuration
|
|
if (aacOverFlow && lastAacPTS) {
|
|
var newPTS = lastAacPTS + frameDuration;
|
|
if (Math.abs(newPTS - pts) > 1) {
|
|
_logger.logger.log('AAC: align PTS for overlapping frames by ' + Math.round((newPTS - pts) / 90));
|
|
pts = newPTS;
|
|
}
|
|
}
|
|
|
|
while (offset + 5 < len) {
|
|
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
|
|
headerLength = !!(data[offset + 1] & 0x01) ? 7 : 9;
|
|
// retrieve frame size
|
|
frameLength = (data[offset + 3] & 0x03) << 11 | data[offset + 4] << 3 | (data[offset + 5] & 0xE0) >>> 5;
|
|
frameLength -= headerLength;
|
|
//stamp = pes.pts;
|
|
|
|
if (frameLength > 0 && offset + headerLength + frameLength <= len) {
|
|
stamp = pts + frameIndex * frameDuration;
|
|
//logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}/${(stamp/90).toFixed(0)}`);
|
|
aacSample = { unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp };
|
|
track.samples.push(aacSample);
|
|
track.len += frameLength;
|
|
offset += frameLength + headerLength;
|
|
frameIndex++;
|
|
// look for ADTS header (0xFFFx)
|
|
for (; offset < len - 1; offset++) {
|
|
if (data[offset] === 0xff && (data[offset + 1] & 0xf0) === 0xf0) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (offset < len) {
|
|
aacOverFlow = data.subarray(offset, len);
|
|
//logger.log(`AAC: overflow detected:${len-offset}`);
|
|
} else {
|
|
aacOverFlow = null;
|
|
}
|
|
this.aacOverFlow = aacOverFlow;
|
|
this.lastAacPTS = stamp;
|
|
}
|
|
}, {
|
|
key: '_parseID3PES',
|
|
value: function _parseID3PES(pes) {
|
|
this._id3Track.samples.push(pes);
|
|
}
|
|
}], [{
|
|
key: 'probe',
|
|
value: function probe(data) {
|
|
// a TS fragment should contain at least 3 TS packets, a PAT, a PMT, and one PID, each starting with 0x47
|
|
if (data.length >= 3 * 188 && data[0] === 0x47 && data[188] === 0x47 && data[2 * 188] === 0x47) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return TSDemuxer;
|
|
}();
|
|
|
|
exports.default = TSDemuxer;
|
|
|
|
},{"../errors":19,"../events":21,"../utils/logger":34,"./adts":12,"./exp-golomb":16}],19:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
var ErrorTypes = exports.ErrorTypes = {
|
|
// Identifier for a network error (loading error / timeout ...)
|
|
NETWORK_ERROR: 'networkError',
|
|
// Identifier for a media Error (video/parsing/mediasource error)
|
|
MEDIA_ERROR: 'mediaError',
|
|
// Identifier for all other errors
|
|
OTHER_ERROR: 'otherError'
|
|
};
|
|
|
|
var ErrorDetails = exports.ErrorDetails = {
|
|
// Identifier for a manifest load error - data: { url : faulty URL, response : XHR response}
|
|
MANIFEST_LOAD_ERROR: 'manifestLoadError',
|
|
// Identifier for a manifest load timeout - data: { url : faulty URL, response : XHR response}
|
|
MANIFEST_LOAD_TIMEOUT: 'manifestLoadTimeOut',
|
|
// Identifier for a manifest parsing error - data: { url : faulty URL, reason : error reason}
|
|
MANIFEST_PARSING_ERROR: 'manifestParsingError',
|
|
// Identifier for a manifest with only incompatible codecs error - data: { url : faulty URL, reason : error reason}
|
|
MANIFEST_INCOMPATIBLE_CODECS_ERROR: 'manifestIncompatibleCodecsError',
|
|
// Identifier for playlist load error - data: { url : faulty URL, response : XHR response}
|
|
LEVEL_LOAD_ERROR: 'levelLoadError',
|
|
// Identifier for playlist load timeout - data: { url : faulty URL, response : XHR response}
|
|
LEVEL_LOAD_TIMEOUT: 'levelLoadTimeOut',
|
|
// Identifier for a level switch error - data: { level : faulty level Id, event : error description}
|
|
LEVEL_SWITCH_ERROR: 'levelSwitchError',
|
|
// Identifier for fragment load error - data: { frag : fragment object, response : XHR response}
|
|
FRAG_LOAD_ERROR: 'fragLoadError',
|
|
// Identifier for fragment loop loading error - data: { frag : fragment object}
|
|
FRAG_LOOP_LOADING_ERROR: 'fragLoopLoadingError',
|
|
// Identifier for fragment load timeout error - data: { frag : fragment object}
|
|
FRAG_LOAD_TIMEOUT: 'fragLoadTimeOut',
|
|
// Identifier for a fragment decryption error event - data: parsing error description
|
|
FRAG_DECRYPT_ERROR: 'fragDecryptError',
|
|
// Identifier for a fragment parsing error event - data: parsing error description
|
|
FRAG_PARSING_ERROR: 'fragParsingError',
|
|
// Identifier for decrypt key load error - data: { frag : fragment object, response : XHR response}
|
|
KEY_LOAD_ERROR: 'keyLoadError',
|
|
// Identifier for decrypt key load timeout error - data: { frag : fragment object}
|
|
KEY_LOAD_TIMEOUT: 'keyLoadTimeOut',
|
|
// Identifier for a buffer append error - data: append error description
|
|
BUFFER_APPEND_ERROR: 'bufferAppendError',
|
|
// Identifier for a buffer appending error event - data: appending error description
|
|
BUFFER_APPENDING_ERROR: 'bufferAppendingError',
|
|
// Identifier for a buffer stalled error event
|
|
BUFFER_STALLED_ERROR: 'bufferStalledError',
|
|
// Identifier for a buffer full event
|
|
BUFFER_FULL_ERROR: 'bufferFullError',
|
|
// Identifier for a buffer seek over hole event
|
|
BUFFER_SEEK_OVER_HOLE: 'bufferSeekOverHole'
|
|
};
|
|
|
|
},{}],20:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
/*
|
|
*
|
|
* All objects in the event handling chain should inherit from this class
|
|
*
|
|
*/
|
|
|
|
//import {logger} from './utils/logger';
|
|
|
|
var EventHandler = function () {
|
|
function EventHandler(hls) {
|
|
_classCallCheck(this, EventHandler);
|
|
|
|
this.hls = hls;
|
|
this.onEvent = this.onEvent.bind(this);
|
|
|
|
for (var _len = arguments.length, events = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
events[_key - 1] = arguments[_key];
|
|
}
|
|
|
|
this.handledEvents = events;
|
|
this.useGenericHandler = true;
|
|
|
|
this.registerListeners();
|
|
}
|
|
|
|
_createClass(EventHandler, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
this.unregisterListeners();
|
|
}
|
|
}, {
|
|
key: 'isEventHandler',
|
|
value: function isEventHandler() {
|
|
return _typeof(this.handledEvents) === 'object' && this.handledEvents.length && typeof this.onEvent === 'function';
|
|
}
|
|
}, {
|
|
key: 'registerListeners',
|
|
value: function registerListeners() {
|
|
if (this.isEventHandler()) {
|
|
this.handledEvents.forEach(function (event) {
|
|
if (event === 'hlsEventGeneric') {
|
|
throw new Error('Forbidden event name: ' + event);
|
|
}
|
|
this.hls.on(event, this.onEvent);
|
|
}.bind(this));
|
|
}
|
|
}
|
|
}, {
|
|
key: 'unregisterListeners',
|
|
value: function unregisterListeners() {
|
|
if (this.isEventHandler()) {
|
|
this.handledEvents.forEach(function (event) {
|
|
this.hls.off(event, this.onEvent);
|
|
}.bind(this));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* arguments: event (string), data (any)
|
|
*/
|
|
|
|
}, {
|
|
key: 'onEvent',
|
|
value: function onEvent(event, data) {
|
|
this.onEventGeneric(event, data);
|
|
}
|
|
}, {
|
|
key: 'onEventGeneric',
|
|
value: function onEventGeneric(event, data) {
|
|
var eventToFunction = function eventToFunction(event, data) {
|
|
var funcName = 'on' + event.replace('hls', '');
|
|
if (typeof this[funcName] !== 'function') {
|
|
throw new Error('Event ' + event + ' has no generic handler in this ' + this.constructor.name + ' class (tried ' + funcName + ')');
|
|
}
|
|
return this[funcName].bind(this, data);
|
|
};
|
|
eventToFunction.call(this, event, data).call();
|
|
}
|
|
}]);
|
|
|
|
return EventHandler;
|
|
}();
|
|
|
|
exports.default = EventHandler;
|
|
|
|
},{}],21:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
// fired before MediaSource is attaching to media element - data: { media }
|
|
MEDIA_ATTACHING: 'hlsMediaAttaching',
|
|
// fired when MediaSource has been succesfully attached to media element - data: { }
|
|
MEDIA_ATTACHED: 'hlsMediaAttached',
|
|
// fired before detaching MediaSource from media element - data: { }
|
|
MEDIA_DETACHING: 'hlsMediaDetaching',
|
|
// fired when MediaSource has been detached from media element - data: { }
|
|
MEDIA_DETACHED: 'hlsMediaDetached',
|
|
// fired when we buffer is going to be resetted
|
|
BUFFER_RESET: 'hlsBufferReset',
|
|
// fired when we know about the codecs that we need buffers for to push into - data: {tracks : { container, codec, levelCodec, initSegment, metadata }}
|
|
BUFFER_CODECS: 'hlsBufferCodecs',
|
|
// fired when we append a segment to the buffer - data: { segment: segment object }
|
|
BUFFER_APPENDING: 'hlsBufferAppending',
|
|
// fired when we are done with appending a media segment to the buffer
|
|
BUFFER_APPENDED: 'hlsBufferAppended',
|
|
// fired when the stream is finished and we want to notify the media buffer that there will be no more data
|
|
BUFFER_EOS: 'hlsBufferEos',
|
|
// fired when the media buffer should be flushed - data {startOffset, endOffset}
|
|
BUFFER_FLUSHING: 'hlsBufferFlushing',
|
|
// fired when the media has been flushed
|
|
BUFFER_FLUSHED: 'hlsBufferFlushed',
|
|
// fired to signal that a manifest loading starts - data: { url : manifestURL}
|
|
MANIFEST_LOADING: 'hlsManifestLoading',
|
|
// fired after manifest has been loaded - data: { levels : [available quality levels] , url : manifestURL, stats : { trequest, tfirst, tload, mtime}}
|
|
MANIFEST_LOADED: 'hlsManifestLoaded',
|
|
// fired after manifest has been parsed - data: { levels : [available quality levels] , firstLevel : index of first quality level appearing in Manifest}
|
|
MANIFEST_PARSED: 'hlsManifestParsed',
|
|
// fired when a level playlist loading starts - data: { url : level URL level : id of level being loaded}
|
|
LEVEL_LOADING: 'hlsLevelLoading',
|
|
// fired when a level playlist loading finishes - data: { details : levelDetails object, level : id of loaded level, stats : { trequest, tfirst, tload, mtime} }
|
|
LEVEL_LOADED: 'hlsLevelLoaded',
|
|
// fired when a level's details have been updated based on previous details, after it has been loaded. - data: { details : levelDetails object, level : id of updated level }
|
|
LEVEL_UPDATED: 'hlsLevelUpdated',
|
|
// fired when a level's PTS information has been updated after parsing a fragment - data: { details : levelDetails object, level : id of updated level, drift: PTS drift observed when parsing last fragment }
|
|
LEVEL_PTS_UPDATED: 'hlsLevelPtsUpdated',
|
|
// fired when a level switch is requested - data: { level : id of new level }
|
|
LEVEL_SWITCH: 'hlsLevelSwitch',
|
|
// fired when a fragment loading starts - data: { frag : fragment object}
|
|
FRAG_LOADING: 'hlsFragLoading',
|
|
// fired when a fragment loading is progressing - data: { frag : fragment object, { trequest, tfirst, loaded}}
|
|
FRAG_LOAD_PROGRESS: 'hlsFragLoadProgress',
|
|
// Identifier for fragment load aborting for emergency switch down - data: {frag : fragment object}
|
|
FRAG_LOAD_EMERGENCY_ABORTED: 'hlsFragLoadEmergencyAborted',
|
|
// fired when a fragment loading is completed - data: { frag : fragment object, payload : fragment payload, stats : { trequest, tfirst, tload, length}}
|
|
FRAG_LOADED: 'hlsFragLoaded',
|
|
// fired when Init Segment has been extracted from fragment - data: { moov : moov MP4 box, codecs : codecs found while parsing fragment}
|
|
FRAG_PARSING_INIT_SEGMENT: 'hlsFragParsingInitSegment',
|
|
// fired when parsing sei text is completed - data: { samples : [ sei samples pes ] }
|
|
FRAG_PARSING_USERDATA: 'hlsFragParsingUserdata',
|
|
// fired when parsing id3 is completed - data: { samples : [ id3 samples pes ] }
|
|
FRAG_PARSING_METADATA: 'hlsFragParsingMetadata',
|
|
// fired when data have been extracted from fragment - data: { data1 : moof MP4 box or TS fragments, data2 : mdat MP4 box or null}
|
|
FRAG_PARSING_DATA: 'hlsFragParsingData',
|
|
// fired when fragment parsing is completed - data: undefined
|
|
FRAG_PARSED: 'hlsFragParsed',
|
|
// fired when fragment remuxed MP4 boxes have all been appended into SourceBuffer - data: { frag : fragment object, stats : { trequest, tfirst, tload, tparsed, tbuffered, length} }
|
|
FRAG_BUFFERED: 'hlsFragBuffered',
|
|
// fired when fragment matching with current media position is changing - data : { frag : fragment object }
|
|
FRAG_CHANGED: 'hlsFragChanged',
|
|
// Identifier for a FPS drop event - data: {curentDropped, currentDecoded, totalDroppedFrames}
|
|
FPS_DROP: 'hlsFpsDrop',
|
|
// Identifier for an error event - data: { type : error type, details : error details, fatal : if true, hls.js cannot/will not try to recover, if false, hls.js will try to recover,other error specific data}
|
|
ERROR: 'hlsError',
|
|
// fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a media to the instance of hls.js to handle mid-rolls for example
|
|
DESTROYING: 'hlsDestroying',
|
|
// fired when a decrypt key loading starts - data: { frag : fragment object}
|
|
KEY_LOADING: 'hlsKeyLoading',
|
|
// fired when a decrypt key loading is completed - data: { frag : fragment object, payload : key payload, stats : { trequest, tfirst, tload, length}}
|
|
KEY_LOADED: 'hlsKeyLoaded'
|
|
};
|
|
|
|
},{}],22:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* Level Helper class, providing methods dealing with playlist sliding and drift
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var LevelHelper = function () {
|
|
function LevelHelper() {
|
|
_classCallCheck(this, LevelHelper);
|
|
}
|
|
|
|
_createClass(LevelHelper, null, [{
|
|
key: 'mergeDetails',
|
|
value: function mergeDetails(oldDetails, newDetails) {
|
|
var start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN,
|
|
end = Math.min(oldDetails.endSN, newDetails.endSN) - newDetails.startSN,
|
|
delta = newDetails.startSN - oldDetails.startSN,
|
|
oldfragments = oldDetails.fragments,
|
|
newfragments = newDetails.fragments,
|
|
ccOffset = 0,
|
|
PTSFrag;
|
|
|
|
// check if old/new playlists have fragments in common
|
|
if (end < start) {
|
|
newDetails.PTSKnown = false;
|
|
return;
|
|
}
|
|
// loop through overlapping SN and update startPTS , cc, and duration if any found
|
|
for (var i = start; i <= end; i++) {
|
|
var oldFrag = oldfragments[delta + i],
|
|
newFrag = newfragments[i];
|
|
ccOffset = oldFrag.cc - newFrag.cc;
|
|
if (!isNaN(oldFrag.startPTS)) {
|
|
newFrag.start = newFrag.startPTS = oldFrag.startPTS;
|
|
newFrag.endPTS = oldFrag.endPTS;
|
|
newFrag.duration = oldFrag.duration;
|
|
PTSFrag = newFrag;
|
|
}
|
|
}
|
|
|
|
if (ccOffset) {
|
|
_logger.logger.log('discontinuity sliding from playlist, take drift into account');
|
|
for (i = 0; i < newfragments.length; i++) {
|
|
newfragments[i].cc += ccOffset;
|
|
}
|
|
}
|
|
|
|
// if at least one fragment contains PTS info, recompute PTS information for all fragments
|
|
if (PTSFrag) {
|
|
LevelHelper.updateFragPTS(newDetails, PTSFrag.sn, PTSFrag.startPTS, PTSFrag.endPTS);
|
|
} else {
|
|
// adjust start by sliding offset
|
|
var sliding = oldfragments[delta].start;
|
|
for (i = 0; i < newfragments.length; i++) {
|
|
newfragments[i].start += sliding;
|
|
}
|
|
}
|
|
// if we are here, it means we have fragments overlapping between
|
|
// old and new level. reliable PTS info is thus relying on old level
|
|
newDetails.PTSKnown = oldDetails.PTSKnown;
|
|
return;
|
|
}
|
|
}, {
|
|
key: 'updateFragPTS',
|
|
value: function updateFragPTS(details, sn, startPTS, endPTS) {
|
|
var fragIdx, fragments, frag, i;
|
|
// exit if sn out of range
|
|
if (sn < details.startSN || sn > details.endSN) {
|
|
return 0;
|
|
}
|
|
fragIdx = sn - details.startSN;
|
|
fragments = details.fragments;
|
|
frag = fragments[fragIdx];
|
|
if (!isNaN(frag.startPTS)) {
|
|
startPTS = Math.min(startPTS, frag.startPTS);
|
|
endPTS = Math.max(endPTS, frag.endPTS);
|
|
}
|
|
|
|
var drift = startPTS - frag.start;
|
|
|
|
frag.start = frag.startPTS = startPTS;
|
|
frag.endPTS = endPTS;
|
|
frag.duration = endPTS - startPTS;
|
|
// adjust fragment PTS/duration from seqnum-1 to frag 0
|
|
for (i = fragIdx; i > 0; i--) {
|
|
LevelHelper.updatePTS(fragments, i, i - 1);
|
|
}
|
|
|
|
// adjust fragment PTS/duration from seqnum to last frag
|
|
for (i = fragIdx; i < fragments.length - 1; i++) {
|
|
LevelHelper.updatePTS(fragments, i, i + 1);
|
|
}
|
|
details.PTSKnown = true;
|
|
//logger.log(` frag start/end:${startPTS.toFixed(3)}/${endPTS.toFixed(3)}`);
|
|
|
|
return drift;
|
|
}
|
|
}, {
|
|
key: 'updatePTS',
|
|
value: function updatePTS(fragments, fromIdx, toIdx) {
|
|
var fragFrom = fragments[fromIdx],
|
|
fragTo = fragments[toIdx],
|
|
fragToPTS = fragTo.startPTS;
|
|
// if we know startPTS[toIdx]
|
|
if (!isNaN(fragToPTS)) {
|
|
// update fragment duration.
|
|
// it helps to fix drifts between playlist reported duration and fragment real duration
|
|
if (toIdx > fromIdx) {
|
|
fragFrom.duration = fragToPTS - fragFrom.start;
|
|
if (fragFrom.duration < 0) {
|
|
_logger.logger.error('negative duration computed for frag ' + fragFrom.sn + ',level ' + fragFrom.level + ', there should be some duration drift between playlist and fragment!');
|
|
}
|
|
} else {
|
|
fragTo.duration = fragFrom.start - fragToPTS;
|
|
if (fragTo.duration < 0) {
|
|
_logger.logger.error('negative duration computed for frag ' + fragTo.sn + ',level ' + fragTo.level + ', there should be some duration drift between playlist and fragment!');
|
|
}
|
|
}
|
|
} else {
|
|
// we dont know startPTS[toIdx]
|
|
if (toIdx > fromIdx) {
|
|
fragTo.start = fragFrom.start + fragFrom.duration;
|
|
} else {
|
|
fragTo.start = fragFrom.start - fragTo.duration;
|
|
}
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return LevelHelper;
|
|
}();
|
|
|
|
exports.default = LevelHelper;
|
|
|
|
},{"../utils/logger":34}],23:[function(require,module,exports){
|
|
/**
|
|
* HLS interface
|
|
*/
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
//import FPSController from './controller/fps-controller';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('./events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _errors = require('./errors');
|
|
|
|
var _playlistLoader = require('./loader/playlist-loader');
|
|
|
|
var _playlistLoader2 = _interopRequireDefault(_playlistLoader);
|
|
|
|
var _fragmentLoader = require('./loader/fragment-loader');
|
|
|
|
var _fragmentLoader2 = _interopRequireDefault(_fragmentLoader);
|
|
|
|
var _abrController = require('./controller/abr-controller');
|
|
|
|
var _abrController2 = _interopRequireDefault(_abrController);
|
|
|
|
var _bufferController = require('./controller/buffer-controller');
|
|
|
|
var _bufferController2 = _interopRequireDefault(_bufferController);
|
|
|
|
var _streamController = require('./controller/stream-controller');
|
|
|
|
var _streamController2 = _interopRequireDefault(_streamController);
|
|
|
|
var _levelController = require('./controller/level-controller');
|
|
|
|
var _levelController2 = _interopRequireDefault(_levelController);
|
|
|
|
var _timelineController = require('./controller/timeline-controller');
|
|
|
|
var _timelineController2 = _interopRequireDefault(_timelineController);
|
|
|
|
var _logger = require('./utils/logger');
|
|
|
|
var _xhrLoader = require('./utils/xhr-loader');
|
|
|
|
var _xhrLoader2 = _interopRequireDefault(_xhrLoader);
|
|
|
|
var _events3 = require('events');
|
|
|
|
var _events4 = _interopRequireDefault(_events3);
|
|
|
|
var _keyLoader = require('./loader/key-loader');
|
|
|
|
var _keyLoader2 = _interopRequireDefault(_keyLoader);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var Hls = function () {
|
|
_createClass(Hls, null, [{
|
|
key: 'isSupported',
|
|
value: function isSupported() {
|
|
return window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
|
|
}
|
|
}, {
|
|
key: 'Events',
|
|
get: function get() {
|
|
return _events2.default;
|
|
}
|
|
}, {
|
|
key: 'ErrorTypes',
|
|
get: function get() {
|
|
return _errors.ErrorTypes;
|
|
}
|
|
}, {
|
|
key: 'ErrorDetails',
|
|
get: function get() {
|
|
return _errors.ErrorDetails;
|
|
}
|
|
}, {
|
|
key: 'DefaultConfig',
|
|
get: function get() {
|
|
if (!Hls.defaultConfig) {
|
|
Hls.defaultConfig = {
|
|
autoStartLoad: true,
|
|
debug: false,
|
|
maxBufferLength: 30,
|
|
maxBufferSize: 60 * 1000 * 1000,
|
|
maxBufferHole: 0.5,
|
|
maxSeekHole: 2,
|
|
maxFragLookUpTolerance: 0.2,
|
|
liveSyncDurationCount: 3,
|
|
liveMaxLatencyDurationCount: Infinity,
|
|
liveSyncDuration: undefined,
|
|
liveMaxLatencyDuration: undefined,
|
|
maxMaxBufferLength: 600,
|
|
enableWorker: true,
|
|
enableSoftwareAES: true,
|
|
manifestLoadingTimeOut: 10000,
|
|
manifestLoadingMaxRetry: 1,
|
|
manifestLoadingRetryDelay: 1000,
|
|
levelLoadingTimeOut: 10000,
|
|
levelLoadingMaxRetry: 4,
|
|
levelLoadingRetryDelay: 1000,
|
|
fragLoadingTimeOut: 20000,
|
|
fragLoadingMaxRetry: 6,
|
|
fragLoadingRetryDelay: 1000,
|
|
fragLoadingLoopThreshold: 3,
|
|
startFragPrefetch: false,
|
|
// fpsDroppedMonitoringPeriod: 5000,
|
|
// fpsDroppedMonitoringThreshold: 0.2,
|
|
appendErrorMaxRetry: 3,
|
|
loader: _xhrLoader2.default,
|
|
fLoader: undefined,
|
|
pLoader: undefined,
|
|
abrController: _abrController2.default,
|
|
bufferController: _bufferController2.default,
|
|
streamController: _streamController2.default,
|
|
timelineController: _timelineController2.default,
|
|
enableCEA708Captions: true,
|
|
enableMP2TPassThrough: false
|
|
};
|
|
}
|
|
return Hls.defaultConfig;
|
|
},
|
|
set: function set(defaultConfig) {
|
|
Hls.defaultConfig = defaultConfig;
|
|
}
|
|
}]);
|
|
|
|
function Hls() {
|
|
var config = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
|
|
|
|
_classCallCheck(this, Hls);
|
|
|
|
var defaultConfig = Hls.DefaultConfig;
|
|
|
|
if ((config.liveSyncDurationCount || config.liveMaxLatencyDurationCount) && (config.liveSyncDuration || config.liveMaxLatencyDuration)) {
|
|
throw new Error('Illegal hls.js config: don\'t mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration');
|
|
}
|
|
|
|
for (var prop in defaultConfig) {
|
|
if (prop in config) {
|
|
continue;
|
|
}
|
|
config[prop] = defaultConfig[prop];
|
|
}
|
|
|
|
if (config.liveMaxLatencyDurationCount !== undefined && config.liveMaxLatencyDurationCount <= config.liveSyncDurationCount) {
|
|
throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be gt "liveSyncDurationCount"');
|
|
}
|
|
|
|
if (config.liveMaxLatencyDuration !== undefined && (config.liveMaxLatencyDuration <= config.liveSyncDuration || config.liveSyncDuration === undefined)) {
|
|
throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be gt "liveSyncDuration"');
|
|
}
|
|
|
|
(0, _logger.enableLogs)(config.debug);
|
|
this.config = config;
|
|
// observer setup
|
|
var observer = this.observer = new _events4.default();
|
|
observer.trigger = function trigger(event) {
|
|
for (var _len = arguments.length, data = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
data[_key - 1] = arguments[_key];
|
|
}
|
|
|
|
observer.emit.apply(observer, [event, event].concat(data));
|
|
};
|
|
|
|
observer.off = function off(event) {
|
|
for (var _len2 = arguments.length, data = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
data[_key2 - 1] = arguments[_key2];
|
|
}
|
|
|
|
observer.removeListener.apply(observer, [event].concat(data));
|
|
};
|
|
this.on = observer.on.bind(observer);
|
|
this.off = observer.off.bind(observer);
|
|
this.trigger = observer.trigger.bind(observer);
|
|
this.playlistLoader = new _playlistLoader2.default(this);
|
|
this.fragmentLoader = new _fragmentLoader2.default(this);
|
|
this.levelController = new _levelController2.default(this);
|
|
this.abrController = new config.abrController(this);
|
|
this.bufferController = new config.bufferController(this);
|
|
this.streamController = new config.streamController(this);
|
|
this.timelineController = new config.timelineController(this);
|
|
this.keyLoader = new _keyLoader2.default(this);
|
|
//this.fpsController = new FPSController(this);
|
|
}
|
|
|
|
_createClass(Hls, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
_logger.logger.log('destroy');
|
|
this.trigger(_events2.default.DESTROYING);
|
|
this.detachMedia();
|
|
this.playlistLoader.destroy();
|
|
this.fragmentLoader.destroy();
|
|
this.levelController.destroy();
|
|
this.bufferController.destroy();
|
|
this.streamController.destroy();
|
|
this.timelineController.destroy();
|
|
this.keyLoader.destroy();
|
|
//this.fpsController.destroy();
|
|
this.url = null;
|
|
this.observer.removeAllListeners();
|
|
}
|
|
}, {
|
|
key: 'attachMedia',
|
|
value: function attachMedia(media) {
|
|
_logger.logger.log('attachMedia');
|
|
this.media = media;
|
|
this.trigger(_events2.default.MEDIA_ATTACHING, { media: media });
|
|
}
|
|
}, {
|
|
key: 'detachMedia',
|
|
value: function detachMedia() {
|
|
_logger.logger.log('detachMedia');
|
|
this.trigger(_events2.default.MEDIA_DETACHING);
|
|
this.media = null;
|
|
}
|
|
}, {
|
|
key: 'loadSource',
|
|
value: function loadSource(url) {
|
|
_logger.logger.log('loadSource:' + url);
|
|
this.url = url;
|
|
// when attaching to a source URL, trigger a playlist load
|
|
this.trigger(_events2.default.MANIFEST_LOADING, { url: url });
|
|
}
|
|
}, {
|
|
key: 'startLoad',
|
|
value: function startLoad() {
|
|
_logger.logger.log('startLoad');
|
|
this.streamController.startLoad();
|
|
}
|
|
}, {
|
|
key: 'swapAudioCodec',
|
|
value: function swapAudioCodec() {
|
|
_logger.logger.log('swapAudioCodec');
|
|
this.streamController.swapAudioCodec();
|
|
}
|
|
}, {
|
|
key: 'recoverMediaError',
|
|
value: function recoverMediaError() {
|
|
_logger.logger.log('recoverMediaError');
|
|
var media = this.media;
|
|
this.detachMedia();
|
|
this.attachMedia(media);
|
|
}
|
|
|
|
/** Return all quality levels **/
|
|
|
|
}, {
|
|
key: 'levels',
|
|
get: function get() {
|
|
return this.levelController.levels;
|
|
}
|
|
|
|
/** Return current playback quality level **/
|
|
|
|
}, {
|
|
key: 'currentLevel',
|
|
get: function get() {
|
|
return this.streamController.currentLevel;
|
|
}
|
|
|
|
/* set quality level immediately (-1 for automatic level selection) */
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set currentLevel:' + newLevel);
|
|
this.loadLevel = newLevel;
|
|
this.streamController.immediateLevelSwitch();
|
|
}
|
|
|
|
/** Return next playback quality level (quality level of next fragment) **/
|
|
|
|
}, {
|
|
key: 'nextLevel',
|
|
get: function get() {
|
|
return this.streamController.nextLevel;
|
|
}
|
|
|
|
/* set quality level for next fragment (-1 for automatic level selection) */
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set nextLevel:' + newLevel);
|
|
this.levelController.manualLevel = newLevel;
|
|
this.streamController.nextLevelSwitch();
|
|
}
|
|
|
|
/** Return the quality level of current/last loaded fragment **/
|
|
|
|
}, {
|
|
key: 'loadLevel',
|
|
get: function get() {
|
|
return this.levelController.level;
|
|
}
|
|
|
|
/* set quality level for current/next loaded fragment (-1 for automatic level selection) */
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set loadLevel:' + newLevel);
|
|
this.levelController.manualLevel = newLevel;
|
|
}
|
|
|
|
/** Return the quality level of next loaded fragment **/
|
|
|
|
}, {
|
|
key: 'nextLoadLevel',
|
|
get: function get() {
|
|
return this.levelController.nextLoadLevel;
|
|
}
|
|
|
|
/** set quality level of next loaded fragment **/
|
|
,
|
|
set: function set(level) {
|
|
this.levelController.nextLoadLevel = level;
|
|
}
|
|
|
|
/** Return first level (index of first level referenced in manifest)
|
|
**/
|
|
|
|
}, {
|
|
key: 'firstLevel',
|
|
get: function get() {
|
|
return this.levelController.firstLevel;
|
|
}
|
|
|
|
/** set first level (index of first level referenced in manifest)
|
|
**/
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set firstLevel:' + newLevel);
|
|
this.levelController.firstLevel = newLevel;
|
|
}
|
|
|
|
/** Return start level (level of first fragment that will be played back)
|
|
if not overrided by user, first level appearing in manifest will be used as start level
|
|
if -1 : automatic start level selection, playback will start from level matching download bandwidth (determined from download of first segment)
|
|
**/
|
|
|
|
}, {
|
|
key: 'startLevel',
|
|
get: function get() {
|
|
return this.levelController.startLevel;
|
|
}
|
|
|
|
/** set start level (level of first fragment that will be played back)
|
|
if not overrided by user, first level appearing in manifest will be used as start level
|
|
if -1 : automatic start level selection, playback will start from level matching download bandwidth (determined from download of first segment)
|
|
**/
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set startLevel:' + newLevel);
|
|
this.levelController.startLevel = newLevel;
|
|
}
|
|
|
|
/** Return the capping/max level value that could be used by automatic level selection algorithm **/
|
|
|
|
}, {
|
|
key: 'autoLevelCapping',
|
|
get: function get() {
|
|
return this.abrController.autoLevelCapping;
|
|
}
|
|
|
|
/** set the capping/max level value that could be used by automatic level selection algorithm **/
|
|
,
|
|
set: function set(newLevel) {
|
|
_logger.logger.log('set autoLevelCapping:' + newLevel);
|
|
this.abrController.autoLevelCapping = newLevel;
|
|
}
|
|
|
|
/* check if we are in automatic level selection mode */
|
|
|
|
}, {
|
|
key: 'autoLevelEnabled',
|
|
get: function get() {
|
|
return this.levelController.manualLevel === -1;
|
|
}
|
|
|
|
/* return manual level */
|
|
|
|
}, {
|
|
key: 'manualLevel',
|
|
get: function get() {
|
|
return this.levelController.manualLevel;
|
|
}
|
|
}]);
|
|
|
|
return Hls;
|
|
}();
|
|
|
|
exports.default = Hls;
|
|
|
|
},{"./controller/abr-controller":3,"./controller/buffer-controller":4,"./controller/level-controller":5,"./controller/stream-controller":6,"./controller/timeline-controller":7,"./errors":19,"./events":21,"./loader/fragment-loader":25,"./loader/key-loader":26,"./loader/playlist-loader":27,"./utils/logger":34,"./utils/xhr-loader":36,"events":1}],24:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
// This is mostly for support of the es6 module export
|
|
// syntax with the babel compiler, it looks like it doesnt support
|
|
// function exports like we are used to in node/commonjs
|
|
module.exports = require('./hls.js').default;
|
|
|
|
},{"./hls.js":23}],25:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Fragment Loader
|
|
*/
|
|
|
|
var FragmentLoader = function (_EventHandler) {
|
|
_inherits(FragmentLoader, _EventHandler);
|
|
|
|
function FragmentLoader(hls) {
|
|
_classCallCheck(this, FragmentLoader);
|
|
|
|
return _possibleConstructorReturn(this, Object.getPrototypeOf(FragmentLoader).call(this, hls, _events2.default.FRAG_LOADING));
|
|
}
|
|
|
|
_createClass(FragmentLoader, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
if (this.loader) {
|
|
this.loader.destroy();
|
|
this.loader = null;
|
|
}
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onFragLoading',
|
|
value: function onFragLoading(data) {
|
|
var frag = data.frag;
|
|
this.frag = frag;
|
|
this.frag.loaded = 0;
|
|
var config = this.hls.config;
|
|
frag.loader = this.loader = typeof config.fLoader !== 'undefined' ? new config.fLoader(config) : new config.loader(config);
|
|
this.loader.load(frag.url, 'arraybuffer', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), config.fragLoadingTimeOut, 1, 0, this.loadprogress.bind(this), frag);
|
|
}
|
|
}, {
|
|
key: 'loadsuccess',
|
|
value: function loadsuccess(event, stats) {
|
|
var payload = event.currentTarget.response;
|
|
stats.length = payload.byteLength;
|
|
// detach fragment loader on load success
|
|
this.frag.loader = undefined;
|
|
this.hls.trigger(_events2.default.FRAG_LOADED, { payload: payload, frag: this.frag, stats: stats });
|
|
}
|
|
}, {
|
|
key: 'loaderror',
|
|
value: function loaderror(event) {
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.FRAG_LOAD_ERROR, fatal: false, frag: this.frag, response: event });
|
|
}
|
|
}, {
|
|
key: 'loadtimeout',
|
|
value: function loadtimeout() {
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.FRAG_LOAD_TIMEOUT, fatal: false, frag: this.frag });
|
|
}
|
|
}, {
|
|
key: 'loadprogress',
|
|
value: function loadprogress(event, stats) {
|
|
this.frag.loaded = stats.loaded;
|
|
this.hls.trigger(_events2.default.FRAG_LOAD_PROGRESS, { frag: this.frag, stats: stats });
|
|
}
|
|
}]);
|
|
|
|
return FragmentLoader;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = FragmentLoader;
|
|
|
|
},{"../errors":19,"../event-handler":20,"../events":21}],26:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /*
|
|
* Decrypt key Loader
|
|
*/
|
|
|
|
var KeyLoader = function (_EventHandler) {
|
|
_inherits(KeyLoader, _EventHandler);
|
|
|
|
function KeyLoader(hls) {
|
|
_classCallCheck(this, KeyLoader);
|
|
|
|
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(KeyLoader).call(this, hls, _events2.default.KEY_LOADING));
|
|
|
|
_this.decryptkey = null;
|
|
_this.decrypturl = null;
|
|
return _this;
|
|
}
|
|
|
|
_createClass(KeyLoader, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
if (this.loader) {
|
|
this.loader.destroy();
|
|
this.loader = null;
|
|
}
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onKeyLoading',
|
|
value: function onKeyLoading(data) {
|
|
var frag = this.frag = data.frag,
|
|
decryptdata = frag.decryptdata,
|
|
uri = decryptdata.uri;
|
|
// if uri is different from previous one or if decrypt key not retrieved yet
|
|
if (uri !== this.decrypturl || this.decryptkey === null) {
|
|
var config = this.hls.config;
|
|
frag.loader = this.loader = new config.loader(config);
|
|
this.decrypturl = uri;
|
|
this.decryptkey = null;
|
|
frag.loader.load(uri, 'arraybuffer', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), config.fragLoadingTimeOut, config.fragLoadingMaxRetry, config.fragLoadingRetryDelay, this.loadprogress.bind(this), frag);
|
|
} else if (this.decryptkey) {
|
|
// we already loaded this key, return it
|
|
decryptdata.key = this.decryptkey;
|
|
this.hls.trigger(_events2.default.KEY_LOADED, { frag: frag });
|
|
}
|
|
}
|
|
}, {
|
|
key: 'loadsuccess',
|
|
value: function loadsuccess(event) {
|
|
var frag = this.frag;
|
|
this.decryptkey = frag.decryptdata.key = new Uint8Array(event.currentTarget.response);
|
|
// detach fragment loader on load success
|
|
frag.loader = undefined;
|
|
this.hls.trigger(_events2.default.KEY_LOADED, { frag: frag });
|
|
}
|
|
}, {
|
|
key: 'loaderror',
|
|
value: function loaderror(event) {
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.KEY_LOAD_ERROR, fatal: false, frag: this.frag, response: event });
|
|
}
|
|
}, {
|
|
key: 'loadtimeout',
|
|
value: function loadtimeout() {
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.KEY_LOAD_TIMEOUT, fatal: false, frag: this.frag });
|
|
}
|
|
}, {
|
|
key: 'loadprogress',
|
|
value: function loadprogress() {}
|
|
}]);
|
|
|
|
return KeyLoader;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = KeyLoader;
|
|
|
|
},{"../errors":19,"../event-handler":20,"../events":21}],27:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _eventHandler = require('../event-handler');
|
|
|
|
var _eventHandler2 = _interopRequireDefault(_eventHandler);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
var _url = require('../utils/url');
|
|
|
|
var _url2 = _interopRequireDefault(_url);
|
|
|
|
var _attrList = require('../utils/attr-list');
|
|
|
|
var _attrList2 = _interopRequireDefault(_attrList);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
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; }
|
|
|
|
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; } /**
|
|
* Playlist Loader
|
|
*/
|
|
|
|
//import {logger} from '../utils/logger';
|
|
|
|
var PlaylistLoader = function (_EventHandler) {
|
|
_inherits(PlaylistLoader, _EventHandler);
|
|
|
|
function PlaylistLoader(hls) {
|
|
_classCallCheck(this, PlaylistLoader);
|
|
|
|
return _possibleConstructorReturn(this, Object.getPrototypeOf(PlaylistLoader).call(this, hls, _events2.default.MANIFEST_LOADING, _events2.default.LEVEL_LOADING));
|
|
}
|
|
|
|
_createClass(PlaylistLoader, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
if (this.loader) {
|
|
this.loader.destroy();
|
|
this.loader = null;
|
|
}
|
|
this.url = this.id = null;
|
|
_eventHandler2.default.prototype.destroy.call(this);
|
|
}
|
|
}, {
|
|
key: 'onManifestLoading',
|
|
value: function onManifestLoading(data) {
|
|
this.load(data.url, null);
|
|
}
|
|
}, {
|
|
key: 'onLevelLoading',
|
|
value: function onLevelLoading(data) {
|
|
this.load(data.url, data.level, data.id);
|
|
}
|
|
}, {
|
|
key: 'load',
|
|
value: function load(url, id1, id2) {
|
|
var config = this.hls.config,
|
|
retry,
|
|
timeout,
|
|
retryDelay;
|
|
this.url = url;
|
|
this.id = id1;
|
|
this.id2 = id2;
|
|
if (this.id === null) {
|
|
retry = config.manifestLoadingMaxRetry;
|
|
timeout = config.manifestLoadingTimeOut;
|
|
retryDelay = config.manifestLoadingRetryDelay;
|
|
} else {
|
|
retry = config.levelLoadingMaxRetry;
|
|
timeout = config.levelLoadingTimeOut;
|
|
retryDelay = config.levelLoadingRetryDelay;
|
|
}
|
|
this.loader = typeof config.pLoader !== 'undefined' ? new config.pLoader(config) : new config.loader(config);
|
|
this.loader.load(url, '', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), timeout, retry, retryDelay);
|
|
}
|
|
}, {
|
|
key: 'resolve',
|
|
value: function resolve(url, baseUrl) {
|
|
return _url2.default.buildAbsoluteURL(baseUrl, url);
|
|
}
|
|
}, {
|
|
key: 'parseMasterPlaylist',
|
|
value: function parseMasterPlaylist(string, baseurl) {
|
|
var levels = [],
|
|
result = undefined;
|
|
|
|
// https://regex101.com is your friend
|
|
var re = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g;
|
|
while ((result = re.exec(string)) != null) {
|
|
var level = {};
|
|
|
|
var attrs = level.attrs = new _attrList2.default(result[1]);
|
|
level.url = this.resolve(result[2], baseurl);
|
|
|
|
var resolution = attrs.decimalResolution('RESOLUTION');
|
|
if (resolution) {
|
|
level.width = resolution.width;
|
|
level.height = resolution.height;
|
|
}
|
|
level.bitrate = attrs.decimalInteger('BANDWIDTH');
|
|
level.name = attrs.NAME;
|
|
|
|
var codecs = attrs.CODECS;
|
|
if (codecs) {
|
|
codecs = codecs.split(',');
|
|
for (var i = 0; i < codecs.length; i++) {
|
|
var codec = codecs[i];
|
|
if (codec.indexOf('avc1') !== -1) {
|
|
level.videoCodec = this.avc1toavcoti(codec);
|
|
} else {
|
|
level.audioCodec = codec;
|
|
}
|
|
}
|
|
}
|
|
|
|
levels.push(level);
|
|
}
|
|
return levels;
|
|
}
|
|
}, {
|
|
key: 'avc1toavcoti',
|
|
value: function avc1toavcoti(codec) {
|
|
var result,
|
|
avcdata = codec.split('.');
|
|
if (avcdata.length > 2) {
|
|
result = avcdata.shift() + '.';
|
|
result += parseInt(avcdata.shift()).toString(16);
|
|
result += ('000' + parseInt(avcdata.shift()).toString(16)).substr(-4);
|
|
} else {
|
|
result = codec;
|
|
}
|
|
return result;
|
|
}
|
|
}, {
|
|
key: 'cloneObj',
|
|
value: function cloneObj(obj) {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
}, {
|
|
key: 'parseLevelPlaylist',
|
|
value: function parseLevelPlaylist(string, baseurl, id) {
|
|
var currentSN = 0,
|
|
totalduration = 0,
|
|
level = { url: baseurl, fragments: [], live: true, startSN: 0 },
|
|
levelkey = { method: null, key: null, iv: null, uri: null },
|
|
cc = 0,
|
|
programDateTime = null,
|
|
frag = null,
|
|
result,
|
|
regexp,
|
|
byteRangeEndOffset,
|
|
byteRangeStartOffset;
|
|
|
|
regexp = /(?:#EXT-X-(MEDIA-SEQUENCE):(\d+))|(?:#EXT-X-(TARGETDURATION):(\d+))|(?:#EXT-X-(KEY):(.*))|(?:#EXT(INF):([\d\.]+)[^\r\n]*([\r\n]+[^#|\r\n]+)?)|(?:#EXT-X-(BYTERANGE):([\d]+[@[\d]*)]*[\r\n]+([^#|\r\n]+)?|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(PROGRAM-DATE-TIME):(.*))/g;
|
|
while ((result = regexp.exec(string)) !== null) {
|
|
result.shift();
|
|
result = result.filter(function (n) {
|
|
return n !== undefined;
|
|
});
|
|
switch (result[0]) {
|
|
case 'MEDIA-SEQUENCE':
|
|
currentSN = level.startSN = parseInt(result[1]);
|
|
break;
|
|
case 'TARGETDURATION':
|
|
level.targetduration = parseFloat(result[1]);
|
|
break;
|
|
case 'ENDLIST':
|
|
level.live = false;
|
|
break;
|
|
case 'DIS':
|
|
cc++;
|
|
break;
|
|
case 'BYTERANGE':
|
|
var params = result[1].split('@');
|
|
if (params.length === 1) {
|
|
byteRangeStartOffset = byteRangeEndOffset;
|
|
} else {
|
|
byteRangeStartOffset = parseInt(params[1]);
|
|
}
|
|
byteRangeEndOffset = parseInt(params[0]) + byteRangeStartOffset;
|
|
if (frag && !frag.url) {
|
|
frag.byteRangeStartOffset = byteRangeStartOffset;
|
|
frag.byteRangeEndOffset = byteRangeEndOffset;
|
|
frag.url = this.resolve(result[2], baseurl);
|
|
}
|
|
break;
|
|
case 'INF':
|
|
var duration = parseFloat(result[1]);
|
|
if (!isNaN(duration)) {
|
|
var fragdecryptdata,
|
|
sn = currentSN++;
|
|
if (levelkey.method && levelkey.uri && !levelkey.iv) {
|
|
fragdecryptdata = this.cloneObj(levelkey);
|
|
var uint8View = new Uint8Array(16);
|
|
for (var i = 12; i < 16; i++) {
|
|
uint8View[i] = sn >> 8 * (15 - i) & 0xff;
|
|
}
|
|
fragdecryptdata.iv = uint8View;
|
|
} else {
|
|
fragdecryptdata = levelkey;
|
|
}
|
|
var url = result[2] ? this.resolve(result[2], baseurl) : null;
|
|
frag = { url: url, duration: duration, start: totalduration, sn: sn, level: id, cc: cc, byteRangeStartOffset: byteRangeStartOffset, byteRangeEndOffset: byteRangeEndOffset, decryptdata: fragdecryptdata, programDateTime: programDateTime };
|
|
level.fragments.push(frag);
|
|
totalduration += duration;
|
|
byteRangeStartOffset = null;
|
|
programDateTime = null;
|
|
}
|
|
break;
|
|
case 'KEY':
|
|
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
|
|
var decryptparams = result[1];
|
|
var keyAttrs = new _attrList2.default(decryptparams);
|
|
var decryptmethod = keyAttrs.enumeratedString('METHOD'),
|
|
decrypturi = keyAttrs.URI,
|
|
decryptiv = keyAttrs.hexadecimalInteger('IV');
|
|
if (decryptmethod) {
|
|
levelkey = { method: null, key: null, iv: null, uri: null };
|
|
if (decrypturi && decryptmethod === 'AES-128') {
|
|
levelkey.method = decryptmethod;
|
|
// URI to get the key
|
|
levelkey.uri = this.resolve(decrypturi, baseurl);
|
|
levelkey.key = null;
|
|
// Initialization Vector (IV)
|
|
levelkey.iv = decryptiv;
|
|
}
|
|
}
|
|
break;
|
|
case 'PROGRAM-DATE-TIME':
|
|
programDateTime = new Date(Date.parse(result[1]));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
//logger.log('found ' + level.fragments.length + ' fragments');
|
|
if (frag && !frag.url) {
|
|
level.fragments.pop();
|
|
totalduration -= frag.duration;
|
|
}
|
|
level.totalduration = totalduration;
|
|
level.endSN = currentSN - 1;
|
|
return level;
|
|
}
|
|
}, {
|
|
key: 'loadsuccess',
|
|
value: function loadsuccess(event, stats) {
|
|
var target = event.currentTarget,
|
|
string = target.responseText,
|
|
url = target.responseURL,
|
|
id = this.id,
|
|
id2 = this.id2,
|
|
hls = this.hls,
|
|
levels;
|
|
// responseURL not supported on some browsers (it is used to detect URL redirection)
|
|
if (url === undefined) {
|
|
// fallback to initial URL
|
|
url = this.url;
|
|
}
|
|
stats.tload = performance.now();
|
|
stats.mtime = new Date(target.getResponseHeader('Last-Modified'));
|
|
if (string.indexOf('#EXTM3U') === 0) {
|
|
if (string.indexOf('#EXTINF:') > 0) {
|
|
// 1 level playlist
|
|
// if first request, fire manifest loaded event, level will be reloaded afterwards
|
|
// (this is to have a uniform logic for 1 level/multilevel playlists)
|
|
if (this.id === null) {
|
|
hls.trigger(_events2.default.MANIFEST_LOADED, { levels: [{ url: url }], url: url, stats: stats });
|
|
} else {
|
|
var levelDetails = this.parseLevelPlaylist(string, url, id);
|
|
stats.tparsed = performance.now();
|
|
hls.trigger(_events2.default.LEVEL_LOADED, { details: levelDetails, level: id, id: id2, stats: stats });
|
|
}
|
|
} else {
|
|
levels = this.parseMasterPlaylist(string, url);
|
|
// multi level playlist, parse level info
|
|
if (levels.length) {
|
|
hls.trigger(_events2.default.MANIFEST_LOADED, { levels: levels, url: url, stats: stats });
|
|
} else {
|
|
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no level found in manifest' });
|
|
}
|
|
}
|
|
} else {
|
|
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: _errors.ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no EXTM3U delimiter' });
|
|
}
|
|
}
|
|
}, {
|
|
key: 'loaderror',
|
|
value: function loaderror(event) {
|
|
var details, fatal;
|
|
if (this.id === null) {
|
|
details = _errors.ErrorDetails.MANIFEST_LOAD_ERROR;
|
|
fatal = true;
|
|
} else {
|
|
details = _errors.ErrorDetails.LEVEL_LOAD_ERROR;
|
|
fatal = false;
|
|
}
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: this.url, loader: this.loader, response: event.currentTarget, level: this.id, id: this.id2 });
|
|
}
|
|
}, {
|
|
key: 'loadtimeout',
|
|
value: function loadtimeout() {
|
|
var details, fatal;
|
|
if (this.id === null) {
|
|
details = _errors.ErrorDetails.MANIFEST_LOAD_TIMEOUT;
|
|
fatal = true;
|
|
} else {
|
|
details = _errors.ErrorDetails.LEVEL_LOAD_TIMEOUT;
|
|
fatal = false;
|
|
}
|
|
if (this.loader) {
|
|
this.loader.abort();
|
|
}
|
|
this.hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: this.url, loader: this.loader, level: this.id, id: this.id2 });
|
|
}
|
|
}]);
|
|
|
|
return PlaylistLoader;
|
|
}(_eventHandler2.default);
|
|
|
|
exports.default = PlaylistLoader;
|
|
|
|
},{"../errors":19,"../event-handler":20,"../events":21,"../utils/attr-list":31,"../utils/url":35}],28:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
/**
|
|
* Generate MP4 Box
|
|
*/
|
|
|
|
//import Hex from '../utils/hex';
|
|
|
|
var MP4 = function () {
|
|
function MP4() {
|
|
_classCallCheck(this, MP4);
|
|
}
|
|
|
|
_createClass(MP4, null, [{
|
|
key: 'init',
|
|
value: function init() {
|
|
MP4.types = {
|
|
avc1: [], // codingname
|
|
avcC: [],
|
|
btrt: [],
|
|
dinf: [],
|
|
dref: [],
|
|
esds: [],
|
|
ftyp: [],
|
|
hdlr: [],
|
|
mdat: [],
|
|
mdhd: [],
|
|
mdia: [],
|
|
mfhd: [],
|
|
minf: [],
|
|
moof: [],
|
|
moov: [],
|
|
mp4a: [],
|
|
mvex: [],
|
|
mvhd: [],
|
|
sdtp: [],
|
|
stbl: [],
|
|
stco: [],
|
|
stsc: [],
|
|
stsd: [],
|
|
stsz: [],
|
|
stts: [],
|
|
tfdt: [],
|
|
tfhd: [],
|
|
traf: [],
|
|
trak: [],
|
|
trun: [],
|
|
trex: [],
|
|
tkhd: [],
|
|
vmhd: [],
|
|
smhd: []
|
|
};
|
|
|
|
var i;
|
|
for (i in MP4.types) {
|
|
if (MP4.types.hasOwnProperty(i)) {
|
|
MP4.types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
|
|
}
|
|
}
|
|
|
|
var videoHdlr = 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'
|
|
]);
|
|
|
|
var audioHdlr = 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'
|
|
]);
|
|
|
|
MP4.HDLR_TYPES = {
|
|
'video': videoHdlr,
|
|
'audio': audioHdlr
|
|
};
|
|
|
|
var 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
|
|
]);
|
|
|
|
var stco = new Uint8Array([0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00 // entry_count
|
|
]);
|
|
|
|
MP4.STTS = MP4.STSC = MP4.STCO = stco;
|
|
|
|
MP4.STSZ = new Uint8Array([0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
|
0x00, 0x00, 0x00, 0x00]);
|
|
// sample_count
|
|
MP4.VMHD = new Uint8Array([0x00, // version
|
|
0x00, 0x00, 0x01, // flags
|
|
0x00, 0x00, // graphicsmode
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor
|
|
]);
|
|
MP4.SMHD = new Uint8Array([0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, // balance
|
|
0x00, 0x00 // reserved
|
|
]);
|
|
|
|
MP4.STSD = new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x01]); // entry_count
|
|
|
|
var majorBrand = new Uint8Array([105, 115, 111, 109]); // isom
|
|
var avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1
|
|
var minorVersion = new Uint8Array([0, 0, 0, 1]);
|
|
|
|
MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand);
|
|
MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref));
|
|
}
|
|
}, {
|
|
key: 'box',
|
|
value: function box(type) {
|
|
var payload = Array.prototype.slice.call(arguments, 1),
|
|
size = 8,
|
|
i = payload.length,
|
|
len = i,
|
|
result;
|
|
// calculate the total size we need to allocate
|
|
while (i--) {
|
|
size += payload[i].byteLength;
|
|
}
|
|
result = new Uint8Array(size);
|
|
result[0] = size >> 24 & 0xff;
|
|
result[1] = size >> 16 & 0xff;
|
|
result[2] = size >> 8 & 0xff;
|
|
result[3] = size & 0xff;
|
|
result.set(type, 4);
|
|
// copy the payload into the result
|
|
for (i = 0, size = 8; i < len; i++) {
|
|
// copy payload[i] array @ offset size
|
|
result.set(payload[i], size);
|
|
size += payload[i].byteLength;
|
|
}
|
|
return result;
|
|
}
|
|
}, {
|
|
key: 'hdlr',
|
|
value: function hdlr(type) {
|
|
return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]);
|
|
}
|
|
}, {
|
|
key: 'mdat',
|
|
value: function mdat(data) {
|
|
return MP4.box(MP4.types.mdat, data);
|
|
}
|
|
}, {
|
|
key: 'mdhd',
|
|
value: function mdhd(timescale, duration) {
|
|
return MP4.box(MP4.types.mdhd, new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
timescale >> 24 & 0xFF, timescale >> 16 & 0xFF, timescale >> 8 & 0xFF, timescale & 0xFF, // timescale
|
|
duration >> 24, duration >> 16 & 0xFF, duration >> 8 & 0xFF, duration & 0xFF, // duration
|
|
0x55, 0xc4, // 'und' language (undetermined)
|
|
0x00, 0x00]));
|
|
}
|
|
}, {
|
|
key: 'mdia',
|
|
value: function mdia(track) {
|
|
return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track));
|
|
}
|
|
}, {
|
|
key: 'mfhd',
|
|
value: function mfhd(sequenceNumber) {
|
|
return MP4.box(MP4.types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags
|
|
sequenceNumber >> 24, sequenceNumber >> 16 & 0xFF, sequenceNumber >> 8 & 0xFF, sequenceNumber & 0xFF]));
|
|
}
|
|
}, {
|
|
key: 'minf',
|
|
// sequence_number
|
|
value: function minf(track) {
|
|
if (track.type === 'audio') {
|
|
return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track));
|
|
} else {
|
|
return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track));
|
|
}
|
|
}
|
|
}, {
|
|
key: 'moof',
|
|
value: function moof(sn, baseMediaDecodeTime, track) {
|
|
return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track, baseMediaDecodeTime));
|
|
}
|
|
/**
|
|
* @param tracks... (optional) {array} the tracks associated with this movie
|
|
*/
|
|
|
|
}, {
|
|
key: 'moov',
|
|
value: function moov(tracks) {
|
|
var i = tracks.length,
|
|
boxes = [];
|
|
|
|
while (i--) {
|
|
boxes[i] = MP4.trak(tracks[i]);
|
|
}
|
|
|
|
return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(tracks[0].timescale, tracks[0].duration)].concat(boxes).concat(MP4.mvex(tracks)));
|
|
}
|
|
}, {
|
|
key: 'mvex',
|
|
value: function mvex(tracks) {
|
|
var i = tracks.length,
|
|
boxes = [];
|
|
|
|
while (i--) {
|
|
boxes[i] = MP4.trex(tracks[i]);
|
|
}
|
|
return MP4.box.apply(null, [MP4.types.mvex].concat(boxes));
|
|
}
|
|
}, {
|
|
key: 'mvhd',
|
|
value: function mvhd(timescale, duration) {
|
|
var bytes = new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x01, // creation_time
|
|
0x00, 0x00, 0x00, 0x02, // modification_time
|
|
timescale >> 24 & 0xFF, timescale >> 16 & 0xFF, timescale >> 8 & 0xFF, timescale & 0xFF, // timescale
|
|
duration >> 24 & 0xFF, duration >> 16 & 0xFF, duration >> 8 & 0xFF, 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 MP4.box(MP4.types.mvhd, bytes);
|
|
}
|
|
}, {
|
|
key: 'sdtp',
|
|
value: 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 MP4.box(MP4.types.sdtp, bytes);
|
|
}
|
|
}, {
|
|
key: 'stbl',
|
|
value: function stbl(track) {
|
|
return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO));
|
|
}
|
|
}, {
|
|
key: 'avc1',
|
|
value: function avc1(track) {
|
|
var sps = [],
|
|
pps = [],
|
|
i,
|
|
data,
|
|
len;
|
|
// assemble the SPSs
|
|
|
|
for (i = 0; i < track.sps.length; i++) {
|
|
data = track.sps[i];
|
|
len = data.byteLength;
|
|
sps.push(len >>> 8 & 0xFF);
|
|
sps.push(len & 0xFF);
|
|
sps = sps.concat(Array.prototype.slice.call(data)); // SPS
|
|
}
|
|
|
|
// assemble the PPSs
|
|
for (i = 0; i < track.pps.length; i++) {
|
|
data = track.pps[i];
|
|
len = data.byteLength;
|
|
pps.push(len >>> 8 & 0xFF);
|
|
pps.push(len & 0xFF);
|
|
pps = pps.concat(Array.prototype.slice.call(data));
|
|
}
|
|
|
|
var avcc = MP4.box(MP4.types.avcC, new Uint8Array([0x01, // version
|
|
sps[3], // profile
|
|
sps[4], // profile compat
|
|
sps[5], // level
|
|
0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes
|
|
0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets
|
|
].concat(sps).concat([track.pps.length // numOfPictureParameterSets
|
|
]).concat(pps))),
|
|
// "PPS"
|
|
width = track.width,
|
|
height = track.height;
|
|
//console.log('avcc:' + Hex.hexDump(avcc));
|
|
return MP4.box(MP4.types.avc1, new Uint8Array([0x00, 0x00, 0x00, // reserved
|
|
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
|
|
width >> 8 & 0xFF, width & 0xff, // width
|
|
height >> 8 & 0xFF, height & 0xff, // height
|
|
0x00, 0x48, 0x00, 0x00, // horizresolution
|
|
0x00, 0x48, 0x00, 0x00, // vertresolution
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // frame_count
|
|
0x12, 0x64, 0x61, 0x69, 0x6C, //dailymotion/hls.js
|
|
0x79, 0x6D, 0x6F, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x68, 0x6C, 0x73, 0x2E, 0x6A, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compressorname
|
|
0x00, 0x18, // depth = 24
|
|
0x11, 0x11]), // pre_defined = -1
|
|
avcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
|
|
0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
|
|
0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate
|
|
);
|
|
}
|
|
}, {
|
|
key: 'esds',
|
|
value: function esds(track) {
|
|
var configlen = track.config.length;
|
|
return new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
|
|
0x03, // descriptor_type
|
|
0x17 + configlen, // length
|
|
0x00, 0x01, //es_id
|
|
0x00, // stream_priority
|
|
|
|
0x04, // descriptor_type
|
|
0x0f + configlen, // length
|
|
0x40, //codec : mpeg4_audio
|
|
0x15, // stream_type
|
|
0x00, 0x00, 0x00, // buffer_size
|
|
0x00, 0x00, 0x00, 0x00, // maxBitrate
|
|
0x00, 0x00, 0x00, 0x00, // avgBitrate
|
|
|
|
0x05 // descriptor_type
|
|
].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor
|
|
}
|
|
}, {
|
|
key: 'mp4a',
|
|
value: function mp4a(track) {
|
|
var audiosamplerate = track.audiosamplerate;
|
|
return MP4.box(MP4.types.mp4a, new Uint8Array([0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // data_reference_index
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, track.channelCount, // channelcount
|
|
0x00, 0x10, // sampleSize:16bits
|
|
0x00, 0x00, 0x00, 0x00, // reserved2
|
|
audiosamplerate >> 8 & 0xFF, audiosamplerate & 0xff, //
|
|
0x00, 0x00]), MP4.box(MP4.types.esds, MP4.esds(track)));
|
|
}
|
|
}, {
|
|
key: 'stsd',
|
|
value: function stsd(track) {
|
|
if (track.type === 'audio') {
|
|
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track));
|
|
} else {
|
|
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track));
|
|
}
|
|
}
|
|
}, {
|
|
key: 'tkhd',
|
|
value: function tkhd(track) {
|
|
var id = track.id,
|
|
duration = track.duration,
|
|
width = track.width,
|
|
height = track.height;
|
|
return MP4.box(MP4.types.tkhd, new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x07, // flags
|
|
0x00, 0x00, 0x00, 0x00, // creation_time
|
|
0x00, 0x00, 0x00, 0x00, // modification_time
|
|
id >> 24 & 0xFF, id >> 16 & 0xFF, id >> 8 & 0xFF, id & 0xFF, // track_ID
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
duration >> 24, duration >> 16 & 0xFF, duration >> 8 & 0xFF, duration & 0xFF, // duration
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, // layer
|
|
0x00, 0x00, // alternate_group
|
|
0x00, 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
|
|
width >> 8 & 0xFF, width & 0xFF, 0x00, 0x00, // width
|
|
height >> 8 & 0xFF, height & 0xFF, 0x00, 0x00 // height
|
|
]));
|
|
}
|
|
}, {
|
|
key: 'traf',
|
|
value: function traf(track, baseMediaDecodeTime) {
|
|
var sampleDependencyTable = MP4.sdtp(track),
|
|
id = track.id;
|
|
return MP4.box(MP4.types.traf, MP4.box(MP4.types.tfhd, new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
id >> 24, id >> 16 & 0XFF, id >> 8 & 0XFF, id & 0xFF])), // track_ID
|
|
MP4.box(MP4.types.tfdt, new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
baseMediaDecodeTime >> 24, baseMediaDecodeTime >> 16 & 0XFF, baseMediaDecodeTime >> 8 & 0XFF, baseMediaDecodeTime & 0xFF])), // baseMediaDecodeTime
|
|
MP4.trun(track, sampleDependencyTable.length + 16 + // tfhd
|
|
16 + // tfdt
|
|
8 + // traf header
|
|
16 + // mfhd
|
|
8 + // moof header
|
|
8), // mdat header
|
|
sampleDependencyTable);
|
|
}
|
|
|
|
/**
|
|
* Generate a track box.
|
|
* @param track {object} a track definition
|
|
* @return {Uint8Array} the track box
|
|
*/
|
|
|
|
}, {
|
|
key: 'trak',
|
|
value: function trak(track) {
|
|
track.duration = track.duration || 0xffffffff;
|
|
return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track));
|
|
}
|
|
}, {
|
|
key: 'trex',
|
|
value: function trex(track) {
|
|
var id = track.id;
|
|
return MP4.box(MP4.types.trex, new Uint8Array([0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
id >> 24, id >> 16 & 0XFF, id >> 8 & 0XFF, 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
|
|
]));
|
|
}
|
|
}, {
|
|
key: 'trun',
|
|
value: function trun(track, offset) {
|
|
var samples = track.samples || [],
|
|
len = samples.length,
|
|
arraylen = 12 + 16 * len,
|
|
array = new Uint8Array(arraylen),
|
|
i,
|
|
sample,
|
|
duration,
|
|
size,
|
|
flags,
|
|
cts;
|
|
offset += 8 + arraylen;
|
|
array.set([0x00, // version 0
|
|
0x00, 0x0f, 0x01, // flags
|
|
len >>> 24 & 0xFF, len >>> 16 & 0xFF, len >>> 8 & 0xFF, len & 0xFF, // sample_count
|
|
offset >>> 24 & 0xFF, offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF // data_offset
|
|
], 0);
|
|
for (i = 0; i < len; i++) {
|
|
sample = samples[i];
|
|
duration = sample.duration;
|
|
size = sample.size;
|
|
flags = sample.flags;
|
|
cts = sample.cts;
|
|
array.set([duration >>> 24 & 0xFF, duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, // sample_duration
|
|
size >>> 24 & 0xFF, size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, // sample_size
|
|
flags.isLeading << 2 | flags.dependsOn, flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.paddingValue << 1 | flags.isNonSync, flags.degradPrio & 0xF0 << 8, flags.degradPrio & 0x0F, // sample_flags
|
|
cts >>> 24 & 0xFF, cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF // sample_composition_time_offset
|
|
], 12 + 16 * i);
|
|
}
|
|
return MP4.box(MP4.types.trun, array);
|
|
}
|
|
}, {
|
|
key: 'initSegment',
|
|
value: function initSegment(tracks) {
|
|
if (!MP4.types) {
|
|
MP4.init();
|
|
}
|
|
var movie = MP4.moov(tracks),
|
|
result;
|
|
result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength);
|
|
result.set(MP4.FTYP);
|
|
result.set(movie, MP4.FTYP.byteLength);
|
|
return result;
|
|
}
|
|
}]);
|
|
|
|
return MP4;
|
|
}();
|
|
|
|
exports.default = MP4;
|
|
|
|
},{}],29:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* fMP4 remuxer
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _mp4Generator = require('../remux/mp4-generator');
|
|
|
|
var _mp4Generator2 = _interopRequireDefault(_mp4Generator);
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var MP4Remuxer = function () {
|
|
function MP4Remuxer(observer) {
|
|
_classCallCheck(this, MP4Remuxer);
|
|
|
|
this.observer = observer;
|
|
this.ISGenerated = false;
|
|
this.PES2MP4SCALEFACTOR = 4;
|
|
this.PES_TIMESCALE = 90000;
|
|
this.MP4_TIMESCALE = this.PES_TIMESCALE / this.PES2MP4SCALEFACTOR;
|
|
}
|
|
|
|
_createClass(MP4Remuxer, [{
|
|
key: 'destroy',
|
|
value: function destroy() {}
|
|
}, {
|
|
key: 'insertDiscontinuity',
|
|
value: function insertDiscontinuity() {
|
|
this._initPTS = this._initDTS = this.nextAacPts = this.nextAvcDts = undefined;
|
|
}
|
|
}, {
|
|
key: 'switchLevel',
|
|
value: function switchLevel() {
|
|
this.ISGenerated = false;
|
|
}
|
|
}, {
|
|
key: 'remux',
|
|
value: function remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous) {
|
|
// generate Init Segment if needed
|
|
if (!this.ISGenerated) {
|
|
this.generateIS(audioTrack, videoTrack, timeOffset);
|
|
}
|
|
//logger.log('nb AVC samples:' + videoTrack.samples.length);
|
|
if (videoTrack.samples.length) {
|
|
this.remuxVideo(videoTrack, timeOffset, contiguous);
|
|
}
|
|
//logger.log('nb AAC samples:' + audioTrack.samples.length);
|
|
if (audioTrack.samples.length) {
|
|
this.remuxAudio(audioTrack, timeOffset, contiguous);
|
|
}
|
|
//logger.log('nb ID3 samples:' + audioTrack.samples.length);
|
|
if (id3Track.samples.length) {
|
|
this.remuxID3(id3Track, timeOffset);
|
|
}
|
|
//logger.log('nb ID3 samples:' + audioTrack.samples.length);
|
|
if (textTrack.samples.length) {
|
|
this.remuxText(textTrack, timeOffset);
|
|
}
|
|
//notify end of parsing
|
|
this.observer.trigger(_events2.default.FRAG_PARSED);
|
|
}
|
|
}, {
|
|
key: 'generateIS',
|
|
value: function generateIS(audioTrack, videoTrack, timeOffset) {
|
|
var observer = this.observer,
|
|
audioSamples = audioTrack.samples,
|
|
videoSamples = videoTrack.samples,
|
|
pesTimeScale = this.PES_TIMESCALE,
|
|
tracks = {},
|
|
data = { tracks: tracks, unique: false },
|
|
computePTSDTS = this._initPTS === undefined,
|
|
initPTS,
|
|
initDTS;
|
|
|
|
if (computePTSDTS) {
|
|
initPTS = initDTS = Infinity;
|
|
}
|
|
|
|
if (audioTrack.config && audioSamples.length) {
|
|
tracks.audio = {
|
|
container: 'audio/mp4',
|
|
codec: audioTrack.codec,
|
|
initSegment: _mp4Generator2.default.initSegment([audioTrack]),
|
|
metadata: {
|
|
channelCount: audioTrack.channelCount
|
|
}
|
|
};
|
|
if (computePTSDTS) {
|
|
// remember first PTS of this demuxing context. for audio, PTS + DTS ...
|
|
initPTS = initDTS = audioSamples[0].pts - pesTimeScale * timeOffset;
|
|
}
|
|
}
|
|
|
|
if (videoTrack.sps && videoTrack.pps && videoSamples.length) {
|
|
tracks.video = {
|
|
container: 'video/mp4',
|
|
codec: videoTrack.codec,
|
|
initSegment: _mp4Generator2.default.initSegment([videoTrack]),
|
|
metadata: {
|
|
width: videoTrack.width,
|
|
height: videoTrack.height
|
|
}
|
|
};
|
|
if (computePTSDTS) {
|
|
initPTS = Math.min(initPTS, videoSamples[0].pts - pesTimeScale * timeOffset);
|
|
initDTS = Math.min(initDTS, videoSamples[0].dts - pesTimeScale * timeOffset);
|
|
}
|
|
}
|
|
|
|
if (!Object.keys(tracks)) {
|
|
observer.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: false, reason: 'no audio/video samples found' });
|
|
} else {
|
|
observer.trigger(_events2.default.FRAG_PARSING_INIT_SEGMENT, data);
|
|
this.ISGenerated = true;
|
|
if (computePTSDTS) {
|
|
this._initPTS = initPTS;
|
|
this._initDTS = initDTS;
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'remuxVideo',
|
|
value: function remuxVideo(track, timeOffset, contiguous) {
|
|
var view,
|
|
offset = 8,
|
|
pesTimeScale = this.PES_TIMESCALE,
|
|
pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
|
|
avcSample,
|
|
mp4Sample,
|
|
mp4SampleLength,
|
|
unit,
|
|
mdat,
|
|
moof,
|
|
firstPTS,
|
|
firstDTS,
|
|
lastDTS,
|
|
pts,
|
|
dts,
|
|
ptsnorm,
|
|
dtsnorm,
|
|
flags,
|
|
samples = [];
|
|
/* concatenate the video data and construct the mdat in place
|
|
(need 8 more bytes to fill length and mpdat type) */
|
|
mdat = new Uint8Array(track.len + 4 * track.nbNalu + 8);
|
|
view = new DataView(mdat.buffer);
|
|
view.setUint32(0, mdat.byteLength);
|
|
mdat.set(_mp4Generator2.default.types.mdat, 4);
|
|
while (track.samples.length) {
|
|
avcSample = track.samples.shift();
|
|
mp4SampleLength = 0;
|
|
// convert NALU bitstream to MP4 format (prepend NALU with size field)
|
|
while (avcSample.units.units.length) {
|
|
unit = avcSample.units.units.shift();
|
|
view.setUint32(offset, unit.data.byteLength);
|
|
offset += 4;
|
|
mdat.set(unit.data, offset);
|
|
offset += unit.data.byteLength;
|
|
mp4SampleLength += 4 + unit.data.byteLength;
|
|
}
|
|
pts = avcSample.pts - this._initDTS;
|
|
dts = avcSample.dts - this._initDTS;
|
|
// ensure DTS is not bigger than PTS
|
|
dts = Math.min(pts, dts);
|
|
//logger.log(`Video/PTS/DTS:${Math.round(pts/90)}/${Math.round(dts/90)}`);
|
|
// if not first AVC sample of video track, normalize PTS/DTS with previous sample value
|
|
// and ensure that sample duration is positive
|
|
if (lastDTS !== undefined) {
|
|
ptsnorm = this._PTSNormalize(pts, lastDTS);
|
|
dtsnorm = this._PTSNormalize(dts, lastDTS);
|
|
var sampleDuration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
|
if (sampleDuration <= 0) {
|
|
_logger.logger.log('invalid sample duration at PTS/DTS: ' + avcSample.pts + '/' + avcSample.dts + ':' + sampleDuration);
|
|
sampleDuration = 1;
|
|
}
|
|
mp4Sample.duration = sampleDuration;
|
|
} else {
|
|
var nextAvcDts = this.nextAvcDts,
|
|
delta;
|
|
// first AVC sample of video track, normalize PTS/DTS
|
|
ptsnorm = this._PTSNormalize(pts, nextAvcDts);
|
|
dtsnorm = this._PTSNormalize(dts, nextAvcDts);
|
|
delta = Math.round((dtsnorm - nextAvcDts) / 90);
|
|
// if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments
|
|
if (contiguous || Math.abs(delta) < 600) {
|
|
if (delta) {
|
|
if (delta > 1) {
|
|
_logger.logger.log('AVC:' + delta + ' ms hole between fragments detected,filling it');
|
|
} else if (delta < -1) {
|
|
_logger.logger.log('AVC:' + -delta + ' ms overlapping between fragments detected');
|
|
}
|
|
// set DTS to next DTS
|
|
dtsnorm = nextAvcDts;
|
|
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
|
|
ptsnorm = Math.max(ptsnorm - delta, dtsnorm);
|
|
_logger.logger.log('Video/PTS/DTS adjusted: ' + ptsnorm + '/' + dtsnorm + ',delta:' + delta);
|
|
}
|
|
}
|
|
// remember first PTS of our avcSamples, ensure value is positive
|
|
firstPTS = Math.max(0, ptsnorm);
|
|
firstDTS = Math.max(0, dtsnorm);
|
|
}
|
|
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${avcSample.pts}/${avcSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(avcSample.pts/4294967296).toFixed(3)}');
|
|
mp4Sample = {
|
|
size: mp4SampleLength,
|
|
duration: 0,
|
|
cts: (ptsnorm - dtsnorm) / pes2mp4ScaleFactor,
|
|
flags: {
|
|
isLeading: 0,
|
|
isDependedOn: 0,
|
|
hasRedundancy: 0,
|
|
degradPrio: 0
|
|
}
|
|
};
|
|
flags = mp4Sample.flags;
|
|
if (avcSample.key === true) {
|
|
// the current sample is a key frame
|
|
flags.dependsOn = 2;
|
|
flags.isNonSync = 0;
|
|
} else {
|
|
flags.dependsOn = 1;
|
|
flags.isNonSync = 1;
|
|
}
|
|
samples.push(mp4Sample);
|
|
lastDTS = dtsnorm;
|
|
}
|
|
var lastSampleDuration = 0;
|
|
if (samples.length >= 2) {
|
|
lastSampleDuration = samples[samples.length - 2].duration;
|
|
mp4Sample.duration = lastSampleDuration;
|
|
}
|
|
// next AVC sample DTS should be equal to last sample DTS + last sample duration
|
|
this.nextAvcDts = dtsnorm + lastSampleDuration * pes2mp4ScaleFactor;
|
|
track.len = 0;
|
|
track.nbNalu = 0;
|
|
if (samples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
|
flags = samples[0].flags;
|
|
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
|
|
// https://code.google.com/p/chromium/issues/detail?id=229412
|
|
flags.dependsOn = 2;
|
|
flags.isNonSync = 0;
|
|
}
|
|
track.samples = samples;
|
|
moof = _mp4Generator2.default.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
|
track.samples = [];
|
|
this.observer.trigger(_events2.default.FRAG_PARSING_DATA, {
|
|
data1: moof,
|
|
data2: mdat,
|
|
startPTS: firstPTS / pesTimeScale,
|
|
endPTS: (ptsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
|
|
startDTS: firstDTS / pesTimeScale,
|
|
endDTS: this.nextAvcDts / pesTimeScale,
|
|
type: 'video',
|
|
nb: samples.length
|
|
});
|
|
}
|
|
}, {
|
|
key: 'remuxAudio',
|
|
value: function remuxAudio(track, timeOffset, contiguous) {
|
|
var view,
|
|
offset = 8,
|
|
pesTimeScale = this.PES_TIMESCALE,
|
|
mp4timeScale = track.timescale,
|
|
pes2mp4ScaleFactor = pesTimeScale / mp4timeScale,
|
|
aacSample,
|
|
mp4Sample,
|
|
unit,
|
|
mdat,
|
|
moof,
|
|
firstPTS,
|
|
firstDTS,
|
|
lastDTS,
|
|
pts,
|
|
dts,
|
|
ptsnorm,
|
|
dtsnorm,
|
|
samples = [],
|
|
samples0 = [];
|
|
|
|
track.samples.sort(function (a, b) {
|
|
return a.pts - b.pts;
|
|
});
|
|
samples0 = track.samples;
|
|
|
|
while (samples0.length) {
|
|
aacSample = samples0.shift();
|
|
unit = aacSample.unit;
|
|
pts = aacSample.pts - this._initDTS;
|
|
dts = aacSample.dts - this._initDTS;
|
|
//logger.log(`Audio/PTS:${Math.round(pts/90)}`);
|
|
// if not first sample
|
|
if (lastDTS !== undefined) {
|
|
ptsnorm = this._PTSNormalize(pts, lastDTS);
|
|
dtsnorm = this._PTSNormalize(dts, lastDTS);
|
|
// let's compute sample duration.
|
|
// there should be 1024 audio samples in one AAC frame
|
|
mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
|
if (Math.abs(mp4Sample.duration - 1024) > 10) {
|
|
// not expected to happen ...
|
|
_logger.logger.log('invalid AAC sample duration at PTS ' + Math.round(pts / 90) + ',should be 1024,found :' + Math.round(mp4Sample.duration));
|
|
}
|
|
mp4Sample.duration = 1024;
|
|
dtsnorm = 1024 * pes2mp4ScaleFactor + lastDTS;
|
|
} else {
|
|
var nextAacPts = this.nextAacPts,
|
|
delta;
|
|
ptsnorm = this._PTSNormalize(pts, nextAacPts);
|
|
dtsnorm = this._PTSNormalize(dts, nextAacPts);
|
|
delta = Math.round(1000 * (ptsnorm - nextAacPts) / pesTimeScale);
|
|
// if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments
|
|
if (contiguous || Math.abs(delta) < 600) {
|
|
// log delta
|
|
if (delta) {
|
|
if (delta > 0) {
|
|
_logger.logger.log(delta + ' ms hole between AAC samples detected,filling it');
|
|
// if we have frame overlap, overlapping for more than half a frame duraion
|
|
} else if (delta < -12) {
|
|
// drop overlapping audio frames... browser will deal with it
|
|
_logger.logger.log(-delta + ' ms overlapping between AAC samples detected, drop frame');
|
|
track.len -= unit.byteLength;
|
|
continue;
|
|
}
|
|
// set DTS to next DTS
|
|
ptsnorm = dtsnorm = nextAacPts;
|
|
}
|
|
}
|
|
// remember first PTS of our aacSamples, ensure value is positive
|
|
firstPTS = Math.max(0, ptsnorm);
|
|
firstDTS = Math.max(0, dtsnorm);
|
|
if (track.len > 0) {
|
|
/* concatenate the audio data and construct the mdat in place
|
|
(need 8 more bytes to fill length and mdat type) */
|
|
mdat = new Uint8Array(track.len + 8);
|
|
view = new DataView(mdat.buffer);
|
|
view.setUint32(0, mdat.byteLength);
|
|
mdat.set(_mp4Generator2.default.types.mdat, 4);
|
|
} else {
|
|
// no audio samples
|
|
return;
|
|
}
|
|
}
|
|
mdat.set(unit, offset);
|
|
offset += unit.byteLength;
|
|
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${aacSample.pts}/${aacSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(aacSample.pts/4294967296).toFixed(3)}');
|
|
mp4Sample = {
|
|
size: unit.byteLength,
|
|
cts: 0,
|
|
duration: 0,
|
|
flags: {
|
|
isLeading: 0,
|
|
isDependedOn: 0,
|
|
hasRedundancy: 0,
|
|
degradPrio: 0,
|
|
dependsOn: 1
|
|
}
|
|
};
|
|
samples.push(mp4Sample);
|
|
lastDTS = dtsnorm;
|
|
}
|
|
var lastSampleDuration = 0;
|
|
var nbSamples = samples.length;
|
|
//set last sample duration as being identical to previous sample
|
|
if (nbSamples >= 2) {
|
|
lastSampleDuration = samples[nbSamples - 2].duration;
|
|
mp4Sample.duration = lastSampleDuration;
|
|
}
|
|
if (nbSamples) {
|
|
// next aac sample PTS should be equal to last sample PTS + duration
|
|
this.nextAacPts = ptsnorm + pes2mp4ScaleFactor * lastSampleDuration;
|
|
//logger.log('Audio/PTS/PTSend:' + aacSample.pts.toFixed(0) + '/' + this.nextAacDts.toFixed(0));
|
|
track.len = 0;
|
|
track.samples = samples;
|
|
moof = _mp4Generator2.default.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
|
track.samples = [];
|
|
this.observer.trigger(_events2.default.FRAG_PARSING_DATA, {
|
|
data1: moof,
|
|
data2: mdat,
|
|
startPTS: firstPTS / pesTimeScale,
|
|
endPTS: this.nextAacPts / pesTimeScale,
|
|
startDTS: firstDTS / pesTimeScale,
|
|
endDTS: (dtsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
|
|
type: 'audio',
|
|
nb: nbSamples
|
|
});
|
|
}
|
|
}
|
|
}, {
|
|
key: 'remuxID3',
|
|
value: function remuxID3(track, timeOffset) {
|
|
var length = track.samples.length,
|
|
sample;
|
|
// consume samples
|
|
if (length) {
|
|
for (var index = 0; index < length; index++) {
|
|
sample = track.samples[index];
|
|
// setting id3 pts, dts to relative time
|
|
// using this._initPTS and this._initDTS to calculate relative time
|
|
sample.pts = (sample.pts - this._initPTS) / this.PES_TIMESCALE;
|
|
sample.dts = (sample.dts - this._initDTS) / this.PES_TIMESCALE;
|
|
}
|
|
this.observer.trigger(_events2.default.FRAG_PARSING_METADATA, {
|
|
samples: track.samples
|
|
});
|
|
}
|
|
|
|
track.samples = [];
|
|
timeOffset = timeOffset;
|
|
}
|
|
}, {
|
|
key: 'remuxText',
|
|
value: function remuxText(track, timeOffset) {
|
|
track.samples.sort(function (a, b) {
|
|
return a.pts - b.pts;
|
|
});
|
|
|
|
var length = track.samples.length,
|
|
sample;
|
|
// consume samples
|
|
if (length) {
|
|
for (var index = 0; index < length; index++) {
|
|
sample = track.samples[index];
|
|
// setting text pts, dts to relative time
|
|
// using this._initPTS and this._initDTS to calculate relative time
|
|
sample.pts = (sample.pts - this._initPTS) / this.PES_TIMESCALE;
|
|
}
|
|
this.observer.trigger(_events2.default.FRAG_PARSING_USERDATA, {
|
|
samples: track.samples
|
|
});
|
|
}
|
|
|
|
track.samples = [];
|
|
timeOffset = timeOffset;
|
|
}
|
|
}, {
|
|
key: '_PTSNormalize',
|
|
value: function _PTSNormalize(value, reference) {
|
|
var offset;
|
|
if (reference === undefined) {
|
|
return value;
|
|
}
|
|
if (reference < value) {
|
|
// - 2^33
|
|
offset = -8589934592;
|
|
} else {
|
|
// + 2^33
|
|
offset = 8589934592;
|
|
}
|
|
/* PTS is 33bit (from 0 to 2^33 -1)
|
|
if diff between value and reference is bigger than half of the amplitude (2^32) then it means that
|
|
PTS looping occured. fill the gap */
|
|
while (Math.abs(value - reference) > 4294967296) {
|
|
value += offset;
|
|
}
|
|
return value;
|
|
}
|
|
}, {
|
|
key: 'passthrough',
|
|
get: function get() {
|
|
return false;
|
|
}
|
|
}, {
|
|
key: 'timescale',
|
|
get: function get() {
|
|
return this.MP4_TIMESCALE;
|
|
}
|
|
}]);
|
|
|
|
return MP4Remuxer;
|
|
}();
|
|
|
|
exports.default = MP4Remuxer;
|
|
|
|
},{"../errors":19,"../events":21,"../remux/mp4-generator":28,"../utils/logger":34}],30:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* passthrough remuxer
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var PassThroughRemuxer = function () {
|
|
function PassThroughRemuxer(observer) {
|
|
_classCallCheck(this, PassThroughRemuxer);
|
|
|
|
this.observer = observer;
|
|
this.ISGenerated = false;
|
|
}
|
|
|
|
_createClass(PassThroughRemuxer, [{
|
|
key: 'destroy',
|
|
value: function destroy() {}
|
|
}, {
|
|
key: 'insertDiscontinuity',
|
|
value: function insertDiscontinuity() {}
|
|
}, {
|
|
key: 'switchLevel',
|
|
value: function switchLevel() {
|
|
this.ISGenerated = false;
|
|
}
|
|
}, {
|
|
key: 'remux',
|
|
value: function remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, rawData) {
|
|
var observer = this.observer;
|
|
// generate Init Segment if needed
|
|
if (!this.ISGenerated) {
|
|
var tracks = {},
|
|
data = { tracks: tracks, unique: true },
|
|
track = videoTrack,
|
|
codec = track.codec;
|
|
|
|
if (codec) {
|
|
data.tracks.video = {
|
|
container: track.container,
|
|
codec: codec,
|
|
metadata: {
|
|
width: track.width,
|
|
height: track.height
|
|
}
|
|
};
|
|
}
|
|
|
|
track = audioTrack;
|
|
codec = track.codec;
|
|
if (codec) {
|
|
data.tracks.audio = {
|
|
container: track.container,
|
|
codec: codec,
|
|
metadata: {
|
|
channelCount: track.channelCount
|
|
}
|
|
};
|
|
}
|
|
this.ISGenerated = true;
|
|
observer.trigger(_events2.default.FRAG_PARSING_INIT_SEGMENT, data);
|
|
}
|
|
observer.trigger(_events2.default.FRAG_PARSING_DATA, {
|
|
data1: rawData,
|
|
startPTS: timeOffset,
|
|
startDTS: timeOffset,
|
|
type: 'audiovideo',
|
|
nb: 1
|
|
});
|
|
}
|
|
}, {
|
|
key: 'passthrough',
|
|
get: function get() {
|
|
return true;
|
|
}
|
|
}, {
|
|
key: 'timescale',
|
|
get: function get() {
|
|
return 0;
|
|
}
|
|
}]);
|
|
|
|
return PassThroughRemuxer;
|
|
}();
|
|
|
|
exports.default = PassThroughRemuxer;
|
|
|
|
},{"../events":21}],31:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
|
|
|
|
var AttrList = function () {
|
|
function AttrList(attrs) {
|
|
_classCallCheck(this, AttrList);
|
|
|
|
if (typeof attrs === 'string') {
|
|
attrs = AttrList.parseAttrList(attrs);
|
|
}
|
|
for (var attr in attrs) {
|
|
if (attrs.hasOwnProperty(attr)) {
|
|
this[attr] = attrs[attr];
|
|
}
|
|
}
|
|
}
|
|
|
|
_createClass(AttrList, [{
|
|
key: 'decimalInteger',
|
|
value: function decimalInteger(attrName) {
|
|
var intValue = parseInt(this[attrName], 10);
|
|
if (intValue > Number.MAX_SAFE_INTEGER) {
|
|
return Infinity;
|
|
}
|
|
return intValue;
|
|
}
|
|
}, {
|
|
key: 'hexadecimalInteger',
|
|
value: function hexadecimalInteger(attrName) {
|
|
if (this[attrName]) {
|
|
var stringValue = (this[attrName] || '0x').slice(2);
|
|
stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
|
|
|
|
var value = new Uint8Array(stringValue.length / 2);
|
|
for (var i = 0; i < stringValue.length / 2; i++) {
|
|
value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
|
|
}
|
|
return value;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'hexadecimalIntegerAsNumber',
|
|
value: function hexadecimalIntegerAsNumber(attrName) {
|
|
var intValue = parseInt(this[attrName], 16);
|
|
if (intValue > Number.MAX_SAFE_INTEGER) {
|
|
return Infinity;
|
|
}
|
|
return intValue;
|
|
}
|
|
}, {
|
|
key: 'decimalFloatingPoint',
|
|
value: function decimalFloatingPoint(attrName) {
|
|
return parseFloat(this[attrName]);
|
|
}
|
|
}, {
|
|
key: 'enumeratedString',
|
|
value: function enumeratedString(attrName) {
|
|
return this[attrName];
|
|
}
|
|
}, {
|
|
key: 'decimalResolution',
|
|
value: function decimalResolution(attrName) {
|
|
var res = /^(\d+)x(\d+)$/.exec(this[attrName]);
|
|
if (res === null) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
width: parseInt(res[1], 10),
|
|
height: parseInt(res[2], 10)
|
|
};
|
|
}
|
|
}], [{
|
|
key: 'parseAttrList',
|
|
value: function parseAttrList(input) {
|
|
var re = /\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g;
|
|
var match,
|
|
attrs = {};
|
|
while ((match = re.exec(input)) !== null) {
|
|
var value = match[2],
|
|
quote = '"';
|
|
|
|
if (value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
attrs[match[1]] = value;
|
|
}
|
|
return attrs;
|
|
}
|
|
}]);
|
|
|
|
return AttrList;
|
|
}();
|
|
|
|
exports.default = AttrList;
|
|
|
|
},{}],32:[function(require,module,exports){
|
|
"use strict";
|
|
|
|
var BinarySearch = {
|
|
/**
|
|
* Searches for an item in an array which matches a certain condition.
|
|
* This requires the condition to only match one item in the array,
|
|
* and for the array to be ordered.
|
|
*
|
|
* @param {Array} list The array to search.
|
|
* @param {Function} comparisonFunction
|
|
* Called and provided a candidate item as the first argument.
|
|
* Should return:
|
|
* > -1 if the item should be located at a lower index than the provided item.
|
|
* > 1 if the item should be located at a higher index than the provided item.
|
|
* > 0 if the item is the item you're looking for.
|
|
*
|
|
* @return {*} The object if it is found or null otherwise.
|
|
*/
|
|
search: function search(list, comparisonFunction) {
|
|
var minIndex = 0;
|
|
var maxIndex = list.length - 1;
|
|
var currentIndex = null;
|
|
var currentElement = null;
|
|
|
|
while (minIndex <= maxIndex) {
|
|
currentIndex = (minIndex + maxIndex) / 2 | 0;
|
|
currentElement = list[currentIndex];
|
|
|
|
var comparisonResult = comparisonFunction(currentElement);
|
|
if (comparisonResult > 0) {
|
|
minIndex = currentIndex + 1;
|
|
} else if (comparisonResult < 0) {
|
|
maxIndex = currentIndex - 1;
|
|
} else {
|
|
return currentElement;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
module.exports = BinarySearch;
|
|
|
|
},{}],33:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }();
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
/*
|
|
* CEA-708 interpreter
|
|
*/
|
|
|
|
var CEA708Interpreter = function () {
|
|
function CEA708Interpreter() {
|
|
_classCallCheck(this, CEA708Interpreter);
|
|
}
|
|
|
|
_createClass(CEA708Interpreter, [{
|
|
key: 'attach',
|
|
value: function attach(media) {
|
|
this.media = media;
|
|
this.display = [];
|
|
this.memory = [];
|
|
}
|
|
}, {
|
|
key: 'detach',
|
|
value: function detach() {
|
|
this.clear();
|
|
}
|
|
}, {
|
|
key: 'destroy',
|
|
value: function destroy() {}
|
|
}, {
|
|
key: '_createCue',
|
|
value: function _createCue() {
|
|
var VTTCue = window.VTTCue || window.TextTrackCue;
|
|
|
|
var cue = this.cue = new VTTCue(-1, -1, '');
|
|
cue.text = '';
|
|
cue.pauseOnExit = false;
|
|
|
|
// make sure it doesn't show up before it's ready
|
|
cue.startTime = Number.MAX_VALUE;
|
|
|
|
// show it 'forever' once we do show it
|
|
// (we'll set the end time once we know it later)
|
|
cue.endTime = Number.MAX_VALUE;
|
|
|
|
this.memory.push(cue);
|
|
}
|
|
}, {
|
|
key: 'clear',
|
|
value: function clear() {
|
|
var textTrack = this._textTrack;
|
|
if (textTrack && textTrack.cues) {
|
|
while (textTrack.cues.length > 0) {
|
|
textTrack.removeCue(textTrack.cues[0]);
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'push',
|
|
value: function push(timestamp, bytes) {
|
|
if (!this.cue) {
|
|
this._createCue();
|
|
}
|
|
|
|
var count = bytes[0] & 31;
|
|
var position = 2;
|
|
var tmpByte, ccbyte1, ccbyte2, ccValid, ccType;
|
|
|
|
for (var j = 0; j < count; j++) {
|
|
tmpByte = bytes[position++];
|
|
ccbyte1 = 0x7F & bytes[position++];
|
|
ccbyte2 = 0x7F & bytes[position++];
|
|
ccValid = (4 & tmpByte) === 0 ? false : true;
|
|
ccType = 3 & tmpByte;
|
|
|
|
if (ccbyte1 === 0 && ccbyte2 === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (ccValid) {
|
|
if (ccType === 0) // || ccType === 1
|
|
{
|
|
// Standard Characters
|
|
if (0x20 & ccbyte1 || 0x40 & ccbyte1) {
|
|
this.cue.text += this._fromCharCode(ccbyte1) + this._fromCharCode(ccbyte2);
|
|
}
|
|
// Special Characters
|
|
else if ((ccbyte1 === 0x11 || ccbyte1 === 0x19) && ccbyte2 >= 0x30 && ccbyte2 <= 0x3F) {
|
|
// extended chars, e.g. musical note, accents
|
|
switch (ccbyte2) {
|
|
case 48:
|
|
this.cue.text += '®';
|
|
break;
|
|
case 49:
|
|
this.cue.text += '°';
|
|
break;
|
|
case 50:
|
|
this.cue.text += '½';
|
|
break;
|
|
case 51:
|
|
this.cue.text += '¿';
|
|
break;
|
|
case 52:
|
|
this.cue.text += '™';
|
|
break;
|
|
case 53:
|
|
this.cue.text += '¢';
|
|
break;
|
|
case 54:
|
|
this.cue.text += '';
|
|
break;
|
|
case 55:
|
|
this.cue.text += '£';
|
|
break;
|
|
case 56:
|
|
this.cue.text += '♪';
|
|
break;
|
|
case 57:
|
|
this.cue.text += ' ';
|
|
break;
|
|
case 58:
|
|
this.cue.text += 'è';
|
|
break;
|
|
case 59:
|
|
this.cue.text += 'â';
|
|
break;
|
|
case 60:
|
|
this.cue.text += 'ê';
|
|
break;
|
|
case 61:
|
|
this.cue.text += 'î';
|
|
break;
|
|
case 62:
|
|
this.cue.text += 'ô';
|
|
break;
|
|
case 63:
|
|
this.cue.text += 'û';
|
|
break;
|
|
}
|
|
}
|
|
if ((ccbyte1 === 0x11 || ccbyte1 === 0x19) && ccbyte2 >= 0x20 && ccbyte2 <= 0x2F) {
|
|
// Mid-row codes: color/underline
|
|
switch (ccbyte2) {
|
|
case 0x20:
|
|
// White
|
|
break;
|
|
case 0x21:
|
|
// White Underline
|
|
break;
|
|
case 0x22:
|
|
// Green
|
|
break;
|
|
case 0x23:
|
|
// Green Underline
|
|
break;
|
|
case 0x24:
|
|
// Blue
|
|
break;
|
|
case 0x25:
|
|
// Blue Underline
|
|
break;
|
|
case 0x26:
|
|
// Cyan
|
|
break;
|
|
case 0x27:
|
|
// Cyan Underline
|
|
break;
|
|
case 0x28:
|
|
// Red
|
|
break;
|
|
case 0x29:
|
|
// Red Underline
|
|
break;
|
|
case 0x2A:
|
|
// Yellow
|
|
break;
|
|
case 0x2B:
|
|
// Yellow Underline
|
|
break;
|
|
case 0x2C:
|
|
// Magenta
|
|
break;
|
|
case 0x2D:
|
|
// Magenta Underline
|
|
break;
|
|
case 0x2E:
|
|
// Italics
|
|
break;
|
|
case 0x2F:
|
|
// Italics Underline
|
|
break;
|
|
}
|
|
}
|
|
if ((ccbyte1 === 0x14 || ccbyte1 === 0x1C) && ccbyte2 >= 0x20 && ccbyte2 <= 0x2F) {
|
|
// Mid-row codes: color/underline
|
|
switch (ccbyte2) {
|
|
case 0x20:
|
|
// TODO: shouldn't affect roll-ups...
|
|
this._clearActiveCues(timestamp);
|
|
// RCL: Resume Caption Loading
|
|
// begin pop on
|
|
break;
|
|
case 0x21:
|
|
// BS: Backspace
|
|
this.cue.text = this.cue.text.substr(0, this.cue.text.length - 1);
|
|
break;
|
|
case 0x22:
|
|
// AOF: reserved (formerly alarm off)
|
|
break;
|
|
case 0x23:
|
|
// AON: reserved (formerly alarm on)
|
|
break;
|
|
case 0x24:
|
|
// DER: Delete to end of row
|
|
break;
|
|
case 0x25:
|
|
// RU2: roll-up 2 rows
|
|
//this._rollup(2);
|
|
break;
|
|
case 0x26:
|
|
// RU3: roll-up 3 rows
|
|
//this._rollup(3);
|
|
break;
|
|
case 0x27:
|
|
// RU4: roll-up 4 rows
|
|
//this._rollup(4);
|
|
break;
|
|
case 0x28:
|
|
// FON: Flash on
|
|
break;
|
|
case 0x29:
|
|
// RDC: Resume direct captioning
|
|
this._clearActiveCues(timestamp);
|
|
break;
|
|
case 0x2A:
|
|
// TR: Text Restart
|
|
break;
|
|
case 0x2B:
|
|
// RTD: Resume Text Display
|
|
break;
|
|
case 0x2C:
|
|
// EDM: Erase Displayed Memory
|
|
this._clearActiveCues(timestamp);
|
|
break;
|
|
case 0x2D:
|
|
// CR: Carriage Return
|
|
// only affects roll-up
|
|
//this._rollup(1);
|
|
break;
|
|
case 0x2E:
|
|
// ENM: Erase non-displayed memory
|
|
this._text = '';
|
|
break;
|
|
case 0x2F:
|
|
this._flipMemory(timestamp);
|
|
// EOC: End of caption
|
|
// hide any displayed captions and show any hidden one
|
|
break;
|
|
}
|
|
}
|
|
if ((ccbyte1 === 0x17 || ccbyte1 === 0x1F) && ccbyte2 >= 0x21 && ccbyte2 <= 0x23) {
|
|
// Mid-row codes: color/underline
|
|
switch (ccbyte2) {
|
|
case 0x21:
|
|
// TO1: tab offset 1 column
|
|
break;
|
|
case 0x22:
|
|
// TO1: tab offset 2 column
|
|
break;
|
|
case 0x23:
|
|
// TO1: tab offset 3 column
|
|
break;
|
|
}
|
|
} else {
|
|
// Probably a pre-amble address code
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: '_fromCharCode',
|
|
value: function _fromCharCode(tmpByte) {
|
|
switch (tmpByte) {
|
|
case 42:
|
|
return 'á';
|
|
|
|
case 2:
|
|
return 'á';
|
|
|
|
case 2:
|
|
return 'é';
|
|
|
|
case 4:
|
|
return 'í';
|
|
|
|
case 5:
|
|
return 'ó';
|
|
|
|
case 6:
|
|
return 'ú';
|
|
|
|
case 3:
|
|
return 'ç';
|
|
|
|
case 4:
|
|
return '÷';
|
|
|
|
case 5:
|
|
return 'Ñ';
|
|
|
|
case 6:
|
|
return 'ñ';
|
|
|
|
case 7:
|
|
return '█';
|
|
|
|
default:
|
|
return String.fromCharCode(tmpByte);
|
|
}
|
|
}
|
|
}, {
|
|
key: '_flipMemory',
|
|
value: function _flipMemory(timestamp) {
|
|
this._clearActiveCues(timestamp);
|
|
this._flushCaptions(timestamp);
|
|
}
|
|
}, {
|
|
key: '_flushCaptions',
|
|
value: function _flushCaptions(timestamp) {
|
|
if (!this._has708) {
|
|
this._textTrack = this.media.addTextTrack('captions', 'English', 'en');
|
|
this._has708 = true;
|
|
}
|
|
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = this.memory[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
var memoryItem = _step.value;
|
|
|
|
memoryItem.startTime = timestamp;
|
|
this._textTrack.addCue(memoryItem);
|
|
this.display.push(memoryItem);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
_iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.memory = [];
|
|
this.cue = null;
|
|
}
|
|
}, {
|
|
key: '_clearActiveCues',
|
|
value: function _clearActiveCues(timestamp) {
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = this.display[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
var displayItem = _step2.value;
|
|
|
|
displayItem.endTime = timestamp;
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return) {
|
|
_iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.display = [];
|
|
}
|
|
|
|
/* _rollUp(n)
|
|
{
|
|
// TODO: implement roll-up captions
|
|
}
|
|
*/
|
|
|
|
}, {
|
|
key: '_clearBufferedCues',
|
|
value: function _clearBufferedCues() {
|
|
//remove them all...
|
|
}
|
|
}]);
|
|
|
|
return CEA708Interpreter;
|
|
}();
|
|
|
|
exports.default = CEA708Interpreter;
|
|
|
|
},{}],34:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
function noop() {}
|
|
|
|
var fakeLogger = {
|
|
trace: noop,
|
|
debug: noop,
|
|
log: noop,
|
|
warn: noop,
|
|
info: noop,
|
|
error: noop
|
|
};
|
|
|
|
var exportedLogger = fakeLogger;
|
|
|
|
//let lastCallTime;
|
|
// function formatMsgWithTimeInfo(type, msg) {
|
|
// const now = Date.now();
|
|
// const diff = lastCallTime ? '+' + (now - lastCallTime) : '0';
|
|
// lastCallTime = now;
|
|
// msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )';
|
|
// return msg;
|
|
// }
|
|
|
|
function formatMsg(type, msg) {
|
|
msg = '[' + type + '] > ' + msg;
|
|
return msg;
|
|
}
|
|
|
|
function consolePrintFn(type) {
|
|
var func = window.console[type];
|
|
if (func) {
|
|
return function () {
|
|
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
|
|
if (args[0]) {
|
|
args[0] = formatMsg(type, args[0]);
|
|
}
|
|
func.apply(window.console, args);
|
|
};
|
|
}
|
|
return noop;
|
|
}
|
|
|
|
function exportLoggerFunctions(debugConfig) {
|
|
for (var _len2 = arguments.length, functions = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
functions[_key2 - 1] = arguments[_key2];
|
|
}
|
|
|
|
functions.forEach(function (type) {
|
|
exportedLogger[type] = debugConfig[type] ? debugConfig[type].bind(debugConfig) : consolePrintFn(type);
|
|
});
|
|
}
|
|
|
|
var enableLogs = exports.enableLogs = function enableLogs(debugConfig) {
|
|
if (debugConfig === true || (typeof debugConfig === 'undefined' ? 'undefined' : _typeof(debugConfig)) === 'object') {
|
|
exportLoggerFunctions(debugConfig,
|
|
// Remove out from list here to hard-disable a log-level
|
|
//'trace',
|
|
'debug', 'log', 'info', 'warn', 'error');
|
|
// Some browsers don't allow to use bind on console object anyway
|
|
// fallback to default if needed
|
|
try {
|
|
exportedLogger.log();
|
|
} catch (e) {
|
|
exportedLogger = fakeLogger;
|
|
}
|
|
} else {
|
|
exportedLogger = fakeLogger;
|
|
}
|
|
};
|
|
|
|
var logger = exports.logger = exportedLogger;
|
|
|
|
},{}],35:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
var URLHelper = {
|
|
|
|
// build an absolute URL from a relative one using the provided baseURL
|
|
// if relativeURL is an absolute URL it will be returned as is.
|
|
buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL) {
|
|
// remove any remaining space and CRLF
|
|
relativeURL = relativeURL.trim();
|
|
if (/^[a-z]+:/i.test(relativeURL)) {
|
|
// complete url, not relative
|
|
return relativeURL;
|
|
}
|
|
|
|
var relativeURLQuery = null;
|
|
var relativeURLHash = null;
|
|
|
|
var relativeURLHashSplit = /^([^#]*)(.*)$/.exec(relativeURL);
|
|
if (relativeURLHashSplit) {
|
|
relativeURLHash = relativeURLHashSplit[2];
|
|
relativeURL = relativeURLHashSplit[1];
|
|
}
|
|
var relativeURLQuerySplit = /^([^\?]*)(.*)$/.exec(relativeURL);
|
|
if (relativeURLQuerySplit) {
|
|
relativeURLQuery = relativeURLQuerySplit[2];
|
|
relativeURL = relativeURLQuerySplit[1];
|
|
}
|
|
|
|
var baseURLHashSplit = /^([^#]*)(.*)$/.exec(baseURL);
|
|
if (baseURLHashSplit) {
|
|
baseURL = baseURLHashSplit[1];
|
|
}
|
|
var baseURLQuerySplit = /^([^\?]*)(.*)$/.exec(baseURL);
|
|
if (baseURLQuerySplit) {
|
|
baseURL = baseURLQuerySplit[1];
|
|
}
|
|
|
|
var baseURLDomainSplit = /^((([a-z]+):)?\/\/[a-z0-9\.\-_~]+(:[0-9]+)?\/)(.*)$/i.exec(baseURL);
|
|
var baseURLProtocol = baseURLDomainSplit[3];
|
|
var baseURLDomain = baseURLDomainSplit[1];
|
|
var baseURLPath = baseURLDomainSplit[5];
|
|
|
|
var builtURL = null;
|
|
if (/^\/\//.test(relativeURL)) {
|
|
builtURL = baseURLProtocol + '://' + URLHelper.buildAbsolutePath('', relativeURL.substring(2));
|
|
} else if (/^\//.test(relativeURL)) {
|
|
builtURL = baseURLDomain + URLHelper.buildAbsolutePath('', relativeURL.substring(1));
|
|
} else {
|
|
builtURL = URLHelper.buildAbsolutePath(baseURLDomain + baseURLPath, relativeURL);
|
|
}
|
|
|
|
// put the query and hash parts back
|
|
if (relativeURLQuery) {
|
|
builtURL += relativeURLQuery;
|
|
}
|
|
if (relativeURLHash) {
|
|
builtURL += relativeURLHash;
|
|
}
|
|
return builtURL;
|
|
},
|
|
|
|
// build an absolute path using the provided basePath
|
|
// adapted from https://developer.mozilla.org/en-US/docs/Web/API/document/cookie#Using_relative_URLs_in_the_path_parameter
|
|
// this does not handle the case where relativePath is "/" or "//". These cases should be handled outside this.
|
|
buildAbsolutePath: function buildAbsolutePath(basePath, relativePath) {
|
|
var sRelPath = relativePath;
|
|
var nUpLn,
|
|
sDir = '',
|
|
sPath = basePath.replace(/[^\/]*$/, sRelPath.replace(/(\/|^)(?:\.?\/+)+/g, '$1'));
|
|
for (var nEnd, nStart = 0; nEnd = sPath.indexOf('/../', nStart), nEnd > -1; nStart = nEnd + nUpLn) {
|
|
nUpLn = /^\/(?:\.\.\/)*/.exec(sPath.slice(nEnd))[0].length;
|
|
sDir = (sDir + sPath.substring(nStart, nEnd)).replace(new RegExp('(?:\\\/+[^\\\/]*){0,' + (nUpLn - 1) / 3 + '}$'), '/');
|
|
}
|
|
return sDir + sPath.substr(nStart);
|
|
}
|
|
};
|
|
|
|
module.exports = URLHelper;
|
|
|
|
},{}],36:[function(require,module,exports){
|
|
'use strict';
|
|
|
|
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; }; }(); /**
|
|
* XHR based logger
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var XhrLoader = function () {
|
|
function XhrLoader(config) {
|
|
_classCallCheck(this, XhrLoader);
|
|
|
|
if (config && config.xhrSetup) {
|
|
this.xhrSetup = config.xhrSetup;
|
|
}
|
|
}
|
|
|
|
_createClass(XhrLoader, [{
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
this.abort();
|
|
this.loader = null;
|
|
}
|
|
}, {
|
|
key: 'abort',
|
|
value: function abort() {
|
|
var loader = this.loader,
|
|
timeoutHandle = this.timeoutHandle;
|
|
if (loader && loader.readyState !== 4) {
|
|
this.stats.aborted = true;
|
|
loader.abort();
|
|
}
|
|
if (timeoutHandle) {
|
|
window.clearTimeout(timeoutHandle);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'load',
|
|
value: function load(url, responseType, onSuccess, onError, onTimeout, timeout, maxRetry, retryDelay) {
|
|
var onProgress = arguments.length <= 8 || arguments[8] === undefined ? null : arguments[8];
|
|
var frag = arguments.length <= 9 || arguments[9] === undefined ? null : arguments[9];
|
|
|
|
this.url = url;
|
|
if (frag && !isNaN(frag.byteRangeStartOffset) && !isNaN(frag.byteRangeEndOffset)) {
|
|
this.byteRange = frag.byteRangeStartOffset + '-' + (frag.byteRangeEndOffset - 1);
|
|
}
|
|
this.responseType = responseType;
|
|
this.onSuccess = onSuccess;
|
|
this.onProgress = onProgress;
|
|
this.onTimeout = onTimeout;
|
|
this.onError = onError;
|
|
this.stats = { trequest: performance.now(), retry: 0 };
|
|
this.timeout = timeout;
|
|
this.maxRetry = maxRetry;
|
|
this.retryDelay = retryDelay;
|
|
this.loadInternal();
|
|
}
|
|
}, {
|
|
key: 'loadInternal',
|
|
value: function loadInternal() {
|
|
var xhr;
|
|
|
|
if (typeof XDomainRequest !== 'undefined') {
|
|
xhr = this.loader = new XDomainRequest();
|
|
} else {
|
|
xhr = this.loader = new XMLHttpRequest();
|
|
}
|
|
|
|
xhr.onloadend = this.loadend.bind(this);
|
|
xhr.onprogress = this.loadprogress.bind(this);
|
|
|
|
xhr.open('GET', this.url, true);
|
|
if (this.byteRange) {
|
|
xhr.setRequestHeader('Range', 'bytes=' + this.byteRange);
|
|
}
|
|
xhr.responseType = this.responseType;
|
|
this.stats.tfirst = null;
|
|
this.stats.loaded = 0;
|
|
if (this.xhrSetup) {
|
|
this.xhrSetup(xhr, this.url);
|
|
}
|
|
this.timeoutHandle = window.setTimeout(this.loadtimeout.bind(this), this.timeout);
|
|
xhr.send();
|
|
}
|
|
}, {
|
|
key: 'loadend',
|
|
value: function loadend(event) {
|
|
var xhr = event.currentTarget,
|
|
status = xhr.status,
|
|
stats = this.stats;
|
|
// don't proceed if xhr has been aborted
|
|
if (!stats.aborted) {
|
|
// http status between 200 to 299 are all successful
|
|
if (status >= 200 && status < 300) {
|
|
window.clearTimeout(this.timeoutHandle);
|
|
stats.tload = performance.now();
|
|
this.onSuccess(event, stats);
|
|
} else {
|
|
// error ...
|
|
if (stats.retry < this.maxRetry) {
|
|
_logger.logger.warn(status + ' while loading ' + this.url + ', retrying in ' + this.retryDelay + '...');
|
|
this.destroy();
|
|
window.setTimeout(this.loadInternal.bind(this), this.retryDelay);
|
|
// exponential backoff
|
|
this.retryDelay = Math.min(2 * this.retryDelay, 64000);
|
|
stats.retry++;
|
|
} else {
|
|
window.clearTimeout(this.timeoutHandle);
|
|
_logger.logger.error(status + ' while loading ' + this.url);
|
|
this.onError(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: 'loadtimeout',
|
|
value: function loadtimeout(event) {
|
|
_logger.logger.warn('timeout while loading ' + this.url);
|
|
this.onTimeout(event, this.stats);
|
|
}
|
|
}, {
|
|
key: 'loadprogress',
|
|
value: function loadprogress(event) {
|
|
var stats = this.stats;
|
|
if (stats.tfirst === null) {
|
|
stats.tfirst = performance.now();
|
|
}
|
|
stats.loaded = event.loaded;
|
|
if (this.onProgress) {
|
|
this.onProgress(event, stats);
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return XhrLoader;
|
|
}();
|
|
|
|
exports.default = XhrLoader;
|
|
|
|
},{"../utils/logger":34}]},{},[24])(24)
|
|
});
|
|
//# sourceMappingURL=hls.js.map
|