@danything/arib 0.0.0-stage → 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/dist/bml.js ADDED
@@ -0,0 +1,510 @@
1
+ /**
2
+ * データ放送 (ARIB STD-B24 の BML) をデコードする。**1局に絞った TS を食わせると、
3
+ * 組み立て終わったファイルが出てくる。**
4
+ *
5
+ * TS → PAT → PMT → データ符号化方式記述子 (0xFD) の付いた ES
6
+ * → DII/DDB (カルーセル) → モジュール → ファイル
7
+ *
8
+ * ## なぜ自前なのか
9
+ *
10
+ * **運び方は衛星のロゴと同じ DSM-CC** で、denpa はそれを既に持っている
11
+ * ([dsmcc.ts](dsmcc.ts) / [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts))。
12
+ * [web-bml](https://github.com/otya128/web-bml) の `decode_ts.ts` は使わない — 要るのは
13
+ * その一部だけで、残り (EIT/SDT/NIT・字幕の PES) は自前で読んでおり
14
+ * ([eit.ts](eit.ts) / [psi.ts](psi.ts) / [b24caption.ts](b24caption.ts))、`@chinachu/aribts` も抱えずに済む。
15
+ *
16
+ * **出す形式は web-bml の頃のまま** ([bml-protocol.ts](bml-protocol.ts))。録画の隣の
17
+ * `.bml.jsonl` がこの形式で残っているので、変えると前に録ったものが読めなくなる。
18
+ * モジュールの中身 (multipart) は [multipart.ts](multipart.ts) がデコードする。
19
+ */
20
+ import { Buffer } from "node:buffer";
21
+ import { inflateSync } from "node:zlib";
22
+ import { u16, u32 } from "./bytes.js";
23
+ import { dataEventIdOf, MAX_MODULE_SIZE, ModuleBuilder, parseDdb, parseDii, TABLE_DII, TABLE_STREAM_DESCRIPTOR, } from "./dsmcc.js";
24
+ import { parseTimeTable, PID_TIME } from "./eit.js";
25
+ import { parseMediaType, readEntity } from "./multipart.js";
26
+ import { descriptors, packetPid, PacketStream, pmtStreams, PmtTracker, SectionAssembler, } from "./psi.js";
27
+ const DESC_STREAM_IDENTIFIER = 0x52;
28
+ /** データ符号化方式記述子 (STD-B10 第2部 6.2.20) */
29
+ const DESC_DATA_COMPONENT = 0xfd;
30
+ /**
31
+ * BML が載っている符号化方式 (STD-B10 第2部 付録J 表J-1)。
32
+ * 0x0C/0x0D が地上波、0x07 が BS、0x0B が CS
33
+ */
34
+ const BXML_COMPONENT_IDS = new Set([0x07, 0x0b, 0x0c, 0x0d]);
35
+ /** zlib。Compression Type 記述子で運用されるのはこれだけ */
36
+ const COMPRESSION_ZLIB = 0;
37
+ /**
38
+ * データ符号化方式記述子の後半を読む。
39
+ *
40
+ * 地上波は TR-B14 第二分冊 2.1.4 表2-3、BS/CS は TR-B15 第一分冊 5.1.5 表5-4。
41
+ *
42
+ * **`data_component_id` は16ビット** (aribts は8ビットで読んでいる)。
43
+ */
44
+ export function parseBxmlInfo(data) {
45
+ let at = 0;
46
+ // 00 のみ運用 (データカルーセル伝送方式およびイベントメッセージ伝送方式)
47
+ const transmissionFormat = (data[at] >> 6) & 0b11;
48
+ // component_tag=0x40 のとき必ず1。startup.xml が最初に起動される
49
+ const entryPointFlag = ((data[at] >> 5) & 1) === 1;
50
+ const info = { transmissionFormat, entryPointFlag };
51
+ if (entryPointFlag) {
52
+ const autoStartFlag = ((data[at] >> 4) & 1) === 1;
53
+ /*
54
+ * 画面の大きさ。運用されるのは 0011 (960x540)、0100 (640x480 16:9)、
55
+ * 0101 (640x480 4:3) の3つ
56
+ */
57
+ const documentResolution = data[at] & 0x0f;
58
+ at++;
59
+ const useXML = ((data[at] >> 7) & 1) === 1;
60
+ const defaultVersionFlag = ((data[at] >> 6) & 1) === 1;
61
+ // BS/CS では 0 のとき単独視聴不可。地上波は常に1
62
+ const independentFlag = ((data[at] >> 5) & 1) === 1;
63
+ const styleForTVFlag = ((data[at] >> 4) & 1) === 1;
64
+ at++;
65
+ // 既定は BS の 1.0。地上波は3、CS は2が入ってくる
66
+ info.entryPointInfo = {
67
+ autoStartFlag,
68
+ documentResolution,
69
+ useXML,
70
+ defaultVersionFlag,
71
+ independentFlag,
72
+ styleForTVFlag,
73
+ bmlMajorVersion: 1,
74
+ bmlMinorVersion: 0,
75
+ };
76
+ if (!defaultVersionFlag) {
77
+ info.entryPointInfo.bmlMajorVersion = u16(data, at);
78
+ info.entryPointInfo.bmlMinorVersion = u16(data, at + 2);
79
+ at += 4;
80
+ // 運用されない
81
+ if (useXML) {
82
+ info.entryPointInfo.bxmlMajorVersion = u16(data, at);
83
+ info.entryPointInfo.bxmlMinorVersion = u16(data, at + 2);
84
+ at += 4;
85
+ }
86
+ }
87
+ }
88
+ else {
89
+ at++;
90
+ }
91
+ if (transmissionFormat === 0) {
92
+ // additional_arib_carousel_info (STD-B24 第三分冊 第三編 C.1)
93
+ const dataEventId = (data[at] >> 4) & 0x0f;
94
+ const eventSectionFlag = ((data[at] >> 3) & 1) === 1;
95
+ at++;
96
+ const ondemandRetrievalFlag = ((data[at] >> 7) & 1) === 1;
97
+ const fileStorableFlag = ((data[at] >> 6) & 1) === 1;
98
+ const startPriority = (data[at] >> 5) & 1;
99
+ info.additionalAribCarouselInfo = {
100
+ dataEventId,
101
+ eventSectionFlag,
102
+ ondemandRetrievalFlag,
103
+ fileStorableFlag,
104
+ startPriority,
105
+ };
106
+ }
107
+ return info;
108
+ }
109
+ /**
110
+ * PMT から ES を並べる。**部品タグ (component_tag) を持たない ES は数に入れない** —
111
+ * データ放送は「何番の部品か」でしか物を指せないので、番号の無いものは指しようがない。
112
+ */
113
+ function parsePmt(section) {
114
+ const components = [];
115
+ for (const [streamType, pid, info] of pmtStreams(section)) {
116
+ let componentId;
117
+ let dataComponentId;
118
+ let bxmlInfo;
119
+ for (const [tag, descriptor] of descriptors(info)) {
120
+ if (tag === DESC_STREAM_IDENTIFIER && descriptor.length >= 1)
121
+ componentId = descriptor[0];
122
+ else if (tag === DESC_DATA_COMPONENT && descriptor.length >= 2) {
123
+ dataComponentId = u16(descriptor, 0);
124
+ if (BXML_COMPONENT_IDS.has(dataComponentId))
125
+ bxmlInfo = parseBxmlInfo(descriptor.subarray(2));
126
+ }
127
+ }
128
+ if (componentId === undefined)
129
+ continue;
130
+ components.push({
131
+ componentId,
132
+ pid,
133
+ streamType,
134
+ ...(dataComponentId === undefined ? {} : { dataComponentId }),
135
+ ...(bxmlInfo === undefined ? {} : { bxmlInfo }),
136
+ });
137
+ }
138
+ return components;
139
+ }
140
+ /** `at` の下位1ビットと続く4バイトで 33 ビット (STC・NPT) */
141
+ const u33 = (data, at) => (data[at] & 1) * 0x100000000 + u32(data, at + 1);
142
+ /** ストリーム記述子 (table_id 0x3D)。NPT と イベントメッセージが載っている */
143
+ function parseEsEvents(section) {
144
+ const events = [];
145
+ const length = u16(section, 1) & 0x0fff;
146
+ const body = section.subarray(8, Math.min(3 + length - 4, section.length));
147
+ for (const [tag, descriptor] of descriptors(body)) {
148
+ if (tag === 0x17 && descriptor.length >= 18) {
149
+ // NPT 参照記述子。放送の時計と番組内の時刻を結び付ける
150
+ events.push({
151
+ type: "nptReference",
152
+ postDiscontinuityIndicator: (descriptor[0] & 0x80) !== 0,
153
+ dsmContentId: descriptor[0] & 0x7f,
154
+ STCReference: u33(descriptor, 1),
155
+ NPTReference: u33(descriptor, 9),
156
+ scaleNumerator: u16(descriptor, 14),
157
+ scaleDenominator: u16(descriptor, 16),
158
+ });
159
+ }
160
+ else if (tag === 0x40 && descriptor.length >= 11) {
161
+ // 汎用イベントメッセージ記述子。運用されるのは time_mode 0 と 2 だけ
162
+ const timeMode = descriptor[2];
163
+ const message = {
164
+ eventMessageGroupId: (u16(descriptor, 0) >> 4) & 0x0fff,
165
+ eventMessageType: descriptor[8],
166
+ eventMessageId: u16(descriptor, 9),
167
+ privateDataByte: [...descriptor.subarray(11)],
168
+ };
169
+ if (timeMode === 0)
170
+ events.push({ type: "immediateEvent", timeMode: 0, ...message });
171
+ else if (timeMode === 2)
172
+ events.push({
173
+ type: "nptEvent",
174
+ timeMode: 2,
175
+ eventMessageNPT: u33(descriptor, 3),
176
+ ...message,
177
+ });
178
+ }
179
+ }
180
+ return events;
181
+ }
182
+ /**
183
+ * **DII に載ったモジュールが来なければ諦める。** 最後にブロックが増えてから、DII が
184
+ * この回数繰り返し、しかもカルーセルが一覧の大きさのこの倍数ぶん流れたら、もう来ないとみなす。
185
+ * 両方を見るのは、DII の繰り返しはカルーセルの一周より短いことがあり (回数だけだと大きい
186
+ * カルーセルの途中で諦める)、よく使うモジュールは一周に何度も流れる (バイト数だけだと一周を
187
+ * 短く見積もる) ため
188
+ */
189
+ const GIVE_UP_ROUNDS = 4;
190
+ /**
191
+ * 1つの `BmlDecoder` で同時に組み立てるモジュールの大きさの合計の上限 (64MiB)。
192
+ * 1つずつは `MAX_MODULE_SIZE` に収まっても、たくさん並べた DII や ES でメモリを使い切らせられる。
193
+ * 実際のカルーセルは全部で数 MB に収まる
194
+ */
195
+ const MAX_CAROUSEL_SIZE = 64 * 1024 * 1024;
196
+ /** `Component.done` に置く印。モジュールのバージョンとカルーセルの回 (dataEventId) の組 */
197
+ function doneMark(component, version) {
198
+ return `${version}:${component.dataEventId}`;
199
+ }
200
+ /**
201
+ * 1局に絞った TS を食わせると、データ放送の通知が出てくる。
202
+ *
203
+ * **1つの選局に1つ。** 局を変えたら作り直す — カルーセルは局ごとに別物で、
204
+ * 組み立て中のものを持ち越すと混ざる。
205
+ */
206
+ export class BmlDecoder {
207
+ send;
208
+ packets = new PacketStream();
209
+ /** **1局に絞ったあとを食わせる前提。** 入っているサービスは1つなので、最初の局の PMT を追う */
210
+ pmt = new PmtTracker();
211
+ // TDT は CRC を持たない。DSM-CC と同じ理由で 'syntax' で受ける
212
+ time = new SectionAssembler(PID_TIME, "syntax");
213
+ /** カルーセルが流れている ES。PID ごと */
214
+ components = new Map();
215
+ /** 最後に配った PMT。**変わったときだけ配る** */
216
+ sent = null;
217
+ constructor(send) {
218
+ this.send = send;
219
+ }
220
+ /** 1局に絞った TS を食わせる */
221
+ feed(chunk) {
222
+ for (const packet of this.packets.feed(chunk))
223
+ this.feedPacket(packet);
224
+ }
225
+ /**
226
+ * 切り分け済みのパケットを1つ。ライブは同じ TS を時計や Hybridcast とも
227
+ * 分け合うので、切り分けは1回で済ませて配る (denpa の `server/live.ts` の `tap`)。
228
+ *
229
+ * **PID で行き先を引く。** 組み立て役を全部に回していた頃は、データ放送と
230
+ * 関係ない映像のパケットのたびにカルーセルの数だけ呼んでいた。
231
+ *
232
+ * `packet` は呼び手の塊の上の窓 (`psi.ts` の `PacketStream`)。**返ったあとまで
233
+ * 持つなら写すこと** — 塊の入れ物は使い回される
234
+ */
235
+ feedPacket(packet) {
236
+ const pid = packetPid(packet);
237
+ // 同じ PID を2役が名乗ることもあるので、else でつながない
238
+ for (const section of this.pmt.feed(packet, pid))
239
+ this.onPmt(section);
240
+ if (pid === PID_TIME)
241
+ for (const section of this.time.feed(packet))
242
+ this.onTime(section);
243
+ const component = this.components.get(pid);
244
+ if (component !== undefined) {
245
+ for (const section of component.sections.feed(packet))
246
+ this.onCarousel(component, section);
247
+ }
248
+ }
249
+ onPmt(section) {
250
+ const components = parsePmt(section);
251
+ if (components.length === 0)
252
+ return;
253
+ /*
254
+ * 放送は同じ表を何度も送る。**変わったときだけ配る** — 受け側は
255
+ * PMT が来るたびに画面を組み直すので、そのまま通すと出っ放しになる
256
+ */
257
+ const shape = JSON.stringify(components);
258
+ if (this.sent !== shape) {
259
+ this.sent = shape;
260
+ this.send({ type: "pmt", components });
261
+ }
262
+ for (const component of components) {
263
+ // カルーセルが流れているのは、BML の符号化方式が付いた ES だけ
264
+ if (component.bxmlInfo === undefined)
265
+ continue;
266
+ if (this.components.has(component.pid))
267
+ continue;
268
+ this.components.set(component.pid, {
269
+ componentId: component.componentId,
270
+ // DSM-CC は section_syntax_indicator が立たないことがある (psi.ts)
271
+ sections: new SectionAssembler(component.pid, "syntax"),
272
+ transactionId: null,
273
+ dataEventId: -1,
274
+ building: new Map(),
275
+ done: new Map(),
276
+ dii: null,
277
+ size: 0,
278
+ abandoned: new Set(),
279
+ });
280
+ }
281
+ }
282
+ /** TDT / TOT。読み方は [eit.ts](eit.ts) にまとめてある */
283
+ onTime(section) {
284
+ const at = parseTimeTable(section);
285
+ if (at !== null)
286
+ this.send({ type: "currentTime", timeUnixMillis: at });
287
+ }
288
+ onCarousel(component, section) {
289
+ if (section[0] === TABLE_STREAM_DESCRIPTOR) {
290
+ const extension = u16(section, 3);
291
+ this.send({
292
+ type: "esEventUpdated",
293
+ componentId: component.componentId,
294
+ events: parseEsEvents(section),
295
+ dataEventId: extension >> 12,
296
+ });
297
+ return;
298
+ }
299
+ if (section[0] === TABLE_DII)
300
+ this.onDii(component, section);
301
+ else
302
+ this.onDdb(component, section);
303
+ }
304
+ onDii(component, section) {
305
+ const dii = parseDii(section);
306
+ if (dii === null)
307
+ return;
308
+ // 同じカルーセルが回ってきただけ。組み立て中のものを捨てない
309
+ if (component.transactionId === dii.transactionId) {
310
+ for (const held of component.building.values())
311
+ held.quietDiis++;
312
+ this.giveUp(component);
313
+ return;
314
+ }
315
+ component.transactionId = dii.transactionId;
316
+ /*
317
+ * **番組が変わったら覚えていたものを捨てる。** dataEventId はカルーセルの
318
+ * 「回」で、ここが変わると前の番組のファイルは使えない
319
+ */
320
+ const dataEventId = dataEventIdOf(dii.downloadId);
321
+ if (component.dataEventId !== dataEventId) {
322
+ component.dataEventId = dataEventId;
323
+ component.done.clear();
324
+ }
325
+ component.building.clear();
326
+ component.abandoned.clear();
327
+ component.dii = dii;
328
+ component.size = 0;
329
+ // ほかの ES で組み立て中のものと合わせて `MAX_CAROUSEL_SIZE` に収める
330
+ let budget = MAX_CAROUSEL_SIZE;
331
+ for (const other of this.components.values())
332
+ if (other !== component)
333
+ budget -= other.size;
334
+ for (const module of dii.modules) {
335
+ /*
336
+ * **大きすぎるモジュールは組み立てない。** 来ないものとして一覧から外す
337
+ * (受け側が待ち続けないように)
338
+ */
339
+ if (module.moduleSize > MAX_MODULE_SIZE ||
340
+ component.size + module.moduleSize > budget) {
341
+ component.abandoned.add(module.moduleId);
342
+ continue;
343
+ }
344
+ component.size += module.moduleSize;
345
+ component.building.set(module.moduleId, {
346
+ module,
347
+ builder: new ModuleBuilder(module.moduleVersion, module.moduleSize, dii.blockSize),
348
+ quietDiis: 0,
349
+ quietBytes: 0,
350
+ });
351
+ }
352
+ this.sendList(component);
353
+ // 大きさ 0 のものは DDB が来ない。待たせずに配る (来ないまま諦めると、受け側は無いものと見なす)
354
+ for (const [moduleId, held] of component.building) {
355
+ if (held.builder.complete)
356
+ this.finish(component, moduleId, held);
357
+ }
358
+ }
359
+ /** 今の DII の一覧を配る。**諦めたモジュールは載せない** */
360
+ sendList(component) {
361
+ const dii = component.dii;
362
+ if (dii === null)
363
+ return;
364
+ this.send({
365
+ type: "moduleListUpdated",
366
+ componentId: component.componentId,
367
+ modules: dii.modules
368
+ .filter((module) => !component.abandoned.has(module.moduleId))
369
+ .map((module) => ({
370
+ id: module.moduleId,
371
+ version: module.moduleVersion,
372
+ size: module.moduleSize,
373
+ })),
374
+ dataEventId: component.dataEventId,
375
+ ...(dii.returnToEntry === null
376
+ ? {}
377
+ : { returnToEntryFlag: dii.returnToEntry }),
378
+ });
379
+ }
380
+ /**
381
+ * **来ないモジュールを諦める** (`GIVE_UP_ROUNDS`)。待っている受け側 (`Resources` の `wait`) は、
382
+ * DII に無いモジュールを「もう来ない」と決める (`gone`) ので、諦めたものを除いた一覧を配り直して
383
+ * 待ちを終わらせる。前の DII で配り終えたバージョンのままのもの (受け側が持っている) は一覧から外さず、
384
+ * 組み立てるのをやめるだけ。次に DII のバージョンが変われば、また待つ
385
+ */
386
+ giveUp(component) {
387
+ const limit = component.size * GIVE_UP_ROUNDS;
388
+ let changed = false;
389
+ for (const [moduleId, held] of component.building) {
390
+ if (held.quietDiis < GIVE_UP_ROUNDS || held.quietBytes < limit)
391
+ continue;
392
+ component.building.delete(moduleId);
393
+ if (component.done.get(moduleId) ===
394
+ doneMark(component, held.module.moduleVersion))
395
+ continue;
396
+ component.abandoned.add(moduleId);
397
+ changed = true;
398
+ }
399
+ if (changed)
400
+ this.sendList(component);
401
+ }
402
+ onDdb(component, section) {
403
+ const ddb = parseDdb(section);
404
+ if (ddb === null)
405
+ return;
406
+ for (const waiting of component.building.values())
407
+ waiting.quietBytes += ddb.block.length;
408
+ const held = component.building.get(ddb.moduleId);
409
+ // バージョンが食い違うブロックを混ぜると壊れたファイルができる
410
+ if (held === undefined || held.builder.moduleVersion !== ddb.moduleVersion)
411
+ return;
412
+ const before = held.builder.blocks;
413
+ const complete = held.builder.add(ddb.blockNumber, ddb.block);
414
+ if (held.builder.blocks > before) {
415
+ held.quietDiis = 0;
416
+ held.quietBytes = 0;
417
+ }
418
+ if (complete)
419
+ this.finish(component, ddb.moduleId, held);
420
+ }
421
+ /** 揃ったモジュールを配る */
422
+ finish(component, moduleId, held) {
423
+ // 次の DII が来るまで組み立て直さない
424
+ component.building.delete(moduleId);
425
+ /*
426
+ * **同じものを配り直さない。** カルーセルは回り続けるので、バージョンが上がって
427
+ * いなくても同じモジュールが何度も揃う。そのたびに配ると、受け側は
428
+ * 中身の変わらない画面を組み直し続ける
429
+ */
430
+ const version = held.builder.moduleVersion;
431
+ const mark = doneMark(component, version);
432
+ if (component.done.get(moduleId) === mark)
433
+ return;
434
+ component.done.set(moduleId, mark);
435
+ // 大きさ 0 は伸ばしようが無い。ファイルの無いモジュールとして配り、待っている受け側を終わらせる
436
+ const empty = held.module.moduleSize === 0;
437
+ const data = empty
438
+ ? held.builder.data
439
+ : inflate(held.builder.data, held.module.compression);
440
+ if (data === null)
441
+ return;
442
+ const files = moduleFiles(data, held.module.contentType);
443
+ if (files.length === 0 && !empty)
444
+ return;
445
+ this.send({
446
+ type: "moduleDownloaded",
447
+ componentId: component.componentId,
448
+ moduleId,
449
+ files,
450
+ version,
451
+ dataEventId: component.dataEventId,
452
+ });
453
+ }
454
+ }
455
+ /**
456
+ * 揃ったモジュールをファイルに分ける。
457
+ *
458
+ * **多くは `multipart/mixed`** で、1つのモジュールに BML と画像がまとめて
459
+ * 入っている。Type 記述子が無いときも中身は multipart なので、同じ扱いにする。
460
+ */
461
+ export function moduleFiles(data, contentType) {
462
+ const declared = contentType === null ? null : parseMediaType(contentType);
463
+ if (declared !== null &&
464
+ !(declared.type === "multipart" && declared.subtype === "mixed")) {
465
+ return [
466
+ {
467
+ contentLocation: null,
468
+ contentType: declared,
469
+ dataBase64: base64(data),
470
+ },
471
+ ];
472
+ }
473
+ const entity = readEntity(data);
474
+ if (entity?.parts == null)
475
+ return [];
476
+ const files = [];
477
+ for (const part of entity.parts) {
478
+ const location = part.headers.find((header) => header.name === "content-location");
479
+ const type = part.headers.find((header) => header.name === "content-type");
480
+ // どちらも必ず入っている。欠けているものは置き場所も読み方も分からない
481
+ if (location === undefined || type === undefined)
482
+ continue;
483
+ const mediaType = parseMediaType(type.value);
484
+ if (mediaType === null)
485
+ continue;
486
+ files.push({
487
+ contentLocation: location.value,
488
+ contentType: mediaType,
489
+ dataBase64: base64(part.body),
490
+ });
491
+ }
492
+ return files;
493
+ }
494
+ function base64(data) {
495
+ return Buffer.from(data).toString("base64");
496
+ }
497
+ /** zlib で縮めてあれば伸ばす。伸ばせなければ諦めて null */
498
+ export function inflate(data, compression) {
499
+ if (compression === null)
500
+ return data;
501
+ if (compression.type !== COMPRESSION_ZLIB)
502
+ return null;
503
+ try {
504
+ // 伸ばした大きさにも上限を置く。小さな DDB から何 GB にも伸びるものを作れる
505
+ return inflateSync(data, { maxOutputLength: MAX_MODULE_SIZE });
506
+ }
507
+ catch {
508
+ return null;
509
+ }
510
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * バイト列の細かい道具。**中身の意味は持たない。** 繋ぐ・読むのはここで済ませる (別名で書き直さない)
3
+ */
4
+ /** バイト列を新しい1本に繋ぐ (いつも写す。`Blob` などへ渡せる `ArrayBuffer` の上に作る) */
5
+ export declare function concatBytes(parts: readonly Uint8Array[]): Uint8Array<ArrayBuffer>;
6
+ /**
7
+ * バイト列を1本に繋ぐ。
8
+ *
9
+ * **1本しかないときは写しません。** 分割された記述子を繋ぎ直すところ
10
+ * ([eit.ts](eit.ts)) では、繋ぐ必要のないほうが多数です
11
+ */
12
+ export declare function joinBytes(parts: readonly Uint8Array[]): Uint8Array;
13
+ /** 大きい桁が先の 16 ビット */
14
+ export declare function u16(data: Uint8Array, at: number): number;
15
+ /** 大きい桁が先の 24 ビット */
16
+ export declare function u24(data: Uint8Array, at: number): number;
17
+ /** 大きい桁が先の 32 ビット (符号なし) */
18
+ export declare function u32(data: Uint8Array, at: number): number;
19
+ /** `length` バイトをそのまま1字ずつの文字列に (MP4 の箱の種別・PNG の塊の名前・言語の符号) */
20
+ export declare function ascii(data: Uint8Array, at: number, length: number): string;
package/dist/bytes.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * バイト列の細かい道具。**中身の意味は持たない。** 繋ぐ・読むのはここで済ませる (別名で書き直さない)
3
+ */
4
+ /** バイト列を新しい1本に繋ぐ (いつも写す。`Blob` などへ渡せる `ArrayBuffer` の上に作る) */
5
+ export function concatBytes(parts) {
6
+ let total = 0;
7
+ for (const part of parts)
8
+ total += part.length;
9
+ const out = new Uint8Array(total);
10
+ let at = 0;
11
+ for (const part of parts) {
12
+ out.set(part, at);
13
+ at += part.length;
14
+ }
15
+ return out;
16
+ }
17
+ /**
18
+ * バイト列を1本に繋ぐ。
19
+ *
20
+ * **1本しかないときは写しません。** 分割された記述子を繋ぎ直すところ
21
+ * ([eit.ts](eit.ts)) では、繋ぐ必要のないほうが多数です
22
+ */
23
+ export function joinBytes(parts) {
24
+ return parts.length === 1 ? parts[0] : concatBytes(parts);
25
+ }
26
+ /** 大きい桁が先の 16 ビット */
27
+ export function u16(data, at) {
28
+ return (data[at] << 8) | data[at + 1];
29
+ }
30
+ /** 大きい桁が先の 24 ビット */
31
+ export function u24(data, at) {
32
+ return (data[at] << 16) | u16(data, at + 1);
33
+ }
34
+ /** 大きい桁が先の 32 ビット (符号なし) */
35
+ export function u32(data, at) {
36
+ return (((data[at] << 24) |
37
+ (data[at + 1] << 16) |
38
+ (data[at + 2] << 8) |
39
+ data[at + 3]) >>>
40
+ 0);
41
+ }
42
+ /** `length` バイトをそのまま1字ずつの文字列に (MP4 の箱の種別・PNG の塊の名前・言語の符号) */
43
+ export function ascii(data, at, length) {
44
+ return String.fromCharCode(...data.subarray(at, at + length));
45
+ }