FFmpeg libav manual


Durante mucho tiempo busqué un libro que se masticara para usar una biblioteca similar a FFmpeg conocida como libav (el nombre significa lib rary a udio v ideo ). Encontré un libro de texto " Cómo escribir un reproductor de video y encajar en menos de mil líneas ". Desafortunadamente, la información allí está desactualizada, así que tuve que crear un manual por mi cuenta.

La mayor parte del código estará en C, pero no se preocupe: comprenderá fácilmente todo y podrá aplicarlo en su idioma favorito. FFmpeg libav tiene muchos enlaces a muchos idiomas (incluidos Python y Go). Pero incluso si su idioma no tiene compatibilidad directa, aún puede adjuntarse a través de ffi (aquí hay un ejemplo conLua )

Comencemos con una breve digresión sobre qué son video, audio, códecs y contenedores. Luego pasamos al curso intensivo sobre el uso de la línea de comando FFmpeg y finalmente escribimos el código. Siéntase libre de ir directamente a la sección "The Thorny Path to Learning FFmpeg libav".

Existe la opinión (y no solo la mía) de que la transmisión de video por Internet ya ha tomado el relevo de la televisión tradicional. Sea como fuere, definitivamente vale la pena explorar FFmpeg libav.

Tabla de contenido


Software EDISON - desarrollo web
EDISON.

, , .

! ;-)


— , !


Si la secuencia de imágenes se cambia a una frecuencia dada (por ejemplo, 24 imágenes por segundo), se crea una ilusión de movimiento. Esta es la idea principal del video: una serie de imágenes (cuadros) que se mueven a una velocidad determinada.

1886 ilustración.

¡El audio es lo que oyes!


Aunque el video silencioso puede causar una amplia variedad de sentimientos, agregar sonido aumenta dramáticamente el grado de placer.

El sonido son ondas vibratorias que se propagan en el aire o en cualquier otro medio de transmisión (como gas, líquido o sólido).

En un sistema de audio digital, un micrófono convierte el sonido en una señal eléctrica analógica. Luego, un convertidor analógico a digital ( ADC ), que generalmente usa modulación de código de pulso ( PCM ), convierte una señal analógica en una digital.


Códec - compresión de datos


Un códec es un circuito electrónico o software que comprime o descomprime audio / video digital. Convierte audio / video digital sin procesar (sin comprimir) en un formato comprimido (o viceversa).

Pero si decidimos empacar millones de imágenes en un archivo y llamarlo una película, podemos obtener un archivo enorme. Calculemos:

digamos que creamos un video con una resolución de 1080 × 1920 (alto × ancho). Gastamos 3 bytes por píxel (el punto mínimo en la pantalla) para la codificación de colores (color de 24 bits, que nos da 16.777.216 colores diferentes). Este video funciona a una velocidad de 24 cuadros por segundo, la duración total de 30 minutos.

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

¡Este video requerirá aproximadamente 250.28 GB de memoria, o 1.11 Gb / s! Es por eso que tienes que usar un códec.

Un contenedor es una forma conveniente de almacenar audio / video


El formato contenedor (contenedor) es un formato de metarchivo cuya especificación describe cómo varios elementos de datos y metadatos coexisten en un archivo de computadora.

Este es un archivo único que contiene todas las transmisiones (principalmente audio y video), que proporciona sincronización, que contiene metadatos comunes (como título, resolución), etc.

Por lo general, el formato de archivo está determinado por su extensión: por ejemplo, video.webm es probablemente un video que usa el contenedor webm.


Línea de comando FFmpeg


Solución multiplataforma autónoma para grabar, convertir y transmitir audio / video.

Para trabajar con multimedia, tenemos una herramienta increíble: una biblioteca llamada FFmpeg . Incluso si no lo usa en el código de su programa, todavía lo usa (¿está usando Chrome?).

La biblioteca tiene un programa de consola para ingresar una línea de comando llamada ffmpeg (en minúsculas, en contraste con el nombre de la biblioteca en sí). Este es un binario simple y poderoso. Por ejemplo, puede convertir de mp4 a avi simplemente escribiendo este comando:

