@zenvor/hls.js 1.0.0

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