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(
name: string,
public fromSupported: boolean,
public toSupported: boolean,
public fromSupported = true,
public toSupported = true,
public isNative = true,
) {
this.name = name;
if (!this.name.startsWith(".")) {

View File

@ -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";
}
};

View File

@ -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"),
];
}

View File

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

View File

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

View File

@ -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;
}

View File

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