a bit better
This commit is contained in:
parent
0fcdfacda8
commit
f708f57b37
255
src/App.svelte
255
src/App.svelte
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
11
src/app.css
11
src/app.css
|
@ -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 {
|
||||
|
|
|
@ -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([]);
|
||||
|
|
Loading…
Reference in New Issue