@bendyline/squisq-video 2.0.2 → 2.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bendyline LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,21 @@
1
+ # Third-Party Notices for @bendyline/squisq-video
2
+
3
+ This notice applies to the `@bendyline/squisq-video` npm package.
4
+ Squisq-authored code is licensed under the MIT license in `LICENSE`.
5
+ Third-party components remain under their respective license terms.
6
+
7
+ ## Runtime dependencies
8
+
9
+ | Package | Version | License | Repository |
10
+ | ----------------- | ------- | ------- | ----------------------------------------- |
11
+ | @bendyline/squisq | 2.1.0 | MIT | https://github.com/bendyline/squisq |
12
+ | @ffmpeg/ffmpeg | 0.12.15 | MIT | https://github.com/ffmpegwasm/ffmpeg.wasm |
13
+ | @ffmpeg/util | 0.12.2 | MIT | https://github.com/ffmpegwasm/ffmpeg.wasm |
14
+
15
+ The `@ffmpeg/ffmpeg` and `@ffmpeg/util` packages provide JavaScript APIs and
16
+ utilities. This npm package does not include `@ffmpeg/core`,
17
+ `ffmpeg-core.js`, or `ffmpeg-core.wasm`; applications that use a WebAssembly
18
+ core supply and distribute it separately.
19
+
20
+ Copyright and complete license texts for the listed dependencies are included
21
+ in their respective npm distributions and source repositories.
package/README.md CHANGED
@@ -65,8 +65,8 @@ const { data, duration } = await framesToMp4Wasm(
65
65
  fps: 30, // default 30
66
66
  quality: 'normal', // 'draft' | 'normal' | 'high' (default 'normal')
67
67
  orientation: 'landscape', // 'landscape' | 'portrait' (default 'landscape')
68
- // width / height override the orientation defaults
69
- // Optional for offline/CSP-controlled hosting:
68
+ // width / height override the orientation defaults (both must be EVEN)
69
+ // REQUIRED see "ffmpeg.wasm runtime assets" below:
70
70
  ffmpegWasm: {
71
71
  coreURL: '/vendor/ffmpeg-core.js',
72
72
  wasmURL: '/vendor/ffmpeg-core.wasm',
@@ -79,6 +79,29 @@ const { data, duration } = await framesToMp4Wasm(
79
79
 
80
80
  Encoding is H.264 (`libx264`, `yuv420p`) with optional AAC audio; frames are scaled/padded to the target dimensions preserving aspect ratio. ffmpeg.wasm needs `SharedArrayBuffer`, which normally means serving COOP/COEP headers. `framesToMp4Wasm` is browser-only; Node callers can use `framesToMp4Native` or `framesToMp4NativeBytes` from `@bendyline/squisq-cli/api`.
81
81
 
82
+ **Dimensions must be even.** `validateVideoExportOptions` (called by `resolveDimensions`, and therefore by every render path) rejects an odd `width`/`height` up front rather than rounding: H.264's `yuv420p` chroma subsampling cannot represent odd dimensions, and silently shipping a size the caller never asked for breaks aspect-ratio-sensitive pipelines. The rule is applied uniformly to MP4 and GIF so it does not depend on output format or runtime.
83
+
84
+ ### ffmpeg.wasm runtime assets
85
+
86
+ **`ffmpegWasm.coreURL` is required for every ffmpeg.wasm code path** (`framesToMp4Wasm`, plus GIF transcode and audio muxing in `@bendyline/squisq-video-react`). Omitting it throws an actionable error.
87
+
88
+ This is deliberate. `@ffmpeg/ffmpeg`'s own `load()` falls back to a hard-coded `https://unpkg.com/@ffmpeg/core@<version>/…` URL, which would make this library fetch and execute unpinned third-party code at runtime — an opaque failure on offline/CSP-restricted hosts and a supply-chain surface for every consumer. Squisq never triggers that fallback.
89
+
90
+ Host wiring: install `@ffmpeg/core`, publish its assets at a same-origin path, and point `ffmpegWasm` at them.
91
+
92
+ ```ts
93
+ import type { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
94
+
95
+ // Copy node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.{js,wasm}
96
+ // into your app's static assets, then:
97
+ export const FFMPEG_WASM: FfmpegWasmLoadConfig = {
98
+ coreURL: '/ffmpeg-core/ffmpeg-core.js',
99
+ wasmURL: '/ffmpeg-core/ffmpeg-core.wasm',
100
+ };
101
+ ```
102
+
103
+ `packages/site/vite.config.ts` (the `ffmpegCorePlugin`) plus `packages/site/src/ffmpegWasmConfig.ts` are a complete worked example for Vite, including the GPL notice that must travel with the core files. To deliberately accept a remote core, pass that URL as `coreURL` explicitly — the requirement is that the choice is yours, not a silent default.
104
+
82
105
  ### Schedule a Doc's Audio
83
106
 
84
107
  `computeAudioTimeline(doc, coverPreRoll?)` turns a doc's narration segments and timed media clips into a flat list of absolute-timed `AudioTimelineClip`s. It's pure and Node-testable, and is the single source of truth both the browser MP4 export and the CLI mix path use to place audio (so the two never drift). Narration segments are laid sequentially; timed media clips are placed at their absolute positions; every start is shifted by `coverPreRoll` (default 0) to stay in sync with a silent cover pre-roll.
package/dist/index.d.ts CHANGED
@@ -87,7 +87,29 @@ interface EncoderResult {
87
87
  /** Video duration in seconds */
88
88
  duration: number;
89
89
  }
90
- /** Fail fast with actionable errors before launching an encoder or browser. */
90
+ /**
91
+ * Fail fast with actionable errors before launching an encoder or browser.
92
+ *
93
+ * Dimensions must be EVEN. Every H.264 path encodes yuv420p, whose 2x2 chroma
94
+ * subsampling cannot represent an odd width or height — libx264 rejects it
95
+ * outright ("width not divisible by 2") and WebCodecs' `isConfigSupported`
96
+ * reports the config unsupported. This validator is the one gate every render
97
+ * path shares (CLI `renderDocToMp4`/`renderDocToGif`, `framesToMp4Native`,
98
+ * `framesToMp4Wasm`, the main-thread and worker WebCodecs encoders, and the
99
+ * browser GIF export's H.264 intermediate via {@link resolveDimensions}), so an
100
+ * odd dimension is rejected before any frame capture happens rather than after.
101
+ *
102
+ * The rule is applied uniformly to MP4 and GIF rather than only where H.264 is
103
+ * strictly involved. Native GIF encoding could technically accept odd sizes, but
104
+ * browser GIF export cannot (it muxes an H.264 intermediate), and a dimension
105
+ * rule that silently depends on output format and runtime is worse than one the
106
+ * user can learn once.
107
+ *
108
+ * Odd values are REJECTED, not silently rounded: width/height are explicit user
109
+ * intent, and quietly shipping a file at dimensions the caller never asked for
110
+ * corrupts aspect-ratio-sensitive pipelines in a way that is very hard to
111
+ * notice. The error names the two nearest legal values so the fix is one edit.
112
+ */
91
113
  declare function validateVideoExportOptions(options: VideoExportOptions): void;
92
114
  /**
93
115
  * Resolve dimensions from options, applying orientation defaults.
@@ -97,6 +119,42 @@ declare function resolveDimensions(options: VideoExportOptions): {
97
119
  height: number;
98
120
  };
99
121
 
122
+ /**
123
+ * ffmpeg.wasm core asset resolution.
124
+ *
125
+ * `@ffmpeg/ffmpeg`'s `load()` falls back to a hard-coded `CORE_URL` pointing at
126
+ * `https://unpkg.com/@ffmpeg/core@<version>/dist/umd/ffmpeg-core.js` whenever the
127
+ * caller omits `coreURL`. For a published library that default is unacceptable:
128
+ * it fetches and executes unpinned third-party code at runtime, breaks offline
129
+ * and CSP-restricted deployments with an opaque load failure, and makes the CDN
130
+ * a supply-chain surface for every consumer.
131
+ *
132
+ * Squisq therefore never lets that fallback trigger. Hosts must point the
133
+ * runtime at core assets they control; {@link resolveFfmpegWasmLoad} turns an
134
+ * absent `coreURL` into an actionable error before any encoder work starts.
135
+ *
136
+ * Browser-pure: no Node builtins, no filesystem access, no bundler assumptions.
137
+ */
138
+
139
+ /**
140
+ * How a host wires up self-hosted core assets. Referenced by the error thrown
141
+ * from {@link resolveFfmpegWasmLoad} and by the package README.
142
+ */
143
+ declare const FFMPEG_WASM_SETUP_HINT: string;
144
+ /**
145
+ * Validate and normalize an {@link FfmpegWasmLoadConfig} for `FFmpeg.load()`.
146
+ *
147
+ * @param config - Host-supplied runtime asset URLs. Must carry a `coreURL`.
148
+ * @param context - Human-readable name of the operation, used in the error.
149
+ * @param defaults - Fallbacks for URLs the package can resolve itself (the
150
+ * bundled class worker ships beside the importing module).
151
+ * @throws Error when no `coreURL` is configured, rather than silently
152
+ * inheriting @ffmpeg/ffmpeg's unpkg CDN default.
153
+ */
154
+ declare function resolveFfmpegWasmLoad(config: FfmpegWasmLoadConfig | undefined, context: string, defaults?: {
155
+ classWorkerURL?: string;
156
+ }): FfmpegWasmLoadConfig;
157
+
100
158
  /**
101
159
  * audioTimeline — Pure scheduling of a doc's audio onto the export timeline.
102
160
  *
@@ -269,4 +327,4 @@ declare function ffmpegGifOutputArgs(options: GifOutputOptions): string[];
269
327
  */
270
328
  declare function framesToMp4Wasm(frames: Uint8Array[], audio: Uint8Array | null, options?: VideoExportOptions): Promise<EncoderResult>;
271
329
 
272
- export { type AudioTimelineClip, type EncoderResult, type FfmpegWasmLoadConfig, type GifDither, type GifFilterOptions, type GifOutputOptions, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type VideoExportOptions, type VideoOrientation, type VideoQuality, audioBitrateArg, bitrateForQuality, computeAudioTimeline, ffmpegAudioMuxArgs, ffmpegGifFilterGraph, ffmpegGifOutputArgs, ffmpegVideoQualityArgs, framesToMp4Wasm, generateRenderHtml, resolveDimensions, validateVideoExportOptions };
330
+ export { type AudioTimelineClip, type EncoderResult, FFMPEG_WASM_SETUP_HINT, type FfmpegWasmLoadConfig, type GifDither, type GifFilterOptions, type GifOutputOptions, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type VideoExportOptions, type VideoOrientation, type VideoQuality, audioBitrateArg, bitrateForQuality, computeAudioTimeline, ffmpegAudioMuxArgs, ffmpegGifFilterGraph, ffmpegGifOutputArgs, ffmpegVideoQualityArgs, framesToMp4Wasm, generateRenderHtml, resolveDimensions, resolveFfmpegWasmLoad, validateVideoExportOptions };
package/dist/index.js CHANGED
@@ -23,6 +23,11 @@ function validateVideoExportOptions(options) {
23
23
  if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) {
24
24
  throw new RangeError(`Video ${label} must be a positive integer.`);
25
25
  }
26
+ if (value !== void 0 && value % 2 !== 0) {
27
+ throw new RangeError(
28
+ `Video ${label} must be an even number of pixels (got ${value}) \u2014 H.264 (yuv420p) cannot encode odd dimensions. Use ${value - 1} or ${value + 1}.`
29
+ );
30
+ }
26
31
  }
27
32
  if (options.quality !== void 0 && !Object.prototype.hasOwnProperty.call(QUALITY_PRESETS, options.quality)) {
28
33
  throw new RangeError(`Unknown video quality: ${String(options.quality)}.`);
@@ -41,14 +46,31 @@ function resolveDimensions(options) {
41
46
  };
42
47
  }
43
48
 
49
+ // src/ffmpegCore.ts
50
+ var FFMPEG_WASM_SETUP_HINT = "Copy @ffmpeg/core (node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.js and ffmpeg-core.wasm) into a same-origin static path and pass ffmpegWasm: { coreURL, wasmURL } pointing at it. See packages/site/vite.config.ts + packages/site/src/ffmpegWasmConfig.ts for a worked example. To deliberately accept a remote core, pass that URL as coreURL explicitly.";
51
+ function resolveFfmpegWasmLoad(config, context, defaults) {
52
+ const coreURL = config?.coreURL?.trim();
53
+ if (!coreURL) {
54
+ throw new Error(
55
+ `${context} needs an ffmpeg.wasm core URL, but none was configured. ${FFMPEG_WASM_SETUP_HINT}`
56
+ );
57
+ }
58
+ const classWorkerURL = config?.classWorkerURL ?? defaults?.classWorkerURL;
59
+ return {
60
+ ...config,
61
+ coreURL,
62
+ ...classWorkerURL ? { classWorkerURL } : {}
63
+ };
64
+ }
65
+
44
66
  // src/audioTimeline.ts
45
67
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
46
68
  function computeAudioTimeline(doc, coverPreRoll = 0) {
47
- const preRoll = coverPreRoll > 0 ? coverPreRoll : 0;
69
+ const preRoll = safeSeconds(coverPreRoll);
48
70
  const clips = [];
49
71
  let cursor = 0;
50
72
  for (const seg of doc.audio?.segments ?? []) {
51
- const durationSec = Math.max(0, seg.duration);
73
+ const durationSec = safeSeconds(seg.duration);
52
74
  if (durationSec > 0 && seg.src) {
53
75
  clips.push({ src: seg.src, startSec: cursor + preRoll, sourceInSec: 0, durationSec });
54
76
  }
@@ -56,17 +78,21 @@ function computeAudioTimeline(doc, coverPreRoll = 0) {
56
78
  }
57
79
  for (const clip of resolveMediaSchedule(doc)) {
58
80
  if (clip.kind !== "audio") continue;
81
+ if (!Number.isFinite(clip.absoluteStart) || !Number.isFinite(clip.absoluteEnd)) continue;
59
82
  const durationSec = Math.max(0, clip.absoluteEnd - clip.absoluteStart);
60
83
  if (durationSec <= 0 || !clip.src) continue;
61
84
  clips.push({
62
85
  src: clip.src,
63
- startSec: clip.absoluteStart + preRoll,
64
- sourceInSec: clip.sourceIn,
86
+ startSec: safeSeconds(clip.absoluteStart) + preRoll,
87
+ sourceInSec: safeSeconds(clip.sourceIn),
65
88
  durationSec
66
89
  });
67
90
  }
68
91
  return clips;
69
92
  }
93
+ function safeSeconds(value) {
94
+ return Number.isFinite(value) && value > 0 ? value : 0;
95
+ }
70
96
 
71
97
  // src/renderHtml.ts
72
98
  var MIME_MAP = {
@@ -88,13 +114,14 @@ function inferMimeType(filename) {
88
114
  const ext = filename.split(".").pop()?.toLowerCase() ?? "";
89
115
  return MIME_MAP[ext] ?? "application/octet-stream";
90
116
  }
117
+ var BINARY_STRING_CHUNK = 32768;
91
118
  function arrayBufferToDataUrl(buffer, mimeType) {
92
119
  const bytes = new Uint8Array(buffer);
93
- let binary = "";
94
- for (let i = 0; i < bytes.length; i++) {
95
- binary += String.fromCharCode(bytes[i]);
120
+ const parts = [];
121
+ for (let i = 0; i < bytes.length; i += BINARY_STRING_CHUNK) {
122
+ parts.push(String.fromCharCode(...bytes.subarray(i, i + BINARY_STRING_CHUNK)));
96
123
  }
97
- return `data:${mimeType};base64,${btoa(binary)}`;
124
+ return `data:${mimeType};base64,${btoa(parts.join(""))}`;
98
125
  }
99
126
  function escapeForScript(str) {
100
127
  return str.replace(/<\/(script)/gi, "<\\/$1");
@@ -235,6 +262,7 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
235
262
  "framesToMp4Wasm is browser-only. In Node.js, use framesToMp4Native or framesToMp4NativeBytes from @bendyline/squisq-cli/api."
236
263
  );
237
264
  }
265
+ const loadConfig = resolveFfmpegWasmLoad(options.ffmpegWasm, "framesToMp4Wasm");
238
266
  const duration = frames.length / fps;
239
267
  const { FFmpeg } = await import("@ffmpeg/ffmpeg");
240
268
  const ffmpeg = new FFmpeg();
@@ -245,7 +273,7 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
245
273
  onProgress(Math.min(percent, 99), "encoding");
246
274
  }
247
275
  });
248
- await ffmpeg.load(options.ffmpegWasm);
276
+ await ffmpeg.load(loadConfig);
249
277
  onProgress?.(0, "writing frames");
250
278
  const padLen = String(frames.length).length;
251
279
  for (let i = 0; i < frames.length; i++) {
@@ -299,6 +327,7 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
299
327
  }
300
328
  }
