[Parte 1/2] Guía de FFmpeg y SDL o Cómo escribir un reproductor de video en menos de 1000 líneas


Aunque esta información ya está desactualizada, el material original sigue siendo una fuente popular de inspiración para diversos contenidos útiles sobre el tema de FFmpeg. Sin embargo, todavía no hay una traducción completa del original al ruso. Corregimos la molesta omisión, porque es mejor tarde que nunca.

Y aunque lo intentamos, las dificultades de traducción son inevitables en un texto tan voluminoso . Informar errores (preferiblemente en mensajes privados): juntos lo haremos mejor.

Tabla de contenido

Software EDISON - desarrollo web
EDISON.

, C C++.

! ;-)


UPD: esta guía se actualizó a partir de febrero de 2015.

FFmpeg es una gran biblioteca para crear aplicaciones de video, así como utilidades de uso general. FFmpeg se encarga de toda la rutina de procesamiento de video, realizando toda la decodificación, codificación, multiplexación y demultiplexación. Lo que simplifica enormemente la creación de aplicaciones multimedia. Todo es bastante simple y rápido, escrito en C, puede decodificar casi cualquier códec disponible en la actualidad, así como codificarlo en otros formatos.

El único inconveniente es que la documentación falta en su mayoría. Hay un tutorial ( en el original, aquí hay un enlace a una página web ya inexistente)), que cubre los conceptos básicos de FFmpeg y la generación automática de muelles de doxygen. Y nada más. Por lo tanto, decidí descubrir de manera independiente cómo usar FFmpeg para crear aplicaciones de audio y video digital que funcionen, y al mismo tiempo documentar el proceso y presentarlo en forma de libro de texto.

Hay un programa FFplay que viene con FFmpeg. Es simple, escrito en C, implementa un reproductor de video completo usando FFmpeg. Mi primera lección es una versión actualizada de la lección original de Martin Boehme ( en el original, un enlace a una página web ya desaparecida, una nota del traductor ). Arrastré algunas piezas desde allí. Y también en una serie de mis lecciones, mostraré el proceso de creación de un reproductor de video funcional basado en ffplay.cFabrice Bellard. Cada lección presentará una nueva idea (o incluso dos) con una explicación de su implementación. Cada capítulo viene con una lista en C, que puede compilar y ejecutar por su cuenta. Los archivos fuente mostrarán cómo funciona este programa, cómo funcionan sus partes individuales y también demostrarán detalles técnicos menores que no están cubiertos en esta guía. Cuando hayamos terminado, ¡tendremos un reproductor de video en funcionamiento escrito en menos de 1000 líneas de código!

Al crear el reproductor, utilizaremos SDL para generar archivos multimedia de audio y video. SDL es una excelente biblioteca multimedia multiplataforma utilizada en programas de reproducción MPEG, emuladores y muchos videojuegos. Deberá descargar e instalar las bibliotecas SDL en su sistema para compilar los programas de esta guía.

Este tutorial es para personas con buena experiencia en programación. Como mínimo, debe conocer C y también comprender conceptos como colas, mutexes, etc. Debe haber cierta comprensión de multimedia; por ejemplo, cosas como formas de onda y similares. Sin embargo, ser un gurú en estos asuntos no es necesario, ya que muchos conceptos se explicarán en el curso de las lecciones.

No dude en enviarme mensajes de error, preguntas, comentarios, ideas, características, lo que sea, a Dranger Doggy Gmail dot Com.







Lea también en el blog de la
empresa EDISON:


FFmpeg libav manual






Lección 1: Crear capturas de pantalla


Listado completo: tutorial01.c
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101 
// on GCC 4.7.2 in Debian February 2015

// A small sample program that shows how to use libavformat and libavcodec to
// read video from a file.
//
// Use
//
// gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz
//
// to build (assuming libavformat and libavcodec are correctly installed
// your system).
//
// Run using
//
// tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM
// format.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;
  
  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
    return;
  
  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  
  // Write pixel data
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
  
  // Close file
  fclose(pFile);
}

int main(int argc, char *argv[]) {
  // Initalizing these to NULL prevents segfaults!
  AVFormatContext   *pFormatCtx = NULL;
  int               i, videoStream;
  AVCodecContext    *pCodecCtxOrig = NULL;
  AVCodecContext    *pCodecCtx = NULL;
  AVCodec           *pCodec = NULL;
  AVFrame           *pFrame = NULL;
  AVFrame           *pFrameRGB = NULL;
  AVPacket          packet;
  int               frameFinished;
  int               numBytes;
  uint8_t           *buffer = NULL;
  struct SwsContext *sws_ctx = NULL;

  if(argc < 2) {
    printf("Please provide a movie file\n");
    return -1;
  }
  // Register all formats and codecs
  av_register_all();
  
  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // Find the first video stream
  videoStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  
  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }
  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();
  
  // Allocate an AVFrame structure
  pFrameRGB=av_frame_alloc();
  if(pFrameRGB==NULL)
    return -1;

  // Determine required buffer size and allocate buffer
  numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
			      pCodecCtx->height);
  buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
  
  // Assign appropriate parts of buffer to image planes in pFrameRGB
  // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
  // of AVPicture
  avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
		 pCodecCtx->width, pCodecCtx->height);
  
  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_RGB24,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );

  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	// Convert the image from its native format to RGB
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pFrameRGB->data, pFrameRGB->linesize);
	
	// Save the frame to disk
	if(++i<=5)
	  SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, 
		    i);
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
  }
  
  // Free the RGB image
  av_free(buffer);
  av_frame_free(&pFrameRGB);
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);

  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

Visión general


Los archivos de película tienen varios componentes principales. Primero, el archivo en sí se llama contenedor , y el tipo de contenedor determina cómo se representan los datos en el archivo. Ejemplos de contenedores son AVI y Quicktime . Además, hay varios hilos en el archivo; en particular, generalmente hay una transmisión de audio y una transmisión de video . ("Secuencia" es una palabra divertida para "una secuencia de elementos de datos disponibles de acuerdo con la línea de tiempo".) Los elementos de datos en una secuencia se denominan cuadros . Cada secuencia está codificada por uno u otro tipo de códec . El códec determina cómo los datos reales a diruyutsya y diciembreAuditado: de ahí el nombre del códec. Ejemplos de códecs son DivX y MP3. Los paquetes se leen de la secuencia. Los paquetes son piezas de datos que pueden contener bits de datos que se decodifican en cuadros sin formato, que finalmente podemos manipular en nuestra aplicación. Para nuestros propósitos, cada paquete contiene cuadros completos (o varios cuadros si es audio).

Trabajar con transmisiones de video y audio es muy simple, incluso en el nivel más básico:

10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

Trabajar con multimedia usando FFmpeg es casi tan simple como en este programa, aunque en algunos programas el paso "HACER [...]" puede ser muy difícil. En este tutorial, abriremos el archivo, contaremos la transmisión de video dentro de él y nuestro "HACER [...]" escribirá el marco en el archivo PPM.

Abrir documento


