reframe-video 0.6.4 → 0.6.6

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.
@@ -341,7 +341,8 @@
341
341
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
342
342
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
343
343
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
344
- image: [...COMMON_PROPS, "src", "width", "height"],
344
+ image: [...COMMON_PROPS, "src", "width", "height", "fit"],
345
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
345
346
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
346
347
  group: COMMON_PROPS
347
348
  };
@@ -780,6 +781,34 @@
780
781
  height,
781
782
  offsetX: -width * ax,
782
783
  offsetY: -height * ay,
784
+ ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
785
+ ...fx,
786
+ ...clipSpread
787
+ });
788
+ return;
789
+ }
790
+ case "video": {
791
+ const width = num(id, "width", node.props.width);
792
+ const height = num(id, "height", node.props.height);
793
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
794
+ const fps = compiled2.ir.fps ?? 30;
795
+ const start = node.props.start ?? 0;
796
+ const rate = node.props.rate ?? 1;
797
+ const clipStart = node.props.clipStart ?? 0;
798
+ const srcT = clipStart + Math.max(0, t - start) * rate;
799
+ const frame = Math.max(0, Math.round(srcT * fps));
800
+ ops.push({
801
+ type: "video",
802
+ id,
803
+ transform: matrix,
804
+ opacity,
805
+ src: str(id, "src", node.props.src),
806
+ width,
807
+ height,
808
+ offsetX: -width * ax,
809
+ offsetY: -height * ay,
810
+ frame,
811
+ ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
783
812
  ...fx,
784
813
  ...clipSpread
785
814
  });
@@ -878,7 +907,7 @@
878
907
  for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
879
908
  return g;
880
909
  }
881
- function renderFrame(ctx2, compiled2, t, images2) {
910
+ function renderFrame(ctx2, compiled2, t, images2, videos2) {
882
911
  const { size, background } = compiled2.ir;
883
912
  ctx2.setTransform(1, 0, 0, 1, 0, 0);
884
913
  ctx2.clearRect(0, 0, size.width, size.height);
@@ -886,9 +915,9 @@
886
915
  ctx2.fillStyle = background;
887
916
  ctx2.fillRect(0, 0, size.width, size.height);
888
917
  }
889
- drawDisplayList(ctx2, evaluate(compiled2, t), images2);
918
+ drawDisplayList(ctx2, evaluate(compiled2, t), images2, videos2);
890
919
  }
