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. let 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. let 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. let NR_ROWS = 15,
  157. NR_COLS = 100;
  158. // Tables to look up row from PAC data
  159. let rowsLowCh1 = { 0x11: 1, 0x12: 3, 0x15: 5, 0x16: 7, 0x17: 9, 0x10: 11, 0x13: 12, 0x14: 14 };
  160. let rowsHighCh1 = { 0x11: 2, 0x12: 4, 0x15: 6, 0x16: 8, 0x17: 10, 0x13: 13, 0x14: 15 };
  161. let rowsLowCh2 = { 0x19: 1, 0x1A: 3, 0x1D: 5, 0x1E: 7, 0x1F: 9, 0x18: 11, 0x1B: 12, 0x1C: 14 };
  162. let rowsHighCh2 = { 0x19: 2, 0x1A: 4, 0x1D: 6, 0x1E: 8, 0x1F: 10, 0x1B: 13, 0x1C: 15 };
  163.  
  164. let 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. let 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. let minLevel = this.verboseFilter[severity];
  200. if (this.verboseLevel >= minLevel) {
  201. // console.log(this.time + ' [' + severity + '] ' + msg);
  202. }
  203. }
  204. };
  205.  
  206. let numArrayToHexArray = function (numArray: number[]): string[] {
  207. let 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. let attribs = ['foreground', 'underline', 'italics', 'background', 'flash'];
  248. for (let i = 0; i < attribs.length; i++) {
  249. let 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('ERROR', 'Negative cursor position ' + this.pos);
  380. this.pos = 0;
  381. } else if (this.pos > NR_COLS) {
  382. logger.log('ERROR', '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. let 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. let 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. let chars: string[] = [];
  441. let empty = true;
  442. for (let i = 0; i < NR_COLS; i++) {
  443. let 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. let 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.reset();
  482. }
  483.  
  484. reset () {
  485. for (let i = 0; i < NR_ROWS; i++) {
  486. this.rows[i].clear();
  487. }
  488.  
  489. this.currRow = NR_ROWS - 1;
  490. }
  491.  
  492. equals (other: CaptionScreen): boolean {
  493. let equal = true;
  494. for (let i = 0; i < NR_ROWS; i++) {
  495. if (!this.rows[i].equals(other.rows[i])) {
  496. equal = false;
  497. break;
  498. }
  499. }
  500. return equal;
  501. }
  502.  
  503. copy (other: CaptionScreen) {
  504. for (let i = 0; i < NR_ROWS; i++) {
  505. this.rows[i].copy(other.rows[i]);
  506. }
  507. }
  508.  
  509. isEmpty (): boolean {
  510. let empty = true;
  511. for (let i = 0; i < NR_ROWS; i++) {
  512. if (!this.rows[i].isEmpty()) {
  513. empty = false;
  514. break;
  515. }
  516. }
  517. return empty;
  518. }
  519.  
  520. backSpace () {
  521. let row = this.rows[this.currRow];
  522. row.backSpace();
  523. }
  524.  
  525. clearToEndOfRow () {
  526. let row = this.rows[this.currRow];
  527. row.clearToEndOfRow();
  528. }
  529.  
  530. /**
  531. * Insert a character (without styling) in the current row.
  532. */
  533. insertChar (char: number) {
  534. let row = this.rows[this.currRow];
  535. row.insertChar(char);
  536. }
  537.  
  538. setPen (styles: Partial<PenStyles>) {
  539. let row = this.rows[this.currRow];
  540. row.setPenStyles(styles);
  541. }
  542.  
  543. moveCursor (relPos: number) {
  544. let row = this.rows[this.currRow];
  545. row.moveCursor(relPos);
  546. }
  547.  
  548. setCursor (absPos: number) {
  549. logger.log('INFO', 'setCursor: ' + absPos);
  550. let row = this.rows[this.currRow];
  551. row.setCursor(absPos);
  552. }
  553.  
  554. setPAC (pacData: PACData) {
  555. logger.log('INFO', 'pacData = ' + JSON.stringify(pacData));
  556. let newRow = pacData.row - 1;
  557. if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
  558. newRow = this.nrRollUpRows - 1;
  559. }
  560.  
  561. // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
  562. if (this.nrRollUpRows && this.currRow !== newRow) {
  563. // clear all rows first
  564. for (let i = 0; i < NR_ROWS; i++) {
  565. this.rows[i].clear();
  566. }
  567.  
  568. // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
  569. // topRowIndex - the start of rows to copy (inclusive index)
  570. let topRowIndex = this.currRow + 1 - (this.nrRollUpRows);
  571. // We only copy if the last position was already shown.
  572. // We use the cueStartTime value to check this.
  573. const lastOutputScreen = this.lastOutputScreen as any;
  574. if (lastOutputScreen) {
  575. let prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
  576. if (prevLineTime && logger.time && prevLineTime < logger.time) {
  577. for (let i = 0; i < this.nrRollUpRows; i++) {
  578. this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]);
  579. }
  580. }
  581. }
  582. }
  583.  
  584. this.currRow = newRow;
  585. let row = this.rows[this.currRow];
  586. if (pacData.indent !== null) {
  587. let indent = pacData.indent;
  588. let prevPos = Math.max(indent - 1, 0);
  589. row.setCursor(pacData.indent);
  590. pacData.color = row.chars[prevPos].penState.foreground;
  591. }
  592. let styles: PenStyles = { foreground: pacData.color, underline: pacData.underline, italics: pacData.italics, background: 'black', flash: false };
  593. this.setPen(styles);
  594. }
  595.  
  596. /**
  597. * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
  598. */
  599. setBkgData (bkgData: Partial<PenStyles>) {
  600. logger.log('INFO', 'bkgData = ' + JSON.stringify(bkgData));
  601. this.backSpace();
  602. this.setPen(bkgData);
  603. this.insertChar(0x20); // Space
  604. }
  605.  
  606. setRollUpRows (nrRows: number | null) {
  607. this.nrRollUpRows = nrRows;
  608. }
  609.  
  610. rollUp () {
  611. if (this.nrRollUpRows === null) {
  612. logger.log('DEBUG', 'roll_up but nrRollUpRows not set yet');
  613. return; // Not properly setup
  614. }
  615. logger.log('TEXT', this.getDisplayText());
  616. let topRowIndex = this.currRow + 1 - this.nrRollUpRows;
  617. let topRow = this.rows.splice(topRowIndex, 1)[0];
  618. topRow.clear();
  619. this.rows.splice(this.currRow, 0, topRow);
  620. logger.log('INFO', 'Rolling up');
  621. // logger.log('TEXT', this.get_display_text())
  622. }
  623.  
  624. /**
  625. * Get all non-empty rows with as unicode text.
  626. */
  627. getDisplayText (asOneRow?: boolean) {
  628. asOneRow = asOneRow || false;
  629. let displayText: string[] = [];
  630. let text = '';
  631. let rowNr = -1;
  632. for (let i = 0; i < NR_ROWS; i++) {
  633. let rowText = this.rows[i].getTextString();
  634. if (rowText) {
  635. rowNr = i + 1;
  636. if (asOneRow) {
  637. displayText.push('Row ' + rowNr + ': \'' + rowText + '\'');
  638. } else {
  639. displayText.push(rowText.trim());
  640. }
  641. }
  642. }
  643. if (displayText.length > 0) {
  644. if (asOneRow) {
  645. text = '[' + displayText.join(' | ') + ']';
  646. } else {
  647. text = displayText.join('\n');
  648. }
  649. }
  650. return text;
  651. }
  652.  
  653. getTextAndFormat () {
  654. return this.rows;
  655. }
  656. }
  657.  
  658. // var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
  659.  
  660. type CaptionModes = 'MODE_ROLL-UP' | 'MODE_POP-ON' | 'MODE_PAINT-ON' | 'MODE_TEXT' | null;
  661.  
  662. class Cea608Channel {
  663. chNr: number;
  664. outputFilter: OutputFilter;
  665. mode: CaptionModes;
  666. verbose: number;
  667. displayedMemory: CaptionScreen;
  668. nonDisplayedMemory: CaptionScreen;
  669. lastOutputScreen: CaptionScreen;
  670. currRollUpRow: Row;
  671. writeScreen: CaptionScreen;
  672. cueStartTime: number | null;
  673. lastCueEndTime: null;
  674. constructor (channelNumber: number, outputFilter: OutputFilter) {
  675. this.chNr = channelNumber;
  676. this.outputFilter = outputFilter;
  677. this.mode = null;
  678. this.verbose = 0;
  679. this.displayedMemory = new CaptionScreen();
  680. this.nonDisplayedMemory = new CaptionScreen();
  681. this.lastOutputScreen = new CaptionScreen();
  682. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  683. this.writeScreen = this.displayedMemory;
  684. this.mode = null;
  685. this.cueStartTime = null; // Keeps track of where a cue started.
  686. }
  687.  
  688. reset () {
  689. this.mode = null;
  690. this.displayedMemory.reset();
  691. this.nonDisplayedMemory.reset();
  692. this.lastOutputScreen.reset();
  693. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  694. this.writeScreen = this.displayedMemory;
  695. this.mode = null;
  696. this.cueStartTime = null;
  697. }
  698.  
  699. getHandler (): OutputFilter {
  700. return this.outputFilter;
  701. }
  702.  
  703. setHandler (newHandler: OutputFilter) {
  704. this.outputFilter = newHandler;
  705. }
  706.  
  707. setPAC (pacData: PACData) {
  708. this.writeScreen.setPAC(pacData);
  709. }
  710.  
  711. setBkgData (bkgData: Partial<PenStyles>) {
  712. this.writeScreen.setBkgData(bkgData);
  713. }
  714.  
  715. setMode (newMode: CaptionModes) {
  716. if (newMode === this.mode) {
  717. return;
  718. }
  719.  
  720. this.mode = newMode;
  721. logger.log('INFO', 'MODE=' + newMode);
  722. if (this.mode === 'MODE_POP-ON') {
  723. this.writeScreen = this.nonDisplayedMemory;
  724. } else {
  725. this.writeScreen = this.displayedMemory;
  726. this.writeScreen.reset();
  727. }
  728. if (this.mode !== 'MODE_ROLL-UP') {
  729. this.displayedMemory.nrRollUpRows = null;
  730. this.nonDisplayedMemory.nrRollUpRows = null;
  731. }
  732. this.mode = newMode;
  733. }
  734.  
  735. insertChars (chars: number[]) {
  736. for (let i = 0; i < chars.length; i++) {
  737. this.writeScreen.insertChar(chars[i]);
  738. }
  739.  
  740. let screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
  741. logger.log('INFO', screen + ': ' + this.writeScreen.getDisplayText(true));
  742. if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
  743. logger.log('TEXT', 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true));
  744. this.outputDataUpdate();
  745. }
  746. }
  747.  
  748. ccRCL () { // Resume Caption Loading (switch mode to Pop On)
  749. logger.log('INFO', 'RCL - Resume Caption Loading');
  750. this.setMode('MODE_POP-ON');
  751. }
  752.  
  753. ccBS () { // BackSpace
  754. logger.log('INFO', 'BS - BackSpace');
  755. if (this.mode === 'MODE_TEXT') {
  756. return;
  757. }
  758.  
  759. this.writeScreen.backSpace();
  760. if (this.writeScreen === this.displayedMemory) {
  761. this.outputDataUpdate();
  762. }
  763. }
  764.  
  765. ccAOF () { // Reserved (formerly Alarm Off)
  766.  
  767. }
  768.  
  769. ccAON () { // Reserved (formerly Alarm On)
  770.  
  771. }
  772.  
  773. ccDER () { // Delete to End of Row
  774. logger.log('INFO', 'DER- Delete to End of Row');
  775. this.writeScreen.clearToEndOfRow();
  776. this.outputDataUpdate();
  777. }
  778.  
  779. ccRU (nrRows: number | null) { // Roll-Up Captions-2,3,or 4 Rows
  780. logger.log('INFO', 'RU(' + nrRows + ') - Roll Up');
  781. this.writeScreen = this.displayedMemory;
  782. this.setMode('MODE_ROLL-UP');
  783. this.writeScreen.setRollUpRows(nrRows);
  784. }
  785.  
  786. ccFON () { // Flash On
  787. logger.log('INFO', 'FON - Flash On');
  788. this.writeScreen.setPen({ flash: true });
  789. }
  790.  
  791. ccRDC () { // Resume Direct Captioning (switch mode to PaintOn)
  792. logger.log('INFO', 'RDC - Resume Direct Captioning');
  793. this.setMode('MODE_PAINT-ON');
  794. }
  795.  
  796. ccTR () { // Text Restart in text mode (not supported, however)
  797. logger.log('INFO', 'TR');
  798. this.setMode('MODE_TEXT');
  799. }
  800.  
  801. ccRTD () { // Resume Text Display in Text mode (not supported, however)
  802. logger.log('INFO', 'RTD');
  803. this.setMode('MODE_TEXT');
  804. }
  805.  
  806. ccEDM () { // Erase Displayed Memory
  807. logger.log('INFO', 'EDM - Erase Displayed Memory');
  808. this.displayedMemory.reset();
  809. this.outputDataUpdate(true);
  810. }
  811.  
  812. ccCR () { // Carriage Return
  813. logger.log('INFO', 'CR - Carriage Return');
  814. this.writeScreen.rollUp();
  815. this.outputDataUpdate(true);
  816. }
  817.  
  818. ccENM () { // Erase Non-Displayed Memory
  819. logger.log('INFO', 'ENM - Erase Non-displayed Memory');
  820. this.nonDisplayedMemory.reset();
  821. }
  822.  
  823. ccEOC () { // End of Caption (Flip Memories)
  824. logger.log('INFO', 'EOC - End Of Caption');
  825. if (this.mode === 'MODE_POP-ON') {
  826. let tmp = this.displayedMemory;
  827. this.displayedMemory = this.nonDisplayedMemory;
  828. this.nonDisplayedMemory = tmp;
  829. this.writeScreen = this.nonDisplayedMemory;
  830. logger.log('TEXT', 'DISP: ' + this.displayedMemory.getDisplayText());
  831. }
  832. this.outputDataUpdate(true);
  833. }
  834.  
  835. ccTO (nrCols: number) { // Tab Offset 1,2, or 3 columns
  836. logger.log('INFO', 'TO(' + nrCols + ') - Tab Offset');
  837. this.writeScreen.moveCursor(nrCols);
  838. }
  839.  
  840. ccMIDROW (secondByte: number) { // Parse MIDROW command
  841. let styles: Partial<PenStyles> = { flash: false };
  842. styles.underline = secondByte % 2 === 1;
  843. styles.italics = secondByte >= 0x2e;
  844. if (!styles.italics) {
  845. let colorIndex = Math.floor(secondByte / 2) - 0x10;
  846. let colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta'];
  847. styles.foreground = colors[colorIndex];
  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. let 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 && 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. field: number;
  901. outputs: OutputFilter[];
  902. channels: Cea608Channel[];
  903. currChNr: number;
  904. lastCmdA: number | null;
  905. lastCmdB: number | null;
  906. lastTime: number | null;
  907. dataCounters: { 'padding': number; 'char': number; 'cmd': number; 'other': number; };
  908. constructor (field: number, out1: OutputFilter, out2: OutputFilter) {
  909. this.field = field || 1;
  910. this.outputs = [out1, out2];
  911. this.channels = [new Cea608Channel(1, out1), new Cea608Channel(2, out2)];
  912. this.currChNr = -1; // Will be 1 or 2
  913. this.lastCmdA = null; // First byte of last command
  914. this.lastCmdB = null; // Second byte of last command
  915. this.lastTime = null;
  916. this.dataCounters = { 'padding': 0, 'char': 0, 'cmd': 0, 'other': 0 };
  917. }
  918.  
  919. getHandler (index: number) {
  920. return this.channels[index].getHandler();
  921. }
  922.  
  923. setHandler (index: number, newHandler: OutputFilter) {
  924. this.channels[index].setHandler(newHandler);
  925. }
  926.  
  927. /**
  928. * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
  929. */
  930. addData (t: number | null, byteList: number[]) {
  931. let cmdFound: boolean, a: number, b: number,
  932. charsFound: number[] | boolean | null = false;
  933.  
  934. this.lastTime = t;
  935. logger.setTime(t);
  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. this.dataCounters.padding += 2;
  942. continue;
  943. } else {
  944. logger.log('DATA', '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')');
  945. }
  946. cmdFound = this.parseCmd(a, b);
  947. if (!cmdFound) {
  948. cmdFound = this.parseMidrow(a, b);
  949. }
  950.  
  951. if (!cmdFound) {
  952. cmdFound = this.parsePAC(a, b);
  953. }
  954.  
  955. if (!cmdFound) {
  956. cmdFound = this.parseBackgroundAttributes(a, b);
  957. }
  958.  
  959. if (!cmdFound) {
  960. charsFound = this.parseChars(a, b);
  961. if (charsFound) {
  962. if (this.currChNr && this.currChNr >= 0) {
  963. let channel = this.channels[this.currChNr - 1];
  964. channel.insertChars(charsFound);
  965. } else {
  966. logger.log('WARNING', 'No channel found yet. TEXT-MODE?');
  967. }
  968. }
  969. }
  970. if (cmdFound) {
  971. this.dataCounters.cmd += 2;
  972. } else if (charsFound) {
  973. this.dataCounters.char += 2;
  974. } else {
  975. this.dataCounters.other += 2;
  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): boolean {
  987. let chNr: number | null = null;
  988.  
  989. let cond1 = (a === 0x14 || a === 0x1C) && (b >= 0x20 && b <= 0x2F);
  990. let cond2 = (a === 0x17 || a === 0x1F) && (b >= 0x21 && b <= 0x23);
  991. if (!(cond1 || cond2)) {
  992. return false;
  993. }
  994.  
  995. if (a === this.lastCmdA && b === this.lastCmdB) {
  996. this.lastCmdA = null;
  997. this.lastCmdB = null; // Repeated commands are dropped (once)
  998. logger.log('DEBUG', 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped');
  999. return true;
  1000. }
  1001.  
  1002. if (a === 0x14 || a === 0x17) {
  1003. chNr = 1;
  1004. } else {
  1005. chNr = 2;
  1006. } // (a === 0x1C || a=== 0x1f)
  1007.  
  1008. let channel = this.channels[chNr - 1];
  1009.  
  1010. if (a === 0x14 || a === 0x1C) {
  1011. if (b === 0x20) {
  1012. channel.ccRCL();
  1013. } else if (b === 0x21) {
  1014. channel.ccBS();
  1015. } else if (b === 0x22) {
  1016. channel.ccAOF();
  1017. } else if (b === 0x23) {
  1018. channel.ccAON();
  1019. } else if (b === 0x24) {
  1020. channel.ccDER();
  1021. } else if (b === 0x25) {
  1022. channel.ccRU(2);
  1023. } else if (b === 0x26) {
  1024. channel.ccRU(3);
  1025. } else if (b === 0x27) {
  1026. channel.ccRU(4);
  1027. } else if (b === 0x28) {
  1028. channel.ccFON();
  1029. } else if (b === 0x29) {
  1030. channel.ccRDC();
  1031. } else if (b === 0x2A) {
  1032. channel.ccTR();
  1033. } else if (b === 0x2B) {
  1034. channel.ccRTD();
  1035. } else if (b === 0x2C) {
  1036. channel.ccEDM();
  1037. } else if (b === 0x2D) {
  1038. channel.ccCR();
  1039. } else if (b === 0x2E) {
  1040. channel.ccENM();
  1041. } else if (b === 0x2F) {
  1042. channel.ccEOC();
  1043. }
  1044. } else { // a == 0x17 || a == 0x1F
  1045. channel.ccTO(b - 0x20);
  1046. }
  1047. this.lastCmdA = a;
  1048. this.lastCmdB = b;
  1049. this.currChNr = chNr;
  1050. return true;
  1051. }
  1052.  
  1053. /**
  1054. * Parse midrow styling command
  1055. * @returns {Boolean}
  1056. */
  1057. parseMidrow (a: number, b: number): boolean {
  1058. let chNr: number | null = null;
  1059.  
  1060. if (((a === 0x11) || (a === 0x19)) && b >= 0x20 && b <= 0x2f) {
  1061. if (a === 0x11) {
  1062. chNr = 1;
  1063. } else {
  1064. chNr = 2;
  1065. }
  1066.  
  1067. if (chNr !== this.currChNr) {
  1068. logger.log('ERROR', 'Mismatch channel in midrow parsing');
  1069. return false;
  1070. }
  1071. let channel = this.channels[chNr - 1];
  1072. channel.ccMIDROW(b);
  1073. logger.log('DEBUG', 'MIDROW (' + numArrayToHexArray([a, b]) + ')');
  1074. return true;
  1075. }
  1076. return false;
  1077. }
  1078. /**
  1079. * Parse Preable Access Codes (Table 53).
  1080. * @returns {Boolean} Tells if PAC found
  1081. */
  1082. parsePAC (a: number, b: number): boolean {
  1083. let chNr: number | null = null;
  1084. let row: number | null = null;
  1085.  
  1086. let case1 = ((a >= 0x11 && a <= 0x17) || (a >= 0x19 && a <= 0x1F)) && (b >= 0x40 && b <= 0x7F);
  1087. let case2 = (a === 0x10 || a === 0x18) && (b >= 0x40 && b <= 0x5F);
  1088. if (!(case1 || case2)) {
  1089. return false;
  1090. }
  1091.  
  1092. if (a === this.lastCmdA && b === this.lastCmdB) {
  1093. this.lastCmdA = null;
  1094. this.lastCmdB = null;
  1095. return true; // Repeated commands are dropped (once)
  1096. }
  1097.  
  1098. chNr = (a <= 0x17) ? 1 : 2;
  1099.  
  1100. if (b >= 0x40 && b <= 0x5F) {
  1101. row = (chNr === 1) ? rowsLowCh1[a] : rowsLowCh2[a];
  1102. } else { // 0x60 <= b <= 0x7F
  1103. row = (chNr === 1) ? rowsHighCh1[a] : rowsHighCh2[a];
  1104. }
  1105. let pacData = this.interpretPAC(row!, b);
  1106. let channel = this.channels[chNr - 1];
  1107. channel.setPAC(pacData);
  1108. this.lastCmdA = a;
  1109. this.lastCmdB = b;
  1110. this.currChNr = chNr;
  1111. return true;
  1112. }
  1113.  
  1114. /**
  1115. * Interpret the second byte of the pac, and return the information.
  1116. * @returns {Object} pacData with style parameters.
  1117. */
  1118. interpretPAC (row: number, byte: number): PACData {
  1119. let pacIndex = byte;
  1120. let pacData: PACData = { color: null, italics: false, indent: null, underline: false, row: row };
  1121.  
  1122. if (byte > 0x5F) {
  1123. pacIndex = byte - 0x60;
  1124. } else {
  1125. pacIndex = byte - 0x40;
  1126. }
  1127.  
  1128. pacData.underline = (pacIndex & 1) === 1;
  1129. if (pacIndex <= 0xd) {
  1130. pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)];
  1131. } else if (pacIndex <= 0xf) {
  1132. pacData.italics = true;
  1133. pacData.color = 'white';
  1134. } else {
  1135. pacData.indent = (Math.floor((pacIndex - 0x10) / 2)) * 4;
  1136. }
  1137. return pacData; // Note that row has zero offset. The spec uses 1.
  1138. }
  1139.  
  1140. /**
  1141. * Parse characters.
  1142. * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
  1143. */
  1144. parseChars (a: number, b: number): number[] | null {
  1145. let channelNr: number | null = null,
  1146. charCodes: number[] | null = null,
  1147. charCode1: number | null = null;
  1148.  
  1149. if (a >= 0x19) {
  1150. channelNr = 2;
  1151. charCode1 = a - 8;
  1152. } else {
  1153. channelNr = 1;
  1154. charCode1 = a;
  1155. }
  1156. if (charCode1 >= 0x11 && charCode1 <= 0x13) {
  1157. // Special character
  1158. let oneCode = b;
  1159. if (charCode1 === 0x11) {
  1160. oneCode = b + 0x50;
  1161. } else if (charCode1 === 0x12) {
  1162. oneCode = b + 0x70;
  1163. } else {
  1164. oneCode = b + 0x90;
  1165. }
  1166.  
  1167. logger.log('INFO', 'Special char \'' + getCharForByte(oneCode) + '\' in channel ' + channelNr);
  1168. charCodes = [oneCode];
  1169. } else if (a >= 0x20 && a <= 0x7f) {
  1170. charCodes = (b === 0) ? [a] : [a, b];
  1171. }
  1172. if (charCodes) {
  1173. let hexCodes = numArrayToHexArray(charCodes);
  1174. logger.log('DEBUG', 'Char codes = ' + hexCodes.join(','));
  1175. this.lastCmdA = null;
  1176. this.lastCmdB = null;
  1177. }
  1178. return charCodes;
  1179. }
  1180.  
  1181. /**
  1182. * Parse extended background attributes as well as new foreground color black.
  1183. * @returns {Boolean} Tells if background attributes are found
  1184. */
  1185. parseBackgroundAttributes (a: number, b: number): boolean {
  1186. let bkgData: Partial<PenStyles>,
  1187. index: number,
  1188. chNr: number,
  1189. channel: Cea608Channel;
  1190.  
  1191. let case1 = (a === 0x10 || a === 0x18) && (b >= 0x20 && b <= 0x2f);
  1192. let case2 = (a === 0x17 || a === 0x1f) && (b >= 0x2d && b <= 0x2f);
  1193. if (!(case1 || case2)) {
  1194. return false;
  1195. }
  1196.  
  1197. bkgData = {};
  1198. if (a === 0x10 || a === 0x18) {
  1199. index = Math.floor((b - 0x20) / 2);
  1200. bkgData.background = backgroundColors[index];
  1201. if (b % 2 === 1) {
  1202. bkgData.background = bkgData.background + '_semi';
  1203. }
  1204. } else if (b === 0x2d) {
  1205. bkgData.background = 'transparent';
  1206. } else {
  1207. bkgData.foreground = 'black';
  1208. if (b === 0x2f) {
  1209. bkgData.underline = true;
  1210. }
  1211. }
  1212. chNr = (a < 0x18) ? 1 : 2;
  1213. channel = this.channels[chNr - 1];
  1214. channel.setBkgData(bkgData);
  1215. this.lastCmdA = null;
  1216. this.lastCmdB = null;
  1217. return true;
  1218. }
  1219.  
  1220. /**
  1221. * Reset state of parser and its channels.
  1222. */
  1223. reset () {
  1224. for (let i = 0; i < this.channels.length; i++) {
  1225. if (this.channels[i]) {
  1226. this.channels[i].reset();
  1227. }
  1228. }
  1229. this.lastCmdA = null;
  1230. this.lastCmdB = null;
  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. if (this.channels[i]) {
  1239. this.channels[i].cueSplitAtTime(t);
  1240. }
  1241. }
  1242. }
  1243. }
  1244.  
  1245. export default Cea608Parser;