From 2191c955007faf7970de6cbf6f2e2f9bffe8a246 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:24:12 +0100 Subject: [PATCH] feat: experimental audio to video --- src/lib/converters/converter.svelte.ts | 5 +- src/lib/converters/ffmpeg.svelte.ts | 103 ++++++++++++++++++++++--- src/lib/converters/pandoc.svelte.ts | 24 +++--- src/lib/converters/vertd.svelte.ts | 16 ++-- src/lib/converters/vips.svelte.ts | 34 ++++---- src/lib/types/file.svelte.ts | 23 ++++-- src/routes/convert/+page.svelte | 10 ++- 7 files changed, 158 insertions(+), 57 deletions(-) diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index 4c0fc85..f321b56 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -5,8 +5,9 @@ export class FormatInfo { constructor( name: string, - public fromSupported: boolean, - public toSupported: boolean, + public fromSupported = true, + public toSupported = true, + public isNative = true, ) { this.name = name; if (!this.name.startsWith(".")) { diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 9ce3e69..b6c5122 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -5,23 +5,27 @@ import { browser } from "$app/environment"; import { error, log } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; +const videoFormats = ["mp4", "mkv", "avi", "mov", "webm"]; + export class FFmpegConverter extends Converter { private ffmpeg: FFmpeg = null!; public name = "ffmpeg"; public ready = $state(false); public supportedFormats = [ - new FormatInfo("mp3", true, true), - new FormatInfo("wav", true, true), - new FormatInfo("flac", true, true), - new FormatInfo("ogg", true, true), - new FormatInfo("aac", true, true), - new FormatInfo("m4a", true, true), - new FormatInfo("wma", true, true), - new FormatInfo("amr", true, true), - new FormatInfo("ac3", true, true), - new FormatInfo("alac", true, true), - new FormatInfo("aiff", true, true), + new FormatInfo("mp3"), + new FormatInfo("wav"), + new FormatInfo("flac"), + new FormatInfo("ogg"), + new FormatInfo("aac"), + new FormatInfo("m4a"), + // TODO: audio to video where it uses the album cover + ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), + new FormatInfo("wma"), + new FormatInfo("amr"), + new FormatInfo("ac3"), + new FormatInfo("alac"), + new FormatInfo("aiff"), ]; public readonly reportsProgress = true; @@ -57,6 +61,9 @@ export class FFmpegConverter extends Converter { ffmpeg.on("progress", (progress) => { input.progress = progress.progress * 100; }); + ffmpeg.on("log", (l) => { + log(["converters", this.name], l.message); + }); const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; await ffmpeg.load({ @@ -69,7 +76,33 @@ export class FFmpegConverter extends Converter { ["converters", this.name], `wrote ${input.name} to ffmpeg virtual fs`, ); - await ffmpeg.exec(["-i", "input", "output" + to]); + if (videoFormats.includes(input.from.slice(1))) { + // create an audio track from the video + await ffmpeg.exec(["-i", "input", "-map", "0:a:0", "output" + to]); + } else if (videoFormats.includes(to.slice(1))) { + // nab the album art + await ffmpeg.exec([ + "-i", + "input", + "-an", + "-vcodec", + "copy", + "cover.png", + ]); + await ffmpeg.exec([ + "-i", + "input", + "-i", + "cover.png", + "-loop", + "1", + ...toArgs(to), + "output" + to, + ]); + } else { + await ffmpeg.exec(["-i", "input", "output" + to]); + } + log(["converters", this.name], `executed ffmpeg command`); const output = (await ffmpeg.readFile( "output" + to, @@ -82,3 +115,49 @@ export class FFmpegConverter extends Converter { return new VertFile(new File([output], input.name), to); } } + +// and here i was, thinking i'd be done with ffmpeg after finishing vertd +// but OH NO we just HAD to have someone suggest to allow album art video generation. +// +// i hate you SO much. +// - love, maddie +const toArgs = (ext: string): string[] => { + const encoder = getEncoder(ext); + const args = ["-c:v", encoder]; + switch (encoder) { + case "libx264": { + args.push( + "-preset", + "ultrafast", + "-crf", + "18", + "-tune", + "stillimage", + "-c:a", + "aac", + ); + break; + } + + case "libvpx": { + args.push("-c:v", "libvpx-vp9", "-c:a", "libvorbis"); + break; + } + } + + return args; +}; + +const getEncoder = (ext: string): string => { + switch (ext) { + case ".mkv": + case ".mp4": + case ".avi": + case ".mov": + return "libx264"; + case ".webm": + return "libvpx"; + default: + return "copy"; + } +}; diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index 8984e25..cf1459b 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -62,18 +62,18 @@ export class PandocConverter extends Converter { } public supportedFormats = [ - new FormatInfo("docx", true, true), - new FormatInfo("doc", true, true), - new FormatInfo("md", true, true), - new FormatInfo("html", true, true), - new FormatInfo("rtf", true, true), - new FormatInfo("csv", true, true), - new FormatInfo("tsv", true, true), - new FormatInfo("json", true, true), - new FormatInfo("rst", true, true), - new FormatInfo("epub", true, true), - new FormatInfo("odt", true, true), - new FormatInfo("docbook", true, true), + new FormatInfo("docx"), + new FormatInfo("doc"), + new FormatInfo("md"), + new FormatInfo("html"), + new FormatInfo("rtf"), + new FormatInfo("csv"), + new FormatInfo("tsv"), + new FormatInfo("json"), + new FormatInfo("rst"), + new FormatInfo("epub"), + new FormatInfo("odt"), + new FormatInfo("docbook"), ]; } diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 94f9d20..b8b022e 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -202,14 +202,14 @@ export class VertdConverter extends Converter { public reportsProgress = true; public supportedFormats = [ - new FormatInfo("mkv", true, true), - new FormatInfo("mp4", true, true), - new FormatInfo("webm", true, true), - new FormatInfo("avi", true, true), - new FormatInfo("wmv", true, true), - new FormatInfo("mov", true, true), - new FormatInfo("gif", true, true), - new FormatInfo("mts", true, true), + new FormatInfo("mkv"), + new FormatInfo("mp4"), + new FormatInfo("webm"), + new FormatInfo("avi"), + new FormatInfo("wmv"), + new FormatInfo("mov"), + new FormatInfo("gif"), + new FormatInfo("mts"), ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts index c8b3c0d..46e7022 100644 --- a/src/lib/converters/vips.svelte.ts +++ b/src/lib/converters/vips.svelte.ts @@ -17,31 +17,31 @@ export class VipsConverter extends Converter { public ready = $state(false); public supportedFormats = [ - new FormatInfo("png", true, true), - new FormatInfo("jpeg", true, true), - new FormatInfo("jpg", true, true), - new FormatInfo("webp", true, true), - new FormatInfo("gif", true, true), + new FormatInfo("png"), + new FormatInfo("jpeg"), + new FormatInfo("jpg"), + new FormatInfo("webp"), + new FormatInfo("gif"), new FormatInfo("ico", true, false), new FormatInfo("cur", true, false), new FormatInfo("ani", true, false), new FormatInfo("heic", true, false), new FormatInfo("nef", true, false), new FormatInfo("cr2", true, false), - new FormatInfo("hdr", true, true), - new FormatInfo("jpe", true, true), + new FormatInfo("hdr"), + new FormatInfo("jpe"), new FormatInfo("dng", true, false), - new FormatInfo("mat", true, true), - new FormatInfo("pbm", true, true), - new FormatInfo("pfm", true, true), - new FormatInfo("pgm", true, true), - new FormatInfo("pnm", true, true), - new FormatInfo("ppm", true, true), + new FormatInfo("mat"), + new FormatInfo("pbm"), + new FormatInfo("pfm"), + new FormatInfo("pgm"), + new FormatInfo("pnm"), + new FormatInfo("ppm"), new FormatInfo("raw", false, true), - new FormatInfo("tif", true, true), - new FormatInfo("tiff", true, true), - new FormatInfo("jfif", true, true), - new FormatInfo("avif", true, true), + new FormatInfo("tif"), + new FormatInfo("tiff"), + new FormatInfo("jfif"), + new FormatInfo("avif"), ]; public readonly reportsProgress = false; diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index f77156c..3ff57c1 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -34,11 +34,24 @@ export class VertFile { } public findConverter() { - const converter = this.converters.find( - (converter) => - converter.formatStrings().includes(this.from) && - converter.formatStrings().includes(this.to), - ); + const converter = this.converters.find((converter) => { + if ( + !converter.formatStrings().includes(this.from) || + !converter.formatStrings().includes(this.to) + ) { + return false; + } + + const theirFrom = converter.supportedFormats.find( + (f) => f.name === this.from, + ); + const theirTo = converter.supportedFormats.find( + (f) => f.name === this.to, + ); + if (!theirFrom || !theirTo) return false; + if (!theirFrom.isNative && !theirTo.isNative) return false; + return true; + }); return converter; } diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index a058220..33932a0 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -227,7 +227,15 @@ - c.formatStrings((f) => f.toSupported), + // c.formatStrings((f) => f.toSupported), + c.supportedFormats.find( + (f) => f.name === file.from, + )?.isNative + ? c.formatStrings((f) => f.toSupported) + : c.formatStrings( + (f) => + f.toSupported && f.isNative, + ), ) .filter( (format) =>