@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.
- package/LICENSE +82 -0
- package/README.md +65 -2
- package/dist/ait.d.ts +97 -0
- package/dist/ait.js +205 -0
- package/dist/aribtext-gaiji.d.ts +25 -0
- package/dist/aribtext-gaiji.js +69 -0
- package/dist/aribtext.d.ts +31 -0
- package/dist/aribtext.js +314 -0
- package/dist/b24-tables.d.ts +50 -0
- package/dist/b24-tables.js +410 -0
- package/dist/b24caption.d.ts +98 -0
- package/dist/b24caption.js +987 -0
- package/dist/bml-protocol.d.ts +143 -0
- package/dist/bml-protocol.js +12 -0
- package/dist/bml.d.ts +87 -0
- package/dist/bml.js +489 -0
- package/dist/bytes.d.ts +20 -0
- package/dist/bytes.js +45 -0
- package/dist/caption-text.d.ts +122 -0
- package/dist/caption-text.js +39 -0
- package/dist/drcs-smooth.d.ts +52 -0
- package/dist/drcs-smooth.js +163 -0
- package/dist/dsmcc.d.ts +92 -0
- package/dist/dsmcc.js +191 -0
- package/dist/eit.d.ts +316 -0
- package/dist/eit.js +708 -0
- package/dist/language.d.ts +7 -0
- package/dist/language.js +20 -0
- package/dist/multipart.d.ts +43 -0
- package/dist/multipart.js +194 -0
- package/dist/pes.d.ts +99 -0
- package/dist/pes.js +231 -0
- package/dist/psi.d.ts +169 -0
- package/dist/psi.js +497 -0
- package/dist/service-filter.d.ts +95 -0
- package/dist/service-filter.js +248 -0
- package/dist/synth-av.d.ts +51 -0
- package/dist/synth-av.js +298 -0
- package/dist/synth-caption.d.ts +38 -0
- package/dist/synth-caption.js +115 -0
- package/dist/synth.d.ts +183 -0
- package/dist/synth.js +508 -0
- package/package.json +30 -4
package/dist/bml.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* データ放送 (ARIB STD-B24 の BML) をデコードする。**1局に絞った TS を食わせると、
|
|
3
|
+
* 組み立て終わったファイルが出てくる。**
|
|
4
|
+
*
|
|
5
|
+
* TS → PAT → PMT → データ符号化方式記述子 (0xFD) の付いた ES
|
|
6
|
+
* → DII/DDB (カルーセル) → モジュール → ファイル
|
|
7
|
+
*
|
|
8
|
+
* ## なぜ自前なのか
|
|
9
|
+
*
|
|
10
|
+
* **運び方は衛星のロゴと同じ DSM-CC** で、denpa はそれを既に持っている
|
|
11
|
+
* ([dsmcc.ts](dsmcc.ts) / [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts))。
|
|
12
|
+
* [web-bml](https://github.com/otya128/web-bml) の `decode_ts.ts` は使わない — 要るのは
|
|
13
|
+
* その一部だけで、残り (EIT/SDT/NIT・字幕の PES) は自前で読んでおり
|
|
14
|
+
* ([eit.ts](eit.ts) / [psi.ts](psi.ts) / ffmpeg)、`@chinachu/aribts` も抱えずに済む。
|
|
15
|
+
*
|
|
16
|
+
* **出す形式は web-bml の頃のまま** ([bml-protocol.ts](bml-protocol.ts))。録画の隣の
|
|
17
|
+
* `.bml.jsonl` がこの形式で残っているので、変えると前に録ったものが読めなくなる。
|
|
18
|
+
* モジュールの中身 (multipart) は [multipart.ts](multipart.ts) がデコードする。
|
|
19
|
+
*/
|
|
20
|
+
import { Buffer } from "node:buffer";
|
|
21
|
+
import { inflateSync } from "node:zlib";
|
|
22
|
+
import { u16, u32 } from "./bytes.js";
|
|
23
|
+
import { dataEventIdOf, ModuleBuilder, parseDdb, parseDii, TABLE_DII, TABLE_STREAM_DESCRIPTOR, } from "./dsmcc.js";
|
|
24
|
+
import { parseTimeTable, PID_TIME } from "./eit.js";
|
|
25
|
+
import { parseMediaType, readEntity } from "./multipart.js";
|
|
26
|
+
import { descriptors, packetPid, PacketStream, pmtStreams, PmtTracker, SectionAssembler, } from "./psi.js";
|
|
27
|
+
const DESC_STREAM_IDENTIFIER = 0x52;
|
|
28
|
+
/** データ符号化方式記述子 (STD-B10 第2部 6.2.20) */
|
|
29
|
+
const DESC_DATA_COMPONENT = 0xfd;
|
|
30
|
+
/**
|
|
31
|
+
* BML が載っている符号化方式 (STD-B10 第2部 付録J 表J-1)。
|
|
32
|
+
* 0x0C/0x0D が地上波、0x07 が BS、0x0B が CS
|
|
33
|
+
*/
|
|
34
|
+
const BXML_COMPONENT_IDS = new Set([0x07, 0x0b, 0x0c, 0x0d]);
|
|
35
|
+
/** zlib。Compression Type 記述子で運用されるのはこれだけ */
|
|
36
|
+
const COMPRESSION_ZLIB = 0;
|
|
37
|
+
/**
|
|
38
|
+
* データ符号化方式記述子の後半を読む。
|
|
39
|
+
*
|
|
40
|
+
* 地上波は TR-B14 第二分冊 2.1.4 表2-3、BS/CS は TR-B15 第一分冊 5.1.5 表5-4。
|
|
41
|
+
*
|
|
42
|
+
* **`data_component_id` は16ビット** (aribts は8ビットで読んでいる)。
|
|
43
|
+
*/
|
|
44
|
+
export function parseBxmlInfo(data) {
|
|
45
|
+
let at = 0;
|
|
46
|
+
// 00 のみ運用 (データカルーセル伝送方式およびイベントメッセージ伝送方式)
|
|
47
|
+
const transmissionFormat = (data[at] >> 6) & 0b11;
|
|
48
|
+
// component_tag=0x40 のとき必ず1。startup.xml が最初に起動される
|
|
49
|
+
const entryPointFlag = ((data[at] >> 5) & 1) === 1;
|
|
50
|
+
const info = { transmissionFormat, entryPointFlag };
|
|
51
|
+
if (entryPointFlag) {
|
|
52
|
+
const autoStartFlag = ((data[at] >> 4) & 1) === 1;
|
|
53
|
+
/*
|
|
54
|
+
* 画面の大きさ。運用されるのは 0011 (960x540)、0100 (640x480 16:9)、
|
|
55
|
+
* 0101 (640x480 4:3) の3つ
|
|
56
|
+
*/
|
|
57
|
+
const documentResolution = data[at] & 0x0f;
|
|
58
|
+
at++;
|
|
59
|
+
const useXML = ((data[at] >> 7) & 1) === 1;
|
|
60
|
+
const defaultVersionFlag = ((data[at] >> 6) & 1) === 1;
|
|
61
|
+
// BS/CS では 0 のとき単独視聴不可。地上波は常に1
|
|
62
|
+
const independentFlag = ((data[at] >> 5) & 1) === 1;
|
|
63
|
+
const styleForTVFlag = ((data[at] >> 4) & 1) === 1;
|
|
64
|
+
at++;
|
|
65
|
+
// 既定は BS の 1.0。地上波は3、CS は2が入ってくる
|
|
66
|
+
info.entryPointInfo = {
|
|
67
|
+
autoStartFlag,
|
|
68
|
+
documentResolution,
|
|
69
|
+
useXML,
|
|
70
|
+
defaultVersionFlag,
|
|
71
|
+
independentFlag,
|
|
72
|
+
styleForTVFlag,
|
|
73
|
+
bmlMajorVersion: 1,
|
|
74
|
+
bmlMinorVersion: 0,
|
|
75
|
+
};
|
|
76
|
+
if (!defaultVersionFlag) {
|
|
77
|
+
info.entryPointInfo.bmlMajorVersion = u16(data, at);
|
|
78
|
+
info.entryPointInfo.bmlMinorVersion = u16(data, at + 2);
|
|
79
|
+
at += 4;
|
|
80
|
+
// 運用されない
|
|
81
|
+
if (useXML) {
|
|
82
|
+
info.entryPointInfo.bxmlMajorVersion = u16(data, at);
|
|
83
|
+
info.entryPointInfo.bxmlMinorVersion = u16(data, at + 2);
|
|
84
|
+
at += 4;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
at++;
|
|
90
|
+
}
|
|
91
|
+
if (transmissionFormat === 0) {
|
|
92
|
+
// additional_arib_carousel_info (STD-B24 第三分冊 第三編 C.1)
|
|
93
|
+
const dataEventId = (data[at] >> 4) & 0x0f;
|
|
94
|
+
const eventSectionFlag = ((data[at] >> 3) & 1) === 1;
|
|
95
|
+
at++;
|
|
96
|
+
const ondemandRetrievalFlag = ((data[at] >> 7) & 1) === 1;
|
|
97
|
+
const fileStorableFlag = ((data[at] >> 6) & 1) === 1;
|
|
98
|
+
const startPriority = (data[at] >> 5) & 1;
|
|
99
|
+
info.additionalAribCarouselInfo = {
|
|
100
|
+
dataEventId,
|
|
101
|
+
eventSectionFlag,
|
|
102
|
+
ondemandRetrievalFlag,
|
|
103
|
+
fileStorableFlag,
|
|
104
|
+
startPriority,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return info;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* PMT から ES を並べる。**部品タグ (component_tag) を持たない ES は数に入れない** —
|
|
111
|
+
* データ放送は「何番の部品か」でしか物を指せないので、番号の無いものは指しようがない。
|
|
112
|
+
*/
|
|
113
|
+
function parsePmt(section) {
|
|
114
|
+
const components = [];
|
|
115
|
+
for (const [streamType, pid, info] of pmtStreams(section)) {
|
|
116
|
+
let componentId;
|
|
117
|
+
let dataComponentId;
|
|
118
|
+
let bxmlInfo;
|
|
119
|
+
for (const [tag, descriptor] of descriptors(info)) {
|
|
120
|
+
if (tag === DESC_STREAM_IDENTIFIER && descriptor.length >= 1)
|
|
121
|
+
componentId = descriptor[0];
|
|
122
|
+
else if (tag === DESC_DATA_COMPONENT && descriptor.length >= 2) {
|
|
123
|
+
dataComponentId = u16(descriptor, 0);
|
|
124
|
+
if (BXML_COMPONENT_IDS.has(dataComponentId))
|
|
125
|
+
bxmlInfo = parseBxmlInfo(descriptor.subarray(2));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (componentId === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
components.push({
|
|
131
|
+
componentId,
|
|
132
|
+
pid,
|
|
133
|
+
streamType,
|
|
134
|
+
...(dataComponentId === undefined ? {} : { dataComponentId }),
|
|
135
|
+
...(bxmlInfo === undefined ? {} : { bxmlInfo }),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return components;
|
|
139
|
+
}
|
|
140
|
+
/** `at` の下位1ビットと続く4バイトで 33 ビット (STC・NPT) */
|
|
141
|
+
const u33 = (data, at) => (data[at] & 1) * 0x100000000 + u32(data, at + 1);
|
|
142
|
+
/** ストリーム記述子 (table_id 0x3D)。NPT と イベントメッセージが載っている */
|
|
143
|
+
function parseEsEvents(section) {
|
|
144
|
+
const events = [];
|
|
145
|
+
const length = u16(section, 1) & 0x0fff;
|
|
146
|
+
const body = section.subarray(8, Math.min(3 + length - 4, section.length));
|
|
147
|
+
for (const [tag, descriptor] of descriptors(body)) {
|
|
148
|
+
if (tag === 0x17 && descriptor.length >= 18) {
|
|
149
|
+
// NPT 参照記述子。放送の時計と番組内の時刻を結び付ける
|
|
150
|
+
events.push({
|
|
151
|
+
type: "nptReference",
|
|
152
|
+
postDiscontinuityIndicator: (descriptor[0] & 0x80) !== 0,
|
|
153
|
+
dsmContentId: descriptor[0] & 0x7f,
|
|
154
|
+
STCReference: u33(descriptor, 1),
|
|
155
|
+
NPTReference: u33(descriptor, 9),
|
|
156
|
+
scaleNumerator: u16(descriptor, 14),
|
|
157
|
+
scaleDenominator: u16(descriptor, 16),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if (tag === 0x40 && descriptor.length >= 11) {
|
|
161
|
+
// 汎用イベントメッセージ記述子。運用されるのは time_mode 0 と 2 だけ
|
|
162
|
+
const timeMode = descriptor[2];
|
|
163
|
+
const message = {
|
|
164
|
+
eventMessageGroupId: (u16(descriptor, 0) >> 4) & 0x0fff,
|
|
165
|
+
eventMessageType: descriptor[8],
|
|
166
|
+
eventMessageId: u16(descriptor, 9),
|
|
167
|
+
privateDataByte: [...descriptor.subarray(11)],
|
|
168
|
+
};
|
|
169
|
+
if (timeMode === 0)
|
|
170
|
+
events.push({ type: "immediateEvent", timeMode: 0, ...message });
|
|
171
|
+
else if (timeMode === 2)
|
|
172
|
+
events.push({
|
|
173
|
+
type: "nptEvent",
|
|
174
|
+
timeMode: 2,
|
|
175
|
+
eventMessageNPT: u33(descriptor, 3),
|
|
176
|
+
...message,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return events;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* **DII に載ったモジュールが来なければ諦める。** 最後にブロックが増えてから、DII が
|
|
184
|
+
* この回数繰り返し、しかもカルーセルが一覧の大きさのこの倍数ぶん流れたら、もう来ないとみなす。
|
|
185
|
+
* 両方を見るのは、DII の繰り返しはカルーセルの一周より短いことがあり (回数だけだと大きい
|
|
186
|
+
* カルーセルの途中で諦める)、よく使うモジュールは一周に何度も流れる (バイト数だけだと一周を
|
|
187
|
+
* 短く見積もる) ため
|
|
188
|
+
*/
|
|
189
|
+
const GIVE_UP_ROUNDS = 4;
|
|
190
|
+
/** `Component.done` に置く印。モジュールの版とカルーセルの回 (dataEventId) の組 */
|
|
191
|
+
function doneMark(component, version) {
|
|
192
|
+
return `${version}:${component.dataEventId}`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 1局に絞った TS を食わせると、データ放送の通知が出てくる。
|
|
196
|
+
*
|
|
197
|
+
* **1つの選局に1つ。** 局を変えたら作り直す — カルーセルは局ごとに別物で、
|
|
198
|
+
* 組み立て中のものを持ち越すと混ざる。
|
|
199
|
+
*/
|
|
200
|
+
export class BmlDecoder {
|
|
201
|
+
send;
|
|
202
|
+
packets = new PacketStream();
|
|
203
|
+
/** **1局に絞ったあとを食わせる前提。** 入っているサービスは1つなので、最初の局の PMT を追う */
|
|
204
|
+
pmt = new PmtTracker();
|
|
205
|
+
// TDT は CRC を持たない。DSM-CC と同じ理由で 'syntax' で受ける
|
|
206
|
+
time = new SectionAssembler(PID_TIME, "syntax");
|
|
207
|
+
/** カルーセルが流れている ES。PID ごと */
|
|
208
|
+
components = new Map();
|
|
209
|
+
/** 最後に配った PMT。**変わったときだけ配る** */
|
|
210
|
+
sent = null;
|
|
211
|
+
constructor(send) {
|
|
212
|
+
this.send = send;
|
|
213
|
+
}
|
|
214
|
+
/** 1局に絞った TS を食わせる */
|
|
215
|
+
feed(chunk) {
|
|
216
|
+
for (const packet of this.packets.feed(chunk))
|
|
217
|
+
this.feedPacket(packet);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 切り分け済みのパケットを1つ。ライブは同じ TS を時計や Hybridcast とも
|
|
221
|
+
* 分け合うので、切り分けは1回で済ませて配る (`server/live.ts` の `tap`)。
|
|
222
|
+
*
|
|
223
|
+
* **PID で行き先を引く。** 組み立て役を全部に回していた頃は、データ放送と
|
|
224
|
+
* 関係ない映像のパケットのたびにカルーセルの数だけ呼んでいた。
|
|
225
|
+
*
|
|
226
|
+
* `packet` は呼び手の塊の上の窓 (`psi.ts` の `PacketStream`)。**返ったあとまで
|
|
227
|
+
* 持つなら写すこと** — 塊の入れ物は使い回される
|
|
228
|
+
*/
|
|
229
|
+
feedPacket(packet) {
|
|
230
|
+
const pid = packetPid(packet);
|
|
231
|
+
// 同じ PID を2役が名乗ることもあるので、else でつながない
|
|
232
|
+
for (const section of this.pmt.feed(packet, pid))
|
|
233
|
+
this.onPmt(section);
|
|
234
|
+
if (pid === PID_TIME)
|
|
235
|
+
for (const section of this.time.feed(packet))
|
|
236
|
+
this.onTime(section);
|
|
237
|
+
const component = this.components.get(pid);
|
|
238
|
+
if (component !== undefined) {
|
|
239
|
+
for (const section of component.sections.feed(packet))
|
|
240
|
+
this.onCarousel(component, section);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
onPmt(section) {
|
|
244
|
+
const components = parsePmt(section);
|
|
245
|
+
if (components.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
/*
|
|
248
|
+
* 放送は同じ表を何度も送る。**変わったときだけ配る** — 受け側は
|
|
249
|
+
* PMT が来るたびに画面を組み直すので、そのまま通すと出っ放しになる
|
|
250
|
+
*/
|
|
251
|
+
const shape = JSON.stringify(components);
|
|
252
|
+
if (this.sent !== shape) {
|
|
253
|
+
this.sent = shape;
|
|
254
|
+
this.send({ type: "pmt", components });
|
|
255
|
+
}
|
|
256
|
+
for (const component of components) {
|
|
257
|
+
// カルーセルが流れているのは、BML の符号化方式が付いた ES だけ
|
|
258
|
+
if (component.bxmlInfo === undefined)
|
|
259
|
+
continue;
|
|
260
|
+
if (this.components.has(component.pid))
|
|
261
|
+
continue;
|
|
262
|
+
this.components.set(component.pid, {
|
|
263
|
+
componentId: component.componentId,
|
|
264
|
+
// DSM-CC は section_syntax_indicator が立たないことがある (psi.ts)
|
|
265
|
+
sections: new SectionAssembler(component.pid, "syntax"),
|
|
266
|
+
transactionId: null,
|
|
267
|
+
dataEventId: -1,
|
|
268
|
+
building: new Map(),
|
|
269
|
+
done: new Map(),
|
|
270
|
+
dii: null,
|
|
271
|
+
size: 0,
|
|
272
|
+
abandoned: new Set(),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** TDT / TOT。読み方は [eit.ts](eit.ts) にまとめてある */
|
|
277
|
+
onTime(section) {
|
|
278
|
+
const at = parseTimeTable(section);
|
|
279
|
+
if (at !== null)
|
|
280
|
+
this.send({ type: "currentTime", timeUnixMillis: at });
|
|
281
|
+
}
|
|
282
|
+
onCarousel(component, section) {
|
|
283
|
+
if (section[0] === TABLE_STREAM_DESCRIPTOR) {
|
|
284
|
+
const extension = u16(section, 3);
|
|
285
|
+
this.send({
|
|
286
|
+
type: "esEventUpdated",
|
|
287
|
+
componentId: component.componentId,
|
|
288
|
+
events: parseEsEvents(section),
|
|
289
|
+
dataEventId: extension >> 12,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (section[0] === TABLE_DII)
|
|
294
|
+
this.onDii(component, section);
|
|
295
|
+
else
|
|
296
|
+
this.onDdb(component, section);
|
|
297
|
+
}
|
|
298
|
+
onDii(component, section) {
|
|
299
|
+
const dii = parseDii(section);
|
|
300
|
+
if (dii === null)
|
|
301
|
+
return;
|
|
302
|
+
// 同じカルーセルが回ってきただけ。組み立て中のものを捨てない
|
|
303
|
+
if (component.transactionId === dii.transactionId) {
|
|
304
|
+
for (const held of component.building.values())
|
|
305
|
+
held.quietDiis++;
|
|
306
|
+
this.giveUp(component);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
component.transactionId = dii.transactionId;
|
|
310
|
+
/*
|
|
311
|
+
* **番組が変わったら覚えていたものを捨てる。** dataEventId はカルーセルの
|
|
312
|
+
* 「回」で、ここが変わると前の番組のファイルは使えない
|
|
313
|
+
*/
|
|
314
|
+
const dataEventId = dataEventIdOf(dii.downloadId);
|
|
315
|
+
if (component.dataEventId !== dataEventId) {
|
|
316
|
+
component.dataEventId = dataEventId;
|
|
317
|
+
component.done.clear();
|
|
318
|
+
}
|
|
319
|
+
component.building.clear();
|
|
320
|
+
component.abandoned.clear();
|
|
321
|
+
component.dii = dii;
|
|
322
|
+
component.size = 0;
|
|
323
|
+
for (const module of dii.modules) {
|
|
324
|
+
component.size += module.moduleSize;
|
|
325
|
+
component.building.set(module.moduleId, {
|
|
326
|
+
module,
|
|
327
|
+
builder: new ModuleBuilder(module.moduleVersion, module.moduleSize, dii.blockSize),
|
|
328
|
+
quietDiis: 0,
|
|
329
|
+
quietBytes: 0,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
this.sendList(component);
|
|
333
|
+
// 大きさ 0 のものは DDB が来ない。待たせずに配る (来ないまま諦めると、受け側は無いものと見なす)
|
|
334
|
+
for (const [moduleId, held] of component.building) {
|
|
335
|
+
if (held.builder.complete)
|
|
336
|
+
this.finish(component, moduleId, held);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** 今の DII の一覧を配る。**諦めたモジュールは載せない** */
|
|
340
|
+
sendList(component) {
|
|
341
|
+
const dii = component.dii;
|
|
342
|
+
if (dii === null)
|
|
343
|
+
return;
|
|
344
|
+
this.send({
|
|
345
|
+
type: "moduleListUpdated",
|
|
346
|
+
componentId: component.componentId,
|
|
347
|
+
modules: dii.modules
|
|
348
|
+
.filter((module) => !component.abandoned.has(module.moduleId))
|
|
349
|
+
.map((module) => ({
|
|
350
|
+
id: module.moduleId,
|
|
351
|
+
version: module.moduleVersion,
|
|
352
|
+
size: module.moduleSize,
|
|
353
|
+
})),
|
|
354
|
+
dataEventId: component.dataEventId,
|
|
355
|
+
...(dii.returnToEntry === null
|
|
356
|
+
? {}
|
|
357
|
+
: { returnToEntryFlag: dii.returnToEntry }),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* **来ないモジュールを諦める** (`GIVE_UP_ROUNDS`)。待っている受け側 (`Resources` の `wait`) は、
|
|
362
|
+
* DII に無いモジュールを「もう来ない」と決める (`gone`) ので、諦めたものを除いた一覧を配り直して
|
|
363
|
+
* 待ちを終わらせる。前の DII で配り終えた版のままのもの (受け側が持っている) は一覧から外さず、
|
|
364
|
+
* 組み立てるのをやめるだけ。次に DII の版が変われば、また待つ
|
|
365
|
+
*/
|
|
366
|
+
giveUp(component) {
|
|
367
|
+
const limit = component.size * GIVE_UP_ROUNDS;
|
|
368
|
+
let changed = false;
|
|
369
|
+
for (const [moduleId, held] of component.building) {
|
|
370
|
+
if (held.quietDiis < GIVE_UP_ROUNDS || held.quietBytes < limit)
|
|
371
|
+
continue;
|
|
372
|
+
component.building.delete(moduleId);
|
|
373
|
+
if (component.done.get(moduleId) ===
|
|
374
|
+
doneMark(component, held.module.moduleVersion))
|
|
375
|
+
continue;
|
|
376
|
+
component.abandoned.add(moduleId);
|
|
377
|
+
changed = true;
|
|
378
|
+
}
|
|
379
|
+
if (changed)
|
|
380
|
+
this.sendList(component);
|
|
381
|
+
}
|
|
382
|
+
onDdb(component, section) {
|
|
383
|
+
const ddb = parseDdb(section);
|
|
384
|
+
if (ddb === null)
|
|
385
|
+
return;
|
|
386
|
+
for (const waiting of component.building.values())
|
|
387
|
+
waiting.quietBytes += ddb.block.length;
|
|
388
|
+
const held = component.building.get(ddb.moduleId);
|
|
389
|
+
// 版が食い違うブロックを混ぜると壊れたファイルができる
|
|
390
|
+
if (held === undefined || held.builder.moduleVersion !== ddb.moduleVersion)
|
|
391
|
+
return;
|
|
392
|
+
const before = held.builder.blocks;
|
|
393
|
+
const complete = held.builder.add(ddb.blockNumber, ddb.block);
|
|
394
|
+
if (held.builder.blocks > before) {
|
|
395
|
+
held.quietDiis = 0;
|
|
396
|
+
held.quietBytes = 0;
|
|
397
|
+
}
|
|
398
|
+
if (complete)
|
|
399
|
+
this.finish(component, ddb.moduleId, held);
|
|
400
|
+
}
|
|
401
|
+
/** 揃ったモジュールを配る */
|
|
402
|
+
finish(component, moduleId, held) {
|
|
403
|
+
// 次の DII が来るまで組み立て直さない
|
|
404
|
+
component.building.delete(moduleId);
|
|
405
|
+
/*
|
|
406
|
+
* **同じものを配り直さない。** カルーセルは回り続けるので、版が上がって
|
|
407
|
+
* いなくても同じモジュールが何度も揃う。そのたびに配ると、受け側は
|
|
408
|
+
* 中身の変わらない画面を組み直し続ける
|
|
409
|
+
*/
|
|
410
|
+
const version = held.builder.moduleVersion;
|
|
411
|
+
const mark = doneMark(component, version);
|
|
412
|
+
if (component.done.get(moduleId) === mark)
|
|
413
|
+
return;
|
|
414
|
+
component.done.set(moduleId, mark);
|
|
415
|
+
// 大きさ 0 は伸ばしようが無い。ファイルの無いモジュールとして配り、待っている受け側を終わらせる
|
|
416
|
+
const empty = held.module.moduleSize === 0;
|
|
417
|
+
const data = empty
|
|
418
|
+
? held.builder.data
|
|
419
|
+
: inflate(held.builder.data, held.module.compression);
|
|
420
|
+
if (data === null)
|
|
421
|
+
return;
|
|
422
|
+
const files = moduleFiles(data, held.module.contentType);
|
|
423
|
+
if (files.length === 0 && !empty)
|
|
424
|
+
return;
|
|
425
|
+
this.send({
|
|
426
|
+
type: "moduleDownloaded",
|
|
427
|
+
componentId: component.componentId,
|
|
428
|
+
moduleId,
|
|
429
|
+
files,
|
|
430
|
+
version,
|
|
431
|
+
dataEventId: component.dataEventId,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* 揃ったモジュールをファイルに分ける。
|
|
437
|
+
*
|
|
438
|
+
* **多くは `multipart/mixed`** で、1つのモジュールに BML と画像がまとめて
|
|
439
|
+
* 入っている。Type 記述子が無いときも中身は multipart なので、同じ扱いにする。
|
|
440
|
+
*/
|
|
441
|
+
export function moduleFiles(data, contentType) {
|
|
442
|
+
const declared = contentType === null ? null : parseMediaType(contentType);
|
|
443
|
+
if (declared !== null &&
|
|
444
|
+
!(declared.type === "multipart" && declared.subtype === "mixed")) {
|
|
445
|
+
return [
|
|
446
|
+
{
|
|
447
|
+
contentLocation: null,
|
|
448
|
+
contentType: declared,
|
|
449
|
+
dataBase64: base64(data),
|
|
450
|
+
},
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
const entity = readEntity(data);
|
|
454
|
+
if (entity?.parts == null)
|
|
455
|
+
return [];
|
|
456
|
+
const files = [];
|
|
457
|
+
for (const part of entity.parts) {
|
|
458
|
+
const location = part.headers.find((header) => header.name === "content-location");
|
|
459
|
+
const type = part.headers.find((header) => header.name === "content-type");
|
|
460
|
+
// どちらも必ず入っている。欠けているものは置き場所も読み方も分からない
|
|
461
|
+
if (location === undefined || type === undefined)
|
|
462
|
+
continue;
|
|
463
|
+
const mediaType = parseMediaType(type.value);
|
|
464
|
+
if (mediaType === null)
|
|
465
|
+
continue;
|
|
466
|
+
files.push({
|
|
467
|
+
contentLocation: location.value,
|
|
468
|
+
contentType: mediaType,
|
|
469
|
+
dataBase64: base64(part.body),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return files;
|
|
473
|
+
}
|
|
474
|
+
function base64(data) {
|
|
475
|
+
return Buffer.from(data).toString("base64");
|
|
476
|
+
}
|
|
477
|
+
/** zlib で縮めてあれば伸ばす。伸ばせなければ諦めて null */
|
|
478
|
+
export function inflate(data, compression) {
|
|
479
|
+
if (compression === null)
|
|
480
|
+
return data;
|
|
481
|
+
if (compression.type !== COMPRESSION_ZLIB)
|
|
482
|
+
return null;
|
|
483
|
+
try {
|
|
484
|
+
return inflateSync(data);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* バイト列の細かい道具。**中身の意味は持たない。** 繋ぐ・読むのはここで済ませる (別名で書き直さない)
|
|
3
|
+
*/
|
|
4
|
+
/** バイト列を新しい1本に繋ぐ (いつも写す。`Blob` などへ渡せる `ArrayBuffer` の上に作る) */
|
|
5
|
+
export declare function concatBytes(parts: readonly Uint8Array[]): Uint8Array<ArrayBuffer>;
|
|
6
|
+
/**
|
|
7
|
+
* バイト列を1本に繋ぐ。
|
|
8
|
+
*
|
|
9
|
+
* **1本しかないときは写しません。** 分割された記述子を繋ぎ直すところ
|
|
10
|
+
* ([eit.ts](eit.ts)) では、繋ぐ必要のないほうが多数です
|
|
11
|
+
*/
|
|
12
|
+
export declare function joinBytes(parts: readonly Uint8Array[]): Uint8Array;
|
|
13
|
+
/** 大きい桁が先の 16 ビット */
|
|
14
|
+
export declare function u16(data: Uint8Array, at: number): number;
|
|
15
|
+
/** 大きい桁が先の 24 ビット */
|
|
16
|
+
export declare function u24(data: Uint8Array, at: number): number;
|
|
17
|
+
/** 大きい桁が先の 32 ビット (符号なし) */
|
|
18
|
+
export declare function u32(data: Uint8Array, at: number): number;
|
|
19
|
+
/** `length` バイトをそのまま1字ずつの文字列に (MP4 の箱の種別・PNG の塊の名前・言語の符号) */
|
|
20
|
+
export declare function ascii(data: Uint8Array, at: number, length: number): string;
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* バイト列の細かい道具。**中身の意味は持たない。** 繋ぐ・読むのはここで済ませる (別名で書き直さない)
|
|
3
|
+
*/
|
|
4
|
+
/** バイト列を新しい1本に繋ぐ (いつも写す。`Blob` などへ渡せる `ArrayBuffer` の上に作る) */
|
|
5
|
+
export function concatBytes(parts) {
|
|
6
|
+
let total = 0;
|
|
7
|
+
for (const part of parts)
|
|
8
|
+
total += part.length;
|
|
9
|
+
const out = new Uint8Array(total);
|
|
10
|
+
let at = 0;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
out.set(part, at);
|
|
13
|
+
at += part.length;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* バイト列を1本に繋ぐ。
|
|
19
|
+
*
|
|
20
|
+
* **1本しかないときは写しません。** 分割された記述子を繋ぎ直すところ
|
|
21
|
+
* ([eit.ts](eit.ts)) では、繋ぐ必要のないほうが多数です
|
|
22
|
+
*/
|
|
23
|
+
export function joinBytes(parts) {
|
|
24
|
+
return parts.length === 1 ? parts[0] : concatBytes(parts);
|
|
25
|
+
}
|
|
26
|
+
/** 大きい桁が先の 16 ビット */
|
|
27
|
+
export function u16(data, at) {
|
|
28
|
+
return (data[at] << 8) | data[at + 1];
|
|
29
|
+
}
|
|
30
|
+
/** 大きい桁が先の 24 ビット */
|
|
31
|
+
export function u24(data, at) {
|
|
32
|
+
return (data[at] << 16) | u16(data, at + 1);
|
|
33
|
+
}
|
|
34
|
+
/** 大きい桁が先の 32 ビット (符号なし) */
|
|
35
|
+
export function u32(data, at) {
|
|
36
|
+
return (((data[at] << 24) |
|
|
37
|
+
(data[at + 1] << 16) |
|
|
38
|
+
(data[at + 2] << 8) |
|
|
39
|
+
data[at + 3]) >>>
|
|
40
|
+
0);
|
|
41
|
+
}
|
|
42
|
+
/** `length` バイトをそのまま1字ずつの文字列に (MP4 の箱の種別・PNG の塊の名前・言語の符号) */
|
|
43
|
+
export function ascii(data, at, length) {
|
|
44
|
+
return String.fromCharCode(...data.subarray(at, at + length));
|
|
45
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **字幕の文字の配置** (第1版)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
|
|
3
|
+
* 決め、受け側 (ブラウザ・テレビのアプリ) はこれを**描くだけ**にする。形式の取り決めは
|
|
4
|
+
* [api.md](https://github.com/danything/denpa/blob/main/docs/api.md#字幕の文字の配置) に書いてある (ここと同じものを、Kotlin でも読む)。
|
|
5
|
+
*
|
|
6
|
+
* 座標はすべて**字幕の面の単位** (`plane`。ふつう 960x540)。受け側は映像の枠に
|
|
7
|
+
* 合わせて縦横それぞれ伸ばす。色は `#rrggbbaa`。
|
|
8
|
+
*/
|
|
9
|
+
/** 形式の版。**互換の無い変え方をしたら上げる** (受け側は知らない版を描かない) */
|
|
10
|
+
export declare const CAPTION_TEXT_VERSION = 1;
|
|
11
|
+
/** 縁取り (ORN) の太さ。面の単位で、字の輪郭の外側へ。libaribcaption の既定 (ffmpeg の `outline_width`) と同じ */
|
|
12
|
+
export declare const STROKE_WIDTH = 1.5;
|
|
13
|
+
/** 点滅 (FLC) の間 (ms)。出ている時間と消えている時間 */
|
|
14
|
+
export declare const FLASH_MS = 500;
|
|
15
|
+
/** 字幕1枚。**前の1枚を丸ごと置き換える** (`runs` が空なら消す) */
|
|
16
|
+
export interface CaptionPage {
|
|
17
|
+
v: typeof CAPTION_TEXT_VERSION;
|
|
18
|
+
/** 字幕の面の大きさ [幅, 高さ] */
|
|
19
|
+
plane: [number, number];
|
|
20
|
+
/** 出しておく長さ (ms)。null なら次の1枚まで */
|
|
21
|
+
duration: number | null;
|
|
22
|
+
/** 字の並び。描く順 */
|
|
23
|
+
runs: CaptionRun[];
|
|
24
|
+
/** 置き換えられなかった外字の画像。`CaptionRun.drcs` で引く */
|
|
25
|
+
drcs?: Record<string, CaptionDrcs>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 同じ見た目で横に並ぶ字。`text` の1字 (コードポイント) ごとに、幅 `w` の区画を
|
|
29
|
+
* `x` から右へ1つずつ使う。
|
|
30
|
+
*/
|
|
31
|
+
export interface CaptionRun {
|
|
32
|
+
/** 1字目の区画の左上 */
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
/** 区画1つの幅・高さ。背景 (`bg`) はこの広さを塗る */
|
|
36
|
+
w: number;
|
|
37
|
+
h: number;
|
|
38
|
+
/** 区画の左上から、字の枠の左上まで */
|
|
39
|
+
fx: number;
|
|
40
|
+
fy: number;
|
|
41
|
+
/** 字の大きさ (em)。字の枠の高さでもある */
|
|
42
|
+
size: number;
|
|
43
|
+
/**
|
|
44
|
+
* 横の縮め方。字の枠の幅は `size * scaleX`。中くらい (MSZ) の全角は 0.5 で縮めて描き、
|
|
45
|
+
* 半角の字 (英数・半角カナ) は 1 のまま (もともと半分の幅なので)
|
|
46
|
+
*/
|
|
47
|
+
scaleX: number;
|
|
48
|
+
text: string;
|
|
49
|
+
/** 置き換えられなかった外字。`text` は「〓」で、この画像を字の枠いっぱいに描く */
|
|
50
|
+
drcs?: string;
|
|
51
|
+
/** 字の色 */
|
|
52
|
+
fg: string;
|
|
53
|
+
/** 背景の色 (区画) */
|
|
54
|
+
bg: string;
|
|
55
|
+
/** 縁取りの色 (ORN)。無ければ縁取らない */
|
|
56
|
+
stroke?: string;
|
|
57
|
+
underline?: true;
|
|
58
|
+
bold?: true;
|
|
59
|
+
italic?: true;
|
|
60
|
+
/** 囲み (HLC)。1 下・2 右・4 上・8 左の和。線は字の色で、太さは面の 1 単位 */
|
|
61
|
+
box?: number;
|
|
62
|
+
/** 点滅 (FLC) */
|
|
63
|
+
flash?: true;
|
|
64
|
+
/** ルビらしい小さな字 (libaribcaption と同じ見分け方) */
|
|
65
|
+
ruby?: true;
|
|
66
|
+
}
|
|
67
|
+
/** 外字の画像。`data` は base64 で、左上から1画素 `bits` ビットずつ (上の桁から) 詰めたもの */
|
|
68
|
+
export interface CaptionDrcs {
|
|
69
|
+
w: number;
|
|
70
|
+
h: number;
|
|
71
|
+
/** 階調の数 (2 なら白黒)。値 v の濃さは v / (depth - 1) */
|
|
72
|
+
depth: number;
|
|
73
|
+
bits: number;
|
|
74
|
+
data: string;
|
|
75
|
+
/**
|
|
76
|
+
* 放送の元の画像の MD5。外字の置き換え表 (`b24-tables.ts` の `DRCS_REPLACE`) に足すときに使う。
|
|
77
|
+
* `data` は送るためになめらかにした画像なので、その MD5 は表と合わない。描くのには使わない
|
|
78
|
+
*/
|
|
79
|
+
md5?: string;
|
|
80
|
+
}
|
|
81
|
+
/** 空白として字を描かない (背景だけ塗る) 字。libaribcaption の IsSpaceCharacter と同じ */
|
|
82
|
+
export declare function isSpace(code: number): boolean;
|
|
83
|
+
/** 録画1本ぶんの字幕 (`GET /api/recordings/<id>/captions.json`)。`at` は動画の頭からの秒 */
|
|
84
|
+
export interface CaptionPages {
|
|
85
|
+
v: typeof CAPTION_TEXT_VERSION;
|
|
86
|
+
/** 第1言語 */
|
|
87
|
+
pages: {
|
|
88
|
+
at: number;
|
|
89
|
+
page: CaptionPage;
|
|
90
|
+
}[];
|
|
91
|
+
/** 第1言語の名前。字幕管理データが読めなければ省く */
|
|
92
|
+
language?: CaptionLanguage;
|
|
93
|
+
/** 第2言語から後ろ。**載っているときだけ** (たいていの番組は第1言語だけ) */
|
|
94
|
+
others?: CaptionLanguagePages[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 字幕の言語1つ。**ARIB の字幕は1本の中に最大8言語を持てる** (データグループの番号 1〜8。
|
|
98
|
+
* 何番がどの言語かは字幕管理データに ISO 639 の3文字で書いてある)。日本の放送はほぼ日本語だけで、
|
|
99
|
+
* 英語の字幕が付く番組がたまにある
|
|
100
|
+
*/
|
|
101
|
+
export interface CaptionLanguage {
|
|
102
|
+
/** 第何言語か (1〜8。データグループの番号) */
|
|
103
|
+
index: number;
|
|
104
|
+
/** ISO 639-2 の3文字 (`jpn` / `eng`)。読めなければ空 */
|
|
105
|
+
code: string;
|
|
106
|
+
/** 読める名前 (日本語・英語)。知らない符号はそのまま */
|
|
107
|
+
label: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 録画の字幕のうち、第2言語から後ろ (`CaptionPages.others`)。**第1言語は今までどおり `pages`** —
|
|
111
|
+
* 版 (`v`) を上げずに足したので、知らない受け側は第1言語だけを描く
|
|
112
|
+
*/
|
|
113
|
+
export interface CaptionLanguagePages extends CaptionLanguage {
|
|
114
|
+
pages: {
|
|
115
|
+
at: number;
|
|
116
|
+
page: CaptionPage;
|
|
117
|
+
}[];
|
|
118
|
+
}
|
|
119
|
+
/** 第何言語かと ISO 639 の符号から、画面に出す名前を付ける */
|
|
120
|
+
export declare function captionLanguage(index: number, code: string): CaptionLanguage;
|
|
121
|
+
/** `?language=` を読む (1〜8)。省いた・読めないときは第1言語 */
|
|
122
|
+
export declare function parseCaptionLanguage(value: string | null): number;
|