@bendyline/squisq-video 1.1.2 → 1.2.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 +58 -29
- package/dist/index.d.ts +87 -1
- package/dist/index.js +50 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -15,29 +15,36 @@ npm install @bendyline/squisq-video
|
|
|
15
15
|
|
|
16
16
|
## What's Inside
|
|
17
17
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
18
|
+
| Export | Description |
|
|
19
|
+
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `generateRenderHtml(doc, options)` | Self-contained HTML page with embedded media for headless frame capture |
|
|
21
|
+
| `framesToMp4Wasm(frames, audio, options)` | Encode PNG frames (+ optional audio) to MP4 via ffmpeg.wasm |
|
|
22
|
+
| `computeAudioTimeline(doc, coverPreRoll?)` | Flatten a doc's narration + timed media into absolute-timed audio clips (browser export and CLI mix share this) |
|
|
23
|
+
| `bitrateForQuality(quality, width, height)` | Target H.264 bitrate (`w*h*bitsPerPixel`) — the single source of truth for WebCodecs bitrate |
|
|
24
|
+
| `QUALITY_PRESETS`, `ORIENTATION_DIMENSIONS`, `resolveDimensions` | Quality/dimension presets and helpers |
|
|
25
|
+
| `VideoExportOptions`, `VideoQuality`, `VideoOrientation`, `QualityPreset`, `EncoderResult`, `AudioTimelineClip` | Shared types |
|
|
26
|
+
| `fetchFile` | Re-export of `@ffmpeg/util`'s `fetchFile` for preparing audio bytes |
|
|
27
|
+
|
|
28
|
+
This package is the shared foundation under [@bendyline/squisq-video-react](https://www.npmjs.com/package/@bendyline/squisq-video-react) (in-browser export UI) and [@bendyline/squisq-cli](https://www.npmjs.com/package/@bendyline/squisq-cli) (`squisq video`, which pairs the render HTML with Playwright capture and native ffmpeg encoding).
|
|
23
29
|
|
|
24
30
|
## Quick Start
|
|
25
31
|
|
|
26
32
|
### Generate Render HTML
|
|
27
33
|
|
|
28
|
-
Create a self-contained HTML page that mounts
|
|
34
|
+
Create a self-contained HTML page that mounts the standalone Squisq player in render mode, with all images and audio embedded as base64 data URIs. The page exposes `window.seekTo(time)` and `window.getDuration()` so a headless browser (Playwright, Puppeteer) can step through frames and screenshot each one:
|
|
29
35
|
|
|
30
36
|
```ts
|
|
31
37
|
import { generateRenderHtml } from '@bendyline/squisq-video';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
playerScript, //
|
|
36
|
-
images, // Map<string, ArrayBuffer>
|
|
37
|
-
audio, // Map<string, ArrayBuffer>
|
|
38
|
+
import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
|
|
39
|
+
|
|
40
|
+
const html = generateRenderHtml(doc, {
|
|
41
|
+
playerScript: PLAYER_BUNDLE, // the IIFE player bundle source string
|
|
42
|
+
images, // Map<string, ArrayBuffer> — embedded as data URIs
|
|
43
|
+
audio, // Map<string, ArrayBuffer> — embedded as data URIs
|
|
44
|
+
width: 1920, // default 1920
|
|
45
|
+
height: 1080, // default 1080
|
|
46
|
+
captionStyle: 'standard', // 'standard' | 'social' — omit for no captions
|
|
38
47
|
});
|
|
39
|
-
|
|
40
|
-
// The rendered page exposes window.seekTo(time) and window.getDuration()
|
|
41
48
|
```
|
|
42
49
|
|
|
43
50
|
### Encode Frames to MP4
|
|
@@ -45,24 +52,42 @@ const html = generateRenderHtml({
|
|
|
45
52
|
```ts
|
|
46
53
|
import { framesToMp4Wasm } from '@bendyline/squisq-video';
|
|
47
54
|
|
|
48
|
-
const
|
|
49
|
-
frames, // Uint8Array[]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
55
|
+
const { data, duration } = await framesToMp4Wasm(
|
|
56
|
+
frames, // Uint8Array[] — PNG frame bytes, in order
|
|
57
|
+
audioBytes, // Uint8Array | null — optional WAV/MP3/AAC track (muxed as AAC)
|
|
58
|
+
{
|
|
59
|
+
fps: 30, // default 30
|
|
60
|
+
quality: 'normal', // 'draft' | 'normal' | 'high' (default 'normal')
|
|
61
|
+
orientation: 'landscape', // 'landscape' | 'portrait' (default 'landscape')
|
|
62
|
+
// width / height override the orientation defaults
|
|
63
|
+
onProgress: (percent, phase) => console.log(`${phase}: ${percent}%`),
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
// data: Uint8Array of MP4 bytes; duration: seconds (frames.length / fps)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
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` — in browsers that means COOP/COEP headers; Node 18+ works out of the box.
|
|
70
|
+
|
|
71
|
+
### Schedule a Doc's Audio
|
|
72
|
+
|
|
73
|
+
`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.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { computeAudioTimeline } from '@bendyline/squisq-video';
|
|
77
|
+
|
|
78
|
+
const clips = computeAudioTimeline(doc, 2); // [{ src, startSec, sourceInSec, durationSec }, …]
|
|
57
79
|
```
|
|
58
80
|
|
|
59
81
|
## Quality Presets
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
83
|
+
Each `QualityPreset` also carries `bitsPerPixel` (for WebCodecs bitrate
|
|
84
|
+
targeting via `bitrateForQuality`) and `audioBitrate` (target AAC bits/sec):
|
|
85
|
+
|
|
86
|
+
| Preset | FFmpeg preset | CRF | bits/pixel | AAC bitrate | Use Case |
|
|
87
|
+
| -------- | ------------- | --- | ---------- | ----------- | ---------------------- |
|
|
88
|
+
| `draft` | ultrafast | 28 | 2 | 96 kbps | Quick previews |
|
|
89
|
+
| `normal` | medium | 23 | 4 | 128 kbps | General-purpose export |
|
|
90
|
+
| `high` | slow | 18 | 8 | 192 kbps | Final output |
|
|
66
91
|
|
|
67
92
|
## Orientation Dimensions
|
|
68
93
|
|
|
@@ -71,12 +96,16 @@ const mp4Bytes = await framesToMp4Wasm({
|
|
|
71
96
|
| `landscape` | 1920 | 1080 |
|
|
72
97
|
| `portrait` | 1080 | 1920 |
|
|
73
98
|
|
|
99
|
+
`resolveDimensions(options)` applies these defaults, honoring explicit `width`/`height` overrides.
|
|
100
|
+
|
|
101
|
+
See the full [API Reference](../../docs/API.md#bendylinesquisq-video) for all types.
|
|
102
|
+
|
|
74
103
|
## Related Packages
|
|
75
104
|
|
|
76
105
|
| Package | Description |
|
|
77
106
|
| -------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
78
107
|
| [@bendyline/squisq](https://www.npmjs.com/package/@bendyline/squisq) | Headless core — schemas, templates, markdown |
|
|
79
|
-
| [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components
|
|
108
|
+
| [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components + standalone player bundle |
|
|
80
109
|
| [@bendyline/squisq-video-react](https://www.npmjs.com/package/@bendyline/squisq-video-react) | React UI for in-browser video export |
|
|
81
110
|
| [@bendyline/squisq-cli](https://www.npmjs.com/package/@bendyline/squisq-cli) | CLI for document conversion and video rendering |
|
|
82
111
|
|
package/dist/index.d.ts
CHANGED
|
@@ -24,9 +24,29 @@ interface QualityPreset {
|
|
|
24
24
|
preset: string;
|
|
25
25
|
/** FFmpeg -crf value (lower = higher quality, 0-51 range) */
|
|
26
26
|
crf: number;
|
|
27
|
+
/**
|
|
28
|
+
* Bits per pixel for WebCodecs bitrate targeting.
|
|
29
|
+
* Target bitrate = width * height * bitsPerPixel (see {@link bitrateForQuality}).
|
|
30
|
+
* These values reproduce the historical per-quality formula exactly:
|
|
31
|
+
* the old code used a `width * height * 4` baseline, halved for draft and
|
|
32
|
+
* doubled for high — i.e. 2 / 4 / 8 bits per pixel.
|
|
33
|
+
*/
|
|
34
|
+
bitsPerPixel: number;
|
|
35
|
+
/** Target AAC audio bitrate in bits/sec for muxed audio tracks. */
|
|
36
|
+
audioBitrate: number;
|
|
27
37
|
}
|
|
28
38
|
/** Quality preset lookup — shared between wasm and native encoders. */
|
|
29
39
|
declare const QUALITY_PRESETS: Record<VideoQuality, QualityPreset>;
|
|
40
|
+
/**
|
|
41
|
+
* Target H.264 bitrate (bits/sec) for a given quality at a given resolution.
|
|
42
|
+
*
|
|
43
|
+
* Computes `width * height * preset.bitsPerPixel`. With bitsPerPixel of
|
|
44
|
+
* 2 / 4 / 8 (draft / normal / high) this is numerically identical to the
|
|
45
|
+
* legacy formula (`width * height * 4` baseline, ×0.5 for draft, ×2 for high)
|
|
46
|
+
* that previously lived in both the main-thread and worker WebCodecs encoders.
|
|
47
|
+
* The single source of truth now lives here so every encode path agrees.
|
|
48
|
+
*/
|
|
49
|
+
declare function bitrateForQuality(q: VideoQuality, width: number, height: number): number;
|
|
30
50
|
/** Viewport dimensions for each orientation. */
|
|
31
51
|
declare const ORIENTATION_DIMENSIONS: Record<VideoOrientation, {
|
|
32
52
|
width: number;
|
|
@@ -66,6 +86,49 @@ declare function resolveDimensions(options: VideoExportOptions): {
|
|
|
66
86
|
height: number;
|
|
67
87
|
};
|
|
68
88
|
|
|
89
|
+
/**
|
|
90
|
+
* audioTimeline — Pure scheduling of a doc's audio onto the export timeline.
|
|
91
|
+
*
|
|
92
|
+
* Browser-pure and Node-testable: turns a {@link Doc} into a flat list of
|
|
93
|
+
* absolute-timed {@link AudioTimelineClip}s. This is the single source of
|
|
94
|
+
* truth the browser MP4 export uses to place audio, and it deliberately
|
|
95
|
+
* replicates the exact schedule math the CLI mix path uses so both agree:
|
|
96
|
+
*
|
|
97
|
+
* - Narration segments (`doc.audio.segments[]`) are laid **sequentially**,
|
|
98
|
+
* each starting where the previous one ended — mirroring the CLI, which
|
|
99
|
+
* concatenates the segment files in order.
|
|
100
|
+
* - Timed media clips (`block.media` + `doc.documentMedia`) are placed at
|
|
101
|
+
* their **absolute** doc-timeline positions via the shared
|
|
102
|
+
* `resolveMediaSchedule()` helper (the same one the CLI calls), honouring
|
|
103
|
+
* each clip's trim window (`sourceIn` / `absoluteEnd - absoluteStart`).
|
|
104
|
+
* - Every start time is shifted by `coverPreRoll` so a cover pre-roll padding
|
|
105
|
+
* (silent leading frames) keeps audio in sync.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/** One audio source placed on the absolute export timeline. */
|
|
109
|
+
interface AudioTimelineClip {
|
|
110
|
+
/** Source path (mp3/webm/mp4/…), relative to the doc's media dir. */
|
|
111
|
+
src: string;
|
|
112
|
+
/** Absolute second on the export timeline where this clip starts. */
|
|
113
|
+
startSec: number;
|
|
114
|
+
/** In-point within the source file to begin playback from. */
|
|
115
|
+
sourceInSec: number;
|
|
116
|
+
/** Played length in seconds (the trimmed window of the source). */
|
|
117
|
+
durationSec: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Flatten a doc's narration + timed-media audio into absolute-timed clips.
|
|
121
|
+
*
|
|
122
|
+
* Pure — depends only on `doc` (and reuses `resolveMediaSchedule` from core so
|
|
123
|
+
* the browser export and the CLI mix never drift). Returns `[]` for a doc with
|
|
124
|
+
* no audio at all.
|
|
125
|
+
*
|
|
126
|
+
* @param doc - The document to schedule audio for.
|
|
127
|
+
* @param coverPreRoll - Leading silent padding (seconds) added ahead of every
|
|
128
|
+
* clip, matching the cover-slide pre-roll frames. Default 0.
|
|
129
|
+
*/
|
|
130
|
+
declare function computeAudioTimeline(doc: Doc, coverPreRoll?: number): AudioTimelineClip[];
|
|
131
|
+
|
|
69
132
|
/**
|
|
70
133
|
* Render HTML Generation for Video Frame Capture
|
|
71
134
|
*
|
|
@@ -111,6 +174,29 @@ interface RenderHtmlOptions {
|
|
|
111
174
|
*/
|
|
112
175
|
declare function generateRenderHtml(doc: Doc, options: RenderHtmlOptions): string;
|
|
113
176
|
|
|
177
|
+
/**
|
|
178
|
+
* FFmpeg argument builders — the single source of truth for translating a
|
|
179
|
+
* {@link VideoQuality} into ffmpeg CLI flags. Shared verbatim by every
|
|
180
|
+
* ffmpeg-based encode path: the wasm encoder ({@link ./wasmEncoder}), the
|
|
181
|
+
* video-react fallback worker, and the CLI native encoder. Deriving these
|
|
182
|
+
* from {@link QUALITY_PRESETS} keeps the browser and CLI byte-for-byte aligned.
|
|
183
|
+
*
|
|
184
|
+
* Pure, dependency-free, and unit-testable in isolation (the actual ffmpeg
|
|
185
|
+
* invocations live behind wasm/child-process boundaries that are awkward to
|
|
186
|
+
* exercise directly).
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* H.264 speed/quality flags (`-preset`, `-crf`) for a quality level.
|
|
191
|
+
* @example ffmpegVideoQualityArgs('high') // ['-preset', 'slow', '-crf', '18']
|
|
192
|
+
*/
|
|
193
|
+
declare function ffmpegVideoQualityArgs(quality: VideoQuality): string[];
|
|
194
|
+
/**
|
|
195
|
+
* AAC audio-bitrate flag value in ffmpeg's `k` shorthand for a quality level.
|
|
196
|
+
* @example audioBitrateArg('high') // '192k'
|
|
197
|
+
*/
|
|
198
|
+
declare function audioBitrateArg(quality: VideoQuality): string;
|
|
199
|
+
|
|
114
200
|
/**
|
|
115
201
|
* WASM Video Encoder
|
|
116
202
|
*
|
|
@@ -131,4 +217,4 @@ declare function generateRenderHtml(doc: Doc, options: RenderHtmlOptions): strin
|
|
|
131
217
|
*/
|
|
132
218
|
declare function framesToMp4Wasm(frames: Uint8Array[], audio: Uint8Array | null, options?: VideoExportOptions): Promise<EncoderResult>;
|
|
133
219
|
|
|
134
|
-
export { type EncoderResult, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type VideoExportOptions, type VideoOrientation, type VideoQuality, framesToMp4Wasm, generateRenderHtml, resolveDimensions };
|
|
220
|
+
export { type AudioTimelineClip, type EncoderResult, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type VideoExportOptions, type VideoOrientation, type VideoQuality, audioBitrateArg, bitrateForQuality, computeAudioTimeline, ffmpegVideoQualityArgs, framesToMp4Wasm, generateRenderHtml, resolveDimensions };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
var QUALITY_PRESETS = {
|
|
3
|
-
draft: { preset: "ultrafast", crf: 28 },
|
|
4
|
-
normal: { preset: "medium", crf: 23 },
|
|
5
|
-
high: { preset: "slow", crf: 18 }
|
|
3
|
+
draft: { preset: "ultrafast", crf: 28, bitsPerPixel: 2, audioBitrate: 96e3 },
|
|
4
|
+
normal: { preset: "medium", crf: 23, bitsPerPixel: 4, audioBitrate: 128e3 },
|
|
5
|
+
high: { preset: "slow", crf: 18, bitsPerPixel: 8, audioBitrate: 192e3 }
|
|
6
6
|
};
|
|
7
|
+
function bitrateForQuality(q, width, height) {
|
|
8
|
+
const preset = QUALITY_PRESETS[q] ?? QUALITY_PRESETS.normal;
|
|
9
|
+
return Math.round(width * height * preset.bitsPerPixel);
|
|
10
|
+
}
|
|
7
11
|
var ORIENTATION_DIMENSIONS = {
|
|
8
12
|
landscape: { width: 1920, height: 1080 },
|
|
9
13
|
portrait: { width: 1080, height: 1920 }
|
|
@@ -17,6 +21,33 @@ function resolveDimensions(options) {
|
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
// src/audioTimeline.ts
|
|
25
|
+
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
26
|
+
function computeAudioTimeline(doc, coverPreRoll = 0) {
|
|
27
|
+
const preRoll = coverPreRoll > 0 ? coverPreRoll : 0;
|
|
28
|
+
const clips = [];
|
|
29
|
+
let cursor = 0;
|
|
30
|
+
for (const seg of doc.audio?.segments ?? []) {
|
|
31
|
+
const durationSec = Math.max(0, seg.duration);
|
|
32
|
+
if (durationSec > 0 && seg.src) {
|
|
33
|
+
clips.push({ src: seg.src, startSec: cursor + preRoll, sourceInSec: 0, durationSec });
|
|
34
|
+
}
|
|
35
|
+
cursor += durationSec;
|
|
36
|
+
}
|
|
37
|
+
for (const clip of resolveMediaSchedule(doc)) {
|
|
38
|
+
if (clip.kind !== "audio") continue;
|
|
39
|
+
const durationSec = Math.max(0, clip.absoluteEnd - clip.absoluteStart);
|
|
40
|
+
if (durationSec <= 0 || !clip.src) continue;
|
|
41
|
+
clips.push({
|
|
42
|
+
src: clip.src,
|
|
43
|
+
startSec: clip.absoluteStart + preRoll,
|
|
44
|
+
sourceInSec: clip.sourceIn,
|
|
45
|
+
durationSec
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return clips;
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
// src/renderHtml.ts
|
|
21
52
|
var MIME_MAP = {
|
|
22
53
|
jpg: "image/jpeg",
|
|
@@ -105,6 +136,16 @@ html,body{margin:0;padding:0;width:${width}px;height:${height}px;overflow:hidden
|
|
|
105
136
|
</html>`;
|
|
106
137
|
}
|
|
107
138
|
|
|
139
|
+
// src/ffmpegArgs.ts
|
|
140
|
+
function ffmpegVideoQualityArgs(quality) {
|
|
141
|
+
const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal;
|
|
142
|
+
return ["-preset", preset.preset, "-crf", String(preset.crf)];
|
|
143
|
+
}
|
|
144
|
+
function audioBitrateArg(quality) {
|
|
145
|
+
const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal;
|
|
146
|
+
return `${preset.audioBitrate / 1e3}k`;
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
// src/wasmEncoder.ts
|
|
109
150
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
110
151
|
import { fetchFile } from "@ffmpeg/util";
|
|
@@ -112,7 +153,6 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
|
|
|
112
153
|
const fps = options.fps ?? 30;
|
|
113
154
|
const quality = options.quality ?? "normal";
|
|
114
155
|
const { width, height } = resolveDimensions(options);
|
|
115
|
-
const preset = QUALITY_PRESETS[quality];
|
|
116
156
|
const onProgress = options.onProgress;
|
|
117
157
|
if (frames.length === 0) {
|
|
118
158
|
throw new Error("No frames provided for encoding");
|
|
@@ -147,17 +187,14 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
|
|
|
147
187
|
args.push(
|
|
148
188
|
"-c:v",
|
|
149
189
|
"libx264",
|
|
150
|
-
|
|
151
|
-
preset.preset,
|
|
152
|
-
"-crf",
|
|
153
|
-
String(preset.crf),
|
|
190
|
+
...ffmpegVideoQualityArgs(quality),
|
|
154
191
|
"-pix_fmt",
|
|
155
192
|
"yuv420p",
|
|
156
193
|
"-vf",
|
|
157
194
|
`scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
|
|
158
195
|
);
|
|
159
196
|
if (audio) {
|
|
160
|
-
args.push("-c:a", "aac", "-b:a",
|
|
197
|
+
args.push("-c:a", "aac", "-b:a", audioBitrateArg(quality), "-shortest");
|
|
161
198
|
}
|
|
162
199
|
args.push("output.mp4");
|
|
163
200
|
await ffmpeg.exec(args);
|
|
@@ -179,7 +216,11 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
|
|
|
179
216
|
export {
|
|
180
217
|
ORIENTATION_DIMENSIONS,
|
|
181
218
|
QUALITY_PRESETS,
|
|
219
|
+
audioBitrateArg,
|
|
220
|
+
bitrateForQuality,
|
|
221
|
+
computeAudioTimeline,
|
|
182
222
|
fetchFile,
|
|
223
|
+
ffmpegVideoQualityArgs,
|
|
183
224
|
framesToMp4Wasm,
|
|
184
225
|
generateRenderHtml,
|
|
185
226
|
resolveDimensions
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/renderHtml.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/** 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\n/** Quality preset lookup — shared between wasm and native encoders. */\nexport const QUALITY_PRESETS: Record<VideoQuality, QualityPreset> = {\n draft: { preset: 'ultrafast', crf: 28 },\n normal: { preset: 'medium', crf: 23 },\n high: { preset: 'slow', crf: 18 },\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 /**\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/**\n * Resolve dimensions from options, applying orientation defaults.\n */\nexport function resolveDimensions(options: VideoExportOptions): {\n width: number;\n height: number;\n} {\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 * 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 exposes `window.seekTo(time)`, `window.getDuration()`, etc.\n * via the SquisqRenderAPI, enabling Playwright (or any headless browser) to step\n * 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// ── 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\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, which exposes the\n * SquisqRenderAPI on `window` (seekTo, getDuration, getCaptions, etc.).\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 { playerScript, images, audio, width = 1920, height = 1080, captionStyle } = 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 SquisqPlayer.mount(document.getElementById(\"squisq-root\"), doc, {\n mode: \"slideshow\",\n images: images,\n audio: audio,\n autoPlay: false,\n basePath: \".\",\n renderMode: true${captionStyle ? `,\\n captionStyle: ${JSON.stringify(captionStyle)}` : ''}\n });\n})();\n</script>\n</body>\n</html>`;\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. Works in any environment with SharedArrayBuffer\n * support (browsers with COOP/COEP headers, or Node 18+).\n *\n * Uses @ffmpeg/ffmpeg for H.264 encoding and optional AAC audio muxing.\n */\n\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile } from '@ffmpeg/util';\n\nimport type { VideoExportOptions, EncoderResult } from './types.js';\nimport { QUALITY_PRESETS, resolveDimensions } from './types.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 preset = QUALITY_PRESETS[quality];\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n\n const duration = frames.length / fps;\n\n // Initialize ffmpeg.wasm\n const ffmpeg = new FFmpeg();\n\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();\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 '-preset',\n preset.preset,\n '-crf',\n String(preset.crf),\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('-c:a', 'aac', '-b:a', '128k', '-shortest');\n }\n\n args.push('output.mp4');\n\n // Run encoding\n await ffmpeg.exec(args);\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 ffmpeg.terminate();\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}\n\n// Re-export fetchFile for convenience — consumers may need it to prepare audio bytes\nexport { fetchFile };\n"],"mappings":"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","names":[]}
|
|
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/** 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 /**\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/**\n * Resolve dimensions from options, applying orientation defaults.\n */\nexport function resolveDimensions(options: VideoExportOptions): {\n width: number;\n height: number;\n} {\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 exposes `window.seekTo(time)`, `window.getDuration()`, etc.\n * via the SquisqRenderAPI, enabling Playwright (or any headless browser) to step\n * 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// ── 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\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, which exposes the\n * SquisqRenderAPI on `window` (seekTo, getDuration, getCaptions, etc.).\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 { playerScript, images, audio, width = 1920, height = 1080, captionStyle } = 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 SquisqPlayer.mount(document.getElementById(\"squisq-root\"), doc, {\n mode: \"slideshow\",\n images: images,\n audio: audio,\n autoPlay: false,\n basePath: \".\",\n renderMode: true${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/**\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 * WASM Video Encoder\n *\n * Encodes PNG frame screenshots into an MP4 video using ffmpeg.wasm.\n * Browser-pure — no Node.js APIs. Works in any environment with SharedArrayBuffer\n * support (browsers with COOP/COEP headers, or Node 18+).\n *\n * Uses @ffmpeg/ffmpeg for H.264 encoding and optional AAC audio muxing.\n */\n\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile } from '@ffmpeg/util';\n\nimport type { VideoExportOptions, EncoderResult } from './types.js';\nimport { resolveDimensions } from './types.js';\nimport { ffmpegVideoQualityArgs, audioBitrateArg } 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 duration = frames.length / fps;\n\n // Initialize ffmpeg.wasm\n const ffmpeg = new FFmpeg();\n\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();\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('-c:a', 'aac', '-b:a', audioBitrateArg(quality), '-shortest');\n }\n\n args.push('output.mp4');\n\n // Run encoding\n await ffmpeg.exec(args);\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 ffmpeg.terminate();\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}\n\n// Re-export fetchFile for convenience — consumers may need it to prepare audio bytes\nexport { fetchFile };\n"],"mappings":";AAuCO,IAAM,kBAAuD;AAAA,EAClE,OAAO,EAAE,QAAQ,aAAa,KAAK,IAAI,cAAc,GAAG,cAAc,KAAO;AAAA,EAC7E,QAAQ,EAAE,QAAQ,UAAU,KAAK,IAAI,cAAc,GAAG,cAAc,MAAQ;AAAA,EAC5E,MAAM,EAAE,QAAQ,QAAQ,KAAK,IAAI,cAAc,GAAG,cAAc,MAAQ;AAC1E;AAWO,SAAS,kBAAkB,GAAiB,OAAe,QAAwB;AACxF,QAAM,SAAS,gBAAgB,CAAC,KAAK,gBAAgB;AACrD,SAAO,KAAK,MAAM,QAAQ,SAAS,OAAO,YAAY;AACxD;AAGO,IAAM,yBAAsF;AAAA,EACjG,WAAW,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EACvC,UAAU,EAAE,OAAO,MAAM,QAAQ,KAAK;AACxC;AAiCO,SAAS,kBAAkB,SAGhC;AACA,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,WAAW,uBAAuB,WAAW;AACnD,SAAO;AAAA,IACL,OAAO,QAAQ,SAAS,SAAS;AAAA,IACjC,QAAQ,QAAQ,UAAU,SAAS;AAAA,EACrC;AACF;;;ACtFA,SAAS,4BAA4B;AAyB9B,SAAS,qBAAqB,KAAU,eAAe,GAAwB;AACpF,QAAM,UAAU,eAAe,IAAI,eAAe;AAClD,QAAM,QAA6B,CAAC;AAGpC,MAAI,SAAS;AACb,aAAW,OAAO,IAAI,OAAO,YAAY,CAAC,GAAG;AAC3C,UAAM,cAAc,KAAK,IAAI,GAAG,IAAI,QAAQ;AAC5C,QAAI,cAAc,KAAK,IAAI,KAAK;AAC9B,YAAM,KAAK,EAAE,KAAK,IAAI,KAAK,UAAU,SAAS,SAAS,aAAa,GAAG,YAAY,CAAC;AAAA,IACtF;AACA,cAAU;AAAA,EACZ;AAGA,aAAW,QAAQ,qBAAqB,GAAG,GAAG;AAC5C,QAAI,KAAK,SAAS,QAAS;AAC3B,UAAM,cAAc,KAAK,IAAI,GAAG,KAAK,cAAc,KAAK,aAAa;AACrE,QAAI,eAAe,KAAK,CAAC,KAAK,IAAK;AACnC,UAAM,KAAK;AAAA,MACT,KAAK,KAAK;AAAA,MACV,UAAU,KAAK,gBAAgB;AAAA,MAC/B,aAAa,KAAK;AAAA,MAClB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;AC5BA,IAAM,WAAmC;AAAA,EACvC,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACR;AAEA,SAAS,cAAc,UAA0B;AAC/C,QAAM,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY,KAAK;AACxD,SAAO,SAAS,GAAG,KAAK;AAC1B;AAQA,SAAS,qBAAqB,QAAqB,UAA0B;AAC3E,QAAM,QAAQ,IAAI,WAAW,MAAM;AACnC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,SAAO,QAAQ,QAAQ,WAAW,KAAK,MAAM,CAAC;AAChD;AAOA,SAAS,gBAAgB,KAAqB;AAC5C,SAAO,IAAI,QAAQ,iBAAiB,QAAQ;AAC9C;AAGA,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAC3B;AAcO,SAAS,mBAAmB,KAAU,SAAoC;AAC/E,QAAM,EAAE,cAAc,QAAQ,OAAO,QAAQ,MAAM,SAAS,MAAM,aAAa,IAAI;AAGnF,QAAM,WAAmC,CAAC;AAC1C,MAAI,QAAQ;AACV,eAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,GAAG;AAC7C,eAAS,IAAI,IAAI,qBAAqB,QAAQ,cAAc,IAAI,CAAC;AAAA,IACnE;AAAA,EACF;AAGA,QAAM,WAAmC,CAAC;AAC1C,MAAI,WAAW;AACf,MAAI,OAAO;AACT,eAAW,CAAC,MAAM,MAAM,KAAK,MAAM,QAAQ,GAAG;AAC5C,eAAS,IAAI,IAAI,qBAAqB,QAAQ,cAAc,IAAI,CAAC;AACjE,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,UAAU,gBAAgB,KAAK,UAAU,GAAG,CAAC;AACnD,QAAM,eAAe,gBAAgB,KAAK,UAAU,QAAQ,CAAC;AAC7D,QAAM,eAAe,WAAW,gBAAgB,KAAK,UAAU,QAAQ,CAAC,IAAI;AAE5E,SAAO;AAAA;AAAA;AAAA;AAAA,uCAI8B,KAAK,YAAY,MAAM;AAAA,SACrD,WAAW,qBAAqB,CAAC;AAAA;AAAA;AAAA,qCAGL,KAAK,aAAa,MAAM;AAAA,qBACxC,KAAK,aAAa,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,UAKnC,gBAAgB,YAAY,CAAC;AAAA;AAAA;AAAA,yBAGd,KAAK,UAAU,OAAO,CAAC;AAAA,4BACpB,KAAK,UAAU,YAAY,CAAC;AAAA,gBACxC,iBAAiB,SAAS,SAAS,gBAAgB,KAAK,UAAU,YAAY,IAAI,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAO/E,eAAe;AAAA,oBAAwB,KAAK,UAAU,YAAY,CAAC,KAAK,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAMhG;;;ACtJO,SAAS,uBAAuB,SAAiC;AACtE,QAAM,SAAS,gBAAgB,OAAO,KAAK,gBAAgB;AAC3D,SAAO,CAAC,WAAW,OAAO,QAAQ,QAAQ,OAAO,OAAO,GAAG,CAAC;AAC9D;AAMO,SAAS,gBAAgB,SAA+B;AAC7D,QAAM,SAAS,gBAAgB,OAAO,KAAK,gBAAgB;AAC3D,SAAO,GAAG,OAAO,eAAe,GAAI;AACtC;;;ACpBA,SAAS,cAAc;AACvB,SAAS,iBAAiB;AAc1B,eAAsB,gBACpB,QACA,OACA,UAA8B,CAAC,GACP;AACxB,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,EAAE,OAAO,OAAO,IAAI,kBAAkB,OAAO;AACnD,QAAM,aAAa,QAAQ;AAE3B,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AAEA,QAAM,WAAW,OAAO,SAAS;AAGjC,QAAM,SAAS,IAAI,OAAO;AAE1B,SAAO,GAAG,YAAY,CAAC,EAAE,SAAS,MAAM;AACtC,QAAI,YAAY;AACd,YAAM,UAAU,KAAK,MAAM,WAAW,GAAG;AACzC,iBAAW,KAAK,IAAI,SAAS,EAAE,GAAG,UAAU;AAAA,IAC9C;AAAA,EACF,CAAC;AAED,QAAM,OAAO,KAAK;AAElB,eAAa,GAAG,gBAAgB;AAGhC,QAAM,SAAS,OAAO,OAAO,MAAM,EAAE;AACrC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,OAAO,SAAS,OAAO,IAAI,CAAC,EAAE,SAAS,QAAQ,GAAG,CAAC;AACzD,UAAM,OAAO,UAAU,MAAM,OAAO,CAAC,CAAC;AAGtC,QAAI,cAAc,IAAI,OAAO,GAAG;AAC9B,iBAAW,KAAK,MAAO,IAAI,OAAO,SAAU,EAAE,GAAG,gBAAgB;AAAA,IACnE;AAAA,EACF;AAGA,MAAI,OAAO;AACT,UAAM,OAAO,UAAU,eAAe,KAAK;AAAA,EAC7C;AAEA,eAAa,IAAI,UAAU;AAG3B,QAAM,aAAa,WAAW,MAAM;AACpC,QAAM,OAAO,CAAC,MAAM,cAAc,OAAO,GAAG,GAAG,MAAM,UAAU;AAG/D,MAAI,OAAO;AACT,SAAK,KAAK,MAAM,aAAa;AAAA,EAC/B;AAGA,OAAK;AAAA,IACH;AAAA,IACA;AAAA,IACA,GAAG,uBAAuB,OAAO;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,KAAK,IAAI,MAAM,6CAA6C,KAAK,IAAI,MAAM;AAAA,EACtF;AAGA,MAAI,OAAO;AACT,SAAK,KAAK,QAAQ,OAAO,QAAQ,gBAAgB,OAAO,GAAG,WAAW;AAAA,EACxE;AAEA,OAAK,KAAK,YAAY;AAGtB,QAAM,OAAO,KAAK,IAAI;AAEtB,eAAa,IAAI,gBAAgB;AAGjC,QAAM,OAAO,MAAM,OAAO,SAAS,YAAY;AAG/C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,OAAO,SAAS,OAAO,IAAI,CAAC,EAAE,SAAS,QAAQ,GAAG,CAAC;AACzD,UAAM,OAAO,WAAW,IAAI;AAAA,EAC9B;AACA,MAAI,OAAO;AACT,UAAM,OAAO,WAAW,aAAa;AAAA,EACvC;AACA,QAAM,OAAO,WAAW,YAAY;AAEpC,SAAO,UAAU;AAEjB,eAAa,KAAK,MAAM;AAGxB,QAAM,aAAa,gBAAgB,aAAa,OAAO,IAAI,YAAY,EAAE,OAAO,IAAc;AAE9F,SAAO,EAAE,MAAM,YAAY,SAAS;AACtC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-video",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Video encoding and render HTML generation for Squisq documents — browser-pure, works in Node and browser",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@bendyline/squisq": "1.
|
|
44
|
-
"@bendyline/squisq-react": "1.3.2",
|
|
43
|
+
"@bendyline/squisq": "1.5.1",
|
|
45
44
|
"@ffmpeg/ffmpeg": "0.12.15",
|
|
46
45
|
"@ffmpeg/util": "0.12.2"
|
|
47
46
|
},
|