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 Filter from "./Filter.svelte";
import FilterPicker from "./FilterPicker.svelte";
import Modal from "./Modal.svelte";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
const baseURL = "https://unpkg.com/@ffmpeg/core-mt@0.12.2/dist/esm";
// const baseURL = "";
// const videoURL = "https://ffmpegwasm.netlify.app/video/video-15s.avi";
const videoURL = "/example.mp4";
const TIMEOUT = 40000;
const ffmpeg = new FFmpeg();
let showFilterModal = false;
let command = "";
let message = "";
let videoValue = null;
let videoValue = "/" + $inputs[0];
let ffmpegLoaded = false;
let rendering = false;
let log = "";
let logbox;
let commandRef;
function newInput() {
$inputs = [...$inputs, ""];
}
function newFilter() {
showFilterModal = true;
$inputs = [...$inputs, "punch.mp4"];
}
function render() {
transcode();
}
function copyCommand() {
commandRef.select();
document.execCommand("copy");
}
async function transcode() {
// try {
message = "Start transcoding";
videoValue = null;
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");
// videoValue = URL.createObjectURL(new Blob([infile.buffer], { type: "video/mp4" }));
// console.log("VIDEO", videoValue);
@ -65,22 +68,7 @@
const cOutput = $output;
const cFilters = $filters
.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(",");
const cFilters = $filters.map(makeFilterArgs).join(",");
let out = `ffmpeg ${cInputs}`;
@ -102,16 +90,21 @@
})
.filter((p) => p !== null)
.join(":");
fCommand += "=" + params;
if (params) fCommand += "=" + params;
}
return fCommand;
}
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 videoFilters = $filters.filter(f => f.type[0] === "V").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 cFilters = $filters.map(makeFilterArgs).join(",");
@ -133,6 +126,8 @@
onMount(async () => {
ffmpeg.on("log", ({ message: msg }) => {
console.log(msg);
log += msg + "\n";
logbox.scrollTop = logbox.scrollHeight;
// message = msg;
});
await ffmpeg.load({
@ -143,68 +138,192 @@
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, "text/javascript"),
});
console.log(ffmpeg);
ffmpegLoaded = true;
});
</script>
<main>
{message}
<section class="command">{command}</section>
<section class="header">
<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">
<h3>Inputs</h3>
<div class="section-header">
<h3>Inputs</h3>
<button on:click={newInput}>Add Input</button>
</div>
{#each $inputs as inp, index}
<Input bind:filename={inp} {index} />
{/each}
<button on:click={newInput}>New Input</button>
{#each $inputs as inp}
<p>{inp}</p>
{/each}
</section>
<section class="filters">
<!-- {JSON.stringify($filters)} -->
<h3>Filters</h3>
<button on:click={newFilter}>Add Filter</button>
<Modal bind:showModal={showFilterModal}>
<FilterPicker bind:showFilterModal />
</Modal>
<div class="filters-holder">
{#each $filters as f, index}
<div class="filter">
<Filter bind:filter={f} {index} />
</div>
{/each}
</div>
<section class="log">
<h3>FFmpeg Log</h3>
<textarea class="the-log" bind:this={logbox}>{log}</textarea>
</section>
<section class="preview">
<video controls src={videoValue} />
</section>
<section class="output">
<h3>Output</h3>
<Output bind:filename={$output} />
<button on:click={render} disabled={!ffmpegLoaded}>
{#if ffmpegLoaded}
{#if rendering}
Rendering...
{:else}
Render
{/if}
{:else}
Loading ffmpeg
{/if}
</button>
{#if videoValue}
<video controls src={videoValue} />
{/if}
</section>
<section class="filters">
<h3>Filters</h3>
<div class="inner-filters">
<div class="filter-picker">
<FilterPicker select={"video"} />
</div>
<div class="filters-holder">
{#each $filters as f, index}
<div class="filter">
<Filter bind:filter={f} {index} />
</div>
{/each}
</div>
</div>
</section>
</main>
<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 {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding-left: 10px;
flex: 1;
align-content: start;
}
.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>

View File

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

View File

@ -3,37 +3,55 @@
import FILTERS from "./filters.json";
import { filters } from "./stores.js";
let allfilters = [...FILTERS];
export let select = "video";
$: selectedFilters = selectFilters(select);
$: allfilters = [...selectedFilters];
let q = "";
export let showFilterModal;
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) {
const newFilter = { ...f };
newFilter.params = f.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
if (f.params) {
newFilter.params = f.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
}
$filters = [...$filters, newFilter];
showFilterModal = false;
console.log(newFilter);
}
function update() {
let newFilters = [];
const [idxs, info, order] = uf.search(
FILTERS.map((m) => m.name + " " + m.description),
selectedFilters.map((m) => m.name + " " + m.description),
q
);
if (idxs) {
for (let i of idxs) {
newFilters.push(FILTERS[i]);
newFilters.push(selectedFilters[i]);
}
allfilters = newFilters;
} else {
allfilters = FILTERS;
allfilters = [...selectedFilters];
}
}
</script>
@ -41,11 +59,15 @@
<div class="holder">
<div class="search">
<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 class="all-filters">
{#each allfilters as 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>
{/each}
@ -56,8 +78,13 @@
.holder {
display: flex;
flex-direction: column;
height: 400px;
height: 100%;
padding: 10px;
}
.type {
color: #999;
font-size: 0.8em;
}
.filter {
border-bottom: 1px solid #000;

View File

@ -1,15 +1,30 @@
<script>
import { inputs } from './stores.js';
export let filename="";
export let index;
import { inputs } from "./stores.js";
export let filename = "punch.mp4";
export let index;
function remove() {
$inputs.splice(index, 1);
$inputs = $inputs;
}
function remove() {
$inputs.splice(index, 1);
$inputs = $inputs;
}
</script>
<div>
<input bind:value={filename} />
<button on:click={remove}>Remove</button>
<select bind:value={filename}>
<option value="punch.mp4">punch.mp4</option>
<option value="shoe.mp4">shoe.mp4</option>
</select>
<button on:click={remove}>X</button>
</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 {
}
a {
color: #000;
}
a:hover {
}
body {
background-color: #eee;
font: 16px Times, serif;
}
textrea, select, input, button {
font: inherit;
}
video {
width: 100%;
}
h1 {

View File

@ -1,5 +1,5 @@
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 filters = writable([]);