reframe-video 0.6.5 → 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 +175 -69
- package/dist/browserEntry.js +82 -35
- package/dist/cli.js +151 -53
- package/dist/index.js +46 -6
- package/dist/labels.js +1 -0
- package/dist/renderer-canvas.js +31 -25
- package/dist/trace-cli.js +1 -0
- package/dist/types/assets.d.ts +3 -1
- package/dist/types/dsl.d.ts +4 -1
- package/dist/types/evaluate.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir.d.ts +21 -0
- package/guides/edsl-guide.md +21 -0
- package/package.json +1 -1
- package/preview/src/main.ts +2 -1
package/dist/browserEntry.js
CHANGED
|
@@ -342,6 +342,7 @@
|
|
|
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
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
|
};
|
|
@@ -786,6 +787,33 @@
|
|
|
786
787
|
});
|
|
787
788
|
return;
|
|
788
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 } : {},
|
|
812
|
+
...fx,
|
|
813
|
+
...clipSpread
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
789
817
|
case "path": {
|
|
790
818
|
const ox = num(id, "originX", node.props.originX ?? 0);
|
|
791
819
|
const oy = num(id, "originY", node.props.originY ?? 0);
|
|
@@ -879,7 +907,7 @@
|
|
|
879
907
|
for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
|
|
880
908
|
return g;
|
|
881
909
|
}
|
|
882
|
-
function renderFrame(ctx2, compiled2, t, images2) {
|
|
910
|
+
function renderFrame(ctx2, compiled2, t, images2, videos2) {
|
|
883
911
|
const { size, background } = compiled2.ir;
|
|
884
912
|
ctx2.setTransform(1, 0, 0, 1, 0, 0);
|
|
885
913
|
ctx2.clearRect(0, 0, size.width, size.height);
|
|
@@ -887,9 +915,9 @@
|
|
|
887
915
|
ctx2.fillStyle = background;
|
|
888
916
|
ctx2.fillRect(0, 0, size.width, size.height);
|
|
889
917
|
}
|
|
890
|
-
drawDisplayList(ctx2, evaluate(compiled2, t), images2);
|
|
918
|
+
drawDisplayList(ctx2, evaluate(compiled2, t), images2, videos2);
|
|
891
919
|
}
|
|
892
|
-
function drawDisplayList(ctx2, ops, images2) {
|
|
920
|
+
function drawDisplayList(ctx2, ops, images2, videos2) {
|
|
893
921
|
for (const op of ops) {
|
|
894
922
|
ctx2.save();
|
|
895
923
|
if (op.clips) {
|
|
@@ -971,28 +999,11 @@
|
|
|
971
999
|
break;
|
|
972
1000
|
}
|
|
973
1001
|
case "image": {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
ctx2.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
|
|
980
|
-
} else {
|
|
981
|
-
ctx2.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
|
|
982
|
-
}
|
|
983
|
-
} else {
|
|
984
|
-
ctx2.fillStyle = "#2A2A30";
|
|
985
|
-
ctx2.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
986
|
-
ctx2.strokeStyle = "#FF00FF";
|
|
987
|
-
ctx2.lineWidth = 2;
|
|
988
|
-
ctx2.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
989
|
-
ctx2.beginPath();
|
|
990
|
-
ctx2.moveTo(op.offsetX, op.offsetY);
|
|
991
|
-
ctx2.lineTo(op.offsetX + op.width, op.offsetY + op.height);
|
|
992
|
-
ctx2.moveTo(op.offsetX + op.width, op.offsetY);
|
|
993
|
-
ctx2.lineTo(op.offsetX, op.offsetY + op.height);
|
|
994
|
-
ctx2.stroke();
|
|
995
|
-
}
|
|
1002
|
+
drawRaster(ctx2, images2?.get(op.src), op);
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
case "video": {
|
|
1006
|
+
drawRaster(ctx2, videos2?.frame(op.src, op.frame), op);
|
|
996
1007
|
break;
|
|
997
1008
|
}
|
|
998
1009
|
case "path": {
|
|
@@ -1047,6 +1058,29 @@
|
|
|
1047
1058
|
const a = img;
|
|
1048
1059
|
return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
|
|
1049
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
|
+
}
|
|
1050
1084
|
function quoteFamily(family) {
|
|
1051
1085
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
1052
1086
|
}
|
|
@@ -1069,9 +1103,22 @@
|
|
|
1069
1103
|
var ctx = null;
|
|
1070
1104
|
var canvas = null;
|
|
1071
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
|
+
};
|
|
1072
1119
|
window.__reframe = {
|
|
1073
|
-
// fully decode every image before the first frame — renderFrame is sync
|
|
1074
|
-
async init(ir, assets = {}) {
|
|
1120
|
+
// fully decode every image/video frame before the first frame — renderFrame is sync
|
|
1121
|
+
async init(ir, assets = {}, videoAssets = {}) {
|
|
1075
1122
|
compiled = compileScene(ir);
|
|
1076
1123
|
canvas = document.createElement("canvas");
|
|
1077
1124
|
canvas.width = ir.size.width;
|
|
@@ -1079,19 +1126,19 @@
|
|
|
1079
1126
|
document.body.appendChild(canvas);
|
|
1080
1127
|
ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1081
1128
|
if (!ctx) throw new Error("could not create 2d context");
|
|
1082
|
-
await Promise.all(
|
|
1083
|
-
Object.entries(assets).map(async ([src, dataUrl]) => {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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)));
|
|
1088
1135
|
})
|
|
1089
|
-
);
|
|
1136
|
+
]);
|
|
1090
1137
|
return { duration: compiled.duration, fps: ir.fps ?? 30 };
|
|
1091
1138
|
},
|
|
1092
1139
|
renderFrame(t) {
|
|
1093
1140
|
if (!compiled || !ctx || !canvas) throw new Error("init() not called");
|
|
1094
|
-
renderFrame(ctx, compiled, t, images);
|
|
1141
|
+
renderFrame(ctx, compiled, t, images, videos);
|
|
1095
1142
|
return canvas.toDataURL("image/png");
|
|
1096
1143
|
}
|
|
1097
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;
|
|
@@ -347,6 +347,7 @@ var PROPS_BY_TYPE = {
|
|
|
347
347
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
348
348
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
349
349
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
350
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
350
351
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
351
352
|
group: COMMON_PROPS
|
|
352
353
|
};
|
|
@@ -1044,13 +1045,13 @@ var EASE_TABLE = {
|
|
|
1044
1045
|
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
1045
1046
|
|
|
1046
1047
|
// ../core/src/assets.ts
|
|
1047
|
-
function
|
|
1048
|
+
function collectSrcs(ir, type) {
|
|
1048
1049
|
const srcs = /* @__PURE__ */ new Set();
|
|
1049
|
-
const
|
|
1050
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1050
1051
|
const walkNodes = (nodes) => {
|
|
1051
1052
|
for (const node of nodes) {
|
|
1052
|
-
if (node.type ===
|
|
1053
|
-
|
|
1053
|
+
if (node.type === type) {
|
|
1054
|
+
ids.add(node.id);
|
|
1054
1055
|
srcs.add(node.props.src);
|
|
1055
1056
|
}
|
|
1056
1057
|
if (node.type === "group") walkNodes(node.children);
|
|
@@ -1059,14 +1060,14 @@ function collectImageSrcs(ir) {
|
|
|
1059
1060
|
walkNodes(ir.nodes);
|
|
1060
1061
|
for (const overrides of Object.values(ir.states ?? {})) {
|
|
1061
1062
|
for (const [nodeId, props] of Object.entries(overrides)) {
|
|
1062
|
-
if (
|
|
1063
|
+
if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
|
|
1063
1064
|
}
|
|
1064
1065
|
}
|
|
1065
1066
|
const walkTimeline = (step) => {
|
|
1066
1067
|
if (!step) return;
|
|
1067
1068
|
if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
|
|
1068
1069
|
for (const child of step.children) walkTimeline(child);
|
|
1069
|
-
} else if (step.kind === "tween" &&
|
|
1070
|
+
} else if (step.kind === "tween" && ids.has(step.target)) {
|
|
1070
1071
|
const src = step.props.src;
|
|
1071
1072
|
if (typeof src === "string") srcs.add(src);
|
|
1072
1073
|
}
|
|
@@ -1074,6 +1075,12 @@ function collectImageSrcs(ir) {
|
|
|
1074
1075
|
walkTimeline(ir.timeline);
|
|
1075
1076
|
return [...srcs];
|
|
1076
1077
|
}
|
|
1078
|
+
function collectImageSrcs(ir) {
|
|
1079
|
+
return collectSrcs(ir, "image");
|
|
1080
|
+
}
|
|
1081
|
+
function collectVideoSrcs(ir) {
|
|
1082
|
+
return collectSrcs(ir, "video");
|
|
1083
|
+
}
|
|
1077
1084
|
|
|
1078
1085
|
// ../render-cli/src/audio/index.ts
|
|
1079
1086
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -1394,10 +1401,10 @@ async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
|
|
|
1394
1401
|
}
|
|
1395
1402
|
|
|
1396
1403
|
// ../render-cli/src/composition.ts
|
|
1397
|
-
import { spawn as
|
|
1398
|
-
import { copyFile, mkdtemp as
|
|
1399
|
-
import { tmpdir as
|
|
1400
|
-
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";
|
|
1401
1408
|
|
|
1402
1409
|
// ../render-cli/src/encode.ts
|
|
1403
1410
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -1420,12 +1427,12 @@ async function encodeMp4(framesDir, fps, outFile) {
|
|
|
1420
1427
|
"+faststart",
|
|
1421
1428
|
outFile
|
|
1422
1429
|
];
|
|
1423
|
-
await new Promise((
|
|
1430
|
+
await new Promise((resolve6, reject) => {
|
|
1424
1431
|
const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1425
1432
|
let stderr = "";
|
|
1426
1433
|
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
1427
1434
|
proc.on("close", (code) => {
|
|
1428
|
-
if (code === 0)
|
|
1435
|
+
if (code === 0) resolve6();
|
|
1429
1436
|
else reject(new Error(`ffmpeg exited with ${code}:
|
|
1430
1437
|
${stderr.slice(-2e3)}`));
|
|
1431
1438
|
});
|
|
@@ -1435,7 +1442,7 @@ ${stderr.slice(-2e3)}`));
|
|
|
1435
1442
|
|
|
1436
1443
|
// ../render-cli/src/frameLoop.ts
|
|
1437
1444
|
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1438
|
-
import { join as
|
|
1445
|
+
import { join as join5, dirname as dirname4 } from "node:path";
|
|
1439
1446
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
1440
1447
|
import { build } from "esbuild";
|
|
1441
1448
|
import { chromium } from "playwright";
|
|
@@ -1496,6 +1503,96 @@ async function buildImageAssets(ir, sceneDir) {
|
|
|
1496
1503
|
return assets;
|
|
1497
1504
|
}
|
|
1498
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
|
+
|
|
1499
1596
|
// ../render-cli/src/vclock.ts
|
|
1500
1597
|
var VCLOCK_SOURCE = String.raw`
|
|
1501
1598
|
(() => {
|
|
@@ -1569,7 +1666,7 @@ async function injectFonts(page) {
|
|
|
1569
1666
|
await document.fonts.ready;
|
|
1570
1667
|
});
|
|
1571
1668
|
}
|
|
1572
|
-
var framePath = (dir, i) =>
|
|
1669
|
+
var framePath = (dir, i) => join5(dir, `${String(i).padStart(5, "0")}.png`);
|
|
1573
1670
|
async function withPage(size, fn) {
|
|
1574
1671
|
const browser = await chromium.launch({
|
|
1575
1672
|
args: ["--force-color-profile=srgb", "--font-render-hinting=none"]
|
|
@@ -1585,14 +1682,14 @@ var bundleCache = null;
|
|
|
1585
1682
|
async function browserBundle() {
|
|
1586
1683
|
if (bundleCache) return bundleCache;
|
|
1587
1684
|
if (true) {
|
|
1588
|
-
const { readFile:
|
|
1589
|
-
bundleCache = await
|
|
1590
|
-
|
|
1685
|
+
const { readFile: readFile6 } = await import("node:fs/promises");
|
|
1686
|
+
bundleCache = await readFile6(
|
|
1687
|
+
join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
|
|
1591
1688
|
"utf8"
|
|
1592
1689
|
);
|
|
1593
1690
|
return bundleCache;
|
|
1594
1691
|
}
|
|
1595
|
-
const entry =
|
|
1692
|
+
const entry = join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
|
|
1596
1693
|
const result = await build({
|
|
1597
1694
|
entryPoints: [entry],
|
|
1598
1695
|
bundle: true,
|
|
@@ -1605,7 +1702,10 @@ async function browserBundle() {
|
|
|
1605
1702
|
}
|
|
1606
1703
|
async function captureIr(ir, opts) {
|
|
1607
1704
|
await mkdir2(opts.framesDir, { recursive: true });
|
|
1608
|
-
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);
|
|
1609
1709
|
const bundle = await browserBundle();
|
|
1610
1710
|
return withPage(ir.size, async (page) => {
|
|
1611
1711
|
await page.setContent(
|
|
@@ -1613,12 +1713,10 @@ async function captureIr(ir, opts) {
|
|
|
1613
1713
|
);
|
|
1614
1714
|
await injectFonts(page);
|
|
1615
1715
|
await page.addScriptTag({ content: bundle });
|
|
1616
|
-
|
|
1617
|
-
([sceneIr, imageAssets]) => window.__reframe.init(sceneIr, imageAssets),
|
|
1618
|
-
[ir, assets]
|
|
1716
|
+
await page.evaluate(
|
|
1717
|
+
([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
|
|
1718
|
+
[ir, assets, videoAssets]
|
|
1619
1719
|
);
|
|
1620
|
-
const fps = opts.fps ?? info.fps;
|
|
1621
|
-
const duration = opts.duration ?? info.duration;
|
|
1622
1720
|
const frameCount = Math.max(1, Math.round(duration * fps));
|
|
1623
1721
|
for (let f = 0; f < frameCount; f++) {
|
|
1624
1722
|
const dataUrl = await page.evaluate((t) => window.__reframe.renderFrame(t), f / fps);
|
|
@@ -1651,25 +1749,25 @@ async function captureHtml(htmlPath, opts) {
|
|
|
1651
1749
|
}
|
|
1652
1750
|
|
|
1653
1751
|
// ../render-cli/src/composition.ts
|
|
1654
|
-
function
|
|
1655
|
-
return new Promise((
|
|
1656
|
-
const proc =
|
|
1752
|
+
function runFfmpeg2(args) {
|
|
1753
|
+
return new Promise((resolve6, reject) => {
|
|
1754
|
+
const proc = spawn4("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1657
1755
|
let stderr = "";
|
|
1658
1756
|
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
1659
|
-
proc.on("close", (code) => code === 0 ?
|
|
1757
|
+
proc.on("close", (code) => code === 0 ? resolve6() : reject(new Error(`ffmpeg exited ${code}:
|
|
1660
1758
|
${stderr.slice(-2e3)}`)));
|
|
1661
1759
|
proc.on("error", reject);
|
|
1662
1760
|
});
|
|
1663
1761
|
}
|
|
1664
1762
|
var sanitize = (id) => id.replace(/[^a-z0-9_-]/gi, "_");
|
|
1665
1763
|
async function renderSceneVideo(scene, sceneDir, fps, out) {
|
|
1666
|
-
const framesDir = await
|
|
1764
|
+
const framesDir = await mkdtemp3(join6(tmpdir4(), "reframe-frames-"));
|
|
1667
1765
|
try {
|
|
1668
1766
|
const result = await captureIr(scene, { framesDir, sceneDir, ...fps !== void 0 && { fps } });
|
|
1669
1767
|
await encodeMp4(result.framesDir, result.fps, out);
|
|
1670
1768
|
return { fps: result.fps, frameCount: result.frameCount };
|
|
1671
1769
|
} finally {
|
|
1672
|
-
await
|
|
1770
|
+
await rm3(framesDir, { recursive: true, force: true });
|
|
1673
1771
|
}
|
|
1674
1772
|
}
|
|
1675
1773
|
async function renderStandaloneScene(scene, sceneDir, fps, noAudio, out) {
|
|
@@ -1677,8 +1775,8 @@ async function renderStandaloneScene(scene, sceneDir, fps, noAudio, out) {
|
|
|
1677
1775
|
if (plan) {
|
|
1678
1776
|
const videoOut = `${out}.video.mp4`;
|
|
1679
1777
|
await renderSceneVideo(scene, sceneDir, fps, videoOut);
|
|
1680
|
-
await buildAudioTrack(plan,
|
|
1681
|
-
await
|
|
1778
|
+
await buildAudioTrack(plan, join6(sceneDir, "scene"), videoOut, out);
|
|
1779
|
+
await rm3(videoOut, { force: true });
|
|
1682
1780
|
} else {
|
|
1683
1781
|
await renderSceneVideo(scene, sceneDir, fps, out);
|
|
1684
1782
|
}
|
|
@@ -1690,10 +1788,10 @@ async function concatVideos(files, out) {
|
|
|
1690
1788
|
}
|
|
1691
1789
|
const list = `${out}.concat.txt`;
|
|
1692
1790
|
await writeFile4(list, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
1693
|
-
await
|
|
1791
|
+
await runFfmpeg2(["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", "-movflags", "+faststart", out]);
|
|
1694
1792
|
}
|
|
1695
1793
|
async function xfade2(a, b, overlap, offset, out) {
|
|
1696
|
-
await
|
|
1794
|
+
await runFfmpeg2([
|
|
1697
1795
|
"-y",
|
|
1698
1796
|
"-i",
|
|
1699
1797
|
a,
|
|
@@ -1721,7 +1819,7 @@ async function combineWithTransitions(videos, out, tmp) {
|
|
|
1721
1819
|
let accDur = videos[0].placement.duration;
|
|
1722
1820
|
for (let i = 1; i < videos.length; i++) {
|
|
1723
1821
|
const { overlap, duration } = videos[i].placement;
|
|
1724
|
-
const step =
|
|
1822
|
+
const step = join6(tmp, `step${i}.mp4`);
|
|
1725
1823
|
if (overlap <= 0) {
|
|
1726
1824
|
await concatVideos([acc, videos[i].file], step);
|
|
1727
1825
|
accDur += duration;
|
|
@@ -1743,15 +1841,15 @@ async function renderComposition(comp, opts) {
|
|
|
1743
1841
|
await renderStandaloneScene(p.scene, sceneDir, opts.fps, opts.noAudio, opts.out);
|
|
1744
1842
|
return { duration: p.duration, sceneCount: 1 };
|
|
1745
1843
|
}
|
|
1746
|
-
const tmp = await
|
|
1844
|
+
const tmp = await mkdtemp3(join6(tmpdir4(), "reframe-comp-"));
|
|
1747
1845
|
try {
|
|
1748
1846
|
const videos = [];
|
|
1749
1847
|
for (const p of cc.scenes) {
|
|
1750
|
-
const file =
|
|
1848
|
+
const file = join6(tmp, `${sanitize(p.id)}.mp4`);
|
|
1751
1849
|
const { fps } = await renderSceneVideo(p.scene, sceneDir, opts.fps, file);
|
|
1752
1850
|
videos.push({ id: p.id, file, placement: p, fps });
|
|
1753
1851
|
}
|
|
1754
|
-
const combined =
|
|
1852
|
+
const combined = join6(tmp, "combined.mp4");
|
|
1755
1853
|
const allCut = cc.scenes.every((s) => s.overlap === 0);
|
|
1756
1854
|
if (allCut) await concatVideos(videos.map((v) => v.file), combined);
|
|
1757
1855
|
else await combineWithTransitions(videos, combined, tmp);
|
|
@@ -1768,19 +1866,19 @@ async function renderComposition(comp, opts) {
|
|
|
1768
1866
|
}
|
|
1769
1867
|
return { duration: cc.duration, sceneCount: cc.scenes.length };
|
|
1770
1868
|
} finally {
|
|
1771
|
-
await
|
|
1869
|
+
await rm3(tmp, { recursive: true, force: true });
|
|
1772
1870
|
}
|
|
1773
1871
|
}
|
|
1774
1872
|
|
|
1775
1873
|
// ../render-cli/src/loadScene.ts
|
|
1776
1874
|
import { build as build2 } from "esbuild";
|
|
1777
|
-
import { readFile as
|
|
1778
|
-
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";
|
|
1779
1877
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1780
1878
|
var HERE = dirname6(fileURLToPath4(import.meta.url));
|
|
1781
|
-
var CORE_ENTRY = true ?
|
|
1879
|
+
var CORE_ENTRY = true ? resolve4(HERE, "index.js") : resolve4(HERE, "..", "..", "core", "src", "index.ts");
|
|
1782
1880
|
async function loadDefault(path2) {
|
|
1783
|
-
if (path2.endsWith(".json")) return JSON.parse(await
|
|
1881
|
+
if (path2.endsWith(".json")) return JSON.parse(await readFile4(path2, "utf8"));
|
|
1784
1882
|
let code;
|
|
1785
1883
|
try {
|
|
1786
1884
|
const out = await build2({
|
|
@@ -1828,7 +1926,7 @@ function parseArgs(argv) {
|
|
|
1828
1926
|
}
|
|
1829
1927
|
const args = {
|
|
1830
1928
|
mode,
|
|
1831
|
-
input:
|
|
1929
|
+
input: resolve5(input),
|
|
1832
1930
|
out: `${basename(input).replace(/\.[^.]+$/, "")}.mp4`,
|
|
1833
1931
|
keepFrames: false,
|
|
1834
1932
|
overlays: [],
|
|
@@ -1840,8 +1938,8 @@ function parseArgs(argv) {
|
|
|
1840
1938
|
else if (a === "--fps") args.fps = Number(rest[++i]);
|
|
1841
1939
|
else if (a === "--duration") args.duration = Number(rest[++i]);
|
|
1842
1940
|
else if (a === "--keep-frames") args.keepFrames = true;
|
|
1843
|
-
else if (a === "--frames-dir") args.framesDir =
|
|
1844
|
-
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]));
|
|
1845
1943
|
else if (a === "--no-audio") args.noAudio = true;
|
|
1846
1944
|
else if (a === "--scene") args.scene = rest[++i];
|
|
1847
1945
|
else {
|
|
@@ -1870,14 +1968,14 @@ async function main() {
|
|
|
1870
1968
|
);
|
|
1871
1969
|
return;
|
|
1872
1970
|
}
|
|
1873
|
-
const framesDir = args.framesDir ?? await
|
|
1971
|
+
const framesDir = args.framesDir ?? await mkdtemp4(join7(tmpdir5(), "reframe-frames-"));
|
|
1874
1972
|
let result;
|
|
1875
1973
|
let audioJob = null;
|
|
1876
1974
|
if (args.mode === "ir") {
|
|
1877
1975
|
let ir = loaded.ir;
|
|
1878
1976
|
if (args.overlays.length > 0) {
|
|
1879
1977
|
const docs = await Promise.all(
|
|
1880
|
-
args.overlays.map(async (p) => JSON.parse(await
|
|
1978
|
+
args.overlays.map(async (p) => JSON.parse(await readFile5(p, "utf8")))
|
|
1881
1979
|
);
|
|
1882
1980
|
const composed = composeScene(ir, ...docs);
|
|
1883
1981
|
console.error(formatComposeReport(composed.report));
|
|
@@ -1909,10 +2007,10 @@ async function main() {
|
|
|
1909
2007
|
await encodeMp4(result.framesDir, result.fps, audioJob ? audioJob.videoOut : args.out);
|
|
1910
2008
|
if (audioJob) {
|
|
1911
2009
|
await buildAudioTrack(audioJob.plan, args.input, audioJob.videoOut, args.out);
|
|
1912
|
-
await
|
|
2010
|
+
await rm4(audioJob.videoOut, { force: true });
|
|
1913
2011
|
}
|
|
1914
2012
|
if (!args.keepFrames && args.framesDir === void 0) {
|
|
1915
|
-
await
|
|
2013
|
+
await rm4(framesDir, { recursive: true, force: true });
|
|
1916
2014
|
}
|
|
1917
2015
|
console.log(
|
|
1918
2016
|
`${args.out} (${result.frameCount} frames @ ${result.fps}fps${audioJob ? `, ${audioJob.plan.cues.length} audio cues` : ""})`
|