$ ffmpeg -i input.mp4 output.avi

Acabamos de volver a mezclar , convertir de un contenedor a otro. Técnicamente, FFmpeg también puede transcodificar, pero más sobre eso más adelante.

Herramienta de línea de comando FFmpeg 101


FFmpeg tiene documentación donde todo se explica perfectamente cómo funciona.

Esquemáticamente, el programa de línea de comandos FFmpeg espera que el siguiente formato de argumento haga su trabajo: ffmpeg {1} {2} -i {3} {4} {5}donde:

{1} - parámetros globales
{2} - parámetros del archivo de entrada
{3} - URL entrante
{4} - parámetros del archivo de salida
{5} - saliente Las

partes de URL {2}, {3}, {4}, {5} especifican tantos argumentos como sea necesario. Es más fácil entender el formato de pasar argumentos usando un ejemplo:

ADVERTENCIA: un archivo por referencia pesa 300 MB

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

Este comando toma un archivo mp4 entrante que contiene dos transmisiones (audio codificado con el códec aac y video codificado con el códec h264), y lo convierte a webm, cambiando también los códecs de audio y video.

Si simplifica el comando anterior, debe considerar que FFmpeg aceptará los valores predeterminados en su lugar. Por ejemplo, si simplemente escribe

ffmpeg -i input.avi output.mp4

¿Qué códec de audio / video utiliza para crear output.mp4?

Werner Robitz escribió una guía de codificación y edición para leer / ejecutar con FFmpeg.

Operaciones básicas de video


Cuando trabajamos con audio / video, generalmente realizamos una serie de tareas relacionadas con multimedia.

Transcodificación (transcodificación)




¿Qué es? El proceso de convertir streaming o audio o video (o ambos al mismo tiempo) de un códec a otro. El formato del archivo (contenedor) no cambia.

¿Para qué? Ocurre que algunos dispositivos (televisores, teléfonos inteligentes, consolas, etc.) no admiten el formato de audio / video X, pero sí el formato de audio / video Y. O los códecs más nuevos son preferibles porque proporcionan una mejor relación de compresión.

¿Cómo? Convierta, por ejemplo, el video H264 (AVC) a H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

Transmultiplexación



¿Qué es? Convierte de un formato (contenedor) a otro.

¿Para qué? Ocurre que algunos dispositivos (televisores, teléfonos inteligentes, consolas, etc.) no admiten el formato de archivo X, pero sí el formato de archivo Y. O bien, los contenedores más nuevos, a diferencia de los antiguos, proporcionan las funciones modernas requeridas.

¿Cómo? Convierte un mp4 a webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

Transfiriendo



¿Qué es? Cambie la velocidad de datos o cree otra vista.

¿Para qué? El usuario puede ver su video tanto en una red 2G en un teléfono inteligente de baja potencia como a través de una conexión a Internet de fibra óptica en un televisor 4K. Por lo tanto, debe ofrecer más de una opción para reproducir el mismo video con diferentes velocidades de datos.

¿Cómo? produce reproducción a una velocidad de bits entre 3856K y 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

Por lo general, la transición se realiza junto con la recalibración. Werner Robitz escribió otro artículo obligatorio sobre el control de velocidad FFmpeg.

Transizing (recalibración)



¿Qué es? Cambio de resolución. Como se indicó anteriormente, el cambio de tamaño a menudo se lleva a cabo simultáneamente con el cambio.

¿Para qué? Por las mismas razones que con la transmisión.

¿Cómo? Reduzca la resolución de 1080 a 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

Bono: transmisión adaptativa



¿Qué es? Creación de muchos permisos (bitrates) y división de los medios en partes y su transmisión a través del protocolo http.

¿Para qué? En aras de proporcionar multimedia flexible que se puede ver incluso en un teléfono inteligente económico, incluso en un plasma 4K, para que se pueda escalar e implementar fácilmente (pero esto puede agregar un retraso).

¿Cómo? Cree WebM receptivo usando DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd

PD: Extraje este ejemplo de las instrucciones para jugar Adaptive WebM usando DASH .

Yendo más allá


