a bit better

This commit is contained in:
Sam Lavigne 2023-08-19 17:31:55 -04:00
parent 0fcdfacda8
commit f708f57b37
6 changed files with 277 additions and 95 deletions

View File

@ -5,43 +5,46 @@
import Output from "./Output.svelte"; import Output from "./Output.svelte";
import Filter from "./Filter.svelte"; import Filter from "./Filter.svelte";
import FilterPicker from "./FilterPicker.svelte"; import FilterPicker from "./FilterPicker.svelte";
import Modal from "./Modal.svelte";
import { FFmpeg } from "@ffmpeg/ffmpeg"; import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util"; import { fetchFile, toBlobURL } from "@ffmpeg/util";
const baseURL = "https://unpkg.com/@ffmpeg/core-mt@0.12.2/dist/esm"; const baseURL = "https://unpkg.com/@ffmpeg/core-mt@0.12.2/dist/esm";
// const baseURL = ""; // const baseURL = "";
// const videoURL = "https://ffmpegwasm.netlify.app/video/video-15s.avi"; // const videoURL = "https://ffmpegwasm.netlify.app/video/video-15s.avi";
const videoURL = "/example.mp4";
const TIMEOUT = 40000; const TIMEOUT = 40000;
const ffmpeg = new FFmpeg(); const ffmpeg = new FFmpeg();
let showFilterModal = false;
let command = ""; let command = "";
let message = ""; let message = "";
let videoValue = null; let videoValue = "/" + $inputs[0];
let ffmpegLoaded = false; let ffmpegLoaded = false;
let rendering = false; let rendering = false;
let log = "";
let logbox;
let commandRef;
function newInput() { function newInput() {
$inputs = [...$inputs, ""]; $inputs = [...$inputs, "punch.mp4"];
}
function newFilter() {
showFilterModal = true;
} }
function render() { function render() {
transcode(); transcode();
} }
function copyCommand() {
commandRef.select();
document.execCommand("copy");
}
async function transcode() { async function transcode() {
// try { // try {
message = "Start transcoding"; message = "Start transcoding";
videoValue = null; videoValue = null;
rendering = true; rendering = true;
await ffmpeg.writeFile("example.mp4", await fetchFile(videoURL)); for (let vid of $inputs) {
await ffmpeg.writeFile(vid, await fetchFile("/" + vid));
}
// const infile = await ffmpeg.readFile("example.mp4"); // const infile = await ffmpeg.readFile("example.mp4");
// videoValue = URL.createObjectURL(new Blob([infile.buffer], { type: "video/mp4" })); // videoValue = URL.createObjectURL(new Blob([infile.buffer], { type: "video/mp4" }));
// console.log("VIDEO", videoValue); // console.log("VIDEO", videoValue);
@ -65,22 +68,7 @@
const cOutput = $output; const cOutput = $output;
const cFilters = $filters const cFilters = $filters.map(makeFilterArgs).join(",");
.map((f) => {
let fCommand = f.name;
if (f.params && f.params.length > 0) {
let params = f.params
.map((p) => {
if (p.value === "" || p.value === null) return null;
return `${p.name}=${p.value}`;
})
.filter((p) => p !== null)
.join(":");
fCommand += "=" + params;
}
return fCommand;
})
.join(",");
let out = `ffmpeg ${cInputs}`; let out = `ffmpeg ${cInputs}`;
@ -102,16 +90,21 @@
}) })
.filter((p) => p !== null) .filter((p) => p !== null)
.join(":"); .join(":");
fCommand += "=" + params; if (params) fCommand += "=" + params;
} }
return fCommand; return fCommand;
} }
function commandList() { function commandList() {
let command = ["-i", "example.mp4"]; let command = [];
for (let vid of $inputs) {
command.push("-i");
command.push(vid);
}
// let command = ["-i", "example.mp4"];
// const audioFilters = $filters.filter(f => f.type[0] === "A").map(makeFilterArgs); // const audioFilters = $filters.filter(f => f.type[0] === "A").map(makeFilterArgs);
// const videoFilters = $filters.filter(f => f.type[0] === "V").map(makeFilterArgs); // const videoFilters = $filters.filter(f => f.type[0] === "V").map(makeFilterArgs);
const cFilters = $filters.map(makeFilterArgs).join(","); const cFilters = $filters.map(makeFilterArgs).join(",");
@ -133,6 +126,8 @@
onMount(async () => { onMount(async () => {
ffmpeg.on("log", ({ message: msg }) => { ffmpeg.on("log", ({ message: msg }) => {
console.log(msg); console.log(msg);
log += msg + "\n";
logbox.scrollTop = logbox.scrollHeight;
// message = msg; // message = msg;
}); });
await ffmpeg.load({ await ffmpeg.load({
@ -143,68 +138,192 @@
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, "text/javascript"), workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, "text/javascript"),
}); });
console.log(ffmpeg);
ffmpegLoaded = true; ffmpegLoaded = true;
}); });
</script> </script>
<main> <main>
{message} <section class="header">
<section class="command">{command}</section> <h1>FFmpeg Explorer</h1>
<p>
A tool to help you explore FFmpeg filters and options. To use: select one or more input videos
(there are currently two options), export and add some filters, and then hit "render" to
preview the output in browser.
Note: this is a work in progress, many things may still be broken! By <a href="https://lav.io"
>Sam Lavigne</a
>.
</p>
</section>
<!-- {message} -->
<section class="command">
<textarea class="actual-command" bind:this={commandRef}>{command}</textarea>
<div>
<button on:click={copyCommand}>Copy</button>
<button on:click={render} disabled={!ffmpegLoaded}>
{#if ffmpegLoaded}
{#if rendering}
Rendering...
{:else}
Render
{/if}
{:else}
Loading ffmpeg
{/if}
</button>
</div>
</section>
<section class="inputs"> <section class="inputs">
<h3>Inputs</h3> <div class="section-header">
<h3>Inputs</h3>
<button on:click={newInput}>Add Input</button>
</div>
{#each $inputs as inp, index} {#each $inputs as inp, index}
<Input bind:filename={inp} {index} /> <Input bind:filename={inp} {index} />
{/each} {/each}
<button on:click={newInput}>New Input</button>
{#each $inputs as inp}
<p>{inp}</p>
{/each}
</section> </section>
<section class="filters"> <section class="log">
<!-- {JSON.stringify($filters)} --> <h3>FFmpeg Log</h3>
<h3>Filters</h3> <textarea class="the-log" bind:this={logbox}>{log}</textarea>
<button on:click={newFilter}>Add Filter</button> </section>
<Modal bind:showModal={showFilterModal}>
<FilterPicker bind:showFilterModal /> <section class="preview">
</Modal> <video controls src={videoValue} />
<div class="filters-holder">
{#each $filters as f, index}
<div class="filter">
<Filter bind:filter={f} {index} />
</div>
{/each}
</div>
</section> </section>
<section class="output"> <section class="output">
<h3>Output</h3> <h3>Output</h3>
<Output bind:filename={$output} /> <Output bind:filename={$output} />
<button on:click={render} disabled={!ffmpegLoaded}> </section>
{#if ffmpegLoaded}
{#if rendering} <section class="filters">
Rendering... <h3>Filters</h3>
{:else} <div class="inner-filters">
Render <div class="filter-picker">
{/if} <FilterPicker select={"video"} />
{:else} </div>
Loading ffmpeg <div class="filters-holder">
{/if} {#each $filters as f, index}
</button> <div class="filter">
{#if videoValue} <Filter bind:filter={f} {index} />
<video controls src={videoValue} /> </div>
{/if} {/each}
</div>
</div>
</section> </section>
</main> </main>
<style> <style>
main {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-areas:
"hdr hdr hdr"
"cmd cmd cmd"
"inp log prv"
"out log prv"
"flt flt flt";
grid-gap: 20px;
}
section {
border: 1px solid #999;
padding: 10px;
background-color: rgb(245, 245, 245);
}
.header {
grid-area: hdr;
}
.command {
grid-area: cmd;
}
.inputs {
grid-area: inp;
}
.preview {
grid-area: prv;
}
.output {
grid-area: out;
}
.log {
grid-area: log;
display: flex;
flex-direction: column;
}
.inner-filters {
display: flex;
}
.filters {
grid-area: flt;
}
.filter-picker {
max-height: 500px;
max-width: 400px;
position: sticky;
top: 0;
left: 0;
overflow: scroll;
background-color: #fff;
}
.filters-holder { .filters-holder {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding-left: 10px;
flex: 1;
align-content: start;
} }
.filter { .filter {
margin: 10px; /* width: 33%; */
}
h1,
h2,
h3 {
font-weight: normal;
margin: 0;
padding: 0;
}
h3 {
margin-bottom: 10px;
}
.header p {
margin: 0;
}
.command {
display: flex;
align-items: center;
margin: 10px 0px;
}
.actual-command {
border: none;
margin-right: 10px;
resize: none;
flex: 1;
font: inherit;
}
.section-header {
display: flex;
}
.section-header h3 {
flex: 1;
}
.the-log {
border: none;
resize: none;
flex: 1;
} }
</style> </style>

View File

@ -9,6 +9,7 @@
}; };
export let index; export let index;
let show = false;
function remove() { function remove() {
$filters.splice(index, 1); $filters.splice(index, 1);
@ -18,13 +19,16 @@
<div class="filter-holder"> <div class="filter-holder">
<div class="head"> <div class="head">
<div class="name">{filter.name}</div> <div class="name"><h3>{filter.name}<h3></div>
<button on:click={remove}>X</button> <div>
<button on:click={() => show = !show}>{show ? "Hide" : "Show"} Options</button>
<button on:click={remove}>X</button>
</div>
</div> </div>
<div class="description">{filter.description}</div> <div class="description">{filter.description}</div>
{#if filter.params.length > 0} {#if filter.params && filter.params.length > 0 && show}
<div class="options"> <div class="options">
{#each filter.params as p} {#each filter.params as p}
<div class="param-holder"> <div class="param-holder">
@ -41,7 +45,7 @@
{/each} {/each}
</select> </select>
{:else if p.type == "float" || p.type == "double" || p.type == "long" || p.type == "int"} {:else if p.type == "float" || p.type == "double" || p.type == "long" || p.type == "int"}
<input type="range" min={p.min} max={p.max} bind:value={p.value} /> <input step={p.type == "int" ? 1 : 0.01 } type="range" min={p.min} max={p.max} bind:value={p.value} />
<input bind:value={p.value} /> <input bind:value={p.value} />
{:else} {:else}
<input bind:value={p.value} /> <input bind:value={p.value} />
@ -56,6 +60,12 @@
</div> </div>
<style> <style>
h3 {
font-weight: normal;
margin: 0;
padding: 0;
font-size: 18px;
}
.filter-holder { .filter-holder {
background-color: #fff; background-color: #fff;
padding: 10px; padding: 10px;

View File

@ -3,37 +3,55 @@
import FILTERS from "./filters.json"; import FILTERS from "./filters.json";
import { filters } from "./stores.js"; import { filters } from "./stores.js";
let allfilters = [...FILTERS]; export let select = "video";
$: selectedFilters = selectFilters(select);
$: allfilters = [...selectedFilters];
let q = ""; let q = "";
export let showFilterModal;
const uf = new uFuzzy(); const uf = new uFuzzy();
function selectFilters(sel) {
if (sel == "video") {
return FILTERS.filter(f => f.type[0] === "V")
} else if (sel == "audio") {
return FILTERS.filter(f => f.type[0] === "A")
} else {
return [...FILTERS];
}
}
function reset() {
console.log('ressetting', select)
selectedFilters = selectFilters(select);
q = "";
}
function add(f) { function add(f) {
const newFilter = { ...f }; const newFilter = { ...f };
newFilter.params = f.params.map((p) => { if (f.params) {
p.value = null; newFilter.params = f.params.map((p) => {
if (p.default != null) p.value = p.default; p.value = null;
return p; if (p.default != null) p.value = p.default;
}); return p;
});
}
$filters = [...$filters, newFilter]; $filters = [...$filters, newFilter];
showFilterModal = false;
console.log(newFilter); console.log(newFilter);
} }
function update() { function update() {
let newFilters = []; let newFilters = [];
const [idxs, info, order] = uf.search( const [idxs, info, order] = uf.search(
FILTERS.map((m) => m.name + " " + m.description), selectedFilters.map((m) => m.name + " " + m.description),
q q
); );
if (idxs) { if (idxs) {
for (let i of idxs) { for (let i of idxs) {
newFilters.push(FILTERS[i]); newFilters.push(selectedFilters[i]);
} }
allfilters = newFilters; allfilters = newFilters;
} else { } else {
allfilters = FILTERS; allfilters = [...selectedFilters];
} }
} }
</script> </script>
@ -41,11 +59,15 @@
<div class="holder"> <div class="holder">
<div class="search"> <div class="search">
<input placeholder="Search Filters" on:keyup={update} bind:value={q} /> <input placeholder="Search Filters" on:keyup={update} bind:value={q} />
<select on:change={reset} bind:value={select}>
<option value="video">Video Filters</option>
<option value="audio">Audio Filters</option>
</select>
</div> </div>
<div class="all-filters"> <div class="all-filters">
{#each allfilters as f} {#each allfilters as f}
<div class="filter" on:click={() => add(f)}> <div class="filter" on:click={() => add(f)}>
<div class="name">{f.name} <span class="type">{f.type}</span></div> <div class="name">{f.name} <span class="type">{f.type.replace("->", "⇒")}</span></div>
<div class="desc">{f.description}</div> <div class="desc">{f.description}</div>
</div> </div>
{/each} {/each}
@ -56,8 +78,13 @@
.holder { .holder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 400px; height: 100%;
padding: 10px;
} }
.type {
color: #999;
font-size: 0.8em;
}
.filter { .filter {
border-bottom: 1px solid #000; border-bottom: 1px solid #000;

View File

@ -1,15 +1,30 @@
<script> <script>
import { inputs } from './stores.js'; import { inputs } from "./stores.js";
export let filename=""; export let filename = "punch.mp4";
export let index; export let index;
function remove() { function remove() {
$inputs.splice(index, 1); $inputs.splice(index, 1);
$inputs = $inputs; $inputs = $inputs;
} }
</script> </script>
<div> <div>
<input bind:value={filename} /> <select bind:value={filename}>
<button on:click={remove}>Remove</button> <option value="punch.mp4">punch.mp4</option>
<option value="shoe.mp4">shoe.mp4</option>
</select>
<button on:click={remove}>X</button>
</div> </div>
<style>
div {
margin-top: 10px;
margin-bottom: 10px;
display: flex;
}
select {
flex: 1;
margin-right: 10px;
}
</style>

View File

@ -1,13 +1,24 @@
* {
box-sizing: border-box;
}
:root { :root {
} }
a { a {
color: #000;
} }
a:hover { a:hover {
} }
body { body {
background-color: #eee; background-color: #eee;
font: 16px Times, serif;
}
textrea, select, input, button {
font: inherit;
}
video {
width: 100%;
} }
h1 { h1 {

View File

@ -1,5 +1,5 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const inputs = writable(["example.mp4"]); export const inputs = writable(["punch.mp4"]);
export const output = writable("out.mp4"); export const output = writable("out.mp4");
export const filters = writable([]); export const filters = writable([]);