@danything/arib 0.1.0 → 1.0.1

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.
@@ -14,12 +14,16 @@
14
14
  * 取っていて常に 0 になり、最初の1回しか読まない。ここは群を正しく見る
15
15
  * (番組が変わって群が替わったときに書式を読み直す)
16
16
  * - **点滅 (FLC) を読み取る。** あちらは読み捨てている
17
+ * - **JIS X 0213 を最後まで読む。** JIS互換漢字2面 (0x3A) を2面の表で、1面 (0x39) の 85〜94区を
18
+ * 第3水準の漢字で引く。か゚ のような2つのコードポイントの字も出す (あちらは空き)
19
+ * - **表で空いている点 (U+FFFD) はゲタ (〓) にする。** 「�」は Denpa Font に無く、端末の字の
20
+ * 「?」の印で出る。定義されていない外字と同じ扱い
17
21
  *
18
22
  * 入力は字幕の PES の中身 (データ識別 0x80 から)。ffmpeg が `-c:s copy` で Matroska
19
23
  * (`S_ARIBSUB`) に入れたコマの中身がそのままこれになる。
20
24
  */
21
25
  import { createHash } from "node:crypto";
22
- import { ADDITIONAL, ALNUM_FULL, ALNUM_HALF, CLUT, DRCS_REPLACE, HIRAGANA, JISX0201, JISX0201_HALF, KANA_SYMBOLS_HALF, KANJI_DIFF, KANJI_SYMBOLS_HALF, KATAKANA, } from "./b24-tables.js";
26
+ import { ADDITIONAL, ALNUM_FULL, ALNUM_HALF, CLUT, DRCS_REPLACE, HIRAGANA, JISX0201, JISX0201_HALF, KANA_SYMBOLS_HALF, KANJI_DIFF, KANJI_SEQUENCES, KANJI_SYMBOLS_HALF, KATAKANA, PLANE1_HIGH, PLANE2, } from "./b24-tables.js";
23
27
  import { ascii, u16, u24 } from "./bytes.js";
24
28
  import { CAPTION_TEXT_VERSION, captionLanguage, } from "./caption-text.js";
25
29
  import { smoothDrcs } from "./drcs-smooth.js";
@@ -29,7 +33,13 @@ const HIRA = { set: "hiragana", bytes: 1 };
29
33
  const KATA = { set: "katakana", bytes: 1 };
30
34
  const MACRO = { set: "macro", bytes: 1 };
31
35
  const MOSAIC = { set: "mosaic", bytes: 1 };
