From 0e5f549804c68c5535d344aaaf10af493d696761 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 27 Oct 2025 22:16:41 +0300 Subject: [PATCH] feat: warn of large file sizes due to browser limits browsers / devices will have different arraybuffer sizes. if exceeded, block conversion or disable certain ones (video to audio) --- messages/en.json | 4 +- .../functional/FormatDropdown.svelte | 12 +++ src/lib/consts.ts | 2 + src/lib/store/index.svelte.ts | 79 ++++++++++++++++++- src/lib/toast/index.svelte.ts | 1 - src/lib/types/file.svelte.ts | 13 ++- src/routes/convert/+page.svelte | 19 ++++- 7 files changed, 125 insertions(+), 5 deletions(-) diff --git a/messages/en.json b/messages/en.json index 4cee4ac..871cd0a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -52,6 +52,7 @@ "extracted": "Extracted {extract_count} files from {filename}. {ignore_count} items were ignored.", "extract_error": "Error extracting {filename}: {error}" }, + "large_file_warning": "Due to browser / device limitations, video to audio conversion is disabled for this file as it is larger than {limit}GB. We recommend using Firefox or Safari for files of this size since they have less limitations.", "external_warning": { "title": "External server warning", "text": "If you choose to convert into a video format, those files will be uploaded to an external server to be converted. Do you want to continue?", @@ -255,7 +256,8 @@ "ffmpeg": "Error loading FFmpeg, some features may not work as expected.", "pandoc": "Error loading Pandoc worker, document conversion may not work as expected.", "no_audio": "No audio stream found.", - "invalid_rate": "Invalid sample rate specified: {rate}Hz" + "invalid_rate": "Invalid sample rate specified: {rate}Hz", + "file_too_large": "This file exceeds the {limit}GB browser / device limit. Try Firefox or Safari to convert this large file, which typically have higher limits." } }, "privacy": { diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 69027ac..097a899 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -7,6 +7,7 @@ import { ChevronDown, SearchIcon } from "lucide-svelte"; import { onMount } from "svelte"; import { quintOut } from "svelte/easing"; + import type { VertFile } from "$lib/types"; type Props = { categories: Categories; @@ -15,6 +16,7 @@ onselect?: (option: string) => void; disabled?: boolean; dropdownSize?: "default" | "large" | "small"; + file?: VertFile; }; let { @@ -24,6 +26,7 @@ onselect, disabled, dropdownSize = "default", + file, }: Props = $props(); let open = $state(false); let dropdown = $state(); @@ -67,6 +70,15 @@ ); if (from === ".gif") finalCategories.push("video"); + // filter out categories that can't handle large files (due to browser/device limitations) + if (file && file.isLarge()) { + // if file is large video, disable audio conversion + if (rootCategory === "video") + finalCategories = finalCategories.filter( + (cat) => cat !== "audio", + ); + } + return finalCategories; }); diff --git a/src/lib/consts.ts b/src/lib/consts.ts index f9d1eb3..207d701 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -15,3 +15,5 @@ export const CONTACT_EMAIL = "hello@vert.sh"; // i'm not entirely sure this should be in consts.ts, but it is technically a constant as .env is static for VERT export const DISABLE_ALL_EXTERNAL_REQUESTS = PUB_DISABLE_ALL_EXTERNAL_REQUESTS === "true"; + +export const GB = 1024 * 1024 * 1024; diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 3102ad4..6d4504e 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -11,6 +11,7 @@ import { m } from "$lib/paraglide/messages"; import sanitizeHtml from "sanitize-html"; import { unzip } from "fflate"; import { ToastManager } from "$lib/toast/index.svelte"; +import { GB } from "$lib/consts"; class Files { public files = $state([]); @@ -259,7 +260,20 @@ class Files { this.files.push(vf); this._addThumbnail(vf); - const isVideo = converter.name === "vertd"; + const convName = converter.name; + if (file.size > MAX_ARRAY_BUFFER_SIZE && convName === "vertd") { + ToastManager.add({ + type: "warning", + message: m["convert.large_file_warning"]({ + limit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2), + }), + durations: { + stay: 10000, + }, + }); + } + + const isVideo = convName === "vertd"; const acceptedExternalWarning = localStorage.getItem("acceptedExternalWarning") === "true"; if (isVideo && !acceptedExternalWarning && !this._warningShown) { @@ -481,3 +495,66 @@ export function sanitize( allowedSchemes: ["http", "https", "mailto", "blob"], }); } + +/** + * Binary search for a max value without knowing the exact value, only that it + * can be under or over It dose not test every number but instead looks for + * 1,2,4,8,16,32,64,128,96,95 to figure out that you thought about #96 from + * 0-infinity + * + * @example findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches) + * @author Jimmy Wärting + * @see {@link https://stackoverflow.com/a/72124984/1008999} + * @param {function} f The function to run the test on (should return truthy or falsy values) + * @param {bigint} [b=1] Where to start looking from + * @param {function} d privately used to calculate the next value to test + * @returns {bigint} Integer + */ +function findFirstPositive( + f: (x: bigint) => number, + b = 1n, + d = (e: bigint, g: bigint, c?: bigint): bigint => + g < e + ? -1n + : 0 < f((c = (e + g) >> 1n)) + ? c == e || 0 >= f(c - 1n) + ? c + : d(e, c - 1n) + : d(c + 1n, g), +): bigint { + for (; 0 >= f(b); b <<= 1n); + return d(b >> 1n, b) - 1n; +} + +export const getMaxArrayBufferSize = (): number => { + if (typeof window === "undefined") return 2 * GB; // default for SSR + + // check cache first + const cached = localStorage.getItem("maxArrayBufferSize"); + if (cached) { + const parsed = Number(cached); + log( + ["converters"], + `using cached max ArrayBuffer size: ${parsed} bytes`, + ); + if (!isNaN(parsed) && parsed > 0) return parsed; + } + + // detect max size using binary search + const maxSize = findFirstPositive((x) => { + try { + new ArrayBuffer(Number(x)); + return 0; // false = can allocate + } catch { + return 1; // true = cannot allocate + } + }); + + const result = Number(maxSize); + localStorage.setItem("maxArrayBufferSize", result.toString()); + log(["converters"], `detected max ArrayBuffer size: ${result} bytes`); + + return result; +}; + +export const MAX_ARRAY_BUFFER_SIZE = getMaxArrayBufferSize(); diff --git a/src/lib/toast/index.svelte.ts b/src/lib/toast/index.svelte.ts index 71dfdf6..a7e3d75 100644 --- a/src/lib/toast/index.svelte.ts +++ b/src/lib/toast/index.svelte.ts @@ -1,5 +1,4 @@ import type { Component } from "svelte"; -import { writable } from "svelte/store"; export type ToastType = "success" | "error" | "info" | "warning"; diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 7501a43..77a0349 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -1,9 +1,9 @@ import { byNative, converters } from "$lib/converters"; import type { Converter } from "$lib/converters/converter.svelte"; -import { error } from "$lib/logger"; import { m } from "$lib/paraglide/messages"; import { ToastManager } from "$lib/toast/index.svelte"; import type { Component } from "svelte"; +import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte"; export class VertFile { public id: string = Math.random().toString(36).slice(2, 8); @@ -63,6 +63,17 @@ export class VertFile { return converter; } + public isLarge(): boolean { + return this.file.size > MAX_ARRAY_BUFFER_SIZE; + } + + public supportsStreaming(): boolean { + // only vertd (video/gif -> video/gif) supports streaming + // rest of converters need entire file in memory, limited by ArrayBuffer limits + const converter = this.findConverter(); + return converter?.name === "vertd"; + } + constructor(file: File, to: string, blobUrl?: string) { const ext = file.name.split(".").pop(); const newFile = new File( diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 13c4875..8b93c9e 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -5,7 +5,7 @@ import Panel from "$lib/components/visual/Panel.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte"; - import { categories, converters, byNative } from "$lib/converters"; + import { categories, converters } from "$lib/converters"; import { effects, files, @@ -29,6 +29,8 @@ } from "lucide-svelte"; import { m } from "$lib/paraglide/messages"; import { Settings } from "$lib/sections/settings/index.svelte"; + import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte"; + import { GB } from "$lib/consts"; let processedFileIds = $state(new Set()); @@ -220,6 +222,7 @@ {@const formatInfo = currentConverter.supportedFormats.find( (f) => f.name === file.from, )} + {@const isLarge = file.isLarge()} {#if formatInfo && !formatInfo.fromSupported}
+ {:else if isLarge && !file.supportsStreaming()} +
+

+ {m["convert.errors.cant_convert"]()} +

+

+ {m["workers.errors.file_too_large"]({ + limit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2), + })} +

+
{:else if currentConverter.status === "downloading"}
handleSelect(option, file)} + {file} />