@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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 物理チャンネル丸ごとの TS から、1つの局のぶんだけを抜き出す。
3
+ *
4
+ * エージェントは**中身を読まずに素の TS を流す**ので
5
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))、選り分けるのは denpa の仕事。
6
+ *
7
+ * **これをやらないと1つの録画に複数の番組が入る。** 実機の TOKYO MX は
8
+ * 1本の TS に MX1 と MX2 を載せていて、丸ごと保存すると ffprobe が2つの
9
+ * プログラムを返す (これで CM 検出の fps を読み違えたことがある)。
10
+ *
11
+ * 残すもの:
12
+ *
13
+ * - **PAT** … その局1つだけに**書き直して**出す。丸ごと通すと ffmpeg には
14
+ * 相変わらず複数のプログラムが見える
15
+ * - **PMT と、そこに載っている ES** … 映像・音声・字幕。ECM も (掛かったまま
16
+ * 録れたものを後から復号するのに要る)
17
+ * - **CAT / NIT / SDT / EIT / TDT** … 局の情報と番組表。EIT は放送の延長を
18
+ * 追うのに使い、録画に残す番組情報の元にもなる
19
+ *
20
+ * 落とすもの: 他局の PMT と ES、そして詰め物 (PID 0x1FFF)。
21
+ */
22
+ /**
23
+ * 局1つぶんに絞る。
24
+ *
25
+ * PAT が来るまでは**何も出さない**。どの PID がその局のものか決まらないためで、
26
+ * PAT は 100ms ごとに流れてくるので待つのは一瞬。
27
+ */
28
+ export declare class ServiceFilter {
29
+ private readonly serviceId;
30
+ private readonly packets;
31
+ private readonly pat;
32
+ private pmt;
33
+ private pmtPid;
34
+ /** その局のものとして通す PID。PMT を読むまでは空 */
35
+ private readonly own;
36
+ /** 書き直した PAT。同じものを出し続けるので作り置きする */
37
+ private rewritten;
38
+ private counter;
39
+ /** PAT に書いてある中継の番号。読むまでは null */
40
+ private tsid;
41
+ private copyNeverNow;
42
+ private streamsNow;
43
+ constructor(serviceId: number);
44
+ /** その局の PMT を読めたか。読めるまで映像は1バイトも出ない */
45
+ get ready(): boolean;
46
+ /**
47
+ * **いま流れているものがコピー禁止か** (最後に読んだ PMT のデジタルコピー制御記述子)。
48
+ * 番組全体 (1つめのループ) と ES ごと (2つめのループ) のどちらかが禁止なら禁止。
49
+ * PMT を読むまでは false。
50
+ *
51
+ * 録画はこれを見て、禁止のあいだは保存しない (`recorder.ts`)。番組表が古い・無いときの備え。
52
+ * 絞り方には効かせない — ライブは保存しないので、禁止の番組もそのまま観られる
53
+ */
54
+ get copyNever(): boolean;
55
+ /**
56
+ * **映像・音声・字幕の並び** (最後に読んだ PMT の `種別:PID`)。PMT を読むまでは null。
57
+ *
58
+ * これが変わったら ffmpeg を作り直す (`StreamsWatch`)。ffmpeg は起きたときの並びのまま
59
+ * 音声・字幕を選んでいるので、番組が替わって音声の本数や字幕の PID が変わると追従しない。
60
+ * データ放送などのセクション (0x0D など) は入れない — 番組ごとに増減しても ffmpeg には関係ない
61
+ */
62
+ get streams(): string | null;
63
+ /**
64
+ * **中継の番号 (transport_stream_id)。** PAT を読むまでは null。
65
+ *
66
+ * 絞るのに要るわけではありません。**データ放送が受信機に訊いてくる**ので
67
+ * 覚えておきます — テレ朝の TVerリンクは `browser.getProgramID(4)` で
68
+ * これを取り、TVer のサーバへそのまま送る。`null` のまま渡すと文字の
69
+ * "null" が飛んで、あちらが断り、画面には `DT-RE` とだけ出ます
70
+ * ([server/live.ts](https://github.com/danything/denpa/blob/main/src/lib/server/live.ts) の `programInfo`)
71
+ */
72
+ get transportStreamId(): number | null;
73
+ /**
74
+ * 任意の長さのバイト列を食わせて、残すぶんを返す。
75
+ *
76
+ * 落とした PID の連続性カウンタは飛ぶが、**残す PID は元のまま**なので
77
+ * 読む側から見て抜けは無い。自分で作るのは PAT だけで、そこは自前で数える。
78
+ */
79
+ filter(chunk: Uint8Array): Uint8Array;
80
+ private readPat;
81
+ private readPmt;
82
+ }
83
+ /**
84
+ * **映像・音声・字幕の並びが変わったか** (`ServiceFilter.streams`) を見る。最初に読んだ並びを覚え、変わったら
85
+ * 1回だけ true を返して新しい並びを覚え直す。
86
+ *
87
+ * 並びを決め打ちで起こした ffmpeg (`-map`) は、番組が替わって音声の本数や字幕の PID が変わっても追従しない。
88
+ * 変わったら起こし直す — ライブと追っかけのエンコード (`server/live.ts` の `Session.pump`)、MPEG-2 をそのまま
89
+ * 流す経路の字幕 (`Session.runRaw`・`recordingCaptions`)
90
+ */
91
+ export declare class StreamsWatch {
92
+ private seen;
93
+ /** `filter` に塊を通したあとに呼ぶ */
94
+ changed(filter: ServiceFilter): boolean;
95
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * 物理チャンネル丸ごとの TS から、1つの局のぶんだけを抜き出す。
3
+ *
4
+ * エージェントは**中身を読まずに素の TS を流す**ので
5
+ * ([agent.md](https://github.com/danything/denpa/blob/main/docs/agent.md))、選り分けるのは denpa の仕事。
6
+ *
7
+ * **これをやらないと1つの録画に複数の番組が入る。** 実機の TOKYO MX は
8
+ * 1本の TS に MX1 と MX2 を載せていて、丸ごと保存すると ffprobe が2つの
9
+ * プログラムを返す (これで CM 検出の fps を読み違えたことがある)。
10
+ *
11
+ * 残すもの:
12
+ *
13
+ * - **PAT** … その局1つだけに**書き直して**出す。丸ごと通すと ffmpeg には
14
+ * 相変わらず複数のプログラムが見える
15
+ * - **PMT と、そこに載っている ES** … 映像・音声・字幕。ECM も (掛かったまま
16
+ * 録れたものを後から復号するのに要る)
17
+ * - **CAT / NIT / SDT / EIT / TDT** … 局の情報と番組表。EIT は放送の延長を
18
+ * 追うのに使い、録画に残す番組情報の元にもなる
19
+ *
20
+ * 落とすもの: 他局の PMT と ES、そして詰め物 (PID 0x1FFF)。
21
+ */
22
+ import { concatBytes, u16 } from "./bytes.js";
23
+ import { DESC_DIGITAL_COPY_CONTROL, descriptors, isCopyNever, PACKET, packetPid, PacketStream, parsePat, PID_PAT, pmtProgramInfo, pmtStreams, SectionAssembler, SYNC, TABLE_PAT, TABLE_PMT, withCrc, } from "./psi.js";
24
+ const PID_NULL = 0x1fff;
25
+ /**
26
+ * 局に関係なく残す PID。**どれも数十バイト/秒**で、落として得るものが無い。
27
+ *
28
+ * CDT (ロゴ) はここに入れない。録画に要らないうえ、衛星では数百KBのカルーセルが
29
+ * 流れてくる。ロゴを取得するのは録画の外でやる (logo.ts)
30
+ */
31
+ const KEEP = new Set([
32
+ 0x0001, // CAT
33
+ 0x0010, // NIT
34
+ 0x0011, // SDT
35
+ 0x0012, // EIT
36
+ 0x0014, // TDT / TOT
37
+ ]);
38
+ const DESC_CA = 0x09;
39
+ /** 1つの局に絞った PAT を組み立てる */
40
+ function buildPat(transportStreamId, version, serviceId, pmtPid) {
41
+ const body = [
42
+ TABLE_PAT,
43
+ 0xb0,
44
+ 0x00, // section_length はあとで埋める
45
+ (transportStreamId >> 8) & 0xff,
46
+ transportStreamId & 0xff,
47
+ 0xc1 | ((version & 0x1f) << 1),
48
+ 0x00, // section_number
49
+ 0x00, // last_section_number
50
+ // NIT。番組表を読む側がいるので残す
51
+ 0x00,
52
+ 0x00,
53
+ 0xe0,
54
+ 0x10,
55
+ (serviceId >> 8) & 0xff,
56
+ serviceId & 0xff,
57
+ 0xe0 | ((pmtPid >> 8) & 0x1f),
58
+ pmtPid & 0xff,
59
+ ];
60
+ return withCrc(body);
61
+ }
62
+ /** セクション1本を TS パケット1つに詰める。PAT は小さいので必ず収まる */
63
+ function packet(pid, section, counter) {
64
+ const out = new Uint8Array(PACKET).fill(0xff);
65
+ out[0] = SYNC;
66
+ out[1] = 0x40 | ((pid >> 8) & 0x1f);
67
+ out[2] = pid & 0xff;
68
+ out[3] = 0x10 | (counter & 0x0f);
69
+ out[4] = 0x00; // pointer_field
70
+ out.set(section.subarray(0, PACKET - 5), 5);
71
+ return out;
72
+ }
73
+ /**
74
+ * 映像・音声・字幕の stream_type (`ServiceFilter.streams`)。MPEG-2 映像・H.264・H.265、MPEG 音声・AAC (ADTS・LATM)、
75
+ * 字幕と文字スーパー (PES の private_data。0x06)
76
+ */
77
+ const MEDIA_STREAMS = new Set([0x02, 0x1b, 0x24, 0x03, 0x04, 0x0f, 0x11, 0x06]);
78
+ /**
79
+ * 局1つぶんに絞る。
80
+ *
81
+ * PAT が来るまでは**何も出さない**。どの PID がその局のものか決まらないためで、
82
+ * PAT は 100ms ごとに流れてくるので待つのは一瞬。
83
+ */
84
+ export class ServiceFilter {
85
+ serviceId;
86
+ packets = new PacketStream();
87
+ pat = new SectionAssembler(PID_PAT);
88
+ pmt = null;
89
+ pmtPid = null;
90
+ /** その局のものとして通す PID。PMT を読むまでは空 */
91
+ own = new Set();
92
+ /** 書き直した PAT。同じものを出し続けるので作り置きする */
93
+ rewritten = null;
94
+ counter = 0;
95
+ /** PAT に書いてある中継の番号。読むまでは null */
96
+ tsid = null;
97
+ copyNeverNow = false;
98
+ streamsNow = null;
99
+ constructor(serviceId) {
100
+ this.serviceId = serviceId;
101
+ }
102
+ /** その局の PMT を読めたか。読めるまで映像は1バイトも出ない */
103
+ get ready() {
104
+ return this.own.size > 0;
105
+ }
106
+ /**
107
+ * **いま流れているものがコピー禁止か** (最後に読んだ PMT のデジタルコピー制御記述子)。
108
+ * 番組全体 (1つめのループ) と ES ごと (2つめのループ) のどちらかが禁止なら禁止。
109
+ * PMT を読むまでは false。
110
+ *
111
+ * 録画はこれを見て、禁止のあいだは保存しない (`recorder.ts`)。番組表が古い・無いときの備え。
112
+ * 絞り方には効かせない — ライブは保存しないので、禁止の番組もそのまま観られる
113
+ */
114
+ get copyNever() {
115
+ return this.copyNeverNow;
116
+ }
117
+ /**
118
+ * **映像・音声・字幕の並び** (最後に読んだ PMT の `種別:PID`)。PMT を読むまでは null。
119
+ *
120
+ * これが変わったら ffmpeg を作り直す (`StreamsWatch`)。ffmpeg は起きたときの並びのまま
121
+ * 音声・字幕を選んでいるので、番組が替わって音声の本数や字幕の PID が変わると追従しない。
122
+ * データ放送などのセクション (0x0D など) は入れない — 番組ごとに増減しても ffmpeg には関係ない
123
+ */
124
+ get streams() {
125
+ return this.streamsNow;
126
+ }
127
+ /**
128
+ * **中継の番号 (transport_stream_id)。** PAT を読むまでは null。
129
+ *
130
+ * 絞るのに要るわけではありません。**データ放送が受信機に訊いてくる**ので
131
+ * 覚えておきます — テレ朝の TVerリンクは `browser.getProgramID(4)` で
132
+ * これを取り、TVer のサーバへそのまま送る。`null` のまま渡すと文字の
133
+ * "null" が飛んで、あちらが断り、画面には `DT-RE` とだけ出ます
134
+ * ([server/live.ts](https://github.com/danything/denpa/blob/main/src/lib/server/live.ts) の `programInfo`)
135
+ */
136
+ get transportStreamId() {
137
+ return this.tsid;
138
+ }
139
+ /**
140
+ * 任意の長さのバイト列を食わせて、残すぶんを返す。
141
+ *
142
+ * 落とした PID の連続性カウンタは飛ぶが、**残す PID は元のまま**なので
143
+ * 読む側から見て抜けは無い。自分で作るのは PAT だけで、そこは自前で数える。
144
+ */
145
+ filter(chunk) {
146
+ const out = [];
147
+ for (const ts of this.packets.feed(chunk)) {
148
+ const pid = packetPid(ts);
149
+ if (pid === PID_NULL)
150
+ continue;
151
+ if (pid === PID_PAT) {
152
+ this.readPat(ts);
153
+ // 元の PAT は捨てて、書き直したものを同じ回数だけ出す
154
+ if (this.rewritten !== null && (ts[1] & 0x40) !== 0)
155
+ out.push(packet(PID_PAT, this.rewritten, this.counter++));
156
+ continue;
157
+ }
158
+ if (pid === this.pmtPid) {
159
+ this.readPmt(ts);
160
+ out.push(ts);
161
+ }
162
+ else if (KEEP.has(pid) || this.own.has(pid))
163
+ out.push(ts);
164
+ }
165
+ return concatBytes(out);
166
+ }
167
+ readPat(ts) {
168
+ for (const section of this.pat.feed(ts)) {
169
+ if (section[0] !== TABLE_PAT)
170
+ continue;
171
+ this.tsid = u16(section, 3);
172
+ const pmtPid = parsePat(section).get(this.serviceId);
173
+ if (pmtPid === undefined)
174
+ continue;
175
+ if (pmtPid === this.pmtPid)
176
+ return;
177
+ // 選局し直した・PAT が入れ替わった。PMT も読み直す
178
+ this.pmtPid = pmtPid;
179
+ this.pmt = new SectionAssembler(pmtPid);
180
+ this.own.clear();
181
+ const version = (section[5] >> 1) & 0x1f;
182
+ this.rewritten = buildPat(this.tsid, version, this.serviceId, pmtPid);
183
+ return;
184
+ }
185
+ }
186
+ readPmt(ts) {
187
+ for (const section of this.pmt?.feed(ts) ?? []) {
188
+ if (section[0] !== TABLE_PMT)
189
+ continue;
190
+ const pids = new Set();
191
+ // PCR。映像と同じ PID のことが多いが、別に振られていることもある
192
+ const pcrPid = u16(section, 8) & 0x1fff;
193
+ if (pcrPid !== PID_NULL)
194
+ pids.add(pcrPid);
195
+ const programInfo = pmtProgramInfo(section);
196
+ collectCa(programInfo, pids);
197
+ let copyNever = hasCopyNever(programInfo);
198
+ const streams = [];
199
+ for (const [streamType, elementaryPid, info] of pmtStreams(section)) {
200
+ if (MEDIA_STREAMS.has(streamType))
201
+ streams.push(`${streamType}:${elementaryPid}`);
202
+ pids.add(elementaryPid);
203
+ collectCa(info, pids);
204
+ if (hasCopyNever(info))
205
+ copyNever = true;
206
+ }
207
+ this.own.clear();
208
+ for (const pid of pids)
209
+ this.own.add(pid);
210
+ this.copyNeverNow = copyNever;
211
+ this.streamsNow = streams.join(",");
212
+ }
213
+ }
214
+ }
215
+ /**
216
+ * **映像・音声・字幕の並びが変わったか** (`ServiceFilter.streams`) を見る。最初に読んだ並びを覚え、変わったら
217
+ * 1回だけ true を返して新しい並びを覚え直す。
218
+ *
219
+ * 並びを決め打ちで起こした ffmpeg (`-map`) は、番組が替わって音声の本数や字幕の PID が変わっても追従しない。
220
+ * 変わったら起こし直す — ライブと追っかけのエンコード (`server/live.ts` の `Session.pump`)、MPEG-2 をそのまま
221
+ * 流す経路の字幕 (`Session.runRaw`・`recordingCaptions`)
222
+ */
223
+ export class StreamsWatch {
224
+ seen = null;
225
+ /** `filter` に塊を通したあとに呼ぶ */
226
+ changed(filter) {
227
+ const now = filter.streams;
228
+ if (now === null)
229
+ return false;
230
+ const before = this.seen;
231
+ this.seen = now;
232
+ return before !== null && before !== now;
233
+ }
234
+ }
235
+ /** 記述子の並びに、コピー禁止のデジタルコピー制御記述子があるか (`psi.isCopyNever`) */
236
+ function hasCopyNever(body) {
237
+ for (const [tag, data] of descriptors(body)) {
238
+ if (tag === DESC_DIGITAL_COPY_CONTROL && isCopyNever(data))
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+ /** CA_descriptor から ECM の PID を読み取る。掛かったまま録れたものを後から復号するのに要る */
244
+ function collectCa(body, into) {
245
+ for (const [tag, data] of descriptors(body))
246
+ if (tag === DESC_CA && data.length >= 4)
247
+ into.add(u16(data, 2) & 0x1fff);
248
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * **本物の MPEG-2 映像と AAC 音声を、ffmpeg 無しで組み立てる** (試験用)。
3
+ *
4
+ * ライブを変換せずに送る経路 (docs/stream.md#放送そのままmpeg-2を送る) は、ブラウザが MPEG-2 をデコードして映像を出す
5
+ * ところまでが仕事で、そこを試すには**本当にデコードできる放送**が要る。偽エージェントの
6
+ * 放送は中身を 0xFF で埋めていて、デコードするものが無い。E2E のイメージには本物の ffmpeg も
7
+ * 入っていない (数十分かかる組み立てを避けている) ので、ここで小さく作る。
8
+ *
9
+ * - 映像 … 256x144 の**I フレームだけ**。マクロブロックごとに色 (DC) だけを置き、
10
+ * AC は持たない (EOB だけ)。コマごとに縞が流れるので、**映像が進んでいるか**を
11
+ * 画素で見分けられる。インタレの印も立て、輝度は上下のフィールドで別の時刻の映像にする (フィールド DCT)
12
+ * - 音声 … 48kHz ステレオの AAC-LC の**無音**を ADTS で。1コマ 7 バイト
13
+ *
14
+ * 規格は ISO/IEC 13818-2 (映像) と 13818-7 / 14496-3 (AAC)。表の出どころは
15
+ * 各関数の頭に書いてある。
16
+ */
17
+ /** 29.97 コマ/秒の1コマ (90kHz)。放送と同じ */
18
+ export declare const FRAME_TICKS = 3003;
19
+ /** AAC の1コマ (1024 標本 @48kHz を 90kHz で) */
20
+ export declare const AUDIO_TICKS = 1920;
21
+ /**
22
+ * I フレームを1枚。**毎枚、系列ヘッダから書く** — どこから受け取っても頭からデコードできる
23
+ * (放送は GOP ごとに書く。試験ではその手間を省く)。
24
+ *
25
+ * @param n 何枚目か。色と time_code に使う
26
+ * @param color マクロブロック (16x16) ごとの色。既定は流れる縞 (スクリーンショットを撮るときはカラーバー。`tests/fake/docs.ts`)
27
+ */
28
+ export declare function mpeg2Frame(n: number, color?: (col: number, row: number, n: number, field: 0 | 1) => [number, number, number]): Uint8Array;
29
+ /**
30
+ * AAC-LC の無音を1コマ、ADTS の頭付きで。48kHz・ステレオ (channel_configuration 2)。
31
+ *
32
+ * 中身は CPE が1つ: 両耳とも `max_sfb = 0` (係数を1つも持たない) で、あとは END。
33
+ * 55 ビットなので7バイトに収まる (14496-3 の raw_data_block)
34
+ */
35
+ export declare function silentAac(): Uint8Array;
36
+ /**
37
+ * PES にする。映像は長さ 0 (「次の頭まで」。放送もそう書く)、音声は長さを書く
38
+ *
39
+ * @param streamId 0xE0 (映像) / 0xC0 (音声)
40
+ */
41
+ export declare function pes(streamId: number, pts: number, data: Uint8Array): Uint8Array;
42
+ /**
43
+ * PES を TS パケットに割る。**最後のパケットは適応フィールドで詰める** (中身を 0xFF で
44
+ * 埋めると PES の続きに見える)。頭のパケットに PCR を載せることもできる
45
+ *
46
+ * @param counter 連続性カウンタの続き。返り値の `counter` を次に渡す
47
+ */
48
+ export declare function packetizePes(pid: number, data: Uint8Array, counter: number, pcr?: number | null): {
49
+ packets: Uint8Array;
50
+ counter: number;
51
+ };