@bendyline/squisq-video-react 2.0.2 → 2.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.
- package/COPYING.GPL-2.0.txt +339 -0
- package/LICENSE +21 -0
- package/NOTICE.md +39 -0
- package/README.md +34 -7
- package/dist/chunk-KQG6DZ7G.js +549 -0
- package/dist/chunk-VI5AIVOQ.js +1951 -0
- package/dist/chunk-VILZP3GL.js +856 -0
- package/dist/chunk-YZN7D4IC.js +300 -0
- package/dist/components/index.d.ts +90 -0
- package/dist/components/index.js +11 -0
- package/dist/encoder/index.d.ts +95 -0
- package/dist/encoder/index.js +13 -0
- package/dist/hooks/index.d.ts +32 -0
- package/dist/hooks/index.js +10 -0
- package/dist/index.d.ts +8 -277
- package/dist/index.js +12 -1503
- package/dist/useVideoExport-NtqZVgaz.d.ts +115 -0
- package/dist/workers/encode.worker.js +37 -20
- package/dist/workers/ffmpeg.class-worker.js +0 -1
- package/package.json +26 -8
- package/dist/chunk-6DKLHXX3.js +0 -47
- package/dist/chunk-6DKLHXX3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/workers/encode.worker.js.map +0 -1
- package/dist/workers/ffmpeg.class-worker.js.map +0 -1
- package/src/VideoExportButton.tsx +0 -87
- package/src/VideoExportModal.tsx +0 -553
- package/src/__tests__/audioTrack.test.ts +0 -108
- package/src/__tests__/gifTranscode.test.ts +0 -118
- package/src/__tests__/mp4MuxAudio.test.ts +0 -72
- package/src/__tests__/useVideoExportGif.test.ts +0 -155
- package/src/__tests__/videoExportProps.test.tsx +0 -148
- package/src/__tests__/workerEncoder.test.ts +0 -143
- package/src/audioTrack.ts +0 -332
- package/src/gifTranscode.ts +0 -75
- package/src/hooks/useFrameCapture.ts +0 -268
- package/src/hooks/useVideoExport.ts +0 -647
- package/src/index.ts +0 -40
- package/src/mainThreadEncoder.ts +0 -158
- package/src/mp4Mux.ts +0 -123
- package/src/workerEncoder.ts +0 -175
- package/src/workers/encode.worker.ts +0 -439
- package/src/workers/ffmpeg.class-worker.ts +0 -11
- package/src/workers/workerTypes.ts +0 -89
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MediaProvider, Doc } from '@bendyline/squisq/schemas';
|
|
2
|
+
import { ResourcePolicy } from '@bendyline/squisq/markdown';
|
|
3
|
+
import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
4
|
+
import { CaptionMode } from '@bendyline/squisq-react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* useVideoExport — Main orchestration hook for browser video export.
|
|
8
|
+
*
|
|
9
|
+
* Coordinates frame capture (hidden iframe + html2canvas) with
|
|
10
|
+
* main-thread WebCodecs encoding via mp4-muxer. Manages the full
|
|
11
|
+
* lifecycle: prepare → capture + encode → download.
|
|
12
|
+
*
|
|
13
|
+
* Encoding runs on the main thread because frame capture via html2canvas
|
|
14
|
+
* (~100-200ms per frame) is the bottleneck, not encoding (~1ms per frame
|
|
15
|
+
* with hardware-accelerated WebCodecs). Worker offloading would add
|
|
16
|
+
* complexity with minimal benefit.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* const { state, progress, phase, startExport, cancel, downloadUrl } = useVideoExport();
|
|
20
|
+
* <button onClick={() => startExport(doc, options)}>Export</button>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type VideoExportState = 'idle' | 'preparing' | 'capturing' | 'encoding' | 'complete' | 'error';
|
|
24
|
+
/** Browser export container format. */
|
|
25
|
+
type VideoOutputFormat = 'mp4' | 'gif';
|
|
26
|
+
type VideoAudioPolicy = 'require' | 'best-effort' | 'omit';
|
|
27
|
+
interface VideoExportConfig {
|
|
28
|
+
/** Output container (default: 'mp4') */
|
|
29
|
+
outputFormat?: VideoOutputFormat;
|
|
30
|
+
/**
|
|
31
|
+
* How authored MP4 audio is handled. `require` (default) fails before
|
|
32
|
+
* capture when audio cannot be loaded/decoded and fails the export if later
|
|
33
|
+
* encoding or muxing fails. `best-effort` explicitly permits video-only
|
|
34
|
+
* degradation; `omit` intentionally skips audio.
|
|
35
|
+
*/
|
|
36
|
+
audioPolicy?: VideoAudioPolicy;
|
|
37
|
+
/** Render authored animations and slide transitions (default: true for MP4, false for GIF). */
|
|
38
|
+
animationsEnabled?: boolean;
|
|
39
|
+
/** Encoding quality preset (default: 'normal') */
|
|
40
|
+
quality?: VideoQuality;
|
|
41
|
+
/** Frames per second (default: 30) */
|
|
42
|
+
fps?: number;
|
|
43
|
+
/** Viewport orientation (default: 'landscape') */
|
|
44
|
+
orientation?: VideoOrientation;
|
|
45
|
+
/** Explicit output width. GIF defaults to 960 landscape / 540 portrait. */
|
|
46
|
+
width?: number;
|
|
47
|
+
/** Explicit output height. GIF defaults to 540 landscape / 960 portrait. */
|
|
48
|
+
height?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Map of relative image paths to binary data.
|
|
51
|
+
* Used to embed images into the render HTML.
|
|
52
|
+
*/
|
|
53
|
+
images?: Map<string, ArrayBuffer>;
|
|
54
|
+
/**
|
|
55
|
+
* Map of audio segment names to binary data.
|
|
56
|
+
* Used to embed audio into the render HTML.
|
|
57
|
+
*/
|
|
58
|
+
audio?: Map<string, ArrayBuffer>;
|
|
59
|
+
/** MediaProvider to resolve media URLs (alternative to passing images directly) */
|
|
60
|
+
mediaProvider?: MediaProvider;
|
|
61
|
+
/** Policy and byte/time limits for MediaProvider URLs. */
|
|
62
|
+
resourcePolicy?: ResourcePolicy;
|
|
63
|
+
/** Caption mode for the exported video (default: 'off') */
|
|
64
|
+
captionMode?: CaptionMode;
|
|
65
|
+
/** Player IIFE bundle (unused in browser export, kept for CLI/Playwright path) */
|
|
66
|
+
playerScript?: string;
|
|
67
|
+
/** Optional self-hosted ffmpeg.wasm core URLs for fallback/offline/CSP use. */
|
|
68
|
+
ffmpegWasm?: FfmpegWasmLoadConfig;
|
|
69
|
+
}
|
|
70
|
+
interface VideoExportResult {
|
|
71
|
+
/** Current export state */
|
|
72
|
+
state: VideoExportState;
|
|
73
|
+
/** 0–100 progress percentage */
|
|
74
|
+
progress: number;
|
|
75
|
+
/** Human-readable description of the current phase */
|
|
76
|
+
phase: string;
|
|
77
|
+
/** Video duration detected from the doc (seconds) */
|
|
78
|
+
duration: number;
|
|
79
|
+
/** Effective output format for the current or most recent export. */
|
|
80
|
+
outputFormat: VideoOutputFormat;
|
|
81
|
+
/** Encoder backend ('webcodecs' when WebCodecs H.264 active, 'ffmpeg-wasm' when worker fallback active, null when idle) */
|
|
82
|
+
backend: 'webcodecs' | 'ffmpeg-wasm' | null;
|
|
83
|
+
/** Blob download URL (populated when state === 'complete') */
|
|
84
|
+
downloadUrl: string | null;
|
|
85
|
+
/** File size in bytes (populated when state === 'complete') */
|
|
86
|
+
fileSize: number;
|
|
87
|
+
/**
|
|
88
|
+
* Whether an audio track was muxed into the exported MP4 (populated when
|
|
89
|
+
* state === 'complete'). False when the doc had no audio or when audio was
|
|
90
|
+
* skipped/degraded — see {@link audioSkippedReason}.
|
|
91
|
+
*/
|
|
92
|
+
audioIncluded: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Why audio is absent, when `audioIncluded` is false. `null` means the doc
|
|
95
|
+
* simply had no audio (not a failure); a non-null string explains a genuine
|
|
96
|
+
* capability shortfall or runtime failure. Audio problems never fail the
|
|
97
|
+
* export — the video always completes.
|
|
98
|
+
*/
|
|
99
|
+
audioSkippedReason: string | null;
|
|
100
|
+
/** Error message (populated when state === 'error') */
|
|
101
|
+
error: string | null;
|
|
102
|
+
/** Seconds elapsed since export started */
|
|
103
|
+
elapsed: number;
|
|
104
|
+
/** Estimated seconds remaining (0 when idle or complete) */
|
|
105
|
+
estimatedRemaining: number;
|
|
106
|
+
/** Start a new export */
|
|
107
|
+
startExport: (doc: Doc, config: VideoExportConfig) => Promise<void>;
|
|
108
|
+
/** Cancel an in-progress export */
|
|
109
|
+
cancel: () => void;
|
|
110
|
+
/** Reset state back to idle (e.g., after complete or error) */
|
|
111
|
+
reset: () => void;
|
|
112
|
+
}
|
|
113
|
+
declare function useVideoExport(): VideoExportResult;
|
|
114
|
+
|
|
115
|
+
export { type VideoAudioPolicy as V, type VideoExportConfig as a, type VideoExportResult as b, type VideoExportState as c, type VideoOutputFormat as d, useVideoExport as u };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMp4Muxer
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-VI5AIVOQ.js";
|
|
4
4
|
|
|
5
5
|
// src/workers/encode.worker.ts
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
bitrateForQuality,
|
|
8
|
+
ffmpegVideoQualityArgs,
|
|
9
|
+
resolveFfmpegWasmLoad
|
|
10
|
+
} from "@bendyline/squisq-video";
|
|
7
11
|
var backend = null;
|
|
8
12
|
var cancelled = false;
|
|
9
13
|
var videoEncoder = null;
|
|
@@ -12,19 +16,27 @@ var ffmpegInstance = null;
|
|
|
12
16
|
var ffmpegFrames = [];
|
|
13
17
|
var ffmpegConfig = null;
|
|
14
18
|
var totalFramesReceived = 0;
|
|
19
|
+
var totalFramesExpected = 0;
|
|
15
20
|
function post(msg, transfer) {
|
|
16
21
|
self.postMessage(msg, { transfer: transfer ?? [] });
|
|
17
22
|
}
|
|
18
23
|
function postProgress(percent, phase) {
|
|
19
24
|
post({ type: "progress", percent, phase });
|
|
20
25
|
}
|
|
26
|
+
function postFrameProgress(scale, phase) {
|
|
27
|
+
if (totalFramesExpected <= 0) {
|
|
28
|
+
post({ type: "progress", percent: 0, phase, indeterminate: true });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const ratio = Math.min(totalFramesReceived / totalFramesExpected, 1);
|
|
32
|
+
post({ type: "progress", percent: Math.round(ratio * scale), phase });
|
|
33
|
+
}
|
|
21
34
|
function postError(message) {
|
|
22
35
|
post({ type: "error", message });
|
|
23
36
|
}
|
|
24
37
|
function disposeFfmpeg() {
|
|
25
|
-
const instance = ffmpegInstance;
|
|
26
38
|
try {
|
|
27
|
-
|
|
39
|
+
ffmpegInstance?.terminate();
|
|
28
40
|
} finally {
|
|
29
41
|
ffmpegInstance = null;
|
|
30
42
|
}
|
|
@@ -84,10 +96,10 @@ async function encodeFrameWebCodecs(msg) {
|
|
|
84
96
|
frame.close();
|
|
85
97
|
msg.bitmap.close();
|
|
86
98
|
totalFramesReceived++;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
postFrameProgress(50, frameLabel("Encoding"));
|
|
100
|
+
}
|
|
101
|
+
function frameLabel(verb) {
|
|
102
|
+
return totalFramesExpected > 0 ? `${verb} frame ${totalFramesReceived}/${totalFramesExpected}` : `${verb} frame ${totalFramesReceived}`;
|
|
91
103
|
}
|
|
92
104
|
async function finalizeWebCodecs() {
|
|
93
105
|
if (!videoEncoder || !muxer || cancelled) return;
|
|
@@ -105,14 +117,14 @@ async function initFfmpegWasm(config) {
|
|
|
105
117
|
ffmpegFrames = [];
|
|
106
118
|
ffmpegSegments.length = 0;
|
|
107
119
|
totalFramesReceived = 0;
|
|
120
|
+
const load = resolveFfmpegWasmLoad(config.ffmpegWasm, "The ffmpeg.wasm encoder fallback", {
|
|
121
|
+
classWorkerURL: new URL("./ffmpeg.class-worker.js", import.meta.url).href
|
|
122
|
+
});
|
|
108
123
|
let ffmpeg = null;
|
|
109
124
|
try {
|
|
110
125
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
111
126
|
ffmpeg = new FFmpeg();
|
|
112
|
-
await ffmpeg.load(
|
|
113
|
-
...config.ffmpegWasm,
|
|
114
|
-
classWorkerURL: config.ffmpegWasm?.classWorkerURL ?? new URL("./ffmpeg.class-worker.js", import.meta.url).href
|
|
115
|
-
});
|
|
127
|
+
await ffmpeg.load(load);
|
|
116
128
|
ffmpegInstance = ffmpeg;
|
|
117
129
|
} catch (err) {
|
|
118
130
|
ffmpeg?.terminate();
|
|
@@ -140,10 +152,7 @@ async function encodeFrameFfmpeg(msg) {
|
|
|
140
152
|
const arrayBuffer = await blob.arrayBuffer();
|
|
141
153
|
ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });
|
|
142
154
|
totalFramesReceived++;
|
|
143
|
-
|
|
144
|
-
Math.round(totalFramesReceived / (totalFramesReceived + 1) * 40),
|
|
145
|
-
`Collecting frame ${totalFramesReceived}`
|
|
146
|
-
);
|
|
155
|
+
postFrameProgress(80, frameLabel("Collecting"));
|
|
147
156
|
const batchSize = (ffmpegConfig?.fps ?? 30) * 10;
|
|
148
157
|
if (ffmpegFrames.length >= batchSize) {
|
|
149
158
|
await encodeFfmpegBatch();
|
|
@@ -156,12 +165,19 @@ async function execOrThrow(ffmpeg, args, operation) {
|
|
|
156
165
|
throw new Error(`${operation} failed with ffmpeg exit code ${exitCode}`);
|
|
157
166
|
}
|
|
158
167
|
}
|
|
168
|
+
async function readBinaryFile(ffmpeg, path) {
|
|
169
|
+
const data = await ffmpeg.readFile(path);
|
|
170
|
+
if (typeof data === "string") {
|
|
171
|
+
throw new Error(`Expected binary data from ffmpeg for ${path}`);
|
|
172
|
+
}
|
|
173
|
+
return data;
|
|
174
|
+
}
|
|
159
175
|
async function encodeFfmpegBatch() {
|
|
160
176
|
if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;
|
|
161
177
|
const ffmpeg = ffmpegInstance;
|
|
162
178
|
const config = ffmpegConfig;
|
|
163
179
|
const batchIndex = ffmpegSegments.length;
|
|
164
|
-
|
|
180
|
+
postFrameProgress(80, `Encoding batch ${batchIndex + 1}\u2026`);
|
|
165
181
|
for (const frame of ffmpegFrames) {
|
|
166
182
|
const paddedIndex = String(frame.index).padStart(6, "0");
|
|
167
183
|
await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);
|
|
@@ -188,7 +204,7 @@ async function encodeFfmpegBatch() {
|
|
|
188
204
|
],
|
|
189
205
|
`Encoding video segment ${batchIndex + 1}`
|
|
190
206
|
);
|
|
191
|
-
const segmentData = await ffmpeg
|
|
207
|
+
const segmentData = await readBinaryFile(ffmpeg, segmentName);
|
|
192
208
|
ffmpegSegments.push(segmentData);
|
|
193
209
|
for (const frame of ffmpegFrames) {
|
|
194
210
|
const paddedIndex = String(frame.index).padStart(6, "0");
|
|
@@ -218,7 +234,7 @@ async function finalizeFfmpeg() {
|
|
|
218
234
|
["-f", "concat", "-safe", "0", "-i", "concat.txt", "-c", "copy", "output.mp4"],
|
|
219
235
|
"Concatenating video segments"
|
|
220
236
|
);
|
|
221
|
-
finalData = await ffmpeg
|
|
237
|
+
finalData = await readBinaryFile(ffmpeg, "output.mp4");
|
|
222
238
|
for (let i = 0; i < ffmpegSegments.length; i++) {
|
|
223
239
|
await ffmpeg.deleteFile(`seg_${i}.mp4`);
|
|
224
240
|
}
|
|
@@ -239,6 +255,7 @@ async function handleMessage(msg) {
|
|
|
239
255
|
case "init": {
|
|
240
256
|
cancelled = false;
|
|
241
257
|
totalFramesReceived = 0;
|
|
258
|
+
totalFramesExpected = typeof msg.totalFrames === "number" && Number.isFinite(msg.totalFrames) ? Math.max(0, Math.floor(msg.totalFrames)) : 0;
|
|
242
259
|
if (await supportsWebCodecsH264(msg)) {
|
|
243
260
|
backend = "webcodecs";
|
|
244
261
|
initWebCodecs(msg);
|
|
@@ -289,6 +306,7 @@ async function handleMessage(msg) {
|
|
|
289
306
|
disposeFfmpeg();
|
|
290
307
|
ffmpegFrames = [];
|
|
291
308
|
ffmpegSegments.length = 0;
|
|
309
|
+
totalFramesExpected = 0;
|
|
292
310
|
backend = null;
|
|
293
311
|
break;
|
|
294
312
|
}
|
|
@@ -303,4 +321,3 @@ self.onmessage = (event) => {
|
|
|
303
321
|
postError(message);
|
|
304
322
|
});
|
|
305
323
|
};
|
|
306
|
-
//# sourceMappingURL=encode.worker.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-video-react",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.14.0"
|
|
28
|
+
},
|
|
26
29
|
"main": "./dist/index.js",
|
|
27
30
|
"types": "./dist/index.d.ts",
|
|
28
31
|
"exports": {
|
|
@@ -30,14 +33,29 @@
|
|
|
30
33
|
"types": "./dist/index.d.ts",
|
|
31
34
|
"import": "./dist/index.js",
|
|
32
35
|
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./components": {
|
|
38
|
+
"types": "./dist/components/index.d.ts",
|
|
39
|
+
"import": "./dist/components/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./hooks": {
|
|
42
|
+
"types": "./dist/hooks/index.d.ts",
|
|
43
|
+
"import": "./dist/hooks/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./encoder": {
|
|
46
|
+
"types": "./dist/encoder/index.d.ts",
|
|
47
|
+
"import": "./dist/encoder/index.js"
|
|
33
48
|
}
|
|
34
49
|
},
|
|
35
50
|
"files": [
|
|
36
51
|
"dist",
|
|
37
|
-
"
|
|
52
|
+
"!dist/**/*.map",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"NOTICE.md",
|
|
55
|
+
"COPYING.GPL-2.0.txt"
|
|
38
56
|
],
|
|
39
57
|
"scripts": {
|
|
40
|
-
"build": "tsup",
|
|
58
|
+
"build": "tsup && node ../../scripts/remove-empty-chunks.mjs dist",
|
|
41
59
|
"dev": "concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
|
|
42
60
|
"typecheck": "tsc --noEmit"
|
|
43
61
|
},
|
|
@@ -46,20 +64,20 @@
|
|
|
46
64
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
47
65
|
},
|
|
48
66
|
"dependencies": {
|
|
49
|
-
"@bendyline/squisq": "2.
|
|
50
|
-
"@bendyline/squisq-video": "2.0
|
|
51
|
-
"@bendyline/squisq-react": "2.
|
|
67
|
+
"@bendyline/squisq": "2.3.0",
|
|
68
|
+
"@bendyline/squisq-video": "2.2.0",
|
|
69
|
+
"@bendyline/squisq-react": "2.3.0",
|
|
52
70
|
"@ffmpeg/core": "0.12.9",
|
|
53
71
|
"@ffmpeg/ffmpeg": "0.12.15",
|
|
54
72
|
"@ffmpeg/util": "0.12.2",
|
|
55
|
-
"html2canvas": "1.4.1"
|
|
56
|
-
"mp4-muxer": "5.2.2"
|
|
73
|
+
"html2canvas": "1.4.1"
|
|
57
74
|
},
|
|
58
75
|
"devDependencies": {
|
|
59
76
|
"@types/react": "18.3.28",
|
|
60
77
|
"@types/react-dom": "18.3.7",
|
|
61
78
|
"react": "18.3.1",
|
|
62
79
|
"react-dom": "18.3.1",
|
|
80
|
+
"mp4-muxer": "5.2.2",
|
|
63
81
|
"tsup": "8.5.1",
|
|
64
82
|
"typescript": "5.9.3"
|
|
65
83
|
},
|
package/dist/chunk-6DKLHXX3.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// src/mp4Mux.ts
|
|
2
|
-
import { Muxer, ArrayBufferTarget } from "mp4-muxer";
|
|
3
|
-
function createMp4Muxer(options) {
|
|
4
|
-
const target = new ArrayBufferTarget();
|
|
5
|
-
const muxer = new Muxer({
|
|
6
|
-
target,
|
|
7
|
-
video: {
|
|
8
|
-
codec: "avc",
|
|
9
|
-
width: options.width,
|
|
10
|
-
height: options.height
|
|
11
|
-
},
|
|
12
|
-
// Only declare the audio track when requested so the video-only
|
|
13
|
-
// configuration stays byte-identical to the historical output.
|
|
14
|
-
...options.audio ? {
|
|
15
|
-
audio: {
|
|
16
|
-
codec: "aac",
|
|
17
|
-
numberOfChannels: options.audio.numberOfChannels,
|
|
18
|
-
sampleRate: options.audio.sampleRate
|
|
19
|
-
}
|
|
20
|
-
} : {},
|
|
21
|
-
fastStart: "in-memory"
|
|
22
|
-
});
|
|
23
|
-
return {
|
|
24
|
-
hasAudioTrack: options.audio !== void 0,
|
|
25
|
-
addVideoChunk(chunk, meta) {
|
|
26
|
-
muxer.addVideoChunk(chunk, meta);
|
|
27
|
-
},
|
|
28
|
-
addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta) {
|
|
29
|
-
muxer.addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta);
|
|
30
|
-
},
|
|
31
|
-
addAudioChunk(chunk, meta) {
|
|
32
|
-
muxer.addAudioChunk(chunk, meta);
|
|
33
|
-
},
|
|
34
|
-
addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta) {
|
|
35
|
-
muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);
|
|
36
|
-
},
|
|
37
|
-
finalize() {
|
|
38
|
-
muxer.finalize();
|
|
39
|
-
return target.buffer;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export {
|
|
45
|
-
createMp4Muxer
|
|
46
|
-
};
|
|
47
|
-
//# sourceMappingURL=chunk-6DKLHXX3.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mp4Mux.ts"],"sourcesContent":["/**\n * mp4Mux — Thin wrapper around mp4-muxer for WebCodecs encoding.\n *\n * Creates a Muxer instance configured for H.264 video (and, optionally, an\n * AAC audio track), accumulates encoded chunks, and produces a final MP4\n * ArrayBuffer. When no `audio` option is supplied the muxer configuration is\n * byte-identical to the video-only path.\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer';\n\nexport interface Mp4MuxerOptions {\n width: number;\n height: number;\n fps: number;\n /**\n * When present, declares an AAC audio track alongside the video track.\n * Feed encoded audio via {@link Mp4MuxerHandle.addAudioChunk} (from a\n * WebCodecs `AudioEncoder`) or {@link Mp4MuxerHandle.addAudioChunkRaw}.\n */\n audio?: {\n numberOfChannels: number;\n sampleRate: number;\n };\n}\n\nexport interface Mp4MuxerHandle {\n /** Add an encoded video chunk to the muxer. */\n addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void;\n /**\n * Add a raw video sample (bytes + timing) without an `EncodedVideoChunk`.\n * Useful where no `VideoEncoder` instance is available (e.g. Node tests).\n */\n addVideoChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedVideoChunkMetadata,\n ): void;\n /** Add an encoded audio chunk (from a WebCodecs `AudioEncoder`). */\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;\n /**\n * Add a raw audio sample (bytes + timing) without an `EncodedAudioChunk`.\n * Useful where no `AudioEncoder` instance is available (e.g. Node tests).\n */\n addAudioChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedAudioChunkMetadata,\n ): void;\n /** Whether this muxer was created with an audio track. */\n readonly hasAudioTrack: boolean;\n /** Finalize and return the MP4 as an ArrayBuffer. */\n finalize(): ArrayBuffer;\n}\n\n/**\n * Create an MP4 muxer configured for H.264 video, plus an optional AAC track.\n */\nexport function createMp4Muxer(options: Mp4MuxerOptions): Mp4MuxerHandle {\n const target = new ArrayBufferTarget();\n\n const muxer = new Muxer({\n target,\n video: {\n codec: 'avc',\n width: options.width,\n height: options.height,\n },\n // Only declare the audio track when requested so the video-only\n // configuration stays byte-identical to the historical output.\n ...(options.audio\n ? {\n audio: {\n codec: 'aac' as const,\n numberOfChannels: options.audio.numberOfChannels,\n sampleRate: options.audio.sampleRate,\n },\n }\n : {}),\n fastStart: 'in-memory',\n });\n\n return {\n hasAudioTrack: options.audio !== undefined,\n\n addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {\n muxer.addVideoChunk(chunk, meta);\n },\n\n addVideoChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedVideoChunkMetadata,\n ) {\n muxer.addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta);\n },\n\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {\n muxer.addAudioChunk(chunk, meta);\n },\n\n addAudioChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedAudioChunkMetadata,\n ) {\n muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);\n },\n\n finalize(): ArrayBuffer {\n muxer.finalize();\n return target.buffer;\n },\n };\n}\n"],"mappings":";AASA,SAAS,OAAO,yBAAyB;AAqDlC,SAAS,eAAe,SAA0C;AACvE,QAAM,SAAS,IAAI,kBAAkB;AAErC,QAAM,QAAQ,IAAI,MAAM;AAAA,IACtB;AAAA,IACA,OAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,IAClB;AAAA;AAAA;AAAA,IAGA,GAAI,QAAQ,QACR;AAAA,MACE,OAAO;AAAA,QACL,OAAO;AAAA,QACP,kBAAkB,QAAQ,MAAM;AAAA,QAChC,YAAY,QAAQ,MAAM;AAAA,MAC5B;AAAA,IACF,IACA,CAAC;AAAA,IACL,WAAW;AAAA,EACb,CAAC;AAED,SAAO;AAAA,IACL,eAAe,QAAQ,UAAU;AAAA,IAEjC,cAAc,OAA0B,MAAkC;AACxE,YAAM,cAAc,OAAO,IAAI;AAAA,IACjC;AAAA,IAEA,iBACE,MACA,MACA,iBACA,gBACA,MACA;AACA,YAAM,iBAAiB,MAAM,MAAM,iBAAiB,gBAAgB,IAAI;AAAA,IAC1E;AAAA,IAEA,cAAc,OAA0B,MAAkC;AACxE,YAAM,cAAc,OAAO,IAAI;AAAA,IACjC;AAAA,IAEA,iBACE,MACA,MACA,iBACA,gBACA,MACA;AACA,YAAM,iBAAiB,MAAM,MAAM,iBAAiB,gBAAgB,IAAI;AAAA,IAC1E;AAAA,IAEA,WAAwB;AACtB,YAAM,SAAS;AACf,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
|