mirror of https://github.com/VERT-sh/VERT.git
feat: ffmpeg/audio settings logic
im sleepy this is probably a terrible implementation rn
This commit is contained in:
parent
7fcbdcd73a
commit
4fca8a9696
|
|
@ -93,9 +93,10 @@
|
||||||
"transparency": "Transparency"
|
"transparency": "Transparency"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate (kbps)",
|
||||||
"sample_rate": "Sample rate",
|
"sample_rate": "Sample rate (Hz)",
|
||||||
"channels": "Audio channels"
|
"channels": "Audio channels",
|
||||||
|
"tracks": "Audio tracks"
|
||||||
},
|
},
|
||||||
"video": {
|
"video": {
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
{...rest}
|
{...rest}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked
|
bind:checked
|
||||||
|
{disabled}
|
||||||
class="w-full p-3 rounded-lg bg-panel border-2 border-button
|
class="w-full p-3 rounded-lg bg-panel border-2 border-button
|
||||||
{prefix ? 'pl-[2rem]' : 'pl-3'}
|
{prefix ? 'pl-[2rem]' : 'pl-3'}
|
||||||
{extension ? 'pr-[4rem]' : 'pr-3'}
|
{extension ? 'pr-[4rem]' : 'pr-3'}
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
<input
|
<input
|
||||||
{...rest}
|
{...rest}
|
||||||
bind:value
|
bind:value
|
||||||
|
{disabled}
|
||||||
class="w-full p-3 rounded-lg bg-panel border-2 border-button
|
class="w-full p-3 rounded-lg bg-panel border-2 border-button
|
||||||
{prefix ? 'pl-[2rem]' : 'pl-3'}
|
{prefix ? 'pl-[2rem]' : 'pl-3'}
|
||||||
{extension ? 'pr-[4rem]' : 'pr-3'}
|
{extension ? 'pr-[4rem]' : 'pr-3'}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
// apply defaults, then existing settings, then new settings on top
|
// apply defaults, then existing settings, then new settings on top
|
||||||
file.conversionSettings = {
|
file.conversionSettings = {
|
||||||
...(await converter.getDefaultSettings()),
|
...(await converter.getDefaultSettings(file)),
|
||||||
...file.conversionSettings,
|
...file.conversionSettings,
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +66,8 @@
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
{#if file}
|
{#if file}
|
||||||
{#await file.getAvailableSettings() then settings}
|
<!-- FIXME: modal loads before settings is finished for some reason -->
|
||||||
|
{#await file.getAvailableSettings(file) then availableSettings}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<p class="text-base">
|
<p class="text-base">
|
||||||
{@html sanitize(
|
{@html sanitize(
|
||||||
|
|
@ -78,13 +79,13 @@
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if settings.length === 0}
|
{#if availableSettings.length === 0}
|
||||||
<p class="text-sm text-muted">
|
<p class="text-sm text-muted">
|
||||||
{m["convert.settings.none"]()}
|
{m["convert.settings.none"]()}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
{#each settings as setting (setting.key)}
|
{#each availableSettings as setting (setting.key)}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm font-bold">
|
<p class="text-sm font-bold">
|
||||||
{setting.label}
|
{setting.label}
|
||||||
|
|
@ -111,6 +112,30 @@
|
||||||
value,
|
value,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{#if setting.hasCustomInput}
|
||||||
|
{@const disabled =
|
||||||
|
(settings[setting.key] ??
|
||||||
|
file.conversionSettings[
|
||||||
|
setting.key
|
||||||
|
]) !== "custom"}
|
||||||
|
<FancyInput
|
||||||
|
type="text"
|
||||||
|
value={settings[
|
||||||
|
setting.customInputKey!
|
||||||
|
] ??
|
||||||
|
file.conversionSettings[
|
||||||
|
setting.customInputKey!
|
||||||
|
] ??
|
||||||
|
""}
|
||||||
|
placeholder={setting.placeholder}
|
||||||
|
{disabled}
|
||||||
|
oninput={(e) =>
|
||||||
|
handleSettingChange(
|
||||||
|
setting.customInputKey!,
|
||||||
|
e.currentTarget.value,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{:else if setting.type === "boolean"}
|
{:else if setting.type === "boolean"}
|
||||||
<FancyInput
|
<FancyInput
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class Converter {
|
||||||
* Can be overridden per converter for format-specific settings.
|
* Can be overridden per converter for format-specific settings.
|
||||||
* @param input The input file.
|
* @param input The input file.
|
||||||
*/
|
*/
|
||||||
public async getAvailableSettings(): Promise<SettingDefinition[]> {
|
public async getAvailableSettings(input?: VertFile): Promise<SettingDefinition[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,9 +63,9 @@ export class Converter {
|
||||||
* Get default settings for a conversion.
|
* Get default settings for a conversion.
|
||||||
* @param input The input file.
|
* @param input The input file.
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import { error, log } from "$lib/util/logger";
|
||||||
import { m } from "$lib/paraglide/messages";
|
import { m } from "$lib/paraglide/messages";
|
||||||
import { Settings } from "$lib/sections/settings/index.svelte";
|
import { Settings } from "$lib/sections/settings/index.svelte";
|
||||||
import { ToastManager } from "$lib/util/toast.svelte";
|
import { ToastManager } from "$lib/util/toast.svelte";
|
||||||
import type { SettingDefinition, ConversionSettings } from "$lib/types/conversion-settings";
|
import type {
|
||||||
|
SettingDefinition,
|
||||||
|
ConversionSettings,
|
||||||
|
} from "$lib/types/conversion-settings";
|
||||||
|
|
||||||
// TODO: differentiate in UI? (not native formats)
|
// TODO: differentiate in UI? (not native formats)
|
||||||
const videoFormats = [
|
const videoFormats = [
|
||||||
|
|
@ -106,66 +109,108 @@ export class FFmpegConverter extends Converter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAvailableSettings(): Promise<SettingDefinition[]> {
|
public async getAvailableSettings(
|
||||||
|
input: VertFile,
|
||||||
|
): Promise<SettingDefinition[]> {
|
||||||
// audio - bitrate, sample rate, channels, normalize, trim silence
|
// audio - bitrate, sample rate, channels, normalize, trim silence
|
||||||
|
|
||||||
// TODO: detect bitrate, sample rate, audio channels and set default/max accordingly
|
const global = Settings.instance.settings;
|
||||||
|
|
||||||
|
const ffmpeg = await this.setupFFmpeg(input, true);
|
||||||
|
const buf = new Uint8Array(await input.file.arrayBuffer());
|
||||||
|
await ffmpeg.writeFile("input", buf);
|
||||||
|
|
||||||
|
// TODO: should we really be doing all this detection here? it adds a lot of time before the settings even show up.
|
||||||
|
// which isn't very nice for the UX guh
|
||||||
|
|
||||||
|
const detectedBitrate = await this.detectAudioBitrate(ffmpeg);
|
||||||
const bitrate: SettingDefinition = {
|
const bitrate: SettingDefinition = {
|
||||||
key: "bitrate",
|
key: "bitrate",
|
||||||
label: m["convert.settings.audio.bitrate"](),
|
label: m["convert.settings.audio.bitrate"](),
|
||||||
type: "select",
|
type: "select",
|
||||||
default: "auto",
|
default: global.ffmpegQuality,
|
||||||
options: CONVERSION_BITRATES.map((b) => ({
|
options: CONVERSION_BITRATES.map((b) => ({
|
||||||
value: b.toString(),
|
value: b,
|
||||||
label: b.toString(),
|
label: b,
|
||||||
})),
|
})),
|
||||||
|
hasCustomInput: true,
|
||||||
|
customInputKey: "customBitrate",
|
||||||
|
placeholder: detectedBitrate ?? "128"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detectedSampleRate = await this.detectAudioSampleRate(ffmpeg);
|
||||||
const sampleRate: SettingDefinition = {
|
const sampleRate: SettingDefinition = {
|
||||||
key: "sampleRate",
|
key: "sampleRate",
|
||||||
label: m["convert.settings.audio.sample_rate"](),
|
label: m["convert.settings.audio.sample_rate"](),
|
||||||
type: "select",
|
type: "select",
|
||||||
default: "auto",
|
default:
|
||||||
|
global.ffmpegSampleRate === "custom"
|
||||||
|
? global.ffmpegCustomSampleRate
|
||||||
|
: global.ffmpegSampleRate,
|
||||||
options: SAMPLE_RATES.map((r) => ({
|
options: SAMPLE_RATES.map((r) => ({
|
||||||
value: r.toString(),
|
value: r,
|
||||||
label: r.toString(),
|
label: r,
|
||||||
})),
|
})),
|
||||||
|
hasCustomInput: true,
|
||||||
|
customInputKey: "customSampleRate",
|
||||||
|
placeholder: detectedSampleRate ?? "44100"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const audioTracks = await this.detectAudioTracks(ffmpeg);
|
||||||
|
const tracks: SettingDefinition = {
|
||||||
|
key: "tracks",
|
||||||
|
label: m["convert.settings.audio.tracks"](),
|
||||||
|
type: "number",
|
||||||
|
default: audioTracks ?? 1,
|
||||||
|
min: 1,
|
||||||
|
max: audioTracks ? audioTracks : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const audioChannels = await this.detectAudioChannels(ffmpeg);
|
||||||
const channels: SettingDefinition = {
|
const channels: SettingDefinition = {
|
||||||
key: "channels",
|
key: "channels",
|
||||||
label: m["convert.settings.audio.channels"](),
|
label: m["convert.settings.audio.channels"](),
|
||||||
type: "number",
|
type: "number",
|
||||||
default: 2,
|
default: audioChannels ?? 2,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 8,
|
max: audioChannels ? audioChannels * 2 : 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const metadata: SettingDefinition = {
|
const metadata: SettingDefinition = {
|
||||||
key: "metadata",
|
key: "metadata",
|
||||||
label: m["convert.settings.common.metadata"](),
|
label: m["convert.settings.common.metadata"](),
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: true,
|
default: global.metadata ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// resize, crop, rotate - prob want a ui
|
// resize, crop, rotate - prob want a ui
|
||||||
|
|
||||||
return [bitrate, sampleRate, channels, metadata];
|
return [bitrate, sampleRate, tracks, channels, metadata];
|
||||||
}
|
}
|
||||||
|
|
||||||
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(input: VertFile, to: string): Promise<VertFile> {
|
public async convert(
|
||||||
|
input: VertFile,
|
||||||
|
to: string,
|
||||||
|
settings: ConversionSettings,
|
||||||
|
): Promise<VertFile> {
|
||||||
if (!to.startsWith(".")) to = `.${to}`;
|
if (!to.startsWith(".")) to = `.${to}`;
|
||||||
|
|
||||||
|
const conversionSettings =
|
||||||
|
Object.keys(settings).length > 0
|
||||||
|
? settings
|
||||||
|
: await this.getDefaultSettings(input); // use defaults if not provided
|
||||||
|
|
||||||
const isAlac = to === ".alac";
|
const isAlac = to === ".alac";
|
||||||
if (isAlac) to = ".m4a";
|
if (isAlac) to = ".m4a";
|
||||||
|
|
||||||
|
|
@ -181,7 +226,10 @@ export class FFmpegConverter extends Converter {
|
||||||
msg.includes("Specified sample rate") &&
|
msg.includes("Specified sample rate") &&
|
||||||
msg.includes("is not supported")
|
msg.includes("is not supported")
|
||||||
) {
|
) {
|
||||||
const rate = Settings.instance.settings.ffmpegCustomSampleRate;
|
const rate =
|
||||||
|
conversionSettings.sampleRate === "custom"
|
||||||
|
? conversionSettings.customSampleRate
|
||||||
|
: conversionSettings.sampleRate;
|
||||||
conversionError = m["workers.errors.invalid_rate"]({
|
conversionError = m["workers.errors.invalid_rate"]({
|
||||||
rate,
|
rate,
|
||||||
});
|
});
|
||||||
|
|
@ -212,6 +260,7 @@ export class FFmpegConverter extends Converter {
|
||||||
ffmpeg,
|
ffmpeg,
|
||||||
input,
|
input,
|
||||||
to,
|
to,
|
||||||
|
conversionSettings,
|
||||||
isAlac,
|
isAlac,
|
||||||
);
|
);
|
||||||
log(["converters", this.name], `FFmpeg command: ${command.join(" ")}`);
|
log(["converters", this.name], `FFmpeg command: ${command.join(" ")}`);
|
||||||
|
|
@ -267,16 +316,21 @@ export class FFmpegConverter extends Converter {
|
||||||
this.activeConversions.delete(input.id);
|
this.activeConversions.delete(input.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupFFmpeg(input: VertFile): Promise<FFmpeg> {
|
private async setupFFmpeg(
|
||||||
|
input: VertFile,
|
||||||
|
temporary = false,
|
||||||
|
): Promise<FFmpeg> {
|
||||||
const ffmpeg = new FFmpeg();
|
const ffmpeg = new FFmpeg();
|
||||||
|
|
||||||
ffmpeg.on("progress", (progress) => {
|
if (!temporary) {
|
||||||
input.progress = progress.progress * 100;
|
ffmpeg.on("progress", (progress) => {
|
||||||
});
|
input.progress = progress.progress * 100;
|
||||||
|
});
|
||||||
|
|
||||||
ffmpeg.on("log", (l) => {
|
ffmpeg.on("log", (l) => {
|
||||||
log(["converters", this.name], l.message);
|
log(["converters", this.name], l.message);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const baseURL =
|
const baseURL =
|
||||||
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
|
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
|
||||||
|
|
@ -370,10 +424,105 @@ export class FFmpegConverter extends Converter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async detectAudioTracks(ffmpeg: FFmpeg): Promise<number | null> {
|
||||||
|
const args = [
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"a",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=index",
|
||||||
|
"-of",
|
||||||
|
"json",
|
||||||
|
"input",
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
const tracksListener = (event: { message: string }) => {
|
||||||
|
output += `${event.message}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
ffmpeg.on("log", tracksListener);
|
||||||
|
|
||||||
|
try {
|
||||||
|
log(
|
||||||
|
["converters", this.name],
|
||||||
|
`Running ffprobe to detect audio tracks with args: ${args.join(" ")}`,
|
||||||
|
);
|
||||||
|
await ffmpeg.ffprobe.call(ffmpeg, args);
|
||||||
|
} finally {
|
||||||
|
ffmpeg.off("log", tracksListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.trim()) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
const tracks = Array.isArray(parsed?.streams)
|
||||||
|
? parsed.streams.length
|
||||||
|
: null;
|
||||||
|
|
||||||
|
log(
|
||||||
|
["converters", this.name],
|
||||||
|
`Detected stream audio tracks: ${tracks}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
} catch (err) {
|
||||||
|
error(
|
||||||
|
["converters", this.name],
|
||||||
|
`Error detecting audio tracks: ${err}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectAudioChannels(ffmpeg: FFmpeg): Promise<number | null> {
|
||||||
|
const args = [
|
||||||
|
"-v",
|
||||||
|
"0",
|
||||||
|
"-select_streams",
|
||||||
|
"a",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=channels",
|
||||||
|
"-of",
|
||||||
|
"compact=p=0:nk=1",
|
||||||
|
"input",
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let channels: number | null = null;
|
||||||
|
|
||||||
|
const channelsListener = (event: { message: string }) => {
|
||||||
|
if (channels !== null) return;
|
||||||
|
const n = parseInt(event.message.trim(), 10);
|
||||||
|
if (!n) return;
|
||||||
|
channels = n;
|
||||||
|
log(
|
||||||
|
["converters", this.name],
|
||||||
|
`Detected stream audio channels: ${channels}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ffmpeg.on("log", channelsListener);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ffmpeg.ffprobe.call(ffmpeg, args);
|
||||||
|
return channels;
|
||||||
|
} finally {
|
||||||
|
ffmpeg.off("log", channelsListener);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async buildConversionCommand(
|
private async buildConversionCommand(
|
||||||
ffmpeg: FFmpeg,
|
ffmpeg: FFmpeg,
|
||||||
input: VertFile,
|
input: VertFile,
|
||||||
to: string,
|
to: string,
|
||||||
|
settings: ConversionSettings,
|
||||||
isAlac: boolean = false,
|
isAlac: boolean = false,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const inputFormat = input.from.slice(1);
|
const inputFormat = input.from.slice(1);
|
||||||
|
|
@ -390,14 +539,16 @@ export class FFmpegConverter extends Converter {
|
||||||
"dsf",
|
"dsf",
|
||||||
"dff",
|
"dff",
|
||||||
];
|
];
|
||||||
const userSetting = Settings.instance.settings.ffmpegQuality;
|
const userBitrate = settings.bitrate;
|
||||||
const userSampleRate = Settings.instance.settings.ffmpegSampleRate;
|
const customBitrate = settings.customBitrate;
|
||||||
const customSampleRate =
|
const userSampleRate = settings.sampleRate;
|
||||||
Settings.instance.settings.ffmpegCustomSampleRate ?? 44100;
|
const customSampleRate = settings.customSampleRate;
|
||||||
const keepMetadata = Settings.instance.settings.metadata;
|
const keepMetadata = settings.metadata;
|
||||||
|
|
||||||
let audioBitrateArgs: string[] = [];
|
let audioBitrateArgs: string[] = [];
|
||||||
let sampleRateArgs: string[] = [];
|
let sampleRateArgs: string[] = [];
|
||||||
|
let channelsArgs: string[] = [];
|
||||||
|
let tracksArgs: string[] = [];
|
||||||
let metadataArgs: string[] = [];
|
let metadataArgs: string[] = [];
|
||||||
let m4aArgs: string[] = [];
|
let m4aArgs: string[] = [];
|
||||||
|
|
||||||
|
|
@ -415,12 +566,15 @@ export class FFmpegConverter extends Converter {
|
||||||
|
|
||||||
const isLosslessToLossy =
|
const isLosslessToLossy =
|
||||||
lossless.includes(inputFormat) && !lossless.includes(outputFormat);
|
lossless.includes(inputFormat) && !lossless.includes(outputFormat);
|
||||||
if (userSetting !== "auto") {
|
if (userBitrate !== "auto") {
|
||||||
// user's setting
|
// user's setting
|
||||||
audioBitrateArgs = ["-b:a", `${userSetting}k`];
|
audioBitrateArgs = [
|
||||||
|
"-b:a",
|
||||||
|
`${userBitrate === "custom" ? customBitrate : userBitrate}k`,
|
||||||
|
];
|
||||||
log(
|
log(
|
||||||
["converters", this.name],
|
["converters", this.name],
|
||||||
`using user setting for audio bitrate: ${userSetting}`,
|
`using user setting for audio bitrate: ${userBitrate}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// detect bitrate of original file and use
|
// detect bitrate of original file and use
|
||||||
|
|
@ -445,14 +599,13 @@ export class FFmpegConverter extends Converter {
|
||||||
|
|
||||||
// sample rate setting
|
// sample rate setting
|
||||||
if (userSampleRate !== "auto") {
|
if (userSampleRate !== "auto") {
|
||||||
const rate =
|
sampleRateArgs = [
|
||||||
userSampleRate === "custom"
|
"-ar",
|
||||||
? customSampleRate.toString()
|
userSampleRate === "custom" ? customSampleRate : userSampleRate,
|
||||||
: userSampleRate;
|
];
|
||||||
sampleRateArgs = ["-ar", rate];
|
|
||||||
log(
|
log(
|
||||||
["converters", this.name],
|
["converters", this.name],
|
||||||
`using user setting for sample rate: ${rate}`,
|
`using user setting for sample rate: ${userSampleRate}Hz`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// detect sample rate of original file and use
|
// detect sample rate of original file and use
|
||||||
|
|
@ -476,7 +629,7 @@ export class FFmpegConverter extends Converter {
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleRateArgs = inputSampleRate
|
sampleRateArgs = inputSampleRate
|
||||||
? ["-ar", inputSampleRate.toString()]
|
? ["-ar", `${inputSampleRate}`]
|
||||||
: [];
|
: [];
|
||||||
log(
|
log(
|
||||||
["converters", this.name],
|
["converters", this.name],
|
||||||
|
|
@ -485,6 +638,33 @@ export class FFmpegConverter extends Converter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// channels setting
|
||||||
|
if (settings.channels !== "auto") {
|
||||||
|
channelsArgs = ["-ac", settings.channels];
|
||||||
|
log(
|
||||||
|
["converters", this.name],
|
||||||
|
`using user setting for audio channels: ${settings.channels}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tracks setting
|
||||||
|
// TODO: select specific tracks? (prob should be for the other settings that need extra ui stuff)
|
||||||
|
if (settings.tracks !== "auto") {
|
||||||
|
// -map for each audio track
|
||||||
|
if (settings.tracks > 1) {
|
||||||
|
for (let i = 0; i < settings.tracks; i++) {
|
||||||
|
tracksArgs.push("-map", `0:a:${i - 1}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracksArgs = ["-map", "0:a:0"]; // default to first audio track if not specified
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
["converters", this.name],
|
||||||
|
`using user setting for audio tracks: ${settings.tracks}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// video to audio
|
// video to audio
|
||||||
if (videoFormats.includes(inputFormat)) {
|
if (videoFormats.includes(inputFormat)) {
|
||||||
log(
|
log(
|
||||||
|
|
@ -499,6 +679,8 @@ export class FFmpegConverter extends Converter {
|
||||||
...metadataArgs,
|
...metadataArgs,
|
||||||
...audioBitrateArgs,
|
...audioBitrateArgs,
|
||||||
...sampleRateArgs,
|
...sampleRateArgs,
|
||||||
|
...channelsArgs,
|
||||||
|
...tracksArgs,
|
||||||
"output" + to,
|
"output" + to,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -538,6 +720,8 @@ export class FFmpegConverter extends Converter {
|
||||||
...metadataArgs,
|
...metadataArgs,
|
||||||
...audioBitrateArgs,
|
...audioBitrateArgs,
|
||||||
...sampleRateArgs,
|
...sampleRateArgs,
|
||||||
|
...channelsArgs,
|
||||||
|
...tracksArgs,
|
||||||
"output" + to,
|
"output" + to,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -558,6 +742,8 @@ export class FFmpegConverter extends Converter {
|
||||||
...metadataArgs,
|
...metadataArgs,
|
||||||
...audioBitrateArgs,
|
...audioBitrateArgs,
|
||||||
...sampleRateArgs,
|
...sampleRateArgs,
|
||||||
|
...channelsArgs,
|
||||||
|
...tracksArgs,
|
||||||
"output" + to,
|
"output" + to,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -580,6 +766,8 @@ export class FFmpegConverter extends Converter {
|
||||||
...metadataArgs,
|
...metadataArgs,
|
||||||
...audioBitrateArgs,
|
...audioBitrateArgs,
|
||||||
...sampleRateArgs,
|
...sampleRateArgs,
|
||||||
|
...channelsArgs,
|
||||||
|
...tracksArgs,
|
||||||
"output" + to,
|
"output" + to,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -758,6 +946,7 @@ const getCodecs = (
|
||||||
|
|
||||||
export const CONVERSION_BITRATES = [
|
export const CONVERSION_BITRATES = [
|
||||||
"auto",
|
"auto",
|
||||||
|
"custom",
|
||||||
320,
|
320,
|
||||||
256,
|
256,
|
||||||
192,
|
192,
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ export interface SettingDefinition {
|
||||||
label: string;
|
label: string;
|
||||||
type: SettingType;
|
type: SettingType;
|
||||||
default?: any;
|
default?: any;
|
||||||
placeholder?: string;
|
placeholder?: any;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
options?: Array<{ value: any; label: string }>; // for select types
|
options?: Array<{ value: any; label: any }>; // for select types
|
||||||
description?: string;
|
description?: string;
|
||||||
|
hasCustomInput?: boolean; // for select types with a "custom" option
|
||||||
|
customInputKey?: string; // key to use for custom input value in settings object
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversionSettings {
|
export interface ConversionSettings {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export class VertFile {
|
||||||
return this.file.name;
|
return this.file.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public conversionSettings = $state<ConversionSettings>({});
|
public conversionSettings = $state<ConversionSettings>({}); // empty object = defaults
|
||||||
public progress = $state(0);
|
public progress = $state(0);
|
||||||
public result = $state<VertFile | null>(null);
|
public result = $state<VertFile | null>(null);
|
||||||
|
|
||||||
|
|
@ -39,10 +39,10 @@ export class VertFile {
|
||||||
|
|
||||||
public isZip = $state(() => this.from === ".zip");
|
public isZip = $state(() => this.from === ".zip");
|
||||||
|
|
||||||
public getAvailableSettings(): Promise<SettingDefinition[]> {
|
public getAvailableSettings(input: VertFile): Promise<SettingDefinition[]> {
|
||||||
const converter = this.findConverter();
|
const converter = this.findConverter();
|
||||||
if (!converter) return Promise.resolve([]);
|
if (!converter) return Promise.resolve([]);
|
||||||
return converter.getAvailableSettings();
|
return converter.getAvailableSettings(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findConverters(supportedFormats: string[] = [this.from]) {
|
public findConverters(supportedFormats: string[] = [this.from]) {
|
||||||
|
|
@ -108,14 +108,6 @@ export class VertFile {
|
||||||
this.blobUrl = blobUrl;
|
this.blobUrl = blobUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public settings() {
|
|
||||||
// settings modal
|
|
||||||
// images - quality/compression/quantize/interlace/depth-DPI, resize, crop, rotate, flip/flop, autoOrient?, color space/bit depth, transparency settings
|
|
||||||
// audio - bitrate, sample rate, channels, normalize, trim silence
|
|
||||||
// video - bitrate, fps, resolution, trim, crop, rotate, flip/flop, audio settings?
|
|
||||||
// common - metadata?
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public async convert(...args: any[]) {
|
public async convert(...args: any[]) {
|
||||||
if (!this.converters.length) throw new Error("No converters found");
|
if (!this.converters.length) throw new Error("No converters found");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue