���� JFIF �� � ( %"1"%)+...383,7(-.-
![]() Server : Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.20 System : Linux st2.domain.com 3.10.0-1127.10.1.el7.x86_64 #1 SMP Wed Jun 3 14:28:03 UTC 2020 x86_64 User : apache ( 48) PHP Version : 7.4.20 Disable Function : NONE Directory : /var/www/html/netphim/layout/default/node_modules/hls.js/src/utils/ |
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', ]; 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; 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 True if a command was found */ parseCmd(a: number, b: number): boolean { 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 */ 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 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 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 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); 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;