@danything/arib 2.0.0 → 2.2.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 +40 -3
- package/dist/b24caption.js +4 -1
- package/dist/bits.d.ts +25 -0
- package/dist/bits.js +66 -0
- package/dist/eit.d.ts +14 -2
- package/dist/eit.js +56 -33
- package/dist/mh-eit.d.ts +43 -0
- package/dist/mh-eit.js +275 -0
- package/dist/mmt-media.d.ts +168 -0
- package/dist/mmt-media.js +536 -0
- package/dist/mmt-service-filter.d.ts +44 -0
- package/dist/mmt-service-filter.js +165 -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 +20 -2
- package/dist/psi.js +41 -12
- 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 +4 -0
- package/dist/synth.js +2 -2
- 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/mmtp.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
import { concatBytes, u16, u32 } from "./bytes.js";
|
|
13
|
+
export const PAYLOAD_MPU = 0x00;
|
|
14
|
+
export const PAYLOAD_SIGNALING = 0x02;
|
|
15
|
+
/** 種類がいくつも入るヘッダー拡張 (B61)。中に種類と長さの組が並ぶ */
|
|
16
|
+
const EXTENSION_MULTI = 0x0000;
|
|
17
|
+
/** ヘッダー拡張の中の、スクランブルの印の種類 */
|
|
18
|
+
const EXTENSION_SCRAMBLE = 0x0001;
|
|
19
|
+
const SCRAMBLING = {
|
|
20
|
+
0: "none",
|
|
21
|
+
2: "even",
|
|
22
|
+
3: "odd",
|
|
23
|
+
};
|
|
24
|
+
/** MMTP のパケットを 1 つ読む。短すぎれば null */
|
|
25
|
+
export function parseMmtp(data) {
|
|
26
|
+
if (data.length < 12)
|
|
27
|
+
return null;
|
|
28
|
+
const flags = data[0];
|
|
29
|
+
let at = 12;
|
|
30
|
+
// packet_counter_flag
|
|
31
|
+
if (flags & 0x20)
|
|
32
|
+
at += 4;
|
|
33
|
+
let scrambling = null;
|
|
34
|
+
// extension_flag
|
|
35
|
+
if (flags & 0x02) {
|
|
36
|
+
if (at + 4 > data.length)
|
|
37
|
+
return null;
|
|
38
|
+
const type = u16(data, at) & 0x7fff;
|
|
39
|
+
const length = u16(data, at + 2);
|
|
40
|
+
const body = data.subarray(at + 4, at + 4 + length);
|
|
41
|
+
if (body.length < length)
|
|
42
|
+
return null;
|
|
43
|
+
if (type === EXTENSION_MULTI)
|
|
44
|
+
scrambling = scramblingOf(body);
|
|
45
|
+
at += 4 + length;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
rap: (flags & 0x01) !== 0,
|
|
49
|
+
payloadType: data[1] & 0x3f,
|
|
50
|
+
packetId: u16(data, 2),
|
|
51
|
+
deliveryTimestamp: u32(data, 4),
|
|
52
|
+
sequenceNumber: u32(data, 8),
|
|
53
|
+
scrambling,
|
|
54
|
+
payload: data.subarray(at),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** 種類がいくつも入るヘッダー拡張から、スクランブルの印を探す。種類の上位 1 ビットは「これで終わり」 */
|
|
58
|
+
function scramblingOf(body) {
|
|
59
|
+
let at = 0;
|
|
60
|
+
while (at + 4 <= body.length) {
|
|
61
|
+
const type = u16(body, at);
|
|
62
|
+
const length = u16(body, at + 2);
|
|
63
|
+
if ((type & 0x7fff) === EXTENSION_SCRAMBLE && length >= 1)
|
|
64
|
+
return SCRAMBLING[body[at + 4] >> 6] ?? null;
|
|
65
|
+
at += 4 + length;
|
|
66
|
+
if (type & 0x8000)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/** 分割の印 (fragmentation_indicator)。0 は分割していない、1 は先頭、2 は途中、3 は最後 */
|
|
72
|
+
const WHOLE = 0;
|
|
73
|
+
const FIRST = 1;
|
|
74
|
+
const LAST = 3;
|
|
75
|
+
export function parseMpu(payload) {
|
|
76
|
+
if (payload.length < 8)
|
|
77
|
+
return null;
|
|
78
|
+
const flags = payload[2];
|
|
79
|
+
return {
|
|
80
|
+
fragmentType: flags >> 4,
|
|
81
|
+
timed: (flags & 0x08) !== 0,
|
|
82
|
+
fragmentation: (flags >> 1) & 0x03,
|
|
83
|
+
aggregated: (flags & 0x01) !== 0,
|
|
84
|
+
fragmentCounter: payload[3],
|
|
85
|
+
mpuSequenceNumber: u32(payload, 4),
|
|
86
|
+
data: payload.subarray(8, Math.min(payload.length, 2 + u16(payload, 0))),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export const MESSAGE_PA = 0x0000;
|
|
90
|
+
export const MESSAGE_M2_SECTION = 0x8000;
|
|
91
|
+
export const MESSAGE_CA = 0x8001;
|
|
92
|
+
export const MESSAGE_M2_SHORT_SECTION = 0x8002;
|
|
93
|
+
/** PA メッセージは長さが 32 ビット、ほかの信号メッセージは 16 ビット */
|
|
94
|
+
function lengthBytes(messageId) {
|
|
95
|
+
return messageId === MESSAGE_PA ? 4 : 2;
|
|
96
|
+
}
|
|
97
|
+
/** 信号メッセージを 1 つ読む。長さが合わなければ null */
|
|
98
|
+
export function parseMessage(data) {
|
|
99
|
+
if (data.length < 3)
|
|
100
|
+
return null;
|
|
101
|
+
const messageId = u16(data, 0);
|
|
102
|
+
const width = lengthBytes(messageId);
|
|
103
|
+
const at = 3 + width;
|
|
104
|
+
if (data.length < at)
|
|
105
|
+
return null;
|
|
106
|
+
const length = width === 4 ? u32(data, 3) : u16(data, 3);
|
|
107
|
+
if (data.length < at + length)
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
messageId,
|
|
111
|
+
version: data[2],
|
|
112
|
+
body: data.subarray(at, at + length),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 信号メッセージに入った表を返す。
|
|
117
|
+
*
|
|
118
|
+
* - PA メッセージは表の数と、表ごとの (table_id・バージョン・長さ) を先に並べ、そのあとに表を並べる。
|
|
119
|
+
* 表の頭は table_id・バージョン・長さ (16 ビット) で、MPT と PLT はこの形式
|
|
120
|
+
* - M2 セクションメッセージ・M2 短セクションメッセージ・CA メッセージは、表を 1 つだけ運ぶ。
|
|
121
|
+
* M2 セクションは TS のセクションと同じ形式で、末尾に CRC32 が付く (確かめるのは `mmt-si.ts`)
|
|
122
|
+
*/
|
|
123
|
+
export function messageTables(message) {
|
|
124
|
+
const body = message.body;
|
|
125
|
+
switch (message.messageId) {
|
|
126
|
+
case MESSAGE_PA: {
|
|
127
|
+
if (body.length < 1)
|
|
128
|
+
return [];
|
|
129
|
+
const tables = [];
|
|
130
|
+
let at = 1 + body[0] * 4;
|
|
131
|
+
while (at + 4 <= body.length) {
|
|
132
|
+
const end = at + 4 + u16(body, at + 2);
|
|
133
|
+
if (end > body.length)
|
|
134
|
+
break;
|
|
135
|
+
tables.push(body.subarray(at, end));
|
|
136
|
+
at = end;
|
|
137
|
+
}
|
|
138
|
+
return tables;
|
|
139
|
+
}
|
|
140
|
+
case MESSAGE_M2_SECTION:
|
|
141
|
+
case MESSAGE_M2_SHORT_SECTION:
|
|
142
|
+
case MESSAGE_CA:
|
|
143
|
+
return body.length > 0 ? [body] : [];
|
|
144
|
+
default:
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 組み立て中のメッセージの大きさの上限。PA メッセージの表は長さが 16 ビット (64 KiB まで) で、M2 セクションは 4 KiB まで。
|
|
150
|
+
* 最後の断片が来ないまま途中の断片が続いても、溜め続けない
|
|
151
|
+
*/
|
|
152
|
+
const PENDING_MOST = 256 * 1024;
|
|
153
|
+
/**
|
|
154
|
+
* MMTP の信号メッセージを組み立てる。**分割と集約の両方を戻す。**
|
|
155
|
+
*
|
|
156
|
+
* - 長いメッセージ (番組表の MH-EIT や大きな MPT) は、いくつもの MMTP のパケットに分割して送られる。
|
|
157
|
+
* 欠けは packet_sequence_number (packet_id ごとに 1 ずつ増える) で見る。続いていなければ、組み立て中のものを
|
|
158
|
+
* 捨てて次の先頭を待つ。**fragment_counter は当てにしない。** 実機のケーブルの BS4K は分割したものでも
|
|
159
|
+
* いつも 0 を入れていた
|
|
160
|
+
* - 短いメッセージは、1 つの MMTP のパケットに長さを前に付けて集約される (aggregation_flag)。
|
|
161
|
+
* 長さは length_extension_flag が立っていれば 32 ビット、立っていなければ 16 ビット
|
|
162
|
+
*
|
|
163
|
+
* 組み立て中のものは、並び (`flow`) と packet_id の組ごとに持つ
|
|
164
|
+
*/
|
|
165
|
+
export class SignalingAssembler {
|
|
166
|
+
pending = new Map();
|
|
167
|
+
/** 欠けたか大きすぎて捨てた、組み立て中のメッセージの数 */
|
|
168
|
+
dropped = 0;
|
|
169
|
+
/** 信号メッセージを運ぶ MMTP のパケットを 1 つ渡す。そろったメッセージを返す */
|
|
170
|
+
feed(flow, packet) {
|
|
171
|
+
const payload = packet.payload;
|
|
172
|
+
if (packet.payloadType !== PAYLOAD_SIGNALING || payload.length < 2)
|
|
173
|
+
return [];
|
|
174
|
+
const fragmentation = payload[0] >> 6;
|
|
175
|
+
const longLength = (payload[0] & 0x02) !== 0;
|
|
176
|
+
const aggregated = (payload[0] & 0x01) !== 0;
|
|
177
|
+
const body = payload.subarray(2);
|
|
178
|
+
const key = `${flow}/${packet.packetId}`;
|
|
179
|
+
let whole = null;
|
|
180
|
+
if (fragmentation === WHOLE) {
|
|
181
|
+
if (this.pending.delete(key))
|
|
182
|
+
this.dropped++;
|
|
183
|
+
whole = body;
|
|
184
|
+
}
|
|
185
|
+
else if (fragmentation === FIRST) {
|
|
186
|
+
if (this.pending.delete(key))
|
|
187
|
+
this.dropped++;
|
|
188
|
+
// 断片を持ち越すのでコピーする (中身は届いたチャンクの一部を指す。`TlvStream`)
|
|
189
|
+
this.pending.set(key, {
|
|
190
|
+
parts: [body.slice()],
|
|
191
|
+
size: body.length,
|
|
192
|
+
sequence: packet.sequenceNumber,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const held = this.pending.get(key);
|
|
197
|
+
if (held === undefined)
|
|
198
|
+
return [];
|
|
199
|
+
held.size += body.length;
|
|
200
|
+
if (packet.sequenceNumber !== (held.sequence + 1) >>> 0 ||
|
|
201
|
+
held.size > PENDING_MOST) {
|
|
202
|
+
this.pending.delete(key);
|
|
203
|
+
this.dropped++;
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
held.parts.push(body.slice());
|
|
207
|
+
held.sequence = packet.sequenceNumber;
|
|
208
|
+
if (fragmentation === LAST) {
|
|
209
|
+
this.pending.delete(key);
|
|
210
|
+
whole = concatBytes(held.parts);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (whole === null)
|
|
214
|
+
return [];
|
|
215
|
+
return aggregated ? splitAggregated(whole, longLength) : [whole];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** 集約されたメッセージを、前に付いた長さで切り分ける */
|
|
219
|
+
function splitAggregated(data, longLength) {
|
|
220
|
+
const width = longLength ? 4 : 2;
|
|
221
|
+
const out = [];
|
|
222
|
+
let at = 0;
|
|
223
|
+
while (at + width <= data.length) {
|
|
224
|
+
const length = longLength ? u32(data, at) : u16(data, at);
|
|
225
|
+
const end = at + width + length;
|
|
226
|
+
if (end > data.length)
|
|
227
|
+
break;
|
|
228
|
+
out.push(data.subarray(at + width, end));
|
|
229
|
+
at = end;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
package/dist/psi.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
|
|
3
3
|
* 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
|
|
4
|
-
* 読むのは NIT と SDT の 2
|
|
4
|
+
* 読むのは NIT と SDT の 2 つ。ケーブルでは、局の無い TS を見分けるのに PAT も読む (`ServiceReader`)。
|
|
5
5
|
*
|
|
6
6
|
* - NIT (PID 0x0010) … ネットワーク ID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
|
|
7
7
|
* (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
|
|
@@ -27,6 +27,8 @@ export declare const TABLE_SDT_ACTUAL = 66;
|
|
|
27
27
|
* 取り込むほう (denpa の epg.ts の DIGITAL_TV)
|
|
28
28
|
*/
|
|
29
29
|
export declare const SERVICE_TYPES: Set<number>;
|
|
30
|
+
/** MPEG-2 の CRC32。セクション末尾の 4 バイトを含めて回すと 0 になる */
|
|
31
|
+
export declare function crc32(data: Uint8Array): number;
|
|
30
32
|
/**
|
|
31
33
|
* セクション本体に CRC32 を付けて仕上げる。`section_length` も実長に直す。
|
|
32
34
|
*
|
|
@@ -48,8 +50,9 @@ export interface TransportInfo {
|
|
|
48
50
|
}
|
|
49
51
|
export interface NetworkInfo {
|
|
50
52
|
networkId: number;
|
|
53
|
+
/** 最初の TS のリモコン番号。地上波の NIT は自分の TS 1 つだけを載せる */
|
|
51
54
|
remoteControlKeyId: number | null;
|
|
52
|
-
transportStreams:
|
|
55
|
+
transportStreams: NetworkTransport[];
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
58
|
* PSI セクションを組み立てる。
|
|
@@ -182,6 +185,8 @@ export type Delivery = SatelliteDelivery | TerrestrialDelivery;
|
|
|
182
185
|
/** NIT の TS の 1 件。サービスの一覧に、どこで流れているかが付く */
|
|
183
186
|
export interface NetworkTransport extends TransportInfo {
|
|
184
187
|
delivery: Delivery | null;
|
|
188
|
+
/** その TS のリモコン番号 (TS 情報記述子)。載せていなければ null */
|
|
189
|
+
remoteControlKeyId: number | null;
|
|
185
190
|
}
|
|
186
191
|
/** そろった NIT。セクションをつないだもの */
|
|
187
192
|
export interface Network {
|
|
@@ -259,13 +264,26 @@ export interface FoundService extends Service {
|
|
|
259
264
|
* Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
|
|
260
265
|
* 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
|
|
261
266
|
* 分からないと設定に書けない。
|
|
267
|
+
*
|
|
268
|
+
* **ケーブル (`requireNit: false`) は SDT[actual] だけで揃ったとみなす。** 1 つのネットワークが
|
|
269
|
+
* 地上波・BS の再送信と自主放送の TS をまとめて流していて、NIT は CATV のネットワークのもの
|
|
270
|
+
* (局の元の放送とは別)。どの放送の局かは SDT の original_network_id で分かる。
|
|
271
|
+
* NIT が先に来ていれば、その TS のリモコン番号だけを使う。
|
|
272
|
+
* PAT に局が 1 つも無い TS (データだけの TS) も揃ったとみなし、局は返さない (`empty`)
|
|
262
273
|
*/
|
|
263
274
|
export declare class ServiceReader {
|
|
264
275
|
private readonly nit;
|
|
265
276
|
private readonly sdt;
|
|
277
|
+
private readonly pat;
|
|
266
278
|
private readonly packets;
|
|
279
|
+
private readonly requireNit;
|
|
267
280
|
network: NetworkInfo | null;
|
|
268
281
|
transport: TransportInfo | null;
|
|
282
|
+
/** PAT に局が 1 つも無かった (`requireNit: false` のときだけ読む) */
|
|
283
|
+
empty: boolean;
|
|
284
|
+
constructor({ requireNit }?: {
|
|
285
|
+
requireNit?: boolean;
|
|
286
|
+
});
|
|
269
287
|
get complete(): boolean;
|
|
270
288
|
/** 任意の長さのバイト列を渡す。揃ったら true */
|
|
271
289
|
feed(chunk: Uint8Array): boolean;
|
package/dist/psi.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
|
|
3
3
|
* 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
|
|
4
|
-
* 読むのは NIT と SDT の 2
|
|
4
|
+
* 読むのは NIT と SDT の 2 つ。ケーブルでは、局の無い TS を見分けるのに PAT も読む (`ServiceReader`)。
|
|
5
5
|
*
|
|
6
6
|
* - NIT (PID 0x0010) … ネットワーク ID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
|
|
7
7
|
* (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
|
|
@@ -55,7 +55,7 @@ const CRC32_TABLE = (() => {
|
|
|
55
55
|
return table;
|
|
56
56
|
})();
|
|
57
57
|
/** MPEG-2 の CRC32。セクション末尾の 4 バイトを含めて回すと 0 になる */
|
|
58
|
-
function crc32(data) {
|
|
58
|
+
export function crc32(data) {
|
|
59
59
|
let crc = 0xffffffff;
|
|
60
60
|
for (const byte of data) {
|
|
61
61
|
crc =
|
|
@@ -481,11 +481,11 @@ function parseNitSection(section) {
|
|
|
481
481
|
at += 6 + loop;
|
|
482
482
|
const services = [];
|
|
483
483
|
let delivery = null;
|
|
484
|
+
let remote = null;
|
|
484
485
|
for (const [tag, descriptor] of descriptors(body)) {
|
|
485
|
-
if (tag === DESC_TS_INFORMATION &&
|
|
486
|
-
|
|
487
|
-
remoteControlKeyId
|
|
488
|
-
remoteControlKeyId = descriptor[0];
|
|
486
|
+
if (tag === DESC_TS_INFORMATION && descriptor.length > 0) {
|
|
487
|
+
remote ??= descriptor[0];
|
|
488
|
+
remoteControlKeyId ??= remote;
|
|
489
489
|
}
|
|
490
490
|
else if (tag === DESC_SERVICE_LIST) {
|
|
491
491
|
for (let i = 0; i + 3 <= descriptor.length; i += 3) {
|
|
@@ -508,13 +508,13 @@ function parseNitSection(section) {
|
|
|
508
508
|
originalNetworkId,
|
|
509
509
|
services,
|
|
510
510
|
delivery,
|
|
511
|
+
remoteControlKeyId: remote,
|
|
511
512
|
});
|
|
512
513
|
}
|
|
513
514
|
return {
|
|
514
515
|
other: tableId === TABLE_NIT_OTHER,
|
|
515
516
|
networkId: u16(section, 3),
|
|
516
517
|
version: (section[5] >> 1) & 0x1f,
|
|
517
|
-
current: (section[5] & 0x01) !== 0,
|
|
518
518
|
sectionNumber: section[6],
|
|
519
519
|
lastSectionNumber: section[7],
|
|
520
520
|
remoteControlKeyId,
|
|
@@ -716,15 +716,30 @@ export class SdtReader {
|
|
|
716
716
|
* Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
|
|
717
717
|
* 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
|
|
718
718
|
* 分からないと設定に書けない。
|
|
719
|
+
*
|
|
720
|
+
* **ケーブル (`requireNit: false`) は SDT[actual] だけで揃ったとみなす。** 1 つのネットワークが
|
|
721
|
+
* 地上波・BS の再送信と自主放送の TS をまとめて流していて、NIT は CATV のネットワークのもの
|
|
722
|
+
* (局の元の放送とは別)。どの放送の局かは SDT の original_network_id で分かる。
|
|
723
|
+
* NIT が先に来ていれば、その TS のリモコン番号だけを使う。
|
|
724
|
+
* PAT に局が 1 つも無い TS (データだけの TS) も揃ったとみなし、局は返さない (`empty`)
|
|
719
725
|
*/
|
|
720
726
|
export class ServiceReader {
|
|
721
727
|
nit = new SectionAssembler(PID_NIT);
|
|
722
728
|
sdt = new SectionAssembler(PID_SDT);
|
|
729
|
+
pat = new SectionAssembler(PID_PAT);
|
|
723
730
|
packets = new PacketStream();
|
|
731
|
+
requireNit;
|
|
724
732
|
network = null;
|
|
725
733
|
transport = null;
|
|
734
|
+
/** PAT に局が 1 つも無かった (`requireNit: false` のときだけ読む) */
|
|
735
|
+
empty = false;
|
|
736
|
+
constructor({ requireNit = true } = {}) {
|
|
737
|
+
this.requireNit = requireNit;
|
|
738
|
+
}
|
|
726
739
|
get complete() {
|
|
727
|
-
return this.
|
|
740
|
+
return this.requireNit
|
|
741
|
+
? this.network !== null && this.transport !== null
|
|
742
|
+
: this.transport !== null || this.empty;
|
|
728
743
|
}
|
|
729
744
|
/** 任意の長さのバイト列を渡す。揃ったら true */
|
|
730
745
|
feed(chunk) {
|
|
@@ -739,16 +754,22 @@ export class ServiceReader {
|
|
|
739
754
|
this.transport = parseSdt(section) ?? this.transport;
|
|
740
755
|
}
|
|
741
756
|
}
|
|
757
|
+
if (!this.requireNit && !this.empty && this.transport === null) {
|
|
758
|
+
for (const section of this.pat.feed(packet)) {
|
|
759
|
+
if (section[0] === TABLE_PAT && parsePat(section).size === 0)
|
|
760
|
+
this.empty = true;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
742
763
|
}
|
|
743
764
|
return this.complete;
|
|
744
765
|
}
|
|
745
766
|
/** 録るに値するサービスだけ。リモコン番号は NIT のものを配る */
|
|
746
767
|
services() {
|
|
747
|
-
|
|
768
|
+
const transport = this.transport;
|
|
769
|
+
if (transport === null)
|
|
748
770
|
return [];
|
|
749
|
-
const {
|
|
750
|
-
const
|
|
751
|
-
return this.transport.services
|
|
771
|
+
const { transportStreamId, originalNetworkId } = transport;
|
|
772
|
+
const found = (networkId, remoteControlKeyId) => transport.services
|
|
752
773
|
.filter((service) => SERVICE_TYPES.has(service.serviceType))
|
|
753
774
|
.map((service) => ({
|
|
754
775
|
...service,
|
|
@@ -756,5 +777,13 @@ export class ServiceReader {
|
|
|
756
777
|
transportStreamId,
|
|
757
778
|
remoteControlKeyId,
|
|
758
779
|
}));
|
|
780
|
+
if (this.requireNit) {
|
|
781
|
+
if (this.network === null)
|
|
782
|
+
return [];
|
|
783
|
+
return found(this.network.networkId, this.network.remoteControlKeyId);
|
|
784
|
+
}
|
|
785
|
+
const own = this.network?.transportStreams.find((ts) => ts.transportStreamId === transportStreamId &&
|
|
786
|
+
ts.originalNetworkId === originalNetworkId);
|
|
787
|
+
return found(originalNetworkId, own?.remoteControlKeyId ?? null);
|
|
759
788
|
}
|
|
760
789
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **本物の HEVC と AAC を MMT/TLV に包んで、4K の局の放送を作る** (試験用。`synth-av.ts` の 4K の放送に当たる)。
|
|
3
|
+
*
|
|
4
|
+
* 素材は ffmpeg で作り、ここに base64 で持つ (E2E のイメージには本物の ffmpeg が無い)。
|
|
5
|
+
*
|
|
6
|
+
* - 映像: `testsrc2` の 64x64・59.94 コマ・16 コマを libx265 で HEVC Main10 にしたもの
|
|
7
|
+
* (keyint 8・B ピクチャ 3・閉じた GOP・AUD と VPS/SPS/PPS をキーフレームごとに)。`-f hevc` の Annex B
|
|
8
|
+
* - 音声: 440 Hz の正弦波を AAC LC (48 kHz・2 チャンネル) にした 13 フレームを `-f latm` (LOAS) で書いたもの
|
|
9
|
+
*
|
|
10
|
+
* どちらも 1 つの mp4 から書き出した。そのときの映像の PTS と DTS (180000 の単位、デコードの順) が `VIDEO_TIMES`。
|
|
11
|
+
* `SynthMmtAv` は映像の GOP (8 コマ) を 1 つの MPU にして繰り返し、時刻と番号を進めながら流し続ける
|
|
12
|
+
* (偽エージェントの 4K の局。`tests/fake/broadcast.ts`)
|
|
13
|
+
*/
|
|
14
|
+
/** 映像の素材 (HEVC Main10 の Annex B。base64) */
|
|
15
|
+
export declare const HEVC: string;
|
|
16
|
+
/** 音声の素材 (AAC LC の LOAS。base64) */
|
|
17
|
+
export declare const LOAS: string;
|
|
18
|
+
/** ffprobe が読んだ映像の [PTS, DTS] (180000 の単位、デコードの順) */
|
|
19
|
+
export declare const VIDEO_TIMES: [number, number][];
|
|
20
|
+
/** 映像の 1 GOP のコマ数 (keyint) */
|
|
21
|
+
export declare const GOP = 8;
|
|
22
|
+
/** 音声の素材のフレーム数 */
|
|
23
|
+
export declare const AUDIO_FRAMES = 13;
|
|
24
|
+
/** MPT の時刻の単位 (実機の BS4K と同じ) */
|
|
25
|
+
export declare const TIMESCALE = 180000;
|
|
26
|
+
/** base64 をバイト列に */
|
|
27
|
+
export declare function base64Bytes(base64: string): Uint8Array;
|
|
28
|
+
/** Annex B を NAL ユニットに切り、AUD (35) ごとにコマにまとめる (デコードの順) */
|
|
29
|
+
export declare function hevcPictures(annexB: Uint8Array): Uint8Array[][];
|
|
30
|
+
/** LOAS の同期のヘッダー (11 ビットの 0x2B7 と 13 ビットの長さ) を外し、AudioMuxElement を並べる */
|
|
31
|
+
export declare function latmElements(loas: Uint8Array): Uint8Array[];
|
|
32
|
+
/** 4K の局 1 つぶんの組み立ての決まり (`SynthMmtAv`) */
|
|
33
|
+
export interface SynthMmtAvOptions {
|
|
34
|
+
/** 局の service_id (MPT の MMT_package_id) */
|
|
35
|
+
serviceId: number;
|
|
36
|
+
/** MPT の packet_id */
|
|
37
|
+
mptPacketId: number;
|
|
38
|
+
/** 映像の packet_id。省くと実機の BS4K と同じ 0xF100 */
|
|
39
|
+
videoPacketId?: number;
|
|
40
|
+
/** 音声の packet_id。省くと 0xF110 */
|
|
41
|
+
audioPacketId?: number;
|
|
42
|
+
/** 字幕の packet_id。省くと 0xF130 */
|
|
43
|
+
captionPacketId?: number;
|
|
44
|
+
/** 映像の最初の MPU の提示時刻 (UNIX 時刻のミリ秒)。放送の時計 (NTP) にして MPT に書く */
|
|
45
|
+
startMs: number;
|
|
46
|
+
/** 字幕の文字。省くと字幕を流さない。映像の MPU `CAPTION_EVERY` 個ごとに TTML の文書を 1 つ流す */
|
|
47
|
+
caption?: string;
|
|
48
|
+
/** ヘッダー圧縮した IP の CID。省くと 1 */
|
|
49
|
+
cid?: number;
|
|
50
|
+
}
|
|
51
|
+
/** 映像の 1 MPU の長さ (ミリ秒)。実時間で流すときの間隔 */
|
|
52
|
+
export declare const MPU_MS: number;
|
|
53
|
+
/**
|
|
54
|
+
* **素材の映像と音声 (と字幕) を、MMT/TLV で流し続ける。** `next` を呼ぶたびに映像の 1 MPU (1 GOP・8 コマ・約 0.13 秒) と、
|
|
55
|
+
* そこまでに要る音声の MPU を、時刻と番号を進めて返す。どの MPU の前にも、これから来る MPU の時刻を並べた MPT を付ける
|
|
56
|
+
* (途中から読み始めても、次の MPU から読める)
|
|
57
|
+
*/
|
|
58
|
+
export declare class SynthMmtAv {
|
|
59
|
+
private readonly options;
|
|
60
|
+
private readonly video;
|
|
61
|
+
private readonly audio;
|
|
62
|
+
private readonly ids;
|
|
63
|
+
private readonly cid;
|
|
64
|
+
/** 映像の最初の MPU の提示時刻 (180000 の単位、NTP の始まりから) */
|
|
65
|
+
private readonly base;
|
|
66
|
+
/** 次に流す映像の MPU (0 から。mpu_sequence_number) */
|
|
67
|
+
private k;
|
|
68
|
+
/** 次に流す音声の MPU (0 から) */
|
|
69
|
+
private j;
|
|
70
|
+
/** packet_id ごとの、次の packet_sequence_number */
|
|
71
|
+
private readonly sequence;
|
|
72
|
+
constructor(options: SynthMmtAvOptions);
|
|
73
|
+
/** 180000 の単位の時刻を NTP に */
|
|
74
|
+
private ntp;
|
|
75
|
+
/** packet_id ごとに続き番号を振りながら MFU を作る */
|
|
76
|
+
private mfu;
|
|
77
|
+
/** 映像の MPU `k` のコマの [PTS, DTS] (180000 の単位、NTP の始まりから。デコードの順) */
|
|
78
|
+
private videoTimes;
|
|
79
|
+
/** これから流す映像・音声・字幕の MPU の時刻を並べた MPT */
|
|
80
|
+
private mpt;
|
|
81
|
+
/** 映像の 1 MPU と、そこまでの音声 (と字幕) を TLV のバイト列で */
|
|
82
|
+
next(): Uint8Array;
|
|
83
|
+
}
|