Primero lo primero, veamos qué sucede primero cuando abres el archivo. Usando FFmpeg, primero inicializamos la biblioteca deseada:

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <ffmpeg/swscale.h>
...
int main(int argc, charg *argv[]) {
av_register_all();

Esto registra todos los formatos de archivo y códecs disponibles en la biblioteca, por lo que se usarán automáticamente al abrir un archivo con el formato / códec apropiado. Tenga en cuenta que debe llamar a av_register_all () solo una vez, por lo que lo hacemos aquí en main (). Si lo desea, puede registrar solo formatos de archivo y códecs selectivos, pero generalmente no hay una razón particular para hacerlo.

Ahora abra el archivo:

AVFormatContext *pFormatCtx = NULL;

// Open video file
if(avformat_open_input(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
  return -1; // Couldn't open file

Obtenga el nombre del archivo del primer argumento. Esta función lee el encabezado del archivo y almacena la información del formato del archivo en la estructura AVFormatContext que pasamos. Los últimos tres argumentos se utilizan para especificar el formato del archivo, el tamaño del búfer y los parámetros de formato. Al establecerlos en NULL o 0, libavformat detectará todo automáticamente.

Esta función solo mira el encabezado, por lo que ahora debemos verificar la información de la secuencia en el archivo:

// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
  return -1; // Couldn't find stream information

Esta función pasa datos válidos a pFormatCtx -> flujos . Nos familiarizamos con una conveniente función de depuración, que nos muestra lo que hay dentro:

// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, argv[1], 0);

Ahora pFormatCtx -> streams es solo una matriz de punteros de tamaño pFormatCtx -> nb_streams . Lo revisaremos hasta que encontremos la transmisión de video:

int i;
AVCodecContext *pCodecCtxOrig = NULL;
AVCodecContext *pCodecCtx = NULL;

// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
    videoStream=i;
    break;
  }
if(videoStream==-1)
  return -1; // Didn't find a video stream

// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;

La información sobre el códec en la secuencia se encuentra en un lugar llamado " contexto de códec ". Contiene toda la información sobre el códec que usa la secuencia, y ahora tenemos un puntero. Pero todavía tenemos que encontrar el códec real y abrirlo:

AVCodec *pCodec = NULL;

// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1; // Codec not found
}
// Copy context
pCodecCtx = avcodec_alloc_context3(pCodec);
if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
// Open codec
if(avcodec_open2(pCodecCtx, pCodec)<0)
  return -1; // Could not open codec

¡Tenga en cuenta que no puede usar directamente AVCodecContext desde la transmisión de video! Por lo tanto, debe usar un vcodec_copy_context () para copiar el contexto a una nueva ubicación (por supuesto, después de asignarle memoria).

Almacenamiento de datos


Ahora necesitamos un lugar para almacenar el marco:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

Como planeamos generar archivos PPM que están almacenados en RGB de 24 bits, necesitaremos convertir nuestro marco de su propio formato a RGB. FFmpeg lo hará por nosotros. Para la mayoría de los proyectos (incluido este), debe convertir el marco de inicio a un formato específico. Seleccione un marco para el marco convertido:

// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();
if(pFrameRGB==NULL)
  return -1;

A pesar del hecho de que seleccionamos el marco, todavía necesitamos un lugar para acomodar los datos sin procesar al convertirlo. Usamos avpicture_get_size para obtener los tamaños correctos y asignar el espacio necesario manualmente:

uint8_t *buffer = NULL;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                            pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

av_malloc es un análogo de la función C malloc de FFmpeg, que es un contenedor simple alrededor de malloc que proporciona la alineación de las direcciones de memoria, etc. Por cierto, esto no protege contra pérdidas de memoria, doble liberación u otros problemas que ocurren con malloc .

Ahora usamos avpicture_fill para asociar el marco con nuestro búfer recién asignado. Con respecto a AVPicture : la estructura AVPicture es un subconjunto de la estructura AVFrame : el comienzo de la estructura AVFrame es idéntico a la estructura AVPraction .

// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
                pCodecCtx->width, pCodecCtx->height);

Ya estamos en la línea de meta! ¡Ahora estamos listos para leer de la transmisión!

Lectura de datos


Ahora, para leer la transmisión de video completa, leemos el siguiente paquete, lo desciframos en nuestro marco y, tan pronto como se completa el descifrado, convertimos el marco y lo guardamos:

struct SwsContext *sws_ctx = NULL;
int frameFinished;
AVPacket packet;
// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
    pCodecCtx->height,
    pCodecCtx->pix_fmt,
    pCodecCtx->width,
    pCodecCtx->height,
    PIX_FMT_RGB24,
    SWS_BILINEAR,
    NULL,
    NULL,
    NULL
    );

i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
  // Is this a packet from the video stream?
  if(packet.stream_index==videoStream) {
	// Decode video frame
    avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
    
    // Did we get a video frame?
    if(frameFinished) {
    // Convert the image from its native format to RGB
        sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pFrameRGB->data, pFrameRGB->linesize);
	
        // Save the frame to disk
        if(++i<=5)
          SaveFrame(pFrameRGB, pCodecCtx->width, 
                    pCodecCtx->height, i);
    }
  }
    
  // Free the packet that was allocated by av_read_frame
  av_free_packet(&packet);
}

Nada complicado: av_read_frame () lee el paquete y lo guarda en la estructura AVPacket . Tenga en cuenta que solo distribuimos la estructura del paquete: FFmpeg nos proporciona los datos internos a los que apunta packet.data . Esto libera av_free_packet () un poco más tarde . avcodec_decode_video () convierte el paquete a trama. Sin embargo, es posible que no tengamos toda la información que necesitamos para el marco después de decodificar el paquete, por lo tanto avcodec_decode_video () establece frameFinished cuando tenemos el siguiente marco. Finalmente, usamos sws_scale () para convertir desde nuestro propio formato ( pCodecCtx ->pix_fmt ) en RGB. Recuerde que puede lanzar unpuntero AVFrame a un puntero AVPicture . Finalmente, pasamos la información sobre el marco, la altura y el ancho de nuestra función SaveFrame .

Hablando de paquetes. Técnicamente, un paquete puede contener solo una parte de una trama, así como otros bits de datos. Sin embargo, el analizador FFmpeg garantiza que los paquetes que recibimos contienen un cuadro completo o incluso varios cuadros.

Ahora todo lo que queda por hacer es usar la función SaveFrame para escribir la información RGB en un archivo PPM. Aunque estamos tratando superficialmente con el formato PPM mismo; créeme, todo funciona aquí:

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;
  
  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
    return;
  
  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  
  // Write pixel data
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
  
  // Close file
  fclose(pFile);
}

Realizamos un archivo estándar abierto, etc., y luego grabamos los datos RGB. El archivo se escribe línea por línea. Un archivo PPM es simplemente un archivo en el que la información RGB se presenta como una línea larga. Si conoce los colores de HTML, será como marcar los colores de cada píxel desde el primer extremo hasta el último, algo así como # ff0000 # ff0000 ... , como para una pantalla roja. (De hecho, está almacenado en formato binario y sin separador, pero espero que capte la idea). El título indica qué tan ancha y alta es la imagen, así como el tamaño máximo de los valores RGB.

Ahora volvamos a nuestra función main (). Tan pronto como terminemos de leer la transmisión de video, solo tenemos que borrar todo:

// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);

// Free the YUV frame
av_free(pFrame);

// Close the codecs
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

// Close the video file
avformat_close_input(&pFormatCtx);

return 0;

Como puede ver, usamos av_free para la memoria asignada usando avcode_alloc_frame y av_malloc .

¡Eso es todo el código! Ahora, si está utilizando Linux o una plataforma similar, ejecute:

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

Si tiene una versión anterior de FFmpeg, es posible que deba eliminar -lavutil :

gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lz -lm

La mayoría de los programas gráficos deben abrir el formato PPM. Compruébalo en algunos archivos de películas cuyas capturas de pantalla se hicieron con nuestro programa.






Lección 2: Mostrar la pantalla


Listado completo: tutorial02.c
// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
// 
// gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial02 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

int main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  float           aspect_ratio;
  struct SwsContext *sws_ctx = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // Find the first video stream
  videoStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  
  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );



  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	SDL_LockYUVOverlay(bmp);

	AVPicture pict;
	pict.data[0] = bmp->pixels[0];
	pict.data[1] = bmp->pixels[2];
	pict.data[2] = bmp->pixels[1];

	pict.linesize[0] = bmp->pitches[0];
	pict.linesize[1] = bmp->pitches[2];
	pict.linesize[2] = bmp->pitches[1];

	// Convert the image into YUV format that SDL uses
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pict.data, pict.linesize);

	SDL_UnlockYUVOverlay(bmp);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
      
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codec
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

SDL y video


Para dibujar en la pantalla usaremos SDL. SDL significa Simple Direct Layer . Es una excelente biblioteca multimedia multiplataforma utilizada en muchos proyectos. Puede obtener la biblioteca en el sitio web oficial o descargar el paquete de desarrollador para su sistema operativo, si corresponde. Necesitará bibliotecas para compilar el código de esta lección (todas las demás lecciones, por cierto, esto también se aplica).

SDL tiene muchos métodos para dibujar en la pantalla. Una forma de mostrar películas es lo que se llama superposición YUV .

Formalmente, ni siquiera YUV, sino YCbCr. Algunas personas, por cierto, se queman mucho cuando se llama "YCbCr" como "YUV". En términos generales, YUV es un formato analógico y YCbCr es un formato digital. FFmpeg y SDL en su código y en macros designan YCbCr como YUV, pero eso es.

YUV es un método para almacenar datos de imágenes en bruto, como RGB. En términos generales, Y es un componente del brillo , y U y V son componentes del color . (Esto es más complicado que RGB porque parte de la información de color se descarta, y solo puede tener 1 medición de U y V por cada 2 mediciones de Y ). Superposición YUVen SDL acepta un conjunto de datos YUV sin procesar y lo muestra. Acepta 4 tipos diferentes de formatos YUV, pero YV12 es el más rápido de ellos. Hay otro formato YUV llamado YUV420P que coincide con YV12, excepto que las matrices de U y V se intercambian. 420 significa que se muestrea en una proporción de 4: 2: 0, es decir, por cada 4 mediciones de brillo hay 1 medición de color, por lo que la información de color se distribuye en cuartos. Esta es una buena manera de ahorrar ancho de banda porque el ojo humano todavía no nota estos cambios. La letra latina "P" en el nombre indica que el formato es "plano", lo que simplemente significa que los componentes son Y ,U y V están en matrices separadas. FFmpeg puede convertir imágenes a YUV420P , lo cual es muy útil, porque muchas transmisiones de video ya están almacenadas en este formato o se pueden convertir fácilmente a él.

