ffmpeg-explorer/src/App.svelte

365 lines
8.1 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-25 14:10:55 -04:00
import { selectedFilter, nodes, inputs, previewCommand } from "./stores.js";
2023-08-18 17:23:56 -04:00
import Filter from "./Filter.svelte";
2023-08-18 18:59:16 -04:00
import FilterPicker from "./FilterPicker.svelte";
2023-08-24 14:14:32 -04:00
import Graph from "./Graph.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
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-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
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-24 23:17:49 -04:00
let clist = $previewCommand
.replaceAll('"', "")
.replace("ffmpeg", "")
.split(" ")
.filter((i) => i.trim() != "");
clist.splice(clist.length-1, 0, "-pix_fmt")
clist.splice(clist.length-1, 0, "yuv420p")
2023-08-23 16:27:48 -04:00
console.log("command", clist);
2023-08-20 13:44:36 -04:00
await ffmpeg.exec(clist, TIMEOUT);
const data = await ffmpeg.readFile("out.mp4");
rendering = false;
videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
} catch (e) {
2023-08-24 23:17:49 -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-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>
2023-08-24 23:17:49 -04:00
<div class="help">
<p>
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
filters. To use:
</p>
<ol>
<li>Add filters from the list on the left.</li>
<li>Click on filters in the center panel to edit options.</li>
<li>Hit "render" to preview the output in browser.</li>
<li>For more complex filtergraphs, disable "automatic layout."</li>
</ol>
<p>
Note: this is a work in progress, many things may still be broken! If it hangs/crashes
refresh the page. Post issues/feedback to
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
</p>
</div>
2023-08-19 17:31:55 -04:00
</section>
<!-- {message} -->
<section class="command">
2023-08-20 16:11:45 -04:00
<h3>Output Command</h3>
<div class="inner-command">
2023-08-25 14:10:55 -04:00
<textarea
readonly
class="actual-command"
bind:this={commandRef}
on:click={() => commandRef.select()}>{$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>
<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-24 23:17:49 -04:00
<div class="vid-holder">
{#if rendering}
<div class="rendering-video"><span>Rendering...</span></div>
{/if}
<video controls src={videoValue} />
</div>
<div style="text-align: right;padding-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>
2023-08-19 17:31:55 -04:00
<section class="filters">
2023-08-20 14:08:08 -04:00
<h3>Filters (click to add)</h3>
2023-08-24 23:17:49 -04:00
<div class="filter-picker">
<FilterPicker select={"video"} />
2023-08-19 17:31:55 -04:00
</div>
2023-08-18 17:23:56 -04:00
</section>
2023-08-22 12:05:49 -04:00
2023-08-24 14:14:32 -04:00
<section class="graph">
<Graph />
</section>
2023-08-24 23:17:49 -04:00
<section class="filter-editor">
{#if $selectedFilter && $nodes.length > 0 && $nodes[$selectedFilter]}
<Filter bind:filter={$nodes[$selectedFilter].data} />
{/if}
</section>
2023-08-18 17:23:56 -04:00
</main>
<style>
2023-08-19 17:31:55 -04:00
main {
display: grid;
2023-08-24 23:17:49 -04:00
grid-template-columns: 300px 1fr 1fr 1fr 1fr 300px;
2023-08-19 17:31:55 -04:00
grid-template-areas:
2023-08-24 23:17:49 -04:00
"hdr log log log prv prv"
"hdr cmd cmd cmd prv prv"
"flt gra gra gra gra edt";
/* grid-template-rows: 16% 17% 77%; */
grid-template-rows: 15% 15% calc(70% - 40px);
padding: 10px;
2023-08-19 17:31:55 -04:00
grid-gap: 20px;
2023-08-24 23:17:49 -04:00
height: 100vh;
align-items: stretch;
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;
2023-08-24 23:17:49 -04:00
overflow: scroll;
2023-08-19 17:31:55 -04:00
}
.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
}
.preview {
grid-area: prv;
2023-08-20 12:08:46 -04:00
position: relative;
2023-08-24 23:17:49 -04:00
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
2023-08-19 17:31:55 -04:00
}
2023-08-24 23:17:49 -04:00
.preview video {
width: 100%;
/* object-fit: contain; */
flex: 1;
}
.vid-holder {
flex: 1;
display: flex;
width: 100%;
height: calc(100% - 30px);
2023-08-19 17:31:55 -04:00
}
.log {
grid-area: log;
display: flex;
flex-direction: column;
}
2023-08-24 23:17:49 -04:00
.filters {
grid-area: flt;
2023-08-19 17:31:55 -04:00
display: flex;
2023-08-24 23:17:49 -04:00
flex-direction: column;
2023-08-19 17:31:55 -04:00
}
2023-08-24 23:17:49 -04:00
.graph {
grid-area: gra;
display: flex;
2023-08-19 17:31:55 -04:00
}
2023-08-24 23:17:49 -04:00
.filter-editor {
grid-area: edt;
display: flex;
}
2023-08-20 13:44:36 -04:00
2023-08-24 23:17:49 -04:00
.filter-picker {
/* max-height: 500px; */
/* width: 400px; */
2023-08-19 17:31:55 -04:00
top: 0;
left: 0;
overflow: scroll;
background-color: #fff;
}
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;
2023-08-20 16:11:45 -04:00
flex: 1;
2023-08-19 17:31:55 -04:00
}
2023-08-24 23:17:49 -04:00
textarea {
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
resize: none;
}
2023-08-19 17:31:55 -04:00
.actual-command {
border: none;
margin-right: 10px;
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
}
.the-log {
border: none;
resize: none;
2023-08-24 23:17:49 -04:00
padding: 5px;
2023-08-19 17:31:55 -04:00
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-24 23:17:49 -04:00
.help {
font-size: 0.9em;
}
ol {
margin: 5px 0px;
padding-left: 20px;
}
ol li {
margin-bottom: 5px;
}
2023-08-20 15:31:30 -04:00
@media only screen and (max-width: 1400px) {
}
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"
"prv prv prv"
"log log log"
2023-08-25 14:10:55 -04:00
"flt flt flt"
"gra gra gra"
"edt edt edt";
grid-gap: 5px;
padding: 10px;
height: auto;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
}
.graph {
height: 50vh;
2023-08-20 13:44:36 -04:00
}
.command {
margin: 0;
margin-bottom: 10px;
}
section {
box-shadow: none;
margin-bottom: 10px;
padding: 10px;
2023-08-25 14:10:55 -04:00
box-shadow: 2px 2px 0px var(--b2);
2023-08-20 13:44:36 -04:00
}
.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
}
}
2023-08-18 17:23:56 -04:00
</style>