videojs-contrib-dash
Version:
A Video.js source-handler providing MPEG-DASH playback.
873 lines (825 loc) • 2.13 MB
JavaScript
/*! @name videojs-contrib-dash @version 4.1.0 @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 setTextDefaultEnabledCalled = false;
var setTextDefaultEnabledValue = 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(9); // 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;
},
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(setTextDefaultEnabledCalled, true, 'MediaPlayer.setTextDefaultEnabled was called');
assert.strictEqual(setTextDefaultEnabledValue, false, 'MediaPlayer.setTextDefaultEnabled 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);
});
},
setTextDefaultEnabled: function setTextDefaultEnabled(value) {
setTextDefaultEnabledCalled = true;
setTextDefaultEnabledValue = value;
}
};
}
};
};
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(11);
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(20);
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(10);
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(9 + 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(11);
});
var cea608Parser = createCommonjsModule(function (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.
*/(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(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>=0x10&&a<=0x1f&&a===this.lastCmdA&&b===this.lastCmdB){this.lastCmdA=null;this.lastCmdB=null;logger.log("DEBUG","Repeated command ("+numArrayToHexArray([a,b])+") is dropped");continue;// Repeated commands are dropped (once)
}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===0x15||a===0x1C||a===0x1D)&&0x20<=b&&b<=0x2F;var cond2=(a===0x17||a===0x1F)&&0x21<=b&&b<=0x23;if(!(cond1||cond2)){return false;}if(a===0x14||a===0x15||a===0x17){chNr=1;}else{chNr=2;// (a === 0x1C || a === 0x1D || a=== 0x1f)
}var channel=this.channels[chNr-1];if(a===0x14||a===0x15||a===0x1C||a===0x1D){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){channel.cc_RU(4);}else if(b===0x28){channel.cc_FON();}else if(b===0x29){channel.cc_RDC();}else if(b===0x2A){channel.cc_TR();}else if(b===0x2B){channel.cc_RTD();}else if(b===0x2C){channel.cc_EDM();}else if(b===0x2D){channel.cc_CR();}else if(b===0x2E){channel.cc_ENM();}else if(b===0x2F){channel.cc_EOC();}}else{//a == 0x17 || a == 0x1F
channel.cc_TO(b-0x20);}this.lastCmdA=a;this.lastCmdB=b;this.currChNr=chNr;return true;},/**
* Parse midrow styling command
* @returns {Boolean}
*/parseMidrow:function parseMidrow(a,b){var chNr=null;if((a===0x11||a===0x19)&&0x20<=b&&b<=0x2f){if(a===0x11){chNr=1;}else{chNr=2;}if(chNr!==this.currChNr){logger.log("ERROR","Mismatch channel in midrow parsing");return false;}var channel=this.channels[chNr-1];// cea608 spec says midrow codes should inject a space
channel.insertChars([0x20]);channel.cc_MIDROW(b);logger.log("DEBUG","MIDROW ("+numArrayToHexArray([a,b])+")");this.lastCmdA=a;this.lastCmdB=b;return true;}return false;},/**
* Parse Preable Access Codes (Table 53).
* @returns {Boolean} Tells if PAC found
*/parsePAC:function parsePAC(a,b){var chNr=null;var row=null;var case1=(0x11<=a&&a<=0x17||0x19<=a&&a<=0x1F)&&0x40<=b&&b<=0x7F;var case2=(a===0x10||a===0x18)&&0x40<=b&&b<=0x5F;if(!(case1||case2)){return false;}chNr=a<=0x17?1:2;if(0x40<=b&&b<=0x5F){row=chNr===1?rowsLowCh1[a]:rowsLowCh2[a];}else{// 0x60 <= b <= 0x7F
row=chNr===1?rowsHighCh1[a]:rowsHighCh2[a];}var pacData=this.interpretPAC(row,b);var channel=this.channels[chNr-1];channel.setPAC(pacData);this.lastCmdA=a;this.lastCmdB=b;this.currChNr=chNr;return true;},/**
* Interpret the second byte of the pac, and return the information.
* @returns {Object} pacData with style parameters.
*/interpretPAC:function interpretPAC(row,byte){var pacIndex=byte;var pacData={color:null,italics:false,indent:null,underline:false,row:row};if(byte>0x5F){pacIndex=byte-0x60;}else{pacIndex=byte-0x40;}pacData.underline=(pacIndex&1)===1;if(pacIndex<=0xd){pacData.color=['white','green','blue','cyan','red','yellow','magenta','white'][Math.floor(pacIndex/2)];}else if(pacIndex<=0xf){pacData.italics=true;pacData.color='white';}else{pacData.indent=Math.floor((pacIndex-0x10)/2)*4;}return pacData;// Note that row has zero offset. The spec uses 1.
},/**
* Parse characters.
* @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
*/parseChars:function parseChars(a,b){var channelNr=null,charCodes=null,charCode1=null;if(a>=0x19){channelNr=2;charCode1=a-8;}else{channelNr=1;charCode1=a;}if(0x11<=charCode1&&charCode1<=0x13){// Special character
var oneCode=b;if(charCode1===0x11){oneCode=b+0x50;}else if(charCode1===0x12){oneCode=b+0x70;}else{oneCode=b+0x90;}logger.log("INFO","Special char '"+getCharForByte(oneCode)+"' in channel "+channelNr);charCodes=[oneCode];this.lastCmdA=a;this.lastCmdB=b;}else if(0x20<=a&&a<=0x7f){charCodes=b===0?[a]:[a,b];this.lastCmdA=null;this.lastCmdB=null;}if(charCodes){var hexCodes=numArrayToHexArray(charCodes);logger.log("DEBUG","Char codes = "+hexCodes.join(","));}return charCodes;},/**
* Parse extended background attributes as well as new foreground color black.
* @returns{Boolean} Tells if background attributes are found
*/parseBackgroundAttributes:function parseBackgroundAttributes(a,b){var bkgData,index,chNr,channel;var case1=(a===0x10||a===0x18)&&0x20<=b&&b<=0x2f;var case2=(a===0x17||a===0x1f)&&0x2d<=b&&b<=0x2f;if(!(case1||case2)){return false;}bkgData={};if(a===0x10||a===0x18){index=Math.floor((b-0x20)/2);bkgData.background=backgroundColors[index];if(b%2===1){bkgData.background=bkgData.background+"_semi";}}else if(b===0x2d){bkgData.background="transparent";}else{bkgData.foreground="black";if(b===0x2f){bkgData.underline=true;}}chNr=a<0x18?1:2;channel=this.channels[chNr-1];channel.setBkgData(bkgData);this.lastCmdA=a;this.lastCmdB=b;return true;},/**
* Reset state of parser and its channels.
*/reset:function reset(){for(var i=0;i<this.channels.length;i++){if(this.channels[i]){this.channels[i].reset();}}this.lastCmdA=null;this.lastCmdB=null;},/**
* Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
*/cueSplitAtTime:function cueSplitAtTime(t){for(var i=0;i<this.channels.length;i++){if(this.channels[i]){this.channels[i].cueSplitAtTime(t);}}}};/**
* Find ranges corresponding to SEA CEA-608 NALUS in sizeprepended NALU array.
* @param {raw} dataView of binary data
* @param {startPos} start position in raw
* @param {size} total size of data in raw to consider
* @returns
*/var findCea608Nalus=function findCea608Nalus(raw,startPos,size){var nalSize=0,cursor=startPos,nalType=0,cea608NaluRanges=[],// Check SEI data according to ANSI-SCTE 128
isCEA608SEI=function isCEA608SEI(payloadType,payloadSize,raw,pos){if(payloadType!==4||payloadSize<8){return null;}var countryCode=raw.getUint8(pos);var providerCode=raw.getUint16(pos+1);var userIdentifier=raw.getUint32(pos+3);var userDataTypeCode=raw.getUint8(pos+7);return countryCode==0xB5&&providerCode==0x31&&userIdentifier==0x47413934&&userDataTypeCode==0x3;};while(cursor<startPos+size){nalSize=raw.getUint32(cursor);nalType=raw.getUint8(cursor+4)&0x1F;//console.log(time + " NAL " + nalType);
if(nalType===6){// SEI NAL Unit. The NAL header is the first byte
//console.log("SEI NALU of size " + nalSize + " at time " + time);
var pos=cursor+5;var payloadType=-1;while(pos<cursor+4+nalSize-1){// The last byte should be rbsp_trailing_bits
payloadType=0;var b=0xFF;while(b===0xFF){b=raw.getUint8(pos);payloadType+=b;pos++;}var payloadSize=0;b=0xFF;while(b===0xFF){b=raw.getUint8(pos);payloadSize+=b;pos++;}if(isCEA608SEI(payloadType,payloadSize,raw,pos)){//console.log("CEA608 SEI " + time + " " + payloadSize);
cea608NaluRanges.push([pos,payloadSize]);}pos+=payloadSize;}}cursor+=nalSize+4;}return cea608NaluRanges;};var extractCea608DataFromRange=function extractCea608DataFromRange(raw,cea608Range){var pos=cea608R