@danything/arib 2.0.0 → 2.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
@@ -1,17 +1,21 @@
1
1
  # @danything/arib
2
2
 
3
- 日本のデジタル放送 (ISDB-T / ISDB-S、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
- - PSI (`psi`): パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む
6
+ - PSI (`psi`): パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む。ケーブルは SDT だけで局をそろえる
7
7
  - 映像・音声の PES と ADTS を取り出す (`pes`)
8
8
  - 番組表の EIT[p/f] と EIT[schedule] を、ジャンル・音声・拡張形式イベント・シリーズ記述子まで読む。放送の時刻 (TDT / TOT) もここ (`eit`)
9
9
  - ARIB STD-B24 の 8 単位符号 (JIS X 0213 の 1 面・2 面まで) と ARIB 外字を文字列にする (`aribtext`、`aribtext-gaiji`)
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
 
@@ -299,13 +299,16 @@ export class B24CaptionDecoder {
299
299
  return;
300
300
  const found = [];
301
301
  for (let i = 0; i < languages; i++) {
302
- if (at + 6 > data.length)
302
+ if (at + 5 > data.length)
303
303
  return;
304
304
  const tag = data[at] >> 5;
305
305
  const dmf = data[at] & 0x0f;
306
306
  at += 1;
307
+ // DC (表示モード) が 1 バイト挟まる
307
308
  if (dmf === 0b1100 || dmf === 0b1101 || dmf === 0b1110)
308
309
  at += 1;
310
+ if (at + 4 > data.length)
311
+ return;
309
312
  const code = ascii(data, at, 3);
310
313
  at += 3; // ISO 639
311
314
  const format = data[at] >> 4;
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
+ }
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
@@ -159,10 +159,12 @@ function readDescriptors(event, body) {
159
159
  for (const [tag, data] of descriptors(body)) {
160
160
  switch (tag) {
161
161
  case DESC_SHORT_EVENT: {
162
+ if (data.length < 4)
163
+ break;
162
164
  const nameLength = data[3];
163
165
  const textAt = 4 + nameLength;
164
166
  event.name = decodeAribText(data.subarray(4, textAt));
165
- const textLength = data[textAt];
167
+ const textLength = data[textAt] ?? 0;
166
168
  event.description = decodeAribText(data.subarray(textAt + 1, textAt + 1 + textLength));
167
169
  break;
168
170
  }
@@ -199,6 +201,9 @@ function readDescriptors(event, body) {
199
201
  break;
200
202
  }
201
203
  case DESC_COMPONENT: {
204
+ // 短いものを読むと、種別 0 の「480i」と出る
205
+ if (data.length < 2)
206
+ break;
202
207
  const streamContent = data[0] & 0x0f;
203
208
  const componentType = data[1];
204
209
  event.video = {
@@ -221,6 +226,9 @@ function readDescriptors(event, body) {
221
226
  * [残り] text_char … **放送が付けた名前**
222
227
  */
223
228
  case DESC_AUDIO_COMPONENT: {
229
+ // 言語の符号まで無いものを読むと、言語が空の音声が 1 本増える
230
+ if (data.length < 9)
231
+ break;
224
232
  const flags = data[5];
225
233
  const multiLingual = (flags & 0x80) !== 0;
226
234
  const langs = [lang(data, 6)];
@@ -245,7 +253,7 @@ function readDescriptors(event, body) {
245
253
  case DESC_CA_CONTRACT_INFO: {
246
254
  if (data.length < 4)
247
255
  break;
248
- const caSystemId = (data[0] << 8) | data[1];
256
+ const caSystemId = u16(data, 0);
249
257
  const numOfComponent = data[2] & 0x0f;
250
258
  let at = 3 + numOfComponent;
251
259
  if (at >= data.length)
@@ -559,30 +567,36 @@ export class EpgReader {
559
567
  for (const packet of this.packets.feed(chunk)) {
560
568
  for (const raw of this.sections.feed(packet)) {
561
569
  const section = parseEit(raw, this.options);
562
- if (section === null)
563
- continue;
564
- if (section.tableId >= SCHEDULE_ACTUAL_MIN) {
565
- if (!this.progress.has(section.serviceId))
566
- this.lastNewService = this.now();
567
- const progress = this.progress.get(section.serviceId) ??
568
- new ScheduleProgress(this.now);
569
- progress.add(section);
570
- this.progress.set(section.serviceId, progress);
571
- }
572
- for (const event of section.events) {
573
- if (section.tableId === TABLE_PF_ACTUAL &&
574
- this.onAir(section, event)) {
575
- this.present.set(event.serviceId, event);
576
- }
577
- if (event.startAt === null || event.duration === null)
578
- continue;
579
- this.merge(event);
570
+ if (section !== null && this.add(section))
580
571
  added = true;
581
- }
582
572
  }
583
573
  }
584
574
  return added;
585
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
+ }
586
600
  /**
587
601
  * 溜めてあるものと混ぜる。**別の表で来るものを消し合わせない。**
588
602
  *
@@ -727,7 +741,9 @@ export class EpgReader {
727
741
  *
728
742
  * **中身で比べる。** 書き換われば version が上がるはずだが、上げずに中身だけ替える
729
743
  * 送り手を当てにしないよう、番組の番号・時刻・尺・タイトルで比べる。p/f は 1 局ぶん数百バイトが
730
- * 数秒おきに来るだけなので、毎回読んでも軽い
744
+ * 数秒おきに来るだけなので、毎回読んでも軽い。
745
+ *
746
+ * 4K の放送 (MMT/TLV) の MH-EIT[p/f] は、読み方だけを替えた `mh-eit.ts` の `MmtPresentFollowing` で追う
731
747
  */
732
748
  export class PresentFollowing {
733
749
  serviceId;
@@ -760,20 +776,27 @@ export class PresentFollowing {
760
776
  continue;
761
777
  if (u16(raw, 3) !== this.serviceId)
762
778
  continue;
763
- const section = parseEit(raw);
764
- if (section === null || section.sectionNumber > 1)
765
- continue;
766
- const slot = section.sectionNumber;
767
- const event = section.events[0] ?? null;
768
- const mark = event === null ? "" : markOf(event);
769
- if (this.marks[slot] === mark)
770
- continue;
771
- this.marks[slot] = mark;
772
- this.held[slot] = event;
773
- changed = true;
779
+ if (this.hold(parseEit(raw)))
780
+ changed = true;
774
781
  }
775
782
  return changed;
776
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
+ }
777
800
  }
778
801
  /** 変わったかどうかを見る中身。**番号・時刻・尺・タイトルだけ** (番組表に入れて画面に出るもの) */
779
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
+ }