@reshotdev/screenshot 0.0.1-beta.11 → 0.0.1-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +84 -51
- package/package.json +20 -16
- package/src/commands/auth.js +38 -8
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-target.js +36 -4
- package/src/commands/drifts.js +13 -1
- package/src/commands/publish.js +137 -12
- package/src/commands/pull.js +13 -8
- package/src/commands/refresh.js +166 -0
- package/src/commands/setup-wizard.js +35 -2
- package/src/commands/status.js +22 -2
- package/src/commands/variation.js +194 -0
- package/src/index.js +189 -47
- package/src/lib/api-client.js +61 -35
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +73 -0
- package/src/lib/capture-script-runner.js +280 -134
- package/src/lib/certification.js +23 -1
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +5 -5
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +0 -4
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +45 -0
- package/src/lib/storage-providers.js +1 -1
- package/src/lib/style-engine.js +5 -5
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +28 -820
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/{index-D2qqcFNN.js → index-D0S2otug.js} +56 -56
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -178
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -165
- package/src/lib/playwright-runner.js +0 -252
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/render/render.ts
|
|
31
|
+
var render_exports = {};
|
|
32
|
+
__export(render_exports, {
|
|
33
|
+
ChromiumNotFoundError: () => ChromiumNotFoundError,
|
|
34
|
+
DEFAULT_DEVICE_SCALE_FACTOR: () => DEFAULT_DEVICE_SCALE_FACTOR,
|
|
35
|
+
DEFAULT_FRAME_RATE: () => DEFAULT_FRAME_RATE,
|
|
36
|
+
captureFrameBuffers: () => captureFrameBuffers,
|
|
37
|
+
compileCompositionFile: () => compileCompositionFile,
|
|
38
|
+
concatMp4Segments: () => concatMp4Segments,
|
|
39
|
+
frameCountFor: () => frameCountFor,
|
|
40
|
+
recordHtml: () => recordHtml,
|
|
41
|
+
render: () => render,
|
|
42
|
+
resolveChromiumExecutable: () => resolveChromiumExecutable
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(render_exports);
|
|
45
|
+
var import_node_crypto = require("crypto");
|
|
46
|
+
var import_promises3 = require("fs/promises");
|
|
47
|
+
var import_node_os2 = require("os");
|
|
48
|
+
var import_node_path3 = require("path");
|
|
49
|
+
var import_node_url2 = require("url");
|
|
50
|
+
var esbuild = __toESM(require("esbuild"), 1);
|
|
51
|
+
|
|
52
|
+
// src/render/playwright-driver.ts
|
|
53
|
+
var import_node_fs = require("fs");
|
|
54
|
+
var import_promises = require("fs/promises");
|
|
55
|
+
var import_node_child_process2 = require("child_process");
|
|
56
|
+
var import_node_os = require("os");
|
|
57
|
+
var import_node_path = require("path");
|
|
58
|
+
var import_node_url = require("url");
|
|
59
|
+
var import_playwright_core = require("playwright-core");
|
|
60
|
+
|
|
61
|
+
// src/render/ffmpeg-transcode.ts
|
|
62
|
+
var import_node_child_process = require("child_process");
|
|
63
|
+
var import_node_util = require("util");
|
|
64
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
65
|
+
async function transcodeGif(input, output) {
|
|
66
|
+
await runFfmpeg([
|
|
67
|
+
"-y",
|
|
68
|
+
"-i",
|
|
69
|
+
input,
|
|
70
|
+
"-vf",
|
|
71
|
+
"fps=12,scale=640:-1:flags=lanczos",
|
|
72
|
+
output
|
|
73
|
+
]);
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
async function runFfmpeg(args) {
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync("ffmpeg", args, { maxBuffer: 1024 * 1024 * 16 });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
throw new Error(`ffmpeg failed: ${message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/render/playwright-driver.ts
|
|
86
|
+
var DEFAULT_FRAME_RATE = 60;
|
|
87
|
+
var DEFAULT_DEVICE_SCALE_FACTOR = 2;
|
|
88
|
+
var CHROMIUM_NOT_FOUND_MESSAGE = `\u2717 Chromium browser not found. Required for rendering compositions.
|
|
89
|
+
|
|
90
|
+
Option 1 (recommended): npx playwright install chromium
|
|
91
|
+
Option 2: set CHROME_PATH to your system Chrome (~200 MB savings)
|
|
92
|
+
|
|
93
|
+
Run one of the above and try again.`;
|
|
94
|
+
var SEEK_CLOCK_SCRIPT = `(() => {
|
|
95
|
+
let vt = 0;
|
|
96
|
+
const realRAF = window.requestAnimationFrame.bind(window);
|
|
97
|
+
let queue = new Map();
|
|
98
|
+
let nextId = 1;
|
|
99
|
+
window.requestAnimationFrame = (cb) => { const id = nextId++; queue.set(id, cb); return id; };
|
|
100
|
+
window.cancelAnimationFrame = (id) => { queue.delete(id); };
|
|
101
|
+
performance.now = () => vt;
|
|
102
|
+
Date.now = () => vt;
|
|
103
|
+
window.__composeSeek = (tMs) => {
|
|
104
|
+
vt = tMs;
|
|
105
|
+
const pending = queue; queue = new Map();
|
|
106
|
+
for (const cb of pending.values()) { try { cb(tMs); } catch (e) {} }
|
|
107
|
+
};
|
|
108
|
+
// Real-rAF paint flush (double-rAF) so a screenshot captures the post-seek frame.
|
|
109
|
+
window.__composeFlush = () => new Promise((resolve) => realRAF(() => realRAF(() => resolve())));
|
|
110
|
+
})();`;
|
|
111
|
+
var ChromiumNotFoundError = class extends Error {
|
|
112
|
+
constructor() {
|
|
113
|
+
super(CHROMIUM_NOT_FOUND_MESSAGE);
|
|
114
|
+
this.name = "ChromiumNotFoundError";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function resolveChromiumExecutable() {
|
|
118
|
+
const candidates = [
|
|
119
|
+
process.env.CHROME_PATH,
|
|
120
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
121
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
122
|
+
safePlaywrightExecutablePath()
|
|
123
|
+
].filter(Boolean);
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
if (candidate && (0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
function frameCountFor(durationMs, fps) {
|
|
130
|
+
return Math.max(1, Math.ceil(durationMs / 1e3 * fps));
|
|
131
|
+
}
|
|
132
|
+
async function stepFrames(page, durationMs, fps, onFrame) {
|
|
133
|
+
const frames = frameCountFor(durationMs, fps);
|
|
134
|
+
for (let index = 0; index < frames; index += 1) {
|
|
135
|
+
const tMs = index / fps * 1e3;
|
|
136
|
+
await seekVideos(page, tMs / 1e3);
|
|
137
|
+
await seekDocumentAnimations(page, tMs);
|
|
138
|
+
await page.evaluate((t) => window.__composeSeek?.(t), tMs);
|
|
139
|
+
await page.evaluate(() => window.__composeFlush?.());
|
|
140
|
+
const frame = await page.screenshot({ type: "png" });
|
|
141
|
+
await onFrame(frame, index);
|
|
142
|
+
}
|
|
143
|
+
return frames;
|
|
144
|
+
}
|
|
145
|
+
async function recordHtml(options) {
|
|
146
|
+
const {
|
|
147
|
+
html,
|
|
148
|
+
durationMs,
|
|
149
|
+
size,
|
|
150
|
+
formats,
|
|
151
|
+
outBase,
|
|
152
|
+
fps = DEFAULT_FRAME_RATE,
|
|
153
|
+
deviceScaleFactor = DEFAULT_DEVICE_SCALE_FACTOR
|
|
154
|
+
} = options;
|
|
155
|
+
const executablePath = resolveChromiumExecutable();
|
|
156
|
+
if (!executablePath) throw new ChromiumNotFoundError();
|
|
157
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(outBase), { recursive: true });
|
|
158
|
+
const tempDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "compose-record-"));
|
|
159
|
+
const wantMp4 = formats.includes("mp4");
|
|
160
|
+
const wantWebm = formats.includes("webm");
|
|
161
|
+
const wantPoster = formats.includes("poster");
|
|
162
|
+
const wantGif = formats.includes("gif");
|
|
163
|
+
const mp4Path = wantMp4 ? `${outBase}.mp4` : wantGif ? (0, import_node_path.join)(tempDir, "gif-source.mp4") : void 0;
|
|
164
|
+
const webmPath = wantWebm ? `${outBase}.webm` : void 0;
|
|
165
|
+
const encoders = [];
|
|
166
|
+
if (mp4Path) encoders.push(spawnEncoder(mp4EncodeArgs(fps, mp4Path)));
|
|
167
|
+
if (webmPath) encoders.push(spawnEncoder(webmEncodeArgs(fps, webmPath)));
|
|
168
|
+
const browser = await import_playwright_core.chromium.launch({ headless: true, executablePath });
|
|
169
|
+
let posterFrame;
|
|
170
|
+
let totalFrames = 0;
|
|
171
|
+
try {
|
|
172
|
+
const context = await browser.newContext({ viewport: size, deviceScaleFactor });
|
|
173
|
+
const page = await context.newPage();
|
|
174
|
+
await page.addInitScript(SEEK_CLOCK_SCRIPT);
|
|
175
|
+
const htmlPath = (0, import_node_path.join)(tempDir, "index.html");
|
|
176
|
+
await (0, import_promises.writeFile)(htmlPath, html, "utf8");
|
|
177
|
+
await page.goto((0, import_node_url.pathToFileURL)(htmlPath).href, { waitUntil: "networkidle" });
|
|
178
|
+
await waitForVideos(page);
|
|
179
|
+
await playVideos(page);
|
|
180
|
+
const frames = frameCountFor(durationMs, fps);
|
|
181
|
+
const posterIndex = Math.floor(frames / 2);
|
|
182
|
+
totalFrames = await stepFrames(page, durationMs, fps, async (frame, index) => {
|
|
183
|
+
if (index === posterIndex) posterFrame = frame;
|
|
184
|
+
await Promise.all(encoders.map((encoder) => writeFrame(encoder, frame)));
|
|
185
|
+
});
|
|
186
|
+
await context.close();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
for (const encoder of encoders) encoder.proc.kill();
|
|
189
|
+
await (0, import_promises.rm)(tempDir, { recursive: true, force: true });
|
|
190
|
+
throw error;
|
|
191
|
+
} finally {
|
|
192
|
+
await browser.close();
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
for (const encoder of encoders) encoder.stdin.end();
|
|
196
|
+
await Promise.all(encoders.map((encoder) => encoder.done));
|
|
197
|
+
const pack = {};
|
|
198
|
+
if (wantMp4 && mp4Path) pack.mp4 = mp4Path;
|
|
199
|
+
if (wantWebm && webmPath) pack.webm = webmPath;
|
|
200
|
+
if (wantPoster) pack.poster = await encodePoster(posterFrame ?? await firstFrameFallback(), `${outBase}.webp`);
|
|
201
|
+
if (wantGif && mp4Path) pack.gif = await transcodeGif(mp4Path, `${outBase}.gif`);
|
|
202
|
+
return pack;
|
|
203
|
+
} finally {
|
|
204
|
+
await (0, import_promises.rm)(tempDir, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
async function firstFrameFallback() {
|
|
207
|
+
throw new Error(`Poster requested but no frame was captured (frames=${totalFrames}).`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function spawnEncoder(args) {
|
|
211
|
+
const proc = (0, import_node_child_process2.spawn)("ffmpeg", args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
212
|
+
const stdin = proc.stdin;
|
|
213
|
+
if (!stdin) throw new Error("ffmpeg stdin pipe unavailable.");
|
|
214
|
+
let stderr = "";
|
|
215
|
+
proc.stderr?.on("data", (chunk) => {
|
|
216
|
+
stderr += String(chunk);
|
|
217
|
+
if (stderr.length > 8192) stderr = stderr.slice(-8192);
|
|
218
|
+
});
|
|
219
|
+
const done = new Promise((resolve2, reject) => {
|
|
220
|
+
proc.on("error", reject);
|
|
221
|
+
proc.on("close", (code) => {
|
|
222
|
+
if (code === 0) resolve2();
|
|
223
|
+
else reject(new Error(`ffmpeg encoder exited with code ${code}:
|
|
224
|
+
${stderr}`));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
stdin.on("error", () => {
|
|
228
|
+
});
|
|
229
|
+
return { proc, stdin, done };
|
|
230
|
+
}
|
|
231
|
+
function writeFrame(encoder, frame) {
|
|
232
|
+
return new Promise((resolve2, reject) => {
|
|
233
|
+
encoder.stdin.write(frame, (error) => error ? reject(error) : resolve2());
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function mp4EncodeArgs(fps, output) {
|
|
237
|
+
return [
|
|
238
|
+
"-y",
|
|
239
|
+
"-f",
|
|
240
|
+
"image2pipe",
|
|
241
|
+
"-framerate",
|
|
242
|
+
String(fps),
|
|
243
|
+
"-i",
|
|
244
|
+
"-",
|
|
245
|
+
"-c:v",
|
|
246
|
+
"libx264",
|
|
247
|
+
"-crf",
|
|
248
|
+
"18",
|
|
249
|
+
"-preset",
|
|
250
|
+
"medium",
|
|
251
|
+
"-pix_fmt",
|
|
252
|
+
"yuv420p",
|
|
253
|
+
"-movflags",
|
|
254
|
+
"+faststart",
|
|
255
|
+
output
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
function webmEncodeArgs(fps, output) {
|
|
259
|
+
return [
|
|
260
|
+
"-y",
|
|
261
|
+
"-f",
|
|
262
|
+
"image2pipe",
|
|
263
|
+
"-framerate",
|
|
264
|
+
String(fps),
|
|
265
|
+
"-i",
|
|
266
|
+
"-",
|
|
267
|
+
"-c:v",
|
|
268
|
+
"libvpx-vp9",
|
|
269
|
+
"-b:v",
|
|
270
|
+
"0",
|
|
271
|
+
"-crf",
|
|
272
|
+
"32",
|
|
273
|
+
"-pix_fmt",
|
|
274
|
+
"yuv420p",
|
|
275
|
+
output
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
function encodePoster(frame, output) {
|
|
279
|
+
return new Promise((resolve2, reject) => {
|
|
280
|
+
const proc = (0, import_node_child_process2.spawn)(
|
|
281
|
+
"ffmpeg",
|
|
282
|
+
["-y", "-f", "image2pipe", "-i", "-", "-frames:v", "1", "-c:v", "libwebp", "-quality", "85", output],
|
|
283
|
+
{ stdio: ["pipe", "ignore", "pipe"] }
|
|
284
|
+
);
|
|
285
|
+
let stderr = "";
|
|
286
|
+
proc.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
287
|
+
proc.on("error", reject);
|
|
288
|
+
proc.on("close", (code) => code === 0 ? resolve2(output) : reject(new Error(`poster ffmpeg exited ${code}:
|
|
289
|
+
${stderr}`)));
|
|
290
|
+
proc.stdin.on("error", () => {
|
|
291
|
+
});
|
|
292
|
+
proc.stdin.end(frame);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function captureFrameBuffers(options) {
|
|
296
|
+
const {
|
|
297
|
+
html,
|
|
298
|
+
durationMs,
|
|
299
|
+
size,
|
|
300
|
+
fps = DEFAULT_FRAME_RATE,
|
|
301
|
+
deviceScaleFactor = DEFAULT_DEVICE_SCALE_FACTOR
|
|
302
|
+
} = options;
|
|
303
|
+
const executablePath = resolveChromiumExecutable();
|
|
304
|
+
if (!executablePath) throw new ChromiumNotFoundError();
|
|
305
|
+
const tempDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "compose-frames-"));
|
|
306
|
+
const browser = await import_playwright_core.chromium.launch({ headless: true, executablePath });
|
|
307
|
+
const frames = [];
|
|
308
|
+
try {
|
|
309
|
+
const context = await browser.newContext({ viewport: size, deviceScaleFactor });
|
|
310
|
+
const page = await context.newPage();
|
|
311
|
+
await page.addInitScript(SEEK_CLOCK_SCRIPT);
|
|
312
|
+
const htmlPath = (0, import_node_path.join)(tempDir, "index.html");
|
|
313
|
+
await (0, import_promises.writeFile)(htmlPath, html, "utf8");
|
|
314
|
+
await page.goto((0, import_node_url.pathToFileURL)(htmlPath).href, { waitUntil: "networkidle" });
|
|
315
|
+
await waitForVideos(page);
|
|
316
|
+
await playVideos(page);
|
|
317
|
+
await stepFrames(page, durationMs, fps, async (frame) => {
|
|
318
|
+
frames.push(frame);
|
|
319
|
+
});
|
|
320
|
+
await context.close();
|
|
321
|
+
return frames;
|
|
322
|
+
} finally {
|
|
323
|
+
await browser.close();
|
|
324
|
+
await (0, import_promises.rm)(tempDir, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function playVideos(page) {
|
|
328
|
+
const failures = await page.evaluate(async () => {
|
|
329
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
330
|
+
const errors = [];
|
|
331
|
+
await Promise.all(
|
|
332
|
+
videos.map(async (video) => {
|
|
333
|
+
if (!video.currentSrc) return;
|
|
334
|
+
video.currentTime = 0;
|
|
335
|
+
try {
|
|
336
|
+
await video.play();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
errors.push({
|
|
339
|
+
src: video.currentSrc,
|
|
340
|
+
message: error instanceof Error ? error.message : String(error)
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
return errors;
|
|
346
|
+
});
|
|
347
|
+
if (failures.length > 0) {
|
|
348
|
+
const summary = failures.map((f) => ` - ${f.src}: ${f.message}`).join("\n");
|
|
349
|
+
throw new Error(`Composition video(s) failed to play; render would be black:
|
|
350
|
+
${summary}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function seekVideos(page, currentTimeSeconds) {
|
|
354
|
+
await page.evaluate(async (currentTime) => {
|
|
355
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
356
|
+
await Promise.all(
|
|
357
|
+
videos.map(
|
|
358
|
+
(video) => new Promise((resolve2) => {
|
|
359
|
+
if (!video.currentSrc) {
|
|
360
|
+
resolve2();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const done = () => resolve2();
|
|
364
|
+
const timeout = window.setTimeout(done, 250);
|
|
365
|
+
video.addEventListener(
|
|
366
|
+
"seeked",
|
|
367
|
+
() => {
|
|
368
|
+
window.clearTimeout(timeout);
|
|
369
|
+
done();
|
|
370
|
+
},
|
|
371
|
+
{ once: true }
|
|
372
|
+
);
|
|
373
|
+
video.currentTime = currentTime;
|
|
374
|
+
})
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
}, currentTimeSeconds);
|
|
378
|
+
}
|
|
379
|
+
async function seekDocumentAnimations(page, currentTimeMs) {
|
|
380
|
+
await page.evaluate((currentTime) => {
|
|
381
|
+
for (const animation of document.documentElement.getAnimations({ subtree: true })) {
|
|
382
|
+
animation.pause();
|
|
383
|
+
animation.currentTime = currentTime;
|
|
384
|
+
}
|
|
385
|
+
}, currentTimeMs);
|
|
386
|
+
}
|
|
387
|
+
async function waitForVideos(page) {
|
|
388
|
+
await page.waitForFunction(
|
|
389
|
+
() => {
|
|
390
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
391
|
+
return videos.every((video) => !video.currentSrc || video.readyState >= 2);
|
|
392
|
+
},
|
|
393
|
+
void 0,
|
|
394
|
+
{ timeout: 8e3 }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
function safePlaywrightExecutablePath() {
|
|
398
|
+
try {
|
|
399
|
+
return import_playwright_core.chromium.executablePath();
|
|
400
|
+
} catch {
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/render/concat.ts
|
|
406
|
+
var import_promises2 = require("fs/promises");
|
|
407
|
+
var import_node_path2 = require("path");
|
|
408
|
+
var import_node_child_process3 = require("child_process");
|
|
409
|
+
var import_node_util2 = require("util");
|
|
410
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
411
|
+
async function concatMp4Segments(inputs, output) {
|
|
412
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(output), { recursive: true });
|
|
413
|
+
const listPath = `${output}.concat.txt`;
|
|
414
|
+
const list = inputs.map((input) => `file '${input.replaceAll("'", "'\\''")}'`).join("\n");
|
|
415
|
+
await (0, import_promises2.writeFile)(listPath, list, "utf8");
|
|
416
|
+
try {
|
|
417
|
+
await execFileAsync2("ffmpeg", [
|
|
418
|
+
"-y",
|
|
419
|
+
"-f",
|
|
420
|
+
"concat",
|
|
421
|
+
"-safe",
|
|
422
|
+
"0",
|
|
423
|
+
"-i",
|
|
424
|
+
listPath,
|
|
425
|
+
"-c:v",
|
|
426
|
+
"libx264",
|
|
427
|
+
"-pix_fmt",
|
|
428
|
+
"yuv420p",
|
|
429
|
+
"-movflags",
|
|
430
|
+
"+faststart",
|
|
431
|
+
"-r",
|
|
432
|
+
"30",
|
|
433
|
+
output
|
|
434
|
+
]);
|
|
435
|
+
return output;
|
|
436
|
+
} finally {
|
|
437
|
+
await (0, import_promises2.rm)(listPath, { force: true });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/render/render.ts
|
|
442
|
+
var import_meta = {};
|
|
443
|
+
var DEFAULT_SIZE = { width: 1920, height: 1080 };
|
|
444
|
+
var DEFAULT_FORMATS = ["mp4", "webm", "poster"];
|
|
445
|
+
var DEFAULT_DURATION_MS = 3e3;
|
|
446
|
+
var MAX_DURATION_MS = 6e4;
|
|
447
|
+
async function compileCompositionFile(compositionPath) {
|
|
448
|
+
const absolutePath = (0, import_node_path3.resolve)(compositionPath);
|
|
449
|
+
const node = await loadComposition(absolutePath);
|
|
450
|
+
return addCompositionBaseHref(await compileRuntimeToHtml(node), absolutePath);
|
|
451
|
+
}
|
|
452
|
+
async function render(compositionPath, options = {}) {
|
|
453
|
+
const absolutePath = (0, import_node_path3.resolve)(compositionPath);
|
|
454
|
+
const slug = options.slug ?? (0, import_node_path3.basename)(absolutePath, (0, import_node_path3.extname)(absolutePath));
|
|
455
|
+
const outBase = (0, import_node_path3.resolve)(options.out ?? (0, import_node_path3.join)(process.cwd(), slug));
|
|
456
|
+
const formats = options.formats ?? DEFAULT_FORMATS;
|
|
457
|
+
const html = await compileCompositionFile(absolutePath);
|
|
458
|
+
let durationMs;
|
|
459
|
+
if (options.durationMs !== void 0) {
|
|
460
|
+
durationMs = options.durationMs;
|
|
461
|
+
} else {
|
|
462
|
+
const inferred = inferDurationMs(html) ?? DEFAULT_DURATION_MS;
|
|
463
|
+
if (inferred > MAX_DURATION_MS) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Inferred composition duration ${inferred}ms exceeds ${MAX_DURATION_MS}ms cap. Fix data-duration-ms in the composition or pass --duration to render this on purpose.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
durationMs = inferred;
|
|
469
|
+
}
|
|
470
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(outBase), { recursive: true });
|
|
471
|
+
const pack = await recordHtml({
|
|
472
|
+
html,
|
|
473
|
+
durationMs,
|
|
474
|
+
size: options.size ?? DEFAULT_SIZE,
|
|
475
|
+
formats,
|
|
476
|
+
outBase,
|
|
477
|
+
fps: options.fps,
|
|
478
|
+
deviceScaleFactor: options.deviceScaleFactor
|
|
479
|
+
});
|
|
480
|
+
return { pack, durationMs };
|
|
481
|
+
}
|
|
482
|
+
async function compileRuntimeToHtml(node) {
|
|
483
|
+
const runtime = await import((0, import_node_url2.pathToFileURL)(resolveRuntimeEntry("index.mjs")).href);
|
|
484
|
+
return runtime.compileToHtml(node);
|
|
485
|
+
}
|
|
486
|
+
async function loadComposition(compositionPath) {
|
|
487
|
+
const source = await (0, import_promises3.readFile)(compositionPath, "utf8");
|
|
488
|
+
const transformed = await esbuild.transform(source, {
|
|
489
|
+
loader: "tsx",
|
|
490
|
+
jsx: "automatic",
|
|
491
|
+
jsxImportSource: "@reshot/compose",
|
|
492
|
+
format: "esm",
|
|
493
|
+
sourcemap: "inline",
|
|
494
|
+
sourcefile: compositionPath
|
|
495
|
+
});
|
|
496
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(compositionPath).update(source).digest("hex").slice(0, 16);
|
|
497
|
+
const tempPath = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `compose-${hash}.mjs`);
|
|
498
|
+
const code = rewriteComposeImports(transformed.code);
|
|
499
|
+
try {
|
|
500
|
+
await (0, import_promises3.writeFile)(tempPath, code, "utf8");
|
|
501
|
+
const moduleUrl = `${(0, import_node_url2.pathToFileURL)(tempPath).href}?t=${Date.now()}`;
|
|
502
|
+
const mod = await import(moduleUrl);
|
|
503
|
+
const exported = mod.default;
|
|
504
|
+
const node = typeof exported === "function" ? exported() : exported;
|
|
505
|
+
if (!node) {
|
|
506
|
+
throw new Error("Composition file must default-export a Composition function.");
|
|
507
|
+
}
|
|
508
|
+
return node;
|
|
509
|
+
} finally {
|
|
510
|
+
await (0, import_promises3.rm)(tempPath, { force: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function rewriteComposeImports(code) {
|
|
514
|
+
const replacements = {
|
|
515
|
+
"@reshot/compose": (0, import_node_url2.pathToFileURL)(resolveRuntimeEntry("index.mjs")).href,
|
|
516
|
+
"@reshot/compose/jsx-runtime": (0, import_node_url2.pathToFileURL)(
|
|
517
|
+
resolveRuntimeEntry("jsx-runtime.mjs")
|
|
518
|
+
).href,
|
|
519
|
+
"@reshot/compose/jsx-dev-runtime": (0, import_node_url2.pathToFileURL)(
|
|
520
|
+
resolveRuntimeEntry("jsx-dev-runtime.mjs")
|
|
521
|
+
).href
|
|
522
|
+
};
|
|
523
|
+
let next = code;
|
|
524
|
+
for (const [specifier, replacement] of Object.entries(replacements)) {
|
|
525
|
+
next = next.replaceAll(`from "${specifier}"`, `from "${replacement}"`);
|
|
526
|
+
next = next.replaceAll(`from '${specifier}'`, `from "${replacement}"`);
|
|
527
|
+
next = next.replaceAll(`import("${specifier}")`, `import("${replacement}")`);
|
|
528
|
+
next = next.replaceAll(`import('${specifier}')`, `import("${replacement}")`);
|
|
529
|
+
}
|
|
530
|
+
return next;
|
|
531
|
+
}
|
|
532
|
+
function resolveRuntimeEntry(fileName) {
|
|
533
|
+
const currentDir = typeof __dirname === "string" ? __dirname : (0, import_node_path3.dirname)((0, import_node_url2.fileURLToPath)(import_meta.url));
|
|
534
|
+
return (0, import_node_path3.join)(currentDir, fileName);
|
|
535
|
+
}
|
|
536
|
+
function addCompositionBaseHref(html, compositionPath) {
|
|
537
|
+
const baseHref = (0, import_node_url2.pathToFileURL)(`${(0, import_node_path3.dirname)(compositionPath)}/`).href;
|
|
538
|
+
return html.replace("<head>", `<head><base href="${baseHref}">`);
|
|
539
|
+
}
|
|
540
|
+
function inferDurationMs(html) {
|
|
541
|
+
const match = html.match(/data-duration-ms="(\d+(?:\.\d+)?)"/);
|
|
542
|
+
if (!match) return void 0;
|
|
543
|
+
const value = Number(match[1]);
|
|
544
|
+
return Number.isFinite(value) ? value : void 0;
|
|
545
|
+
}
|
|
546
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
547
|
+
0 && (module.exports = {
|
|
548
|
+
ChromiumNotFoundError,
|
|
549
|
+
DEFAULT_DEVICE_SCALE_FACTOR,
|
|
550
|
+
DEFAULT_FRAME_RATE,
|
|
551
|
+
captureFrameBuffers,
|
|
552
|
+
compileCompositionFile,
|
|
553
|
+
concatMp4Segments,
|
|
554
|
+
frameCountFor,
|
|
555
|
+
recordHtml,
|
|
556
|
+
render,
|
|
557
|
+
resolveChromiumExecutable
|
|
558
|
+
});
|