Guides

Repairing files in the browser: how IntactFile works

Most "corrupt" media files are not corrupt at all — the data is intact, only the index that maps it is missing. IntactFile rebuilds that index locally: the file is read from disk, repaired in the tab via WebAssembly, and written back. Zero bytes are uploaded, and you can prove it in the Network tab. This is how it works, and where it honestly stops working.

The dead file that isn't dead

Someone pulls an SD card while the camera light is still on. A drone hits water. OBS crashes at the three-hour mark. A phone thermal-throttles and the recording app is killed mid-write. In every one of these cases the result is the same: a multi-gigabyte video file that every player refuses to open. ffmpeg prints moov atom not found. The operating system offers to delete it. It looks, by every outward sign, dead.

It usually isn't. An MP4 or MOV file is mostly one enormous block of compressed frames — the mdat box — with a small index at the end called the moov atom. The moov is the map: it records where every frame begins, how large it is, when it should be displayed, and the codec configuration a decoder needs to set itself up. A player reads the index first and uses it to navigate the data. Without it, the player doesn't know where a single frame starts, so it gives up completely rather than playing "most of" the file.

Here is the cruel part of the design: the moov atom is written last, in one pass, at the moment a recording is finalized. It can't be written earlier, because a frame's byte offset can't be known before the frame exists, and the camera has no idea whether you'll stop in ten seconds or two hours. So recorders stream frames into mdat as they encode them and write the index only when you press stop. Interrupt that finalization — battery pop, thermal shutdown, card pull, crash — and you get a file full of real, intact footage with no index at all. The bytes are there. The map never got written.

IntactFile exists for exactly this gap between "the data survived" and "the file opens." For a missing-moov MP4 or MOV, it rebuilds the index. For a lot of other formats, it rebuilds whatever equivalent structure went missing. And it does all of it on your machine, without the file ever leaving the tab.

Why repair means upload — and why we didn't

Search for a file-repair tool and nearly every result works the same way: you upload your file to a server, a process on that server runs the actual repair, and you download the result. There's a straightforward reason for this. The repair logic is usually a native binary — a C or C++ program built around libraries like ffmpeg — and the easy place to run a native binary is a machine you control. The browser was, for years, the wrong place to run that kind of code.

We didn't want to build it that way, for reasons that are practical before they are philosophical. The files people need repaired are exactly the files that are painful to upload: multi-gigabyte recordings, often on a slow connection, often the only copy of something irreplaceable. Asking someone to upload a 6 GB wedding video to a stranger's server, wait through the transfer twice, and trust that the original is deleted afterward is a bad experience wrapped around a worse privacy story. The contents of the file are none of our business, and the simplest way to honor that is to make it impossible for us to see them.

So the constraint we set was blunt: the file never leaves your device. Not "encrypted in transit," not "deleted after processing" — not transferred at all. That turned the whole problem into an engineering question: can the repair itself run in the browser? Thanks to WebAssembly, the answer is now yes, and the rest of this post is about what it took to get there and where the seams still show.

Rebuilding a moov atom, step by step

Reconstructing a missing moov is the flagship case, so it's worth walking through. The problem: you have ftyp (a tiny "this is an MP4" header), a giant mdat, and no index. The opening that makes rebuilding possible is that mdat isn't truly opaque to a parser that understands the codecs. Frames are stored as length-prefixed units, and each codec's bitstream carries recognizable structure — start codes and headers that mark a frame's type, its dimensions, its boundaries. A parser that knows what an H.264 or HEVC access unit looks like can walk the raw data and recover the boundaries the index used to describe.

This is the class of repair the open-source tool untruncestablished. We treat untrunc strictly as an algorithmic reference — it's GPL-licensed — and our video engine is a clean-room reimplementation in TypeScript, which is what lets the identical code run in a browser tab and in Node. The steps are:

  1. Find the frame boundaries. Scan mdat, using the length prefixes and codec signatures to locate where each video and audio frame starts and ends.
  2. Classify and measure. Assign each frame to its stream and record its size and byte offset, rebuilding sample by sample the raw material of the sample-size (stsz) and chunk-offset (stco) tables.
  3. Recover timing and configuration. The per-frame timing and the decoder configuration lived in the lost index. When the broken file can't supply them, a healthy reference clip from the same camera, recorded at the same settings, teaches the engine what "normal" looks like — the frame rate, the timescale, the codec parameter sets.
  4. Write a fresh index and finalize. Assemble the recovered sample tables into a new moov and write out a spec-correct file.