No hay otros usos para FFmpeg. Lo uso con iMovie para crear / editar algunos videos de YouTube. Y, por supuesto, nada le impide usarlo profesionalmente.

El camino espinoso del aprendizaje FFmpeg libav

¿No es sorprendente de vez en cuando que se perciba a través del oído y la vista?

Biólogo David Robert Jones
FFmpeg es extremadamente útil como herramienta de línea de comandos para realizar operaciones importantes con archivos multimedia. ¿Quizás también se pueda usar en programas?

FFmpeg consta de varias bibliotecas que se pueden integrar en nuestros propios programas. Por lo general, cuando instala FFmpeg, todas estas bibliotecas se instalan automáticamente. Me referiré a un conjunto de estas bibliotecas como FFmpeg libav .

El título de la sección es un homenaje a la serie de Zed Shaw The Thorny Path of Learning [...] , en particular a su libro The Thorny Path of Learning C.

Capítulo 0 - El simple Hello World


En nuestro Hello World , realmente no darás la bienvenida al mundo en lenguaje de consola. En su lugar, imprima la siguiente información sobre el video: formato (contenedor), duración, resolución, canales de audio y, finalmente, descifre algunos cuadros y guárdelos como archivos de imagen.

Arquitectura de FFmpeg libav


Pero antes de comenzar a escribir el código, veamos cómo funciona la arquitectura FFmpeg libav en general y cómo sus componentes interactúan con otros.

Aquí está el diagrama del proceso de decodificación de video:

Primero, el archivo multimedia se carga en un componente llamado AVFormatContext (el contenedor de video también es un formato). De hecho, no descarga completamente el archivo completo: a menudo solo se lee el encabezado.

Una vez que haya descargado el encabezado mínimo de nuestro contenedor , puede acceder a sus transmisiones (se pueden representar como datos elementales de audio y video). Cada transmisión estará disponible en el componente AVStream .

Supongamos que nuestro video tiene dos transmisiones: audio codificado con el códec AAC y video codificado con el códec H264 ( AVC ). De cada flujo podemos extraer datos llamados paquetesque se cargan en componentes llamados AVPacket .

Los datos dentro de los paquetes todavía están codificados (comprimidos), y para decodificar los paquetes necesitamos pasarlos a un AVCodec específico .

AVCodec los decodifica en un AVFrame , como resultado de lo cual este componente nos da un marco sin comprimir. Tenga en cuenta que la terminología y el proceso son los mismos para las transmisiones de audio y video.

Requisitos


Como a veces hay problemas al compilar o ejecutar ejemplos, usaremos Docker como entorno de desarrollo / tiempo de ejecución. También usaremos un video con un gran conejo , así que si no lo tiene en su computadora local, simplemente ejecute el comando make fetch_small_bunny_video en la consola .

En realidad, el código


TLDR muéstrame un ejemplo de código ejecutable, hermano:

$ make run_hello

Omitiremos algunos detalles, pero no se preocupe: el código fuente está disponible en github.

Vamos a asignar memoria para el componente AVFormatContext , que contendrá información sobre el formato (contenedor).

AVFormatContext *pFormatContext = avformat_alloc_context();

Ahora vamos a abrir el archivo, leer su encabezado y llenar AVFormatContext con información de formato mínima (tenga en cuenta que los códecs generalmente no se abren). Para hacer esto, use la función avformat_open_input . Espera AVFormatContext , un nombre de archivo y dos argumentos opcionales: AVInputFormat (si pasa NULL, FFmpeg determinará el formato) y AVDictionary (que son opciones de demultiplexor).

avformat_open_input(&pFormatContext, filename, NULL, NULL);

También puede imprimir el nombre del formato y la duración de los medios:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

Para acceder a las transmisiones, necesitamos leer los datos de los medios. Esto se hace mediante la función avformat_find_stream_info . Ahora pFormatContext-> nb_streams contendrá el número de subprocesos, y pFormatContext-> streams [i] nos dará el flujo i th en una fila ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

Veamos el bucle en todos los hilos:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

