image-to-pixel 3.0.1 → 3.2.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.
@@ -1,131 +1,320 @@
1
- // Minimal MP4 (ISO BMFF) muxer for one H.264 video track. Everything is buffered in memory and
2
- // written with the 'moov' box first ("fast start"), so the file plays and seeks while downloading.
3
- // Input: AVCC samples (length-prefixed NAL units, as WebCodecs' VideoEncoder emits with
4
- // avc.format 'avc') plus the AVCDecoderConfigurationRecord ('description').
5
-
6
- const ENC = new TextEncoder();
7
-
8
- class Writer {
9
- constructor() { this.parts = []; this.size = 0; }
10
- u8(v) { return this.bytes([v & 255]); }
11
- u16(v) { return this.bytes([(v >>> 8) & 255, v & 255]); }
12
- u32(v) { return this.bytes([(v >>> 24) & 255, (v >>> 16) & 255, (v >>> 8) & 255, v & 255]); }
13
- str(s) { return this.bytes(ENC.encode(s)); }
14
- bytes(b) { const a = b instanceof Uint8Array ? b : Uint8Array.from(b); this.parts.push(a); this.size += a.length; return this; }
15
- zeros(n) { return this.bytes(new Uint8Array(n)); }
16
- out() { const r = new Uint8Array(this.size); let o = 0; for (const p of this.parts) { r.set(p, o); o += p.length; } return r; }
17
- }
18
-
19
- /** A box: 4-byte size, 4-char type, then the body produced by fill(w). */
20
- function box(type, fill) {
21
- const w = new Writer();
22
- fill(w);
23
- const body = w.out();
24
- const head = new Writer().u32(body.length + 8).str(type).out();
25
- const r = new Uint8Array(body.length + 8); r.set(head); r.set(body, 8);
26
- return r;
27
- }
28
- const fullBox = (type, version, flags, fill) => box(type, w => { w.u8(version).u8(flags >> 16).u8(flags >> 8).u8(flags); fill(w); });
29
- const MATRIX = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000];
30
-
31
- /**
32
- * @param {{ width: number, height: number, timescale?: number }} opts
33
- */
34
- export function createMp4Muxer({ width, height, timescale = 90000 }) {
35
- const samples = []; // { data, key, ts, dur } with ts/dur in microseconds
36
- let description = null;
37
-
38
- return {
39
- /** Use with a VideoEncoder `output` callback. */
40
- addChunk(chunk, meta) {
41
- if (meta && meta.decoderConfig && meta.decoderConfig.description && !description) {
42
- const d = meta.decoderConfig.description;
43
- description = new Uint8Array(d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength));
44
- }
45
- const data = new Uint8Array(chunk.byteLength);
46
- chunk.copyTo(data);
47
- samples.push({ data, key: chunk.type === 'key', ts: chunk.timestamp, dur: chunk.duration || 0 });
48
- },
49
- /** Lower-level: add an AVCC sample directly. ts/dur in microseconds. */
50
- addSample(data, { key, timestamp, duration }) { samples.push({ data, key: !!key, ts: timestamp, dur: duration || 0 }); },
51
- /** AVCDecoderConfigurationRecord ('avcC' contents). */
52
- setDescription(d) { description = d; },
53
-
54
- /** @returns {Uint8Array} the .mp4 file */
55
- finish() {
56
- if (!samples.length) throw new Error('No video frames were encoded');
57
- if (!description) throw new Error('The encoder gave no H.264 decoder configuration (avcC)');
58
- // Samples arrive in decode order with presentation timestamps (B-frames make them non-monotonic).
59
- // Decode times are the sorted presentation times; 'ctts' holds the difference when there is one.
60
- const t = us => Math.round(us * timescale / 1e6);
61
- const pts = samples.map(s => t(s.ts));
62
- const dts = pts.slice().sort((a, b) => a - b);
63
- const lastDur = t(samples[samples.length - 1].dur) || (dts.length > 1 ? dts[dts.length - 1] - dts[dts.length - 2] : t(33333));
64
- const durs = dts.map((d, i) => (i + 1 < dts.length ? dts[i + 1] - d : lastDur));
65
- const offsets = pts.map((p, i) => p - dts[i]);
66
- const hasOffsets = offsets.some(o => o !== 0);
67
- const mediaDur = durs.reduce((a, b) => a + b, 0);
68
- const movieDur = Math.round(mediaDur * 1000 / timescale);
69
-
70
- const stts = [];
71
- for (const d of durs) { const last = stts[stts.length - 1]; if (last && last[1] === d) last[0]++; else stts.push([1, d]); }
72
- const ctts = [];
73
- for (const o of offsets) { const last = ctts[ctts.length - 1]; if (last && last[1] === o) last[0]++; else ctts.push([1, o]); }
74
- const keys = samples.map((s, i) => (s.key ? i + 1 : 0)).filter(Boolean);
75
- const dataSize = samples.reduce((a, s) => a + s.data.length, 0);
76
-
77
- const ftyp = box('ftyp', w => w.str('isom').u32(512).str('isom').str('iso2').str('avc1').str('mp41'));
78
- const buildMoov = chunkOffset => box('moov', w => {
79
- w.bytes(fullBox('mvhd', 0, 0, b => {
80
- b.u32(0).u32(0).u32(1000).u32(movieDur).u32(0x00010000).u16(0x0100).zeros(10);
81
- MATRIX.forEach(v => b.u32(v)); b.zeros(24).u32(2);
82
- }));
83
- w.bytes(box('trak', tr => {
84
- tr.bytes(fullBox('tkhd', 0, 3, b => {
85
- b.u32(0).u32(0).u32(1).u32(0).u32(movieDur).zeros(8).u16(0).u16(0).u16(0).u16(0);
86
- MATRIX.forEach(v => b.u32(v)); b.u32(width << 16).u32(height << 16);
87
- }));
88
- tr.bytes(box('mdia', md => {
89
- md.bytes(fullBox('mdhd', 0, 0, b => b.u32(0).u32(0).u32(timescale).u32(mediaDur).u16(0x55c4).u16(0)));
90
- md.bytes(fullBox('hdlr', 0, 0, b => b.u32(0).str('vide').zeros(12).str('VideoHandler').u8(0)));
91
- md.bytes(box('minf', mi => {
92
- mi.bytes(fullBox('vmhd', 0, 1, b => b.u16(0).u16(0).u16(0).u16(0)));
93
- mi.bytes(box('dinf', di => di.bytes(fullBox('dref', 0, 0, b => { b.u32(1); b.bytes(fullBox('url ', 0, 1, () => { })); }))));
94
- mi.bytes(box('stbl', st => {
95
- st.bytes(fullBox('stsd', 0, 0, b => {
96
- b.u32(1);
97
- b.bytes(box('avc1', a => {
98
- a.zeros(6).u16(1).zeros(16).u16(width).u16(height).u32(0x00480000).u32(0x00480000).u32(0).u16(1);
99
- const name = ENC.encode('image-to-pixel').slice(0, 31);
100
- a.u8(name.length).bytes(name).zeros(31 - name.length).u16(0x0018).u16(0xffff);
101
- a.bytes(box('avcC', c => c.bytes(description)));
102
- }));
103
- }));
104
- st.bytes(fullBox('stts', 0, 0, b => { b.u32(stts.length); for (const [n, d] of stts) b.u32(n).u32(d); }));
105
- // version 1: signed offsets, so negative composition offsets are allowed
106
- if (hasOffsets) st.bytes(fullBox('ctts', 1, 0, b => { b.u32(ctts.length); for (const [n, o] of ctts) b.u32(n).u32(o >>> 0); }));
107
- if (keys.length < samples.length) st.bytes(fullBox('stss', 0, 0, b => { b.u32(keys.length); for (const k of keys) b.u32(k); }));
108
- st.bytes(fullBox('stsc', 0, 0, b => b.u32(1).u32(1).u32(samples.length).u32(1)));
109
- st.bytes(fullBox('stsz', 0, 0, b => { b.u32(0).u32(samples.length); for (const s of samples) b.u32(s.data.length); }));
110
- st.bytes(fullBox('stco', 0, 0, b => b.u32(1).u32(chunkOffset)));
111
- }));
112
- }));
113
- }));
114
- }));
115
- });
116
- // the moov size doesn't depend on the offset value, so measure once and build again
117
- const moovSize = buildMoov(0).length;
118
- const offset = ftyp.length + moovSize + 8;
119
- if (offset + dataSize > 0xffffffff) throw new Error('Video is larger than 4 GB');
120
- const moov = buildMoov(offset);
121
- const out = new Uint8Array(offset + dataSize);
122
- out.set(ftyp, 0); out.set(moov, ftyp.length);
123
- const dv = new DataView(out.buffer);
124
- dv.setUint32(ftyp.length + moov.length, dataSize + 8);
125
- out.set(ENC.encode('mdat'), ftyp.length + moov.length + 4);
126
- let o = offset;
127
- for (const s of samples) { out.set(s.data, o); o += s.data.length; }
128
- return out;
129
- },
130
- };
131
- }
1
+ // Minimal MP4 (ISO BMFF) muxer for one H.264 video track and, optionally, one AAC or Opus audio track.
2
+ // Everything is buffered in memory and written with the 'moov' box first ("fast start"), so the file
3
+ // plays and seeks while downloading.
4
+ // Video input: AVCC samples (length-prefixed NAL units, as WebCodecs' VideoEncoder emits with
5
+ // avc.format 'avc') plus the AVCDecoderConfigurationRecord ('description').
6
+ // Audio input: raw AAC frames plus the AudioSpecificConfig ('description'), as AudioEncoder emits with
7
+ // aac.format 'aac'; or Opus packets.
8
+
9
+ import { parseOpusHead } from './audio.js';
10
+
11
+ const ENC = new TextEncoder();
12
+
13
+ class Writer {
14
+ constructor() { this.parts = []; this.size = 0; }
15
+ u8(v) { return this.bytes([v & 255]); }
16
+ u16(v) { return this.bytes([(v >>> 8) & 255, v & 255]); }
17
+ u32(v) { return this.bytes([(v >>> 24) & 255, (v >>> 16) & 255, (v >>> 8) & 255, v & 255]); }
18
+ str(s) { return this.bytes(ENC.encode(s)); }
19
+ bytes(b) { const a = b instanceof Uint8Array ? b : Uint8Array.from(b); this.parts.push(a); this.size += a.length; return this; }
20
+ zeros(n) { return this.bytes(new Uint8Array(n)); }
21
+ out() { const r = new Uint8Array(this.size); let o = 0; for (const p of this.parts) { r.set(p, o); o += p.length; } return r; }
22
+ }
23
+
24
+ /** A box: 4-byte size, 4-char type, then the body produced by fill(w). */
25
+ function box(type, fill) {
26
+ const w = new Writer();
27
+ fill(w);
28
+ const body = w.out();
29
+ const head = new Writer().u32(body.length + 8).str(type).out();
30
+ const r = new Uint8Array(body.length + 8); r.set(head); r.set(body, 8);
31
+ return r;
32
+ }
33
+ const fullBox = (type, version, flags, fill) => box(type, w => { w.u8(version).u8(flags >> 16).u8(flags >> 8).u8(flags); fill(w); });
34
+ const MATRIX = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000];
35
+
36
+ const toBytes = d => new Uint8Array(d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength));
37
+
38
+ /** MPEG-4 descriptor: tag, one-byte length (every descriptor here is under 128 bytes), body. */
39
+ const descriptor = (tag, body) => [tag, body.length, ...body];
40
+ const be32 = v => [(v >>> 24) & 255, (v >>> 16) & 255, (v >>> 8) & 255, v & 255];
41
+
42
+ /** 'esds' for AAC: ES_Descriptor > DecoderConfigDescriptor > DecoderSpecificInfo (the AudioSpecificConfig). */
43
+ function esds(asc, bitrate) {
44
+ const decSpecific = descriptor(0x05, [...asc]);
45
+ // 0x40 = MPEG-4 audio, 0x15 = audio stream type; buffer size 0; max and average bitrate
46
+ const decConfig = descriptor(0x04, [0x40, 0x15, 0, 0, 0, ...be32(bitrate), ...be32(bitrate), ...decSpecific]);
47
+ const sl = descriptor(0x06, [0x02]);
48
+ return fullBox('esds', 0, 0, b => b.bytes(descriptor(0x03, [0x00, 0x02, 0x00, ...decConfig, ...sl]))); // ES_ID 2
49
+ }
50
+
51
+ // ---- H.264 decoder configuration (avcC)
52
+ // Some encoders hand WebCodecs a malformed avcC: Chrome on Windows (Media Foundation) has been seen
53
+ // repeating each parameter set's NAL header byte, which strict players (Windows Media Player, QuickTime)
54
+ // refuse. The encoder also writes SPS/PPS into the keyframes, so the muxer checks the record and, if it
55
+ // doesn't hold together, rebuilds it from those in-band parameter sets.
56
+
57
+ const HIGH_PROFILES = new Set([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135]);
58
+
59
+ /** Is this a well-formed AVCDecoderConfigurationRecord whose SPS agrees with its header? */
60
+ export function isValidAvcC(d) {
61
+ if (!d || d.length < 7 || d[0] !== 1) return false;
62
+ let o = 5;
63
+ const nSps = d[o++] & 31;
64
+ if (!nSps) return false;
65
+ for (let i = 0; i < nSps; i++) {
66
+ if (o + 2 > d.length) return false;
67
+ const len = (d[o] << 8) | d[o + 1]; o += 2;
68
+ if (len < 4 || o + len > d.length) return false;
69
+ const sps = d.subarray(o, o + len);
70
+ if ((sps[0] & 31) !== 7) return false;
71
+ if (i === 0 && (sps[1] !== d[1] || sps[2] !== d[2] || sps[3] !== d[3])) return false; // profile, constraints, level
72
+ o += len;
73
+ }
74
+ if (o >= d.length) return false;
75
+ const nPps = d[o++];
76
+ if (!nPps) return false;
77
+ for (let i = 0; i < nPps; i++) {
78
+ if (o + 2 > d.length) return false;
79
+ const len = (d[o] << 8) | d[o + 1]; o += 2;
80
+ if (len < 1 || o + len > d.length || (d[o] & 31) !== 8) return false;
81
+ o += len;
82
+ }
83
+ return true;
84
+ }
85
+
86
+ /** NAL units of an AVCC sample (4-byte lengths). */
87
+ function nalUnits(data) {
88
+ const out = [];
89
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
90
+ for (let o = 0; o + 4 <= data.length;) {
91
+ const len = dv.getUint32(o); o += 4;
92
+ if (!len || o + len > data.length) break;
93
+ out.push(data.subarray(o, o + len)); o += len;
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /** chroma_format_idc and bit depths from an SPS (High profiles), for the avcC extension. */
99
+ function spsChroma(sps) {
100
+ const rbsp = [];
101
+ for (let i = 1; i < sps.length; i++) { // skip the NAL header; drop emulation-prevention bytes
102
+ if (i >= 3 && sps[i] === 3 && sps[i - 1] === 0 && sps[i - 2] === 0) continue;
103
+ rbsp.push(sps[i]);
104
+ }
105
+ let bit = 24; // after profile_idc, constraint flags, level_idc
106
+ const u1 = () => { const v = (rbsp[bit >> 3] >> (7 - (bit & 7))) & 1; bit++; return v; };
107
+ const ue = () => { let z = 0; while (bit < rbsp.length * 8 && !u1()) z++; let v = 0; for (let i = 0; i < z; i++) v = v * 2 + u1(); return (2 ** z) - 1 + v; };
108
+ ue(); // seq_parameter_set_id
109
+ const chroma = ue();
110
+ if (chroma === 3) u1(); // separate_colour_plane_flag
111
+ return { chroma, lumaBits: ue(), chromaBits: ue() };
112
+ }
113
+
114
+ /** Build an avcC from the SPS/PPS carried inside a keyframe, or null if it has none. */
115
+ export function avcCFromSample(data) {
116
+ const nals = nalUnits(data);
117
+ const sps = nals.filter(n => (n[0] & 31) === 7);
118
+ const pps = nals.filter(n => (n[0] & 31) === 8);
119
+ if (!sps.length || !pps.length || sps[0].length < 4) return null;
120
+ const out = [1, sps[0][1], sps[0][2], sps[0][3], 0xff, 0xe0 | sps.length];
121
+ for (const s of sps) out.push(s.length >> 8, s.length & 255, ...s);
122
+ out.push(pps.length);
123
+ for (const p of pps) out.push(p.length >> 8, p.length & 255, ...p);
124
+ if (HIGH_PROFILES.has(sps[0][1])) {
125
+ const c = spsChroma(sps[0]);
126
+ out.push(0xfc | (c.chroma & 3), 0xf8 | (c.lumaBits & 7), 0xf8 | (c.chromaBits & 7), 0);
127
+ }
128
+ return Uint8Array.from(out);
129
+ }
130
+
131
+ /** Collapse a list of numbers into [count, value] runs (stts / ctts). */
132
+ function runs(list) {
133
+ const out = [];
134
+ for (const v of list) { const last = out[out.length - 1]; if (last && last[1] === v) last[0]++; else out.push([1, v]); }
135
+ return out;
136
+ }
137
+
138
+ /**
139
+ * @param {{ width: number, height: number, timescale?: number,
140
+ * audio?: { codec: 'aac' | 'opus', sampleRate: number, numberOfChannels: number, bitrate?: number } }} opts
141
+ * audio: add a second track. Feed it with addAudioChunk() (AudioEncoder output) or addAudioSample().
142
+ */
143
+ export function createMp4Muxer({ width, height, timescale = 90000, audio = null }) {
144
+ const samples = []; // video: { data, key, ts, dur }, ts/dur in microseconds
145
+ const audioSamples = []; // audio: { data, ts, dur }, microseconds
146
+ let description = null;
147
+ let audioDescription = null;
148
+
149
+ /** Durations, sample entry and length of the audio track; null when there is none. */
150
+ function audioTrack() {
151
+ if (!audio || !audioSamples.length) return null;
152
+ const rate = audio.sampleRate;
153
+ const ch = audio.numberOfChannels;
154
+ const t0 = audioSamples[0].ts;
155
+ const starts = audioSamples.map(s => Math.round((s.ts - t0) * rate / 1e6));
156
+ const lastDur = Math.round(audioSamples[audioSamples.length - 1].dur * rate / 1e6) || (audio.codec === 'aac' ? 1024 : 960);
157
+ const durs = starts.map((s, i) => (i + 1 < starts.length ? Math.max(1, starts[i + 1] - s) : lastDur));
158
+ let entry;
159
+ if (audio.codec === 'aac') {
160
+ if (!audioDescription) throw new Error('The audio encoder gave no AAC decoder configuration (AudioSpecificConfig)');
161
+ entry = box('mp4a', a => {
162
+ a.zeros(6).u16(1).zeros(8).u16(ch).u16(16).u16(0).u16(0).u32(rate <= 0xffff ? rate * 65536 : 0);
163
+ a.bytes(esds(audioDescription, audio.bitrate || 128000));
164
+ });
165
+ } else {
166
+ // Opus in ISO BMFF: an 'Opus' sample entry (always 48 kHz) with a 'dOps' box (big-endian, unlike OpusHead)
167
+ const head = parseOpusHead(audioDescription);
168
+ const preSkip = head ? head.preSkip : 312;
169
+ entry = box('Opus', a => {
170
+ a.zeros(6).u16(1).zeros(8).u16(ch).u16(16).u16(0).u16(0).u32(48000 * 65536);
171
+ a.bytes(box('dOps', d => d.u8(0).u8(ch).u16(preSkip).u32(rate).u16(0).u8(0)));
172
+ });
173
+ }
174
+ return { rate, durs, entry, mediaDur: durs.reduce((a, b) => a + b, 0) };
175
+ }
176
+
177
+ return {
178
+ /** Use with a VideoEncoder `output` callback. */
179
+ addChunk(chunk, meta) {
180
+ if (meta && meta.decoderConfig && meta.decoderConfig.description && !description) description = toBytes(meta.decoderConfig.description);
181
+ const data = new Uint8Array(chunk.byteLength);
182
+ chunk.copyTo(data);
183
+ samples.push({ data, key: chunk.type === 'key', ts: chunk.timestamp, dur: chunk.duration || 0 });
184
+ },
185
+ /** Lower-level: add an AVCC sample directly. ts/dur in microseconds. */
186
+ addSample(data, { key, timestamp, duration }) { samples.push({ data, key: !!key, ts: timestamp, dur: duration || 0 }); },
187
+ /** AVCDecoderConfigurationRecord ('avcC' contents). */
188
+ setDescription(d) { description = d; },
189
+
190
+ /** Use with an AudioEncoder `output` callback (needs the `audio` option). */
191
+ addAudioChunk(chunk, meta) {
192
+ if (meta && meta.decoderConfig && meta.decoderConfig.description && !audioDescription) audioDescription = toBytes(meta.decoderConfig.description);
193
+ const data = new Uint8Array(chunk.byteLength);
194
+ chunk.copyTo(data);
195
+ audioSamples.push({ data, ts: chunk.timestamp, dur: chunk.duration || 0 });
196
+ },
197
+ /** Lower-level: add one encoded audio frame, in order. ts/dur in microseconds. */
198
+ addAudioSample(data, { timestamp, duration }) { audioSamples.push({ data, ts: timestamp, dur: duration || 0 }); },
199
+ /** AAC AudioSpecificConfig (for Opus, an optional OpusHead to take the pre-skip from). */
200
+ setAudioDescription(d) { audioDescription = d; },
201
+
202
+ /** @returns {Uint8Array} the .mp4 file */
203
+ finish() {
204
+ if (!samples.length) throw new Error('No video frames were encoded');
205
+ // Trust the encoder's avcC only if it holds together; otherwise use the keyframe's own SPS/PPS.
206
+ if (!isValidAvcC(description)) {
207
+ const key = samples.find(s => s.key) || samples[0];
208
+ description = avcCFromSample(key.data) || description;
209
+ }
210
+ if (!description) throw new Error('The encoder gave no H.264 decoder configuration (avcC)');
211
+ // Samples arrive in decode order with presentation timestamps (B-frames make them non-monotonic).
212
+ // Decode times are the sorted presentation times; 'ctts' holds the difference when there is one.
213
+ const t = us => Math.round(us * timescale / 1e6);
214
+ const pts = samples.map(s => t(s.ts));
215
+ const dts = pts.slice().sort((a, b) => a - b);
216
+ const lastDur = t(samples[samples.length - 1].dur) || (dts.length > 1 ? dts[dts.length - 1] - dts[dts.length - 2] : t(33333));
217
+ const durs = dts.map((d, i) => (i + 1 < dts.length ? dts[i + 1] - d : lastDur));
218
+ const offsets = pts.map((p, i) => p - dts[i]);
219
+ const hasOffsets = offsets.some(o => o !== 0);
220
+ const mediaDur = durs.reduce((a, b) => a + b, 0);
221
+ const videoMs = Math.round(mediaDur * 1000 / timescale);
222
+
223
+ const stts = runs(durs);
224
+ const ctts = runs(offsets);
225
+ const keys = samples.map((s, i) => (s.key ? i + 1 : 0)).filter(Boolean);
226
+ const videoSize = samples.reduce((a, s) => a + s.data.length, 0);
227
+
228
+ const at = audioTrack();
229
+ const audioMs = at ? Math.round(at.mediaDur * 1000 / at.rate) : 0;
230
+ const audioSize = at ? audioSamples.reduce((a, s) => a + s.data.length, 0) : 0;
231
+ const movieDur = Math.max(videoMs, audioMs);
232
+ const dataSize = videoSize + audioSize;
233
+
234
+ const ftyp = box('ftyp', w => w.str('isom').u32(512).str('isom').str('iso2').str('avc1').str('mp41'));
235
+ const dinf = () => box('dinf', di => di.bytes(fullBox('dref', 0, 0, b => { b.u32(1); b.bytes(fullBox('url ', 0, 1, () => { })); })));
236
+ // one chunk per track: all video samples, then all audio samples
237
+ const stsc = n => fullBox('stsc', 0, 0, b => b.u32(1).u32(1).u32(n).u32(1));
238
+ const stsz = list => fullBox('stsz', 0, 0, b => { b.u32(0).u32(list.length); for (const s of list) b.u32(s.data.length); });
239
+ const stco = offset => fullBox('stco', 0, 0, b => b.u32(1).u32(offset));
240
+
241
+ const buildMoov = dataStart => box('moov', w => {
242
+ w.bytes(fullBox('mvhd', 0, 0, b => {
243
+ b.u32(0).u32(0).u32(1000).u32(movieDur).u32(0x00010000).u16(0x0100).zeros(10);
244
+ MATRIX.forEach(v => b.u32(v)); b.zeros(24).u32(at ? 3 : 2); // next track ID
245
+ }));
246
+ // ---- video
247
+ w.bytes(box('trak', tr => {
248
+ tr.bytes(fullBox('tkhd', 0, 3, b => {
249
+ b.u32(0).u32(0).u32(1).u32(0).u32(videoMs).zeros(8).u16(0).u16(0).u16(0).u16(0);
250
+ MATRIX.forEach(v => b.u32(v)); b.u32(width << 16).u32(height << 16);
251
+ }));
252
+ tr.bytes(box('mdia', md => {
253
+ md.bytes(fullBox('mdhd', 0, 0, b => b.u32(0).u32(0).u32(timescale).u32(mediaDur).u16(0x55c4).u16(0)));
254
+ md.bytes(fullBox('hdlr', 0, 0, b => b.u32(0).str('vide').zeros(12).str('VideoHandler').u8(0)));
255
+ md.bytes(box('minf', mi => {
256
+ mi.bytes(fullBox('vmhd', 0, 1, b => b.u16(0).u16(0).u16(0).u16(0)));
257
+ mi.bytes(dinf());
258
+ mi.bytes(box('stbl', st => {
259
+ st.bytes(fullBox('stsd', 0, 0, b => {
260
+ b.u32(1);
261
+ b.bytes(box('avc1', a => {
262
+ a.zeros(6).u16(1).zeros(16).u16(width).u16(height).u32(0x00480000).u32(0x00480000).u32(0).u16(1);
263
+ const name = ENC.encode('image-to-pixel').slice(0, 31);
264
+ a.u8(name.length).bytes(name).zeros(31 - name.length).u16(0x0018).u16(0xffff);
265
+ a.bytes(box('avcC', c => c.bytes(description)));
266
+ }));
267
+ }));
268
+ st.bytes(fullBox('stts', 0, 0, b => { b.u32(stts.length); for (const [n, d] of stts) b.u32(n).u32(d); }));
269
+ // version 1: signed offsets, so negative composition offsets are allowed
270
+ if (hasOffsets) st.bytes(fullBox('ctts', 1, 0, b => { b.u32(ctts.length); for (const [n, o] of ctts) b.u32(n).u32(o >>> 0); }));
271
+ if (keys.length < samples.length) st.bytes(fullBox('stss', 0, 0, b => { b.u32(keys.length); for (const k of keys) b.u32(k); }));
272
+ st.bytes(stsc(samples.length));
273
+ st.bytes(stsz(samples));
274
+ st.bytes(stco(dataStart));
275
+ }));
276
+ }));
277
+ }));
278
+ }));
279
+ // ---- audio
280
+ if (at) w.bytes(box('trak', tr => {
281
+ tr.bytes(fullBox('tkhd', 0, 3, b => {
282
+ // layer 0, alternate group 1, volume 1.0
283
+ b.u32(0).u32(0).u32(2).u32(0).u32(audioMs).zeros(8).u16(0).u16(1).u16(0x0100).u16(0);
284
+ MATRIX.forEach(v => b.u32(v)); b.u32(0).u32(0);
285
+ }));
286
+ tr.bytes(box('mdia', md => {
287
+ md.bytes(fullBox('mdhd', 0, 0, b => b.u32(0).u32(0).u32(at.rate).u32(at.mediaDur).u16(0x55c4).u16(0)));
288
+ md.bytes(fullBox('hdlr', 0, 0, b => b.u32(0).str('soun').zeros(12).str('SoundHandler').u8(0)));
289
+ md.bytes(box('minf', mi => {
290
+ mi.bytes(fullBox('smhd', 0, 0, b => b.u16(0).u16(0)));
291
+ mi.bytes(dinf());
292
+ mi.bytes(box('stbl', st => {
293
+ st.bytes(fullBox('stsd', 0, 0, b => { b.u32(1); b.bytes(at.entry); }));
294
+ const r = runs(at.durs);
295
+ st.bytes(fullBox('stts', 0, 0, b => { b.u32(r.length); for (const [n, d] of r) b.u32(n).u32(d); }));
296
+ st.bytes(stsc(audioSamples.length));
297
+ st.bytes(stsz(audioSamples));
298
+ st.bytes(stco(dataStart + videoSize));
299
+ }));
300
+ }));
301
+ }));
302
+ }));
303
+ });
304
+ // the moov size doesn't depend on the offset values, so measure once and build again
305
+ const moovSize = buildMoov(0).length;
306
+ const offset = ftyp.length + moovSize + 8;
307
+ if (offset + dataSize > 0xffffffff) throw new Error('Video is larger than 4 GB');
308
+ const moov = buildMoov(offset);
309
+ const out = new Uint8Array(offset + dataSize);
310
+ out.set(ftyp, 0); out.set(moov, ftyp.length);
311
+ const dv = new DataView(out.buffer);
312
+ dv.setUint32(ftyp.length + moov.length, dataSize + 8);
313
+ out.set(ENC.encode('mdat'), ftyp.length + moov.length + 4);
314
+ let o = offset;
315
+ for (const s of samples) { out.set(s.data, o); o += s.data.length; }
316
+ if (at) for (const s of audioSamples) { out.set(s.data, o); o += s.data.length; }
317
+ return out;
318
+ },
319
+ };
320
+ }