Por lo tanto, nuestro plan actual es reemplazar la función SaveFrame () de la lección anterior y mostrar nuestro marco en su lugar. Pero primero debe familiarizarse con las características básicas de la biblioteca SDL. Para comenzar, conecte las bibliotecas e inicialice el SDL:

#include <SDL.h>
#include <SDL_thread.h>

if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
  fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
  exit(1);
}

SDL_Init () esencialmente le dice a la biblioteca qué funciones usaremos. SDL_GetError (), por supuesto, esta es nuestra función conveniente para la depuración.

Creación de pantalla


Ahora necesitamos un lugar en la pantalla para organizar los elementos. El área principal para mostrar imágenes con SDL se llama superficie :

SDL_Surface *screen;

screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if(!screen) {
  fprintf(stderr, "SDL: could not set video mode - exiting\n");
  exit(1);
}

Así que configuramos una pantalla con un ancho y alto dados. La siguiente opción es la profundidad de bits de la pantalla - 0 - este es un valor especial que significa "lo mismo que la pantalla actual".

Ahora creamos una superposición de YUV en esta pantalla para que podamos enviarle video y configurar nuestro SWSContext para convertir datos de imagen a YUV420 :

SDL_Overlay     *bmp = NULL;
struct SWSContext *sws_ctx = NULL;

bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
                           SDL_YV12_OVERLAY, screen);

// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
                         pCodecCtx->height,
			 pCodecCtx->pix_fmt,
			 pCodecCtx->width,
			 pCodecCtx->height,
			 PIX_FMT_YUV420P,
			 SWS_BILINEAR,
			 NULL,
			 NULL,
			 NULL
			 );

Como se mencionó, usamos YV12 para mostrar la imagen y obtener los datos YUV420 de FFmpeg.

Visualización de la imagen


Bueno, eso fue bastante fácil! Ahora solo tenemos que mostrar la imagen. Vayamos hasta el lugar donde tuvimos el tiro terminado. Podemos deshacernos de todo lo que teníamos para el cuadro RGB y vamos a reemplazar SaveFrame () con nuestro código de visualización. Para mostrar la imagen, vamos a crear una estructura AVPicture y establecer los punteros de datos y el tamaño de línea para nuestra superposición YUV :

  if(frameFinished) {
    SDL_LockYUVOverlay(bmp);

    AVPicture pict;
    pict.data[0] = bmp->pixels[0];
    pict.data[1] = bmp->pixels[2];
    pict.data[2] = bmp->pixels[1];

    pict.linesize[0] = bmp->pitches[0];
    pict.linesize[1] = bmp->pitches[2];
    pict.linesize[2] = bmp->pitches[1];

    // Convert the image into YUV format that SDL uses
    sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, pCodecCtx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(bmp);

Al principio, bloqueamos la superposición, porque planeamos escribirle. Este es un buen hábito para que luego no haya problemas. La estructura AVPicture , como se muestra arriba, tiene un puntero de datos, que es una matriz de 4 punteros. Como aquí estamos tratando con el YUV420P , tenemos solo 3 canales y, por lo tanto, solo 3 conjuntos de datos. Otros formatos pueden tener un cuarto puntero para el canal alfa o algo más. El tamaño de la línea es lo que parece. Estructuras similares en nuestra superposición YUV son variables para píxeles y alturas. (Tonos, tonos: si se expresa en términos de SDL para indicar el ancho de una cadena de datos dada). Entonces, indicamos tres matrices de datos pict. En nuestra superposición, así que cuando escribimos enpict , en realidad estamos grabando en nuestra superposición, que, por supuesto, ya tiene el espacio necesario asignado específicamente para ello. Del mismo modo, obtenemos información del tamaño de línea directamente de nuestra superposición. Cambiamos el formato de conversión a PIX_FMT_YUV420P y usamos sws_scale como antes.

Dibujo de la imagen


Pero aún necesitamos especificar el SDL para que realmente muestre los datos que le proporcionamos. También pasamos un rectángulo a esta función, que indica a dónde debe ir la película, a qué ancho y altura debe escalarse. Por lo tanto, el SDL escala para nosotros, y esto puede ayudar a su GPU a escalar más rápido:

SDL_Rect rect;

  if(frameFinished) {
    /* ... code ... */
    // Convert the image into YUV format that SDL uses
    sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
              pFrame->linesize, 0, pCodecCtx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(bmp);
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
  }

¡Ahora se muestra nuestro video!

Muestremos una característica más de SDL: sistema de eventos . SDL está configurado de tal manera que cuando ingresa o mueve el mouse en la aplicación SDL o le envía una señal, se genera un evento. Su programa luego verifica estos eventos si está destinado a procesar la entrada del usuario. Su programa también puede crear eventos para enviar eventos SDL al sistema. Esto es especialmente útil para la programación de subprocesos múltiples con SDL, que veremos en la lección número 4. En nuestro programa, vamos a verificar los eventos inmediatamente después del procesamiento del paquete. En este momento, vamos a manejar el evento SDL_QUIT para que podamos salir:

SDL_Event       event;

    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

¡Y así vivimos! Nos deshacemos de toda la basura vieja y estamos listos para compilar. Si usa Linux o algo parecido a Linux, la mejor manera de compilar usando las bibliotecas SDL es:

gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

sdl-config simplemente muestra los indicadores necesarios para que gcc habilite correctamente las bibliotecas SDL. Puede que tenga que hacer algo más para hacer esta compilación en su sistema; por favor revise la documentación SDL para su sistema para cualquier bombero. Una vez compilado, continúe y ejecute.

¿Qué sucede cuando ejecutas este programa? ¡El video parece estar volviéndose loco! De hecho, simplemente mostramos todos los cuadros de video tan rápido como podemos extraerlos del archivo de película. No tenemos el código en este momento para saber cuándo necesitamos mostrar el video. Al final (en la lección número 5) comenzaremos a sincronizar el video. Pero por el momento nos falta algo igualmente importante: ¡el sonido!






Lección 3: Reproducir sonido


Listado completo: tutorial03.c
// tutorial03.c
// A pedagogical video player that will stream through every video frame as fast as it can
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
//
// gcc -o tutorial03 tutorial03.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial03 myvideofile.mpg
//
// to play the stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

PacketQueue audioq;

int quit = 0;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  
  SDL_LockMutex(q->mutex);
  
  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;
  
  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {

  static AVPacket pkt;
  static uint8_t *audio_pkt_data = NULL;
  static int audio_pkt_size = 0;
  static AVFrame frame;

  int len1, data_size = 0;

  for(;;) {
    while(audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	audio_pkt_size = 0;
	break;
      }
      audio_pkt_data += len1;
      audio_pkt_size -= len1;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, frame.data[0], data_size);
      }
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt.data)
      av_free_packet(&pkt);

    if(quit) {
      return -1;
    }

    if(packet_queue_get(&audioq, &pkt, 1) < 0) {
      return -1;
    }
    audio_pkt_data = pkt.data;
    audio_pkt_size = pkt.size;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
  int len1, audio_size;

  static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2];
  static unsigned int audio_buf_size = 0;
  static unsigned int audio_buf_index = 0;

  while(len > 0) {
    if(audio_buf_index >= audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	audio_buf_size = 1024; // arbitrary?
	memset(audio_buf, 0, audio_buf_size);
      } else {
	audio_buf_size = audio_size;
      }
      audio_buf_index = 0;
    }
    len1 = audio_buf_size - audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
    len -= len1;
    stream += len1;
    audio_buf_index += len1;
  }
}

int main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream, audioStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  struct SwsContext *sws_ctx = NULL;
  
  AVCodecContext  *aCodecCtxOrig = NULL;
  AVCodecContext  *aCodecCtx = NULL;
  AVCodec         *aCodec = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;
  SDL_AudioSpec   wanted_spec, spec;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
    
  // Find the first video stream
  videoStream=-1;
  audioStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       videoStream < 0) {
      videoStream=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audioStream < 0) {
      audioStream=i;
    }
  }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  if(audioStream==-1)
    return -1;
   
  aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;
  aCodec = avcodec_find_decoder(aCodecCtxOrig->codec_id);
  if(!aCodec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  // Copy context
  aCodecCtx = avcodec_alloc_context3(aCodec);
  if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Set audio settings from codec info
  wanted_spec.freq = aCodecCtx->sample_rate;
  wanted_spec.format = AUDIO_S16SYS;
  wanted_spec.channels = aCodecCtx->channels;
  wanted_spec.silence = 0;
  wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
  wanted_spec.callback = audio_callback;
  wanted_spec.userdata = aCodecCtx;
  
  if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
    fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
    return -1;
  }

  avcodec_open2(aCodecCtx, aCodec, NULL);

  // audio_st = pFormatCtx->streams[index]
  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video

