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.
- package/dist/bin.js +179 -71
- package/dist/browserEntry.js +95 -30
- package/dist/cli.js +154 -54
- package/dist/index.js +51 -7
- package/dist/labels.js +4 -1
- package/dist/renderer-canvas.js +43 -19
- package/dist/trace-cli.js +2 -1
- package/dist/types/assets.d.ts +3 -1
- package/dist/types/dsl.d.ts +4 -1
- package/dist/types/evaluate.d.ts +15 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir.d.ts +30 -0
- package/dist/types/montage.d.ts +4 -4
- package/guides/edsl-guide.md +30 -6
- package/package.json +1 -1
- package/preview/src/main.ts +2 -1
package/dist/browserEntry.js
CHANGED
|
@@ -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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
|
5
|
-
import { tmpdir as
|
|
6
|
-
import { basename, dirname as dirname7, join as
|
|
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
|
|
1048
|
+
function collectSrcs(ir, type) {
|
|
1046
1049
|
const srcs = /* @__PURE__ */ new Set();
|
|
1047
|
-
const
|
|
1050
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1048
1051
|
const walkNodes = (nodes) => {
|
|
1049
1052
|
for (const node of nodes) {
|
|
1050
|
-
if (node.type ===
|
|
1051
|
-
|
|
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 (
|
|
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" &&
|
|
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
|
|
1396
|
-
import { copyFile, mkdtemp as
|
|
1397
|
-
import { tmpdir as
|
|
1398
|
-
import { dirname as dirname5, join as
|
|
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((
|
|
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)
|
|
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
|
|
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) =>
|
|
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:
|
|
1587
|
-
bundleCache = await
|
|
1588
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
1653
|
-
return new Promise((
|
|
1654
|
-
const proc =
|
|
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 ?
|
|
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
|
|
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
|
|
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,
|
|
1679
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
1776
|
-
import { dirname as dirname6, resolve as
|
|
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 ?
|
|
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
|
|
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:
|
|
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 =
|
|
1842
|
-
else if (a === "--overlay") args.overlays.push(
|
|
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
|
|
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
|
|
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
|
|
2010
|
+
await rm4(audioJob.videoOut, { force: true });
|
|
1911
2011
|
}
|
|
1912
2012
|
if (!args.keepFrames && args.framesDir === void 0) {
|
|
1913
|
-
await
|
|
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` : ""})`
|