video-scrub
Reconstruye un video en un archivo limpio: la imagen y el sonido se transfieren, pero no un solo byte de metadatos.
{baseDir} = directorio donde se encuentra este archivo. El script {baseDir}/scripts/video-scrub.mjs, sin dependencias, se ejecuta directamente con node.
Principio: lista blanca, no lista negra
Hay dos enfoques para limpiar metadatos, y la diferencia es de vida o muerte:
| Cómo se hace | Problema | |
|---|---|---|
| Lista negra | Se enumeran los campos a eliminar uno por uno (como con exiftool) | No puedes eliminar lo que no sabes que existe — udta privado del fabricante, pistas de telemetría de GoPro, SEI en el flujo de bits; uno que se pase, uno que se pierde |
| Lista blanca | No se dice qué eliminar, sino qué llevar: un flujo de imagen, un flujo de sonido, nada más | — |
Con lista blanca, los metadatos del video original no tienen ningún conducto para entrar. No es que "se eliminen", es que nunca se transportan.
Pero ffmpeg traicionaría esta estructura por defecto dos veces, hay que sujetarlo:
- Por defecto copia las etiquetas del contenedor original a la salida →
-map_metadata -1 - Por defecto selecciona flujos automáticamente, arrastrando consigo la pista de telemetría
gpmdde GoPro →-map 0:v:0 -map 0:a:0?
El punto 2 es la única defensa para videos tipo GoPro/DJI: su GPS no está en las etiquetas, está en una pista independiente de metadatos temporales. Limpiar etiquetas no lo elimina; solo "seleccionar solo dos flujos" lo detiene.
El verdadero desafío: las capas invisibles
Con lo anterior controlado, la capa de etiquetas queda limpia, y ffprobe muestra silencio. Pero aún hay tres identificadores ocultos en el archivo:
| Dónde se esconde | Contenido | ¿Lo ve ffprobe? |
|---|---|---|
| SEI de H.264 | Cadena completa de parámetros x264 cabac=1 ref=3 … crf=23.0 | ✗ |
| DSE del flujo AAC | Lavc62.28.102 | ✗ |
| compressorname de avc1 | Lavc libx264 | ✗ |
Los cinco escondites y cómo taparlos están en {baseDir}/references/residue-map.md — cada uno tiene registro de pruebas reales,
incluyendo varias "trampas" que parecen funcionar pero no (-flags:v +bitexact y -x264-params info=0 no detienen el SEI).
Dos modos
| Modo | Video | Audio | Velocidad | ¿Qué más elimina? |
|---|---|---|---|---|
copy (por defecto) | Copia byte a byte | Recodifica | 53 s de video en 1.3 s | — |
encode | Recodifica | Recodifica | Mucho más lento | Marcas de agua en flujo de bits, esteganografía frágil |
El audio se recodifica en ambos modos — el número de versión de AAC está escrito dentro del DSE, que está en el flujo de bits; -c:a copy no puede eliminarlo.
Recodificarlo es barato y deja la pista de audio limpia. En el modo copy, el video no se toca ni un bit (verificado con cmp,
excepto por el SEI eliminado).
Sobre marcas de agua: encode elimina más marcas de agua en el flujo de bits y esteganografía frágil, que son raras en material real.
Las marcas de agua de píxel robustas (SynthID, marcas forenses de video) están diseñadas para resistir la recodificación; ningún modo las elimina.
Así que elige el modo por calidad y velocidad, no esperes que recodificar elimine las marcas de agua.
Cuatro perfiles
Después de limpiar, se escribe la capa de salida:
| Perfil | ¿Qué escribe? |
|---|---|
obs (por defecto) | Apariencia de OBS Studio. OBS ya usa ffmpeg internamente, así que esto no es una falsificación, sino mantener la apariencia normal de una herramienta similar |
quicktime | Grabación de pantalla de macOS QuickTime: Core Media Video / Core Media Audio |
screencapture | Grabación de pantalla de ScreenCaptureKit |
bare | No escribe nada. Máxima privacidad — no deja ninguna declaración que pueda ser verificada |
Hay algo que no se puede hacer, y se menciona aquí para que no se intente: el campo encoder del contenedor es ocupado por el propio muxer,
-metadata encoder=Lavf60.16.100 no lo sobrescribe (ni siquiera vaciarlo lo elimina, probado). Solo -fflags +bitexact lo hace desaparecer por completo.
Así que el perfil obs no falsifica números de versión; simplemente deja que ffmpeg escriba su propia versión real.
Paso 0 — Examina primero qué trae el video original
node {baseDir}/scripts/video-scrub.mjs inspect <video>
Muestra todo en una pantalla: etiquetas del contenedor, etiquetas y side data de cada flujo, capítulos, y las cadenas de identidad dentro del flujo de bits que ffprobe no ve.
Al final te dirá cuántas "agujas" se usarán en la verificación y si se tocará el SEI.
Mira esta pantalla antes de actuar. Presta especial atención a:
- Si hay flujos
data/timecode— si los hay, el video original puede tener pistas de telemetría (GoPro, DJI) - Si es HDR — en ese caso los SEI no se eliminan (explicado más abajo)
Paso 1 — Limpia
node {baseDir}/scripts/video-scrub.mjs scrub <video> -o out.mp4
Por defecto --mode copy --profile obs. Parámetros comunes:
--mode encode # Recodificación completa
--profile bare # No escribe nada, máxima privacidad
--crf 18 # Solo tiene sentido en modo encode
--date 2026-01-01T00:00:00Z # Especifica la marca de tiempo de creación; si no se da, se usa la actual
Paso 2 — Verificación ⛔ No se puede omitir
node {baseDir}/scripts/video-scrub.mjs verify <original> <salida>
O todo en uno (úsalo a diario):
node {baseDir}/scripts/video-scrub.mjs run <video> -o out.mp4
El núcleo de la verificación es una frase: todo lo anterior es "he ajustado los parámetros correctos", que es una afirmación; este paso es "lo he escaneado, realmente no está", que es una prueba.
El método consiste en extraer todas las cadenas de metadatos del video original como "agujas" — claves y valores de etiquetas, nombres de handlers, títulos de capítulos — y escanear byte a byte el archivo de salida en busca de ellas. Si se clava una, se marca en rojo.
Dos detalles de implementación:
- Las agujas deben tener ≥5 bytes. Cadenas más cortas pueden causar falsos positivos aleatorios en datos comprimidos; las agujas omitidas se listan en el mensaje.
- Las cadenas genéricas no cuentan como agujas.
VideoHandlerestá por todas partes, aporta cero información;Lavf58.45.100fija una versión específica, es una huella. La línea divisoria es "¿puede esta cadena reducir el alcance a una persona, un dispositivo, una exportación específica?".
12 puertas
| Puerta | ¿Qué bloquea? |
|---|---|
| Residuos a nivel de byte | Cadenas de metadatos del original encontradas en la salida |
| Residuos a nivel de etiquetas | Valores de etiquetas de salida que provienen del original |
| Información de ubicación | Cualquier campo GPS / location |
| Identificadores de dispositivo/software | Campos make / model / software / artist / comment, etc. |
| Marca de tiempo del original | La creation_time de salida es igual a la del original |
| Matriz de rotación | En modo encode, la rotación no se ha horneado en el video (modo copy se salta — eliminarla dejaría el video torcido) |
| Composición de flujos | Se han infiltrado flujos data / timecode / attachment |
| Capítulos | Los capítulos se han transferido |
| Cadenas de identidad en el flujo de bits | SEI / DSE / compressorname aún contienen el nombre del codificador |
| Coherencia del perfil | Las etiquetas restantes no coinciden con el perfil declarado |
| Duración | La salida se ha truncado silenciosamente |
| Decodificable | Se ha producido un archivo dañado que no se puede abrir |
HDR y subtítulos incrustados: cuándo no se debe tocar el SEI
ffmpeg solo ofrece granularidad a nivel de NAL — filter_units elimina por tipo de NAL,
h264_metadata tampoco tiene opción para "eliminar solo ciertos payloads de SEI". Así que remove_types=6 elimina
todos los SEI juntos. Y dentro de los SEI, además de la basura de x264, viven:
- Metadatos HDR estáticos (mastering display / content light level) — eliminarlos provoca pérdida de color, el tone mapping se rompe
- Subtítulos incrustados CEA-608/708 — eliminarlos hace que desaparezcan los subtítulos
Por eso la estrategia del script es: solo toca el SEI si realmente se detecta una cadena de identidad; los videos HDR nunca se tocan (se indica claramente que no se ha tocado
y por qué). Esta decisión la toma seiPlan(), no necesitas preocuparte, pero debes saber que existe.
Límites (lo que no hace)
No elimina marcas de agua (las marcas de agua de píxel robustas no se eliminan ni con recodificación, no esperes), no modifica el contenido del video (no recorta, no escala, no rota), no falsifica modelos de dispositivo ni GPS (el perfil solo escribe información de la herramienta, no inventa dispositivos ni ubicaciones de captura), no escanea directorios en lote (un video a la vez), no maneja contenedores que no sean mp4/mov.
Autocomprobación
node {baseDir}/scripts/selftest.mjs
117 aserciones, no toca ffmpeg, no toca archivos reales. Cada una de las 12 puertas tiene casos de prueba que la atraviesan — para demostrar que realmente bloquean. Ejecuta esto después de modificar el script.