#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );

  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	SDL_LockYUVOverlay(bmp);

	AVPicture pict;
	pict.data[0] = bmp->pixels[0];
	pict.data[1] = bmp->pixels[2];
	pict.data[2] = bmp->pixels[1];

	pict.linesize[0] = bmp->pitches[0];
	pict.linesize[1] = bmp->pitches[2];
	pict.linesize[2] = bmp->pitches[1];

	// Convert the image into YUV format that SDL uses	
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pict.data, pict.linesize);
	
	SDL_UnlockYUVOverlay(bmp);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
	av_free_packet(&packet);
      }
    } else if(packet.stream_index==audioStream) {
      packet_queue_put(&audioq, &packet);
    } else {
      av_free_packet(&packet);
    }
    // Free the packet that was allocated by av_read_frame
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      quit = 1;
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }

  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtxOrig);
  avcodec_close(pCodecCtx);
  avcodec_close(aCodecCtxOrig);
  avcodec_close(aCodecCtx);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

Audio


Ahora nos gustaría que el sonido se reproduzca en la aplicación. SDL también nos proporciona métodos para reproducir sonido. La función SDL_OpenAudio () se usa para abrir el dispositivo de audio. Toma como argumentos la estructura SDL_AudioSpec , que contiene toda la información sobre el audio que vamos a reproducir.

Antes de mostrar cómo configurar esto, primero explicamos cómo la computadora procesa el audio en general. El audio digital consiste en una larga secuencia de muestras, cada uno de los cuales representa un significado específico de una onda de sonido. Los sonidos se graban a una frecuencia de muestreo específica, que simplemente indica qué tan rápido se reproduce cada muestra y se mide por el número de muestras por segundo. Las frecuencias de muestreo aproximadas son 22.050 y 44.100 muestras por segundo, que son las velocidades utilizadas para radio y CD, respectivamente. Además, la mayoría del audio puede tener más de un canal para sonido estéreo o envolvente, por lo que, por ejemplo, si la muestra está en estéreo, las muestras vendrán de dos en dos. Cuando obtenemos los datos del archivo de película, no sabemos cuántas muestras obtendremos, pero FFmpeg no produce muestras rotas; esto también significa que tampoco separará la muestra estéreo.

El método para reproducir audio en SDL es el siguiente. Los parámetros de sonido están configurados: frecuencia de muestreo, número de canales, etc. Y también configure la función de devolución de llamada y los datos del usuario. Cuando comenzamos a reproducir sonido, el SDL llamará constantemente a esta función de devolución de llamada y le pedirá que llene el búfer de audio con un cierto número de bytes. Después de poner esta información en la estructura SDL_AudioSpec , llamamos a SDL_OpenAudio (), que abrirá el dispositivo de audio y nos devolverá otra estructura AudioSpec . Estas son las características que realmente utilizaremos: ¡no hay garantía de que obtengamos exactamente lo que pedimos!

Configuracion de audio


Solo tenlo en cuenta por ahora, porque todavía no tenemos información sobre las transmisiones de audio. Volvamos al lugar en nuestro código donde encontramos la transmisión de video y descubramos qué transmisión es la transmisión de audio:

// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO
     &&
       videoStream < 0) {
    videoStream=i;
  }
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
     audioStream < 0) {
    audioStream=i;
  }
}
if(videoStream==-1)
  return -1; // Didn't find a video stream
if(audioStream==-1)
  return -1;

Aquí podemos obtener toda la información que queremos del AVCodecContext de la transmisión, tal como lo hicimos con la transmisión de video:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;

Si recuerdas, en lecciones anteriores, todavía tenemos que abrir el códec de audio. Es simple:

AVCodec         *aCodec;

aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1;
}
// Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
/* set up SDL Audio here */

avcodec_open2(aCodecCtx, aCodec, NULL);

En el contexto del códec contiene toda la información necesaria para configurar nuestro audio:

wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;

if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
  fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
  return -1;
}

Repasemos cada elemento:
  • freq (frecuencia): frecuencia de muestreo, como se explicó anteriormente.
  • format (): SDL , . «S» «S16SYS» «», 16 , 16 , «SYS» , , . , avcodec_decode_audio2 .
  • channels (): .
  • silence (): , . 0.
  • samples (): , , SDL , . - 512 8192; FFplay, , 1024.
  • devolución de llamada (devolución de llamada): aquí pasamos la función de devolución de llamada real. Hablaremos más sobre la función de devolución de llamada más tarde.
  • userdata : el SDL le dará a nuestra devolución de llamada un puntero nulo a cualquier dato de usuario que queramos. Queremos hacerle saber sobre nuestro contexto de códec; un poco más abajo, quedará claro por qué.

Finalmente, abra el audio con SDL_OpenAudio .

Colas


Y es necesario! Ahora estamos listos para extraer información de audio de la transmisión. ¿Pero qué hacer con esta información? Continuamente recibiremos paquetes del archivo de película, pero al mismo tiempo, el SDL llamará a la función de devolución de llamada. ¡La solución será crear algún tipo de estructura global en la que podamos insertar paquetes de audio para que nuestro audio_callback tenga algo para recibir datos de audio! Entonces, esto es lo que haremos para crear la cola de paquetes. FFmpeg incluso tiene una estructura para ayudar con esto: AVPacketList , que es solo una lista vinculada para paquetes. Aquí está nuestra estructura de cola:

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

Primero, debemos indicar que nb_packets es diferente en tamaño: el tamaño se refiere al tamaño del byte que obtenemos de paquete-> tamaño . Tenga en cuenta que tenemos un mutex y una variable de condición. Esto se debe a que el SDL realiza el proceso de audio como una secuencia separada. Si no bloqueamos la cola correctamente, realmente podemos arruinar nuestros datos. Veamos cómo se implementa la cola. Todo programador que se precie debe saber cómo crear colas, pero también le mostraremos cómo hacerlo para que le resulte más fácil aprender las funciones SDL.

Primero, creamos una función para inicializar la cola:

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}

Luego cree una función para colocar objetos en nuestra cola:

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  
  SDL_LockMutex(q->mutex);
  
  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}

SDL_LockMutex () bloquea el mutex en la cola para que podamos agregar algo, y luego SDL_CondSignal () envía una señal a nuestra función get (si lo espera) a través de nuestra variable condicional para decirle que hay datos y puede continuar, para más Desbloquear mutex.

Aquí está la función get correspondiente . Observe cómo SDL_CondWait () crea el bloque de funciones (es decir, se detiene hasta que obtengamos los datos) si le decimos que haga esto:

int quit = 0;

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
  AVPacketList *pkt1;
  int ret;
  
  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

Como puede ver, envolvimos la función en un ciclo eterno, por lo que definitivamente obtendremos algunos datos si queremos bloquearla. Evitamos los bucles para siempre usando la función SDL_CondWait (). Esencialmente, todo lo que hace CondWait es esperar una señal de SDL_CondSignal () (o SDL_CondBroadcast ()) y luego continuar. Sin embargo, parece que lo atrapamos en un mutex: si mantenemos el bloqueo, ¡nuestra función put no puede poner en cola nada! Sin embargo, lo que SDL_CondWait () también hace por nosotros es desbloquear el mutex que le damos, y luego intentar nuevamente bloquearlo tan pronto como recibamos la señal.

Por cada bombero


