pro-visu 0.3.0 → 0.4.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/dist/cli/index.js +258 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var TOOL_VERSION = true ? "0.
|
|
7
|
+
var TOOL_VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
8
8
|
|
|
9
9
|
// src/cli/update-check.ts
|
|
10
10
|
import updateNotifier from "update-notifier";
|
|
@@ -163,17 +163,88 @@ async function ensureChromium(opts) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// src/media/ensure-ffmpeg.ts
|
|
166
|
-
import path5 from "path";
|
|
167
166
|
import { existsSync as existsSync2 } from "fs";
|
|
168
|
-
import { rm as rm3 } from "fs/promises";
|
|
169
167
|
import { spawn as spawn3 } from "child_process";
|
|
170
|
-
import { createRequire as createRequire2 } from "module";
|
|
171
168
|
|
|
172
169
|
// src/media/ffmpeg.ts
|
|
173
|
-
import
|
|
170
|
+
import path5 from "path";
|
|
174
171
|
import { spawn as spawn2 } from "child_process";
|
|
175
|
-
import { rm as
|
|
176
|
-
|
|
172
|
+
import { rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
173
|
+
|
|
174
|
+
// src/media/ffmpeg-binary.ts
|
|
175
|
+
import os from "os";
|
|
176
|
+
import path4 from "path";
|
|
177
|
+
import https from "https";
|
|
178
|
+
import { createWriteStream } from "fs";
|
|
179
|
+
import { mkdir as mkdir2, rename, rm as rm2, chmod } from "fs/promises";
|
|
180
|
+
import { createGunzip } from "zlib";
|
|
181
|
+
import { pipeline } from "stream/promises";
|
|
182
|
+
var FFMPEG_RELEASE = process.env.FFMPEG_BINARY_RELEASE || "b6.1.1";
|
|
183
|
+
var BINARIES_URL = process.env.FFMPEG_BINARIES_URL || "https://github.com/eugeneware/ffmpeg-static/releases/download";
|
|
184
|
+
var SUPPORTED = {
|
|
185
|
+
darwin: ["x64", "arm64"],
|
|
186
|
+
linux: ["x64", "ia32", "arm64", "arm"],
|
|
187
|
+
win32: ["x64", "ia32"],
|
|
188
|
+
freebsd: ["x64"]
|
|
189
|
+
};
|
|
190
|
+
function ffmpegIsSupported() {
|
|
191
|
+
return (SUPPORTED[process.platform] ?? []).includes(process.arch);
|
|
192
|
+
}
|
|
193
|
+
function ffmpegCacheDir() {
|
|
194
|
+
const base = process.env.PROVISU_FFMPEG_DIR || path4.join(os.homedir(), ".cache", "pro-visu", "ffmpeg");
|
|
195
|
+
return path4.join(base, FFMPEG_RELEASE);
|
|
196
|
+
}
|
|
197
|
+
function ffmpegCachedBinary() {
|
|
198
|
+
return path4.join(ffmpegCacheDir(), process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg");
|
|
199
|
+
}
|
|
200
|
+
function ffmpegBinaryPath() {
|
|
201
|
+
return process.env.FFMPEG_BIN || ffmpegCachedBinary();
|
|
202
|
+
}
|
|
203
|
+
function ffmpegDownloadUrl() {
|
|
204
|
+
if (!ffmpegIsSupported()) return null;
|
|
205
|
+
return `${BINARIES_URL}/${FFMPEG_RELEASE}/ffmpeg-${process.platform}-${process.arch}.gz`;
|
|
206
|
+
}
|
|
207
|
+
function getFollowing(url, redirects = 5) {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const req = https.get(url, { headers: { "User-Agent": "pro-visu" } }, (res) => {
|
|
210
|
+
const status = res.statusCode ?? 0;
|
|
211
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
212
|
+
res.resume();
|
|
213
|
+
if (redirects <= 0) return reject(new Error("Too many redirects fetching ffmpeg."));
|
|
214
|
+
resolve(getFollowing(new URL(res.headers.location, url).toString(), redirects - 1));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (status !== 200) {
|
|
218
|
+
res.resume();
|
|
219
|
+
reject(new Error(`ffmpeg download failed: HTTP ${status} for ${url}`));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
resolve(res);
|
|
223
|
+
});
|
|
224
|
+
req.on("error", reject);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function downloadFfmpeg() {
|
|
228
|
+
const url = ffmpegDownloadUrl();
|
|
229
|
+
if (!url) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const dest = ffmpegCachedBinary();
|
|
235
|
+
const tmp = `${dest}.download`;
|
|
236
|
+
await mkdir2(path4.dirname(dest), { recursive: true });
|
|
237
|
+
await rm2(tmp, { force: true });
|
|
238
|
+
const res = await getFollowing(url);
|
|
239
|
+
await pipeline(res, createGunzip(), createWriteStream(tmp));
|
|
240
|
+
await chmod(tmp, 493).catch(() => {
|
|
241
|
+
});
|
|
242
|
+
await rm2(dest, { force: true });
|
|
243
|
+
await rename(tmp, dest);
|
|
244
|
+
return dest;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/media/ffmpeg.ts
|
|
177
248
|
var SCALE_COLOR = "out_color_matrix=bt709:out_range=tv";
|
|
178
249
|
var COLOR_TAGS = [
|
|
179
250
|
"-colorspace",
|
|
@@ -186,11 +257,7 @@ var COLOR_TAGS = [
|
|
|
186
257
|
"tv"
|
|
187
258
|
];
|
|
188
259
|
function ffmpegPath() {
|
|
189
|
-
|
|
190
|
-
if (!p) {
|
|
191
|
-
throw new Error("ffmpeg-static did not provide a binary for this platform.");
|
|
192
|
-
}
|
|
193
|
-
return p;
|
|
260
|
+
return ffmpegBinaryPath();
|
|
194
261
|
}
|
|
195
262
|
function buildTranscodeArgs(args) {
|
|
196
263
|
const seek = args.startOffsetSeconds && args.startOffsetSeconds > 0 ? ["-ss", args.startOffsetSeconds.toFixed(3)] : [];
|
|
@@ -332,7 +399,7 @@ function buildConcatArgs(listFile, outPath) {
|
|
|
332
399
|
];
|
|
333
400
|
}
|
|
334
401
|
async function concatMp4(segments, outPath, logger, signal) {
|
|
335
|
-
await ensureDir(
|
|
402
|
+
await ensureDir(path5.dirname(outPath));
|
|
336
403
|
const listFile = `${outPath}.concat.txt`;
|
|
337
404
|
const list = segments.map((s) => `file '${s.replace(/\\/g, "/")}'`).join("\n");
|
|
338
405
|
await writeFile2(listFile, `${list}
|
|
@@ -340,7 +407,7 @@ async function concatMp4(segments, outPath, logger, signal) {
|
|
|
340
407
|
try {
|
|
341
408
|
await runFfmpeg(buildConcatArgs(listFile, outPath), logger, signal);
|
|
342
409
|
} finally {
|
|
343
|
-
await
|
|
410
|
+
await rm3(listFile, { force: true });
|
|
344
411
|
}
|
|
345
412
|
}
|
|
346
413
|
async function runFfmpeg(argv, logger, signal) {
|
|
@@ -362,7 +429,7 @@ ${stderr.slice(-2e3)}`));
|
|
|
362
429
|
});
|
|
363
430
|
}
|
|
364
431
|
async function transcodeToMp4(args) {
|
|
365
|
-
await ensureDir(
|
|
432
|
+
await ensureDir(path5.dirname(args.outputPath));
|
|
366
433
|
await runFfmpeg(buildTranscodeArgs(args), args.logger, args.signal);
|
|
367
434
|
}
|
|
368
435
|
function aspectTarget(aspect) {
|
|
@@ -467,7 +534,6 @@ function buildStillSegmentArgs(a) {
|
|
|
467
534
|
}
|
|
468
535
|
|
|
469
536
|
// src/media/ensure-ffmpeg.ts
|
|
470
|
-
var require3 = createRequire2(import.meta.url);
|
|
471
537
|
async function ffmpegWorks() {
|
|
472
538
|
let bin;
|
|
473
539
|
try {
|
|
@@ -488,42 +554,26 @@ async function ffmpegWorks() {
|
|
|
488
554
|
child.on("close", (code) => resolve(code === 0));
|
|
489
555
|
});
|
|
490
556
|
}
|
|
491
|
-
function resolveInstallScript() {
|
|
492
|
-
try {
|
|
493
|
-
return require3.resolve("ffmpeg-static/install.js");
|
|
494
|
-
} catch {
|
|
495
|
-
try {
|
|
496
|
-
const pkg = require3.resolve("ffmpeg-static/package.json");
|
|
497
|
-
return path5.join(path5.dirname(pkg), "install.js");
|
|
498
|
-
} catch {
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
557
|
async function ensureFfmpeg(opts) {
|
|
504
558
|
if (await ffmpegWorks()) return true;
|
|
505
559
|
if (opts.checkOnly) return false;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
560
|
+
if (process.env.FFMPEG_BIN) {
|
|
561
|
+
opts.logger.error(`FFMPEG_BIN is set to "${process.env.FFMPEG_BIN}" but that binary won't run.`);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (!ffmpegIsSupported()) {
|
|
565
|
+
opts.logger.error(
|
|
566
|
+
`No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
|
|
567
|
+
);
|
|
509
568
|
return false;
|
|
510
569
|
}
|
|
511
570
|
opts.logger.info("Fetching ffmpeg (one-time, ~80 MB)\u2026");
|
|
512
571
|
try {
|
|
513
|
-
await
|
|
514
|
-
} catch {
|
|
572
|
+
await downloadFfmpeg();
|
|
573
|
+
} catch (err) {
|
|
574
|
+
opts.logger.error(`ffmpeg download failed: ${err.message}`);
|
|
575
|
+
return false;
|
|
515
576
|
}
|
|
516
|
-
await new Promise((resolve, reject) => {
|
|
517
|
-
const child = spawn3(process.execPath, [installScript], {
|
|
518
|
-
cwd: path5.dirname(installScript),
|
|
519
|
-
stdio: "inherit"
|
|
520
|
-
});
|
|
521
|
-
child.on("error", reject);
|
|
522
|
-
child.on(
|
|
523
|
-
"close",
|
|
524
|
-
(code) => code === 0 ? resolve() : reject(new Error(`ffmpeg download failed (exit code ${code}).`))
|
|
525
|
-
);
|
|
526
|
-
});
|
|
527
577
|
if (!await ffmpegWorks()) {
|
|
528
578
|
opts.logger.error("ffmpeg was downloaded but still won't run on this platform.");
|
|
529
579
|
return false;
|
|
@@ -578,6 +628,16 @@ var serverSettingsSchema = z.object({
|
|
|
578
628
|
/** If a server is already reachable at the URL, use it as-is (don't start or stop one). */
|
|
579
629
|
reuseExisting: z.boolean().default(true).describe("If a server is already reachable at the URL, use it as-is (don't start or stop one). Default true.")
|
|
580
630
|
}).strict();
|
|
631
|
+
var captureSettingsSchema = z.object({
|
|
632
|
+
/** Query params appended to every URL-based asset (e.g. `{ capture: "1" }` → `?capture=1`). */
|
|
633
|
+
query: z.record(z.string(), z.string()).optional().describe('Query params appended to every URL-based asset, e.g. { capture: "1" }.'),
|
|
634
|
+
/** Cookies set on every capture context before navigation, scoped to the asset's origin. */
|
|
635
|
+
cookies: z.array(z.object({ name: z.string().min(1), value: z.string() }).strict()).optional().describe("Cookies set on every capture context before navigation (scoped to the asset's origin)."),
|
|
636
|
+
/** localStorage entries seeded before the page's own scripts run. */
|
|
637
|
+
localStorage: z.record(z.string(), z.string()).optional().describe("localStorage entries seeded (per origin) before the page's own scripts run."),
|
|
638
|
+
/** JS source run in every page before its own scripts — e.g. set a global capture flag. */
|
|
639
|
+
initScript: z.string().optional().describe("JS run in every page before its own scripts (e.g. `window.__PV_CAPTURE__ = true`).")
|
|
640
|
+
}).strict();
|
|
581
641
|
var settingsSchema = z.object({
|
|
582
642
|
/** Output directory, relative to the repo root. */
|
|
583
643
|
outDir: z.string().min(1).default("pro-visu").describe('Output directory for generated assets, relative to the repo root (default "pro-visu").'),
|
|
@@ -599,6 +659,8 @@ var settingsSchema = z.object({
|
|
|
599
659
|
defaults: z.record(z.string(), z.record(z.string(), z.unknown())).default({}).describe("Per-generator option defaults, keyed by generator id, merged underneath each asset's own options."),
|
|
600
660
|
/** Optional managed dev/prod server lifecycle (build → start → wait → … → stop). */
|
|
601
661
|
server: serverSettingsSchema.optional().describe("Build \u2192 start \u2192 wait \u2192 capture \u2192 stop a server automatically."),
|
|
662
|
+
/** "Capture mode" toggles (query/cookies/localStorage/init script) applied to every URL capture. */
|
|
663
|
+
capture: captureSettingsSchema.optional().describe("Capture-mode toggles applied to every URL-based asset (e.g. disable animations / hide the cookie banner)."),
|
|
602
664
|
/** Render quality. "draft" lowers fps/scale and speeds the encoder for fast iteration. */
|
|
603
665
|
quality: z.enum(["draft", "final"]).default("final").describe('Render quality; "draft" lowers fps/scale and speeds the encoder for fast iteration (default "final").'),
|
|
604
666
|
/** Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). */
|
|
@@ -943,6 +1005,38 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
943
1005
|
import path6 from "path";
|
|
944
1006
|
import { mkdtemp } from "fs/promises";
|
|
945
1007
|
|
|
1008
|
+
// src/pipeline/capture.ts
|
|
1009
|
+
function withCaptureQuery(url, capture) {
|
|
1010
|
+
if (!url || !capture?.query) return url;
|
|
1011
|
+
try {
|
|
1012
|
+
const u = new URL(url);
|
|
1013
|
+
for (const [k, v] of Object.entries(capture.query)) u.searchParams.set(k, v);
|
|
1014
|
+
return u.toString();
|
|
1015
|
+
} catch {
|
|
1016
|
+
return url;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function applyCapture(context, capture, url) {
|
|
1020
|
+
if (!capture) return;
|
|
1021
|
+
if (capture.cookies?.length && url) {
|
|
1022
|
+
try {
|
|
1023
|
+
const origin = new URL(url).origin;
|
|
1024
|
+
await context.addCookies(
|
|
1025
|
+
capture.cookies.map((c) => ({ name: c.name, value: c.value, url: origin }))
|
|
1026
|
+
);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const scripts = [];
|
|
1031
|
+
if (capture.localStorage) {
|
|
1032
|
+
scripts.push(
|
|
1033
|
+
`try{var e=${JSON.stringify(capture.localStorage)};for(var k in e)localStorage.setItem(k,e[k]);}catch(_){}`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (capture.initScript) scripts.push(capture.initScript);
|
|
1037
|
+
if (scripts.length) await context.addInitScript(scripts.join("\n"));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
946
1040
|
// src/generators/scroll-reel/scroll.ts
|
|
947
1041
|
var EASINGS = {
|
|
948
1042
|
linear: (t) => t,
|
|
@@ -957,16 +1051,21 @@ async function prepareScroll(args) {
|
|
|
957
1051
|
const target = findScrollTarget(document, getComputedStyle);
|
|
958
1052
|
forceInstant(target);
|
|
959
1053
|
const sleep2 = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
|
1054
|
+
const withCap = (p, ms) => Promise.race([p ?? Promise.resolve(), sleep2(ms)]);
|
|
960
1055
|
scrollTargetTo(target, maxScrollOf(target));
|
|
961
1056
|
await sleep2(Math.max(0, args.settleMs));
|
|
962
1057
|
try {
|
|
963
|
-
await (document.fonts?.ready ??
|
|
1058
|
+
await withCap(document.fonts?.ready ?? null, 5e3);
|
|
964
1059
|
} catch {
|
|
965
1060
|
}
|
|
966
1061
|
try {
|
|
967
1062
|
const imgs = Array.from(document.images ?? []);
|
|
968
|
-
await
|
|
969
|
-
|
|
1063
|
+
await withCap(
|
|
1064
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1065
|
+
Promise.all(imgs.map((im) => im.decode ? im.decode().catch(() => {
|
|
1066
|
+
}) : null)),
|
|
1067
|
+
4e3
|
|
1068
|
+
);
|
|
970
1069
|
} catch {
|
|
971
1070
|
}
|
|
972
1071
|
scrollTargetTo(target, 0);
|
|
@@ -1044,7 +1143,8 @@ async function detectSectionOffsets(args) {
|
|
|
1044
1143
|
continue;
|
|
1045
1144
|
}
|
|
1046
1145
|
if (!args.selector && rect.height < minH) continue;
|
|
1047
|
-
const
|
|
1146
|
+
const rawTop = docTarget ? rect.top + curScroll : rect.top - containerTop + curScroll;
|
|
1147
|
+
const top = rawTop - (args.headerInsetPx ?? 0);
|
|
1048
1148
|
const y = Math.max(0, Math.min(distance, top));
|
|
1049
1149
|
offsets.push(y / distance);
|
|
1050
1150
|
}
|
|
@@ -1102,7 +1202,8 @@ async function measureNormalizedOffsets(args) {
|
|
|
1102
1202
|
}
|
|
1103
1203
|
if (!el) return null;
|
|
1104
1204
|
const rect = el.getBoundingClientRect();
|
|
1105
|
-
const
|
|
1205
|
+
const rawTop = docTarget ? rect.top + curScroll : rect.top - containerTop + curScroll;
|
|
1206
|
+
const offsetTop = rawTop - (args.headerInsetPx ?? 0);
|
|
1106
1207
|
const y = Math.max(0, Math.min(distance, offsetTop));
|
|
1107
1208
|
return distance > 0 ? y / distance : 0;
|
|
1108
1209
|
});
|
|
@@ -1138,6 +1239,79 @@ async function measureNormalizedOffsets(args) {
|
|
|
1138
1239
|
return Math.max(0, t.scrollHeight - t.clientHeight);
|
|
1139
1240
|
}
|
|
1140
1241
|
}
|
|
1242
|
+
async function measureTopInset(args) {
|
|
1243
|
+
const target = findScrollTarget(document, getComputedStyle);
|
|
1244
|
+
const vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1245
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1246
|
+
const maxFraction = args.maxFraction ?? 0.4;
|
|
1247
|
+
const distance = maxScrollOf(target);
|
|
1248
|
+
const cur = isDocTarget(target) ? window.pageYOffset || document.documentElement.scrollTop || 0 : target.scrollTop;
|
|
1249
|
+
scrollTargetTo(target, Math.min(cur + vh, distance));
|
|
1250
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
1251
|
+
let inset = 0;
|
|
1252
|
+
try {
|
|
1253
|
+
const all = document.querySelectorAll("body *");
|
|
1254
|
+
for (let i = 0; i < all.length; i++) {
|
|
1255
|
+
const el = all[i];
|
|
1256
|
+
let position = "";
|
|
1257
|
+
try {
|
|
1258
|
+
position = getComputedStyle(el).position;
|
|
1259
|
+
} catch {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (position !== "fixed" && position !== "sticky") continue;
|
|
1263
|
+
let r;
|
|
1264
|
+
try {
|
|
1265
|
+
r = el.getBoundingClientRect();
|
|
1266
|
+
} catch {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (r.top <= 2 && r.width >= vw * 0.6 && r.height > 0 && r.bottom > inset && r.bottom <= vh * maxFraction) {
|
|
1270
|
+
inset = r.bottom;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
scrollTargetTo(target, cur);
|
|
1276
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
1277
|
+
return inset;
|
|
1278
|
+
function findScrollTarget(doc, gcs) {
|
|
1279
|
+
const se = doc.scrollingElement || doc.documentElement;
|
|
1280
|
+
if (se && se.scrollHeight - se.clientHeight > 1) return se;
|
|
1281
|
+
let best = null;
|
|
1282
|
+
let bestArea = 0;
|
|
1283
|
+
const all = doc.querySelectorAll("*");
|
|
1284
|
+
for (let i = 0; i < all.length; i++) {
|
|
1285
|
+
const el = all[i];
|
|
1286
|
+
let oy = "";
|
|
1287
|
+
try {
|
|
1288
|
+
oy = gcs(el).overflowY;
|
|
1289
|
+
} catch {
|
|
1290
|
+
oy = "";
|
|
1291
|
+
}
|
|
1292
|
+
const scrollable = oy === "auto" || oy === "scroll" || oy === "overlay";
|
|
1293
|
+
if (scrollable && el.scrollHeight - el.clientHeight > 1) {
|
|
1294
|
+
const area = el.clientWidth * el.clientHeight;
|
|
1295
|
+
if (area > bestArea) {
|
|
1296
|
+
bestArea = area;
|
|
1297
|
+
best = el;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return best || se;
|
|
1302
|
+
}
|
|
1303
|
+
function isDocTarget(t) {
|
|
1304
|
+
return t === (document.scrollingElement || document.documentElement) || t === document.documentElement || t === document.body;
|
|
1305
|
+
}
|
|
1306
|
+
function scrollTargetTo(t, y) {
|
|
1307
|
+
if (isDocTarget(t)) window.scrollTo({ top: y, left: 0, behavior: "instant" });
|
|
1308
|
+
else if (t.scrollTo) t.scrollTo({ top: y, left: 0, behavior: "instant" });
|
|
1309
|
+
else t.scrollTop = y;
|
|
1310
|
+
}
|
|
1311
|
+
function maxScrollOf(t) {
|
|
1312
|
+
return Math.max(0, t.scrollHeight - t.clientHeight);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1141
1315
|
async function settleInView() {
|
|
1142
1316
|
try {
|
|
1143
1317
|
await (document.fonts?.ready ?? Promise.resolve());
|
|
@@ -1335,6 +1509,7 @@ async function captureScrollWebm(args) {
|
|
|
1335
1509
|
size: { width: options.width, height: options.height }
|
|
1336
1510
|
}
|
|
1337
1511
|
});
|
|
1512
|
+
await applyCapture(context, args.capture, url);
|
|
1338
1513
|
const page = await context.newPage();
|
|
1339
1514
|
const video = page.video();
|
|
1340
1515
|
const recStart = Date.now();
|
|
@@ -1369,10 +1544,10 @@ async function captureScrollWebm(args) {
|
|
|
1369
1544
|
}
|
|
1370
1545
|
|
|
1371
1546
|
// src/media/frame-capture.ts
|
|
1372
|
-
import
|
|
1547
|
+
import os2 from "os";
|
|
1373
1548
|
import path7 from "path";
|
|
1374
1549
|
function autoWorkers() {
|
|
1375
|
-
const cores =
|
|
1550
|
+
const cores = os2.cpus()?.length ?? 2;
|
|
1376
1551
|
return Math.max(1, Math.min(6, Math.floor(cores / 2)));
|
|
1377
1552
|
}
|
|
1378
1553
|
function planFrames(totalFrames, workers) {
|
|
@@ -1848,7 +2023,8 @@ async function buildScrollTimeline(page, options, totalSeconds, logger) {
|
|
|
1848
2023
|
const finalize = (spec) => resolveTimeline(options.loop === "boomerang" ? boomerangSpec(spec) : spec, totalSeconds);
|
|
1849
2024
|
if (steps && steps.length > 0) {
|
|
1850
2025
|
const selectors = steps.map((s) => s.to).filter((to) => typeof to === "string" && !to.trim().endsWith("%"));
|
|
1851
|
-
const
|
|
2026
|
+
const headerInsetPx = selectors.length > 0 ? await page.evaluate(measureTopInset, {}) : 0;
|
|
2027
|
+
const measured = selectors.length > 0 ? await page.evaluate(measureNormalizedOffsets, { selectors, headerInsetPx }) : [];
|
|
1852
2028
|
let mi = 0;
|
|
1853
2029
|
let prevY = 0;
|
|
1854
2030
|
const resolved = steps.map((s) => {
|
|
@@ -1884,10 +2060,12 @@ async function buildScrollTimeline(page, options, totalSeconds, logger) {
|
|
|
1884
2060
|
}
|
|
1885
2061
|
if (options.autoSections) {
|
|
1886
2062
|
const cfg = options.autoSections === true ? {} : options.autoSections;
|
|
2063
|
+
const headerInsetPx = await page.evaluate(measureTopInset, {});
|
|
1887
2064
|
const offsets = await page.evaluate(detectSectionOffsets, {
|
|
1888
2065
|
minHeightFraction: cfg.minHeightFraction ?? DEFAULT_AUTO_MIN_HEIGHT_FRACTION,
|
|
1889
2066
|
selector: cfg.selector ?? null,
|
|
1890
|
-
maxSections: cfg.maxSections ?? DEFAULT_AUTO_MAX_SECTIONS
|
|
2067
|
+
maxSections: cfg.maxSections ?? DEFAULT_AUTO_MAX_SECTIONS,
|
|
2068
|
+
headerInsetPx
|
|
1891
2069
|
});
|
|
1892
2070
|
const autoSteps = autoSectionSteps({
|
|
1893
2071
|
offsets,
|
|
@@ -1941,6 +2119,7 @@ async function captureScrollFrames(a) {
|
|
|
1941
2119
|
signal: a.signal,
|
|
1942
2120
|
prepare: async (page, { logger }) => {
|
|
1943
2121
|
if (a.colorScheme) await page.emulateMedia({ colorScheme: a.colorScheme });
|
|
2122
|
+
await applyCapture(page.context(), a.capture, a.url);
|
|
1944
2123
|
await installNetworkHygiene(page, options);
|
|
1945
2124
|
await installPreNav(page, options);
|
|
1946
2125
|
logger.debug(`navigating to ${a.url} (waitUntil=${options.waitUntil})`);
|
|
@@ -2405,6 +2584,7 @@ async function captureInteractionWebm(args) {
|
|
|
2405
2584
|
deviceScaleFactor: options.deviceScaleFactor,
|
|
2406
2585
|
recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
|
|
2407
2586
|
});
|
|
2587
|
+
await applyCapture(context, args.capture, url);
|
|
2408
2588
|
const page = await context.newPage();
|
|
2409
2589
|
const video = page.video();
|
|
2410
2590
|
const actions = options.actions ?? [];
|
|
@@ -2467,6 +2647,7 @@ async function captureFocusWebm(args) {
|
|
|
2467
2647
|
deviceScaleFactor: options.deviceScaleFactor,
|
|
2468
2648
|
recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
|
|
2469
2649
|
});
|
|
2650
|
+
await applyCapture(context, args.capture, url);
|
|
2470
2651
|
const page = await context.newPage();
|
|
2471
2652
|
const video = page.video();
|
|
2472
2653
|
const actions = focus.actions ?? [];
|
|
@@ -2561,6 +2742,7 @@ async function run(ctx, options) {
|
|
|
2561
2742
|
ctx.logger.info(`recording ${url} (focus: ${options.focus.selector})`);
|
|
2562
2743
|
const { webmPath, leadSeconds, durationSeconds, cropBox } = await captureFocusWebm({
|
|
2563
2744
|
browser: ctx.browser,
|
|
2745
|
+
capture: ctx.capture,
|
|
2564
2746
|
url,
|
|
2565
2747
|
options,
|
|
2566
2748
|
colorScheme: scheme,
|
|
@@ -2614,6 +2796,7 @@ async function run(ctx, options) {
|
|
|
2614
2796
|
ctx.logger.info(`recording ${url} (interaction, ${options.actions.length} action(s))`);
|
|
2615
2797
|
const { webmPath, leadSeconds, durationSeconds } = await captureInteractionWebm({
|
|
2616
2798
|
browser: ctx.browser,
|
|
2799
|
+
capture: ctx.capture,
|
|
2617
2800
|
url,
|
|
2618
2801
|
options,
|
|
2619
2802
|
colorScheme: scheme,
|
|
@@ -2677,6 +2860,7 @@ async function run(ctx, options) {
|
|
|
2677
2860
|
);
|
|
2678
2861
|
await captureScrollFrames({
|
|
2679
2862
|
browser: ctx.browser,
|
|
2863
|
+
capture: ctx.capture,
|
|
2680
2864
|
url: routeUrl,
|
|
2681
2865
|
options: routeOpts,
|
|
2682
2866
|
outPath: segPath,
|
|
@@ -2732,6 +2916,7 @@ async function run(ctx, options) {
|
|
|
2732
2916
|
ctx.logger.info(`recording ${url} (realtime)`);
|
|
2733
2917
|
const { webmPath, leadSeconds } = await captureScrollWebm({
|
|
2734
2918
|
browser: ctx.browser,
|
|
2919
|
+
capture: ctx.capture,
|
|
2735
2920
|
url,
|
|
2736
2921
|
options,
|
|
2737
2922
|
tmpDir: ctx.tmpDir,
|
|
@@ -2795,6 +2980,7 @@ async function run(ctx, options) {
|
|
|
2795
2980
|
ctx.logger.info(`recording ${url}${label} (frame-stepped, ${workers} worker(s))`);
|
|
2796
2981
|
await captureScrollFrames({
|
|
2797
2982
|
browser: ctx.browser,
|
|
2983
|
+
capture: ctx.capture,
|
|
2798
2984
|
url,
|
|
2799
2985
|
options: vopts,
|
|
2800
2986
|
outPath: captureMp4,
|
|
@@ -2921,6 +3107,7 @@ async function captureBreakpoint(args, bp) {
|
|
|
2921
3107
|
viewport: { width: bp.width, height: bp.height },
|
|
2922
3108
|
deviceScaleFactor: bp.deviceScaleFactor ?? options.deviceScaleFactor
|
|
2923
3109
|
});
|
|
3110
|
+
await applyCapture(context, args.capture, url);
|
|
2924
3111
|
const page = await context.newPage();
|
|
2925
3112
|
try {
|
|
2926
3113
|
logger.debug(`[${bp.name}] navigating to ${url}`);
|
|
@@ -2991,7 +3178,8 @@ async function run2(ctx, options) {
|
|
|
2991
3178
|
browser: ctx.browser,
|
|
2992
3179
|
url,
|
|
2993
3180
|
options,
|
|
2994
|
-
logger: ctx.logger
|
|
3181
|
+
logger: ctx.logger,
|
|
3182
|
+
capture: ctx.capture
|
|
2995
3183
|
});
|
|
2996
3184
|
const records = [];
|
|
2997
3185
|
for (const shot of shots) {
|
|
@@ -3371,7 +3559,7 @@ var wallOptionsSchema = z6.object({
|
|
|
3371
3559
|
// src/generators/scene/index.ts
|
|
3372
3560
|
import path13 from "path";
|
|
3373
3561
|
import { fileURLToPath } from "url";
|
|
3374
|
-
import { copyFile as copyFile2, rename, stat as stat4 } from "fs/promises";
|
|
3562
|
+
import { copyFile as copyFile2, rename as rename2, stat as stat4 } from "fs/promises";
|
|
3375
3563
|
|
|
3376
3564
|
// src/scene/serve.ts
|
|
3377
3565
|
import http from "http";
|
|
@@ -3696,7 +3884,7 @@ async function renderScene(ctx, options, generatorId = SCENE_ID) {
|
|
|
3696
3884
|
});
|
|
3697
3885
|
}
|
|
3698
3886
|
try {
|
|
3699
|
-
await
|
|
3887
|
+
await rename2(composedTmp, outPath);
|
|
3700
3888
|
} catch {
|
|
3701
3889
|
await copyFile2(composedTmp, outPath);
|
|
3702
3890
|
}
|
|
@@ -4838,7 +5026,7 @@ function killTreeByPid(pid) {
|
|
|
4838
5026
|
// src/server/manage-server.ts
|
|
4839
5027
|
import path20 from "path";
|
|
4840
5028
|
import http2 from "http";
|
|
4841
|
-
import
|
|
5029
|
+
import https2 from "https";
|
|
4842
5030
|
import { spawn as spawn6 } from "child_process";
|
|
4843
5031
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4844
5032
|
function resolveServerUrl(server) {
|
|
@@ -4846,7 +5034,7 @@ function resolveServerUrl(server) {
|
|
|
4846
5034
|
}
|
|
4847
5035
|
function probe(url) {
|
|
4848
5036
|
return new Promise((resolve) => {
|
|
4849
|
-
const lib = url.startsWith("https:") ?
|
|
5037
|
+
const lib = url.startsWith("https:") ? https2 : http2;
|
|
4850
5038
|
const req = lib.get(url, (res) => {
|
|
4851
5039
|
res.resume();
|
|
4852
5040
|
resolve((res.statusCode ?? 0) > 0);
|
|
@@ -5007,7 +5195,7 @@ async function startManagedServer(server, baseCwd, logger, tasks = {}, signal) {
|
|
|
5007
5195
|
}
|
|
5008
5196
|
|
|
5009
5197
|
// src/pipeline/runner.ts
|
|
5010
|
-
import
|
|
5198
|
+
import os3 from "os";
|
|
5011
5199
|
import path23 from "path";
|
|
5012
5200
|
import { existsSync as existsSync7 } from "fs";
|
|
5013
5201
|
import { mkdtemp as mkdtemp4 } from "fs/promises";
|
|
@@ -5031,7 +5219,10 @@ async function createContext(args) {
|
|
|
5031
5219
|
await ensureDir(genDir);
|
|
5032
5220
|
return {
|
|
5033
5221
|
browser: args.browser,
|
|
5034
|
-
target
|
|
5222
|
+
// Fold the capture query params into the target URL so every generator's `requireUrl(ctx)`
|
|
5223
|
+
// captures in capture mode without each one re-applying it.
|
|
5224
|
+
target: { ...args.target, url: withCaptureQuery(args.target.url, args.capture) },
|
|
5225
|
+
capture: args.capture,
|
|
5035
5226
|
resolvedInputs: args.resolvedInputs,
|
|
5036
5227
|
outDir: args.outDir,
|
|
5037
5228
|
resolveOutPath: (filename) => path21.join(genDir, filename),
|
|
@@ -5118,7 +5309,7 @@ function computeCacheKey(parts) {
|
|
|
5118
5309
|
// src/manifest/manifest.ts
|
|
5119
5310
|
import path22 from "path";
|
|
5120
5311
|
import { existsSync as existsSync6 } from "fs";
|
|
5121
|
-
import { readFile as readFile7, rename as
|
|
5312
|
+
import { readFile as readFile7, rename as rename3, rm as rm5, writeFile as writeFile8 } from "fs/promises";
|
|
5122
5313
|
|
|
5123
5314
|
// src/manifest/schema.ts
|
|
5124
5315
|
import { z as z10 } from "zod";
|
|
@@ -5185,7 +5376,7 @@ async function writeManifest(outDir, manifest) {
|
|
|
5185
5376
|
const maxAttempts = 5;
|
|
5186
5377
|
for (let attempt = 1; ; attempt++) {
|
|
5187
5378
|
try {
|
|
5188
|
-
await
|
|
5379
|
+
await rename3(tmp, file);
|
|
5189
5380
|
return;
|
|
5190
5381
|
} catch (err) {
|
|
5191
5382
|
const code = err.code;
|
|
@@ -5246,7 +5437,7 @@ async function runPipeline(opts) {
|
|
|
5246
5437
|
if (specs.length === 0) return [];
|
|
5247
5438
|
await ensureDir(opts.outDir);
|
|
5248
5439
|
const manifest = await ManifestStore.load(opts.outDir);
|
|
5249
|
-
const tmpRoot = await mkdtemp4(path23.join(
|
|
5440
|
+
const tmpRoot = await mkdtemp4(path23.join(os3.tmpdir(), "pro-visu-"));
|
|
5250
5441
|
const browser = await launchBrowser(opts.config.settings.browser);
|
|
5251
5442
|
opts.onResources?.({ tmpDir: tmpRoot });
|
|
5252
5443
|
const concurrency = Math.max(1, opts.concurrency ?? opts.config.settings.concurrency);
|
|
@@ -5298,7 +5489,9 @@ async function runPipeline(opts) {
|
|
|
5298
5489
|
inputs: inputHashes,
|
|
5299
5490
|
files: fileHashes,
|
|
5300
5491
|
quality,
|
|
5301
|
-
toolVersion: opts.toolVersion
|
|
5492
|
+
toolVersion: opts.toolVersion,
|
|
5493
|
+
// Capture-mode toggles change the rendered output, so a change must bust the cache.
|
|
5494
|
+
capture: opts.config.settings.capture
|
|
5302
5495
|
});
|
|
5303
5496
|
if (cacheEnabled) {
|
|
5304
5497
|
const existing = manifest.find(spec.name);
|
|
@@ -5327,6 +5520,7 @@ async function runPipeline(opts) {
|
|
|
5327
5520
|
toolVersion: opts.toolVersion,
|
|
5328
5521
|
quality,
|
|
5329
5522
|
manifest,
|
|
5523
|
+
capture: opts.config.settings.capture,
|
|
5330
5524
|
onProgress: reporter ? (v) => reporter.progress(spec.name, v) : void 0,
|
|
5331
5525
|
signal: opts.signal
|
|
5332
5526
|
});
|