Pegando frames de vídeos no Linux

Há algum tempo eu fiz uma pequena experiência, no Windows, usando a API vfw (video for windows) para ler um arquivo AVI, extrair os frames e usá-los como textura, numa aplicaçãozinha usando OpenGL… Nos últimos anos tenho buscado maneiras de fazer coisas que fazia no ambiente da Microsoft de maneira mais abrangente, isto é, desenvolver código que funcione nas três grandes plataformas: Windows, Linux e Mac OS/X. É suficiente dizer que, se você pretende fazer o mesmo, comece a desenvolver para ambiente Linux!

Acontece que o ambiente Mac OS/X também é derivado do Unix e no Windows temos ferramentas que possibilitam desenvolver, com poucas mudanças, quase da mesma maneira que nos outros dois ambientes. Esqueça o Visual Studio, use o MinGW32 (baixe aqui) ou o Cygwin (baixe aqui).

Para ler frames de um vídeo — qualquer um, não só AVIs — você pode usar o conjunto de bibliotecas libavformat, libavcodec, libavutil e libswscale, como mostrarei abaixo… O código exemplo que você verá aqui foi feito em 2011… Outros tutoriais que você pode achar usam rotinas obsoletas das mesmas libs… Algumas sequer existem mais… Portanto, se no futuro esse exemplo não puder mais ser compilado, procure a documentação das bibliotecas na Internet, ok?

O processo de extração de frames é o seguinte (em pseudo-código):

Inicializar as bibliotecas;
Criar o contexto do formato, abrindo o arquivo de vídeo;
Obter informações dos streams (opcional);
Procurar o stream de vídeo;
Obter o contexto do decodificador do stream selecionado;
ler um "pacote";
enquanto existirem "pacotes";
  se o pacote veio do stream escolhido; então
    decodifica o pacote para um "frame";
    converte o formato do frame para RGB;
    usa a imagem contida no frame; 
  fim-se
  ler o próximo pacote
fim-enquanto;

O código parecerá um pouco mais complicado do que isso, mas o pseudo-código mostra, em essência, o que deve ser feito. Ei-lo:

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

#include <stdio.h>
#include <stdint.h>

/* Protótipo da função que grava um frame */
static void SaveFrame(AVFrame *pFrame, 
                      int width, int height, 
                      int iFrame);

