How on-device file tools work (and why nothing gets uploaded)

You are about to drop a signed contract, a folder of photos or a three-gigabyte screen recording onto a web page that promises nothing is uploaded, and you would like something better than a promise. This guide explains what happens between picking a file and the result landing in your downloads folder: which engines do the work, where the bytes live, why some jobs are fast and others slow, and how to check every claim with the browser’s own developer tools. You will also learn exactly what the one tool that does talk to a server can and cannot see.

What “nothing is uploaded” means, step by step

The classic online converter works in three hops. Your browser sends the file to a server with an HTTP POST; the server queues it, runs a converter and stores the result; you download it; a retention policy decides when the copies are deleted. Every hop is a place your bytes exist on hardware you do not control.

An on-device tool replaces all three hops with memory operations inside one tab. Opening the page downloads code — JavaScript and, for heavy jobs, a WebAssembly engine. When you pick a file, the browser hands the page a File object: a handle to bytes on your disk, exposed by the File API. The page reads it with file.arrayBuffer(), or in slices with file.slice(), and runs the engine on those bytes, usually in a Web Worker so the page stays responsive. The result is a Blob in memory, and “download” means the page creates a blob: URL for it and triggers a save — a copy from your RAM to your disk. Nowhere in that chain is a network request that carries the file.

The PDF merger is the simplest example on the site. It accepts only names ending in .pdf, keeps the merge button disabled until you have added two files, and lets you reorder them with the arrows. Merge & download loads each file with pdf-lib’s PDFDocument.load(await file.arrayBuffer()), copies every page into a fresh document with copyPages, serialises it with save() and downloads the bytes as merged.pdf. Pages are copied as objects, not re-rendered, so text stays selectable and images keep their resolution.

The engines, and why each one was chosen

EngineUsed forWhy this one
mediabunny + WebCodecsMP4, MOV, MKV and WebM in any combinationUses the device’s hardware video encoder; no WebAssembly heap limit, so 4K converts at full size
ffmpeg.wasm (multi-threaded core 0.12)AVI, FLV and WMV inputs; AVI, FLV and GIF outputs; all audio conversion; codecs WebCodecs cannot decodeThe only engine that reads every legacy container. MP3 output uses LAME in V2 VBR mode
Canvas + wasm-imagemagickCanvas encodes JPG, PNG and WebP; ImageMagick decodes TIFF and PSD and writes GIF, BMP, TIFF and ICOThe browser’s own encoder is fastest; ImageMagick covers what it lacks. HEIC decodes via heic2any, camera RAW via dcraw
pdf-lib, pdf.js, qpdfpdf-lib writes (merge, split, build); pdf.js reads and renders; qpdf applies and removes AES-256 encryptionEach is the reference library for its half of the job
ONNX Runtime WebOCR: a text-detection model (det.onnx, 9.9 MB) and a recognition model (rec.onnx, 21 MB)Runs neural networks in WebAssembly and WebGPU; the models are self-hosted
WebRTCDevice-to-device file transferBrowsers already ship an encrypted peer-to-peer data channel

The FFmpeg core is a 31 MiB .wasm file, over the 25 MiB per-file limit of the static host, so it ships as a 10 MiB ffmpeg-core.wasm.gz and is inflated in your browser with DecompressionStream. On video pages it is not fetched until your first click, keypress or drag — a visitor who bounces from a search result never downloads it.

One conversion, followed end to end

Take the MP4 converter. Its drop zone lists avi · flv · mov · mp4 · mpeg · ogg · webm · wmv & more. The moment you drop a file it is probed: mediabunny reads the primary video track’s dimensions and duration and asks WebCodecs canDecode() — can this browser, on this hardware, decode this codec? That answer decides which of two paths runs.

Path one: hardware codecs

If the answer is yes and the source is a container mediabunny can demux (.mp4, .m4v, .mov, .mkv, .webm, .3gp, .ts, .mts), frames are decoded and re-encoded by the device’s media block, not by software. A 4K source stays 4K, and the options dialog says so: “High-resolution source — converts at original size using your device’s hardware encoder.” Above 1.5 GiB, and only in Chrome or Edge, the page asks where to save before converting and streams the output to that file through the File System Access API, so the result never sits in memory as one buffer.

Path two: FFmpeg in WebAssembly

Everything else goes through ffmpeg.wasm with a command you could run on a desktop:

ffmpeg -i input.avi -c:v libx264 -preset superfast -threads 4 input_output.mp4

