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