feat: experimental audio to video

This commit is contained in:
not-nullptr 2025-04-14 23:24:12 +01:00
parent 5fb9dbcf35
commit 2191c95500
7 changed files with 158 additions and 57 deletions

View File

@ -5,8 +5,9 @@ export class FormatInfo {
constructor( constructor(
name: string, name: string,
public fromSupported: boolean, public fromSupported = true,
public toSupported: boolean, public toSupported = true,
public isNative = true,
) { ) {
this.name = name; this.name = name;
if (!this.name.startsWith(".")) { if (!this.name.startsWith(".")) {

View File

@ -5,23 +5,27 @@ import { browser } from "$app/environment";
import { error, log } from "$lib/logger"; import { error, log } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
const videoFormats = ["mp4", "mkv", "avi", "mov", "webm"];
export class FFmpegConverter extends Converter { export class FFmpegConverter extends Converter {
private ffmpeg: FFmpeg = null!; private ffmpeg: FFmpeg = null!;
public name = "ffmpeg"; public name = "ffmpeg";
public ready = $state(false); public ready = $state(false);
public supportedFormats = [ public supportedFormats = [
new FormatInfo("mp3", true, true), new FormatInfo("mp3"),
new FormatInfo("wav", true, true), new FormatInfo("wav"),
new FormatInfo("flac", true, true), new FormatInfo("flac"),
new FormatInfo("ogg", true, true), new FormatInfo("ogg"),
new FormatInfo("aac", true, true), new FormatInfo("aac"),
new FormatInfo("m4a", true, true), new FormatInfo("m4a"),
new FormatInfo("wma", true, true), // TODO: audio to video where it uses the album cover
new FormatInfo("amr", true, true), ...videoFormats.map((f) => new FormatInfo(f, true, true, false)),
new FormatInfo("ac3", true, true), new FormatInfo("wma"),
new FormatInfo("alac", true, true), new FormatInfo("amr"),
new FormatInfo("aiff", true, true), new FormatInfo("ac3"),
new FormatInfo("alac"),
new FormatInfo("aiff"),
]; ];
public readonly reportsProgress = true; public readonly reportsProgress = true;
@ -57,6 +61,9 @@ export class FFmpegConverter extends Converter {
ffmpeg.on("progress", (progress) => { ffmpeg.on("progress", (progress) => {
input.progress = progress.progress * 100; input.progress = progress.progress * 100;
}); });
ffmpeg.on("log", (l) => {
log(["converters", this.name], l.message);
});
const baseURL = const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await ffmpeg.load({ await ffmpeg.load({
@ -69,7 +76,33 @@ export class FFmpegConverter extends Converter {
["converters", this.name], ["converters", this.name],
`wrote ${input.name} to ffmpeg virtual fs`, `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`); log(["converters", this.name], `executed ffmpeg command`);
const output = (await ffmpeg.readFile( const output = (await ffmpeg.readFile(
"output" + to, "output" + to,
@ -82,3 +115,49 @@ export class FFmpegConverter extends Converter {
return new VertFile(new File([output], input.name), to); 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";
}
};

View File

@ -62,18 +62,18 @@ export class PandocConverter extends Converter {
} }
public supportedFormats = [ public supportedFormats = [
new FormatInfo("docx", true, true), new FormatInfo("docx"),
new FormatInfo("doc", true, true), new FormatInfo("doc"),
new FormatInfo("md", true, true), new FormatInfo("md"),
new FormatInfo("html", true, true), new FormatInfo("html"),
new FormatInfo("rtf", true, true), new FormatInfo("rtf"),
new FormatInfo("csv", true, true), new FormatInfo("csv"),
new FormatInfo("tsv", true, true), new FormatInfo("tsv"),
new FormatInfo("json", true, true), new FormatInfo("json"),
new FormatInfo("rst", true, true), new FormatInfo("rst"),
new FormatInfo("epub", true, true), new FormatInfo("epub"),
new FormatInfo("odt", true, true), new FormatInfo("odt"),
new FormatInfo("docbook", true, true), new FormatInfo("docbook"),
]; ];
} }

View File

@ -202,14 +202,14 @@ export class VertdConverter extends Converter {
public reportsProgress = true; public reportsProgress = true;
public supportedFormats = [ public supportedFormats = [
new FormatInfo("mkv", true, true), new FormatInfo("mkv"),
new FormatInfo("mp4", true, true), new FormatInfo("mp4"),
new FormatInfo("webm", true, true), new FormatInfo("webm"),
new FormatInfo("avi", true, true), new FormatInfo("avi"),
new FormatInfo("wmv", true, true), new FormatInfo("wmv"),
new FormatInfo("mov", true, true), new FormatInfo("mov"),
new FormatInfo("gif", true, true), new FormatInfo("gif"),
new FormatInfo("mts", true, true), new FormatInfo("mts"),
]; ];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -17,31 +17,31 @@ export class VipsConverter extends Converter {
public ready = $state(false); public ready = $state(false);
public supportedFormats = [ public supportedFormats = [
new FormatInfo("png", true, true), new FormatInfo("png"),
new FormatInfo("jpeg", true, true), new FormatInfo("jpeg"),
new FormatInfo("jpg", true, true), new FormatInfo("jpg"),
new FormatInfo("webp", true, true), new FormatInfo("webp"),
new FormatInfo("gif", true, true), new FormatInfo("gif"),
new FormatInfo("ico", true, false), new FormatInfo("ico", true, false),
new FormatInfo("cur", true, false), new FormatInfo("cur", true, false),
new FormatInfo("ani", true, false), new FormatInfo("ani", true, false),
new FormatInfo("heic", true, false), new FormatInfo("heic", true, false),
new FormatInfo("nef", true, false), new FormatInfo("nef", true, false),
new FormatInfo("cr2", true, false), new FormatInfo("cr2", true, false),
new FormatInfo("hdr", true, true), new FormatInfo("hdr"),
new FormatInfo("jpe", true, true), new FormatInfo("jpe"),
new FormatInfo("dng", true, false), new FormatInfo("dng", true, false),
new FormatInfo("mat", true, true), new FormatInfo("mat"),
new FormatInfo("pbm", true, true), new FormatInfo("pbm"),
new FormatInfo("pfm", true, true), new FormatInfo("pfm"),
new FormatInfo("pgm", true, true), new FormatInfo("pgm"),
new FormatInfo("pnm", true, true), new FormatInfo("pnm"),
new FormatInfo("ppm", true, true), new FormatInfo("ppm"),
new FormatInfo("raw", false, true), new FormatInfo("raw", false, true),
new FormatInfo("tif", true, true), new FormatInfo("tif"),
new FormatInfo("tiff", true, true), new FormatInfo("tiff"),
new FormatInfo("jfif", true, true), new FormatInfo("jfif"),
new FormatInfo("avif", true, true), new FormatInfo("avif"),
]; ];
public readonly reportsProgress = false; public readonly reportsProgress = false;

View File

@ -34,11 +34,24 @@ export class VertFile {
} }
public findConverter() { public findConverter() {
const converter = this.converters.find( const converter = this.converters.find((converter) => {
(converter) => if (
converter.formatStrings().includes(this.from) && !converter.formatStrings().includes(this.from) ||
converter.formatStrings().includes(this.to), !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; return converter;
} }

View File

@ -227,7 +227,15 @@
<Dropdown <Dropdown
options={availableConverters options={availableConverters
.flatMap((c) => .flatMap((c) =>
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( .filter(
(format) => (format) =>