@bendyline/squisq-video-react 2.4.19 → 2.4.21
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 +30 -0
- package/dist/chunk-2W6JQSIY.js +51 -0
- package/dist/{chunk-MI4EJXTD.js → chunk-47ODXCBA.js} +33 -44
- package/dist/{chunk-QJWEZRZS.js → chunk-A52VG7RA.js} +1 -1
- package/dist/{chunk-IFQJAW4V.js → chunk-BNNLPKT2.js} +1 -1
- package/dist/{chunk-4GFZ3GUE.js → chunk-C7NEHV5X.js} +20507 -17327
- package/dist/chunk-FMQI6WK2.js +157 -0
- package/dist/{chunk-GBIYYDMY.js → chunk-ISYJJJXN.js} +67 -1
- package/dist/{chunk-KJQVUVOG.js → chunk-JDIMPME5.js} +1 -1
- package/dist/{chunk-JKWCZION.js → chunk-OLFWEKY5.js} +1 -1
- package/dist/{chunk-A6ED7EVY.js → chunk-WGHDQUGC.js} +1 -1
- package/dist/components/index.js +6 -6
- package/dist/cover-image/index.js +3 -3
- package/dist/dashboard-image/index.js +2 -2
- package/dist/encoder/index.d.ts +126 -2
- package/dist/encoder/index.js +12 -1
- package/dist/hooks/index.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/lossyEncoder-EIXBAOLG.js +236 -0
- package/dist/media-edit/index.js +1 -1
- package/dist/{mediaEditEngine-ZLG7D3TB.js → mediaEditEngine-JLZ4Z5BB.js} +2 -2
- package/dist/workers/mediaEdit.worker.js +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -220,6 +220,36 @@ available, and otherwise skipped with `audioIncluded: false` plus an
|
|
|
220
220
|
completes. GIF skips audio preparation entirely. `supportsWebCodecsH264(config)` probes a specific encoder
|
|
221
221
|
configuration; `EncoderConfig` and `FfmpegWasmLoadConfig` are also exported.
|
|
222
222
|
|
|
223
|
+
## Audio export
|
|
224
|
+
|
|
225
|
+
`@bendyline/squisq-video-react/encoder` also exports a streaming audio-file
|
|
226
|
+
encoder and a whole-document audio mix for hosts that export audio on its own
|
|
227
|
+
(or save generated narration):
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import {
|
|
231
|
+
createAudioFileEncoder,
|
|
232
|
+
renderDocumentAudio,
|
|
233
|
+
supportedAudioFileFormats,
|
|
234
|
+
} from '@bendyline/squisq-video-react/encoder';
|
|
235
|
+
|
|
236
|
+
const formats = await supportedAudioFileFormats(); // 'm4a' (AAC), 'opus-webm', 'wav' (always)
|
|
237
|
+
const mixed = await renderDocumentAudio(doc, { readMedia: (src) => container.readFile(src) });
|
|
238
|
+
if (mixed) {
|
|
239
|
+
const encoder = await createAudioFileEncoder({
|
|
240
|
+
format: formats[0],
|
|
241
|
+
sampleRate: mixed.sampleRate,
|
|
242
|
+
channels: 2,
|
|
243
|
+
});
|
|
244
|
+
await encoder.append([mixed.getChannelData(0), mixed.getChannelData(1)]);
|
|
245
|
+
const file = await encoder.finish(); // encoder.mimeType / encoder.extension
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
M4A is an audio-only MP4; input at rates the codec rejects (24 kHz TTS for
|
|
250
|
+
AAC) is resampled in-stream. See the
|
|
251
|
+
[host guide](https://github.com/bendyline/squisq/blob/main/docs/narration-and-audio-export.md).
|
|
252
|
+
|
|
223
253
|
## Full API Reference
|
|
224
254
|
|
|
225
255
|
See [docs/API.md](https://github.com/bendyline/squisq/blob/main/docs/API.md)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/audioFile/shared.ts
|
|
2
|
+
var LOSSY_FORMATS = {
|
|
3
|
+
m4a: {
|
|
4
|
+
format: "m4a",
|
|
5
|
+
codec: "aac",
|
|
6
|
+
label: "AAC (M4A)",
|
|
7
|
+
mimeType: "audio/mp4",
|
|
8
|
+
extension: "m4a",
|
|
9
|
+
bitrate: { 1: 96e3, 2: 128e3 }
|
|
10
|
+
},
|
|
11
|
+
"opus-webm": {
|
|
12
|
+
format: "opus-webm",
|
|
13
|
+
codec: "opus",
|
|
14
|
+
label: "Opus (WebM)",
|
|
15
|
+
mimeType: "audio/webm",
|
|
16
|
+
extension: "webm",
|
|
17
|
+
bitrate: { 1: 64e3, 2: 96e3 }
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function blockFrames(block, channels) {
|
|
21
|
+
if (block.length !== channels) {
|
|
22
|
+
throw new RangeError(`Expected ${channels} channel(s) of PCM, got ${block.length}.`);
|
|
23
|
+
}
|
|
24
|
+
const frames = block[0].length;
|
|
25
|
+
for (const channel of block) {
|
|
26
|
+
if (!(channel instanceof Float32Array)) {
|
|
27
|
+
throw new TypeError("PCM channels must be Float32Arrays.");
|
|
28
|
+
}
|
|
29
|
+
if (channel.length !== frames) throw new RangeError("Every channel must have the same length.");
|
|
30
|
+
}
|
|
31
|
+
return frames;
|
|
32
|
+
}
|
|
33
|
+
function createSerialQueue() {
|
|
34
|
+
let tail = Promise.resolve();
|
|
35
|
+
return (task) => {
|
|
36
|
+
const run = tail.then(task);
|
|
37
|
+
tail = run.catch(() => void 0);
|
|
38
|
+
return run;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function assertOpen(state) {
|
|
42
|
+
if (state === "finished") throw new Error("The audio file encoder is already finished.");
|
|
43
|
+
if (state === "cancelled") throw new Error("The audio file encoder was cancelled.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
LOSSY_FORMATS,
|
|
48
|
+
blockFrames,
|
|
49
|
+
createSerialQueue,
|
|
50
|
+
assertOpen
|
|
51
|
+
};
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useFrameCapture
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-A52VG7RA.js";
|
|
4
4
|
import {
|
|
5
5
|
EXPORT_AUDIO_CHANNELS,
|
|
6
6
|
EXPORT_AUDIO_SAMPLE_RATE,
|
|
7
7
|
audioBufferToWav,
|
|
8
8
|
createEncoder,
|
|
9
|
+
documentAudioTimeline,
|
|
9
10
|
encodeAacTrack,
|
|
11
|
+
loadAudioTimelineSources,
|
|
12
|
+
mixAudioTimeline,
|
|
10
13
|
muxAudioWithFfmpegWasm,
|
|
11
|
-
renderAudioTimeline,
|
|
12
14
|
selectAudioTier,
|
|
13
15
|
supportsWebCodecs,
|
|
14
16
|
supportsWebCodecsAac,
|
|
15
17
|
supportsWebCodecsH264
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ISYJJJXN.js";
|
|
17
19
|
|
|
18
20
|
// src/hooks/useVideoExport.ts
|
|
19
21
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
@@ -26,12 +28,7 @@ import {
|
|
|
26
28
|
MAX_COVER_SLIDE_DURATION_SECONDS,
|
|
27
29
|
resolveCoverSlideSettings
|
|
28
30
|
} from "@bendyline/squisq/doc";
|
|
29
|
-
import {
|
|
30
|
-
resolveDimensions,
|
|
31
|
-
computeAudioTimeline,
|
|
32
|
-
resolveFfmpegWasmLoad as resolveFfmpegWasmLoad2,
|
|
33
|
-
QUALITY_PRESETS
|
|
34
|
-
} from "@bendyline/squisq-video";
|
|
31
|
+
import { resolveDimensions, resolveFfmpegWasmLoad as resolveFfmpegWasmLoad2, QUALITY_PRESETS } from "@bendyline/squisq-video";
|
|
35
32
|
|
|
36
33
|
// src/workerEncoder.ts
|
|
37
34
|
import { validateVideoExportOptions } from "@bendyline/squisq-video";
|
|
@@ -420,24 +417,19 @@ function collectDocumentMediaReferences(doc) {
|
|
|
420
417
|
visit(doc);
|
|
421
418
|
return references;
|
|
422
419
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
data = toArrayBuffer(resource.bytes);
|
|
435
|
-
} catch {
|
|
436
|
-
}
|
|
420
|
+
function exportMediaReader(sources) {
|
|
421
|
+
return async (src) => {
|
|
422
|
+
const data = sources.audio?.get(src) ?? sources.images?.get(src);
|
|
423
|
+
if (data) return data;
|
|
424
|
+
if (!sources.mediaProvider) return null;
|
|
425
|
+
try {
|
|
426
|
+
const url = await sources.mediaProvider.resolveUrl(src);
|
|
427
|
+
const resource = await fetchResourceBytes(url, { policy: sources.resourcePolicy });
|
|
428
|
+
return toArrayBuffer(resource.bytes);
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
437
431
|
}
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
return out;
|
|
432
|
+
};
|
|
441
433
|
}
|
|
442
434
|
function resolveVideoExportCover(doc, config = {}) {
|
|
443
435
|
const legacyPreRollOverride = config.coverPreRoll;
|
|
@@ -689,9 +681,10 @@ function useVideoExport(options = {}) {
|
|
|
689
681
|
).catch(() => false);
|
|
690
682
|
if (cancelled()) return;
|
|
691
683
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
692
|
-
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ?
|
|
693
|
-
|
|
694
|
-
|
|
684
|
+
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? documentAudioTimeline(doc, {
|
|
685
|
+
offsetSec: coverPlan.audioOffset,
|
|
686
|
+
...config.processedAudio ? { processedAudio: config.processedAudio } : {}
|
|
687
|
+
}) : [];
|
|
695
688
|
const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
|
|
696
689
|
if (cancelled()) return;
|
|
697
690
|
const tierDecision = selectAudioTier({
|
|
@@ -709,17 +702,17 @@ function useVideoExport(options = {}) {
|
|
|
709
702
|
if (tierDecision.tier === 1 || tierDecision.tier === 2) {
|
|
710
703
|
setPhase("Preparing audio\u2026");
|
|
711
704
|
try {
|
|
712
|
-
const buffers = await
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
705
|
+
const { buffers, missing: missingSources } = await loadAudioTimelineSources(
|
|
706
|
+
timeline,
|
|
707
|
+
exportMediaReader({
|
|
708
|
+
audio: config.audio,
|
|
709
|
+
images,
|
|
710
|
+
mediaProvider: config.mediaProvider,
|
|
711
|
+
resourcePolicy: config.resourcePolicy
|
|
712
|
+
})
|
|
713
|
+
);
|
|
718
714
|
try {
|
|
719
715
|
if (cancelled()) return;
|
|
720
|
-
const missingSources = [...new Set(timeline.map((clip) => clip.src))].filter(
|
|
721
|
-
(src) => !buffers.has(src)
|
|
722
|
-
);
|
|
723
716
|
if (missingSources.length > 0) {
|
|
724
717
|
audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
|
|
725
718
|
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
@@ -727,14 +720,10 @@ function useVideoExport(options = {}) {
|
|
|
727
720
|
if (buffers.size === 0) {
|
|
728
721
|
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
729
722
|
} else {
|
|
730
|
-
|
|
731
|
-
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
732
|
-
exportDuration
|
|
733
|
-
);
|
|
734
|
-
renderedAudio = await renderAudioTimeline(
|
|
723
|
+
renderedAudio = await mixAudioTimeline(
|
|
735
724
|
timeline,
|
|
736
725
|
buffers,
|
|
737
|
-
|
|
726
|
+
exportDuration,
|
|
738
727
|
EXPORT_AUDIO_SAMPLE_RATE
|
|
739
728
|
);
|
|
740
729
|
if (cancelled()) return;
|