@bendyline/squisq-cli 1.2.2 → 2.0.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/README.md +81 -30
- package/dist/api-6KN3F6IQ.js +34 -0
- package/dist/api-6KN3F6IQ.js.map +1 -0
- package/dist/api.d.ts +172 -36
- package/dist/api.js +263 -121
- package/dist/api.js.map +1 -1
- package/dist/chunk-ESFEFKY4.js +175 -0
- package/dist/chunk-ESFEFKY4.js.map +1 -0
- package/dist/chunk-JEE26SZS.js +668 -0
- package/dist/chunk-JEE26SZS.js.map +1 -0
- package/dist/chunk-VM3ZFP3J.js +177 -0
- package/dist/chunk-VM3ZFP3J.js.map +1 -0
- package/dist/index.js +116 -539
- package/dist/index.js.map +1 -1
- package/dist/nativeEncoder-IQPN2RMC.js +15 -0
- package/dist/nativeEncoder-IQPN2RMC.js.map +1 -0
- package/dist/nativeEncoder-WHUIQZLL.js +16 -0
- package/dist/nativeEncoder-WHUIQZLL.js.map +1 -0
- package/package.json +7 -7
- package/dist/nativeEncoder-2HGWDEZZ.js +0 -86
- package/dist/nativeEncoder-2HGWDEZZ.js.map +0 -1
- package/dist/nativeEncoder-DLLAT2RT.js +0 -88
- package/dist/nativeEncoder-DLLAT2RT.js.map +0 -1
package/dist/api.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GIF_EXPORT_DEFAULTS,
|
|
3
|
+
framesToGifNative,
|
|
4
|
+
framesToGifNativeBytes,
|
|
5
|
+
framesToMp4Native,
|
|
6
|
+
framesToMp4NativeBytes
|
|
7
|
+
} from "./chunk-ESFEFKY4.js";
|
|
8
|
+
|
|
1
9
|
// src/api.ts
|
|
2
10
|
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
3
11
|
import { flattenBlocks } from "@bendyline/squisq/doc";
|
|
4
|
-
import { generateRenderHtml } from "@bendyline/squisq-video";
|
|
12
|
+
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
5
13
|
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
6
14
|
import { convert as formatsConvert } from "@bendyline/squisq-formats";
|
|
7
15
|
|
|
@@ -49,10 +57,6 @@ async function detectFfmpegDetailed() {
|
|
|
49
57
|
}
|
|
50
58
|
return null;
|
|
51
59
|
}
|
|
52
|
-
async function detectFfmpeg() {
|
|
53
|
-
const detection = await detectFfmpegDetailed();
|
|
54
|
-
return detection?.path ?? null;
|
|
55
|
-
}
|
|
56
60
|
|
|
57
61
|
// src/util/audioMix.ts
|
|
58
62
|
import { computeAudioTimeline } from "@bendyline/squisq-video";
|
|
@@ -127,6 +131,14 @@ async function mixTimelineClips(ffmpegPath, usable) {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
// src/util/coverPreRoll.ts
|
|
135
|
+
function resolveAppliedCoverPreRoll(requestedSeconds, hasCover) {
|
|
136
|
+
if (!Number.isFinite(requestedSeconds) || requestedSeconds < 0) {
|
|
137
|
+
throw new Error("Cover pre-roll must be a finite number of seconds greater than or equal to 0");
|
|
138
|
+
}
|
|
139
|
+
return hasCover ? requestedSeconds : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
// src/registry.ts
|
|
131
143
|
import { randomBytes } from "crypto";
|
|
132
144
|
import { readFile, rm } from "fs/promises";
|
|
@@ -137,7 +149,19 @@ var MP4_DEFAULTS = {
|
|
|
137
149
|
fps: 30,
|
|
138
150
|
quality: "normal",
|
|
139
151
|
orientation: "landscape",
|
|
140
|
-
coverPreRoll: 0
|
|
152
|
+
coverPreRoll: 0,
|
|
153
|
+
animationsEnabled: true
|
|
154
|
+
};
|
|
155
|
+
var GIF_DEFAULTS = {
|
|
156
|
+
fps: 10,
|
|
157
|
+
orientation: "landscape",
|
|
158
|
+
width: 960,
|
|
159
|
+
height: 540,
|
|
160
|
+
coverPreRoll: 0,
|
|
161
|
+
animationsEnabled: false,
|
|
162
|
+
loop: 0,
|
|
163
|
+
maxColors: 256,
|
|
164
|
+
dither: "sierra2_4a"
|
|
141
165
|
};
|
|
142
166
|
function mp4Format() {
|
|
143
167
|
return {
|
|
@@ -151,14 +175,20 @@ function mp4Format() {
|
|
|
151
175
|
const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
|
|
152
176
|
const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
|
|
153
177
|
const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
|
|
178
|
+
const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
|
|
154
179
|
const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
155
180
|
try {
|
|
156
|
-
await
|
|
181
|
+
const { renderDocToMp4: renderDocToMp42 } = await import("./api.js");
|
|
182
|
+
await renderDocToMp42(input.doc, input.container, {
|
|
157
183
|
outputPath,
|
|
158
184
|
fps,
|
|
159
185
|
quality,
|
|
160
186
|
orientation,
|
|
161
|
-
|
|
187
|
+
width: mp4Opts.width,
|
|
188
|
+
height: mp4Opts.height,
|
|
189
|
+
captionStyle: mp4Opts.captionStyle,
|
|
190
|
+
coverPreRoll,
|
|
191
|
+
animationsEnabled
|
|
162
192
|
});
|
|
163
193
|
const data = await readFile(outputPath);
|
|
164
194
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -169,27 +199,64 @@ function mp4Format() {
|
|
|
169
199
|
}
|
|
170
200
|
};
|
|
171
201
|
}
|
|
202
|
+
function gifFormat() {
|
|
203
|
+
return {
|
|
204
|
+
id: "gif",
|
|
205
|
+
label: "Animated GIF",
|
|
206
|
+
mimeType: "image/gif",
|
|
207
|
+
extensions: [".gif"],
|
|
208
|
+
async exportDoc(input, options) {
|
|
209
|
+
const gifOpts = options.formatOptions?.gif ?? {};
|
|
210
|
+
const orientation = typeof gifOpts.orientation === "string" ? gifOpts.orientation : GIF_DEFAULTS.orientation;
|
|
211
|
+
const portrait = orientation === "portrait";
|
|
212
|
+
const defaultWidth = portrait ? GIF_DEFAULTS.height : GIF_DEFAULTS.width;
|
|
213
|
+
const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
|
|
214
|
+
const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
|
|
215
|
+
try {
|
|
216
|
+
const { renderDocToGif: renderDocToGif2 } = await import("./api.js");
|
|
217
|
+
const result = await renderDocToGif2(input.doc, input.container, {
|
|
218
|
+
outputPath,
|
|
219
|
+
fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
|
|
220
|
+
orientation,
|
|
221
|
+
width: typeof gifOpts.width === "number" ? gifOpts.width : defaultWidth,
|
|
222
|
+
height: typeof gifOpts.height === "number" ? gifOpts.height : defaultHeight,
|
|
223
|
+
captionStyle: gifOpts.captionStyle,
|
|
224
|
+
coverPreRoll: typeof gifOpts.coverPreRoll === "number" ? gifOpts.coverPreRoll : GIF_DEFAULTS.coverPreRoll,
|
|
225
|
+
animationsEnabled: typeof gifOpts.animationsEnabled === "boolean" ? gifOpts.animationsEnabled : GIF_DEFAULTS.animationsEnabled,
|
|
226
|
+
loop: typeof gifOpts.loop === "number" ? gifOpts.loop : GIF_DEFAULTS.loop,
|
|
227
|
+
maxColors: typeof gifOpts.maxColors === "number" ? gifOpts.maxColors : GIF_DEFAULTS.maxColors,
|
|
228
|
+
dither: gifOpts.dither ?? GIF_DEFAULTS.dither,
|
|
229
|
+
bayerScale: gifOpts.bayerScale
|
|
230
|
+
});
|
|
231
|
+
const data = await readFile(outputPath);
|
|
232
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
233
|
+
return {
|
|
234
|
+
bytes,
|
|
235
|
+
mimeType: "image/gif",
|
|
236
|
+
suggestedFilename: "",
|
|
237
|
+
warnings: result.warnings
|
|
238
|
+
};
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(outputPath, { force: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
172
245
|
function createCliRegistry() {
|
|
173
246
|
const registry = defaultRegistry();
|
|
174
247
|
registry.register(mp4Format());
|
|
248
|
+
registry.register(gifFormat());
|
|
175
249
|
return registry;
|
|
176
250
|
}
|
|
177
251
|
|
|
178
252
|
// src/api.ts
|
|
179
253
|
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
180
254
|
|
|
181
|
-
// src/util/domPolyfill.ts
|
|
182
|
-
import { DOMParser as XmldomDOMParser } from "@xmldom/xmldom";
|
|
183
|
-
var globalScope = globalThis;
|
|
184
|
-
if (typeof globalScope.DOMParser === "undefined") {
|
|
185
|
-
globalScope.DOMParser = XmldomDOMParser;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
255
|
// src/util/readInput.ts
|
|
189
256
|
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
190
257
|
import { join as join2, extname } from "path";
|
|
191
258
|
import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
192
|
-
import { markdownToDoc } from "@bendyline/squisq/doc";
|
|
259
|
+
import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
|
|
193
260
|
import { MemoryContentContainer } from "@bendyline/squisq/storage";
|
|
194
261
|
import { zipToContainer } from "@bendyline/squisq-formats/container";
|
|
195
262
|
import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
|
|
@@ -226,7 +293,12 @@ async function walkDir(root, prefix = "") {
|
|
|
226
293
|
}
|
|
227
294
|
return paths;
|
|
228
295
|
}
|
|
229
|
-
async function readInput(inputPath) {
|
|
296
|
+
async function readInput(inputPath, options) {
|
|
297
|
+
const result = await readInputRaw(inputPath, options);
|
|
298
|
+
const doc = await resolveAudioMapping(result.doc, result.container);
|
|
299
|
+
return doc === result.doc ? result : { ...result, doc };
|
|
300
|
+
}
|
|
301
|
+
async function readInputRaw(inputPath, options) {
|
|
230
302
|
const info = await stat(inputPath);
|
|
231
303
|
if (info.isDirectory()) {
|
|
232
304
|
return readFolder(inputPath);
|
|
@@ -241,7 +313,7 @@ async function readInput(inputPath) {
|
|
|
241
313
|
if (IMPORTER_EXTS.includes(ext)) {
|
|
242
314
|
const def = defaultRegistry2().byExtension(ext);
|
|
243
315
|
if (def && (def.importContainer || def.importDoc)) {
|
|
244
|
-
return readViaImporter(inputPath, def);
|
|
316
|
+
return readViaImporter(inputPath, def, options);
|
|
245
317
|
}
|
|
246
318
|
}
|
|
247
319
|
return readMarkdownFile(inputPath);
|
|
@@ -263,16 +335,24 @@ async function readDocJsonFile(filePath) {
|
|
|
263
335
|
const container = new MemoryContentContainer();
|
|
264
336
|
return { doc, container, sourceFormat: "json" };
|
|
265
337
|
}
|
|
266
|
-
async function readViaImporter(filePath, def) {
|
|
338
|
+
async function readViaImporter(filePath, def, options) {
|
|
267
339
|
const buffer = await readArrayBuffer(filePath);
|
|
340
|
+
const convertOptions = {
|
|
341
|
+
formatOptions: {
|
|
342
|
+
[def.id]: {
|
|
343
|
+
inferTheme: options?.inferTheme !== false,
|
|
344
|
+
inferLayouts: options?.inferLayouts !== false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
268
348
|
let container;
|
|
269
349
|
let markdownDoc;
|
|
270
350
|
if (def.importContainer) {
|
|
271
|
-
container = await def.importContainer(buffer,
|
|
351
|
+
container = await def.importContainer(buffer, convertOptions);
|
|
272
352
|
const text = await container.readDocument();
|
|
273
353
|
markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
|
|
274
354
|
} else {
|
|
275
|
-
markdownDoc = await def.importDoc(buffer,
|
|
355
|
+
markdownDoc = await def.importDoc(buffer, convertOptions);
|
|
276
356
|
const mem = new MemoryContentContainer();
|
|
277
357
|
await mem.writeDocument(stringifyMarkdown(markdownDoc));
|
|
278
358
|
container = mem;
|
|
@@ -331,22 +411,10 @@ async function convert(source, to, options = {}) {
|
|
|
331
411
|
...options
|
|
332
412
|
});
|
|
333
413
|
}
|
|
334
|
-
async function
|
|
335
|
-
const {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
quality = "normal",
|
|
339
|
-
orientation = "landscape",
|
|
340
|
-
captionStyle,
|
|
341
|
-
coverPreRoll = 0,
|
|
342
|
-
onProgress
|
|
343
|
-
} = options;
|
|
344
|
-
const dimensions = resolveDimensions({
|
|
345
|
-
orientation,
|
|
346
|
-
width: options.width,
|
|
347
|
-
height: options.height
|
|
348
|
-
});
|
|
349
|
-
const ffmpegPath = await detectFfmpeg();
|
|
414
|
+
async function captureDocFrames(doc, container, options) {
|
|
415
|
+
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress } = options;
|
|
416
|
+
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
417
|
+
const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
|
|
350
418
|
if (!ffmpegPath) {
|
|
351
419
|
throw new Error(
|
|
352
420
|
"ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg\nOr: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary."
|
|
@@ -354,25 +422,20 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
354
422
|
}
|
|
355
423
|
onProgress?.("collecting media", 0);
|
|
356
424
|
const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
357
|
-
const imagePaths = collectImagePaths(doc);
|
|
358
425
|
const images = /* @__PURE__ */ new Map();
|
|
359
|
-
for (const imgPath of
|
|
426
|
+
for (const imgPath of collectImagePaths(doc)) {
|
|
360
427
|
const data = await container.readFile(imgPath);
|
|
361
|
-
if (data)
|
|
362
|
-
images.set(imgPath, data);
|
|
363
|
-
}
|
|
428
|
+
if (data) images.set(imgPath, data);
|
|
364
429
|
}
|
|
365
430
|
const audio = /* @__PURE__ */ new Map();
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
audio.set(seg.name, data);
|
|
372
|
-
}
|
|
431
|
+
for (const seg of doc.audio?.segments ?? []) {
|
|
432
|
+
const data = await container.readFile(seg.src);
|
|
433
|
+
if (data) {
|
|
434
|
+
audio.set(seg.src, data);
|
|
435
|
+
audio.set(seg.name, data);
|
|
373
436
|
}
|
|
374
437
|
}
|
|
375
|
-
const mediaSrcs = new Set(resolveMediaSchedule(doc).map((
|
|
438
|
+
const mediaSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
|
|
376
439
|
for (const block of flattenBlocks(doc.blocks)) {
|
|
377
440
|
for (const layer of block.layers ?? []) {
|
|
378
441
|
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
@@ -389,9 +452,10 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
389
452
|
playerScript: PLAYER_BUNDLE,
|
|
390
453
|
images,
|
|
391
454
|
audio: audio.size > 0 ? audio : void 0,
|
|
392
|
-
width
|
|
393
|
-
height
|
|
394
|
-
captionStyle
|
|
455
|
+
width,
|
|
456
|
+
height,
|
|
457
|
+
captionStyle,
|
|
458
|
+
animationsEnabled
|
|
395
459
|
});
|
|
396
460
|
onProgress?.("launching browser", 15);
|
|
397
461
|
const { chromium } = await import("playwright-core");
|
|
@@ -405,91 +469,163 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
405
469
|
(launch failed: ${detail})`
|
|
406
470
|
);
|
|
407
471
|
}
|
|
408
|
-
const page = await browser.newPage({
|
|
409
|
-
viewport: { width: dimensions.width, height: dimensions.height }
|
|
410
|
-
});
|
|
472
|
+
const page = await browser.newPage({ viewport: { width, height } });
|
|
411
473
|
const pageErrors = [];
|
|
412
474
|
page.on("pageerror", (err) => pageErrors.push(err.message));
|
|
413
|
-
|
|
414
|
-
await page.waitForTimeout(500);
|
|
475
|
+
let renderAPI = null;
|
|
415
476
|
try {
|
|
416
|
-
await page.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
477
|
+
await page.setContent(renderHtml, { waitUntil: "load" });
|
|
478
|
+
await page.waitForTimeout(500);
|
|
479
|
+
try {
|
|
480
|
+
await page.waitForFunction(
|
|
481
|
+
() => {
|
|
482
|
+
const root = document.getElementById("squisq-root");
|
|
483
|
+
const player = window.SquisqPlayer;
|
|
484
|
+
return root ? player?.getHandle(root)?.getRenderAPI() != null : false;
|
|
485
|
+
},
|
|
486
|
+
{ timeout: 15e3 }
|
|
487
|
+
);
|
|
488
|
+
} catch {
|
|
489
|
+
const errorDetail = pageErrors.length ? `
|
|
423
490
|
Page errors:
|
|
424
491
|
${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
437
|
-
const preRollFrameCount = Math.ceil(coverPreRoll * fps);
|
|
438
|
-
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
439
|
-
const frames = [];
|
|
440
|
-
onProgress?.("capturing frames", 20);
|
|
441
|
-
if (preRollFrameCount > 0) {
|
|
442
|
-
const hasCover = await page.evaluate(() => {
|
|
443
|
-
const w = window;
|
|
444
|
-
return typeof w.hasCoverBlock === "function" ? w.hasCoverBlock() : false;
|
|
492
|
+
throw new Error(
|
|
493
|
+
`The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
renderAPI = await page.evaluateHandle(() => {
|
|
497
|
+
const root = document.getElementById("squisq-root");
|
|
498
|
+
const player = window.SquisqPlayer;
|
|
499
|
+
const api = root ? player?.getHandle(root)?.getRenderAPI() : null;
|
|
500
|
+
if (!api) throw new Error("Squisq render API disappeared after initialization.");
|
|
501
|
+
return api;
|
|
445
502
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
503
|
+
const docDuration = await renderAPI.evaluate((api) => api.getDuration());
|
|
504
|
+
if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
|
|
505
|
+
const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
506
|
+
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
507
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
508
|
+
const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
|
|
509
|
+
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
510
|
+
const frames = [];
|
|
511
|
+
onProgress?.("capturing frames", 20);
|
|
512
|
+
if (preRollFrameCount > 0) {
|
|
513
|
+
await renderAPI.evaluate((api) => api.showCover());
|
|
450
514
|
await page.waitForTimeout(100);
|
|
451
515
|
const coverFrame = new Uint8Array(await page.screenshot({ type: "png" }));
|
|
452
|
-
for (let i = 0; i < preRollFrameCount; i++)
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
await page.evaluate(() => {
|
|
456
|
-
window.hideCover();
|
|
457
|
-
});
|
|
516
|
+
for (let i = 0; i < preRollFrameCount; i++) frames.push(coverFrame);
|
|
517
|
+
await renderAPI.evaluate((api) => api.hideCover());
|
|
458
518
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
frames.push(new Uint8Array(screenshot));
|
|
468
|
-
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
469
|
-
const pct = 20 + Math.round(frames.length / totalFrames * 60);
|
|
470
|
-
onProgress?.("capturing frames", pct);
|
|
519
|
+
const frameInterval = 1 / fps;
|
|
520
|
+
for (let i = 0; i < storyFrameCount; i++) {
|
|
521
|
+
const time = i * frameInterval;
|
|
522
|
+
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
523
|
+
frames.push(new Uint8Array(await page.screenshot({ type: "png" })));
|
|
524
|
+
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
525
|
+
onProgress?.("capturing frames", 20 + Math.round(frames.length / totalFrames * 60));
|
|
526
|
+
}
|
|
471
527
|
}
|
|
528
|
+
return {
|
|
529
|
+
frames,
|
|
530
|
+
totalDuration: docDuration + appliedCoverPreRoll,
|
|
531
|
+
appliedCoverPreRoll,
|
|
532
|
+
ffmpegPath
|
|
533
|
+
};
|
|
534
|
+
} finally {
|
|
535
|
+
await renderAPI?.dispose();
|
|
536
|
+
await browser.close();
|
|
472
537
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
|
|
538
|
+
}
|
|
539
|
+
async function renderDocToMp4(doc, container, options) {
|
|
540
|
+
const fps = options.fps ?? 30;
|
|
541
|
+
const quality = options.quality ?? "normal";
|
|
542
|
+
const orientation = options.orientation ?? "landscape";
|
|
543
|
+
const dimensions = resolveDimensions({
|
|
544
|
+
orientation,
|
|
545
|
+
width: options.width,
|
|
546
|
+
height: options.height,
|
|
547
|
+
fps,
|
|
548
|
+
quality
|
|
549
|
+
});
|
|
550
|
+
const capture = await captureDocFrames(doc, container, {
|
|
551
|
+
fps,
|
|
552
|
+
width: dimensions.width,
|
|
553
|
+
height: dimensions.height,
|
|
554
|
+
captionStyle: options.captionStyle,
|
|
555
|
+
coverPreRoll: options.coverPreRoll ?? 0,
|
|
556
|
+
animationsEnabled: options.animationsEnabled ?? true,
|
|
557
|
+
onProgress: options.onProgress
|
|
558
|
+
});
|
|
559
|
+
options.onProgress?.("encoding video", 80);
|
|
560
|
+
const encodingAudio = await buildMixedAudioTrack(
|
|
561
|
+
doc,
|
|
562
|
+
container,
|
|
563
|
+
capture.ffmpegPath,
|
|
564
|
+
capture.appliedCoverPreRoll
|
|
565
|
+
);
|
|
566
|
+
const { framesToMp4Native: framesToMp4Native2 } = await import("./nativeEncoder-IQPN2RMC.js");
|
|
567
|
+
await framesToMp4Native2(capture.ffmpegPath, capture.frames, encodingAudio, options.outputPath, {
|
|
478
568
|
fps,
|
|
479
569
|
quality,
|
|
480
570
|
orientation,
|
|
481
571
|
width: dimensions.width,
|
|
482
572
|
height: dimensions.height,
|
|
483
|
-
onProgress: (percent, phase) => {
|
|
484
|
-
|
|
485
|
-
|
|
573
|
+
onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
|
|
574
|
+
});
|
|
575
|
+
options.onProgress?.("done", 100);
|
|
576
|
+
return {
|
|
577
|
+
duration: capture.totalDuration,
|
|
578
|
+
frameCount: capture.frames.length,
|
|
579
|
+
outputPath: options.outputPath
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function renderDocToGif(doc, container, options) {
|
|
583
|
+
const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;
|
|
584
|
+
if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {
|
|
585
|
+
throw new RangeError("GIF FPS must be a finite number between 1 and 100.");
|
|
586
|
+
}
|
|
587
|
+
const orientation = options.orientation ?? "landscape";
|
|
588
|
+
const portrait = orientation === "portrait";
|
|
589
|
+
const width = options.width ?? (portrait ? GIF_EXPORT_DEFAULTS.height : GIF_EXPORT_DEFAULTS.width);
|
|
590
|
+
const height = options.height ?? (portrait ? GIF_EXPORT_DEFAULTS.width : GIF_EXPORT_DEFAULTS.height);
|
|
591
|
+
resolveDimensions({ orientation, width, height, fps });
|
|
592
|
+
ffmpegGifOutputArgs({
|
|
593
|
+
width,
|
|
594
|
+
height,
|
|
595
|
+
loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,
|
|
596
|
+
maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,
|
|
597
|
+
dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,
|
|
598
|
+
bayerScale: options.bayerScale
|
|
599
|
+
});
|
|
600
|
+
const capture = await captureDocFrames(doc, container, {
|
|
601
|
+
fps,
|
|
602
|
+
width,
|
|
603
|
+
height,
|
|
604
|
+
captionStyle: options.captionStyle,
|
|
605
|
+
coverPreRoll: options.coverPreRoll ?? 0,
|
|
606
|
+
animationsEnabled: options.animationsEnabled ?? false,
|
|
607
|
+
onProgress: options.onProgress
|
|
608
|
+
});
|
|
609
|
+
options.onProgress?.("encoding GIF", 80);
|
|
610
|
+
const { framesToGifNative: framesToGifNative2 } = await import("./nativeEncoder-IQPN2RMC.js");
|
|
611
|
+
await framesToGifNative2(capture.ffmpegPath, capture.frames, options.outputPath, {
|
|
612
|
+
fps,
|
|
613
|
+
orientation,
|
|
614
|
+
width,
|
|
615
|
+
height,
|
|
616
|
+
loop: options.loop,
|
|
617
|
+
maxColors: options.maxColors,
|
|
618
|
+
dither: options.dither,
|
|
619
|
+
bayerScale: options.bayerScale,
|
|
620
|
+
onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
|
|
486
621
|
});
|
|
487
|
-
onProgress?.("done", 100);
|
|
488
|
-
const
|
|
622
|
+
options.onProgress?.("done", 100);
|
|
623
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule(doc).some((clip) => clip.kind === "audio");
|
|
489
624
|
return {
|
|
490
|
-
duration: totalDuration,
|
|
491
|
-
frameCount: frames.length,
|
|
492
|
-
outputPath
|
|
625
|
+
duration: capture.totalDuration,
|
|
626
|
+
frameCount: capture.frames.length,
|
|
627
|
+
outputPath: options.outputPath,
|
|
628
|
+
warnings: hasAudio ? ["Animated GIF does not support audio; audio tracks were omitted."] : []
|
|
493
629
|
};
|
|
494
630
|
}
|
|
495
631
|
async function extractThumbnails(options) {
|
|
@@ -497,7 +633,7 @@ async function extractThumbnails(options) {
|
|
|
497
633
|
const { existsSync } = await import("fs");
|
|
498
634
|
const { join: join3 } = await import("path");
|
|
499
635
|
const { execFile: execFile2 } = await import("child_process");
|
|
500
|
-
const ffmpegPath = await
|
|
636
|
+
const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
|
|
501
637
|
if (!ffmpegPath) {
|
|
502
638
|
throw new Error(
|
|
503
639
|
"ffmpeg is required for thumbnail extraction but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg"
|
|
@@ -521,11 +657,17 @@ async function extractThumbnails(options) {
|
|
|
521
657
|
}
|
|
522
658
|
export {
|
|
523
659
|
ConversionError,
|
|
660
|
+
GIF_EXPORT_DEFAULTS,
|
|
524
661
|
MemoryContentContainer2 as MemoryContentContainer,
|
|
525
662
|
convert,
|
|
526
663
|
createCliRegistry,
|
|
527
664
|
extractThumbnails,
|
|
665
|
+
framesToGifNative,
|
|
666
|
+
framesToGifNativeBytes,
|
|
667
|
+
framesToMp4Native,
|
|
668
|
+
framesToMp4NativeBytes,
|
|
528
669
|
readInput,
|
|
670
|
+
renderDocToGif,
|
|
529
671
|
renderDocToMp4
|
|
530
672
|
};
|
|
531
673
|
//# sourceMappingURL=api.js.map
|