@pixerate/flair 0.28.0 → 0.30.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/CHANGELOG.md +19 -0
- package/README.md +54 -6
- package/dist/catalog/index.d.ts +11 -6
- package/dist/catalog/index.js +1 -1
- package/dist/{chunk-JL6TI6II.js → chunk-7KYQ4R76.js} +2 -2
- package/dist/chunk-7KYQ4R76.js.map +1 -0
- package/dist/{chunk-STKEKXCK.js → chunk-AUH3PEFW.js} +2 -2
- package/dist/{chunk-PFGGPIMT.js → chunk-DA4UUSDO.js} +58 -3
- package/dist/chunk-DA4UUSDO.js.map +1 -0
- package/dist/{chunk-RDU63CDO.js → chunk-HDYBY3PQ.js} +380 -1
- package/dist/chunk-HDYBY3PQ.js.map +1 -0
- package/dist/{chunk-GXP4YLAP.js → chunk-HSYDTVTD.js} +9 -9
- package/dist/chunk-HSYDTVTD.js.map +1 -0
- package/dist/{chunk-VLQLRKGW.js → chunk-MAYKC3RF.js} +260 -33
- package/dist/chunk-MAYKC3RF.js.map +1 -0
- package/dist/{chunk-H6VDFBIO.js → chunk-SMR2MJXB.js} +156 -5
- package/dist/chunk-SMR2MJXB.js.map +1 -0
- package/dist/{chunk-4LKAPRFM.js → chunk-UIODYJ6B.js} +2274 -176
- package/dist/chunk-UIODYJ6B.js.map +1 -0
- package/dist/{chunk-YBGRXVAE.js → chunk-X3C2VFTM.js} +3 -3
- package/dist/{chunk-CK6EV37I.js → chunk-ZULFHADW.js} +69 -6
- package/dist/chunk-ZULFHADW.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/editor/index.d.ts +3 -3
- package/dist/editor/index.js +3 -3
- package/dist/{effect-GGP66JI6.js → effect-RGABT3BK.js} +10 -10
- package/dist/embed/index.d.ts +36 -8
- package/dist/embed/index.js +5 -5
- package/dist/{frames-VEcqa40g.d.ts → frames-CGJudAbu.d.ts} +11 -1
- package/dist/index.d.ts +69 -6
- package/dist/index.js +35 -7
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +4 -4
- package/dist/node.js +9 -9
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{recorder-CDPPTJMG.js → recorder-TYMEZHOT.js} +3 -3
- package/dist/{renderer-teaa98RE.d.ts → renderer-tk1nbVkW.d.ts} +252 -2
- package/dist/svelte/index.d.ts +4 -4
- package/dist/svelte/index.js +7 -7
- package/dist/svelte/index.js.map +1 -1
- package/dist/{svg-BhRboN9q.d.ts → svg-DaKRPA9v.d.ts} +39 -7
- package/dist/{types-DUOMxZaq.d.ts → types-DuGVdJbt.d.ts} +2 -2
- package/dist/vector/index.d.ts +2 -2
- package/dist/vector/index.js +5 -3
- package/package.json +1 -1
- package/dist/chunk-4LKAPRFM.js.map +0 -1
- package/dist/chunk-CK6EV37I.js.map +0 -1
- package/dist/chunk-GXP4YLAP.js.map +0 -1
- package/dist/chunk-H6VDFBIO.js.map +0 -1
- package/dist/chunk-JL6TI6II.js.map +0 -1
- package/dist/chunk-PFGGPIMT.js.map +0 -1
- package/dist/chunk-RDU63CDO.js.map +0 -1
- package/dist/chunk-VLQLRKGW.js.map +0 -1
- /package/dist/{chunk-STKEKXCK.js.map → chunk-AUH3PEFW.js.map} +0 -0
- /package/dist/{chunk-YBGRXVAE.js.map → chunk-X3C2VFTM.js.map} +0 -0
- /package/dist/{effect-GGP66JI6.js.map → effect-RGABT3BK.js.map} +0 -0
- /package/dist/{recorder-CDPPTJMG.js.map → recorder-TYMEZHOT.js.map} +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ShaderFrames,
|
|
3
3
|
ShaderRenderer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SMR2MJXB.js";
|
|
5
5
|
import {
|
|
6
|
+
analyseAudioBuffer,
|
|
6
7
|
seekVideo
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HDYBY3PQ.js";
|
|
8
9
|
import {
|
|
9
10
|
effectDefaults,
|
|
10
11
|
effectInputs,
|
|
11
12
|
getEffect,
|
|
12
13
|
renderVectorEffect,
|
|
13
14
|
textImage
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-UIODYJ6B.js";
|
|
15
16
|
import {
|
|
16
17
|
parseColor
|
|
17
18
|
} from "./chunk-VYOMXE6I.js";
|
|
@@ -19,9 +20,15 @@ import {
|
|
|
19
20
|
SvgPlayer,
|
|
20
21
|
svgDuration,
|
|
21
22
|
svgView
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-DA4UUSDO.js";
|
|
23
24
|
|
|
24
25
|
// src/export/mp4mux.ts
|
|
26
|
+
var AAC_RATES = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
|
|
27
|
+
function aacConfig(sampleRate, channels) {
|
|
28
|
+
const index = AAC_RATES.indexOf(sampleRate);
|
|
29
|
+
if (index < 0) throw new Error(`AAC has no standard rate of ${sampleRate} Hz; resample to 48000 or 44100.`);
|
|
30
|
+
return Uint8Array.of(2 << 3 | index >> 1, (index & 1) << 7 | channels << 3);
|
|
31
|
+
}
|
|
25
32
|
var MOVIE_TIMESCALE = 1e3;
|
|
26
33
|
var UINT32 = 4294967296;
|
|
27
34
|
var ascii = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
|
|
@@ -98,19 +105,80 @@ function stts(ticks) {
|
|
|
98
105
|
for (const [count, delta] of runs) w.u32(count).u32(delta);
|
|
99
106
|
}));
|
|
100
107
|
}
|
|
108
|
+
var descriptor = (tag, ...content) => {
|
|
109
|
+
const body = fields((w) => content.forEach((c) => w.bytes(c)));
|
|
110
|
+
if (body.length > 127) throw new Error("An esds descriptor this long is not written here.");
|
|
111
|
+
return fields((w) => w.u8(tag).u8(body.length).bytes(body));
|
|
112
|
+
};
|
|
113
|
+
function oneChunk(samples, offset, wide) {
|
|
114
|
+
const stsz = fullBox("stsz", 0, 0, fields((w) => {
|
|
115
|
+
w.u32(0).u32(samples.length);
|
|
116
|
+
for (const s of samples) w.u32(s.data.length);
|
|
117
|
+
}));
|
|
118
|
+
const stsc = fullBox("stsc", 0, 0, fields((w) => samples.length ? w.u32(1).u32(1).u32(samples.length).u32(1) : w.u32(0)));
|
|
119
|
+
const stco = wide ? fullBox("co64", 0, 0, fields((w) => w.u32(1).u64(offset))) : fullBox("stco", 0, 0, fields((w) => w.u32(1).u32(offset)));
|
|
120
|
+
return [stsz, stsc, stco];
|
|
121
|
+
}
|
|
122
|
+
function audioTrak(audio, offset, wide) {
|
|
123
|
+
const timescale = audio.sampleRate;
|
|
124
|
+
const ticks = sampleTicks(audio.samples, timescale);
|
|
125
|
+
const trackTicks = ticks.reduce((a, b) => a + b, 0);
|
|
126
|
+
const movieTicks = Math.round(trackTicks * MOVIE_TIMESCALE / timescale);
|
|
127
|
+
const tkhd = fullBox("tkhd", 0, 3, fields(
|
|
128
|
+
(w) => w.u32(0).u32(0).u32(2).u32(0).u32(movieTicks).zeros(8).u16(0).u16(1).u16(256).u16(0).bytes(MATRIX).u32(0).u32(0)
|
|
129
|
+
));
|
|
130
|
+
const mdhd = fullBox("mdhd", 0, 0, fields((w) => w.u32(0).u32(0).u32(timescale).u32(trackTicks).u16(21956).u16(0)));
|
|
131
|
+
const hdlr = fullBox("hdlr", 0, 0, fields((w) => w.u32(0).bytes(ascii("soun")).zeros(12).bytes(ascii("SoundHandler\0"))));
|
|
132
|
+
const smhd = fullBox("smhd", 0, 0, new Uint8Array(4));
|
|
133
|
+
const dinf = box("dinf", fullBox("dref", 0, 0, fields((w) => w.u32(1)), fullBox("url ", 0, 1)));
|
|
134
|
+
const bitrate = Math.round(audio.samples.reduce((n, s) => n + s.data.length, 0) * 8 / Math.max(1e-3, trackTicks / timescale));
|
|
135
|
+
const esds = fullBox(
|
|
136
|
+
"esds",
|
|
137
|
+
0,
|
|
138
|
+
0,
|
|
139
|
+
descriptor(
|
|
140
|
+
3,
|
|
141
|
+
// ES_Descriptor
|
|
142
|
+
fields((w) => w.u16(2).u8(0)),
|
|
143
|
+
// ES id, flags
|
|
144
|
+
descriptor(
|
|
145
|
+
4,
|
|
146
|
+
// DecoderConfigDescriptor
|
|
147
|
+
fields((w) => w.u8(64).u8(21).u8(0).u16(0).u32(bitrate).u32(bitrate)),
|
|
148
|
+
// MPEG-4 audio, audio stream, buffer size, bitrates
|
|
149
|
+
descriptor(5, audio.config ?? aacConfig(audio.sampleRate, audio.channels))
|
|
150
|
+
// DecoderSpecificInfo
|
|
151
|
+
),
|
|
152
|
+
descriptor(6, Uint8Array.of(2))
|
|
153
|
+
// SLConfigDescriptor
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
const mp4a = box(
|
|
157
|
+
"mp4a",
|
|
158
|
+
fields((w) => w.zeros(6).u16(1).zeros(8).u16(audio.channels).u16(16).u16(0).u16(0).u32(Math.min(audio.sampleRate, 65535) * 65536)),
|
|
159
|
+
esds
|
|
160
|
+
);
|
|
161
|
+
const stsd = fullBox("stsd", 0, 0, fields((w) => w.u32(1)), mp4a);
|
|
162
|
+
const stbl = box("stbl", stsd, stts(ticks), ...oneChunk(audio.samples, offset, wide));
|
|
163
|
+
const trak = box("trak", tkhd, box("mdia", mdhd, hdlr, box("minf", smhd, dinf, stbl)));
|
|
164
|
+
return { trak, movieTicks };
|
|
165
|
+
}
|
|
101
166
|
function moov(options, ticks, dataOffset, wide) {
|
|
102
167
|
const { width, height, avcC, samples } = options;
|
|
103
168
|
const timescale = options.timescale ?? 9e4;
|
|
104
169
|
const trackTicks = ticks.reduce((a, b) => a + b, 0);
|
|
105
|
-
const
|
|
170
|
+
const videoTicks = Math.round(trackTicks * MOVIE_TIMESCALE / timescale);
|
|
171
|
+
const videoBytes = samples.reduce((n, s) => n + s.data.length, 0);
|
|
172
|
+
const sound = options.audio?.samples.length ? audioTrak(options.audio, dataOffset + videoBytes, wide) : void 0;
|
|
173
|
+
const movieTicks = Math.max(videoTicks, sound?.movieTicks ?? 0);
|
|
106
174
|
const mvhd = fullBox("mvhd", 0, 0, fields(
|
|
107
|
-
(w) => w.u32(0).u32(0).u32(MOVIE_TIMESCALE).u32(movieTicks).u32(65536).u16(256).zeros(10).bytes(MATRIX).zeros(24).u32(2)
|
|
175
|
+
(w) => w.u32(0).u32(0).u32(MOVIE_TIMESCALE).u32(movieTicks).u32(65536).u16(256).zeros(10).bytes(MATRIX).zeros(24).u32(sound ? 3 : 2)
|
|
108
176
|
// pre_defined, next track id
|
|
109
177
|
));
|
|
110
178
|
const tkhd = fullBox("tkhd", 0, 3, fields(
|
|
111
179
|
(w) => (
|
|
112
180
|
// enabled, in the movie
|
|
113
|
-
w.u32(0).u32(0).u32(1).u32(0).u32(
|
|
181
|
+
w.u32(0).u32(0).u32(1).u32(0).u32(videoTicks).zeros(8).u16(0).u16(0).u16(0).u16(0).bytes(MATRIX).u32(width * 65536).u32(height * 65536)
|
|
114
182
|
)
|
|
115
183
|
));
|
|
116
184
|
const mdhd = fullBox("mdhd", 0, 0, fields((w) => w.u32(0).u32(0).u32(timescale).u32(trackTicks).u16(21956).u16(0)));
|
|
@@ -131,16 +199,10 @@ function moov(options, ticks, dataOffset, wide) {
|
|
|
131
199
|
w.u32(keys.length);
|
|
132
200
|
for (const k of keys) w.u32(k);
|
|
133
201
|
}))];
|
|
134
|
-
const
|
|
135
|
-
w.u32(0).u32(samples.length);
|
|
136
|
-
for (const s of samples) w.u32(s.data.length);
|
|
137
|
-
}));
|
|
138
|
-
const stsc = fullBox("stsc", 0, 0, fields((w) => samples.length ? w.u32(1).u32(1).u32(samples.length).u32(1) : w.u32(0)));
|
|
139
|
-
const stco = wide ? fullBox("co64", 0, 0, fields((w) => w.u32(1).u64(dataOffset))) : fullBox("stco", 0, 0, fields((w) => w.u32(1).u32(dataOffset)));
|
|
140
|
-
const stbl = box("stbl", stsd, stts(ticks), ...stss, stsz, stsc, stco);
|
|
202
|
+
const stbl = box("stbl", stsd, stts(ticks), ...stss, ...oneChunk(samples, dataOffset, wide));
|
|
141
203
|
const minf = box("minf", vmhd, dinf, stbl);
|
|
142
204
|
const trak = box("trak", tkhd, box("mdia", mdhd, hdlr, minf));
|
|
143
|
-
return box("moov", mvhd, trak);
|
|
205
|
+
return box("moov", mvhd, trak, ...sound ? [sound.trak] : []);
|
|
144
206
|
}
|
|
145
207
|
function muxMp4(options) {
|
|
146
208
|
const { samples } = options;
|
|
@@ -148,7 +210,8 @@ function muxMp4(options) {
|
|
|
148
210
|
if (!samples[0].key) throw new Error("An MP4's first frame must be a keyframe.");
|
|
149
211
|
const ftyp = box("ftyp", ascii("isom"), fields((w) => w.u32(512)), ascii("isomiso2avc1mp41"));
|
|
150
212
|
const ticks = sampleTicks(samples, options.timescale ?? 9e4);
|
|
151
|
-
const
|
|
213
|
+
const all = [...samples, ...options.audio?.samples ?? []];
|
|
214
|
+
const payload = all.reduce((n, s) => n + s.data.length, 0);
|
|
152
215
|
const large = payload + 8 >= UINT32;
|
|
153
216
|
const mdatHeader = large ? 16 : 8;
|
|
154
217
|
const wide = ftyp.length + moov(options, ticks, 0, large).length + mdatHeader + payload >= UINT32;
|
|
@@ -162,7 +225,7 @@ function muxMp4(options) {
|
|
|
162
225
|
const out = new Uint8Array(head.length + payload);
|
|
163
226
|
out.set(head);
|
|
164
227
|
let at = head.length;
|
|
165
|
-
for (const s of
|
|
228
|
+
for (const s of all) {
|
|
166
229
|
out.set(s.data, at);
|
|
167
230
|
at += s.data.length;
|
|
168
231
|
}
|
|
@@ -170,6 +233,16 @@ function muxMp4(options) {
|
|
|
170
233
|
}
|
|
171
234
|
|
|
172
235
|
// src/export/webmmux.ts
|
|
236
|
+
function opusHead(sampleRate, channels, preSkip = 312) {
|
|
237
|
+
const b = new Uint8Array(19);
|
|
238
|
+
b.set(new TextEncoder().encode("OpusHead"));
|
|
239
|
+
const v = new DataView(b.buffer);
|
|
240
|
+
v.setUint8(8, 1);
|
|
241
|
+
v.setUint8(9, channels);
|
|
242
|
+
v.setUint16(10, preSkip, true);
|
|
243
|
+
v.setUint32(12, sampleRate, true);
|
|
244
|
+
return b;
|
|
245
|
+
}
|
|
173
246
|
var ID = {
|
|
174
247
|
EBML: 440786851,
|
|
175
248
|
EBMLVersion: 17030,
|
|
@@ -193,6 +266,12 @@ var ID = {
|
|
|
193
266
|
FlagLacing: 156,
|
|
194
267
|
CodecID: 134,
|
|
195
268
|
DefaultDuration: 2352003,
|
|
269
|
+
CodecPrivate: 25506,
|
|
270
|
+
CodecDelay: 22186,
|
|
271
|
+
SeekPreRoll: 22203,
|
|
272
|
+
Audio: 225,
|
|
273
|
+
SamplingFrequency: 181,
|
|
274
|
+
Channels: 159,
|
|
196
275
|
MaxBlockAdditionID: 21998,
|
|
197
276
|
Video: 224,
|
|
198
277
|
PixelWidth: 176,
|
|
@@ -255,9 +334,9 @@ var float = (id, n) => {
|
|
|
255
334
|
return el(id, b);
|
|
256
335
|
};
|
|
257
336
|
var str = (id, s) => el(id, new TextEncoder().encode(s));
|
|
258
|
-
function blockHeader(offset, flags) {
|
|
337
|
+
function blockHeader(offset, flags, track = 1) {
|
|
259
338
|
const b = new Uint8Array(4);
|
|
260
|
-
b[0] =
|
|
339
|
+
b[0] = 128 | track;
|
|
261
340
|
new DataView(b.buffer).setInt16(1, offset);
|
|
262
341
|
b[3] = flags;
|
|
263
342
|
return b;
|
|
@@ -267,6 +346,7 @@ function muxWebm(options) {
|
|
|
267
346
|
if (!frames.length) throw new Error("A WebM needs at least one frame.");
|
|
268
347
|
if (!frames[0].key) throw new Error("A WebM's first frame must be a keyframe.");
|
|
269
348
|
const alpha = frames.some((f) => f.alpha);
|
|
349
|
+
const sound = options.audio?.packets.length ? options.audio : void 0;
|
|
270
350
|
const ms = (us) => Math.round(us / 1e3);
|
|
271
351
|
const last = frames[frames.length - 1];
|
|
272
352
|
const frameUs = fps ? 1e6 / fps : frames.length > 1 ? (last.timestamp - frames[0].timestamp) / (frames.length - 1) : 1e6 / 30;
|
|
@@ -302,7 +382,8 @@ function muxWebm(options) {
|
|
|
302
382
|
...fps ? [uint(ID.DefaultDuration, Math.round(1e9 / fps))] : [],
|
|
303
383
|
...alpha ? [uint(ID.MaxBlockAdditionID, 1)] : [],
|
|
304
384
|
el(ID.Video, uint(ID.PixelWidth, width), uint(ID.PixelHeight, height), ...alpha ? [uint(ID.AlphaMode, 1)] : [])
|
|
305
|
-
)
|
|
385
|
+
),
|
|
386
|
+
...sound ? [soundTrack(sound)] : []
|
|
306
387
|
);
|
|
307
388
|
const clusters = [];
|
|
308
389
|
let blocks = [];
|
|
@@ -312,11 +393,26 @@ function muxWebm(options) {
|
|
|
312
393
|
if (blocks.length) clusters.push(el(ID.Cluster, uint(ID.Timestamp, clusterMs), ...blocks));
|
|
313
394
|
blocks = [];
|
|
314
395
|
};
|
|
315
|
-
frames.
|
|
316
|
-
|
|
317
|
-
|
|
396
|
+
const items = [...frames.map((frame) => ({ frame })), ...(sound?.packets ?? []).map((packet) => ({ packet }))];
|
|
397
|
+
const timeOf = (item) => (item.frame ?? item.packet).timestamp;
|
|
398
|
+
items.sort((a, b) => timeOf(a) - timeOf(b) || (a.frame ? -1 : 0) - (b.frame ? -1 : 0));
|
|
399
|
+
let started = false;
|
|
400
|
+
for (const item of items) {
|
|
401
|
+
const at = ms(timeOf(item));
|
|
402
|
+
const frame = item.frame;
|
|
403
|
+
if (!frame) {
|
|
404
|
+
if (!started || at - clusterMs > MAX_OFFSET) {
|
|
405
|
+
close();
|
|
406
|
+
clusterMs = at;
|
|
407
|
+
started = true;
|
|
408
|
+
}
|
|
409
|
+
blocks.push(el(ID.SimpleBlock, blockHeader(at - clusterMs, 128, 2), item.packet.data));
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (!started || frame.key || at - clusterMs > MAX_OFFSET) {
|
|
318
413
|
close();
|
|
319
414
|
clusterMs = at;
|
|
415
|
+
started = true;
|
|
320
416
|
}
|
|
321
417
|
const offset = at - clusterMs;
|
|
322
418
|
if (frame.alpha) {
|
|
@@ -332,11 +428,28 @@ function muxWebm(options) {
|
|
|
332
428
|
blocks.push(el(ID.SimpleBlock, blockHeader(offset, frame.key ? 128 : 0), frame.data));
|
|
333
429
|
}
|
|
334
430
|
previousMs = at;
|
|
335
|
-
}
|
|
431
|
+
}
|
|
336
432
|
close();
|
|
337
433
|
const segment = el(ID.Segment, info, tracks, ...clusters);
|
|
338
434
|
return concat([header, segment]);
|
|
339
435
|
}
|
|
436
|
+
function soundTrack(sound) {
|
|
437
|
+
const head = sound.codecPrivate?.length ? sound.codecPrivate : opusHead(sound.sampleRate, sound.channels);
|
|
438
|
+
const preSkip = head.length >= 12 ? head[10] | head[11] << 8 : 312;
|
|
439
|
+
return el(
|
|
440
|
+
ID.TrackEntry,
|
|
441
|
+
uint(ID.TrackNumber, 2),
|
|
442
|
+
uint(ID.TrackUID, 2),
|
|
443
|
+
uint(ID.TrackType, 2),
|
|
444
|
+
// audio
|
|
445
|
+
uint(ID.FlagLacing, 0),
|
|
446
|
+
str(ID.CodecID, "A_OPUS"),
|
|
447
|
+
el(ID.CodecPrivate, head),
|
|
448
|
+
uint(ID.CodecDelay, Math.round(preSkip * 1e9 / 48e3)),
|
|
449
|
+
uint(ID.SeekPreRoll, 8e7),
|
|
450
|
+
el(ID.Audio, float(ID.SamplingFrequency, sound.sampleRate), uint(ID.Channels, sound.channels))
|
|
451
|
+
);
|
|
452
|
+
}
|
|
340
453
|
function signed(n) {
|
|
341
454
|
for (let len = 1; len <= 8; len++) {
|
|
342
455
|
if (n >= -(2 ** (8 * len - 1)) && n < 2 ** (8 * len - 1)) {
|
|
@@ -423,6 +536,16 @@ function zipStore(entries) {
|
|
|
423
536
|
}
|
|
424
537
|
|
|
425
538
|
// src/embed/record.ts
|
|
539
|
+
var AUDIO_RATE = 48e3;
|
|
540
|
+
async function audioConfig(format) {
|
|
541
|
+
if (typeof AudioEncoder === "undefined" || typeof AudioData === "undefined") return void 0;
|
|
542
|
+
const config = { codec: format === "mp4" ? "mp4a.40.2" : "opus", sampleRate: AUDIO_RATE, numberOfChannels: 2, bitrate: 128e3 };
|
|
543
|
+
try {
|
|
544
|
+
return (await AudioEncoder.isConfigSupported(config)).supported ? config : void 0;
|
|
545
|
+
} catch {
|
|
546
|
+
return void 0;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
426
549
|
var DEFAULT_SECONDS = 4;
|
|
427
550
|
var yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
428
551
|
var formats = (list) => list.map((f) => `"${f}"`).join(", ").replace(/, ([^,]*)$/, " or $1");
|
|
@@ -538,7 +661,24 @@ async function recordingPlan(effect, options = {}) {
|
|
|
538
661
|
width = w;
|
|
539
662
|
height = h;
|
|
540
663
|
}
|
|
541
|
-
|
|
664
|
+
let audioEncoder;
|
|
665
|
+
if (options.audio !== void 0 && options.audioTrack !== false && (format === "mp4" || format === "webm")) {
|
|
666
|
+
audioEncoder = await audioConfig(format);
|
|
667
|
+
if (!audioEncoder && format === "mp4" && !options.format) {
|
|
668
|
+
const webm = await videoConfig("webm", width, height, fps, encoder.bitrate ?? Math.round(width * height * fps * 0.15));
|
|
669
|
+
const opus = webm && await audioConfig("webm");
|
|
670
|
+
if (webm && opus) {
|
|
671
|
+
format = "webm";
|
|
672
|
+
encoder = webm;
|
|
673
|
+
audioEncoder = opus;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (!audioEncoder) {
|
|
677
|
+
const codec = format === "mp4" ? "AAC" : "Opus";
|
|
678
|
+
throw new Error(`This browser cannot encode ${codec} audio, so it cannot put the sound in ${format === "mp4" ? "an mp4" : "a webm"}; ${format === "mp4" ? 'record "webm" (Opus), or ' : ""}pass audioTrack: false to record the picture alone (the sound still drives it).`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return { effect: def, format, width, height, fps, duration, start, frames: Math.max(1, Math.round(duration * fps)), alpha, ...encoder ? { encoder } : {}, ...audioEncoder ? { audioEncoder } : {} };
|
|
542
682
|
}
|
|
543
683
|
function defaultDuration(def, options) {
|
|
544
684
|
if (def.kind === "shader") return def.loop ?? DEFAULT_SECONDS;
|
|
@@ -551,7 +691,7 @@ function effectValues(def, options) {
|
|
|
551
691
|
const s = subjectValues(def, options.subject);
|
|
552
692
|
return { values: { ...values, ...s.values }, live: s.live };
|
|
553
693
|
}
|
|
554
|
-
function shaderSource(plan, values, live, options) {
|
|
694
|
+
function shaderSource(plan, values, live, options, sound) {
|
|
555
695
|
const def = plan.effect;
|
|
556
696
|
const frames = new ShaderFrames({ source: def.source, values, width: plan.width, height: plan.height, alpha: plan.alpha || Boolean(def.transparent) });
|
|
557
697
|
const videos = [];
|
|
@@ -569,7 +709,7 @@ function shaderSource(plan, values, live, options) {
|
|
|
569
709
|
await options.beforeFrame?.(time, index);
|
|
570
710
|
await frames.ready();
|
|
571
711
|
const pointer = typeof script === "function" ? script(time) : script;
|
|
572
|
-
frames.render(time, { frame: index, delta: 1 / plan.fps, ...pointer });
|
|
712
|
+
frames.render(time, { frame: index, delta: 1 / plan.fps, ...pointer, ...sound ? { audio: sound.at(time) } : {} });
|
|
573
713
|
frames.readPixels(out);
|
|
574
714
|
},
|
|
575
715
|
dispose() {
|
|
@@ -682,7 +822,64 @@ function collector(config, fail) {
|
|
|
682
822
|
out.encoder.configure(config);
|
|
683
823
|
return out;
|
|
684
824
|
}
|
|
685
|
-
function
|
|
825
|
+
async function decodeAudio(url) {
|
|
826
|
+
if (typeof OfflineAudioContext === "undefined") throw new Error("This browser has no Web Audio (OfflineAudioContext), so it cannot decode the audio.");
|
|
827
|
+
let bytes;
|
|
828
|
+
try {
|
|
829
|
+
const response = await fetch(url);
|
|
830
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
831
|
+
bytes = await response.arrayBuffer();
|
|
832
|
+
} catch (e) {
|
|
833
|
+
throw new Error(`The audio at ${url} could not be loaded (${e.message}); a file from another site needs CORS.`, { cause: e });
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
return await new OfflineAudioContext(2, 1, AUDIO_RATE).decodeAudioData(bytes);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
throw new Error(`The audio at ${url} could not be decoded; give it an mp3, AAC, Ogg, Opus, FLAC or WAV file the browser plays.`, { cause: e });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function encodeSound(buffer, plan) {
|
|
842
|
+
const config = plan.audioEncoder;
|
|
843
|
+
const channels = config.numberOfChannels;
|
|
844
|
+
const length = Math.max(1, Math.round(plan.duration * AUDIO_RATE));
|
|
845
|
+
const offline = new OfflineAudioContext(channels, length, AUDIO_RATE);
|
|
846
|
+
const node = offline.createBufferSource();
|
|
847
|
+
node.buffer = buffer;
|
|
848
|
+
node.connect(offline.destination);
|
|
849
|
+
node.start(0, Math.min(plan.start, buffer.duration));
|
|
850
|
+
const rendered = await offline.startRendering();
|
|
851
|
+
const out = { codec: config.codec === "opus" ? "opus" : "aac", sampleRate: AUDIO_RATE, channels, chunks: [] };
|
|
852
|
+
let failure;
|
|
853
|
+
const encoder = new AudioEncoder({
|
|
854
|
+
output(chunk, metadata) {
|
|
855
|
+
const data = new Uint8Array(chunk.byteLength);
|
|
856
|
+
chunk.copyTo(data);
|
|
857
|
+
const desc = metadata?.decoderConfig?.description;
|
|
858
|
+
if (desc && !out.description) out.description = bytesOf(desc);
|
|
859
|
+
out.chunks.push({ data, timestamp: chunk.timestamp, ...chunk.duration ? { duration: chunk.duration } : {}, key: true });
|
|
860
|
+
},
|
|
861
|
+
error: (e) => failure ??= new Error(`The audio encoder failed: ${e.message}`, { cause: e })
|
|
862
|
+
});
|
|
863
|
+
encoder.configure(config);
|
|
864
|
+
const block = 4800;
|
|
865
|
+
for (let at = 0; at < length; at += block) {
|
|
866
|
+
const n = Math.min(block, length - at);
|
|
867
|
+
const planes = new Float32Array(n * channels);
|
|
868
|
+
for (let c = 0; c < channels; c++) planes.set(rendered.getChannelData(c).subarray(at, at + n), c * n);
|
|
869
|
+
const data = new AudioData({ format: "f32-planar", sampleRate: AUDIO_RATE, numberOfFrames: n, numberOfChannels: channels, timestamp: Math.round(at * 1e6 / AUDIO_RATE), data: planes });
|
|
870
|
+
try {
|
|
871
|
+
encoder.encode(data);
|
|
872
|
+
} finally {
|
|
873
|
+
data.close();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
await encoder.flush();
|
|
877
|
+
encoder.close();
|
|
878
|
+
if (failure) throw failure;
|
|
879
|
+
out.chunks.sort((a, b) => a.timestamp - b.timestamp);
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
function videoEncoder(plan, sound) {
|
|
686
883
|
const config = plan.encoder;
|
|
687
884
|
let failure;
|
|
688
885
|
const fail = (e) => failure ??= e;
|
|
@@ -721,9 +918,16 @@ function videoEncoder(plan) {
|
|
|
721
918
|
for (const e of encoders) e.close();
|
|
722
919
|
const byTime = (a, b) => a.timestamp - b.timestamp;
|
|
723
920
|
const chunks = color.chunks.sort(byTime);
|
|
921
|
+
const audio = await sound;
|
|
724
922
|
if (plan.format === "mp4") {
|
|
725
923
|
if (!color.description) throw new Error("The H.264 encoder gave no decoder configuration (avcC), so the mp4 cannot be written.");
|
|
726
|
-
const file2 = muxMp4({
|
|
924
|
+
const file2 = muxMp4({
|
|
925
|
+
width,
|
|
926
|
+
height,
|
|
927
|
+
avcC: color.description,
|
|
928
|
+
samples: chunks,
|
|
929
|
+
...audio ? { audio: { sampleRate: audio.sampleRate, channels: audio.channels, ...audio.description ? { config: audio.description } : {}, samples: audio.chunks } } : {}
|
|
930
|
+
});
|
|
727
931
|
return new Blob([file2], { type: "video/mp4" });
|
|
728
932
|
}
|
|
729
933
|
const alphas = new Map(alpha?.chunks.map((c) => [c.timestamp, c.data]));
|
|
@@ -733,7 +937,15 @@ function videoEncoder(plan) {
|
|
|
733
937
|
return { ...c, ...a ? { alpha: a } : {} };
|
|
734
938
|
});
|
|
735
939
|
const codec = config.codec.startsWith("vp8") ? "vp8" : "vp9";
|
|
736
|
-
const file = muxWebm({
|
|
940
|
+
const file = muxWebm({
|
|
941
|
+
codec,
|
|
942
|
+
width,
|
|
943
|
+
height,
|
|
944
|
+
frames,
|
|
945
|
+
duration: plan.frames * frameUs,
|
|
946
|
+
fps: plan.fps,
|
|
947
|
+
...audio ? { audio: { sampleRate: audio.sampleRate, channels: audio.channels, ...audio.description ? { codecPrivate: audio.description } : {}, packets: audio.chunks } } : {}
|
|
948
|
+
});
|
|
737
949
|
return new Blob([file], { type: "video/webm" });
|
|
738
950
|
},
|
|
739
951
|
close() {
|
|
@@ -746,10 +958,14 @@ async function recordEffect(effect, options = {}) {
|
|
|
746
958
|
const { signal, onProgress } = options;
|
|
747
959
|
const { values, live } = effectValues(plan.effect, options);
|
|
748
960
|
const background = (parseColor(options.background ?? (plan.effect.kind === "shader" ? "#000000" : "#ffffff")) ?? [0, 0, 0]).map((c) => Math.round(c * 255));
|
|
749
|
-
const
|
|
961
|
+
const buffer = options.audio === void 0 ? void 0 : typeof options.audio === "string" ? await decodeAudio(options.audio) : options.audio;
|
|
962
|
+
const sound = buffer && plan.effect.kind === "shader" && effectInputs(plan.effect).accepts.includes("audio") ? analyseAudioBuffer(buffer) : void 0;
|
|
963
|
+
const track = buffer && plan.audioEncoder ? encodeSound(buffer, plan) : void 0;
|
|
964
|
+
track?.catch(() => void 0);
|
|
965
|
+
const source = plan.effect.kind === "shader" ? shaderSource(plan, values, live, options, sound) : vectorSource(plan, values, options);
|
|
750
966
|
let encoder;
|
|
751
967
|
try {
|
|
752
|
-
encoder = plan.format === "gif" ? await gifEncoder(plan) : plan.format === "png" ? pngEncoder(plan) : videoEncoder(plan);
|
|
968
|
+
encoder = plan.format === "gif" ? await gifEncoder(plan) : plan.format === "png" ? pngEncoder(plan) : videoEncoder(plan, track);
|
|
753
969
|
const rgba = new Uint8ClampedArray(plan.width * plan.height * 4);
|
|
754
970
|
for (let i = 0; i < plan.frames; i++) {
|
|
755
971
|
if (signal?.aborted) throw new DOMException("Cancelled", "AbortError");
|
|
@@ -792,6 +1008,12 @@ function subjectValues(effect, subject, slot) {
|
|
|
792
1008
|
for (const s of Array.isArray(subject) ? subject : [subject]) {
|
|
793
1009
|
const kind = kindOf(s);
|
|
794
1010
|
const free = (slots) => slots.filter((x) => !used.has(x.name));
|
|
1011
|
+
if (kind === "audio") {
|
|
1012
|
+
if (!inputs.accepts.includes("audio"))
|
|
1013
|
+
throw new Error(`"${effect.id}" does not react to sound; findEffects({ accepts: ["audio"] }) lists the effects that do.`);
|
|
1014
|
+
out.audio = s.audio;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
795
1017
|
if (kind === "path" || kind === "text" && effect.kind === "vector") {
|
|
796
1018
|
const holds = kind === "path" ? "path" : "copy";
|
|
797
1019
|
const text = pick(free(inputs.texts), (t) => t.holds === holds, slot, effect, kind === "path" ? "path data" : "text");
|
|
@@ -819,11 +1041,13 @@ function mountEffect(host, effect, options = {}) {
|
|
|
819
1041
|
if (!def) throw new Error(`There is no effect "${String(effect)}" in the catalog; findEffects() lists them.`);
|
|
820
1042
|
let values = { ...effectDefaults(def, { ...options.preset ? { preset: options.preset } : {}, ...options.values ? { values: options.values } : {} }) };
|
|
821
1043
|
let live = {};
|
|
1044
|
+
let audio;
|
|
822
1045
|
const apply = (subject) => {
|
|
823
1046
|
if (!subject) return;
|
|
824
1047
|
const s = subjectValues(def, subject);
|
|
825
1048
|
values = { ...values, ...s.values };
|
|
826
1049
|
live = s.live;
|
|
1050
|
+
audio = s.audio;
|
|
827
1051
|
};
|
|
828
1052
|
apply(options.subject);
|
|
829
1053
|
if (def.kind === "shader") {
|
|
@@ -840,15 +1064,18 @@ function mountEffect(host, effect, options = {}) {
|
|
|
840
1064
|
for (const [name, source] of Object.entries(live)) renderer.setLiveSource(name, source);
|
|
841
1065
|
};
|
|
842
1066
|
setLive({});
|
|
1067
|
+
if (audio) renderer.setAudioSource(audio);
|
|
843
1068
|
return {
|
|
844
1069
|
effect: def,
|
|
845
1070
|
element: renderer.canvas,
|
|
846
1071
|
renderer,
|
|
847
1072
|
setSubject(subject) {
|
|
848
1073
|
const before = live;
|
|
1074
|
+
const heard = audio;
|
|
849
1075
|
apply(subject);
|
|
850
1076
|
renderer.setValues(values);
|
|
851
1077
|
setLive(before);
|
|
1078
|
+
if (audio !== heard) renderer.setAudioSource(audio ?? null);
|
|
852
1079
|
},
|
|
853
1080
|
setValues(next) {
|
|
854
1081
|
values = { ...values, ...next };
|
|
@@ -899,4 +1126,4 @@ export {
|
|
|
899
1126
|
subjectValues,
|
|
900
1127
|
mountEffect
|
|
901
1128
|
};
|
|
902
|
-
//# sourceMappingURL=chunk-
|
|
1129
|
+
//# sourceMappingURL=chunk-MAYKC3RF.js.map
|