diff --git a/messages/en.json b/messages/en.json index 224582b..73ce8d1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -123,7 +123,9 @@ "audio_bitrate": "Audio bitrate (kbps)", "bitrate_placeholder": "Custom bitrate", "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": { "something": "Something" diff --git a/src/lib/components/functional/popups/SettingsModal.svelte b/src/lib/components/functional/popups/SettingsModal.svelte index 03e73c4..727c162 100644 --- a/src/lib/components/functional/popups/SettingsModal.svelte +++ b/src/lib/components/functional/popups/SettingsModal.svelte @@ -119,7 +119,6 @@ }} /> - {#key settings} {#await file.getAvailableSettings(file, settings.converter) then availableSettings}
diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index 031ab32..d0171c0 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -21,7 +21,6 @@ import { registerAc3Decoder, registerAc3Encoder } from "@mediabunny/ac3"; import { registerMp3Encoder } from "@mediabunny/mp3-encoder"; import { registerFlacEncoder } from "@mediabunny/flac-encoder"; import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; -import { ToastManager } from "$lib/util/toast.svelte"; import { error, log } from "$lib/util/logger"; import { m } from "$lib/paraglide/messages"; import type { @@ -29,108 +28,49 @@ import type { ConversionSettings, } from "$lib/types/conversion-settings"; 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 -// 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 = { video: { - mp4: ["avc", "hevc", "vp8", "vp9", "av1"], - m4v: ["avc", "hevc", "vp8", "vp9", "av1"], - f4v: ["avc", "hevc", "vp8", "vp9", "av1"], - "3gp": ["avc", "hevc", "vp8", "vp9", "av1"], - "3g2": ["avc", "hevc", "vp8", "vp9", "av1"], - mkv: ["avc", "hevc", "vp8", "vp9", "av1"], + mp4: mp4VideoCodecs, + m4v: mp4VideoCodecs, + f4v: mp4VideoCodecs, + "3gp": mp4VideoCodecs, + "3g2": mp4VideoCodecs, + mkv: mp4VideoCodecs, webm: ["vp8", "vp9", "av1"], - mov: ["avc", "hevc", "vp8", "vp9", "av1"], + mov: mp4VideoCodecs, ts: ["avc", "hevc"], }, audio: { - mp4: [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "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", - ], + mp4: mp4AudioCodecs, + m4v: mp4AudioCodecs, + f4v: mp4AudioCodecs, + "3gp": mp4AudioCodecs, + "3g2": mp4AudioCodecs, + m4a: mp4AudioCodecs, + m4b: mp4AudioCodecs, + m4p: mp4AudioCodecs, mkv: [ "aac", "opus", @@ -173,6 +113,80 @@ const codecCompatibility = { }, } 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 => { + const config: Record = {}; + + 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 => { + const config: Record = {}; + + 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 { public name = "mediabunny"; 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)), ]; + // 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() { super(); + this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); + // additional mediabunny coders // currently the official ones -- maybe add our own in the future this.initializeCodecs(); @@ -216,7 +238,9 @@ export class MediabunnyConverter extends Converter { registerAc3Encoder(); } - public async getAvailableSettings(): Promise { + public async getAvailableSettings( + input: VertFile, + ): Promise { // TODO: maybe have a slider for conversion speed/quality like vertd const fps: SettingDefinition = { @@ -290,6 +314,38 @@ export class MediabunnyConverter extends Converter { 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 */ @@ -346,6 +402,8 @@ export class MediabunnyConverter extends Converter { return [ videoBitrate, resolution, + videoCodec, + audioCodec, fps, metadata, audioBitrate, @@ -353,16 +411,22 @@ export class MediabunnyConverter extends Converter { ]; } - public async getDefaultSettings(): Promise { + public async getDefaultSettings( + input: VertFile, + ): Promise { const defaults: ConversionSettings = {}; - const settings = await this.getAvailableSettings(); + const settings = await this.getAvailableSettings(input); settings.forEach((setting) => { defaults[setting.key] = setting.default; }); return defaults; } - public async convert(file: VertFile, to: string): Promise { + public async convert( + file: VertFile, + to: string, + settings: ConversionSettings, + ): Promise { const input = new Input({ // TODO: add settings & special handling for certain formats & codecs formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS], @@ -374,27 +438,41 @@ export class MediabunnyConverter extends Converter { 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({ input, output, + video: videoConfig, + audio: audioConfig, + ...(conversionSettings.metadata === "false" ? { tags: {} } : {}), }); - if (!conversion.isValid) { - for (const discarded of conversion.discardedTracks) { - ToastManager.add({ - type: "error", - message: `Mediabunny discarded unsupported track: ${discarded.reason}`, - }); - } + this.activeConversions.set(file.id, conversion); - 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) => { file.progress = progress * 100; }; - this.activeConversions.set(file.id, conversion); await conversion.execute(); this.activeConversions.delete(file.id); @@ -431,7 +509,7 @@ export class MediabunnyConverter extends Converter { case ".mov": return new MovOutputFormat(); case ".ts": - return new MpegTsOutputFormat(); // FIXME: audio tracks discarded - prob needs another audio codec + return new MpegTsOutputFormat(); default: throw new Error(`Unsupported format: ${ext}`); } @@ -440,17 +518,13 @@ export class MediabunnyConverter extends Converter { public async cancel(input: VertFile): Promise { const conversion = this.activeConversions.get(input.id); if (!conversion) { - error( - ["converters", this.name], + this.error( `no active conversion found for file ${input.name}`, ); return; } - log( - ["converters", this.name], - `cancelling conversion for file ${input.name}`, - ); + this.log(`cancelling conversion for file ${input.name}`); conversion.cancel(); this.activeConversions.delete(input.id);