Screencasting eficiente y de calidad con FFmpeg: VAAPI, frames intra, y MJPEG
Este post trata sobre el screencasting, la grabación de una pantalla/escritorio junto con una narración, en este caso no sólo de audio sino también con vídeo, usando una webcam.

Anteriormente lo había hecho cutremente, grabando sólo el escritorio con una ventana mostrando el preview de la webcam. Creo que hacerlo por separado es más flexible. Podemos grabar la webcam en más calidad y alternar entre PIP y pantalla completa, etc.
VAAPI con intra frames
Ya escribí sobre KMSgrab para grabar en alta calidad codificando con la tarjeta gráfica (VAAPI), haciendo un uso muy reducido de la CPU. La salida es un h264. No está mal como formato de salida «definitivo», pero en mi caso quiero hacer edición de vídeo para incluir la webcam. Eso significa un reencoding adicional. Para conservar la mayor calidad posible, podemos especificar la opción -intra
, que usa sólamente I-frames, prescindiendo de los P-frames y B-frames (similar a lo que hacen formatos como Cineform, ProRes y DNxHD/R). Realmente en el post que escribí ya se quitaban los B-frames (opción -bf 0
), porque no es posible utilizarlos de momento con mi driver AMDGPU. Ahora podemos deshacernos también de los P-frames:
sudo env PULSE_COOKIE=$HOME/.config/pulse/cookie PULSE_SERVER=$XDG_RUNTIME_DIR/pulse/native \
ffmpeg -y \
-f pulse -thread_queue_size 1024 -i alsa_output.pci-0000_00_1b.0.iec958-stereo.monitor \
-crtc_id 49 -framerate 60 -f kmsgrab -thread_queue_size 1024 -i - -vsync 0 -init_hw_device vaapi=v:/dev/dri/renderD128 \
-vaapi_device /dev/dri/renderD128 \
-vf 'hwmap=derive_device=vaapi,crop=1920:1200:1200:320,scale_vaapi=1920:1200:nv12,hwupload' \
-c:v h264_vaapi -profile:v main -intra # Antes -bf 0 en lugar de -intra\
desktop.mp4
NOTA: En este comando también estoy grabando el audio interno del PC (aplicaciones).
Con la utilidad ffprobe podemos examinar algunos frames para comprobar su tipo. Comparemos:
j ~ Documents ocio video 4 ffprobe -hide_banner -report -loglevel "quiet" -show_frames -select_streams "v:0" -show_entries "frame=key_frame,pkt_size,pict_type,coded_picture_number" -print_format "csv" -i './only_intra.mp4' | head -n30frame,1,177965,I,0frame,1,178066,I,1
frame,1,178062,I,2
frame,1,178062,I,3
frame,1,178062,I,4
frame,1,178062,I,5
frame,1,178062,I,6
frame,1,178062,I,7
frame,1,178062,I,8
frame,1,178062,I,9
frame,1,178062,I,10
frame,1,178103,I,11
frame,1,178103,I,12
frame,1,178103,I,13
frame,1,178103,I,14
frame,1,178103,I,15
frame,1,178103,I,16
frame,1,178103,I,17
frame,1,178103,I,18
frame,1,178103,I,19
frame,1,178081,I,20
frame,1,178081,I,21
frame,1,178081,I,22
frame,1,178081,I,23
frame,1,178081,I,24
frame,1,178081,I,25
frame,1,178081,I,26
frame,1,178081,I,27
frame,1,178081,I,28
frame,1,178081,I,29
j ~ Documents ocio video 141 0
j ~ Documents ocio video 141 0
j ~ Documents ocio video 141 0 ffprobe -hide_banner -report -loglevel "quiet" -show_frames -select_streams "v:0" -show_entries "frame=key_frame,pkt_size,pict_type,coded_picture_number" -print_format "csv" -i './no_b_frames.mp4' | head -n30
frame,1,179869,I,0
frame,0,14128,P,1
frame,0,1788,P,2
frame,0,8491,P,3
frame,0,1115,P,4
frame,0,662,P,5
frame,0,549,P,6
frame,0,500,P,7
frame,0,470,P,8
frame,0,8044,P,9
frame,0,963,P,10
frame,0,473,P,11
frame,0,385,P,12
frame,0,362,P,13
frame,0,373,P,14
frame,0,7998,P,15
frame,0,883,P,16
frame,0,433,P,17
frame,0,357,P,18
frame,0,354,P,19
frame,0,7834,P,20
frame,0,854,P,21
frame,0,420,P,22
frame,0,324,P,23
frame,0,315,P,24
frame,0,314,P,25
frame,0,8095,P,26
frame,0,879,P,27
frame,0,417,P,28
frame,0,329,P,29
Como podemos observar, only_intra.mp4
sólo tiene «I», mientras que no_b_frames.mp4
tiene I seguido de muchos P. Por supuesto, trabajar sólo con I-frames aumenta el tamaño enormemente:
-rw-r--r--. 1 root root 705K Aug 25 03:44 no_b_frames.mp4
-rw-r--r--. 1 root root 34M Aug 25 03:43 only_intra.mp4
Ambos ficheros duran 3 segundos y muestran más o menos lo mismo, un escritorio estático salvo una ventana de gnome-system-monitor.
A pesar del tamaño, es mucho más pequeño que uno sin comprimir. Según Wikipedia, 1 segundo:
24-bit, 1080p @ 60 fps: 24 × 1920×1080 × 60 = 2.98 Gbit/s.
Esto equivale a 372,5 MiB/s
Un inconveniente es que envío tanto audio como vídeo a codificar. h264_vaapi transforma el audio a AAC, que comprime. Se puede evitar grabando en PCM con otro comando ffmpeg por separado, pero tendremos que sincronizar audio y vídeo manualmente después. Es inevitable porque cada comando ffmpeg empezaría a grabar en momentos distintos, aunque intentásemos ejecutarlos al mismo tiempo. Al final hay que sincronizar el vídeo del escritorio con el de la webcam, así que prefiero perder algo de calidad y evitar otra sincronización manual adicional.
NOTA: para el escritorio no he empleado MJPEG porque mi tarjeta gráfica lo soporta para decoding pero no para encoding (las entradas terminadas en Slice
):
j ~ Documents ocio video vainfo
libva info: VA-API version 1.6.0
libva info: va_getDriverName() returns 0
libva info: Trying to open /usr/lib64/dri/radeonsi_drv_video.so
libva info: Found init function __vaDriverInit_1_6
libva info: va_openDriver() returns 0
vainfo: VA-API version: 1.6 (libva 2.6.0.pre1)
vainfo: Driver version: Mesa Gallium driver 19.2.8 for Radeon RX 570 Series (POLARIS10, DRM 3.33.0, 5.3.11-300.fc31.x86_64, LLVM 9.0.0)
vainfo: Supported profile and entrypoints
VAProfileMPEG2Simple : VAEntrypointVLD
VAProfileMPEG2Main : VAEntrypointVLD
VAProfileVC1Simple : VAEntrypointVLD
VAProfileVC1Main : VAEntrypointVLD
VAProfileVC1Advanced : VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointVLD
VAProfileH264Main : VAEntrypointEncSlice
VAProfileH264High : VAEntrypointVLD
VAProfileH264High : VAEntrypointEncSlice
VAProfileHEVCMain : VAEntrypointVLD
VAProfileHEVCMain : VAEntrypointEncSlice
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileJPEGBaseline : VAEntrypointVLD
VAProfileNone : VAEntrypointVideoProc
Webcam con MJPEG
Muchas cámaras permiten grabar con MJPEG, así que nuevamente descargamos a la CPU de la tarea de compresión.
ffmpeg -y \
-f v4l2 -thread_queue_size 1024 -framerate 60 -video_size 1920x1080 -input_format mjpeg -i /dev/video0 \
-f pulse -thread_queue_size 1024 -i alsa_input.usb-046d_Logitech_BRIO_69515057-02.analog-stereo \
-acodec copy -vcodec copy \
camera_2.mkv
Copiamos directamente («copy
» de las opciones «-acodec
» y «-vcodec
» el vídeo (en mjpeg) y audio (pcm_s16le, 48000 Hz, stereo, s16) de la cámara, y los guardamos en un MKV. El micrófono está incorporado en la cámara (Logitech BRIO). NOTA: Algo con lo que me he atascado es que primero debo especificar el input de vídeo (-f v4l2) y después el de audio (-f pulse), o de lo contrario audio y vídeo están desincronizados desde el principio. Me imagino que se debe a la diferencia de tiempo de inicialización del micrófono (muy rápida) y del sensor de imagen (muy lenta). Supongo que en este caso FFmpeg almacena el sonido en un buffer hasta que se inicializa la imagen…
Juntando vídeo de webcam y escritorio
Ahora ya tenemos dos vídeos poco comprimidos para sincronizar y mezclar en PIP en un editor de vídeo (en mi caso, KDEnlive). La forma más fácil de sincronizar es apuntar la webcam a la pantalla y hacer algo «instantáneo», por ejemplo, pulsar una tecla y sincronizar en base al momento en que aparece el carácter en pantalla. Yo he usado el comando date, que puedo ejecutar varias veces y distinguir fácilmente, creando varios «puntos de sincronización». Si no queremos mover la cámara, podemos hacerlo por el sonido de las teclas, o del comando beep.