mirror of https://github.com/VERT-sh/VERT.git
feat: mediabunny video/audio codec, add custom inputs, show discarded tracks
This commit is contained in:
parent
9201f1e92f
commit
fb1bd521f3
|
|
@ -123,7 +123,9 @@
|
||||||
"audio_bitrate": "Audio bitrate (kbps)",
|
"audio_bitrate": "Audio bitrate (kbps)",
|
||||||
"bitrate_placeholder": "Custom bitrate",
|
"bitrate_placeholder": "Custom bitrate",
|
||||||
"sample_rate": "Audio sample rate (Hz)",
|
"sample_rate": "Audio sample rate (Hz)",
|
||||||
"sample_rate_placeholder": "Custom sample rate"
|
"sample_rate_placeholder": "Custom sample rate",
|
||||||
|
"video_codec": "Video codec",
|
||||||
|
"audio_codec": "Audio codec"
|
||||||
},
|
},
|
||||||
"document": {
|
"document": {
|
||||||
"something": "Something"
|
"something": "Something"
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,6 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- FIXME: modal loads before settings is finished for some reason -->
|
|
||||||
{#key settings}
|
{#key settings}
|
||||||
{#await file.getAvailableSettings(file, settings.converter) then availableSettings}
|
{#await file.getAvailableSettings(file, settings.converter) then availableSettings}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import { registerAc3Decoder, registerAc3Encoder } from "@mediabunny/ac3";
|
||||||
import { registerMp3Encoder } from "@mediabunny/mp3-encoder";
|
import { registerMp3Encoder } from "@mediabunny/mp3-encoder";
|
||||||
import { registerFlacEncoder } from "@mediabunny/flac-encoder";
|
import { registerFlacEncoder } from "@mediabunny/flac-encoder";
|
||||||
import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte";
|
import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte";
|
||||||
import { ToastManager } from "$lib/util/toast.svelte";
|
|
||||||
import { error, log } from "$lib/util/logger";
|
import { error, log } from "$lib/util/logger";
|
||||||
import { m } from "$lib/paraglide/messages";
|
import { m } from "$lib/paraglide/messages";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -29,108 +28,49 @@ import type {
|
||||||
ConversionSettings,
|
ConversionSettings,
|
||||||
} from "$lib/types/conversion-settings";
|
} from "$lib/types/conversion-settings";
|
||||||
import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte";
|
import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte";
|
||||||
|
import { ToastManager } from "$lib/util/toast.svelte";
|
||||||
|
|
||||||
// codec compatibility object, based on docs
|
// codec compatibility stuff, based on mediabunny's docs
|
||||||
// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table
|
// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const mp4VideoCodecs = ["avc", "hevc", "vp8", "vp9", "av1"] as const;
|
||||||
|
const mp4AudioCodecs = [
|
||||||
|
"aac",
|
||||||
|
"opus",
|
||||||
|
"mp3",
|
||||||
|
"vorbis",
|
||||||
|
"flac",
|
||||||
|
"ac3",
|
||||||
|
"eac3",
|
||||||
|
"pcm-s16",
|
||||||
|
"pcm-s16be",
|
||||||
|
"pcm-s24",
|
||||||
|
"pcm-s24be",
|
||||||
|
"pcm-s32",
|
||||||
|
"pcm-s32be",
|
||||||
|
"pcm-f32",
|
||||||
|
"pcm-f64",
|
||||||
|
] as const;
|
||||||
const codecCompatibility = {
|
const codecCompatibility = {
|
||||||
video: {
|
video: {
|
||||||
mp4: ["avc", "hevc", "vp8", "vp9", "av1"],
|
mp4: mp4VideoCodecs,
|
||||||
m4v: ["avc", "hevc", "vp8", "vp9", "av1"],
|
m4v: mp4VideoCodecs,
|
||||||
f4v: ["avc", "hevc", "vp8", "vp9", "av1"],
|
f4v: mp4VideoCodecs,
|
||||||
"3gp": ["avc", "hevc", "vp8", "vp9", "av1"],
|
"3gp": mp4VideoCodecs,
|
||||||
"3g2": ["avc", "hevc", "vp8", "vp9", "av1"],
|
"3g2": mp4VideoCodecs,
|
||||||
mkv: ["avc", "hevc", "vp8", "vp9", "av1"],
|
mkv: mp4VideoCodecs,
|
||||||
webm: ["vp8", "vp9", "av1"],
|
webm: ["vp8", "vp9", "av1"],
|
||||||
mov: ["avc", "hevc", "vp8", "vp9", "av1"],
|
mov: mp4VideoCodecs,
|
||||||
ts: ["avc", "hevc"],
|
ts: ["avc", "hevc"],
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
mp4: [
|
mp4: mp4AudioCodecs,
|
||||||
"aac",
|
m4v: mp4AudioCodecs,
|
||||||
"opus",
|
f4v: mp4AudioCodecs,
|
||||||
"mp3",
|
"3gp": mp4AudioCodecs,
|
||||||
"vorbis",
|
"3g2": mp4AudioCodecs,
|
||||||
"flac",
|
m4a: mp4AudioCodecs,
|
||||||
"ac3",
|
m4b: mp4AudioCodecs,
|
||||||
"eac3",
|
m4p: mp4AudioCodecs,
|
||||||
"pcm-s16",
|
|
||||||
"pcm-s16be",
|
|
||||||
"pcm-s24",
|
|
||||||
"pcm-s24be",
|
|
||||||
"pcm-s32",
|
|
||||||
"pcm-s32be",
|
|
||||||
"pcm-f32",
|
|
||||||
"pcm-f64",
|
|
||||||
],
|
|
||||||
m4v: [
|
|
||||||
"aac",
|
|
||||||
"opus",
|
|
||||||
"mp3",
|
|
||||||
"vorbis",
|
|
||||||
"flac",
|
|
||||||
"ac3",
|
|
||||||
"eac3",
|
|
||||||
"pcm-s16",
|
|
||||||
"pcm-s16be",
|
|
||||||
"pcm-s24",
|
|
||||||
"pcm-s24be",
|
|
||||||
"pcm-s32",
|
|
||||||
"pcm-s32be",
|
|
||||||
"pcm-f32",
|
|
||||||
"pcm-f64",
|
|
||||||
],
|
|
||||||
f4v: [
|
|
||||||
"aac",
|
|
||||||
"opus",
|
|
||||||
"mp3",
|
|
||||||
"vorbis",
|
|
||||||
"flac",
|
|
||||||
"ac3",
|
|
||||||
"eac3",
|
|
||||||
"pcm-s16",
|
|
||||||
"pcm-s16be",
|
|
||||||
"pcm-s24",
|
|
||||||
"pcm-s24be",
|
|
||||||
"pcm-s32",
|
|
||||||
"pcm-s32be",
|
|
||||||
"pcm-f32",
|
|
||||||
"pcm-f64",
|
|
||||||
],
|
|
||||||
"3gp": [
|
|
||||||
"aac",
|
|
||||||
"opus",
|
|
||||||
"mp3",
|
|
||||||
"vorbis",
|
|
||||||
"flac",
|
|
||||||
"ac3",
|
|
||||||
"eac3",
|
|
||||||
"pcm-s16",
|
|
||||||
"pcm-s16be",
|
|
||||||
"pcm-s24",
|
|
||||||
"pcm-s24be",
|
|
||||||
"pcm-s32",
|
|
||||||
"pcm-s32be",
|
|
||||||
"pcm-f32",
|
|
||||||
"pcm-f64",
|
|
||||||
],
|
|
||||||
"3g2": [
|
|
||||||
"aac",
|
|
||||||
"opus",
|
|
||||||
"mp3",
|
|
||||||
"vorbis",
|
|
||||||
"flac",
|
|
||||||
"ac3",
|
|
||||||
"eac3",
|
|
||||||
"pcm-s16",
|
|
||||||
"pcm-s16be",
|
|
||||||
"pcm-s24",
|
|
||||||
"pcm-s24be",
|
|
||||||
"pcm-s32",
|
|
||||||
"pcm-s32be",
|
|
||||||
"pcm-f32",
|
|
||||||
"pcm-f64",
|
|
||||||
],
|
|
||||||
mkv: [
|
mkv: [
|
||||||
"aac",
|
"aac",
|
||||||
"opus",
|
"opus",
|
||||||
|
|
@ -173,6 +113,80 @@ const codecCompatibility = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const getCompatibleCodecs = (
|
||||||
|
type: keyof typeof codecCompatibility,
|
||||||
|
format: string,
|
||||||
|
) => {
|
||||||
|
const normalized = format.replace(/^\./, "").toLowerCase();
|
||||||
|
const direct =
|
||||||
|
codecCompatibility[type][
|
||||||
|
normalized as keyof (typeof codecCompatibility)[typeof type]
|
||||||
|
];
|
||||||
|
if (direct) return [...direct];
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildVideoConfig = (
|
||||||
|
settings: ConversionSettings,
|
||||||
|
): Record<string, unknown> => {
|
||||||
|
const config: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (settings.videoCodec !== "auto") config.codec = settings.videoCodec;
|
||||||
|
|
||||||
|
if (settings.videoBitrate !== "auto") {
|
||||||
|
const bitrate =
|
||||||
|
settings.videoBitrate === "custom"
|
||||||
|
? settings.customVideoBitrate
|
||||||
|
: settings.videoBitrate;
|
||||||
|
config.bitrate = Number(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.fps !== "auto") {
|
||||||
|
const fps =
|
||||||
|
settings.fps === "custom" ? settings.customFps : settings.fps;
|
||||||
|
config.frameRate = Number(fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.resolution !== "auto") {
|
||||||
|
const resolution =
|
||||||
|
settings.resolution === "custom"
|
||||||
|
? settings.customResolution
|
||||||
|
: settings.resolution;
|
||||||
|
const [width, height] = resolution.split("x").map(Number);
|
||||||
|
config.width = width;
|
||||||
|
config.height = height;
|
||||||
|
config.fit = "contain"; // TODO: maybe allow changing this?
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildAudioConfig = (
|
||||||
|
settings: ConversionSettings,
|
||||||
|
): Record<string, unknown> => {
|
||||||
|
const config: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (settings.audioCodec !== "auto") config.codec = settings.audioCodec;
|
||||||
|
|
||||||
|
if (settings.audioBitrate !== "auto") {
|
||||||
|
const bitrate =
|
||||||
|
settings.audioBitrate === "custom"
|
||||||
|
? settings.customAudioBitrate
|
||||||
|
: settings.audioBitrate;
|
||||||
|
config.bitrate = Number(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.sampleRate !== "auto") {
|
||||||
|
const sampleRate =
|
||||||
|
settings.sampleRate === "custom"
|
||||||
|
? settings.customSampleRate
|
||||||
|
: settings.sampleRate;
|
||||||
|
config.sampleRate = Number(sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
export class MediabunnyConverter extends Converter {
|
export class MediabunnyConverter extends Converter {
|
||||||
public name = "mediabunny";
|
public name = "mediabunny";
|
||||||
public status: WorkerStatus = $state("ready");
|
public status: WorkerStatus = $state("ready");
|
||||||
|
|
@ -197,9 +211,17 @@ export class MediabunnyConverter extends Converter {
|
||||||
...this.formats.map((f) => new FormatInfo(f, true, true, true, 2)),
|
...this.formats.map((f) => new FormatInfo(f, true, true, true, 2)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private log: (...msg: any[]) => void = () => {};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private error: (...msg: any[]) => void = () => {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.log = (msg) => log(["converters", this.name], msg);
|
||||||
|
this.error = (msg) => error(["converters", this.name], msg);
|
||||||
|
|
||||||
// additional mediabunny coders
|
// additional mediabunny coders
|
||||||
// currently the official ones -- maybe add our own in the future
|
// currently the official ones -- maybe add our own in the future
|
||||||
this.initializeCodecs();
|
this.initializeCodecs();
|
||||||
|
|
@ -216,7 +238,9 @@ export class MediabunnyConverter extends Converter {
|
||||||
registerAc3Encoder();
|
registerAc3Encoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAvailableSettings(): Promise<SettingDefinition[]> {
|
public async getAvailableSettings(
|
||||||
|
input: VertFile,
|
||||||
|
): Promise<SettingDefinition[]> {
|
||||||
// TODO: maybe have a slider for conversion speed/quality like vertd
|
// TODO: maybe have a slider for conversion speed/quality like vertd
|
||||||
|
|
||||||
const fps: SettingDefinition = {
|
const fps: SettingDefinition = {
|
||||||
|
|
@ -290,6 +314,38 @@ export class MediabunnyConverter extends Converter {
|
||||||
placeholder: m["convert.settings.video.bitrate_placeholder"](),
|
placeholder: m["convert.settings.video.bitrate_placeholder"](),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toFormat = input.to;
|
||||||
|
const supportedVideoCodecs = getCompatibleCodecs("video", toFormat);
|
||||||
|
const videoCodec: SettingDefinition = {
|
||||||
|
key: "videoCodec",
|
||||||
|
label: m["convert.settings.video.video_codec"](),
|
||||||
|
type: "select",
|
||||||
|
default: "auto",
|
||||||
|
// TODO: get supported from codecCompatibility based on output format
|
||||||
|
options: [
|
||||||
|
{ value: "auto", label: m["convert.settings.common.auto"]() },
|
||||||
|
...supportedVideoCodecs.map((codec) => ({
|
||||||
|
value: codec,
|
||||||
|
label: codec,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const supportedAudioCodecs = getCompatibleCodecs("audio", toFormat);
|
||||||
|
const audioCodec: SettingDefinition = {
|
||||||
|
key: "audioCodec",
|
||||||
|
label: m["convert.settings.video.audio_codec"](),
|
||||||
|
type: "select",
|
||||||
|
default: "auto",
|
||||||
|
options: [
|
||||||
|
{ value: "auto", label: m["convert.settings.common.auto"]() },
|
||||||
|
...supportedAudioCodecs.map((codec) => ({
|
||||||
|
value: codec,
|
||||||
|
label: codec,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* audio settings
|
* audio settings
|
||||||
*/
|
*/
|
||||||
|
|
@ -346,6 +402,8 @@ export class MediabunnyConverter extends Converter {
|
||||||
return [
|
return [
|
||||||
videoBitrate,
|
videoBitrate,
|
||||||
resolution,
|
resolution,
|
||||||
|
videoCodec,
|
||||||
|
audioCodec,
|
||||||
fps,
|
fps,
|
||||||
metadata,
|
metadata,
|
||||||
audioBitrate,
|
audioBitrate,
|
||||||
|
|
@ -353,16 +411,22 @@ export class MediabunnyConverter extends Converter {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDefaultSettings(): Promise<ConversionSettings> {
|
public async getDefaultSettings(
|
||||||
|
input: VertFile,
|
||||||
|
): Promise<ConversionSettings> {
|
||||||
const defaults: ConversionSettings = {};
|
const defaults: ConversionSettings = {};
|
||||||
const settings = await this.getAvailableSettings();
|
const settings = await this.getAvailableSettings(input);
|
||||||
settings.forEach((setting) => {
|
settings.forEach((setting) => {
|
||||||
defaults[setting.key] = setting.default;
|
defaults[setting.key] = setting.default;
|
||||||
});
|
});
|
||||||
return defaults;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async convert(file: VertFile, to: string): Promise<VertFile> {
|
public async convert(
|
||||||
|
file: VertFile,
|
||||||
|
to: string,
|
||||||
|
settings: ConversionSettings,
|
||||||
|
): Promise<VertFile> {
|
||||||
const input = new Input({
|
const input = new Input({
|
||||||
// TODO: add settings & special handling for certain formats & codecs
|
// TODO: add settings & special handling for certain formats & codecs
|
||||||
formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS],
|
formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS],
|
||||||
|
|
@ -374,27 +438,41 @@ export class MediabunnyConverter extends Converter {
|
||||||
target: new BufferTarget(),
|
target: new BufferTarget(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const conversionSettings =
|
||||||
|
Object.keys(settings).length > 0
|
||||||
|
? settings // user-provided settings
|
||||||
|
: await this.getDefaultSettings(file); // use defaults if not provided
|
||||||
|
|
||||||
|
const videoConfig = buildVideoConfig(conversionSettings);
|
||||||
|
const audioConfig = buildAudioConfig(conversionSettings);
|
||||||
|
|
||||||
const conversion = await Conversion.init({
|
const conversion = await Conversion.init({
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
video: videoConfig,
|
||||||
|
audio: audioConfig,
|
||||||
|
...(conversionSettings.metadata === "false" ? { tags: {} } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!conversion.isValid) {
|
this.activeConversions.set(file.id, conversion);
|
||||||
for (const discarded of conversion.discardedTracks) {
|
|
||||||
ToastManager.add({
|
|
||||||
type: "error",
|
|
||||||
message: `Mediabunny discarded unsupported track: ${discarded.reason}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Mediabunny conversion not valid`);
|
this.log(`videoConfig: ${JSON.stringify(videoConfig)}`);
|
||||||
|
this.log(`audioConfig: ${JSON.stringify(audioConfig)}`);
|
||||||
|
|
||||||
|
for (const discarded of conversion.discardedTracks) {
|
||||||
|
ToastManager.add({
|
||||||
|
type: "error",
|
||||||
|
message: `Mediabunny discarded ${discarded.track.type} track ${discarded.track.id} (${discarded.track.codec}) for reason: ${discarded.reason}`,
|
||||||
|
durations: {
|
||||||
|
stay: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
conversion.onProgress = (progress) => {
|
conversion.onProgress = (progress) => {
|
||||||
file.progress = progress * 100;
|
file.progress = progress * 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.activeConversions.set(file.id, conversion);
|
|
||||||
await conversion.execute();
|
await conversion.execute();
|
||||||
this.activeConversions.delete(file.id);
|
this.activeConversions.delete(file.id);
|
||||||
|
|
||||||
|
|
@ -431,7 +509,7 @@ export class MediabunnyConverter extends Converter {
|
||||||
case ".mov":
|
case ".mov":
|
||||||
return new MovOutputFormat();
|
return new MovOutputFormat();
|
||||||
case ".ts":
|
case ".ts":
|
||||||
return new MpegTsOutputFormat(); // FIXME: audio tracks discarded - prob needs another audio codec
|
return new MpegTsOutputFormat();
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported format: ${ext}`);
|
throw new Error(`Unsupported format: ${ext}`);
|
||||||
}
|
}
|
||||||
|
|
@ -440,17 +518,13 @@ export class MediabunnyConverter extends Converter {
|
||||||
public async cancel(input: VertFile): Promise<void> {
|
public async cancel(input: VertFile): Promise<void> {
|
||||||
const conversion = this.activeConversions.get(input.id);
|
const conversion = this.activeConversions.get(input.id);
|
||||||
if (!conversion) {
|
if (!conversion) {
|
||||||
error(
|
this.error(
|
||||||
["converters", this.name],
|
|
||||||
`no active conversion found for file ${input.name}`,
|
`no active conversion found for file ${input.name}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
this.log(`cancelling conversion for file ${input.name}`);
|
||||||
["converters", this.name],
|
|
||||||
`cancelling conversion for file ${input.name}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
conversion.cancel();
|
conversion.cancel();
|
||||||
this.activeConversions.delete(input.id);
|
this.activeConversions.delete(input.id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue