1421 lines
37 KiB
TypeScript
1421 lines
37 KiB
TypeScript
import OutputFilter from './output-filter';
|
|
import { logger } from '../utils/logger';
|
|
|
|
/**
|
|
*
|
|
* This code was ported from the dash.js project at:
|
|
* https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js
|
|
* https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2
|
|
*
|
|
* The original copyright appears below:
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes
|
|
*/
|
|
|
|
const 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
|
|
};
|
|
|
|
/**
|
|
* Utils
|
|
*/
|
|
const getCharForByte = (byte: number) =>
|
|
String.fromCharCode(specialCea608CharsCodes[byte] || byte);
|
|
|
|
const NR_ROWS = 15;
|
|
const NR_COLS = 100;
|
|
// Tables to look up row from PAC data
|
|
const rowsLowCh1 = {
|
|
0x11: 1,
|
|
0x12: 3,
|
|
0x15: 5,
|
|
0x16: 7,
|
|
0x17: 9,
|
|
0x10: 11,
|
|
0x13: 12,
|
|
0x14: 14,
|
|
};
|
|
const rowsHighCh1 = {
|
|
0x11: 2,
|
|
0x12: 4,
|
|
0x15: 6,
|
|
0x16: 8,
|
|
0x17: 10,
|
|
0x13: 13,
|
|
0x14: 15,
|
|
};
|
|
const rowsLowCh2 = {
|
|
0x19: 1,
|
|
0x1a: 3,
|
|
0x1d: 5,
|
|
0x1e: 7,
|
|
0x1f: 9,
|
|
0x18: 11,
|
|
0x1b: 12,
|
|
0x1c: 14,
|
|
};
|
|
const rowsHighCh2 = {
|
|
0x19: 2,
|
|
0x1a: 4,
|
|
0x1d: 6,
|
|
0x1e: 8,
|
|
0x1f: 10,
|
|
0x1b: 13,
|
|
0x1c: 15,
|
|
};
|
|
|
|
const backgroundColors = [
|
|
'white',
|
|
'green',
|
|
'blue',
|
|
'cyan',
|
|
'red',
|
|
'yellow',
|
|
'magenta',
|
|
'black',
|
|
'transparent',
|
|
];
|
|
|
|
const enum VerboseLevel {
|
|
ERROR = 0,
|
|
TEXT = 1,
|
|
WARNING = 2,
|
|
INFO = 2,
|
|
DEBUG = 3,
|
|
DATA = 3,
|
|
}
|
|
|
|
class CaptionsLogger {
|
|
public time: number | null = null;
|
|
public verboseLevel: VerboseLevel = VerboseLevel.ERROR;
|
|
|
|
log(severity: VerboseLevel, msg: string | (() => string)): void {
|
|
if (this.verboseLevel >= severity) {
|
|
const m: string = typeof msg === 'function' ? msg() : msg;
|
|
logger.log(`${this.time} [${severity}] ${m}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const numArrayToHexArray = function (numArray: number[]): string[] {
|
|
const hexArray: string[] = [];
|
|
for (let j = 0; j < numArray.length; j++) {
|
|
hexArray.push(numArray[j].toString(16));
|
|
}
|
|
|
|
return hexArray;
|
|
};
|
|
|
|
type PenStyles = {
|
|
foreground: string | null;
|
|
underline: boolean;
|
|
italics: boolean;
|
|
background: string;
|
|
flash: boolean;
|
|
};
|
|
|
|
class PenState {
|
|
public foreground: string = 'white';
|
|
public underline: boolean = false;
|
|
public italics: boolean = false;
|
|
public background: string = 'black';
|
|
public flash: boolean = false;
|
|
|
|
reset() {
|
|
this.foreground = 'white';
|
|
this.underline = false;
|
|
this.italics = false;
|
|
this.background = 'black';
|
|
this.flash = false;
|
|
}
|
|
|
|
setStyles(styles: Partial<PenStyles>) {
|
|
const attribs = [
|
|
'foreground',
|
|
'underline',
|
|
'italics',
|
|
'background',
|
|
'flash',
|
|
];
|
|
for (let i = 0; i < attribs.length; i++) {
|
|
const style = attribs[i];
|
|
if (styles.hasOwnProperty(style)) {
|
|
this[style] = styles[style];
|
|
}
|
|
}
|
|
}
|
|
|
|
isDefault() {
|
|
return (
|
|
this.foreground === 'white' &&
|
|
!this.underline &&
|
|
!this.italics &&
|
|
this.background === 'black' &&
|
|
!this.flash
|
|
);
|
|
}
|
|
|
|
equals(other: PenState) {
|
|
return (
|
|
this.foreground === other.foreground &&
|
|
this.underline === other.underline &&
|
|
this.italics === other.italics &&
|
|
this.background === other.background &&
|
|
this.flash === other.flash
|
|
);
|
|
}
|
|
|
|
copy(newPenState: PenState) {
|
|
this.foreground = newPenState.foreground;
|
|
this.underline = newPenState.underline;
|
|
this.italics = newPenState.italics;
|
|
this.background = newPenState.background;
|
|
this.flash = newPenState.flash;
|
|
}
|
|
|
|
toString(): string {
|
|
return (
|
|
'color=' +
|
|
this.foreground +
|
|
', underline=' +
|
|
this.underline +
|
|
', italics=' +
|
|
this.italics +
|
|
', background=' +
|
|
this.background +
|
|
', flash=' +
|
|
this.flash
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unicode character with styling and background.
|
|
* @constructor
|
|
*/
|
|
class StyledUnicodeChar {
|
|
uchar: string = ' ';
|
|
penState: PenState = new PenState();
|
|
|
|
reset() {
|
|
this.uchar = ' ';
|
|
this.penState.reset();
|
|
}
|
|
|
|
setChar(uchar: string, newPenState: PenState) {
|
|
this.uchar = uchar;
|
|
this.penState.copy(newPenState);
|
|
}
|
|
|
|
setPenState(newPenState: PenState) {
|
|
this.penState.copy(newPenState);
|
|
}
|
|
|
|
equals(other: StyledUnicodeChar) {
|
|
return this.uchar === other.uchar && this.penState.equals(other.penState);
|
|
}
|
|
|
|
copy(newChar: StyledUnicodeChar) {
|
|
this.uchar = newChar.uchar;
|
|
this.penState.copy(newChar.penState);
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
return this.uchar === ' ' && this.penState.isDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
|
|
* @constructor
|
|
*/
|
|
export class Row {
|
|
public chars: StyledUnicodeChar[] = [];
|
|
public pos: number = 0;
|
|
public currPenState: PenState = new PenState();
|
|
public cueStartTime: number | null = null;
|
|
private logger: CaptionsLogger;
|
|
|
|
constructor(logger: CaptionsLogger) {
|
|
for (let i = 0; i < NR_COLS; i++) {
|
|
this.chars.push(new StyledUnicodeChar());
|
|
}
|
|
this.logger = logger;
|
|
}
|
|
|
|
equals(other: Row) {
|
|
for (let i = 0; i < NR_COLS; i++) {
|
|
if (!this.chars[i].equals(other.chars[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
copy(other: Row) {
|
|
for (let i = 0; i < NR_COLS; i++) {
|
|
this.chars[i].copy(other.chars[i]);
|
|
}
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
let empty = true;
|
|
for (let i = 0; i < NR_COLS; i++) {
|
|
if (!this.chars[i].isEmpty()) {
|
|
empty = false;
|
|
break;
|
|
}
|
|
}
|
|
return empty;
|
|
}
|
|
|
|
/**
|
|
* Set the cursor to a valid column.
|
|
*/
|
|
setCursor(absPos: number) {
|
|
if (this.pos !== absPos) {
|
|
this.pos = absPos;
|
|
}
|
|
|
|
if (this.pos < 0) {
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
'Negative cursor position ' + this.pos,
|
|
);
|
|
this.pos = 0;
|
|
} else if (this.pos > NR_COLS) {
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
'Too large cursor position ' + this.pos,
|
|
);
|
|
this.pos = NR_COLS;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the cursor relative to current position.
|
|
*/
|
|
moveCursor(relPos: number) {
|
|
const newPos = this.pos + relPos;
|
|
if (relPos > 1) {
|
|
for (let 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() {
|
|
this.moveCursor(-1);
|
|
this.chars[this.pos].setChar(' ', this.currPenState);
|
|
}
|
|
|
|
insertChar(byte: number) {
|
|
if (byte >= 0x90) {
|
|
// Extended char
|
|
this.backSpace();
|
|
}
|
|
const char = getCharForByte(byte);
|
|
if (this.pos >= NR_COLS) {
|
|
this.logger.log(
|
|
VerboseLevel.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(startPos: number) {
|
|
let i: number;
|
|
for (i = startPos; i < NR_COLS; i++) {
|
|
this.chars[i].reset();
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.clearFromPos(0);
|
|
this.pos = 0;
|
|
this.currPenState.reset();
|
|
}
|
|
|
|
clearToEndOfRow() {
|
|
this.clearFromPos(this.pos);
|
|
}
|
|
|
|
getTextString() {
|
|
const chars: string[] = [];
|
|
let empty = true;
|
|
for (let i = 0; i < NR_COLS; i++) {
|
|
const char = this.chars[i].uchar;
|
|
if (char !== ' ') {
|
|
empty = false;
|
|
}
|
|
|
|
chars.push(char);
|
|
}
|
|
if (empty) {
|
|
return '';
|
|
} else {
|
|
return chars.join('');
|
|
}
|
|
}
|
|
|
|
setPenStyles(styles: Partial<PenStyles>) {
|
|
this.currPenState.setStyles(styles);
|
|
const currChar = this.chars[this.pos];
|
|
currChar.setPenState(this.currPenState);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keep a CEA-608 screen of 32x15 styled characters
|
|
* @constructor
|
|
*/
|
|
export class CaptionScreen {
|
|
rows: Row[] = [];
|
|
currRow: number = NR_ROWS - 1;
|
|
nrRollUpRows: number | null = null;
|
|
lastOutputScreen: CaptionScreen | null = null;
|
|
logger: CaptionsLogger;
|
|
|
|
constructor(logger: CaptionsLogger) {
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
this.rows.push(new Row(logger));
|
|
}
|
|
this.logger = logger;
|
|
}
|
|
|
|
reset() {
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
this.rows[i].clear();
|
|
}
|
|
this.currRow = NR_ROWS - 1;
|
|
}
|
|
|
|
equals(other: CaptionScreen): boolean {
|
|
let equal = true;
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
if (!this.rows[i].equals(other.rows[i])) {
|
|
equal = false;
|
|
break;
|
|
}
|
|
}
|
|
return equal;
|
|
}
|
|
|
|
copy(other: CaptionScreen) {
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
this.rows[i].copy(other.rows[i]);
|
|
}
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
let empty = true;
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
if (!this.rows[i].isEmpty()) {
|
|
empty = false;
|
|
break;
|
|
}
|
|
}
|
|
return empty;
|
|
}
|
|
|
|
backSpace() {
|
|
const row = this.rows[this.currRow];
|
|
row.backSpace();
|
|
}
|
|
|
|
clearToEndOfRow() {
|
|
const row = this.rows[this.currRow];
|
|
row.clearToEndOfRow();
|
|
}
|
|
|
|
/**
|
|
* Insert a character (without styling) in the current row.
|
|
*/
|
|
insertChar(char: number) {
|
|
const row = this.rows[this.currRow];
|
|
row.insertChar(char);
|
|
}
|
|
|
|
setPen(styles: Partial<PenStyles>) {
|
|
const row = this.rows[this.currRow];
|
|
row.setPenStyles(styles);
|
|
}
|
|
|
|
moveCursor(relPos: number) {
|
|
const row = this.rows[this.currRow];
|
|
row.moveCursor(relPos);
|
|
}
|
|
|
|
setCursor(absPos: number) {
|
|
this.logger.log(VerboseLevel.INFO, 'setCursor: ' + absPos);
|
|
const row = this.rows[this.currRow];
|
|
row.setCursor(absPos);
|
|
}
|
|
|
|
setPAC(pacData: PACData) {
|
|
this.logger.log(
|
|
VerboseLevel.INFO,
|
|
() => 'pacData = ' + JSON.stringify(pacData),
|
|
);
|
|
let newRow = pacData.row - 1;
|
|
if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
|
|
newRow = this.nrRollUpRows - 1;
|
|
}
|
|
|
|
// Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
|
|
if (this.nrRollUpRows && this.currRow !== newRow) {
|
|
// clear all rows first
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
this.rows[i].clear();
|
|
}
|
|
|
|
// Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
|
|
// topRowIndex - the start of rows to copy (inclusive index)
|
|
const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
|
|
// We only copy if the last position was already shown.
|
|
// We use the cueStartTime value to check this.
|
|
const lastOutputScreen = this.lastOutputScreen;
|
|
if (lastOutputScreen) {
|
|
const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
|
|
const time = this.logger.time;
|
|
if (prevLineTime !== null && time !== null && prevLineTime < time) {
|
|
for (let i = 0; i < this.nrRollUpRows; i++) {
|
|
this.rows[newRow - this.nrRollUpRows + i + 1].copy(
|
|
lastOutputScreen.rows[topRowIndex + i],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.currRow = newRow;
|
|
const row = this.rows[this.currRow];
|
|
if (pacData.indent !== null) {
|
|
const indent = pacData.indent;
|
|
const prevPos = Math.max(indent - 1, 0);
|
|
row.setCursor(pacData.indent);
|
|
pacData.color = row.chars[prevPos].penState.foreground;
|
|
}
|
|
const styles: PenStyles = {
|
|
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(bkgData: Partial<PenStyles>) {
|
|
this.logger.log(
|
|
VerboseLevel.INFO,
|
|
() => 'bkgData = ' + JSON.stringify(bkgData),
|
|
);
|
|
this.backSpace();
|
|
this.setPen(bkgData);
|
|
this.insertChar(0x20); // Space
|
|
}
|
|
|
|
setRollUpRows(nrRows: number | null) {
|
|
this.nrRollUpRows = nrRows;
|
|
}
|
|
|
|
rollUp() {
|
|
if (this.nrRollUpRows === null) {
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
'roll_up but nrRollUpRows not set yet',
|
|
);
|
|
return; // Not properly setup
|
|
}
|
|
this.logger.log(VerboseLevel.TEXT, () => this.getDisplayText());
|
|
const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
|
|
const topRow = this.rows.splice(topRowIndex, 1)[0];
|
|
topRow.clear();
|
|
this.rows.splice(this.currRow, 0, topRow);
|
|
this.logger.log(VerboseLevel.INFO, 'Rolling up');
|
|
// this.logger.log(VerboseLevel.TEXT, this.get_display_text())
|
|
}
|
|
|
|
/**
|
|
* Get all non-empty rows with as unicode text.
|
|
*/
|
|
getDisplayText(asOneRow?: boolean) {
|
|
asOneRow = asOneRow || false;
|
|
const displayText: string[] = [];
|
|
let text = '';
|
|
let rowNr = -1;
|
|
for (let i = 0; i < NR_ROWS; i++) {
|
|
const 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() {
|
|
return this.rows;
|
|
}
|
|
}
|
|
|
|
// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
|
|
|
|
type CaptionModes =
|
|
| 'MODE_ROLL-UP'
|
|
| 'MODE_POP-ON'
|
|
| 'MODE_PAINT-ON'
|
|
| 'MODE_TEXT'
|
|
| null;
|
|
|
|
class Cea608Channel {
|
|
chNr: number;
|
|
outputFilter: OutputFilter;
|
|
mode: CaptionModes;
|
|
verbose: number;
|
|
displayedMemory: CaptionScreen;
|
|
nonDisplayedMemory: CaptionScreen;
|
|
lastOutputScreen: CaptionScreen;
|
|
currRollUpRow: Row;
|
|
writeScreen: CaptionScreen;
|
|
cueStartTime: number | null;
|
|
logger: CaptionsLogger;
|
|
|
|
constructor(
|
|
channelNumber: number,
|
|
outputFilter: OutputFilter,
|
|
logger: CaptionsLogger,
|
|
) {
|
|
this.chNr = channelNumber;
|
|
this.outputFilter = outputFilter;
|
|
this.mode = null;
|
|
this.verbose = 0;
|
|
this.displayedMemory = new CaptionScreen(logger);
|
|
this.nonDisplayedMemory = new CaptionScreen(logger);
|
|
this.lastOutputScreen = new CaptionScreen(logger);
|
|
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.
|
|
this.logger = logger;
|
|
}
|
|
|
|
reset() {
|
|
this.mode = null;
|
|
this.displayedMemory.reset();
|
|
this.nonDisplayedMemory.reset();
|
|
this.lastOutputScreen.reset();
|
|
this.outputFilter.reset();
|
|
this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
|
|
this.writeScreen = this.displayedMemory;
|
|
this.mode = null;
|
|
this.cueStartTime = null;
|
|
}
|
|
|
|
getHandler(): OutputFilter {
|
|
return this.outputFilter;
|
|
}
|
|
|
|
setHandler(newHandler: OutputFilter) {
|
|
this.outputFilter = newHandler;
|
|
}
|
|
|
|
setPAC(pacData: PACData) {
|
|
this.writeScreen.setPAC(pacData);
|
|
}
|
|
|
|
setBkgData(bkgData: Partial<PenStyles>) {
|
|
this.writeScreen.setBkgData(bkgData);
|
|
}
|
|
|
|
setMode(newMode: CaptionModes) {
|
|
if (newMode === this.mode) {
|
|
return;
|
|
}
|
|
|
|
this.mode = newMode;
|
|
this.logger.log(VerboseLevel.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(chars: number[]) {
|
|
for (let i = 0; i < chars.length; i++) {
|
|
this.writeScreen.insertChar(chars[i]);
|
|
}
|
|
|
|
const screen =
|
|
this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
|
|
this.logger.log(
|
|
VerboseLevel.INFO,
|
|
() => screen + ': ' + this.writeScreen.getDisplayText(true),
|
|
);
|
|
if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
|
|
this.logger.log(
|
|
VerboseLevel.TEXT,
|
|
() => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true),
|
|
);
|
|
this.outputDataUpdate();
|
|
}
|
|
}
|
|
|
|
ccRCL() {
|
|
// Resume Caption Loading (switch mode to Pop On)
|
|
this.logger.log(VerboseLevel.INFO, 'RCL - Resume Caption Loading');
|
|
this.setMode('MODE_POP-ON');
|
|
}
|
|
|
|
ccBS() {
|
|
// BackSpace
|
|
this.logger.log(VerboseLevel.INFO, 'BS - BackSpace');
|
|
if (this.mode === 'MODE_TEXT') {
|
|
return;
|
|
}
|
|
|
|
this.writeScreen.backSpace();
|
|
if (this.writeScreen === this.displayedMemory) {
|
|
this.outputDataUpdate();
|
|
}
|
|
}
|
|
|
|
ccAOF() {
|
|
// Reserved (formerly Alarm Off)
|
|
}
|
|
|
|
ccAON() {
|
|
// Reserved (formerly Alarm On)
|
|
}
|
|
|
|
ccDER() {
|
|
// Delete to End of Row
|
|
this.logger.log(VerboseLevel.INFO, 'DER- Delete to End of Row');
|
|
this.writeScreen.clearToEndOfRow();
|
|
this.outputDataUpdate();
|
|
}
|
|
|
|
ccRU(nrRows: number | null) {
|
|
// Roll-Up Captions-2,3,or 4 Rows
|
|
this.logger.log(VerboseLevel.INFO, 'RU(' + nrRows + ') - Roll Up');
|
|
this.writeScreen = this.displayedMemory;
|
|
this.setMode('MODE_ROLL-UP');
|
|
this.writeScreen.setRollUpRows(nrRows);
|
|
}
|
|
|
|
ccFON() {
|
|
// Flash On
|
|
this.logger.log(VerboseLevel.INFO, 'FON - Flash On');
|
|
this.writeScreen.setPen({ flash: true });
|
|
}
|
|
|
|
ccRDC() {
|
|
// Resume Direct Captioning (switch mode to PaintOn)
|
|
this.logger.log(VerboseLevel.INFO, 'RDC - Resume Direct Captioning');
|
|
this.setMode('MODE_PAINT-ON');
|
|
}
|
|
|
|
ccTR() {
|
|
// Text Restart in text mode (not supported, however)
|
|
this.logger.log(VerboseLevel.INFO, 'TR');
|
|
this.setMode('MODE_TEXT');
|
|
}
|
|
|
|
ccRTD() {
|
|
// Resume Text Display in Text mode (not supported, however)
|
|
this.logger.log(VerboseLevel.INFO, 'RTD');
|
|
this.setMode('MODE_TEXT');
|
|
}
|
|
|
|
ccEDM() {
|
|
// Erase Displayed Memory
|
|
this.logger.log(VerboseLevel.INFO, 'EDM - Erase Displayed Memory');
|
|
this.displayedMemory.reset();
|
|
this.outputDataUpdate(true);
|
|
}
|
|
|
|
ccCR() {
|
|
// Carriage Return
|
|
this.logger.log(VerboseLevel.INFO, 'CR - Carriage Return');
|
|
this.writeScreen.rollUp();
|
|
this.outputDataUpdate(true);
|
|
}
|
|
|
|
ccENM() {
|
|
// Erase Non-Displayed Memory
|
|
this.logger.log(VerboseLevel.INFO, 'ENM - Erase Non-displayed Memory');
|
|
this.nonDisplayedMemory.reset();
|
|
}
|
|
|
|
ccEOC() {
|
|
// End of Caption (Flip Memories)
|
|
this.logger.log(VerboseLevel.INFO, 'EOC - End Of Caption');
|
|
if (this.mode === 'MODE_POP-ON') {
|
|
const tmp = this.displayedMemory;
|
|
this.displayedMemory = this.nonDisplayedMemory;
|
|
this.nonDisplayedMemory = tmp;
|
|
this.writeScreen = this.nonDisplayedMemory;
|
|
this.logger.log(
|
|
VerboseLevel.TEXT,
|
|
() => 'DISP: ' + this.displayedMemory.getDisplayText(),
|
|
);
|
|
}
|
|
this.outputDataUpdate(true);
|
|
}
|
|
|
|
ccTO(nrCols: number) {
|
|
// Tab Offset 1,2, or 3 columns
|
|
this.logger.log(VerboseLevel.INFO, 'TO(' + nrCols + ') - Tab Offset');
|
|
this.writeScreen.moveCursor(nrCols);
|
|
}
|
|
|
|
ccMIDROW(secondByte: number) {
|
|
// Parse MIDROW command
|
|
const styles: Partial<PenStyles> = { flash: false };
|
|
styles.underline = secondByte % 2 === 1;
|
|
styles.italics = secondByte >= 0x2e;
|
|
if (!styles.italics) {
|
|
const colorIndex = Math.floor(secondByte / 2) - 0x10;
|
|
const colors = [
|
|
'white',
|
|
'green',
|
|
'blue',
|
|
'cyan',
|
|
'red',
|
|
'yellow',
|
|
'magenta',
|
|
];
|
|
styles.foreground = colors[colorIndex];
|
|
} else {
|
|
styles.foreground = 'white';
|
|
}
|
|
this.logger.log(VerboseLevel.INFO, 'MIDROW: ' + JSON.stringify(styles));
|
|
this.writeScreen.setPen(styles);
|
|
}
|
|
|
|
outputDataUpdate(dispatch: boolean = false) {
|
|
const time = this.logger.time;
|
|
if (time === null) {
|
|
return;
|
|
}
|
|
|
|
if (this.outputFilter) {
|
|
if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) {
|
|
// Start of a new cue
|
|
this.cueStartTime = time;
|
|
} else {
|
|
if (!this.displayedMemory.equals(this.lastOutputScreen)) {
|
|
this.outputFilter.newCue(
|
|
this.cueStartTime!,
|
|
time,
|
|
this.lastOutputScreen,
|
|
);
|
|
if (dispatch && this.outputFilter.dispatchCue) {
|
|
this.outputFilter.dispatchCue();
|
|
}
|
|
|
|
this.cueStartTime = this.displayedMemory.isEmpty() ? null : time;
|
|
}
|
|
}
|
|
this.lastOutputScreen.copy(this.displayedMemory);
|
|
}
|
|
}
|
|
|
|
cueSplitAtTime(t: number) {
|
|
if (this.outputFilter) {
|
|
if (!this.displayedMemory.isEmpty()) {
|
|
if (this.outputFilter.newCue) {
|
|
this.outputFilter.newCue(this.cueStartTime!, t, this.displayedMemory);
|
|
}
|
|
|
|
this.cueStartTime = t;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
interface PACData {
|
|
row: number;
|
|
indent: number | null;
|
|
color: string | null;
|
|
underline: boolean;
|
|
italics: boolean;
|
|
}
|
|
|
|
type SupportedField = 1 | 3;
|
|
|
|
type Channels = 0 | 1 | 2; // Will be 1 or 2 when parsing captions
|
|
|
|
type CmdHistory = {
|
|
a: number | null;
|
|
b: number | null;
|
|
};
|
|
|
|
class Cea608Parser {
|
|
channels: Array<Cea608Channel | null>;
|
|
currentChannel: Channels = 0;
|
|
cmdHistory: CmdHistory = createCmdHistory();
|
|
logger: CaptionsLogger;
|
|
|
|
constructor(field: SupportedField, out1: OutputFilter, out2: OutputFilter) {
|
|
const logger = (this.logger = new CaptionsLogger());
|
|
this.channels = [
|
|
null,
|
|
new Cea608Channel(field, out1, logger),
|
|
new Cea608Channel(field + 1, out2, logger),
|
|
];
|
|
}
|
|
|
|
getHandler(channel: number) {
|
|
return (this.channels[channel] as Cea608Channel).getHandler();
|
|
}
|
|
|
|
setHandler(channel: number, newHandler: OutputFilter) {
|
|
(this.channels[channel] as Cea608Channel).setHandler(newHandler);
|
|
}
|
|
|
|
/**
|
|
* Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
|
|
*/
|
|
addData(time: number | null, byteList: number[]) {
|
|
this.logger.time = time;
|
|
for (let i = 0; i < byteList.length; i += 2) {
|
|
const a = byteList[i] & 0x7f;
|
|
const b = byteList[i + 1] & 0x7f;
|
|
let cmdFound: boolean = false;
|
|
let charsFound: number[] | null = null;
|
|
|
|
if (a === 0 && b === 0) {
|
|
continue;
|
|
} else {
|
|
this.logger.log(
|
|
VerboseLevel.DATA,
|
|
() =>
|
|
'[' +
|
|
numArrayToHexArray([byteList[i], byteList[i + 1]]) +
|
|
'] -> (' +
|
|
numArrayToHexArray([a, b]) +
|
|
')',
|
|
);
|
|
}
|
|
|
|
const cmdHistory = this.cmdHistory;
|
|
const isControlCode = a >= 0x10 && a <= 0x1f;
|
|
if (isControlCode) {
|
|
// Skip redundant control codes
|
|
if (hasCmdRepeated(a, b, cmdHistory)) {
|
|
setLastCmd(null, null, cmdHistory);
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
() =>
|
|
'Repeated command (' +
|
|
numArrayToHexArray([a, b]) +
|
|
') is dropped',
|
|
);
|
|
continue;
|
|
}
|
|
setLastCmd(a, b, this.cmdHistory);
|
|
|
|
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);
|
|
}
|
|
} else {
|
|
setLastCmd(null, null, cmdHistory);
|
|
}
|
|
if (!cmdFound) {
|
|
charsFound = this.parseChars(a, b);
|
|
if (charsFound) {
|
|
const currChNr = this.currentChannel;
|
|
if (currChNr && currChNr > 0) {
|
|
const channel = this.channels[currChNr] as Cea608Channel;
|
|
channel.insertChars(charsFound);
|
|
} else {
|
|
this.logger.log(
|
|
VerboseLevel.WARNING,
|
|
'No channel found yet. TEXT-MODE?',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (!cmdFound && !charsFound) {
|
|
this.logger.log(
|
|
VerboseLevel.WARNING,
|
|
() =>
|
|
"Couldn't parse cleaned data " +
|
|
numArrayToHexArray([a, b]) +
|
|
' orig: ' +
|
|
numArrayToHexArray([byteList[i], byteList[i + 1]]),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse Command.
|
|
* @returns True if a command was found
|
|
*/
|
|
parseCmd(a: number, b: number): boolean {
|
|
const cond1 =
|
|
(a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) &&
|
|
b >= 0x20 &&
|
|
b <= 0x2f;
|
|
const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23;
|
|
if (!(cond1 || cond2)) {
|
|
return false;
|
|
}
|
|
|
|
const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2;
|
|
const channel = this.channels[chNr] as Cea608Channel;
|
|
|
|
if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) {
|
|
if (b === 0x20) {
|
|
channel.ccRCL();
|
|
} else if (b === 0x21) {
|
|
channel.ccBS();
|
|
} else if (b === 0x22) {
|
|
channel.ccAOF();
|
|
} else if (b === 0x23) {
|
|
channel.ccAON();
|
|
} else if (b === 0x24) {
|
|
channel.ccDER();
|
|
} else if (b === 0x25) {
|
|
channel.ccRU(2);
|
|
} else if (b === 0x26) {
|
|
channel.ccRU(3);
|
|
} else if (b === 0x27) {
|
|
channel.ccRU(4);
|
|
} else if (b === 0x28) {
|
|
channel.ccFON();
|
|
} else if (b === 0x29) {
|
|
channel.ccRDC();
|
|
} else if (b === 0x2a) {
|
|
channel.ccTR();
|
|
} else if (b === 0x2b) {
|
|
channel.ccRTD();
|
|
} else if (b === 0x2c) {
|
|
channel.ccEDM();
|
|
} else if (b === 0x2d) {
|
|
channel.ccCR();
|
|
} else if (b === 0x2e) {
|
|
channel.ccENM();
|
|
} else if (b === 0x2f) {
|
|
channel.ccEOC();
|
|
}
|
|
} else {
|
|
// a == 0x17 || a == 0x1F
|
|
channel.ccTO(b - 0x20);
|
|
}
|
|
this.currentChannel = chNr;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse midrow styling command
|
|
*/
|
|
parseMidrow(a: number, b: number): boolean {
|
|
let chNr: number = 0;
|
|
|
|
if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) {
|
|
if (a === 0x11) {
|
|
chNr = 1;
|
|
} else {
|
|
chNr = 2;
|
|
}
|
|
|
|
if (chNr !== this.currentChannel) {
|
|
this.logger.log(
|
|
VerboseLevel.ERROR,
|
|
'Mismatch channel in midrow parsing',
|
|
);
|
|
return false;
|
|
}
|
|
const channel = this.channels[chNr];
|
|
if (!channel) {
|
|
return false;
|
|
}
|
|
channel.ccMIDROW(b);
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
() => 'MIDROW (' + numArrayToHexArray([a, b]) + ')',
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse Preable Access Codes (Table 53).
|
|
* @returns {Boolean} Tells if PAC found
|
|
*/
|
|
parsePAC(a: number, b: number): boolean {
|
|
let row: number;
|
|
|
|
const case1 =
|
|
((a >= 0x11 && a <= 0x17) || (a >= 0x19 && a <= 0x1f)) &&
|
|
b >= 0x40 &&
|
|
b <= 0x7f;
|
|
const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f;
|
|
if (!(case1 || case2)) {
|
|
return false;
|
|
}
|
|
|
|
const chNr: Channels = a <= 0x17 ? 1 : 2;
|
|
|
|
if (b >= 0x40 && b <= 0x5f) {
|
|
row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a];
|
|
} else {
|
|
// 0x60 <= b <= 0x7F
|
|
row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a];
|
|
}
|
|
const channel = this.channels[chNr];
|
|
if (!channel) {
|
|
return false;
|
|
}
|
|
channel.setPAC(this.interpretPAC(row, b));
|
|
this.currentChannel = chNr;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Interpret the second byte of the pac, and return the information.
|
|
* @returns pacData with style parameters
|
|
*/
|
|
interpretPAC(row: number, byte: number): PACData {
|
|
let pacIndex;
|
|
const pacData: 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(a: number, b: number): number[] | null {
|
|
let channelNr: Channels;
|
|
let charCodes: number[] | null = null;
|
|
let charCode1: number | null = null;
|
|
|
|
if (a >= 0x19) {
|
|
channelNr = 2;
|
|
charCode1 = a - 8;
|
|
} else {
|
|
channelNr = 1;
|
|
charCode1 = a;
|
|
}
|
|
if (charCode1 >= 0x11 && charCode1 <= 0x13) {
|
|
// Special character
|
|
let oneCode;
|
|
if (charCode1 === 0x11) {
|
|
oneCode = b + 0x50;
|
|
} else if (charCode1 === 0x12) {
|
|
oneCode = b + 0x70;
|
|
} else {
|
|
oneCode = b + 0x90;
|
|
}
|
|
|
|
this.logger.log(
|
|
VerboseLevel.INFO,
|
|
() =>
|
|
"Special char '" +
|
|
getCharForByte(oneCode) +
|
|
"' in channel " +
|
|
channelNr,
|
|
);
|
|
charCodes = [oneCode];
|
|
} else if (a >= 0x20 && a <= 0x7f) {
|
|
charCodes = b === 0 ? [a] : [a, b];
|
|
}
|
|
if (charCodes) {
|
|
this.logger.log(
|
|
VerboseLevel.DEBUG,
|
|
() =>
|
|
'Char codes = ' +
|
|
numArrayToHexArray(charCodes as number[]).join(','),
|
|
);
|
|
}
|
|
return charCodes;
|
|
}
|
|
|
|
/**
|
|
* Parse extended background attributes as well as new foreground color black.
|
|
* @returns True if background attributes are found
|
|
*/
|
|
parseBackgroundAttributes(a: number, b: number): boolean {
|
|
const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f;
|
|
const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f;
|
|
if (!(case1 || case2)) {
|
|
return false;
|
|
}
|
|
let index: number;
|
|
const bkgData: Partial<PenStyles> = {};
|
|
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;
|
|
}
|
|
}
|
|
const chNr: Channels = a <= 0x17 ? 1 : 2;
|
|
const channel: Cea608Channel = this.channels[chNr] as Cea608Channel;
|
|
channel.setBkgData(bkgData);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reset state of parser and its channels.
|
|
*/
|
|
reset() {
|
|
for (let i = 0; i < Object.keys(this.channels).length; i++) {
|
|
const channel = this.channels[i];
|
|
if (channel) {
|
|
channel.reset();
|
|
}
|
|
}
|
|
setLastCmd(null, null, this.cmdHistory);
|
|
}
|
|
|
|
/**
|
|
* Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
|
|
*/
|
|
cueSplitAtTime(t: number) {
|
|
for (let i = 0; i < this.channels.length; i++) {
|
|
const channel = this.channels[i];
|
|
if (channel) {
|
|
channel.cueSplitAtTime(t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setLastCmd(
|
|
a: number | null,
|
|
b: number | null,
|
|
cmdHistory: CmdHistory,
|
|
) {
|
|
cmdHistory.a = a;
|
|
cmdHistory.b = b;
|
|
}
|
|
|
|
function hasCmdRepeated(a: number, b: number, cmdHistory: CmdHistory) {
|
|
return cmdHistory.a === a && cmdHistory.b === b;
|
|
}
|
|
|
|
function createCmdHistory(): CmdHistory {
|
|
return {
|
|
a: null,
|
|
b: null,
|
|
};
|
|
}
|
|
|
|
export default Cea608Parser;
|