891
- function drawDisplayList(ctx2, ops, images2) {
920
+ function drawDisplayList(ctx2, ops, images2, videos2) {
892
921
  for (const op of ops) {
893
922
  ctx2.save();
894
923
  if (op.clips) {
@@ -970,22 +999,11 @@
970
999
  break;
971
1000
  }
972
1001
  case "image": {
973
- const img = images2?.get(op.src);
974
- if (img) {
975
- ctx2.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
976
- } else {
977
- ctx2.fillStyle = "#2A2A30";
978
- ctx2.fillRect(op.offsetX, op.offsetY, op.width, op.height);
979
- ctx2.strokeStyle = "#FF00FF";
980
- ctx2.lineWidth = 2;
981
- ctx2.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
982
- ctx2.beginPath();
983
- ctx2.moveTo(op.offsetX, op.offsetY);
984
- ctx2.lineTo(op.offsetX + op.width, op.offsetY + op.height);
985
- ctx2.moveTo(op.offsetX + op.width, op.offsetY);
986
- ctx2.lineTo(op.offsetX, op.offsetY + op.height);
987
- ctx2.stroke();
988
- }
1002
+ drawRaster(ctx2, images2?.get(op.src), op);
1003
+ break;
1004
+ }
1005
+ case "video": {
1006
+ drawRaster(ctx2, videos2?.frame(op.src, op.frame), op);
989
1007
  break;
990
1008
  }
991
1009
  case "path": {
@@ -1029,6 +1047,40 @@
1029
1047
  function mapBlend(blend) {
1030
1048
  return blend === "add" ? "lighter" : blend;
1031
1049
  }
1050
+ function coverRect(iw, ih, dw, dh) {
1051
+ if (iw <= 0 || ih <= 0 || dw <= 0 || dh <= 0) return { sx: 0, sy: 0, sw: iw, sh: ih };
1052
+ const s = Math.max(dw / iw, dh / ih);
1053
+ const sw = dw / s;
1054
+ const sh = dh / s;
1055
+ return { sx: (iw - sw) / 2, sy: (ih - sh) / 2, sw, sh };
1056
+ }
1057
+ function intrinsicSize(img) {
1058
+ const a = img;
1059
+ return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
1060
+ }
1061
+ function drawRaster(ctx2, img, op) {
1062
+ if (img) {
1063
+ if (op.fit === "cover") {
1064
+ const [iw, ih] = intrinsicSize(img);
1065
+ const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
1066
+ ctx2.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
1067
+ } else {
1068
+ ctx2.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
1069
+ }
1070
+ return;
1071
+ }
1072
+ ctx2.fillStyle = "#2A2A30";
1073
+ ctx2.fillRect(op.offsetX, op.offsetY, op.width, op.height);
1074
+ ctx2.strokeStyle = "#FF00FF";
1075
+ ctx2.lineWidth = 2;
1076
+ ctx2.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
1077
+ ctx2.beginPath();
1078
+ ctx2.moveTo(op.offsetX, op.offsetY);
1079
+ ctx2.lineTo(op.offsetX + op.width, op.offsetY + op.height);
1080
+ ctx2.moveTo(op.offsetX + op.width, op.offsetY);
1081
+ ctx2.lineTo(op.offsetX, op.offsetY + op.height);
1082
+ ctx2.stroke();
1083
+ }
1032
1084
  function quoteFamily(family) {
1033
1085
  return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
1034
1086
  }
@@ -1051,9 +1103,22 @@
1051
1103
  var ctx = null;
1052
1104
  var canvas = null;
1053
1105
  var images = /* @__PURE__ */ new Map();
1106
+ var videoFrames = /* @__PURE__ */ new Map();
1107
+ var decode = (dataUrl) => {
1108
+ const img = new Image();
1109
+ img.src = dataUrl;
1110
+ return img.decode().then(() => img);
1111
+ };
1112
+ var videos = {
1113
+ frame(src, index) {
1114
+ const frames = videoFrames.get(src);
1115
+ if (!frames || frames.length === 0) return void 0;
1116
+ return frames[Math.max(0, Math.min(index, frames.length - 1))];
1117
+ }
1118
+ };
1054
1119
  window.__reframe = {
1055
- // fully decode every image before the first frame — renderFrame is sync
1056
- async init(ir, assets = {}) {
1120
+ // fully decode every image/video frame before the first frame — renderFrame is sync
1121
+ async init(ir, assets = {}, videoAssets = {}) {
1057
1122
  compiled = compileScene(ir);
1058
1123
  canvas = document.createElement("canvas");
1059
1124
  canvas.width = ir.size.width;
@@ -1061,19 +1126,19 @@
1061
1126
  document.body.appendChild(canvas);
1062
1127
  ctx = canvas.getContext("2d", { willReadFrequently: true });
1063
1128
  if (!ctx) throw new Error("could not create 2d context");
1064
- await Promise.all(
1065
- Object.entries(assets).map(async ([src, dataUrl]) => {
1066
- const img = new Image();
1067
- img.src = dataUrl;
1068
- await img.decode();
1069
- images.set(src, img);
1129
+ await Promise.all([
1130
+ ...Object.entries(assets).map(async ([src, dataUrl]) => {
1131
+ images.set(src, await decode(dataUrl));
1132
+ }),
1133
+ ...Object.entries(videoAssets).map(async ([src, frames]) => {
1134
+ videoFrames.set(src, await Promise.all(frames.map(decode)));
1070
1135
  })
1071
- );
1136
+ ]);
1072
1137
  return { duration: compiled.duration, fps: ir.fps ?? 30 };
1073
1138
  },
1074
1139
  renderFrame(t) {
1075
1140
  if (!compiled || !ctx || !canvas) throw new Error("init() not called");
1076
- renderFrame(ctx, compiled, t, images);
1141
+ renderFrame(ctx, compiled, t, images, videos);
1077
1142
  return canvas.toDataURL("image/png");
1078
1143
  }
1079
1144
  };
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  // ../render-cli/src/cli.ts
4
- import { mkdtemp as mkdtemp3, readFile as readFile4, rm as rm3 } from "node:fs/promises";
5
- import { tmpdir as tmpdir4 } from "node:os";
6
- import { basename, dirname as dirname7, join as join6, resolve as resolve4 } from "node:path";
4
+ import { mkdtemp as mkdtemp4, readFile as readFile5, rm as rm4 } from "node:fs/promises";
5
+ import { tmpdir as tmpdir5 } from "node:os";
6
+ import { basename, dirname as dirname7, join as join7, resolve as resolve5 } from "node:path";
7
7
 
8
8
  // ../core/src/ir.ts
9
9
  var DEFAULT_CROSSFADE = 0.5;
@@ -338,6 +338,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
338
338
  "hard-light",
339
339
  "difference"
340
340
  ]);
341
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
341
342
  var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
342
343
  var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
343
344
  var PROPS_BY_TYPE = {
@@ -345,7 +346,8 @@ var PROPS_BY_TYPE = {
345
346
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
346
347
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
347
348
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
348
- image: [...COMMON_PROPS, "src", "width", "height"],
349
+ image: [...COMMON_PROPS, "src", "width", "height", "fit"],
350
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
349
351
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
350
352
  group: COMMON_PROPS
351
353
  };
@@ -392,6 +394,7 @@ function validateScene(ir) {
392
394
  if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
393
395
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
394
396
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
397
+ if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
395
398
  if (node.type === "group") {
396
399
  const clip = node.props.clip;
397
400
  if (clip) {
@@ -1042,13 +1045,13 @@ var EASE_TABLE = {
1042
1045
  var EASE_NAMES = Object.keys(EASE_TABLE);
1043
1046
 
1044
1047
  // ../core/src/assets.ts
1045
- function collectImageSrcs(ir) {
1048
+ function collectSrcs(ir, type) {
1046
1049
  const srcs = /* @__PURE__ */ new Set();
1047
- const imageIds = /* @__PURE__ */ new Set();
1050
+ const ids = /* @__PURE__ */ new Set();
1048
1051
  const walkNodes = (nodes) => {
1049
1052
  for (const node of nodes) {
1050
- if (node.type === "image") {
1051
- imageIds.add(node.id);
1053
+ if (node.type === type) {
1054
+ ids.add(node.id);
1052
1055
  srcs.add(node.props.src);
1053
1056
  }
1054
1057
  if (node.type === "group") walkNodes(node.children);
@@ -1057,14 +1060,14 @@ function collectImageSrcs(ir) {
1057
1060
  walkNodes(ir.nodes);
1058
1061
  for (const overrides of Object.values(ir.states ?? {})) {
1059
1062
  for (const [nodeId, props] of Object.entries(overrides)) {
1060
- if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
1063
+ if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
1061
1064
  }
1062
1065
  }
1063
1066
  const walkTimeline = (step) => {
1064
1067
  if (!step) return;
1065
1068
  if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
1066
1069
  for (const child of step.children) walkTimeline(child);
1067
- } else if (step.kind === "tween" && imageIds.has(step.target)) {
1070
+ } else if (step.kind === "tween" && ids.has(step.target)) {
1068
1071
  const src = step.props.src;
1069
1072
  if (typeof src === "string") srcs.add(src);
1070
1073
  }
@@ -1072,6 +1075,12 @@ function collectImageSrcs(ir) {
1072
1075
  walkTimeline(ir.timeline);
1073
1076
  return [...srcs];
1074
1077
  }
1078
+ function collectImageSrcs(ir) {
1079
+ return collectSrcs(ir, "image");
1080
+ }
1081
+ function collectVideoSrcs(ir) {
1082
+ return collectSrcs(ir, "video");
1083
+ }
1075
1084
 
1076
1085
  // ../render-cli/src/audio/index.ts
1077
1086
  import { dirname as dirname2 } from "node:path";
@@ -1392,10 +1401,10 @@ async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
1392
1401
  }
1393
1402
 
1394
1403
  // ../render-cli/src/composition.ts
1395
- import { spawn as spawn3 } from "node:child_process";
1396
- import { copyFile, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1397
- import { tmpdir as tmpdir3 } from "node:os";
1398
- import { dirname as dirname5, join as join5 } from "node:path";
1404
+ import { spawn as spawn4 } from "node:child_process";
1405
+ import { copyFile, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
1406
+ import { tmpdir as tmpdir4 } from "node:os";
1407
+ import { dirname as dirname5, join as join6 } from "node:path";
1399
1408
 
1400
1409
  // ../render-cli/src/encode.ts
1401
1410
  import { spawn as spawn2 } from "node:child_process";
@@ -1418,12 +1427,12 @@ async function encodeMp4(framesDir, fps, outFile) {
1418
1427
  "+faststart",
1419
1428
  outFile
1420
1429
  ];
1421
- await new Promise((resolve5, reject) => {
1430
+ await new Promise((resolve6, reject) => {
1422
1431
  const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1423
1432
  let stderr = "";
1424
1433
  proc.stderr.on("data", (d) => stderr += d.toString());
1425
1434
  proc.on("close", (code) => {
1426
- if (code === 0) resolve5();
1435
+ if (code === 0) resolve6();
1427
1436
  else reject(new Error(`ffmpeg exited with ${code}:
1428
1437
  ${stderr.slice(-2e3)}`));
1429
1438
  });
@@ -1433,7 +1442,7 @@ ${stderr.slice(-2e3)}`));
1433
1442
 
1434
1443
  // ../render-cli/src/frameLoop.ts
1435
1444
  import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
1436
- import { join as join4, dirname as dirname4 } from "node:path";
1445
+ import { join as join5, dirname as dirname4 } from "node:path";
1437
1446
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
1438
1447
  import { build } from "esbuild";
1439
1448
  import { chromium } from "playwright";
@@ -1494,6 +1503,96 @@ async function buildImageAssets(ir, sceneDir) {
1494
1503
  return assets;
1495
1504
  }
1496
1505
 
1506
+ // ../render-cli/src/videos.ts
1507
+ import { spawn as spawn3 } from "node:child_process";
1508
+ import { mkdtemp as mkdtemp2, readFile as readFile3, readdir, rm as rm2 } from "node:fs/promises";
1509
+ import { existsSync as existsSync3 } from "node:fs";
1510
+ import { tmpdir as tmpdir3 } from "node:os";
1511
+ import { extname as extname2, isAbsolute as isAbsolute3, join as join4, resolve as resolve3 } from "node:path";
1512
+ var VIDEO_EXT = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".m4v", ".mkv"]);
1513
+ function runFfmpeg(args) {
1514
+ return new Promise((resolve6, reject) => {
1515
+ const proc = spawn3("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1516
+ let stderr = "";
1517
+ proc.stderr.on("data", (d) => stderr += d.toString());
1518
+ proc.on(
1519
+ "close",
1520
+ (code) => code === 0 ? resolve6() : reject(new Error(`ffmpeg exited ${code}:
1521
+ ${stderr.slice(-2e3)}`))
1522
+ );
1523
+ proc.on("error", reject);
1524
+ });
1525
+ }
1526
+ function neededSeconds(node, duration) {
1527
+ const start = node.props.start ?? 0;
1528
+ const rate = node.props.rate ?? 1;
1529
+ const clipStart = node.props.clipStart ?? 0;
1530
+ return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
1531
+ }
1532
+ function videoNodes(ir) {
1533
+ const out = [];
1534
+ const walk = (nodes) => {
1535
+ for (const n of nodes) {
1536
+ if (n.type === "video") out.push(n);
1537
+ if (n.type === "group") walk(n.children);
1538
+ }
1539
+ };
1540
+ walk(ir.nodes);
1541
+ return out;
1542
+ }
1543
+ async function buildVideoFrameAssets(ir, sceneDir, fps, duration) {
1544
+ const srcs = collectVideoSrcs(ir);
1545
+ if (srcs.length === 0) return {};
1546
+ const nodes = videoNodes(ir);
1547
+ const reachBySrc = /* @__PURE__ */ new Map();
1548
+ for (const n of nodes) {
1549
+ const reach = neededSeconds(n, duration);
1550
+ reachBySrc.set(n.props.src, Math.max(reachBySrc.get(n.props.src) ?? 0, reach));
1551
+ }
1552
+ const assets = {};
1553
+ for (const src of srcs) {
1554
+ if (!VIDEO_EXT.has(extname2(src).toLowerCase())) {
1555
+ throw new Error(
1556
+ `video "${src}": unsupported format "${extname2(src)}" \u2014 supported: ${[...VIDEO_EXT].join(" ")}`
1557
+ );
1558
+ }
1559
+ const candidates = [isAbsolute3(src) ? src : null, resolve3(sceneDir, src)].filter(
1560
+ (c) => c !== null
1561
+ );
1562
+ const found = candidates.find((c) => existsSync3(c));
1563
+ if (!found) throw new Error(`video "${src}" not found (tried: ${candidates.join(", ")})`);
1564
+ const dir = await mkdtemp2(join4(tmpdir3(), "reframe-vframes-"));
1565
+ try {
1566
+ const seconds = Math.max(1 / fps, reachBySrc.get(src) ?? duration);
1567
+ await runFfmpeg([
1568
+ "-y",
1569
+ "-i",
1570
+ found,
1571
+ "-t",
1572
+ seconds.toFixed(3),
1573
+ "-vf",
1574
+ `fps=${fps},scale='min(iw,1280)':-2`,
1575
+ "-q:v",
1576
+ "4",
1577
+ join4(dir, "%05d.jpg")
1578
+ ]);
1579
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".jpg")).sort();
1580
+ assets[src] = await Promise.all(
1581
+ files.map(async (f) => `data:image/jpeg;base64,${(await readFile3(join4(dir, f))).toString("base64")}`)
1582
+ );
1583
+ if (assets[src].length === 0) throw new Error(`video "${src}": ffmpeg extracted no frames`);
1584
+ } finally {
1585
+ await rm2(dir, { recursive: true, force: true });
1586
+ }
1587
+ }
1588
+ return assets;
1589
+ }
1590
+ function resolveTiming(ir, opts) {
1591
+ const fps = opts.fps ?? ir.fps ?? 30;
1592
+ const duration = opts.duration ?? compileScene(ir).duration;
1593
+ return { fps, duration };
1594
+ }
1595
+
1497
1596
  // ../render-cli/src/vclock.ts
1498
1597
  var VCLOCK_SOURCE = String.raw`
1499
1598
  (() => {
@@ -1567,7 +1666,7 @@ async function injectFonts(page) {
1567
1666
  await document.fonts.ready;
1568
1667
  });
1569
1668
  }
1570
- var framePath = (dir, i) => join4(dir, `${String(i).padStart(5, "0")}.png`);
1669
+ var framePath = (dir, i) => join5(dir, `${String(i).padStart(5, "0")}.png`);
1571
1670
  async function withPage(size, fn) {
1572
1671
  const browser = await chromium.launch({
1573
1672
  args: ["--force-color-profile=srgb", "--font-render-hinting=none"]
@@ -1583,14 +1682,14 @@ var bundleCache = null;
1583
1682
  async function browserBundle() {
1584
1683
  if (bundleCache) return bundleCache;
1585
1684
  if (true) {
1586
- const { readFile: readFile5 } = await import("node:fs/promises");
1587
- bundleCache = await readFile5(
1588
- join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1685
+ const { readFile: readFile6 } = await import("node:fs/promises");
1686
+ bundleCache = await readFile6(
1687
+ join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1589
1688
  "utf8"
1590
1689
  );
1591
1690
  return bundleCache;
1592
1691
  }
1593
- const entry = join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
1692
+ const entry = join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
1594
1693
  const result = await build({
1595
1694
  entryPoints: [entry],
1596
1695
  bundle: true,
@@ -1603,7 +1702,10 @@ async function browserBundle() {
1603
1702
  }
1604
1703
  async function captureIr(ir, opts) {
1605
1704
  await mkdir2(opts.framesDir, { recursive: true });
1606
- const assets = await buildImageAssets(ir, opts.sceneDir ?? process.cwd());
1705
+ const sceneDir = opts.sceneDir ?? process.cwd();
1706
+ const assets = await buildImageAssets(ir, sceneDir);
1707
+ const { fps, duration } = resolveTiming(ir, opts);
1708
+ const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
1607
1709
  const bundle = await browserBundle();
1608
1710
  return withPage(ir.size, async (page) => {
1609
1711
  await page.setContent(
@@ -1611,12 +1713,10 @@ async function captureIr(ir, opts) {
1611
1713
  );
1612
1714
  await injectFonts(page);
1613
1715
  await page.addScriptTag({ content: bundle });
1614
- const info = await page.evaluate(
1615
- ([sceneIr, imageAssets]) => window.__reframe.init(sceneIr, imageAssets),
1616
- [ir, assets]
1716
+ await page.evaluate(
1717
+ ([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
1718
+ [ir, assets, videoAssets]
1617
1719
  );
1618
- const fps = opts.fps ?? info.fps;
1619
- const duration = opts.duration ?? info.duration;
1620
1720
  const frameCount = Math.max(1, Math.round(duration * fps));
1621
1721
  for (let f = 0; f < frameCount; f++) {
1622
1722
  const dataUrl = await page.evaluate((t) => window.__reframe.renderFrame(t), f / fps);
@@ -1649,25 +1749,25 @@ async function captureHtml(htmlPath, opts) {
1649
1749
  }
1650
1750
 
1651
1751
  // ../render-cli/src/composition.ts
1652
- function runFfmpeg(args) {
1653
- return new Promise((resolve5, reject) => {
1654
- const proc = spawn3("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1752
+ function runFfmpeg2(args) {
1753
+ return new Promise((resolve6, reject) => {
1754
+ const proc = spawn4("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1655
1755
  let stderr = "";
1656
1756
  proc.stderr.on("data", (d) => stderr += d.toString());
1657
- proc.on("close", (code) => code === 0 ? resolve5() : reject(new Error(`ffmpeg exited ${code}:
1757
+ proc.on("close", (code) => code === 0 ? resolve6() : reject(new Error(`ffmpeg exited ${code}:
1658
1758
  ${stderr.slice(-2e3)}`)));
1659
1759
  proc.on("error", reject);
1660
1760
  });
1661
1761
  }
1662
1762
  var sanitize = (id) => id.replace(/[^a-z0-9_-]/gi, "_");
1663
1763
  async function renderSceneVideo(scene, sceneDir, fps, out) {
1664
- const framesDir = await mkdtemp2(join5(tmpdir3(), "reframe-frames-"));
1764
+ const framesDir = await mkdtemp3(join6(tmpdir4(), "reframe-frames-"));
1665
1765
  try {
1666
1766
  const result = await captureIr(scene, { framesDir, sceneDir, ...fps !== void 0 && { fps } });
1667
1767
  await encodeMp4(result.framesDir, result.fps, out);
1668
1768
  return { fps: result.fps, frameCount: result.frameCount };
1669
1769
  } finally {
1670
- await rm2(framesDir, { recursive: true, force: true });
1770
+ await rm3(framesDir, { recursive: true, force: true });
1671
1771
  }
1672
1772
  }
1673
1773
  async function renderStandaloneScene(scene, sceneDir, fps, noAudio, out) {
@@ -1675,8 +1775,8 @@ async function renderStandaloneScene(scene, sceneDir, fps, noAudio, out) {
1675
1775
  if (plan) {
1676
1776
  const videoOut = `${out}.video.mp4`;
1677
1777
  await renderSceneVideo(scene, sceneDir, fps, videoOut);
1678
- await buildAudioTrack(plan, join5(sceneDir, "scene"), videoOut, out);
1679
- await rm2(videoOut, { force: true });
1778
+ await buildAudioTrack(plan, join6(sceneDir, "scene"), videoOut, out);
1779
+ await rm3(videoOut, { force: true });
1680
1780
  } else {
1681
1781
  await renderSceneVideo(scene, sceneDir, fps, out);
1682
1782
  }
@@ -1688,10 +1788,10 @@ async function concatVideos(files, out) {
1688
1788
  }
1689
1789
  const list = `${out}.concat.txt`;
1690
1790
  await writeFile4(list, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
1691
- await runFfmpeg(["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", "-movflags", "+faststart", out]);
1791
+ await runFfmpeg2(["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", "-movflags", "+faststart", out]);
1692
1792
  }
1693
1793
  async function xfade2(a, b, overlap, offset, out) {
1694
- await runFfmpeg([
1794
+ await runFfmpeg2([
1695
1795
  "-y",
1696
1796
  "-i",
1697
1797
  a,
@@ -1719,7 +1819,7 @@ async function combineWithTransitions(videos, out, tmp) {
1719
1819
  let accDur = videos[0].placement.duration;
1720
1820
  for (let i = 1; i < videos.length; i++) {
1721
1821
  const { overlap, duration } = videos[i].placement;
1722
- const step = join5(tmp, `step${i}.mp4`);
1822
+ const step = join6(tmp, `step${i}.mp4`);
1723
1823
  if (overlap <= 0) {
1724
1824
  await concatVideos([acc, videos[i].file], step);
1725
1825
  accDur += duration;
@@ -1741,15 +1841,15 @@ async function renderComposition(comp, opts) {
1741
1841
  await renderStandaloneScene(p.scene, sceneDir, opts.fps, opts.noAudio, opts.out);
1742
1842
  return { duration: p.duration, sceneCount: 1 };
1743
1843
  }
1744
- const tmp = await mkdtemp2(join5(tmpdir3(), "reframe-comp-"));
1844
+ const tmp = await mkdtemp3(join6(tmpdir4(), "reframe-comp-"));
1745
1845
  try {
1746
1846
  const videos = [];
1747
1847
  for (const p of cc.scenes) {
1748
- const file = join5(tmp, `${sanitize(p.id)}.mp4`);
1848
+ const file = join6(tmp, `${sanitize(p.id)}.mp4`);
1749
1849
  const { fps } = await renderSceneVideo(p.scene, sceneDir, opts.fps, file);
1750
1850
  videos.push({ id: p.id, file, placement: p, fps });
1751
1851
  }
1752
- const combined = join5(tmp, "combined.mp4");
1852
+ const combined = join6(tmp, "combined.mp4");
1753
1853
  const allCut = cc.scenes.every((s) => s.overlap === 0);
1754
1854
  if (allCut) await concatVideos(videos.map((v) => v.file), combined);
1755
1855
  else await combineWithTransitions(videos, combined, tmp);
@@ -1766,19 +1866,19 @@ async function renderComposition(comp, opts) {
1766
1866
  }
1767
1867
  return { duration: cc.duration, sceneCount: cc.scenes.length };
1768
1868
  } finally {
1769
- await rm2(tmp, { recursive: true, force: true });
1869
+ await rm3(tmp, { recursive: true, force: true });
1770
1870
  }
1771
1871
  }
1772
1872
 
1773
1873
  // ../render-cli/src/loadScene.ts
1774
1874
  import { build as build2 } from "esbuild";
1775
- import { readFile as readFile3 } from "node:fs/promises";
1776
- import { dirname as dirname6, resolve as resolve3 } from "node:path";
1875
+ import { readFile as readFile4 } from "node:fs/promises";
1876
+ import { dirname as dirname6, resolve as resolve4 } from "node:path";
1777
1877
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1778
1878
  var HERE = dirname6(fileURLToPath4(import.meta.url));
1779
- var CORE_ENTRY = true ? resolve3(HERE, "index.js") : resolve3(HERE, "..", "..", "core", "src", "index.ts");
1879
+ var CORE_ENTRY = true ? resolve4(HERE, "index.js") : resolve4(HERE, "..", "..", "core", "src", "index.ts");
1780
1880
  async function loadDefault(path2) {
1781
- if (path2.endsWith(".json")) return JSON.parse(await readFile3(path2, "utf8"));
1881
+ if (path2.endsWith(".json")) return JSON.parse(await readFile4(path2, "utf8"));
1782
1882
  let code;
1783
1883
  try {
1784
1884
  const out = await build2({
@@ -1826,7 +1926,7 @@ function parseArgs(argv) {
1826
1926
  }
1827
1927
  const args = {
1828
1928
  mode,
1829
- input: resolve4(input),
1929
+ input: resolve5(input),
1830
1930
  out: `${basename(input).replace(/\.[^.]+$/, "")}.mp4`,
1831
1931
  keepFrames: false,
1832
1932
  overlays: [],
@@ -1838,8 +1938,8 @@ function parseArgs(argv) {
1838
1938
  else if (a === "--fps") args.fps = Number(rest[++i]);
1839
1939
  else if (a === "--duration") args.duration = Number(rest[++i]);
1840
1940
  else if (a === "--keep-frames") args.keepFrames = true;
1841
- else if (a === "--frames-dir") args.framesDir = resolve4(rest[++i]);
1842
- else if (a === "--overlay") args.overlays.push(resolve4(rest[++i]));
1941
+ else if (a === "--frames-dir") args.framesDir = resolve5(rest[++i]);
1942
+ else if (a === "--overlay") args.overlays.push(resolve5(rest[++i]));
1843
1943
  else if (a === "--no-audio") args.noAudio = true;
1844
1944
  else if (a === "--scene") args.scene = rest[++i];
1845
1945
  else {
@@ -1868,14 +1968,14 @@ async function main() {
1868
1968
  );
1869
1969
  return;
1870
1970
  }
1871
- const framesDir = args.framesDir ?? await mkdtemp3(join6(tmpdir4(), "reframe-frames-"));
1971
+ const framesDir = args.framesDir ?? await mkdtemp4(join7(tmpdir5(), "reframe-frames-"));
1872
1972
  let result;
1873
1973
  let audioJob = null;
1874
1974
  if (args.mode === "ir") {
1875
1975
  let ir = loaded.ir;
1876
1976
  if (args.overlays.length > 0) {
1877
1977
  const docs = await Promise.all(
1878
- args.overlays.map(async (p) => JSON.parse(await readFile4(p, "utf8")))
1978
+ args.overlays.map(async (p) => JSON.parse(await readFile5(p, "utf8")))
1879
1979
  );
1880
1980
  const composed = composeScene(ir, ...docs);
1881
1981
  console.error(formatComposeReport(composed.report));
@@ -1907,10 +2007,10 @@ async function main() {
1907
2007
  await encodeMp4(result.framesDir, result.fps, audioJob ? audioJob.videoOut : args.out);
1908
2008
  if (audioJob) {
1909
2009
  await buildAudioTrack(audioJob.plan, args.input, audioJob.videoOut, args.out);
1910
- await rm3(audioJob.videoOut, { force: true });
2010
+ await rm4(audioJob.videoOut, { force: true });
1911
2011
  }
1912
2012
  if (!args.keepFrames && args.framesDir === void 0) {
1913
- await rm3(framesDir, { recursive: true, force: true });
2013
+ await rm4(framesDir, { recursive: true, force: true });
1914
2014
  }
1915
2015
  console.log(
1916
2016
  `${args.out} (${result.frameCount} frames @ ${result.fps}fps${audioJob ? `, ${audioJob.plan.cues.length} audio cues` : ""})`