@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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LANGUAGE } from "./language.js";
|
|
2
|
+
/**
|
|
3
|
+
* **字幕の文字の配置** (第1版)。サーバが放送の字幕 (ARIB STD-B24) をデコードして置き場所まで
|
|
4
|
+
* 決め、受け側 (ブラウザ・テレビのアプリ) はこれを**描くだけ**にする。形式の取り決めは
|
|
5
|
+
* [api.md](https://github.com/danything/denpa/blob/main/docs/api.md#字幕の文字の配置) に書いてある (ここと同じものを、Kotlin でも読む)。
|
|
6
|
+
*
|
|
7
|
+
* 座標はすべて**字幕の面の単位** (`plane`。ふつう 960x540)。受け側は映像の枠に
|
|
8
|
+
* 合わせて縦横それぞれ伸ばす。色は `#rrggbbaa`。
|
|
9
|
+
*/
|
|
10
|
+
/** 形式の版。**互換の無い変え方をしたら上げる** (受け側は知らない版を描かない) */
|
|
11
|
+
export const CAPTION_TEXT_VERSION = 1;
|
|
12
|
+
/** 縁取り (ORN) の太さ。面の単位で、字の輪郭の外側へ。libaribcaption の既定 (ffmpeg の `outline_width`) と同じ */
|
|
13
|
+
export const STROKE_WIDTH = 1.5;
|
|
14
|
+
/** 点滅 (FLC) の間 (ms)。出ている時間と消えている時間 */
|
|
15
|
+
export const FLASH_MS = 500;
|
|
16
|
+
/** 空白として字を描かない (背景だけ塗る) 字。libaribcaption の IsSpaceCharacter と同じ */
|
|
17
|
+
export function isSpace(code) {
|
|
18
|
+
return (code === 0x09 ||
|
|
19
|
+
code === 0x20 ||
|
|
20
|
+
code === 0xa0 ||
|
|
21
|
+
code === 0x1680 ||
|
|
22
|
+
code === 0x3000 ||
|
|
23
|
+
code === 0x202f ||
|
|
24
|
+
code === 0x205f ||
|
|
25
|
+
(code >= 0x2000 && code <= 0x200a));
|
|
26
|
+
}
|
|
27
|
+
/** 第何言語かと ISO 639 の符号から、画面に出す名前を付ける */
|
|
28
|
+
export function captionLanguage(index, code) {
|
|
29
|
+
return {
|
|
30
|
+
index,
|
|
31
|
+
code,
|
|
32
|
+
label: LANGUAGE[code] ?? (code === "" ? `字幕${index}` : code),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** `?language=` を読む (1〜8)。省いた・読めないときは第1言語 */
|
|
36
|
+
export function parseCaptionLanguage(value) {
|
|
37
|
+
const number = Number(value);
|
|
38
|
+
return Number.isInteger(number) && number >= 1 && number <= 8 ? number : 1;
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
|
|
3
|
+
*
|
|
4
|
+
* 放送は置き換え表で字にできない外字を、16〜36 画素の白黒の画像で送ってくる。受け側はそれを字の枠いっぱい
|
|
5
|
+
* (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
|
|
6
|
+
* なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
|
|
7
|
+
*
|
|
8
|
+
* 以前は libaribcaption の描き手にパッチを当てて同じことをしていた (#554)。描くのを受け側に移したとき (#571〜#586)
|
|
9
|
+
* にその直しが落ちたので、**サーバで1回だけ** やって、濃淡の画像 (`bits: 4`・`depth: 16`) で送る。受け側は
|
|
10
|
+
* もともと `bits` 1・2・4・8 を読み、濃さを字の色の不透明度に掛けて引き伸ばすので変えなくてよい。
|
|
11
|
+
*
|
|
12
|
+
* 1. **水増しを畳む** (`collapseDoubled`)。放送はよく 18x18 の字を縦横 2 倍の 36x36 で送ってくる。
|
|
13
|
+
* 同じ並びが 2 つずつ続く行・列を 1 つにする (失うものは無い)。そうしないと次の段で階段を階段と見られない
|
|
14
|
+
* 2. **Scale2x (EPX) で斜めを丸める** (`scale2x`)。斜めの階段を細かくする。縦横の線はまっすぐのまま残るが、
|
|
15
|
+
* 塗りの出っ張った角は元の1点ぶんほど丸まる (EPX の性質。#554 のパッチも同じで、フォントの字に近い見た目になる)
|
|
16
|
+
* 3. **縮めて濃淡にする** (`boxDown`・`pack4`)。細かくした画像を 4x4 ずつ平均し、縁に中間の濃さを付ける。
|
|
17
|
+
* 縦横の線は元の画素の境目に落ちるので、ぼけずにくっきり残る
|
|
18
|
+
*
|
|
19
|
+
* **置き換え表の MD5 は元の画像から取る** (b24caption.ts)。ここで作った画像は送るためだけのもの。
|
|
20
|
+
*/
|
|
21
|
+
import type { CaptionDrcs } from "./caption-text.js";
|
|
22
|
+
/** 濃さ 0〜255 の画像 (左上から1画素1バイト) */
|
|
23
|
+
export interface Grey {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
pixels: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
/** 放送の詰め方 (左上から1画素 `bits` ビット、上の桁から) を開いて、濃さ 0〜255 にする */
|
|
29
|
+
export declare function decodeGrey(width: number, height: number, depth: number, bits: number, data: Uint8Array): Grey;
|
|
30
|
+
/**
|
|
31
|
+
* 縦横に水増しされた画像を畳む。偶数の幅で、どの行も (0,1) (2,3)… の列が同じなら列を半分に、
|
|
32
|
+
* 行も同じように見る。畳めなくなるまで繰り返す (4 倍で送ってきても畳める)
|
|
33
|
+
*/
|
|
34
|
+
export declare function collapseDoubled(src: Grey): Grey;
|
|
35
|
+
/**
|
|
36
|
+
* Scale2x (EPX)。縦横 2 倍にしながら、斜めに接する点どうしの間を埋めて階段を細かくする。
|
|
37
|
+
* 画像の外は縁で折り返した内側と同じと見る。縁に接した塗りは角が欠けず (隣の字とつながる線が切れない)、
|
|
38
|
+
* 縁へ抜ける斜めの線にも余計な点が付かない
|
|
39
|
+
*/
|
|
40
|
+
export declare function scale2x(src: Grey): Grey;
|
|
41
|
+
/** `n`x`n` ずつ平均して 1/n に縮める (幅・高さは n の倍数であること) */
|
|
42
|
+
export declare function boxDown(src: Grey, n: number): Grey;
|
|
43
|
+
/**
|
|
44
|
+
* 外字の画像を、送る濃淡の画像にする。畳んだ画像の一辺が `TARGET` に届くまで 2 倍ずつ (一辺 `MAX` まで) 大きくする。
|
|
45
|
+
* 畳んだ 18x18 なら 72x72、36x36 (水増しなし) なら 72x72
|
|
46
|
+
*/
|
|
47
|
+
export declare function smoothDrcs(width: number, height: number, depth: number, bits: number, data: Uint8Array): CaptionDrcs;
|
|
48
|
+
/**
|
|
49
|
+
* 濃さを 16 階調にして1画素4ビットで詰める (左の画素が上の桁)。4x4 の平均は白黒の画像からだと 17 階調しか
|
|
50
|
+
* 出ないので、8 ビットで送っても見た目は変わらず倍の大きさになるだけ (外字の画像は字幕の1枚ごとに付く)
|
|
51
|
+
*/
|
|
52
|
+
export declare function pack4(g: Grey): Uint8Array;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **字幕の外字 (DRCS) の画像を、送る前になめらかにする** ([b24caption.ts](b24caption.ts) が JSON に入れるところで使う)。
|
|
3
|
+
*
|
|
4
|
+
* 放送は置き換え表で字にできない外字を、16〜36 画素の白黒の画像で送ってくる。受け側はそれを字の枠いっぱい
|
|
5
|
+
* (1920x1080 の全画面で 72 画素ほど) に引き伸ばすので、そのまま渡すと 1 点が 4x4 の階段になり、
|
|
6
|
+
* なめらかに引き伸ばしても階段がぼやけて残る (スピーカー 🔊 の外字など)。
|
|
7
|
+
*
|
|
8
|
+
* 以前は libaribcaption の描き手にパッチを当てて同じことをしていた (#554)。描くのを受け側に移したとき (#571〜#586)
|
|
9
|
+
* にその直しが落ちたので、**サーバで1回だけ** やって、濃淡の画像 (`bits: 4`・`depth: 16`) で送る。受け側は
|
|
10
|
+
* もともと `bits` 1・2・4・8 を読み、濃さを字の色の不透明度に掛けて引き伸ばすので変えなくてよい。
|
|
11
|
+
*
|
|
12
|
+
* 1. **水増しを畳む** (`collapseDoubled`)。放送はよく 18x18 の字を縦横 2 倍の 36x36 で送ってくる。
|
|
13
|
+
* 同じ並びが 2 つずつ続く行・列を 1 つにする (失うものは無い)。そうしないと次の段で階段を階段と見られない
|
|
14
|
+
* 2. **Scale2x (EPX) で斜めを丸める** (`scale2x`)。斜めの階段を細かくする。縦横の線はまっすぐのまま残るが、
|
|
15
|
+
* 塗りの出っ張った角は元の1点ぶんほど丸まる (EPX の性質。#554 のパッチも同じで、フォントの字に近い見た目になる)
|
|
16
|
+
* 3. **縮めて濃淡にする** (`boxDown`・`pack4`)。細かくした画像を 4x4 ずつ平均し、縁に中間の濃さを付ける。
|
|
17
|
+
* 縦横の線は元の画素の境目に落ちるので、ぼけずにくっきり残る
|
|
18
|
+
*
|
|
19
|
+
* **置き換え表の MD5 は元の画像から取る** (b24caption.ts)。ここで作った画像は送るためだけのもの。
|
|
20
|
+
*/
|
|
21
|
+
/** 送る画像の一辺。これに届くまで倍にする (全画面の字の枠と同じくらい) */
|
|
22
|
+
const TARGET = 64;
|
|
23
|
+
/** 送る画像の一辺の上限。受け側 (denpa-tv) もこれより大きい画像は読まない */
|
|
24
|
+
const MAX = 256;
|
|
25
|
+
/** 最後に平均する一辺。Scale2x をこのぶん余計にかけておく (4 なら 2 回) */
|
|
26
|
+
const BOX = 4;
|
|
27
|
+
/** 放送の詰め方 (左上から1画素 `bits` ビット、上の桁から) を開いて、濃さ 0〜255 にする */
|
|
28
|
+
export function decodeGrey(width, height, depth, bits, data) {
|
|
29
|
+
const pixels = new Uint8Array(width * height);
|
|
30
|
+
const mask = (1 << bits) - 1;
|
|
31
|
+
for (let i = 0; i < width * height; i++) {
|
|
32
|
+
const bit = i * bits;
|
|
33
|
+
const value = ((data[bit >> 3] ?? 0) >> (8 - (bit & 7) - bits)) & mask;
|
|
34
|
+
pixels[i] = Math.round((Math.min(value, depth - 1) * 255) / (depth - 1));
|
|
35
|
+
}
|
|
36
|
+
return { width, height, pixels };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 縦横に水増しされた画像を畳む。偶数の幅で、どの行も (0,1) (2,3)… の列が同じなら列を半分に、
|
|
40
|
+
* 行も同じように見る。畳めなくなるまで繰り返す (4 倍で送ってきても畳める)
|
|
41
|
+
*/
|
|
42
|
+
export function collapseDoubled(src) {
|
|
43
|
+
let g = src;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { width, height, pixels } = g;
|
|
46
|
+
let cols = width % 2 === 0;
|
|
47
|
+
let rows = height % 2 === 0;
|
|
48
|
+
for (let y = 0; y < height && (cols || rows); y++) {
|
|
49
|
+
for (let x = 0; x < width; x++) {
|
|
50
|
+
const p = pixels[y * width + x];
|
|
51
|
+
if (cols && x % 2 === 0 && p !== pixels[y * width + x + 1])
|
|
52
|
+
cols = false;
|
|
53
|
+
if (rows && y % 2 === 0 && p !== pixels[(y + 1) * width + x])
|
|
54
|
+
rows = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!cols && !rows)
|
|
58
|
+
return g;
|
|
59
|
+
const w = cols ? width / 2 : width;
|
|
60
|
+
const h = rows ? height / 2 : height;
|
|
61
|
+
const out = new Uint8Array(w * h);
|
|
62
|
+
for (let y = 0; y < h; y++) {
|
|
63
|
+
for (let x = 0; x < w; x++)
|
|
64
|
+
out[y * w + x] =
|
|
65
|
+
pixels[(rows ? y * 2 : y) * width + (cols ? x * 2 : x)];
|
|
66
|
+
}
|
|
67
|
+
g = { width: w, height: h, pixels: out };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scale2x (EPX)。縦横 2 倍にしながら、斜めに接する点どうしの間を埋めて階段を細かくする。
|
|
72
|
+
* 画像の外は縁で折り返した内側と同じと見る。縁に接した塗りは角が欠けず (隣の字とつながる線が切れない)、
|
|
73
|
+
* 縁へ抜ける斜めの線にも余計な点が付かない
|
|
74
|
+
*/
|
|
75
|
+
export function scale2x(src) {
|
|
76
|
+
const { width, height, pixels } = src;
|
|
77
|
+
const w = width * 2;
|
|
78
|
+
const out = new Uint8Array(w * height * 2);
|
|
79
|
+
const at = (x, y) => {
|
|
80
|
+
const cx = x < 0 ? Math.min(1, width - 1) : x >= width ? Math.max(0, width - 2) : x;
|
|
81
|
+
const cy = y < 0
|
|
82
|
+
? Math.min(1, height - 1)
|
|
83
|
+
: y >= height
|
|
84
|
+
? Math.max(0, height - 2)
|
|
85
|
+
: y;
|
|
86
|
+
return pixels[cy * width + cx];
|
|
87
|
+
};
|
|
88
|
+
for (let y = 0; y < height; y++) {
|
|
89
|
+
for (let x = 0; x < width; x++) {
|
|
90
|
+
const p = pixels[y * width + x];
|
|
91
|
+
const a = at(x, y - 1); // 上
|
|
92
|
+
const b = at(x + 1, y); // 右
|
|
93
|
+
const c = at(x - 1, y); // 左
|
|
94
|
+
const d = at(x, y + 1); // 下
|
|
95
|
+
const top = y * 2 * w + x * 2;
|
|
96
|
+
out[top] = c === a && c !== d && a !== b ? a : p;
|
|
97
|
+
out[top + 1] = a === b && a !== c && b !== d ? b : p;
|
|
98
|
+
out[top + w] = d === c && d !== b && c !== a ? c : p;
|
|
99
|
+
out[top + w + 1] = b === d && b !== a && d !== c ? d : p;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { width: w, height: height * 2, pixels: out };
|
|
103
|
+
}
|
|
104
|
+
/** `n`x`n` ずつ平均して 1/n に縮める (幅・高さは n の倍数であること) */
|
|
105
|
+
export function boxDown(src, n) {
|
|
106
|
+
const w = src.width / n;
|
|
107
|
+
const h = src.height / n;
|
|
108
|
+
const out = new Uint8Array(w * h);
|
|
109
|
+
for (let y = 0; y < h; y++) {
|
|
110
|
+
for (let x = 0; x < w; x++) {
|
|
111
|
+
let sum = 0;
|
|
112
|
+
for (let dy = 0; dy < n; dy++) {
|
|
113
|
+
const row = (y * n + dy) * src.width + x * n;
|
|
114
|
+
for (let dx = 0; dx < n; dx++)
|
|
115
|
+
sum += src.pixels[row + dx];
|
|
116
|
+
}
|
|
117
|
+
out[y * w + x] = Math.round(sum / (n * n));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { width: w, height: h, pixels: out };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 外字の画像を、送る濃淡の画像にする。畳んだ画像の一辺が `TARGET` に届くまで 2 倍ずつ (一辺 `MAX` まで) 大きくする。
|
|
124
|
+
* 畳んだ 18x18 なら 72x72、36x36 (水増しなし) なら 72x72
|
|
125
|
+
*/
|
|
126
|
+
export function smoothDrcs(width, height, depth, bits, data) {
|
|
127
|
+
if (width <= 0 || height <= 0 || depth < 2 || ![1, 2, 4, 8].includes(bits)) {
|
|
128
|
+
// 読めない画像はそのまま渡す (受け側も読まない)
|
|
129
|
+
return {
|
|
130
|
+
w: width,
|
|
131
|
+
h: height,
|
|
132
|
+
depth,
|
|
133
|
+
bits,
|
|
134
|
+
data: Buffer.from(data).toString("base64"),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let g = collapseDoubled(decodeGrey(width, height, depth, bits, data));
|
|
138
|
+
const side = Math.max(g.width, g.height);
|
|
139
|
+
let scale = 1;
|
|
140
|
+
while (side * scale < TARGET && side * scale * 2 <= MAX)
|
|
141
|
+
scale *= 2;
|
|
142
|
+
for (let s = 1; s < scale * BOX; s *= 2)
|
|
143
|
+
g = scale2x(g);
|
|
144
|
+
g = boxDown(g, BOX);
|
|
145
|
+
return {
|
|
146
|
+
w: g.width,
|
|
147
|
+
h: g.height,
|
|
148
|
+
depth: 16,
|
|
149
|
+
bits: 4,
|
|
150
|
+
data: Buffer.from(pack4(g)).toString("base64"),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 濃さを 16 階調にして1画素4ビットで詰める (左の画素が上の桁)。4x4 の平均は白黒の画像からだと 17 階調しか
|
|
155
|
+
* 出ないので、8 ビットで送っても見た目は変わらず倍の大きさになるだけ (外字の画像は字幕の1枚ごとに付く)
|
|
156
|
+
*/
|
|
157
|
+
export function pack4(g) {
|
|
158
|
+
const out = new Uint8Array(Math.ceil(g.pixels.length / 2));
|
|
159
|
+
g.pixels.forEach((v, i) => {
|
|
160
|
+
out[i >> 1] |= Math.round((v * 15) / 255) << (i & 1 ? 0 : 4);
|
|
161
|
+
});
|
|
162
|
+
return out;
|
|
163
|
+
}
|
package/dist/dsmcc.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* データカルーセル (DSM-CC) をデコードする。**放送が同じものを何度も回してくる**中から、
|
|
3
|
+
* ブロックを集めて1つのモジュールに組み立て直すところまで。
|
|
4
|
+
*
|
|
5
|
+
* 使う側が2つある。**中身の読み方だけが違って、運び方は同じ**なので、ここに寄せる。
|
|
6
|
+
*
|
|
7
|
+
* - [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts) … 衛星の局ロゴ (ARIB STD-B21)
|
|
8
|
+
* - [bml.ts](bml.ts) … データ放送 (ARIB STD-B24 / TR-B14・TR-B15)
|
|
9
|
+
*
|
|
10
|
+
* ## 流れ
|
|
11
|
+
*
|
|
12
|
+
* 1. **DII** (table_id 0x3B) が「どのモジュールが何バイトで、何ブロックか」を伝える
|
|
13
|
+
* 2. **DDB** (0x3C) がそのモジュールを `blockSize` ごとに割って何度も流す
|
|
14
|
+
* 3. 全ブロック揃うとモジュールになる
|
|
15
|
+
*
|
|
16
|
+
* セクションを取り出すのは [psi.ts](psi.ts) の `SectionAssembler`。**`'syntax'` を
|
|
17
|
+
* 渡すこと** — DSM-CC は `section_syntax_indicator` が立たないことがあり、そのときは
|
|
18
|
+
* 末尾4バイトが CRC ではなく Checksum なので、いつも確かめると全部捨ててしまう。
|
|
19
|
+
*/
|
|
20
|
+
export declare const TABLE_DII = 59;
|
|
21
|
+
/** ストリーム記述子。NPT と イベントメッセージが載る (データ放送だけが使う) */
|
|
22
|
+
export declare const TABLE_STREAM_DESCRIPTOR = 61;
|
|
23
|
+
/**
|
|
24
|
+
* 同じカルーセルの中の「回」。**downloadId の上位4ビットに入っている**
|
|
25
|
+
* (下位28ビットは常に1で運用される)。
|
|
26
|
+
*
|
|
27
|
+
* 番組が変わるとここが変わるので、**前の番組のモジュールを捨てる合図**になる。
|
|
28
|
+
*/
|
|
29
|
+
export declare function dataEventIdOf(downloadId: number): number;
|
|
30
|
+
export interface DiiModule {
|
|
31
|
+
moduleId: number;
|
|
32
|
+
moduleSize: number;
|
|
33
|
+
moduleVersion: number;
|
|
34
|
+
/** モジュール記述子の名前。ロゴかどうかの見分けに使う */
|
|
35
|
+
name: string | null;
|
|
36
|
+
/** Type 記述子。`multipart/mixed` など。データ放送だけが使う */
|
|
37
|
+
contentType: string | null;
|
|
38
|
+
/** Compression Type 記述子。**0 が zlib**。伸ばしたあとの大きさ付き */
|
|
39
|
+
compression: {
|
|
40
|
+
type: number;
|
|
41
|
+
originalSize: number;
|
|
42
|
+
} | null;
|
|
43
|
+
}
|
|
44
|
+
export interface Dii {
|
|
45
|
+
downloadId: number;
|
|
46
|
+
/** カルーセルの版。**変わっていなければ組み立て直さない** */
|
|
47
|
+
transactionId: number;
|
|
48
|
+
blockSize: number;
|
|
49
|
+
modules: DiiModule[];
|
|
50
|
+
/**
|
|
51
|
+
* ARIB の private データ記述子 (0xF0) の入口フラグ。
|
|
52
|
+
* データ放送で「次に入口へ戻すか」を伝える。無ければ null
|
|
53
|
+
*/
|
|
54
|
+
returnToEntry: boolean | null;
|
|
55
|
+
}
|
|
56
|
+
export interface Ddb {
|
|
57
|
+
downloadId: number;
|
|
58
|
+
moduleId: number;
|
|
59
|
+
moduleVersion: number;
|
|
60
|
+
blockNumber: number;
|
|
61
|
+
block: Uint8Array;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* DII (Download Info Indication)。
|
|
65
|
+
*
|
|
66
|
+
* 頭は共通で protocolDiscriminator/dsmccType/messageId/transaction_id と続き、
|
|
67
|
+
* adaptationLength ぶん飛ばした先に本体が入っている。
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseDii(section: Uint8Array): Dii | null;
|
|
70
|
+
/** DDB (Download Data Block)。モジュールを割ったブロックが1つ入っている */
|
|
71
|
+
export declare function parseDdb(section: Uint8Array): Ddb | null;
|
|
72
|
+
/**
|
|
73
|
+
* 組み立て中のモジュール。**DII が言った大きさぶんの入れ物を先に用意して**、
|
|
74
|
+
* DDB が来るたびに然るべき位置へ置く。
|
|
75
|
+
*
|
|
76
|
+
* 順番は保証されない (途中から見れば真ん中から始まる) ので、
|
|
77
|
+
* **受け取ったブロック番号を数えて**揃ったかを見る。
|
|
78
|
+
*/
|
|
79
|
+
export declare class ModuleBuilder {
|
|
80
|
+
readonly moduleVersion: number;
|
|
81
|
+
private readonly blockSize;
|
|
82
|
+
readonly data: Uint8Array;
|
|
83
|
+
private readonly received;
|
|
84
|
+
private readonly total;
|
|
85
|
+
constructor(moduleVersion: number, moduleSize: number, blockSize: number);
|
|
86
|
+
/** 受け取ったブロックの数 (同じブロックを2度受けても1つ) */
|
|
87
|
+
get blocks(): number;
|
|
88
|
+
/** 揃ったか。**大きさ 0 のモジュールは初めから揃っている** (運ぶブロックが無いので DDB を待たない) */
|
|
89
|
+
get complete(): boolean;
|
|
90
|
+
/** ブロックを1つ置く。**揃ったら true** */
|
|
91
|
+
add(blockNumber: number, block: Uint8Array): boolean;
|
|
92
|
+
}
|
package/dist/dsmcc.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* データカルーセル (DSM-CC) をデコードする。**放送が同じものを何度も回してくる**中から、
|
|
3
|
+
* ブロックを集めて1つのモジュールに組み立て直すところまで。
|
|
4
|
+
*
|
|
5
|
+
* 使う側が2つある。**中身の読み方だけが違って、運び方は同じ**なので、ここに寄せる。
|
|
6
|
+
*
|
|
7
|
+
* - [logo-dsmcc.ts](https://github.com/danything/denpa/blob/main/src/lib/ts/logo-dsmcc.ts) … 衛星の局ロゴ (ARIB STD-B21)
|
|
8
|
+
* - [bml.ts](bml.ts) … データ放送 (ARIB STD-B24 / TR-B14・TR-B15)
|
|
9
|
+
*
|
|
10
|
+
* ## 流れ
|
|
11
|
+
*
|
|
12
|
+
* 1. **DII** (table_id 0x3B) が「どのモジュールが何バイトで、何ブロックか」を伝える
|
|
13
|
+
* 2. **DDB** (0x3C) がそのモジュールを `blockSize` ごとに割って何度も流す
|
|
14
|
+
* 3. 全ブロック揃うとモジュールになる
|
|
15
|
+
*
|
|
16
|
+
* セクションを取り出すのは [psi.ts](psi.ts) の `SectionAssembler`。**`'syntax'` を
|
|
17
|
+
* 渡すこと** — DSM-CC は `section_syntax_indicator` が立たないことがあり、そのときは
|
|
18
|
+
* 末尾4バイトが CRC ではなく Checksum なので、いつも確かめると全部捨ててしまう。
|
|
19
|
+
*/
|
|
20
|
+
import { u16, u32 } from "./bytes.js";
|
|
21
|
+
import { descriptors } from "./psi.js";
|
|
22
|
+
export const TABLE_DII = 0x3b;
|
|
23
|
+
const TABLE_DDB = 0x3c;
|
|
24
|
+
/** ストリーム記述子。NPT と イベントメッセージが載る (データ放送だけが使う) */
|
|
25
|
+
export const TABLE_STREAM_DESCRIPTOR = 0x3d;
|
|
26
|
+
/** モジュール記述子 (STD-B24 第三分冊 第三編 6.2.3) */
|
|
27
|
+
const MODULE_DESC_TYPE = 0x01;
|
|
28
|
+
const MODULE_DESC_NAME = 0x02;
|
|
29
|
+
const MODULE_DESC_COMPRESSION = 0xc2;
|
|
30
|
+
/** DII が blockSize を持っていないときの既定 (ARIB TR-B15) */
|
|
31
|
+
const DEFAULT_BLOCK_SIZE = 4066;
|
|
32
|
+
/**
|
|
33
|
+
* 同じカルーセルの中の「回」。**downloadId の上位4ビットに入っている**
|
|
34
|
+
* (下位28ビットは常に1で運用される)。
|
|
35
|
+
*
|
|
36
|
+
* 番組が変わるとここが変わるので、**前の番組のモジュールを捨てる合図**になる。
|
|
37
|
+
*/
|
|
38
|
+
export function dataEventIdOf(downloadId) {
|
|
39
|
+
return (downloadId >>> 28) & 0x0f;
|
|
40
|
+
}
|
|
41
|
+
/** DSM-CC セクションの中身 (メッセージ本体) を切り出す */
|
|
42
|
+
function messageOf(section) {
|
|
43
|
+
const length = u16(section, 1) & 0x0fff;
|
|
44
|
+
const end = 3 + length - 4;
|
|
45
|
+
if (end <= 8 || end > section.length)
|
|
46
|
+
return null;
|
|
47
|
+
return section.subarray(8, end);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* DII (Download Info Indication)。
|
|
51
|
+
*
|
|
52
|
+
* 頭は共通で protocolDiscriminator/dsmccType/messageId/transaction_id と続き、
|
|
53
|
+
* adaptationLength ぶん飛ばした先に本体が入っている。
|
|
54
|
+
*/
|
|
55
|
+
export function parseDii(section) {
|
|
56
|
+
if (section[0] !== TABLE_DII)
|
|
57
|
+
return null;
|
|
58
|
+
const message = messageOf(section);
|
|
59
|
+
if (message === null || message.length < 20)
|
|
60
|
+
return null;
|
|
61
|
+
const transactionId = u32(message, 4);
|
|
62
|
+
const adaptationLength = message[9];
|
|
63
|
+
let at = 12 + adaptationLength;
|
|
64
|
+
if (at + 20 > message.length)
|
|
65
|
+
return null;
|
|
66
|
+
const downloadId = u32(message, at);
|
|
67
|
+
const blockSize = u16(message, at + 4) || DEFAULT_BLOCK_SIZE;
|
|
68
|
+
at += 4 + 2 + 1 + 1 + 4 + 4;
|
|
69
|
+
// compatibilityDescriptor。中身は使わないので長さぶん飛ばす
|
|
70
|
+
if (at + 2 > message.length)
|
|
71
|
+
return null;
|
|
72
|
+
at += 2 + u16(message, at);
|
|
73
|
+
if (at + 2 > message.length)
|
|
74
|
+
return null;
|
|
75
|
+
const count = u16(message, at);
|
|
76
|
+
at += 2;
|
|
77
|
+
const modules = [];
|
|
78
|
+
for (let i = 0; i < count; i++) {
|
|
79
|
+
if (at + 8 > message.length)
|
|
80
|
+
return null;
|
|
81
|
+
const moduleId = u16(message, at);
|
|
82
|
+
const moduleSize = u32(message, at + 2);
|
|
83
|
+
const moduleVersion = message[at + 6];
|
|
84
|
+
const infoLength = message[at + 7];
|
|
85
|
+
const info = message.subarray(at + 8, at + 8 + infoLength);
|
|
86
|
+
at += 8 + infoLength;
|
|
87
|
+
let name = null;
|
|
88
|
+
let contentType = null;
|
|
89
|
+
let compression = null;
|
|
90
|
+
for (const [tag, descriptor] of descriptors(info)) {
|
|
91
|
+
if (tag === MODULE_DESC_NAME)
|
|
92
|
+
name = new TextDecoder().decode(descriptor);
|
|
93
|
+
else if (tag === MODULE_DESC_TYPE)
|
|
94
|
+
contentType = new TextDecoder("ascii").decode(descriptor);
|
|
95
|
+
else if (tag === MODULE_DESC_COMPRESSION && descriptor.length >= 5) {
|
|
96
|
+
// 先頭1バイトは符号付き。運用されるのは 0 (zlib) だけ
|
|
97
|
+
compression = {
|
|
98
|
+
type: (descriptor[0] << 24) >> 24,
|
|
99
|
+
originalSize: u32(descriptor, 1),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
modules.push({
|
|
104
|
+
moduleId,
|
|
105
|
+
moduleSize,
|
|
106
|
+
moduleVersion,
|
|
107
|
+
name,
|
|
108
|
+
contentType,
|
|
109
|
+
compression,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/*
|
|
113
|
+
* privateData。長さの手前まで飛ばしたところに記述子が並ぶ。
|
|
114
|
+
* **無いほうがふつう**なので、読めなければ null のまま進む
|
|
115
|
+
*/
|
|
116
|
+
let returnToEntry = null;
|
|
117
|
+
if (at + 2 <= message.length) {
|
|
118
|
+
const length = u16(message, at);
|
|
119
|
+
for (const [tag, descriptor] of descriptors(message.subarray(at + 2, at + 2 + length))) {
|
|
120
|
+
// arib_bxml_privatedata_descriptor (STD-B24 第二分冊 (1/2) 第二編 9.3.4)
|
|
121
|
+
if (tag === 0xf0 && descriptor.length >= 1)
|
|
122
|
+
returnToEntry = (descriptor[0] & 0x80) !== 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { downloadId, transactionId, blockSize, modules, returnToEntry };
|
|
126
|
+
}
|
|
127
|
+
/** DDB (Download Data Block)。モジュールを割ったブロックが1つ入っている */
|
|
128
|
+
export function parseDdb(section) {
|
|
129
|
+
if (section[0] !== TABLE_DDB)
|
|
130
|
+
return null;
|
|
131
|
+
const message = messageOf(section);
|
|
132
|
+
if (message === null || message.length < 12)
|
|
133
|
+
return null;
|
|
134
|
+
const downloadId = u32(message, 4);
|
|
135
|
+
const adaptationLength = message[9];
|
|
136
|
+
const messageLength = u16(message, 10);
|
|
137
|
+
let at = 12 + adaptationLength;
|
|
138
|
+
if (at + 6 > message.length)
|
|
139
|
+
return null;
|
|
140
|
+
const moduleId = u16(message, at);
|
|
141
|
+
const moduleVersion = message[at + 2];
|
|
142
|
+
const blockNumber = u16(message, at + 4);
|
|
143
|
+
at += 6;
|
|
144
|
+
const size = messageLength - adaptationLength - 6;
|
|
145
|
+
if (size < 0 || at + size > message.length)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
downloadId,
|
|
149
|
+
moduleId,
|
|
150
|
+
moduleVersion,
|
|
151
|
+
blockNumber,
|
|
152
|
+
block: message.subarray(at, at + size),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 組み立て中のモジュール。**DII が言った大きさぶんの入れ物を先に用意して**、
|
|
157
|
+
* DDB が来るたびに然るべき位置へ置く。
|
|
158
|
+
*
|
|
159
|
+
* 順番は保証されない (途中から見れば真ん中から始まる) ので、
|
|
160
|
+
* **受け取ったブロック番号を数えて**揃ったかを見る。
|
|
161
|
+
*/
|
|
162
|
+
export class ModuleBuilder {
|
|
163
|
+
moduleVersion;
|
|
164
|
+
blockSize;
|
|
165
|
+
data;
|
|
166
|
+
received = new Set();
|
|
167
|
+
total;
|
|
168
|
+
constructor(moduleVersion, moduleSize, blockSize) {
|
|
169
|
+
this.moduleVersion = moduleVersion;
|
|
170
|
+
this.blockSize = blockSize;
|
|
171
|
+
this.data = new Uint8Array(moduleSize);
|
|
172
|
+
this.total = Math.ceil(moduleSize / blockSize);
|
|
173
|
+
}
|
|
174
|
+
/** 受け取ったブロックの数 (同じブロックを2度受けても1つ) */
|
|
175
|
+
get blocks() {
|
|
176
|
+
return this.received.size;
|
|
177
|
+
}
|
|
178
|
+
/** 揃ったか。**大きさ 0 のモジュールは初めから揃っている** (運ぶブロックが無いので DDB を待たない) */
|
|
179
|
+
get complete() {
|
|
180
|
+
return this.received.size >= this.total;
|
|
181
|
+
}
|
|
182
|
+
/** ブロックを1つ置く。**揃ったら true** */
|
|
183
|
+
add(blockNumber, block) {
|
|
184
|
+
const at = this.blockSize * blockNumber;
|
|
185
|
+
if (at + block.length > this.data.length)
|
|
186
|
+
return false;
|
|
187
|
+
this.data.set(block, at);
|
|
188
|
+
this.received.add(blockNumber);
|
|
189
|
+
return this.complete;
|
|
190
|
+
}
|
|
191
|
+
}
|