src/utils/cea-608-parser.ts
- 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 = function (byte: number) {
- let charCode = byte;
- if (specialCea608CharsCodes.hasOwnProperty(byte)) {
- charCode = specialCea608CharsCodes[byte];
- }
-
- return String.fromCharCode(charCode);
- };
-
- 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',
- ];
-
- 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): void {
- if (this.verboseLevel >= severity) {
- logger.log(`${this.time} [${severity}] ${msg}`);
- }
- }
- }
-
- 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;
- public underline: boolean;
- public italics: boolean;
- public background: string;
- public flash: boolean;
-
- constructor(
- foreground?: string,
- underline?: boolean,
- italics?: boolean,
- background?: string,
- flash?: boolean
- ) {
- this.foreground = foreground || 'white';
- this.underline = underline || false;
- this.italics = italics || false;
- this.background = background || 'black';
- this.flash = flash || 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;
-
- constructor(
- uchar?: string,
- foreground?: string,
- underline?: boolean,
- italics?: boolean,
- background?: string,
- flash?: boolean
- ) {
- this.uchar = uchar || ' '; // unicode character
- this.penState = new PenState(
- foreground,
- underline,
- italics,
- background,
- flash
- );
- }
-
- 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;
- public currPenState: PenState;
- public cueStartTime?: number;
- logger: CaptionsLogger;
-
- constructor(logger: CaptionsLogger) {
- this.chars = [];
- for (let i = 0; i < NR_COLS; i++) {
- this.chars.push(new StyledUnicodeChar());
- }
-
- this.logger = logger;
- this.pos = 0;
- this.currPenState = new PenState();
- }
-
- equals(other: Row) {
- let equal = true;
- for (let i = 0; i < NR_COLS; i++) {
- if (!this.chars[i].equals(other.chars[i])) {
- equal = false;
- break;
- }
- }
- return equal;
- }
-
- 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;
- nrRollUpRows: number | null;
- lastOutputScreen: CaptionScreen | null;
- logger: CaptionsLogger;
-
- constructor(logger: CaptionsLogger) {
- this.rows = [];
- for (let i = 0; i < NR_ROWS; i++) {
- this.rows.push(new Row(logger));
- } // Note that we use zero-based numbering (0-14)
-
- this.logger = logger;
- this.currRow = NR_ROWS - 1;
- this.nrRollUpRows = null;
- this.lastOutputScreen = null;
- this.reset();
- }
-
- 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 && 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;
- logger: CaptionsLogger;
-
- constructor(field: SupportedField, out1: OutputFilter, out2: OutputFilter) {
- const logger = new CaptionsLogger();
- this.channels = [
- null,
- new Cea608Channel(field, out1, logger),
- new Cea608Channel(field + 1, out2, logger),
- ];
- this.cmdHistory = createCmdHistory();
- this.logger = 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[]) {
- let cmdFound: boolean;
- let a: number;
- let b: number;
- let charsFound: number[] | boolean | null = false;
-
- this.logger.time = time;
-
- for (let i = 0; i < byteList.length; i += 2) {
- a = byteList[i] & 0x7f;
- b = byteList[i + 1] & 0x7f;
- if (a === 0 && b === 0) {
- continue;
- } else {
- this.logger.log(
- VerboseLevel.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) {
- 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 {Boolean} Tells if a command was found
- */
- parseCmd(a: number, b: number) {
- const { cmdHistory } = this;
- 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;
- }
-
- if (hasCmdRepeated(a, b, cmdHistory)) {
- setLastCmd(null, null, cmdHistory);
- this.logger.log(
- VerboseLevel.DEBUG,
- 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped'
- );
- return true;
- }
-
- 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);
- }
- setLastCmd(a, b, cmdHistory);
- this.currentChannel = chNr;
- return true;
- }
-
- /**
- * Parse midrow styling command
- * @returns {Boolean}
- */
- parseMidrow(a: number, b: number) {
- 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 cmdHistory = this.cmdHistory;
-
- 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;
- }
-
- if (hasCmdRepeated(a, b, cmdHistory)) {
- setLastCmd(null, null, cmdHistory);
- return true; // Repeated commands are dropped (once)
- }
-
- 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));
- setLastCmd(a, b, cmdHistory);
- this.currentChannel = chNr;
- return true;
- }
-
- /**
- * Interpret the second byte of the pac, and return the information.
- * @returns {Object} 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) {
- const hexCodes = numArrayToHexArray(charCodes);
- this.logger.log(
- VerboseLevel.DEBUG,
- 'Char codes = ' + hexCodes.join(',')
- );
- setLastCmd(a, b, this.cmdHistory);
- }
- return charCodes;
- }
-
- /**
- * Parse extended background attributes as well as new foreground color black.
- * @returns {Boolean} Tells 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);
- setLastCmd(a, b, this.cmdHistory);
- 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();
- }
- }
- this.cmdHistory = createCmdHistory();
- }
-
- /**
- * 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;