The result plays everything that physically survived, ending where the data ends. The honest limits fall straight out of the mechanism. The very last seconds — the ones still sitting in the camera's memory buffer, never flushed to storage — are genuinely gone; no scan can find frames that were never written. Recordings with highly irregular timing can come back with minor audio/video drift, because the reconstructed clock is inferred rather than recorded. And if mdat itself is zeros because the storage failed rather than the recorder, there are no frames to find. MOV is the QuickTime format MP4 grew out of and shares this exact structure, so a .mov that won't open and an .mp4that won't open are almost always the same problem in different clothes.

The other formats, briefly

Once you frame corruption as "the payload is fine, the index is broken," the same shape appears in format after format. Most container and document formats keep their data in one place and a table of contents somewhere else, and it's almost always the table of contents that dies first. Each engine below rebuilds a different flavor of that table.

What each engine rebuilds, and how it's implemented.
FormatWhat's brokenHow it's rebuilt
MP4 / MOVMissing or truncated moov indexClean-room TypeScript reconstruction from the surviving frames
ZIP / OOXMLLost central directory (the archive's index)Pure TypeScript: scan local file headers, rebuild the central directory
PDFBroken cross-reference (xref) tableA TypeScript xref pre-pass, then qpdf compiled to WebAssembly
JPEGDamaged or missing header tablesmozjpeg / jSquash decode plus a donor-header transplant
PNGCorrupt chunk lengths, CRCs or structurePure TypeScript byte surgery on the chunk stream
SQLiteDamaged B-tree pagesRecover reachable records, then rebuild the database
RAR / 7zArchive won't extractExtraction via 7z compiled to WebAssembly

A few are worth a sentence more. ZIP and OOXML are the same engine: a .docx, .xlsx or.pptx is a ZIP archive, and ZIP keeps a "central directory" at the end listing every entry. Lose it and the entries themselves are still there, each with its own local header, so the engine walks the file finding those headers and rebuilds the directory around them — no library, pure TypeScript. PDF keeps a cross-reference table mapping every object to a byte offset; when it's broken, a TypeScript pre-pass re-derives the offsets before handing the file to qpdf, which we compiled to WebAssembly rather than shelling out to a binary. JPEG is the odd one: the compressed scan data often survives while the header carrying the quantization and Huffman tables does not, so we transplant a donor header from a healthy photo taken on the same camera, then decode through mozjpeg. Different tables, same principle throughout: rebuild the index, keep the data untouched.

The architecture: WebAssembly, in your tab

Running all of this client-side is a WebAssembly story with a few sharp edges worth being honest about. Some engines are pure TypeScript and just run. Others wrap existing native code compiled to WASM — qpdf for PDF, 7z for archives, and, for the video work, libav.js. The libav.js choice is deliberate and licensing-driven: it's an LGPL build of the FFmpeg libraries, which is shippable in a product, rather than a GPLffmpeg build, which would not be. Licensing is a real engineering constraint here, not an afterthought.

The two harder constraints are memory and isolation.

Memory: stream, don't buffer

WebAssembly runs in a bounded address space — in practice something like 2–4 GB depending on the browser and the build. The files people most need to repair are frequently larger than that: an unfinalized recording can be many gigabytes. So the engines can't simply load the whole file into memory and go. They stream — reading and processing the file in ranges, holding only what a given step needs, and writing output incrementally. That keeps memory bounded and makes it possible to repair a file in a tab that could never hold the entire thing at once. (We keep performance claims qualitative on purpose: it depends heavily on the file, the codec, and the machine.)

Isolation: two origins, on purpose

Fast WASM wants threads, and threads in the browser needSharedArrayBuffer, which browsers only enable for pages that are cross-origin isolated — pages that send the right COOP and COEP headers and accept the restrictions that come with them. Those restrictions are awkward for a normal marketing page that carries third-party things like ads. Rather than compromise one for the other, we split the product across two origins: a conventional marketing origin that can carry ads, and a separate, cross-origin-isolated app origin serving COOP/COEP so the multithreaded WASM engines can run at full speed. It's more moving parts than a single site, but it lets the reading material and the repair machinery each have the environment they need.

Honesty as a return value

The most important design decision isn't in any of the codecs. It's that every engine returns an explicit outcome — success,partial, or failed — together with a damage class describing what it found. It never fabricates a success.

This matters because file repair is a field full of tools that write out a file no matter what, leaving you to discover hours later that it doesn't actually play, or plays for four seconds and freezes. When the map is missing and the data survived, we can rebuild it and say so. When the data is physically gone — overwritten, zeroed by a failing disk, truncated away — there is nothing to rebuild, and the right answer is to say that, clearly, rather than emit a plausible-looking file that fails when it matters. "Partial" is a first-class result too: recovering the first two hours of a three-hour recording and being upfront that the last hour didn't survive is a genuinely useful outcome, as long as we don't dress it up as a complete one.

Treating the failure case as an honest, named result rather than an embarrassment to be hidden is, we think, the difference between a tool you can trust with the only copy of something and one you can't.

Verify it yourself: open the Network tab

You don't have to take "0 bytes uploaded" on faith. It's the kind of claim you can check in under a minute, and we'd rather you did:

  1. Open the repair tool in your browser.
  2. Open DevTools (F12 or Cmd/Ctrl + Shift + I) and switch to the Network tab.
  3. Drop a broken file onto the page.
  4. Watch the requests. You'll see the page's own assets load, but no request carries the contents of your file. The bytes go from disk into the tab and back to disk — never onto the wire.

For the truly suspicious, the stronger test: pull your network connection out entirely and run the repair offline. It still works, because there was never a server in the loop to begin with. The same engines also power a Node package for command-line and automated use, but on the website the repair happens where your file already is.

FAQ

Does IntactFile upload my file to a server?

No. Every engine runs client-side in your browser, compiled to WebAssembly. The file is read from your disk with the File API, rebuilt in the tab, and saved back to your disk. Zero bytes of the file are sent over the network. You can confirm this yourself: open DevTools, watch the Network tab, drop a file, and observe that no request carries its contents. The same engines also run in Node for command-line and server use, but on the website nothing leaves your machine.

Is this the same as untrunc?

It solves the same class of problem — reconstructing a missing MP4/MOV index from the surviving media data — but the code is not untrunc. untrunc is GPL-licensed, so we use it only as an algorithmic reference and wrote a clean-room implementation in TypeScript. That also lets the exact same engine run unmodified in the browser and in Node, which a native C++ tool can't do without a separate build.

Why do some repairs ask for a second, healthy file?

When an MP4/MOV lost its index, the timing and decoder configuration that lived there are gone too. A healthy reference clip recorded on the same camera, at the same settings, lets the engine learn what "normal" frame timing and codec setup look like for your device and apply them to the recovered frames. For JPEG, the same idea applies to headers: a donor header from a photo taken with the same camera supplies the tables a decoder needs. If the broken file still carries enough of its own metadata, no reference is needed.

Can it fix any corrupt file?

No, and it won't pretend to. Container and index repair works when the payload survived and only the map was lost or damaged — which covers a large share of real-world "corruption." When the actual data is physically gone (overwritten, zeroed by a failing disk, or truncated away), there is nothing to rebuild, and every honest tool has to say so. Each engine returns an explicit outcome — success, partial, or failed — with a damage class, rather than emitting a plausible-looking file that doesn't play.

How does it handle very large video files?

Unfinalized recordings are often several gigabytes, and browser WebAssembly has a bounded address space (roughly 2–4 GB depending on the browser and build). So the engines stream: they read and process the file in ranges rather than buffering the whole thing into memory at once. That keeps memory use bounded and lets multi-gigabyte files be repaired in a tab that could never hold them all at once.

Want the byte-level tour of the format at the center of all this? ReadMP4 anatomy: what the moov atom is and why your video won't play.