@bendyline/squisq-video-react 2.4.11 → 2.4.12
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/NOTICE.md +20 -9
- package/THIRD_PARTY_LICENSES.txt +378 -0
- package/dist/{chunk-BRE3IRIM.js → chunk-3EJKBFEK.js} +1 -1
- package/dist/chunk-4GFZ3GUE.js +24645 -0
- package/dist/{chunk-4NI2QIFE.js → chunk-7ZMJ25DC.js} +316 -18
- package/dist/{chunk-JGSJQZGU.js → chunk-BNUM3BXO.js} +39 -3
- package/dist/{chunk-BGS2NEYC.js → chunk-ED7QK6VC.js} +5 -3
- package/dist/{chunk-32QCPFXE.js → chunk-GBIYYDMY.js} +31 -1
- package/dist/chunk-IFQJAW4V.js +321 -0
- package/dist/chunk-SLLH2RTX.js +452 -0
- package/dist/chunk-TL2IMDIZ.js +27 -0
- package/dist/components/index.d.ts +9 -2
- package/dist/components/index.js +6 -5
- package/dist/cover-image/index.js +3 -2
- package/dist/dashboard-image/index.js +2 -1
- package/dist/encoder/index.js +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +4 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +19 -5
- package/dist/media-edit/index.d.ts +29 -0
- package/dist/media-edit/index.js +15 -0
- package/dist/mediaEditEngine-ZLG7D3TB.js +13 -0
- package/dist/mediaEditRenderManager-W_o6RRhT.d.ts +167 -0
- package/dist/{useVideoExport-B2eVCvTX.d.ts → useVideoExport-DsNDpjmO.d.ts} +7 -1
- package/dist/workers/mediaEdit.worker.d.ts +2 -0
- package/dist/workers/mediaEdit.worker.js +63 -0
- package/package.json +11 -5
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADTS,
|
|
3
|
+
AudioSample,
|
|
4
|
+
AudioSampleSink,
|
|
5
|
+
AudioSampleSource,
|
|
6
|
+
BlobSource,
|
|
7
|
+
BufferTarget,
|
|
8
|
+
FLAC,
|
|
9
|
+
Input,
|
|
10
|
+
MATROSKA,
|
|
11
|
+
MP3,
|
|
12
|
+
MP4,
|
|
13
|
+
OGG,
|
|
14
|
+
Output,
|
|
15
|
+
QTFF,
|
|
16
|
+
WAVE,
|
|
17
|
+
WEBM,
|
|
18
|
+
WavOutputFormat,
|
|
19
|
+
WebMOutputFormat,
|
|
20
|
+
canEncodeAudio
|
|
21
|
+
} from "./chunk-4GFZ3GUE.js";
|
|
22
|
+
|
|
23
|
+
// src/mediaEdit/mediaEditEngine.ts
|
|
24
|
+
import {
|
|
25
|
+
MEDIA_FX_ENGINE,
|
|
26
|
+
analyzeMediaFx,
|
|
27
|
+
correctedLoudnessAnalysis,
|
|
28
|
+
createBreathAnalyzer,
|
|
29
|
+
detectPauseCuts,
|
|
30
|
+
parseMediaFx,
|
|
31
|
+
renderMediaFx
|
|
32
|
+
} from "@bendyline/squisq/mediaEdit";
|
|
33
|
+
|
|
34
|
+
// src/mediaEdit/audioRenderSink.ts
|
|
35
|
+
var OPUS_BITRATE_PER_CHANNEL = 96e3;
|
|
36
|
+
async function createAudioRenderSink(channels, sampleRate) {
|
|
37
|
+
const bitrate = OPUS_BITRATE_PER_CHANNEL * channels;
|
|
38
|
+
const opus = await canEncodeAudio("opus", { numberOfChannels: channels, sampleRate, bitrate });
|
|
39
|
+
const target = new BufferTarget();
|
|
40
|
+
const output = new Output({
|
|
41
|
+
format: opus ? new WebMOutputFormat() : new WavOutputFormat(),
|
|
42
|
+
target
|
|
43
|
+
});
|
|
44
|
+
const source = new AudioSampleSource(opus ? { codec: "opus", bitrate } : { codec: "pcm-s16" });
|
|
45
|
+
output.addAudioTrack(source);
|
|
46
|
+
await output.start();
|
|
47
|
+
let written = 0;
|
|
48
|
+
return {
|
|
49
|
+
async write(block) {
|
|
50
|
+
const frames = block[0]?.length ?? 0;
|
|
51
|
+
if (frames === 0) return;
|
|
52
|
+
const data = new Float32Array(frames * channels);
|
|
53
|
+
for (let c = 0; c < channels; c++) data.set(block[c], c * frames);
|
|
54
|
+
const sample = new AudioSample({
|
|
55
|
+
data,
|
|
56
|
+
format: "f32-planar",
|
|
57
|
+
numberOfChannels: channels,
|
|
58
|
+
sampleRate,
|
|
59
|
+
timestamp: written / sampleRate
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
await source.add(sample);
|
|
63
|
+
} finally {
|
|
64
|
+
sample.close();
|
|
65
|
+
}
|
|
66
|
+
written += frames;
|
|
67
|
+
},
|
|
68
|
+
async finish() {
|
|
69
|
+
source.close();
|
|
70
|
+
await output.finalize();
|
|
71
|
+
if (!target.buffer) throw new Error("The audio render produced no output.");
|
|
72
|
+
return opus ? { bytes: target.buffer, mimeType: "audio/webm", extension: "webm" } : { bytes: target.buffer, mimeType: "audio/wav", extension: "wav" };
|
|
73
|
+
},
|
|
74
|
+
cancel: () => output.cancel()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/mediaEdit/mediaAudioSource.ts
|
|
79
|
+
import {
|
|
80
|
+
MEDIA_FX_SAMPLE_RATE,
|
|
81
|
+
createResampler
|
|
82
|
+
} from "@bendyline/squisq/mediaEdit";
|
|
83
|
+
var INPUT_FORMATS = [MP4, QTFF, WEBM, MATROSKA, WAVE, MP3, OGG, FLAC, ADTS];
|
|
84
|
+
async function openMediaAudio(blob, options = {}) {
|
|
85
|
+
const input = new Input({ source: new BlobSource(blob), formats: INPUT_FORMATS });
|
|
86
|
+
const track = await input.getPrimaryAudioTrack();
|
|
87
|
+
if (!track) {
|
|
88
|
+
input.dispose();
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const trackDurationSec = await track.computeDuration();
|
|
92
|
+
const sourceRate = track.sampleRate;
|
|
93
|
+
const channels = Math.min(2, Math.max(1, track.numberOfChannels));
|
|
94
|
+
const startSec = Math.max(0, options.startSec ?? 0);
|
|
95
|
+
const endSec = Math.max(startSec, Math.min(options.endSec ?? trackDurationSec, trackDurationSec));
|
|
96
|
+
const durationSec = endSec - startSec;
|
|
97
|
+
const frames = Math.round(durationSec * MEDIA_FX_SAMPLE_RATE);
|
|
98
|
+
const sourceFrames = Math.round(durationSec * sourceRate);
|
|
99
|
+
async function* open() {
|
|
100
|
+
const sink = new AudioSampleSink(track);
|
|
101
|
+
const resamplers = Array.from(
|
|
102
|
+
{ length: channels },
|
|
103
|
+
() => createResampler(sourceRate, MEDIA_FX_SAMPLE_RATE)
|
|
104
|
+
);
|
|
105
|
+
let placed = 0;
|
|
106
|
+
let emitted = 0;
|
|
107
|
+
const feed = (planes) => {
|
|
108
|
+
const out = resamplers.map((r, c) => r.process(planes[c]));
|
|
109
|
+
const room2 = frames - emitted;
|
|
110
|
+
const take = Math.min(out[0].length, room2);
|
|
111
|
+
if (take <= 0) return null;
|
|
112
|
+
emitted += take;
|
|
113
|
+
return out.map((ch) => ch.length === take ? ch : ch.subarray(0, take));
|
|
114
|
+
};
|
|
115
|
+
for await (const sample of sink.samples(startSec, endSec)) {
|
|
116
|
+
try {
|
|
117
|
+
const at = Math.round((sample.timestamp - startSec) * sourceRate);
|
|
118
|
+
let planes = Array.from({ length: channels }, (_, c) => {
|
|
119
|
+
const plane = new Float32Array(sample.numberOfFrames);
|
|
120
|
+
sample.copyTo(plane, {
|
|
121
|
+
planeIndex: Math.min(c, sample.numberOfChannels - 1),
|
|
122
|
+
format: "f32-planar"
|
|
123
|
+
});
|
|
124
|
+
return plane;
|
|
125
|
+
});
|
|
126
|
+
if (at > placed) {
|
|
127
|
+
const gap = Math.min(at - placed, sourceFrames - placed);
|
|
128
|
+
if (gap > 0) {
|
|
129
|
+
const out2 = feed(Array.from({ length: channels }, () => new Float32Array(gap)));
|
|
130
|
+
placed += gap;
|
|
131
|
+
if (out2) yield out2;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const overlap = placed - at;
|
|
135
|
+
if (overlap > 0) planes = planes.map((p) => p.subarray(Math.min(overlap, p.length)));
|
|
136
|
+
const room2 = sourceFrames - placed;
|
|
137
|
+
if (room2 <= 0) break;
|
|
138
|
+
if (planes[0].length > room2) planes = planes.map((p) => p.subarray(0, room2));
|
|
139
|
+
if (planes[0].length === 0) continue;
|
|
140
|
+
placed += planes[0].length;
|
|
141
|
+
const out = feed(planes);
|
|
142
|
+
if (out) yield out;
|
|
143
|
+
} finally {
|
|
144
|
+
sample.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (placed < sourceFrames) {
|
|
148
|
+
const out = feed(
|
|
149
|
+
Array.from({ length: channels }, () => new Float32Array(sourceFrames - placed))
|
|
150
|
+
);
|
|
151
|
+
if (out) yield out;
|
|
152
|
+
}
|
|
153
|
+
const tail = resamplers.map((r) => r.flush());
|
|
154
|
+
const room = frames - emitted;
|
|
155
|
+
if (room > 0) {
|
|
156
|
+
const padded = tail.map((ch) => {
|
|
157
|
+
const full = new Float32Array(room);
|
|
158
|
+
full.set(ch.subarray(0, room));
|
|
159
|
+
return full;
|
|
160
|
+
});
|
|
161
|
+
emitted += room;
|
|
162
|
+
yield padded;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
sampleRate: MEDIA_FX_SAMPLE_RATE,
|
|
167
|
+
channels,
|
|
168
|
+
frames,
|
|
169
|
+
durationSec,
|
|
170
|
+
trackDurationSec,
|
|
171
|
+
sourceSampleRate: sourceRate,
|
|
172
|
+
open,
|
|
173
|
+
dispose: () => input.dispose()
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/mediaEdit/mediaEditEngine.ts
|
|
178
|
+
var PREVIEW_PREROLL_SEC = 1;
|
|
179
|
+
var FIRST_PASS_SHARE = 0.67;
|
|
180
|
+
function chainOf(fx) {
|
|
181
|
+
const chain = parseMediaFx(fx);
|
|
182
|
+
if (!chain || chain.ops.length === 0) throw new Error(`Nothing to render in fx "${fx}".`);
|
|
183
|
+
return chain;
|
|
184
|
+
}
|
|
185
|
+
function concatBlocks(blocks, channels) {
|
|
186
|
+
return Array.from({ length: channels }, (_, c) => {
|
|
187
|
+
const total = blocks.reduce((n, b) => n + b[c].length, 0);
|
|
188
|
+
const out = new Float32Array(total);
|
|
189
|
+
let at = 0;
|
|
190
|
+
for (const b of blocks) {
|
|
191
|
+
out.set(b[c], at);
|
|
192
|
+
at += b[c].length;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function renderMediaEditAudio(request, options = {}) {
|
|
198
|
+
const fx = chainOf(request.fx);
|
|
199
|
+
const source = await openMediaAudio(request.source);
|
|
200
|
+
if (!source) throw new Error("This media has no audio track to process.");
|
|
201
|
+
try {
|
|
202
|
+
let analysis;
|
|
203
|
+
for (let attempt = 0; ; attempt++) {
|
|
204
|
+
const from = attempt === 0 ? 0 : FIRST_PASS_SHARE;
|
|
205
|
+
const span = attempt === 0 ? FIRST_PASS_SHARE : 1 - FIRST_PASS_SHARE;
|
|
206
|
+
const sink = await createAudioRenderSink(source.channels, source.sampleRate);
|
|
207
|
+
try {
|
|
208
|
+
const result = await renderMediaFx({
|
|
209
|
+
fx,
|
|
210
|
+
source,
|
|
211
|
+
denoiser: options.denoiser,
|
|
212
|
+
write: (block) => sink.write(block),
|
|
213
|
+
onProgress: (f) => options.onProgress?.(from + f * span),
|
|
214
|
+
signal: options.signal,
|
|
215
|
+
...analysis ? { analysis } : {}
|
|
216
|
+
});
|
|
217
|
+
const corrected = attempt === 0 ? correctedLoudnessAnalysis(fx, result) : null;
|
|
218
|
+
if (corrected) {
|
|
219
|
+
await sink.cancel();
|
|
220
|
+
analysis = corrected;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const encoded = await sink.finish();
|
|
224
|
+
options.onProgress?.(1);
|
|
225
|
+
return {
|
|
226
|
+
...encoded,
|
|
227
|
+
analysis: result.analysis,
|
|
228
|
+
ignoredOps: result.ignoredOps,
|
|
229
|
+
sourceDuration: source.trackDurationSec,
|
|
230
|
+
engine: MEDIA_FX_ENGINE
|
|
231
|
+
};
|
|
232
|
+
} catch (err) {
|
|
233
|
+
await sink.cancel().catch(() => {
|
|
234
|
+
});
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
source.dispose();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function analyzeMediaEditAudio(request, options = {}) {
|
|
243
|
+
const fx = chainOf(request.fx);
|
|
244
|
+
const source = await openMediaAudio(request.source);
|
|
245
|
+
if (!source) return null;
|
|
246
|
+
try {
|
|
247
|
+
return await analyzeMediaFx({
|
|
248
|
+
fx,
|
|
249
|
+
source,
|
|
250
|
+
denoiser: options.denoiser,
|
|
251
|
+
onProgress: options.onProgress,
|
|
252
|
+
signal: options.signal
|
|
253
|
+
});
|
|
254
|
+
} finally {
|
|
255
|
+
source.dispose();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function previewMediaEditAudio(request, options = {}) {
|
|
259
|
+
const fx = chainOf(request.fx);
|
|
260
|
+
const decodeFrom = Math.max(0, request.startSec - PREVIEW_PREROLL_SEC);
|
|
261
|
+
const source = await openMediaAudio(request.source, {
|
|
262
|
+
startSec: decodeFrom,
|
|
263
|
+
endSec: request.endSec
|
|
264
|
+
});
|
|
265
|
+
if (!source) throw new Error("This media has no audio track to process.");
|
|
266
|
+
try {
|
|
267
|
+
const originalBlocks = [];
|
|
268
|
+
for await (const block of source.open()) originalBlocks.push(block.map((ch) => ch.slice()));
|
|
269
|
+
const processedBlocks = [];
|
|
270
|
+
await renderMediaFx({
|
|
271
|
+
fx,
|
|
272
|
+
source,
|
|
273
|
+
startSec: decodeFrom,
|
|
274
|
+
denoiser: options.denoiser,
|
|
275
|
+
write: (block) => {
|
|
276
|
+
processedBlocks.push(block.map((ch) => ch.slice()));
|
|
277
|
+
},
|
|
278
|
+
onProgress: options.onProgress,
|
|
279
|
+
signal: options.signal,
|
|
280
|
+
...request.analysis ? { analysis: request.analysis } : {}
|
|
281
|
+
});
|
|
282
|
+
const skip = Math.round((request.startSec - decodeFrom) * source.sampleRate);
|
|
283
|
+
const trim = (channels) => channels.map((ch) => ch.slice(skip));
|
|
284
|
+
return {
|
|
285
|
+
sampleRate: source.sampleRate,
|
|
286
|
+
startSec: request.startSec,
|
|
287
|
+
original: trim(concatBlocks(originalBlocks, source.channels)),
|
|
288
|
+
processed: trim(concatBlocks(processedBlocks, source.channels))
|
|
289
|
+
};
|
|
290
|
+
} finally {
|
|
291
|
+
source.dispose();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function analyzeMediaEditPauses(request, options = {}) {
|
|
295
|
+
const source = await openMediaAudio(request.source);
|
|
296
|
+
if (!source) throw new Error("This media has no audio track to analyze.");
|
|
297
|
+
try {
|
|
298
|
+
const analyzer = createBreathAnalyzer(source.sampleRate);
|
|
299
|
+
let seen = 0;
|
|
300
|
+
for await (const block of source.open()) {
|
|
301
|
+
options.signal?.throwIfAborted();
|
|
302
|
+
const mono = block.length === 1 ? block[0] : block[0].map((_, i) => block.reduce((sum, ch) => sum + ch[i], 0) / block.length);
|
|
303
|
+
analyzer.push(mono);
|
|
304
|
+
seen += block[0].length;
|
|
305
|
+
options.onProgress?.(Math.min(1, seen / Math.max(1, source.frames)));
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
cuts: detectPauseCuts(analyzer.frames(), request.options),
|
|
309
|
+
durationSec: source.trackDurationSec
|
|
310
|
+
};
|
|
311
|
+
} finally {
|
|
312
|
+
source.dispose();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export {
|
|
317
|
+
renderMediaEditAudio,
|
|
318
|
+
analyzeMediaEditAudio,
|
|
319
|
+
previewMediaEditAudio,
|
|
320
|
+
analyzeMediaEditPauses
|
|
321
|
+
};
|