videojs-contrib-dash
Version:
A Video.js source-handler providing MPEG-DASH playback.
1,710 lines (1,553 loc) • 2.86 MB
JavaScript
/*! @name videojs-contrib-dash @version 5.1.1 @license Apache-2.0 */
(function (QUnit,videojs) {
'use strict';
QUnit = QUnit && QUnit.hasOwnProperty('default') ? QUnit['default'] : QUnit;
videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var minDoc = {};
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
typeof window !== 'undefined' ? window : {};
var doccy;
if (typeof document !== 'undefined') {
doccy = document;
} else {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
if (!doccy) {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
}
}
var document_1 = doccy;
var when = function when(element, type, fn, condition) {
var func = function func() {
if (condition()) {
element.off(type, func);
fn.apply(this, arguments);
}
};
element.on(type, func);
};
QUnit.module('Webpack/Browserify Integration', {
beforeEach: function beforeEach(assert) {
var done = assert.async();
this.fixture = document_1.createElement('div');
document_1.body.appendChild(this.fixture);
var videoEl = document_1.createElement('video');
videoEl.id = 'vid';
videoEl.setAttribute('controls', '');
videoEl.setAttribute('width', '600');
videoEl.setAttribute('height', '300');
videoEl.setAttribute('muted', 'true');
videoEl.className = 'video-js vjs-default-skin';
this.fixture.appendChild(videoEl);
var player = videojs('vid');
this.player = player;
player.ready(function () {
player.one('loadstart', done);
player.src({
src: 'http://dash.edgesuite.net/akamai/bbb_30fps/bbb_30fps.mpd',
type: 'application/dash+xml'
});
});
},
afterEach: function afterEach() {
this.player.dispose();
this.fixture.innerHTML = '';
}
});
QUnit.test('should play', function (assert) {
var done = assert.async();
var player = this.player;
assert.expect(2);
when(player, 'timeupdate', function () {
assert.ok(true, 'played for at least two seconds');
assert.equal(player.error(), null, 'has no player errors');
done();
}, function () {
return player.currentTime() >= 2;
});
player.play();
});
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof commonjsGlobal !== "undefined") {
win = commonjsGlobal;
} else if (typeof self !== "undefined"){
win = self;
} else {
win = {};
}
var window_1 = win;
var dashjs$1 = window_1.dashjs;
var sampleSrc = {
src: 'movie.mpd',
type: 'application/dash+xml',
keySystemOptions: [{
name: 'com.widevine.alpha',
options: {
extra: 'data',
licenseUrl: 'https://example.com/license'
}
}]
};
var sampleSrcNoDRM = {
src: 'movie.mpd',
type: 'application/dash+xml'
};
var testHandleSource = function testHandleSource(assert, source, expectedKeySystemOptions, config) {
if (config === undefined) {
config = {};
}
var eventHandlers = config.eventHandlers ? config.eventHandlers : {};
var startupCalled = false;
var attachViewCalled = false;
var setLimitBitrateByPortalCalled = false;
var setLimitBitrateByPortalValue = null;
var el = document_1.createElement('div');
var fixture = document_1.querySelector('#qunit-fixture'); // stubs
var origMediaPlayer = dashjs$1.MediaPlayer;
var origVJSXHR = videojs.xhr;
assert.expect(7); // Default limitBitrateByPortal to false
var limitBitrateByPortal = config.limitBitrateByPortal || false;
el.setAttribute('id', 'test-vid');
fixture.appendChild(el);
var Html5 = videojs.getTech('Html5');
var tech = new Html5({
playerId: el.getAttribute('id')
});
var options = {
playerId: el.getAttribute('id'),
dash: {
limitBitrateByPortal: limitBitrateByPortal
}
};
tech.el = function () {
return el;
};
tech.triggerReady = function () {};
dashjs$1.MediaPlayer = function () {
return {
create: function create() {
return {
initialize: function initialize() {
startupCalled = true;
},
attachTTMLRenderingDiv: function attachTTMLRenderingDiv() {},
attachView: function attachView() {
attachViewCalled = true;
},
setAutoPlay: function setAutoPlay(autoplay) {
assert.strictEqual(autoplay, false, 'autoplay is set to false by default');
},
setProtectionData: function setProtectionData(keySystemOptions) {
assert.deepEqual(keySystemOptions, expectedKeySystemOptions, 'src and manifest key system options are merged');
},
attachSource: function attachSource(manifest) {
assert.deepEqual(manifest, source.src, 'manifest url is sent to attachSource');
assert.strictEqual(setLimitBitrateByPortalCalled, true, 'MediaPlayer.setLimitBitrateByPortal was called');
assert.strictEqual(setLimitBitrateByPortalValue, limitBitrateByPortal, 'MediaPlayer.setLimitBitrateByPortal was called with the correct value');
assert.strictEqual(startupCalled, true, 'MediaPlayer.startup was called');
assert.strictEqual(attachViewCalled, true, 'MediaPlayer.attachView was called');
tech.dispose(); // Restore
dashjs$1.MediaPlayer = origMediaPlayer;
videojs.xhr = origVJSXHR;
},
setLimitBitrateByPortal: function setLimitBitrateByPortal(value) {
setLimitBitrateByPortalCalled = true;
setLimitBitrateByPortalValue = value;
},
on: function on(event, fn) {
if (!eventHandlers[event]) {
eventHandlers[event] = [];
}
eventHandlers[event].push(fn);
},
reset: config.resetCallback,
trigger: function trigger(event, data) {
if (!eventHandlers[event]) {
return;
}
eventHandlers[event].forEach(function (handler) {
handler(data);
});
}
};
}
};
};
dashjs$1.MediaPlayer.events = origMediaPlayer.events;
var dashSourceHandler = Html5.selectSourceHandler(source);
return dashSourceHandler.handleSource(source, tech, options);
};
QUnit.module('videojs-dash dash.js SourceHandler', {
afterEach: function afterEach() {
videojs.Html5DashJS.hooks_ = {};
sampleSrc = {
src: 'movie.mpd',
type: 'application/dash+xml',
keySystemOptions: [{
name: 'com.widevine.alpha',
options: {
extra: 'data',
licenseUrl: 'https://example.com/license'
}
}]
};
}
});
QUnit.test('validate the Dash.js SourceHandler in Html5', function (assert) {
var dashSource = {
src: 'some.mpd',
type: 'application/dash+xml'
};
var maybeDashSource = {
src: 'some.mpd'
};
var nonDashSource = {
src: 'some.mp4',
type: 'video/mp4'
};
var dashSourceHandler = videojs.getTech('Html5').selectSourceHandler(dashSource);
assert.ok(dashSourceHandler, 'A DASH handler was found');
assert.strictEqual(dashSourceHandler.canHandleSource(dashSource), 'probably', 'canHandleSource with proper mime-type returns "probably"');
assert.strictEqual(dashSourceHandler.canHandleSource(maybeDashSource), 'maybe', 'canHandleSource with expected extension returns "maybe"');
assert.strictEqual(dashSourceHandler.canHandleSource(nonDashSource), '', 'canHandleSource with anything else returns ""');
assert.strictEqual(dashSourceHandler.canPlayType(dashSource.type), 'probably', 'canPlayType with proper mime-type returns "probably"');
assert.strictEqual(dashSourceHandler.canPlayType(nonDashSource.type), '', 'canPlayType with anything else returns ""');
});
QUnit.test('validate buildDashJSProtData function', function (assert) {
var output = videojs.Html5DashJS.buildDashJSProtData(sampleSrc.keySystemOptions);
var empty = videojs.Html5DashJS.buildDashJSProtData(undefined);
assert.strictEqual(output['com.widevine.alpha'].serverURL, 'https://example.com/license', 'licenceUrl converted to serverURL');
assert.equal(empty, null, 'undefined keySystemOptions returns null');
});
QUnit.test('validate handleSource function with src-provided key options', function (assert) {
var mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
testHandleSource(assert, sampleSrc, mergedKeySystemOptions);
});
QUnit.test('validate handleSource function with "limit bitrate by portal" option', function (assert) {
var mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {
limitBitrateByPortal: true
});
});
QUnit.test('validate handleSource function with invalid manifest', function (assert) {
var mergedKeySystemOptions = null;
testHandleSource(assert, sampleSrcNoDRM, mergedKeySystemOptions);
});
QUnit.test('update the source keySystemOptions', function (assert) {
var mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
},
'com.widevine.alpha1': {
serverURL: 'https://example.com/anotherlicense'
}
};
var updateSourceData = function updateSourceData(source) {
var numOfKeySystems = source.keySystemOptions.length;
source.keySystemOptions.push({
name: 'com.widevine.alpha' + numOfKeySystems,
options: {
serverURL: 'https://example.com/anotherlicense'
}
});
return source;
};
videojs.Html5DashJS.hook('updatesource', updateSourceData);
testHandleSource(assert, sampleSrc, mergedKeySystemOptions);
});
QUnit.test('registers hook callbacks correctly', function (assert) {
var cb1Count = 0;
var cb2Count = 0;
var cb1 = function cb1(source) {
cb1Count++;
return source;
};
var cb2 = function cb2() {
cb2Count++;
};
var mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
videojs.Html5DashJS.hook('updatesource', cb1);
videojs.Html5DashJS.hook('beforeinitialize', cb2);
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {
limitBitrateByPortal: true
});
assert.expect(9);
assert.equal(cb1Count, 2, 'registered first callback and called');
assert.equal(cb2Count, 1, 'registered second callback and called');
});
QUnit.test('removes callbacks with removeInitializationHook correctly', function (assert) {
var cb1Count = 0;
var cb2Count = 0;
var cb3Count = 0;
var cb4Count = 0;
var cb1 = function cb1() {
cb1Count++;
};
var cb2 = function cb2() {
cb2Count++;
assert.ok(videojs.Html5DashJS.removeHook('beforeinitialize', cb2), 'removed hook cb2');
};
var cb3 = function cb3(source) {
cb3Count++;
return source;
};
var cb4 = function cb4(source) {
cb4Count++;
return source;
};
var mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
videojs.Html5DashJS.hook('beforeinitialize', [cb1, cb2]);
videojs.Html5DashJS.hook('updatesource', [cb3, cb4]);
assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 2, 'added 2 hooks to beforeinitialize');
assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 2, 'added 2 hooks to updatesource');
assert.ok(!videojs.Html5DashJS.removeHook('beforeinitialize', cb3), 'nothing removed if callback not found');
assert.ok(videojs.Html5DashJS.removeHook('updatesource', cb3), 'removed cb3');
assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 1, 'removed hook cb3');
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {
limitBitrateByPortal: true
});
assert.expect(18);
assert.equal(cb1Count, 1, 'called cb1');
assert.equal(cb2Count, 1, 'called cb2');
assert.equal(cb3Count, 0, 'did not call cb3');
assert.equal(cb4Count, 2, 'called cb4');
assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 1, 'cb2 removed itself');
});
QUnit.test('attaches dash.js error handler', function (assert) {
var eventHandlers = {};
var sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {
eventHandlers: eventHandlers
});
assert.expect(8);
assert.equal(eventHandlers[dashjs$1.MediaPlayer.events.ERROR][0], sourceHandler.retriggerError_);
});
QUnit.test('handles various errors', function (assert) {
var errors = [{
receive: {
error: 'capability',
event: 'mediasource'
},
trigger: {
code: 4,
message: 'The media cannot be played because it requires a feature ' + 'that your browser does not support.'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'createParser',
message: 'manifest type unsupported'
}
},
trigger: {
code: 4,
message: 'manifest type unsupported'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'codec',
message: 'Codec (h264) is not supported'
}
},
trigger: {
code: 4,
message: 'Codec (h264) is not supported'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'nostreams',
message: 'No streams to play.'
}
},
trigger: {
code: 4,
message: 'No streams to play.'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'nostreamscomposed',
message: 'Error creating stream.'
}
},
trigger: {
code: 4,
message: 'Error creating stream.'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'parse',
message: 'parsing the manifest failed'
}
},
trigger: {
code: 4,
message: 'parsing the manifest failed'
}
}, {
receive: {
error: 'manifestError',
event: {
id: 'nostreams',
message: 'Multiplexed representations are intentionally not ' + 'supported, as they are not compliant with the DASH-AVC/264 guidelines'
}
},
trigger: {
code: 4,
message: 'Multiplexed representations are intentionally not ' + 'supported, as they are not compliant with the DASH-AVC/264 guidelines'
}
}, {
receive: {
error: 'mediasource',
event: 'MEDIA_ERR_ABORTED: Some context'
},
trigger: {
code: 1,
message: 'MEDIA_ERR_ABORTED: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'MEDIA_ERR_NETWORK: Some context'
},
trigger: {
code: 2,
message: 'MEDIA_ERR_NETWORK: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'MEDIA_ERR_DECODE: Some context'
},
trigger: {
code: 3,
message: 'MEDIA_ERR_DECODE: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'
},
trigger: {
code: 4,
message: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'MEDIA_ERR_ENCRYPTED: Some context'
},
trigger: {
code: 5,
message: 'MEDIA_ERR_ENCRYPTED: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'UNKNOWN: Some context'
},
trigger: {
code: 4,
message: 'UNKNOWN: Some context'
}
}, {
receive: {
error: 'mediasource',
event: 'Error creating video source buffer'
},
trigger: {
code: 4,
message: 'Error creating video source buffer'
}
}, {
receive: {
error: 'capability',
event: 'encryptedmedia'
},
trigger: {
code: 5,
message: 'The media cannot be played because it requires encryption ' + 'features that your browser does not support.'
}
}, {
receive: {
error: 'key_session',
event: 'Some encryption error'
},
trigger: {
code: 5,
message: 'Some encryption error'
}
}, {
receive: {
error: 'download',
event: {
id: 'someId',
url: 'http://some/url',
request: {}
}
},
trigger: {
code: 2,
message: 'The media playback was aborted because too many ' + 'consecutive download errors occurred.'
}
}, {
receive: {
error: 'mssError',
event: 'MSS_NO_TFRF : Missing tfrf in live media segment'
},
trigger: {
code: 3,
message: 'MSS_NO_TFRF : Missing tfrf in live media segment'
}
}]; // Make sure the MediaPlayer gets reset enough times
var done = assert.async(errors.length);
var resetCallback = function resetCallback() {
done();
};
var eventHandlers = {};
var sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {
eventHandlers: eventHandlers,
resetCallback: resetCallback
});
assert.expect(7 + errors.length * 2);
var i;
sourceHandler.player.on('error', function () {
assert.equal(sourceHandler.player.error().code, errors[i].trigger.code, 'error code matches');
assert.equal(sourceHandler.player.error().message, errors[i].trigger.message, 'error message matches');
}); // dispatch all handled errors and see if they throw the correct details
for (i = 0; i < errors.length; i++) {
sourceHandler.mediaPlayer_.trigger(dashjs$1.MediaPlayer.events.ERROR, errors[i].receive);
}
});
QUnit.test('ignores unknown errors', function (assert) {
var resetCalled = false;
var resetCallback = function resetCallback() {
resetCalled = true;
};
var sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {
resetCallback: resetCallback
});
var done = assert.async(1);
sourceHandler.mediaPlayer_.trigger(dashjs$1.MediaPlayer.events.ERROR, {
error: 'unknown'
});
assert.equal(sourceHandler.player.error(), null, 'No error dispatched'); // The error handler waits 10ms before firing reset, so we wait for
// 20ms here to make sure it doesn't fire
setTimeout(function () {
assert.notOk(resetCalled, 'MediaPlayer has not been reset');
done();
}, 20);
assert.expect(9);
});
var dash_all_debug = createCommonjsModule(function (module, exports) {
(function webpackUniversalModuleDefinition(root, factory) {
module.exports = factory();
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/dist/";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./externals/base64.js":
/*!*****************************!*\
!*** ./externals/base64.js ***!
\*****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* $Date: 2007-06-12 18:02:31 $ */
// from: http://bannister.us/weblog/2007/06/09/simple-base64-encodedecode-javascript/
// Handles encode/decode of ASCII and Unicode strings.
var UTF8 = {};
UTF8.encode = function (s) {
var u = [];
for (var i = 0; i < s.length; ++i) {
var c = s.charCodeAt(i);
if (c < 0x80) {
u.push(c);
} else if (c < 0x800) {
u.push(0xC0 | c >> 6);
u.push(0x80 | 63 & c);
} else if (c < 0x10000) {
u.push(0xE0 | c >> 12);
u.push(0x80 | 63 & c >> 6);
u.push(0x80 | 63 & c);
} else {
u.push(0xF0 | c >> 18);
u.push(0x80 | 63 & c >> 12);
u.push(0x80 | 63 & c >> 6);
u.push(0x80 | 63 & c);
}
}
return u;
};
UTF8.decode = function (u) {
var a = [];
var i = 0;
while (i < u.length) {
var v = u[i++];
if (v < 0x80) ; else if (v < 0xE0) {
v = (31 & v) << 6;
v |= 63 & u[i++];
} else if (v < 0xF0) {
v = (15 & v) << 12;
v |= (63 & u[i++]) << 6;
v |= 63 & u[i++];
} else {
v = (7 & v) << 18;
v |= (63 & u[i++]) << 12;
v |= (63 & u[i++]) << 6;
v |= 63 & u[i++];
}
a.push(String.fromCharCode(v));
}
return a.join('');
};
var BASE64 = {};
(function (T) {
var encodeArray = function encodeArray(u) {
var i = 0;
var a = [];
var n = 0 | u.length / 3;
while (0 < n--) {
var v = (u[i] << 16) + (u[i + 1] << 8) + u[i + 2];
i += 3;
a.push(T.charAt(63 & v >> 18));
a.push(T.charAt(63 & v >> 12));
a.push(T.charAt(63 & v >> 6));
a.push(T.charAt(63 & v));
}
if (2 == u.length - i) {
var v = (u[i] << 16) + (u[i + 1] << 8);
a.push(T.charAt(63 & v >> 18));
a.push(T.charAt(63 & v >> 12));
a.push(T.charAt(63 & v >> 6));
a.push('=');
} else if (1 == u.length - i) {
var v = u[i] << 16;
a.push(T.charAt(63 & v >> 18));
a.push(T.charAt(63 & v >> 12));
a.push('==');
}
return a.join('');
};
var R = function () {
var a = [];
for (var i = 0; i < T.length; ++i) {
a[T.charCodeAt(i)] = i;
}
a['='.charCodeAt(0)] = 0;
return a;
}();
var decodeArray = function decodeArray(s) {
var i = 0;
var u = [];
var n = 0 | s.length / 4;
while (0 < n--) {
var v = (R[s.charCodeAt(i)] << 18) + (R[s.charCodeAt(i + 1)] << 12) + (R[s.charCodeAt(i + 2)] << 6) + R[s.charCodeAt(i + 3)];
u.push(255 & v >> 16);
u.push(255 & v >> 8);
u.push(255 & v);
i += 4;
}
if (u) {
if ('=' == s.charAt(i - 2)) {
u.pop();
u.pop();
} else if ('=' == s.charAt(i - 1)) {
u.pop();
}
}
return u;
};
var ASCII = {};
ASCII.encode = function (s) {
var u = [];
for (var i = 0; i < s.length; ++i) {
u.push(s.charCodeAt(i));
}
return u;
};
ASCII.decode = function (u) {
for (var i = 0; i < s.length; ++i) {
a[i] = String.fromCharCode(a[i]);
}
return a.join('');
};
BASE64.decodeArray = function (s) {
var u = decodeArray(s);
return new Uint8Array(u);
};
BASE64.encodeASCII = function (s) {
var u = ASCII.encode(s);
return encodeArray(u);
};
BASE64.decodeASCII = function (s) {
var a = decodeArray(s);
return ASCII.decode(a);
};
BASE64.encode = function (s) {
var u = UTF8.encode(s);
return encodeArray(u);
};
BASE64.decode = function (s) {
var u = decodeArray(s);
return UTF8.decode(u);
};
})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
/*The following polyfills are not used in dash.js but have caused multiplayer integration issues.
Therefore commenting them out.
if (undefined === btoa) {
var btoa = BASE64.encode;
}
if (undefined === atob) {
var atob = BASE64.decode;
}
*/
{
exports.decode = BASE64.decode;
exports.decodeArray = BASE64.decodeArray;
exports.encode = BASE64.encode;
exports.encodeASCII = BASE64.encodeASCII;
}
/***/ }),
/***/ "./externals/cea608-parser.js":
/*!************************************!*\
!*** ./externals/cea608-parser.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2015-2016, DASH Industry Forum.
* 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.
* * 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.
* 2. Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 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 THE 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.
*/
(function (exports) {
/**
* Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes
*/
var specialCea608CharsCodes = {
0x2a: 0xe1,
// lowercase a, acute accent
0x5c: 0xe9,
// lowercase e, acute accent
0x5e: 0xed,
// lowercase i, acute accent
0x5f: 0xf3,
// lowercase o, acute accent
0x60: 0xfa,
// lowercase u, acute accent
0x7b: 0xe7,
// lowercase c with cedilla
0x7c: 0xf7,
// division symbol
0x7d: 0xd1,
// uppercase N tilde
0x7e: 0xf1,
// lowercase n tilde
0x7f: 0x2588,
// Full block
// THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F
// THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES
0x80: 0xae,
// Registered symbol (R)
0x81: 0xb0,
// degree sign
0x82: 0xbd,
// 1/2 symbol
0x83: 0xbf,
// Inverted (open) question mark
0x84: 0x2122,
// Trademark symbol (TM)
0x85: 0xa2,
// Cents symbol
0x86: 0xa3,
// Pounds sterling
0x87: 0x266a,
// Music 8'th note
0x88: 0xe0,
// lowercase a, grave accent
0x89: 0x20,
// transparent space (regular)
0x8a: 0xe8,
// lowercase e, grave accent
0x8b: 0xe2,
// lowercase a, circumflex accent
0x8c: 0xea,
// lowercase e, circumflex accent
0x8d: 0xee,
// lowercase i, circumflex accent
0x8e: 0xf4,
// lowercase o, circumflex accent
0x8f: 0xfb,
// lowercase u, circumflex accent
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F
0x90: 0xc1,
// capital letter A with acute
0x91: 0xc9,
// capital letter E with acute
0x92: 0xd3,
// capital letter O with acute
0x93: 0xda,
// capital letter U with acute
0x94: 0xdc,
// capital letter U with diaresis
0x95: 0xfc,
// lowercase letter U with diaeresis
0x96: 0x2018,
// opening single quote
0x97: 0xa1,
// inverted exclamation mark
0x98: 0x2a,
// asterisk
0x99: 0x2019,
// closing single quote
0x9a: 0x2501,
// box drawings heavy horizontal
0x9b: 0xa9,
// copyright sign
0x9c: 0x2120,
// Service mark
0x9d: 0x2022,
// (round) bullet
0x9e: 0x201c,
// Left double quotation mark
0x9f: 0x201d,
// Right double quotation mark
0xa0: 0xc0,
// uppercase A, grave accent
0xa1: 0xc2,
// uppercase A, circumflex
0xa2: 0xc7,
// uppercase C with cedilla
0xa3: 0xc8,
// uppercase E, grave accent
0xa4: 0xca,
// uppercase E, circumflex
0xa5: 0xcb,
// capital letter E with diaresis
0xa6: 0xeb,
// lowercase letter e with diaresis
0xa7: 0xce,
// uppercase I, circumflex
0xa8: 0xcf,
// uppercase I, with diaresis
0xa9: 0xef,
// lowercase i, with diaresis
0xaa: 0xd4,
// uppercase O, circumflex
0xab: 0xd9,
// uppercase U, grave accent
0xac: 0xf9,
// lowercase u, grave accent
0xad: 0xdb,
// uppercase U, circumflex
0xae: 0xab,
// left-pointing double angle quotation mark
0xaf: 0xbb,
// right-pointing double angle quotation mark
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F
0xb0: 0xc3,
// Uppercase A, tilde
0xb1: 0xe3,
// Lowercase a, tilde
0xb2: 0xcd,
// Uppercase I, acute accent
0xb3: 0xcc,
// Uppercase I, grave accent
0xb4: 0xec,
// Lowercase i, grave accent
0xb5: 0xd2,
// Uppercase O, grave accent
0xb6: 0xf2,
// Lowercase o, grave accent
0xb7: 0xd5,
// Uppercase O, tilde
0xb8: 0xf5,
// Lowercase o, tilde
0xb9: 0x7b,
// Open curly brace
0xba: 0x7d,
// Closing curly brace
0xbb: 0x5c,
// Backslash
0xbc: 0x5e,
// Caret
0xbd: 0x5f,
// Underscore
0xbe: 0x7c,
// Pipe (vertical line)
0xbf: 0x223c,
// Tilde operator
0xc0: 0xc4,
// Uppercase A, umlaut
0xc1: 0xe4,
// Lowercase A, umlaut
0xc2: 0xd6,
// Uppercase O, umlaut
0xc3: 0xf6,
// Lowercase o, umlaut
0xc4: 0xdf,
// Esszett (sharp S)
0xc5: 0xa5,
// Yen symbol
0xc6: 0xa4,
// Generic currency sign
0xc7: 0x2503,
// Box drawings heavy vertical
0xc8: 0xc5,
// Uppercase A, ring
0xc9: 0xe5,
// Lowercase A, ring
0xca: 0xd8,
// Uppercase O, stroke
0xcb: 0xf8,
// Lowercase o, strok
0xcc: 0x250f,
// Box drawings heavy down and right
0xcd: 0x2513,
// Box drawings heavy down and left
0xce: 0x2517,
// Box drawings heavy up and right
0xcf: 0x251b // Box drawings heavy up and left
};
/**
* Get Unicode Character from CEA-608 byte code
*/
var getCharForByte = function getCharForByte(_byte) {
var charCode = _byte;
if (specialCea608CharsCodes.hasOwnProperty(_byte)) {
charCode = specialCea608CharsCodes[_byte];
}
return String.fromCharCode(charCode);
};
var NR_ROWS = 15,
NR_COLS = 32; // Tables to look up row from PAC data
var rowsLowCh1 = {
0x11: 1,
0x12: 3,
0x15: 5,
0x16: 7,
0x17: 9,
0x10: 11,
0x13: 12,
0x14: 14
};
var rowsHighCh1 = {
0x11: 2,
0x12: 4,
0x15: 6,
0x16: 8,
0x17: 10,
0x13: 13,
0x14: 15
};
var rowsLowCh2 = {
0x19: 1,
0x1A: 3,
0x1D: 5,
0x1E: 7,
0x1F: 9,
0x18: 11,
0x1B: 12,
0x1C: 14
};
var rowsHighCh2 = {
0x19: 2,
0x1A: 4,
0x1D: 6,
0x1E: 8,
0x1F: 10,
0x1B: 13,
0x1C: 15
};
var backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent'];
/**
* Simple logger class to be able to write with time-stamps and filter on level.
*/
var logger = {
verboseFilter: {
'DATA': 3,
'DEBUG': 3,
'INFO': 2,
'WARNING': 2,
'TEXT': 1,
'ERROR': 0
},
time: null,
verboseLevel: 0,
// Only write errors
setTime: function setTime(newTime) {
this.time = newTime;
},
log: function log(severity, msg) {
var minLevel = this.verboseFilter[severity];
if (this.verboseLevel >= minLevel) {
console.log(this.time + " [" + severity + "] " + msg);
}
}
};
var numArrayToHexArray = function numArrayToHexArray(numArray) {
var hexArray = [];
for (var j = 0; j < numArray.length; j++) {
hexArray.push(numArray[j].toString(16));
}
return hexArray;
};
/**
* State of CEA-608 pen or character
* @constructor
*/
var PenState = function PenState(foreground, underline, italics, background, flash) {
this.foreground = foreground || "white";
this.underline = underline || false;
this.italics = italics || false;
this.background = background || "black";
this.flash = flash || false;
};
PenState.prototype = {
reset: function reset() {
this.foreground = "white";
this.underline = false;
this.italics = false;
this.background = "black";
this.flash = false;
},
setStyles: function setStyles(styles) {
var attribs = ["foreground", "underline", "italics", "background", "flash"];
for (var i = 0; i < attribs.length; i++) {
var style = attribs[i];
if (styles.hasOwnProperty(style)) {
this[style] = styles[style];
}
}
},
isDefault: function isDefault() {
return this.foreground === "white" && !this.underline && !this.italics && this.background === "black" && !this.flash;
},
equals: function equals(other) {
return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash;
},
copy: function copy(newPenState) {
this.foreground = newPenState.foreground;
this.underline = newPenState.underline;
this.italics = newPenState.italics;
this.background = newPenState.background;
this.flash = newPenState.flash;
},
toString: function toString() {
return "color=" + this.foreground + ", underline=" + this.underline + ", italics=" + this.italics + ", background=" + this.background + ", flash=" + this.flash;
}
};
/**
* Unicode character with styling and background.
* @constructor
*/
var StyledUnicodeChar = function StyledUnicodeChar(uchar, foreground, underline, italics, background, flash) {
this.uchar = uchar || ' '; // unicode character
this.penState = new PenState(foreground, underline, italics, background, flash);
};
StyledUnicodeChar.prototype = {
reset: function reset() {
this.uchar = ' ';
this.penState.reset();
},
setChar: function setChar(uchar, newPenState) {
this.uchar = uchar;
this.penState.copy(newPenState);
},
setPenState: function setPenState(newPenState) {
this.penState.copy(newPenState);
},
equals: function equals(other) {
return this.uchar === other.uchar && this.penState.equals(other.penState);
},
copy: function copy(newChar) {
this.uchar = newChar.uchar;
this.penState.copy(newChar.penState);
},
isEmpty: function isEmpty() {
return this.uchar === ' ' && this.penState.isDefault();
}
};
/**
* CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
* @constructor
*/
var Row = function Row() {
this.chars = [];
for (var i = 0; i < NR_COLS; i++) {
this.chars.push(new StyledUnicodeChar());
}
this.pos = 0;
this.currPenState = new PenState();
};
Row.prototype = {
equals: function equals(other) {
var equal = true;
for (var i = 0; i < NR_COLS; i++) {
if (!this.chars[i].equals(other.chars[i])) {
equal = false;
break;
}
}
return equal;
},
copy: function copy(other) {
for (var i = 0; i < NR_COLS; i++) {
this.chars[i].copy(other.chars[i]);
}
},
isEmpty: function isEmpty() {
var empty = true;
for (var i = 0; i < NR_COLS; i++) {
if (!this.chars[i].isEmpty()) {
empty = false;
break;
}
}
return empty;
},
/**
* Set the cursor to a valid column.
*/
setCursor: function setCursor(absPos) {
if (this.pos !== absPos) {
this.pos = absPos;
}
if (this.pos < 0) {
logger.log("ERROR", "Negative cursor position " + this.pos);
this.pos = 0;
} else if (this.pos > NR_COLS) {
logger.log("ERROR", "Too large cursor position " + this.pos);
this.pos = NR_COLS;
}
},
/**
* Move the cursor relative to current position.
*/
moveCursor: function moveCursor(relPos) {
var newPos = this.pos + relPos;
if (relPos > 1) {
for (var i = this.pos + 1; i < newPos + 1; i++) {
this.chars[i].setPenState(this.currPenState);
}
}
this.setCursor(newPos);
},
/**
* Backspace, move one step back and clear character.
*/
backSpace: function backSpace() {
this.moveCursor(-1);
this.chars[this.pos].setChar(' ', this.currPenState);
},
insertChar: function insertChar(_byte2) {
if (_byte2 >= 0x90) {
//Extended char
this.backSpace();
}
var _char = getCharForByte(_byte2);
if (this.pos >= NR_COLS) {
logger.log("ERROR", "Cannot insert " + _byte2.toString(16) + " (" + _char + ") at position " + this.pos + ". Skipping it!");
return;
}
this.chars[this.pos].setChar(_char, this.currPenState);
this.moveCursor(1);
},
clearFromPos: function clearFromPos(startPos) {
var i;
for (i = startPos; i < NR_COLS; i++) {
this.chars[i].reset();
}
},
clear: function clear() {
this.clearFromPos(0);
this.pos = 0;
this.currPenState.reset();
},
clearToEndOfRow: function clearToEndOfRow() {
this.clearFromPos(this.pos);
},
getTextString: function getTextString() {
var chars = [];
var empty = true;
for (var i = 0; i < NR_COLS; i++) {
var _char2 = this.chars[i].uchar;
if (_char2 !== " ") {
empty = false;
}
chars.push(_char2);
}
if (empty) {
return "";
} else {
return chars.join("");
}
},
setPenStyles: function setPenStyles(styles) {
this.currPenState.setStyles(styles);
var currChar = this.chars[this.pos];
currChar.setPenState(this.currPenState);
}
};
/**
* Keep a CEA-608 screen of 32x15 styled characters
* @constructor
*/
var CaptionScreen = function CaptionScreen() {
this.rows = [];
for (var i = 0; i < NR_ROWS; i++) {
this.rows.push(new Row()); // Note that we use zero-based numbering (0-14)
}
this.currRow = NR_ROWS - 1;
this.nrRollUpRows = null;
this.reset();
};
CaptionScreen.prototype = {
reset: function reset() {
for (var i = 0; i < NR_ROWS; i++) {
this.rows[i].clear();
}
this.currRow = NR_ROWS - 1;
},
equals: function equals(other) {
var equal = true;
for (var i = 0; i < NR_ROWS; i++) {
if (!this.rows[i].equals(other.rows[i])) {
equal = false;
break;
}
}
return equal;
},
copy: function copy(other) {
for (var i = 0; i < NR_ROWS; i++) {
this.rows[i].copy(other.rows[i]);
}
},
isEmpty: function isEmpty() {
var empty = true;
for (var i = 0; i < NR_ROWS; i++) {
if (!this.rows[i].isEmpty()) {
empty = false;
break;
}
}
return empty;
},
backSpace: function backSpace() {
var row = this.rows[this.currRow];
row.backSpace();
},
clearToEndOfRow: function clearToEndOfRow() {
var row = this.rows[this.currRow];
row.clearToEndOfRow();
},
/**
* Insert a character (without styling) in the current row.
*/
insertChar: function insertChar(_char3) {
var row = this.rows[this.currRow];
row.insertChar(_char3);
},
setPen: function setPen(styles) {
var row = this.rows[this.currRow];
row.setPenStyles(styles);
},
moveCursor: function moveCursor(relPos) {
var row = this.rows[this.currRow];
row.moveCursor(relPos);
},
setCursor: function setCursor(absPos) {
logger.log("INFO", "setCursor: " + absPos);
var row = this.rows[this.currRow];
row.setCursor(absPos);
},
setPAC: function setPAC(pacData) {
logger.log("INFO", "pacData = " + JSON.stringify(pacData));
var newRow = pacData.row - 1;
if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
newRow = this.nrRollUpRows - 1;
}
this.currRow = newRow;
var row = this.rows[this.currRow];
if (pacData.indent !== null) {
var indent = pacData.indent;
var prevPos = Math.max(indent - 1, 0);
row.setCursor(pacData.indent);
pacData.color = row.chars[prevPos].penState.foreground;
}
var styles = {
foreground: pacData.color,
underline: pacData.underline,
italics: pacData.italics,
background: 'black',
flash: false
};
this.setPen(styles);
},
/**
* Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
*/
setBkgData: function setBkgData(bkgData) {
logger.log("INFO", "bkgData = " + JSON.stringify(bkgData));
this.backSpace();
this.setPen(bkgData);
this.insertChar(0x20); //Space
},
setRollUpRows: function setRollUpRows(nrRows) {
this.nrRollUpRows = nrRows;
},
rollUp: function rollUp() {
if (this.nrRollUpRows === null) {
logger.log("DEBUG", "roll_up but nrRollUpRows not set yet");
return; //Not properly setup
}
logger.log("TEXT", this.getDisplayText());
var topRowIndex = this.currRow + 1 - this.nrRollUpRows;
var topRow = this.rows.splice(topRowIndex, 1)[0];
topRow.clear();
this.rows.splice(this.currRow, 0, topRow);
logger.log("INFO", "Rolling up"); //logger.log("TEXT", this.get_display_text())
},
/**
* Get all non-empty rows with as unicode text.
*/
getDisplayText: function getDisplayText(asOneRow) {
asOneRow = asOneRow || false;
var displayText = [];
var text = "";
var rowNr = -1;
for (var i = 0; i < NR_ROWS; i++) {
var rowText = this.rows[i].getTextString();
if (rowText) {
rowNr = i + 1;
if (asOneRow) {
displayText.push("Row " + rowNr + ': "' + rowText + '"');
} else {
displayText.push(rowText.trim());
}
}
}
if (displayText.length > 0) {
if (asOneRow) {
text = "[" + displayText.join(" | ") + "]";
} else {
text = displayText.join("\n");
}
}
return text;
},
getTextAndFormat: function getTextAndFormat() {
return this.rows;
}
};
/**
* Handle a CEA-608 channel and send decoded data to outputFilter
* @constructor
* @param {Number} channelNumber (1 or 2)
* @param {CueHandler} outputFilter Output from channel1 newCue(startTime, endTime, captionScreen)
*/
var Cea608Channel = function Cea608Channel(channelNumber, outputFilter) {
this.chNr = channelNumber;
this.outputFilter = outputFilter;
this.mode = null;
this.verbose = 0;
this.displayedMemory = new CaptionScreen();
this.nonDisplayedMemory = new CaptionScreen();
this.lastOutputScreen = new CaptionScreen();
this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
this.writeScreen = this.displayedMemory;
this.mode = null;
this.cueStartTime = null; // Keeps track of where a cue started.
};
Cea608Channel.prototype = {
modes: ["MODE_ROLL-UP", "MODE_POP-ON", "MODE_PAINT-ON", "MODE_TEXT"],
reset: function reset() {
this.mode = null;
this.displayedMemory.reset();
this.nonDisplayedMemory.reset();
this.lastOutputScreen.reset();
this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
this.writeScreen = this.displayedMemory;
this.mode = null;
this.cueStartTime = null;
this.lastCueEndTime = null;
},
getHandler: function getHandler() {
return this.outputFilter;
},
setHandler: function setHandler(newHandler) {
this.outputFilter = newHandler;
},
setPAC: function setPAC(pacData) {
this.writeScreen.setPAC(pacData);
},
setBkgData: function setBkgData(bkgData) {
this.writeScreen.setBkgData(bkgData);
},
setMode: function setMode(newMode) {
if (newMode === this.mode) {
return;
}
this.mode = newMode;
logger.log("INFO", "MODE=" + newMode);
if (this.mode == "MODE_POP-ON") {
this.writeScreen = this.nonDisplayedMemory;
} else {
this.writeScreen = this.displayedMemory;
this.writeScreen.reset();
}
if (this.mode !== "MODE_ROLL-UP") {
this.displayedMemory.nrRollUpRows = null;
this.nonDisplayedMemory.nrRollUpRows = null;
}
this.mode = newMode;
},
insertChars: fun