@danything/arib 2.1.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * TLV パケットに入った IP を読み、UDP の中身 (MMTP・NTP) を返す (ARIB STD-B32 第 3 部)。
3
+ *
4
+ * 4K の放送の映像・音声・信号は、IPv6 の UDP で運ぶ MMTP に入っている。帯域を節約するため、
5
+ * ほとんどのパケットは IP と UDP のヘッダーを省いた「ヘッダー圧縮した IP」(TLV の種類 0x03) で来る。
6
+ * 同じ宛先の並びには CID (context_id) が振られ、ときどき来る全部のヘッダー (圧縮の種類 0x60) で
7
+ * 宛先を知らせ、あいだは CID だけ (0x61) になる。
8
+ *
9
+ * 実機 (ケーブルで受けた BS4K) で来たのは、0x60 と 0x61 のほかは NTP を運ぶ素の IPv6 だけだった。
10
+ * IPv4 の圧縮 (0x20・0x21) は来ていないので読まない。
11
+ */
12
+ import { type TlvPacket } from "./tlv.js";
13
+ /** UDP の宛先と送り元 */
14
+ export interface UdpAddress {
15
+ source: string;
16
+ destination: string;
17
+ sourcePort: number;
18
+ destinationPort: number;
19
+ }
20
+ export interface UdpDatagram {
21
+ /**
22
+ * 同じ宛先の並びを見分けるキー。圧縮したものは `cid:<CID>`、圧縮していないものは宛先のアドレスとポート。
23
+ * MMTP の packet_id はこの並びの中でだけ決まっている
24
+ */
25
+ flow: string;
26
+ /** 宛先と送り元。0x61 の CID の 0x60 をまだ受けていなければ null */
27
+ address: UdpAddress | null;
28
+ payload: Uint8Array;
29
+ }
30
+ /** IPv6 のアドレス 16 バイトを文字列に (0 の続く所は縮めず、各グループの先頭の 0 だけ落とす) */
31
+ export declare function ipv6Text(data: Uint8Array, at: number): string;
32
+ /**
33
+ * TLV パケットから UDP を取り出す。**CID ごとの宛先を覚える**ので、1 つの TLV のストリームに 1 つ使う。
34
+ *
35
+ * 圧縮したものは、CID の宛先をまだ知らなくても中身を返す (`address` が null)。
36
+ * 圧縮できるのは UDP だけなので中身は UDP の中身と決まっていて、MMTP は宛先を知らなくても読める。
37
+ * 全部のヘッダーは 1 秒に 10 回ほどしか来ないので、待つと選局したての 0.1 秒ぶんを捨てることになる
38
+ */
39
+ export declare class TlvIpReader {
40
+ private readonly contexts;
41
+ /** IP を運ぶ TLV パケットでなければ・UDP でなければ null */
42
+ read(packet: TlvPacket): UdpDatagram | null;
43
+ private compressed;
44
+ }
package/dist/tlv-ip.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * TLV パケットに入った IP を読み、UDP の中身 (MMTP・NTP) を返す (ARIB STD-B32 第 3 部)。
3
+ *
4
+ * 4K の放送の映像・音声・信号は、IPv6 の UDP で運ぶ MMTP に入っている。帯域を節約するため、
5
+ * ほとんどのパケットは IP と UDP のヘッダーを省いた「ヘッダー圧縮した IP」(TLV の種類 0x03) で来る。
6
+ * 同じ宛先の並びには CID (context_id) が振られ、ときどき来る全部のヘッダー (圧縮の種類 0x60) で
7
+ * 宛先を知らせ、あいだは CID だけ (0x61) になる。
8
+ *
9
+ * 実機 (ケーブルで受けた BS4K) で来たのは、0x60 と 0x61 のほかは NTP を運ぶ素の IPv6 だけだった。
10
+ * IPv4 の圧縮 (0x20・0x21) は来ていないので読まない。
11
+ */
12
+ import { u16 } from "./bytes.js";
13
+ import { TLV_COMPRESSED_IP, TLV_IPV4, TLV_IPV6, } from "./tlv.js";
14
+ /** 0x60: IPv6 のヘッダー (payload_length を除いた 38 バイト) と UDP のヘッダー (length と checksum を除いた 4 バイト) */
15
+ const COMPRESSED_IPV6_UDP = 0x60;
16
+ /** 0x61: ヘッダー無し。宛先は同じ CID の 0x60 で知らせたもの */
17
+ const COMPRESSED_NONE = 0x61;
18
+ const COMPRESSED_HEAD = 3;
19
+ const PARTIAL_IPV6 = 38;
20
+ const PARTIAL_UDP = 4;
21
+ const IPV6_HEADER = 40;
22
+ const UDP_HEADER = 8;
23
+ const PROTOCOL_UDP = 17;
24
+ /** IPv6 のアドレス 16 バイトを文字列に (0 の続く所は縮めず、各グループの先頭の 0 だけ落とす) */
25
+ export function ipv6Text(data, at) {
26
+ const groups = [];
27
+ for (let i = 0; i < 16; i += 2)
28
+ groups.push(u16(data, at + i).toString(16));
29
+ return groups.join(":");
30
+ }
31
+ function ipv4Text(data, at) {
32
+ return [0, 1, 2, 3].map((i) => data[at + i]).join(".");
33
+ }
34
+ /**
35
+ * TLV パケットから UDP を取り出す。**CID ごとの宛先を覚える**ので、1 つの TLV のストリームに 1 つ使う。
36
+ *
37
+ * 圧縮したものは、CID の宛先をまだ知らなくても中身を返す (`address` が null)。
38
+ * 圧縮できるのは UDP だけなので中身は UDP の中身と決まっていて、MMTP は宛先を知らなくても読める。
39
+ * 全部のヘッダーは 1 秒に 10 回ほどしか来ないので、待つと選局したての 0.1 秒ぶんを捨てることになる
40
+ */
41
+ export class TlvIpReader {
42
+ contexts = new Map();
43
+ /** IP を運ぶ TLV パケットでなければ・UDP でなければ null */
44
+ read(packet) {
45
+ const data = packet.payload;
46
+ switch (packet.type) {
47
+ case TLV_COMPRESSED_IP:
48
+ return this.compressed(data);
49
+ case TLV_IPV6:
50
+ return ipv6(data);
51
+ case TLV_IPV4:
52
+ return ipv4(data);
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+ compressed(data) {
58
+ if (data.length < COMPRESSED_HEAD)
59
+ return null;
60
+ // CID (12 ビット) と sequence number (4 ビット)、圧縮の種類
61
+ const cid = u16(data, 0) >> 4;
62
+ const flow = `cid:${cid}`;
63
+ switch (data[2]) {
64
+ case COMPRESSED_IPV6_UDP: {
65
+ const at = COMPRESSED_HEAD;
66
+ const payloadAt = at + PARTIAL_IPV6 + PARTIAL_UDP;
67
+ if (data.length < payloadAt)
68
+ return null;
69
+ // 38 バイトの並びは version・traffic class・flow label (4)、next header (1)、hop limit (1)、送り元 (16)、宛先 (16)
70
+ const address = {
71
+ source: ipv6Text(data, at + 6),
72
+ destination: ipv6Text(data, at + 22),
73
+ sourcePort: u16(data, at + PARTIAL_IPV6),
74
+ destinationPort: u16(data, at + PARTIAL_IPV6 + 2),
75
+ };
76
+ this.contexts.set(cid, address);
77
+ return { flow, address, payload: data.subarray(payloadAt) };
78
+ }
79
+ case COMPRESSED_NONE:
80
+ return {
81
+ flow,
82
+ address: this.contexts.get(cid) ?? null,
83
+ payload: data.subarray(COMPRESSED_HEAD),
84
+ };
85
+ default:
86
+ return null;
87
+ }
88
+ }
89
+ }
90
+ /** 素の IPv6 の UDP。拡張ヘッダーの付いたものは読まない (来ていない) */
91
+ function ipv6(data) {
92
+ if (data.length < IPV6_HEADER + UDP_HEADER || data[6] !== PROTOCOL_UDP)
93
+ return null;
94
+ const end = Math.min(data.length, IPV6_HEADER + u16(data, 4));
95
+ return udp(data, ipv6Text(data, 8), ipv6Text(data, 24), IPV6_HEADER, end);
96
+ }
97
+ function ipv4(data) {
98
+ if (data.length < 20 || data[9] !== PROTOCOL_UDP)
99
+ return null;
100
+ const header = (data[0] & 0x0f) * 4;
101
+ const end = Math.min(data.length, u16(data, 2));
102
+ return udp(data, ipv4Text(data, 12), ipv4Text(data, 16), header, end);
103
+ }
104
+ function udp(data, source, destination, at, end) {
105
+ if (at + UDP_HEADER > end)
106
+ return null;
107
+ const address = {
108
+ source,
109
+ destination,
110
+ sourcePort: u16(data, at),
111
+ destinationPort: u16(data, at + 2),
112
+ };
113
+ return {
114
+ flow: `${destination}:${address.destinationPort}`,
115
+ address,
116
+ payload: data.subarray(at + UDP_HEADER, Math.min(end, at + u16(data, at + 4))),
117
+ };
118
+ }
package/dist/tlv.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * TLV パケット (ARIB STD-B32 第 3 部) を切り出す。4K の放送 (MMT/TLV) の土台で、TS の `PacketStream` と同じ役。
3
+ *
4
+ * TLV パケットは `0x7F`・種類 (1 バイト)・長さ (2 バイト) のヘッダーと、長さぶんの中身でできている。
5
+ * 長さが毎回違うので、TS のように決まった間隔で同期のバイトを探すことはできない。
6
+ * エージェントはケーブルの分割 TLV を組み立て直した TLV のバイト列を流す
7
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))。
8
+ */
9
+ export declare const TLV_IPV4 = 1;
10
+ export declare const TLV_IPV6 = 2;
11
+ /** ヘッダー圧縮した IP (`tlv-ip.ts`) */
12
+ export declare const TLV_COMPRESSED_IP = 3;
13
+ /** 伝送制御信号 (TLV-SI。TLV-NIT・AMT のセクションが並ぶ) */
14
+ export declare const TLV_SIGNALING = 254;
15
+ export declare const TLV_NULL = 255;
16
+ export interface TlvPacket {
17
+ type: number;
18
+ /** ヘッダーを除いた中身 */
19
+ payload: Uint8Array;
20
+ /** ヘッダーを含めたパケット全体。1 局に絞るとき (`mmt-service-filter.ts`) はこれをそのまま出す */
21
+ bytes: Uint8Array;
22
+ }
23
+ /**
24
+ * 任意の長さで届く TLV のバイト列を TLV パケットに切り分ける。
25
+ *
26
+ * **先頭がパケットの先頭とは限らない。** 共有に途中から入った利用者は、TLV パケットの途中から受け取る
27
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))。そこで、同期が取れていないときは
28
+ * 先頭のバイトと種類が合い、**長さの先にある次のパケットの先頭も合う**位置を探して合わせる。
29
+ * 1 か所だけで決めると、中身にたまたまある `7F 03` を先頭と取り違える。そのため、同期を取る最初のパケットは次のパケットの先頭が届くまで出さない。
30
+ * 同期が取れたあとは、先頭が合わなくなるまでヘッダーの長さのとおりに進む。
31
+ *
32
+ * 出すパケットの中身は、渡されたチャンクか前の残りの一部を指す (コピーしない)。
33
+ * **次の `feed` より後まで持つならコピーすること** (`PacketStream` と同じ)
34
+ */
35
+ export declare class TlvStream {
36
+ private rest;
37
+ private synced;
38
+ /** 合わせるために読み飛ばしたバイトの合計 */
39
+ skipped: number;
40
+ /** 同期を失って合わせ直した回数 */
41
+ resyncs: number;
42
+ feed(chunk: Uint8Array): Generator<TlvPacket>;
43
+ }
package/dist/tlv.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * TLV パケット (ARIB STD-B32 第 3 部) を切り出す。4K の放送 (MMT/TLV) の土台で、TS の `PacketStream` と同じ役。
3
+ *
4
+ * TLV パケットは `0x7F`・種類 (1 バイト)・長さ (2 バイト) のヘッダーと、長さぶんの中身でできている。
5
+ * 長さが毎回違うので、TS のように決まった間隔で同期のバイトを探すことはできない。
6
+ * エージェントはケーブルの分割 TLV を組み立て直した TLV のバイト列を流す
7
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))。
8
+ */
9
+ import { concatBytes, u16 } from "./bytes.js";
10
+ /** TLV パケットの先頭のバイト (上位 2 ビットが 01、残りは予約で 1) */
11
+ const TLV_SYNC = 0x7f;
12
+ const HEADER = 4;
13
+ export const TLV_IPV4 = 0x01;
14
+ export const TLV_IPV6 = 0x02;
15
+ /** ヘッダー圧縮した IP (`tlv-ip.ts`) */
16
+ export const TLV_COMPRESSED_IP = 0x03;
17
+ /** 伝送制御信号 (TLV-SI。TLV-NIT・AMT のセクションが並ぶ) */
18
+ export const TLV_SIGNALING = 0xfe;
19
+ export const TLV_NULL = 0xff;
20
+ const TYPES = new Set([
21
+ TLV_IPV4,
22
+ TLV_IPV6,
23
+ TLV_COMPRESSED_IP,
24
+ TLV_SIGNALING,
25
+ TLV_NULL,
26
+ ]);
27
+ /** `at` が TLV パケットの先頭に見えるか (先頭のバイトと種類) */
28
+ function looksLikeHead(data, at) {
29
+ return data[at] === TLV_SYNC && TYPES.has(data[at + 1]);
30
+ }
31
+ /**
32
+ * 任意の長さで届く TLV のバイト列を TLV パケットに切り分ける。
33
+ *
34
+ * **先頭がパケットの先頭とは限らない。** 共有に途中から入った利用者は、TLV パケットの途中から受け取る
35
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))。そこで、同期が取れていないときは
36
+ * 先頭のバイトと種類が合い、**長さの先にある次のパケットの先頭も合う**位置を探して合わせる。
37
+ * 1 か所だけで決めると、中身にたまたまある `7F 03` を先頭と取り違える。そのため、同期を取る最初のパケットは次のパケットの先頭が届くまで出さない。
38
+ * 同期が取れたあとは、先頭が合わなくなるまでヘッダーの長さのとおりに進む。
39
+ *
40
+ * 出すパケットの中身は、渡されたチャンクか前の残りの一部を指す (コピーしない)。
41
+ * **次の `feed` より後まで持つならコピーすること** (`PacketStream` と同じ)
42
+ */
43
+ export class TlvStream {
44
+ rest = new Uint8Array(0);
45
+ synced = false;
46
+ /** 合わせるために読み飛ばしたバイトの合計 */
47
+ skipped = 0;
48
+ /** 同期を失って合わせ直した回数 */
49
+ resyncs = 0;
50
+ *feed(chunk) {
51
+ let data = chunk;
52
+ if (this.rest.length > 0) {
53
+ /*
54
+ * 前の残りが同期の取れたパケットの途中なら、そのパケットだけをチャンクの先頭と繋ぐ。
55
+ * **チャンクの全体は写さない** (38 Mbps を毎回写すことになる)
56
+ */
57
+ const head = concatBytes([this.rest, chunk.subarray(0, HEADER)]);
58
+ if (this.synced && head.length >= HEADER && looksLikeHead(head, 0)) {
59
+ const used = HEADER + u16(head, 2) - this.rest.length;
60
+ if (used > chunk.length) {
61
+ this.rest = concatBytes([this.rest, chunk]);
62
+ return;
63
+ }
64
+ const packet = concatBytes([this.rest, chunk.subarray(0, used)]);
65
+ yield {
66
+ type: packet[1],
67
+ payload: packet.subarray(HEADER),
68
+ bytes: packet,
69
+ };
70
+ data = chunk.subarray(used);
71
+ }
72
+ else
73
+ data = concatBytes([this.rest, chunk]);
74
+ }
75
+ let at = 0;
76
+ for (;;) {
77
+ if (!this.synced) {
78
+ const found = findHead(data, at);
79
+ // 見つからなければ、次のチャンクと繋げて探せるよう末尾の 3 バイトだけを残す
80
+ const next = found?.at ?? Math.max(at, data.length - (HEADER - 1));
81
+ this.skipped += next - at;
82
+ at = next;
83
+ if (found === null || found.wait)
84
+ break;
85
+ this.synced = true;
86
+ }
87
+ if (at + HEADER > data.length)
88
+ break;
89
+ if (!looksLikeHead(data, at)) {
90
+ this.synced = false;
91
+ this.resyncs++;
92
+ continue;
93
+ }
94
+ const end = at + HEADER + u16(data, at + 2);
95
+ if (end > data.length)
96
+ break;
97
+ yield {
98
+ type: data[at + 1],
99
+ payload: data.subarray(at + HEADER, end),
100
+ bytes: data.subarray(at, end),
101
+ };
102
+ at = end;
103
+ }
104
+ this.rest = data.slice(at);
105
+ }
106
+ }
107
+ /** `from` から、先頭と次のパケットの先頭がどちらも合う位置を探す */
108
+ function findHead(data, from) {
109
+ for (let at = from; at + HEADER <= data.length; at++) {
110
+ if (!looksLikeHead(data, at))
111
+ continue;
112
+ const next = at + HEADER + u16(data, at + 2);
113
+ if (next + 2 > data.length)
114
+ return { at, wait: true };
115
+ if (looksLikeHead(data, next))
116
+ return { at, wait: false };
117
+ }
118
+ return null;
119
+ }
package/dist/ttml.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 4K の放送 (MMT/TLV) の字幕 (ARIB STD-B62 の TTML) を、字幕の文字の配置 (`caption-text.ts`) にする。
3
+ * TS の字幕の `b24caption.ts` に当たり、受け側は TS の字幕と同じように描くだけで済む。
4
+ *
5
+ * 字幕の文書は MPU 1 つに 1 つ来る (`mmt-media.ts` の `CaptionUnit`)。文書の中の時刻 (begin・end・dur) は、
6
+ * MPU の最初の提示時刻からのずれとして読む (推定。字幕のパケットは実機の録りにまだ来ていない)。
7
+ *
8
+ * 読むのは文字・位置 (region の origin・extent、textAlign・displayAlign)・大きさ (fontSize・lineHeight)・
9
+ * 色 (color・backgroundColor・textOutline)・太字・斜体・下線・ルビ。画像を参照する字幕 (PNG) と
10
+ * 縦書き・アニメーション (set) は読まない
11
+ */
12
+ import { type CaptionPage } from "./caption-text.js";
13
+ /** 字幕 1 枚と、出す時刻 (90 kHz) */
14
+ export interface TimedCaptionPage {
15
+ at: number;
16
+ page: CaptionPage;
17
+ }
18
+ /**
19
+ * TTML の文書を、出す時刻ごとの字幕の文字の配置にする。`start` は文書の時刻の始まり (MPU の最初の提示時刻、90 kHz)。
20
+ *
21
+ * 段落 (p) の出る・消える時刻ごとに 1 枚を作る (`CaptionPage` は前の 1 枚を丸ごと置き換えるので、
22
+ * その時刻に出ている段落を全部並べる)。最初の 1 枚は `start` で、何も出ていなければ空 (前の文書の字幕を消す)。
23
+ * 読めない文書は空の並びを返す
24
+ */
25
+ export declare function ttmlPages(document: string, start: number): TimedCaptionPage[];