También verá que tenemos una variable global de salida que verificamos para asegurarnos de que no hayamos configurado la señal de salida en el programa (SDL procesa automáticamente las señales TERM , etc.). De lo contrario, el hilo continuará para siempre, y tendremos que matar el programa con kill -9 :

  SDL_PollEvent(&event);
  switch(event.type) {
  case SDL_QUIT:
    quit = 1;

Configuraremos la bandera de salida a 1.

Alimentamos paquetes


Solo queda configurar nuestra cola:

PacketQueue audioq;
main() {
...
  avcodec_open2(aCodecCtx, aCodec, NULL);

  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

SDL_PauseAudio () finalmente inicia la unidad de audio. Reproduce silencio si no recibe datos; Pero esto no sucede de inmediato.

Entonces, tenemos una cola configurada, ahora estamos listos para enviarle paquetes. Pasamos a nuestro ciclo de lectura de paquetes:

while(av_read_frame(pFormatCtx, &packet)>=0) {
  // Is this a packet from the video stream?
  if(packet.stream_index==videoStream) {
    // Decode video frame
    ....
    }
  } else if(packet.stream_index==audioStream) {
    packet_queue_put(&audioq, &packet);
  } else {
    av_free_packet(&packet);
  }

Tenga en cuenta que no lanzamos el paquete después de ponerlo en cola. Lo lanzaremos más tarde cuando descifremos.

Recuperando paquetes


Ahora, finalmente, hagamos que nuestra función audio_callback recupere paquetes de la cola. La devolución de llamada debería verse así:

void callback(void *userdata, Uint8 *stream, int len)

userdata , es el puntero que le dimos al SDL, stream es el búfer en el que escribiremos datos de audio, y len es el tamaño de este búfer. Aquí está el código:

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
  int len1, audio_size;

  static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  static unsigned int audio_buf_size = 0;
  static unsigned int audio_buf_index = 0;

  while(len > 0) {
    if(audio_buf_index >= audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(aCodecCtx, audio_buf,
                                      sizeof(audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	audio_buf_size = 1024;
	memset(audio_buf, 0, audio_buf_size);
      } else {
	audio_buf_size = audio_size;
      }
      audio_buf_index = 0;
    }
    len1 = audio_buf_size - audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
    len -= len1;
    stream += len1;
    audio_buf_index += len1;
  }
}

De hecho, este es un bucle simple que extrae datos de otra función que escribimos, audio_decode_frame (), guarda el resultado en un búfer intermedio, intenta escribir bytes len en la secuencia y recibe más datos si aún no tenemos suficiente o los guardamos para más tarde, si nos queda algo El tamaño de audio_buf es 1,5 veces el tamaño del cuadro de audio más grande que nos dará FFmpeg, lo que nos da un buen margen.

Descifrado de audio final


Veamos el interior del decodificador audio_decode_frame :

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
                       int buf_size) {

  static AVPacket pkt;
  static uint8_t *audio_pkt_data = NULL;
  static int audio_pkt_size = 0;
  static AVFrame frame;

  int len1, data_size = 0;

  for(;;) {
    while(audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	audio_pkt_size = 0;
	break;
      }
      audio_pkt_data += len1;
      audio_pkt_size -= len1;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, frame.data[0], data_size);
      }
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt.data)
      av_free_packet(&pkt);

    if(quit) {
      return -1;
    }

    if(packet_queue_get(&audioq, &pkt, 1) < 0) {
      return -1;
    }
    audio_pkt_data = pkt.data;
    audio_pkt_size = pkt.size;
  }
}

Todo el proceso en realidad comienza cerca del final de la función, donde llamamos a packet_queue_get (). Tomamos el paquete de la cola y guardamos la información. Luego, cuando tenemos el paquete para trabajar, llamamos a avcodec_decode_audio4 (), que es muy similar a su función hermana avcodec_decode_video (), excepto que en este caso el paquete puede tener más de un cuadro. Por lo tanto, es posible que deba llamarlo varias veces para obtener todos los datos del paquete. Una vez recibido el marco, simplemente lo copiamos a nuestro búfer de audio, asegurándonos de que data_size sea más pequeño que nuestro búfer de audio. Además, recuerda sobre el casting de audio_bufal tipo correcto, porque SDL proporciona un búfer int de 8 bits, y FFmpeg nos da datos en un búfer int de 16 bits. También debe considerar la diferencia entre len1 y data_size . len1 es el tamaño del paquete que usamos, y data_size es la cantidad de datos sin procesar devueltos.

Cuando tenemos algunos datos, volvemos inmediatamente para averiguar si necesitamos obtener más datos de la cola o si hemos terminado. Si todavía necesitamos procesar el paquete, entonces quédese con él. Si ha completado el paquete, finalmente libérelo.

Y es todo! Tenemos el audio transferido desde el bucle de lectura principal a la cola, que luego es leída por la función audio_callback, que transfiere estos datos a la SDL, y la SDL se transfiere a su tarjeta de sonido. Adelante y compila:

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

Gip-gip-hurra! El video todavía se transmite a la velocidad máxima, pero el sonido ya se reproduce como debería. ¿Porqué es eso? Sí, porque la información de audio tiene una frecuencia de muestreo: bombeamos la información de audio tan rápido como resulta, pero el audio simplemente se reproduce en esta secuencia de acuerdo con su frecuencia de muestreo.

Estamos casi listos para la sincronización de video y audio, pero primero necesitamos llevar a cabo una pequeña reorganización del programa. El método de poner en cola el sonido y reproducirlo usando una transmisión separada funcionó muy bien: hizo que el código fuera más manejable y más modular. Antes de comenzar a sincronizar video con audio, necesitamos simplificar el código. ¡En la próxima serie produciremos flujos de control!






Lección 4: Múltiples hilos


Listado completo tutorial04.c
// tutorial04.c
// A pedagogical video player that will stream through every video frame as fast as it can,
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial04 tutorial04.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVFrame         audio_frame;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  SDL_LockMutex(q->mutex);

  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)
      av_free_packet(pkt);

    if(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Now, normally here goes a ton of code
	 about timing, etc. we're just going to
	 guess at a delay for now. You can
	 increase and decrease this value and hard code
	 the timing - but I don't suggest that ;)
	 We'll learn how to do it for real later.
      */
      schedule_refresh(is, 40);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);
    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }      
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

Visión general


La última vez, agregamos soporte de audio usando las funciones de audio SDL. SDL lanzó un hilo haciendo devoluciones de llamada para la función que definimos cada vez que se necesitaba un sonido. Ahora vamos a hacer lo mismo con la pantalla de video. Esto hace que el código sea más modular y más fácil de trabajar, especialmente si desea agregar sincronización. Así que ¿por dónde empezamos?

Tenga en cuenta que nuestra función principal maneja mucho: pasa por el bucle de eventos, lee paquetes y decodifica el video. Lo que vamos a hacer es dividir todo en partes: tendremos una secuencia responsable de decodificar los paquetes; entonces estos paquetes se agregan a la cola y se leen por las secuencias de audio y video correspondientes. Ya hemos sintonizado la transmisión de audio según sea necesario; con una transmisión de video será algo más difícil, ya que tendremos que asegurarnos de que el video se muestre por nuestra cuenta. Agregaremos el código de visualización real al bucle principal. Pero en lugar de mostrar el video cada vez que ejecutamos el bucle, integramos la pantalla de video en el bucle de eventos. La idea es decodificar el video, guardar el cuadro recibido en otra cola y luego crear su propio evento ( FF_REFRESH_EVENT), que agregamos al sistema de eventos, luego, cuando nuestro bucle de eventos ve este evento, mostrará el siguiente fotograma en la cola. Aquí hay una ilustración ASCII conveniente de lo que está sucediendo:


La razón principal para mover el control de la pantalla de video a través del bucle de eventos es que con la secuencia SDL_Delay podemos controlar con precisión cuándo aparece el siguiente cuadro de video en la pantalla. Cuando finalmente sincronicemos el video en la próxima lección, solo agregue un código que programará la próxima actualización de video para que la imagen correcta aparezca en la pantalla en el momento adecuado.

Simplifica el código


Borremos un poco el código. Tenemos toda esta información sobre códecs de audio y video, y vamos a agregar colas, buffers, y Dios sabe qué más. Todas estas cosas son para una determinada unidad lógica, a saber, para la película. Entonces, tenemos la intención de crear una estructura grande que contenga toda esta información llamada VideoState .

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

Aquí vemos indicios de lo que vamos a obtener al final. Primero, vemos la información básica: el contexto de formato y los índices de transmisión de audio y video, así como los objetos AVStream correspondientes . Luego vemos que algunos de estos búferes de audio se mueven a esta estructura. Ellos ( audio_buf , audio_buf_size , etc.) estaban destinados a obtener información sobre el audio que todavía estaba allí (o que faltaba). Agregamos otra cola para video y un búfer (que se usará como cola; para esto no necesitamos colas extravagantes) para cuadros decodificados (guardados como una superposición). Estructura de imagen de video- Esta es nuestra propia creación (veremos qué habrá en ella cuando lleguemos a ella). También puede observar que hemos asignado punteros para dos transmisiones adicionales que crearemos, así como un indicador de salida y un nombre de archivo de película.

Entonces, ahora volvemos a la función principal para ver cómo esto cambia nuestro programa. Configuremos nuestra estructura VideoState :

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz () es una buena función que asignará memoria para nosotros y la pondrá a cero.

Luego, inicializamos nuestros bloqueos para el búfer de visualización ( pictq ), porque dado que el bucle de eventos llama a nuestra función de visualización, recuerde, la función de visualización recuperará marcos precodificados de pictq . Al mismo tiempo, nuestro decodificador de video incluirá información; no sabemos quién llega primero. Espero que entiendas que esta es una condición clásica de carrera. Por lo tanto, lo distribuimos ahora antes de comenzar cualquier tema. Copiemos también el nombre de nuestra película en VideoState :

av_strlcpy(is->filename, argv[1], sizeof(is->filename));

is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();

av_strlcpy es una función de FFmpeg que realiza algunas comprobaciones de bordes adicionales además de strncpy .

Nuestro primer hilo


Ejecutemos nuestros hilos y hagamos algo real:

schedule_refresh(is, 40);

is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
  av_free(is);
  return -1;
}

