ffmpeg-explorer/src/App.svelte

389 lines
8.9 KiB
Svelte
Raw Normal View History

2023-08-18 17:23:56 -04:00
<script>
2023-08-19 15:17:59 -04:00
import { onMount } from "svelte";
2023-08-22 21:57:53 -04:00
import { addNode, nodes, inputs, output, filters, previewCommand } from "./stores.js";
2023-08-18 17:23:56 -04:00
import Input from "./Input.svelte";
import Output from "./Output.svelte";
import Filter from "./Filter.svelte";
2023-08-18 18:59:16 -04:00
import FilterPicker from "./FilterPicker.svelte";
2023-08-23 19:30:04 -04:00
// import Graph from "./Graph.svelte";
2023-08-23 18:10:28 -04:00
import GraphOld from "./GraphOld.svelte";
2023-08-19 13:01:37 -04:00
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
2023-08-20 12:08:46 -04:00
import { dndzone } from "svelte-dnd-action";
2023-08-19 13:01:37 -04:00
2023-08-20 12:08:46 -04:00
const isChrome = navigator.userAgent.match(/chrome|chromium|crios/i);
const baseURL = `https://unpkg.com/@ffmpeg/core${!isChrome ? "-mt" : ""}@0.12.2/dist/esm`;
2023-08-19 15:17:59 -04:00
// const baseURL = "";
const TIMEOUT = 40000;
2023-08-19 13:01:37 -04:00
const ffmpeg = new FFmpeg();
2023-08-18 18:59:16 -04:00
2023-08-19 13:01:37 -04:00
let command = "";
2023-08-22 12:05:49 -04:00
let videoValue = "/" + $inputs[0].name;
2023-08-19 15:17:59 -04:00
let ffmpegLoaded = false;
let rendering = false;
2023-08-19 17:31:55 -04:00
let log = "";
let logbox;
let commandRef;
2023-08-18 17:23:56 -04:00
function newInput() {
2023-08-22 21:57:53 -04:00
addNode({ name: "punch.mp4" }, "input");
2023-08-18 17:23:56 -04:00
}
2023-08-19 13:01:37 -04:00
function render() {
transcode();
}
2023-08-18 17:23:56 -04:00
2023-08-19 17:31:55 -04:00
function copyCommand() {
commandRef.select();
document.execCommand("copy");
}
2023-08-19 13:01:37 -04:00
async function transcode() {
2023-08-19 15:17:59 -04:00
videoValue = null;
rendering = true;
2023-08-20 13:44:36 -04:00
try {
for (let vid of $inputs) {
2023-08-22 12:05:49 -04:00
await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
2023-08-20 13:44:36 -04:00
}
2023-08-23 16:27:48 -04:00
let clist = $previewCommand.replaceAll('"', '').replace("ffmpeg", "").split(" ").filter(i => i.trim() != '');
console.log("command", clist);
// command.push("-pix_fmt");
// command.push("yuv420p");
// command.push("out.mp4");
2023-08-20 13:44:36 -04:00
await ffmpeg.exec(clist, TIMEOUT);
// await ffmpeg.exec(["-f", "lavfi", "-i", "color=size=1280x720:rate=25:color=red", "-t", "5", "out.mp4"])
const data = await ffmpeg.readFile("out.mp4");
rendering = false;
videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
} catch (e) {
2023-08-23 16:27:48 -04:00
console.log(e);
2023-08-20 13:44:36 -04:00
log += "Failed";
2023-08-19 17:31:55 -04:00
}
2023-08-19 15:17:59 -04:00
rendering = false;
2023-08-19 13:01:37 -04:00
}
2023-08-18 17:23:56 -04:00
2023-08-20 12:08:46 -04:00
async function loadFFmpeg() {
2023-08-20 12:01:50 -04:00
ffmpeg.on("log", ({ message: msg }) => {
log += msg + "\n";
logbox.scrollTop = logbox.scrollHeight;
});
2023-08-20 12:08:46 -04:00
if (isChrome) {
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
// workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, "text/javascript"),
});
} else {
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, "text/javascript"),
});
}
2023-08-20 12:01:50 -04:00
ffmpegLoaded = true;
2023-08-20 12:08:46 -04:00
}
2023-08-20 12:01:50 -04:00
2023-08-18 17:23:56 -04:00
2023-08-19 15:17:59 -04:00
2023-08-19 13:01:37 -04:00
2023-08-20 12:08:46 -04:00
function handleFilterSort(e) {
filters.set(e.detail.items);
}
2023-08-20 12:01:50 -04:00
2023-08-22 21:57:53 -04:00
// inputs.subscribe(updateCommand);
// output.subscribe(updateCommand);
// filters.subscribe(updateCommand);
2023-08-19 13:01:37 -04:00
2023-08-19 15:17:59 -04:00
onMount(async () => {
2023-08-20 12:08:46 -04:00
loadFFmpeg();
2023-08-19 15:17:59 -04:00
});
2023-08-18 17:23:56 -04:00
</script>
<main>
2023-08-19 17:31:55 -04:00
<section class="header">
<h1>FFmpeg Explorer</h1>
<p>
2023-08-20 13:44:36 -04:00
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
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! Only audio to audio and video to video
filters are included. If it hangs/crashes refresh the page. Post issues/feedback to
2023-08-22 21:57:53 -04:00
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
2023-08-19 17:31:55 -04:00
</p>
</section>
<!-- {message} -->
<section class="command">
2023-08-20 16:11:45 -04:00
<h3>Output Command</h3>
<div class="inner-command">
2023-08-22 21:57:53 -04:00
<textarea readonly class="actual-command" bind:this={commandRef}>{$previewCommand}</textarea>
2023-08-20 16:11:45 -04:00
<div>
<button on:click={copyCommand}>Copy Command</button>
</div>
2023-08-19 17:31:55 -04:00
</div>
</section>
2023-08-18 17:23:56 -04:00
<section class="inputs">
2023-08-19 17:31:55 -04:00
<div class="section-header">
<h3>Inputs</h3>
<button on:click={newInput}>Add Input</button>
</div>
2023-08-22 21:57:53 -04:00
{#each $nodes as node, index}
{#if node.nodeType === "input"}
<Input bind:filename={node.data.name} id={node.id} {index} />
{/if}
2023-08-18 17:23:56 -04:00
{/each}
2023-08-19 17:31:55 -04:00
</section>
2023-08-18 17:23:56 -04:00
2023-08-19 17:31:55 -04:00
<section class="log">
<h3>FFmpeg Log</h3>
2023-08-20 12:01:50 -04:00
<textarea readonly class="the-log" bind:this={logbox}>{log}</textarea>
2023-08-18 17:23:56 -04:00
</section>
2023-08-19 17:31:55 -04:00
<section class="preview">
2023-08-20 12:08:46 -04:00
{#if rendering}
<div class="rendering-video"><span>Rendering...</span></div>
{/if}
2023-08-19 17:31:55 -04:00
<video controls src={videoValue} />
2023-08-20 16:11:45 -04:00
<div style="text-align: right;margin-top:5px;">
2023-08-20 15:02:07 -04:00
<button on:click={render} disabled={!ffmpegLoaded || rendering}>
{#if ffmpegLoaded}
{#if rendering}
Rendering...
{:else}
Render Preview
{/if}
{:else}
Loading ffmpeg
{/if}
</button>
2023-08-20 16:11:45 -04:00
</div>
2023-08-18 17:23:56 -04:00
</section>
<section class="output">
<h3>Output</h3>
2023-08-22 21:57:53 -04:00
{#each $nodes as node}
{#if node.nodeType==="output"}
<Output bind:filename={node.data.name} />
{/if}
{/each}
2023-08-19 17:31:55 -04:00
</section>
<section class="filters">
2023-08-20 14:08:08 -04:00
<h3>Filters (click to add)</h3>
2023-08-19 17:31:55 -04:00
<div class="inner-filters">
<div class="filter-picker">
<FilterPicker select={"video"} />
</div>
2023-08-20 12:08:46 -04:00
<div
class="filters-holder"
use:dndzone={{ items: $filters }}
on:consider={handleFilterSort}
on:finalize={handleFilterSort}
>
2023-08-22 21:57:53 -04:00
{#each $nodes as f (f.id)}
{#if f.nodeType === "filter"}
<div class="filter">
<Filter bind:filter={f.data} />
</div>
{/if}
2023-08-19 17:31:55 -04:00
{/each}
</div>
</div>
2023-08-18 17:23:56 -04:00
</section>
2023-08-22 12:05:49 -04:00
2023-08-22 21:57:53 -04:00
<section class="graph">
2023-08-23 18:10:28 -04:00
<GraphOld />
2023-08-22 21:57:53 -04:00
</section>
2023-08-23 18:10:28 -04:00
<!-- <section class="graph"> -->
<!-- <Graph /> -->
<!-- </section> -->
2023-08-18 17:23:56 -04:00
</main>
<style>
2023-08-19 17:31:55 -04:00
main {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-areas:
2023-08-20 15:02:07 -04:00
"hdr cmd cmd"
2023-08-19 17:31:55 -04:00
"inp log prv"
"out log prv"
"flt flt flt";
grid-gap: 20px;
2023-08-20 13:44:36 -04:00
padding: 20px;
2023-08-19 17:31:55 -04:00
}
section {
2023-08-20 14:08:08 -04:00
/* border: 1px solid #999; */
/* box-shadow: 7px 7px 0px rgba(0, 0, 0, 0.7); */
border: 1px solid var(--b1);
box-shadow: 7px 7px var(--b2);
2023-08-19 17:31:55 -04:00
padding: 10px;
background-color: rgb(245, 245, 245);
}
.header {
grid-area: hdr;
}
.command {
grid-area: cmd;
2023-08-20 16:11:45 -04:00
display: flex;
flex-direction: column;
2023-08-19 17:31:55 -04:00
}
.inputs {
grid-area: inp;
}
.preview {
grid-area: prv;
2023-08-20 12:08:46 -04:00
position: relative;
2023-08-19 17:31:55 -04:00
}
.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;
2023-08-20 13:44:36 -04:00
width: 400px;
2023-08-19 17:31:55 -04:00
position: sticky;
top: 0;
left: 0;
overflow: scroll;
background-color: #fff;
}
2023-08-18 17:23:56 -04:00
.filters-holder {
2023-08-20 13:44:36 -04:00
min-height: 500px;
2023-08-18 17:23:56 -04:00
display: grid;
grid-template-columns: repeat(3, 1fr);
2023-08-19 17:31:55 -04:00
grid-gap: 10px;
padding-left: 10px;
flex: 1;
align-content: start;
2023-08-18 17:23:56 -04:00
}
2023-08-19 17:31:55 -04:00
h1,
h3 {
font-weight: normal;
margin: 0;
padding: 0;
}
2023-08-20 13:44:36 -04:00
h1 {
margin-bottom: 5px;
}
2023-08-19 17:31:55 -04:00
h3 {
margin-bottom: 10px;
}
.header p {
margin: 0;
}
2023-08-20 15:02:07 -04:00
.inner-command {
2023-08-19 17:31:55 -04:00
display: flex;
align-items: center;
margin: 10px 0px;
2023-08-20 16:11:45 -04:00
flex: 1;
2023-08-19 17:31:55 -04:00
}
.actual-command {
border: none;
margin-right: 10px;
resize: none;
flex: 1;
font: inherit;
2023-08-20 13:44:36 -04:00
padding: 5px;
2023-08-20 16:11:45 -04:00
height: 100%;
2023-08-19 17:31:55 -04:00
}
.section-header {
display: flex;
}
.section-header h3 {
flex: 1;
}
.the-log {
border: none;
resize: none;
flex: 1;
2023-08-18 17:23:56 -04:00
}
2023-08-20 12:08:46 -04:00
.rendering-video {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
background-color: rgba(255, 255, 255, 0.8);
top: 0;
left: 0;
display: grid;
align-items: center;
justify-content: center;
}
2023-08-20 13:44:36 -04:00
2023-08-20 15:31:30 -04:00
@media only screen and (max-width: 1400px) {
.filters-holder {
grid-template-columns: repeat(2, 1fr);
}
}
2023-08-20 13:44:36 -04:00
@media only screen and (max-width: 600px) {
main {
grid-template-areas:
"hdr hdr hdr"
"cmd cmd cmd"
"inp inp inp"
"out out out"
"prv prv prv"
"log log log"
"flt flt flt";
grid-gap: 0px;
padding: 0px;
}
.command {
margin: 0;
margin-bottom: 10px;
}
section {
box-shadow: none;
margin-bottom: 10px;
padding: 10px;
box-shadow: 2px 2px 0px #000;
}
.inner-filters {
display: block;
}
.filter-picker {
width: 100%;
margin-bottom: 20px;
2023-08-20 14:08:08 -04:00
height: 300px;
position: static;
2023-08-20 13:44:36 -04:00
}
.filters-holder {
grid-template-columns: repeat(1, 1fr);
grid-gap: 10px;
flex: 1;
align-content: start;
2023-08-20 14:08:08 -04:00
padding: 0;
2023-08-20 13:44:36 -04:00
}
}
2023-08-18 17:23:56 -04:00
</style>