@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,476 @@
1
+ /**
2
+ * 試験用の TLV のバイト列 (TLV・IP・MMTP・MMT-SI) を組み立てる。TS の `synth.ts` と同じ役で、
3
+ * 読む側 (`tlv.ts`・`tlv-ip.ts`・`mmtp.ts`・`mmt-si.ts`・`mh-eit.ts`・`mmt-service-filter.ts`・`mmt-media.ts`) と対になる書く側。
4
+ *
5
+ * スクランブルは印 (ヘッダー拡張) だけを付け、中身は暗号にしない。
6
+ */
7
+ import { concatBytes } from "./bytes.js";
8
+ import { withCrc } from "./psi.js";
9
+ import { bcdDuration, mjdTime } from "./synth.js";
10
+ import { TLV_COMPRESSED_IP, TLV_IPV6, TLV_SIGNALING } from "./tlv.js";
11
+ const be = (value) => [(value >> 8) & 0xff, value & 0xff];
12
+ const be32 = (value) => [...be(value >>> 16), ...be(value & 0xffff)];
13
+ const utf8 = (text) => [...new TextEncoder().encode(text)];
14
+ const JPN = [0x6a, 0x70, 0x6e];
15
+ /** TLV のパケット 1 つ */
16
+ export function tlvPacket(type, payload) {
17
+ const body = Uint8Array.from(payload);
18
+ return Uint8Array.from([0x7f, type, ...be(body.length), ...body]);
19
+ }
20
+ /** IPv6 のアドレスを 16 バイトに (`ff3e::a000:1000` のような縮めた形も読む) */
21
+ function ipv6Bytes(text) {
22
+ const [head = "", tail] = text.split("::");
23
+ const groups = (part) => part === "" ? [] : part.split(":").map((g) => parseInt(g, 16));
24
+ const front = groups(head);
25
+ const back = tail === undefined ? [] : groups(tail);
26
+ const zeros = new Array(8 - front.length - back.length).fill(0);
27
+ return [...front, ...zeros, ...back].flatMap(be);
28
+ }
29
+ /**
30
+ * ヘッダー圧縮した IP の TLV パケット。`address` を渡せば全部のヘッダー (圧縮の種類 0x60)、
31
+ * 渡さなければヘッダー無し (0x61)
32
+ */
33
+ export function compressedIp(cid, payload, address, sequence = 0) {
34
+ const head = [(cid >> 4) & 0xff, ((cid & 0x0f) << 4) | (sequence & 0x0f)];
35
+ if (address === undefined)
36
+ return tlvPacket(TLV_COMPRESSED_IP, [...head, 0x61, ...payload]);
37
+ const ip = [
38
+ 0x60,
39
+ 0x00,
40
+ 0x00,
41
+ 0x00, // version・traffic class・flow label
42
+ 17, // next header (UDP)
43
+ 0x40, // hop limit
44
+ ...ipv6Bytes(address.source ?? "2401:dbc0:1003::b111"),
45
+ ...ipv6Bytes(address.destination ?? "ff3e::a000:1000"),
46
+ ...be(address.sourcePort ?? 50000),
47
+ ...be(address.destinationPort ?? 51216),
48
+ ];
49
+ return tlvPacket(TLV_COMPRESSED_IP, [...head, 0x60, ...ip, ...payload]);
50
+ }
51
+ /** 素の IPv6 の UDP の TLV パケット (NTP など) */
52
+ export function ipv6Udp(destination, destinationPort, payload) {
53
+ const udp = [
54
+ ...be(destinationPort),
55
+ ...be(destinationPort),
56
+ ...be(8 + payload.length),
57
+ 0x00,
58
+ 0x00,
59
+ ...payload,
60
+ ];
61
+ return tlvPacket(TLV_IPV6, [
62
+ 0x60,
63
+ 0x00,
64
+ 0x00,
65
+ 0x00,
66
+ ...be(udp.length),
67
+ 17,
68
+ 0x01,
69
+ ...ipv6Bytes("fe80::1"),
70
+ ...ipv6Bytes(destination),
71
+ ...udp,
72
+ ]);
73
+ }
74
+ /** TLV-SI の TLV パケット。セクションを並べ、残りは詰めない */
75
+ export function tlvSi(...sections) {
76
+ return tlvPacket(TLV_SIGNALING, concatBytes(sections));
77
+ }
78
+ const SCRAMBLING_BITS = {
79
+ none: 0,
80
+ even: 2,
81
+ odd: 3,
82
+ };
83
+ /** MMTP のパケット 1 つ (UDP の中身) */
84
+ export function mmtpPacket(packet) {
85
+ const extension = packet.scrambling === undefined
86
+ ? []
87
+ : [
88
+ // 種類がいくつも入る拡張 (0x0000) の中に、スクランブルの印 (種類 0x0001・長さ 1。上位 1 ビットは「これで終わり」)
89
+ ...be(0x0000),
90
+ ...be(5),
91
+ ...be(0x8001),
92
+ ...be(1),
93
+ (SCRAMBLING_BITS[packet.scrambling] << 6) | 0x3f,
94
+ ];
95
+ return Uint8Array.from([
96
+ // version 0、packet_counter_flag 0、FEC 0、extension_flag、RAP_flag
97
+ (extension.length > 0 ? 0x02 : 0) | (packet.rap === true ? 0x01 : 0),
98
+ packet.payloadType & 0x3f,
99
+ ...be(packet.packetId),
100
+ ...be32(0), // delivery_timestamp
101
+ ...be32(packet.sequenceNumber ?? 0),
102
+ ...extension,
103
+ ...packet.payload,
104
+ ]);
105
+ }
106
+ /** MPU のパケットの中身 (ペイロードヘッダー 8 バイトと、その後ろ) */
107
+ export function mpuPayload(data, mpuSequenceNumber = 0) {
108
+ const body = [
109
+ 0x28, // fragment_type 2 (MFU)、timed、分割なし、集約なし
110
+ 0x00,
111
+ ...be32(mpuSequenceNumber),
112
+ ...data,
113
+ ];
114
+ return Uint8Array.from([...be(body.length), ...body]);
115
+ }
116
+ /** 時刻のあるデータユニットのヘッダー (14 バイト。offset はサンプルの中の位置) */
117
+ function timedHeader(sample, offset) {
118
+ return [...be32(0), ...be32(sample), ...be32(offset), 0x00, 0x00];
119
+ }
120
+ /**
121
+ * MPU の中身 (MFU) を運ぶ MMTP のパケットの並び。データユニットは時刻のあるもの (映像・音声・字幕)。
122
+ * 分割したデータユニットは、断片ごとにヘッダーを付ける
123
+ */
124
+ export function mfuPackets(packetId, mpuSequenceNumber, units, options = {}) {
125
+ const start = options.sequenceNumber ?? 0;
126
+ let sequenceNumber = start;
127
+ const packet = (indicator, aggregated, body) => {
128
+ const payload = [
129
+ // fragment_type 2 (MFU)、timed、分割の印、集約の印
130
+ 0x28 | (indicator << 1) | (aggregated ? 0x01 : 0),
131
+ 0x00,
132
+ ...be32(mpuSequenceNumber),
133
+ ...body,
134
+ ];
135
+ return mmtpPacket({
136
+ packetId,
137
+ payloadType: 0,
138
+ rap: sequenceNumber === start && options.rap === true,
139
+ sequenceNumber: sequenceNumber++,
140
+ payload: [...be(payload.length), ...payload],
141
+ });
142
+ };
143
+ if (options.aggregate === true) {
144
+ const body = units.flatMap((unit) => {
145
+ const du = [...timedHeader(unit.sample, 0), ...unit.data];
146
+ return [...be(du.length), ...du];
147
+ });
148
+ return [packet(0, true, body)];
149
+ }
150
+ const out = [];
151
+ for (const unit of units) {
152
+ const data = [...unit.data];
153
+ const size = options.fragment ?? data.length;
154
+ if (data.length <= size) {
155
+ out.push(packet(0, false, [...timedHeader(unit.sample, 0), ...data]));
156
+ continue;
157
+ }
158
+ for (let at = 0; at < data.length; at += size) {
159
+ const indicator = at === 0 ? 1 : at + size >= data.length ? 3 : 2;
160
+ out.push(packet(indicator, false, [
161
+ ...timedHeader(unit.sample, at),
162
+ ...data.slice(at, at + size),
163
+ ]));
164
+ }
165
+ }
166
+ return out;
167
+ }
168
+ /** MPU タイムスタンプ記述子。MPU ごとに `[mpu_sequence_number, 最初の提示時刻]` */
169
+ export function mpuTimestampDescriptor(mpus) {
170
+ return mmtDescriptor(0x0001, mpus.flatMap(([sequence, [seconds, fraction]]) => [
171
+ ...be32(sequence),
172
+ ...be32(seconds),
173
+ ...be32(fraction),
174
+ ]));
175
+ }
176
+ /**
177
+ * MPU 拡張タイムスタンプ記述子。`duration` を渡せば pts_offset をどれも同じ値で書き (種類 1。実機の BS4K はこれ)、
178
+ * 渡さなければアクセスユニットごとに書く (種類 2)
179
+ */
180
+ export function mpuExtendedTimestampDescriptor(timescale, mpus, duration) {
181
+ const type = duration === undefined ? 2 : 1;
182
+ return mmtDescriptor(0x8026, [
183
+ 0xf8 | (type << 1) | 0x01,
184
+ ...be32(timescale),
185
+ ...(duration === undefined ? [] : be(duration)),
186
+ ...mpus.flatMap((mpu) => [
187
+ ...be32(mpu.sequence),
188
+ 0x3f,
189
+ ...be(mpu.decodingOffset),
190
+ mpu.dtsPtsOffsets.length,
191
+ ...mpu.dtsPtsOffsets.flatMap((offset, i) => [
192
+ ...be(offset),
193
+ ...(type === 2 ? be(mpu.durations?.[i] ?? 0) : []),
194
+ ]),
195
+ ]),
196
+ ]);
197
+ }
198
+ /** ビットを詰めていく (LATM を書くため) */
199
+ class BitWriter {
200
+ bytes = [];
201
+ at = 0;
202
+ put(value, count) {
203
+ for (let i = count - 1; i >= 0; i--) {
204
+ if (this.at % 8 === 0)
205
+ this.bytes.push(0);
206
+ if (Math.floor(value / 2 ** i) % 2)
207
+ this.bytes[this.bytes.length - 1] |= 0x80 >> (this.at % 8);
208
+ this.at++;
209
+ }
210
+ return this;
211
+ }
212
+ get done() {
213
+ return Uint8Array.from(this.bytes);
214
+ }
215
+ }
216
+ /**
217
+ * LATM の AudioMuxElement (muxConfigPresent = 1。LOAS の同期のヘッダーは付けない)。
218
+ * `audioSpecificConfig` を渡せば StreamMuxConfig を書き、渡さなければ「前と同じ」にする。
219
+ * AudioSpecificConfig は、長さがバイトの区切りに揃うもの (AAC LC の 2 バイトなど) を渡す
220
+ */
221
+ export function latmElement(frame, audioSpecificConfig) {
222
+ const bits = new BitWriter();
223
+ bits.put(audioSpecificConfig === undefined ? 1 : 0, 1);
224
+ if (audioSpecificConfig !== undefined) {
225
+ // audioMuxVersion 0、allStreamsSameTimeFraming 1、サブフレーム・番組・層は 1 つずつ
226
+ bits.put(0, 1).put(1, 1).put(0, 6).put(0, 4).put(0, 3);
227
+ for (const byte of audioSpecificConfig)
228
+ bits.put(byte, 8);
229
+ // frameLengthType 0、latmBufferFullness 0xFF、otherData なし、CRC なし
230
+ bits.put(0, 3).put(0xff, 8).put(0, 1).put(0, 1);
231
+ }
232
+ let left = frame.length;
233
+ for (; left >= 255; left -= 255)
234
+ bits.put(255, 8);
235
+ bits.put(left, 8);
236
+ for (const byte of frame)
237
+ bits.put(byte, 8);
238
+ return bits.done;
239
+ }
240
+ /** PA メッセージ。表 (MPT・PLT) を並べる */
241
+ export function paMessage(...tables) {
242
+ const body = [
243
+ tables.length,
244
+ ...tables.flatMap((table) => [table[0], table[1], table[2], table[3]]),
245
+ ...tables.flatMap((table) => [...table]),
246
+ ];
247
+ return Uint8Array.from([...be(0x0000), 0x00, ...be32(body.length), ...body]);
248
+ }
249
+ /** M2 セクションメッセージ。セクション (MH-EIT・MH-SDT・ECM など) を 1 つ運ぶ */
250
+ export function m2SectionMessage(section) {
251
+ return Uint8Array.from([
252
+ ...be(0x8000),
253
+ 0x00,
254
+ ...be(section.length),
255
+ ...section,
256
+ ]);
257
+ }
258
+ /**
259
+ * 信号メッセージを運ぶ MMTP のパケットの並び。集約するときは全部を 1 つに、分割するときは 1 つのメッセージを
260
+ * いくつものパケットに分ける。fragment_counter は実機と同じく 0 のまま
261
+ */
262
+ export function signalingPackets(packetId, messages, options = {}) {
263
+ let sequenceNumber = options.sequenceNumber ?? 0;
264
+ const packet = (indicator, flags, body) => mmtpPacket({
265
+ packetId,
266
+ payloadType: 2,
267
+ sequenceNumber: sequenceNumber++,
268
+ payload: [(indicator << 6) | flags, 0x00, ...body],
269
+ });
270
+ if (options.aggregate !== undefined) {
271
+ const long = options.aggregate === "long";
272
+ const body = messages.flatMap((message) => [
273
+ ...(long ? be32(message.length) : be(message.length)),
274
+ ...message,
275
+ ]);
276
+ return [packet(0, (long ? 0x02 : 0) | 0x01, body)];
277
+ }
278
+ const out = [];
279
+ for (const message of messages) {
280
+ const size = options.fragment ?? message.length;
281
+ if (message.length <= size) {
282
+ out.push(packet(0, 0, [...message]));
283
+ continue;
284
+ }
285
+ for (let at = 0; at < message.length; at += size) {
286
+ const last = at + size >= message.length;
287
+ const indicator = at === 0 ? 1 : last ? 3 : 2;
288
+ out.push(packet(indicator, 0, [...message.subarray(at, at + size)]));
289
+ }
290
+ }
291
+ return out;
292
+ }
293
+ /** PLT。package ごとに `[package_id, MPT の packet_id]` */
294
+ export function pltTable(packages, version = 0) {
295
+ const body = [
296
+ packages.length,
297
+ ...packages.flatMap(([packageId, packetId]) => [
298
+ 2,
299
+ ...be(packageId),
300
+ 0x00,
301
+ ...be(packetId),
302
+ ]),
303
+ 0x00, // IP_delivery の数
304
+ ];
305
+ return Uint8Array.from([0x80, version, ...be(body.length), ...body]);
306
+ }
307
+ /** MMT の記述子。タグが 0xF000 以上なら長さは 2 バイト */
308
+ export function mmtDescriptor(tag, body) {
309
+ return tag >= 0xf000
310
+ ? [...be(tag), ...be(body.length), ...body]
311
+ : [...be(tag), body.length, ...body];
312
+ }
313
+ /** MPT。アセットはどれも同じ IP の並びの packet_id (在りかの種類 0) に置く */
314
+ export function mptTable(mpt) {
315
+ const descriptors = mpt.access === undefined
316
+ ? []
317
+ : mmtDescriptor(0x8004, [
318
+ ...be(mpt.access.caSystemId),
319
+ 0x00,
320
+ ...be(mpt.access.ecmPacketId),
321
+ ]);
322
+ const assets = mpt.assets.flatMap((asset) => {
323
+ const assetDescriptors = [
324
+ ...(asset.componentTag === undefined
325
+ ? []
326
+ : mmtDescriptor(0x8011, be(asset.componentTag))),
327
+ ...(asset.descriptors ?? []),
328
+ ];
329
+ return [
330
+ 0x00, // identifier_type
331
+ ...be32(0), // asset_id_scheme
332
+ 2,
333
+ ...be(asset.packetId), // asset_id
334
+ ...[...asset.type].map((c) => c.charCodeAt(0)),
335
+ 0xfe, // asset_clock_relation_flag 0
336
+ 1, // location_count
337
+ 0x00,
338
+ ...be(asset.packetId),
339
+ ...be(assetDescriptors.length),
340
+ ...assetDescriptors,
341
+ ];
342
+ });
343
+ const body = [
344
+ 0xfc, // MPT_mode 0
345
+ 2,
346
+ ...be(mpt.packageId),
347
+ ...be(descriptors.length),
348
+ ...descriptors,
349
+ mpt.assets.length,
350
+ ...assets,
351
+ ];
352
+ return Uint8Array.from([0x20, mpt.version ?? 0, ...be(body.length), ...body]);
353
+ }
354
+ /** MMT の表のセクションの頭 (table_id・長さ・拡張・バージョン・セクション番号)。長さは `withCrc` が埋める */
355
+ function head(tableId, extension, version = 0) {
356
+ return [tableId, 0x00, 0x00, ...be(extension), 0xc1 | (version << 1), 0, 0];
357
+ }
358
+ /** MH-SDT。`other` なら MH-SDT[other] (0xA0) */
359
+ export function mhSdtSection(tlvStreamId, originalNetworkId, services, other = false) {
360
+ const body = [
361
+ ...head(other ? 0xa0 : 0x9f, tlvStreamId),
362
+ ...be(originalNetworkId),
363
+ 0xff,
364
+ ];
365
+ for (const service of services) {
366
+ const provider = utf8(service.providerName ?? "");
367
+ const name = utf8(service.name);
368
+ const descriptor = mmtDescriptor(0x8019, [
369
+ service.serviceType ?? 0x01,
370
+ provider.length,
371
+ ...provider,
372
+ name.length,
373
+ ...name,
374
+ ]);
375
+ body.push(...be(service.serviceId), 0xe3, // EIT の予定と p/f がある
376
+ 0x80 | ((descriptor.length >> 8) & 0x0f), // running_status 4、free_CA_mode 0
377
+ descriptor.length & 0xff, ...descriptor);
378
+ }
379
+ return withCrc(body);
380
+ }
381
+ /** TLV-NIT。TLV のストリームごとに `[tlv_stream_id, original_network_id, [service_id, 種類][]]` */
382
+ export function tlvNitSection(networkId, name, streams) {
383
+ const nameBytes = utf8(name);
384
+ const network = [0x40, nameBytes.length, ...nameBytes];
385
+ const loop = streams.flatMap(([tlvStreamId, onid, services]) => {
386
+ const list = services.flatMap(([serviceId, type]) => [
387
+ ...be(serviceId),
388
+ type,
389
+ ]);
390
+ const descriptors = [0x41, list.length, ...list];
391
+ return [
392
+ ...be(tlvStreamId),
393
+ ...be(onid),
394
+ ...be(0xf000 | descriptors.length),
395
+ ...descriptors,
396
+ ];
397
+ });
398
+ return withCrc([
399
+ ...head(0x40, networkId),
400
+ ...be(0xf000 | network.length),
401
+ ...network,
402
+ ...be(0xf000 | loop.length),
403
+ ...loop,
404
+ ]);
405
+ }
406
+ /** MH-TOT。放送の現在時刻 */
407
+ export function mhTotSection(unixMs) {
408
+ return withCrc([0xa1, 0x00, 0x00, ...mjdTime(unixMs), 0xf0, 0x00]);
409
+ }
410
+ function eventDescriptors(event) {
411
+ const name = utf8(event.name);
412
+ const text = utf8(event.description ?? "");
413
+ const out = mmtDescriptor(0xf001, [
414
+ ...JPN,
415
+ name.length,
416
+ ...name,
417
+ ...be(text.length),
418
+ ...text,
419
+ ]);
420
+ for (const [heading, body] of event.extended ?? []) {
421
+ const bytes = utf8(body);
422
+ const size = event.split ?? bytes.length;
423
+ const items = [];
424
+ for (let at = 0; at < bytes.length; at += size) {
425
+ const title = at === 0 ? utf8(heading) : [];
426
+ const part = bytes.slice(at, at + size);
427
+ items.push(title.length, ...title, ...be(part.length), ...part);
428
+ }
429
+ out.push(...mmtDescriptor(0xf002, [
430
+ 0x00,
431
+ ...JPN,
432
+ ...be(items.length),
433
+ ...items,
434
+ 0x00,
435
+ 0x00,
436
+ ]));
437
+ }
438
+ if (event.genres !== undefined)
439
+ out.push(...mmtDescriptor(0x8012, event.genres.flatMap(([lv1, lv2]) => [(lv1 << 4) | lv2, 0xff])));
440
+ if (event.resolution !== undefined)
441
+ // 16:9、プログレッシブ、59.94 コマ、component_tag 0、言語
442
+ out.push(...mmtDescriptor(0x8010, [
443
+ (event.resolution << 4) | 0x03,
444
+ 0xe8,
445
+ 0x00,
446
+ 0x00,
447
+ 0x5f,
448
+ ...JPN,
449
+ ]));
450
+ if (event.copyControl !== undefined)
451
+ out.push(...mmtDescriptor(0x8038, [(event.copyControl << 6) | 0x0f, 0xff]));
452
+ return out;
453
+ }
454
+ /** MH-EIT */
455
+ export function mhEitSection(options) {
456
+ const body = [
457
+ options.tableId,
458
+ 0x00,
459
+ 0x00,
460
+ ...be(options.serviceId),
461
+ 0xc1 | ((options.version ?? 0) << 1),
462
+ options.sectionNumber ?? 0,
463
+ options.lastSectionNumber ?? 0,
464
+ ...be(options.tlvStreamId),
465
+ ...be(options.originalNetworkId),
466
+ options.lastSectionNumber ?? 0,
467
+ options.lastTableId ?? options.tableId,
468
+ ];
469
+ for (const event of options.events) {
470
+ const descriptors = eventDescriptors(event);
471
+ body.push(...be(event.eventId), ...mjdTime(event.startAt), ...bcdDuration(event.duration), ((options.runningStatus ?? 0) << 5) |
472
+ (event.isFree === false ? 0x10 : 0) |
473
+ ((descriptors.length >> 8) & 0x0f), descriptors.length & 0xff, ...descriptors);
474
+ }
475
+ return withCrc(body);
476
+ }
package/dist/synth.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * TS のセクションを組み立てる。
3
3
  *