schedule_refresh es una función que definiremos más adelante. Lo que hace es decirle al sistema que produzca FF_REFRESH_EVENT después del número especificado de milisegundos. Esto, a su vez, llamará a la función de actualización de video cuando la veamos en la cola de eventos. Pero ahora veamos SDL_CreateThread ().

SDL_CreateThread () hace exactamente eso: genera un nuevo hilo que tiene acceso completo a toda la memoria del proceso original, y comienza el hilo ejecutado por la función que le damos. Esta función también transmitirá datos definidos por el usuario. En este caso, llamamos a decode_thread () y adjuntamos nuestra estructura VideoState. No hay nada nuevo en la primera mitad de la función; simplemente hace el trabajo de abrir el archivo y encontrar el índice de transmisiones de audio y video. Lo único que hacemos de manera diferente es mantener el contexto de formato en nuestra gran estructura. Después de encontrar nuestros índices de secuencia, llamamos a otra función que definimos, stream_component_open (). Esta es una forma bastante natural de separarse, y dado que hacemos muchas cosas similares para configurar el códec de video y audio, reutilizamos parte del código, convirtiéndolo en una función.

Función Stream_component_open() Es el lugar donde descubrimos nuestro decodificador de códec, configuramos los parámetros de sonido, guardamos información importante en nuestra gran estructura y lanzamos nuestras transmisiones de audio y video. Aquí también insertamos otros parámetros, como el uso forzado del códec en lugar de su detección automática, etc. Me gusta esto:

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx;
  AVCodec *codec;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    /* ...etc... */
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height,
				 is->video_st->codec->pix_fmt, is->video_st->codec->width,
				 is->video_st->codec->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

Esto es casi lo mismo que el código que teníamos antes, excepto que ahora está generalizado para audio y video. Tenga en cuenta que en lugar de aCodecCtx, hemos configurado nuestra gran estructura como datos de usuario para nuestra devolución de llamada de audio. También guardamos las transmisiones como audio_st y video_st . También agregamos nuestra cola de video y la configuramos como nuestra cola de audio. La conclusión es ejecutar secuencias de video y audio. Estos bits hacen esto:

    SDL_PauseAudio(0);
    break;

/* ...... */

    is->video_tid = SDL_CreateThread(video_thread, is);

Recuerde SDL_PauseAudio () de la última lección. SDL_CreateThread () se usa de la misma manera. Volver a nuestra función video_thread ().

Antes de eso, volvamos a la segunda mitad de nuestra función decode_thread (). Esencialmente, es solo un ciclo for que lee un paquete y lo coloca en la cola correcta:

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if((is->pFormatCtx->pb->error) == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }

No hay nada realmente nuevo aquí, excepto que ahora tenemos un tamaño máximo para nuestra cola de audio y video, y hemos agregado la verificación de errores de lectura. El contexto de formato tiene una estructura ByteIOContext dentro llamada pb . ByteIOContext es una estructura que básicamente almacena toda la información sobre archivos de bajo nivel.

Después de nuestro ciclo for, tenemos todo el código para esperar a que el resto del programa se complete o informe al respecto. Este código es instructivo porque muestra cómo empujamos los eventos, algo que necesitaremos más adelante para mostrar el video:

  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;

Obtenemos valores para eventos personalizados utilizando la constante SDL SDL_USEREVENT . El primer evento de usuario debe establecerse en SDL_USEREVENT , el siguiente SDL_USEREVENT + 1 , etc. FF_QUIT_EVENT se define en nuestro programa como SDL_USEREVENT + 1 . También podemos pasar datos de usuario si es necesario, y aquí pasamos nuestro puntero a una estructura grande. Finalmente, llamamos a SDL_PushEvent (). En nuestro cambio de bucle de eventos, simplemente colocamos esto en la sección SDL_QUIT_EVENTque teníamos antes Veremos nuestro ciclo de eventos con más detalle; por ahora, solo asegúrese de que cuando presionamos FF_QUIT_EVENT lo atrapemos más tarde y cambiemos la bandera de salida.

Recibir fotograma: video_thread


Después de haber preparado el códec, puede iniciar la transmisión de video. Este flujo lee los paquetes de la cola de video, decodifica el video en cuadros y luego llama a la función queue_picture para colocar el cuadro procesado en la cola de imágenes:

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);

    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_free(pFrame);
  return 0;
}

La mayor parte de esta función ya debería entenderse. Acabamos de copiar la función avcodec_decode_video2 aquí , simplemente reemplazando algunos argumentos; por ejemplo, tenemos un AVStream almacenado en nuestra gran estructura, por lo que obtenemos nuestro códec desde allí. Seguimos recibiendo paquetes de nuestra cola de videos hasta que alguien nos diga que salgamos o que encontremos un error.

Marco de la cola


Echemos un vistazo a la función que almacena nuestro pFrame decodificado en nuestra cola de imágenes. Dado que nuestra cola de imágenes es una superposición de SDL (presumiblemente para permitir que la función de visualización de video realice la menor cantidad de cálculos posible), necesitamos convertir nuestro marco en ella. Los datos que almacenamos en la cola de imágenes son la estructura que creamos:

typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

Nuestra gran estructura contiene un búfer de estos archivos, donde podemos almacenarlos. Sin embargo, necesitamos distribuir SDL_Overlay nosotros mismos (preste atención al indicador asignado, que muestra si lo hicimos o no).

Para usar esta cola, tenemos dos punteros: el índice de escritura y el índice de lectura. También rastreamos cuántas imágenes reales hay en el búfer. Para escribir en la cola, primero esperamos hasta que se borre nuestro búfer, de modo que tengamos un lugar para almacenar nuestra VideoPicture . ¿Luego verificamos si establecemos la superposición en nuestro índice de registro? Si no, debe asignar memoria. ¡También debemos reasignar el búfer si el tamaño de la ventana ha cambiado!

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_st->codec->width ||
     vp->height != is->video_st->codec->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

Echemos un vistazo a la función alloc_picture ():

void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
				 is->video_st->codec->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);
  vp->width = is->video_st->codec->width;
  vp->height = is->video_st->codec->height;  
  vp->allocated = 1;
}

Debería reconocer la función SDL_CreateYUVOverlay , que hemos trasladado desde nuestro bucle principal a esta sección. Este código ya debería estar razonablemente claro. Sin embargo, ahora tenemos un bloqueo de exclusión mutua, ¡porque dos hilos no pueden escribir información simultáneamente en la pantalla! Esto no permitirá que nuestra función alloc_picture interfiera con otra función que mostrará la imagen. (Creamos este bloqueo como una variable global y lo inicializamos en main (); vea el código). Recuerde que mantenemos el ancho y la altura en la estructura VideoPicture , porque debemos asegurarnos de que el tamaño de nuestro video no cambie por alguna razón.
Ok, lo hemos resuelto, y tenemos nuestra superposición YUV, dedicado y listo para recibir la imagen. Volvamos a queue_picture y miremos el código para copiar el marco en la superposición. Esta parte debería serle familiar:

int queue_picture(VideoState *is, AVFrame *pFrame) {

  /* Allocate a frame if we need it... */
  /* ... */
  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_st->codec->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

Aquí, la mayor parte es solo el código que usamos anteriormente para llenar la superposición YUV con nuestro marco. El último bit simplemente "agrega" nuestro valor a la cola. La cola funciona, se le agregan valores hasta que está llena, y la lectura se produce mientras hay al menos algo en ella. Por lo tanto, todo depende del valor de is -> pictq_size , lo que requiere que lo bloqueemos . Entonces, ¿qué estamos haciendo aquí: aumentar el puntero de registro (y si es necesario, comenzar de nuevo), luego bloquear la cola y aumentar su tamaño. Ahora nuestro lector sabrá que hay más información sobre la cola, y si esto hace que nuestra cola esté llena, y nuestra grabadora lo sabrá.

Visualización de vídeo


¡Eso es todo por nuestro hilo de video! Ahora hemos completado todos los hilos libres, excepto uno: ¿recuerdas cómo llamamos a la función schedule_refresh () hace mucho tiempo ? Echa un vistazo a lo que realmente sucedió:

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

SDL_AddTimer () es una función SDL que simplemente realiza una devolución de llamada a una función definida por el usuario después de un cierto número de milisegundos (y, si es necesario, transfiere algunos datos definidos por el usuario). Utilizaremos esta función para programar actualizaciones de video: cada vez que lo llamamos, establece un temporizador que activará un evento, lo que, a su vez, hará que nuestra función main () llame a una función que extraiga un marco de nuestra imagen de cola y muestre ¡su! ¡Uf! ¡Tres “cuál / cuál / cuál” en una oración! Entonces, hagamos lo primero: disparar este evento. Esto nos envía a:

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

El evento es lanzado por nuestro viejo amigo. FF_REFRESH_EVENT se define aquí como SDL_USEREVENT + 1 . Cabe señalar que cuando devolvemos 0, el SDL detiene el temporizador, por lo que la devolución de llamada no se ejecuta nuevamente.

Ahora que hemos llamado a FF_REFRESH_EVENT nuevamente , necesitamos procesarlo en nuestro bucle de eventos:

for(;;) {

  SDL_WaitEvent(&event);
  switch(event.type) {
  /* ... */
  case FF_REFRESH_EVENT:
    video_refresh_timer(event.user.data1);
    break;

lo que nos envía aquí a esta función, que en realidad extrae datos de nuestra cola de imágenes:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Timing code goes here */

      schedule_refresh(is, 80);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

Por el momento, esta función es bastante simple: procesa la cola mientras tenemos algo, establece un temporizador para mostrar el siguiente cuadro de video, llama a video_display para mostrar realmente el video en la pantalla, luego aumenta el contador en la cola y reduce su tamaño. Puede notar que realmente no estamos haciendo nada con vp en esta función, y he aquí por qué: esto está por delante. Pero un poco mas tarde. Lo usaremos para acceder a la información de tiempo cuando comencemos a sincronizar video con audio. Aquí, eche un vistazo al lugar en el código donde está escrito el comentario "El código de tiempo va aquí". En esta sección vamos a averiguar qué tan pronto debemos mostrar el siguiente cuadro de video y luego ingresar este valor en la función schedule_refresh() Por el momento, solo ingresamos un valor ficticio de 80. Técnicamente, puede adivinar y verificar este valor y volver a compilarlo para cada película, pero: 1) comenzará a disminuir después de un tiempo y 2) es bastante estúpido. Aunque, en el futuro volveremos a este punto.

Casi terminamos. Solo queda una cosa por hacer: ¡mostrar el video! Aquí está la función video_display :

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_st->codec->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) *
	is->video_st->codec->width / is->video_st->codec->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_st->codec->width /
	(float)is->video_st->codec->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);
  }
}

