@danything/arib 0.0.0-stage → 0.1.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/dist/psi.d.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
3
+ * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
+ * 読むのは NIT と SDT の2つ。
5
+ *
6
+ * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号
7
+ * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別
8
+ *
9
+ * 局名も SDT から読む。ARIB の文字符号は独自だが、番組表を自分で集めるなら
10
+ * どのみち要るので [aribtext.ts](aribtext.ts) に寄せてある。
11
+ */
12
+ export declare const PACKET = 188;
13
+ export declare const SYNC = 71;
14
+ /** TS パケットの PID */
15
+ export declare function packetPid(packet: Uint8Array): number;
16
+ /** PAT。どの PID に PMT があるかを持つ。**入口はいつもここ** */
17
+ export declare const PID_PAT = 0;
18
+ export declare const PID_SDT = 17;
19
+ export declare const TABLE_PAT = 0;
20
+ export declare const TABLE_PMT = 2;
21
+ export declare const TABLE_SDT_ACTUAL = 66;
22
+ /**
23
+ * セクション本体に CRC32 を付けて仕上げる。`section_length` も実長に直す。
24
+ *
25
+ * **書く側が2つある。** PAT を書き直して流す `service-filter.ts` と、
26
+ * 偽の放送を組み立てる `synth.ts`。長さの直し方を間違えると読む側が黙って
27
+ * 捨てるだけなので、1箇所に置く。
28
+ */
29
+ export declare function withCrc(body: number[]): Uint8Array;
30
+ export interface Service {
31
+ serviceId: number;
32
+ serviceType: number;
33
+ /** 局名。SDT からしか取れないので、NIT 由来のものは空 */
34
+ name: string;
35
+ }
36
+ export interface TransportInfo {
37
+ transportStreamId: number;
38
+ originalNetworkId: number;
39
+ services: Service[];
40
+ }
41
+ export interface NetworkInfo {
42
+ networkId: number;
43
+ remoteControlKeyId: number | null;
44
+ transportStreams: TransportInfo[];
45
+ }
46
+ /**
47
+ * PSI セクションを組み立てる。
48
+ *
49
+ * セクションは TS パケットをまたいで運ばれ、パケットの切れ目とは無関係な
50
+ * 位置で終わる。payload_unit_start_indicator と pointer_field を見て
51
+ * 頭を合わせ、section_length ぶん溜まったら1本として吐く。
52
+ */
53
+ export declare class SectionAssembler {
54
+ private readonly pid;
55
+ private readonly crc;
56
+ private buffer;
57
+ /**
58
+ * @param pid 読むPID
59
+ * @param crc CRC の見方。
60
+ * `always` … 末尾4バイトを CRC32 とみなして必ず確かめる (PSI はこちら)
61
+ * `syntax` … `section_syntax_indicator` が立っているときだけ確かめる。
62
+ * DSM-CC は立っていないと末尾が CRC ではなく Checksum なので、
63
+ * いつも確かめると**全部捨ててしまう**
64
+ */
65
+ constructor(pid: number, crc?: "always" | "syntax");
66
+ /**
67
+ * パケットを1つ食わせる。組み上がったセクションを返す。
68
+ *
69
+ * 返りは**読むだけ**にすること。何も組み上がらないとき (ほとんどのパケット) は
70
+ * 使い回しの空配列を返す — 関係ないパケットのたびに配列を作っていた
71
+ */
72
+ feed(packet: Uint8Array): readonly Uint8Array[];
73
+ private append;
74
+ private flush;
75
+ }
76
+ /** `from` 以降 `until` の手前までで、188 間隔に 0x47 が続くところを探す。無ければ -1 (`seek.ts` も使う) */
77
+ export declare function findSync(data: Uint8Array, from?: number, until?: number): number;
78
+ /**
79
+ * 任意の長さで届くバイト列を 188 バイトのパケットに切り分ける。
80
+ *
81
+ * 頭が必ずパケットの先頭とは限らず、電波が弱いと途中で数バイト落ちる。
82
+ * ずれたままだと**以降ずっと1パケットも読めなくなる**ので、頭が 0x47 でなければ
83
+ * 取り直す。普段は先頭が 0x47 なので、探しに行くのはずれたときだけ。
84
+ *
85
+ * **出すパケットは渡された塊の上の窓** (写しではない。写すと録画の処理の 1/3 を食う)。
86
+ * 塊の入れ物を使い回す呼び出し側がある
87
+ * (`server/recorded-bml.ts`) ので、**次の `feed` より後まで持つなら写すこと**。
88
+ * いまのパーサ (`SectionAssembler`・`PesDemuxer`) は溜めるときに写している
89
+ */
90
+ export declare class PacketStream {
91
+ private rest;
92
+ feed(chunk: Uint8Array): Generator<Uint8Array>;
93
+ }
94
+ /**
95
+ * PAT から1つの局の PMT を追う。`serviceId` を渡さなければ PAT の最初の局
96
+ * (1局に絞った TS を食わせるところ)。PAT で PMT の PID が替われば組み立て直す
97
+ */
98
+ export declare class PmtTracker {
99
+ private readonly serviceId?;
100
+ private readonly pat;
101
+ private pmt;
102
+ /** 追っている PMT の PID。PAT を読むまでは null。替わったかは呼ぶ前後で比べる */
103
+ pid: number | null;
104
+ constructor(serviceId?: number | undefined);
105
+ /** パケットを1つ (`pid` はその PID)。組み上がった PMT のセクションを返す */
106
+ feed(packet: Uint8Array, pid: number): readonly Uint8Array[];
107
+ }
108
+ /** 記述子の並びを (tag, 中身) に切り分ける */
109
+ export declare function descriptors(data: Uint8Array): Generator<[number, Uint8Array]>;
110
+ /** デジタルコピー制御記述子。EIT の番組ごとと PMT に入る */
111
+ export declare const DESC_DIGITAL_COPY_CONTROL = 193;
112
+ /**
113
+ * デジタルコピー制御記述子の中身 (タグと長さを除いたもの) から、**コピー禁止か**を読む。
114
+ *
115
+ * 先頭2ビットの digital_recording_control_data が 11 なら禁止
116
+ * (00 コピー可、01 事業者定義、10 1世代のみ)。禁止の番組は録らない
117
+ * ([data.md](https://github.com/danything/denpa/blob/main/docs/data.md)「コピー禁止の番組」)
118
+ */
119
+ export declare function isCopyNever(data: Uint8Array): boolean;
120
+ /**
121
+ * PMT の**番組ぜんたいに掛かる記述子** (program_info)。CA (ECM の PID) はここにも入る
122
+ */
123
+ export declare function pmtProgramInfo(section: Uint8Array): Uint8Array;
124
+ /**
125
+ * PMT の ES の並びを `[stream_type, PID, 記述子]` で返す。
126
+ *
127
+ * **`stream_type` では物を見分けられません。** データ放送も AIT もロゴも同じ
128
+ * DSM-CC セクション (0x0D) を名乗るので、見分けるのは記述子のほう
129
+ * (component_tag や application_signalling) です。だからここは**素通しで並べる
130
+ * だけ**にして、選り分けは呼ぶ側に置いてあります。
131
+ */
132
+ export declare function pmtStreams(section: Uint8Array): Generator<[number, number, Uint8Array]>;
133
+ /** PAT から `サービスID → PMT の PID`。サービス0 は NIT なので飛ばす */
134
+ export declare function parsePat(section: Uint8Array): Map<number, number>;
135
+ /**
136
+ * SDT のサービスの並びを (service_id, 記述子の並び) に切り分ける。
137
+ *
138
+ * **読む側が2つある。** 局名 (`parseSdt`) とロゴの対応 (`ts/logo.ts`) で、
139
+ * 欲しい記述子が違うだけで歩き方は同じ。並びの読み方をここ1箇所に置く。
140
+ */
141
+ export declare function sdtServices(section: Uint8Array): Generator<[number, Uint8Array]>;
142
+ /** SDT からサービスの一覧を読む。自分の TS のものだけ */
143
+ export declare function parseSdt(section: Uint8Array): TransportInfo | null;
144
+ /** NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む */
145
+ export declare function parseNit(section: Uint8Array): NetworkInfo | null;
146
+ export interface FoundService extends Service {
147
+ networkId: number;
148
+ transportStreamId: number;
149
+ remoteControlKeyId: number | null;
150
+ }
151
+ /**
152
+ * 流れてくる TS を食べて、NIT と SDT が揃うまで待つ。
153
+ *
154
+ * Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
155
+ * 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
156
+ * 分からないと設定に書けない。
157
+ */
158
+ export declare class ServiceReader {
159
+ private readonly nit;
160
+ private readonly sdt;
161
+ private readonly packets;
162
+ network: NetworkInfo | null;
163
+ transport: TransportInfo | null;
164
+ get complete(): boolean;
165
+ /** 任意の長さのバイト列を食わせる。揃ったら true */
166
+ feed(chunk: Uint8Array): boolean;
167
+ /** 録るに値するサービスだけ。リモコン番号は NIT のものを配る */
168
+ services(): FoundService[];
169
+ }
package/dist/psi.js ADDED
@@ -0,0 +1,497 @@
1
+ /**
2
+ * TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
3
+ * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
+ * 読むのは NIT と SDT の2つ。
5
+ *
6
+ * - NIT (PID 0x0010) … ネットワークID と、地上波ならリモコン番号
7
+ * - SDT (PID 0x0011) … その TS に入っているサービスのIDと種別
8
+ *
9
+ * 局名も SDT から読む。ARIB の文字符号は独自だが、番組表を自分で集めるなら
10
+ * どのみち要るので [aribtext.ts](aribtext.ts) に寄せてある。
11
+ */
12
+ import { decodeAribText } from "./aribtext.js";
13
+ import { concatBytes, u16 } from "./bytes.js";
14
+ export const PACKET = 188;
15
+ export const SYNC = 0x47;
16
+ /** TS パケットの PID */
17
+ export function packetPid(packet) {
18
+ return ((packet[1] & 0x1f) << 8) | packet[2];
19
+ }
20
+ /** PAT。どの PID に PMT があるかを持つ。**入口はいつもここ** */
21
+ export const PID_PAT = 0x0000;
22
+ const PID_NIT = 0x0010;
23
+ export const PID_SDT = 0x0011;
24
+ export const TABLE_PAT = 0x00;
25
+ export const TABLE_PMT = 0x02;
26
+ const TABLE_NIT_ACTUAL = 0x40;
27
+ export const TABLE_SDT_ACTUAL = 0x42;
28
+ const DESC_SERVICE_LIST = 0x41;
29
+ const DESC_SERVICE = 0x48;
30
+ const DESC_TS_INFORMATION = 0xcd;
31
+ /**
32
+ * スキャンで残すサービス種別。Mirakurun のスキャンが通しているものと同じで、
33
+ * ラジオ (0x02) とデータ (0xc0) も入る。映像の無いものを番組表に並べないのは
34
+ * 取り込むほう (epg.ts の DIGITAL_TV)
35
+ */
36
+ const SERVICE_TYPES = new Set([0x01, 0x02, 0xa1, 0xa4, 0xa5, 0xad, 0xc0]);
37
+ const CRC32_TABLE = (() => {
38
+ const table = new Uint32Array(256);
39
+ for (let i = 0; i < 256; i++) {
40
+ let crc = i << 24;
41
+ for (let bit = 0; bit < 8; bit++) {
42
+ crc =
43
+ crc & 0x80000000 ? ((crc << 1) ^ 0x04c11db7) >>> 0 : (crc << 1) >>> 0;
44
+ }
45
+ table[i] = crc >>> 0;
46
+ }
47
+ return table;
48
+ })();
49
+ /** MPEG-2 の CRC32。セクション末尾の4バイトを含めて回すと 0 になる */
50
+ function crc32(data) {
51
+ let crc = 0xffffffff;
52
+ for (const byte of data) {
53
+ crc =
54
+ (((crc << 8) >>> 0) ^ CRC32_TABLE[((crc >>> 24) ^ byte) & 0xff]) >>> 0;
55
+ }
56
+ return crc >>> 0;
57
+ }
58
+ /**
59
+ * セクション本体に CRC32 を付けて仕上げる。`section_length` も実長に直す。
60
+ *
61
+ * **書く側が2つある。** PAT を書き直して流す `service-filter.ts` と、
62
+ * 偽の放送を組み立てる `synth.ts`。長さの直し方を間違えると読む側が黙って
63
+ * 捨てるだけなので、1箇所に置く。
64
+ */
65
+ export function withCrc(body) {
66
+ const section = Uint8Array.from(body);
67
+ const length = section.length - 3 + 4;
68
+ section[1] = 0xb0 | ((length >> 8) & 0x0f);
69
+ section[2] = length & 0xff;
70
+ const out = new Uint8Array(section.length + 4);
71
+ out.set(section);
72
+ new DataView(out.buffer).setUint32(section.length, crc32(section));
73
+ return out;
74
+ }
75
+ /** 何も組み上がらなかったときの返り。使い回すので凍らせてある */
76
+ const NONE = Object.freeze([]);
77
+ /**
78
+ * PSI セクションを組み立てる。
79
+ *
80
+ * セクションは TS パケットをまたいで運ばれ、パケットの切れ目とは無関係な
81
+ * 位置で終わる。payload_unit_start_indicator と pointer_field を見て
82
+ * 頭を合わせ、section_length ぶん溜まったら1本として吐く。
83
+ */
84
+ export class SectionAssembler {
85
+ pid;
86
+ crc;
87
+ buffer = new Uint8Array(0);
88
+ /**
89
+ * @param pid 読むPID
90
+ * @param crc CRC の見方。
91
+ * `always` … 末尾4バイトを CRC32 とみなして必ず確かめる (PSI はこちら)
92
+ * `syntax` … `section_syntax_indicator` が立っているときだけ確かめる。
93
+ * DSM-CC は立っていないと末尾が CRC ではなく Checksum なので、
94
+ * いつも確かめると**全部捨ててしまう**
95
+ */
96
+ constructor(pid, crc = "always") {
97
+ this.pid = pid;
98
+ this.crc = crc;
99
+ }
100
+ /**
101
+ * パケットを1つ食わせる。組み上がったセクションを返す。
102
+ *
103
+ * 返りは**読むだけ**にすること。何も組み上がらないとき (ほとんどのパケット) は
104
+ * 使い回しの空配列を返す — 関係ないパケットのたびに配列を作っていた
105
+ */
106
+ feed(packet) {
107
+ // トランスポートエラーが立っているものは信用しない
108
+ if (packet[0] !== SYNC ||
109
+ packetPid(packet) !== this.pid ||
110
+ packet[1] & 0x80)
111
+ return NONE;
112
+ const adaptation = (packet[3] >> 4) & 0x03;
113
+ if (adaptation === 0 || adaptation === 2)
114
+ return NONE;
115
+ let offset = 4;
116
+ if (adaptation === 3)
117
+ offset += 1 + packet[4];
118
+ if (offset >= PACKET)
119
+ return NONE;
120
+ const payload = packet.subarray(offset);
121
+ if (packet[1] & 0x40) {
122
+ const pointer = payload[0];
123
+ if (1 + pointer > payload.length)
124
+ return NONE;
125
+ // pointer_field の手前は前のセクションの続き
126
+ this.append(payload.subarray(1, 1 + pointer));
127
+ const done = this.flush();
128
+ // 写す。パケットは届いた塊の上の窓で、塊は使い回される (PacketStream)
129
+ this.buffer = payload.slice(1 + pointer);
130
+ const next = this.flush();
131
+ if (done.length === 0)
132
+ return next;
133
+ return next.length === 0 ? done : [...done, ...next];
134
+ }
135
+ this.append(payload);
136
+ return this.flush();
137
+ }
138
+ append(data) {
139
+ if (this.buffer.length > 0)
140
+ this.buffer = concatBytes([this.buffer, data]);
141
+ }
142
+ flush() {
143
+ let sections = null;
144
+ for (;;) {
145
+ if (this.buffer.length < 3)
146
+ return sections ?? NONE;
147
+ // 詰め物。ここから先にセクションは無い
148
+ if (this.buffer[0] === 0xff) {
149
+ this.buffer = new Uint8Array(0);
150
+ return sections ?? NONE;
151
+ }
152
+ const length = 3 + (u16(this.buffer, 1) & 0x0fff);
153
+ if (this.buffer.length < length)
154
+ return sections ?? NONE;
155
+ const section = this.buffer.slice(0, length);
156
+ this.buffer = this.buffer.slice(length);
157
+ // 壊れたセクションを読むと嘘の局が並ぶので、CRC を通ったものだけ使う
158
+ const syntax = (section[1] & 0x80) !== 0;
159
+ if ((this.crc === "syntax" && !syntax) || crc32(section) === 0) {
160
+ sections ??= [];
161
+ sections.push(section);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * 同期が取れているとみなすのに必要な連続パケット数。
168
+ * 1つだけでは中身のたまたまの 0x47 を頭と誤認する
169
+ */
170
+ const CONFIRM = 3;
171
+ /** `from` 以降 `until` の手前までで、188 間隔に 0x47 が続くところを探す。無ければ -1 (`seek.ts` も使う) */
172
+ export function findSync(data, from = 0, until = data.length) {
173
+ for (let at = from; at < until && at + PACKET * (CONFIRM - 1) < data.length; at++) {
174
+ let i = 0;
175
+ while (i < CONFIRM && data[at + i * PACKET] === SYNC)
176
+ i++;
177
+ if (i === CONFIRM)
178
+ return at;
179
+ }
180
+ return -1;
181
+ }
182
+ /**
183
+ * 任意の長さで届くバイト列を 188 バイトのパケットに切り分ける。
184
+ *
185
+ * 頭が必ずパケットの先頭とは限らず、電波が弱いと途中で数バイト落ちる。
186
+ * ずれたままだと**以降ずっと1パケットも読めなくなる**ので、頭が 0x47 でなければ
187
+ * 取り直す。普段は先頭が 0x47 なので、探しに行くのはずれたときだけ。
188
+ *
189
+ * **出すパケットは渡された塊の上の窓** (写しではない。写すと録画の処理の 1/3 を食う)。
190
+ * 塊の入れ物を使い回す呼び出し側がある
191
+ * (`server/recorded-bml.ts`) ので、**次の `feed` より後まで持つなら写すこと**。
192
+ * いまのパーサ (`SectionAssembler`・`PesDemuxer`) は溜めるときに写している
193
+ */
194
+ export class PacketStream {
195
+ rest = new Uint8Array(0);
196
+ *feed(chunk) {
197
+ /*
198
+ * 前の残り (`rest`) と塊を繋いだ並びの上を歩く。位置 `at` はその並びでの位置。
199
+ * 繋ぐのは**残りに頭があるパケットを読むのに要るぶんだけ** (頭を探すのに
200
+ * 3パケットぶん先まで見る)
201
+ */
202
+ const split = this.rest.length;
203
+ const total = split + chunk.length;
204
+ let at = 0;
205
+ let lost = false;
206
+ if (split > 0) {
207
+ const head = new Uint8Array(split + Math.min(chunk.length, PACKET * CONFIRM));
208
+ head.set(this.rest);
209
+ head.set(chunk.subarray(0, head.length - split), split);
210
+ while (at < split && at + PACKET <= total) {
211
+ if (head[at] !== SYNC) {
212
+ const found = findSync(head, at, split);
213
+ // 残りの中には無い。塊のほうを探す
214
+ if (found < 0) {
215
+ lost = true;
216
+ break;
217
+ }
218
+ at = found;
219
+ }
220
+ yield head.subarray(at, at + PACKET);
221
+ at += PACKET;
222
+ }
223
+ }
224
+ let offset = at - split;
225
+ if (lost) {
226
+ const found = findSync(chunk);
227
+ if (found >= 0) {
228
+ offset = found;
229
+ lost = false;
230
+ }
231
+ }
232
+ if (!lost) {
233
+ while (offset + PACKET <= chunk.length) {
234
+ if (chunk[offset] !== SYNC) {
235
+ const found = findSync(chunk, offset);
236
+ if (found < 0)
237
+ break;
238
+ offset = found;
239
+ if (offset + PACKET > chunk.length)
240
+ break;
241
+ }
242
+ yield chunk.subarray(offset, offset + PACKET);
243
+ offset += PACKET;
244
+ }
245
+ at = split + offset;
246
+ }
247
+ // 同期が取れないまま溜め込まないよう、頭を探せるぶんだけ残す
248
+ const from = Math.max(at, total - PACKET * CONFIRM);
249
+ if (from >= split) {
250
+ this.rest = chunk.slice(from - split);
251
+ }
252
+ else {
253
+ // 塊が短くて、前の残りにまだ食い込んでいる
254
+ const rest = new Uint8Array(total - from);
255
+ rest.set(this.rest.subarray(from));
256
+ rest.set(chunk, split - from);
257
+ this.rest = rest;
258
+ }
259
+ }
260
+ }
261
+ /**
262
+ * PAT から1つの局の PMT を追う。`serviceId` を渡さなければ PAT の最初の局
263
+ * (1局に絞った TS を食わせるところ)。PAT で PMT の PID が替われば組み立て直す
264
+ */
265
+ export class PmtTracker {
266
+ serviceId;
267
+ pat = new SectionAssembler(PID_PAT);
268
+ pmt = null;
269
+ /** 追っている PMT の PID。PAT を読むまでは null。替わったかは呼ぶ前後で比べる */
270
+ pid = null;
271
+ constructor(serviceId) {
272
+ this.serviceId = serviceId;
273
+ }
274
+ /** パケットを1つ (`pid` はその PID)。組み上がった PMT のセクションを返す */
275
+ feed(packet, pid) {
276
+ if (pid === PID_PAT) {
277
+ for (const section of this.pat.feed(packet)) {
278
+ const programs = parsePat(section);
279
+ const next = this.serviceId === undefined
280
+ ? programs.values().next().value
281
+ : programs.get(this.serviceId);
282
+ if (next === undefined || next === this.pid)
283
+ continue;
284
+ this.pid = next;
285
+ this.pmt = new SectionAssembler(next);
286
+ }
287
+ }
288
+ return pid === this.pid ? this.pmt.feed(packet) : NONE;
289
+ }
290
+ }
291
+ /** 記述子の並びを (tag, 中身) に切り分ける */
292
+ export function* descriptors(data) {
293
+ let at = 0;
294
+ while (at + 2 <= data.length) {
295
+ const tag = data[at];
296
+ const length = data[at + 1];
297
+ const body = data.subarray(at + 2, at + 2 + length);
298
+ if (body.length < length)
299
+ return;
300
+ yield [tag, body];
301
+ at += 2 + length;
302
+ }
303
+ }
304
+ /** デジタルコピー制御記述子。EIT の番組ごとと PMT に入る */
305
+ export const DESC_DIGITAL_COPY_CONTROL = 0xc1;
306
+ /**
307
+ * デジタルコピー制御記述子の中身 (タグと長さを除いたもの) から、**コピー禁止か**を読む。
308
+ *
309
+ * 先頭2ビットの digital_recording_control_data が 11 なら禁止
310
+ * (00 コピー可、01 事業者定義、10 1世代のみ)。禁止の番組は録らない
311
+ * ([data.md](https://github.com/danything/denpa/blob/main/docs/data.md)「コピー禁止の番組」)
312
+ */
313
+ export function isCopyNever(data) {
314
+ return data.length >= 1 && data[0] >> 6 === 0b11;
315
+ }
316
+ /**
317
+ * PMT の**番組ぜんたいに掛かる記述子** (program_info)。CA (ECM の PID) はここにも入る
318
+ */
319
+ export function pmtProgramInfo(section) {
320
+ if (section[0] !== TABLE_PMT || section.length < 16)
321
+ return new Uint8Array(0);
322
+ const length = u16(section, 10) & 0x0fff;
323
+ return section.subarray(12, 12 + length);
324
+ }
325
+ /**
326
+ * PMT の ES の並びを `[stream_type, PID, 記述子]` で返す。
327
+ *
328
+ * **`stream_type` では物を見分けられません。** データ放送も AIT もロゴも同じ
329
+ * DSM-CC セクション (0x0D) を名乗るので、見分けるのは記述子のほう
330
+ * (component_tag や application_signalling) です。だからここは**素通しで並べる
331
+ * だけ**にして、選り分けは呼ぶ側に置いてあります。
332
+ */
333
+ export function* pmtStreams(section) {
334
+ if (section[0] !== TABLE_PMT || section.length < 16)
335
+ return;
336
+ const programInfoLength = u16(section, 10) & 0x0fff;
337
+ let at = 12 + programInfoLength;
338
+ const end = section.length - 4;
339
+ while (at + 5 <= end) {
340
+ const pid = u16(section, at + 1) & 0x1fff;
341
+ const infoLength = u16(section, at + 3) & 0x0fff;
342
+ yield [section[at], pid, section.subarray(at + 5, at + 5 + infoLength)];
343
+ at += 5 + infoLength;
344
+ }
345
+ }
346
+ /** PAT から `サービスID → PMT の PID`。サービス0 は NIT なので飛ばす */
347
+ export function parsePat(section) {
348
+ const programs = new Map();
349
+ if (section[0] !== TABLE_PAT)
350
+ return programs;
351
+ const end = section.length - 4;
352
+ for (let at = 8; at + 4 <= end; at += 4) {
353
+ const serviceId = u16(section, at);
354
+ if (serviceId === 0)
355
+ continue;
356
+ programs.set(serviceId, u16(section, at + 2) & 0x1fff);
357
+ }
358
+ return programs;
359
+ }
360
+ /**
361
+ * SDT のサービスの並びを (service_id, 記述子の並び) に切り分ける。
362
+ *
363
+ * **読む側が2つある。** 局名 (`parseSdt`) とロゴの対応 (`ts/logo.ts`) で、
364
+ * 欲しい記述子が違うだけで歩き方は同じ。並びの読み方をここ1箇所に置く。
365
+ */
366
+ export function* sdtServices(section) {
367
+ let at = 11;
368
+ const end = section.length - 4;
369
+ while (at + 5 <= end) {
370
+ const serviceId = u16(section, at);
371
+ const loop = u16(section, at + 3) & 0x0fff;
372
+ yield [serviceId, section.subarray(at + 5, at + 5 + loop)];
373
+ at += 5 + loop;
374
+ }
375
+ }
376
+ /** SDT からサービスの一覧を読む。自分の TS のものだけ */
377
+ export function parseSdt(section) {
378
+ if (section[0] !== TABLE_SDT_ACTUAL)
379
+ return null;
380
+ const services = [];
381
+ for (const [serviceId, body] of sdtServices(section)) {
382
+ for (const [tag, descriptor] of descriptors(body)) {
383
+ if (tag === DESC_SERVICE && descriptor.length > 0) {
384
+ /*
385
+ * service_descriptor は 種別 → 事業者名 → サービス名 の順。
386
+ * 事業者名 (「東京メトロポリタンテレビジョン」) は要らないので飛ばし、
387
+ * その先のサービス名 (「TOKYO MX1」) を取る
388
+ */
389
+ const providerLength = descriptor[1] ?? 0;
390
+ const nameAt = 2 + providerLength;
391
+ const nameLength = descriptor[nameAt] ?? 0;
392
+ services.push({
393
+ serviceId,
394
+ serviceType: descriptor[0],
395
+ name: decodeAribText(descriptor.subarray(nameAt + 1, nameAt + 1 + nameLength)),
396
+ });
397
+ break;
398
+ }
399
+ }
400
+ }
401
+ return {
402
+ transportStreamId: u16(section, 3),
403
+ originalNetworkId: u16(section, 8),
404
+ services,
405
+ };
406
+ }
407
+ /** NIT からネットワークIDとリモコン番号、他の TS の顔ぶれを読む */
408
+ export function parseNit(section) {
409
+ if (section[0] !== TABLE_NIT_ACTUAL)
410
+ return null;
411
+ const networkLength = u16(section, 8) & 0x0fff;
412
+ let at = 10 + networkLength;
413
+ if (at + 2 > section.length)
414
+ return null;
415
+ at += 2; // transport_stream_loop_length
416
+ let remoteControlKeyId = null;
417
+ const transportStreams = [];
418
+ const end = section.length - 4;
419
+ while (at + 6 <= end) {
420
+ const transportStreamId = u16(section, at);
421
+ const originalNetworkId = u16(section, at + 2);
422
+ const loop = u16(section, at + 4) & 0x0fff;
423
+ const body = section.subarray(at + 6, at + 6 + loop);
424
+ at += 6 + loop;
425
+ const services = [];
426
+ for (const [tag, descriptor] of descriptors(body)) {
427
+ if (tag === DESC_TS_INFORMATION &&
428
+ descriptor.length > 0 &&
429
+ remoteControlKeyId === null) {
430
+ remoteControlKeyId = descriptor[0];
431
+ }
432
+ else if (tag === DESC_SERVICE_LIST) {
433
+ for (let i = 0; i + 3 <= descriptor.length; i += 3) {
434
+ services.push({
435
+ serviceId: u16(descriptor, i),
436
+ serviceType: descriptor[i + 2],
437
+ name: "",
438
+ });
439
+ }
440
+ }
441
+ }
442
+ transportStreams.push({ transportStreamId, originalNetworkId, services });
443
+ }
444
+ return {
445
+ networkId: u16(section, 3),
446
+ remoteControlKeyId,
447
+ transportStreams,
448
+ };
449
+ }
450
+ /**
451
+ * 流れてくる TS を食べて、NIT と SDT が揃うまで待つ。
452
+ *
453
+ * Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
454
+ * 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
455
+ * 分からないと設定に書けない。
456
+ */
457
+ export class ServiceReader {
458
+ nit = new SectionAssembler(PID_NIT);
459
+ sdt = new SectionAssembler(PID_SDT);
460
+ packets = new PacketStream();
461
+ network = null;
462
+ transport = null;
463
+ get complete() {
464
+ return this.network !== null && this.transport !== null;
465
+ }
466
+ /** 任意の長さのバイト列を食わせる。揃ったら true */
467
+ feed(chunk) {
468
+ for (const packet of this.packets.feed(chunk)) {
469
+ if (this.network === null) {
470
+ for (const section of this.nit.feed(packet)) {
471
+ this.network = parseNit(section) ?? this.network;
472
+ }
473
+ }
474
+ if (this.transport === null) {
475
+ for (const section of this.sdt.feed(packet)) {
476
+ this.transport = parseSdt(section) ?? this.transport;
477
+ }
478
+ }
479
+ }
480
+ return this.complete;
481
+ }
482
+ /** 録るに値するサービスだけ。リモコン番号は NIT のものを配る */
483
+ services() {
484
+ if (this.network === null || this.transport === null)
485
+ return [];
486
+ const { networkId, remoteControlKeyId } = this.network;
487
+ const { transportStreamId } = this.transport;
488
+ return this.transport.services
489
+ .filter((service) => SERVICE_TYPES.has(service.serviceType))
490
+ .map((service) => ({
491
+ ...service,
492
+ networkId,
493
+ transportStreamId,
494
+ remoteControlKeyId,
495
+ }));
496
+ }
497
+ }