motionrender 0.2.2 → 0.2.4

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/core/src/index.ts
2
2
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
3
- import { dirname as dirname3, extname as extname3, join as join9, resolve as resolve3 } from "node:path";
3
+ import { dirname as dirname3, extname as extname3, join as join10, resolve as resolve3 } from "node:path";
4
4
 
5
5
  // packages/schema/src/index.ts
6
6
  import { z as z3 } from "zod";
@@ -865,7 +865,7 @@ var describeError = (error) => error instanceof z3.ZodError ? z3.prettifyError(e
865
865
  // packages/core/src/media.ts
866
866
  import { spawn as spawn2 } from "node:child_process";
867
867
  import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "node:fs/promises";
868
- import { join as join3 } from "node:path";
868
+ import { join as join4 } from "node:path";
869
869
  import { createHash } from "node:crypto";
870
870
 
871
871
  // packages/animation/src/index.ts
@@ -1222,11 +1222,143 @@ async function toDataUrl(source) {
1222
1222
  // packages/ffmpeg/src/index.ts
1223
1223
  import { spawn } from "node:child_process";
1224
1224
  import { once } from "node:events";
1225
+ import { join as join3 } from "node:path";
1226
+
1227
+ // packages/ffmpeg/src/auto-install.ts
1228
+ import { homedir } from "node:os";
1225
1229
  import { join as join2 } from "node:path";
1230
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, renameSync } from "node:fs";
1231
+ import { spawnSync } from "node:child_process";
1232
+ import { pipeline } from "node:stream/promises";
1233
+ import { Readable } from "node:stream";
1234
+ import { createGunzip } from "node:zlib";
1235
+ var isWin = process.platform === "win32";
1236
+ var binDir = join2(homedir(), ".motionrender", "bin");
1237
+ var localFfmpegPath = join2(binDir, isWin ? "ffmpeg.exe" : "ffmpeg");
1238
+ var localFfprobePath = join2(binDir, isWin ? "ffprobe.exe" : "ffprobe");
1239
+ var checkedSystemFfmpeg = false;
1240
+ var systemFfmpegAvailable = false;
1241
+ var checkedSystemFfprobe = false;
1242
+ var systemFfprobeAvailable = false;
1243
+ function hasSystemFfmpeg() {
1244
+ if (checkedSystemFfmpeg) return systemFfmpegAvailable;
1245
+ try {
1246
+ const res = spawnSync("ffmpeg", ["-version"], { stdio: "ignore" });
1247
+ systemFfmpegAvailable = res.status === 0;
1248
+ } catch {
1249
+ systemFfmpegAvailable = false;
1250
+ }
1251
+ checkedSystemFfmpeg = true;
1252
+ return systemFfmpegAvailable;
1253
+ }
1254
+ function hasSystemFfprobe() {
1255
+ if (checkedSystemFfprobe) return systemFfprobeAvailable;
1256
+ try {
1257
+ const res = spawnSync("ffprobe", ["-version"], { stdio: "ignore" });
1258
+ systemFfprobeAvailable = res.status === 0;
1259
+ } catch {
1260
+ systemFfprobeAvailable = false;
1261
+ }
1262
+ checkedSystemFfprobe = true;
1263
+ return systemFfprobeAvailable;
1264
+ }
1265
+ var getFfmpegPath = () => {
1266
+ if (process.env.OVE_FFMPEG_PATH) return process.env.OVE_FFMPEG_PATH;
1267
+ if (existsSync(localFfmpegPath)) return localFfmpegPath;
1268
+ if (hasSystemFfmpeg()) return "ffmpeg";
1269
+ return localFfmpegPath;
1270
+ };
1271
+ var getFfprobePath = () => {
1272
+ if (process.env.OVE_FFPROBE_PATH) return process.env.OVE_FFPROBE_PATH;
1273
+ if (existsSync(localFfprobePath)) return localFfprobePath;
1274
+ if (hasSystemFfprobe()) return "ffprobe";
1275
+ return localFfprobePath;
1276
+ };
1277
+ var RELEASE_TAG = "b6.1.1";
1278
+ var BASE_URL = `https://github.com/eugeneware/ffmpeg-static/releases/download/${RELEASE_TAG}`;
1279
+ function getPlatformArch() {
1280
+ const p = process.platform;
1281
+ const a = process.arch;
1282
+ if (p === "win32") return { platform: "win32", arch: "x64" };
1283
+ if (p === "darwin") return { platform: "darwin", arch: a === "arm64" ? "arm64" : "x64" };
1284
+ if (p === "linux") return { platform: "linux", arch: a === "arm64" ? "arm64" : a === "arm" ? "arm" : "x64" };
1285
+ return { platform: p, arch: a };
1286
+ }
1287
+ async function downloadAndExtract(name2, destPath, onProgress) {
1288
+ const { platform, arch } = getPlatformArch();
1289
+ const url = `${BASE_URL}/${name2}-${platform}-${arch}.gz`;
1290
+ if (onProgress) onProgress(`Downloading ${name2} (${platform}-${arch})...`);
1291
+ const res = await fetch(url, { redirect: "follow" });
1292
+ if (!res.ok || !res.body) {
1293
+ throw new Error(`Failed to download ${name2} from ${url} (HTTP ${res.status})`);
1294
+ }
1295
+ const tmpPath = `${destPath}.tmp-${Date.now()}`;
1296
+ try {
1297
+ const fileStream = createWriteStream(tmpPath);
1298
+ const nodeReadable = Readable.fromWeb(res.body);
1299
+ await pipeline(nodeReadable, createGunzip(), fileStream);
1300
+ if (!isWin) {
1301
+ chmodSync(tmpPath, 493);
1302
+ }
1303
+ if (existsSync(destPath)) {
1304
+ try {
1305
+ unlinkSync(destPath);
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ renameSync(tmpPath, destPath);
1310
+ } catch (err) {
1311
+ try {
1312
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
1313
+ } catch {
1314
+ }
1315
+ throw err;
1316
+ }
1317
+ }
1318
+ var ensureFfmpegPromise = null;
1319
+ async function ensureFfmpeg(options) {
1320
+ if (ensureFfmpegPromise) return ensureFfmpegPromise;
1321
+ ensureFfmpegPromise = (async () => {
1322
+ const ffmpegFromEnv = process.env.OVE_FFMPEG_PATH;
1323
+ const ffprobeFromEnv = process.env.OVE_FFPROBE_PATH;
1324
+ const hasLocalFfmpeg = existsSync(localFfmpegPath);
1325
+ const hasLocalFfprobe = existsSync(localFfprobePath);
1326
+ const hasSysFfmpeg = !ffmpegFromEnv && !hasLocalFfmpeg ? hasSystemFfmpeg() : false;
1327
+ const hasSysFfprobe = !ffprobeFromEnv && !hasLocalFfprobe ? hasSystemFfprobe() : false;
1328
+ const ffmpegReady = Boolean(ffmpegFromEnv || hasLocalFfmpeg || hasSysFfmpeg);
1329
+ const ffprobeReady = Boolean(ffprobeFromEnv || hasLocalFfprobe || hasSysFfprobe);
1330
+ if (ffmpegReady && ffprobeReady) {
1331
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1332
+ }
1333
+ if (!existsSync(binDir)) {
1334
+ mkdirSync(binDir, { recursive: true });
1335
+ }
1336
+ const log = options?.onProgress ?? ((msg) => console.log(msg));
1337
+ if (!ffmpegReady) {
1338
+ log("FFmpeg not found. Automatically downloading static build (one-time setup)...");
1339
+ await downloadAndExtract("ffmpeg", localFfmpegPath, log);
1340
+ log("\u2713 FFmpeg installed successfully.");
1341
+ }
1342
+ if (!ffprobeReady) {
1343
+ log("ffprobe not found. Automatically downloading static build (one-time setup)...");
1344
+ await downloadAndExtract("ffprobe", localFfprobePath, log);
1345
+ log("\u2713 ffprobe installed successfully.");
1346
+ }
1347
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1348
+ })();
1349
+ try {
1350
+ return await ensureFfmpegPromise;
1351
+ } catch (err) {
1352
+ ensureFfmpegPromise = null;
1353
+ throw err;
1354
+ }
1355
+ }
1356
+
1357
+ // packages/ffmpeg/src/index.ts
1226
1358
  var x264Presets = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
1227
1359
  var defaultPreset = "veryfast";
1228
- var ffmpegPath = () => process.env.OVE_FFMPEG_PATH || "ffmpeg";
1229
- var ffprobePath = () => process.env.OVE_FFPROBE_PATH || "ffprobe";
1360
+ var ffmpegPath = () => getFfmpegPath();
1361
+ var ffprobePath = () => getFfprobePath();
1230
1362
  var notFound = (tool, env) => new Error(`${tool} was not found. Install FFmpeg (it includes ${tool}) and put it on PATH, or set ${env} to the executable.`);
1231
1363
  var num2 = (n) => {
1232
1364
  const text = n.toFixed(5).replace(/\.?0+$/, "");
@@ -1336,12 +1468,14 @@ function concatSegments(segments, listFile, output, audio, options = {}) {
1336
1468
  const list = segments.map((s) => `file '${s.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`).join("\n");
1337
1469
  return import("node:fs/promises").then((fs) => fs.writeFile(listFile, list)).then(() => run(["-y", "-f", "concat", "-safe", "0", "-i", listFile, ...audioCodec ? audioMixArgs(audio, audioCodec) : [], ...audio && audioCodec ? [] : ["-map", "0:v"], "-c:v", "copy", ...options.format === "mp4" || options.format === void 0 ? ["-movflags", "+faststart"] : [], output], "ignore").done);
1338
1470
  }
1339
- function extractFrames(source, outDir, options) {
1471
+ async function extractFrames(source, outDir, options) {
1472
+ await ensureFfmpeg();
1340
1473
  const { fps, maxWidth, maxHeight, durationSeconds, startSeconds } = options;
1341
- return run(["-v", "error", "-y", ...startSeconds ? ["-ss", String(startSeconds)] : [], ...durationSeconds === void 0 ? [] : ["-t", String(durationSeconds)], "-i", source, "-an", "-vf", `fps=${fps},scale=w='min(iw,${maxWidth})':h='min(ih,${maxHeight})':force_original_aspect_ratio=decrease`, "-q:v", "2", join2(outDir, "%06d.jpg")], "ignore").done;
1474
+ return run(["-v", "error", "-y", ...startSeconds ? ["-ss", String(startSeconds)] : [], ...durationSeconds === void 0 ? [] : ["-t", String(durationSeconds)], "-i", source, "-an", "-vf", `fps=${fps},scale=w='min(iw,${maxWidth})':h='min(ih,${maxHeight})':force_original_aspect_ratio=decrease`, "-q:v", "2", join3(outDir, "%06d.jpg")], "ignore").done;
1342
1475
  }
1343
1476
  var requiredEncoders = { mp4: ["libx264", "aac"], "mp4 (h265)": ["libx265"], webm: ["libvpx-vp9", "libopus"], mov: ["prores_ks", "pcm_s16le"], gif: ["gif"], "video layers": ["mjpeg"] };
1344
- function checkFfmpeg() {
1477
+ async function checkFfmpeg() {
1478
+ await ensureFfmpeg();
1345
1479
  return new Promise((resolve4, reject) => {
1346
1480
  const child = spawn(ffmpegPath(), ["-hide_banner", "-encoders"]);
1347
1481
  let out = "";
@@ -1358,7 +1492,8 @@ function checkFfmpeg() {
1358
1492
  });
1359
1493
  });
1360
1494
  }
1361
- function hasAudioStream(path) {
1495
+ async function hasAudioStream(path) {
1496
+ await ensureFfmpeg();
1362
1497
  return new Promise((resolve4, reject) => {
1363
1498
  const child = spawn(ffprobePath(), ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", path]);
1364
1499
  let out = "";
@@ -1367,7 +1502,8 @@ function hasAudioStream(path) {
1367
1502
  child.on("close", (c) => c === 0 ? resolve4(out.trim().length > 0) : reject(new Error(`ffprobe could not read ${path}`)));
1368
1503
  });
1369
1504
  }
1370
- function probe(path) {
1505
+ async function probe(path) {
1506
+ await ensureFfmpeg();
1371
1507
  return new Promise((resolve4, reject) => {
1372
1508
  const child = spawn(ffprobePath(), ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path]);
1373
1509
  let out = "";
@@ -1391,7 +1527,7 @@ async function measureSource(source) {
1391
1527
  const version = isUrl(source) ? "" : await stat2(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1392
1528
  throw new Error(`Media not found: ${source}`);
1393
1529
  });
1394
- const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file = join3(mediaCache(), `${key}.json`);
1530
+ const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file = join4(mediaCache(), `${key}.json`);
1395
1531
  const cached2 = await readFile2(file, "utf8").then(JSON.parse, () => void 0);
1396
1532
  if (cached2) {
1397
1533
  void touchCacheEntry(file);
@@ -1466,7 +1602,7 @@ async function prepareComposition(raw, values = {}, baseDir = process.cwd()) {
1466
1602
 
1467
1603
  // packages/renderer/src/index.ts
1468
1604
  import { chromium } from "playwright";
1469
- import { existsSync as existsSync3 } from "node:fs";
1605
+ import { existsSync as existsSync4 } from "node:fs";
1470
1606
  import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1471
1607
  import { availableParallelism } from "node:os";
1472
1608
 
@@ -1558,18 +1694,18 @@ var prepareOperations = (operations, fps = 30) => {
1558
1694
 
1559
1695
  // packages/renderer/src/video.ts
1560
1696
  import { mkdir as mkdir2, readdir as readdir2, rename, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
1561
- import { join as join4 } from "node:path";
1697
+ import { join as join5 } from "node:path";
1562
1698
  import { createHash as createHash2, randomUUID } from "node:crypto";
1563
- var videoFrameFile = (video, index) => join4(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1699
+ var videoFrameFile = (video, index) => join5(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1564
1700
  var videoCacheRoot = () => cacheDir("video");
1565
1701
  var complete = ".complete";
1566
1702
  async function decode(source, key, first = 0) {
1567
1703
  const version = isUrl(source) ? "" : await stat3(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1568
1704
  throw new Error(`Video not found: ${source}`);
1569
1705
  });
1570
- const dir = join4(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1706
+ const dir = join5(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1571
1707
  const count = async () => (await readdir2(dir)).filter((f) => f.endsWith(".jpg")).length;
1572
- if (await stat3(join4(dir, complete)).then(() => true, () => false)) {
1708
+ if (await stat3(join5(dir, complete)).then(() => true, () => false)) {
1573
1709
  void touchCacheEntry(dir);
1574
1710
  return { dir, count: first + await count(), first };
1575
1711
  }
@@ -1577,9 +1713,9 @@ async function decode(source, key, first = 0) {
1577
1713
  await mkdir2(scratch, { recursive: true });
1578
1714
  try {
1579
1715
  await extractFrames(source, scratch, key);
1580
- await writeFile2(join4(scratch, complete), "");
1716
+ await writeFile2(join5(scratch, complete), "");
1581
1717
  await rename(scratch, dir).catch(async (error) => {
1582
- if (!await stat3(join4(dir, complete)).then(() => true, () => false)) throw error;
1718
+ if (!await stat3(join5(dir, complete)).then(() => true, () => false)) throw error;
1583
1719
  });
1584
1720
  } finally {
1585
1721
  await rm2(scratch, { recursive: true, force: true });
@@ -1670,34 +1806,34 @@ async function createPagePool(open, discard, size, timeoutMs = defaultFrameTimeo
1670
1806
  }
1671
1807
 
1672
1808
  // packages/graphics/src/fonts.ts
1673
- import { existsSync } from "node:fs";
1809
+ import { existsSync as existsSync2 } from "node:fs";
1674
1810
  import { readFile as readFile3 } from "node:fs/promises";
1675
1811
  import { createRequire } from "node:module";
1676
- import { dirname, join as join5 } from "node:path";
1812
+ import { dirname, join as join6 } from "node:path";
1677
1813
  import { fileURLToPath } from "node:url";
1678
1814
  function builtinFontDir() {
1679
1815
  const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
1680
- if (existsSync(join5(bundled, builtinFontFaces[0].file))) return bundled;
1681
- return join5(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1816
+ if (existsSync2(join6(bundled, builtinFontFaces[0].file))) return bundled;
1817
+ return join6(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1682
1818
  }
1683
1819
  var cached;
1684
- var builtinFonts = () => cached ??= Promise.all(builtinFontFaces.map(async (face) => ({ ...face, src: `builtin:${face.file}`, url: `data:font/woff2;base64,${(await readFile3(join5(builtinFontDir(), face.file))).toString("base64")}` })));
1820
+ var builtinFonts = () => cached ??= Promise.all(builtinFontFaces.map(async (face) => ({ ...face, src: `builtin:${face.file}`, url: `data:font/woff2;base64,${(await readFile3(join6(builtinFontDir(), face.file))).toString("base64")}` })));
1685
1821
 
1686
1822
  // packages/graphics/src/runtime.ts
1687
- import { existsSync as existsSync2, readFileSync } from "node:fs";
1823
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
1688
1824
  import { createRequire as createRequire2 } from "node:module";
1689
- import { join as join6 } from "node:path";
1825
+ import { join as join7 } from "node:path";
1690
1826
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1691
1827
  var runtimeDir = () => {
1692
1828
  const bundled = fileURLToPath2(new URL("./runtime/", import.meta.url));
1693
- return existsSync2(join6(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1829
+ return existsSync3(join7(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1694
1830
  };
1695
- var read = (name2) => readFileSync(join6(runtimeDir(), name2), "utf8");
1831
+ var read = (name2) => readFileSync(join7(runtimeDir(), name2), "utf8");
1696
1832
  var textLayoutSource = read("text-layout.js");
1697
1833
  var lottieFile = "lottie_light_canvas.min.js";
1698
1834
  var lottiePath = () => {
1699
- const bundled = join6(runtimeDir(), lottieFile);
1700
- return existsSync2(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1835
+ const bundled = join7(runtimeDir(), lottieFile);
1836
+ return existsSync3(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1701
1837
  };
1702
1838
  var lottieSource = readFileSync(lottiePath(), "utf8");
1703
1839
  var canvasRuntimeSource = `${textLayoutSource}
@@ -1716,25 +1852,68 @@ for(let i=0;i<images.length;i++){const img=new Image();img.src=images[i];await i
1716
1852
  var allLayers = (composition) => composition.scenes.flatMap((scene) => scene.layers);
1717
1853
  var browserArgs = ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", ...(process.env.OVE_CHROMIUM_ARGS ?? "").split(/\s+/).filter(Boolean)];
1718
1854
  var browserInstallHint = `Install one with "ove browser install" (Playwright's Chromium, about 150 MB), install Google Chrome, or set OVE_CHROMIUM_EXECUTABLE to a Chromium-based browser.`;
1855
+ var ensureChromiumPromise = null;
1856
+ async function ensureChromium(options) {
1857
+ if (existsSync4(chromium.executablePath())) return;
1858
+ for (const channel of ["chrome", "msedge"]) {
1859
+ try {
1860
+ const b = await chromium.launch({ headless: true, channel, args: browserArgs });
1861
+ await b.close();
1862
+ return;
1863
+ } catch {
1864
+ }
1865
+ }
1866
+ if (ensureChromiumPromise) return ensureChromiumPromise;
1867
+ ensureChromiumPromise = (async () => {
1868
+ const log = options?.onProgress ?? ((msg) => console.log(msg));
1869
+ log("No Chrome, Edge, or Chromium browser detected.");
1870
+ log("Downloading Playwright Chromium (one-time setup)...");
1871
+ const { createRequire: createRequire3 } = await import("node:module");
1872
+ const { dirname: dirname4, join: join11 } = await import("node:path");
1873
+ const { spawn: spawn4 } = await import("node:child_process");
1874
+ const req = createRequire3(import.meta.url);
1875
+ const cli = join11(dirname4(req.resolve("playwright/package.json")), "cli.js");
1876
+ await new Promise((resolve4, reject) => {
1877
+ const child = spawn4(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
1878
+ child.on("close", (code) => {
1879
+ if (code === 0) resolve4();
1880
+ else reject(new Error(`Chromium installation failed (exit code ${code})`));
1881
+ });
1882
+ child.on("error", reject);
1883
+ });
1884
+ log("\u2713 Playwright Chromium installed successfully.");
1885
+ })();
1886
+ try {
1887
+ await ensureChromiumPromise;
1888
+ } catch (err) {
1889
+ ensureChromiumPromise = null;
1890
+ throw err;
1891
+ }
1892
+ }
1719
1893
  async function launchBrowser() {
1720
1894
  const explicit = process.env.OVE_CHROMIUM_EXECUTABLE;
1721
1895
  if (explicit) {
1722
- if (!existsSync3(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
1896
+ if (!existsSync4(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
1723
1897
  return chromium.launch({ headless: true, executablePath: explicit, args: browserArgs });
1724
1898
  }
1725
- if (existsSync3(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
1899
+ if (existsSync4(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
1726
1900
  for (const channel of ["chrome", "msedge"]) {
1727
1901
  try {
1728
1902
  return await chromium.launch({ headless: true, channel, args: browserArgs });
1729
1903
  } catch {
1730
1904
  }
1731
1905
  }
1732
- throw new Error(`No Chromium-based browser was found. ${browserInstallHint}`);
1906
+ try {
1907
+ await ensureChromium();
1908
+ return await chromium.launch({ headless: true, args: browserArgs });
1909
+ } catch (error) {
1910
+ throw new Error(`No Chromium-based browser was found and auto-install failed: ${error.message}. ${browserInstallHint}`);
1911
+ }
1733
1912
  }
1734
1913
  async function describeBrowser() {
1735
1914
  const browser = await launchBrowser();
1736
1915
  try {
1737
- return `${browser.browserType().name()} ${browser.version()}${process.env.OVE_CHROMIUM_EXECUTABLE ? ` (OVE_CHROMIUM_EXECUTABLE=${process.env.OVE_CHROMIUM_EXECUTABLE})` : existsSync3(chromium.executablePath()) ? " (Playwright Chromium)" : " (installed Chrome/Edge)"}`;
1916
+ return `${browser.browserType().name()} ${browser.version()}${process.env.OVE_CHROMIUM_EXECUTABLE ? ` (OVE_CHROMIUM_EXECUTABLE=${process.env.OVE_CHROMIUM_EXECUTABLE})` : existsSync4(chromium.executablePath()) ? " (Playwright Chromium)" : " (installed Chrome/Edge)"}`;
1738
1917
  } finally {
1739
1918
  await browser.close();
1740
1919
  }
@@ -1864,10 +2043,10 @@ async function* renderFrames(renderer, from, to, options = {}) {
1864
2043
  import { stat as stat4 } from "node:fs/promises";
1865
2044
  import { spawn as spawn3 } from "node:child_process";
1866
2045
  import { mkdtemp, rm as rm3, mkdir as mkdir3, rename as rename2 } from "node:fs/promises";
1867
- import { existsSync as existsSync4 } from "node:fs";
2046
+ import { existsSync as existsSync5 } from "node:fs";
1868
2047
  import { createHash as createHash3 } from "node:crypto";
1869
2048
  import { tmpdir as tmpdir2 } from "node:os";
1870
- import { join as join7 } from "node:path";
2049
+ import { join as join8 } from "node:path";
1871
2050
  var runFfmpeg = (args) => new Promise((resolve4, reject) => {
1872
2051
  const child = spawn3(ffmpegPath(), args, { stdio: ["ignore", "ignore", "pipe"] });
1873
2052
  let err = "";
@@ -1878,8 +2057,8 @@ var runFfmpeg = (args) => new Promise((resolve4, reject) => {
1878
2057
  var sfxDir = () => cacheDir("sfx");
1879
2058
  var making = /* @__PURE__ */ new Map();
1880
2059
  function sfxFile(name2) {
1881
- const file = join7(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
1882
- if (existsSync4(file)) {
2060
+ const file = join8(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
2061
+ if (existsSync5(file)) {
1883
2062
  void touchCacheEntry(file);
1884
2063
  return Promise.resolve(file);
1885
2064
  }
@@ -1933,7 +2112,7 @@ async function planAudio(composition, baseDir, probe2 = hasAudioStream) {
1933
2112
  }
1934
2113
  async function measureMix(mix) {
1935
2114
  if (!mix.clips.length) return void 0;
1936
- const dir = await mkdtemp(join7(tmpdir2(), "ove-loudness-")), wav = join7(dir, "mix.wav");
2115
+ const dir = await mkdtemp(join8(tmpdir2(), "ove-loudness-")), wav = join8(dir, "mix.wav");
1937
2116
  try {
1938
2117
  const args = audioMixArgs(mix, ["-c:a", "pcm_s16le"]), video = args.indexOf("0:v");
1939
2118
  if (video > 0) args.splice(video - 1, 2);
@@ -2122,7 +2301,7 @@ async function reviewComposition(composition, options = {}) {
2122
2301
  // packages/core/src/distributed.ts
2123
2302
  import { mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
2124
2303
  import { tmpdir as tmpdir3 } from "node:os";
2125
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
2304
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "node:path";
2126
2305
  import { timingSafeEqual } from "node:crypto";
2127
2306
  function planChunks(first, last, chunkSize) {
2128
2307
  if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new Error("Chunk size must be a positive whole number of frames");
@@ -2146,7 +2325,7 @@ async function renderDistributed(composition, options) {
2146
2325
  if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last < first || last >= composition.durationFrames) throw new Error(`Frame range must be within 0\u2013${composition.durationFrames - 1}`);
2147
2326
  const chunkSettings = { format, codec: settings.codec, transparent: settings.transparent, preset: options.preset ?? defaultPreset, crf: settings.crf, bitrate: settings.bitrate, scale: settings.scale };
2148
2327
  const jobs = planChunks(first, last, options.chunkSize ?? Math.max(30, Math.ceil((last - first + 1) / (options.workers.length * 2)))), total = last - first + 1, retries = options.retries ?? 2;
2149
- const work = await mkdtemp2(join8(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2328
+ const work = await mkdtemp2(join9(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2150
2329
  const abort = new AbortController();
2151
2330
  const onOuterAbort = () => abort.abort(options.signal?.reason);
2152
2331
  options.signal?.addEventListener("abort", onOuterAbort);
@@ -2160,7 +2339,7 @@ async function renderDistributed(composition, options) {
2160
2339
  const job = queue.shift();
2161
2340
  try {
2162
2341
  const bytes = await worker.renderChunk(target, job, chunkSettings, abort.signal);
2163
- const file = join8(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2342
+ const file = join9(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2164
2343
  await writeFile4(file, bytes);
2165
2344
  segments[job.index] = file;
2166
2345
  failures = 0;
@@ -2193,7 +2372,7 @@ async function renderDistributed(composition, options) {
2193
2372
  if (segments.some((s) => !s)) throw new Error("Some chunks were not rendered");
2194
2373
  await mkdir4(dirname2(output), { recursive: true });
2195
2374
  const audio = await masterAudio(target, baseDir).then((mix) => mix && { ...mix, durationSeconds: total / composition.fps, offsetSeconds: first / composition.fps });
2196
- await concatSegments(segments, join8(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2375
+ await concatSegments(segments, join9(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2197
2376
  return { output, format, frames: total, durationSeconds: Number(await probe(output)) || total / composition.fps };
2198
2377
  } finally {
2199
2378
  options.signal?.removeEventListener("abort", onOuterAbort);
@@ -2226,6 +2405,7 @@ function checkRange(composition, range) {
2226
2405
  return [first, last];
2227
2406
  }
2228
2407
  async function renderMedia(composition, options) {
2408
+ await ensureFfmpeg();
2229
2409
  const baseDir = options.baseDir ?? process.cwd(), output = resolve3(options.output);
2230
2410
  const format = options.format ?? formatForOutput(output, composition.export.format);
2231
2411
  const settings = exportSchema.parse({ ...composition.export, ...Object.fromEntries(Object.entries({ crf: options.crf, bitrate: options.bitrate, audioBitrate: options.audioBitrate, scale: options.scale, loudness: options.loudness }).filter(([, v]) => v !== void 0)), format, codec: options.codec ?? (format === "mp4" ? composition.export.codec : "h264"), transparent: options.transparent ?? composition.export.transparent });
@@ -2244,7 +2424,7 @@ async function renderMedia(composition, options) {
2244
2424
  for await (const png of renderFrames(renderer, first, last + 1)) {
2245
2425
  throwIfAborted();
2246
2426
  if (encoder) await encoder.write(png);
2247
- else await writeFile5(join9(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2427
+ else await writeFile5(join10(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2248
2428
  frame++;
2249
2429
  options.onProgress?.({ frame: frame - 1, rendered: frame - first, total });
2250
2430
  }
@@ -2310,6 +2490,8 @@ export {
2310
2490
  dropFillerWords,
2311
2491
  duckSchema,
2312
2492
  easingSchema,
2493
+ ensureChromium,
2494
+ ensureFfmpeg,
2313
2495
  evaluateFrame,
2314
2496
  expandMattes,
2315
2497
  expandSceneGrades,
@@ -56,6 +56,8 @@ export declare function composeSmart(provider: AiProvider, options: ComposeOptio
56
56
  full?: boolean;
57
57
  noPlan?: boolean;
58
58
  registry?: Registry;
59
+ theme?: string;
60
+ template?: string;
59
61
  }): Promise<ComposeResult & {
60
62
  template?: string;
61
63
  source?: Record<string, unknown>;
@@ -8,8 +8,8 @@ export { cacheRoot, cacheUsage, cleanCache, type CacheUsage } from '../../assets
8
8
  export { reviewComposition, type ReviewIssue, type ReviewResult, type ReviewOptions } from './review.js';
9
9
  export { measureMedia, attachCaptions, prepareComposition, captionTrack } from './media.js';
10
10
  export * from './distributed.js';
11
- export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
12
- export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, type X264Preset } from '../../ffmpeg/src/index.js';
11
+ export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, ensureChromium, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
12
+ export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, ensureFfmpeg, type X264Preset } from '../../ffmpeg/src/index.js';
13
13
  export type OutputFormat = typeof outputFormats[number];
14
14
  export type LoadedComposition = {
15
15
  composition: Composition;
@@ -0,0 +1,12 @@
1
+ export declare const localFfmpegPath: string;
2
+ export declare const localFfprobePath: string;
3
+ export declare function hasSystemFfmpeg(): boolean;
4
+ export declare function hasSystemFfprobe(): boolean;
5
+ export declare const getFfmpegPath: () => string;
6
+ export declare const getFfprobePath: () => string;
7
+ export declare function ensureFfmpeg(options?: {
8
+ onProgress?: (msg: string) => void;
9
+ }): Promise<{
10
+ ffmpeg: string;
11
+ ffprobe: string;
12
+ }>;
@@ -1,3 +1,4 @@
1
+ export { ensureFfmpeg, hasSystemFfmpeg, hasSystemFfprobe, localFfmpegPath, localFfprobePath } from './auto-install.js';
1
2
  export declare const x264Presets: readonly ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
2
3
  export type X264Preset = typeof x264Presets[number];
3
4
  export type EncodeOptions = {
@@ -30,6 +30,9 @@ export interface JobPaths {
30
30
  export type JobCapabilities = {
31
31
  composeSmart(provider: AiProvider, options: ComposeOptions, mode: {
32
32
  full?: boolean;
33
+ noPlan?: boolean;
34
+ theme?: string;
35
+ template?: string;
33
36
  }): Promise<ComposeResult & {
34
37
  template?: string;
35
38
  source?: Record<string, unknown>;
@@ -26,6 +26,9 @@ export declare const scaledSize: (composition: Composition, scale?: number) => {
26
26
  height: number;
27
27
  };
28
28
  export declare const browserInstallHint = "Install one with \"ove browser install\" (Playwright's Chromium, about 150 MB), install Google Chrome, or set OVE_CHROMIUM_EXECUTABLE to a Chromium-based browser.";
29
+ export declare function ensureChromium(options?: {
30
+ onProgress?: (msg: string) => void;
31
+ }): Promise<void>;
29
32
  export declare function launchBrowser(): Promise<Browser>;
30
33
  export declare function describeBrowser(): Promise<string>;
31
34
  export declare const defaultConcurrency: () => number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motionrender",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "JSON-first, frame-deterministic motion graphics video engine: render compositions to MP4, WebM, ProRes, GIF or PNG frames from Node or the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",