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>
|