demo-scrub/ is 28K of text and no video, because video-scrub's output looks
exactly like its source -- that is the design goal. What is worth reading is
the reconciliation, all of it generated by real runs, none hand-written:
- before.txt a plain film's checkup
- dirty-before.txt the same file with GPS, an email address, a platform video
id, a device name, a chapter and a data track injected
- dirty-after.txt cleaned: 14 needles all miss, 12 gates green
- naive.txt counter-example: a naive -c copy, 7 gates red, with byte
offsets
- half.txt the one worth reading
half.txt runs the textbook recipe -- -map_metadata -1, map only the two
streams, drop chapters. ffprobe comes back spotless: no GPS, no email, no
account id, no chapters, 11 gates green. The twelfth reports x264's encoder
settings SEI in 4 places and the AAC DSE in 8. The file puts ffprobe's clean
output and the gate's verdict side by side, which makes the gap between
"looks clean" and "is clean" obvious at a glance.
demo2.mp4 is now gitignored. Nothing references it: the self-test is pure
functions that touch neither ffmpeg nor a real file, so it stays green without
it. Tracking a 12MB binary would also showcase nothing, since a scrubbed file
is visually identical to its source. demo-scrub/README.md carries commands
that reproduce the whole demo from any video.
Both root READMEs get the skill table row and an examples paragraph; both
video-scrub READMEs link to demo-scrub/.
Allowlist, not blocklist: never enumerate what to delete, only declare what
comes across (one video stream, one audio stream). The source's metadata has
no channel into the output. The privacy guarantee comes from the structure,
not from enumeration.
The tags are the easy part. Measured on ffmpeg 8.1.2, a tool writes its
identity into five places:
- container tag's Lavf -> -fflags +bitexact
- avc1 compressorname -> -metadata:s:v:0 encoder=
(container-level blanking does nothing;
stream-level works)
- AAC bitstream DSE -> -flags:a +bitexact
- H.264 SEI (x264's full -> -bsf:v filter_units=remove_types=6
encoder settings string)
- handler_name -> must be written explicitly; blanking falls
back to ffmpeg's default VideoHandler
The fourth is the nasty one: -flags:v +bitexact and -x264-params info=0 both
fail to stop it, and no ffprobe command shows it at all. The full trail,
including what did not work, is in references/residue-map.md so it never has
to be re-derived.
One more that is not among those five: GoPro and DJI put GPS on a separate
gpmd timed-metadata track. Stripping tags does not touch it; only
-map 0:v:0 -map 0:a:0? keeps it out. Hence "stream layout" is its own gate.
Two modes:
- copy (default) keeps the picture byte-for-byte (cmp-verified), 1.3s for a
53s film
- encode re-encodes fully, additionally killing bitstream-domain watermarks
and fragile steganography
Audio is re-encoded in both: the DSE lives inside the audio bitstream, so
-c:a copy cannot reach it.
Four profiles: obs (default) / quicktime / screencapture / bare. obs does not
forge a version number -- the muxer owns that field, -metadata can neither
override nor clear it, so obs simply lets ffmpeg write its own real version;
OBS output is ffmpeg-muxed anyway. A profile only touches the muxing tool's
trace, handler names and creation time. It never writes a device model or GPS.
Verification is a proof, not a claim: every metadata string in the source
becomes a needle and the output is scanned byte by byte. Needles must be >=5
bytes (shorter ones collide at random inside compressed data) and generic
strings are excluded (VideoHandler carries zero information; Lavf58.45.100
pins one version and is a fingerprint).
The SEI knife only comes out conditionally. ffmpeg offers NAL-level
granularity only, so remove_types=6 also removes HDR static metadata and
CEA-608/708 embedded captions. It therefore fires only when an identity
string is actually found, never on an HDR source, and says so either way.
12 gates, 117 self-test assertions, a breach case for every gate. Mutation
tested: gutting the GPS gate, disabling the audio re-encode and disabling
needle filtering were each caught by the assertions.