If you landed here because a .mov from an NDI recorder application such as NDI Video Monitor won’t open due to a hung/crashed recorder and ffmpeg says moov atom not found, the video is probably fine and you can get it back. After going through this a few times myself, I created a simple python script called ndirecover for this specific purpose.
Background
I recorded 31 minutes of 1080p60 off an NDI source and ran the disk out of space partway through. The recorder didn’t stop cleanly. What I had afterward was a 29 GB file that nothing would open. Since NDI Video Monitor writes large files, for long duration recordings I’ve had a few issues w/ corruptions and recovery is a pain. This is what the error looks like:
$ ffmpeg -i video.mov -c copy fixed.mov
[mov,mp4,m4a,3gp,3g2,mj2] Format mov,mp4,m4a,3gp,3g2,mj2 detected only with low score of 1
[mov,mp4,m4a,3gp,3g2,mj2] moov atom not found
The first 16 bytes explain it:
$ xxd -l 16 video.mov
00000000: 4e44 495f 5245 4255 494c 4400 0000 0000 NDI_REBUILD.....
NDI’s recorder reserves 4096 bytes at the top of the file and writes frame data immediately after. The QuickTime header, which says where every frame starts and how long it lasts, only gets written into that reserved space when recording stops cleanly. If the recorder doesn’t cleanly stop/exit, that space will stay blank even though the actual video content is technically present.
The dead ends
I’ve done some video file recovery before since my last career was in production but the usual suspects didn’t work well here. These are the tools I tried, unsuccessfully, at first.
untrunc assumes the header was written and the tail was lost. Here the opposite happened. Even -sm, which searches for an mdat atom rather than trusting the file structure, finds nothing, because no mdat was ever written.
movrepair transplants atoms from a reference file around an existing mdat. Same problem. Run it on one of these files and it reads NDI_REBUILD as an atom type, treats the next four bytes as a length, and walks off into noise.
Feeding the raw data to ffmpeg gets you exactly one frame. The rawvideo demuxer slices at fixed byte boundaries with no idea where frames actually start.
What worked
SpeedHQ frames have a header you can find. Byte 0 is a quality value that drifts frame to frame. Bytes 1 to 3 are a 24-bit offset to the second field, and the value 04 00 00 is the single-field case, which every frame in a progressive recording carries.
Three bytes is thin. Over 29 GB you get hundreds of random matches. The thing that makes it work is that NDI pads every frame to a 4096-byte boundary. Real frame headers land on alignment. Random matches essentially never do. Across my file, alignment filtering dropped 23 false positives and left 112,008 frames with no gaps anywhere.
It also means the scan only has to look at one byte in 4096.
From there it’s basic math. Frame sizes are the distances between offsets. Feed those into stsz, the offsets into co64, a constant frame duration into stts, and lift the stsd sample description verbatim from any clip off the same recorder that stopped cleanly. Write an ftyp and an mdat header into the reserved 4096 bytes so the existing video becomes a valid mdat payload, append the moov at the end, and it’s a real MOV.
On the 29 GB file it wrote 1.3 MB and took a few seconds.
$ ffprobe video.mov
codec_name=speedhq
codec_tag_string=SHQ2
width=1920
height=1080
r_frame_rate=60/1
duration=1866.783333
The tool
ndirecover is one Python file with no dependencies. scan reports what it finds, recover rebuilds the header, dry run by default.
./ndirecover.py scan broken.mov
./ndirecover.py recover broken.mov --ref good.mov --write --backup header.bak
You will need a reference recording to run this. You can use any clip from the same recorder at the same resolution and frame rate that stopped cleanly. Ten seconds is plenty.
There are some caveats. Right now, this does not recover audio since NDI interleaves PCM chunks between video frames and I didn’t need them. It also drops the final frame by design. This is because the length can’t be determined and it’s likely to be a partial write since the recording crashed. This is also not a general purpose MOV recovery tool, it assumes the NDI layout.


Comments
9 responses to “How to recover an NDI recording that never finalized (moov atom not found)”
i have a base model M1 studio with a pitiful amount of storage so i unfortunately run into corrupted recordings pretty frequently when i forget to stop a recording
recovering a corrupt file isn't particular complex, but it's not something that typical video file recovery tools work on in ime
I worked through the process of using an external SSD for an M4 Mini and it was a pain but it was worth it for 2TB. Blog post is still half-done but there’s a couple permissions to set and you want to make sure to have a non-external-admin-user in case the drive disconnects.
ooooh
ooh, have been meaning to pick up one of those nice m.2 enclosure/hub combos designed to fit a mini/studio but procrastinated and now everything is stupid expensive
Yeah, this only worked for me because I got the literally last $500 M4 Mini as Tim Apple was discontinuing them and had bought a 2TB last year to use for Time Machine backups of my laptop.
The gotcha is that when you make a new User dir on an external it belongs to the user that made it.
`sudo diskutil enableOwnership /Volumes/DriveName`
`sudo chown -R username /Volumes/DriveName/username`
I should finish this draft from May. I let Claude help me edit it and it got ickfeel.
YOLO maxfenton.com/in/2026/sili…
hell yeah
Recovery notes like this are useful because the failure looks worse than it is. If the media is intact and only the container never finalized, having a repeatable repair path saves a lot of unnecessary re-recording.