Picking a resolution appends -vf scale=-2:720,setsar=1:1; the compress tick adds -crf 23; mute adds -an. A source taller than 1088 pixels is capped to 1080p on this path and the dialog says: “High-resolution source — output is capped at 1080p for in-browser conversion on this device. Pick 720p for a smaller, faster file.” The exit code is checked, so a failed run throws ffmpeg exited with code 1instead of producing a truncated file. A target size in MB becomes a bitrate budget (bytes × 8 × 0.93 ÷ duration, minus 128 kbps for audio); an overshoot triggers one re-encode at a corrected bitrate, with x264’s strict -maxrate/-bufsize control as the final guarantee.

How to verify it yourself

  1. Network tab. Open the developer tools (F12), pick Network, clear it, and run a conversion. You will see GET requests for engine files — /js/ffmpeg/ffmpeg-core.wasm.gz, /js/imagemagick/magickApi.js, /js/pdfjs/pdf.worker.min.mjs — and nothing else. Type method:POST into the filter box: the list is empty.
  2. Airplane mode. Open a tool, click once so the engine loads, then disconnect and convert. A service worker keeps /js/, /_next/static/ and /css/ in a cache bucket named omd-runtime-<build id>, replaced on every site update; only the OCR models (omd-models-…, ~37 MiB) are kept across updates.
  3. Console. Type crossOriginIsolated and press Enter. On /convert-to-mp4 it prints true; on /merge-pdf it prints false.

The exception: device-to-device sharing

Share Files is the one tool with a server-side piece. When you pick files it opens a WebSocket to wss://signal.onmydevice.app and gets a share code; the link and QR code point at /share-files#<code>. The receiver opens the same room, and a Cloudflare Worker relays the WebRTC handshake between the two sockets — offers, answers and ICE candidates — wrapped as { "type": "signal", "raw": "…" } without parsing them. What that Worker sees: the room code, two connections and their IP addresses, and the handshake blobs. What it never sees: file names, sizes, bytes or the PIN. It stores nothing, and the room ceases to exist when both sockets close. Each browser also sends one STUN request to stun.cloudflare.com:3478 to learn its public address.

After the handshake the file moves over a DTLS-encrypted data channel in 64 KiB chunks. The optional 4-digit PIN is checked over that same channel — three wrong entries lock the share — and not even the file list is announced until it matches. Chrome and Edge stream the chunks straight to a file or folder you pick; other browsers assemble them in memory. If no direct route forms within 20 seconds the page asks the Worker for short-lived relay credentials, and when a relay is in use it tells you: “No direct peer-to-peer connection was possible, so this transfer is falling back to a relay server.” The relay forwards ciphertext it cannot read; the large-file transfer guide covers the networking in more depth.

Where the limits come from

LimitCauseWhat you see
~2 GiB per file on the FFmpeg pathThe whole file is loaded into one ArrayBuffer, and browsers cap a single buffer near 2 GiB“Conversion failed. The file may be unsupported, corrupted, or too large to process in this browser.”
1080p maximum output on the FFmpeg pathThe core is compiled with a fixed 1 GiB heap; larger frames abort with out-of-memoryResolution pre-set to 1080p with the cap notice in the dialog
Save dialog before convertingInputs over 1.5 GiB stream to disk instead of buffering (Chrome/Edge only)A native “Save as” prompt appears first
First-load delay10 MiB FFmpeg download plus a 31 MiB inflate; ~37 MiB for OCRA loading state before the first conversion; instant on the second

Speed follows the same logic. A hardware encoder is a dedicated chip that runs at many times real time and barely touches the battery; x264 in WebAssembly is software on four threads, so a ten-minute 1080p clip can take longer than ten minutes on a phone. The same file is therefore quick in Chrome on a laptop and slow in a browser that cannot decode it with WebCodecs.

The image compressor shows the image-side trade-offs. Its editor caps the working image at 4096 px on the long edge, because it holds several full-size copies of the bitmap and a 48 MP phone photo would exhaust a mobile tab. Saving re-encodes JPG, PNG or WebP at quality 0.72 and pulls any photo wider than 2560 px down to that edge; JPG is the default output because PNG has no quality knob. Typing a number into Max file size (KB) switches to a binary search over quality between 0.05 and 0.97, shrinking the dimensions by a quarter whenever even the lowest quality is too large, until the output is under the cap or the search gives up with cannot fit image under N bytes.

Cross-origin isolation in plain terms

The multi-threaded FFmpeg core shares one memory between its worker threads, and the browser only allows that — the SharedArrayBuffer type — when the page is served with two headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Together they put the document in a state called cross-origin isolation, which is what crossOriginIsolatedreports. The second header has a side effect: the browser refuses any cross-origin resource that has not explicitly opted in, and advertising and analytics scripts have not. So the 18 video and audio routes that need the headers carry no third-party scripts — not as a policy statement, but because they would be blocked. Isolation belongs to the loaded document, so every link into or out of those routes is a full page load rather than a client-side transition; that is why switching between a PDF tool and a video tool feels like a “real” navigation. Every other tool is served without the headers.

