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() != "");
|
2023-08-25 14:20:06 -04:00
|
|
|
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>
|