UNPKG

videojs-contrib-dash

Version:

A Video.js source-handler providing MPEG-DASH playback.

1,710 lines (1,553 loc) 2.86 MB
/*! @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