Home Reference Source

src/utils/cea-608-parser.ts

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