refactor command generator

This commit is contained in:
Sam Lavigne 2023-08-29 01:09:36 -04:00
parent 115c30466b
commit ef892c6732
4 changed files with 264 additions and 201 deletions

View File

@ -22,7 +22,7 @@
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types. * Disable this if you'd like to use dynamic types.
*/ */
"checkJs": true "checkJs": false
}, },
/** /**
* Use global.d.ts instead of compilerOptions.types * Use global.d.ts instead of compilerOptions.types

View File

@ -33,7 +33,7 @@
videoValue = null; videoValue = null;
rendering = true; rendering = true;
try { try {
if (log.trim() != '') log += "\n\n"; if (log.trim() != "") log += "\n\n";
for (let vid of $inputs) { for (let vid of $inputs) {
await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name)); await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
} }

View File

@ -10,249 +10,283 @@ addNode({ name: "punch.mp4" }, "input");
addNode({ name: "out.mp4" }, "output"); addNode({ name: "out.mp4" }, "output");
export function makeFilterArgs(f) { export function makeFilterArgs(f) {
let fCommand = f.name; let fCommand = f.name;
if (f.params && f.params.length > 0) { if (f.params && f.params.length > 0) {
let params = f.params let params = f.params
.map((p) => { .map((p) => {
if (p.value === "" || p.value === null || p.value === p.default) return null; if (p.value === "" || p.value === null || p.value === p.default) return null;
return `${p.name}=${p.value}`; return `${p.name}=${p.value}`;
}) })
.filter((p) => p !== null) .filter((p) => p !== null)
.join(":"); .join(":");
if (params) fCommand += "=" + params; if (params) fCommand += "=" + params;
} }
return fCommand; return fCommand;
} }
export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => { export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
let hasVid = false; let finalCommand = [];
let hasAud = false; let filtergraph = [];
let labelIndex = 1;
const edgeIds = {};
const inputIdMap = {};
let finalCommand = []; const inputs = $nodes.filter((n) => n.nodeType === "input");
const inputIds = inputs.map((n) => n.id);
const inputEdges = $edges.filter((e) => inputIds.includes(e.source));
const outputs = $nodes.filter((n) => n.nodeType === "output");
let filtergraph = []; inputs.forEach((inp, i) => (inputIdMap[inp.id] = i));
const inputs = $nodes.filter((n) => n.nodeType == "input"); function traverseEdges(edg, type) {
const outputs = $nodes.filter((n) => n.nodeType == "output"); const outEdges = $edges.filter((e) => e.source === edg.target && e.sourceHandle.includes(type));
const inputIds = {}; let label;
for (let i = 0; i < inputs.length; i++) {
const inp = inputs[i];
inputIds[inp.id] = i;
}
const edgeIds = {}; const inNode = $nodes.find((n) => n.id === edg.source);
for (let i = 0; i < $edges.length; i++) { const outNode = $nodes.find((n) => n.id === edg.target);
const e = $edges[i];
edgeIds[e.id] = i + 1;
const source = $nodes.find((n) => n.id === e.source); if (inNode && outNode) {
const target = $nodes.find((n) => n.id === e.target); if (inNode.nodeType === "input" && outNode.nodeType === "filter") {
label = inputIdMap[inNode.id] + ":" + edg.sourceHandle[0];
if (source && target) { } else if (inNode.nodeType === "filter" && outNode.nodeType === "filter") {
label = labelIndex;
if (source.nodeType === "input") { labelIndex++;
if (e.sourceHandle.includes("v")) { } else if (inNode.nodeType === "filter" && outNode.nodeType === "output") {
edgeIds[e.id] = inputIds[source.id] + ":v"; label = "out_" + type;
} } else if (inNode.nodeType === "input" && outNode.nodeType === "output") {
if (e.sourceHandle.includes("a")) { label = "FILTERLESS_" + inputIdMap[inNode.id] + ":" + type;
edgeIds[e.id] = inputIds[source.id] + ":a"; } else {
} label = "UNKNOWN";
} }
if (target.nodeType === "output") { edgeIds[edg.id] = label;
const outType = e.targetHandle.includes("a") ? "aud_out" : "vid_out"; }
edgeIds[e.id] = outType;
for (let e2 of outEdges) {
traverseEdges(e2, type);
}
}
for (let inp of inputEdges) {
for (let t of ["v", "a"]) {
if (inp.sourceHandle.includes(t)) {
traverseEdges(inp, t);
} }
} }
} }
for (let n of $nodes.filter((n) => n.nodeType == "filter")) { for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
let cmd = ""; let cmd = { weight: 0, in: [], out: [], cmd: "" };
const outs = $edges.filter((e) => e.source == n.id); const outs = $edges.filter((e) => e.source == n.id);
const ins = $edges.filter((e) => e.target == n.id); const ins = $edges.filter((e) => e.target == n.id);
if (outs.length == 0 && ins.length == 0) continue; if (outs.length == 0 && ins.length == 0) continue;
for (let i of ins) { for (let i of ins) {
const eid = edgeIds[i.id]; const eid = edgeIds[i.id];
cmd += `[${eid}]`; if (typeof eid == "string" && eid.includes(":")) cmd.weight = -1000;
} cmd.in.push(eid);
cmd += makeFilterArgs(n.data); }
for (let o of outs) {
const eid = edgeIds[o.id];
cmd += `[${eid}]`;
}
filtergraph.push(cmd);
}
finalCommand.push("ffmpeg"); cmd.cmd = makeFilterArgs(n.data);
for (let inp of inputs) { for (let o of outs) {
finalCommand.push("-i"); const eid = edgeIds[o.id];
finalCommand.push(inp.data.name); if (typeof eid == "string" && eid.includes("out")) cmd.weight = 1000;
} else cmd.weight = eid;
cmd.out.push(eid);
}
if (filtergraph.length > 0) { filtergraph.push(cmd);
const fg = '"' + filtergraph.join(";") + '"'; }
hasVid = fg.includes(":v]");
hasAud = fg.includes(":a]");
finalCommand.push("-filter_complex"); filtergraph.sort((a, b) => {
finalCommand.push(fg); return a.weight - b.weight;
});
finalCommand.push("-map"); filtergraph = filtergraph.map((c) => {
if (hasAud) { return c.in.map((i) => `[${i}]`).join("") + c.cmd + c.out.map((i) => `[${i}]`).join("");
finalCommand.push('"[aud_out]"'); });
} else {
finalCommand.push("0:a");
}
finalCommand.push("-map"); finalCommand.push("ffmpeg");
if (hasVid) {
finalCommand.push('"[vid_out]"');
} else {
finalCommand.push("0:v");
}
}
for (let out of outputs) { for (let inp of inputs) {
finalCommand.push(out.data.name); finalCommand.push("-i");
} finalCommand.push(inp.data.name);
}
const entireCommand = finalCommand.join(" "); let hasVid = false;
return entireCommand; let hasAud = false;
let mediaMaps = Object.values(edgeIds)
.map((eid) => {
if (String(eid).includes("FILTERLESS")) {
return eid.split("_")[1];
}
return null;
})
.filter((m) => m !== null);
if (filtergraph.length > 0) {
let fg = `"${filtergraph.join(";")}"`;
// this crazy thing replaces stuff like [1];[1] with a comma!
fg = fg.replaceAll(/(\[\d+\]);\1(?!\[)/g, ",");
hasVid = fg.includes(":v]");
hasAud = fg.includes(":a]");
finalCommand.push("-filter_complex", fg);
if (hasAud) {
finalCommand.push("-map", '"[out_a]"');
}
if (hasVid) {
finalCommand.push("-map", '"[out_v]"');
}
for (let m of mediaMaps) {
finalCommand.push("-map", m);
}
}
for (let out of outputs) {
finalCommand.push(out.data.name);
}
return finalCommand.join(" ");
}); });
export const inputs = derived(nodes, ($nodes) => { export const inputs = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "input").map((n) => n.data); return $nodes.filter((n) => n.nodeType === "input").map((n) => n.data);
}); });
nodes.subscribe(($nodes) => { nodes.subscribe(($nodes) => {
const isAuto = get(auto); const isAuto = get(auto);
if (!isAuto) return; if (!isAuto) return;
const outputNodes = $nodes.filter((n) => n.nodeType === "output"); const outputNodes = $nodes.filter((n) => n.nodeType === "output");
const inputNodes = $nodes.filter((n) => n.nodeType === "input"); const inputNodes = $nodes.filter((n) => n.nodeType === "input");
const filterNodes = $nodes.filter((n) => n.nodeType === "filter"); const filterNodes = $nodes.filter((n) => n.nodeType === "filter");
const orderedNodes = [].concat(filterNodes, outputNodes).filter((n) => n != undefined); const orderedNodes = [].concat(filterNodes, outputNodes).filter((n) => n != undefined);
const filled = []; const filled = [];
let newEdges = []; let newEdges = [];
function connectNode(n1, rest) { function connectNode(n1, rest) {
for (let i = 0; i < n1.data.outputs.length; i++) { for (let i = 0; i < n1.data.outputs.length; i++) {
const edgeType = n1.data.outputs[i]; const edgeType = n1.data.outputs[i];
for (let j = 0; j < rest.length; j++) { for (let j = 0; j < rest.length; j++) {
let found = false; let found = false;
const n2 = rest[j]; const n2 = rest[j];
for (let k = 0; k < n2.data.inputs.length; k++) { for (let k = 0; k < n2.data.inputs.length; k++) {
const targetEdgeType = n2.data.inputs[k]; const targetEdgeType = n2.data.inputs[k];
if (edgeType === targetEdgeType && !filled.includes(n2.id + k)) { if (edgeType === targetEdgeType && !filled.includes(n2.id + k)) {
newEdges.push({ newEdges.push({
id: uuidv4(), id: uuidv4(),
type: "default", type: "default",
source: n1.id, source: n1.id,
target: n2.id, target: n2.id,
sourceHandle: edgeType + "_" + i, sourceHandle: edgeType + "_" + i,
targetHandle: edgeType + "_" + k, targetHandle: edgeType + "_" + k,
}); });
filled.push(n2.id + k); filled.push(n2.id + k);
found = true; found = true;
break; break;
} }
} }
if (found) break; if (found) break;
} }
} }
const nextNode = rest.shift(); const nextNode = rest.shift();
if (nextNode) { if (nextNode) {
connectNode(nextNode, rest); connectNode(nextNode, rest);
} }
} }
for (let inpNode of inputNodes) { for (let inpNode of inputNodes) {
connectNode(inpNode, [...orderedNodes]); connectNode(inpNode, [...orderedNodes]);
} }
// console.log("new", newEdges);
edges.set(newEdges); edges.set(newEdges);
}); });
export function addNode(_data, type) { export function addNode(_data, type) {
const data = JSON.parse(JSON.stringify(_data)); const data = JSON.parse(JSON.stringify(_data));
let ins = []; let ins = [];
let outs = []; let outs = [];
if (type === "input") { if (type === "input") {
outs = ["v", "a"]; outs = ["v", "a"];
} else if (type === "output") { } else if (type === "output") {
ins = ["v", "a"]; ins = ["v", "a"];
} else if (type === "filter") { } else if (type === "filter") {
const [_ins, _outs] = data.type.split("->"); const [_ins, _outs] = data.type.split("->");
ins = _ins.toLowerCase().split(""); ins = _ins.toLowerCase().split("");
outs = _outs.toLowerCase().split(""); outs = _outs.toLowerCase().split("");
if (data.params) { if (data.params) {
data.params = data.params.map((p) => { data.params = data.params.map((p) => {
p.value = null; p.value = null;
if (p.default != null) p.value = p.default; if (p.default != null) p.value = p.default;
return p; return p;
}); });
} }
} }
data.nodeType = type; data.nodeType = type;
data.inputs = ins; data.inputs = ins;
data.outputs = outs; data.outputs = outs;
let node = { let node = {
id: uuidv4(), id: uuidv4(),
type: "ffmpeg", type: "ffmpeg",
data: data, data: data,
nodeType: type, nodeType: type,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
}; };
nodes.update((_nodes) => { nodes.update((_nodes) => {
_nodes.push(node); _nodes.push(node);
const isAuto = get(auto); const isAuto = get(auto);
if (isAuto) { if (isAuto) {
const w = 120; const w = 120;
const h = 50; const h = 50;
const margin = 50; const margin = 50;
let prev = null; let prev = null;
for (let n of _nodes) { for (let n of _nodes) {
if (n.nodeType === "input") { if (n.nodeType === "input") {
n.position = { x: 0, y: prev ? prev.position.y + h + margin : 0 }; n.position = { x: 0, y: prev ? prev.position.y + h + margin : 0 };
prev = n; prev = n;
} }
} }
for (let n of _nodes) { for (let n of _nodes) {
if (n.nodeType === "filter") { if (n.nodeType === "filter") {
let _w = prev && prev.width ? prev.width : w; let _w = prev && prev.width ? prev.width : w;
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -50 }; n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -50 };
prev = n; prev = n;
} }
} }
for (let n of _nodes) { for (let n of _nodes) {
if (n.nodeType === "output") { if (n.nodeType === "output") {
let _w = prev && prev.width ? prev.width : w; let _w = prev && prev.width ? prev.width : w;
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: 0 }; n.position = { x: prev ? prev.position.x + _w + margin : 0, y: 0 };
} }
} }
} }
if (node.nodeType === "filter") { if (node.nodeType === "filter") {
selectedFilter.set(_nodes.length - 1); selectedFilter.set(_nodes.length - 1);
} }
return _nodes; return _nodes;
}); });
} }
export function resetNodes() { export function resetNodes() {
@ -262,17 +296,17 @@ export function resetNodes() {
} }
export function removeNode(id) { export function removeNode(id) {
nodes.update((_nodes) => { nodes.update((_nodes) => {
const index = _nodes.findIndex((n) => n.id === id); const index = _nodes.findIndex((n) => n.id === id);
_nodes.splice(index, 1); _nodes.splice(index, 1);
return _nodes; return _nodes;
}); });
} }
export function removeEdge(id) { export function removeEdge(id) {
edges.update((_edges) => { edges.update((_edges) => {
const index = _edges.findIndex((e) => e.id === id); const index = _edges.findIndex((e) => e.id === id);
_edges.splice(index, 1); _edges.splice(index, 1);
return _edges; return _edges;
}); });
} }

View File

@ -1,6 +1,6 @@
import { expect, test, describe } from "vitest"; import { expect, test, describe } from "vitest";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { nodes, edges, addNode, resetNodes, makeFilterArgs, previewCommand } from "../../src/stores.js"; import { addNode, resetNodes, makeFilterArgs, previewCommand } from "../../src/stores.js";
describe("Filter param builder", () => { describe("Filter param builder", () => {
test("No params", () => { test("No params", () => {
@ -58,7 +58,7 @@ describe("Command builder", () => {
resetNodes(); resetNodes();
addNode({ name: "filter", type: "V->V" }, "filter"); addNode({ name: "filter", type: "V->V" }, "filter");
expect(get(previewCommand)).toBe( expect(get(previewCommand)).toBe(
`ffmpeg -i punch.mp4 -filter_complex "[0:v]filter[vid_out]" -map 0:a -map "[vid_out]" out.mp4` `ffmpeg -i punch.mp4 -filter_complex "[0:v]filter[out_v]" -map "[out_v]" -map 0:a out.mp4`
); );
}); });
@ -66,7 +66,36 @@ describe("Command builder", () => {
resetNodes(); resetNodes();
addNode({ name: "filter", type: "A->A" }, "filter"); addNode({ name: "filter", type: "A->A" }, "filter");
expect(get(previewCommand)).toBe( expect(get(previewCommand)).toBe(
`ffmpeg -i punch.mp4 -filter_complex "[0:a]filter[aud_out]" -map "[aud_out]" -map 0:v out.mp4` `ffmpeg -i punch.mp4 -filter_complex "[0:a]filter[out_a]" -map "[out_a]" -map 0:v out.mp4`
);
});
test("One audio, one video filter", () => {
resetNodes();
addNode({ name: "afilter", type: "A->A" }, "filter");
addNode({ name: "vfilter", type: "V->V" }, "filter");
expect(get(previewCommand)).toBe(
`ffmpeg -i punch.mp4 -filter_complex "[0:a]afilter[out_a];[0:v]vfilter[out_v]" -map "[out_a]" -map "[out_v]" out.mp4`
);
});
test("video filter chain", () => {
resetNodes();
addNode({ name: "vfilter", type: "V->V" }, "filter");
addNode({ name: "vfilter2", type: "V->V" }, "filter");
addNode({ name: "vfilter3", type: "V->V" }, "filter");
expect(get(previewCommand)).toBe(
`ffmpeg -i punch.mp4 -filter_complex "[0:v]vfilter,vfilter2,vfilter3[out_v]" -map "[out_v]" -map 0:a out.mp4`
);
});
test("One audio, two video filters", () => {
resetNodes();
addNode({ name: "afilter", type: "A->A" }, "filter");
addNode({ name: "vfilter", type: "V->V" }, "filter");
addNode({ name: "vfilter2", type: "V->V" }, "filter");
expect(get(previewCommand)).toBe(
`ffmpeg -i punch.mp4 -filter_complex "[0:v]vfilter[1];[0:a]afilter[out_a];[1]vfilter2[out_v]" -map "[out_a]" -map "[out_v]" out.mp4`
); );
}); });
}); });