Blog

Screencasting eficiente y de calidad con FFmpeg: VAAPI, frames intra, y MJPEG

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.

Capturando el escritorio a 1920×1200 y cámara a 1080p, ambos a 60Hz. Uso de CPU contenido.

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.

Composición final