Para cada transmisión, vamos a guardar AVCodecParameters , que describe las propiedades del códec utilizado por la i- ésima transmisión:

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


Usando las propiedades de los códecs, podemos encontrar el correspondiente solicitando la función avcodec_find_decoder , también podemos encontrar el decodificador registrado para el identificador de códec y devolver AVCodec , un componente que sabe cómo codificar y decodificar la secuencia:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

Ahora podemos imprimir la información del códec:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);

Usando el códec, asignamos memoria para AVCodecContext , que contendrá el contexto para nuestro proceso de decodificación / codificación. Pero luego necesita llenar este contexto de códec con parámetros CODEC ; lo hacemos usando avcodec_parameters_to_context .

Después de completar el contexto del códec, debe abrir el códec. Llamamos a la función avcodec_open2 y luego podemos usarla:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);

Ahora vamos a leer los paquetes de la secuencia y decodificarlos en cuadros, pero primero debemos asignar memoria para ambos componentes ( AVPacket y AVFrame ).

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();

Alimentemos nuestros paquetes desde las secuencias de la función av_read_frame mientras tiene los paquetes:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

Ahora enviaremos el paquete de datos sin formato (marco comprimido) al decodificador a través del contexto de códec utilizando la función avcodec_send_packet :

avcodec_send_packet(pCodecContext, pPacket);

Y obtengamos un marco de datos sin procesar (un marco sin comprimir) del decodificador a través del mismo contexto de códec utilizando la función avcodec_receive_frame :

avcodec_receive_frame(pCodecContext, pFrame);

Podemos imprimir el número de cuadro, PTS, DTS, tipo de cuadro, etc.

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);

Y finalmente, podemos guardar nuestro marco decodificado en una simple imagen gris. El proceso es muy simple: usaremos pFrame-> data , donde el índice está asociado con los espacios de color Y , Cb y Cr . Simplemente seleccione 0 (Y) para guardar nuestra imagen gris:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

¡Y voilá! Ahora tenemos una imagen en escala de grises de 2 MB:


Capítulo 1 - Sincronizar audio y video

Estar en el juego es cuando un joven desarrollador de JS escribe un nuevo reproductor de video MSE.
Antes de comenzar a escribir el código de transcodificación, hablemos sobre la sincronización o cómo el reproductor de video descubre el momento adecuado para reproducir un cuadro.

En el ejemplo anterior, guardamos varios cuadros:


cuando diseñamos un reproductor de video, necesitamos reproducir cada cuadro a un cierto ritmo, de lo contrario es difícil disfrutar el video porque se reproduce demasiado rápido o demasiado lento.

Por lo tanto, necesitamos definir algo de lógica para una reproducción fluida de cada cuadro. En este sentido, cada cuadro tiene una marca de representación de tiempo ( PTS - de p resentation t ime s tamp), que es un número creciente tomado en cuenta en la variablebase de tiempo , que es un número racional (donde el denominador se conoce como la escala de tiempo - escala de tiempo ) dividido por la velocidad de fotogramas ( fps ).

Es más fácil de entender con ejemplos. Simulemos algunos escenarios.

Para fps = 60/1 y base de tiempo = 1/60000, cada PTS aumentará la escala de tiempo / fps = 1000 , por lo que el tiempo PTS real para cada cuadro puede ser (siempre que comience en 0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033

Casi el mismo escenario, pero con una escala de tiempo igual a 1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050

Para fps = 25/1 y base de tiempo = 1/75, cada PTS aumentará la escala de tiempo / fps = 3 , y el tiempo de PTS puede ser:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56

Ahora con pts_time podemos encontrar una manera de visualizar esto sincronizado con el sonido de pts_time o con el reloj del sistema. FFmpeg libav proporciona esta información a través de su API:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base


Solo por curiosidad, los cuadros que guardamos se enviaron en orden DTS (cuadros: 1, 6, 4, 2, 3, 5), pero se reprodujeron en orden PTS (cuadros: 1, 2, 3, 4, 5). También tenga en cuenta cuánto más baratos son los cuadros B en comparación con los cuadros P o I :

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]

Capítulo 2 - Remultiplexación


