Home Reference Source

src/utils/cea-608-parser.ts

  1. import OutputFilter from './output-filter';
  2. import { logger } from '../utils/logger';
  3.  
  4. /**
  5. *
  6. * This code was ported from the dash.js project at:
  7. * https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js
  8. * https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2
  9. *
  10. * The original copyright appears below:
  11. *
  12. * The copyright in this software is being made available under the BSD License,
  13. * included below. This software may be subject to other third party and contributor
  14. * rights, including patent rights, and no such rights are granted under this license.
  15. *
  16. * Copyright (c) 2015-2016, DASH Industry Forum.
  17. * All rights reserved.
  18. *
  19. * Redistribution and use in source and binary forms, with or without modification,
  20. * are permitted provided that the following conditions are met:
  21. * 1. Redistributions of source code must retain the above copyright notice, this
  22. * list of conditions and the following disclaimer.
  23. * * Redistributions in binary form must reproduce the above copyright notice,
  24. * this list of conditions and the following disclaimer in the documentation and/or
  25. * other materials provided with the distribution.
  26. * 2. Neither the name of Dash Industry Forum nor the names of its
  27. * contributors may be used to endorse or promote products derived from this software
  28. * without specific prior written permission.
  29. *
  30. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
  31. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  32. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  33. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
  34. * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  35. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  36. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  37. * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  38. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  39. * POSSIBILITY OF SUCH DAMAGE.
  40. */
  41. /**
  42. * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes
  43. */
  44.  
  45. const specialCea608CharsCodes = {
  46. 0x2a: 0xe1, // lowercase a, acute accent
  47. 0x5c: 0xe9, // lowercase e, acute accent
  48. 0x5e: 0xed, // lowercase i, acute accent
  49. 0x5f: 0xf3, // lowercase o, acute accent
  50. 0x60: 0xfa, // lowercase u, acute accent
  51. 0x7b: 0xe7, // lowercase c with cedilla
  52. 0x7c: 0xf7, // division symbol
  53. 0x7d: 0xd1, // uppercase N tilde
  54. 0x7e: 0xf1, // lowercase n tilde
  55. 0x7f: 0x2588, // Full block
  56. // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
  57. // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F
  58. // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES
  59. 0x80: 0xae, // Registered symbol (R)
  60. 0x81: 0xb0, // degree sign
  61. 0x82: 0xbd, // 1/2 symbol
  62. 0x83: 0xbf, // Inverted (open) question mark
  63. 0x84: 0x2122, // Trademark symbol (TM)
  64. 0x85: 0xa2, // Cents symbol
  65. 0x86: 0xa3, // Pounds sterling
  66. 0x87: 0x266a, // Music 8'th note
  67. 0x88: 0xe0, // lowercase a, grave accent
  68. 0x89: 0x20, // transparent space (regular)
  69. 0x8a: 0xe8, // lowercase e, grave accent
  70. 0x8b: 0xe2, // lowercase a, circumflex accent
  71. 0x8c: 0xea, // lowercase e, circumflex accent
  72. 0x8d: 0xee, // lowercase i, circumflex accent
  73. 0x8e: 0xf4, // lowercase o, circumflex accent
  74. 0x8f: 0xfb, // lowercase u, circumflex accent
  75. // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
  76. // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F
  77. 0x90: 0xc1, // capital letter A with acute
  78. 0x91: 0xc9, // capital letter E with acute
  79. 0x92: 0xd3, // capital letter O with acute
  80. 0x93: 0xda, // capital letter U with acute
  81. 0x94: 0xdc, // capital letter U with diaresis
  82. 0x95: 0xfc, // lowercase letter U with diaeresis
  83. 0x96: 0x2018, // opening single quote
  84. 0x97: 0xa1, // inverted exclamation mark
  85. 0x98: 0x2a, // asterisk
  86. 0x99: 0x2019, // closing single quote
  87. 0x9a: 0x2501, // box drawings heavy horizontal
  88. 0x9b: 0xa9, // copyright sign
  89. 0x9c: 0x2120, // Service mark
  90. 0x9d: 0x2022, // (round) bullet
  91. 0x9e: 0x201c, // Left double quotation mark
  92. 0x9f: 0x201d, // Right double quotation mark
  93. 0xa0: 0xc0, // uppercase A, grave accent
  94. 0xa1: 0xc2, // uppercase A, circumflex
  95. 0xa2: 0xc7, // uppercase C with cedilla
  96. 0xa3: 0xc8, // uppercase E, grave accent
  97. 0xa4: 0xca, // uppercase E, circumflex
  98. 0xa5: 0xcb, // capital letter E with diaresis
  99. 0xa6: 0xeb, // lowercase letter e with diaresis
  100. 0xa7: 0xce, // uppercase I, circumflex
  101. 0xa8: 0xcf, // uppercase I, with diaresis
  102. 0xa9: 0xef, // lowercase i, with diaresis
  103. 0xaa: 0xd4, // uppercase O, circumflex
  104. 0xab: 0xd9, // uppercase U, grave accent
  105. 0xac: 0xf9, // lowercase u, grave accent
  106. 0xad: 0xdb, // uppercase U, circumflex
  107. 0xae: 0xab, // left-pointing double angle quotation mark
  108. 0xaf: 0xbb, // right-pointing double angle quotation mark
  109. // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
  110. // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F
  111. 0xb0: 0xc3, // Uppercase A, tilde
  112. 0xb1: 0xe3, // Lowercase a, tilde
  113. 0xb2: 0xcd, // Uppercase I, acute accent
  114. 0xb3: 0xcc, // Uppercase I, grave accent
  115. 0xb4: 0xec, // Lowercase i, grave accent
  116. 0xb5: 0xd2, // Uppercase O, grave accent
  117. 0xb6: 0xf2, // Lowercase o, grave accent
  118. 0xb7: 0xd5, // Uppercase O, tilde
  119. 0xb8: 0xf5, // Lowercase o, tilde
  120. 0xb9: 0x7b, // Open curly brace
  121. 0xba: 0x7d, // Closing curly brace
  122. 0xbb: 0x5c, // Backslash
  123. 0xbc: 0x5e, // Caret
  124. 0xbd: 0x5f, // Underscore
  125. 0xbe: 0x7c, // Pipe (vertical line)
  126. 0xbf: 0x223c, // Tilde operator
  127. 0xc0: 0xc4, // Uppercase A, umlaut
  128. 0xc1: 0xe4, // Lowercase A, umlaut
  129. 0xc2: 0xd6, // Uppercase O, umlaut
  130. 0xc3: 0xf6, // Lowercase o, umlaut
  131. 0xc4: 0xdf, // Esszett (sharp S)
  132. 0xc5: 0xa5, // Yen symbol
  133. 0xc6: 0xa4, // Generic currency sign
  134. 0xc7: 0x2503, // Box drawings heavy vertical
  135. 0xc8: 0xc5, // Uppercase A, ring
  136. 0xc9: 0xe5, // Lowercase A, ring
  137. 0xca: 0xd8, // Uppercase O, stroke
  138. 0xcb: 0xf8, // Lowercase o, strok
  139. 0xcc: 0x250f, // Box drawings heavy down and right
  140. 0xcd: 0x2513, // Box drawings heavy down and left
  141. 0xce: 0x2517, // Box drawings heavy up and right
  142. 0xcf: 0x251b // Box drawings heavy up and left
  143. };
  144.  
  145. /**
  146. * Utils
  147. */
  148. const getCharForByte = function (byte: number) {
  149. let charCode = byte;
  150. if (specialCea608CharsCodes.hasOwnProperty(byte)) {
  151. charCode = specialCea608CharsCodes[byte];
  152. }
  153.  
  154. return String.fromCharCode(charCode);
  155. };
  156.  
  157. const NR_ROWS = 15;
  158. const NR_COLS = 100;
  159. // Tables to look up row from PAC data
  160. const rowsLowCh1 = { 0x11: 1, 0x12: 3, 0x15: 5, 0x16: 7, 0x17: 9, 0x10: 11, 0x13: 12, 0x14: 14 };
  161. const rowsHighCh1 = { 0x11: 2, 0x12: 4, 0x15: 6, 0x16: 8, 0x17: 10, 0x13: 13, 0x14: 15 };
  162. const rowsLowCh2 = { 0x19: 1, 0x1A: 3, 0x1D: 5, 0x1E: 7, 0x1F: 9, 0x18: 11, 0x1B: 12, 0x1C: 14 };
  163. const rowsHighCh2 = { 0x19: 2, 0x1A: 4, 0x1D: 6, 0x1E: 8, 0x1F: 10, 0x1B: 13, 0x1C: 15 };
  164.  
  165. const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent'];
  166.  
  167. enum VerboseLevel {
  168. ERROR = 0,
  169. TEXT = 1,
  170. WARNING = 2,
  171. INFO = 2,
  172. DEBUG = 3,
  173. DATA = 3,
  174. }
  175.  
  176. class CaptionsLogger {
  177. public time: number | null = null;
  178. public verboseLevel: VerboseLevel = VerboseLevel.ERROR;
  179.  
  180. log (severity: VerboseLevel, msg: string): void {
  181. if (this.verboseLevel >= severity) {
  182. logger.log(`${this.time} [${severity}] ${msg}`);
  183. }
  184. }
  185. }
  186.  
  187. const numArrayToHexArray = function (numArray: number[]): string[] {
  188. const hexArray: string[] = [];
  189. for (let j = 0; j < numArray.length; j++) {
  190. hexArray.push(numArray[j].toString(16));
  191. }
  192.  
  193. return hexArray;
  194. };
  195.  
  196. type PenStyles = {
  197. foreground: string | null,
  198. underline: boolean,
  199. italics: boolean,
  200. background: string,
  201. flash: boolean,
  202. };
  203.  
  204. class PenState {
  205. public foreground: string;
  206. public underline: boolean;
  207. public italics: boolean;
  208. public background: string;
  209. public flash: boolean;
  210.  
  211. constructor (foreground?: string, underline?: boolean, italics?: boolean, background?: string, flash?: boolean) {
  212. this.foreground = foreground || 'white';
  213. this.underline = underline || false;
  214. this.italics = italics || false;
  215. this.background = background || 'black';
  216. this.flash = flash || false;
  217. }
  218.  
  219. reset () {
  220. this.foreground = 'white';
  221. this.underline = false;
  222. this.italics = false;
  223. this.background = 'black';
  224. this.flash = false;
  225. }
  226.  
  227. setStyles (styles: Partial<PenStyles>) {
  228. const attribs = ['foreground', 'underline', 'italics', 'background', 'flash'];
  229. for (let i = 0; i < attribs.length; i++) {
  230. const style = attribs[i];
  231. if (styles.hasOwnProperty(style)) {
  232. this[style] = styles[style];
  233. }
  234. }
  235. }
  236.  
  237. isDefault () {
  238. return (this.foreground === 'white' && !this.underline && !this.italics &&
  239. this.background === 'black' && !this.flash);
  240. }
  241.  
  242. equals (other: PenState) {
  243. return ((this.foreground === other.foreground) &&
  244. (this.underline === other.underline) &&
  245. (this.italics === other.italics) &&
  246. (this.background === other.background) &&
  247. (this.flash === other.flash));
  248. }
  249.  
  250. copy (newPenState: PenState) {
  251. this.foreground = newPenState.foreground;
  252. this.underline = newPenState.underline;
  253. this.italics = newPenState.italics;
  254. this.background = newPenState.background;
  255. this.flash = newPenState.flash;
  256. }
  257.  
  258. toString (): string {
  259. return ('color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics +
  260. ', background=' + this.background + ', flash=' + this.flash);
  261. }
  262. }
  263.  
  264. /**
  265. * Unicode character with styling and background.
  266. * @constructor
  267. */
  268. class StyledUnicodeChar {
  269. uchar: string;
  270. penState: PenState;
  271.  
  272. constructor (uchar?: string, foreground?: string, underline?: boolean, italics?: boolean, background?: string, flash?: boolean) {
  273. this.uchar = uchar || ' '; // unicode character
  274. this.penState = new PenState(foreground, underline, italics, background, flash);
  275. }
  276.  
  277. reset () {
  278. this.uchar = ' ';
  279. this.penState.reset();
  280. }
  281.  
  282. setChar (uchar: string, newPenState: PenState) {
  283. this.uchar = uchar;
  284. this.penState.copy(newPenState);
  285. }
  286.  
  287. setPenState (newPenState: PenState) {
  288. this.penState.copy(newPenState);
  289. }
  290.  
  291. equals (other: StyledUnicodeChar) {
  292. return this.uchar === other.uchar && this.penState.equals(other.penState);
  293. }
  294.  
  295. copy (newChar: StyledUnicodeChar) {
  296. this.uchar = newChar.uchar;
  297. this.penState.copy(newChar.penState);
  298. }
  299.  
  300. isEmpty (): boolean {
  301. return this.uchar === ' ' && this.penState.isDefault();
  302. }
  303. }
  304.  
  305. /**
  306. * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
  307. * @constructor
  308. */
  309. export class Row {
  310. public chars: StyledUnicodeChar[];
  311. public pos: number;
  312. public currPenState: PenState;
  313. public cueStartTime?: number;
  314. logger: CaptionsLogger;
  315.  
  316. constructor (logger: CaptionsLogger) {
  317. this.chars = [];
  318. for (let i = 0; i < NR_COLS; i++) {
  319. this.chars.push(new StyledUnicodeChar());
  320. }
  321.  
  322. this.logger = logger;
  323. this.pos = 0;
  324. this.currPenState = new PenState();
  325. }
  326.  
  327. equals (other: Row) {
  328. let equal = true;
  329. for (let i = 0; i < NR_COLS; i++) {
  330. if (!this.chars[i].equals(other.chars[i])) {
  331. equal = false;
  332. break;
  333. }
  334. }
  335. return equal;
  336. }
  337.  
  338. copy (other: Row) {
  339. for (let i = 0; i < NR_COLS; i++) {
  340. this.chars[i].copy(other.chars[i]);
  341. }
  342. }
  343.  
  344. isEmpty (): boolean {
  345. let empty = true;
  346. for (let i = 0; i < NR_COLS; i++) {
  347. if (!this.chars[i].isEmpty()) {
  348. empty = false;
  349. break;
  350. }
  351. }
  352. return empty;
  353. }
  354.  
  355. /**
  356. * Set the cursor to a valid column.
  357. */
  358. setCursor (absPos: number) {
  359. if (this.pos !== absPos) {
  360. this.pos = absPos;
  361. }
  362.  
  363. if (this.pos < 0) {
  364. this.logger.log(VerboseLevel.DEBUG, 'Negative cursor position ' + this.pos);
  365. this.pos = 0;
  366. } else if (this.pos > NR_COLS) {
  367. this.logger.log(VerboseLevel.DEBUG, 'Too large cursor position ' + this.pos);
  368. this.pos = NR_COLS;
  369. }
  370. }
  371.  
  372. /**
  373. * Move the cursor relative to current position.
  374. */
  375. moveCursor (relPos: number) {
  376. const newPos = this.pos + relPos;
  377. if (relPos > 1) {
  378. for (let i = this.pos + 1; i < newPos + 1; i++) {
  379. this.chars[i].setPenState(this.currPenState);
  380. }
  381. }
  382. this.setCursor(newPos);
  383. }
  384.  
  385. /**
  386. * Backspace, move one step back and clear character.
  387. */
  388. backSpace () {
  389. this.moveCursor(-1);
  390. this.chars[this.pos].setChar(' ', this.currPenState);
  391. }
  392.  
  393. insertChar (byte: number) {
  394. if (byte >= 0x90) { // Extended char
  395. this.backSpace();
  396. }
  397. const char = getCharForByte(byte);
  398. if (this.pos >= NR_COLS) {
  399. this.logger.log(VerboseLevel.ERROR, 'Cannot insert ' + byte.toString(16) +
  400. ' (' + char + ') at position ' + this.pos + '. Skipping it!');
  401. return;
  402. }
  403. this.chars[this.pos].setChar(char, this.currPenState);
  404. this.moveCursor(1);
  405. }
  406.  
  407. clearFromPos (startPos: number) {
  408. let i: number;
  409. for (i = startPos; i < NR_COLS; i++) {
  410. this.chars[i].reset();
  411. }
  412. }
  413.  
  414. clear () {
  415. this.clearFromPos(0);
  416. this.pos = 0;
  417. this.currPenState.reset();
  418. }
  419.  
  420. clearToEndOfRow () {
  421. this.clearFromPos(this.pos);
  422. }
  423.  
  424. getTextString () {
  425. const chars: string[] = [];
  426. let empty = true;
  427. for (let i = 0; i < NR_COLS; i++) {
  428. const char = this.chars[i].uchar;
  429. if (char !== ' ') {
  430. empty = false;
  431. }
  432.  
  433. chars.push(char);
  434. }
  435. if (empty) {
  436. return '';
  437. } else {
  438. return chars.join('');
  439. }
  440. }
  441.  
  442. setPenStyles (styles: Partial<PenStyles>) {
  443. this.currPenState.setStyles(styles);
  444. const currChar = this.chars[this.pos];
  445. currChar.setPenState(this.currPenState);
  446. }
  447. }
  448.  
  449. /**
  450. * Keep a CEA-608 screen of 32x15 styled characters
  451. * @constructor
  452. */
  453. export class CaptionScreen {
  454. rows: Row[];
  455. currRow: number;
  456. nrRollUpRows: number | null;
  457. lastOutputScreen: CaptionScreen | null;
  458. logger: CaptionsLogger;
  459.  
  460. constructor (logger: CaptionsLogger) {
  461. this.rows = [];
  462. for (let i = 0; i < NR_ROWS; i++) {
  463. this.rows.push(new Row(logger));
  464. } // Note that we use zero-based numbering (0-14)
  465.  
  466. this.logger = logger;
  467. this.currRow = NR_ROWS - 1;
  468. this.nrRollUpRows = null;
  469. this.lastOutputScreen = null;
  470. this.reset();
  471. }
  472.  
  473. reset () {
  474. for (let i = 0; i < NR_ROWS; i++) {
  475. this.rows[i].clear();
  476. }
  477.  
  478. this.currRow = NR_ROWS - 1;
  479. }
  480.  
  481. equals (other: CaptionScreen): boolean {
  482. let equal = true;
  483. for (let i = 0; i < NR_ROWS; i++) {
  484. if (!this.rows[i].equals(other.rows[i])) {
  485. equal = false;
  486. break;
  487. }
  488. }
  489. return equal;
  490. }
  491.  
  492. copy (other: CaptionScreen) {
  493. for (let i = 0; i < NR_ROWS; i++) {
  494. this.rows[i].copy(other.rows[i]);
  495. }
  496. }
  497.  
  498. isEmpty (): boolean {
  499. let empty = true;
  500. for (let i = 0; i < NR_ROWS; i++) {
  501. if (!this.rows[i].isEmpty()) {
  502. empty = false;
  503. break;
  504. }
  505. }
  506. return empty;
  507. }
  508.  
  509. backSpace () {
  510. const row = this.rows[this.currRow];
  511. row.backSpace();
  512. }
  513.  
  514. clearToEndOfRow () {
  515. const row = this.rows[this.currRow];
  516. row.clearToEndOfRow();
  517. }
  518.  
  519. /**
  520. * Insert a character (without styling) in the current row.
  521. */
  522. insertChar (char: number) {
  523. const row = this.rows[this.currRow];
  524. row.insertChar(char);
  525. }
  526.  
  527. setPen (styles: Partial<PenStyles>) {
  528. const row = this.rows[this.currRow];
  529. row.setPenStyles(styles);
  530. }
  531.  
  532. moveCursor (relPos: number) {
  533. const row = this.rows[this.currRow];
  534. row.moveCursor(relPos);
  535. }
  536.  
  537. setCursor (absPos: number) {
  538. this.logger.log(VerboseLevel.INFO, 'setCursor: ' + absPos);
  539. const row = this.rows[this.currRow];
  540. row.setCursor(absPos);
  541. }
  542.  
  543. setPAC (pacData: PACData) {
  544. this.logger.log(VerboseLevel.INFO, 'pacData = ' + JSON.stringify(pacData));
  545. let newRow = pacData.row - 1;
  546. if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
  547. newRow = this.nrRollUpRows - 1;
  548. }
  549.  
  550. // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
  551. if (this.nrRollUpRows && this.currRow !== newRow) {
  552. // clear all rows first
  553. for (let i = 0; i < NR_ROWS; i++) {
  554. this.rows[i].clear();
  555. }
  556.  
  557. // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
  558. // topRowIndex - the start of rows to copy (inclusive index)
  559. const topRowIndex = this.currRow + 1 - (this.nrRollUpRows);
  560. // We only copy if the last position was already shown.
  561. // We use the cueStartTime value to check this.
  562. const lastOutputScreen = this.lastOutputScreen;
  563. if (lastOutputScreen) {
  564. const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
  565. const time = this.logger.time;
  566. if (prevLineTime && time !== null && prevLineTime < time) {
  567. for (let i = 0; i < this.nrRollUpRows; i++) {
  568. this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]);
  569. }
  570. }
  571. }
  572. }
  573.  
  574. this.currRow = newRow;
  575. const row = this.rows[this.currRow];
  576. if (pacData.indent !== null) {
  577. const indent = pacData.indent;
  578. const prevPos = Math.max(indent - 1, 0);
  579. row.setCursor(pacData.indent);
  580. pacData.color = row.chars[prevPos].penState.foreground;
  581. }
  582. const styles: PenStyles = { foreground: pacData.color, underline: pacData.underline, italics: pacData.italics, background: 'black', flash: false };
  583. this.setPen(styles);
  584. }
  585.  
  586. /**
  587. * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
  588. */
  589. setBkgData (bkgData: Partial<PenStyles>) {
  590. this.logger.log(VerboseLevel.INFO, 'bkgData = ' + JSON.stringify(bkgData));
  591. this.backSpace();
  592. this.setPen(bkgData);
  593. this.insertChar(0x20); // Space
  594. }
  595.  
  596. setRollUpRows (nrRows: number | null) {
  597. this.nrRollUpRows = nrRows;
  598. }
  599.  
  600. rollUp () {
  601. if (this.nrRollUpRows === null) {
  602. this.logger.log(VerboseLevel.DEBUG, 'roll_up but nrRollUpRows not set yet');
  603. return; // Not properly setup
  604. }
  605. this.logger.log(VerboseLevel.TEXT, this.getDisplayText());
  606. const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
  607. const topRow = this.rows.splice(topRowIndex, 1)[0];
  608. topRow.clear();
  609. this.rows.splice(this.currRow, 0, topRow);
  610. this.logger.log(VerboseLevel.INFO, 'Rolling up');
  611. // this.logger.log(VerboseLevel.TEXT, this.get_display_text())
  612. }
  613.  
  614. /**
  615. * Get all non-empty rows with as unicode text.
  616. */
  617. getDisplayText (asOneRow?: boolean) {
  618. asOneRow = asOneRow || false;
  619. const displayText: string[] = [];
  620. let text = '';
  621. let rowNr = -1;
  622. for (let i = 0; i < NR_ROWS; i++) {
  623. const rowText = this.rows[i].getTextString();
  624. if (rowText) {
  625. rowNr = i + 1;
  626. if (asOneRow) {
  627. displayText.push('Row ' + rowNr + ': \'' + rowText + '\'');
  628. } else {
  629. displayText.push(rowText.trim());
  630. }
  631. }
  632. }
  633. if (displayText.length > 0) {
  634. if (asOneRow) {
  635. text = '[' + displayText.join(' | ') + ']';
  636. } else {
  637. text = displayText.join('\n');
  638. }
  639. }
  640. return text;
  641. }
  642.  
  643. getTextAndFormat () {
  644. return this.rows;
  645. }
  646. }
  647.  
  648. // var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
  649.  
  650. type CaptionModes = 'MODE_ROLL-UP' | 'MODE_POP-ON' | 'MODE_PAINT-ON' | 'MODE_TEXT' | null;
  651.  
  652. class Cea608Channel {
  653. chNr: number;
  654. outputFilter: OutputFilter;
  655. mode: CaptionModes;
  656. verbose: number;
  657. displayedMemory: CaptionScreen;
  658. nonDisplayedMemory: CaptionScreen;
  659. lastOutputScreen: CaptionScreen;
  660. currRollUpRow: Row;
  661. writeScreen: CaptionScreen;
  662. cueStartTime: number | null;
  663. logger: CaptionsLogger;
  664.  
  665. constructor (channelNumber: number, outputFilter: OutputFilter, logger: CaptionsLogger) {
  666. this.chNr = channelNumber;
  667. this.outputFilter = outputFilter;
  668. this.mode = null;
  669. this.verbose = 0;
  670. this.displayedMemory = new CaptionScreen(logger);
  671. this.nonDisplayedMemory = new CaptionScreen(logger);
  672. this.lastOutputScreen = new CaptionScreen(logger);
  673. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  674. this.writeScreen = this.displayedMemory;
  675. this.mode = null;
  676. this.cueStartTime = null; // Keeps track of where a cue started.
  677. this.logger = logger;
  678. }
  679.  
  680. reset () {
  681. this.mode = null;
  682. this.displayedMemory.reset();
  683. this.nonDisplayedMemory.reset();
  684. this.lastOutputScreen.reset();
  685. this.outputFilter.reset();
  686. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  687. this.writeScreen = this.displayedMemory;
  688. this.mode = null;
  689. this.cueStartTime = null;
  690. }
  691.  
  692. getHandler (): OutputFilter {
  693. return this.outputFilter;
  694. }
  695.  
  696. setHandler (newHandler: OutputFilter) {
  697. this.outputFilter = newHandler;
  698. }
  699.  
  700. setPAC (pacData: PACData) {
  701. this.writeScreen.setPAC(pacData);
  702. }
  703.  
  704. setBkgData (bkgData: Partial<PenStyles>) {
  705. this.writeScreen.setBkgData(bkgData);
  706. }
  707.  
  708. setMode (newMode: CaptionModes) {
  709. if (newMode === this.mode) {
  710. return;
  711. }
  712.  
  713. this.mode = newMode;
  714. this.logger.log(VerboseLevel.INFO, 'MODE=' + newMode);
  715. if (this.mode === 'MODE_POP-ON') {
  716. this.writeScreen = this.nonDisplayedMemory;
  717. } else {
  718. this.writeScreen = this.displayedMemory;
  719. this.writeScreen.reset();
  720. }
  721. if (this.mode !== 'MODE_ROLL-UP') {
  722. this.displayedMemory.nrRollUpRows = null;
  723. this.nonDisplayedMemory.nrRollUpRows = null;
  724. }
  725. this.mode = newMode;
  726. }
  727.  
  728. insertChars (chars: number[]) {
  729. for (let i = 0; i < chars.length; i++) {
  730. this.writeScreen.insertChar(chars[i]);
  731. }
  732.  
  733. const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
  734. this.logger.log(VerboseLevel.INFO, screen + ': ' + this.writeScreen.getDisplayText(true));
  735. if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
  736. this.logger.log(VerboseLevel.TEXT, 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true));
  737. this.outputDataUpdate();
  738. }
  739. }
  740.  
  741. ccRCL () { // Resume Caption Loading (switch mode to Pop On)
  742. this.logger.log(VerboseLevel.INFO, 'RCL - Resume Caption Loading');
  743. this.setMode('MODE_POP-ON');
  744. }
  745.  
  746. ccBS () { // BackSpace
  747. this.logger.log(VerboseLevel.INFO, 'BS - BackSpace');
  748. if (this.mode === 'MODE_TEXT') {
  749. return;
  750. }
  751.  
  752. this.writeScreen.backSpace();
  753. if (this.writeScreen === this.displayedMemory) {
  754. this.outputDataUpdate();
  755. }
  756. }
  757.  
  758. ccAOF () { // Reserved (formerly Alarm Off)
  759.  
  760. }
  761.  
  762. ccAON () { // Reserved (formerly Alarm On)
  763.  
  764. }
  765.  
  766. ccDER () { // Delete to End of Row
  767. this.logger.log(VerboseLevel.INFO, 'DER- Delete to End of Row');
  768. this.writeScreen.clearToEndOfRow();
  769. this.outputDataUpdate();
  770. }
  771.  
  772. ccRU (nrRows: number | null) { // Roll-Up Captions-2,3,or 4 Rows
  773. this.logger.log(VerboseLevel.INFO, 'RU(' + nrRows + ') - Roll Up');
  774. this.writeScreen = this.displayedMemory;
  775. this.setMode('MODE_ROLL-UP');
  776. this.writeScreen.setRollUpRows(nrRows);
  777. }
  778.  
  779. ccFON () { // Flash On
  780. this.logger.log(VerboseLevel.INFO, 'FON - Flash On');
  781. this.writeScreen.setPen({ flash: true });
  782. }
  783.  
  784. ccRDC () { // Resume Direct Captioning (switch mode to PaintOn)
  785. this.logger.log(VerboseLevel.INFO, 'RDC - Resume Direct Captioning');
  786. this.setMode('MODE_PAINT-ON');
  787. }
  788.  
  789. ccTR () { // Text Restart in text mode (not supported, however)
  790. this.logger.log(VerboseLevel.INFO, 'TR');
  791. this.setMode('MODE_TEXT');
  792. }
  793.  
  794. ccRTD () { // Resume Text Display in Text mode (not supported, however)
  795. this.logger.log(VerboseLevel.INFO, 'RTD');
  796. this.setMode('MODE_TEXT');
  797. }
  798.  
  799. ccEDM () { // Erase Displayed Memory
  800. this.logger.log(VerboseLevel.INFO, 'EDM - Erase Displayed Memory');
  801. this.displayedMemory.reset();
  802. this.outputDataUpdate(true);
  803. }
  804.  
  805. ccCR () { // Carriage Return
  806. this.logger.log(VerboseLevel.INFO, 'CR - Carriage Return');
  807. this.writeScreen.rollUp();
  808. this.outputDataUpdate(true);
  809. }
  810.  
  811. ccENM () { // Erase Non-Displayed Memory
  812. this.logger.log(VerboseLevel.INFO, 'ENM - Erase Non-displayed Memory');
  813. this.nonDisplayedMemory.reset();
  814. }
  815.  
  816. ccEOC () { // End of Caption (Flip Memories)
  817. this.logger.log(VerboseLevel.INFO, 'EOC - End Of Caption');
  818. if (this.mode === 'MODE_POP-ON') {
  819. const tmp = this.displayedMemory;
  820. this.displayedMemory = this.nonDisplayedMemory;
  821. this.nonDisplayedMemory = tmp;
  822. this.writeScreen = this.nonDisplayedMemory;
  823. this.logger.log(VerboseLevel.TEXT, 'DISP: ' + this.displayedMemory.getDisplayText());
  824. }
  825. this.outputDataUpdate(true);
  826. }
  827.  
  828. ccTO (nrCols: number) { // Tab Offset 1,2, or 3 columns
  829. this.logger.log(VerboseLevel.INFO, 'TO(' + nrCols + ') - Tab Offset');
  830. this.writeScreen.moveCursor(nrCols);
  831. }
  832.  
  833. ccMIDROW (secondByte: number) { // Parse MIDROW command
  834. const styles: Partial<PenStyles> = { flash: false };
  835. styles.underline = secondByte % 2 === 1;
  836. styles.italics = secondByte >= 0x2e;
  837. if (!styles.italics) {
  838. const colorIndex = Math.floor(secondByte / 2) - 0x10;
  839. const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta'];
  840. styles.foreground = colors[colorIndex];
  841. } else {
  842. styles.foreground = 'white';
  843. }
  844. this.logger.log(VerboseLevel.INFO, 'MIDROW: ' + JSON.stringify(styles));
  845. this.writeScreen.setPen(styles);
  846. }
  847.  
  848. outputDataUpdate (dispatch: boolean = false) {
  849. const time = this.logger.time;
  850. if (time === null) {
  851. return;
  852. }
  853.  
  854. if (this.outputFilter) {
  855. if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) { // Start of a new cue
  856. this.cueStartTime = time;
  857. } else {
  858. if (!this.displayedMemory.equals(this.lastOutputScreen)) {
  859. this.outputFilter.newCue(this.cueStartTime!, time, this.lastOutputScreen);
  860. if (dispatch && this.outputFilter.dispatchCue) {
  861. this.outputFilter.dispatchCue();
  862. }
  863.  
  864. this.cueStartTime = this.displayedMemory.isEmpty() ? null : time;
  865. }
  866. }
  867. this.lastOutputScreen.copy(this.displayedMemory);
  868. }
  869. }
  870.  
  871. cueSplitAtTime (t: number) {
  872. if (this.outputFilter) {
  873. if (!this.displayedMemory.isEmpty()) {
  874. if (this.outputFilter.newCue) {
  875. this.outputFilter.newCue(this.cueStartTime!, t, this.displayedMemory);
  876. }
  877.  
  878. this.cueStartTime = t;
  879. }
  880. }
  881. }
  882. }
  883.  
  884. interface PACData {
  885. row: number;
  886. indent: number | null;
  887. color: string | null;
  888. underline: boolean;
  889. italics: boolean;
  890. }
  891.  
  892. type SupportedField = 1 | 3;
  893.  
  894. type Channels = 0 | 1 | 2; // Will be 1 or 2 when parsing captions
  895.  
  896. type CmdHistory = {
  897. a: number | null,
  898. b: number | null
  899. };
  900.  
  901. class Cea608Parser {
  902. channels: Array<Cea608Channel | null>;
  903. currentChannel: Channels = 0;
  904. cmdHistory: CmdHistory;
  905. logger: CaptionsLogger;
  906.  
  907. constructor (field: SupportedField, out1: OutputFilter, out2: OutputFilter) {
  908. const logger = new CaptionsLogger();
  909. this.channels = [
  910. null,
  911. new Cea608Channel(field, out1, logger),
  912. new Cea608Channel(field + 1, out2, logger)
  913. ];
  914. this.cmdHistory = createCmdHistory();
  915. this.logger = logger;
  916. }
  917.  
  918. getHandler (channel: number) {
  919. return (this.channels[channel] as Cea608Channel).getHandler();
  920. }
  921.  
  922. setHandler (channel: number, newHandler: OutputFilter) {
  923. (this.channels[channel] as Cea608Channel).setHandler(newHandler);
  924. }
  925.  
  926. /**
  927. * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
  928. */
  929. addData (time: number | null, byteList: number[]) {
  930. let cmdFound: boolean;
  931. let a: number;
  932. let b: number;
  933. let charsFound: number[] | boolean | null = false;
  934.  
  935. this.logger.time = time;
  936.  
  937. for (let i = 0; i < byteList.length; i += 2) {
  938. a = byteList[i] & 0x7f;
  939. b = byteList[i + 1] & 0x7f;
  940. if (a === 0 && b === 0) {
  941. continue;
  942. } else {
  943. this.logger.log(VerboseLevel.DATA, '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')');
  944. }
  945.  
  946. cmdFound = this.parseCmd(a, b);
  947.  
  948. if (!cmdFound) {
  949. cmdFound = this.parseMidrow(a, b);
  950. }
  951.  
  952. if (!cmdFound) {
  953. cmdFound = this.parsePAC(a, b);
  954. }
  955.  
  956. if (!cmdFound) {
  957. cmdFound = this.parseBackgroundAttributes(a, b);
  958. }
  959.  
  960. if (!cmdFound) {
  961. charsFound = this.parseChars(a, b);
  962. if (charsFound) {
  963. const currChNr = this.currentChannel;
  964. if (currChNr && currChNr > 0) {
  965. const channel = this.channels[currChNr] as Cea608Channel;
  966. channel.insertChars(charsFound);
  967. } else {
  968. this.logger.log(VerboseLevel.WARNING, 'No channel found yet. TEXT-MODE?');
  969. }
  970. }
  971. }
  972. if (!cmdFound && !charsFound) {
  973. this.logger.log(VerboseLevel.WARNING, 'Couldn\'t parse cleaned data ' + numArrayToHexArray([a, b]) +
  974. ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]]));
  975. }
  976. }
  977. }
  978.  
  979. /**
  980. * Parse Command.
  981. * @returns {Boolean} Tells if a command was found
  982. */
  983. parseCmd (a: number, b: number) {
  984. const { cmdHistory } = this;
  985. const cond1 = (a === 0x14 || a === 0x1C || a === 0x15 || a === 0x1D) && (b >= 0x20 && b <= 0x2F);
  986. const cond2 = (a === 0x17 || a === 0x1F) && (b >= 0x21 && b <= 0x23);
  987. if (!(cond1 || cond2)) {
  988. return false;
  989. }
  990.  
  991. if (hasCmdRepeated(a, b, cmdHistory)) {
  992. setLastCmd(null, null, cmdHistory);
  993. this.logger.log(VerboseLevel.DEBUG, 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped');
  994. return true;
  995. }
  996.  
  997. const chNr = (a === 0x14 || a === 0x15 || a === 0x17) ? 1 : 2;
  998. const channel = this.channels[chNr] as Cea608Channel;
  999.  
  1000. if (a === 0x14 || a === 0x15 || a === 0x1C || a === 0x1D) {
  1001. if (b === 0x20) {
  1002. channel.ccRCL();
  1003. } else if (b === 0x21) {
  1004. channel.ccBS();
  1005. } else if (b === 0x22) {
  1006. channel.ccAOF();
  1007. } else if (b === 0x23) {
  1008. channel.ccAON();
  1009. } else if (b === 0x24) {
  1010. channel.ccDER();
  1011. } else if (b === 0x25) {
  1012. channel.ccRU(2);
  1013. } else if (b === 0x26) {
  1014. channel.ccRU(3);
  1015. } else if (b === 0x27) {
  1016. channel.ccRU(4);
  1017. } else if (b === 0x28) {
  1018. channel.ccFON();
  1019. } else if (b === 0x29) {
  1020. channel.ccRDC();
  1021. } else if (b === 0x2A) {
  1022. channel.ccTR();
  1023. } else if (b === 0x2B) {
  1024. channel.ccRTD();
  1025. } else if (b === 0x2C) {
  1026. channel.ccEDM();
  1027. } else if (b === 0x2D) {
  1028. channel.ccCR();
  1029. } else if (b === 0x2E) {
  1030. channel.ccENM();
  1031. } else if (b === 0x2F) {
  1032. channel.ccEOC();
  1033. }
  1034. } else { // a == 0x17 || a == 0x1F
  1035. channel.ccTO(b - 0x20);
  1036. }
  1037. setLastCmd(a, b, cmdHistory);
  1038. this.currentChannel = chNr;
  1039. return true;
  1040. }
  1041.  
  1042. /**
  1043. * Parse midrow styling command
  1044. * @returns {Boolean}
  1045. */
  1046. parseMidrow (a: number, b: number) {
  1047. let chNr: number = 0;
  1048.  
  1049. if (((a === 0x11) || (a === 0x19)) && b >= 0x20 && b <= 0x2f) {
  1050. if (a === 0x11) {
  1051. chNr = 1;
  1052. } else {
  1053. chNr = 2;
  1054. }
  1055.  
  1056. if (chNr !== this.currentChannel) {
  1057. this.logger.log(VerboseLevel.ERROR, 'Mismatch channel in midrow parsing');
  1058. return false;
  1059. }
  1060. const channel = this.channels[chNr];
  1061. if (!channel) {
  1062. return false;
  1063. }
  1064. channel.ccMIDROW(b);
  1065. this.logger.log(VerboseLevel.DEBUG, 'MIDROW (' + numArrayToHexArray([a, b]) + ')');
  1066. return true;
  1067. }
  1068. return false;
  1069. }
  1070.  
  1071. /**
  1072. * Parse Preable Access Codes (Table 53).
  1073. * @returns {Boolean} Tells if PAC found
  1074. */
  1075. parsePAC (a: number, b: number): boolean {
  1076. let row: number;
  1077. const cmdHistory = this.cmdHistory;
  1078.  
  1079. const case1 = ((a >= 0x11 && a <= 0x17) || (a >= 0x19 && a <= 0x1F)) && (b >= 0x40 && b <= 0x7F);
  1080. const case2 = (a === 0x10 || a === 0x18) && (b >= 0x40 && b <= 0x5F);
  1081. if (!(case1 || case2)) {
  1082. return false;
  1083. }
  1084.  
  1085. if (hasCmdRepeated(a, b, cmdHistory)) {
  1086. setLastCmd(null, null, cmdHistory);
  1087. return true; // Repeated commands are dropped (once)
  1088. }
  1089.  
  1090. const chNr: Channels = (a <= 0x17) ? 1 : 2;
  1091.  
  1092. if (b >= 0x40 && b <= 0x5F) {
  1093. row = (chNr === 1) ? rowsLowCh1[a] : rowsLowCh2[a];
  1094. } else { // 0x60 <= b <= 0x7F
  1095. row = (chNr === 1) ? rowsHighCh1[a] : rowsHighCh2[a];
  1096. }
  1097. const channel = this.channels[chNr];
  1098. if (!channel) {
  1099. return false;
  1100. }
  1101. channel.setPAC(this.interpretPAC(row, b));
  1102. setLastCmd(a, b, cmdHistory);
  1103. this.currentChannel = chNr;
  1104. return true;
  1105. }
  1106.  
  1107. /**
  1108. * Interpret the second byte of the pac, and return the information.
  1109. * @returns {Object} pacData with style parameters.
  1110. */
  1111. interpretPAC (row: number, byte: number): PACData {
  1112. let pacIndex = byte;
  1113. const pacData: PACData = { color: null, italics: false, indent: null, underline: false, row: row };
  1114.  
  1115. if (byte > 0x5F) {
  1116. pacIndex = byte - 0x60;
  1117. } else {
  1118. pacIndex = byte - 0x40;
  1119. }
  1120.  
  1121. pacData.underline = (pacIndex & 1) === 1;
  1122. if (pacIndex <= 0xd) {
  1123. pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)];
  1124. } else if (pacIndex <= 0xf) {
  1125. pacData.italics = true;
  1126. pacData.color = 'white';
  1127. } else {
  1128. pacData.indent = (Math.floor((pacIndex - 0x10) / 2)) * 4;
  1129. }
  1130. return pacData; // Note that row has zero offset. The spec uses 1.
  1131. }
  1132.  
  1133. /**
  1134. * Parse characters.
  1135. * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
  1136. */
  1137. parseChars (a: number, b: number): number[] | null {
  1138. let channelNr: Channels;
  1139. let charCodes: number[] | null = null;
  1140. let charCode1: number | null = null;
  1141.  
  1142. if (a >= 0x19) {
  1143. channelNr = 2;
  1144. charCode1 = a - 8;
  1145. } else {
  1146. channelNr = 1;
  1147. charCode1 = a;
  1148. }
  1149. if (charCode1 >= 0x11 && charCode1 <= 0x13) {
  1150. // Special character
  1151. let oneCode = b;
  1152. if (charCode1 === 0x11) {
  1153. oneCode = b + 0x50;
  1154. } else if (charCode1 === 0x12) {
  1155. oneCode = b + 0x70;
  1156. } else {
  1157. oneCode = b + 0x90;
  1158. }
  1159.  
  1160. this.logger.log(VerboseLevel.INFO, 'Special char \'' + getCharForByte(oneCode) + '\' in channel ' + channelNr);
  1161. charCodes = [oneCode];
  1162. } else if (a >= 0x20 && a <= 0x7f) {
  1163. charCodes = (b === 0) ? [a] : [a, b];
  1164. }
  1165. if (charCodes) {
  1166. const hexCodes = numArrayToHexArray(charCodes);
  1167. this.logger.log(VerboseLevel.DEBUG, 'Char codes = ' + hexCodes.join(','));
  1168. setLastCmd(a, b, this.cmdHistory);
  1169. }
  1170. return charCodes;
  1171. }
  1172.  
  1173. /**
  1174. * Parse extended background attributes as well as new foreground color black.
  1175. * @returns {Boolean} Tells if background attributes are found
  1176. */
  1177. parseBackgroundAttributes (a: number, b: number): boolean {
  1178. const case1 = (a === 0x10 || a === 0x18) && (b >= 0x20 && b <= 0x2f);
  1179. const case2 = (a === 0x17 || a === 0x1f) && (b >= 0x2d && b <= 0x2f);
  1180. if (!(case1 || case2)) {
  1181. return false;
  1182. }
  1183. let index: number;
  1184. const bkgData: Partial<PenStyles> = {};
  1185. if (a === 0x10 || a === 0x18) {
  1186. index = Math.floor((b - 0x20) / 2);
  1187. bkgData.background = backgroundColors[index];
  1188. if (b % 2 === 1) {
  1189. bkgData.background = bkgData.background + '_semi';
  1190. }
  1191. } else if (b === 0x2d) {
  1192. bkgData.background = 'transparent';
  1193. } else {
  1194. bkgData.foreground = 'black';
  1195. if (b === 0x2f) {
  1196. bkgData.underline = true;
  1197. }
  1198. }
  1199. const chNr: Channels = (a <= 0x17) ? 1 : 2;
  1200. const channel: Cea608Channel = this.channels[chNr] as Cea608Channel;
  1201. channel.setBkgData(bkgData);
  1202. setLastCmd(a, b, this.cmdHistory);
  1203. return true;
  1204. }
  1205.  
  1206. /**
  1207. * Reset state of parser and its channels.
  1208. */
  1209. reset () {
  1210. for (let i = 0; i < Object.keys(this.channels).length; i++) {
  1211. const channel = this.channels[i];
  1212. if (channel) {
  1213. channel.reset();
  1214. }
  1215. }
  1216. this.cmdHistory = createCmdHistory();
  1217. }
  1218.  
  1219. /**
  1220. * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
  1221. */
  1222. cueSplitAtTime (t: number) {
  1223. for (let i = 0; i < this.channels.length; i++) {
  1224. const channel = this.channels[i];
  1225. if (channel) {
  1226. channel.cueSplitAtTime(t);
  1227. }
  1228. }
  1229. }
  1230. }
  1231.  
  1232. function setLastCmd (a: number | null, b: number | null, cmdHistory: CmdHistory) {
  1233. cmdHistory.a = a;
  1234. cmdHistory.b = b;
  1235. }
  1236.  
  1237. function hasCmdRepeated (a: number, b: number, cmdHistory: CmdHistory) {
  1238. return cmdHistory.a === a && cmdHistory.b === b;
  1239. }
  1240.  
  1241. function createCmdHistory (): CmdHistory {
  1242. return {
  1243. a: null,
  1244. b: null
  1245. };
  1246. }
  1247.  
  1248. export default Cea608Parser;