image-to-pixel 3.0.1 → 3.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/README.md +69 -69
- package/dist/editor.css +1 -1
- package/dist/editor.css.map +2 -2
- package/dist/editor.esm.js +60 -60
- package/dist/editor.esm.js.map +4 -4
- package/dist/image-to-pixel.bundle.esm.js +10 -10
- package/dist/image-to-pixel.bundle.esm.js.map +3 -3
- package/dist/image-to-pixel.cjs +2 -2
- package/dist/image-to-pixel.cjs.map +2 -2
- package/dist/image-to-pixel.esm.js +2 -2
- package/dist/image-to-pixel.esm.js.map +2 -2
- package/dist/image-to-pixel.js +2 -2
- package/dist/image-to-pixel.js.map +2 -2
- package/dist/image-to-pixel.min.js +9 -9
- package/dist/image-to-pixel.min.js.map +3 -3
- package/dist/video.esm.js +2 -2
- package/dist/video.esm.js.map +4 -4
- package/lib/core/video/audio.js +159 -0
- package/lib/core/video/frames.js +182 -144
- package/lib/core/video/mux-mp4.js +320 -131
- package/lib/core/video/mux-webm.js +168 -127
- package/lib/editor/exporter.js +225 -185
- package/lib/editor/icons.js +65 -61
- package/lib/editor/index.js +1002 -793
- package/lib/editor/inspector.js +343 -328
- package/lib/editor/library.js +188 -182
- package/lib/editor/styles/editor.css +418 -385
- package/lib/editor/workspace.js +62 -0
- package/lib/index.js +31 -31
- package/lib/video.js +122 -92
- package/package.json +92 -92
- package/types/video.d.ts +99 -43
|
@@ -1,131 +1,320 @@
|
|
|
1
|
-
// Minimal MP4 (ISO BMFF) muxer for one H.264 video track
|
|
2
|
-
// written with the 'moov' box first ("fast start"), so the file
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|