Remultiplexación (reordenamiento, remuxing): la transición de un formato (contenedor) a otro. Por ejemplo, podemos reemplazar fácilmente el video MPEG-4 con MPEG-TS usando FFmpeg:

ffmpeg input.mp4 -c copy output.ts

El archivo MP4 se demultiplexará, mientras que el archivo no se decodificará ni codificará ( copia -c ) y, al final, obtendremos el archivo mpegts. Si no especifica el formato -f , ffmpeg intentará adivinarlo en función de la extensión del archivo.

El uso general de FFmpeg o libav sigue dicho patrón / arquitectura o flujo de trabajo:

  • nivel de protocolo : acepta datos de entrada (por ejemplo, un archivo, pero también puede ser rtmp o descarga HTTP)
  • — , , ,
  • — (, ),
  • … :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(Este gráfico está muy inspirado en el trabajo de Leixiaohua y Slhck )

Ahora creemos un ejemplo usando libav para proporcionar el mismo efecto que al ejecutar este comando:

ffmpeg input.mp4 -c copy output.ts

Vamos a leer desde input ( input_format_context ) y cambiarlo a otra salida ( output_format_context ):

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;

Por lo general, comenzamos asignando memoria y abriendo el formato de entrada. Para este caso específico, vamos a abrir el archivo de entrada y asignar memoria para el archivo de salida:

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

Remultiplexaremos solo transmisiones de video, audio y subtítulos. Por lo tanto, arreglamos qué flujos usaremos en una matriz de índices:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

Inmediatamente después de asignar la memoria necesaria, necesitamos recorrer todas las secuencias, y para cada una de ellas necesitamos crear una nueva secuencia de salida en nuestro contexto del formato de salida utilizando la función avformat_new_stream . Tenga en cuenta que marcamos todas las transmisiones que no son video, audio o subtítulos para que podamos omitirlas.

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

Ahora cree el archivo de salida:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

Después de eso, puede copiar secuencias, paquete por paquete, de nuestra entrada a nuestras secuencias de salida. Esto sucede en un bucle, siempre que haya paquetes ( av_read_frame ), para cada paquete debe recalcular PTS y DTS para finalmente escribirlo ( av_interleaved_write_frame ) en nuestro contexto del formato de salida.

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

Para completar, necesitamos escribir el avance del flujo en el archivo de medios de salida usando la función av_write_trailer :

av_write_trailer(output_format_context);

Ahora estamos listos para probar el código. Y la primera prueba será la conversión del formato (contenedor de video) del archivo de video MP4 a MPEG-TS. Básicamente creamos una línea de comando ffmpeg input.mp4 -c para copiar output.ts usando libav.

make run_remuxing_ts

¡Funciona! No me creas ?! Verifique con ffprobe :

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

Para resumir lo que hemos hecho, ahora podemos volver a nuestra idea original de cómo funciona libav. Pero nos perdimos parte del códec, que se muestra en el diagrama.


Antes de terminar este capítulo, me gustaría mostrar una parte tan importante del proceso de remultiplexación, donde puede pasar parámetros al multiplexor. Suponga que desea proporcionar el formato MPEG-DASH, por lo que necesita usar mp4 fragmentado (a veces llamado fmp4 ) en lugar de MPEG-TS o MPEG-4 ordinario.

Usar la línea de comando es fácil:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4

Es casi tan simple como esto en la versión libav, simplemente pasamos las opciones al escribir el encabezado de salida, inmediatamente antes de copiar los paquetes:

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

Ahora podemos generar este archivo mp4 fragmentado:

make run_remuxing_fragmented_mp4

Para asegurarse de que todo sea justo, puede usar el increíble sitio de herramientas gpac / mp4box.js o http://mp4parser.com/ para ver las diferencias: primero descargue mp4.

Como puede ver, tiene un bloque mdat indivisible : este es el lugar donde se ubican los cuadros de audio y video. Ahora descargue mp4 fragmentado para ver cómo extiende los bloques mdat:

Capítulo 3 - Transcodificación


TLDR muéstrame el código y la ejecución:

$ make run_transcoding