Dado que la pantalla puede ser de cualquier tamaño (instalamos 640x480, y hay formas de configurarla para que el usuario cambie el tamaño), debe determinar dinámicamente qué tan grande debe ser el área rectangular para nuestra película. Entonces, primero debe averiguar la relación de aspecto de nuestra película, solo el ancho dividido por la altura. Algunos códecs tendrán una relación de aspecto impar de la muestra, que es simplemente el ancho / alto de un píxel o muestra. Dado que los valores de altura y ancho en nuestro contexto de códec se miden en píxeles, la relación de aspecto real es igual a la relación de aspecto multiplicada por la relación de aspecto de la muestra. Algunos códecs mostrarán una relación de aspecto de 0, lo que significa que cada píxel simplemente tiene un tamaño de 1x1. Luego escalamos la película de tal manerapara que quepa en la pantalla tanto como sea posible. Inversión de bits& -3 simplemente redondea el valor al múltiplo de cuatro más cercano. Luego, centre la película y llame a SDL_DisplayYUVOverlay () para asegurarse de que se utiliza el mutex de la pantalla para acceder a ella.

¿Y es todo? ¿Terminamos? Aún necesita reescribir el código de audio para usar el nuevo VideoStruct , pero estos son cambios triviales que se pueden ver en el código de muestra. Lo último que debemos hacer es cambiar nuestra devolución de llamada para la función de devolución de llamada de salida interna en FFmpeg:

VideoState *global_video_state;

int decode_interrupt_cb(void) {
  return (global_video_state && global_video_state->quit);
}

Establezca global_video_state en una estructura grande en main ().

¡Eso es todo! Compilamos:

gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

y disfruta de la película sin sincronizar! ¡En el siguiente paso, finalmente crearemos un reproductor de video realmente funcional !






Lección 5: Sincronización de video


