Every phone-as-a-camera project treats "recording started" as the finish line. Capture is the demo. What decides whether the project was worth doing is what's on disk nine hours later, after something went wrong.
Two failure modes account for most "I had it recording all night and got nothing" reports. Neither is a bug in the camera code. One is the container format. The other is where the file was written.
Why won't my MP4 play after a crash?
The player says the file is corrupt. You pull it off the device and it's 2.3 GB. The video is in there. What's missing is the part that says where.
An MP4 is a sequence of boxes (ISO base media file format "atoms"). Two matter here:
-
mdat— the media data. Encoded frames, appended as they're produced. -
moov— the movie header: track definitions, timescales, and the sample tables mapping every frame to a byte offset and a presentation time.
A muxer can't write moov until it knows the complete sample table, and it doesn't know the complete sample table until recording stops. So the normal write order is ftyp, then mdat growing for the entire recording, then moov at the very end.
Kill the process before that last step — a crash, an OOM kill, a battery pull, someone unplugging the phone — and you have a file containing every frame you recorded and no index describing them. Players can't seek and can't decode. They report it as damaged.
This inverts the usual intuition about risk: the longer the recording ran, the more you lose. A ten-second clip and a nine-hour overnight run fail in exactly the same way. The nine-hour one costs you nine hours.
+faststart does not fix this
The common reply is "just move the moov atom to the front." That's -movflags +faststart in ffmpeg and it's a real thing, but it's a post-processing step: ffmpeg records normally, writes moov at the end, then rewrites the file with moov relocated so a player can start before the download finishes. It runs after a successful recording. It cannot help a recording that never finished.
The fix that survives an interruption: stop writing one enormous file
MediaRecorder has supported rolling output since API 26. You queue the next file in advance; when a size or duration limit is hit, the recorder rolls over to it and fires MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED, which is your cue to queue another.
recorder.setMaxDuration(5 * 60 * 1000) // 5 minutes per segment
recorder.setOnInfoListener { _, what, _ ->
when (what) {
MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED -> {
// previous segment is closed and complete; queue the one after this
recorder.setNextOutputFile(nextSegmentFile())
}
}
}
recorder.setOutputFile(firstSegmentFile())
recorder.prepare()
recorder.start()
recorder.setNextOutputFile(nextSegmentFile()) // must be queued before the limit hits
Every segment is closed properly and gets its own moov. An interruption now costs you the current segment and nothing else. Bounded loss instead of total loss. MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING gives you the same hook keyed to bytes rather than time, which is the one you want if your bitrate varies.
Segmenting also quietly solves the other overnight killer. A single-file recorder that runs out of storage has exactly one option: stop. A segmented recorder has a better one: delete the oldest segment and keep going. That converts "recording stopped at 2 a.m. because the card filled up" into a rolling retention window — which is what you wanted from a camera in the first place.
If you already have a broken file
Don't overwrite it, and don't keep recording to the same volume.
Recovery works by borrowing the missing structure from a healthy file. Record a short clip on the same device, same camera, same resolution, framerate and codec settings, then hand both files to a tool like untrunc: it reads the sample-table layout from the reference and rebuilds an index over your orphaned mdat. The settings really do have to match. A reference clip from a different phone, or the same phone at a different resolution, produces garbage or nothing.
Where the file lives decides whether it survives an uninstall
The second failure mode has no error message at all. The recordings were fine. Then they weren't there.
Scoped storage arrived in Android 10 and was enforced in Android 11, and it split external storage into two places with very different lifetimes:
-
context.getExternalFilesDir(...)→/sdcard/Android/data/<package>/files/…. No permission needed, which is why so much sample code uses it. It is deleted when the app is uninstalled. It's also not scanned into the media index, so the user's gallery never shows it and people go looking for footage that is technically still on the device. -
MediaStore, inserting intoMediaStore.Video.Media.EXTERNAL_CONTENT_URIwithRELATIVE_PATHunderEnvironment.DIRECTORY_MOVIES. Indexed, visible in the gallery, and it survives the uninstall.
val values = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, "cam_${System.currentTimeMillis()}.mp4")
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
put(MediaStore.Video.Media.RELATIVE_PATH, "${Environment.DIRECTORY_MOVIES}/MyCam")
put(MediaStore.Video.Media.IS_PENDING, 1) // hide it while it's half-written
}
Clear IS_PENDING when the segment closes. Without it, the gallery cheerfully shows users a stream of incomplete files.
There's a real tradeoff on the shared side: after an uninstall and reinstall your app no longer owns those entries, so on API 30+ it needs MediaStore.createWriteRequest or createTrashRequest — a user consent dialog — to modify or delete them. For footage somebody might actually need, outliving the app that made it is worth a consent prompt.
Neither of these shows up in a demo
You record thirty seconds, you stop cleanly, the file plays, you ship. Both failure modes need an interruption or an uninstall to appear, and neither happens while you're testing. They happen on the night that mattered.
If you're building this: segment the output, write to a shared collection, and test by killing the process mid-recording rather than by pressing stop.
I work on Background Camera RemoteStream, an Android app for running a phone as a screen-off camera with a live remote view and no cloud account — more at superfunicular.com. None of the above is specific to it. The moov atom and scoped storage behave the same way for every app on the platform, which is exactly why it's worth knowing before you trust a phone with an overnight recording.
Related: how an Android phone keeps recording with the screen off covers the process-lifecycle half of the problem, and "local-only" is not the same as "private" covers who else on your network can reach the stream.
Top comments (0)