@danything/arib 0.0.0-stage → 0.1.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 +82 -0
- package/README.md +65 -2
- package/dist/ait.d.ts +97 -0
- package/dist/ait.js +205 -0
- package/dist/aribtext-gaiji.d.ts +25 -0
- package/dist/aribtext-gaiji.js +69 -0
- package/dist/aribtext.d.ts +31 -0
- package/dist/aribtext.js +314 -0
- package/dist/b24-tables.d.ts +50 -0
- package/dist/b24-tables.js +410 -0
- package/dist/b24caption.d.ts +98 -0
- package/dist/b24caption.js +987 -0
- package/dist/bml-protocol.d.ts +143 -0
- package/dist/bml-protocol.js +12 -0
- package/dist/bml.d.ts +87 -0
- package/dist/bml.js +489 -0
- package/dist/bytes.d.ts +20 -0
- package/dist/bytes.js +45 -0
- package/dist/caption-text.d.ts +122 -0
- package/dist/caption-text.js +39 -0
- package/dist/drcs-smooth.d.ts +52 -0
- package/dist/drcs-smooth.js +163 -0
- package/dist/dsmcc.d.ts +92 -0
- package/dist/dsmcc.js +191 -0
- package/dist/eit.d.ts +316 -0
- package/dist/eit.js +708 -0
- package/dist/language.d.ts +7 -0
- package/dist/language.js +20 -0
- package/dist/multipart.d.ts +43 -0
- package/dist/multipart.js +194 -0
- package/dist/pes.d.ts +99 -0
- package/dist/pes.js +231 -0
- package/dist/psi.d.ts +169 -0
- package/dist/psi.js +497 -0
- package/dist/service-filter.d.ts +95 -0
- package/dist/service-filter.js +248 -0
- package/dist/synth-av.d.ts +51 -0
- package/dist/synth-av.js +298 -0
- package/dist/synth-caption.d.ts +38 -0
- package/dist/synth-caption.js +115 -0
- package/dist/synth.d.ts +183 -0
- package/dist/synth.js +508 -0
- package/package.json +30 -4
package/dist/synth-av.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **本物の MPEG-2 映像と AAC 音声を、ffmpeg 無しで組み立てる** (試験用)。
|
|
3
|
+
*
|
|
4
|
+
* ライブを変換せずに送る経路 (docs/stream.md#放送そのままmpeg-2を送る) は、ブラウザが MPEG-2 をデコードして映像を出す
|
|
5
|
+
* ところまでが仕事で、そこを試すには**本当にデコードできる放送**が要る。偽エージェントの
|
|
6
|
+
* 放送は中身を 0xFF で埋めていて、デコードするものが無い。E2E のイメージには本物の ffmpeg も
|
|
7
|
+
* 入っていない (数十分かかる組み立てを避けている) ので、ここで小さく作る。
|
|
8
|
+
*
|
|
9
|
+
* - 映像 … 256x144 の**I フレームだけ**。マクロブロックごとに色 (DC) だけを置き、
|
|
10
|
+
* AC は持たない (EOB だけ)。コマごとに縞が流れるので、**映像が進んでいるか**を
|
|
11
|
+
* 画素で見分けられる。インタレの印も立て、輝度は上下のフィールドで別の時刻の映像にする (フィールド DCT)
|
|
12
|
+
* - 音声 … 48kHz ステレオの AAC-LC の**無音**を ADTS で。1コマ 7 バイト
|
|
13
|
+
*
|
|
14
|
+
* 規格は ISO/IEC 13818-2 (映像) と 13818-7 / 14496-3 (AAC)。表の出どころは
|
|
15
|
+
* 各関数の頭に書いてある。
|
|
16
|
+
*/
|
|
17
|
+
import { concatBytes } from "./bytes.js";
|
|
18
|
+
import { PACKET, SYNC } from "./psi.js";
|
|
19
|
+
import { putPcr } from "./synth.js";
|
|
20
|
+
/** 29.97 コマ/秒の1コマ (90kHz)。放送と同じ */
|
|
21
|
+
export const FRAME_TICKS = 3003;
|
|
22
|
+
/** AAC の1コマ (1024 標本 @48kHz を 90kHz で) */
|
|
23
|
+
export const AUDIO_TICKS = 1920;
|
|
24
|
+
/** 映像の大きさ。**16 の倍数** (マクロブロックで割り切る) */
|
|
25
|
+
const WIDTH = 256;
|
|
26
|
+
const HEIGHT = 144;
|
|
27
|
+
class Bits {
|
|
28
|
+
out = [];
|
|
29
|
+
acc = 0;
|
|
30
|
+
used = 0;
|
|
31
|
+
put(value, width) {
|
|
32
|
+
for (let i = width - 1; i >= 0; i--) {
|
|
33
|
+
this.acc = (this.acc << 1) | ((value >>> i) & 1);
|
|
34
|
+
if (++this.used === 8) {
|
|
35
|
+
this.out.push(this.acc);
|
|
36
|
+
this.acc = 0;
|
|
37
|
+
this.used = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 0 で詰めてバイトの切れ目に揃える (next_start_code / byte_alignment) */
|
|
42
|
+
align() {
|
|
43
|
+
if (this.used > 0)
|
|
44
|
+
this.put(0, 8 - this.used);
|
|
45
|
+
}
|
|
46
|
+
startCode(code) {
|
|
47
|
+
this.align();
|
|
48
|
+
this.out.push(0x00, 0x00, 0x01, code);
|
|
49
|
+
}
|
|
50
|
+
bytes() {
|
|
51
|
+
this.align();
|
|
52
|
+
return Uint8Array.from(this.out);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** dct_dc_size の符号 (表 B-12 輝度 / B-13 色差)。`[符号, 長さ]` */
|
|
56
|
+
const DC_LUMA = [
|
|
57
|
+
[0b100, 3],
|
|
58
|
+
[0b00, 2],
|
|
59
|
+
[0b01, 2],
|
|
60
|
+
[0b101, 3],
|
|
61
|
+
[0b110, 3],
|
|
62
|
+
[0b1110, 4],
|
|
63
|
+
[0b11110, 5],
|
|
64
|
+
[0b111110, 6],
|
|
65
|
+
[0b1111110, 7],
|
|
66
|
+
];
|
|
67
|
+
const DC_CHROMA = [
|
|
68
|
+
[0b00, 2],
|
|
69
|
+
[0b01, 2],
|
|
70
|
+
[0b10, 2],
|
|
71
|
+
[0b110, 3],
|
|
72
|
+
[0b1110, 4],
|
|
73
|
+
[0b11110, 5],
|
|
74
|
+
[0b111110, 6],
|
|
75
|
+
[0b1111110, 7],
|
|
76
|
+
[0b11111110, 8],
|
|
77
|
+
];
|
|
78
|
+
/** 1ブロック。**DC の差分だけ書いて、すぐ EOB** (表 B-14 の '10') */
|
|
79
|
+
function block(bits, diff, table) {
|
|
80
|
+
const size = diff === 0 ? 0 : Math.floor(Math.log2(Math.abs(diff))) + 1;
|
|
81
|
+
const [code, length] = table[size];
|
|
82
|
+
bits.put(code, length);
|
|
83
|
+
// 負の差分は「足して (2^size − 1)」を書く (7.2.1)
|
|
84
|
+
if (size > 0)
|
|
85
|
+
bits.put(diff > 0 ? diff : diff + (1 << size) - 1, size);
|
|
86
|
+
bits.put(0b10, 2);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* n コマ目の色 (Y, Cb, Cr)。**列と時刻で縞を流す** — 進んでいれば画素が変わる。
|
|
90
|
+
* 輝度は**フィールドごとの時刻で**流す (下のフィールドは上の 1/59.94 秒後の位置)。フィールドごとに
|
|
91
|
+
* 出せているか (bob) と、出す間合いを画素で見分けられる。1フィールドで 16 進み、30 コマ (60 フィールド)
|
|
92
|
+
* でちょうど一周するので、偽の放送が 30 コマを繰り返しても段差が出ない
|
|
93
|
+
*/
|
|
94
|
+
function colorAt(col, row, n, field) {
|
|
95
|
+
const y = 32 + ((col * 12 + (2 * n + field) * 16) % 192);
|
|
96
|
+
return [y, 96 + ((row * 16) % 64), 160 - ((col * 4) % 64)];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* I フレームを1枚。**毎枚、系列ヘッダから書く** — どこから受け取っても頭からデコードできる
|
|
100
|
+
* (放送は GOP ごとに書く。試験ではその手間を省く)。
|
|
101
|
+
*
|
|
102
|
+
* @param n 何枚目か。色と time_code に使う
|
|
103
|
+
* @param color マクロブロック (16x16) ごとの色。既定は流れる縞 (スクリーンショットを撮るときはカラーバー。`tests/fake/docs.ts`)
|
|
104
|
+
*/
|
|
105
|
+
export function mpeg2Frame(n, color = colorAt) {
|
|
106
|
+
const bits = new Bits();
|
|
107
|
+
const mbWidth = WIDTH / 16;
|
|
108
|
+
const mbHeight = HEIGHT / 16;
|
|
109
|
+
// sequence_header (6.2.2.1)。16:9、29.97、intra/non-intra の量子化表は既定
|
|
110
|
+
bits.startCode(0xb3);
|
|
111
|
+
bits.put(WIDTH, 12);
|
|
112
|
+
bits.put(HEIGHT, 12);
|
|
113
|
+
bits.put(3, 4); // aspect_ratio_information = 16:9
|
|
114
|
+
bits.put(4, 4); // frame_rate_code = 30000/1001
|
|
115
|
+
bits.put(0x3ffff, 18); // bit_rate_value
|
|
116
|
+
bits.put(1, 1); // marker
|
|
117
|
+
bits.put(112, 10); // vbv_buffer_size_value
|
|
118
|
+
bits.put(0, 3); // constrained / load_intra / load_non_intra
|
|
119
|
+
// sequence_extension (6.2.2.3)。Main@Main、4:2:0、インタレ系列、B 無し
|
|
120
|
+
bits.startCode(0xb5);
|
|
121
|
+
bits.put(1, 4);
|
|
122
|
+
bits.put(0x48, 8);
|
|
123
|
+
bits.put(0, 1); // progressive_sequence = 0 (インタレ)
|
|
124
|
+
bits.put(1, 2); // chroma_format = 4:2:0
|
|
125
|
+
bits.put(0, 4); // horizontal / vertical_size_extension
|
|
126
|
+
bits.put(0, 12); // bit_rate_extension
|
|
127
|
+
bits.put(1, 1); // marker
|
|
128
|
+
bits.put(0, 8); // vbv_buffer_size_extension
|
|
129
|
+
bits.put(1, 1); // low_delay
|
|
130
|
+
bits.put(0, 7); // frame_rate_extension_n / _d
|
|
131
|
+
// group_of_pictures_header (6.2.2.6)。閉じた GOP
|
|
132
|
+
bits.startCode(0xb8);
|
|
133
|
+
const seconds = Math.floor(n / 30);
|
|
134
|
+
bits.put(0, 1); // drop_frame_flag
|
|
135
|
+
bits.put(Math.floor(seconds / 3600) % 24, 5);
|
|
136
|
+
bits.put(Math.floor(seconds / 60) % 60, 6);
|
|
137
|
+
bits.put(1, 1); // marker
|
|
138
|
+
bits.put(seconds % 60, 6);
|
|
139
|
+
bits.put(n % 30, 6);
|
|
140
|
+
bits.put(1, 1); // closed_gop
|
|
141
|
+
bits.put(0, 1); // broken_link
|
|
142
|
+
// picture_header (6.2.3)
|
|
143
|
+
bits.startCode(0x00);
|
|
144
|
+
bits.put(0, 10); // temporal_reference (GOP ごとに1枚なので 0)
|
|
145
|
+
bits.put(1, 3); // picture_coding_type = I
|
|
146
|
+
bits.put(0xffff, 16); // vbv_delay
|
|
147
|
+
bits.put(0, 1); // extra_bit_picture
|
|
148
|
+
// picture_coding_extension (6.2.3.1)。フレーム構造・上が先・インタレのコマ
|
|
149
|
+
bits.startCode(0xb5);
|
|
150
|
+
bits.put(8, 4);
|
|
151
|
+
bits.put(0xffff, 16); // f_code (I では使わない = 15)
|
|
152
|
+
bits.put(0, 2); // intra_dc_precision = 8bit
|
|
153
|
+
bits.put(3, 2); // picture_structure = frame
|
|
154
|
+
bits.put(1, 1); // top_field_first
|
|
155
|
+
bits.put(0, 1); // frame_pred_frame_dct = 0 (マクロブロックごとに dct_type を書く。フィールドで分ける)
|
|
156
|
+
bits.put(0, 1); // concealment_motion_vectors
|
|
157
|
+
bits.put(0, 1); // q_scale_type
|
|
158
|
+
bits.put(0, 1); // intra_vlc_format
|
|
159
|
+
bits.put(0, 1); // alternate_scan
|
|
160
|
+
bits.put(0, 1); // repeat_first_field
|
|
161
|
+
bits.put(0, 1); // chroma_420_type
|
|
162
|
+
bits.put(0, 1); // progressive_frame = 0
|
|
163
|
+
bits.put(0, 1); // composite_display_flag
|
|
164
|
+
for (let row = 0; row < mbHeight; row++) {
|
|
165
|
+
// 1行を1スライスに。予測は行の頭で 128 に戻る (7.2.1)
|
|
166
|
+
bits.startCode(row + 1);
|
|
167
|
+
bits.put(8, 5); // quantiser_scale_code (DC だけなので効かない)
|
|
168
|
+
bits.put(0, 1); // extra_bit_slice
|
|
169
|
+
const predictor = [128, 128, 128];
|
|
170
|
+
for (let col = 0; col < mbWidth; col++) {
|
|
171
|
+
bits.put(1, 1); // macroblock_address_increment = 1
|
|
172
|
+
bits.put(1, 1); // macroblock_type = intra
|
|
173
|
+
bits.put(1, 1); // dct_type = 1 (フィールド DCT: 輝度の上2ブロックが上のフィールド、下2つが下)
|
|
174
|
+
const [top, cb, cr] = color(col, row, n, 0);
|
|
175
|
+
const [bottom] = color(col, row, n, 1);
|
|
176
|
+
for (let i = 0; i < 4; i++) {
|
|
177
|
+
const y = i < 2 ? top : bottom;
|
|
178
|
+
block(bits, y - predictor[0], DC_LUMA);
|
|
179
|
+
predictor[0] = y;
|
|
180
|
+
}
|
|
181
|
+
block(bits, cb - predictor[1], DC_CHROMA);
|
|
182
|
+
predictor[1] = cb;
|
|
183
|
+
block(bits, cr - predictor[2], DC_CHROMA);
|
|
184
|
+
predictor[2] = cr;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return bits.bytes();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* AAC-LC の無音を1コマ、ADTS の頭付きで。48kHz・ステレオ (channel_configuration 2)。
|
|
191
|
+
*
|
|
192
|
+
* 中身は CPE が1つ: 両耳とも `max_sfb = 0` (係数を1つも持たない) で、あとは END。
|
|
193
|
+
* 55 ビットなので7バイトに収まる (14496-3 の raw_data_block)
|
|
194
|
+
*/
|
|
195
|
+
export function silentAac() {
|
|
196
|
+
const body = new Bits();
|
|
197
|
+
body.put(1, 3); // id_syn_ele = CPE
|
|
198
|
+
body.put(0, 4); // element_instance_tag
|
|
199
|
+
body.put(0, 1); // common_window
|
|
200
|
+
for (let ch = 0; ch < 2; ch++) {
|
|
201
|
+
body.put(100, 8); // global_gain
|
|
202
|
+
body.put(0, 1); // ics_reserved_bit
|
|
203
|
+
body.put(0, 2); // window_sequence = ONLY_LONG
|
|
204
|
+
body.put(0, 1); // window_shape
|
|
205
|
+
body.put(0, 6); // max_sfb = 0
|
|
206
|
+
body.put(0, 1); // predictor_data_present
|
|
207
|
+
body.put(0, 3); // pulse / tns / gain_control
|
|
208
|
+
}
|
|
209
|
+
body.put(7, 3); // END
|
|
210
|
+
const payload = body.bytes();
|
|
211
|
+
const head = new Bits();
|
|
212
|
+
const length = 7 + payload.length;
|
|
213
|
+
head.put(0xfff, 12);
|
|
214
|
+
head.put(0, 1); // ID = MPEG-4
|
|
215
|
+
head.put(0, 2); // layer
|
|
216
|
+
head.put(1, 1); // protection_absent
|
|
217
|
+
head.put(1, 2); // profile = LC
|
|
218
|
+
head.put(3, 4); // sampling_frequency_index = 48kHz
|
|
219
|
+
head.put(0, 1);
|
|
220
|
+
head.put(2, 3); // channel_configuration
|
|
221
|
+
head.put(0, 4);
|
|
222
|
+
head.put(length, 13);
|
|
223
|
+
head.put(0x7ff, 11); // buffer fullness = VBR
|
|
224
|
+
head.put(0, 2); // number_of_raw_data_blocks - 1
|
|
225
|
+
return Uint8Array.from([...head.bytes(), ...payload]);
|
|
226
|
+
}
|
|
227
|
+
/** PTS/DTS の5バイト (2.4.3.7)。頭の4ビットは '0010' (PTS だけ) */
|
|
228
|
+
function stamp(pts) {
|
|
229
|
+
const value = pts % 2 ** 33;
|
|
230
|
+
const high = Math.floor(value / 2 ** 30) & 0x07;
|
|
231
|
+
const mid = Math.floor(value / 2 ** 15) & 0x7fff;
|
|
232
|
+
const low = value & 0x7fff;
|
|
233
|
+
return [
|
|
234
|
+
0x21 | (high << 1),
|
|
235
|
+
mid >> 7,
|
|
236
|
+
((mid & 0x7f) << 1) | 1,
|
|
237
|
+
low >> 7,
|
|
238
|
+
((low & 0x7f) << 1) | 1,
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* PES にする。映像は長さ 0 (「次の頭まで」。放送もそう書く)、音声は長さを書く
|
|
243
|
+
*
|
|
244
|
+
* @param streamId 0xE0 (映像) / 0xC0 (音声)
|
|
245
|
+
*/
|
|
246
|
+
export function pes(streamId, pts, data) {
|
|
247
|
+
const header = [0x80, 0x80, 5, ...stamp(pts)];
|
|
248
|
+
const length = streamId >= 0xe0 ? 0 : header.length + data.length;
|
|
249
|
+
return Uint8Array.from([
|
|
250
|
+
0x00,
|
|
251
|
+
0x00,
|
|
252
|
+
0x01,
|
|
253
|
+
streamId,
|
|
254
|
+
length >> 8,
|
|
255
|
+
length & 0xff,
|
|
256
|
+
...header,
|
|
257
|
+
...data,
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* PES を TS パケットに割る。**最後のパケットは適応フィールドで詰める** (中身を 0xFF で
|
|
262
|
+
* 埋めると PES の続きに見える)。頭のパケットに PCR を載せることもできる
|
|
263
|
+
*
|
|
264
|
+
* @param counter 連続性カウンタの続き。返り値の `counter` を次に渡す
|
|
265
|
+
*/
|
|
266
|
+
export function packetizePes(pid, data, counter, pcr = null) {
|
|
267
|
+
const out = [];
|
|
268
|
+
let at = 0;
|
|
269
|
+
while (at < data.length) {
|
|
270
|
+
const packet = new Uint8Array(PACKET).fill(0xff);
|
|
271
|
+
const withPcr = at === 0 && pcr !== null;
|
|
272
|
+
const rest = data.length - at;
|
|
273
|
+
/*
|
|
274
|
+
* 適応フィールドの長さ。PCR を載せるなら旗1バイト + 6バイト。**中身が
|
|
275
|
+
* 足りないときもここを伸ばして詰める** (中身を 0xFF で埋めると PES の続きに見える)
|
|
276
|
+
*/
|
|
277
|
+
const least = withPcr ? 7 : 0;
|
|
278
|
+
const adapted = withPcr || rest < PACKET - 4;
|
|
279
|
+
const take = adapted ? Math.min(rest, PACKET - 5 - least) : PACKET - 4;
|
|
280
|
+
packet[0] = SYNC;
|
|
281
|
+
packet[1] = (at === 0 ? 0x40 : 0) | ((pid >> 8) & 0x1f);
|
|
282
|
+
packet[2] = pid & 0xff;
|
|
283
|
+
packet[3] = (adapted ? 0x30 : 0x10) | (counter & 0x0f);
|
|
284
|
+
if (adapted) {
|
|
285
|
+
const length = PACKET - 5 - take;
|
|
286
|
+
packet[4] = length;
|
|
287
|
+
if (length > 0)
|
|
288
|
+
packet[5] = withPcr ? 0x10 : 0x00;
|
|
289
|
+
if (withPcr)
|
|
290
|
+
putPcr(packet, pcr);
|
|
291
|
+
}
|
|
292
|
+
packet.set(data.subarray(at, at + take), PACKET - take);
|
|
293
|
+
at += take;
|
|
294
|
+
counter = (counter + 1) & 0x0f;
|
|
295
|
+
out.push(packet);
|
|
296
|
+
}
|
|
297
|
+
return { packets: concatBytes(out), counter };
|
|
298
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **放送の字幕 (ARIB STD-B24) を組み立てる** (試験用)。読む側は [b24caption.ts](../../../packages/arib/src/b24caption.ts)。
|
|
3
|
+
*
|
|
4
|
+
* 出来るのは字幕の PES の中身 (データ識別 0x80 から) で、ffmpeg が `-c:s copy` で
|
|
5
|
+
* Matroska に入れたコマと同じ形式。TS に載せるなら `synth-av.ts` の `pes(0xbd, …)` で包む。
|
|
6
|
+
*/
|
|
7
|
+
import { encodeAribText } from "./synth.js";
|
|
8
|
+
export { encodeAribText };
|
|
9
|
+
/** データユニット。0x20 本文・0x30 1バイト DRCS・0x31 2バイト DRCS */
|
|
10
|
+
export declare function unit(parameter: number, data: number[]): number[];
|
|
11
|
+
/**
|
|
12
|
+
* 字幕管理データ。言語は `languages` (ISO 639 の3文字。既定は日本語1つ)・書式は `format` (SWF+1。既定 8 = 960x540 横書き)。
|
|
13
|
+
* 言語の順に第1言語・第2言語…になる (言語タグ 0, 1, …)
|
|
14
|
+
*/
|
|
15
|
+
export declare function management(options?: {
|
|
16
|
+
format?: number;
|
|
17
|
+
formats?: number[];
|
|
18
|
+
group?: number;
|
|
19
|
+
units?: number[];
|
|
20
|
+
languages?: string[];
|
|
21
|
+
}): Uint8Array;
|
|
22
|
+
/** 字幕文。`units` はデータユニットを並べたもの。本文だけなら `text(...)` を使う。`language` は第何言語か (1〜8) */
|
|
23
|
+
export declare function statement(units: number[], language?: number): Uint8Array;
|
|
24
|
+
/** 本文1つだけの字幕文 (`language` は第何言語か) */
|
|
25
|
+
export declare function text(body: number[], language?: number): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* 1バイト DRCS (DRCS-1 の面) の定義。`rows` は '#' を点にした図 (白黒)
|
|
28
|
+
*
|
|
29
|
+
* @param code 符号 (0x21〜0x7E)。呼び出すときは `ESC ( 0x20 0x41` で G0 に載せて、この符号を書く
|
|
30
|
+
*/
|
|
31
|
+
export declare function drcsUnit(code: number, rows: string[]): number[];
|
|
32
|
+
/**
|
|
33
|
+
* ffmpeg が `-c:s copy -f matroska` で書くのと同じ骨組みの Matroska (トラック1本・コマごとに塊1つ)。
|
|
34
|
+
* 中身は字幕の PES の中身 (`text(...)` など) をそのまま入れる
|
|
35
|
+
*
|
|
36
|
+
* @param frames [時刻 (ミリ秒。65535 まで), 中身] の並び
|
|
37
|
+
*/
|
|
38
|
+
export declare function captionMkv(frames: [ms: number, data: Uint8Array][], codec?: string): Uint8Array;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **放送の字幕 (ARIB STD-B24) を組み立てる** (試験用)。読む側は [b24caption.ts](../../../packages/arib/src/b24caption.ts)。
|
|
3
|
+
*
|
|
4
|
+
* 出来るのは字幕の PES の中身 (データ識別 0x80 から) で、ffmpeg が `-c:s copy` で
|
|
5
|
+
* Matroska に入れたコマと同じ形式。TS に載せるなら `synth-av.ts` の `pes(0xbd, …)` で包む。
|
|
6
|
+
*/
|
|
7
|
+
import { encodeAribText } from "./synth.js";
|
|
8
|
+
export { encodeAribText };
|
|
9
|
+
/** CRC-16 (ITU-T。データグループの末尾) */
|
|
10
|
+
function crc16(data) {
|
|
11
|
+
let crc = 0;
|
|
12
|
+
for (const byte of data) {
|
|
13
|
+
crc ^= byte << 8;
|
|
14
|
+
for (let i = 0; i < 8; i++)
|
|
15
|
+
crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
|
|
16
|
+
}
|
|
17
|
+
return crc;
|
|
18
|
+
}
|
|
19
|
+
const u24 = (n) => [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
20
|
+
/** データユニット。0x20 本文・0x30 1バイト DRCS・0x31 2バイト DRCS */
|
|
21
|
+
export function unit(parameter, data) {
|
|
22
|
+
return [0x1f, parameter, ...u24(data.length), ...data];
|
|
23
|
+
}
|
|
24
|
+
/** データグループで包んで PES の中身にする。`id` は 0 (管理) / 1〜8 (第何言語か)、`group` は 0 (A) / 1 (B) */
|
|
25
|
+
function dataGroup(id, group, body) {
|
|
26
|
+
const groupId = (group << 5) | id;
|
|
27
|
+
const head = [
|
|
28
|
+
groupId << 2,
|
|
29
|
+
0x00,
|
|
30
|
+
0x00,
|
|
31
|
+
(body.length >> 8) & 0xff,
|
|
32
|
+
body.length & 0xff,
|
|
33
|
+
...body,
|
|
34
|
+
];
|
|
35
|
+
const crc = crc16(head);
|
|
36
|
+
// データ識別 0x80 / 私的ストリーム識別 0xFF / PES データパケットヘッダ長 0 (上位は予約で 1)
|
|
37
|
+
return Uint8Array.from([0x80, 0xff, 0xf0, ...head, crc >> 8, crc & 0xff]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 字幕管理データ。言語は `languages` (ISO 639 の3文字。既定は日本語1つ)・書式は `format` (SWF+1。既定 8 = 960x540 横書き)。
|
|
41
|
+
* 言語の順に第1言語・第2言語…になる (言語タグ 0, 1, …)
|
|
42
|
+
*/
|
|
43
|
+
export function management(options = {}) {
|
|
44
|
+
const { format = 8, formats = [], group = 0, units = [], languages = ["jpn"], } = options;
|
|
45
|
+
const body = [
|
|
46
|
+
0x3f, // TMD 00 (自由)
|
|
47
|
+
languages.length, // 言語の数
|
|
48
|
+
...languages.flatMap((code, tag) => [
|
|
49
|
+
(tag << 5) | 0x10, // 言語タグ / DMF 0000 (予約ビット 1)
|
|
50
|
+
...[...code].map((c) => c.charCodeAt(0)),
|
|
51
|
+
((formats[tag] ?? format) << 4) | 0x00, // 書式 (言語ごと。`formats` で替えられる) / TCS 0 (8単位符号) / ロールアップ無し
|
|
52
|
+
]),
|
|
53
|
+
...u24(units.length),
|
|
54
|
+
...units,
|
|
55
|
+
];
|
|
56
|
+
return dataGroup(0, group, body);
|
|
57
|
+
}
|
|
58
|
+
/** 字幕文。`units` はデータユニットを並べたもの。本文だけなら `text(...)` を使う。`language` は第何言語か (1〜8) */
|
|
59
|
+
export function statement(units, language = 1) {
|
|
60
|
+
return dataGroup(language, 0, [0x3f, ...u24(units.length), ...units]);
|
|
61
|
+
}
|
|
62
|
+
/** 本文1つだけの字幕文 (`language` は第何言語か) */
|
|
63
|
+
export function text(body, language = 1) {
|
|
64
|
+
return statement(unit(0x20, body), language);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 1バイト DRCS (DRCS-1 の面) の定義。`rows` は '#' を点にした図 (白黒)
|
|
68
|
+
*
|
|
69
|
+
* @param code 符号 (0x21〜0x7E)。呼び出すときは `ESC ( 0x20 0x41` で G0 に載せて、この符号を書く
|
|
70
|
+
*/
|
|
71
|
+
export function drcsUnit(code, rows) {
|
|
72
|
+
const height = rows.length;
|
|
73
|
+
const width = rows[0]?.length ?? 0;
|
|
74
|
+
const bits = [];
|
|
75
|
+
for (const row of rows)
|
|
76
|
+
for (const cell of row)
|
|
77
|
+
bits.push(cell === "#" ? 1 : 0);
|
|
78
|
+
const bytes = [];
|
|
79
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
80
|
+
let byte = 0;
|
|
81
|
+
for (let b = 0; b < 8; b++)
|
|
82
|
+
byte = (byte << 1) | (bits[i + b] ?? 0);
|
|
83
|
+
bytes.push(byte);
|
|
84
|
+
}
|
|
85
|
+
return unit(0x30, [
|
|
86
|
+
1, // 符号の数
|
|
87
|
+
0x41, // DRCS-1 の面 (上位) と
|
|
88
|
+
code, // 符号
|
|
89
|
+
1, // 字体の数
|
|
90
|
+
0x00, // 字体番号 0 / 方式 0000 (2階調)
|
|
91
|
+
0, // 階調 - 2
|
|
92
|
+
width,
|
|
93
|
+
height,
|
|
94
|
+
...bytes,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
/** EBML の要素1つ。大きさは8バイトで書く (読む側はどの長さでも読める) */
|
|
98
|
+
function element(id, body) {
|
|
99
|
+
const size = body.length;
|
|
100
|
+
return [...id, 0x01, 0, 0, 0, (size >>> 24) & 0xff, ...u24(size), ...body];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* ffmpeg が `-c:s copy -f matroska` で書くのと同じ骨組みの Matroska (トラック1本・コマごとに塊1つ)。
|
|
104
|
+
* 中身は字幕の PES の中身 (`text(...)` など) をそのまま入れる
|
|
105
|
+
*
|
|
106
|
+
* @param frames [時刻 (ミリ秒。65535 まで), 中身] の並び
|
|
107
|
+
*/
|
|
108
|
+
export function captionMkv(frames, codec = "S_ARIBSUB") {
|
|
109
|
+
const tracks = element([0x16, 0x54, 0xae, 0x6b], element([0xae], element([0x86], [...new TextEncoder().encode(codec)])));
|
|
110
|
+
const clusters = frames.flatMap(([ms, data]) => element([0x1f, 0x43, 0xb6, 0x75], [
|
|
111
|
+
...element([0xe7], [(ms >> 8) & 0xff, ms & 0xff]),
|
|
112
|
+
...element([0xa3], [0x81, 0x00, 0x00, 0x80, ...data]),
|
|
113
|
+
]));
|
|
114
|
+
return Uint8Array.from(element([0x18, 0x53, 0x80, 0x67], [...tracks, ...clusters]));
|
|
115
|
+
}
|
package/dist/synth.d.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS のセクションを組み立てる。
|
|
3
|
+
*
|
|
4
|
+
* 実チューナーが無いので、解析のテストにも偽エージェントにも「それらしい TS」が要る。
|
|
5
|
+
* 読む側 (psi.ts・eit.ts・dsmcc.ts・logo.ts・ait.ts など) と対になる書く側で、ここだけが仕様の写し。
|
|
6
|
+
*/
|
|
7
|
+
import { withCrc } from "./psi.js";
|
|
8
|
+
export { withCrc };
|
|
9
|
+
/** PCR の基準部 (90kHz) を適応フィールドに書く (旗は呼ぶ側)。拡張部 (27MHz の端数) は読む側が見ていないので 0 */
|
|
10
|
+
export declare function putPcr(packet: Uint8Array, pcr: number): void;
|
|
11
|
+
/**
|
|
12
|
+
* 文字列を ARIB STD-B24 の8単位符号にする。
|
|
13
|
+
*
|
|
14
|
+
* 初期状態 (G0=漢字) から始めて、ASCII のところだけ英数集合へ切り替える。
|
|
15
|
+
* 本物の放送はもっと凝った切り替え方をするが、**読む側が同じ経路を通れば足りる**。
|
|
16
|
+
* 文字の大きさは触らない (字幕は呼ぶ側が MSZ などを前に置く)。SI に書くなら `encodeSiText`
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeAribText(text: string, half?: boolean): number[];
|
|
19
|
+
export declare function sdtSection(transportStreamId: number, originalNetworkId: number, services: [number, number, string?][]): Uint8Array;
|
|
20
|
+
export interface SynthEvent {
|
|
21
|
+
eventId: number;
|
|
22
|
+
/** epoch ms */
|
|
23
|
+
startAt: number;
|
|
24
|
+
/** ms */
|
|
25
|
+
duration: number;
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
extended?: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* 記述子をそのまま足す。**実際の放送から取ったバイトを試験に使うため。**
|
|
31
|
+
* `extended` は文字列から組み立てるので、分割して送られてきたものや
|
|
32
|
+
* 文字集合の切り替えが混ざったものを再現できない
|
|
33
|
+
*/
|
|
34
|
+
rawDescriptors?: number[];
|
|
35
|
+
genres?: [number, number][];
|
|
36
|
+
/** 音声。並べたぶんだけ audio_component_descriptor を積む */
|
|
37
|
+
audios?: SynthAudio[];
|
|
38
|
+
/** component_descriptor の [stream_content, component_type] */
|
|
39
|
+
video?: [number, number];
|
|
40
|
+
isFree?: boolean;
|
|
41
|
+
/** 4 = 放送中。EIT[p/f] で「いま」を表すのに使う */
|
|
42
|
+
runningStatus?: number;
|
|
43
|
+
/**
|
|
44
|
+
* デジタルコピー制御記述子の digital_recording_control_data (2ビット)。
|
|
45
|
+
* 3 がコピー禁止、2 が1世代のみ。省くと記述子を載せない ({@link copyControlDescriptor})
|
|
46
|
+
*/
|
|
47
|
+
copyControl?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* デジタルコピー制御記述子 (0xC1)。EIT の番組と PMT に載る。
|
|
51
|
+
* 中身は1バイトだけ: 記録制御 (2) 最大ビットレートの有無 (1) 部品ごとの制御の有無 (1) 出力の制御 (4)
|
|
52
|
+
*/
|
|
53
|
+
export declare function copyControlDescriptor(control: number): number[];
|
|
54
|
+
/** 音声1本ぶん。**放送が付けた名前まで持てる** (解説放送の見分けに要る) */
|
|
55
|
+
interface SynthAudio {
|
|
56
|
+
/** audio_component_descriptor の component_type (3 = ステレオ) */
|
|
57
|
+
type: number;
|
|
58
|
+
/** ISO 639-2。省くと jpn */
|
|
59
|
+
lang?: string;
|
|
60
|
+
/** 「主音声ステレオ」「解説ステレオ」。省くと名乗らない */
|
|
61
|
+
text?: string;
|
|
62
|
+
/** 主音声か。省くと主音声 */
|
|
63
|
+
main?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface EitOptions {
|
|
66
|
+
tableId: number;
|
|
67
|
+
serviceId: number;
|
|
68
|
+
transportStreamId: number;
|
|
69
|
+
originalNetworkId: number;
|
|
70
|
+
version?: number;
|
|
71
|
+
sectionNumber?: number;
|
|
72
|
+
lastSectionNumber?: number;
|
|
73
|
+
segmentLastSectionNumber?: number;
|
|
74
|
+
lastTableId?: number;
|
|
75
|
+
events: SynthEvent[];
|
|
76
|
+
}
|
|
77
|
+
/** EIT。番組表そのもの。table_id で p/f (0x4E) と schedule (0x50〜) が分かれる */
|
|
78
|
+
export declare function eitSection(options: EitOptions): Uint8Array;
|
|
79
|
+
export declare function nitSection(networkId: number, remoteControlKeyId: number | null, streams: [number, number, [number, number][]][]): Uint8Array;
|
|
80
|
+
/** セクションを TS パケットに詰める。頭のパケットには pointer_field が付く */
|
|
81
|
+
export declare function packetize(pid: number, section: Uint8Array, counter?: number): Uint8Array;
|
|
82
|
+
/** 部品を1本のバイト列に繋ぐ。組んだパケットを流し込むときの糊 */
|
|
83
|
+
export declare function stream(...parts: Uint8Array[]): Uint8Array;
|
|
84
|
+
/** PCR を1つ運ぶだけのパケット */
|
|
85
|
+
export declare function pcrPacket(pid: number, seconds: number, counter?: number): Uint8Array;
|
|
86
|
+
/** 放送の実時刻 (TDT) を1パケットに。BML の `getCurrentDateTime` が見る */
|
|
87
|
+
export declare function tdtPacket(unixMs: number): Uint8Array;
|
|
88
|
+
/** PAT。サービスID → PMT の PID */
|
|
89
|
+
export declare function patSection(programs: [number, number][], transportStreamId?: number): Uint8Array;
|
|
90
|
+
/**
|
|
91
|
+
* PMT。ロゴのカルーセルが乗る ES を1つだけ持たせる。
|
|
92
|
+
* 目印は stream_identifier_descriptor (0x52) の component_tag
|
|
93
|
+
*/
|
|
94
|
+
export declare function pmtSection(serviceId: number, esPid: number, componentTag: number): Uint8Array;
|
|
95
|
+
/**
|
|
96
|
+
* ふつうの PMT。映像・音声のような ES を並べる。
|
|
97
|
+
*
|
|
98
|
+
* ロゴ用の `pmtSection` と違って中身を選べる。局を選り分ける
|
|
99
|
+
* [service-filter.ts](../../../packages/arib/src/service-filter.ts) を試すのに要る
|
|
100
|
+
*/
|
|
101
|
+
export declare function programMap(serviceId: number, pcrPid: number,
|
|
102
|
+
/** `[種別, PID]`。**3つ目は記述子**の並び (字幕の部品タグなど) */
|
|
103
|
+
streams: ([number, number] | [number, number, number[]])[], ecmPid?: number,
|
|
104
|
+
/** 番組ぜんたいに掛かる記述子 (program_info) に足すもの。コピー制御 ({@link copyControlDescriptor}) など */
|
|
105
|
+
extra?: number[]): Uint8Array;
|
|
106
|
+
/**
|
|
107
|
+
* データ符号化方式記述子 (0xFD)。**BML が載っている ES の目印。**
|
|
108
|
+
*
|
|
109
|
+
* 中身は「入口かどうか」と、カルーセルの回 (dataEventId)。
|
|
110
|
+
* 既定は地上波 (0x0C) の入口つき・960x540
|
|
111
|
+
*/
|
|
112
|
+
export declare function bxmlDescriptor(options?: {
|
|
113
|
+
dataComponentId?: number;
|
|
114
|
+
entryPoint?: boolean;
|
|
115
|
+
}): number[];
|
|
116
|
+
export interface DiiModuleSpec {
|
|
117
|
+
moduleId: number;
|
|
118
|
+
moduleSize: number;
|
|
119
|
+
moduleVersion: number;
|
|
120
|
+
/** 名前 (記述子 0x02)。**ロゴだけが使う** */
|
|
121
|
+
name?: string;
|
|
122
|
+
/** Type 記述子 (0x01)。**データ放送だけが使う** */
|
|
123
|
+
contentType?: string;
|
|
124
|
+
/** Compression Type 記述子 (0xC2)。0 が zlib */
|
|
125
|
+
compression?: {
|
|
126
|
+
type: number;
|
|
127
|
+
originalSize: number;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* DII。「このモジュールは何バイトで、名前は何か」を伝える。
|
|
132
|
+
*
|
|
133
|
+
* **複数のモジュールを載せられる。** 本物の BS はここに `LOGO-05` と
|
|
134
|
+
* `CS_LOGO-05` を並べて流していて (実機の `BS15_0` で確認)、1つしか
|
|
135
|
+
* 載せられない作りでは CS を取りこぼす経路を試せない。
|
|
136
|
+
*/
|
|
137
|
+
export declare function diiSection(downloadId: number, blockSize: number, modules: DiiModuleSpec | DiiModuleSpec[],
|
|
138
|
+
/** カルーセルの版。**同じ番号で送り直しても組み立て直されない** */
|
|
139
|
+
transactionId?: number): Uint8Array;
|
|
140
|
+
/** DDB。モジュールを割ったブロックを1つ運ぶ */
|
|
141
|
+
export declare function ddbSection(downloadId: number, moduleId: number, moduleVersion: number, blockNumber: number, block: Uint8Array): Uint8Array;
|
|
142
|
+
/** ロゴデータモジュール (ARIB STD-B21)。カルーセルで運ばれる中身 */
|
|
143
|
+
export declare function logoModule(logoType: number, logos: {
|
|
144
|
+
logoId: number;
|
|
145
|
+
services: [number, number][];
|
|
146
|
+
data: Uint8Array;
|
|
147
|
+
}[]): Uint8Array;
|
|
148
|
+
export declare const BML_TS: {
|
|
149
|
+
readonly service: 1024;
|
|
150
|
+
readonly pmtPid: 496;
|
|
151
|
+
readonly videoPid: 273;
|
|
152
|
+
readonly bmlPid: 2048;
|
|
153
|
+
readonly downloadId: 4026531841;
|
|
154
|
+
readonly componentTag: 64;
|
|
155
|
+
};
|
|
156
|
+
/** BML の ES が1本だけ載った PMT (`BML_TS` の割り振り) */
|
|
157
|
+
export declare function bmlPmt(): Uint8Array;
|
|
158
|
+
/** PAT と PMT。データ放送のストリームの頭 */
|
|
159
|
+
export declare function bmlHead(): Uint8Array[];
|
|
160
|
+
/**
|
|
161
|
+
* モジュール1つぶんの中身。**放送は multipart/mixed で複数のファイルを詰める。**
|
|
162
|
+
* 本物と同じく、頭に Content-Type、境界で区切って各ファイルに Content-Location
|
|
163
|
+
*/
|
|
164
|
+
export declare function multipartModule(files: [location: string, type: string, body: string][]): Uint8Array;
|
|
165
|
+
/** PMT の ES に付ける application_signalling_descriptor (0x6F)。**AIT の在り処の印** */
|
|
166
|
+
export declare function aitSignallingDescriptor(applicationType?: number, version?: number): number[];
|
|
167
|
+
export interface SynthHybridcastApp {
|
|
168
|
+
organisationId: number;
|
|
169
|
+
applicationId: number;
|
|
170
|
+
/** `CONTROL` の値。既定は autostart */
|
|
171
|
+
controlCode?: number;
|
|
172
|
+
name?: string;
|
|
173
|
+
/** URL の頭。**通信路で運ぶもの** (protocol_id = 3) として入れる */
|
|
174
|
+
base?: string;
|
|
175
|
+
/** その先のパス */
|
|
176
|
+
path?: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* AIT (table_id 0x74)。**Hybridcast のアプリのアドレス (URL) が載っている表。**
|
|
180
|
+
*
|
|
181
|
+
* 中身そのものは電波に乗っていないので、ここに入るのは URL と名前だけ
|
|
182
|
+
*/
|
|
183
|
+
export declare function aitSection(applications: SynthHybridcastApp[], applicationType?: number): Uint8Array;
|