diff --git a/messages/en.json b/messages/en.json
index 086d0bc..6009e9c 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -84,7 +84,7 @@
"settings": {
"settings": "Settings",
"title": "File conversion settings",
- "description": "Change the conversion settings for {filename}, which is using {converter}. These settings may not be available for all formats.",
+ "description": "Change the conversion settings for {filename}, which is using {converter}. These settings may not be available for all formats. This is an early beta and may have some issues.",
"none": "No settings available for this format.",
"image": {
"quality": "Quality",
@@ -94,30 +94,42 @@
},
"audio": {
"bitrate": "Bitrate (kbps)",
+ "bitrate_placeholder": "Custom bitrate",
"sample_rate": "Sample rate (Hz)",
+ "sample_rate_placeholder": "Custom sample rate",
"channels": "Audio channels",
- "tracks": "Audio tracks"
+ "channels_placeholder": "Custom audio channels",
+ "tracks": "Audio tracks",
+ "tracks_placeholder": "Custom audio tracks"
},
"video": {
"quality": "Quality",
"metadata": "Metadata",
"speed": "Conversion speed",
- "speed_very_slow": "Very Slow",
- "speed_slower": "Slower",
- "speed_slow": "Slow",
- "speed_medium": "Medium",
- "speed_fast": "Fast",
- "speed_ultra_fast": "Ultra Fast",
+ "speed_description": "This will be overridden if you manually set the bitrate or resolution below - selecting options other than \"auto\".",
+ "speed_very_slow": "Highest quality (slowest)",
+ "speed_slower": "Higher quality (slower)",
+ "speed_slow": "High quality (slow)",
+ "speed_medium": "Medium quality (average)",
+ "speed_fast": "Lower quality (faster)",
+ "speed_ultra_fast": "Lowest quality (fastest)",
"fps": "Frame rate (FPS)",
- "fps_placeholder": "Auto",
+ "fps_placeholder": "Custom frame rate",
"resolution": "Resolution",
- "resolution_placeholder": "Auto (e.g., 1920x1080)"
+ "resolution_placeholder": "Custom resolution",
+ "video_bitrate": "Video bitrate (kbps)",
+ "audio_bitrate": "Audio bitrate (kbps)",
+ "bitrate_placeholder": "Custom bitrate",
+ "sample_rate": "Audio sample rate (Hz)",
+ "sample_rate_placeholder": "Custom sample rate"
},
"document": {
"something": "Something"
},
"common": {
- "metadata": "Metadata"
+ "metadata": "Metadata",
+ "auto": "auto",
+ "custom": "custom"
}
},
"tooltips": {
diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/SettingsModal.svelte
index 904197b..0b90919 100644
--- a/src/lib/components/functional/SettingsModal.svelte
+++ b/src/lib/components/functional/SettingsModal.svelte
@@ -86,13 +86,17 @@
{:else}
{#each availableSettings as setting (setting.key)}
-
+
{setting.label}
{#if setting.description}
-
+
{setting.description}
{/if}
@@ -102,9 +106,11 @@
options={setting.options?.map(
(opt) => opt.value,
) || []}
- selected={file.conversionSettings[
- setting.key
- ] ?? setting.default}
+ selected={settings[setting.key] ??
+ file.conversionSettings[
+ setting.key
+ ] ??
+ setting.default}
settingsStyle
onselect={(value) =>
handleSettingChange(
@@ -139,9 +145,11 @@
{:else if setting.type === "boolean"}
handleSettingChange(
@@ -149,12 +157,51 @@
e.currentTarget.checked,
)}
/>
+ {:else if setting.type === "range"}
+ {@const rangeValue = (settings[
+ setting.key
+ ] ??
+ file.conversionSettings[
+ setting.key
+ ] ??
+ setting.default ??
+ setting.min ??
+ 0) as number}
+ {@const rangeLabel =
+ setting.options?.[rangeValue]
+ ?.label ?? rangeValue}
+
+ {
+ const nextValue =
+ e.currentTarget
+ .valueAsNumber;
+ handleSettingChange(
+ setting.key,
+ nextValue,
+ );
+ }}
+ />
+
+ {rangeLabel}
+
+
{:else}
handleSettingChange(
diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts
index fb8fdaa..9d7baad 100644
--- a/src/lib/converters/ffmpeg.svelte.ts
+++ b/src/lib/converters/ffmpeg.svelte.ts
@@ -109,21 +109,11 @@ export class FFmpegConverter extends Converter {
}
}
- public async getAvailableSettings(
- input: VertFile,
- ): Promise {
+ public async getAvailableSettings(): Promise {
// audio - bitrate, sample rate, channels, normalize, trim silence
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 = {
key: "bitrate",
label: m["convert.settings.audio.bitrate"](),
@@ -135,10 +125,9 @@ export class FFmpegConverter extends Converter {
})),
hasCustomInput: true,
customInputKey: "customBitrate",
- placeholder: detectedBitrate ?? "128"
+ placeholder: m["convert.settings.audio.bitrate_placeholder"](),
};
- const detectedSampleRate = await this.detectAudioSampleRate(ffmpeg);
const sampleRate: SettingDefinition = {
key: "sampleRate",
label: m["convert.settings.audio.sample_rate"](),
@@ -153,31 +142,31 @@ export class FFmpegConverter extends Converter {
})),
hasCustomInput: true,
customInputKey: "customSampleRate",
- placeholder: detectedSampleRate ?? "44100"
+ placeholder: m["convert.settings.audio.sample_rate_placeholder"](),
};
- const audioTracks = await this.detectAudioTracks(ffmpeg);
const tracks: SettingDefinition = {
key: "tracks",
label: m["convert.settings.audio.tracks"](),
type: "number",
- default: audioTracks ?? 1,
+ default: 1,
min: 1,
- max: audioTracks ? audioTracks : 1,
- placeholder: audioTracks ?? 1
+ placeholder: m["convert.settings.audio.tracks_placeholder"](),
};
- const audioChannels = await this.detectAudioChannels(ffmpeg);
const channels: SettingDefinition = {
key: "channels",
label: m["convert.settings.audio.channels"](),
type: "number",
- default: audioChannels ?? 2,
+ default: 2,
min: 1,
- max: audioChannels ? audioChannels * 2 : 5,
- placeholder: audioChannels ?? 2
+ max: 8,
+ placeholder: m["convert.settings.audio.channels_placeholder"](),
};
+ /*
+ * common
+ */
const metadata: SettingDefinition = {
key: "metadata",
label: m["convert.settings.common.metadata"](),
@@ -426,100 +415,6 @@ export class FFmpegConverter extends Converter {
}
}
- private async detectAudioTracks(ffmpeg: FFmpeg): Promise {
- 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 {
- 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(
ffmpeg: FFmpeg,
input: VertFile,
@@ -962,12 +857,12 @@ export type ConversionBitrate = (typeof CONVERSION_BITRATES)[number];
export const SAMPLE_RATES = [
"auto",
"custom",
- "48000",
- "44100",
- "32000",
- "22050",
- "16000",
- "11025",
- "8000",
+ 48000,
+ 44100,
+ 32000,
+ 22050,
+ 16000,
+ 11025,
+ 8000,
] as const;
export type SampleRate = (typeof SAMPLE_RATES)[number];
diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts
index 93279d3..52adcaf 100644
--- a/src/lib/converters/vertd.svelte.ts
+++ b/src/lib/converters/vertd.svelte.ts
@@ -12,6 +12,7 @@ import type {
SettingDefinition,
ConversionSettings,
} from "$lib/types/conversion-settings";
+import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte";
interface UploadResponse {
id: string;
@@ -85,6 +86,15 @@ export type ConversionSpeed =
| "fast"
| "ultraFast";
+const vertdSpeedValues: ConversionSpeed[] = [
+ "verySlow",
+ "slower",
+ "slow",
+ "medium",
+ "fast",
+ "ultraFast",
+];
+
interface StartJobMessage {
type: "startJob";
data: {
@@ -375,30 +385,128 @@ export class VertdConverter extends Converter {
},
];
- const quality: SettingDefinition = {
- key: "vertdSpeed",
+ const qualitySpeedRange: SettingDefinition = {
+ key: "vertdSpeedSlider",
label: m["convert.settings.video.speed"](),
- type: "select",
- default: "medium",
- options: qualityOptions,
+ description: m["convert.settings.video.speed_description"](),
+ type: "range",
+ min: 0,
+ max: qualityOptions.length - 1,
+ step: 1,
+ default: 3,
+ options: qualityOptions.map((option, index) => ({
+ value: index,
+ label: option.label,
+ speedValue: option.value,
+ })),
+ forceFullWidth: true,
};
- // TODO: for fps and resolution, set placeholder to detected values
const fps: SettingDefinition = {
key: "fps",
label: m["convert.settings.video.fps"](),
+ type: "select",
+ default: "auto",
+ options: [
+ { value: "auto", label: m["convert.settings.common.auto"]() },
+ {
+ value: "custom",
+ label: m["convert.settings.common.custom"](),
+ },
+ { value: 24, label: "24" },
+ { value: 30, label: "30" },
+ { value: 60, label: "60" },
+ { value: 120, label: "120" },
+ { value: 144, label: "144" },
+ { value: 240, label: "240" },
+ ],
+ hasCustomInput: true,
+ customInputKey: "customFps",
placeholder: m["convert.settings.video.fps_placeholder"](),
- type: "number",
- min: 1,
};
const resolution: SettingDefinition = {
key: "resolution",
label: m["convert.settings.video.resolution"](),
+ type: "select",
+ default: "auto",
+ options: [
+ { value: "auto", label: m["convert.settings.common.auto"]() },
+ {
+ value: "custom",
+ label: m["convert.settings.common.custom"](),
+ },
+ { value: "426x240", label: "426x240" },
+ { value: "640x360", label: "640x360" },
+ { value: "854x480", label: "854x480" },
+ { value: "1280x720", label: "1280x720" },
+ { value: "1920x1080", label: "1920x1080" },
+ { value: "2560x1440", label: "2560x1440" },
+ { value: "3840x2160", label: "3840x2160" },
+ ],
+ hasCustomInput: true,
+ customInputKey: "customResolution",
placeholder: m["convert.settings.video.resolution_placeholder"](),
- type: "string",
};
+ // TODO: allow CRF for consistent quality?
+ const videoBitrate: SettingDefinition = {
+ key: "videoBitrate",
+ label: m["convert.settings.video.video_bitrate"](),
+ type: "select",
+ default: "auto",
+ options: [
+ { value: "auto", label: m["convert.settings.common.auto"]() },
+ {
+ value: "custom",
+ label: m["convert.settings.common.custom"](),
+ },
+ { value: 1000, label: "1000 kbps" },
+ { value: 2500, label: "2500 kbps" },
+ { value: 5000, label: "5000 kbps" },
+ { value: 8000, label: "8000 kbps" },
+ { value: 12000, label: "12000 kbps" },
+ { value: 18000, label: "18000 kbps" },
+ ],
+ hasCustomInput: true,
+ customInputKey: "customBitrate",
+ placeholder: m["convert.settings.video.bitrate_placeholder"](),
+ };
+
+ /*
+ * audio settings
+ */
+ const audioBitrate: SettingDefinition = {
+ key: "audioBitrate",
+ label: m["convert.settings.video.audio_bitrate"](),
+ type: "select",
+ default: "auto",
+ options: CONVERSION_BITRATES.map((b) => ({
+ value: b,
+ label: b,
+ })),
+ hasCustomInput: true,
+ customInputKey: "customBitrate",
+ placeholder: m["convert.settings.audio.bitrate_placeholder"](),
+ };
+
+ const sampleRate: SettingDefinition = {
+ key: "sampleRate",
+ label: m["convert.settings.audio.sample_rate"](),
+ type: "select",
+ default: "auto",
+ options: SAMPLE_RATES.map((r) => ({
+ value: r,
+ label: r,
+ })),
+ hasCustomInput: true,
+ customInputKey: "customSampleRate",
+ placeholder: m["convert.settings.audio.sample_rate_placeholder"](),
+ };
+
+ /*
+ * common
+ */
const metadata: SettingDefinition = {
key: "metadata",
label: m["convert.settings.common.metadata"](),
@@ -408,9 +516,15 @@ export class VertdConverter extends Converter {
// trim/crop/rotate - also have another ui for this prob
- // import all audio settings?
-
- return [quality, fps, resolution, metadata];
+ return [
+ qualitySpeedRange,
+ videoBitrate,
+ resolution,
+ fps,
+ metadata,
+ audioBitrate,
+ sampleRate,
+ ];
}
public async getDefaultSettings(): Promise {
@@ -419,10 +533,19 @@ export class VertdConverter extends Converter {
settings.forEach((setting) => {
defaults[setting.key] = setting.default;
});
+
+ if (defaults.vertdSpeedSlider !== undefined) {
+ const sliderIndex = defaults.vertdSpeedSlider as number;
+ defaults.vertdSpeed = vertdSpeedValues[sliderIndex];
+ }
return defaults;
}
- public async convert(input: VertFile, to: string, settings: ConversionSettings): Promise {
+ public async convert(
+ input: VertFile,
+ to: string,
+ settings: ConversionSettings,
+ ): Promise {
if (to.startsWith(".")) to = to.slice(1);
let fileUpload = input;
@@ -481,7 +604,14 @@ export class VertdConverter extends Converter {
});
ws.onopen = () => {
- const speed = Settings.instance.settings.vertdSpeed;
+ let speed = settings.vertdSpeed as ConversionSpeed | undefined;
+ const sliderIndex = settings.vertdSpeedSlider as
+ | number
+ | undefined;
+ if (sliderIndex !== undefined) {
+ speed = vertdSpeedValues[sliderIndex] || speed;
+ }
+ if (!speed) speed = Settings.instance.settings.vertdSpeed;
const keepMetadata = Settings.instance.settings.metadata;
this.log(
`opened ws connection to vertd for file ${input.name}`,
diff --git a/src/lib/css/app.scss b/src/lib/css/app.scss
index becfa7a..97e8880 100644
--- a/src/lib/css/app.scss
+++ b/src/lib/css/app.scss
@@ -413,6 +413,36 @@ body {
@apply outline outline-accent outline-2;
}
+ input[type="range"].range-slider {
+ @apply w-full h-[10px] appearance-none bg-transparent;
+ }
+
+ input[type="range"].range-slider:focus {
+ @apply outline-none;
+ }
+
+ // for some reason, thumb and tracks behave differently in webkit (chromium) and firefox
+ // so i had to do some manual adjustments to get them similar :sob: -maya
+ input[type="range"].range-slider::-webkit-slider-runnable-track {
+ @apply h-[10px] rounded-full bg-button;
+ appearance: none;
+ }
+
+ input[type="range"].range-slider::-webkit-slider-thumb {
+ @apply bg-panel w-[18px] h-[18px] -mt-1 rounded-full cursor-pointer shadow-md;
+ appearance: none;
+ border: 2px solid var(--accent);
+ }
+
+ input[type="range"].range-slider::-moz-range-track {
+ @apply h-[10px] rounded-full bg-button;
+ appearance: none;
+ }
+
+ input[type="range"].range-slider::-moz-range-thumb {
+ @apply bg-panel border-2 border-accent w-4 h-4 rounded-full cursor-pointer shadow-md;
+ }
+
hr {
@apply border-separator;
}
diff --git a/src/lib/types/conversion-settings.ts b/src/lib/types/conversion-settings.ts
index fff8d4b..7b34912 100644
--- a/src/lib/types/conversion-settings.ts
+++ b/src/lib/types/conversion-settings.ts
@@ -2,20 +2,21 @@
export type SettingType = "number" | "select" | "boolean" | "string" | "range";
export interface SettingDefinition {
- key: string;
- label: string;
- type: SettingType;
- default?: any;
- placeholder?: any;
- min?: number;
- max?: number;
- step?: number;
- options?: Array<{ value: any; label: any }>; // for select types
- description?: string;
- hasCustomInput?: boolean; // for select types with a "custom" option
- customInputKey?: string; // key to use for custom input value in settings object
+ key: string;
+ label: string;
+ type: SettingType;
+ default?: any;
+ placeholder?: any;
+ min?: number;
+ max?: number;
+ step?: number;
+ options?: Array<{ value: any; label: any; speedValue?: any }>; // for select/range types
+ description?: string;
+ hasCustomInput?: boolean; // for select types with a "custom" option
+ customInputKey?: string; // key to use for custom input value in settings object
+ forceFullWidth?: boolean; // force setting to take up full width (usually grid 2)
}
export interface ConversionSettings {
- [key: string]: any;
-}
\ No newline at end of file
+ [key: string]: any;
+}