Skip to content

argano

An MP4 or MOV holds the encoded picture and sound in mdat, and the index that locates every frame in moov. A recorder writes the media as it goes and the index last, when you press stop. Interrupt it, with a dead battery, a force quit, a full disk, or a bug, and the file is almost entirely intact and completely unplayable.

argano reads that media with no index at all. It finds where each frame begins by parsing the frames themselves, puts picture and sound back on a clock, and writes a new index. The media bytes in the output are the media bytes from the input, so nothing is re-encoded and the original file is never modified. It was written for long Photo Booth recordings on macOS.

A web page does the same job with nothing installed: the same algorithm in browser JavaScript, reading the file from disk, sending it nowhere, working offline after the first visit.

Supported
Containers MP4, MOV, M4V
Video H.264, H.265
Audio AAC, uncompressed PCM
Damage index never written, index destroyed, file truncated, noise written over the media
  • It will not repair a file whose media is genuinely gone. If the bytes were never written or were overwritten, nothing recovers them, and a tool that claims otherwise is guessing pixels.
  • It will not touch AV1, VP9, ProRes, MKV or AVI. None of that is written yet.
  • It will not fix a video that plays with visible corruption. That is damage inside the encoded picture, a different problem from a missing index.
  • It will not re-encode anything to make a file play. If it cannot index your media it says so rather than producing something that opens.
  • It cannot always keep the sound. Where damage sits inside a run of audio, the frames under it are gone, and argano reports how many.
Terminal window
pip install argano

The repair itself is pure Python with no dependencies. Sound needs libfaad, the one piece that is not a Python package: AAC frames carry no length of their own, so finding where each one ends takes a decoder that reports how many bytes it consumed.

Terminal window
brew install faad2 # macOS
sudo apt install libfaad2 # Debian, Ubuntu

Without it the picture is still recovered in full and argano says the sound was left out. The window needs pip install 'argano[gui]'. With ffmpeg present, argano decodes the finished file end to end as a last check.

Terminal window
argano inspect "broken.mov" # what is wrong, before changing anything
argano repair "broken.mov" --reference "working.mov" # writes "broken (repaired).mov" beside the original
argano repair "broken.mov" -r "working.mov" -o fixed.mp4 --faststart
argano repair "silent.mov" -r "working.mov" --fps 30 # no sound, so a frame rate has to be assumed
argano gui

As a library:

from argano import repair, RepairOptions
result = repair("broken.mov", RepairOptions(reference="working.mov"))
print(result.video_frames, result.duration, result.scan.damage)

Frames are found by reading them. Walking the stream by the length in front of each unit of video fails: four bytes of compressed picture read as a length give a plausible number often enough that the walk lands mid frame and produces noise. argano reads the slice header behind each length, whose first three Exp-Golomb fields have to agree with the settings the file declared, which keeps the walk on the rails for hundreds of thousands of frames.

Timing comes from the audio. Phones and screen recorders write variable frame rate video, so a constant rate assumed over fifteen minutes drifts until picture and sound part company. Every compressed audio frame is a fixed number of samples, so counting the frames in a run gives its exact duration. argano anchors each run of video to the audio beside it and never needs a frame rate.

The decoder is reset after damage. libfaad handed a stretch of noise stays broken: its state carries prediction and windowing from the previous frame, so every frame after the noise decodes wrong, silently, with no error reported. A fresh decoder per run of audio, and again after every error, is the whole fix.

The output is read back from its byte offsets. A track header written four bytes too long still parses by name, so QuickTime played it fine, while players reading by offset took a cell of the display matrix as the picture width and stretched the video about ten to one. That bug shipped. The finished file is now checked against three sources that have to agree: the lengths the format fixes, the picture size in the encoder’s parameter sets, and the sample tables’ arithmetic.

Terminal window
pip install -e '.[dev]'
pytest # the Python engine
node --test web/lib/*.test.mjs # the browser engine and the AAC parser

The tests build their own samples with ffmpeg, break them in the ways that happen to people, repair them and compare against the originals. Frame counts have to match exactly, and the output media bytes have to be identical to the input media bytes, which is the check that catches a re-encode creeping in. Two unit tests are regressions for the bugs above.

The browser engine carries its own AAC parser, written from the format specification because the C library for this is under a licence the project cannot bundle. It is tested against 1218 real frame boundaries from four recordings, which it gets right exactly. Both engines are held to the same answers on the same bytes, which stops the ports drifting.

untrunc solves the same problem and has repaired a great many files. It follows sample lengths, so it loses alignment at the first stretch of noise: on the fifteen minute recording argano was written for, it recovered 25 seconds. That is one file and not a benchmark.