@synapse-sdk/media 0.3.0 → 0.3.3

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/dist/annexb.js DELETED
@@ -1,227 +0,0 @@
1
- /** NAL unit types this package cares about (H.264 Table 7-1). */
2
- export const NAL_TYPE = { SLICE: 1, SLICE_DPA: 2, SLICE_DPB: 3, SLICE_DPC: 4, IDR: 5, SEI: 6, SPS: 7, PPS: 8, AUD: 9 };
3
- /** SEI payload type of a recovery point (H.264 D.1.8). */
4
- export const SEI_RECOVERY_POINT = 6;
5
- /** `nal_unit_type` (low 5 bits of the header byte); 0 for an empty NAL. */
6
- export function nalType(n) { return n.bytes.length ? n.bytes[0] & 0x1f : 0; }
7
- /** Payload after the 1-byte NAL header (escaped RBSP). */
8
- export function nalPayload(n) { return n.bytes.subarray(1); }
9
- /** A VCL NAL type: a coded slice (types 1-5). */
10
- export function isVclType(t) { return t >= NAL_TYPE.SLICE && t <= NAL_TYPE.IDR; }
11
- export function nalIsVcl(n) { return isVclType(nalType(n)); }
12
- /** Start code plus NAL unit, as it sits in the stream (a view). */
13
- export function nalWithStartCode(buf, n) { return buf.subarray(n.offset, n.offset + n.startCodeLen + n.bytes.length); }
14
- /** Next Annex-B start code (`00 00 01` or `00 00 00 01`) at or after `from`: `[offset, len]`. */
15
- export function findStartCode(buf, from) {
16
- const n = buf.length;
17
- for (let i = from; i + 3 <= n; i++) {
18
- if (buf[i] === 0 && buf[i + 1] === 0) {
19
- if (buf[i + 2] === 1)
20
- return [i, 3];
21
- if (i + 4 <= n && buf[i + 2] === 0 && buf[i + 3] === 1)
22
- return [i, 4];
23
- }
24
- }
25
- return undefined;
26
- }
27
- /**
28
- * The NAL units of an Annex-B buffer, in order. Bytes before the first start code are skipped;
29
- * trailing zero bytes (`trailing_zero_8bits`) stay attached to the NAL before them, which is
30
- * harmless for type/flag decisions and keeps the bytes of a walked AU identical to the input.
31
- */
32
- export function nalUnits(buf) {
33
- const out = [];
34
- let sc = findStartCode(buf, 0);
35
- while (sc) {
36
- const [s, l] = sc;
37
- const start = s + l;
38
- const next = findStartCode(buf, start);
39
- out.push({ offset: s, startCodeLen: l, bytes: buf.subarray(start, next ? next[0] : buf.length) });
40
- sc = next;
41
- }
42
- return out;
43
- }
44
- /** The SPS + PPS NAL units of an access unit (with their start codes, in stream order), if it carries any. Defined ⇒ the AU deserves the `CONFIG` flag. */
45
- export function auParamSets(au) {
46
- const parts = [];
47
- for (const n of nalUnits(au)) {
48
- const t = nalType(n);
49
- if (t === NAL_TYPE.SPS || t === NAL_TYPE.PPS)
50
- parts.push(nalWithStartCode(au, n));
51
- }
52
- return parts.length ? concatBytes(parts) : undefined;
53
- }
54
- /**
55
- * The sync points an Annex-B access unit offers.
56
- * - `idr`: an IDR slice (NAL type 5) is present → instantaneous start point (`KEY`).
57
- * - `recovery`: a recovery point SEI (NAL 6, payload type 6) is present → gradual start point,
58
- * clean one refresh period later (`RECOVERY`; x264 and nvenc both emit it at each intra-refresh sweep).
59
- * Decided from the bitstream rather than the encoder's keyframe flag, which libx264 raises for
60
- * both kinds and nvenc only for IDRs.
61
- */
62
- export function auSyncPoints(au) {
63
- let idr = false, recovery = false;
64
- for (const n of nalUnits(au)) {
65
- const t = nalType(n);
66
- if (t === NAL_TYPE.IDR)
67
- idr = true;
68
- else if (t === NAL_TYPE.SEI)
69
- recovery ||= seiHasRecoveryPoint(nalPayload(n));
70
- }
71
- return { idr, recovery };
72
- }
73
- /** An IDR is present: the AU is a keyframe in the `KEY` sense. */
74
- export function isKeyframe(au) { return auSyncPoints(au).idr; }
75
- /** The AU carries at least one coded slice (otherwise it is parameter sets / SEI only). */
76
- export function hasVcl(au) { return nalUnits(au).some(nalIsVcl); }
77
- /** Whether an SEI NAL payload (after the NAL header byte) contains a recovery point message (payload type 6). */
78
- export function seiHasRecoveryPoint(payload) { return seiPayloadTypes(payload).includes(SEI_RECOVERY_POINT); }
79
- /** The payload types of the SEI messages in an SEI NAL payload (D.1.1), in order. Emulation-prevention bytes are removed first. */
80
- export function seiPayloadTypes(payload) {
81
- const rbsp = rbspUnescape(payload);
82
- const out = [];
83
- let i = 0;
84
- // 0x80 is the rbsp_trailing_bits byte that ends the SEI NAL.
85
- while (i < rbsp.length && rbsp[i] !== 0x80) {
86
- // payloadType and payloadSize: runs of 0xFF each adding 255, then a final byte.
87
- let ty = 0;
88
- while (i < rbsp.length && rbsp[i] === 0xff) {
89
- ty += 255;
90
- i++;
91
- }
92
- if (i >= rbsp.length)
93
- break;
94
- ty += rbsp[i];
95
- i++;
96
- let sz = 0;
97
- while (i < rbsp.length && rbsp[i] === 0xff) {
98
- sz += 255;
99
- i++;
100
- }
101
- if (i >= rbsp.length)
102
- break;
103
- sz += rbsp[i];
104
- i++;
105
- out.push(ty);
106
- i += sz;
107
- }
108
- return out;
109
- }
110
- /** Strip H.264 emulation-prevention bytes (the `03` in `00 00 03`), giving the RBSP. */
111
- export function rbspUnescape(nal) {
112
- const out = new Uint8Array(nal.length);
113
- let o = 0, zeros = 0;
114
- for (const b of nal) {
115
- if (zeros >= 2 && b === 3) {
116
- zeros = 0;
117
- continue;
118
- }
119
- out[o++] = b;
120
- zeros = b === 0 ? zeros + 1 : 0;
121
- }
122
- return out.subarray(0, o);
123
- }
124
- /** The SPS of an AU (NAL bytes without start code), if present. */
125
- export function findSps(au) {
126
- for (const n of nalUnits(au))
127
- if (nalType(n) === NAL_TYPE.SPS)
128
- return n.bytes;
129
- return undefined;
130
- }
131
- /** The WebCodecs / RFC 6381 codec string for an AU that carries an SPS: `avc1.` + profile_idc + constraint flags + level_idc as hex (e.g. `avc1.42c01e`). */
132
- export function codecStringFromSps(au) {
133
- const sps = findSps(au);
134
- if (!sps || sps.length < 4)
135
- return undefined;
136
- const hex = (b) => b.toString(16).padStart(2, "0");
137
- return `avc1.${hex(sps[1])}${hex(sps[2])}${hex(sps[3])}`;
138
- }
139
- /** `parts` joined into one new buffer (a single part is copied too, so the result never aliases the input). */
140
- export function concatBytes(parts) {
141
- let n = 0;
142
- for (const p of parts)
143
- n += p.length;
144
- const out = new Uint8Array(n);
145
- let o = 0;
146
- for (const p of parts) {
147
- out.set(p, o);
148
- o += p.length;
149
- }
150
- return out;
151
- }
152
- /**
153
- * Splits a continuous Annex-B byte stream (a file, an encoder pipe) into access units. Rule
154
- * (dylithium's reader loop): an AU ends with its coded slice; the next NAL of any kind starts a
155
- * new AU, so non-VCL NALs (SPS, PPS, SEI, AUD) are attached to the picture that follows them.
156
- * Sufficient for one-slice-per-picture streams (x264 `slices=1`, nvenc, WebCodecs), which is what
157
- * the encoder options enforce.
158
- */
159
- export class AuSplitter {
160
- #pending = new Uint8Array(0);
161
- #au = [];
162
- #auHasVcl = false;
163
- /** Feeds bytes; returns every access unit completed by them (in order). */
164
- push(bytes) {
165
- this.#pending = this.#pending.length ? concatBytes([this.#pending, bytes]) : bytes.slice();
166
- const out = [];
167
- const p = this.#pending;
168
- let pos = 0;
169
- for (;;) {
170
- const sc0 = findStartCode(p, pos);
171
- if (!sc0)
172
- break; // no start code yet (maybe a partial one): keep the bytes
173
- const [s0, l0] = sc0;
174
- // The NAL is complete only once the next start code is visible.
175
- const sc1 = findStartCode(p, s0 + l0);
176
- if (!sc1) {
177
- pos = s0;
178
- break;
179
- }
180
- const isVcl = s0 + l0 < p.length && isVclType(p[s0 + l0] & 0x1f);
181
- if (this.#auHasVcl)
182
- out.push(this.#take()); // the previous AU already has its slice: this NAL belongs to the next one
183
- this.#au.push(p.slice(s0, sc1[0]));
184
- if (isVcl)
185
- this.#auHasVcl = true;
186
- pos = sc1[0];
187
- }
188
- this.#pending = pos > 0 ? p.slice(pos) : p;
189
- return out;
190
- }
191
- /** End of stream: the last NAL (whose end no start code marks) closes the last AU, or starts one more if the current AU already has its slice. */
192
- finish() {
193
- const out = [];
194
- const sc0 = findStartCode(this.#pending, 0);
195
- if (sc0) {
196
- if (this.#auHasVcl)
197
- out.push(this.#take());
198
- this.#au.push(this.#pending.slice(sc0[0]));
199
- }
200
- if (this.#au.length)
201
- out.push(this.#take());
202
- this.#pending = new Uint8Array(0);
203
- return out;
204
- }
205
- #take() {
206
- const au = this.#au.length === 1 ? this.#au[0] : concatBytes(this.#au);
207
- this.#au = [];
208
- this.#auHasVcl = false;
209
- return au;
210
- }
211
- }
212
- /** Splits a whole Annex-B buffer into access units (see {@link AuSplitter}). */
213
- export function splitAccessUnits(stream) {
214
- const sp = new AuSplitter();
215
- const out = sp.push(stream);
216
- out.push(...sp.finish());
217
- return out;
218
- }
219
- /**
220
- * A `MediaFrame` for an H.264 Annex-B access unit with `key`/`config`/`recovery` derived from the
221
- * NAL units (IDR present / SPS+PPS present / recovery-point SEI present), so the header flags
222
- * always agree with the bytes (R27 §2 publisher rule). `tUs` is the capture time (0 = stamp at publish).
223
- */
224
- export function mediaFrameFromAnnexb(au, tUs = 0) {
225
- const { idr, recovery } = auSyncPoints(au);
226
- return { data: au, tUs, key: idr, config: auParamSets(au) !== undefined, recovery };
227
- }