@danything/arib 2.1.0 → 2.3.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
@@ -1,6 +1,6 @@
1
1
  # @danything/arib
2
2
 
3
- 日本のデジタル放送 (ISDB-T / ISDB-S / ISDB-C、ARIB 規格) の MPEG-TS を TypeScript で読むライブラリです。
3
+ 日本のデジタル放送 (ISDB-T / ISDB-S / ISDB-C、ARIB 規格) の MPEG-TS と、4K の放送の MMT/TLV を TypeScript で読むライブラリです。
4
4
  録画サーバ [denpa](https://github.com/danything/denpa) の中で使っているものを切り出しました。
5
5
 
6
6
  - PSI (`psi`): パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む。ケーブルは SDT だけで局をそろえる
@@ -10,8 +10,12 @@
10
10
  - 字幕は STD-B24 の字幕文を、字の置き場所まで決めた JSON にする (`b24caption`、形式は `caption-text`)
11
11
  - データ放送は DSM-CC のカルーセルからモジュールとファイルを組み立てる (`dsmcc`、`multipart`、`bml`、形式は `bml-protocol`)
12
12
  - Hybridcast の AIT を読む (`ait`)。1 つの局だけを抜き出す (`service-filter`)
13
+ - 4K の放送 (MMT/TLV。ARIB STD-B32・B60) の土台として、TLV パケットの切り出し (`tlv`)、IP とヘッダー圧縮した IP から UDP の中身を取り出す処理 (`tlv-ip`)、MMTP のヘッダーとスクランブルの印、信号メッセージの分割と集約 (`mmtp`)
14
+ - 4K の放送の表は、TLV-NIT・PLT・MPT・MH-SDT・MH-TOT (`mmt-si`)。局の一覧と、局ごとの CA_system_id (MPT のアクセス制御記述子) をそろえて読む `TlvServiceReader` もここ (TS の `psi` の `ServiceReader` に当たる)。番組表の MH-EIT は TS と同じ形式の番組にする (`mh-eit`)。1 つの局だけを TLV のまま抜き出す (`mmt-service-filter`)
15
+ - 4K の放送の映像 (HEVC)・音声 (AAC)・字幕 (TTML) のアクセスユニットを、MPT の記述子から付けた PTS・DTS (90 kHz) と一緒に取り出す (`mmt-media`)。音声は LATM を外した AAC のフレームと AudioSpecificConfig、映像は NAL ユニットの並び。字幕の TTML は、TS の字幕と同じ字幕の文字の配置にする (`ttml`)。スクランブルの復号は無い
13
16
  - 試験用の TS (PSI・EIT・字幕・カルーセル・映像と音声) を組み立てる (`synth`、`synth-caption`、`synth-av`)
14
- - ほかのモジュールが使う道具として、バイト列 (`bytes`)、言語の名前 (`language`)、文字の表 (`b24-tables`)、字幕の外字の画像をなめらかにする処理 (`drcs-smooth`)
17
+ - 試験用の TLV (TLV・IP・MMTP・MMT-SI と、MPU の中身・時刻の記述子・LATM) を組み立てる (`synth-mmt`)。本物の HEVC と AAC を時刻を進めて流し続ける (`synth-mmt-av`)
18
+ - ほかのモジュールが使う道具として、バイト列 (`bytes`)、ビット単位の読み取り (`bits`)、言語の名前 (`language`)、文字の表 (`b24-tables`)、字幕の外字の画像をなめらかにする処理 (`drcs-smooth`)
15
19
 
16
20
  依存するパッケージはありません。
17
21
 
@@ -49,6 +53,39 @@ import { decodeAribText } from "@danything/arib/aribtext";
49
53
  decodeAribText(bytes); // "🈟ニュース"
50
54
  ```
51
55
 
56
+ 4K の放送は TLV のバイト列を渡します。番組は TS の `EpgReader` と同じ形式 (`EitEvent`) で返ります。
57
+
58
+ ```ts
59
+ import { MmtEpgReader } from "@danything/arib/mh-eit";
60
+
61
+ const epg = new MmtEpgReader();
62
+ for await (const chunk of tlv) {
63
+ if (epg.feed(chunk)) {
64
+ for (const event of epg.takeChanged()) console.log(event.name); // "🈑報道1930"
65
+ }
66
+ }
67
+ ```
68
+
69
+ 映像・音声・字幕は、1 局に絞った TLV のバイト列から取り出します。局 (MMT_package_id = service_id) を渡します。
70
+
71
+ ```ts
72
+ import { MmtMediaReader } from "@danything/arib/mmt-media";
73
+ import { ttmlPages } from "@danything/arib/ttml";
74
+
75
+ const media = new MmtMediaReader(0xa1);
76
+ for await (const chunk of tlv) {
77
+ for (const unit of media.feed(chunk)) {
78
+ if (unit.kind === "assets") continue; // 選んだアセットが変わった (受け側を作り直す)
79
+ if (unit.kind === "video") decodeHevc(unit.nals, unit.pts, unit.dts); // 時刻は 90 kHz
80
+ if (unit.kind === "audio")
81
+ decodeAac(unit.frame, unit.config.audioSpecificConfig);
82
+ if (unit.kind === "caption")
83
+ for (const { at, page } of ttmlPages(unit.document, unit.pts))
84
+ draw(at, page);
85
+ }
86
+ }
87
+ ```
88
+
52
89
  ```ts
53
90
  import { B24CaptionDecoder } from "@danything/arib/b24caption";
54
91
 
package/dist/bits.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ビット単位の読み取り (大きい桁が先)。LATM の AudioMuxElement (`mmt-media.ts`) と、HEVC の SPS のように
3
+ * バイトの区切りに揃っていない並びを読む。**中身の意味は持たない。**
4
+ *
5
+ * 終わりを越えて読むと 0 を返し、`overrun` を立てる。読む側は最後に `overrun` を見て、壊れた並びを捨てる
6
+ * (1 ビットごとに長さを確かめなくて済む)
7
+ */
8
+ export declare class BitReader {
9
+ private readonly data;
10
+ /** 次に読むビットの位置 (先頭から) */
11
+ at: number;
12
+ /** 終わりを越えて読もうとした */
13
+ overrun: boolean;
14
+ constructor(data: Uint8Array);
15
+ /** 残りのビット数 */
16
+ get left(): number;
17
+ /** `count` ビット (32 まで) を符号なしの整数で読む */
18
+ bits(count: number): number;
19
+ bit(): number;
20
+ skip(count: number): void;
21
+ /** 指数ゴロム符号 (符号なし。H.264・HEVC の ue(v)) */
22
+ ue(): number;
23
+ /** `from` ビット目から `count` ビットを、先頭に詰めたバイト列にする (端数の後ろは 0) */
24
+ slice(from: number, count: number): Uint8Array;
25
+ }
package/dist/bits.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ビット単位の読み取り (大きい桁が先)。LATM の AudioMuxElement (`mmt-media.ts`) と、HEVC の SPS のように
3
+ * バイトの区切りに揃っていない並びを読む。**中身の意味は持たない。**
4
+ *
5
+ * 終わりを越えて読むと 0 を返し、`overrun` を立てる。読む側は最後に `overrun` を見て、壊れた並びを捨てる
6
+ * (1 ビットごとに長さを確かめなくて済む)
7
+ */
8
+ export class BitReader {
9
+ data;
10
+ /** 次に読むビットの位置 (先頭から) */
11
+ at = 0;
12
+ /** 終わりを越えて読もうとした */
13
+ overrun = false;
14
+ constructor(data) {
15
+ this.data = data;
16
+ }
17
+ /** 残りのビット数 */
18
+ get left() {
19
+ return this.data.length * 8 - this.at;
20
+ }
21
+ /** `count` ビット (32 まで) を符号なしの整数で読む */
22
+ bits(count) {
23
+ let value = 0;
24
+ for (let i = 0; i < count; i++)
25
+ value = value * 2 + this.bit();
26
+ return value;
27
+ }
28
+ bit() {
29
+ const byte = this.data[this.at >> 3];
30
+ if (byte === undefined) {
31
+ this.overrun = true;
32
+ this.at++;
33
+ return 0;
34
+ }
35
+ const value = (byte >> (7 - (this.at & 7))) & 1;
36
+ this.at++;
37
+ return value;
38
+ }
39
+ skip(count) {
40
+ this.at += count;
41
+ if (this.at > this.data.length * 8)
42
+ this.overrun = true;
43
+ }
44
+ /** 指数ゴロム符号 (符号なし。H.264・HEVC の ue(v)) */
45
+ ue() {
46
+ let zeros = 0;
47
+ while (this.bit() === 0) {
48
+ // 32 を越える 0 は壊れた並び (overrun で止まる)
49
+ if (++zeros > 32 || this.overrun) {
50
+ this.overrun = true;
51
+ return 0;
52
+ }
53
+ }
54
+ return 2 ** zeros - 1 + this.bits(zeros);
55
+ }
56
+ /** `from` ビット目から `count` ビットを、先頭に詰めたバイト列にする (端数の後ろは 0) */
57
+ slice(from, count) {
58
+ const out = new Uint8Array(Math.ceil(count / 8));
59
+ const reader = new BitReader(this.data);
60
+ reader.at = from;
61
+ for (let i = 0; i < count; i++)
62
+ if (reader.bit())
63
+ out[i >> 3] |= 0x80 >> (i & 7);
64
+ return out;
65
+ }
66
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
3
3
  *
4
- * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒か 4 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
4
+ * 放送は置き換え表で字にできない外字を、16〜72 画素の白黒か 4 階調・16 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
5
5
  * (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
6
6
  * なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
3
3
  *
4
- * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒か 4 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
4
+ * 放送は置き換え表で字にできない外字を、16〜72 画素の白黒か 4 階調・16 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
5
5
  * (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
6
6
  * なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
7
7
  *
package/dist/eit.d.ts CHANGED
@@ -259,6 +259,11 @@ export declare class EpgReader {
259
259
  });
260
260
  /** 任意の長さのバイト列を渡す。番組が 1 つでも増えたら true */
261
261
  feed(chunk: Uint8Array): boolean;
262
+ /**
263
+ * 読んだセクションを 1 つ足す。番組が 1 つでも増えたら true。
264
+ * 4K の放送の番組表 (`mh-eit.ts` の `MmtEpgReader`) は、読み方だけを替えてここに渡す
265
+ */
266
+ protected add(section: EitSection): boolean;
262
267
  /**
263
268
  * 溜めてあるものと混ぜる。**別の表で来るものを消し合わせない。**
264
269
  *
@@ -331,10 +336,12 @@ export declare class EpgReader {
331
336
  *
332
337
  * **中身で比べる。** 書き換われば version が上がるはずだが、上げずに中身だけ替える
333
338
  * 送り手を当てにしないよう、番組の番号・時刻・尺・タイトルで比べる。p/f は 1 局ぶん数百バイトが
334
- * 数秒おきに来るだけなので、毎回読んでも軽い
339
+ * 数秒おきに来るだけなので、毎回読んでも軽い。
340
+ *
341
+ * 4K の放送 (MMT/TLV) の MH-EIT[p/f] は、読み方だけを替えた `mh-eit.ts` の `MmtPresentFollowing` で追う
335
342
  */
336
343
  export declare class PresentFollowing {
337
- private readonly serviceId;
344
+ protected readonly serviceId: number;
338
345
  private readonly sections;
339
346
  /** section 0 (いま) と 1 (次) で最後に読んだ番組。来ていなければ null */
340
347
  private readonly held;
@@ -351,5 +358,10 @@ export declare class PresentFollowing {
351
358
  * 最初に読めたときも true (知らなかったものが分かった)
352
359
  */
353
360
  feedPacket(packet: Uint8Array): boolean;
361
+ /**
362
+ * 読んだ p/f[actual] のセクションを 1 本覚える。**いまか次のどちらかが変わったら true**。
363
+ * 渡すのは追う局のものだけ (table_id と service_id は呼ぶ側が確かめる)
364
+ */
365
+ protected hold(section: EitSection | null): boolean;
354
366
  }
355
367
  export {};
package/dist/eit.js CHANGED
@@ -567,30 +567,36 @@ export class EpgReader {
567
567
  for (const packet of this.packets.feed(chunk)) {
568
568
  for (const raw of this.sections.feed(packet)) {
569
569
  const section = parseEit(raw, this.options);
570
- if (section === null)
571
- continue;
572
- if (section.tableId >= SCHEDULE_ACTUAL_MIN) {
573
- if (!this.progress.has(section.serviceId))
574
- this.lastNewService = this.now();
575
- const progress = this.progress.get(section.serviceId) ??
576
- new ScheduleProgress(this.now);
577
- progress.add(section);
578
- this.progress.set(section.serviceId, progress);
579
- }
580
- for (const event of section.events) {
581
- if (section.tableId === TABLE_PF_ACTUAL &&
582
- this.onAir(section, event)) {
583
- this.present.set(event.serviceId, event);
584
- }
585
- if (event.startAt === null || event.duration === null)
586
- continue;
587
- this.merge(event);
570
+ if (section !== null && this.add(section))
588
571
  added = true;
589
- }
590
572
  }
591
573
  }
592
574
  return added;
593
575
  }
576
+ /**
577
+ * 読んだセクションを 1 つ足す。番組が 1 つでも増えたら true。
578
+ * 4K の放送の番組表 (`mh-eit.ts` の `MmtEpgReader`) は、読み方だけを替えてここに渡す
579
+ */
580
+ add(section) {
581
+ let added = false;
582
+ if (section.tableId >= SCHEDULE_ACTUAL_MIN) {
583
+ if (!this.progress.has(section.serviceId))
584
+ this.lastNewService = this.now();
585
+ const progress = this.progress.get(section.serviceId) ?? new ScheduleProgress(this.now);
586
+ progress.add(section);
587
+ this.progress.set(section.serviceId, progress);
588
+ }
589
+ for (const event of section.events) {
590
+ if (section.tableId === TABLE_PF_ACTUAL && this.onAir(section, event)) {
591
+ this.present.set(event.serviceId, event);
592
+ }
593
+ if (event.startAt === null || event.duration === null)
594
+ continue;
595
+ this.merge(event);
596
+ added = true;
597
+ }
598
+ return added;
599
+ }
594
600
  /**
595
601
  * 溜めてあるものと混ぜる。**別の表で来るものを消し合わせない。**
596
602
  *
@@ -735,7 +741,9 @@ export class EpgReader {
735
741
  *
736
742
  * **中身で比べる。** 書き換われば version が上がるはずだが、上げずに中身だけ替える
737
743
  * 送り手を当てにしないよう、番組の番号・時刻・尺・タイトルで比べる。p/f は 1 局ぶん数百バイトが
738
- * 数秒おきに来るだけなので、毎回読んでも軽い
744
+ * 数秒おきに来るだけなので、毎回読んでも軽い。
745
+ *
746
+ * 4K の放送 (MMT/TLV) の MH-EIT[p/f] は、読み方だけを替えた `mh-eit.ts` の `MmtPresentFollowing` で追う
739
747
  */
740
748
  export class PresentFollowing {
741
749
  serviceId;
@@ -768,20 +776,27 @@ export class PresentFollowing {
768
776
  continue;
769
777
  if (u16(raw, 3) !== this.serviceId)
770
778
  continue;
771
- const section = parseEit(raw);
772
- if (section === null || section.sectionNumber > 1)
773
- continue;
774
- const slot = section.sectionNumber;
775
- const event = section.events[0] ?? null;
776
- const mark = event === null ? "" : markOf(event);
777
- if (this.marks[slot] === mark)
778
- continue;
779
- this.marks[slot] = mark;
780
- this.held[slot] = event;
781
- changed = true;
779
+ if (this.hold(parseEit(raw)))
780
+ changed = true;
782
781
  }
783
782
  return changed;
784
783
  }
784
+ /**
785
+ * 読んだ p/f[actual] のセクションを 1 本覚える。**いまか次のどちらかが変わったら true**。
786
+ * 渡すのは追う局のものだけ (table_id と service_id は呼ぶ側が確かめる)
787
+ */
788
+ hold(section) {
789
+ if (section === null || section.sectionNumber > 1)
790
+ return false;
791
+ const slot = section.sectionNumber;
792
+ const event = section.events[0] ?? null;
793
+ const mark = event === null ? "" : markOf(event);
794
+ if (this.marks[slot] === mark)
795
+ return false;
796
+ this.marks[slot] = mark;
797
+ this.held[slot] = event;
798
+ return true;
799
+ }
785
800
  }
786
801
  /** 変わったかどうかを見る中身。**番号・時刻・尺・タイトルだけ** (番組表に入れて画面に出るもの) */
787
802
  function markOf(event) {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 4K の放送 (MMT/TLV) の番組表 MH-EIT (ARIB STD-B60) を、TS の番組表と同じ `EitEvent` に読む。
3
+ *
4
+ * 並びは TS の EIT と同じで、違うのは次の所 (実機のケーブルの BS4K で確かめた)。
5
+ *
6
+ * - TS の transport_stream_id の位置に tlv_stream_id が入る。`EitEvent.transportStreamId` にはそれを入れる
7
+ * - table_id は p/f が 0x8B、予定の基本が 0x8C から、詳細が 0x94 から。**TS の番号 (0x4E・0x50・0x58) に直して返す**
8
+ * ので、揃ったかの数え方 (`ScheduleProgress`) と混ぜ方 (`EpgReader`) をそのまま使える
9
+ * - 記述子のタグが 2 バイトで (`mmt-si.ts` の `mmtDescriptors`)、文字は UTF-8。番組名の「🈑」なども Unicode の文字で来る
10
+ *
11
+ * 番組の記述子で読むのは、MH-短形式イベント (0xF001)・MH-拡張形式イベント (0xF002)・MH-コンテント (0x8012)・
12
+ * 映像コンポーネント (0x8010)・MH-音声コンポーネント (0x8014)・コンテンツコピー制御 (0x8038)。
13
+ * シリーズの記述子は来ていないので読まない (`series` を付けない。「この表は言っていない」)
14
+ */
15
+ import { type EitSection, EpgReader, PresentFollowing } from "./eit.js";
16
+ /**
17
+ * MH-EIT のセクションを 1 本読む。table_id は TS の番号に直す。番組表でなければ null。
18
+ * CRC は読む前に確かめてある (`MmtTableStream`) ので、長さの合わない番組の手前まで返す
19
+ */
20
+ export declare function parseMhEit(section: Uint8Array): EitSection | null;
21
+ /**
22
+ * 流れてくる TLV のバイト列から番組表を溜める。**読み方のほかは TS の `EpgReader` と同じ**
23
+ * (揃ったかの数え方、別の表で来るものの混ぜ方、p/f の「いま」)。
24
+ *
25
+ * MH-EIT は自分の TLV のストリームの局のぶんだけが来る (実機のケーブルの BS4K で確かめた)。
26
+ * TS の `other` (ほかの TS の局の番組表) に当たるものは無い
27
+ */
28
+ export declare class MmtEpgReader extends EpgReader {
29
+ private readonly tables;
30
+ /** @param now いまの時刻。過ぎた時間帯の予定を待たないのに使う (`ScheduleProgress`) */
31
+ constructor(now?: () => number);
32
+ /** 任意の長さの TLV のバイト列を渡す。番組が 1 つでも増えたら true */
33
+ feed(chunk: Uint8Array): boolean;
34
+ }
35
+ /**
36
+ * **1 局の「いま」と「次」(MH-EIT[p/f]) を追う。** TS の `PresentFollowing` を 4K の放送 (MMT/TLV) で使えるようにしたもので、
37
+ * 違うのは読み方だけ (覚え方と、変わったかの比べ方は同じ)。ライブで番組の切り替わりを知るのに使う
38
+ */
39
+ export declare class MmtPresentFollowing extends PresentFollowing {
40
+ private readonly tables;
41
+ /** 任意の長さの TLV のバイト列を渡す。**いまか次のどちらかが変わったら true** (最初に読めたときも true) */
42
+ feed(chunk: Uint8Array): boolean;
43
+ }
package/dist/mh-eit.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * 4K の放送 (MMT/TLV) の番組表 MH-EIT (ARIB STD-B60) を、TS の番組表と同じ `EitEvent` に読む。
3
+ *
4
+ * 並びは TS の EIT と同じで、違うのは次の所 (実機のケーブルの BS4K で確かめた)。
5
+ *
6
+ * - TS の transport_stream_id の位置に tlv_stream_id が入る。`EitEvent.transportStreamId` にはそれを入れる
7
+ * - table_id は p/f が 0x8B、予定の基本が 0x8C から、詳細が 0x94 から。**TS の番号 (0x4E・0x50・0x58) に直して返す**
8
+ * ので、揃ったかの数え方 (`ScheduleProgress`) と混ぜ方 (`EpgReader`) をそのまま使える
9
+ * - 記述子のタグが 2 バイトで (`mmt-si.ts` の `mmtDescriptors`)、文字は UTF-8。番組名の「🈑」なども Unicode の文字で来る
10
+ *
11
+ * 番組の記述子で読むのは、MH-短形式イベント (0xF001)・MH-拡張形式イベント (0xF002)・MH-コンテント (0x8012)・
12
+ * 映像コンポーネント (0x8010)・MH-音声コンポーネント (0x8014)・コンテンツコピー制御 (0x8038)。
13
+ * シリーズの記述子は来ていないので読まない (`series` を付けない。「この表は言っていない」)
14
+ */
15
+ import { ascii, joinBytes, u16 } from "./bytes.js";
16
+ import { EpgReader, parseBcdDuration, parseMjdTime, PresentFollowing, } from "./eit.js";
17
+ import { mmtDescriptors, MmtTableStream, mmtText } from "./mmt-si.js";
18
+ import { isCopyNever } from "./psi.js";
19
+ const MH_PF_ACTUAL = 0x8b;
20
+ const MH_SCHEDULE_MIN = 0x8c;
21
+ const MH_SCHEDULE_MAX = 0x9b;
22
+ /** TS の EIT の番号 (p/f 0x4E、予定 0x50〜0x5F) */
23
+ const TS_PF_ACTUAL = 0x4e;
24
+ const TS_SCHEDULE_MIN = 0x50;
25
+ /** TS の詳細の表の始まり。これより後ろの表は基本の記述子 (コピー制御など) を載せない */
26
+ const TS_SCHEDULE_EXTENDED = 0x58;
27
+ const DESC_SHORT_EVENT = 0xf001;
28
+ const DESC_EXTENDED_EVENT = 0xf002;
29
+ const DESC_CONTENT = 0x8012;
30
+ const DESC_VIDEO_COMPONENT = 0x8010;
31
+ const DESC_AUDIO_COMPONENT = 0x8014;
32
+ const DESC_CONTENT_COPY_CONTROL = 0x8038;
33
+ /** MH-EIT の table_id を TS の EIT の番号に。番組表でなければ null */
34
+ function tsTableId(tableId) {
35
+ if (tableId === MH_PF_ACTUAL)
36
+ return TS_PF_ACTUAL;
37
+ if (tableId >= MH_SCHEDULE_MIN && tableId <= MH_SCHEDULE_MAX)
38
+ return TS_SCHEDULE_MIN + (tableId - MH_SCHEDULE_MIN);
39
+ return null;
40
+ }
41
+ /** video_resolution (4 ビット) の走査線の数。0 は未定義 */
42
+ const LINES = {
43
+ 1: 180,
44
+ 2: 240,
45
+ 3: 480,
46
+ 4: 720,
47
+ 5: 1080,
48
+ 6: 2160,
49
+ 7: 4320,
50
+ };
51
+ /** sampling_rate (3 ビット)。TS の音声コンポーネント記述子と同じ符号 */
52
+ const SAMPLING_RATE = {
53
+ 1: 16000,
54
+ 2: 22050,
55
+ 3: 24000,
56
+ 5: 32000,
57
+ 6: 44100,
58
+ 7: 48000,
59
+ };
60
+ const lang = (data, at) => ascii(data, at, 3).toLowerCase();
61
+ /**
62
+ * 番組の記述子を読んで埋める。**拡張形式イベントは項目ごとに、バイトのまま繋いでから文字にする。**
63
+ * UTF-8 の 1 文字が記述子の切れ目をまたぐことがあり、部分ごとに文字にすると、またいだ字が U+FFFD になる。
64
+ * 見出しが空の項目は直前の項目の続き (TS の `eit.ts` と同じ決まり)
65
+ */
66
+ function readDescriptors(event, body) {
67
+ const pending = new Map();
68
+ let lastHeading = "";
69
+ for (const [tag, data] of mmtDescriptors(body)) {
70
+ switch (tag) {
71
+ case DESC_SHORT_EVENT: {
72
+ // 言語 (3)・番組名の長さ (1)・番組名・説明の長さ (2)・説明
73
+ if (data.length < 4)
74
+ break;
75
+ const textAt = 4 + data[3];
76
+ event.name = mmtText(data.subarray(4, textAt));
77
+ if (textAt + 2 > data.length)
78
+ break;
79
+ event.description = mmtText(data.subarray(textAt + 2, textAt + 2 + u16(data, textAt)));
80
+ break;
81
+ }
82
+ case DESC_EXTENDED_EVENT: {
83
+ // 番号 (1)・言語 (3)・項目の長さ (2)。項目は見出し (長さ 1 バイト) と本文 (長さ 2 バイト) の組
84
+ if (data.length < 6)
85
+ break;
86
+ const itemsEnd = Math.min(data.length, 6 + u16(data, 4));
87
+ let at = 6;
88
+ while (at + 1 <= itemsEnd) {
89
+ const headingEnd = at + 1 + data[at];
90
+ if (headingEnd + 2 > itemsEnd)
91
+ break;
92
+ const heading = mmtText(data.subarray(at + 1, headingEnd));
93
+ const textEnd = headingEnd + 2 + u16(data, headingEnd);
94
+ const text = data.subarray(headingEnd + 2, textEnd);
95
+ at = textEnd;
96
+ if (heading === "") {
97
+ if (lastHeading === "")
98
+ continue;
99
+ pending.get(lastHeading).at(-1).push(text);
100
+ continue;
101
+ }
102
+ const items = pending.get(heading);
103
+ if (items === undefined)
104
+ pending.set(heading, [[text]]);
105
+ else
106
+ items.push([text]);
107
+ lastHeading = heading;
108
+ }
109
+ break;
110
+ }
111
+ case DESC_CONTENT: {
112
+ for (let at = 0; at + 2 <= data.length; at += 2)
113
+ event.genres.push({ lv1: data[at] >> 4, lv2: data[at] & 0x0f });
114
+ break;
115
+ }
116
+ case DESC_VIDEO_COMPONENT: {
117
+ // video_resolution (4)・アスペクト比 (4)、走査 (1: プログレッシブ)・予約 (2)・コマの速さ (5)
118
+ if (data.length < 2)
119
+ break;
120
+ const lines = LINES[data[0] >> 4];
121
+ const progressive = (data[1] & 0x80) !== 0;
122
+ // 符号化の方式はこの記述子に無い (MPT のアセットの型で分かる)
123
+ event.video = {
124
+ type: null,
125
+ resolution: lines === undefined ? null : `${lines}${progressive ? "p" : "i"}`,
126
+ };
127
+ break;
128
+ }
129
+ /*
130
+ * MH-音声コンポーネント記述子。TS の音声コンポーネント記述子と比べて、component_tag が 2 バイトになっている。
131
+ *
132
+ * [0] reserved(4) stream_content(4)
133
+ * [1] component_type
134
+ * [2..3] component_tag
135
+ * [4] stream_type
136
+ * [5] simulcast_group_tag
137
+ * [6] 多言語(1) 主音声(1) 品質(2) 標本化周波数(3) reserved(1)
138
+ * [7..9] ISO 639-2
139
+ * [10..12] ISO 639-2 (多言語のときだけ)
140
+ * [残り] text_char (UTF-8)
141
+ */
142
+ case DESC_AUDIO_COMPONENT: {
143
+ if (data.length < 10)
144
+ break;
145
+ const flags = data[6];
146
+ const multiLingual = (flags & 0x80) !== 0;
147
+ const langs = [lang(data, 7)];
148
+ if (multiLingual)
149
+ langs.push(lang(data, 10));
150
+ const text = mmtText(data.subarray(multiLingual ? 13 : 10)).trim();
151
+ const samplingRate = SAMPLING_RATE[(flags >> 1) & 0x07];
152
+ event.audios.push({
153
+ componentType: data[1],
154
+ langs,
155
+ ...(samplingRate === undefined ? {} : { samplingRate }),
156
+ ...(text === "" ? {} : { text }),
157
+ main: (flags & 0x40) !== 0,
158
+ });
159
+ break;
160
+ }
161
+ case DESC_CONTENT_COPY_CONTROL: {
162
+ // 先頭 2 ビットが digital_recording_control_data で、TS のデジタルコピー制御記述子と同じ符号
163
+ event.copyNever = isCopyNever(data);
164
+ break;
165
+ }
166
+ default:
167
+ break;
168
+ }
169
+ }
170
+ for (const [heading, items] of pending)
171
+ event.extended[heading] = items
172
+ .map((parts) => mmtText(joinBytes(parts)))
173
+ .join("");
174
+ }
175
+ /**
176
+ * MH-EIT のセクションを 1 本読む。table_id は TS の番号に直す。番組表でなければ null。
177
+ * CRC は読む前に確かめてある (`MmtTableStream`) ので、長さの合わない番組の手前まで返す
178
+ */
179
+ export function parseMhEit(section) {
180
+ if (section.length < 18)
181
+ return null;
182
+ const tableId = tsTableId(section[0]);
183
+ if (tableId === null)
184
+ return null;
185
+ const extended = tableId >= TS_SCHEDULE_EXTENDED;
186
+ const lastTableId = tsTableId(section[13]) ?? tableId;
187
+ const result = {
188
+ tableId,
189
+ serviceId: u16(section, 3),
190
+ version: (section[5] >> 1) & 0x1f,
191
+ sectionNumber: section[6],
192
+ lastSectionNumber: section[7],
193
+ transportStreamId: u16(section, 8),
194
+ originalNetworkId: u16(section, 10),
195
+ segmentLastSectionNumber: section[12],
196
+ lastTableId,
197
+ events: [],
198
+ };
199
+ let at = 14;
200
+ const end = section.length - 4;
201
+ while (at + 12 <= end) {
202
+ const descriptorsLength = u16(section, at + 10) & 0x0fff;
203
+ const body = section.subarray(at + 12, at + 12 + descriptorsLength);
204
+ const event = {
205
+ serviceId: result.serviceId,
206
+ transportStreamId: result.transportStreamId,
207
+ originalNetworkId: result.originalNetworkId,
208
+ eventId: u16(section, at),
209
+ startAt: parseMjdTime(section, at + 2),
210
+ duration: parseBcdDuration(section, at + 7),
211
+ runningStatus: (section[at + 10] >> 5) & 0x07,
212
+ isFree: (section[at + 10] & 0x10) === 0,
213
+ name: "",
214
+ description: "",
215
+ extended: {},
216
+ genres: [],
217
+ audios: [],
218
+ video: null,
219
+ copyNever: extended ? null : false,
220
+ // B-CAS の契約確認は使わない
221
+ ...(extended ? {} : { contractVerification: null }),
222
+ };
223
+ at += 12 + descriptorsLength;
224
+ if (body.length < descriptorsLength)
225
+ break;
226
+ readDescriptors(event, body);
227
+ result.events.push(event);
228
+ }
229
+ return result;
230
+ }
231
+ /**
232
+ * 流れてくる TLV のバイト列から番組表を溜める。**読み方のほかは TS の `EpgReader` と同じ**
233
+ * (揃ったかの数え方、別の表で来るものの混ぜ方、p/f の「いま」)。
234
+ *
235
+ * MH-EIT は自分の TLV のストリームの局のぶんだけが来る (実機のケーブルの BS4K で確かめた)。
236
+ * TS の `other` (ほかの TS の局の番組表) に当たるものは無い
237
+ */
238
+ export class MmtEpgReader extends EpgReader {
239
+ tables = new MmtTableStream();
240
+ /** @param now いまの時刻。過ぎた時間帯の予定を待たないのに使う (`ScheduleProgress`) */
241
+ constructor(now = Date.now) {
242
+ super(now);
243
+ }
244
+ /** 任意の長さの TLV のバイト列を渡す。番組が 1 つでも増えたら true */
245
+ feed(chunk) {
246
+ let added = false;
247
+ for (const { table } of this.tables.feed(chunk)) {
248
+ const section = parseMhEit(table);
249
+ if (section !== null && this.add(section))
250
+ added = true;
251
+ }
252
+ return added;
253
+ }
254
+ }
255
+ /**
256
+ * **1 局の「いま」と「次」(MH-EIT[p/f]) を追う。** TS の `PresentFollowing` を 4K の放送 (MMT/TLV) で使えるようにしたもので、
257
+ * 違うのは読み方だけ (覚え方と、変わったかの比べ方は同じ)。ライブで番組の切り替わりを知るのに使う
258
+ */
259
+ export class MmtPresentFollowing extends PresentFollowing {
260
+ tables = new MmtTableStream();
261
+ /** 任意の長さの TLV のバイト列を渡す。**いまか次のどちらかが変わったら true** (最初に読めたときも true) */
262
+ feed(chunk) {
263
+ let changed = false;
264
+ for (const { table } of this.tables.feed(chunk)) {
265
+ // 予定 (0x8C〜) は読まずに捨てる。同じ並びに流れてくる
266
+ if (table[0] !== MH_PF_ACTUAL || table.length < 18)
267
+ continue;
268
+ if (u16(table, 3) !== this.serviceId)
269
+ continue;
270
+ if (this.hold(parseMhEit(table)))
271
+ changed = true;
272
+ }
273
+ return changed;
274
+ }
275
+ }