@danything/arib 1.0.2 → 1.2.1

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/psi.js CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
3
3
  * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
- * 読むのは NIT と SDT の2つ。
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++) {
@@ -46,7 +54,7 @@ const CRC32_TABLE = (() => {
46
54
  }
47
55
  return table;
48
56
  })();
49
- /** MPEG-2 の CRC32。セクション末尾の4バイトを含めて回すと 0 になる */
57
+ /** MPEG-2 の CRC32。セクション末尾の 4 バイトを含めて回すと 0 になる */
50
58
  function crc32(data) {
51
59
  let crc = 0xffffffff;
52
60
  for (const byte of data) {
@@ -58,9 +66,9 @@ function crc32(data) {
58
66
  /**
59
67
  * セクション本体に CRC32 を付けて仕上げる。`section_length` も実長に直す。
60
68
  *
61
- * **書く側が2つある。** PAT を書き直して流す `service-filter.ts` と、
69
+ * **書く側が 2 つある。** PAT を書き直して流す `service-filter.ts` と、
62
70
  * 偽の放送を組み立てる `synth.ts`。長さの直し方を間違えると読む側が黙って
63
- * 捨てるだけなので、1箇所に置く。
71
+ * 捨てるだけなので、1 箇所に置く。
64
72
  */
65
73
  export function withCrc(body) {
66
74
  const section = Uint8Array.from(body);
@@ -79,16 +87,16 @@ const NONE = Object.freeze([]);
79
87
  *
80
88
  * セクションは TS パケットをまたいで運ばれ、パケットの切れ目とは無関係な
81
89
  * 位置で終わる。payload_unit_start_indicator と pointer_field を見て
82
- * 頭を合わせ、section_length ぶん溜まったら1本として吐く。
90
+ * 頭を合わせ、section_length ぶん溜まったら 1 本として返す。
83
91
  */
84
92
  export class SectionAssembler {
85
93
  pid;
86
94
  crc;
87
95
  buffer = new Uint8Array(0);
88
96
  /**
89
- * @param pid 読むPID
97
+ * @param pid 読む PID
90
98
  * @param crc CRC の見方。
91
- * `always` … 末尾4バイトを CRC32 とみなして必ず確かめる (PSI はこちら)
99
+ * `always` … 末尾 4 バイトを CRC32 とみなして必ず確かめる (PSI はこちら)
92
100
  * `syntax` … `section_syntax_indicator` が立っているときだけ確かめる。
93
101
  * DSM-CC は立っていないと末尾が CRC ではなく Checksum なので、
94
102
  * いつも確かめると**全部捨ててしまう**
@@ -98,10 +106,10 @@ export class SectionAssembler {
98
106
  this.crc = crc;
99
107
  }
100
108
  /**
101
- * パケットを1つ食わせる。組み上がったセクションを返す。
109
+ * パケットを 1 つ渡す。組み上がったセクションを返す。
102
110
  *
103
111
  * 返りは**読むだけ**にすること。何も組み上がらないとき (ほとんどのパケット) は
104
- * 使い回しの空配列を返す — 関係ないパケットのたびに配列を作っていた
112
+ * 使い回しの空配列を返す (関係ないパケットのたびに配列を作っていた)
105
113
  */
106
114
  feed(packet) {
107
115
  // トランスポートエラーが立っているものは信用しない
@@ -165,7 +173,7 @@ export class SectionAssembler {
165
173
  }
166
174
  /**
167
175
  * 同期が取れているとみなすのに必要な連続パケット数。
168
- * 1つだけでは中身のたまたまの 0x47 を頭と誤認する
176
+ * 1 つだけでは中身のたまたまの 0x47 を頭と誤認する
169
177
  */
170
178
  const CONFIRM = 3;
171
179
  /** `from` 以降 `until` の手前までで、188 間隔に 0x47 が続くところを探す。無ければ -1 (denpa の `seek.ts` も使う) */
@@ -183,7 +191,7 @@ export function findSync(data, from = 0, until = data.length) {
183
191
  * 任意の長さで届くバイト列を 188 バイトのパケットに切り分ける。
184
192
  *
185
193
  * 頭が必ずパケットの先頭とは限らず、電波が弱いと途中で数バイト落ちる。
186
- * ずれたままだと**以降ずっと1パケットも読めなくなる**ので、頭が 0x47 でなければ
194
+ * ずれたままだと**以降ずっと 1 パケットも読めなくなる**ので、頭が 0x47 でなければ
187
195
  * 取り直す。普段は先頭が 0x47 なので、探しに行くのはずれたときだけ。
188
196
  *
189
197
  * **出すパケットは渡された塊の上の窓** (写しではない。写すと録画の処理の 1/3 を食う)。
@@ -197,7 +205,7 @@ export class PacketStream {
197
205
  /*
198
206
  * 前の残り (`rest`) と塊を繋いだ並びの上を歩く。位置 `at` はその並びでの位置。
199
207
  * 繋ぐのは**残りに頭があるパケットを読むのに要るぶんだけ** (頭を探すのに
200
- * 3パケットぶん先まで見る)
208
+ * 3 パケットぶん先まで見る)
201
209
  */
202
210
  const split = this.rest.length;
203
211
  const total = split + chunk.length;
@@ -259,8 +267,8 @@ export class PacketStream {
259
267
  }
260
268
  }
261
269
  /**
262
- * PAT から1つの局の PMT を追う。`serviceId` を渡さなければ PAT の最初の局
263
- * (1局に絞った TS を食わせるところ)。PAT で PMT の PID が替われば組み立て直す
270
+ * PAT から 1 つの局の PMT を追う。`serviceId` を渡さなければ PAT の最初の局
271
+ * (1 局に絞った TS を渡すところ)。PAT で PMT の PID が替われば組み立て直す
264
272
  */
265
273
  export class PmtTracker {
266
274
  serviceId;
@@ -271,7 +279,7 @@ export class PmtTracker {
271
279
  constructor(serviceId) {
272
280
  this.serviceId = serviceId;
273
281
  }
274
- /** パケットを1つ (`pid` はその PID)。組み上がった PMT のセクションを返す */
282
+ /** パケットを 1 つ (`pid` はその PID)。組み上がった PMT のセクションを返す */
275
283
  feed(packet, pid) {
276
284
  if (pid === PID_PAT) {
277
285
  for (const section of this.pat.feed(packet)) {
@@ -306,8 +314,8 @@ export const DESC_DIGITAL_COPY_CONTROL = 0xc1;
306
314
  /**
307
315
  * デジタルコピー制御記述子の中身 (タグと長さを除いたもの) から、**コピー禁止か**を読む。
308
316
  *
309
- * 先頭2ビットの digital_recording_control_data が 11 なら禁止
310
- * (00 コピー可、01 事業者定義、10 1世代のみ)。禁止の番組は録らない
317
+ * 先頭 2 ビットの digital_recording_control_data が 11 なら禁止
318
+ * (00 コピー可、01 事業者定義、10 1 世代のみ)。禁止の番組は録らない
311
319
  * ([data.md](https://github.com/danything/denpa/blob/main/docs/data.md)「コピー禁止の番組は録らない」)
312
320
  */
313
321
  export function isCopyNever(data) {
@@ -343,7 +351,7 @@ export function* pmtStreams(section) {
343
351
  at += 5 + infoLength;
344
352
  }
345
353
  }
346
- /** PAT から `サービスID → PMT の PID`。サービス0 は NIT なので飛ばす */
354
+ /** PAT から `サービスID → PMT の PID`。サービス 0 は NIT なので飛ばす */
347
355
  export function parsePat(section) {
348
356
  const programs = new Map();
349
357
  if (section[0] !== TABLE_PAT)
@@ -360,8 +368,8 @@ export function parsePat(section) {
360
368
  /**
361
369
  * SDT のサービスの並びを (service_id, 記述子の並び) に切り分ける。
362
370
  *
363
- * **読む側が2つある。** 局名 (`parseSdt`) とロゴの対応 (denpa の `ts/logo.ts`) で、
364
- * 欲しい記述子が違うだけで歩き方は同じ。並びの読み方をここ1箇所に置く。
371
+ * **読む側が 2 つある。** 局名 (`parseSdt`) とロゴの対応 (denpa の `ts/logo.ts`) で、
372
+ * 欲しい記述子が違うだけで歩き方は同じ。並びの読み方をここ 1 箇所に置く。
365
373
  */
366
374
  export function* sdtServices(section) {
367
375
  let at = 11;
@@ -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,17 +505,222 @@ 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
  }
450
533
  /**
451
- * 流れてくる TS を食べて、NIT と SDT が揃うまで待つ。
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
+ }
722
+ /**
723
+ * 流れてくる TS を読んで、NIT と SDT が揃うまで待つ。
452
724
  *
453
725
  * Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
454
726
  * 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
@@ -463,7 +735,7 @@ export class ServiceReader {
463
735
  get complete() {
464
736
  return this.network !== null && this.transport !== null;
465
737
  }
466
- /** 任意の長さのバイト列を食わせる。揃ったら true */
738
+ /** 任意の長さのバイト列を渡す。揃ったら true */
467
739
  feed(chunk) {
468
740
  for (const packet of this.packets.feed(chunk)) {
469
741
  if (this.network === null) {
@@ -1,16 +1,16 @@
1
1
  /**
2
- * 物理チャンネル丸ごとの TS から、1つの局のぶんだけを抜き出す。
2
+ * 物理チャンネル丸ごとの TS から、1 つの局のぶんだけを抜き出す。
3
3
  *
4
4
  * エージェントは**中身を読まずに素の TS を流す**ので
5
5
  * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))、選り分けるのは denpa の仕事。
6
6
  *
7
- * **これをやらないと1つの録画に複数の番組が入る。** 実機の TOKYO MX は
8
- * 1本の TS に MX1 と MX2 を載せていて、丸ごと保存すると ffprobe が2つの
7
+ * **これをやらないと 1 つの録画に複数の番組が入る。** 実機の TOKYO MX は
8
+ * 1 本の TS に MX1 と MX2 を載せていて、丸ごと保存すると ffprobe が 2 つの
9
9
  * プログラムを返す (これで CM 検出の fps を読み違えたことがある)。
10
10
  *
11
- * 残すもの:
11
+ * 残すものは次のとおり。
12
12
  *
13
- * - **PAT** … その局1つだけに**書き直して**出す。丸ごと通すと ffmpeg には
13
+ * - **PAT** … その局 1 つだけに**書き直して**出す。丸ごと通すと ffmpeg には
14
14
  * 相変わらず複数のプログラムが見える
15
15
  * - **PMT と、そこに載っている ES** … 映像・音声・字幕。ECM も (掛かったまま
16
16
  * 録れたものを後から復号するのに要る)
@@ -20,7 +20,7 @@
20
20
  * 落とすもの: 他局の PMT と ES、そして詰め物 (PID 0x1FFF)。
21
21
  */
22
22
  /**
23
- * 局1つぶんに絞る。
23
+ * 局 1 つぶんに絞る。
24
24
  *
25
25
  * PAT と PMT を読むまでは**何も出さない**。どの PID がその局のものか決まらないためで、
26
26
  * どちらも 100ms ごとに流れてくるので待つのは一瞬。**待つ間に届いたものは取っておき**、PMT を読んだら
@@ -44,15 +44,15 @@ export declare class ServiceFilter {
44
44
  /** PMT を読む前に届いた、局の分からないパケット (写したもの)。届いた順 */
45
45
  private held;
46
46
  constructor(serviceId: number);
47
- /** その局の PMT を読めたか。読めるまで映像は1バイトも出ない (届いたものは取っておく) */
47
+ /** その局の PMT を読めたか。読めるまで映像は 1 バイトも出ない (届いたものは取っておく) */
48
48
  get ready(): boolean;
49
49
  /**
50
50
  * **いま流れているものがコピー禁止か** (最後に読んだ PMT のデジタルコピー制御記述子)。
51
- * 番組全体 (1つめのループ) と ES ごと (2つめのループ) のどちらかが禁止なら禁止。
51
+ * 番組全体 (1 つめのループ) と ES ごと (2 つめのループ) のどちらかが禁止なら禁止。
52
52
  * PMT を読むまでは false。
53
53
  *
54
54
  * 録画はこれを見て、禁止のあいだは保存しない (denpa の `recorder.ts`)。番組表が古い・無いときの備え。
55
- * 絞り方には効かせない — ライブは保存しないので、禁止の番組もそのまま観られる
55
+ * 絞り方には効かせない。ライブは保存しないので、禁止の番組もそのまま観られる
56
56
  */
57
57
  get copyNever(): boolean;
58
58
  /**
@@ -60,21 +60,21 @@ export declare class ServiceFilter {
60
60
  *
61
61
  * これが変わったら ffmpeg を作り直す (`StreamsWatch`)。ffmpeg は起きたときの並びのまま
62
62
  * 音声・字幕を選んでいるので、番組が替わって音声の本数や字幕の PID が変わると追従しない。
63
- * データ放送などのセクション (0x0D など) は入れない — 番組ごとに増減しても ffmpeg には関係ない
63
+ * データ放送などのセクション (0x0D など) は入れない。番組ごとに増減しても ffmpeg には関係ない
64
64
  */
65
65
  get streams(): string | null;
66
66
  /**
67
67
  * **中継の番号 (transport_stream_id)。** PAT を読むまでは null。
68
68
  *
69
69
  * 絞るのに要るわけではありません。**データ放送が受信機に訊いてくる**ので
70
- * 覚えておきます — テレ朝の TVerリンクは `browser.getProgramID(4)` で
70
+ * 覚えておきます。テレ朝の TVerリンクは `browser.getProgramID(4)` で
71
71
  * これを取り、TVer のサーバへそのまま送る。`null` のまま渡すと文字の
72
72
  * "null" が飛んで、あちらが断り、画面には `DT-RE` とだけ出ます
73
73
  * ([server/live.ts](https://github.com/danything/denpa/blob/main/src/lib/server/live.ts) の `programInfo`)
74
74
  */
75
75
  get transportStreamId(): number | null;
76
76
  /**
77
- * 任意の長さのバイト列を食わせて、残すぶんを返す。
77
+ * 任意の長さのバイト列を渡すと、残すぶんを返す。
78
78
  *
79
79
  * 落とした PID の連続性カウンタは飛ぶが、**残す PID は元のまま**なので
80
80
  * 読む側から見て抜けは無い。自分で作るのは PAT だけで、そこは自前で数える。
@@ -93,10 +93,10 @@ export declare class ServiceFilter {
93
93
  }
94
94
  /**
95
95
  * **映像・音声・字幕の並びが変わったか** (`ServiceFilter.streams`) を見る。最初に読んだ並びを覚え、変わったら
96
- * 1回だけ true を返して新しい並びを覚え直す。
96
+ * 1 回だけ true を返して新しい並びを覚え直す。
97
97
  *
98
98
  * 並びを決め打ちで起こした ffmpeg (`-map`) は、番組が替わって音声の本数や字幕の PID が変わっても追従しない。
99
- * 変わったら起こし直す — ライブと追っかけのエンコード (denpa の `server/live.ts` の `Session.pump`)、MPEG-2 をそのまま
99
+ * 変わったら起こし直す。使うのはライブと追っかけのエンコード (denpa の `server/live.ts` の `Session.pump`)、MPEG-2 をそのまま
100
100
  * 流す経路の字幕 (`Session.runRaw`・`recordingCaptions`)
101
101
  */
102
102
  export declare class StreamsWatch {