@bendyline/squisq-video-react 1.2.2 → 2.0.1
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 +63 -22
- package/dist/index.d.ts +33 -13
- package/dist/index.js +511 -212
- package/dist/index.js.map +1 -1
- package/dist/workers/encode.worker.js +110 -80
- package/dist/workers/encode.worker.js.map +1 -1
- package/dist/workers/ffmpeg.class-worker.d.ts +2 -0
- package/dist/workers/ffmpeg.class-worker.js +180 -0
- package/dist/workers/ffmpeg.class-worker.js.map +1 -0
- package/package.json +7 -5
- package/src/VideoExportButton.tsx +12 -5
- package/src/VideoExportModal.tsx +219 -62
- package/src/__tests__/gifTranscode.test.ts +118 -0
- package/src/__tests__/useVideoExportGif.test.ts +155 -0
- package/src/__tests__/videoExportProps.test.tsx +82 -7
- package/src/__tests__/workerEncoder.test.ts +143 -0
- package/src/audioTrack.ts +18 -9
- package/src/gifTranscode.ts +75 -0
- package/src/hooks/useFrameCapture.ts +26 -21
- package/src/hooks/useVideoExport.ts +104 -15
- package/src/index.ts +2 -0
- package/src/mainThreadEncoder.ts +5 -10
- package/src/workerEncoder.ts +66 -14
- package/src/workers/encode.worker.ts +134 -86
- package/src/workers/ffmpeg.class-worker.ts +11 -0
- package/src/workers/workerTypes.ts +9 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bendyline/squisq-video-react
|
|
2
2
|
|
|
3
|
-
React components and hooks for exporting Squisq documents to MP4 video directly in the browser.
|
|
3
|
+
React components and hooks for exporting Squisq documents to MP4 video or animated GIF directly in the browser. MP4 uses WebCodecs for hardware-accelerated H.264 encoding (with an ffmpeg.wasm worker fallback); GIF uses that compact video as an intermediate for an ffmpeg.wasm global-palette pass. html2canvas provides deterministic frame capture. MP4 carries an **audio** track (narration + timed media); GIF is silent by design.
|
|
4
4
|
|
|
5
5
|
Part of the [Squisq](https://github.com/bendyline/squisq) monorepo.
|
|
6
6
|
|
|
@@ -27,10 +27,18 @@ function App() {
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
To open directly in the compact GIF preset:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<VideoExportButton doc={myDoc} defaultConfig={{ outputFormat: 'gif' }} />
|
|
34
|
+
```
|
|
35
|
+
|
|
30
36
|
**v1.5:** `playerScript` is now **optional** — the browser export captures frames
|
|
31
37
|
from a live in-page `DocPlayer`, so the standalone bundle is only needed for
|
|
32
38
|
CLI/Playwright-style pipelines. A new `defaultConfig?: Partial<VideoExportConfig>`
|
|
33
39
|
prop seeds the modal's initial quality/fps/orientation/caption selections.
|
|
40
|
+
Both components also accept `colorScheme="light" | "dark"` so their portaled
|
|
41
|
+
modal can match the host application; the default remains `light`.
|
|
34
42
|
|
|
35
43
|
### Full Export Modal
|
|
36
44
|
|
|
@@ -58,10 +66,10 @@ function App() {
|
|
|
58
66
|
|
|
59
67
|
## Components
|
|
60
68
|
|
|
61
|
-
| Component | Description
|
|
62
|
-
| ------------------- |
|
|
63
|
-
| `VideoExportModal` | Full modal UI — configure
|
|
64
|
-
| `VideoExportButton` | Drop-in button that opens the export modal via portal
|
|
69
|
+
| Component | Description |
|
|
70
|
+
| ------------------- | ------------------------------------------------------------------------ |
|
|
71
|
+
| `VideoExportModal` | Full modal UI — configure MP4/GIF, motion, quality, fps, and orientation |
|
|
72
|
+
| `VideoExportButton` | Drop-in button that opens the export modal via portal |
|
|
65
73
|
|
|
66
74
|
## Hooks
|
|
67
75
|
|
|
@@ -74,10 +82,12 @@ function App() {
|
|
|
74
82
|
|
|
75
83
|
The `VideoExportModal` lets users configure:
|
|
76
84
|
|
|
85
|
+
- **Format:** MP4 video or animated GIF
|
|
77
86
|
- **Quality:** draft, normal, or high
|
|
78
|
-
- **FPS:** 15, 24, or 30
|
|
79
|
-
- **Orientation:**
|
|
87
|
+
- **FPS:** 10, 15, 24, or 30
|
|
88
|
+
- **Orientation:** MP4 defaults to 1920x1080/1080x1920; GIF defaults to 960x540/540x960
|
|
80
89
|
- **Captions:** off, standard, or social
|
|
90
|
+
- **Animations & transitions:** enabled by default for MP4 and disabled by default for GIF
|
|
81
91
|
|
|
82
92
|
## Using the Hook Directly
|
|
83
93
|
|
|
@@ -90,6 +100,7 @@ function CustomExport({ doc, images, audio }) {
|
|
|
90
100
|
const {
|
|
91
101
|
state, // 'idle' | 'preparing' | 'capturing' | 'encoding' | 'complete' | 'error'
|
|
92
102
|
progress, // 0–100
|
|
103
|
+
outputFormat, // 'mp4' | 'gif'
|
|
93
104
|
backend, // 'webcodecs' | 'ffmpeg-wasm' | null
|
|
94
105
|
elapsed,
|
|
95
106
|
estimatedRemaining,
|
|
@@ -105,12 +116,20 @@ function CustomExport({ doc, images, audio }) {
|
|
|
105
116
|
|
|
106
117
|
return (
|
|
107
118
|
<div>
|
|
108
|
-
<button
|
|
109
|
-
|
|
119
|
+
<button
|
|
120
|
+
onClick={() =>
|
|
121
|
+
startExport(doc, {
|
|
122
|
+
outputFormat: 'gif',
|
|
123
|
+
images,
|
|
124
|
+
animationsEnabled: false,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
Export GIF
|
|
110
129
|
</button>
|
|
111
130
|
{state === 'capturing' && <p>Progress: {progress}%</p>}
|
|
112
131
|
{downloadUrl && (
|
|
113
|
-
<a href={downloadUrl} download=
|
|
132
|
+
<a href={downloadUrl} download={`document.${outputFormat}`}>
|
|
114
133
|
Download
|
|
115
134
|
</a>
|
|
116
135
|
)}
|
|
@@ -124,7 +143,25 @@ function CustomExport({ doc, images, audio }) {
|
|
|
124
143
|
WebCodecs H.264 encoding requires Chrome 94+ or Edge 94+. When WebCodecs H.264
|
|
125
144
|
is unavailable, the export automatically falls back to an ffmpeg.wasm worker —
|
|
126
145
|
which requires `SharedArrayBuffer` (i.e. Cross-Origin-Isolation headers on the
|
|
127
|
-
host page).
|
|
146
|
+
host page). Animated GIF always performs an ffmpeg.wasm palette pass and therefore
|
|
147
|
+
also requires `SharedArrayBuffer`. The packaged class worker is bundler-safe.
|
|
148
|
+
|
|
149
|
+
`@ffmpeg/core` is pinned as a runtime dependency. Hosts should publish its ESM
|
|
150
|
+
`ffmpeg-core.js` and `ffmpeg-core.wasm` files from the same origin and pass their
|
|
151
|
+
URLs, especially for offline or Content-Security-Policy-controlled deployments:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const config = {
|
|
155
|
+
ffmpegWasm: {
|
|
156
|
+
// Copy from node_modules/@ffmpeg/core/dist/esm/ during your build.
|
|
157
|
+
coreURL: '/ffmpeg-core/ffmpeg-core.js',
|
|
158
|
+
wasmURL: '/ffmpeg-core/ffmpeg-core.wasm',
|
|
159
|
+
workerURL: '/vendor/ffmpeg-core.worker.js', // for a multithread core
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use `supportsWebCodecs()` to probe at runtime:
|
|
128
165
|
|
|
129
166
|
```ts
|
|
130
167
|
import {
|
|
@@ -138,11 +175,12 @@ if (!supportsWebCodecs()) {
|
|
|
138
175
|
}
|
|
139
176
|
```
|
|
140
177
|
|
|
141
|
-
**Audio tiers.**
|
|
142
|
-
(`supportsWebCodecsAac()`)
|
|
143
|
-
`audioIncluded: false`
|
|
144
|
-
the export — the video always
|
|
145
|
-
|
|
178
|
+
**Audio tiers.** MP4 audio is muxed via WebCodecs AAC when available
|
|
179
|
+
(`supportsWebCodecsAac()`), then via ffmpeg.wasm when cross-origin isolation is
|
|
180
|
+
available, and otherwise skipped with `audioIncluded: false` plus an
|
|
181
|
+
`audioSkippedReason`. Audio problems never fail the export — the video always
|
|
182
|
+
completes. GIF skips audio preparation entirely. `supportsWebCodecsH264(config)` probes a specific encoder
|
|
183
|
+
configuration; `EncoderConfig` and `FfmpegWasmLoadConfig` are also exported.
|
|
146
184
|
|
|
147
185
|
## Full API Reference
|
|
148
186
|
|
|
@@ -151,13 +189,16 @@ for complete prop tables, `VideoExportConfig`, and the encoder utilities.
|
|
|
151
189
|
|
|
152
190
|
## Related Packages
|
|
153
191
|
|
|
154
|
-
| Package | Description
|
|
155
|
-
| -------------------------------------------------------------------------------- |
|
|
156
|
-
| [@bendyline/squisq-video](https://www.npmjs.com/package/@bendyline/squisq-video) | Headless video rendering and WASM
|
|
157
|
-
| [@bendyline/squisq](https://www.npmjs.com/package/@bendyline/squisq) | Headless core — schemas, templates, markdown
|
|
158
|
-
| [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components for rendering docs
|
|
159
|
-
| [@bendyline/squisq-cli](https://www.npmjs.com/package/@bendyline/squisq-cli) | CLI for document conversion and
|
|
192
|
+
| Package | Description |
|
|
193
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
194
|
+
| [@bendyline/squisq-video](https://www.npmjs.com/package/@bendyline/squisq-video) | Headless video/GIF rendering and WASM helpers |
|
|
195
|
+
| [@bendyline/squisq](https://www.npmjs.com/package/@bendyline/squisq) | Headless core — schemas, templates, markdown |
|
|
196
|
+
| [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components for rendering docs |
|
|
197
|
+
| [@bendyline/squisq-cli](https://www.npmjs.com/package/@bendyline/squisq-cli) | CLI for document conversion and MP4/GIF rendering |
|
|
160
198
|
|
|
161
199
|
## License
|
|
162
200
|
|
|
163
201
|
[MIT](https://github.com/bendyline/squisq/blob/main/LICENSE)
|
|
202
|
+
|
|
203
|
+
The separately distributed `@ffmpeg/core` runtime retains its own
|
|
204
|
+
GPL-2.0-or-later license.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { MediaProvider, Doc } from '@bendyline/squisq/schemas';
|
|
3
|
-
import { VideoQuality, VideoOrientation, RenderHtmlOptions } from '@bendyline/squisq-video';
|
|
3
|
+
import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig, RenderHtmlOptions } from '@bendyline/squisq-video';
|
|
4
|
+
export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
4
5
|
import { CaptionMode } from '@bendyline/squisq-react';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -21,13 +22,23 @@ import { CaptionMode } from '@bendyline/squisq-react';
|
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
type VideoExportState = 'idle' | 'preparing' | 'capturing' | 'encoding' | 'complete' | 'error';
|
|
25
|
+
/** Browser export container format. */
|
|
26
|
+
type VideoOutputFormat = 'mp4' | 'gif';
|
|
24
27
|
interface VideoExportConfig {
|
|
28
|
+
/** Output container (default: 'mp4') */
|
|
29
|
+
outputFormat?: VideoOutputFormat;
|
|
30
|
+
/** Render authored animations and slide transitions (default: true for MP4, false for GIF). */
|
|
31
|
+
animationsEnabled?: boolean;
|
|
25
32
|
/** Encoding quality preset (default: 'normal') */
|
|
26
33
|
quality?: VideoQuality;
|
|
27
34
|
/** Frames per second (default: 30) */
|
|
28
35
|
fps?: number;
|
|
29
36
|
/** Viewport orientation (default: 'landscape') */
|
|
30
37
|
orientation?: VideoOrientation;
|
|
38
|
+
/** Explicit output width. GIF defaults to 960 landscape / 540 portrait. */
|
|
39
|
+
width?: number;
|
|
40
|
+
/** Explicit output height. GIF defaults to 540 landscape / 960 portrait. */
|
|
41
|
+
height?: number;
|
|
31
42
|
/**
|
|
32
43
|
* Map of relative image paths to binary data.
|
|
33
44
|
* Used to embed images into the render HTML.
|
|
@@ -44,6 +55,8 @@ interface VideoExportConfig {
|
|
|
44
55
|
captionMode?: CaptionMode;
|
|
45
56
|
/** Player IIFE bundle (unused in browser export, kept for CLI/Playwright path) */
|
|
46
57
|
playerScript?: string;
|
|
58
|
+
/** Optional self-hosted ffmpeg.wasm core URLs for fallback/offline/CSP use. */
|
|
59
|
+
ffmpegWasm?: FfmpegWasmLoadConfig;
|
|
47
60
|
}
|
|
48
61
|
interface VideoExportResult {
|
|
49
62
|
/** Current export state */
|
|
@@ -54,6 +67,8 @@ interface VideoExportResult {
|
|
|
54
67
|
phase: string;
|
|
55
68
|
/** Video duration detected from the doc (seconds) */
|
|
56
69
|
duration: number;
|
|
70
|
+
/** Effective output format for the current or most recent export. */
|
|
71
|
+
outputFormat: VideoOutputFormat;
|
|
57
72
|
/** Encoder backend ('webcodecs' when WebCodecs H.264 active, 'ffmpeg-wasm' when worker fallback active, null when idle) */
|
|
58
73
|
backend: 'webcodecs' | 'ffmpeg-wasm' | null;
|
|
59
74
|
/** Blob download URL (populated when state === 'complete') */
|
|
@@ -104,17 +119,19 @@ interface VideoExportModalProps {
|
|
|
104
119
|
/** Pre-collected audio map */
|
|
105
120
|
audio?: Map<string, ArrayBuffer>;
|
|
106
121
|
/**
|
|
107
|
-
* Seeds the modal's initial quality
|
|
108
|
-
*
|
|
109
|
-
* host can share one config shape with `useVideoExport`.
|
|
110
|
-
* `images`/`audio`/`mediaProvider`/`playerScript` props still
|
|
111
|
-
* precedence over any matching key here.
|
|
122
|
+
* Seeds the modal's initial format, motion, quality, FPS, orientation, and
|
|
123
|
+
* caption selections. It is merged (as a base) into the config passed to the
|
|
124
|
+
* export hook, so a host can share one config shape with `useVideoExport`.
|
|
125
|
+
* The individual `images`/`audio`/`mediaProvider`/`playerScript` props still
|
|
126
|
+
* take precedence over any matching key here.
|
|
112
127
|
*/
|
|
113
128
|
defaultConfig?: Partial<VideoExportConfig>;
|
|
129
|
+
/** Visual color scheme for the portaled dialog. Defaults to light. */
|
|
130
|
+
colorScheme?: 'light' | 'dark';
|
|
114
131
|
/** Called when the modal should close */
|
|
115
132
|
onClose: () => void;
|
|
116
133
|
}
|
|
117
|
-
declare function VideoExportModal({ doc, playerScript, mediaProvider, images, audio, defaultConfig, onClose, }: VideoExportModalProps): react_jsx_runtime.JSX.Element;
|
|
134
|
+
declare function VideoExportModal({ doc, playerScript, mediaProvider, images, audio, defaultConfig, colorScheme, onClose, }: VideoExportModalProps): react_jsx_runtime.JSX.Element;
|
|
118
135
|
|
|
119
136
|
interface VideoExportButtonProps {
|
|
120
137
|
/** The document to export */
|
|
@@ -132,18 +149,21 @@ interface VideoExportButtonProps {
|
|
|
132
149
|
/** Pre-collected audio map */
|
|
133
150
|
audio?: Map<string, ArrayBuffer>;
|
|
134
151
|
/**
|
|
135
|
-
* Seeds the modal's initial export settings and is merged
|
|
136
|
-
* config passed to the export hook. Forwarded to
|
|
152
|
+
* Seeds the modal's initial output format and export settings and is merged
|
|
153
|
+
* as a base into the config passed to the export hook. Forwarded to
|
|
154
|
+
* {@link VideoExportModal}.
|
|
137
155
|
*/
|
|
138
156
|
defaultConfig?: Partial<VideoExportConfig>;
|
|
139
|
-
/**
|
|
157
|
+
/** Visual color scheme forwarded to the portaled modal. Defaults to light. */
|
|
158
|
+
colorScheme?: 'light' | 'dark';
|
|
159
|
+
/** Button label (defaults to "Export Video", or "Export GIF" for a GIF default config) */
|
|
140
160
|
label?: string;
|
|
141
161
|
/** Additional inline styles for the button */
|
|
142
162
|
style?: React.CSSProperties;
|
|
143
163
|
/** Whether the button is disabled */
|
|
144
164
|
disabled?: boolean;
|
|
145
165
|
}
|
|
146
|
-
declare function VideoExportButton({ doc, playerScript, mediaProvider, images, audio, defaultConfig, label, style, disabled, }: VideoExportButtonProps): react_jsx_runtime.JSX.Element;
|
|
166
|
+
declare function VideoExportButton({ doc, playerScript, mediaProvider, images, audio, defaultConfig, colorScheme, label, style, disabled, }: VideoExportButtonProps): react_jsx_runtime.JSX.Element;
|
|
147
167
|
|
|
148
168
|
/**
|
|
149
169
|
* useFrameCapture — Hidden div + html2canvas frame capture.
|
|
@@ -199,7 +219,7 @@ interface EncoderConfig {
|
|
|
199
219
|
}
|
|
200
220
|
interface MainThreadEncoder {
|
|
201
221
|
/** Encode a single frame. The bitmap is closed after encoding. */
|
|
202
|
-
encodeFrame(bitmap: ImageBitmap, frameIndex: number): void
|
|
222
|
+
encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void>;
|
|
203
223
|
/**
|
|
204
224
|
* Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.
|
|
205
225
|
* Only valid when the encoder was created with an `audio` config; otherwise a
|
|
@@ -255,4 +275,4 @@ declare function createEncoder(config: EncoderConfig): MainThreadEncoder;
|
|
|
255
275
|
*/
|
|
256
276
|
declare function supportsWebCodecsAac(sampleRate?: number, channels?: number): Promise<boolean>;
|
|
257
277
|
|
|
258
|
-
export { type EncoderConfig, type FrameCaptureHandle, type MainThreadEncoder, VideoExportButton, type VideoExportButtonProps, type VideoExportConfig, VideoExportModal, type VideoExportModalProps, type VideoExportResult, type VideoExportState, createEncoder, supportsWebCodecs, supportsWebCodecsAac, supportsWebCodecsH264, useFrameCapture, useVideoExport };
|
|
278
|
+
export { type EncoderConfig, type FrameCaptureHandle, type MainThreadEncoder, VideoExportButton, type VideoExportButtonProps, type VideoExportConfig, VideoExportModal, type VideoExportModalProps, type VideoExportResult, type VideoExportState, type VideoOutputFormat, createEncoder, supportsWebCodecs, supportsWebCodecsAac, supportsWebCodecsH264, useFrameCapture, useVideoExport };
|