Omitiremos algunos detalles, pero no se preocupe: el código fuente está disponible en github.

En este capítulo, vamos a crear un transcodificador minimalista escrito en C, que puede convertir vídeos de H264 a H265 usando bibliotecas FFmpeg la libav, en particular, libavcodec , libavformat y libavutil .


AVFormatContext es una abstracción para el formato de archivo multimedia, es decir para un contenedor (MKV, MP4, Webm, TS)
AVStream representa cada tipo de datos para un formato determinado (por ejemplo: audio, video, subtítulos, metadatos)
AVPacket es un fragmento de datos comprimidos recibidos de AVStream que puede decodificarse utilizando AVCodec (por ejemplo : av1, h264, vp9, hevc) que generan datos sin procesar llamados AVFrame .

Transmultiplexación


Comencemos con una conversión simple, luego cargue el archivo de entrada.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

Ahora configure el decodificador. AVFormatContext nos dará acceso a todos los componentes de AVStream , y para cada uno de ellos podemos obtener su AVCodec y crear un AVCodecContext específico . Y finalmente, podemos abrir este códec para ir al proceso de decodificación.

AVCodecContext contiene datos de configuración de medios, como velocidad de datos, velocidad de cuadros, frecuencia de muestreo, canales, tono y muchos otros.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

También debe preparar el archivo multimedia de salida para la conversión. Primero, asigne memoria para la salida AVFormatContext . Cree cada secuencia en el formato de salida. Para empacar correctamente la secuencia, copie los parámetros del códec del decodificador.

Establezca el indicador AV_CODEC_FLAG_GLOBAL_HEADER , que le dice al codificador que puede usar encabezados globales y, finalmente, abra el archivo de salida para escribir y guardar los encabezados:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

Obtenemos AVPacket del decodificador, ajustamos las marcas de tiempo y escribimos el paquete correctamente en el archivo de salida. A pesar de que la función av_interleaved_write_frame informa “ marco de escritura ”, guardamos el paquete. Completamos el proceso de permutación escribiendo el avance de la transmisión en un archivo.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

Transcodificación


En la sección anterior, había un programa simple para la conversión, ahora agregaremos la capacidad de codificar archivos, en particular, transcodificar video de h264 a h265 .

Después de preparar el decodificador, pero antes de organizar el archivo multimedia de salida, configure el codificador.

  • Cree un video AVStream en el codificador avformat_new_stream .
  • Utilizamos AVCodec con el nombre libx265 , avcodec_find_encoder_by_name .
  • Cree un AVCodecContext basado en el códec avcodec_alloc_context3 creado .
  • Establezca los atributos básicos para una sesión de transcodificación y ...
  • ... abra el códec y copie los parámetros del contexto a la secuencia ( avcodec_open2 y avcodec_parameters_from_context ).

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

Es necesario expandir el ciclo de decodificación para transcodificar una transmisión de video:

  • Enviamos un AVPacket vacío al decodificador ( avcodec_send_packet ).
  • Obtenga el AVFrame sin comprimir ( avcodec_receive_frame ).
  • Comenzamos a recodificar el marco sin formato.
  • Enviamos el marco sin formato ( avcodec_send_frame ).
  • Nosotros obtenemos la compresión basada en nuestra AVPacket códec ( avcodec_receive_packet ).
  • Establezca la marca de tiempo ( av_packet_rescale_ts ).
  • Escribimos en el archivo de salida ( av_interleaved_write_frame ).

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

Convertimos el flujo de medios de h264 a h265 . Como se esperaba, la versión del archivo multimedia h265 es más pequeña que h264, mientras que el programa tiene muchas oportunidades:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

De la mano al corazón, confieso que fue un poco más complicado de lo que parecía al principio. Tuve que elegir el código fuente de la línea de comandos FFmpeg y probar mucho. Probablemente me perdí algo en alguna parte, porque tuve que usar force-cfr para h264 , y todavía aparecen algunos mensajes de advertencia, por ejemplo, que el tipo de trama (5) se cambió por la fuerza al tipo de trama (3).

Traducciones en el blog de Edison:


All Articles