On-device versus upload-and-convert

On-device (this site)Upload service
File leaves your deviceNo; Share Files sends it only to the device you choseYes, to the provider’s servers
RetentionNone; memory is released when you close the tabSet by the provider’s policy
Size limitsYour own memory and disk: ~2 GiB on the FFmpeg path, more when streamingSet by your plan
Works offlineYes, after the first loadNo
Time on a large fileConversion time onlyUpload, queue, conversion, then download
Cost modelAdvertising and analytics on non-video pagesFree tier, then subscription

What we do collect

Nothing about your files — no names, sizes, contents or hashes reach us. The site is not free of data collection, though. The hosting infrastructure keeps ordinary server logs (IP address, browser type, pages requested). Pages outside the isolated video and audio routes load Google AdSense, Google Analytics and Cloudflare Web Analytics; in the EEA, UK and Switzerland a consent message asks before advertising cookies are set, and the tools themselves set no cookies. We also record anonymous outcome events — a video conversion fell back to FFmpeg, a share failed with reason connection-failed — so we know which paths break; on isolated routes they wait in localStorage until your next visit to a normal page. The full statement is on the privacy policy page.

Common mistakes and how to read the errors

  • Closing the tab mid-conversion.There is no server job to come back to; the work lived in that tab’s memory. Share Files shows the browser’s “Leave site?” prompt and holds a screen wake lock while bytes move, but cannot stop you.
  • “Could not merge these files. One of them may be corrupted or password-protected.” pdf-lib can open an encrypted file but cannot copy its pages. Remove the password with the PDF unlocker first, then merge.
  • A 3 GB AVI fails outright. AVI, FLV and WMV have no hardware path, so the whole file must fit the FFmpeg buffer. Trim it first; for MP4/MOV/MKV/WebM sources, use Chrome or Edge so the streaming path applies.
  • “Could not read this image. The file may be corrupted or in a format this browser cannot decode.” The canvas, ImageMagick and heic2any all declined. A HEIC variant that fails gets its own message; export it from the phone as JPG.
  • “This share link is no longer active — ask the sender to share the file again.” The sender closed their tab. Share links are live sessions, not stored files; both tabs must be open at once.
  • “Couldn’t establish a connection between the two devices.” Usually one side is on mobile data behind carrier-grade NAT. Joining both devices to the same Wi-Fi gives a direct route without any relay.

Do this

  • Before trusting any “in-browser” tool, run one conversion with the Network tab open and filter for method:POST.
  • Load the tool once online, then work offline if the file is sensitive — the engine is cached by the service worker.
  • For video over 1.5 GiB or above 1080p, use Chrome or Edge and an MP4/MOV/MKV/WebM target so the hardware path and disk streaming apply.
  • Unlock password-protected PDFs before merging; pdf-lib cannot copy encrypted pages.
  • On Share Files, put both devices on the same Wi-Fi for a guaranteed direct route, and keep both tabs open until the progress bar reaches 100%.

Frequently asked questions

How can I check that a browser-based converter is not uploading my file?

Open the browser’s developer tools, switch to the Network tab, clear it, and run the conversion. You should see only GET requests for code (WebAssembly engines, workers), never a POST or PUT carrying a body the size of your file. As a second test, load the tool, disconnect from the internet and convert again — an on-device tool still works.

Why does the first conversion take longer than the second?

The engine has to be downloaded once: about 10 MiB (gzipped) for the FFmpeg core, roughly 37 MiB for the OCR runtime and models. The browser caches it afterwards, so later conversions start immediately. After a site update the FFmpeg core is fetched again; the OCR models are kept until they change.

Is there a file size limit for in-browser conversion?

Roughly 2 GiB per file on the FFmpeg path, because the whole file must fit in one memory buffer, and 1080p maximum output there because of its 1 GiB heap. MP4/MOV/MKV/WebM conversions in Chrome or Edge use the hardware-codec path instead, which converts 4K at full size and streams files above 1.5 GiB straight to disk.

Does the device-to-device share tool upload files to a server?

No. A small signaling service relays only the WebRTC handshake so the two browsers can find each other; the file itself travels over an encrypted data channel between them. On networks where a direct connection is impossible, an encrypted relay may forward the bytes, and the page tells you when that happens — the relay sees ciphertext only.

Do these tools work offline?

Yes, once a tool has been opened online and its engine downloaded. A service worker keeps the page code and engine files cached, so you can convert with the connection off. The one exception is the share-files tool, which needs a connection to set up the transfer.

Tools used in this guide

Every one of these runs in your browser — the files you work on never leave your device.