Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for N-to-N filters like "concat" and dynamic inputs for A/V-to-N and N-to-A/V cases #31

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
763 changes: 763 additions & 0 deletions public/examples/cut-bluer-grayscale.json

Large diffs are not rendered by default.

937 changes: 937 additions & 0 deletions public/examples/trim-concat.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
{ name: "Slow Down Smoothly", url: "/examples/smooth_slow.json" },
{ name: "Video Grid", url: "/examples/grid.json" },
{ name: "Mirror", url: "/examples/mirror.json" },
{ name: "Cut Parts From Video And Audio", url: "/examples/trim-concat.json" },
{ name: 'Cut Parts Apply Blur And Grayscale', url: '/examples/cut-bluer-grayscale.json' },
];

let videoValue = "/" + $inputs[0].name;
Expand Down Expand Up @@ -121,7 +123,7 @@
);
if (outname.endsWith("mp4")) {
setTimeout(() => {
vidPlayerRef.seekToNextFrame();
vidPlayerRef?.seekToNextFrame?.();
}, 100);
}
} catch (e) {
Expand Down
171 changes: 171 additions & 0 deletions src/DynamicFilterModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<script>
import Modal from "./Modal.svelte";
import { addNode } from "./stores.js";

export let selectedFilter = null;
export let showModal = false;
let modalInputs = [];
let modalOutputs = [];

function add(f) {
addNode(f, "filter");
}
</script>

{#if showModal}
<Modal
bind:showModal
onConfirm={() => {
// force update the ui
modalInputs = [
...selectedFilter.inputs.filter((i) => i === "v" || i === "a"),
...modalInputs,
];
modalOutputs = [
...selectedFilter.outputs.filter((i) => i === "v" || i === "a"),
...modalOutputs,
];

selectedFilter.inputs = modalInputs;
selectedFilter.outputs = modalOutputs;
selectedFilter.isCustom = true; // for handling the ordering of the inputs in the previewCommand in store.js

add(selectedFilter);
selectedFilter = null;
showModal = false;
}}
>
<h2 slot="header">
{selectedFilter.name}
<i
style="
color: #999;
font-size: 0.8em;
margin-left: 10px;
"
>(Either the input or output can have dynamic types or a variable number
of streams, and the order in which they are entered is important.)</i
>
<p>{selectedFilter.description}</p>
</h2>

<div slot="body" class="flex-row-start">
<div style="margin: 0.5em;" class="flex-col-center">
<label for="input">Input</label>
<hr style="width: 100%;" />

<ul>
{#if selectedFilter.inputs.length === 0}
<li>None</li>
{:else}
{#each selectedFilter.inputs as input}
{#if input === "v"}
<li>Video</li>
{:else if input === "a"}
<li>Audio</li>
{:else}
<button
on:click={() => {
modalInputs = [...modalInputs, "v"];
}}
>
+
</button>
{/if}
{/each}
{/if}

{#each modalInputs as customInput, i}
<li>
<select
on:change={(e) => {
modalInputs[i] = e.target.value;
}}
>
<option value="v" selected={customInput === "v"}>Video</option>
<option value="a" selected={customInput === "a"}>Audio</option>
</select>
</li>
{/each}
</ul>
</div>
<!-- vertical line -->
<div
style="
border-left: 1px solid #999;
height: 100px;
margin: 0.5em;
"
></div>

<div style="margin: 0.5em;" class="flex-col-center">
<label for="output">Output</label>
<hr style="width: 100%;" />
<ul class="reset">
{#if selectedFilter.outputs.length === 0}
<li>None</li>
{:else}
{#each selectedFilter.outputs as output}
{#if output === "v"}
<li>Video</li>
{:else if output === "a"}
<li>Audio</li>
{:else}
<button
on:click={() => {
modalOutputs = [...modalOutputs, "v"];
}}
>
+
</button>
{/if}
{/each}
{/if}

{#each modalOutputs as customOutput, i}
<li>
<select
on:change={(e) => {
modalOutputs[i] = e.target.value;
}}
>
<option value="v" selected={customOutput === "v"}>Video</option>
<option value="a" selected={customOutput === "a"}>Audio</option>
</select>
</li>
{/each}
</ul>
</div>
</div>
</Modal>
{/if}

<style>
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
margin: 0.5em 0;
padding: 0;
}

.flex-row-start {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.flex-col-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

button {
margin-left: 1px;
margin-right: 10px;
}
</style>
47 changes: 41 additions & 6 deletions src/FilterPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@
import uFuzzy from "@leeoniya/ufuzzy";
import FILTERS from "./filters.json";
import { addNode } from "./stores.js";
import DynamicFilterModal from "./DynamicFilterModal.svelte";

export let select = "video";
$: selectedFilters = selectFilters(select);
$: allfilters = [...selectedFilters];
let q = "";

let selectedFilter = null;
let showModal = false;

const uf = new uFuzzy();

function selectFilters(sel) {
if (sel == "video") {
// return FILTERS.filter((f) => f.type.startsWith("V") || f.type.endsWith("V"));
return FILTERS.filter((f) => f.type[0] === "V" || f.type === "N->V");
// add support to N->N inputs
return FILTERS.filter(
(f) => f.type[0] === "V" || f.type === "N->V" || f.type === "N->N"
);
} else if (sel == "audio") {
// return FILTERS.filter((f) => f.type.startsWith("A") || f.type.endsWith("A"));
return FILTERS.filter((f) => f.type[0] === "A" || f.type === "N->A");
// add support to N->N inputs
return FILTERS.filter(
(f) => f.type[0] === "A" || f.type === "N->A" || f.type === "N->N"
);
} else {
return [...FILTERS];
}
Expand Down Expand Up @@ -62,14 +70,41 @@
</div>
<div class="all-filters">
{#each allfilters as f}
<div class="filter" on:click={() => add(f)}>
<div class="name">{f.name} <span class="type">{f.type.replace("->", "⇒")}</span></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="filter"
on:click={() => {
// if input or output is N (dynamic) show modal
if (
f.type.startsWith("N") ||
f.type.endsWith("N") ||
f.type === "N"
) {
// deep copy current filter data so when overwriting it, it doesn't affect the original
selectedFilter = {...f, inputs: [...f.inputs], outputs: [...f.outputs]};
showModal = true;
} else {
add(f);
}
}}
>
<div class="name">
{f.name} <span class="type">{f.type.replace("->", "⇒")}</span>
</div>
<div class="desc">{f.description}</div>
</div>
{/each}
</div>
</div>

{#if showModal}
<DynamicFilterModal
bind:showModal
selectedFilter={selectedFilter}
/>
{/if}

<style>
.holder {
display: flex;
Expand Down
75 changes: 75 additions & 0 deletions src/Modal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script>
export let showModal; // boolean
export let onConfirm; // function
let dialog; // HTMLDialogElement

$: if (dialog && showModal) dialog.showModal();
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog
bind:this={dialog}
on:close={() => (showModal = false)}
on:click|self={() => dialog.close()}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click|stopPropagation>
<slot name="header" />
<hr />
<slot />
<!-- the rest is here -->

<slot name="body"></slot>

<div class="footer">
<button on:click={onConfirm}>Confirm</button>
<button on:click={() => (showModal = false)}>Cancel</button>
</div>
</div>
</dialog>

<style>
dialog {
max-width: 32em;
border-radius: 0.2em;
border: none;
padding: 0;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.3);
}
dialog > div {
padding: 1em;
}
dialog[open] {
animation: zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes zoom {
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
}
dialog[open]::backdrop {
animation: fade 0.2s ease-out;
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
button {
margin: 0.5em;
display: block;
}

.footer {
display: flex;
justify-content: flex-end;
}
</style>
24 changes: 16 additions & 8 deletions src/stores.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
let finalCommand = [];
let filtergraph = [];
let labelIndex = 1;
let outs = 0;
const edgeIds = {};
const inputIdMap = {};

Expand Down Expand Up @@ -70,7 +71,9 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
label = labelIndex;
labelIndex++;
} else if (inNode.nodeType === "filter" && outNode.nodeType === "output") {
label = "out_" + type;
// this was breaking when it was IDing by the type, as work around i made it current-outs-count based ID and it works
label = 'out_' + outs;
outs++
} else if (inNode.nodeType === "input" && outNode.nodeType === "output") {
label = "FILTERLESS_" + inputIdMap[inNode.id] + ":" + type;
} else {
Expand All @@ -97,7 +100,14 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
let cmd = { weight: 0, in: [], out: [], cmd: "" };

const outs = $edges.filter((e) => e.source == n.id);
const ins = $edges.filter((e) => e.target == n.id);
let ins = $edges.filter((e) => e.target == n.id)

// respect the user define input order (not just when the edges were created) this fixes a lot of issues whit complex filter like concat (v a v a) -> (v a)
if (n?.data?.isCustom) {
ins = ins.sort((a, b) => {
return Number(a.targetHandle.split('_')[1]) - Number(b.targetHandle.split('_')[1])
})
}

if (outs.length == 0 && ins.length == 0) continue;

Expand Down Expand Up @@ -155,13 +165,11 @@ export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {

finalCommand.push("-filter_complex", fg);

if (fg.includes("[out_a]")) {
finalCommand.push("-map", '"[out_a]"');
}
const getMappers = fg.match(/\[out_\d+\]/g) || [] // get all the out_0 out_1 etc

if (fg.includes("[out_v]")) {
finalCommand.push("-map", '"[out_v]"');
}
for (let m of getMappers) {
finalCommand.push('-map', `"${m}"`)
}

for (let m of mediaMaps) {
finalCommand.push("-map", m);
Expand Down
Loading