32
- /** 終端バイト → 集合 (G の指示)。プロポーショナルも JIS X 0213 も同じ表で引く (libaribcaption と同じ) */
36
+ /**
37
+ * 終端バイト → 集合 (G の指示)。プロポーショナルは同じ表で引く (libaribcaption と同じ)。
38
+ *
39
+ * JIS X 0213 の1面 (0x39) は 1〜84区を漢字集合 (0x42) と同じ表で引き (漢字集合でも JIS X 0213 にだけある字を出す)、
40
+ * 85〜94区は JIS X 0213 の漢字 (`PLANE1_HIGH`)。漢字集合と追加記号 (0x3B) の 85区から先は追加記号。
41
+ * 2面 (0x3A) は別の表 (`PLANE2`)。libaribcaption は 1面・2面とも漢字集合の表で引いていた
42
+ */
33
43
  const G_SETS = new Map([
34
44
  [0x42, KANJI],
35
45
  [0x4a, ALNUM],
@@ -43,8 +53,8 @@ const G_SETS = new Map([
43
53
  [0x37, HIRA],
44
54
  [0x38, KATA],
45
55
  [0x49, { set: "jisx0201", bytes: 1 }],
46
- [0x39, KANJI],
47
- [0x3a, KANJI],
56
+ [0x39, { set: "plane1", bytes: 2 }],
57
+ [0x3a, { set: "plane2", bytes: 2 }],
48
58
  [0x3b, { set: "additional", bytes: 2 }],
49
59
  ]);
50
60
  /** 終端バイト → DRCS の面 (0x40 は2バイトの面 0)。0x70 はマクロ */
@@ -81,7 +91,7 @@ const MACROS = [
81
91
  return Uint8Array.from([...bytes, ...MACRO_TAIL]);
82
92
  });
83
93
  const eucjp = new TextDecoder("euc-jp");
84
- /** 漢字の表 (区×94+点 → 字)。EUC-JP で読み、libaribcaption と違うところだけ差し替える */
94
+ /** 漢字の表 (区×94+点 → 字)。EUC-JP で読み、違うところ (`KANJI_DIFF`) だけ差し替える */
85
95
  const kanjiOverride = new Map();
86
96
  for (const [start, chars] of KANJI_DIFF) {
87
97
  let i = start;
@@ -110,7 +120,12 @@ const T = {
110
120
  alnumHalf: points(ALNUM_HALF),
111
121
  kanjiHalf: points(KANJI_SYMBOLS_HALF),
112
122
  additional: points(ADDITIONAL),
123
+ plane1High: points(PLANE1_HIGH),
124
+ /** 2面の区 (0 から) → 94 字 */
125
+ plane2: new Map(PLANE2.map(([ku, row]) => [ku - 1, points(row)])),
113
126
  };
127
+ /** 1区画に2つのコードポイントを描く字 (区×94+点 → 字)。JIS X 0213 の か゚ など */
128
+ const sequences = new Map(KANJI_SEQUENCES);
114
129
  /** 色。CLUT の組 (0〜7) と番号 (0〜15) */
115
130
  function clut(palette, index) {
116
131
  return `#${CLUT[palette * 16 + index] ?? CLUT[index]}`;
@@ -151,7 +166,7 @@ function sendable(drcs) {
151
166
  * ストリームごとに1つ作って、届いた順に食わせる。
152
167
  *
153
168
  * **読むのは1つの言語だけ** (`language`。1〜8)。言語ごとに書式も位置も別に続くので、2つの言語を出し分けるなら
154
- * 言語ごとに1つ作って、同じコマを両方に食わせる (`server/captions.ts` の `CaptionText`)
169
+ * 言語ごとに1つ作って、同じコマを両方に食わせる (denpa の `server/captions.ts` の `CaptionText`)
155
170
  */
156
171
  export class B24CaptionDecoder {
157
172
  language;
@@ -764,13 +779,15 @@ export class B24CaptionDecoder {
764
779
  this.push((this.msz() ? T.jisx0201Half : T.jisx0201)[index]);
765
780
  this.moveRelative(1, 0);
766
781
  }
767
- else if (set === "kanji" || set === "additional") {
782
+ else if (set === "kanji" || set === "plane1" || set === "additional") {
768
783
  const ku = ch - 0x21;
769
784
  const ten = ch2 - 0x21;
770
785
  let code;
786
+ let joined = null;
771
787
  if (ku < 84) {
772
788
  const i = ku * 94 + ten;
773
- code = kanji(i);
789
+ joined = sequences.get(i) ?? null;
790
+ code = joined?.codePointAt(0) ?? kanji(i);
774
791
  if (this.msz()) {
775
792
  if (ku < 2)
776
793
  code = T.kanjiHalf[i];
@@ -778,10 +795,17 @@ export class B24CaptionDecoder {
778
795
  code = (code & 0xff) + 0x20;
779
796
  }
780
797
  }
798
+ else if (set === "plane1") {
799
+ code = T.plane1High[(ku - 84) * 94 + ten] ?? 0xfffd;
800
+ }
781
801
  else {
782
802
  code = T.additional[(ku - 84) * 94 + ten] ?? 0xfffd;
783
803
  }
784
- this.push(code);
804
+ this.push(code, null, joined);
805
+ this.moveRelative(1, 0);
806
+ }
807
+ else if (set === "plane2") {
808
+ this.push(T.plane2.get(ch - 0x21)?.[ch2 - 0x21] ?? 0xfffd);
785
809
  this.moveRelative(1, 0);
786
810
  }
787
811
  else if (set === "alnum") {
@@ -812,9 +836,11 @@ export class B24CaptionDecoder {
812
836
  // モザイクは描かない (libaribcaption と同じ)
813
837
  return entry.bytes;
814
838
  }
815
- push(code, drcs = null) {
839
+ push(code, drcs = null, joined = null) {
816
840
  this.caption.chars.push({
817
- code,
841
+ // 表で空いている点 (規格の空き・JIS X 0213 にも無い区点) もゲタで埋める
842
+ code: code === 0xfffd ? 0x3013 : code,
843
+ joined,
818
844
  drcs,
819
845
  x: this.posX,
820
846
  y: this.posY - this.sectionHeight(),
@@ -920,10 +946,12 @@ function toPage(caption, plane) {
920
946
  fy: (char.vSpacing * char.vScale) / 2,
921
947
  size,
922
948
  scaleX,
923
- text: String.fromCodePoint(char.code),
949
+ text: char.joined ?? String.fromCodePoint(char.code),
924
950
  fg: char.fg,
925
951
  bg: char.bg,
926
952
  };
953
+ if (char.joined !== null)
954
+ run.joined = true;
927
955
  if (char.drcs !== null) {
928
956
  const key = String(char.drcs);
929
957
  run.drcs = key;
@@ -946,11 +974,12 @@ function toPage(caption, plane) {
946
974
  run.flash = true;
947
975
  if (char.ruby)
948
976
  run.ruby = true;
949
- // 見た目が同じで、すぐ右に続いていれば束ねる。外字の画像は1字ずつ
977
+ // 見た目が同じで、すぐ右に続いていれば束ねる。外字の画像と、2つのコードポイントの字は1字ずつ
950
978
  const { x: _x, y: _y, text: _t, ...looks } = run;
951
979
  const key = JSON.stringify(looks);
952
980
  if (last !== null &&
953
981
  run.drcs === undefined &&
982
+ run.joined === undefined &&
954
983
  last.key === key &&
955
984
  last.run.y === run.y &&
956
985
  last.run.x + last.count * last.run.w === run.x) {
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * データ放送の通知の形式。**デコードする側 ([bml.ts](bml.ts)) と描く側の取り決め。**
3
3
  *
4
- * もとは web-bml の `protocol` (`ws_api.ts`) を借りていた。形式はそのまま写してある —
4
+ * もとは web-bml の `server/ws_api.ts` の型を借りていた。形式はそのまま写してある —
5
5
  * 録画の隣に置いた `.bml.jsonl` はこの形式の JSON を1行ずつ持っているので、
6
6
  * **形式を変えると前に録ったものが読めなくなる**。足すのはよいが、名前や意味は変えない。
7
+ * web-bml は MIT で、著作権表示と許諾の文は [LICENSE](../LICENSE) に置いてある。
7
8
  *
8
9
  * 載せるのは denpa が実際に作るものだけ (PMT・モジュール一覧・揃ったモジュール・
9
10
  * ストリーム記述子のイベント・番組の名乗り・放送の時刻)。web-bml が自前の
@@ -122,7 +123,7 @@ export interface ESEventUpdatedMessage {
122
123
  events: ESEvent[];
123
124
  dataEventId: number;
124
125
  }
125
- /** 番組の名乗り。TS からではなく DB から組む (`server/live.ts`・`server/recorded-bml.ts`) */
126
+ /** 番組の名乗り。TS からではなく DB から組む (denpa の `server/live.ts`・`server/recorded-bml.ts`) */
126
127
  export interface ProgramInfoMessage {
127
128
  type: "programInfo";
128
129
  originalNetworkId: number | null;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * データ放送の通知の形式。**デコードする側 ([bml.ts](bml.ts)) と描く側の取り決め。**
3
3
  *
4
- * もとは web-bml の `protocol` (`ws_api.ts`) を借りていた。形式はそのまま写してある —
4
+ * もとは web-bml の `server/ws_api.ts` の型を借りていた。形式はそのまま写してある —
5
5
  * 録画の隣に置いた `.bml.jsonl` はこの形式の JSON を1行ずつ持っているので、
6
6
  * **形式を変えると前に録ったものが読めなくなる**。足すのはよいが、名前や意味は変えない。
7
+ * web-bml は MIT で、著作権表示と許諾の文は [LICENSE](../LICENSE) に置いてある。
7
8
  *
8
9
  * 載せるのは denpa が実際に作るものだけ (PMT・モジュール一覧・揃ったモジュール・
9
10
  * ストリーム記述子のイベント・番組の名乗り・放送の時刻)。web-bml が自前の
package/dist/bml.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * ([dsmcc.ts](dsmcc.ts) / [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts))。
12
12
  * [web-bml](https://github.com/otya128/web-bml) の `decode_ts.ts` は使わない — 要るのは
13
13
  * その一部だけで、残り (EIT/SDT/NIT・字幕の PES) は自前で読んでおり
14
- * ([eit.ts](eit.ts) / [psi.ts](psi.ts) / ffmpeg)、`@chinachu/aribts` も抱えずに済む。
14
+ * ([eit.ts](eit.ts) / [psi.ts](psi.ts) / [b24caption.ts](b24caption.ts))、`@chinachu/aribts` も抱えずに済む。
15
15
  *
16
16
  * **出す形式は web-bml の頃のまま** ([bml-protocol.ts](bml-protocol.ts))。録画の隣の
17
17
  * `.bml.jsonl` がこの形式で残っているので、変えると前に録ったものが読めなくなる。
@@ -47,7 +47,7 @@ export declare class BmlDecoder {
47
47
  feed(chunk: Uint8Array): void;
48
48
  /**
49
49
  * 切り分け済みのパケットを1つ。ライブは同じ TS を時計や Hybridcast とも
50
- * 分け合うので、切り分けは1回で済ませて配る (`server/live.ts` の `tap`)。
50
+ * 分け合うので、切り分けは1回で済ませて配る (denpa の `server/live.ts` の `tap`)。
51
51
  *
52
52
  * **PID で行き先を引く。** 組み立て役を全部に回していた頃は、データ放送と
53
53
  * 関係ない映像のパケットのたびにカルーセルの数だけ呼んでいた。
@@ -66,8 +66,8 @@ export declare class BmlDecoder {
66
66
  /**
67
67
  * **来ないモジュールを諦める** (`GIVE_UP_ROUNDS`)。待っている受け側 (`Resources` の `wait`) は、
68
68
  * DII に無いモジュールを「もう来ない」と決める (`gone`) ので、諦めたものを除いた一覧を配り直して
69
- * 待ちを終わらせる。前の DII で配り終えた版のままのもの (受け側が持っている) は一覧から外さず、
70
- * 組み立てるのをやめるだけ。次に DII の版が変われば、また待つ
69
+ * 待ちを終わらせる。前の DII で配り終えたバージョンのままのもの (受け側が持っている) は一覧から外さず、
70
+ * 組み立てるのをやめるだけ。次に DII のバージョンが変われば、また待つ
71
71
  */
72
72
  private giveUp;
73
73
  private onDdb;
package/dist/bml.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * ([dsmcc.ts](dsmcc.ts) / [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts))。
12
12
  * [web-bml](https://github.com/otya128/web-bml) の `decode_ts.ts` は使わない — 要るのは
13
13
  * その一部だけで、残り (EIT/SDT/NIT・字幕の PES) は自前で読んでおり
14
- * ([eit.ts](eit.ts) / [psi.ts](psi.ts) / ffmpeg)、`@chinachu/aribts` も抱えずに済む。
14
+ * ([eit.ts](eit.ts) / [psi.ts](psi.ts) / [b24caption.ts](b24caption.ts))、`@chinachu/aribts` も抱えずに済む。
15
15
  *
16
16
  * **出す形式は web-bml の頃のまま** ([bml-protocol.ts](bml-protocol.ts))。録画の隣の
17
17
  * `.bml.jsonl` がこの形式で残っているので、変えると前に録ったものが読めなくなる。
@@ -20,7 +20,7 @@
20
20
  import { Buffer } from "node:buffer";
21
21
  import { inflateSync } from "node:zlib";
22
22
  import { u16, u32 } from "./bytes.js";
23
- import { dataEventIdOf, ModuleBuilder, parseDdb, parseDii, TABLE_DII, TABLE_STREAM_DESCRIPTOR, } from "./dsmcc.js";
23
+ import { dataEventIdOf, MAX_MODULE_SIZE, ModuleBuilder, parseDdb, parseDii, TABLE_DII, TABLE_STREAM_DESCRIPTOR, } from "./dsmcc.js";
24
24
  import { parseTimeTable, PID_TIME } from "./eit.js";
25
25
  import { parseMediaType, readEntity } from "./multipart.js";
26
26
  import { descriptors, packetPid, PacketStream, pmtStreams, PmtTracker, SectionAssembler, } from "./psi.js";
@@ -187,7 +187,13 @@ function parseEsEvents(section) {
187
187
  * 短く見積もる) ため
188
188
  */
189
189
  const GIVE_UP_ROUNDS = 4;
190
- /** `Component.done` に置く印。モジュールの版とカルーセルの回 (dataEventId) の組 */
190
+ /**
191
+ * 1つの `BmlDecoder` で同時に組み立てるモジュールの大きさの合計の上限 (64MiB)。
192
+ * 1つずつは `MAX_MODULE_SIZE` に収まっても、たくさん並べた DII や ES でメモリを使い切らせられる。
193
+ * 実際のカルーセルは全部で数 MB に収まる
194
+ */
195
+ const MAX_CAROUSEL_SIZE = 64 * 1024 * 1024;
196
+ /** `Component.done` に置く印。モジュールのバージョンとカルーセルの回 (dataEventId) の組 */
191
197
  function doneMark(component, version) {
192
198
  return `${version}:${component.dataEventId}`;
193
199
  }
@@ -218,7 +224,7 @@ export class BmlDecoder {
218
224
  }
219
225
  /**
220
226
  * 切り分け済みのパケットを1つ。ライブは同じ TS を時計や Hybridcast とも
221
- * 分け合うので、切り分けは1回で済ませて配る (`server/live.ts` の `tap`)。
227
+ * 分け合うので、切り分けは1回で済ませて配る (denpa の `server/live.ts` の `tap`)。
222
228
  *
223
229
  * **PID で行き先を引く。** 組み立て役を全部に回していた頃は、データ放送と
224
230
  * 関係ない映像のパケットのたびにカルーセルの数だけ呼んでいた。
@@ -320,7 +326,21 @@ export class BmlDecoder {
320
326
  component.abandoned.clear();
321
327
  component.dii = dii;
322
328
  component.size = 0;
329
+ // ほかの ES で組み立て中のものと合わせて `MAX_CAROUSEL_SIZE` に収める
330
+ let budget = MAX_CAROUSEL_SIZE;
331
+ for (const other of this.components.values())
332
+ if (other !== component)
333
+ budget -= other.size;
323
334
  for (const module of dii.modules) {
335
+ /*
336
+ * **大きすぎるモジュールは組み立てない。** 来ないものとして一覧から外す
337
+ * (受け側が待ち続けないように)
338
+ */
339
+ if (module.moduleSize > MAX_MODULE_SIZE ||
340
+ component.size + module.moduleSize > budget) {
341
+ component.abandoned.add(module.moduleId);
342
+ continue;
343
+ }
324
344
  component.size += module.moduleSize;
325
345
  component.building.set(module.moduleId, {
326
346
  module,
@@ -360,8 +380,8 @@ export class BmlDecoder {
360
380
  /**
361
381
  * **来ないモジュールを諦める** (`GIVE_UP_ROUNDS`)。待っている受け側 (`Resources` の `wait`) は、
362
382
  * DII に無いモジュールを「もう来ない」と決める (`gone`) ので、諦めたものを除いた一覧を配り直して
363
- * 待ちを終わらせる。前の DII で配り終えた版のままのもの (受け側が持っている) は一覧から外さず、
364
- * 組み立てるのをやめるだけ。次に DII の版が変われば、また待つ
383
+ * 待ちを終わらせる。前の DII で配り終えたバージョンのままのもの (受け側が持っている) は一覧から外さず、
384
+ * 組み立てるのをやめるだけ。次に DII のバージョンが変われば、また待つ
365
385
  */
366
386
  giveUp(component) {
367
387
  const limit = component.size * GIVE_UP_ROUNDS;
@@ -386,7 +406,7 @@ export class BmlDecoder {
386
406
  for (const waiting of component.building.values())
387
407
  waiting.quietBytes += ddb.block.length;
388
408
  const held = component.building.get(ddb.moduleId);
389
- // 版が食い違うブロックを混ぜると壊れたファイルができる
409
+ // バージョンが食い違うブロックを混ぜると壊れたファイルができる
390
410
  if (held === undefined || held.builder.moduleVersion !== ddb.moduleVersion)
391
411
  return;
392
412
  const before = held.builder.blocks;
@@ -403,7 +423,7 @@ export class BmlDecoder {
403
423
  // 次の DII が来るまで組み立て直さない
404
424
  component.building.delete(moduleId);
405
425
  /*
406
- * **同じものを配り直さない。** カルーセルは回り続けるので、版が上がって
426
+ * **同じものを配り直さない。** カルーセルは回り続けるので、バージョンが上がって
407
427
  * いなくても同じモジュールが何度も揃う。そのたびに配ると、受け側は
408
428
  * 中身の変わらない画面を組み直し続ける
409
429
  */
@@ -481,7 +501,8 @@ export function inflate(data, compression) {
481
501
  if (compression.type !== COMPRESSION_ZLIB)
482
502
  return null;
483
503
  try {
484
- return inflateSync(data);
504
+ // 伸ばした大きさにも上限を置く。小さな DDB から何 GB にも伸びるものを作れる
505
+ return inflateSync(data, { maxOutputLength: MAX_MODULE_SIZE });
485
506
  }
486
507
  catch {
487
508
  return null;
@@ -1,12 +1,12 @@
1
1
  /**
2
- * **字幕の文字の配置** (第1版)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
2
+ * **字幕の文字の配置** (バージョン 1)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
3
3
  * 決め、受け側 (ブラウザ・テレビのアプリ) はこれを**描くだけ**にする。形式の取り決めは
4
4
  * [api.md](https://github.com/danything/denpa/blob/main/docs/api.md#字幕の文字の配置) に書いてある (ここと同じものを、Kotlin でも読む)。
5
5
  *
6
6
  * 座標はすべて**字幕の面の単位** (`plane`。ふつう 960x540)。受け側は映像の枠に
7
7
  * 合わせて縦横それぞれ伸ばす。色は `#rrggbbaa`。
8
8
  */
9
- /** 形式の版。**互換の無い変え方をしたら上げる** (受け側は知らない版を描かない) */
9
+ /** 形式のバージョン。**互換の無い変え方をしたら上げる** (受け側は知らないバージョンを描かない) */
10
10
  export declare const CAPTION_TEXT_VERSION = 1;
11
11
  /** 縁取り (ORN) の太さ。面の単位で、字の輪郭の外側へ。libaribcaption の既定 (ffmpeg の `outline_width`) と同じ */
12
12
  export declare const STROKE_WIDTH = 1.5;
@@ -46,6 +46,11 @@ export interface CaptionRun {
46
46
  */
47
47
  scaleX: number;
48
48
  text: string;
49
+ /**
50
+ * `text` 全体で1つの区画 (JIS X 0213 の か゚・æ̀・˩˥ のように、2つのコードポイントで1字のもの)。
51
+ * この run は1字だけで、`text` をまとめて描く (フォントが1字に組む)
52
+ */
53
+ joined?: true;
49
54
  /** 置き換えられなかった外字。`text` は「〓」で、この画像を字の枠いっぱいに描く */
50
55
  drcs?: string;
51
56
  /** 字の色 */
@@ -108,7 +113,7 @@ export interface CaptionLanguage {
108
113
  }
109
114
  /**
110
115
  * 録画の字幕のうち、第2言語から後ろ (`CaptionPages.others`)。**第1言語は今までどおり `pages`** —
111
- * 版 (`v`) を上げずに足したので、知らない受け側は第1言語だけを描く
116
+ * バージョン (`v`) を上げずに足したので、知らない受け側は第1言語だけを描く
112
117
  */
113
118
  export interface CaptionLanguagePages extends CaptionLanguage {
114
119
  pages: {
@@ -1,13 +1,13 @@
1
1
  import { LANGUAGE } from "./language.js";
2
2
  /**
3
- * **字幕の文字の配置** (第1版)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
3
+ * **字幕の文字の配置** (バージョン 1)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
4
4
  * 決め、受け側 (ブラウザ・テレビのアプリ) はこれを**描くだけ**にする。形式の取り決めは
5
5
  * [api.md](https://github.com/danything/denpa/blob/main/docs/api.md#字幕の文字の配置) に書いてある (ここと同じものを、Kotlin でも読む)。
6
6
  *
7
7
  * 座標はすべて**字幕の面の単位** (`plane`。ふつう 960x540)。受け側は映像の枠に
8
8
  * 合わせて縦横それぞれ伸ばす。色は `#rrggbbaa`。
9
9
  */
10
- /** 形式の版。**互換の無い変え方をしたら上げる** (受け側は知らない版を描かない) */
10
+ /** 形式のバージョン。**互換の無い変え方をしたら上げる** (受け側は知らないバージョンを描かない) */
11
11
  export const CAPTION_TEXT_VERSION = 1;
12
12
  /** 縁取り (ORN) の太さ。面の単位で、字の輪郭の外側へ。libaribcaption の既定 (ffmpeg の `outline_width`) と同じ */
13
13
  export const STROKE_WIDTH = 1.5;
@@ -29,7 +29,7 @@ export function captionLanguage(index, code) {
29
29
  return {
30
30
  index,
31
31
  code,
32
- label: LANGUAGE[code] ?? (code === "" ? `字幕${index}` : code),
32
+ label: LANGUAGE[code] ?? (code === "" ? `字幕 ${index}` : code),
33
33
  };
34
34
  }
35
35
  /** `?language=` を読む (1〜8)。省いた・読めないときは第1言語 */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
3
3
  *
4
- * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒の画像で送ってくる。受け側はそれを字の枠いっぱい
4
+ * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒か 4 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
5
5
  * (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
6
6
  * なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
3
3
  *
4
- * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒の画像で送ってくる。受け側はそれを字の枠いっぱい
4
+ * 放送は置き換え表で字にできない外字を、16〜36 画素の白黒か 4 階調の画像で送ってくる。受け側はそれを字の枠いっぱい
5
5
  * (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
6
6
  * なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
7
7
  *
@@ -18,6 +18,7 @@
18
18
  *
19
19
  * **置き換え表の MD5 は元の画像から取る** (b24caption.ts)。ここで作った画像は送るためだけのもの。
20
20
  */
21
+ import { Buffer } from "node:buffer";
21
22
  /** 送る画像の一辺。これに届くまで倍にする (全画面の字の枠と同じくらい) */
22
23
  const TARGET = 64;
23
24
  /** 送る画像の一辺の上限。受け側 (denpa-tv) もこれより大きい画像は読まない */
package/dist/dsmcc.d.ts CHANGED
@@ -43,7 +43,7 @@ export interface DiiModule {
43
43
  }
44
44
  export interface Dii {
45
45
  downloadId: number;
46
- /** カルーセルの版。**変わっていなければ組み立て直さない** */
46
+ /** カルーセルのバージョン。**変わっていなければ組み立て直さない** */
47
47
  transactionId: number;
48
48
  blockSize: number;
49
49
  modules: DiiModule[];
@@ -70,23 +70,41 @@ export declare function parseDii(section: Uint8Array): Dii | null;
70
70
  /** DDB (Download Data Block)。モジュールを割ったブロックが1つ入っている */
71
71
  export declare function parseDdb(section: Uint8Array): Ddb | null;
72
72
  /**
73
- * 組み立て中のモジュール。**DII が言った大きさぶんの入れ物を先に用意して**、
74
- * DDB が来るたびに然るべき位置へ置く。
73
+ * 組み立てるモジュールの大きさの上限 (16MiB)。**これより大きいと言う DII は信じない。**
74
+ *
75
+ * `moduleSize` は32ビットで、そのまま信じると DII 1つで 4GB を確保させられる。
76
+ * 実際のデータ放送のモジュールは数 KB〜数百 KB、衛星のロゴも全局ぶんで数百 KB なので、
77
+ * 十分に余裕を見た値。zlib で伸ばしたあとの大きさにも同じ上限を使う ([bml.ts](bml.ts) の `inflate`)
78
+ */
79
+ export declare const MAX_MODULE_SIZE: number;
80
+ /**
81
+ * 組み立て中のモジュール。DDB が来るたびに然るべき位置へ置く。
82
+ *
83
+ * **入れ物は届いたところまでしか確保しない。** DII の言う大きさを先に確保すると、
84
+ * 大きいと言うだけの DII でメモリを使い切らせられる。`MAX_MODULE_SIZE` を超える
85
+ * モジュールはブロックを受け取らない (いつまでも揃わない)。
75
86
  *
76
87
  * 順番は保証されない (途中から見れば真ん中から始まる) ので、
77
88
  * **受け取ったブロック番号を数えて**揃ったかを見る。
78
89
  */
79
90
  export declare class ModuleBuilder {
80
91
  readonly moduleVersion: number;
92
+ private readonly moduleSize;
81
93
  private readonly blockSize;
82
- readonly data: Uint8Array;
83
- private readonly received;
94
+ private buffer;
95
+ /** 受け取ったブロック番号 (1ブロック1ビット)。最初のブロックが来てから確保する */
96
+ private received;
97
+ private count;
84
98
  private readonly total;
85
99
  constructor(moduleVersion: number, moduleSize: number, blockSize: number);
100
+ /** 組み立てた中身。まだ届いていないところは 0。**揃ってから読むこと** (読むとモジュールの大きさぶんを確保する) */
101
+ get data(): Uint8Array;
86
102
  /** 受け取ったブロックの数 (同じブロックを2度受けても1つ) */
87
103
  get blocks(): number;
88
104
  /** 揃ったか。**大きさ 0 のモジュールは初めから揃っている** (運ぶブロックが無いので DDB を待たない) */
89
105
  get complete(): boolean;
90
106
  /** ブロックを1つ置く。**揃ったら true** */
91
107
  add(blockNumber: number, block: Uint8Array): boolean;
108
+ /** 入れ物を広げる。何度も写し直さないよう倍々で、モジュールの大きさまで */
109
+ private grow;
92
110
  }
package/dist/dsmcc.js CHANGED
@@ -153,39 +153,79 @@ export function parseDdb(section) {
153
153
  };
154
154
  }
155
155
  /**
156
- * 組み立て中のモジュール。**DII が言った大きさぶんの入れ物を先に用意して**、
157
- * DDB が来るたびに然るべき位置へ置く。
156
+ * 組み立てるモジュールの大きさの上限 (16MiB)。**これより大きいと言う DII は信じない。**
157
+ *
158
+ * `moduleSize` は32ビットで、そのまま信じると DII 1つで 4GB を確保させられる。
159
+ * 実際のデータ放送のモジュールは数 KB〜数百 KB、衛星のロゴも全局ぶんで数百 KB なので、
160
+ * 十分に余裕を見た値。zlib で伸ばしたあとの大きさにも同じ上限を使う ([bml.ts](bml.ts) の `inflate`)
161
+ */
162
+ export const MAX_MODULE_SIZE = 16 * 1024 * 1024;
163
+ /**
164
+ * 組み立て中のモジュール。DDB が来るたびに然るべき位置へ置く。
165
+ *
166
+ * **入れ物は届いたところまでしか確保しない。** DII の言う大きさを先に確保すると、
167
+ * 大きいと言うだけの DII でメモリを使い切らせられる。`MAX_MODULE_SIZE` を超える
168
+ * モジュールはブロックを受け取らない (いつまでも揃わない)。
158
169
  *
159
170
  * 順番は保証されない (途中から見れば真ん中から始まる) ので、
160
171
  * **受け取ったブロック番号を数えて**揃ったかを見る。
161
172
  */
162
173
  export class ModuleBuilder {
163
174
  moduleVersion;
175
+ moduleSize;
164
176
  blockSize;
165
- data;
166
- received = new Set();
177
+ buffer = new Uint8Array(0);
178
+ /** 受け取ったブロック番号 (1ブロック1ビット)。最初のブロックが来てから確保する */
179
+ received = null;
180
+ count = 0;
167
181
  total;
168
182
  constructor(moduleVersion, moduleSize, blockSize) {
169
183
  this.moduleVersion = moduleVersion;
184
+ this.moduleSize = moduleSize;
170
185
  this.blockSize = blockSize;
171
- this.data = new Uint8Array(moduleSize);
172
186
  this.total = Math.ceil(moduleSize / blockSize);
173
187
  }
188
+ /** 組み立てた中身。まだ届いていないところは 0。**揃ってから読むこと** (読むとモジュールの大きさぶんを確保する) */
189
+ get data() {
190
+ if (this.buffer.length < this.moduleSize &&
191
+ this.moduleSize <= MAX_MODULE_SIZE)
192
+ this.grow(this.moduleSize);
193
+ return this.buffer;
194
+ }
174
195
  /** 受け取ったブロックの数 (同じブロックを2度受けても1つ) */
175
196
  get blocks() {
176
- return this.received.size;
197
+ return this.count;
177
198
  }
178
199
  /** 揃ったか。**大きさ 0 のモジュールは初めから揃っている** (運ぶブロックが無いので DDB を待たない) */
179
200
  get complete() {
180
- return this.received.size >= this.total;
201
+ return this.count >= this.total;
181
202
  }
182
203
  /** ブロックを1つ置く。**揃ったら true** */
183
204
  add(blockNumber, block) {
205
+ if (this.moduleSize > MAX_MODULE_SIZE)
206
+ return false;
207
+ // blockSize を超えるブロックは受けない。重なったブロックで受け取る量が膨らむ
208
+ if (blockNumber >= this.total || block.length > this.blockSize)
209
+ return false;
184
210
  const at = this.blockSize * blockNumber;
185
- if (at + block.length > this.data.length)
211
+ if (at + block.length > this.moduleSize)
186
212
  return false;
187
- this.data.set(block, at);
188
- this.received.add(blockNumber);
213
+ if (at + block.length > this.buffer.length)
214
+ this.grow(at + block.length);
215
+ this.buffer.set(block, at);
216
+ this.received ??= new Uint8Array(Math.ceil(this.total / 8));
217
+ const index = blockNumber >> 3;
218
+ const bit = 1 << (blockNumber & 7);
219
+ if ((this.received[index] & bit) === 0) {
220
+ this.received[index] = this.received[index] | bit;
221
+ this.count++;
222
+ }
189
223
  return this.complete;
190
224
  }
225
+ /** 入れ物を広げる。何度も写し直さないよう倍々で、モジュールの大きさまで */
226
+ grow(length) {
227
+ const next = new Uint8Array(Math.min(this.moduleSize, Math.max(length, this.buffer.length * 2)));
228
+ next.set(this.buffer);
229
+ this.buffer = next;
230
+ }
191
231
  }