@danything/arib 1.0.1 → 1.2.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  日本のデジタル放送 (ISDB-T / ISDB-S、ARIB 規格) の MPEG-TS を TypeScript で読むライブラリです。
4
4
  録画サーバー [denpa](https://github.com/danything/denpa) の中で使っているものを切り出しました。
5
5
 
6
- - **PSI** — パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT (`psi`)
6
+ - **PSI** — パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT (`psi`)。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む
7
7
  - **PES** — 映像・音声の PES と ADTS (`pes`)
8
8
  - **番組表** — EIT[p/f] と EIT[schedule]。ジャンル・音声・拡張形式イベント・シリーズ記述子まで (`eit`)
9
9
  - **ARIB の文字** — STD-B24 の8単位符号 (JIS X 0213 の1面・2面まで) と ARIB 外字 (`aribtext`、`aribtext-gaiji`)
@@ -498,4 +498,7 @@ export const DRCS_REPLACE = new Map([
498
498
  ['d9b5d861024ce112c12fa5f155bb1af3', 0x1f50a], // denpa: 音の出ているスピーカー 🔊 (36x36・4階調)。テレビ朝日 2026-10-17 のアニメの字幕で1本に5回
499
499
  ['d1e6371a34eaf49154ea6a4d0e9064ac', 0x1f50a], // denpa: 音の出ているスピーカー 🔊。TBS 2026-10 のアニメの字幕で1本に22回 (テレビ朝日のものと画像が違う)
500
500
  ['22db8ff6ac75b60b76a15a6e9c42a5db', 0x1f4fa], // denpa: テレビ 📺。上と同じ番組で2回
501
+ ['c42450812c184aee2ae01cc5e39ae957', 0x1f4fb], // denpa: ラジオ 📻 (72x72・16階調)。テレ東 2026-11-01 のアニメの字幕で1本に28回
502
+ ['06cb56043b9c4006bcfbe07cc831feaf', 0x1f50a], // denpa: 音の出ているスピーカー 🔊 (72x72・16階調)。上と同じ番組で4回 (テレビ朝日・TBS のものと画像が違う)
503
+ ['063c95566807d5e7b51ab706426bedf9', 0x1f4f1], // denpa: 携帯電話 📱 (72x72・16階調)。上と同じ番組で4回 (NHK総合のものと画像が違う)
501
504
  ]);
package/dist/psi.d.ts CHANGED
@@ -3,8 +3,10 @@
3
3
  * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
4
  * 読むのは NIT と SDT の2つ。
5
5
  *
6
- * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号
7
- * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別
6
+ * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
7
+ * (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
8
+ * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別。衛星はほかの TS のぶん
9
+ * (SDT[other]) も流れている (`SdtReader`)
8
10
  *
9
11
  * 局名も SDT から読む。ARIB の文字符号は独自だが、番組表を自分で集めるなら
10
12
  * どのみち要るので [aribtext.ts](aribtext.ts) に寄せてある。
@@ -15,10 +17,20 @@ export declare const SYNC = 71;
15
17
  export declare function packetPid(packet: Uint8Array): number;
16
18
  /** PAT。どの PID に PMT があるかを持つ。**入口はいつもここ** */
17
19
  export declare const PID_PAT = 0;
20
+ export declare const PID_NIT = 16;
18
21
  export declare const PID_SDT = 17;
19
22
  export declare const TABLE_PAT = 0;
20
23
  export declare const TABLE_PMT = 2;
24
+ export declare const TABLE_NIT_ACTUAL = 64;
25
+ export declare const TABLE_NIT_OTHER = 65;
21
26
  export declare const TABLE_SDT_ACTUAL = 66;
27
+ export declare const TABLE_SDT_OTHER = 70;
28
+ /**
29
+ * スキャンで残すサービス種別。Mirakurun のスキャンが通しているものと同じで、
30
+ * ラジオ (0x02) とデータ (0xc0) も入る。映像の無いものを番組表に並べないのは
31
+ * 取り込むほう (denpa の epg.ts の DIGITAL_TV)
32
+ */
33
+ export declare const SERVICE_TYPES: Set<number>;
22
34
  /**
23
35
  * セクション本体に CRC32 を付けて仕上げる。`section_length` も実長に直す。
24
36
  *
@@ -141,8 +153,134 @@ export declare function parsePat(section: Uint8Array): Map<number, number>;
141
153
  export declare function sdtServices(section: Uint8Array): Generator<[number, Uint8Array]>;
142
154
  /** SDT からサービスの一覧を読む。自分の TS のものだけ */
143
155
  export declare function parseSdt(section: Uint8Array): TransportInfo | null;
144
- /** NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む */
156
+ /**
157
+ * SDT[other] からサービスの一覧を読む。**ほかの TS のもの。**
158
+ *
159
+ * 衛星は、どの TS にもネットワークのほかの TS の局名を流している (番組表の EIT[other] と同じ考え)。
160
+ * 開いていない TS の局名をこれで知る。TS ごとに別のセクションで来るので、集めるのは `SdtReader`
161
+ */
162
+ export declare function parseSdtOther(section: Uint8Array): TransportInfo | null;
163
+ /** 衛星の分配の記述子 (0x43)。どの周波数で流れているか */
164
+ export interface SatelliteDelivery {
165
+ kind: "satellite";
166
+ /**
167
+ * 周波数 (kHz)。**放送の周波数そのもの** (BS-1 は 11727480)。
168
+ * チューナーに渡す中間周波数は、ここから LNB の局発の周波数 (BS・CS は 10678 MHz) を引いたもの
169
+ */
170
+ frequency: number;
171
+ /** 軌道位置 (0.1 度単位。東経 110 度は 1100) */
172
+ orbitalPosition: number;
173
+ /** 東経なら true */
174
+ east: boolean;
175
+ /** 偏波。0 水平・1 垂直・2 左旋・3 右旋 */
176
+ polarization: number;
177
+ /** 変調方式 (ARIB STD-B10 の値のまま) */
178
+ modulation: number;
179
+ /** シンボルレート (シンボル/秒) */
180
+ symbolRate: number;
181
+ /** 内符号 (ARIB STD-B10 の値のまま) */
182
+ fec: number;
183
+ }
184
+ /** 地上の分配の記述子 (0xFA)。地上波は自分の TS のぶんしか載らない */
185
+ export interface TerrestrialDelivery {
186
+ kind: "terrestrial";
187
+ /** 地域の識別 (ARIB STD-B10 の area_code) */
188
+ areaCode: number;
189
+ guardInterval: number;
190
+ transmissionMode: number;
191
+ /** 周波数 (Hz)。中継局ごとに並ぶことがある。放送は 1/7 MHz 単位で書く */
192
+ frequencies: number[];
193
+ }
194
+ export type Delivery = SatelliteDelivery | TerrestrialDelivery;
195
+ /** NIT の TS の 1 件。サービスの一覧に、どこで流れているかが付く */
196
+ export interface NetworkTransport extends TransportInfo {
197
+ delivery: Delivery | null;
198
+ }
199
+ /** NIT のセクション 1 つ。表は何本かのセクションに分かれて来るので、そろえるのは `NitReader` */
200
+ export interface NitSection {
201
+ /** NIT[other] (ほかのネットワークのもの) なら true */
202
+ other: boolean;
203
+ networkId: number;
204
+ version: number;
205
+ /** current_next_indicator。0 (これから使う表) のものは `NitReader` が読まない */
206
+ current: boolean;
207
+ sectionNumber: number;
208
+ lastSectionNumber: number;
209
+ remoteControlKeyId: number | null;
210
+ transportStreams: NetworkTransport[];
211
+ }
212
+ /** NIT のセクションを 1 つ読む。actual (0x40) と other (0x41) のどちらも */
213
+ export declare function parseNitSection(section: Uint8Array): NitSection | null;
214
+ /**
215
+ * NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む。**セクション 1 つぶん。**
216
+ * スキャンで「どのネットワークか」を知るにはこれで足りる。ネットワークの全部の TS が要るなら `NitReader`
217
+ */
145
218
  export declare function parseNit(section: Uint8Array): NetworkInfo | null;
219
+ /** そろった NIT。セクションをつないだもの */
220
+ export interface Network {
221
+ /** NIT[other] なら true */
222
+ other: boolean;
223
+ networkId: number;
224
+ version: number;
225
+ remoteControlKeyId: number | null;
226
+ transportStreams: NetworkTransport[];
227
+ }
228
+ /**
229
+ * NIT を**全部のセクション**そろえて読む。
230
+ *
231
+ * 衛星の NIT[actual] には、そのネットワークの全部の TS (TSID・周波数・サービスの一覧) が載っている。
232
+ * TS が多いと 1 セクション (1024 バイトまで) に収まらないので、ネットワークごとに (actual と other を
233
+ * 分けて) 集め、そろったものを返す。
234
+ *
235
+ * **version が変わっても、新しい表がそろうまでは前の表を返す。** 読む側が見るのは、いつも 1 つの
236
+ * version でそろった表。全部のセクションが 1 回りするまで (NIT は 10 秒に 1 回は流れるので、長くて 10 秒ほど) は何も返らない。
237
+ *
238
+ * 置き換えた 1 つ前の version のセクションが遅れて届いても読まない。読むと、そろったときに表が前に戻る。
239
+ *
240
+ * 1 つの中継を読む前提で作ってある。別の中継へ移るときは作り直す。`actual` は、最初にそろった自分のネットワークの表を返す
241
+ */
242
+ export declare class NitReader {
243
+ private readonly assembler;
244
+ private readonly packets;
245
+ private readonly pending;
246
+ private readonly done;
247
+ /** 置き換えた表の version (表ごと) */
248
+ private readonly replaced;
249
+ /** 任意の長さのバイト列を渡す。そろった表が増えたか変わったら true */
250
+ feed(chunk: Uint8Array): boolean;
251
+ private add;
252
+ /** 自分のネットワークの表 (NIT[actual])。まだそろっていなければ null */
253
+ get actual(): Network | null;
254
+ /** そろった表を全部。actual が先 */
255
+ networks(): Network[];
256
+ }
257
+ /** そろった SDT。TS 1 本ぶん */
258
+ export interface ServiceTable extends TransportInfo {
259
+ /** SDT[other] (ほかの TS のもの) なら true */
260
+ other: boolean;
261
+ version: number;
262
+ }
263
+ /**
264
+ * SDT を TS ごとに**全部のセクション**そろえて読む。自分の TS (actual) とほかの TS (other) の両方。
265
+ *
266
+ * 衛星はほかの TS の局名も流しているので、1 つの TS を開けばネットワークの局名がそろう
267
+ * (そろうかは放送しだい。来なかった TS は返らない)。version の扱いと、1 つの中継を読む前提は `NitReader` と同じ
268
+ */
269
+ export declare class SdtReader {
270
+ private readonly assembler;
271
+ private readonly packets;
272
+ private readonly pending;
273
+ private readonly done;
274
+ /** 置き換えた表の version (TS ごと) */
275
+ private readonly replaced;
276
+ /** 任意の長さのバイト列を渡す。そろった表が増えたか変わったら true */
277
+ feed(chunk: Uint8Array): boolean;
278
+ private add;
279
+ /** 自分の TS の表 (SDT[actual])。まだそろっていなければ null */
280
+ get actual(): ServiceTable | null;
281
+ /** そろった表を全部。actual が先、あとはネットワーク・TSID の順 */
282
+ tables(): ServiceTable[];
283
+ }
146
284
  export interface FoundService extends Service {
147
285
  networkId: number;
148
286
  transportStreamId: number;
package/dist/psi.js CHANGED
@@ -3,8 +3,10 @@
3
3
  * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
4
  * 読むのは NIT と SDT の2つ。
5
5
  *
6
- * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号
7
- * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別
6
+ * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
7
+ * (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
8
+ * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別。衛星はほかの TS のぶん
9
+ * (SDT[other]) も流れている (`SdtReader`)
8
10
  *
9
11
  * 局名も SDT から読む。ARIB の文字符号は独自だが、番組表を自分で集めるなら
10
12
  * どのみち要るので [aribtext.ts](aribtext.ts) に寄せてある。
@@ -19,13 +21,17 @@ export function packetPid(packet) {
19
21
  }
20
22
  /** PAT。どの PID に PMT があるかを持つ。**入口はいつもここ** */
21
23
  export const PID_PAT = 0x0000;
22
- const PID_NIT = 0x0010;
24
+ export const PID_NIT = 0x0010;
23
25
  export const PID_SDT = 0x0011;
24
26
  export const TABLE_PAT = 0x00;
25
27
  export const TABLE_PMT = 0x02;
26
- const TABLE_NIT_ACTUAL = 0x40;
28
+ export const TABLE_NIT_ACTUAL = 0x40;
29
+ export const TABLE_NIT_OTHER = 0x41;
27
30
  export const TABLE_SDT_ACTUAL = 0x42;
31
+ export const TABLE_SDT_OTHER = 0x46;
28
32
  const DESC_SERVICE_LIST = 0x41;
33
+ const DESC_SATELLITE_DELIVERY = 0x43;
34
+ const DESC_TERRESTRIAL_DELIVERY = 0xfa;
29
35
  const DESC_SERVICE = 0x48;
30
36
  const DESC_TS_INFORMATION = 0xcd;
31
37
  /**
@@ -33,7 +39,9 @@ const DESC_TS_INFORMATION = 0xcd;
33
39
  * ラジオ (0x02) とデータ (0xc0) も入る。映像の無いものを番組表に並べないのは
34
40
  * 取り込むほう (denpa の epg.ts の DIGITAL_TV)
35
41
  */
36
- const SERVICE_TYPES = new Set([0x01, 0x02, 0xa1, 0xa4, 0xa5, 0xad, 0xc0]);
42
+ export const SERVICE_TYPES = new Set([
43
+ 0x01, 0x02, 0xa1, 0xa4, 0xa5, 0xad, 0xc0,
44
+ ]);
37
45
  const CRC32_TABLE = (() => {
38
46
  const table = new Uint32Array(256);
39
47
  for (let i = 0; i < 256; i++) {
@@ -373,10 +381,8 @@ export function* sdtServices(section) {
373
381
  at += 5 + loop;
374
382
  }
375
383
  }
376
- /** SDT からサービスの一覧を読む。自分の TS のものだけ */
377
- export function parseSdt(section) {
378
- if (section[0] !== TABLE_SDT_ACTUAL)
379
- return null;
384
+ /** SDT のサービスの並び。自分の TS (actual) とほかの TS (other) で形式は同じ */
385
+ function sdtTransport(section) {
380
386
  const services = [];
381
387
  for (const [serviceId, body] of sdtServices(section)) {
382
388
  for (const [tag, descriptor] of descriptors(body)) {
@@ -404,9 +410,69 @@ export function parseSdt(section) {
404
410
  services,
405
411
  };
406
412
  }
407
- /** NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む */
408
- export function parseNit(section) {
409
- if (section[0] !== TABLE_NIT_ACTUAL)
413
+ /** SDT からサービスの一覧を読む。自分の TS のものだけ */
414
+ export function parseSdt(section) {
415
+ return section[0] === TABLE_SDT_ACTUAL ? sdtTransport(section) : null;
416
+ }
417
+ /**
418
+ * SDT[other] からサービスの一覧を読む。**ほかの TS のもの。**
419
+ *
420
+ * 衛星は、どの TS にもネットワークのほかの TS の局名を流している (番組表の EIT[other] と同じ考え)。
421
+ * 開いていない TS の局名をこれで知る。TS ごとに別のセクションで来るので、集めるのは `SdtReader`
422
+ */
423
+ export function parseSdtOther(section) {
424
+ return section[0] === TABLE_SDT_OTHER ? sdtTransport(section) : null;
425
+ }
426
+ /** BCD の桁を左から `digits` 桁ぶん読んで、10 進の整数にする */
427
+ function bcdDigits(data, at, digits) {
428
+ let value = 0;
429
+ for (let i = 0; i < digits; i++) {
430
+ const byte = data[at + (i >> 1)] ?? 0;
431
+ value = value * 10 + ((i & 1) === 0 ? byte >> 4 : byte & 0x0f);
432
+ }
433
+ return value;
434
+ }
435
+ /**
436
+ * 衛星の分配の記述子を読む。周波数は 8 桁の BCD で、上から 3 桁が GHz (011.72748 GHz)。
437
+ * シンボルレートは 7 桁で、上から 3 桁が Msym/s
438
+ */
439
+ function satelliteDelivery(descriptor) {
440
+ if (descriptor.length < 11)
441
+ return null;
442
+ return {
443
+ kind: "satellite",
444
+ frequency: bcdDigits(descriptor, 0, 8) * 10,
445
+ orbitalPosition: bcdDigits(descriptor, 4, 4),
446
+ east: (descriptor[6] & 0x80) !== 0,
447
+ polarization: (descriptor[6] >> 5) & 0x03,
448
+ modulation: descriptor[6] & 0x1f,
449
+ symbolRate: bcdDigits(descriptor, 7, 7) * 100,
450
+ fec: descriptor[10] & 0x0f,
451
+ };
452
+ }
453
+ /** 地上の分配の記述子を読む。周波数は 16 ビットずつ、1/7 MHz 単位 */
454
+ function terrestrialDelivery(descriptor) {
455
+ if (descriptor.length < 2)
456
+ return null;
457
+ const head = u16(descriptor, 0);
458
+ const frequencies = [];
459
+ for (let at = 2; at + 2 <= descriptor.length; at += 2) {
460
+ frequencies.push(Math.round((u16(descriptor, at) * 1_000_000) / 7));
461
+ }
462
+ return {
463
+ kind: "terrestrial",
464
+ areaCode: head >> 4,
465
+ guardInterval: (head >> 2) & 0x03,
466
+ transmissionMode: head & 0x03,
467
+ frequencies,
468
+ };
469
+ }
470
+ /** NIT のセクションを 1 つ読む。actual (0x40) と other (0x41) のどちらも */
471
+ export function parseNitSection(section) {
472
+ const tableId = section[0];
473
+ if (tableId !== TABLE_NIT_ACTUAL && tableId !== TABLE_NIT_OTHER)
474
+ return null;
475
+ if (section.length < 16)
410
476
  return null;
411
477
  const networkLength = u16(section, 8) & 0x0fff;
412
478
  let at = 10 + networkLength;
@@ -420,9 +486,10 @@ export function parseNit(section) {
420
486
  const transportStreamId = u16(section, at);
421
487
  const originalNetworkId = u16(section, at + 2);
422
488
  const loop = u16(section, at + 4) & 0x0fff;
423
- const body = section.subarray(at + 6, at + 6 + loop);
489
+ const body = section.subarray(at + 6, Math.min(at + 6 + loop, end));
424
490
  at += 6 + loop;
425
491
  const services = [];
492
+ let delivery = null;
426
493
  for (const [tag, descriptor] of descriptors(body)) {
427
494
  if (tag === DESC_TS_INFORMATION &&
428
495
  descriptor.length > 0 &&
@@ -438,15 +505,220 @@ export function parseNit(section) {
438
505
  });
439
506
  }
440
507
  }
508
+ else if (tag === DESC_SATELLITE_DELIVERY) {
509
+ delivery = satelliteDelivery(descriptor) ?? delivery;
510
+ }
511
+ else if (tag === DESC_TERRESTRIAL_DELIVERY) {
512
+ delivery = terrestrialDelivery(descriptor) ?? delivery;
513
+ }
441
514
  }
442
- transportStreams.push({ transportStreamId, originalNetworkId, services });
515
+ transportStreams.push({
516
+ transportStreamId,
517
+ originalNetworkId,
518
+ services,
519
+ delivery,
520
+ });
443
521
  }
444
522
  return {
523
+ other: tableId === TABLE_NIT_OTHER,
445
524
  networkId: u16(section, 3),
525
+ version: (section[5] >> 1) & 0x1f,
526
+ current: (section[5] & 0x01) !== 0,
527
+ sectionNumber: section[6],
528
+ lastSectionNumber: section[7],
446
529
  remoteControlKeyId,
447
530
  transportStreams,
448
531
  };
449
532
  }
533
+ /**
534
+ * NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む。**セクション 1 つぶん。**
535
+ * スキャンで「どのネットワークか」を知るにはこれで足りる。ネットワークの全部の TS が要るなら `NitReader`
536
+ */
537
+ export function parseNit(section) {
538
+ if (section[0] !== TABLE_NIT_ACTUAL)
539
+ return null;
540
+ const parsed = parseNitSection(section);
541
+ if (parsed === null)
542
+ return null;
543
+ const { networkId, remoteControlKeyId, transportStreams } = parsed;
544
+ return { networkId, remoteControlKeyId, transportStreams };
545
+ }
546
+ /**
547
+ * 1 つの表のセクションを集める。**version が変わったら集め直す。**
548
+ *
549
+ * 表は何本かのセクション (section_number 0〜last_section_number) に分かれて来る。
550
+ * 違う version のものを混ぜると、書き換わる前と後の TS が 1 つの表に並ぶ
551
+ */
552
+ class SectionSet {
553
+ version = -1;
554
+ last = -1;
555
+ parts = new Map();
556
+ /** 足す。そろったら true */
557
+ add(version, number, last, part) {
558
+ if (version !== this.version || last !== this.last) {
559
+ this.version = version;
560
+ this.last = last;
561
+ this.parts = new Map();
562
+ }
563
+ if (number <= last)
564
+ this.parts.set(number, part);
565
+ return this.parts.size === last + 1;
566
+ }
567
+ /** section_number の順に */
568
+ ordered() {
569
+ return [...this.parts.entries()]
570
+ .sort(([a], [b]) => a - b)
571
+ .map(([, part]) => part);
572
+ }
573
+ }
574
+ /**
575
+ * NIT を**全部のセクション**そろえて読む。
576
+ *
577
+ * 衛星の NIT[actual] には、そのネットワークの全部の TS (TSID・周波数・サービスの一覧) が載っている。
578
+ * TS が多いと 1 セクション (1024 バイトまで) に収まらないので、ネットワークごとに (actual と other を
579
+ * 分けて) 集め、そろったものを返す。
580
+ *
581
+ * **version が変わっても、新しい表がそろうまでは前の表を返す。** 読む側が見るのは、いつも 1 つの
582
+ * version でそろった表。全部のセクションが 1 回りするまで (NIT は 10 秒に 1 回は流れるので、長くて 10 秒ほど) は何も返らない。
583
+ *
584
+ * 置き換えた 1 つ前の version のセクションが遅れて届いても読まない。読むと、そろったときに表が前に戻る。
585
+ *
586
+ * 1 つの中継を読む前提で作ってある。別の中継へ移るときは作り直す。`actual` は、最初にそろった自分のネットワークの表を返す
587
+ */
588
+ export class NitReader {
589
+ assembler = new SectionAssembler(PID_NIT);
590
+ packets = new PacketStream();
591
+ pending = new Map();
592
+ done = new Map();
593
+ /** 置き換えた表の version (表ごと) */
594
+ replaced = new Map();
595
+ /** 任意の長さのバイト列を渡す。そろった表が増えたか変わったら true */
596
+ feed(chunk) {
597
+ let changed = false;
598
+ for (const packet of this.packets.feed(chunk)) {
599
+ for (const section of this.assembler.feed(packet)) {
600
+ const tableId = section[0];
601
+ if (tableId !== TABLE_NIT_ACTUAL && tableId !== TABLE_NIT_OTHER)
602
+ continue;
603
+ // current_next_indicator が 0 (これから使う表) のものは読まない
604
+ if (section.length < 16 || (section[5] & 0x01) === 0)
605
+ continue;
606
+ const key = `${tableId === TABLE_NIT_OTHER ? "other" : "actual"}:${u16(section, 3)}`;
607
+ // そろっている表と同じ version なら、繰り返し流れてきただけ。中身を読む前に見る (同じものが何度も来る)
608
+ const version = (section[5] >> 1) & 0x1f;
609
+ if (this.done.get(key)?.version === version ||
610
+ this.replaced.get(key) === version)
611
+ continue;
612
+ const parsed = parseNitSection(section);
613
+ if (parsed !== null && this.add(key, parsed))
614
+ changed = true;
615
+ }
616
+ }
617
+ return changed;
618
+ }
619
+ add(key, section) {
620
+ const set = this.pending.get(key) ?? new SectionSet();
621
+ this.pending.set(key, set);
622
+ const complete = set.add(section.version, section.sectionNumber, section.lastSectionNumber, section);
623
+ if (!complete)
624
+ return false;
625
+ const parts = set.ordered();
626
+ this.pending.delete(key);
627
+ const before = this.done.get(key);
628
+ if (before !== undefined)
629
+ this.replaced.set(key, before.version);
630
+ this.done.set(key, {
631
+ other: section.other,
632
+ networkId: section.networkId,
633
+ version: section.version,
634
+ remoteControlKeyId: parts.find((part) => part.remoteControlKeyId !== null)
635
+ ?.remoteControlKeyId ?? null,
636
+ transportStreams: parts.flatMap((part) => part.transportStreams),
637
+ });
638
+ return true;
639
+ }
640
+ /** 自分のネットワークの表 (NIT[actual])。まだそろっていなければ null */
641
+ get actual() {
642
+ for (const network of this.done.values()) {
643
+ if (!network.other)
644
+ return network;
645
+ }
646
+ return null;
647
+ }
648
+ /** そろった表を全部。actual が先 */
649
+ networks() {
650
+ return [...this.done.values()].sort((a, b) => Number(a.other) - Number(b.other) || a.networkId - b.networkId);
651
+ }
652
+ }
653
+ /**
654
+ * SDT を TS ごとに**全部のセクション**そろえて読む。自分の TS (actual) とほかの TS (other) の両方。
655
+ *
656
+ * 衛星はほかの TS の局名も流しているので、1 つの TS を開けばネットワークの局名がそろう
657
+ * (そろうかは放送しだい。来なかった TS は返らない)。version の扱いと、1 つの中継を読む前提は `NitReader` と同じ
658
+ */
659
+ export class SdtReader {
660
+ assembler = new SectionAssembler(PID_SDT);
661
+ packets = new PacketStream();
662
+ pending = new Map();
663
+ done = new Map();
664
+ /** 置き換えた表の version (TS ごと) */
665
+ replaced = new Map();
666
+ /** 任意の長さのバイト列を渡す。そろった表が増えたか変わったら true */
667
+ feed(chunk) {
668
+ let changed = false;
669
+ for (const packet of this.packets.feed(chunk)) {
670
+ for (const section of this.assembler.feed(packet)) {
671
+ const tableId = section[0];
672
+ if (tableId !== TABLE_SDT_ACTUAL && tableId !== TABLE_SDT_OTHER)
673
+ continue;
674
+ // current_next_indicator が 0 (これから使う表) のものは読まない
675
+ if (section.length < 15 || (section[5] & 0x01) === 0)
676
+ continue;
677
+ if (this.add(section, tableId === TABLE_SDT_OTHER))
678
+ changed = true;
679
+ }
680
+ }
681
+ return changed;
682
+ }
683
+ add(section, other) {
684
+ const version = (section[5] >> 1) & 0x1f;
685
+ const key = `${other ? "other" : "actual"}:${u16(section, 8)}:${u16(section, 3)}`;
686
+ // そろっている表と同じ version なら、繰り返し流れてきただけ。局名を読む前に見る (2 秒おきに同じものが来る)
687
+ if (this.done.get(key)?.version === version ||
688
+ this.replaced.get(key) === version)
689
+ return false;
690
+ const transport = sdtTransport(section);
691
+ const set = this.pending.get(key) ?? new SectionSet();
692
+ this.pending.set(key, set);
693
+ if (!set.add(version, section[6], section[7], transport))
694
+ return false;
695
+ this.pending.delete(key);
696
+ const before = this.done.get(key);
697
+ if (before !== undefined)
698
+ this.replaced.set(key, before.version);
699
+ this.done.set(key, {
700
+ ...transport,
701
+ services: set.ordered().flatMap((part) => part.services),
702
+ other,
703
+ version,
704
+ });
705
+ return true;
706
+ }
707
+ /** 自分の TS の表 (SDT[actual])。まだそろっていなければ null */
708
+ get actual() {
709
+ for (const table of this.done.values()) {
710
+ if (!table.other)
711
+ return table;
712
+ }
713
+ return null;
714
+ }
715
+ /** そろった表を全部。actual が先、あとはネットワーク・TSID の順 */
716
+ tables() {
717
+ return [...this.done.values()].sort((a, b) => Number(a.other) - Number(b.other) ||
718
+ a.originalNetworkId - b.originalNetworkId ||
719
+ a.transportStreamId - b.transportStreamId);
720
+ }
721
+ }
450
722
  /**
451
723
  * 流れてくる TS を食べて、NIT と SDT が揃うまで待つ。
452
724
  *
package/dist/synth.d.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  */
7
7
  import { withCrc } from "./psi.js";
8
8
  export { withCrc };
9
+ /** 表のバージョンと、何本に分けたうちのどれか。指定しなければバージョン 0・節 0/0 */
10
+ export interface SectionPart {
11
+ version?: number;
12
+ sectionNumber?: number;
13
+ lastSectionNumber?: number;
14
+ }
9
15
  /** PCR の基準部 (90kHz) を適応フィールドに書く (旗は呼ぶ側)。拡張部 (27MHz の端数) は読む側が見ていないので 0 */
10
16
  export declare function putPcr(packet: Uint8Array, pcr: number): void;
11
17
  /**
@@ -16,7 +22,11 @@ export declare function putPcr(packet: Uint8Array, pcr: number): void;
16
22
  * 文字の大きさは触らない (字幕は呼ぶ側が MSZ などを前に置く)。SI に書くなら `encodeSiText`
17
23
  */
18
24
  export declare function encodeAribText(text: string, half?: boolean): number[];
19
- export declare function sdtSection(transportStreamId: number, originalNetworkId: number, services: [number, number, string?][]): Uint8Array;
25
+ export declare function sdtSection(transportStreamId: number, originalNetworkId: number, services: [number, number, string?][],
26
+ /** `other` なら SDT[other] (0x46。ほかの TS のもの) */
27
+ options?: SectionPart & {
28
+ other?: boolean;
29
+ }): Uint8Array;
20
30
  export interface SynthEvent {
21
31
  eventId: number;
22
32
  /** epoch ms */
@@ -87,7 +97,22 @@ export interface EitOptions {
87
97
  }
88
98
  /** EIT。番組表そのもの。table_id で p/f (0x4E) と schedule (0x50〜) が分かれる */
89
99
  export declare function eitSection(options: EitOptions): Uint8Array;
90
- export declare function nitSection(networkId: number, remoteControlKeyId: number | null, streams: [number, number, [number, number][]][]): Uint8Array;
100
+ /**
101
+ * NIT。TS ごとに `[TSID, ONID, [サービスID, 種別][], 足す記述子]`。
102
+ * 足す記述子は分配の記述子 (`satelliteDeliveryDescriptor`・`terrestrialDeliveryDescriptor`) など
103
+ */
104
+ export declare function nitSection(networkId: number, remoteControlKeyId: number | null, streams: [number, number, [number, number][], number[]?][],
105
+ /** `other` なら NIT[other] (0x41。ほかのネットワークのもの) */
106
+ options?: SectionPart & {
107
+ other?: boolean;
108
+ }): Uint8Array;
109
+ /**
110
+ * 衛星の分配の記述子 (0x43)。周波数は kHz、シンボルレートはシンボル/秒で渡す
111
+ * (BS-1 は 11727480 kHz・28860000)。東経 110 度・右旋・ISDB-S (変調 0x08) にしておく
112
+ */
113
+ export declare function satelliteDeliveryDescriptor(frequency: number, symbolRate?: number): number[];
114
+ /** 地上の分配の記述子 (0xFA)。周波数は Hz で渡す (1/7 MHz 単位に丸める) */
115
+ export declare function terrestrialDeliveryDescriptor(areaCode: number, frequencies: number[]): number[];
91
116
  /** セクションを TS パケットに詰める。頭のパケットには pointer_field が付く */
92
117
  export declare function packetize(pid: number, section: Uint8Array, counter?: number): Uint8Array;
93
118
  /** 部品を1本のバイト列に繋ぐ。組んだパケットを流し込むときの糊 */
@@ -149,7 +174,7 @@ export interface DiiModuleSpec {
149
174
  * DII。「このモジュールは何バイトで、名前は何か」を伝える。
150
175
  *
151
176
  * **複数のモジュールを載せられる。** 本物の BS はここに `LOGO-05` と
152
- * `CS_LOGO-05` を並べて流していて (実機の `BS15_0` で確認)、1つしか
177
+ * `CS_LOGO-05` を並べて流していて (実機の `BS15:16625` で確認)、1つしか
153
178
  * 載せられない作りでは CS を取りこぼす経路を試せない。
154
179
  */
155
180
  export declare function diiSection(downloadId: number, blockSize: number, modules: DiiModuleSpec | DiiModuleSpec[],
package/dist/synth.js CHANGED
@@ -10,9 +10,17 @@ import { PACKET, SYNC, withCrc } from "./psi.js";
10
10
  export { withCrc };
11
11
  const be = (value) => [(value >> 8) & 0xff, value & 0xff];
12
12
  const be32 = (value) => [...be(value >>> 16), ...be(value & 0xffff)];
13
- /** セクションの頭8バイト (長さは `withCrc` が埋める)。バージョン 0・節 0/0 */
14
- function head(tableId, extension) {
15
- return [tableId, 0x00, 0x00, ...be(extension), 0xc1, 0x00, 0x00];
13
+ /** セクションの頭8バイト (長さは `withCrc` が埋める) */
14
+ function head(tableId, extension, part = {}) {
15
+ return [
16
+ tableId,
17
+ 0x00,
18
+ 0x00,
19
+ ...be(extension),
20
+ 0xc1 | (((part.version ?? 0) & 0x1f) << 1),
21
+ part.sectionNumber ?? 0,
22
+ part.lastSectionNumber ?? 0,
23
+ ];
16
24
  }
17
25
  /** PCR の基準部 (90kHz) を適応フィールドに書く (旗は呼ぶ側)。拡張部 (27MHz の端数) は読む側が見ていないので 0 */
18
26
  export function putPcr(packet, pcr) {
@@ -93,9 +101,11 @@ const sized = (text) => {
93
101
  const body = encodeSiText(text);
94
102
  return [body.length, ...body];
95
103
  };
96
- export function sdtSection(transportStreamId, originalNetworkId, services) {
104
+ export function sdtSection(transportStreamId, originalNetworkId, services,
105
+ /** `other` なら SDT[other] (0x46。ほかの TS のもの) */
106
+ options = {}) {
97
107
  const body = [
98
- ...head(0x42, transportStreamId),
108
+ ...head(options.other === true ? 0x46 : 0x42, transportStreamId, options),
99
109
  ...be(originalNetworkId),
100
110
  0xff,
101
111
  ];
@@ -235,21 +245,66 @@ export function eitSection(options) {
235
245
  }
236
246
  return withCrc(body);
237
247
  }
238
- export function nitSection(networkId, remoteControlKeyId, streams) {
239
- const body = [...head(0x40, networkId), ...be(0xf000)];
248
+ /**
249
+ * NIT。TS ごとに `[TSID, ONID, [サービスID, 種別][], 足す記述子]`。
250
+ * 足す記述子は分配の記述子 (`satelliteDeliveryDescriptor`・`terrestrialDeliveryDescriptor`) など
251
+ */
252
+ export function nitSection(networkId, remoteControlKeyId, streams,
253
+ /** `other` なら NIT[other] (0x41。ほかのネットワークのもの) */
254
+ options = {}) {
255
+ const body = [
256
+ ...head(options.other === true ? 0x41 : 0x40, networkId, options),
257
+ ...be(0xf000),
258
+ ];
240
259
  const loop = [];
241
- for (const [transportStreamId, originalNetworkId, services] of streams) {
260
+ for (const [transportStreamId, originalNetworkId, services, extra,] of streams) {
242
261
  const descriptors = [];
243
262
  // TS information descriptor (0xCD)
244
263
  if (remoteControlKeyId !== null)
245
264
  descriptors.push(0xcd, 0x02, remoteControlKeyId, 0x00);
246
265
  const serviceList = services.flatMap(([id, type]) => [...be(id), type]);
247
266
  descriptors.push(0x41, serviceList.length, ...serviceList);
267
+ descriptors.push(...(extra ?? []));
248
268
  loop.push(...be(transportStreamId), ...be(originalNetworkId), ...be(0xf000 | descriptors.length), ...descriptors);
249
269
  }
250
270
  body.push(...be(0xf000 | loop.length), ...loop);
251
271
  return withCrc(body);
252
272
  }
273
+ /** 10 進の整数を `digits` 桁の BCD のバイト列にする (桁が奇数なら最後の半バイトは 0) */
274
+ function bcdBytes(value, digits) {
275
+ const text = String(value).padStart(digits, "0");
276
+ const out = [];
277
+ for (let i = 0; i < digits; i += 2) {
278
+ out.push((Number(text[i]) << 4) | Number(text[i + 1] ?? 0));
279
+ }
280
+ return out;
281
+ }
282
+ /**
283
+ * 衛星の分配の記述子 (0x43)。周波数は kHz、シンボルレートはシンボル/秒で渡す
284
+ * (BS-1 は 11727480 kHz・28860000)。東経 110 度・右旋・ISDB-S (変調 0x08) にしておく
285
+ */
286
+ export function satelliteDeliveryDescriptor(frequency, symbolRate = 28_860_000) {
287
+ const symbol = bcdBytes(symbolRate / 100, 7);
288
+ return [
289
+ 0x43,
290
+ 11,
291
+ ...bcdBytes(frequency / 10, 8),
292
+ ...bcdBytes(1100, 4),
293
+ 0x80 | (3 << 5) | 0x08,
294
+ symbol[0],
295
+ symbol[1],
296
+ symbol[2],
297
+ (symbol[3] & 0xf0) | 0x01,
298
+ ];
299
+ }
300
+ /** 地上の分配の記述子 (0xFA)。周波数は Hz で渡す (1/7 MHz 単位に丸める) */
301
+ export function terrestrialDeliveryDescriptor(areaCode, frequencies) {
302
+ const body = [
303
+ ...be((areaCode << 4) | (1 << 2) | 0x02),
304
+ ...frequencies.flatMap((hz) => be(Math.round((hz * 7) / 1_000_000))),
305
+ ];
306
+ return [0xfa, body.length, ...body];
307
+ }
253
308
  /** セクションを TS パケットに詰める。頭のパケットには pointer_field が付く */
254
309
  export function packetize(pid, section, counter = 0) {
255
310
  const packets = [];
@@ -378,7 +433,7 @@ function dsmccSection(tableId, message) {
378
433
  * DII。「このモジュールは何バイトで、名前は何か」を伝える。
379
434
  *
380
435
  * **複数のモジュールを載せられる。** 本物の BS はここに `LOGO-05` と
381
- * `CS_LOGO-05` を並べて流していて (実機の `BS15_0` で確認)、1つしか
436
+ * `CS_LOGO-05` を並べて流していて (実機の `BS15:16625` で確認)、1つしか
382
437
  * 載せられない作りでは CS を取りこぼす経路を試せない。
383
438
  */
384
439
  export function diiSection(downloadId, blockSize, modules,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danything/arib",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "日本のデジタル放送 (ARIB) の TS を読む: PSI・PES・EIT・ARIB の文字・字幕・データ放送",
5
5
  "license": "MIT",
6
6
  "repository": {