Introducción
Hace un año compré una cámara de vigilancia económica de la marca Easytao, listada en Amazon como modelo Q25. Estas cámaras son poco menos que genéricas, aunque tienen un desempeño decente. No se comparan con otras opciones económicas como las de marca Imou, pero son relativamente más baratas y descartables si son dañadas o robadas.
Recientemente tuve la necesidad de revisar los videos grabados en la tarjeta
microSD de la cámara y luego compartirlos por WhatsApp. Extraje la tarjeta y la
conecté a mi computadora (Debian Sid), pero VLC no reproducía los archivos con
extensión .h264. El comando file tampoco los reconocía y sólo respondía
data. Esto me llevó a investigar qué formato tenían en realidad y cómo
convertirlos a un contenedor MP4 compatible con WhatsApp. Claro, con la ayuda de
“Claudio” y FFmpeg.
Tabla de contenido
Tabla de contenido
Los archivos
Las cámaras de vigilancia económicas, como las de la marca Easytao, guardan sus grabaciones en una tarjeta microSD con nombres como este:
17.37.43-17.37.48[R][@0][0].h264El nombre contiene la hora de inicio y de fin del clip. La letra entre
corchetes parece indicar el tipo de grabación: la mayoría de los clips llevan
R y algunos llevan A, probablemente por una alarma de movimiento. La
extensión promete un video H.264, pero VLC no lo reproduce y el comando file
sólo responde data.
En este artículo analizarás el contenido del archivo byte por byte y descubrirás qué formato tiene en realidad. Luego lo convertirás a MP4 sin perder calidad y lo recodificarás para compartirlo por WhatsApp. Al final tendrás un script de Bash que detecta el formato de entrada por sí mismo, sin confiar en la extensión.
Qué hay dentro del archivo
Un archivo sin contenedor no tiene metadatos que lo identifiquen, así que hay
que leer sus primeros bytes. od viene instalado en cualquier sistema Linux:
od -A x -t x1 -N 48 '23.40.19-23.40.24[R][@0][0].h264'000000 00 00 01 fc 13 0c 40 b4 13 7a 6f 6a ce e0 00 00000010 00 00 00 01 40 01 0c 01 ff ff 01 60 00 00 03 00000020 b0 00 00 03 00 00 03 00 99 ac 09 00 00 00 01 42Los videos H.264 y H.265 (HEVC) sin contenedor usan el formato Annex B: una
secuencia de unidades NAL (Network Abstraction Layer), cada una precedida por
el código de inicio 00 00 01 o 00 00 00 01. El byte que sigue al código
de inicio es el encabezado de la unidad y codifica su tipo.
Aquí conviven dos cosas distintas:
- Un bloque propietario de 16 bytes que empieza con
00 00 01 FC. No pertenece a ningún estándar de video y su estructura no está documentada. Algunos de sus campos cambian de un archivo a otro, así que probablemente guardan una marca de tiempo o un contador del fabricante. - El video en sí, a partir del offset
0x10, donde aparece el código de inicio00 00 00 01seguido del byte40.
H.264 y HEVC calculan el tipo de NAL de forma distinta
El mismo byte significa cosas diferentes en cada estándar:
- En H.264, el tipo son los 5 bits menos significativos:
byte & 0x1F. - En HEVC, el tipo ocupa los 6 bits que siguen al bit prohibido:
(byte >> 1) & 0x3F. El encabezado de HEVC mide además dos bytes, y el segundo suele ser01.
Si aplicas las dos fórmulas a los bytes que siguen a cada código de inicio del archivo, obtienes esta tabla:
| Byte | Como HEVC | Como H.264 |
|---|---|---|
40 | 32: VPS (Video Parameter Set) | 0: sin especificar |
42 | 33: SPS | 2: partición A de un slice |
44 | 34: PPS | 4: partición C de un slice |
4E | 39: SEI | 14: prefijo de NAL (extensión SVC) |
26 | 19: cuadro IDR (keyframe) | 6: SEI |
Leído como HEVC, el archivo tiene la secuencia de arranque canónica: VPS, SPS,
PPS, SEI y un keyframe. Leído como H.264 no tiene sentido: no aparecen ni SPS
ni PPS, y el keyframe se confunde con un mensaje SEI. La pareja XX 01
después de cada código de inicio delata también el encabezado de dos bytes de
HEVC.
Conclusión: el archivo es HEVC crudo en formato Annex B, con un bloque
propietario al inicio, aunque su extensión diga .h264. Las cámaras baratas
suelen grabar en H.265 para ahorrar espacio en la tarjeta, y su firmware
conserva la extensión de los modelos anteriores.
Por qué VLC y la autodetección de FFmpeg fallan
Un stream Annex B carece de “número mágico” propio: el código de inicio es igual en H.264 y en HEVC. Los reproductores tienen que adivinar, y adivinan mal por dos razones:
- La extensión engaña. Con
.h264, FFmpeg elige el demuxer de H.264, anuncia un stream válido y después decodifica 0 cuadros. El registro se llena de mensajesmissing picture in access unityPicture timing SEI payload too large, que corresponden a la columna derecha de la tabla anterior. - El bloque propietario oculta la firma. Si renombras el archivo sin
extensión o con una extensión falsa, el detector de FFmpeg encuentra primero
la unidad inválida
FCy respondeInvalid data found when processing input.
Puedes comprobarlo con ffprobe. La opción -count_frames obliga a
decodificar el stream completo y a contar los cuadros que realmente salen:
f='23.40.19-23.40.24[R][@0][0].h264'ffprobe -v quiet -select_streams v:0 -count_frames \ -show_entries stream=codec_name,width,height,nb_read_frames -of csv=p=0 "$f"ffprobe -v quiet -f hevc -select_streams v:0 -count_frames \ -show_entries stream=codec_name,width,height,nb_read_frames -of csv=p=0 "$f"h264,0,0,N/Ahevc,2560,1440,65Con la autodetección, FFmpeg no obtiene ni la resolución. Al forzar el formato
con -f hevc, aparecen la resolución de 2560×1440 y los 65 cuadros del clip.
Recuperar el video sin recodificar
Ya que sabes el formato, basta con pasar el stream a un contenedor MP4.
-c copy copia los datos comprimidos tal cual, así que la conversión es
instantánea y no pierde calidad:
ffmpeg -f hevc -framerate 12.5 -i '17.37.43-17.37.48[R][@0][0].h264' \ -c copy salida.mp4Un stream crudo no guarda marcas de tiempo, así que el contenedor no sabe a
qué velocidad reproducir los cuadros. Por eso hace falta -framerate. Para
calcular el valor, divide los cuadros del clip entre su duración, que sale del
nombre del archivo: 65 cuadros entre 17:37:43 y 17:37:48 dan unos 12.5 cuadros
por segundo. Si el resultado se ve acelerado o en cámara lenta, prueba con 15
o 25, que son frecuencias habituales en estas cámaras.
Para convertir una carpeta completa:
for f in *.h264; do ffmpeg -nostdin -v error -f hevc -framerate 12.5 -i "$f" -c copy "${f%.h264}.mp4"doneEl MP4 resultante se reproduce en VLC y en la mayoría de los reproductores de escritorio, pero sigue siendo HEVC por dentro.
Convertir para WhatsApp
WhatsApp no garantiza la reproducción de HEVC en todos los teléfonos: según el dispositivo, rechaza el archivo o lo envía como documento en lugar de video. La combinación más compatible es H.264 con audio AAC dentro de un MP4, así que esta vez sí hay que recodificar:
ffmpeg -nostdin -f hevc -framerate 12.5 -i '10.04.47-10.04.58[A][@0][0].h264' \ -vf "scale=-2:1080:out_range=tv,format=yuv420p,fps=25" \ -c:v libx264 -preset medium -crf 23 -profile:v high -level 4.0 \ -movflags +faststart salidcam2whatsapp.mp4Cada opción resuelve un problema concreto:
| Opción | Motivo |
|---|---|
-c:v libx264 -profile:v high -level 4.0 | H.264 High, nivel 4.0: cualquier teléfono de la última década lo decodifica por hardware en 1080p. |
scale=-2:1080 | Reduce 2560×1440 a 1920×1080. WhatsApp vuelve a comprimir el video de todos modos, así que enviar 1440p sólo gasta datos. -2 calcula un ancho par que respeta la proporción. |
out_range=tv,format=yuv420p | La cámara graba en rango de color completo (yuvj420p). Algunos reproductores lo muestran deslavado o no lo reproducen; el rango limitado es el estándar para video. |
fps=25 | Duplica cuadros hasta una frecuencia constante de 25 fps. Algunos reproductores móviles se atoran con frecuencias poco comunes como 12.5. |
-crf 23 -preset medium | Calidad visual constante con un tamaño razonable: unos 2.8 MB por cada 10 segundos de este video. |
-movflags +faststart | Coloca el índice del MP4 al principio del archivo, para que la reproducción empiece antes de terminar la descarga. |
Sin format=yuv420p, FFmpeg conserva el formato yuvj420p de la cámara aunque
indiques -pix_fmt yuv420p. Compruébalo en el archivo de salida:
ffprobe -v error -show_entries stream=codec_name,profile,pix_fmt,color_range \ salidcam2whatsapp.mp4[STREAM]codec_name=h264profile=Highpix_fmt=yuv420pcolor_range=tv[/STREAM]Si necesitas archivos más pequeños, usa scale=-2:720 o sube el valor a
-crf 26. Cualquiera de los dos cambios reduce el tamaño a la mitad,
aproximadamente.
Detectar el formato de forma genérica
El comando anterior depende de saber de antemano que el archivo es HEVC. Si mezclas grabaciones de varias cámaras, o si un firmware nuevo cambia el códec, necesitas detectar el formato automáticamente.
Como ya viste, ni la extensión, ni file, ni la autodetección de FFmpeg
sirven con estos archivos. La estrategia robusta es empírica: probar varios
formatos candidatos y quedarse con el que decodifique más cuadros. La
autodetección entra como un candidato más, y así los MP4, MKV o AVI normales
se procesan con su contenedor.
Contar cuadros descarta los falsos positivos de forma natural: el demuxer de
H.264 “abre” el archivo HEVC sin error, pero produce 0 cuadros, así que pierde
frente a -f hevc. El costo es bajo: decodificar un clip de 10 segundos toma
menos de un segundo.
El script completo
Guarda el script como cam2whatsapp.sh. Recibe una lista de archivos, detecta
el formato de cada uno y escribe los resultados en una carpeta whatsapp/:
#!/usr/bin/env bash# Convierte videos (con contenedor o streams crudos de cámara) a MP4 compatible con WhatsApp.# Uso: ./cam2whatsapp.sh archivo... ; salida en ./whatsapp/# FPS para streams crudos sin timestamps (la cámara graba ~12.5).FPS_CRUDO=${FPS_CRUDO:-12.5}
# Devuelve cuántos cuadros se decodifican con el formato dado ("" = autodetección).cuadros() { local fmt=$1 f=$2 ffprobe -v quiet ${fmt:+-f $fmt} -select_streams v:0 -count_frames \ -show_entries stream=nb_read_frames -of csv=p=0 "$f" | tr -dc 0-9}
mkdir -p whatsappfor f in "$@"; do mejor= max=0 # Lista fija de candidatos; agrega mjpeg o mpeg4 si tu cámara usa otro códec. for fmt in "" hevc h264; do n=$(cuadros "$fmt" "$f"); n=${n:-0} (( n > max )) && max=$n mejor=${fmt:-auto} done if (( max == 0 )); then echo "✗ $f: formato no reconocido"; continue; fi echo "→ $f: $mejor ($max cuadros)"
entrada=(-i "$f") [[ $mejor != auto ]] && entrada=(-f "$mejor" -framerate "$FPS_CRUDO" -i "$f") ffmpeg -nostdin -v error -y "${entrada[@]}" -map 0:v:0 -map '0:a:0?' \ -vf "scale=-2:'min(1080,ih)':out_range=tv,format=yuv420p,fps=25" \ -c:v libx264 -preset medium -crf 23 -profile:v high -level 4.0 \ -c:a aac -b:a 128k -movflags +faststart \ "whatsapp/$(basename "${f%.*}").mp4" 2>&1 | grep -v -e NALU -e PPSdoneAlgunos detalles de la implementación:
${fmt:+-f $fmt}agrega-f formatosólo si el candidato no está vacío. Con el candidato vacío,ffprobeusa su autodetección normal.tr -dc 0-9limpia la salida deffprobe, que puede traer saltos de línea oN/A. Si no queda ningún dígito,${n:-0}lo convierte en cero.-frameratesólo se aplica a los streams crudos. Un archivo con contenedor ya trae sus marcas de tiempo y no debe sobrescribirse.-map '0:a:0?'copia la primera pista de audio si existe. El signo de interrogación evita un error cuando el archivo no tiene audio, como ocurre con esta cámara. Las comillas impiden que el shell interprete?como comodín.min(1080,ih)reduce la resolución sólo si es mayor que 1080p. Un video de 720p conserva su tamaño en lugar de agrandarse.grep -v -e NALU -e PPSoculta los avisos que provoca el bloque propietario de la cámara. Son inofensivos, y sin el filtro llenan la terminal.- los archivos se almacenan en el subdirectorio
whatsapp/con el mismo nombre base y extensión.mp4. Modifica el script de ser necesario para cambiar la carpeta de salida.
Uso
Asumamos que los archivos de video de la cámara están en el directorio videos.
Dale al script permisos de ejecución y pásale los archivos que quieras convertir:
chmod +x cam2whatsapp.shcd videos../cam2whatsapp.sh *.h264El script informa qué formato eligió para cada archivo:
→ 23.40.19-23.40.24[R][@0][0].h264: hevc (65 cuadros)→ copia-sin-extension: hevc (65 cuadros)→ otro-video.mkv: auto (50 cuadros)La segunda línea corresponde al mismo archivo sin extensión, y la tercera a un
MKV normal con video HEVC y audio MP3. En los tres casos, el resultado es H.264
con yuv420p, más AAC cuando el original trae audio.
Si el video de otra cámara se ve acelerado o lento, cambia la frecuencia supuesta para los streams crudos:
FPS_CRUDO=15 ../cam2whatsapp.sh *.h264Resolución de problemas
| Problema | Comprobación y solución |
|---|---|
✗ formato no reconocido | Revisa los primeros bytes con od -A x -t x1 -N 64. Si no aparecen códigos de inicio 00 00 01, el archivo no es Annex B; prueba ffmpeg -formats y agrega el formato adecuado a la lista de candidatos. |
| El video se reproduce demasiado rápido o lento | Cuenta los cuadros con ffprobe -f hevc -count_frames y divídelos entre la duración que indica el nombre del archivo. Usa ese valor en FPS_CRUDO. |
Aparecen PPS id out of range o Failed to parse header | Son los bloques propietarios de la cámara, que el decodificador descarta. Confirma que el número de cuadros de la salida coincide con el de -count_frames. |
| WhatsApp envía el video como documento | Comprueba con ffprobe que la salida sea h264 con yuv420p. Si el archivo es muy grande, reduce la resolución a 720p o sube el valor de -crf. |
| Los colores se ven deslavados en el teléfono | Falta la conversión de rango. Verifica que el filtro incluya out_range=tv,format=yuv420p y que ffprobe reporte color_range=tv. |
| El bucle se salta archivos | Falta -nostdin en alguna llamada a ffmpeg dentro del bucle. |
