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 = {
  161. 0x11: 1,
  162. 0x12: 3,
  163. 0x15: 5,
  164. 0x16: 7,
  165. 0x17: 9,
  166. 0x10: 11,
  167. 0x13: 12,
  168. 0x14: 14,
  169. };
  170. const rowsHighCh1 = {
  171. 0x11: 2,
  172. 0x12: 4,
  173. 0x15: 6,
  174. 0x16: 8,
  175. 0x17: 10,
  176. 0x13: 13,
  177. 0x14: 15,
  178. };
  179. const rowsLowCh2 = {
  180. 0x19: 1,
  181. 0x1a: 3,
  182. 0x1d: 5,
  183. 0x1e: 7,
  184. 0x1f: 9,
  185. 0x18: 11,
  186. 0x1b: 12,
  187. 0x1c: 14,
  188. };
  189. const rowsHighCh2 = {
  190. 0x19: 2,
  191. 0x1a: 4,
  192. 0x1d: 6,
  193. 0x1e: 8,
  194. 0x1f: 10,
  195. 0x1b: 13,
  196. 0x1c: 15,
  197. };
  198.  
  199. const backgroundColors = [
  200. 'white',
  201. 'green',
  202. 'blue',
  203. 'cyan',
  204. 'red',
  205. 'yellow',
  206. 'magenta',
  207. 'black',
  208. 'transparent',
  209. ];
  210.  
  211. enum VerboseLevel {
  212. ERROR = 0,
  213. TEXT = 1,
  214. WARNING = 2,
  215. INFO = 2,
  216. DEBUG = 3,
  217. DATA = 3,
  218. }
  219.  
  220. class CaptionsLogger {
  221. public time: number | null = null;
  222. public verboseLevel: VerboseLevel = VerboseLevel.ERROR;
  223.  
  224. log(severity: VerboseLevel, msg: string): void {
  225. if (this.verboseLevel >= severity) {
  226. logger.log(`${this.time} [${severity}] ${msg}`);
  227. }
  228. }
  229. }
  230.  
  231. const numArrayToHexArray = function (numArray: number[]): string[] {
  232. const hexArray: string[] = [];
  233. for (let j = 0; j < numArray.length; j++) {
  234. hexArray.push(numArray[j].toString(16));
  235. }
  236.  
  237. return hexArray;
  238. };
  239.  
  240. type PenStyles = {
  241. foreground: string | null;
  242. underline: boolean;
  243. italics: boolean;
  244. background: string;
  245. flash: boolean;
  246. };
  247.  
  248. class PenState {
  249. public foreground: string;
  250. public underline: boolean;
  251. public italics: boolean;
  252. public background: string;
  253. public flash: boolean;
  254.  
  255. constructor(
  256. foreground?: string,
  257. underline?: boolean,
  258. italics?: boolean,
  259. background?: string,
  260. flash?: boolean
  261. ) {
  262. this.foreground = foreground || 'white';
  263. this.underline = underline || false;
  264. this.italics = italics || false;
  265. this.background = background || 'black';
  266. this.flash = flash || false;
  267. }
  268.  
  269. reset() {
  270. this.foreground = 'white';
  271. this.underline = false;
  272. this.italics = false;
  273. this.background = 'black';
  274. this.flash = false;
  275. }
  276.  
  277. setStyles(styles: Partial<PenStyles>) {
  278. const attribs = [
  279. 'foreground',
  280. 'underline',
  281. 'italics',
  282. 'background',
  283. 'flash',
  284. ];
  285. for (let i = 0; i < attribs.length; i++) {
  286. const style = attribs[i];
  287. if (styles.hasOwnProperty(style)) {
  288. this[style] = styles[style];
  289. }
  290. }
  291. }
  292.  
  293. isDefault() {
  294. return (
  295. this.foreground === 'white' &&
  296. !this.underline &&
  297. !this.italics &&
  298. this.background === 'black' &&
  299. !this.flash
  300. );
  301. }
  302.  
  303. equals(other: PenState) {
  304. return (
  305. this.foreground === other.foreground &&
  306. this.underline === other.underline &&
  307. this.italics === other.italics &&
  308. this.background === other.background &&
  309. this.flash === other.flash
  310. );
  311. }
  312.  
  313. copy(newPenState: PenState) {
  314. this.foreground = newPenState.foreground;
  315. this.underline = newPenState.underline;
  316. this.italics = newPenState.italics;
  317. this.background = newPenState.background;
  318. this.flash = newPenState.flash;
  319. }
  320.  
  321. toString(): string {
  322. return (
  323. 'color=' +
  324. this.foreground +
  325. ', underline=' +
  326. this.underline +
  327. ', italics=' +
  328. this.italics +
  329. ', background=' +
  330. this.background +
  331. ', flash=' +
  332. this.flash
  333. );
  334. }
  335. }
  336.  
  337. /**
  338. * Unicode character with styling and background.
  339. * @constructor
  340. */
  341. class StyledUnicodeChar {
  342. uchar: string;
  343. penState: PenState;
  344.  
  345. constructor(
  346. uchar?: string,
  347. foreground?: string,
  348. underline?: boolean,
  349. italics?: boolean,
  350. background?: string,
  351. flash?: boolean
  352. ) {
  353. this.uchar = uchar || ' '; // unicode character
  354. this.penState = new PenState(
  355. foreground,
  356. underline,
  357. italics,
  358. background,
  359. flash
  360. );
  361. }
  362.  
  363. reset() {
  364. this.uchar = ' ';
  365. this.penState.reset();
  366. }
  367.  
  368. setChar(uchar: string, newPenState: PenState) {
  369. this.uchar = uchar;
  370. this.penState.copy(newPenState);
  371. }
  372.  
  373. setPenState(newPenState: PenState) {
  374. this.penState.copy(newPenState);
  375. }
  376.  
  377. equals(other: StyledUnicodeChar) {
  378. return this.uchar === other.uchar && this.penState.equals(other.penState);
  379. }
  380.  
  381. copy(newChar: StyledUnicodeChar) {
  382. this.uchar = newChar.uchar;
  383. this.penState.copy(newChar.penState);
  384. }
  385.  
  386. isEmpty(): boolean {
  387. return this.uchar === ' ' && this.penState.isDefault();
  388. }
  389. }
  390.  
  391. /**
  392. * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
  393. * @constructor
  394. */
  395. export class Row {
  396. public chars: StyledUnicodeChar[];
  397. public pos: number;
  398. public currPenState: PenState;
  399. public cueStartTime?: number;
  400. logger: CaptionsLogger;
  401.  
  402. constructor(logger: CaptionsLogger) {
  403. this.chars = [];
  404. for (let i = 0; i < NR_COLS; i++) {
  405. this.chars.push(new StyledUnicodeChar());
  406. }
  407.  
  408. this.logger = logger;
  409. this.pos = 0;
  410. this.currPenState = new PenState();
  411. }
  412.  
  413. equals(other: Row) {
  414. let equal = true;
  415. for (let i = 0; i < NR_COLS; i++) {
  416. if (!this.chars[i].equals(other.chars[i])) {
  417. equal = false;
  418. break;
  419. }
  420. }
  421. return equal;
  422. }
  423.  
  424. copy(other: Row) {
  425. for (let i = 0; i < NR_COLS; i++) {
  426. this.chars[i].copy(other.chars[i]);
  427. }
  428. }
  429.  
  430. isEmpty(): boolean {
  431. let empty = true;
  432. for (let i = 0; i < NR_COLS; i++) {
  433. if (!this.chars[i].isEmpty()) {
  434. empty = false;
  435. break;
  436. }
  437. }
  438. return empty;
  439. }
  440.  
  441. /**
  442. * Set the cursor to a valid column.
  443. */
  444. setCursor(absPos: number) {
  445. if (this.pos !== absPos) {
  446. this.pos = absPos;
  447. }
  448.  
  449. if (this.pos < 0) {
  450. this.logger.log(
  451. VerboseLevel.DEBUG,
  452. 'Negative cursor position ' + this.pos
  453. );
  454. this.pos = 0;
  455. } else if (this.pos > NR_COLS) {
  456. this.logger.log(
  457. VerboseLevel.DEBUG,
  458. 'Too large cursor position ' + this.pos
  459. );
  460. this.pos = NR_COLS;
  461. }
  462. }
  463.  
  464. /**
  465. * Move the cursor relative to current position.
  466. */
  467. moveCursor(relPos: number) {
  468. const newPos = this.pos + relPos;
  469. if (relPos > 1) {
  470. for (let i = this.pos + 1; i < newPos + 1; i++) {
  471. this.chars[i].setPenState(this.currPenState);
  472. }
  473. }
  474. this.setCursor(newPos);
  475. }
  476.  
  477. /**
  478. * Backspace, move one step back and clear character.
  479. */
  480. backSpace() {
  481. this.moveCursor(-1);
  482. this.chars[this.pos].setChar(' ', this.currPenState);
  483. }
  484.  
  485. insertChar(byte: number) {
  486. if (byte >= 0x90) {
  487. // Extended char
  488. this.backSpace();
  489. }
  490. const char = getCharForByte(byte);
  491. if (this.pos >= NR_COLS) {
  492. this.logger.log(
  493. VerboseLevel.ERROR,
  494. 'Cannot insert ' +
  495. byte.toString(16) +
  496. ' (' +
  497. char +
  498. ') at position ' +
  499. this.pos +
  500. '. Skipping it!'
  501. );
  502. return;
  503. }
  504. this.chars[this.pos].setChar(char, this.currPenState);
  505. this.moveCursor(1);
  506. }
  507.  
  508. clearFromPos(startPos: number) {
  509. let i: number;
  510. for (i = startPos; i < NR_COLS; i++) {
  511. this.chars[i].reset();
  512. }
  513. }
  514.  
  515. clear() {
  516. this.clearFromPos(0);
  517. this.pos = 0;
  518. this.currPenState.reset();
  519. }
  520.  
  521. clearToEndOfRow() {
  522. this.clearFromPos(this.pos);
  523. }
  524.  
  525. getTextString() {
  526. const chars: string[] = [];
  527. let empty = true;
  528. for (let i = 0; i < NR_COLS; i++) {
  529. const char = this.chars[i].uchar;
  530. if (char !== ' ') {
  531. empty = false;
  532. }
  533.  
  534. chars.push(char);
  535. }
  536. if (empty) {
  537. return '';
  538. } else {
  539. return chars.join('');
  540. }
  541. }
  542.  
  543. setPenStyles(styles: Partial<PenStyles>) {
  544. this.currPenState.setStyles(styles);
  545. const currChar = this.chars[this.pos];
  546. currChar.setPenState(this.currPenState);
  547. }
  548. }
  549.  
  550. /**
  551. * Keep a CEA-608 screen of 32x15 styled characters
  552. * @constructor
  553. */
  554. export class CaptionScreen {
  555. rows: Row[];
  556. currRow: number;
  557. nrRollUpRows: number | null;
  558. lastOutputScreen: CaptionScreen | null;
  559. logger: CaptionsLogger;
  560.  
  561. constructor(logger: CaptionsLogger) {
  562. this.rows = [];
  563. for (let i = 0; i < NR_ROWS; i++) {
  564. this.rows.push(new Row(logger));
  565. } // Note that we use zero-based numbering (0-14)
  566.  
  567. this.logger = logger;
  568. this.currRow = NR_ROWS - 1;
  569. this.nrRollUpRows = null;
  570. this.lastOutputScreen = null;
  571. this.reset();
  572. }
  573.  
  574. reset() {
  575. for (let i = 0; i < NR_ROWS; i++) {
  576. this.rows[i].clear();
  577. }
  578.  
  579. this.currRow = NR_ROWS - 1;
  580. }
  581.  
  582. equals(other: CaptionScreen): boolean {
  583. let equal = true;
  584. for (let i = 0; i < NR_ROWS; i++) {
  585. if (!this.rows[i].equals(other.rows[i])) {
  586. equal = false;
  587. break;
  588. }
  589. }
  590. return equal;
  591. }
  592.  
  593. copy(other: CaptionScreen) {
  594. for (let i = 0; i < NR_ROWS; i++) {
  595. this.rows[i].copy(other.rows[i]);
  596. }
  597. }
  598.  
  599. isEmpty(): boolean {
  600. let empty = true;
  601. for (let i = 0; i < NR_ROWS; i++) {
  602. if (!this.rows[i].isEmpty()) {
  603. empty = false;
  604. break;
  605. }
  606. }
  607. return empty;
  608. }
  609.  
  610. backSpace() {
  611. const row = this.rows[this.currRow];
  612. row.backSpace();
  613. }
  614.  
  615. clearToEndOfRow() {
  616. const row = this.rows[this.currRow];
  617. row.clearToEndOfRow();
  618. }
  619.  
  620. /**
  621. * Insert a character (without styling) in the current row.
  622. */
  623. insertChar(char: number) {
  624. const row = this.rows[this.currRow];
  625. row.insertChar(char);
  626. }
  627.  
  628. setPen(styles: Partial<PenStyles>) {
  629. const row = this.rows[this.currRow];
  630. row.setPenStyles(styles);
  631. }
  632.  
  633. moveCursor(relPos: number) {
  634. const row = this.rows[this.currRow];
  635. row.moveCursor(relPos);
  636. }
  637.  
  638. setCursor(absPos: number) {
  639. this.logger.log(VerboseLevel.INFO, 'setCursor: ' + absPos);
  640. const row = this.rows[this.currRow];
  641. row.setCursor(absPos);
  642. }
  643.  
  644. setPAC(pacData: PACData) {
  645. this.logger.log(VerboseLevel.INFO, 'pacData = ' + JSON.stringify(pacData));
  646. let newRow = pacData.row - 1;
  647. if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
  648. newRow = this.nrRollUpRows - 1;
  649. }
  650.  
  651. // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
  652. if (this.nrRollUpRows && this.currRow !== newRow) {
  653. // clear all rows first
  654. for (let i = 0; i < NR_ROWS; i++) {
  655. this.rows[i].clear();
  656. }
  657.  
  658. // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
  659. // topRowIndex - the start of rows to copy (inclusive index)
  660. const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
  661. // We only copy if the last position was already shown.
  662. // We use the cueStartTime value to check this.
  663. const lastOutputScreen = this.lastOutputScreen;
  664. if (lastOutputScreen) {
  665. const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
  666. const time = this.logger.time;
  667. if (prevLineTime && time !== null && prevLineTime < time) {
  668. for (let i = 0; i < this.nrRollUpRows; i++) {
  669. this.rows[newRow - this.nrRollUpRows + i + 1].copy(
  670. lastOutputScreen.rows[topRowIndex + i]
  671. );
  672. }
  673. }
  674. }
  675. }
  676.  
  677. this.currRow = newRow;
  678. const row = this.rows[this.currRow];
  679. if (pacData.indent !== null) {
  680. const indent = pacData.indent;
  681. const prevPos = Math.max(indent - 1, 0);
  682. row.setCursor(pacData.indent);
  683. pacData.color = row.chars[prevPos].penState.foreground;
  684. }
  685. const styles: PenStyles = {
  686. foreground: pacData.color,
  687. underline: pacData.underline,
  688. italics: pacData.italics,
  689. background: 'black',
  690. flash: false,
  691. };
  692. this.setPen(styles);
  693. }
  694.  
  695. /**
  696. * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
  697. */
  698. setBkgData(bkgData: Partial<PenStyles>) {
  699. this.logger.log(VerboseLevel.INFO, 'bkgData = ' + JSON.stringify(bkgData));
  700. this.backSpace();
  701. this.setPen(bkgData);
  702. this.insertChar(0x20); // Space
  703. }
  704.  
  705. setRollUpRows(nrRows: number | null) {
  706. this.nrRollUpRows = nrRows;
  707. }
  708.  
  709. rollUp() {
  710. if (this.nrRollUpRows === null) {
  711. this.logger.log(
  712. VerboseLevel.DEBUG,
  713. 'roll_up but nrRollUpRows not set yet'
  714. );
  715. return; // Not properly setup
  716. }
  717. this.logger.log(VerboseLevel.TEXT, this.getDisplayText());
  718. const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
  719. const topRow = this.rows.splice(topRowIndex, 1)[0];
  720. topRow.clear();
  721. this.rows.splice(this.currRow, 0, topRow);
  722. this.logger.log(VerboseLevel.INFO, 'Rolling up');
  723. // this.logger.log(VerboseLevel.TEXT, this.get_display_text())
  724. }
  725.  
  726. /**
  727. * Get all non-empty rows with as unicode text.
  728. */
  729. getDisplayText(asOneRow?: boolean) {
  730. asOneRow = asOneRow || false;
  731. const displayText: string[] = [];
  732. let text = '';
  733. let rowNr = -1;
  734. for (let i = 0; i < NR_ROWS; i++) {
  735. const rowText = this.rows[i].getTextString();
  736. if (rowText) {
  737. rowNr = i + 1;
  738. if (asOneRow) {
  739. displayText.push('Row ' + rowNr + ": '" + rowText + "'");
  740. } else {
  741. displayText.push(rowText.trim());
  742. }
  743. }
  744. }
  745. if (displayText.length > 0) {
  746. if (asOneRow) {
  747. text = '[' + displayText.join(' | ') + ']';
  748. } else {
  749. text = displayText.join('\n');
  750. }
  751. }
  752. return text;
  753. }
  754.  
  755. getTextAndFormat() {
  756. return this.rows;
  757. }
  758. }
  759.  
  760. // var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
  761.  
  762. type CaptionModes =
  763. | 'MODE_ROLL-UP'
  764. | 'MODE_POP-ON'
  765. | 'MODE_PAINT-ON'
  766. | 'MODE_TEXT'
  767. | null;
  768.  
  769. class Cea608Channel {
  770. chNr: number;
  771. outputFilter: OutputFilter;
  772. mode: CaptionModes;
  773. verbose: number;
  774. displayedMemory: CaptionScreen;
  775. nonDisplayedMemory: CaptionScreen;
  776. lastOutputScreen: CaptionScreen;
  777. currRollUpRow: Row;
  778. writeScreen: CaptionScreen;
  779. cueStartTime: number | null;
  780. logger: CaptionsLogger;
  781.  
  782. constructor(
  783. channelNumber: number,
  784. outputFilter: OutputFilter,
  785. logger: CaptionsLogger
  786. ) {
  787. this.chNr = channelNumber;
  788. this.outputFilter = outputFilter;
  789. this.mode = null;
  790. this.verbose = 0;
  791. this.displayedMemory = new CaptionScreen(logger);
  792. this.nonDisplayedMemory = new CaptionScreen(logger);
  793. this.lastOutputScreen = new CaptionScreen(logger);
  794. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  795. this.writeScreen = this.displayedMemory;
  796. this.mode = null;
  797. this.cueStartTime = null; // Keeps track of where a cue started.
  798. this.logger = logger;
  799. }
  800.  
  801. reset() {
  802. this.mode = null;
  803. this.displayedMemory.reset();
  804. this.nonDisplayedMemory.reset();
  805. this.lastOutputScreen.reset();
  806. this.outputFilter.reset();
  807. this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
  808. this.writeScreen = this.displayedMemory;
  809. this.mode = null;
  810. this.cueStartTime = null;
  811. }
  812.  
  813. getHandler(): OutputFilter {
  814. return this.outputFilter;
  815. }
  816.  
  817. setHandler(newHandler: OutputFilter) {
  818. this.outputFilter = newHandler;
  819. }
  820.  
  821. setPAC(pacData: PACData) {
  822. this.writeScreen.setPAC(pacData);
  823. }
  824.  
  825. setBkgData(bkgData: Partial<PenStyles>) {
  826. this.writeScreen.setBkgData(bkgData);
  827. }
  828.  
  829. setMode(newMode: CaptionModes) {
  830. if (newMode === this.mode) {
  831. return;
  832. }
  833.  
  834. this.mode = newMode;
  835. this.logger.log(VerboseLevel.INFO, 'MODE=' + newMode);
  836. if (this.mode === 'MODE_POP-ON') {
  837. this.writeScreen = this.nonDisplayedMemory;
  838. } else {
  839. this.writeScreen = this.displayedMemory;
  840. this.writeScreen.reset();
  841. }
  842. if (this.mode !== 'MODE_ROLL-UP') {
  843. this.displayedMemory.nrRollUpRows = null;
  844. this.nonDisplayedMemory.nrRollUpRows = null;
  845. }
  846. this.mode = newMode;
  847. }
  848.  
  849. insertChars(chars: number[]) {
  850. for (let i = 0; i < chars.length; i++) {
  851. this.writeScreen.insertChar(chars[i]);
  852. }
  853.  
  854. const screen =
  855. this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
  856. this.logger.log(
  857. VerboseLevel.INFO,
  858. screen + ': ' + this.writeScreen.getDisplayText(true)
  859. );
  860. if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
  861. this.logger.log(
  862. VerboseLevel.TEXT,
  863. 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)
  864. );
  865. this.outputDataUpdate();
  866. }
  867. }
  868.  
  869. ccRCL() {
  870. // Resume Caption Loading (switch mode to Pop On)
  871. this.logger.log(VerboseLevel.INFO, 'RCL - Resume Caption Loading');
  872. this.setMode('MODE_POP-ON');
  873. }
  874.  
  875. ccBS() {
  876. // BackSpace
  877. this.logger.log(VerboseLevel.INFO, 'BS - BackSpace');
  878. if (this.mode === 'MODE_TEXT') {
  879. return;
  880. }
  881.  
  882. this.writeScreen.backSpace();
  883. if (this.writeScreen === this.displayedMemory) {
  884. this.outputDataUpdate();
  885. }
  886. }
  887.  
  888. ccAOF() {
  889. // Reserved (formerly Alarm Off)
  890. }
  891.  
  892. ccAON() {
  893. // Reserved (formerly Alarm On)
  894. }
  895.  
  896. ccDER() {
  897. // Delete to End of Row
  898. this.logger.log(VerboseLevel.INFO, 'DER- Delete to End of Row');
  899. this.writeScreen.clearToEndOfRow();
  900. this.outputDataUpdate();
  901. }
  902.  
  903. ccRU(nrRows: number | null) {
  904. // Roll-Up Captions-2,3,or 4 Rows
  905. this.logger.log(VerboseLevel.INFO, 'RU(' + nrRows + ') - Roll Up');
  906. this.writeScreen = this.displayedMemory;
  907. this.setMode('MODE_ROLL-UP');
  908. this.writeScreen.setRollUpRows(nrRows);
  909. }
  910.  
  911. ccFON() {
  912. // Flash On
  913. this.logger.log(VerboseLevel.INFO, 'FON - Flash On');
  914. this.writeScreen.setPen({ flash: true });
  915. }
  916.  
  917. ccRDC() {
  918. // Resume Direct Captioning (switch mode to PaintOn)
  919. this.logger.log(VerboseLevel.INFO, 'RDC - Resume Direct Captioning');
  920. this.setMode('MODE_PAINT-ON');
  921. }
  922.  
  923. ccTR() {
  924. // Text Restart in text mode (not supported, however)
  925. this.logger.log(VerboseLevel.INFO, 'TR');
  926. this.setMode('MODE_TEXT');
  927. }
  928.  
  929. ccRTD() {
  930. // Resume Text Display in Text mode (not supported, however)
  931. this.logger.log(VerboseLevel.INFO, 'RTD');
  932. this.setMode('MODE_TEXT');
  933. }
  934.  
  935. ccEDM() {
  936. // Erase Displayed Memory
  937. this.logger.log(VerboseLevel.INFO, 'EDM - Erase Displayed Memory');
  938. this.displayedMemory.reset();
  939. this.outputDataUpdate(true);
  940. }
  941.  
  942. ccCR() {
  943. // Carriage Return
  944. this.logger.log(VerboseLevel.INFO, 'CR - Carriage Return');
  945. this.writeScreen.rollUp();
  946. this.outputDataUpdate(true);
  947. }
  948.  
  949. ccENM() {
  950. // Erase Non-Displayed Memory
  951. this.logger.log(VerboseLevel.INFO, 'ENM - Erase Non-displayed Memory');
  952. this.nonDisplayedMemory.reset();
  953. }
  954.  
  955. ccEOC() {
  956. // End of Caption (Flip Memories)
  957. this.logger.log(VerboseLevel.INFO, 'EOC - End Of Caption');
  958. if (this.mode === 'MODE_POP-ON') {
  959. const tmp = this.displayedMemory;
  960. this.displayedMemory = this.nonDisplayedMemory;
  961. this.nonDisplayedMemory = tmp;
  962. this.writeScreen = this.nonDisplayedMemory;
  963. this.logger.log(
  964. VerboseLevel.TEXT,
  965. 'DISP: ' + this.displayedMemory.getDisplayText()
  966. );
  967. }
  968. this.outputDataUpdate(true);
  969. }
  970.  
  971. ccTO(nrCols: number) {
  972. // Tab Offset 1,2, or 3 columns
  973. this.logger.log(VerboseLevel.INFO, 'TO(' + nrCols + ') - Tab Offset');
  974. this.writeScreen.moveCursor(nrCols);
  975. }
  976.  
  977. ccMIDROW(secondByte: number) {
  978. // Parse MIDROW command
  979. const styles: Partial<PenStyles> = { flash: false };
  980. styles.underline = secondByte % 2 === 1;
  981. styles.italics = secondByte >= 0x2e;
  982. if (!styles.italics) {
  983. const colorIndex = Math.floor(secondByte / 2) - 0x10;
  984. const colors = [
  985. 'white',
  986. 'green',
  987. 'blue',
  988. 'cyan',
  989. 'red',
  990. 'yellow',
  991. 'magenta',
  992. ];
  993. styles.foreground = colors[colorIndex];
  994. } else {
  995. styles.foreground = 'white';
  996. }
  997. this.logger.log(VerboseLevel.INFO, 'MIDROW: ' + JSON.stringify(styles));
  998. this.writeScreen.setPen(styles);
  999. }
  1000.  
  1001. outputDataUpdate(dispatch: boolean = false) {
  1002. const time = this.logger.time;
  1003. if (time === null) {
  1004. return;
  1005. }
  1006.  
  1007. if (this.outputFilter) {
  1008. if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) {
  1009. // Start of a new cue
  1010. this.cueStartTime = time;
  1011. } else {
  1012. if (!this.displayedMemory.equals(this.lastOutputScreen)) {
  1013. this.outputFilter.newCue(
  1014. this.cueStartTime!,
  1015. time,
  1016. this.lastOutputScreen
  1017. );
  1018. if (dispatch && this.outputFilter.dispatchCue) {
  1019. this.outputFilter.dispatchCue();
  1020. }
  1021.  
  1022. this.cueStartTime = this.displayedMemory.isEmpty() ? null : time;
  1023. }
  1024. }
  1025. this.lastOutputScreen.copy(this.displayedMemory);
  1026. }
  1027. }
  1028.  
  1029. cueSplitAtTime(t: number) {
  1030. if (this.outputFilter) {
  1031. if (!this.displayedMemory.isEmpty()) {
  1032. if (this.outputFilter.newCue) {
  1033. this.outputFilter.newCue(this.cueStartTime!, t, this.displayedMemory);
  1034. }
  1035.  
  1036. this.cueStartTime = t;
  1037. }
  1038. }
  1039. }
  1040. }
  1041.  
  1042. interface PACData {
  1043. row: number;
  1044. indent: number | null;
  1045. color: string | null;
  1046. underline: boolean;
  1047. italics: boolean;
  1048. }
  1049.  
  1050. type SupportedField = 1 | 3;
  1051.  
  1052. type Channels = 0 | 1 | 2; // Will be 1 or 2 when parsing captions
  1053.  
  1054. type CmdHistory = {
  1055. a: number | null;
  1056. b: number | null;
  1057. };
  1058.  
  1059. class Cea608Parser {
  1060. channels: Array<Cea608Channel | null>;
  1061. currentChannel: Channels = 0;
  1062. cmdHistory: CmdHistory;
  1063. logger: CaptionsLogger;
  1064.  
  1065. constructor(field: SupportedField, out1: OutputFilter, out2: OutputFilter) {
  1066. const logger = new CaptionsLogger();
  1067. this.channels = [
  1068. null,
  1069. new Cea608Channel(field, out1, logger),
  1070. new Cea608Channel(field + 1, out2, logger),
  1071. ];
  1072. this.cmdHistory = createCmdHistory();
  1073. this.logger = logger;
  1074. }
  1075.  
  1076. getHandler(channel: number) {
  1077. return (this.channels[channel] as Cea608Channel).getHandler();
  1078. }
  1079.  
  1080. setHandler(channel: number, newHandler: OutputFilter) {
  1081. (this.channels[channel] as Cea608Channel).setHandler(newHandler);
  1082. }
  1083.  
  1084. /**
  1085. * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
  1086. */
  1087. addData(time: number | null, byteList: number[]) {
  1088. let cmdFound: boolean;
  1089. let a: number;
  1090. let b: number;
  1091. let charsFound: number[] | boolean | null = false;
  1092.  
  1093. this.logger.time = time;
  1094.  
  1095. for (let i = 0; i < byteList.length; i += 2) {
  1096. a = byteList[i] & 0x7f;
  1097. b = byteList[i + 1] & 0x7f;
  1098. if (a === 0 && b === 0) {
  1099. continue;
  1100. } else {
  1101. this.logger.log(
  1102. VerboseLevel.DATA,
  1103. '[' +
  1104. numArrayToHexArray([byteList[i], byteList[i + 1]]) +
  1105. '] -> (' +
  1106. numArrayToHexArray([a, b]) +
  1107. ')'
  1108. );
  1109. }
  1110.  
  1111. cmdFound = this.parseCmd(a, b);
  1112.  
  1113. if (!cmdFound) {
  1114. cmdFound = this.parseMidrow(a, b);
  1115. }
  1116.  
  1117. if (!cmdFound) {
  1118. cmdFound = this.parsePAC(a, b);
  1119. }
  1120.  
  1121. if (!cmdFound) {
  1122. cmdFound = this.parseBackgroundAttributes(a, b);
  1123. }
  1124.  
  1125. if (!cmdFound) {
  1126. charsFound = this.parseChars(a, b);
  1127. if (charsFound) {
  1128. const currChNr = this.currentChannel;
  1129. if (currChNr && currChNr > 0) {
  1130. const channel = this.channels[currChNr] as Cea608Channel;
  1131. channel.insertChars(charsFound);
  1132. } else {
  1133. this.logger.log(
  1134. VerboseLevel.WARNING,
  1135. 'No channel found yet. TEXT-MODE?'
  1136. );
  1137. }
  1138. }
  1139. }
  1140. if (!cmdFound && !charsFound) {
  1141. this.logger.log(
  1142. VerboseLevel.WARNING,
  1143. "Couldn't parse cleaned data " +
  1144. numArrayToHexArray([a, b]) +
  1145. ' orig: ' +
  1146. numArrayToHexArray([byteList[i], byteList[i + 1]])
  1147. );
  1148. }
  1149. }
  1150. }
  1151.  
  1152. /**
  1153. * Parse Command.
  1154. * @returns {Boolean} Tells if a command was found
  1155. */
  1156. parseCmd(a: number, b: number) {
  1157. const { cmdHistory } = this;
  1158. const cond1 =
  1159. (a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) &&
  1160. b >= 0x20 &&
  1161. b <= 0x2f;
  1162. const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23;
  1163. if (!(cond1 || cond2)) {
  1164. return false;
  1165. }
  1166.  
  1167. if (hasCmdRepeated(a, b, cmdHistory)) {
  1168. setLastCmd(null, null, cmdHistory);
  1169. this.logger.log(
  1170. VerboseLevel.DEBUG,
  1171. 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped'
  1172. );
  1173. return true;
  1174. }
  1175.  
  1176. const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2;
  1177. const channel = this.channels[chNr] as Cea608Channel;
  1178.  
  1179. if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) {
  1180. if (b === 0x20) {
  1181. channel.ccRCL();
  1182. } else if (b === 0x21) {
  1183. channel.ccBS();
  1184. } else if (b === 0x22) {
  1185. channel.ccAOF();
  1186. } else if (b === 0x23) {
  1187. channel.ccAON();
  1188. } else if (b === 0x24) {
  1189. channel.ccDER();
  1190. } else if (b === 0x25) {
  1191. channel.ccRU(2);
  1192. } else if (b === 0x26) {
  1193. channel.ccRU(3);
  1194. } else if (b === 0x27) {
  1195. channel.ccRU(4);
  1196. } else if (b === 0x28) {
  1197. channel.ccFON();
  1198. } else if (b === 0x29) {
  1199. channel.ccRDC();
  1200. } else if (b === 0x2a) {
  1201. channel.ccTR();
  1202. } else if (b === 0x2b) {
  1203. channel.ccRTD();
  1204. } else if (b === 0x2c) {
  1205. channel.ccEDM();
  1206. } else if (b === 0x2d) {
  1207. channel.ccCR();
  1208. } else if (b === 0x2e) {
  1209. channel.ccENM();
  1210. } else if (b === 0x2f) {
  1211. channel.ccEOC();
  1212. }
  1213. } else {
  1214. // a == 0x17 || a == 0x1F
  1215. channel.ccTO(b - 0x20);
  1216. }
  1217. setLastCmd(a, b, cmdHistory);
  1218. this.currentChannel = chNr;
  1219. return true;
  1220. }
  1221.  
  1222. /**
  1223. * Parse midrow styling command
  1224. * @returns {Boolean}
  1225. */
  1226. parseMidrow(a: number, b: number) {
  1227. let chNr: number = 0;
  1228.  
  1229. if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) {
  1230. if (a === 0x11) {
  1231. chNr = 1;
  1232. } else {
  1233. chNr = 2;
  1234. }
  1235.  
  1236. if (chNr !== this.currentChannel) {
  1237. this.logger.log(
  1238. VerboseLevel.ERROR,
  1239. 'Mismatch channel in midrow parsing'
  1240. );
  1241. return false;
  1242. }
  1243. const channel = this.channels[chNr];
  1244. if (!channel) {
  1245. return false;
  1246. }
  1247. channel.ccMIDROW(b);
  1248. this.logger.log(
  1249. VerboseLevel.DEBUG,
  1250. 'MIDROW (' + numArrayToHexArray([a, b]) + ')'
  1251. );
  1252. return true;
  1253. }
  1254. return false;
  1255. }
  1256.  
  1257. /**
  1258. * Parse Preable Access Codes (Table 53).
  1259. * @returns {Boolean} Tells if PAC found
  1260. */
  1261. parsePAC(a: number, b: number): boolean {
  1262. let row: number;
  1263. const cmdHistory = this.cmdHistory;
  1264.  
  1265. const case1 =
  1266. ((a >= 0x11 && a <= 0x17) || (a >= 0x19 && a <= 0x1f)) &&
  1267. b >= 0x40 &&
  1268. b <= 0x7f;
  1269. const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f;
  1270. if (!(case1 || case2)) {
  1271. return false;
  1272. }
  1273.  
  1274. if (hasCmdRepeated(a, b, cmdHistory)) {
  1275. setLastCmd(null, null, cmdHistory);
  1276. return true; // Repeated commands are dropped (once)
  1277. }
  1278.  
  1279. const chNr: Channels = a <= 0x17 ? 1 : 2;
  1280.  
  1281. if (b >= 0x40 && b <= 0x5f) {
  1282. row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a];
  1283. } else {
  1284. // 0x60 <= b <= 0x7F
  1285. row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a];
  1286. }
  1287. const channel = this.channels[chNr];
  1288. if (!channel) {
  1289. return false;
  1290. }
  1291. channel.setPAC(this.interpretPAC(row, b));
  1292. setLastCmd(a, b, cmdHistory);
  1293. this.currentChannel = chNr;
  1294. return true;
  1295. }
  1296.  
  1297. /**
  1298. * Interpret the second byte of the pac, and return the information.
  1299. * @returns {Object} pacData with style parameters.
  1300. */
  1301. interpretPAC(row: number, byte: number): PACData {
  1302. let pacIndex;
  1303. const pacData: PACData = {
  1304. color: null,
  1305. italics: false,
  1306. indent: null,
  1307. underline: false,
  1308. row: row,
  1309. };
  1310.  
  1311. if (byte > 0x5f) {
  1312. pacIndex = byte - 0x60;
  1313. } else {
  1314. pacIndex = byte - 0x40;
  1315. }
  1316.  
  1317. pacData.underline = (pacIndex & 1) === 1;
  1318. if (pacIndex <= 0xd) {
  1319. pacData.color = [
  1320. 'white',
  1321. 'green',
  1322. 'blue',
  1323. 'cyan',
  1324. 'red',
  1325. 'yellow',
  1326. 'magenta',
  1327. 'white',
  1328. ][Math.floor(pacIndex / 2)];
  1329. } else if (pacIndex <= 0xf) {
  1330. pacData.italics = true;
  1331. pacData.color = 'white';
  1332. } else {
  1333. pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4;
  1334. }
  1335. return pacData; // Note that row has zero offset. The spec uses 1.
  1336. }
  1337.  
  1338. /**
  1339. * Parse characters.
  1340. * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
  1341. */
  1342. parseChars(a: number, b: number): number[] | null {
  1343. let channelNr: Channels;
  1344. let charCodes: number[] | null = null;
  1345. let charCode1: number | null = null;
  1346.  
  1347. if (a >= 0x19) {
  1348. channelNr = 2;
  1349. charCode1 = a - 8;
  1350. } else {
  1351. channelNr = 1;
  1352. charCode1 = a;
  1353. }
  1354. if (charCode1 >= 0x11 && charCode1 <= 0x13) {
  1355. // Special character
  1356. let oneCode;
  1357. if (charCode1 === 0x11) {
  1358. oneCode = b + 0x50;
  1359. } else if (charCode1 === 0x12) {
  1360. oneCode = b + 0x70;
  1361. } else {
  1362. oneCode = b + 0x90;
  1363. }
  1364.  
  1365. this.logger.log(
  1366. VerboseLevel.INFO,
  1367. "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr
  1368. );
  1369. charCodes = [oneCode];
  1370. } else if (a >= 0x20 && a <= 0x7f) {
  1371. charCodes = b === 0 ? [a] : [a, b];
  1372. }
  1373. if (charCodes) {
  1374. const hexCodes = numArrayToHexArray(charCodes);
  1375. this.logger.log(
  1376. VerboseLevel.DEBUG,
  1377. 'Char codes = ' + hexCodes.join(',')
  1378. );
  1379. setLastCmd(a, b, this.cmdHistory);
  1380. }
  1381. return charCodes;
  1382. }
  1383.  
  1384. /**
  1385. * Parse extended background attributes as well as new foreground color black.
  1386. * @returns {Boolean} Tells if background attributes are found
  1387. */
  1388. parseBackgroundAttributes(a: number, b: number): boolean {
  1389. const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f;
  1390. const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f;
  1391. if (!(case1 || case2)) {
  1392. return false;
  1393. }
  1394. let index: number;
  1395. const bkgData: Partial<PenStyles> = {};
  1396. if (a === 0x10 || a === 0x18) {
  1397. index = Math.floor((b - 0x20) / 2);
  1398. bkgData.background = backgroundColors[index];
  1399. if (b % 2 === 1) {
  1400. bkgData.background = bkgData.background + '_semi';
  1401. }
  1402. } else if (b === 0x2d) {
  1403. bkgData.background = 'transparent';
  1404. } else {
  1405. bkgData.foreground = 'black';
  1406. if (b === 0x2f) {
  1407. bkgData.underline = true;
  1408. }
  1409. }
  1410. const chNr: Channels = a <= 0x17 ? 1 : 2;
  1411. const channel: Cea608Channel = this.channels[chNr] as Cea608Channel;
  1412. channel.setBkgData(bkgData);
  1413. setLastCmd(a, b, this.cmdHistory);
  1414. return true;
  1415. }
  1416.  
  1417. /**
  1418. * Reset state of parser and its channels.
  1419. */
  1420. reset() {
  1421. for (let i = 0; i < Object.keys(this.channels).length; i++) {
  1422. const channel = this.channels[i];
  1423. if (channel) {
  1424. channel.reset();
  1425. }
  1426. }
  1427. this.cmdHistory = createCmdHistory();
  1428. }
  1429.  
  1430. /**
  1431. * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
  1432. */
  1433. cueSplitAtTime(t: number) {
  1434. for (let i = 0; i < this.channels.length; i++) {
  1435. const channel = this.channels[i];
  1436. if (channel) {
  1437. channel.cueSplitAtTime(t);
  1438. }
  1439. }
  1440. }
  1441. }
  1442.  
  1443. function setLastCmd(
  1444. a: number | null,
  1445. b: number | null,
  1446. cmdHistory: CmdHistory
  1447. ) {
  1448. cmdHistory.a = a;
  1449. cmdHistory.b = b;
  1450. }
  1451.  
  1452. function hasCmdRepeated(a: number, b: number, cmdHistory: CmdHistory) {
  1453. return cmdHistory.a === a && cmdHistory.b === b;
  1454. }
  1455.  
  1456. function createCmdHistory(): CmdHistory {
  1457. return {
  1458. a: null,
  1459. b: null,
  1460. };
  1461. }
  1462.  
  1463. export default Cea608Parser;