@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.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- 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;
|