4
4
  * 実チューナーが無いので、解析のテストにも偽エージェントにも「それらしい TS」が要る。
5
- * 読む側 (psi.ts・eit.ts・dsmcc.ts・ait.ts、denpa の logo.ts など) と対になる書く側で、仕様を書き写しているのはここだけ。
5
+ * 読む側 (psi.ts・eit.ts・dsmcc.ts・ait.ts、denpa の logo.ts など) と対になる書く側で、TS の PSI・SI の仕様を書き写しているのはここだけ。
6
6
  */
7
7
  import { withCrc } from "./psi.js";
8
8
  export { withCrc };
@@ -27,6 +27,10 @@ export declare function sdtSection(transportStreamId: number, originalNetworkId:
27
27
  options?: SectionPart & {
28
28
  other?: boolean;
29
29
  }): Uint8Array;
30
+ /** epoch ms → MJD + BCD の 5 バイト。放送の時刻は日本時間で書く */
31
+ export declare function mjdTime(at: number): number[];
32
+ /** ms → BCD 3 バイトの尺 */
33
+ export declare function bcdDuration(ms: number): number[];
30
34
  export interface SynthEvent {
31
35
  eventId: number;
32
36
  /** epoch ms */
package/dist/synth.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * TS のセクションを組み立てる。
3
3
  *
4
4
  * 実チューナーが無いので、解析のテストにも偽エージェントにも「それらしい TS」が要る。
5
- * 読む側 (psi.ts・eit.ts・dsmcc.ts・ait.ts、denpa の logo.ts など) と対になる書く側で、仕様を書き写しているのはここだけ。
5
+ * 読む側 (psi.ts・eit.ts・dsmcc.ts・ait.ts、denpa の logo.ts など) と対になる書く側で、TS の PSI・SI の仕様を書き写しているのはここだけ。
6
6
  */
7
7
  import { joinBytes } from "./bytes.js";
8
8
  import { PACKET, SYNC, withCrc } from "./psi.js";
@@ -120,7 +120,7 @@ options = {}) {
120
120
  /** 2 桁の 10 進を BCD の 1 バイトに */
121
121
  const bcd = (value) => ((Math.floor(value / 10) << 4) | (value % 10)) & 0xff;
122
122
  /** epoch ms → MJD + BCD の 5 バイト。放送の時刻は日本時間で書く */
123
- function mjdTime(at) {
123
+ export function mjdTime(at) {
124
124
  const jst = Math.floor(at / 1000) + 9 * 3600;
125
125
  const mjd = Math.floor(jst / 86400) + 40587;
126
126
  const rest = ((jst % 86400) + 86400) % 86400;
@@ -132,7 +132,7 @@ function mjdTime(at) {
132
132
  ];
133
133
  }
134
134
  /** ms → BCD 3 バイトの尺 */
135
- function bcdDuration(ms) {
135
+ export function bcdDuration(ms) {
136
136
  const total = Math.floor(ms / 1000);
137
137
  return [
138
138
  bcd(Math.floor(total / 3600)),
@@ -410,7 +410,7 @@ export function bxmlDescriptor(options = {}) {
410
410
  const body = [
411
411
  ...be(dataComponentId),
412
412
  // 伝送方式 00 / 入口 / 自動起動 / 解像度 0011 (960x540)
413
- ...(entryPoint ? [0x33, 0x70] : [0x00, 0xff]),
413
+ ...(entryPoint ? [0x33, 0x70] : [0x00]),
414
414
  0xf8, // dataEventId 0xF / event_section_flag
415
415
  0xa0, // 即時取得あり / 蓄積不可 / 起動優先
416
416
  ];
@@ -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
+ }