@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.
@@ -0,0 +1,539 @@
1
+ /**
2
+ * 4K の放送 (MMT/TLV) の映像・音声・字幕を取り出す (ISO/IEC 23008-1、ARIB STD-B60)。TS の `pes.ts` に当たる。
3
+ *
4
+ * MMT では、映像・音声・字幕は MPU (頭出しできる所から次の頭出しできる所まで) に分けて送られる。
5
+ * MPU の中身はアクセスユニット (映像の 1 コマ、音声の 1 フレーム) の並びで、データユニットを
6
+ * MMTP のパケットに分割したり、小さいものを 1 つに集約したりして運ぶ (`mmtp.ts` の `parseMpu`)。
7
+ *
8
+ * **時刻はパケットには無く、MPT の記述子にある。** MPU タイムスタンプ記述子が MPU の最初の提示時刻を、
9
+ * MPU 拡張タイムスタンプ記述子がアクセスユニットごとのずれを、これから来る MPU のぶんまで並べる
10
+ * (実機のケーブルの BS4K で確かめた。映像は 1 MPU に 32 コマ、音声は 4 フレーム)。
11
+ * 時刻は NTP の 64 ビットで来るので、ここで **90 kHz** に直す (TS の PTS と同じ物差し。一周はしない)。
12
+ *
13
+ * 中身の形式は次のとおり。実機の録りは映像・音声がスクランブルされていて中身を読めないので、どれも推定。
14
+ *
15
+ * - 映像 (`hev1`): データユニット 1 つが、長さ (4 バイト) を前に付けた NAL ユニット。MPU は ISO BMFF の形式なので、
16
+ * サンプルの中身も MP4 と同じ形と見ている。同じ sample_number のデータユニットが 1 つのコマ
17
+ * - 音声 (`mp4a`): データユニット 1 つが LATM の AudioMuxElement (LOAS の同期のヘッダーは付かない)。
18
+ * MPT の音声コンポーネント記述子の stream_type は 0x11 (LATM/LOAS) だった (確かめた)
19
+ * - 字幕 (`stpp`): MPU 1 つが TTML の文書 1 つ (`ttml.ts` で読む)。文書の中の画像 (PNG) は扱わない
20
+ *
21
+ * スクランブルされたパケットは数えて捨てる (`scrambled`)。復号はエージェントの仕事
22
+ */
23
+ import { BitReader } from "./bits.js";
24
+ import { concatBytes, u16, u32 } from "./bytes.js";
25
+ import { mmtDescriptors, parseMpt, TABLE_MPT } from "./mmt-si.js";
26
+ import { MESSAGE_PA, messageTables, parseMessage, parseMmtp, parseMpu, PAYLOAD_MPU, PAYLOAD_SIGNALING, SignalingAssembler, } from "./mmtp.js";
27
+ import { ADTS_RATES, CLOCK } from "./pes.js";
28
+ import { TlvIpReader } from "./tlv-ip.js";
29
+ import { TLV_COMPRESSED_IP, TlvStream } from "./tlv.js";
30
+ /** 放送の時刻の物差し (TS の PTS と同じ。`pes.ts` の `CLOCK`) */
31
+ export { CLOCK };
32
+ const DESC_MPU_TIMESTAMP = 0x0001;
33
+ const DESC_MPU_EXTENDED_TIMESTAMP = 0x8026;
34
+ /** MPU のペイロードヘッダーの fragment_type。2 は MFU (中身)。0・1 はメタデータで、放送では使わない */
35
+ const FRAGMENT_MFU = 2;
36
+ /** 時刻のあるデータユニットのヘッダー (movie_fragment_sequence_number・sample_number・offset・priority・dependency_counter) */
37
+ const TIMED_HEADER = 14;
38
+ /** 時刻の無いデータユニットのヘッダー (item_id) */
39
+ const ITEM_HEADER = 4;
40
+ const WHOLE = 0;
41
+ const FIRST = 1;
42
+ const LAST = 3;
43
+ /** 取っておく MPU の時刻の数 (アセットごと)。実機の MPT は、これから来る 3〜15 MPU ぶんを並べていた */
44
+ const TIMINGS_MOST = 64;
45
+ /** 組み立て中のアクセスユニットの大きさの上限。4K の I ピクチャでも数 MB */
46
+ const UNIT_MOST = 16 * 1024 * 1024;
47
+ /** NTP の 64 ビットの時刻 (上位 32 ビットが秒、下位 32 ビットが端数) を 90 kHz に */
48
+ export function ntpToClock(data, at) {
49
+ return Math.round(u32(data, at) * CLOCK + (u32(data, at + 4) * CLOCK) / 2 ** 32);
50
+ }
51
+ /** MPU タイムスタンプ記述子を読む。`[mpu_sequence_number, 最初の提示時刻 (90 kHz)]` の並び */
52
+ export function parseMpuTimestamps(body) {
53
+ const out = [];
54
+ for (let at = 0; at + 12 <= body.length; at += 12)
55
+ out.push([u32(body, at), ntpToClock(body, at + 4)]);
56
+ return out;
57
+ }
58
+ /**
59
+ * MPU 拡張タイムスタンプ記述子を読む。次のものは DTS を進められないので、空を返す (その MPU は時刻が分からないものとして捨てる)
60
+ *
61
+ * - timescale を持たないもの (MPU のメタデータの mdhd を見る、の意味)。放送ではメタデータを送らない
62
+ * - pts_offset を持たないもの (pts_offset_type 0)。コマの長さが分からない
63
+ */
64
+ export function parseMpuExtendedTimestamps(body) {
65
+ if (body.length < 5 || (body[0] & 0x01) === 0)
66
+ return [];
67
+ const offsetType = (body[0] >> 1) & 0x03;
68
+ if (offsetType === 0)
69
+ return [];
70
+ const timescale = u32(body, 1);
71
+ let at = 5;
72
+ let fixed = 0;
73
+ if (offsetType === 1) {
74
+ if (at + 2 > body.length)
75
+ return [];
76
+ fixed = u16(body, at);
77
+ at += 2;
78
+ }
79
+ const step = offsetType === 2 ? 4 : 2;
80
+ const out = [];
81
+ while (at + 8 <= body.length && timescale > 0) {
82
+ const mpuSequenceNumber = u32(body, at);
83
+ // leap_indicator (2)・予約 (6) は飛ばす
84
+ const decodingOffset = u16(body, at + 5);
85
+ const count = body[at + 7];
86
+ at += 8;
87
+ if (at + count * step > body.length)
88
+ break;
89
+ const dtsPtsOffsets = [];
90
+ const durations = [];
91
+ for (let i = 0; i < count; i++, at += step) {
92
+ dtsPtsOffsets.push(u16(body, at));
93
+ durations.push(offsetType === 2 ? u16(body, at + 2) : fixed);
94
+ }
95
+ out.push({
96
+ mpuSequenceNumber,
97
+ timescale,
98
+ decodingOffset,
99
+ dtsPtsOffsets,
100
+ durations,
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ /**
106
+ * MPU の `index` 番目 (デコードの順、0 から) のアクセスユニットの時刻。
107
+ * DTS は最初の提示時刻から decodingOffset だけ戻った所に始まり、durations を足して進む。
108
+ * PTS は DTS に dtsPtsOffsets を足したもの。記述子に無い番目なら null
109
+ */
110
+ export function unitTime(presentation, extended, index) {
111
+ const offset = extended.dtsPtsOffsets[index];
112
+ const length = extended.durations[index];
113
+ if (offset === undefined || length === undefined)
114
+ return null;
115
+ let decoding = -extended.decodingOffset;
116
+ for (let i = 0; i < index; i++)
117
+ decoding += extended.durations[i];
118
+ // timescale の単位で足してから直す (1 つずつ直すと、59.94 コマの 1501.5 の端数が積もる)
119
+ const at = (ticks) => presentation + Math.round((ticks * CLOCK) / extended.timescale);
120
+ const dts = at(decoding);
121
+ return {
122
+ dts,
123
+ pts: at(decoding + offset),
124
+ duration: at(decoding + length) - dts,
125
+ };
126
+ }
127
+ /** GASpecificConfig を持つ audioObjectType */
128
+ const GA_OBJECT_TYPES = new Set([1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23]);
129
+ /** channelConfiguration → チャンネルの数 */
130
+ const CHANNELS = [0, 1, 2, 3, 4, 5, 6, 8];
131
+ function objectType(bits) {
132
+ const value = bits.bits(5);
133
+ return value === 31 ? 32 + bits.bits(6) : value;
134
+ }
135
+ function sampleRate(bits) {
136
+ const index = bits.bits(4);
137
+ return index === 15 ? bits.bits(24) : (ADTS_RATES[index] ?? 0);
138
+ }
139
+ /** AudioSpecificConfig を読む。program_config_element が要るもの (channelConfiguration 0) は読まない */
140
+ function readAudioSpecificConfig(bits) {
141
+ let type = objectType(bits);
142
+ const rate = sampleRate(bits);
143
+ const channelConfiguration = bits.bits(4);
144
+ // SBR・PS を明示したもの (5・29) は、拡張の周波数のあとに元の audioObjectType が来る
145
+ if (type === 5 || type === 29) {
146
+ sampleRate(bits);
147
+ type = objectType(bits);
148
+ }
149
+ if (!GA_OBJECT_TYPES.has(type) || channelConfiguration === 0)
150
+ return null;
151
+ bits.skip(1); // frameLengthFlag
152
+ if (bits.bit())
153
+ bits.skip(14); // dependsOnCoreCoder → coreCoderDelay
154
+ const extension = bits.bit();
155
+ if (type === 6 || type === 20)
156
+ bits.skip(3);
157
+ if (extension) {
158
+ if (type === 22)
159
+ bits.skip(16);
160
+ if (type === 17 || type === 19 || type === 20 || type === 23)
161
+ bits.skip(3);
162
+ bits.skip(1);
163
+ }
164
+ return {
165
+ objectType: type,
166
+ sampleRate: rate,
167
+ channels: CHANNELS[channelConfiguration] ?? 0,
168
+ };
169
+ }
170
+ /** LatmGetValue (ISO/IEC 14496-3) */
171
+ function latmValue(bits) {
172
+ const count = bits.bits(2);
173
+ let value = 0;
174
+ for (let i = 0; i <= count; i++)
175
+ value = value * 256 + bits.bits(8);
176
+ return value;
177
+ }
178
+ /**
179
+ * LATM の AudioMuxElement (muxConfigPresent = 1) から、AAC のフレームを取り出す。
180
+ * **設定 (StreamMuxConfig) は来たときだけ書いてあり、あいだは「前と同じ」になる** ので、前の設定を覚えておく。
181
+ *
182
+ * 読むのは放送で使う形 (番組 1・層 1・サブフレーム 1・frameLengthType 0) だけ。ほかの形は null を返す
183
+ */
184
+ export class LatmReader {
185
+ config = null;
186
+ read(element) {
187
+ const bits = new BitReader(element);
188
+ // useSameStreamMux
189
+ if (!bits.bit())
190
+ this.config = this.readStreamMuxConfig(bits);
191
+ const config = this.config;
192
+ if (config === null)
193
+ return null;
194
+ // PayloadLengthInfo: 255 が続くあいだ足す
195
+ let length = 0;
196
+ for (;;) {
197
+ const part = bits.bits(8);
198
+ length += part;
199
+ if (part !== 255 || bits.overrun)
200
+ break;
201
+ }
202
+ if (bits.overrun || bits.left < length * 8)
203
+ return null;
204
+ // PayloadMux はバイトの区切りに揃っていない
205
+ return { config, frame: bits.slice(bits.at, length * 8) };
206
+ }
207
+ readStreamMuxConfig(bits) {
208
+ const version = bits.bit();
209
+ if (version && bits.bit())
210
+ return null; // audioMuxVersionA は 0 だけ
211
+ if (version)
212
+ latmValue(bits); // taraBufferFullness
213
+ bits.skip(1); // allStreamsSameTimeFraming
214
+ const subFrames = bits.bits(6);
215
+ const programs = bits.bits(4);
216
+ const layers = bits.bits(3);
217
+ if (subFrames !== 0 || programs !== 0 || layers !== 0)
218
+ return null;
219
+ // audioMuxVersion 1 は AudioSpecificConfig の長さが前に付く。0 は読んで長さを知る
220
+ const declared = version ? latmValue(bits) : null;
221
+ const start = bits.at;
222
+ const fields = readAudioSpecificConfig(bits);
223
+ if (fields === null)
224
+ return null;
225
+ if (declared !== null)
226
+ bits.at = start + declared;
227
+ const audioSpecificConfig = bits.slice(start, bits.at - start);
228
+ if (bits.bits(3) !== 0)
229
+ return null; // frameLengthType は 0 だけ
230
+ bits.skip(8); // latmBufferFullness
231
+ if (bits.bit()) {
232
+ // otherDataPresent
233
+ if (version)
234
+ latmValue(bits);
235
+ else
236
+ for (let more = 1; more && !bits.overrun;) {
237
+ more = bits.bit();
238
+ bits.skip(8);
239
+ }
240
+ }
241
+ if (bits.bit())
242
+ bits.skip(8); // crcCheckPresent
243
+ return bits.overrun ? null : { ...fields, audioSpecificConfig };
244
+ }
245
+ }
246
+ const KINDS = {
247
+ hev1: "video",
248
+ hvc1: "video",
249
+ mp4a: "audio",
250
+ stpp: "caption",
251
+ };
252
+ function newTrack(asset) {
253
+ return {
254
+ asset,
255
+ presentations: new Map(),
256
+ extended: new Map(),
257
+ mpu: null,
258
+ index: 0,
259
+ sample: null,
260
+ units: [],
261
+ size: 0,
262
+ fragment: null,
263
+ sequence: null,
264
+ broken: false,
265
+ latm: new LatmReader(),
266
+ };
267
+ }
268
+ /** 覚える数を超えたら、古いものから捨てる */
269
+ function remember(map, key, value) {
270
+ map.delete(key);
271
+ map.set(key, value);
272
+ for (const old of map.keys()) {
273
+ if (map.size <= TIMINGS_MOST)
274
+ break;
275
+ map.delete(old);
276
+ }
277
+ }
278
+ /** 長さ (4 バイト) を前に付けた NAL ユニットの並びを切り分ける。長さが合わなければ null */
279
+ function lengthPrefixed(data) {
280
+ const out = [];
281
+ let at = 0;
282
+ while (at + 4 <= data.length) {
283
+ const end = at + 4 + u32(data, at);
284
+ if (end > data.length)
285
+ return null;
286
+ out.push(data.subarray(at + 4, end));
287
+ at = end;
288
+ }
289
+ return at === data.length && out.length > 0 ? out : null;
290
+ }
291
+ /** NAL ユニットの種類が IRAP (16〜23) */
292
+ function irap(nal) {
293
+ const type = (nal[0] >> 1) & 0x3f;
294
+ return type >= 16 && type <= 23;
295
+ }
296
+ const utf8 = new TextDecoder();
297
+ /**
298
+ * 1 局に絞った TLV のバイト列 (`mmt-service-filter.ts`) から、映像・音声・字幕のアクセスユニットを時刻つきで取り出す。
299
+ *
300
+ * - 局 (`serviceId` = MMT_package_id) の MPT を読んで、映像・音声・字幕のそれぞれ最初のアセットを選ぶ。
301
+ * アセットは MPT と同じ IP の並びのもの (在りかの種類 0) だけを読む
302
+ * - 時刻は MPT の記述子から引く。時刻がまだ分からない MPU (最初の MPT より先に来たもの) は数えて捨てる (`untimed`)
303
+ * - パケットが欠けた (packet_sequence_number が続かない) アクセスユニットは捨てる (`dropped`)
304
+ * - アクセスユニットは、次のアクセスユニット (か次の MPU) の始まりが来たときに返す。最後のものは `end` で返す
305
+ */
306
+ export class MmtMediaReader {
307
+ serviceId;
308
+ tlv = new TlvStream();
309
+ ip = new TlvIpReader();
310
+ signaling = new SignalingAssembler();
311
+ /** `並び/packet_id` → 組み立て */
312
+ tracks = new Map();
313
+ /** 選んだアセットの並び (変わったかを見る) */
314
+ selection = "";
315
+ /** 選んだアセットの MMTP のパケット (MPU) の数。スクランブルされていたもの (`scrambled`) も数える */
316
+ media = 0;
317
+ /** スクランブルされていて読めなかった MMTP のパケットの数 */
318
+ scrambled = 0;
319
+ /** 時刻が分からずに捨てたアクセスユニットの数 */
320
+ untimed = 0;
321
+ /** パケットが欠けたか、中身を読めずに捨てたアクセスユニットの数 */
322
+ dropped = 0;
323
+ constructor(serviceId) {
324
+ this.serviceId = serviceId;
325
+ }
326
+ /** 選んでいるアセット */
327
+ get assets() {
328
+ return [...this.tracks.values()].map((t) => t.asset);
329
+ }
330
+ *feed(chunk) {
331
+ for (const packet of this.tlv.feed(chunk)) {
332
+ if (packet.type !== TLV_COMPRESSED_IP)
333
+ continue;
334
+ const datagram = this.ip.read(packet);
335
+ if (datagram === null)
336
+ continue;
337
+ const mmtp = parseMmtp(datagram.payload);
338
+ if (mmtp === null)
339
+ continue;
340
+ if (mmtp.payloadType === PAYLOAD_SIGNALING) {
341
+ for (const raw of this.signaling.feed(datagram.flow, mmtp)) {
342
+ const message = parseMessage(raw);
343
+ if (message?.messageId !== MESSAGE_PA)
344
+ continue;
345
+ for (const table of messageTables(message)) {
346
+ if (table[0] !== TABLE_MPT)
347
+ continue;
348
+ const mpt = parseMpt(table);
349
+ if (mpt?.packageId === this.serviceId)
350
+ yield* this.readMpt(datagram.flow, mpt);
351
+ }
352
+ }
353
+ continue;
354
+ }
355
+ if (mmtp.payloadType !== PAYLOAD_MPU)
356
+ continue;
357
+ const track = this.tracks.get(`${datagram.flow}/${mmtp.packetId}`);
358
+ if (track === undefined)
359
+ continue;
360
+ this.media++;
361
+ // パケットが欠けたら、組み立て中のアクセスユニットはもう組めない
362
+ if (track.sequence !== null &&
363
+ mmtp.sequenceNumber !== (track.sequence + 1) >>> 0) {
364
+ track.broken = true;
365
+ track.fragment = null;
366
+ }
367
+ track.sequence = mmtp.sequenceNumber;
368
+ if (mmtp.scrambling === "odd" || mmtp.scrambling === "even") {
369
+ this.scrambled++;
370
+ track.broken = true;
371
+ track.fragment = null;
372
+ continue;
373
+ }
374
+ yield* this.readMpu(track, mmtp.payload);
375
+ }
376
+ }
377
+ /** 終わりまで読んだとき、組み立て中のアクセスユニットを閉じて返す (録画のファイルの最後など) */
378
+ *end() {
379
+ for (const track of this.tracks.values()) {
380
+ track.fragment = null;
381
+ yield* this.finish(track);
382
+ }
383
+ }
384
+ /** MPT から、アセットを選び直して時刻を覚える */
385
+ *readMpt(flow, mpt) {
386
+ const chosen = new Map();
387
+ for (const asset of mpt.assets) {
388
+ const kind = KINDS[asset.type];
389
+ const location = asset.locations[0];
390
+ if (kind === undefined || chosen.has(kind))
391
+ continue;
392
+ if (location?.type !== 0 || location.packetId === null)
393
+ continue;
394
+ chosen.set(kind, {
395
+ kind,
396
+ type: asset.type,
397
+ packetId: location.packetId,
398
+ componentTag: asset.componentTag,
399
+ });
400
+ }
401
+ const selection = JSON.stringify([flow, ...chosen.values()]);
402
+ if (selection !== this.selection) {
403
+ this.selection = selection;
404
+ this.tracks = new Map([...chosen.values()].map((asset) => [
405
+ `${flow}/${asset.packetId}`,
406
+ newTrack(asset),
407
+ ]));
408
+ yield { kind: "assets", assets: this.assets };
409
+ }
410
+ for (const asset of mpt.assets) {
411
+ const location = asset.locations[0];
412
+ const track = this.tracks.get(`${flow}/${location?.packetId}`);
413
+ if (track === undefined || track.asset.type !== asset.type)
414
+ continue;
415
+ for (const [tag, body] of mmtDescriptors(asset.descriptors)) {
416
+ if (tag === DESC_MPU_TIMESTAMP)
417
+ for (const [sequence, time] of parseMpuTimestamps(body))
418
+ remember(track.presentations, sequence, time);
419
+ if (tag === DESC_MPU_EXTENDED_TIMESTAMP)
420
+ for (const entry of parseMpuExtendedTimestamps(body))
421
+ remember(track.extended, entry.mpuSequenceNumber, entry);
422
+ }
423
+ }
424
+ }
425
+ /** MPU のパケットの中身を 1 つ読む */
426
+ *readMpu(track, payload) {
427
+ const mpu = parseMpu(payload);
428
+ if (mpu === null || mpu.fragmentType !== FRAGMENT_MFU)
429
+ return;
430
+ if (mpu.mpuSequenceNumber !== track.mpu) {
431
+ yield* this.finish(track);
432
+ track.mpu = mpu.mpuSequenceNumber;
433
+ track.index = 0;
434
+ track.sample = null;
435
+ }
436
+ const header = mpu.timed ? TIMED_HEADER : ITEM_HEADER;
437
+ const data = mpu.data;
438
+ if (mpu.aggregated) {
439
+ // データユニットごとに長さ (ヘッダーを含む) が前に付く
440
+ let at = 0;
441
+ while (at + 2 <= data.length) {
442
+ const end = at + 2 + u16(data, at);
443
+ if (end > data.length || end - at - 2 < header)
444
+ break;
445
+ yield* this.dataUnit(track, mpu.timed, data.subarray(at + 2, end), WHOLE);
446
+ at = end;
447
+ }
448
+ return;
449
+ }
450
+ if (data.length < header)
451
+ return;
452
+ yield* this.dataUnit(track, mpu.timed, data, mpu.fragmentation);
453
+ }
454
+ /** データユニット (分割したものはその断片) を 1 つ受ける。ヘッダーは断片ごとに付いている */
455
+ *dataUnit(track, timed, unit, fragmentation) {
456
+ // 時刻の無いもの (MPU 全体で 1 つ) は sample_number が無いので 0 とする
457
+ const sample = timed ? u32(unit, 4) : 0;
458
+ const body = unit.subarray(timed ? TIMED_HEADER : ITEM_HEADER);
459
+ if (fragmentation === WHOLE || fragmentation === FIRST) {
460
+ if (track.sample !== null && sample !== track.sample)
461
+ yield* this.finish(track);
462
+ track.sample = sample;
463
+ }
464
+ // 中身は届いたチャンクの一部を指すので、持ち越すものはコピーする (`TlvStream`)
465
+ if (fragmentation === WHOLE) {
466
+ this.keep(track, body.slice());
467
+ return;
468
+ }
469
+ if (fragmentation === FIRST) {
470
+ track.fragment = [body.slice()];
471
+ return;
472
+ }
473
+ if (track.fragment === null) {
474
+ track.broken = true;
475
+ return;
476
+ }
477
+ track.fragment.push(body.slice());
478
+ if (fragmentation === LAST) {
479
+ this.keep(track, concatBytes(track.fragment));
480
+ track.fragment = null;
481
+ }
482
+ }
483
+ keep(track, unit) {
484
+ track.size += unit.length;
485
+ if (track.size > UNIT_MOST)
486
+ track.broken = true;
487
+ else
488
+ track.units.push(unit);
489
+ }
490
+ /** 組み立て中のアクセスユニットを閉じて返す */
491
+ *finish(track) {
492
+ const { units, broken, index, mpu } = track;
493
+ track.units = [];
494
+ track.size = 0;
495
+ track.broken = false;
496
+ if ((units.length === 0 && !broken) || mpu === null)
497
+ return;
498
+ track.index++;
499
+ if (broken) {
500
+ this.dropped++;
501
+ return;
502
+ }
503
+ const presentation = track.presentations.get(mpu);
504
+ const extended = track.extended.get(mpu);
505
+ const time = presentation === undefined || extended === undefined
506
+ ? null
507
+ : unitTime(presentation, extended, index);
508
+ if (time === null) {
509
+ this.untimed++;
510
+ return;
511
+ }
512
+ const base = { ...time, asset: track.asset, mpuSequenceNumber: mpu };
513
+ switch (track.asset.kind) {
514
+ case "video": {
515
+ const nals = lengthPrefixed(concatBytes(units));
516
+ if (nals === null)
517
+ break;
518
+ yield { ...base, kind: "video", nals, keyframe: nals.some(irap) };
519
+ return;
520
+ }
521
+ case "audio": {
522
+ // データユニット 1 つが 1 フレーム。設定を覚えるために全部読み、最後のものを返す (放送では 1 つ)
523
+ const read = units.map((u) => track.latm.read(u)).at(-1) ?? null;
524
+ if (read === null)
525
+ break;
526
+ yield { ...base, kind: "audio", ...read };
527
+ return;
528
+ }
529
+ case "caption":
530
+ yield {
531
+ ...base,
532
+ kind: "caption",
533
+ document: utf8.decode(concatBytes(units)),
534
+ };
535
+ return;
536
+ }
537
+ this.dropped++;
538
+ }
539
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * チャンネル丸ごとの TLV のバイト列から、1 つの局のぶんだけを抜き出す。TS の `service-filter.ts` に当たり、
3
+ * 出力も TLV のバイト列のまま (TLV のパケットを 1 バイトも変えずに、残すものだけを並べる)。
4
+ *
5
+ * 局を見分けるのは MPT (MMT_package_id が service_id)。MPT は IP の並び (`tlv-ip.ts` の `flow`) の中を
6
+ * 運ばれ、MMTP の packet_id はその並びの中でだけ決まっているので、残すかどうかは並びと packet_id の組で決める。
7
+ *
8
+ * - 残す … TLV-SI (TLV-NIT など)、IP のヘッダーを圧縮していない IP (NTP)、その局の MPT が載った並びの信号
9
+ * (PLT・MPT・番組表・ECM)、その局の MPT が挙げるアセット (映像・音声・字幕) の MPU
10
+ * - 落とす … 詰め物、ほかの局の MPT とそのアセット、PLT がほかの局だけを挙げている並び
11
+ *
12
+ * 実機のケーブルの BS4K は、1 つの TLV のストリームに局が 1 つ (IP の並びも 1 つ) だったので、たいていは
13
+ * 詰め物のほかは全部残る。AMT (局ごとの IP の宛先) は 0.8 秒の間に来なかったので使わない。
14
+ * MPT は 0.1 秒おきに来ていた
15
+ */
16
+ import { type Mpt } from "./mmt-si.js";
17
+ /**
18
+ * 1 つの局に絞る。
19
+ *
20
+ * その局の MPT を読むまでは、MPU (映像・音声) を出さずに取っておき、読んだら MPT の後ろに出す
21
+ * (並びは届いた順のまま)。信号は待たずに出す
22
+ */
23
+ export declare class MmtServiceFilter {
24
+ private readonly serviceId;
25
+ private readonly tlv;
26
+ private readonly ip;
27
+ private readonly signaling;
28
+ private readonly flows;
29
+ private held;
30
+ private mptNow;
31
+ /** @param serviceId 残す局 (MPT の MMT_package_id) */
32
+ constructor(serviceId: number);
33
+ /** その局の MPT を読めたか。読めるまで映像と音声は 1 バイトも出ない (届いたものは取っておく) */
34
+ get ready(): boolean;
35
+ /** 最後に読んだ、その局の MPT。読むまでは null */
36
+ get mpt(): Mpt | null;
37
+ /** 任意の長さの TLV のバイト列を渡すと、残すぶんを返す */
38
+ filter(chunk: Uint8Array): Uint8Array;
39
+ private flow;
40
+ private readSignaling;
41
+ /** MPT を読む前のパケットを取っておく。**コピーする** (パケットは届いたチャンクの一部を指す。`TlvStream`) */
42
+ private hold;
43
+ private release;
44
+ }