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