301
329
  export {
330
+ FFMPEG_WASM_SETUP_HINT,
302
331
  ORIENTATION_DIMENSIONS,
303
332
  QUALITY_PRESETS,
304
333
  audioBitrateArg,
@@ -312,6 +341,6 @@ export {
312
341
  framesToMp4Wasm,
313
342
  generateRenderHtml,
314
343
  resolveDimensions,
344
+ resolveFfmpegWasmLoad,
315
345
  validateVideoExportOptions
316
346
  };
317
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Cross-runtime video and animated-GIF helpers with browser-based ffmpeg.wasm encoding for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -33,7 +33,10 @@
33
33
  }
34
34
  },
35
35
  "files": [
36
- "dist"
36
+ "dist",
37
+ "!dist/**/*.map",
38
+ "LICENSE",
39
+ "NOTICE.md"
37
40
  ],
38
41
  "scripts": {
39
42
  "build": "tsup",
@@ -41,7 +44,7 @@
41
44
  "typecheck": "tsc --noEmit"
42
45
  },
43
46
  "dependencies": {
44
- "@bendyline/squisq": "2.1.0",
47
+ "@bendyline/squisq": "2.2.0",
45
48
  "@ffmpeg/ffmpeg": "0.12.15",
46
49
  "@ffmpeg/util": "0.12.2"
47
50
  },
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/types.ts","../src/audioTimeline.ts","../src/renderHtml.ts","../src/ffmpegArgs.ts","../src/wasmEncoder.ts"],"sourcesContent":["/**\n * Video Export Types\n *\n * Shared type definitions for video encoding and render HTML generation.\n * Used by both the browser-based encoder and the CLI native encoder.\n */\n\n/**\n * Video quality preset.\n * Controls the H.264 encoding speed/quality trade-off and constant rate factor.\n *\n * - draft: ultrafast preset, CRF 28 — fast encode, lower quality (~1-2 Mbps)\n * - normal: medium preset, CRF 23 — balanced (~3-5 Mbps)\n * - high: slow preset, CRF 18 — best quality, slowest (~8-12 Mbps)\n */\nexport type VideoQuality = 'draft' | 'normal' | 'high';\n\n/** Viewport orientation for video output. */\nexport type VideoOrientation = 'landscape' | 'portrait';\n\n/** Optional URLs for self-hosting or pinning ffmpeg.wasm runtime assets. */\nexport interface FfmpegWasmLoadConfig {\n coreURL?: string;\n wasmURL?: string;\n workerURL?: string;\n classWorkerURL?: string;\n}\n\n/** Encoding preset parameters mapped from VideoQuality. */\nexport interface QualityPreset {\n /** FFmpeg -preset value (ultrafast, medium, slow) */\n preset: string;\n /** FFmpeg -crf value (lower = higher quality, 0-51 range) */\n crf: number;\n /**\n * Bits per pixel for WebCodecs bitrate targeting.\n * Target bitrate = width * height * bitsPerPixel (see {@link bitrateForQuality}).\n * These values reproduce the historical per-quality formula exactly:\n * the old code used a `width * height * 4` baseline, halved for draft and\n * doubled for high — i.e. 2 / 4 / 8 bits per pixel.\n */\n bitsPerPixel: number;\n /** Target AAC audio bitrate in bits/sec for muxed audio tracks. */\n audioBitrate: number;\n}\n\n/** Quality preset lookup — shared between wasm and native encoders. */\nexport const QUALITY_PRESETS: Record<VideoQuality, QualityPreset> = {\n draft: { preset: 'ultrafast', crf: 28, bitsPerPixel: 2, audioBitrate: 96_000 },\n normal: { preset: 'medium', crf: 23, bitsPerPixel: 4, audioBitrate: 128_000 },\n high: { preset: 'slow', crf: 18, bitsPerPixel: 8, audioBitrate: 192_000 },\n};\n\n/**\n * Target H.264 bitrate (bits/sec) for a given quality at a given resolution.\n *\n * Computes `width * height * preset.bitsPerPixel`. With bitsPerPixel of\n * 2 / 4 / 8 (draft / normal / high) this is numerically identical to the\n * legacy formula (`width * height * 4` baseline, ×0.5 for draft, ×2 for high)\n * that previously lived in both the main-thread and worker WebCodecs encoders.\n * The single source of truth now lives here so every encode path agrees.\n */\nexport function bitrateForQuality(q: VideoQuality, width: number, height: number): number {\n const preset = QUALITY_PRESETS[q] ?? QUALITY_PRESETS.normal;\n return Math.round(width * height * preset.bitsPerPixel);\n}\n\n/** Viewport dimensions for each orientation. */\nexport const ORIENTATION_DIMENSIONS: Record<VideoOrientation, { width: number; height: number }> = {\n landscape: { width: 1920, height: 1080 },\n portrait: { width: 1080, height: 1920 },\n};\n\n/** Options for video export encoding. */\nexport interface VideoExportOptions {\n /** Frames per second (default: 30) */\n fps?: number;\n /** Video width in pixels (default: based on orientation) */\n width?: number;\n /** Video height in pixels (default: based on orientation) */\n height?: number;\n /** Encoding quality preset (default: 'normal') */\n quality?: VideoQuality;\n /** Viewport orientation (default: 'landscape') */\n orientation?: VideoOrientation;\n /** Optional self-hosted ffmpeg.wasm assets for offline/CSP-controlled use. */\n ffmpegWasm?: FfmpegWasmLoadConfig;\n /**\n * Progress callback. Called during encoding with completion percentage and phase description.\n * @param percent - 0-100 completion percentage\n * @param phase - Human-readable description of current phase (e.g., 'encoding', 'muxing')\n */\n onProgress?: (percent: number, phase: string) => void;\n}\n\n/** Result from the wasm encoder. */\nexport interface EncoderResult {\n /** MP4 file bytes */\n data: Uint8Array;\n /** Video duration in seconds */\n duration: number;\n}\n\n/** Fail fast with actionable errors before launching an encoder or browser. */\nexport function validateVideoExportOptions(options: VideoExportOptions): void {\n if (\n options.fps !== undefined &&\n (!Number.isFinite(options.fps) || options.fps <= 0 || options.fps > 120)\n ) {\n throw new RangeError('Video FPS must be a finite number between 1 and 120.');\n }\n for (const [label, value] of [\n ['width', options.width],\n ['height', options.height],\n ] as const) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {\n throw new RangeError(`Video ${label} must be a positive integer.`);\n }\n }\n if (\n options.quality !== undefined &&\n !Object.prototype.hasOwnProperty.call(QUALITY_PRESETS, options.quality)\n ) {\n throw new RangeError(`Unknown video quality: ${String(options.quality)}.`);\n }\n if (\n options.orientation !== undefined &&\n !Object.prototype.hasOwnProperty.call(ORIENTATION_DIMENSIONS, options.orientation)\n ) {\n throw new RangeError(`Unknown video orientation: ${String(options.orientation)}.`);\n }\n}\n\n/**\n * Resolve dimensions from options, applying orientation defaults.\n */\nexport function resolveDimensions(options: VideoExportOptions): {\n width: number;\n height: number;\n} {\n validateVideoExportOptions(options);\n const orientation = options.orientation ?? 'landscape';\n const defaults = ORIENTATION_DIMENSIONS[orientation];\n return {\n width: options.width ?? defaults.width,\n height: options.height ?? defaults.height,\n };\n}\n","/**\n * audioTimeline — Pure scheduling of a doc's audio onto the export timeline.\n *\n * Browser-pure and Node-testable: turns a {@link Doc} into a flat list of\n * absolute-timed {@link AudioTimelineClip}s. This is the single source of\n * truth the browser MP4 export uses to place audio, and it deliberately\n * replicates the exact schedule math the CLI mix path uses so both agree:\n *\n * - Narration segments (`doc.audio.segments[]`) are laid **sequentially**,\n * each starting where the previous one ended — mirroring the CLI, which\n * concatenates the segment files in order.\n * - Timed media clips (`block.media` + `doc.documentMedia`) are placed at\n * their **absolute** doc-timeline positions via the shared\n * `resolveMediaSchedule()` helper (the same one the CLI calls), honouring\n * each clip's trim window (`sourceIn` / `absoluteEnd - absoluteStart`).\n * - Every start time is shifted by `coverPreRoll` so a cover pre-roll padding\n * (silent leading frames) keeps audio in sync.\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport { resolveMediaSchedule } from '@bendyline/squisq/schemas';\n\n/** One audio source placed on the absolute export timeline. */\nexport interface AudioTimelineClip {\n /** Source path (mp3/webm/mp4/…), relative to the doc's media dir. */\n src: string;\n /** Absolute second on the export timeline where this clip starts. */\n startSec: number;\n /** In-point within the source file to begin playback from. */\n sourceInSec: number;\n /** Played length in seconds (the trimmed window of the source). */\n durationSec: number;\n}\n\n/**\n * Flatten a doc's narration + timed-media audio into absolute-timed clips.\n *\n * Pure — depends only on `doc` (and reuses `resolveMediaSchedule` from core so\n * the browser export and the CLI mix never drift). Returns `[]` for a doc with\n * no audio at all.\n *\n * @param doc - The document to schedule audio for.\n * @param coverPreRoll - Leading silent padding (seconds) added ahead of every\n * clip, matching the cover-slide pre-roll frames. Default 0.\n */\nexport function computeAudioTimeline(doc: Doc, coverPreRoll = 0): AudioTimelineClip[] {\n const preRoll = coverPreRoll > 0 ? coverPreRoll : 0;\n const clips: AudioTimelineClip[] = [];\n\n // ── Narration: laid sequentially (matches the CLI's ordered concat). ──\n let cursor = 0;\n for (const seg of doc.audio?.segments ?? []) {\n const durationSec = Math.max(0, seg.duration);\n if (durationSec > 0 && seg.src) {\n clips.push({ src: seg.src, startSec: cursor + preRoll, sourceInSec: 0, durationSec });\n }\n cursor += durationSec;\n }\n\n // ── Timed media clips: absolute positions from the shared schedule. ──\n for (const clip of resolveMediaSchedule(doc)) {\n if (clip.kind !== 'audio') continue;\n const durationSec = Math.max(0, clip.absoluteEnd - clip.absoluteStart);\n if (durationSec <= 0 || !clip.src) continue;\n clips.push({\n src: clip.src,\n startSec: clip.absoluteStart + preRoll,\n sourceInSec: clip.sourceIn,\n durationSec,\n });\n }\n\n return clips;\n}\n","/**\n * Render HTML Generation for Video Frame Capture\n *\n * Generates a self-contained HTML document that loads the SquisqPlayer standalone\n * bundle in renderMode, embedding all images and audio as base64 data URIs.\n *\n * The generated page mounts one standalone player whose instance handle is\n * available through `SquisqPlayer.getHandle(root)`. Headless callers use that\n * handle's render API to step through frames and capture screenshots.\n *\n * Browser-pure: uses only btoa() and Uint8Array — no Node.js APIs.\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface RenderHtmlOptions {\n /** The IIFE player bundle source code (from PLAYER_BUNDLE) */\n playerScript: string;\n\n /**\n * Map of relative image paths (as they appear in the Doc) to binary data.\n * Converted to base64 data URIs and embedded in the HTML.\n */\n images?: Map<string, ArrayBuffer>;\n\n /**\n * Map of audio segment names/paths to binary audio data.\n * Converted to base64 data URIs and embedded in the HTML.\n */\n audio?: Map<string, ArrayBuffer>;\n\n /** Viewport width in CSS pixels (default: 1920) */\n width?: number;\n\n /** Viewport height in CSS pixels (default: 1080) */\n height?: number;\n\n /** Caption style for the rendered video. Omit for no captions. */\n captionStyle?: 'standard' | 'social';\n\n /**\n * Whether Squisq layer animations and block transitions are rendered.\n * Defaults to true. Embedded/timed media and document timing are unaffected.\n */\n animationsEnabled?: boolean;\n}\n\n// ── MIME Detection ─────────────────────────────────────────────────\n\nconst MIME_MAP: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n avif: 'image/avif',\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n mp4: 'video/mp4',\n webm: 'video/webm',\n};\n\nfunction inferMimeType(filename: string): string {\n const ext = filename.split('.').pop()?.toLowerCase() ?? '';\n return MIME_MAP[ext] ?? 'application/octet-stream';\n}\n\n// ── Base64 Encoding (browser-pure) ────────────────────────────────\n\n/**\n * Convert an ArrayBuffer to a base64 data URI.\n * Uses only standard Web APIs (Uint8Array + btoa).\n */\nfunction arrayBufferToDataUrl(buffer: ArrayBuffer, mimeType: string): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return `data:${mimeType};base64,${btoa(binary)}`;\n}\n\n// ── Escaping ───────────────────────────────────────────────────────\n\n/**\n * Prevent `</script>` from prematurely closing the script tag.\n */\nfunction escapeForScript(str: string): string {\n return str.replace(/<\\/(script)/gi, '<\\\\/$1');\n}\n\n/** Escape HTML special characters. */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n// ── HTML Generation ────────────────────────────────────────────────\n\n/**\n * Generate a self-contained HTML document for headless video frame capture.\n *\n * The page mounts the SquisqPlayer in renderMode. Call\n * `SquisqPlayer.getHandle(root)` to obtain this instance's render API.\n *\n * @param doc - The Doc to render\n * @param options - Render HTML options including player script and media\n * @returns Complete HTML string ready to be loaded in a headless browser\n */\nexport function generateRenderHtml(doc: Doc, options: RenderHtmlOptions): string {\n const {\n playerScript,\n images,\n audio,\n width = 1920,\n height = 1080,\n captionStyle,\n animationsEnabled = true,\n } = options;\n\n // Build base64 image map\n const imageMap: Record<string, string> = {};\n if (images) {\n for (const [path, buffer] of images.entries()) {\n imageMap[path] = arrayBufferToDataUrl(buffer, inferMimeType(path));\n }\n }\n\n // Build base64 audio map\n const audioMap: Record<string, string> = {};\n let hasAudio = false;\n if (audio) {\n for (const [name, buffer] of audio.entries()) {\n audioMap[name] = arrayBufferToDataUrl(buffer, inferMimeType(name));\n hasAudio = true;\n }\n }\n\n const docJson = escapeForScript(JSON.stringify(doc));\n const imageMapJson = escapeForScript(JSON.stringify(imageMap));\n const audioMapJson = hasAudio ? escapeForScript(JSON.stringify(audioMap)) : 'null';\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=${width}, height=${height}\">\n<title>${escapeHtml('Squisq Video Render')}</title>\n<style>\n*,*::before,*::after{box-sizing:border-box}\nhtml,body{margin:0;padding:0;width:${width}px;height:${height}px;overflow:hidden;background:#000}\n#squisq-root{width:${width}px;height:${height}px;display:flex;align-items:center;justify-content:center}\n</style>\n</head>\n<body>\n<div id=\"squisq-root\"></div>\n<script>${escapeForScript(playerScript)}</script>\n<script>\n(function(){\n var doc = JSON.parse(${JSON.stringify(docJson)});\n var images = JSON.parse(${JSON.stringify(imageMapJson)});\n var audio = ${audioMapJson === 'null' ? 'null' : 'JSON.parse(' + JSON.stringify(audioMapJson) + ')'};\n var root = document.getElementById(\"squisq-root\");\n SquisqPlayer.mount(root, doc, {\n mode: \"slideshow\",\n images: images,\n audio: audio,\n autoPlay: false,\n basePath: \".\",\n renderMode: true,\n animationsEnabled: ${JSON.stringify(animationsEnabled)}${captionStyle ? `,\\n captionStyle: ${JSON.stringify(captionStyle)}` : ''}\n });\n})();\n</script>\n</body>\n</html>`;\n}\n","/**\n * FFmpeg argument builders — the single source of truth for translating a\n * {@link VideoQuality} into ffmpeg CLI flags. Shared verbatim by every\n * ffmpeg-based encode path: the wasm encoder ({@link ./wasmEncoder}), the\n * video-react fallback worker, and the CLI native encoder. Deriving these\n * from {@link QUALITY_PRESETS} keeps the browser and CLI byte-for-byte aligned.\n *\n * Pure, dependency-free, and unit-testable in isolation (the actual ffmpeg\n * invocations live behind wasm/child-process boundaries that are awkward to\n * exercise directly).\n */\n\nimport { QUALITY_PRESETS, type VideoQuality } from './types.js';\n\n/** Dithering algorithms intentionally exposed by Squisq's GIF encoder. */\nexport type GifDither = 'bayer' | 'sierra2_4a' | 'none';\n\n/** Options for the shared palette-based animated GIF filter graph. */\nexport interface GifFilterOptions {\n width: number;\n height: number;\n /** Palette size, from 2 through GIF's maximum of 256. */\n maxColors?: number;\n /** Palette dithering algorithm (default: `sierra2_4a`). */\n dither?: GifDither;\n /** Ordered Bayer strength, used only when `dither` is `bayer` (0-5). */\n bayerScale?: number;\n}\n\n/** Options for GIF muxing in addition to the palette filter graph. */\nexport interface GifOutputOptions extends GifFilterOptions {\n /** Number of repeats; 0 loops forever and -1 disables looping. */\n loop?: number;\n}\n\n/**\n * H.264 speed/quality flags (`-preset`, `-crf`) for a quality level.\n * @example ffmpegVideoQualityArgs('high') // ['-preset', 'slow', '-crf', '18']\n */\nexport function ffmpegVideoQualityArgs(quality: VideoQuality): string[] {\n const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal;\n return ['-preset', preset.preset, '-crf', String(preset.crf)];\n}\n\n/**\n * AAC audio-bitrate flag value in ffmpeg's `k` shorthand for a quality level.\n * @example audioBitrateArg('high') // '192k'\n */\nexport function audioBitrateArg(quality: VideoQuality): string {\n const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal;\n return `${preset.audioBitrate / 1000}k`;\n}\n\n/**\n * AAC muxing flags that preserve the complete video timeline.\n *\n * `-shortest` by itself truncates video whenever narration ends early. Padding\n * the audio stream first makes the video stream the shortest input instead, so\n * audio longer than the video is trimmed while shorter audio becomes silence.\n */\nexport function ffmpegAudioMuxArgs(bitrate: string | number): string[] {\n return ['-c:a', 'aac', '-b:a', String(bitrate), '-af', 'apad', '-shortest'];\n}\n\n/**\n * Build a high-quality, compression-friendly GIF palette filter graph.\n *\n * A single palette is derived from the parts of the frame that change, while\n * `diff_mode=rectangle` keeps error diffusion inside the changed rectangle so\n * static slide backgrounds remain byte-stable and compress efficiently.\n */\nexport function ffmpegGifFilterGraph(options: GifFilterOptions): string {\n const { width, height } = options;\n const maxColors = options.maxColors ?? 256;\n const dither = options.dither ?? 'sierra2_4a';\n const bayerScale = options.bayerScale ?? 3;\n\n for (const [label, value] of [\n ['width', width],\n ['height', height],\n ] as const) {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new RangeError(`GIF ${label} must be a positive integer.`);\n }\n }\n if (!Number.isSafeInteger(maxColors) || maxColors < 2 || maxColors > 256) {\n throw new RangeError('GIF maxColors must be an integer between 2 and 256.');\n }\n if (!['bayer', 'sierra2_4a', 'none'].includes(dither)) {\n throw new RangeError(`Unknown GIF dither: ${String(dither)}.`);\n }\n if (!Number.isSafeInteger(bayerScale) || bayerScale < 0 || bayerScale > 5) {\n throw new RangeError('GIF bayerScale must be an integer between 0 and 5.');\n }\n\n const ditherArgs =\n dither === 'bayer' ? `dither=bayer:bayer_scale=${bayerScale}` : `dither=${dither}`;\n return (\n `[0:v]scale=${width}:${height}:force_original_aspect_ratio=decrease:flags=lanczos,` +\n `pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black,split[gif_frames][gif_palette_source];` +\n `[gif_palette_source]palettegen=stats_mode=diff:max_colors=${maxColors}:reserve_transparent=0[gif_palette];` +\n `[gif_frames][gif_palette]paletteuse=${ditherArgs}:diff_mode=rectangle[gif_out]`\n );\n}\n\n/** Build the output-side FFmpeg arguments for an animated GIF. */\nexport function ffmpegGifOutputArgs(options: GifOutputOptions): string[] {\n const loop = options.loop ?? 0;\n if (!Number.isSafeInteger(loop) || loop < -1 || loop > 65_535) {\n throw new RangeError('GIF loop must be an integer between -1 and 65535.');\n }\n return [\n '-filter_complex',\n ffmpegGifFilterGraph(options),\n '-map',\n '[gif_out]',\n '-an',\n '-loop',\n String(loop),\n ];\n}\n","/**\n * WASM Video Encoder\n *\n * Encodes PNG frame screenshots into an MP4 video using ffmpeg.wasm.\n * Browser-pure — no Node.js APIs. The encoder requires a browser runtime with\n * SharedArrayBuffer support (normally supplied via COOP/COEP headers).\n *\n * Uses @ffmpeg/ffmpeg for H.264 encoding and optional AAC audio muxing.\n */\n\nimport { fetchFile } from '@ffmpeg/util';\n\nimport type { VideoExportOptions, EncoderResult } from './types.js';\nimport { resolveDimensions } from './types.js';\nimport { ffmpegVideoQualityArgs, audioBitrateArg, ffmpegAudioMuxArgs } from './ffmpegArgs.js';\n\n/**\n * Encode an array of PNG frame screenshots into an MP4 video.\n *\n * @param frames - Array of PNG image bytes (one per frame, in order)\n * @param audio - Optional WAV/MP3/AAC audio bytes to mux into the video\n * @param options - Encoding options (fps, quality, dimensions, progress)\n * @returns Encoded MP4 data and duration metadata\n */\nexport async function framesToMp4Wasm(\n frames: Uint8Array[],\n audio: Uint8Array | null,\n options: VideoExportOptions = {},\n): Promise<EncoderResult> {\n const fps = options.fps ?? 30;\n const quality = options.quality ?? 'normal';\n const { width, height } = resolveDimensions(options);\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n\n const runtime = globalThis as typeof globalThis & {\n process?: { versions?: { node?: string } };\n };\n if (runtime.process?.versions?.node && typeof window === 'undefined') {\n throw new Error(\n 'framesToMp4Wasm is browser-only. In Node.js, use framesToMp4Native or framesToMp4NativeBytes from @bendyline/squisq-cli/api.',\n );\n }\n\n const duration = frames.length / fps;\n\n // Initialize ffmpeg.wasm\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n const ffmpeg = new FFmpeg();\n try {\n ffmpeg.on('progress', ({ progress }) => {\n if (onProgress) {\n const percent = Math.round(progress * 100);\n onProgress(Math.min(percent, 99), 'encoding');\n }\n });\n\n await ffmpeg.load(options.ffmpegWasm);\n\n onProgress?.(0, 'writing frames');\n\n // Write frame PNGs to virtual filesystem\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await ffmpeg.writeFile(name, frames[i]);\n\n // Report frame-write progress (0-40% of total)\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 40), 'writing frames');\n }\n }\n\n // Write audio if provided\n if (audio) {\n await ffmpeg.writeFile('audio-input', audio);\n }\n\n onProgress?.(40, 'encoding');\n\n // Build ffmpeg command\n const padPattern = `frame-%0${padLen}d.png`;\n const args = ['-y', '-framerate', String(fps), '-i', padPattern];\n\n // Add audio input\n if (audio) {\n args.push('-i', 'audio-input');\n }\n\n // Video encoding settings\n args.push(\n '-c:v',\n 'libx264',\n ...ffmpegVideoQualityArgs(quality),\n '-pix_fmt',\n 'yuv420p',\n '-vf',\n `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`,\n );\n\n // Audio encoding\n if (audio) {\n args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));\n }\n\n args.push('output.mp4');\n\n // Run encoding\n const exitCode = await ffmpeg.exec(args);\n if (exitCode !== 0) {\n throw new Error(`ffmpeg.wasm failed with exit code ${exitCode}`);\n }\n\n onProgress?.(95, 'reading output');\n\n // Read the output file\n const data = await ffmpeg.readFile('output.mp4');\n\n // Cleanup virtual filesystem\n for (let i = 0; i < frames.length; i++) {\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await ffmpeg.deleteFile(name);\n }\n if (audio) {\n await ffmpeg.deleteFile('audio-input');\n }\n await ffmpeg.deleteFile('output.mp4');\n\n onProgress?.(100, 'done');\n\n // ffmpeg.readFile returns Uint8Array for binary files\n const outputData = data instanceof Uint8Array ? data : new TextEncoder().encode(data as string);\n\n return { data: outputData, duration };\n } finally {\n ffmpeg.terminate();\n }\n}\n\n// Re-export fetchFile for convenience — consumers may need it to prepare audio bytes\nexport { fetchFile };\n"],"mappings":"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","names":[]}