@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/language.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 639-2 のうち日本の放送で出てくるもの。
|
|
3
|
+
*
|
|
4
|
+
* **音声も字幕も同じ表を使う** — 字幕と音声で別に持っていた頃は
|
|
5
|
+
* 4つ足りず、同じ放送の音声が「イタリア語」、字幕が「ita」と出る形になっていた
|
|
6
|
+
*/
|
|
7
|
+
export const LANGUAGE = {
|
|
8
|
+
jpn: "日本語",
|
|
9
|
+
eng: "英語",
|
|
10
|
+
deu: "ドイツ語",
|
|
11
|
+
fra: "フランス語",
|
|
12
|
+
ita: "イタリア語",
|
|
13
|
+
rus: "ロシア語",
|
|
14
|
+
zho: "中国語",
|
|
15
|
+
kor: "韓国語",
|
|
16
|
+
spa: "スペイン語",
|
|
17
|
+
por: "ポルトガル語",
|
|
18
|
+
tha: "タイ語",
|
|
19
|
+
etc: "その他",
|
|
20
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* モジュールの中身を読む。**ヘッダ + 空行 + 本体** の形式 (STD-B24 第二編 第9章) で、
|
|
3
|
+
* 多くは `multipart/mixed` に BML と画像がまとめて入っている。
|
|
4
|
+
*
|
|
5
|
+
* Content-Type: multipart/mixed; boundary=XXX
|
|
6
|
+
*
|
|
7
|
+
* --XXX
|
|
8
|
+
* Content-Type: text/X-arib-bml; charset="EUC-JP"
|
|
9
|
+
* Content-Location: startup.bml
|
|
10
|
+
*
|
|
11
|
+
* <?xml ...
|
|
12
|
+
* --XXX--
|
|
13
|
+
*
|
|
14
|
+
* もとは web-bml の `entity_parser.ts` を借りていた。運用される形式は上のとおり狭いので、
|
|
15
|
+
* RFC 2068 の字句を全部なぞらず、**ヘッダの行を取り出して `Content-Type` をデコードし、
|
|
16
|
+
* 区切りで切る**だけにしてある。
|
|
17
|
+
*/
|
|
18
|
+
import type { MediaType } from "./bml-protocol.js";
|
|
19
|
+
/** ヘッダ 1 つ。名前は小文字にそろえる */
|
|
20
|
+
export interface EntityHeader {
|
|
21
|
+
name: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
/** 読んだ中身。`multipart/mixed` なら `parts` に1つずつ入る */
|
|
25
|
+
export interface Entity {
|
|
26
|
+
headers: EntityHeader[];
|
|
27
|
+
body: Uint8Array;
|
|
28
|
+
parts: Entity[] | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `type/subtype; a=b; c="d"` を読む。名前は小文字にそろえ、元の綴りも残す。
|
|
32
|
+
*
|
|
33
|
+
* **引数の途中で崩れていても、読めたところまでは返す** — 型と下位型が分かれば
|
|
34
|
+
* 読み方は決まる。型すら読めなければ null
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseMediaType(text: string): MediaType | null;
|
|
37
|
+
/**
|
|
38
|
+
* 中身を1つ読む。**ヘッダの終わり (空行) が無いものは null** — どこからが本体か
|
|
39
|
+
* 分からない。`multipart/mixed` なら中までデコードする (入れ子も同じ形)。
|
|
40
|
+
*
|
|
41
|
+
* @param part multipart の中の部品か。部品はヘッダの行でちょうど終わってよい (下の説明)
|
|
42
|
+
*/
|
|
43
|
+
export declare function readEntity(data: Uint8Array, part?: boolean): Entity | null;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* モジュールの中身を読む。**ヘッダ + 空行 + 本体** の形式 (STD-B24 第二編 第9章) で、
|
|
3
|
+
* 多くは `multipart/mixed` に BML と画像がまとめて入っている。
|
|
4
|
+
*
|
|
5
|
+
* Content-Type: multipart/mixed; boundary=XXX
|
|
6
|
+
*
|
|
7
|
+
* --XXX
|
|
8
|
+
* Content-Type: text/X-arib-bml; charset="EUC-JP"
|
|
9
|
+
* Content-Location: startup.bml
|
|
10
|
+
*
|
|
11
|
+
* <?xml ...
|
|
12
|
+
* --XXX--
|
|
13
|
+
*
|
|
14
|
+
* もとは web-bml の `entity_parser.ts` を借りていた。運用される形式は上のとおり狭いので、
|
|
15
|
+
* RFC 2068 の字句を全部なぞらず、**ヘッダの行を取り出して `Content-Type` をデコードし、
|
|
16
|
+
* 区切りで切る**だけにしてある。
|
|
17
|
+
*/
|
|
18
|
+
const CR = 0x0d;
|
|
19
|
+
const LF = 0x0a;
|
|
20
|
+
const CRLF = Uint8Array.of(CR, LF);
|
|
21
|
+
const utf8 = new TextDecoder();
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
/** token に使えない字 (RFC 2068 2.2 の tspecials と制御文字) */
|
|
24
|
+
const TSPECIALS = '()<>@,;:\\"/[]?={} \t';
|
|
25
|
+
function isTokenChar(char) {
|
|
26
|
+
const code = char.charCodeAt(0);
|
|
27
|
+
return code > 0x20 && code < 0x7f && !TSPECIALS.includes(char);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `type/subtype; a=b; c="d"` を読む。名前は小文字にそろえ、元の綴りも残す。
|
|
31
|
+
*
|
|
32
|
+
* **引数の途中で崩れていても、読めたところまでは返す** — 型と下位型が分かれば
|
|
33
|
+
* 読み方は決まる。型すら読めなければ null
|
|
34
|
+
*/
|
|
35
|
+
export function parseMediaType(text) {
|
|
36
|
+
let at = 0;
|
|
37
|
+
const skipSpace = () => {
|
|
38
|
+
while (at < text.length && (text[at] === " " || text[at] === "\t"))
|
|
39
|
+
at++;
|
|
40
|
+
};
|
|
41
|
+
const token = () => {
|
|
42
|
+
const from = at;
|
|
43
|
+
while (at < text.length && isTokenChar(text[at]))
|
|
44
|
+
at++;
|
|
45
|
+
return at === from ? null : text.slice(from, at);
|
|
46
|
+
};
|
|
47
|
+
const quoted = () => {
|
|
48
|
+
if (text[at] !== '"')
|
|
49
|
+
return null;
|
|
50
|
+
at++;
|
|
51
|
+
let out = "";
|
|
52
|
+
while (at < text.length && text[at] !== '"') {
|
|
53
|
+
if (text[at] === "\\" && at + 1 < text.length)
|
|
54
|
+
at++;
|
|
55
|
+
out += text[at++];
|
|
56
|
+
}
|
|
57
|
+
at++;
|
|
58
|
+
return out;
|
|
59
|
+
};
|
|
60
|
+
skipSpace();
|
|
61
|
+
const type = token();
|
|
62
|
+
if (type === null)
|
|
63
|
+
return null;
|
|
64
|
+
const parameters = [];
|
|
65
|
+
const made = (subtype) => ({
|
|
66
|
+
type: type.toLowerCase(),
|
|
67
|
+
originalType: type,
|
|
68
|
+
subtype: subtype.toLowerCase(),
|
|
69
|
+
originalSubtype: subtype,
|
|
70
|
+
parameters,
|
|
71
|
+
});
|
|
72
|
+
if (text[at] !== "/")
|
|
73
|
+
return made("");
|
|
74
|
+
at++;
|
|
75
|
+
const subtype = token() ?? "";
|
|
76
|
+
skipSpace();
|
|
77
|
+
while (text[at] === ";") {
|
|
78
|
+
at++;
|
|
79
|
+
skipSpace();
|
|
80
|
+
const attribute = token();
|
|
81
|
+
if (attribute === null || text[at] !== "=")
|
|
82
|
+
break;
|
|
83
|
+
at++;
|
|
84
|
+
const value = quoted() ?? token();
|
|
85
|
+
if (value === null)
|
|
86
|
+
break;
|
|
87
|
+
parameters.push({
|
|
88
|
+
attribute: attribute.toLowerCase(),
|
|
89
|
+
originalAttribute: attribute,
|
|
90
|
+
value,
|
|
91
|
+
});
|
|
92
|
+
skipSpace();
|
|
93
|
+
}
|
|
94
|
+
return made(subtype);
|
|
95
|
+
}
|
|
96
|
+
/** `needle` を `from` から探す。無ければ -1 */
|
|
97
|
+
function find(haystack, needle, from) {
|
|
98
|
+
const first = needle[0];
|
|
99
|
+
for (let at = haystack.indexOf(first, from); at !== -1; at = haystack.indexOf(first, at + 1)) {
|
|
100
|
+
if (at + needle.length > haystack.length)
|
|
101
|
+
return -1;
|
|
102
|
+
let same = true;
|
|
103
|
+
for (let k = 1; k < needle.length; k++) {
|
|
104
|
+
if (haystack[at + k] !== needle[k]) {
|
|
105
|
+
same = false;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (same)
|
|
110
|
+
return at;
|
|
111
|
+
}
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 中身を1つ読む。**ヘッダの終わり (空行) が無いものは null** — どこからが本体か
|
|
116
|
+
* 分からない。`multipart/mixed` なら中までデコードする (入れ子も同じ形)。
|
|
117
|
+
*
|
|
118
|
+
* @param part multipart の中の部品か。部品はヘッダの行でちょうど終わってよい (下の説明)
|
|
119
|
+
*/
|
|
120
|
+
export function readEntity(data, part = false) {
|
|
121
|
+
const headers = [];
|
|
122
|
+
let at = 0;
|
|
123
|
+
for (;;) {
|
|
124
|
+
// 中身が空の部品は、ヘッダの空行の CRLF が次の区切りの頭に食われて、ヘッダの行で
|
|
125
|
+
// ちょうど終わる (`--X\r\nContent-Type: a\r\n\r\n--X--`)。空の本体として受ける
|
|
126
|
+
// (web-bml の entity-parser はここで null を返し、モジュールごと捨てていた)
|
|
127
|
+
if (part && at === data.length && headers.length > 0)
|
|
128
|
+
break;
|
|
129
|
+
const end = find(data, CRLF, at);
|
|
130
|
+
if (end === -1)
|
|
131
|
+
return null;
|
|
132
|
+
if (end === at) {
|
|
133
|
+
at += 2;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const line = utf8.decode(data.subarray(at, end));
|
|
137
|
+
at = end + 2;
|
|
138
|
+
// 行頭が空白なら前の行の続き (折り返し)
|
|
139
|
+
if ((line[0] === " " || line[0] === "\t") && headers.length > 0) {
|
|
140
|
+
const last = headers[headers.length - 1];
|
|
141
|
+
last.value = `${last.value} ${line.trim()}`;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const colon = line.indexOf(":");
|
|
145
|
+
if (colon <= 0)
|
|
146
|
+
return null;
|
|
147
|
+
headers.push({
|
|
148
|
+
name: line.slice(0, colon).trim().toLowerCase(),
|
|
149
|
+
value: line.slice(colon + 1).trim(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const body = data.subarray(at);
|
|
153
|
+
const type = headers.find((header) => header.name === "content-type");
|
|
154
|
+
const media = type === undefined ? null : parseMediaType(type.value);
|
|
155
|
+
let parts = null;
|
|
156
|
+
if (media?.type === "multipart" && media.subtype === "mixed") {
|
|
157
|
+
const boundary = media.parameters.find((parameter) => parameter.attribute === "boundary")?.value;
|
|
158
|
+
parts = boundary === undefined ? null : splitParts(body, boundary);
|
|
159
|
+
}
|
|
160
|
+
return { headers, body, parts };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 区切りで切る。**閉じの区切り (`--XXX--`) まで揃っていないものは捨てる** —
|
|
164
|
+
* 最後の1つがどこで終わるか分からない
|
|
165
|
+
*/
|
|
166
|
+
function splitParts(body, boundary) {
|
|
167
|
+
const open = encoder.encode(`--${boundary}\r\n`);
|
|
168
|
+
const next = encoder.encode(`\r\n--${boundary}`);
|
|
169
|
+
let at = find(body, open, 0);
|
|
170
|
+
if (at === -1)
|
|
171
|
+
return null;
|
|
172
|
+
at += open.length;
|
|
173
|
+
const parts = [];
|
|
174
|
+
for (let from = at;;) {
|
|
175
|
+
const end = find(body, next, from);
|
|
176
|
+
if (end === -1)
|
|
177
|
+
return null;
|
|
178
|
+
const after = end + next.length;
|
|
179
|
+
// `--` が続けば閉じ、CRLF なら次の1つ。どちらでもなければ本体の中の似た並び
|
|
180
|
+
const close = body[after] === 0x2d && body[after + 1] === 0x2d;
|
|
181
|
+
if (!close && (body[after] !== CR || body[after + 1] !== LF)) {
|
|
182
|
+
from = end + 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const part = readEntity(body.subarray(at, end), true);
|
|
186
|
+
if (part === null)
|
|
187
|
+
return null;
|
|
188
|
+
parts.push(part);
|
|
189
|
+
if (close)
|
|
190
|
+
return parts;
|
|
191
|
+
at = after + 2;
|
|
192
|
+
from = at;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/dist/pes.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **1局に絞った TS から、映像と音声の PES を取り出す** (ライブを変換せずに送る経路。docs/stream.md#放送そのままmpeg-2を送る)。
|
|
3
|
+
*
|
|
4
|
+
* エンコードする経路ではここを ffmpeg がやっていた。変換せずに送ると TS がそのままブラウザへ来るので、
|
|
5
|
+
* 映像も音声も WASM のデコーダ (`wasm/decoder.c`) に渡す前に、
|
|
6
|
+
* こちらで PES に戻す。サーバが1局に絞ってある (`ServiceFilter`) ので、PAT に
|
|
7
|
+
* 載っている局は1つだけ — その PMT を読んで、映像1本と音声 (並んだ順) を取り出す。
|
|
8
|
+
*
|
|
9
|
+
* DOM も WASM も触らないので、ここだけ単体で確かめられる (`pes.test.ts`)。
|
|
10
|
+
*/
|
|
11
|
+
/** PTS・PCR の刻み (90kHz)。取り決めの時刻もこの刻み (docs/stream.md#websocket-プロトコル) */
|
|
12
|
+
export declare const CLOCK = 90000;
|
|
13
|
+
/** PTS の一周 (33ビット)。26.5 時間 */
|
|
14
|
+
export declare const WRAP: number;
|
|
15
|
+
export interface Pes {
|
|
16
|
+
pid: number;
|
|
17
|
+
kind: "video" | "audio";
|
|
18
|
+
/** 90kHz。無ければ null */
|
|
19
|
+
pts: number | null;
|
|
20
|
+
/** 90kHz。PTS と同じなら書かれないので、そのときは PTS を入れる */
|
|
21
|
+
dts: number | null;
|
|
22
|
+
/** PES の中身 (ES) */
|
|
23
|
+
data: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
/** PMT から分かったこと。**音声は並んだ順** — 画面の「何本目の音声」(`AudioTrack.stream`) と揃う */
|
|
26
|
+
export interface Streams {
|
|
27
|
+
video: number | null;
|
|
28
|
+
audio: number[];
|
|
29
|
+
}
|
|
30
|
+
/** 5バイトの PTS / DTS を読む (ISO/IEC 13818-1 2.4.3.7) */
|
|
31
|
+
export declare function readStamp(data: Uint8Array, at: number): number;
|
|
32
|
+
/** PES の頭を読んで中身を切り出す。**壊れていれば null** (頭の 00 00 01 が無いなど) */
|
|
33
|
+
export declare function parsePes(data: Uint8Array): {
|
|
34
|
+
pts: number | null;
|
|
35
|
+
dts: number | null;
|
|
36
|
+
data: Uint8Array;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* TS を食わせて PES を吐く。**選局し直したら作り直す** (前の局の切れ端を持ち越さない)。
|
|
40
|
+
*
|
|
41
|
+
* 映像は長さ 0 (次の頭で終わりが分かる) なので1枚ぶん遅れて出るが、貯めの中に収まる。
|
|
42
|
+
* 音声は長さが書いてあるので揃った時点で出す
|
|
43
|
+
*/
|
|
44
|
+
export declare class PesDemuxer {
|
|
45
|
+
private readonly packets;
|
|
46
|
+
/** 絞ってあるので局は1つだけのはず。万一複数あっても先頭を採る */
|
|
47
|
+
private readonly pmt;
|
|
48
|
+
private readonly partial;
|
|
49
|
+
/** いま分かっている ES。**変わったら `feed` の返りで知らせる** */
|
|
50
|
+
streams: Streams;
|
|
51
|
+
/** デコードする音声の PID。**選ばれた1本だけ**組み立てる (他は捨てる) */
|
|
52
|
+
audioPid: number | null;
|
|
53
|
+
/** 何本目の音声を取り出すか (`AudioTrack.stream`)。無ければ先頭に落とす */
|
|
54
|
+
audioIndex: number;
|
|
55
|
+
feed(chunk: Uint8Array): {
|
|
56
|
+
pes: Pes[];
|
|
57
|
+
changed: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** 音声を選び直す。**組み立てかけのものは捨てる** (前の音声の続きになる) */
|
|
60
|
+
selectAudio(index: number): void;
|
|
61
|
+
private readPmt;
|
|
62
|
+
private collect;
|
|
63
|
+
private finish;
|
|
64
|
+
}
|
|
65
|
+
/** ADTS の標本化周波数の表 (ISO/IEC 14496-3 表 1.18) */
|
|
66
|
+
export declare const ADTS_RATES: number[];
|
|
67
|
+
export interface AdtsFrame {
|
|
68
|
+
/** 頭 (7 か 9 バイト) ごと。WASM の AAC デコーダは ADTS のまま受け取る */
|
|
69
|
+
data: Uint8Array;
|
|
70
|
+
/** 90kHz */
|
|
71
|
+
pts: number;
|
|
72
|
+
sampleRate: number;
|
|
73
|
+
channels: number;
|
|
74
|
+
/** このコマの長さ (90kHz)。AAC は1コマ 1024 標本 */
|
|
75
|
+
duration: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* PES の中身を ADTS のコマに割る。**コマは PES をまたぐことがある**ので、余りは持ち越す。
|
|
79
|
+
*
|
|
80
|
+
* 時刻は PES の PTS を頭のコマに付け、続くコマには1コマぶんずつ足す。持ち越した
|
|
81
|
+
* 余りから始まるコマは、前の続きの時刻になる
|
|
82
|
+
*/
|
|
83
|
+
export declare class AdtsSplitter {
|
|
84
|
+
private rest;
|
|
85
|
+
private next;
|
|
86
|
+
feed(data: Uint8Array, pts: number | null): AdtsFrame[];
|
|
87
|
+
reset(): void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* **33ビットで一周する時刻を、手元の物差しの近くへ伸ばす。**
|
|
91
|
+
*
|
|
92
|
+
* PTS は 26.5 時間で 0 に戻る。受け側は「いま鳴らしている時刻」を一周をまたいでも
|
|
93
|
+
* 増え続ける数で持つので、届いた時刻は**それにいちばん近い周**に置き直して比べる
|
|
94
|
+
* (字幕・時計の通知・次の PES)
|
|
95
|
+
*
|
|
96
|
+
* @param pts 届いた時刻 (90kHz。一周の中)
|
|
97
|
+
* @param near 手元の物差し (90kHz。伸ばしたもの)
|
|
98
|
+
*/
|
|
99
|
+
export declare function unwrap(pts: number, near: number): number;
|
package/dist/pes.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **1局に絞った TS から、映像と音声の PES を取り出す** (ライブを変換せずに送る経路。docs/stream.md#放送そのままmpeg-2を送る)。
|
|
3
|
+
*
|
|
4
|
+
* エンコードする経路ではここを ffmpeg がやっていた。変換せずに送ると TS がそのままブラウザへ来るので、
|
|
5
|
+
* 映像も音声も WASM のデコーダ (`wasm/decoder.c`) に渡す前に、
|
|
6
|
+
* こちらで PES に戻す。サーバが1局に絞ってある (`ServiceFilter`) ので、PAT に
|
|
7
|
+
* 載っている局は1つだけ — その PMT を読んで、映像1本と音声 (並んだ順) を取り出す。
|
|
8
|
+
*
|
|
9
|
+
* DOM も WASM も触らないので、ここだけ単体で確かめられる (`pes.test.ts`)。
|
|
10
|
+
*/
|
|
11
|
+
import { concatBytes, u16 } from "./bytes.js";
|
|
12
|
+
import { PACKET, packetPid, PacketStream, PID_PAT, pmtStreams, PmtTracker, } from "./psi.js";
|
|
13
|
+
/** PTS・PCR の刻み (90kHz)。取り決めの時刻もこの刻み (docs/stream.md#websocket-プロトコル) */
|
|
14
|
+
export const CLOCK = 90_000;
|
|
15
|
+
/** PTS の一周 (33ビット)。26.5 時間 */
|
|
16
|
+
export const WRAP = 2 ** 33;
|
|
17
|
+
/** PMT の stream_type。**放送の映像は MPEG-2、音声は ADTS の AAC** (ARIB STD-B32) */
|
|
18
|
+
const STREAM_MPEG2 = 0x02;
|
|
19
|
+
const STREAM_AAC = 0x0f;
|
|
20
|
+
/** 5バイトの PTS / DTS を読む (ISO/IEC 13818-1 2.4.3.7) */
|
|
21
|
+
export function readStamp(data, at) {
|
|
22
|
+
return (((data[at] >> 1) & 0x07) * 2 ** 30 +
|
|
23
|
+
(data[at + 1] << 22) +
|
|
24
|
+
((data[at + 2] >> 1) << 15) +
|
|
25
|
+
(data[at + 3] << 7) +
|
|
26
|
+
(data[at + 4] >> 1));
|
|
27
|
+
}
|
|
28
|
+
/** PES の頭を読んで中身を切り出す。**壊れていれば null** (頭の 00 00 01 が無いなど) */
|
|
29
|
+
export function parsePes(data) {
|
|
30
|
+
if (data.length < 9 || data[0] !== 0 || data[1] !== 0 || data[2] !== 1)
|
|
31
|
+
return null;
|
|
32
|
+
const flags = data[7] >> 6;
|
|
33
|
+
const headerLength = data[8];
|
|
34
|
+
const start = 9 + headerLength;
|
|
35
|
+
if (start > data.length)
|
|
36
|
+
return null;
|
|
37
|
+
const length = u16(data, 4);
|
|
38
|
+
// 長さが書いてあればそこまで (後ろに詰め物が付くことがある)。0 は「次の頭まで」
|
|
39
|
+
const end = length === 0 ? data.length : Math.min(data.length, 6 + length);
|
|
40
|
+
const pts = flags & 0x02 && data.length >= 14 ? readStamp(data, 9) : null;
|
|
41
|
+
const dts = flags === 0x03 && data.length >= 19 ? readStamp(data, 14) : pts;
|
|
42
|
+
return { pts, dts, data: data.subarray(start, end) };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* TS を食わせて PES を吐く。**選局し直したら作り直す** (前の局の切れ端を持ち越さない)。
|
|
46
|
+
*
|
|
47
|
+
* 映像は長さ 0 (次の頭で終わりが分かる) なので1枚ぶん遅れて出るが、貯めの中に収まる。
|
|
48
|
+
* 音声は長さが書いてあるので揃った時点で出す
|
|
49
|
+
*/
|
|
50
|
+
export class PesDemuxer {
|
|
51
|
+
packets = new PacketStream();
|
|
52
|
+
/** 絞ってあるので局は1つだけのはず。万一複数あっても先頭を採る */
|
|
53
|
+
pmt = new PmtTracker();
|
|
54
|
+
partial = new Map();
|
|
55
|
+
/** いま分かっている ES。**変わったら `feed` の返りで知らせる** */
|
|
56
|
+
streams = { video: null, audio: [] };
|
|
57
|
+
/** デコードする音声の PID。**選ばれた1本だけ**組み立てる (他は捨てる) */
|
|
58
|
+
audioPid = null;
|
|
59
|
+
/** 何本目の音声を取り出すか (`AudioTrack.stream`)。無ければ先頭に落とす */
|
|
60
|
+
audioIndex = 0;
|
|
61
|
+
feed(chunk) {
|
|
62
|
+
const out = [];
|
|
63
|
+
let changed = false;
|
|
64
|
+
for (const packet of this.packets.feed(chunk)) {
|
|
65
|
+
// 誤りの印が立っているもの・スクランブルが残っているものは読まない
|
|
66
|
+
if (packet[1] & 0x80 || packet[3] & 0xc0)
|
|
67
|
+
continue;
|
|
68
|
+
const pid = packetPid(packet);
|
|
69
|
+
if (pid === PID_PAT || pid === this.pmt.pid) {
|
|
70
|
+
for (const section of this.pmt.feed(packet, pid))
|
|
71
|
+
changed = this.readPmt(section) || changed;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (pid === this.streams.video)
|
|
75
|
+
this.collect(packet, pid, "video", out);
|
|
76
|
+
else if (pid === this.audioPid)
|
|
77
|
+
this.collect(packet, pid, "audio", out);
|
|
78
|
+
}
|
|
79
|
+
return { pes: out, changed };
|
|
80
|
+
}
|
|
81
|
+
/** 音声を選び直す。**組み立てかけのものは捨てる** (前の音声の続きになる) */
|
|
82
|
+
selectAudio(index) {
|
|
83
|
+
this.audioIndex = index;
|
|
84
|
+
const next = this.streams.audio[index] ?? this.streams.audio[0] ?? null;
|
|
85
|
+
if (next !== this.audioPid && this.audioPid !== null)
|
|
86
|
+
this.partial.delete(this.audioPid);
|
|
87
|
+
this.audioPid = next;
|
|
88
|
+
}
|
|
89
|
+
readPmt(section) {
|
|
90
|
+
let video = null;
|
|
91
|
+
const audio = [];
|
|
92
|
+
for (const [type, pid] of pmtStreams(section)) {
|
|
93
|
+
if (type === STREAM_MPEG2 && video === null)
|
|
94
|
+
video = pid;
|
|
95
|
+
else if (type === STREAM_AAC)
|
|
96
|
+
audio.push(pid);
|
|
97
|
+
}
|
|
98
|
+
const same = video === this.streams.video &&
|
|
99
|
+
audio.length === this.streams.audio.length &&
|
|
100
|
+
audio.every((pid, i) => pid === this.streams.audio[i]);
|
|
101
|
+
if (same)
|
|
102
|
+
return false;
|
|
103
|
+
this.streams = { video, audio };
|
|
104
|
+
this.partial.clear();
|
|
105
|
+
this.selectAudio(this.audioIndex);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
collect(packet, pid, kind, out) {
|
|
109
|
+
const control = (packet[3] >> 4) & 0x03;
|
|
110
|
+
if (control === 0 || control === 2)
|
|
111
|
+
return;
|
|
112
|
+
let offset = 4;
|
|
113
|
+
if (control === 3)
|
|
114
|
+
offset += 1 + packet[4];
|
|
115
|
+
if (offset >= PACKET)
|
|
116
|
+
return;
|
|
117
|
+
const payload = packet.subarray(offset);
|
|
118
|
+
if (packet[1] & 0x40) {
|
|
119
|
+
// 次の頭が来た。**長さ 0 の PES (映像) はここで終わりが分かる**
|
|
120
|
+
this.finish(pid, kind, out);
|
|
121
|
+
const expected = payload.length >= 6 ? 6 + u16(payload, 4) : 0;
|
|
122
|
+
// 溜めるものは写す。パケットは届いた塊の上の窓で、塊は使い回される (psi.ts の PacketStream)
|
|
123
|
+
this.partial.set(pid, {
|
|
124
|
+
parts: [payload.slice()],
|
|
125
|
+
size: payload.length,
|
|
126
|
+
expected: expected === 6 ? 0 : expected,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const partial = this.partial.get(pid);
|
|
131
|
+
// 頭を見ていない続きは捨てる (途中から受け取った)
|
|
132
|
+
if (partial === undefined)
|
|
133
|
+
return;
|
|
134
|
+
partial.parts.push(payload.slice());
|
|
135
|
+
partial.size += payload.length;
|
|
136
|
+
}
|
|
137
|
+
const partial = this.partial.get(pid);
|
|
138
|
+
if (partial !== undefined &&
|
|
139
|
+
partial.expected > 0 &&
|
|
140
|
+
partial.size >= partial.expected) {
|
|
141
|
+
this.finish(pid, kind, out);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finish(pid, kind, out) {
|
|
145
|
+
const partial = this.partial.get(pid);
|
|
146
|
+
if (partial === undefined)
|
|
147
|
+
return;
|
|
148
|
+
this.partial.delete(pid);
|
|
149
|
+
const parsed = parsePes(concatBytes(partial.parts));
|
|
150
|
+
if (parsed === null || parsed.data.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
out.push({ pid, kind, ...parsed });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** ADTS の標本化周波数の表 (ISO/IEC 14496-3 表 1.18) */
|
|
156
|
+
export const ADTS_RATES = [
|
|
157
|
+
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025,
|
|
158
|
+
8000, 7350,
|
|
159
|
+
];
|
|
160
|
+
/**
|
|
161
|
+
* PES の中身を ADTS のコマに割る。**コマは PES をまたぐことがある**ので、余りは持ち越す。
|
|
162
|
+
*
|
|
163
|
+
* 時刻は PES の PTS を頭のコマに付け、続くコマには1コマぶんずつ足す。持ち越した
|
|
164
|
+
* 余りから始まるコマは、前の続きの時刻になる
|
|
165
|
+
*/
|
|
166
|
+
export class AdtsSplitter {
|
|
167
|
+
rest = new Uint8Array(0);
|
|
168
|
+
next = null;
|
|
169
|
+
feed(data, pts) {
|
|
170
|
+
// 余りが無いときだけ PES の時刻に合わせ直す (余りがあれば頭のコマはその続き)
|
|
171
|
+
if (pts !== null && this.rest.length === 0)
|
|
172
|
+
this.next = pts;
|
|
173
|
+
const buffer = this.rest.length === 0 ? data : concatBytes([this.rest, data]);
|
|
174
|
+
const out = [];
|
|
175
|
+
let at = 0;
|
|
176
|
+
while (at + 7 <= buffer.length) {
|
|
177
|
+
if (buffer[at] !== 0xff || (buffer[at + 1] & 0xf6) !== 0xf0) {
|
|
178
|
+
at++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const length = ((buffer[at + 3] & 0x03) << 11) |
|
|
182
|
+
(buffer[at + 4] << 3) |
|
|
183
|
+
(buffer[at + 5] >> 5);
|
|
184
|
+
if (length < 7) {
|
|
185
|
+
at++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (at + length > buffer.length)
|
|
189
|
+
break;
|
|
190
|
+
const rate = ADTS_RATES[(buffer[at + 2] >> 2) & 0x0f] ?? 48000;
|
|
191
|
+
const channels = ((buffer[at + 2] & 0x01) << 2) | (buffer[at + 3] >> 6);
|
|
192
|
+
const duration = Math.round((1024 * CLOCK) / rate);
|
|
193
|
+
if (this.next !== null) {
|
|
194
|
+
out.push({
|
|
195
|
+
data: buffer.slice(at, at + length),
|
|
196
|
+
pts: this.next,
|
|
197
|
+
sampleRate: rate,
|
|
198
|
+
// 0 は「中身に書いてある」(PCE)。放送のデュアルモノはここに来るので、2本とみなす
|
|
199
|
+
channels: channels === 0 ? 2 : channels,
|
|
200
|
+
duration,
|
|
201
|
+
});
|
|
202
|
+
this.next += duration;
|
|
203
|
+
}
|
|
204
|
+
at += length;
|
|
205
|
+
}
|
|
206
|
+
this.rest = buffer.slice(at);
|
|
207
|
+
// 同期が取れないまま溜め込まない
|
|
208
|
+
if (this.rest.length > 8192)
|
|
209
|
+
this.rest = new Uint8Array(0);
|
|
210
|
+
if (pts !== null && this.rest.length === 0 && out.length === 0)
|
|
211
|
+
this.next = pts;
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
reset() {
|
|
215
|
+
this.rest = new Uint8Array(0);
|
|
216
|
+
this.next = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* **33ビットで一周する時刻を、手元の物差しの近くへ伸ばす。**
|
|
221
|
+
*
|
|
222
|
+
* PTS は 26.5 時間で 0 に戻る。受け側は「いま鳴らしている時刻」を一周をまたいでも
|
|
223
|
+
* 増え続ける数で持つので、届いた時刻は**それにいちばん近い周**に置き直して比べる
|
|
224
|
+
* (字幕・時計の通知・次の PES)
|
|
225
|
+
*
|
|
226
|
+
* @param pts 届いた時刻 (90kHz。一周の中)
|
|
227
|
+
* @param near 手元の物差し (90kHz。伸ばしたもの)
|
|
228
|
+
*/
|
|
229
|
+
export function unwrap(pts, near) {
|
|
230
|
+
return pts + Math.round((near - pts) / WRAP) * WRAP;
|
|
231
|
+
}
|