Listado completo tutorial05.c
// tutorial05.c
// A pedagogical video player that really works!
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial05 tutorial05.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define AV_SYNC_THRESHOLD 0.01
#define AV_NOSYNC_THRESHOLD 10.0

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
  double pts;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;

  double          audio_clock;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVFrame         audio_frame;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  int             audio_hw_buf_size;  
  double          frame_timer;
  double          frame_last_pts;
  double          frame_last_delay;
  double          video_clock; ///<pts of last decoded frame / predicted pts of next decoded frame
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  SDL_LockMutex(q->mutex);

  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_ctx->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_ctx->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;
  double pts;
  int n;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_ctx->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_ctx->sample_rate);
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)
      av_free_packet(pkt);

    if(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;
  double pts;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    vp->pts = pts;
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_ctx->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;
  double pts;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;

    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);

    if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE) {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
    is->audio_hw_buf_size = spec.size;
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

ADVERTENCIA


Cuando acabo de escribir esta guía, todo mi código de sincronización se tomó de la versión de ffplay.c . Hoy es un programa completamente diferente, y las actualizaciones en las bibliotecas FFmpeg (y en ffplay.c) han llevado a cambios fundamentales. Aunque este código todavía funciona, ya está desactualizado y hay muchas otras mejoras que podrían usarse en esta guía.

Cómo se sincroniza el video


Hasta ahora, teníamos un reproductor de películas casi inútil. Sí, reproduce video y sí, reproduce audio, pero eso no es exactamente lo que llamaríamos una película. Entonces, ¿qué hacemos ahora?

PTS y DTS


Afortunadamente, las transmisiones de audio y video contienen información sobre qué tan rápido y en qué momentos deben reproducirse. Las transmisiones de audio tienen una frecuencia de muestreo y las transmisiones de video tienen fotogramas por segundo. Sin embargo, si solo sincronizamos el video contando el número de fotogramas y multiplicándolo por la velocidad de fotogramas, existe una buena posibilidad de que no se sincronice con el sonido. Por lo tanto, iremos por el otro lado. Los paquetes de la transmisión pueden tener la llamada marca de tiempo de decodificación ( DTS - de d ecoding t ime s tamp ) y la marca de tiempo de presentación ( PTS - de p resentation t ime stamp ) Para comprender estos dos significados, necesita saber cómo se almacenan las películas. Algunos formatos, como MPEG, usan lo que llaman cuadros B ( Bed and es bidireccional, Inglaterra. Bidireccional ). Otros dos tipos de tramas se denominan tramas I y tramas-P ( I es interna , i nner , y P medio predicho , p redicted ). I-frames contienen la imagen completa. Marcos Pdependen de cuadros I y P anteriores y son diferentes de los cuadros anteriores, o también puede nombrar - deltas. Los cuadros B son similares a los cuadros P, ¡pero dependen de la información contenida en los cuadros anteriores y posteriores! El hecho de que un fotograma no contenga la imagen en sí, pero las diferencias con otros fotogramas explica por qué es posible que no tengamos un fotograma terminado después de llamar a avcodec_decode_video2 .

Digamos que tenemos una película en la que 4 cuadros en esta secuencia: IBBP . Luego, necesitamos encontrar la información del último cuadro P antes de que podamos mostrar cualquiera de los dos cuadros B anteriores. Debido a esto, los cuadros se pueden almacenar en una secuencia que no coincide con el orden de visualización real: IPBB. Para eso sirven la marca de tiempo de decodificación y la marca de tiempo de presentación para cada cuadro. La marca de tiempo de decodificación nos dice cuándo necesitamos decodificar algo, y la marca de tiempo de presentación nos dice cuándo necesitamos mostrar algo. Entonces, en este caso, nuestra transmisión puede verse así:

   PTS: 1 4 2 3
   DTS: 1 2 3 4 Transmisión
: IPBB

Como regla, PTS y DTS son diferentes solo cuando la transmisión que se está reproduciendo contiene B-frames.

Cuando recibimos un paquete de av_read_frame (), contiene los valores PTS y DTS para la información que está dentro del paquete. Pero lo que realmente necesitamos es el PTS de nuestro marco sin procesar recién decodificado, en cuyo caso sabemos cuándo debe mostrarse.

Afortunadamente, FFmpeg nos proporciona la "mejor marca de tiempo posible" que podemos obtener usando la función av_frame_get_best_effort_timestamp ().

Sincronización


Para que los cuadros se muestren por turnos, sería bueno saber cuándo mostrar un cuadro de video específico. Pero, ¿cómo lo hacemos exactamente? La idea es esta: después de mostrar el marco, descubrimos cuándo debe mostrarse el siguiente marco. Luego, haga una pausa, luego de lo cual actualizaremos el video después de este período de tiempo. Como se esperaba, verificamos el valor de PTS del siguiente cuadro en el reloj del sistema para ver cuánto tiempo debe ser nuestro tiempo de espera. Este enfoque funciona, pero hay dos problemas que deben abordarse.

Primero, la pregunta es, ¿cuándo será el próximo PTS? Dirá que simplemente puede agregar la frecuencia de video al PTS actual y, en principio, tendrá razón. Sin embargo, algunas variedades de video requerirán cuadros repetidos. Esto significa que tiene que repetir el cuadro actual un cierto número de veces. Esto puede hacer que el programa muestre el siguiente fotograma demasiado pronto. Esto debe tenerse en cuenta.

El segundo problema es que, en el programa que hemos escrito en este momento, el video y el audio avanzan alegremente hasta que se molestan en sincronizar. No tendríamos que preocuparnos si todo funcionara perfectamente. Pero su computadora no es perfecta, como lo son muchos archivos de video. Por lo tanto, tenemos tres opciones: sincronizar audio con video, sincronizar video con audio o sincronizar audio y video con un reloj externo (por ejemplo, con su computadora). Ahora vamos a sincronizar el video con el audio.

Codificación: recibir una trama PTS


Ahora escribamos algo directamente. Necesitamos agregar algunas partes más a nuestra gran estructura, y lo haremos de la manera que necesitamos. Primero, echemos un vistazo a nuestro hilo de video. ¿Recuerdas que aquí recopilamos paquetes que han sido puestos en cola por nuestra secuencia de decodificación? En esta parte del código, necesitamos obtener el PTS para el marco que nos dio avcodec_decode_video2 . La primera forma en que hablamos es obtener el DTS del último paquete procesado, que es bastante simple:

  double pts;

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;
    // Decode video frame
    len1 = avcodec_decode_video2(is->video_st->codec,
                                pFrame, &frameFinished, packet);
    if(packet->dts != AV_NOPTS_VALUE) {
      pts = av_frame_get_best_effort_timestamp(pFrame);
    } else {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

Establecemos el PTS en cero si no podemos determinar su valor.

Bueno, eso fue fácil. Nota técnica: como puede ver, usamos int64 para PTS. Esto se debe a que el PTS se almacena como un entero. Este valor es una marca de tiempo que corresponde a la dimensión de tiempo en timebb . Por ejemplo, si la transmisión tiene 24 cuadros por segundo, el PTS de 42 indicará que el cuadro debe usarse donde debería estar el cuadro 42, siempre que tengamos cuadros reemplazados cada 1/24 de segundo (por supuesto, esto no necesariamente será así) de hecho).

Podemos convertir este valor a segundos dividiéndolo por la velocidad de fotogramas. Time_base valorel flujo será igual a 1 dividido por la velocidad de fotogramas (para contenido con una velocidad de fotogramas fija), por lo tanto, para obtener el PTS en segundos, multiplicamos por time_base .

Código adicional: sincronización y uso de PTS


Así que ahora tenemos todos los PTS listos para usar. Ahora nos ocuparemos de esos dos problemas de sincronización, que se discutieron un poco más arriba. Vamos a definir una función synchronize_video que actualizará el PTS para sincronizarlo con todo. Finalmente, esta función también se ocupará de casos en los que no obtengamos el valor PTS para nuestro marco. Al mismo tiempo, necesitamos rastrear cuándo se espera el próximo fotograma para poder establecer correctamente la frecuencia de actualización. Podemos hacer esto usando el valor interno video_clock , que rastrea cuánto tiempo ha pasado para el video. Agregamos este valor a nuestra gran estructura:

typedef struct VideoState {
  double          video_clock; // pts of last decoded frame / predicted pts of next decoded frame

Aquí está la función syncize_video , que es bastante clara:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_st->codec->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

Como puede ver, tenemos en cuenta los marcos repetidos en esta función.

Ahora, obtengamos nuestro PTS correcto y coloquemos el marco en cola usando queue_picture agregando un nuevo argumento pts :

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }

Lo único que cambia en queue_picture es que almacenamos este valor de pts en la estructura VideoPicture que ponemos en cola. Por lo tanto, debemos agregar la variable pts a la estructura y agregar estas líneas de código:

typedef struct VideoPicture {
  ...
  double pts;
}
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
  ... stuff ...
  if(vp->bmp) {
    ... convert picture ...
    vp->pts = pts;
    ... alert queue ...
  }

Entonces, ahora tenemos las imágenes en cola con los valores correctos de PTS, así que echemos un vistazo a nuestra función de actualización de video. Puede recordar de la última lección que simplemente lo falsificamos e instalamos una actualización de 80 ms. Bueno, ahora vamos a descubrir qué debería estar realmente allí.

Nuestra estrategia es predecir el tiempo del próximo PTS simplemente midiendo el tiempo entre el pts actual y el anterior. Al mismo tiempo, necesitamos sincronizar el video con el audio. Vamos a hacer un reloj de audio.: Un valor interno que realiza un seguimiento de la posición del audio que estamos reproduciendo. Es como una lectura digital en cualquier reproductor de mp3. Dado que sincronizamos el video con el sonido, la transmisión de video usa este valor para averiguar si está demasiado adelante o demasiado atrás.

Volveremos a la implementación más tarde; Ahora supongamos que tenemos la función get_audio_clocklo que nos dará tiempo en el reloj de audio. Tan pronto como obtengamos este valor, ¿qué se debe hacer si el video y el audio no están sincronizados? Sería una tontería intentar saltar al paquete correcto mediante una búsqueda u otra cosa. En cambio, simplemente ajustamos el valor que calculamos para la próxima actualización: si el PTS está muy por detrás del tiempo de audio, duplicamos nuestro retraso estimado. Si el PTS está demasiado adelantado para el tiempo de reproducción, simplemente actualizamos lo más rápido posible. Ahora que tenemos el tiempo de actualización o retraso configurado, lo vamos a comparar con el reloj de nuestra computadora, dejando frame_timer funcionando . Este temporizador de cuadros resume todos nuestros retrasos estimados durante la reproducción de películas. En otras palabras, este frame_timer- Este es el tiempo que indica cuándo mostrar el siguiente cuadro. Simplemente agregamos un nuevo retraso al temporizador de cuadros, lo comparamos con la hora del reloj de nuestra computadora y usamos este valor para planificar la próxima actualización. Esto puede ser un poco confuso, así que lea el código cuidadosamente:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

Realizamos varias verificaciones: en primer lugar, nos aseguramos de que el retraso entre el PTS actual y el PTS anterior tenga sentido. Si no hay necesidad de un retraso, entonces el audio y el video simplemente coincidieron en este punto y solo usaron el último retraso. Luego nos aseguramos de que se cumpla el umbral de sincronización, porque la sincronización perfecta nunca ocurre. FFplay usa un valor de 0.01 para el umbral. También nos aseguramos de que el umbral de sincronización nunca sea menor que los intervalos entre los valores de PTS. Finalmente, establezca el valor mínimo de actualización en 10 milisegundos (de hecho, parece que deberían omitir el marco aquí, pero no nos preocupemos por eso).

Agregamos un montón de variables a la gran estructura, así que no olvides revisar el código. Además, no olvide inicializar el temporizador de trama y el retraso inicial de la trama anterior en stream_component_open :

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;

Sincronización: reloj de audio


Ha llegado el momento de darse cuenta del reloj de audio. Podemos actualizar el tiempo en nuestra función audio_decode_frame , donde decodificamos el audio. Ahora recuerde que no siempre procesamos un nuevo paquete cada vez que llamamos a esta función, por lo que hay dos áreas en las que necesita actualizar el reloj. El primer lugar es donde obtenemos el nuevo paquete: simplemente instale el reloj de sonido en el paquete PTS. Luego, si el paquete tiene varios cuadros, ahorramos el tiempo de reproducción de audio contando el número de muestras y multiplicándolas por una frecuencia de muestreo dada por segundo. Entonces, cuando tenemos el paquete:

    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }

Y tan pronto como procesemos el paquete:

      /* Keep audio_clock up-to-date */
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_st->codec->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_st->codec->sample_rate);

Algunos matices menores: la plantilla de función se ha cambiado y ahora incluye pts_ptr , así que asegúrese de cambiarla. pts_ptr es el puntero que usamos para decirle a audio_callback el paquete de audio pts . Esto se usará la próxima vez para sincronizar audio con video.

Ahora finalmente podemos implementar nuestra función get_audio_clock . No es tan simple como obtener el valor es -> audio_clock , si lo piensa. Tenga en cuenta que configuramos el audio PTS cada vez que lo procesamos, pero si observa la función audio_callback, llevará tiempo mover todos los datos de nuestro paquete de audio a nuestro búfer de salida. Esto significa que el valor en nuestro reloj de audio puede estar muy por delante. Por lo tanto, debemos verificar cuánto tenemos que escribir. Aquí está el código completo:

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_st->codec->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_st->codec->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

Ahora debe entender por qué funciona esta función;) ¡

Entonces, eso es todo! Compilamos:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

¡Ocurrió! Puede ver la película en un reproductor hecho a sí mismo. En la próxima lección, veremos la sincronización de audio y luego aprenderemos cómo buscar.

Guía FFmpeg y SDL o Cómo escribir un reproductor de video en menos de 1000 líneas - Parte 2



Traducciones en el blog de Edison:


All Articles