video-scrub
Reconstruit une vidéo en un fichier propre : l'image et le son sont conservés, pas un seul octet de métadonnées n'est transféré.
{baseDir} = répertoire contenant ce fichier. Script {baseDir}/scripts/video-scrub.mjs, zéro dépendance, s'exécute directement avec node.
Principe : liste blanche, pas liste noire
Il existe deux approches pour nettoyer les métadonnées, la différence est cruciale :
| Comment ça marche | Problème | |
|---|---|---|
| Liste noire | On liste les champs à supprimer un par un (comme avec exiftool) | On ne peut pas supprimer ce qu'on ignore – les udta propriétaires, les pistes télémétriques GoPro, les SEI dans le flux, un oubli et c'est une fuite |
| Liste blanche | On ne dit pas ce qu'il faut supprimer, on indique ce qu'il faut emporter : un flux image, un flux audio, rien d'autre | — |
Avec la liste blanche, les métadonnées de la source n'ont aucun canal pour entrer. Elles ne sont pas « supprimées », elles n'ont jamais été transportées.
Mais ffmpeg trahit par défaut cette structure à deux reprises, il faut le brider :
- Il copie par défaut les tags du conteneur source vers la sortie →
-map_metadata -1 - Il sélectionne automatiquement les flux, et emportera la piste télémétrique
gpmdde GoPro →-map 0:v:0 -map 0:a:0?
Le point 2 est la seule défense pour les sources comme GoPro / DJI : leur GPS n'est pas dans les tags, mais dans une piste de métadonnées temporelles indépendante. Supprimer les tags ne suffit pas, seul le fait de « ne sélectionner que deux flux » bloque cela.
La vraie difficulté : les couches invisibles
En bridant les deux points ci-dessus, la couche des tags est propre, ffprobe est silencieux. Mais le fichier contient encore trois chaînes d'identité :
| Caché dans | Contenu | Visible par ffprobe |
|---|---|---|
| SEI du H.264 | Chaîne de paramètres complète de x264 cabac=1 ref=3 … crf=23.0 | ✗ |
| DSE du flux AAC | Lavc62.28.102 | ✗ |
| compressorName d'avc1 | Lavc libx264 | ✗ |
Les cinq cachettes et leurs méthodes de colmatage respectives sont détaillées dans {baseDir}/references/residue-map.md – chaque méthode est accompagnée d'un enregistrement de test pratique, y compris les pièges « qui semblaient devoir fonctionner mais n'ont pas marché » (ni -flags:v +bitexact ni -x264-params info=0 ne bloquent le SEI).
Deux modes
| Mode | Image | Audio | Vitesse | Élimine en plus |
|---|---|---|---|---|
copy (par défaut) | Copie octet par octet | Ré-encodé | 53 secondes de vidéo traitées en 1,3 s | — |
encode | Ré-encodé | Ré-encodé | Beaucoup plus lent | Filigranes de flux, stéganographie fragile |
L'audio est ré-encodé dans les deux modes – le numéro de version de l'AAC est écrit dans le DSE, qui se trouve à l'intérieur du flux, -c:a copy ne peut pas l'effacer. Le ré-encodage est peu coûteux et garantit une piste audio propre. En mode copy, l'image n'est vraiment pas modifiée d'un seul bit (vérifié avec cmp, hormis le SEI retiré).
À propos des filigranes : Le mode encode élimine en plus les filigranes de flux et la stéganographie fragile, rares dans les contenus réels. Les filigranes pixel robustes vraiment dangereux (SynthID, filigranes médico-légaux) sont conçus pour résister au ré-encodage, les deux modes sont impuissants. Choisissez donc le mode en fonction de la qualité et de la vitesse, n'espérez pas qu'un ré-encodage efface un filigrane.
Quatre profils
La couche qui est réécrite après le nettoyage :
| Profil | Ce qui est écrit |
|---|---|
obs (par défaut) | Aspect d'OBS Studio. OBS utilise déjà ffmpeg pour l'encapsulation, donc ce profil n'est pas une falsification, mais le maintien d'une apparence normale d'outil similaire |
quicktime | Capture d'écran macOS QuickTime : Core Media Video / Core Media Audio |
screencapture | Capture d'écran ScreenCaptureKit |
bare | Rien n'est écrit. Confidentialité maximale – aucune déclaration falsifiable laissée |
Une chose impossible à faire, mentionnée ici pour éviter d'autres essais : le champ encoder du conteneur est occupé par le muxer lui-même, -metadata encoder=Lavf60.16.100 ne peut pas le remplacer, même le vider est impossible (testé). Seul -fflags +bitexact peut le faire disparaître complètement. Donc le profil obs ne falsifie pas le numéro de version, il laisse ffmpeg écrire sa propre version vraie.
Étape 0 — Voir d'abord ce que la source contient
node {baseDir}/scripts/video-scrub.mjs inspect <video>
Affiche en un écran tout le contenu : tags du conteneur, tags de chaque flux et side data, chapitres, ainsi que les chaînes d'identité dans le flux invisibles pour ffprobe.
À la fin, il indique combien d'« aiguilles » seront utilisées pour la validation et si la coupure du SEI sera effectuée.
Regardez cet écran avant d'agir. Portez une attention particulière à deux choses :
- Y a-t-il des flux
data/timecode– si oui, la source peut contenir une piste télémétrique (GoPro, DJI) - Est-ce du HDR – si oui, les SEI ne sont jamais supprimés (explication ci-dessous)
Étape 1 — Nettoyage
node {baseDir}/scripts/video-scrub.mjs scrub <video> -o out.mp4
Par défaut --mode copy --profile obs. Paramètres courants :
--mode encode # Ré-encodage complet
--profile bare # Rien n'est écrit, confidentialité maximale
--crf 18 # Significatif seulement en mode encode
--date 2026-01-01T00:00:00Z # Spécifie la date de création, par défaut l'heure actuelle
Étape 2 — Validation ⛔ Ne pas sauter
node {baseDir}/scripts/video-scrub.mjs verify <source> <sortie>
Ou en une seule commande (à utiliser au quotidien) :
node {baseDir}/scripts/video-scrub.mjs run <video> -o out.mp4
Le cœur de la validation est simple : tout ce qui précède est « j'ai réglé les bons paramètres », c'est une affirmation ; cette étape est « j'ai scanné, c'est vraiment parti », c'est une preuve.
La méthode consiste à extraire toutes les chaînes de métadonnées de la vidéo source comme des « aiguilles » – les clés et valeurs des tags, les noms de handlers, les titres de chapitres – et à effectuer une recherche au niveau octet dans le fichier de sortie. La moindre piqûre fait échouer le test.
Deux considérations d'implémentation :
- Les aiguilles doivent avoir ≥5 octets. Des chaînes plus courtes pourraient générer des faux positifs aléatoires dans les données compressées. Les aiguilles ignorées sont listées dans les messages.
- Les chaînes génériques ne sont pas des aiguilles.
VideoHandlerexiste partout, son information est nulle ;Lavf58.45.100pointe vers une version spécifique, c'est une empreinte. La frontière est « peut-on réduire la portée à une personne, un appareil, une exportation spécifique ? ».
Les 12 vérifications
| Vérification | Ce qui est bloqué |
|---|---|
| Résidu au niveau octet | Une chaîne de métadonnées de la source se retrouve dans la sortie |
| Résidu au niveau des tags | La valeur d'un tag de la sortie provient de la source |
| Information de localisation | Tout champ GPS / location |
| Identifiants d'appareil et logiciel | make / model / software / artist / comment, etc. |
| Horodatage de la source | Le creation_time de la sortie est égal à celui de la source |
| Matrice de rotation | En mode encode, la rotation n'est pas cuite dans l'image (ignoré en mode copy – la supprimer rendrait l'image de travers) |
| Composition des flux | Un flux data / timecode / pièce jointe s'est glissé dedans |
| Chapitres | Les chapitres ont été transportés |
| Chaîne d'identité du flux | SEI / DSE / compressorName contient encore le nom du codec |
| Conformité au profil | Les tags restants ne correspondent pas au profil déclaré |
| Durée | La sortie a été tronquée sans le dire |
| Décodabilité | Le fichier produit est corrompu et ne peut pas être ouvert |
HDR et sous-titres incrustés : quand ne pas toucher au SEI
ffmpeg ne permet qu'une granularité au niveau NAL – filter_units supprime par type NAL, h264_metadata n'a pas d'option pour « supprimer seulement un certain payload SEI ». Donc remove_types=6 supprime tous les SEI en une fois. Or, dans les SEI, en plus de la chaîne de paramètres inutile de x264, on trouve :
- Les métadonnées statiques HDR (mastering display / content light level) – les supprimer entraîne une perte de couleur, le tone mapping est cassé
- Les sous-titres incrustés CEA-608/708 – les supprimer fait disparaître les sous-titres
La stratégie du script est donc : ne supprimer les SEI que si une chaîne d'identité est effectivement détectée, et jamais sur une source HDR (le script indique clairement qu'il n'a pas agi et pourquoi). Cette décision est prise par seiPlan(), vous n'avez pas à vous en soucier, mais vous devez savoir qu'elle existe.
Limites (ce qui n'est pas fait)
Ne supprime pas les filigranes (les filigranes pixel robustes résistent au ré-encodage, n'y comptez pas), ne modifie pas le contenu de l'image (pas de recadrage, pas de redimensionnement, pas de rotation), ne falsifie pas le modèle d'appareil ni le GPS (les profils n'écrivent que des informations d'outil, pas d'appareil de prise de vue ni de localisation), ne fait pas de scan de répertoire (une vidéo à la fois), ne traite pas les conteneurs autres que mp4/mov.
Auto-test
node {baseDir}/scripts/selftest.mjs
117 assertions, ne touche pas à ffmpeg, ne touche pas à de vrais fichiers. Chacune des 12 vérifications a un cas de test de contournement – prouvant qu'elle bloque réellement. Exécutez ce test après toute modification du script.