@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.
- package/README.md +39 -2
- package/dist/bits.d.ts +25 -0
- package/dist/bits.js +66 -0
- package/dist/drcs-smooth.d.ts +1 -1
- package/dist/drcs-smooth.js +1 -1
- package/dist/eit.d.ts +14 -2
- package/dist/eit.js +46 -31
- package/dist/mh-eit.d.ts +43 -0
- package/dist/mh-eit.js +275 -0
- package/dist/mmt-media.d.ts +170 -0
- package/dist/mmt-media.js +539 -0
- package/dist/mmt-service-filter.d.ts +44 -0
- package/dist/mmt-service-filter.js +166 -0
- package/dist/mmt-si.d.ts +183 -0
- package/dist/mmt-si.js +456 -0
- package/dist/mmtp.d.ts +84 -0
- package/dist/mmtp.js +232 -0
- package/dist/psi.d.ts +4 -2
- package/dist/psi.js +3 -3
- package/dist/service-filter.js +7 -6
- package/dist/synth-mmt-av.d.ts +83 -0
- package/dist/synth-mmt-av.js +336 -0
- package/dist/synth-mmt.d.ts +176 -0
- package/dist/synth-mmt.js +476 -0
- package/dist/synth.d.ts +5 -1
- package/dist/synth.js +4 -4
- package/dist/tlv-ip.d.ts +44 -0
- package/dist/tlv-ip.js +118 -0
- package/dist/tlv.d.ts +43 -0
- package/dist/tlv.js +119 -0
- package/dist/ttml.d.ts +25 -0
- package/dist/ttml.js +455 -0
- package/package.json +5 -3
package/dist/mmt-si.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 4K の放送 (MMT/TLV) の信号の表を読む (ARIB STD-B60、TLV-SI は ARIB STD-B32 第 3 部)。
|
|
3
|
+
* TS の `psi.ts` に当たる。
|
|
4
|
+
*
|
|
5
|
+
* 表は 2 か所から来る。
|
|
6
|
+
*
|
|
7
|
+
* - TLV-SI (TLV の種類 0xFE) … TLV-NIT。TS の NIT と同じ形式のセクションで、記述子も TS と同じ (タグ 1 バイト)
|
|
8
|
+
* - MMTP の信号メッセージ (`mmtp.ts`) … PA メッセージの PLT・MPT と、M2 セクションメッセージの MH-SDT・MH-EIT・MH-TOT・ECM。
|
|
9
|
+
* 記述子のタグは 2 バイトで、**タグが 0xF000 未満なら長さは 1 バイト、0xF000 以上なら 2 バイト**
|
|
10
|
+
*
|
|
11
|
+
* 局名や番組名の文字は UTF-8 (TS の ARIB の 8 単位符号ではない)。
|
|
12
|
+
* 実機のケーブルの BS4K では、PLT・MPT・MH-SDT[other]・MH-EIT・TLV-NIT を読めた。どれもスクランブルされていない。
|
|
13
|
+
*/
|
|
14
|
+
import { ascii, u16 } from "./bytes.js";
|
|
15
|
+
import { parseMjdTime } from "./eit.js";
|
|
16
|
+
import { MESSAGE_M2_SECTION, MESSAGE_PA, messageTables, parseMessage, parseMmtp, PAYLOAD_SIGNALING, SignalingAssembler, } from "./mmtp.js";
|
|
17
|
+
import { crc32, descriptors, SERVICE_TYPES, } from "./psi.js";
|
|
18
|
+
import { ipv6Text, TlvIpReader } from "./tlv-ip.js";
|
|
19
|
+
import { TLV_COMPRESSED_IP, TLV_SIGNALING, TlvStream } from "./tlv.js";
|
|
20
|
+
export const TABLE_MPT = 0x20;
|
|
21
|
+
export const TABLE_PLT = 0x80;
|
|
22
|
+
const TABLE_TLV_NIT_ACTUAL = 0x40;
|
|
23
|
+
const TABLE_TLV_NIT_OTHER = 0x41;
|
|
24
|
+
const TABLE_MH_SDT_ACTUAL = 0x9f;
|
|
25
|
+
const TABLE_MH_SDT_OTHER = 0xa0;
|
|
26
|
+
const TABLE_MH_TOT = 0xa1;
|
|
27
|
+
const DESC_NETWORK_NAME = 0x40;
|
|
28
|
+
const DESC_SERVICE_LIST = 0x41;
|
|
29
|
+
/** アクセス制御記述子。CA_system_id と、ECM の在りか */
|
|
30
|
+
const DESC_ACCESS_CONTROL = 0x8004;
|
|
31
|
+
const DESC_MH_SERVICE = 0x8019;
|
|
32
|
+
/** MH-ストリーム識別記述子。アセットの component_tag */
|
|
33
|
+
const DESC_STREAM_IDENTIFIER = 0x8011;
|
|
34
|
+
const utf8 = new TextDecoder();
|
|
35
|
+
/** 局名・番組名の文字 (UTF-8)。壊れた並びは U+FFFD になる */
|
|
36
|
+
export function mmtText(data) {
|
|
37
|
+
return utf8.decode(data);
|
|
38
|
+
}
|
|
39
|
+
/** MMT の記述子の並びを (タグ, 中身) に切り分ける。タグが 0xF000 以上なら長さは 2 バイト */
|
|
40
|
+
export function* mmtDescriptors(data) {
|
|
41
|
+
let at = 0;
|
|
42
|
+
while (at + 3 <= data.length) {
|
|
43
|
+
const tag = u16(data, at);
|
|
44
|
+
const wide = tag >= 0xf000;
|
|
45
|
+
if (wide && at + 4 > data.length)
|
|
46
|
+
return;
|
|
47
|
+
const length = wide ? u16(data, at + 2) : data[at + 2];
|
|
48
|
+
const bodyAt = at + (wide ? 4 : 3);
|
|
49
|
+
const body = data.subarray(bodyAt, bodyAt + length);
|
|
50
|
+
if (body.length < length)
|
|
51
|
+
return;
|
|
52
|
+
yield [tag, body];
|
|
53
|
+
at = bodyAt + length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* セクションの長さが合い、`crc` なら末尾の CRC32 も合っているか。
|
|
58
|
+
* TLV-SI と M2 セクションはいつも CRC32 を持つ (MH-TOT のように section_syntax_indicator が 0 の表も)。
|
|
59
|
+
* M2 短セクションは持たない
|
|
60
|
+
*/
|
|
61
|
+
function sectionIntact(section, crc) {
|
|
62
|
+
if (section.length < 3)
|
|
63
|
+
return false;
|
|
64
|
+
if (3 + (u16(section, 1) & 0x0fff) !== section.length)
|
|
65
|
+
return false;
|
|
66
|
+
return !crc || (section.length >= 7 && crc32(section) === 0);
|
|
67
|
+
}
|
|
68
|
+
/** TLV-SI の中身を、セクションに切り分ける。詰め物 (0xFF) から先は無い。壊れたセクションは落とす */
|
|
69
|
+
export function tlvSiSections(payload) {
|
|
70
|
+
const out = [];
|
|
71
|
+
let at = 0;
|
|
72
|
+
while (at + 3 <= payload.length && payload[at] !== 0xff) {
|
|
73
|
+
const end = at + 3 + (u16(payload, at + 1) & 0x0fff);
|
|
74
|
+
if (end > payload.length)
|
|
75
|
+
break;
|
|
76
|
+
const section = payload.subarray(at, end);
|
|
77
|
+
if (sectionIntact(section, true))
|
|
78
|
+
out.push(section);
|
|
79
|
+
at = end;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** 在りかを読む。読めない種類 (3・4 は確かめていない) なら null で、そこから先は読めない */
|
|
84
|
+
function readLocation(data, at) {
|
|
85
|
+
const type = data[at];
|
|
86
|
+
const none = { packetId: null, destination: null, destinationPort: null };
|
|
87
|
+
switch (type) {
|
|
88
|
+
case 0x00:
|
|
89
|
+
if (at + 3 > data.length)
|
|
90
|
+
return null;
|
|
91
|
+
return [{ ...none, type, packetId: u16(data, at + 1) }, at + 3];
|
|
92
|
+
case 0x01: {
|
|
93
|
+
// 送り元 (4)・宛先 (4)・宛先のポート (2)・packet_id (2)
|
|
94
|
+
if (at + 13 > data.length)
|
|
95
|
+
return null;
|
|
96
|
+
const destination = [5, 6, 7, 8].map((i) => data[at + i]).join(".");
|
|
97
|
+
const location = {
|
|
98
|
+
type,
|
|
99
|
+
packetId: u16(data, at + 11),
|
|
100
|
+
destination,
|
|
101
|
+
destinationPort: u16(data, at + 9),
|
|
102
|
+
};
|
|
103
|
+
return [location, at + 13];
|
|
104
|
+
}
|
|
105
|
+
case 0x02: {
|
|
106
|
+
// 送り元 (16)・宛先 (16)・宛先のポート (2)・packet_id (2)
|
|
107
|
+
if (at + 37 > data.length)
|
|
108
|
+
return null;
|
|
109
|
+
const location = {
|
|
110
|
+
type,
|
|
111
|
+
packetId: u16(data, at + 35),
|
|
112
|
+
destination: ipv6Text(data, at + 17),
|
|
113
|
+
destinationPort: u16(data, at + 33),
|
|
114
|
+
};
|
|
115
|
+
return [location, at + 37];
|
|
116
|
+
}
|
|
117
|
+
case 0x05: {
|
|
118
|
+
if (at + 2 > data.length)
|
|
119
|
+
return null;
|
|
120
|
+
const end = at + 2 + data[at + 1];
|
|
121
|
+
return end > data.length ? null : [{ ...none, type }, end];
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** 番号のバイト列を整数に (package_id と asset_id。放送では 2 バイト) */
|
|
128
|
+
function idNumber(data) {
|
|
129
|
+
let value = 0;
|
|
130
|
+
for (const byte of data)
|
|
131
|
+
value = value * 256 + byte;
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* PLT (package list table)。**どの package の MPT がどの packet_id で来るか**を持つ。
|
|
136
|
+
* IP で運ぶもの (IP_delivery) の並びは読まない
|
|
137
|
+
*/
|
|
138
|
+
export function parsePlt(table) {
|
|
139
|
+
if (table[0] !== TABLE_PLT || table.length < 5)
|
|
140
|
+
return null;
|
|
141
|
+
const end = Math.min(table.length, 4 + u16(table, 2));
|
|
142
|
+
const count = table[4];
|
|
143
|
+
const packages = [];
|
|
144
|
+
let at = 5;
|
|
145
|
+
for (let i = 0; i < count && at < end; i++) {
|
|
146
|
+
const idEnd = at + 1 + table[at];
|
|
147
|
+
const location = readLocation(table.subarray(0, end), idEnd);
|
|
148
|
+
if (location === null)
|
|
149
|
+
break;
|
|
150
|
+
packages.push({
|
|
151
|
+
packageId: idNumber(table.subarray(at + 1, idEnd)),
|
|
152
|
+
location: location[0],
|
|
153
|
+
});
|
|
154
|
+
at = location[1];
|
|
155
|
+
}
|
|
156
|
+
return { version: table[1], packages };
|
|
157
|
+
}
|
|
158
|
+
/** 記述子の並びから、アクセス制御記述子を読む */
|
|
159
|
+
function accessControls(body) {
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const [tag, data] of mmtDescriptors(body)) {
|
|
162
|
+
if (tag !== DESC_ACCESS_CONTROL || data.length < 3)
|
|
163
|
+
continue;
|
|
164
|
+
const location = readLocation(data, 2);
|
|
165
|
+
if (location !== null)
|
|
166
|
+
out.push({ caSystemId: u16(data, 0), location: location[0] });
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function componentTagOf(body) {
|
|
171
|
+
for (const [tag, data] of mmtDescriptors(body))
|
|
172
|
+
if (tag === DESC_STREAM_IDENTIFIER && data.length >= 2)
|
|
173
|
+
return u16(data, 0);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* MPT (MMT package table)。**局の中身の一覧**で、TS の PMT に当たる。
|
|
178
|
+
* アセット (映像・音声・字幕) ごとに、型と packet_id と記述子を持つ。読めない在りかの種類が来たら、
|
|
179
|
+
* そこまでに読めたアセットを返す
|
|
180
|
+
*/
|
|
181
|
+
export function parseMpt(table) {
|
|
182
|
+
if (table[0] !== TABLE_MPT || table.length < 7)
|
|
183
|
+
return null;
|
|
184
|
+
const end = Math.min(table.length, 4 + u16(table, 2));
|
|
185
|
+
const data = table.subarray(0, end);
|
|
186
|
+
let at = 5;
|
|
187
|
+
const idEnd = at + 1 + data[at];
|
|
188
|
+
if (idEnd + 2 > end)
|
|
189
|
+
return null;
|
|
190
|
+
const packageId = idNumber(data.subarray(at + 1, idEnd));
|
|
191
|
+
const descriptorsEnd = idEnd + 2 + u16(data, idEnd);
|
|
192
|
+
if (descriptorsEnd + 1 > end)
|
|
193
|
+
return null;
|
|
194
|
+
const descriptors = data.subarray(idEnd + 2, descriptorsEnd);
|
|
195
|
+
const count = data[descriptorsEnd];
|
|
196
|
+
at = descriptorsEnd + 1;
|
|
197
|
+
const assets = [];
|
|
198
|
+
for (let i = 0; i < count; i++) {
|
|
199
|
+
// identifier_type (1)・asset_id_scheme (4)・asset_id_length (1)
|
|
200
|
+
if (at + 6 > end)
|
|
201
|
+
break;
|
|
202
|
+
const assetIdEnd = at + 6 + data[at + 5];
|
|
203
|
+
if (assetIdEnd + 6 > end)
|
|
204
|
+
break;
|
|
205
|
+
const assetId = data.subarray(at + 6, assetIdEnd);
|
|
206
|
+
const type = ascii(data, assetIdEnd, 4);
|
|
207
|
+
at = assetIdEnd + 5;
|
|
208
|
+
// asset_clock_relation_flag があれば、clock_relation_id (1) と timescale のフラグ (1)・timescale (4)
|
|
209
|
+
if (data[assetIdEnd + 4] & 0x01) {
|
|
210
|
+
if (at + 2 > end)
|
|
211
|
+
break;
|
|
212
|
+
at += 2 + (data[at + 1] & 0x01 ? 4 : 0);
|
|
213
|
+
}
|
|
214
|
+
if (at >= end)
|
|
215
|
+
break;
|
|
216
|
+
const locationCount = data[at];
|
|
217
|
+
at++;
|
|
218
|
+
const locations = [];
|
|
219
|
+
for (let j = 0; j < locationCount && at > 0; j++) {
|
|
220
|
+
const location = readLocation(data, at);
|
|
221
|
+
// 読めない種類なら長さが分からず、そこから先は読めない
|
|
222
|
+
at = location?.[1] ?? -1;
|
|
223
|
+
if (location !== null)
|
|
224
|
+
locations.push(location[0]);
|
|
225
|
+
}
|
|
226
|
+
if (at < 0 || at + 2 > end)
|
|
227
|
+
break;
|
|
228
|
+
const body = data.subarray(at + 2, at + 2 + u16(data, at));
|
|
229
|
+
at += 2 + body.length;
|
|
230
|
+
assets.push({
|
|
231
|
+
type,
|
|
232
|
+
assetId,
|
|
233
|
+
locations,
|
|
234
|
+
componentTag: componentTagOf(body),
|
|
235
|
+
access: accessControls(body),
|
|
236
|
+
descriptors: body,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
version: data[1],
|
|
241
|
+
packageId,
|
|
242
|
+
access: accessControls(descriptors),
|
|
243
|
+
descriptors,
|
|
244
|
+
assets,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** MH-SDT。局名と種類。TS の SDT と同じ並びで、記述子と文字が MMT のもの */
|
|
248
|
+
export function parseMhSdt(section) {
|
|
249
|
+
const tableId = section[0];
|
|
250
|
+
if (tableId !== TABLE_MH_SDT_ACTUAL && tableId !== TABLE_MH_SDT_OTHER)
|
|
251
|
+
return null;
|
|
252
|
+
if (section.length < 15)
|
|
253
|
+
return null;
|
|
254
|
+
const services = [];
|
|
255
|
+
const end = section.length - 4;
|
|
256
|
+
let at = 11;
|
|
257
|
+
while (at + 5 <= end) {
|
|
258
|
+
const loop = u16(section, at + 3) & 0x0fff;
|
|
259
|
+
const body = section.subarray(at + 5, at + 5 + loop);
|
|
260
|
+
let service = null;
|
|
261
|
+
for (const [tag, data] of mmtDescriptors(body)) {
|
|
262
|
+
if (tag !== DESC_MH_SERVICE || data.length < 2)
|
|
263
|
+
continue;
|
|
264
|
+
// 種類 → 事業者名 → サービス名 (長さはどれも 1 バイト)
|
|
265
|
+
const nameAt = 2 + data[1];
|
|
266
|
+
service = {
|
|
267
|
+
serviceId: u16(section, at),
|
|
268
|
+
serviceType: data[0],
|
|
269
|
+
name: mmtText(data.subarray(nameAt + 1, nameAt + 1 + (data[nameAt] ?? 0))),
|
|
270
|
+
providerName: mmtText(data.subarray(2, nameAt)),
|
|
271
|
+
isFree: (section[at + 3] & 0x10) === 0,
|
|
272
|
+
eitSchedule: (section[at + 2] & 0x02) !== 0,
|
|
273
|
+
eitPresentFollowing: (section[at + 2] & 0x01) !== 0,
|
|
274
|
+
};
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (service !== null)
|
|
278
|
+
services.push(service);
|
|
279
|
+
at += 5 + loop;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
other: tableId === TABLE_MH_SDT_OTHER,
|
|
283
|
+
tlvStreamId: u16(section, 3),
|
|
284
|
+
originalNetworkId: u16(section, 8),
|
|
285
|
+
version: (section[5] >> 1) & 0x1f,
|
|
286
|
+
sectionNumber: section[6],
|
|
287
|
+
lastSectionNumber: section[7],
|
|
288
|
+
services,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** TLV-NIT。TS の NIT と同じ並びで、TS の代わりに TLV のストリームが並ぶ */
|
|
292
|
+
export function parseTlvNit(section) {
|
|
293
|
+
const tableId = section[0];
|
|
294
|
+
if (tableId !== TABLE_TLV_NIT_ACTUAL && tableId !== TABLE_TLV_NIT_OTHER)
|
|
295
|
+
return null;
|
|
296
|
+
if (section.length < 16)
|
|
297
|
+
return null;
|
|
298
|
+
const end = section.length - 4;
|
|
299
|
+
const networkEnd = 10 + (u16(section, 8) & 0x0fff);
|
|
300
|
+
if (networkEnd + 2 > end)
|
|
301
|
+
return null;
|
|
302
|
+
let name = "";
|
|
303
|
+
for (const [tag, data] of descriptors(section.subarray(10, networkEnd)))
|
|
304
|
+
if (tag === DESC_NETWORK_NAME)
|
|
305
|
+
name = mmtText(data);
|
|
306
|
+
const streams = [];
|
|
307
|
+
const loopEnd = Math.min(end, networkEnd + 2 + (u16(section, networkEnd) & 0x0fff));
|
|
308
|
+
let at = networkEnd + 2;
|
|
309
|
+
while (at + 6 <= loopEnd) {
|
|
310
|
+
const length = u16(section, at + 4) & 0x0fff;
|
|
311
|
+
const body = section.subarray(at + 6, at + 6 + length);
|
|
312
|
+
const services = [];
|
|
313
|
+
for (const [tag, data] of descriptors(body)) {
|
|
314
|
+
if (tag !== DESC_SERVICE_LIST)
|
|
315
|
+
continue;
|
|
316
|
+
for (let i = 0; i + 3 <= data.length; i += 3)
|
|
317
|
+
services.push({ serviceId: u16(data, i), serviceType: data[i + 2] });
|
|
318
|
+
}
|
|
319
|
+
streams.push({
|
|
320
|
+
tlvStreamId: u16(section, at),
|
|
321
|
+
originalNetworkId: u16(section, at + 2),
|
|
322
|
+
services,
|
|
323
|
+
descriptors: body,
|
|
324
|
+
});
|
|
325
|
+
at += 6 + length;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
other: tableId === TABLE_TLV_NIT_OTHER,
|
|
329
|
+
networkId: u16(section, 3),
|
|
330
|
+
version: (section[5] >> 1) & 0x1f,
|
|
331
|
+
sectionNumber: section[6],
|
|
332
|
+
lastSectionNumber: section[7],
|
|
333
|
+
name,
|
|
334
|
+
streams,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** MH-TOT から放送の現在時刻 (epoch ms) を読む。TS の TOT と同じく、table_id のあとに MJD + BCD の 5 バイト */
|
|
338
|
+
export function parseMhTot(section) {
|
|
339
|
+
if (section[0] !== TABLE_MH_TOT || section.length < 8)
|
|
340
|
+
return null;
|
|
341
|
+
return parseMjdTime(section, 3);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* TLV のバイト列から、信号の表を全部取り出す。番組表 (`mh-eit.ts`) や局の一覧を読むところの入口で、
|
|
345
|
+
* TS で `PacketStream` と `SectionAssembler` を並べるのに当たる。
|
|
346
|
+
*
|
|
347
|
+
* MPU (映像・音声) の MMTP は、ヘッダーだけ読んで読み飛ばす
|
|
348
|
+
*/
|
|
349
|
+
export class MmtTableStream {
|
|
350
|
+
tlv = new TlvStream();
|
|
351
|
+
ip = new TlvIpReader();
|
|
352
|
+
signaling = new SignalingAssembler();
|
|
353
|
+
*feed(chunk) {
|
|
354
|
+
for (const packet of this.tlv.feed(chunk)) {
|
|
355
|
+
if (packet.type === TLV_SIGNALING) {
|
|
356
|
+
for (const table of tlvSiSections(packet.payload))
|
|
357
|
+
yield { flow: null, packetId: null, table };
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// MMTP はヘッダー圧縮した IP で来る。圧縮していない IP (NTP) を MMTP として読まない
|
|
361
|
+
if (packet.type !== TLV_COMPRESSED_IP)
|
|
362
|
+
continue;
|
|
363
|
+
const datagram = this.ip.read(packet);
|
|
364
|
+
if (datagram === null)
|
|
365
|
+
continue;
|
|
366
|
+
const mmtp = parseMmtp(datagram.payload);
|
|
367
|
+
if (mmtp === null || mmtp.payloadType !== PAYLOAD_SIGNALING)
|
|
368
|
+
continue;
|
|
369
|
+
for (const raw of this.signaling.feed(datagram.flow, mmtp)) {
|
|
370
|
+
const message = parseMessage(raw);
|
|
371
|
+
if (message === null)
|
|
372
|
+
continue;
|
|
373
|
+
for (const table of messageTables(message)) {
|
|
374
|
+
// PA メッセージの表 (MPT・PLT) はセクションの形式ではなく、CRC32 も無い
|
|
375
|
+
if (message.messageId !== MESSAGE_PA &&
|
|
376
|
+
!sectionIntact(table, message.messageId === MESSAGE_M2_SECTION))
|
|
377
|
+
continue;
|
|
378
|
+
yield { flow: datagram.flow, packetId: mmtp.packetId, table };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 流れてくる TLV を読んで、局の一覧を作る。**TS の `ServiceReader` に当たる** (チャンネルスキャンで使う)。
|
|
386
|
+
*
|
|
387
|
+
* TLV-NIT[actual] と MH-SDT[actual] が両方来たら揃ったとみなす。局は MH-SDT[actual] のもので、
|
|
388
|
+
* 局の元の放送は MH-SDT の original_network_id (ケーブルで受けた BS4K なら 0x000B)、TSID の代わりに tlv_stream_id を入れる。
|
|
389
|
+
* TLV にはリモコン番号が無い (TLV-NIT に見当たらない) ので null。
|
|
390
|
+
*
|
|
391
|
+
* 局ごとの CA_system_id は、PLT から package (= service_id) の MPT を引き、そのアクセス制御記述子から読む (`caSystemIdsOf`)。
|
|
392
|
+
* free_CA_mode は使わない。実機の BS4K は 0 なのに、映像と音声はスクランブルされていた
|
|
393
|
+
*/
|
|
394
|
+
export class TlvServiceReader {
|
|
395
|
+
tables = new MmtTableStream();
|
|
396
|
+
plt = null;
|
|
397
|
+
/** package_id (= service_id) → MPT */
|
|
398
|
+
mpts = new Map();
|
|
399
|
+
network = null;
|
|
400
|
+
transport = null;
|
|
401
|
+
get complete() {
|
|
402
|
+
return this.network !== null && this.transport !== null;
|
|
403
|
+
}
|
|
404
|
+
/** PLT が来て、そこにある package の MPT が全部 1 回は来たか。スキャンで、局ごとの CA_system_id が分かるまで少し長く読むのに使う */
|
|
405
|
+
get mptsKnown() {
|
|
406
|
+
return (this.plt !== null &&
|
|
407
|
+
this.plt.packages.every((entry) => this.mpts.has(entry.packageId)));
|
|
408
|
+
}
|
|
409
|
+
/** 任意の長さのバイト列を渡す。揃ったら true */
|
|
410
|
+
feed(chunk) {
|
|
411
|
+
for (const { table } of this.tables.feed(chunk)) {
|
|
412
|
+
const tableId = table[0];
|
|
413
|
+
if (tableId === TABLE_PLT)
|
|
414
|
+
this.plt = parsePlt(table) ?? this.plt;
|
|
415
|
+
else if (tableId === TABLE_MPT) {
|
|
416
|
+
const mpt = parseMpt(table);
|
|
417
|
+
if (mpt !== null)
|
|
418
|
+
this.mpts.set(mpt.packageId, mpt);
|
|
419
|
+
}
|
|
420
|
+
else if (tableId === TABLE_TLV_NIT_ACTUAL && this.network === null)
|
|
421
|
+
this.network = parseTlvNit(table);
|
|
422
|
+
else if (tableId === TABLE_MH_SDT_ACTUAL && this.transport === null)
|
|
423
|
+
this.transport = parseMhSdt(table);
|
|
424
|
+
}
|
|
425
|
+
return this.complete;
|
|
426
|
+
}
|
|
427
|
+
/** 録画の対象になる種類の局だけ (`psi.SERVICE_TYPES`)。揃うまでは空 */
|
|
428
|
+
services() {
|
|
429
|
+
const transport = this.transport;
|
|
430
|
+
if (this.network === null || transport === null)
|
|
431
|
+
return [];
|
|
432
|
+
return transport.services
|
|
433
|
+
.filter((service) => SERVICE_TYPES.has(service.serviceType))
|
|
434
|
+
.map((service) => ({
|
|
435
|
+
serviceId: service.serviceId,
|
|
436
|
+
serviceType: service.serviceType,
|
|
437
|
+
name: service.name,
|
|
438
|
+
networkId: transport.originalNetworkId,
|
|
439
|
+
transportStreamId: transport.tlvStreamId,
|
|
440
|
+
remoteControlKeyId: null,
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
/** その局の MPT のアクセス制御記述子 (package 全体とアセットのもの)。MPT が来なかった局は空 */
|
|
444
|
+
accessOf(serviceId) {
|
|
445
|
+
const mpt = this.mpts.get(serviceId);
|
|
446
|
+
if (mpt === undefined)
|
|
447
|
+
return [];
|
|
448
|
+
return [...mpt.access, ...mpt.assets.flatMap((asset) => asset.access)];
|
|
449
|
+
}
|
|
450
|
+
/** その局の CA_system_id (重なりなし・小さい順)。アクセス制御記述子が無い・MPT が来なかった局は空 */
|
|
451
|
+
caSystemIdsOf(serviceId) {
|
|
452
|
+
return [
|
|
453
|
+
...new Set(this.accessOf(serviceId).map((entry) => entry.caSystemId)),
|
|
454
|
+
].sort((a, b) => a - b);
|
|
455
|
+
}
|
|
456
|
+
}
|
package/dist/mmtp.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMTP のパケット (ISO/IEC 23008-1、ARIB STD-B60) を読み、信号メッセージを組み立てる。
|
|
3
|
+
*
|
|
4
|
+
* MMTP のパケットは UDP 1 つに 1 つ入っている (`tlv-ip.ts`)。中身は 2 種類で、
|
|
5
|
+
* 映像・音声・字幕を運ぶ MPU (payload_type 0) と、局の中身の一覧や番組表を運ぶ信号メッセージ (2)。
|
|
6
|
+
* どちらかは packet_id ごとに決まっている。
|
|
7
|
+
*
|
|
8
|
+
* **4K の放送では映像と音声がスクランブルされ、信号はされていない** (実機のケーブルの BS4K で確かめた)。
|
|
9
|
+
* スクランブルの印はヘッダー拡張にあり (ARIB STD-B61)、MPU のペイロードヘッダー (8 バイト) までは読める。
|
|
10
|
+
* 復号はエージェントの仕事 (まだ扱わない)。
|
|
11
|
+
*/
|
|
12
|
+
export declare const PAYLOAD_MPU = 0;
|
|
13
|
+
export declare const PAYLOAD_SIGNALING = 2;
|
|
14
|
+
/** スクランブルの印 (上位 2 ビット)。01 は予約 */
|
|
15
|
+
export type Scrambling = "none" | "even" | "odd";
|
|
16
|
+
export interface MmtpPacket {
|
|
17
|
+
/** 頭出しできる所か (RAP_flag)。映像なら IRAP のピクチャを含む MPU の始まり */
|
|
18
|
+
rap: boolean;
|
|
19
|
+
payloadType: number;
|
|
20
|
+
packetId: number;
|
|
21
|
+
/** 送り出した時刻 (NTP の短い形式。上位 16 ビットが秒、下位 16 ビットが端数) */
|
|
22
|
+
deliveryTimestamp: number;
|
|
23
|
+
/** packet_id ごとの通し番号 */
|
|
24
|
+
sequenceNumber: number;
|
|
25
|
+
/** スクランブルの印。ヘッダー拡張に無ければ null (スクランブルしていない) */
|
|
26
|
+
scrambling: Scrambling | null;
|
|
27
|
+
/** ヘッダーとヘッダー拡張を除いた中身 */
|
|
28
|
+
payload: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
/** MMTP のパケットを 1 つ読む。短すぎれば null */
|
|
31
|
+
export declare function parseMmtp(data: Uint8Array): MmtpPacket | null;
|
|
32
|
+
/** MPU のペイロードヘッダー (MPU の中身の手前の 8 バイト)。スクランブルされていても読める */
|
|
33
|
+
export interface MpuHeader {
|
|
34
|
+
/** 0 は MPU のメタデータ、1 はムービーフラグメントのメタデータ、2 は MFU */
|
|
35
|
+
fragmentType: number;
|
|
36
|
+
timed: boolean;
|
|
37
|
+
fragmentation: number;
|
|
38
|
+
aggregated: boolean;
|
|
39
|
+
fragmentCounter: number;
|
|
40
|
+
mpuSequenceNumber: number;
|
|
41
|
+
/** ペイロードヘッダーより後ろ。スクランブルされていれば、ここから先が暗号 */
|
|
42
|
+
data: Uint8Array;
|
|
43
|
+
}
|
|
44
|
+
export declare function parseMpu(payload: Uint8Array): MpuHeader | null;
|
|
45
|
+
export declare const MESSAGE_PA = 0;
|
|
46
|
+
export declare const MESSAGE_M2_SECTION = 32768;
|
|
47
|
+
export declare const MESSAGE_CA = 32769;
|
|
48
|
+
export declare const MESSAGE_M2_SHORT_SECTION = 32770;
|
|
49
|
+
export interface SignalingMessage {
|
|
50
|
+
messageId: number;
|
|
51
|
+
version: number;
|
|
52
|
+
/** 長さまでのヘッダーを除いた中身 */
|
|
53
|
+
body: Uint8Array;
|
|
54
|
+
}
|
|
55
|
+
/** 信号メッセージを 1 つ読む。長さが合わなければ null */
|
|
56
|
+
export declare function parseMessage(data: Uint8Array): SignalingMessage | null;
|
|
57
|
+
/**
|
|
58
|
+
* 信号メッセージに入った表を返す。
|
|
59
|
+
*
|
|
60
|
+
* - PA メッセージは表の数と、表ごとの (table_id・バージョン・長さ) を先に並べ、そのあとに表を並べる。
|
|
61
|
+
* 表の頭は table_id・バージョン・長さ (16 ビット) で、MPT と PLT はこの形式
|
|
62
|
+
* - M2 セクションメッセージ・M2 短セクションメッセージ・CA メッセージは、表を 1 つだけ運ぶ。
|
|
63
|
+
* M2 セクションは TS のセクションと同じ形式で、末尾に CRC32 が付く (確かめるのは `mmt-si.ts`)
|
|
64
|
+
*/
|
|
65
|
+
export declare function messageTables(message: SignalingMessage): Uint8Array[];
|
|
66
|
+
/**
|
|
67
|
+
* MMTP の信号メッセージを組み立てる。**分割と集約の両方を戻す。**
|
|
68
|
+
*
|
|
69
|
+
* - 長いメッセージ (番組表の MH-EIT や大きな MPT) は、いくつもの MMTP のパケットに分割して送られる。
|
|
70
|
+
* 欠けは packet_sequence_number (packet_id ごとに 1 ずつ増える) で見る。続いていなければ、組み立て中のものを
|
|
71
|
+
* 捨てて次の先頭を待つ。**fragment_counter は当てにしない。** 実機のケーブルの BS4K は分割したものでも
|
|
72
|
+
* いつも 0 を入れていた
|
|
73
|
+
* - 短いメッセージは、1 つの MMTP のパケットに長さを前に付けて集約される (aggregation_flag)。
|
|
74
|
+
* 長さは length_extension_flag が立っていれば 32 ビット、立っていなければ 16 ビット
|
|
75
|
+
*
|
|
76
|
+
* 組み立て中のものは、並び (`flow`) と packet_id の組ごとに持つ
|
|
77
|
+
*/
|
|
78
|
+
export declare class SignalingAssembler {
|
|
79
|
+
private readonly pending;
|
|
80
|
+
/** 欠けたか大きすぎて捨てた、組み立て中のメッセージの数 */
|
|
81
|
+
dropped: number;
|
|
82
|
+
/** 信号メッセージを運ぶ MMTP のパケットを 1 つ渡す。そろったメッセージを返す */
|
|
83
|
+
feed(flow: string, packet: MmtpPacket): Uint8Array[];
|
|
84
|
+
}
|