videojs-contrib-dash-s1
Version:
A Video.js source-handler providing MPEG-DASH playback.
1,376 lines (1,226 loc) • 1.42 MB
JavaScript
/**
* videojs-contrib-dash-s1
* @version 2.1.5
* @copyright 2016 Brightcove, Inc.
* @license Apache-2.0
*
* Includes dash.js <https://github.com/Dash-Industry-Forum/dash.js>
* Available under a BSD License
* <https://github.com/Dash-Industry-Forum/dash.js/blob/development/LICENSE.md>
*/
(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.videojsContribDash = 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){
(function (global){
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null);
var _videoJs2 = _interopRequireDefault(_videoJs);
var _html5Dashjs = require('./html5-dashjs');
var _html5Dashjs2 = _interopRequireDefault(_html5Dashjs);
/*
* The source handler to play using Dash.js
*
* @param {Object} source The source object
* @param {Html5} tech The instance of the Html5 tech
*/
var dashSourceHandler = {};
dashSourceHandler.canHandleSource = function (source) {
var dashExtRE = /\.mpd/i;
if (dashSourceHandler.canPlayType(source.type)) {
return 'probably';
} else if (dashExtRE.test(source.src)) {
return 'maybe';
}
return '';
};
dashSourceHandler.handleSource = function (source, tech) {
return new _html5Dashjs2['default'](source, tech);
};
dashSourceHandler.canPlayType = function (type) {
var dashTypeRE = /^application\/dash\+xml/i;
if (dashTypeRE.test(type)) {
return 'probably';
}
return '';
};
// Only add the SourceHandler if the browser supports MediaSourceExtensions
if (window.MediaSource) {
_videoJs2['default'].getComponent('Html5').registerSourceHandler(dashSourceHandler, 0);
}
exports['default'] = dashSourceHandler;
module.exports = exports['default'];
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./html5-dashjs":2}],2:[function(require,module,exports){
(function (global){
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
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; }; })();
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 _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null);
var _videoJs2 = _interopRequireDefault(_videoJs);
require('dashjs/dist/dash.all.debug');
/**
* Use Dash.js to playback DASH content inside of Video.js via a SourceHandler
*/
var Html5DashJS = (function () {
function Html5DashJS(source, tech) {
_classCallCheck(this, Html5DashJS);
var manifestSource = source.src;
this.tech_ = tech;
this.el_ = tech.el();
this.elParent_ = this.el_.parentNode;
// Do nothing if the src is falsey
if (!source.src) {
return;
}
// While the manifest is loading and Dash.js has not finished initializing
// we must defer events and functions calls with isReady_ and then `triggerReady`
// again later once everything is setup
tech.isReady_ = false;
this.keySystemOptions_ = Html5DashJS.buildDashJSProtData(source.keySystemOptions);
// We have to hide errors since SRC_UNSUPPORTED is thrown by the video element when
// we set src = '' in order to clear the mediaKeys
this.hideErrors();
// Must be before anything is initialized since we are overridding a global object
// injection
if (Html5DashJS.useVideoJSDebug) {
Html5DashJS.useVideoJSDebug(_videoJs2['default']);
}
// Save the context after the first initialization for subsequent instances
Html5DashJS.context_ = Html5DashJS.context_ || {};
// But make a fresh MediaPlayer each time the sourceHandler is used
this.mediaPlayer_ = window.dashjs.MediaPlayer(Html5DashJS.context_).create();
// Initialize the media player with the element and autoplay settings
this.mediaPlayer_.initialize();
this.mediaPlayer_.setLimitBitrateByPortal(true);
this.mediaPlayer_.attachView(this.el_);
// Dash.js autoplays by default
if (!tech.options_.autoplay) {
this.mediaPlayer_.setAutoPlay(false);
}
// Fetches and parses the manifest - WARNING the callback is non-standard
// "error-last" style
this.mediaPlayer_.retrieveManifest(manifestSource, _videoJs2['default'].bind(this, this.initializeDashJS));
}
_createClass(Html5DashJS, [{
key: 'initializeDashJS',
value: function initializeDashJS(manifest, err) {
var _this = this;
var manifestProtectionData = {};
if (err) {
this.showErrors();
this.tech_.triggerReady();
this.dispose();
return;
}
// If we haven't received protection data from the outside world try to get it from
// the manifest, We merge the two allowing the manifest to override any
// keySystemOptions provided via src()
if (Html5DashJS.getWidevineProtectionData) {
manifestProtectionData = Html5DashJS.getWidevineProtectionData(manifest);
this.keySystemOptions_ = _videoJs2['default'].mergeOptions(this.keySystemOptions_, manifestProtectionData);
}
// We have to reset any mediaKeys before the attachSource call below
this.resetSrc_(function () {
_this.showErrors();
// Attach the source with any protection data
_this.mediaPlayer_.attachSource(manifest, null, _this.keySystemOptions_);
_this.tech_.triggerReady();
});
}
/**
* Add a css-class that is used to temporarily hide the error dialog while so that
* we don't see a flash of the dialog box when we remove the video element's src
* to reset MediaKeys in resetSrc_
*/
}, {
key: 'hideErrors',
value: function hideErrors() {
this.elParent_.className += ' vjs-dashjs-hide-errors';
}
/**
* Remove the css-class above to enable the error dialog to be shown once again
*/
}, {
key: 'showErrors',
value: function showErrors() {
var _this2 = this;
// The video element's src is set asynchronously so we have to wait a while
// before we unhide any errors
// 250ms is arbitrary but I haven't seen dash.js take longer than that to initialize
// in my testing
setTimeout(function () {
_this2.elParent_.className = _this2.elParent_.className.replace(' vjs-dashjs-hide-errors', '');
}, 250);
}
/**
* Iterate over the `keySystemOptions` array and convert each object into
* the type of object Dash.js expects in the `protData` argument.
*
* Also rename 'licenseUrl' property in the options to an 'serverURL' property
*/
}, {
key: 'resetSrc_',
/**
* Helper function to clear any EME keys that may have been set on the video element
*
* The MediaKeys has to be explicitly set to null before any DRM content can be loaded
* into a video element that already contained DRM content.
*/
value: function resetSrc_(callback) {
// In Chrome, MediaKeys can NOT be changed when a src is loaded in the video element
// Dash.js has a bug where it doesn't correctly reset the data so we do it manually
// The order of these two lines is important. The video element's src must be reset
// to allow `mediaKeys` to changed otherwise a DOMException is thrown.
if (this.el_) {
this.el_.src = '';
if (this.el_.setMediaKeys) {
this.el_.setMediaKeys(null).then(callback, callback);
} else {
callback();
}
}
}
}, {
key: 'dispose',
value: function dispose() {
if (this.mediaPlayer_) {
this.mediaPlayer_.reset();
}
this.resetSrc_(function () {});
}
}], [{
key: 'buildDashJSProtData',
value: function buildDashJSProtData(keySystemOptions) {
var output = {};
if (!keySystemOptions || !Array.isArray(keySystemOptions)) {
return output;
}
for (var i = 0; i < keySystemOptions.length; i++) {
var keySystem = keySystemOptions[i];
var options = _videoJs2['default'].mergeOptions({}, keySystem.options);
if (options.licenseUrl) {
options.serverURL = options.licenseUrl;
delete options.licenseUrl;
}
output[keySystem.name] = options;
}
return output;
}
}]);
return Html5DashJS;
})();
exports['default'] = Html5DashJS;
module.exports = exports['default'];
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"dashjs/dist/dash.all.debug":3}],3:[function(require,module,exports){
(function (global){
(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(_dereq_,module,exports){
/* $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.
'use strict';
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) {
// no need to mask byte
} 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+/");
if (undefined === btoa) {
var btoa = BASE64.encode;
}
if (undefined === atob) {
var atob = BASE64.decode;
}
if (typeof exports !== 'undefined') {
exports.decode = BASE64.decode;
exports.decodeArray = BASE64.decodeArray;
}
},{}],2:[function(_dereq_,module,exports){
/**
* 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.
*/
'use strict';
(function (exports) {
"use strict";
/**
* 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(byte) {
if (byte >= 0x90) {
//Extended char
this.backSpace();
}
var char = getCharForByte(byte);
if (this.pos >= NR_COLS) {
logger.log("ERROR", "Cannot insert " + byte.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 char = this.chars[i].uchar;
if (char !== " ") {
empty = false;
}
chars.push(char);
}
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(char) {
var row = this.rows[this.currRow];
row.insertChar(char);
},
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: function insertChars(chars) {
for (var i = 0; i < chars.length; i++) {
this.writeScreen.insertChar(chars[i]);
}
var screen = this.writeScreen === this.displayedMemory ? "DISP" : "NON_DISP";
logger.log("INFO", screen + ": " + this.writeScreen.getDisplayText(true));
if (this.mode === "MODE_PAINT-ON" || this.mode === "MODE_ROLL-UP") {
logger.log("TEXT", "DISPLAYED: " + this.displayedMemory.getDisplayText(true));
this.outputDataUpdate();
}
},
cc_RCL: function cc_RCL() {
// Resume Caption Loading (switch mode to Pop On)
logger.log("INFO", "RCL - Resume Caption Loading");
this.setMode("MODE_POP-ON");
},
cc_BS: function cc_BS() {
// BackSpace
logger.log("INFO", "BS - BackSpace");
if (this.mode === "MODE_TEXT") {
return;
}
this.writeScreen.backSpace();
if (this.writeScreen === this.displayedMemory) {
this.outputDataUpdate();
}
},
cc_AOF: function cc_AOF() {
// Reserved (formerly Alarm Off)
return;
},
cc_AON: function cc_AON() {
// Reserved (formerly Alarm On)
return;
},
cc_DER: function cc_DER() {
// Delete to End of Row
logger.log("INFO", "DER- Delete to End of Row");
this.writeScreen.clearToEndOfRow();
this.outputDataUpdate();
},
cc_RU: function cc_RU(nrRows) {
//Roll-Up Captions-2,3,or 4 Rows
logger.log("INFO", "RU(" + nrRows + ") - Roll Up");
this.writeScreen = this.displayedMemory;
this.setMode("MODE_ROLL-UP");
this.writeScreen.setRollUpRows(nrRows);
},
cc_FON: function cc_FON() {
//Flash On
logger.log("INFO", "FON - Flash On");
this.writeScreen.setPen({ flash: true });
},
cc_RDC: function cc_RDC() {
// Resume Direct Captioning (switch mode to PaintOn)
logger.log("INFO", "RDC - Resume Direct Captioning");
this.setMode("MODE_PAINT-ON");
},
cc_TR: function cc_TR() {
// Text Restart in text mode (not supported, however)
logger.log("INFO", "TR");
this.setMode("MODE_TEXT");
},
cc_RTD: function cc_RTD() {
// Resume Text Display in Text mode (not supported, however)
logger.log("INFO", "RTD");
this.setMode("MODE_TEXT");
},
cc_EDM: function cc_EDM() {
// Erase Displayed Memory
logger.log("INFO", "EDM - Erase Displayed Memory");
this.displayedMemory.reset();
this.outputDataUpdate();
},
cc_CR: function cc_CR() {
// Carriage Return
logger.log("CR - Carriage Return");
this.writeScreen.rollUp();
this.outputDataUpdate();
},
cc_ENM: function cc_ENM() {
//Erase Non-Displayed Memory
logger.log("INFO", "ENM - Erase Non-displayed Memory");
this.nonDisplayedMemory.reset();
},
cc_EOC: function cc_EOC() {
//End of Caption (Flip Memories)
logger.log("INFO", "EOC - End Of Caption");
if (this.mode === "MODE_POP-ON") {
var tmp = this.displayedMemory;
this.displayedMemory = this.nonDisplayedMemory;
this.nonDisplayedMemory = tmp;
this.writeScreen = this.nonDisplayedMemory;
logger.log("TEXT", "DISP: " + this.displayedMemory.getDisplayText());
}
this.outputDataUpdate();
},
cc_TO: function cc_TO(nrCols) {
// Tab Offset 1,2, or 3 columns
logger.log("INFO", "TO(" + nrCols + ") - Tab Offset");
this.writeScreen.moveCursor(nrCols);
},
cc_MIDROW: function cc_MIDROW(secondByte) {
// Parse MIDROW command
var styles = { flash: false };
styles.underline = secondByte % 2 === 1;
styles.italics = secondByte >= 0x2e;
if (!styles.italics) {
var colorIndex = Math.floor(secondByte / 2) - 0x10;
var colors = ["white", "green", "blue", "cyan", "red", "yellow", "magenta"];
styles.foreground = colors[colorIndex];
} else {
styles.foreground = "white";
}
logger.log("INFO", "MIDROW: " + JSON.stringify(styles));
this.writeScreen.setPen(styles);
},
outputDataUpdate: function outputDataUpdate() {
var t = logger.time;
if (t === null) {
return;
}
if (this.outputFilter) {
if (this.outputFilter.updateData) {
this.outputFilter.updateData(t, this.displayedMemory);
}
if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) {
// Start of a new cue
this.cueStartTime = t;
} else {
if (!this.displayedMemory.equals(this.lastOutputScreen)) {
if (this.outputFilter.newCue) {
this.outputFilter.newCue(this.cueStartTime, t, this.lastOutputScreen);
}
this.cueStartTime = this.displayedMemory.isEmpty() ? null : t;
}
}
this.lastOutputScreen.copy(this.displayedMemory);
}
},
cueSplitAtTime: function cueSplitAtTime(t) {
if (this.outputFilter) {
if (!this.displayedMemory.isEmpty()) {
if (this.outputFilter.newCue) {
this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory);
}
this.cueStartTime = t;
}
}
}
};
/**
* Parse CEA-608 data and send decoded data to out1 and out2.
* @constructor
* @param {Number} field CEA-608 field (1 or 2)
* @param {CueHandler} out1 Output from channel1 newCue(startTime, endTime, captionScreen)
* @param {CueHandler} out2 Output from channel2 newCue(startTime, endTime, captionScreen)
*/
var Cea608Parser = function Cea608Parser(field, out1, out2) {
this.field = field || 1;
this.outputs = [out1, out2];
this.channels = [new Cea608Channel(1, out1), new Cea608Channel(2, out2)];
this.currChNr = -1; // Will be 1 or 2
this.lastCmdA = null; // First byte of last command
this.lastCmdB = null; // Second byte of last command
this.bufferedData = [];
this.startTime = null;
this.lastTime = null;
this.dataCounters = { 'padding': 0, 'char': 0, 'cmd': 0, 'other': 0 };
};
Cea608Parser.prototype = {
getHandler: function getHandler(index) {
return this.channels[index].getHandler();
},
setHandler: function setHandler(index, newHandler) {
this.channels[index].setHandler(newHandler);
},
/**
* Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
*/
addData: function addData(t, byteList) {
var cmdFound,
a,
b,
charsFound = false;
this.lastTime = t;
logger.setTime(t);
for (var i = 0; i < byteList.length; i += 2) {
a = byteList[i] & 0x7f;
b = byteList[i + 1] & 0x7f;
if (a === 0 && b === 0) {
this.dataCounters.padding += 2;
continue;
} else {
logger.log("DATA", "[" + numArrayToHexArray([byteList[i], byteList[i + 1]]) + "] -> (" + numArrayToHexArray([a, b]) + ")");
}
cmdFound = this.parseCmd(a, b);
if (!cmdFound) {
cmdFound = this.parseMidrow(a, b);
}
if (!cmdFound) {
cmdFound = this.parsePAC(a, b);
}
if (!cmdFound) {
cmdFound = this.parseBackgroundAttributes(a, b);
}
if (!cmdFound) {
charsFound = this.parseChars(a, b);
if (charsFound) {
if (this.currChNr && this.currChNr >= 0) {
var channel = this.channels[this.currChNr - 1];
channel.insertChars(charsFound);
} else {
logger.log("WARNING", "No channel found yet. TEXT-MODE?");
}
}
}
if (cmdFound) {
this.dataCounters.cmd += 2;
} else if (charsFound) {
this.dataCounters.char += 2;
} else {
this.dataCounters.other += 2;
logger.log("WARNING", "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + " orig: " + numArrayToHexArray([byteList[i], byteList[i + 1]]));
}
}
},
/**
* Parse Command.
* @returns {Boolean} Tells if a command was found
*/
parseCmd: function parseCmd(a, b) {
var chNr = null;
var cond1 = (a === 0x14 || a === 0x1C) && 0x20 <= b && b <= 0x2F;
var cond2 = (a === 0x17 || a === 0x1F) && 0x21 <= b && b <= 0x23;
if (!(cond1 || cond2)) {
return false;
}
if (a === this.lastCmdA && b === this.lastCmdB) {
this.lastCmdA = null;
this.lastCmdB = null; // Repeated commands are dropped (once)
logger.log("DEBUG", "Repeated command (" + numArrayToHexArray([a, b]) + ") is dropped");
return true;
}
if (a === 0x14 || a === 0x17) {
chNr = 1;
} else {
chNr = 2; // (a === 0x1C || a=== 0x1f)
}
var channel = this.channels[chNr - 1];
if (a === 0x14 || a === 0x1C) {
if (b === 0x20) {
channel.cc_RCL();
} else if (b === 0x21) {
channel.cc_BS();
} else if (b === 0x22) {
channel.cc_AOF();
} else if (b === 0x23) {
channel.cc_AON();
} else if (b === 0x24) {
channel.cc_DER();
} else if (b === 0x25) {
channel.cc_RU(2);
} else if (b === 0x26) {
channel.cc_RU(3);
} else if (b === 0x27) {