@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 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. Uses WebCodecs for hardware-accelerated H.264 encoding (with an ffmpeg.wasm worker fallback) and html2canvas for frame capture. As of v1.5 the exported MP4 also carries an **audio** track (narration + timed media).
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 quality/fps/orientation, export, and download |
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:** landscape (1920x1080) or portrait (1080x1920)
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 onClick={() => startExport(doc, { images, audio, quality: 'normal', fps: 30 })}>
109
- Export
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="video.mp4">
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). Use `supportsWebCodecs()` to probe at runtime:
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.** The audio track is muxed via WebCodecs AAC when available
142
- (`supportsWebCodecsAac()`); otherwise it is skipped and the export reports
143
- `audioIncluded: false` with an `audioSkippedReason`. Audio problems never fail
144
- the export — the video always completes. `supportsWebCodecsH264(config)` probes a
145
- specific encoder configuration; `EncoderConfig` is also exported.
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 encoding |
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 video rendering |
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/fps/orientation/captionMode selections
108
- * and is merged (as a base) into the config passed to the export hook, so a
109
- * host can share one config shape with `useVideoExport`. The individual
110
- * `images`/`audio`/`mediaProvider`/`playerScript` props still take
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 as a base into the
136
- * config passed to the export hook. Forwarded to {@link VideoExportModal}.
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
- /** Button label (default: "Export Video") */
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 };