int main (int argc, const char **argv)
{
  AVFormatContext *pFormatCtx;
  AVCodecContext  *pCodecCtx;
  AVCodec         *pCodec;
  AVFrame         *pFrame;
  AVFrame         *pFrameRGB;
  AVPacket        packet;

  int             i, videoStream;
  int             frameFinished;
  int             numBytes;
  uint8_t         *buffer;

  if (argc != 2)
  {
    fprintf(stderr, 
            "Usage: avcodec_example <video-filename>\n");
    return 1;
  }

  // Register all formats and codecs
  av_register_all();

  // Open video file
  if (av_open_input_file(
        &pFormatCtx, argv[1], NULL, 0, NULL) != 0)
  {
    fprintf(stderr, "Could not open file\n");
    return 1;
  }

  // Retrieve stream information
  if (av_find_stream_info(pFormatCtx) < 0)
  {
    fprintf(stderr, 
            "Cound not find stream information\n");
    return 1;
  }

  // Opcional. Só mostra infos do vídeo (estilo ffmpeg).
  dump_format(pFormatCtx, 0, argv[1], false);

  // Encontra o stream que queremos (o de vídeo)
  videoStream = -1;
  for (i = 0; i < pFormatCtx->nb_streams; i++)
    if (pFormatCtx->streams[i]->codec->codec_type == 
          CODEC_TYPE_VIDEO)
    {
      videoStream = i;
      break;
    }

  if (videoStream == -1)
  {
    fprintf(stderr, "Didn't find a video stream\n");
    return 1;
  }

  // Pega o contexto do codec para o stream selecinado.
  pCodecCtx = pFormatCtx->streams[videoStream]->codec;

  // Tenta encontrar o decoder para o stream.
  pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
  if (pCodec == NULL)
  {
    fprintf(stderr, "Codec not found\n");
    return 1;
  }

  // Abre o codec...
  if (avcodec_open(pCodecCtx, pCodec) < 0)
  {
    fprintf(stderr, "Cound not open codec\n");
    return 1;
  }

  // Aloca espaço para as estruturas AVFrame:
  pFrame = avcodec_alloc_frame();
  pFrameRGB = avcodec_alloc_frame();
  if ((pFrame == NULL) || (pFrameRGB == NULL))
  {
    fprintf(stderr, 
            "Could not allocate frame structures\n");
    return 1;
  }

  // pFrameRGB aponta para o frame que usaremos para obter
  // a conversão do frame original. Daí, precisamos alocar
  // espaço para o frame nós mesmos...
  numBytes = avpicture_get_size(
                PIX_FMT_RGB24, 
                pCodecCtx->width, 
                pCodecCtx->height);

  buffer = malloc(numBytes);

  // Assinala o buffer à pFrameRGB.
  avpicture_fill((AVPicture *)pFrameRGB, 
                 buffer, 
                 PIX_FMT_RGB24, 
                 pCodecCtx->width, 
                 pCodecCtx->height);

  // Loop de leitura de pacotes...
  i=0;
  while (av_read_frame(pFormatCtx, &packet) >= 0)
  {
    // O pacote lido é do stream que queremos?
    if (packet.stream_index == videoStream)
    {
      // Decodifica um frame.
      avcodec_decode_video2(
        pCodecCtx, 
        pFrame, 
        &frameFinished, 
        &packet);

      // Pegamos um frame?
      if (frameFinished)
      {
        static struct SwsContext *img_convert_ctx = NULL;

#if 0
        // Você pode achar este código em algum tutorial.
        // ELE NÂO FUNCIONA!
        img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,
            (AVPicture*)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width,
            pCodecCtx->height);
#endif

        // Converte o frame lido para RGB.
        if (img_convert_ctx == NULL)
        {
          img_convert_ctx = sws_getContext(
            pCodecCtx->width, pCodecCtx->height,
            pCodecCtx->pix_fmt,
            pCodecCtx->width, pCodecCtx->height,
            PIX_FMT_RGB24,
            SWS_BICUBIC,
            NULL, NULL, NULL);

          if (img_convert_ctx == NULL)
          {
            fprintf(stderr, 
              "Cannot initialize the conversion context!\n");
            return 1;
          }
        }

        sws_scale(img_convert_ctx,
                  (const uint8_t * const *)pFrame->data,
                  pFrame->linesize,
                  0, pCodecCtx->height,
                  pFrameRGB->data,
                  pFrameRGB->linesize);

        // Grava o frmae e incrementa contador de frames...
        SaveFrame(
          pFrameRGB, 
          pCodecCtx->width, 
          pCodecCtx->height, 
          i++);
      }
    }

    // Joga o pacote fora, não precisamos mais dele.
    av_free_packet(&packet);
  }

  // Joga fora as estruturas e buffer que não precisamos mais.
  free(buffer);
  av_free(pFrameRGB);
  av_free(pFrame);

  // Livra-se dos contextos do codec e formato,
  // fechando o arquivo de vídeo.
  avcodec_close(pCodecCtx);
  av_close_input_file(pFormatCtx);

  return 0;
}

// Essa é a mesma função que mostrei no post anterior, mas com
// parâmetros diferentes! 
void SaveFrame(AVFrame *pFrame, 
               int width, int height, 
               int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int  y;

  // Formata nome do arquivo de imagem e cria o arquivo.
  snprintf(szFilename, 32, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if (pFile == NULL)
    return;

  // Escreve header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

  // Escreve dados.
  for (y = 0; y < height; y++)
    fwrite(pFrame->data[0]+y * pFrame->linesize[0], 
           1, width * 3, pFile);

  fclose(pFile);
}

É claro que neste exemplo deixei de fora a temporização dos frames (todo frame precisa ficar “exposto” por uma quantidade de tempo, para dar a ilusão de continuidade!) e também não mostrei como integrar isso ai com OpenGL. Também não mostrei como extrair áudio (porque é mais complicado e porque apenas o stream de vídeo me interessa agora).

Para compilar o cíodigo acima, use o seguinte makefile:

CFLAGS=-march=native -O3 
LDFLAGS=-lavformat -lavcodec -lavutil -lswscale -lz

avcodec_example: avcodec_example.o

avcodec_example.o: avcodec_example.c

É isso…

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s