Руководство по FFmpeg libav. Печально известный hello world

Добавлено 26 февраля 2022 в 23:24

Продолжение серии статей об основах работы с FFmpeg libav.

Сложный способ изучения FFmpeg libav

Поскольку FFmpeg настолько полезен как инструмент командной строки для выполнения важных задач с медиафайлами, как мы можем использовать его в наших программах?

FFmpeg состоит из нескольких библиотек, которые можно интегрировать в наши собственные программы. Обычно, когда вы устанавливаете FFmpeg, он автоматически устанавливает все эти библиотеки. Я буду называть набор этих библиотек FFmpeg libav.

Глава 0. Печально известный hello world

Этот hello world на самом деле не будет отображать сообщение «hello world» в терминале. Вместо этого мы собираемся распечатать информацию о видео, такую, как его формат (контейнер), продолжительность, разрешение, аудиоканалы и, наконец, мы декодируем некоторые кадры и сохраним их как файлы изображений.

Архитектура библиотеки FFmpeg libav

Но прежде чем мы начнем писать код, давайте узнаем, как работает архитектура FFmpeg libav, и как ее компоненты взаимодействуют с друг с другом.

Ниже показана схема процесса декодирования видео:

Архитектура ffmpeg libav, процесс декодирования

Сначала вам нужно загрузить медиафайл в компонент с названием AVFormatContext (контейнер видео также известен как формат). На самом деле он не загружает весь файл полностью: часто он читает только заголовок.

Как только мы загрузили минимальный заголовок нашего контейнера, мы можем получить доступ к его потокам (представьте их как элементарные аудио- и видеоданные). Каждый поток будет доступен в компоненте под названием AVStream.

Поток – это название для непрерывной последовательности данных.

Предположим, наше видео имеет два потока: аудио, закодированное с помощью кодека AAC, и видео, закодированное с помощью кодека H264 (AVC). Из каждого потока мы можем извлечь фрагменты (срезы) данных, называемые пакетами, которые будут загружены в компоненты с названием AVPacket.

Данные внутри пакетов по-прежнему закодированы (сжаты), и для декодирования этих пакетов нам необходимо передать их в определенный AVCodec.

AVCodec декодирует их в AVFrame, и, наконец, этот компонент дает нам несжатый кадр. Замечено, что одна и та же терминология/процесс используется как для аудио-, так и для видеопотоков.

Требования

Поскольку некоторые люди сталкивались с проблемами при компиляции или запуске примеров, мы собираемся использовать Docker в качестве нашей среды разработки/исполнения; мы также будем использовать видео с большим кроликом, поэтому, если у вас его нет локально, просто запустите команду make fetch_small_bunny_video.

Пошаговое руководство по коду главы 0

TLDR; покажите мне код и исполнение.

$ make run_hello

Мы пропустим некоторые детали, но не волнуйтесь: исходный код доступен на github.

/*
 * http://ffmpeg.org/doxygen/trunk/index.html
 *
 * Основные компоненты
 *
 * Формат (контейнер) - оболочка, обеспечивающая синхронизацию, метаданные
                      и мультиплексирование потоков.
 * Поток - непрерывный во времени поток (аудио или видео) данных.
 * Кодек — определяет способ кодирования (от Кадра к Пакету)
 *        и декодирования (от Пакета к Кадру) данных.
 * Пакет - это данные (своего рода фрагменты данных потока), которые
 *       должны быть декодированы как необработанные кадры.
 * Кадр - декодированный необработанный кадр (для кодирования или фильтрации).
 */

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

// распечатывает шаги и ошибки
static void logging(const char *fmt, ...);
// декодирует пакеты в кадры
static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame);
// сохраняет кадр в файл .pgm
static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename);

int main(int argc, const char *argv[])
{
  if (argc < 2) 
  {
    printf("You need to specify a media file.\n");
    return -1;
  }
  
  logging("initializing all the containers, codecs and protocols.");

  // AVFormatContext содержит информацию заголовка из формата (контейнера).
  // Выделение памяти для этого компонента
  // http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html
  AVFormatContext *pFormatContext = avformat_alloc_context();
  if (!pFormatContext) 
  {
    logging("ERROR could not allocate memory for Format Context");
    return -1;
  }

  logging("opening the input file (%s) and loading format (container) header", argv[1]);
  // Открыть файл и прочитать его заголовок. Кодеки не открываются.
  // Аргументы функции:
  // AVFormatContext (компонент, для которого мы выделили память),
  // URL (имя файла),
  // AVInputFormat (если передадите NULL, будет выполнено автоматическое определение)
  // AVDictionary (опции демультиплексора)
  // http://ffmpeg.org/doxygen/trunk/group__lavf__decoding.html
  if (avformat_open_input(&pFormatContext, argv[1], NULL, NULL) != 0) 
  {
    logging("ERROR could not open the file");
    return -1;
  }

  // теперь у нас есть доступ к некоторой информации о нашем файле
  // поскольку мы читаем его заголовок, мы можем сказать, какой это формат (контейнер)
  // и некоторую другую информацию, относящуюся к самому формату.
  logging("format %s, duration %lld us, bit_rate %lld", 
           pFormatContext->iformat->name, pFormatContext->duration, pFormatContext->bit_rate);

  logging("finding stream info from format");
  // прочитать пакеты из формата, чтобы получить информацию о потоке
  // эта функция заполняет pFormatContext->streams
  // (размер равен pFormatContext->nb_streams)
  // аргументы:
  // AVFormatContext
  // и опции, которые содержат опции для кодека, соответствующего i-му потоку.
  // При возврате каждый словарь будет заполнен опциями, которые не были найдены.
  // https://ffmpeg.org/doxygen/trunk/group__lavf__decoding.html
  if (avformat_find_stream_info(pFormatContext,  NULL) < 0) 
  {
    logging("ERROR could not get the stream info");
    return -1;
  }

  // компонент, который умеет кодировать и декодировать поток
  // это кодек (аудио или видео)
  // http://ffmpeg.org/doxygen/trunk/structAVCodec.html
  AVCodec *pCodec = NULL;
  // этот компонент описывает свойства кодека, используемого потоком i
  // https://ffmpeg.org/doxygen/trunk/structAVCodecParameters.html
  AVCodecParameters *pCodecParameters =  NULL;
  int video_stream_index = -1;

  // перебрать все потоки и вывести их основную информацию
  for (int i = 0; i < pFormatContext->nb_streams; i++)
  {
    AVCodecParameters *pLocalCodecParameters =  NULL;
    pLocalCodecParameters = pFormatContext->streams[i]->codecpar;
    logging("AVStream->time_base before open coded %d/%d", 
            pFormatContext->streams[i]->time_base.num,
            pFormatContext->streams[i]->time_base.den);
    logging("AVStream->r_frame_rate before open coded %d/%d", 
            pFormatContext->streams[i]->r_frame_rate.num, 
            pFormatContext->streams[i]->r_frame_rate.den);
    logging("AVStream->start_time %" PRId64, pFormatContext->streams[i]->start_time);
    logging("AVStream->duration %" PRId64, pFormatContext->streams[i]->duration);

    logging("finding the proper decoder (CODEC)");

    AVCodec *pLocalCodec = NULL;

    // ищет зарегистрированный декодер для ID кодека
    // https://ffmpeg.org/doxygen/trunk/group__lavc__decoding.html
    pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

    if (pLocalCodec == NULL) 
    {
      logging("ERROR unsupported codec!");
      // В этом примере, если кодек не найден, мы просто пропускаем его.
      continue;
    }

    // когда поток представляет собой видео, мы сохраняем его индекс,
    // параметры кодека и кодек
    if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) 
    {
      if (video_stream_index == -1) 
      {
        video_stream_index = i;
        pCodec = pLocalCodec;
        pCodecParameters = pLocalCodecParameters;
      }

      logging("Video Codec: resolution %d x %d",
              pLocalCodecParameters->width, pLocalCodecParameters->height);
    } 
    else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) 
    {
      logging("Audio Codec: %d channels, sample rate %d", 
              pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
    }

    // печатаем его имя, id и битрейт
    logging("\tCodec %s ID %d bit_rate %lld", 
            pLocalCodec->name, pLocalCodec->id, pLocalCodecParameters->bit_rate);
  }

  if (video_stream_index == -1) 
  {
    logging("File %s does not contain a video stream!", argv[1]);
    return -1;
  }

  // https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html
  AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
  if (!pCodecContext)
  {
    logging("failed to allocated memory for AVCodecContext");
    return -1;
  }

  // Заполнить контекст кодека на основе значений из предоставленных параметров кодека.
  // https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
  if (avcodec_parameters_to_context(pCodecContext, pCodecParameters) < 0)
  {
    logging("failed to copy codec params to codec context");
    return -1;
  }

  // Инициализировать AVCodecContext для использования данного AVCodec.
  // https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
  if (avcodec_open2(pCodecContext, pCodec, NULL) < 0)
  {
    logging("failed to open codec through avcodec_open2");
    return -1;
  }

  // https://ffmpeg.org/doxygen/trunk/structAVFrame.html
  AVFrame *pFrame = av_frame_alloc();
  if (!pFrame)
  {
    logging("failed to allocated memory for AVFrame");
    return -1;
  }
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html
  AVPacket *pPacket = av_packet_alloc();
  if (!pPacket)
  {
    logging("failed to allocated memory for AVPacket");
    return -1;
  }

  int response = 0;
  int how_many_packets_to_process = 8;

  // заполнить Пакет данными из Потока
  // https://ffmpeg.org/doxygen/trunk/group__lavf__decoding.html
  while (av_read_frame(pFormatContext, pPacket) >= 0)
  {
    // если это поток видео
    if (pPacket->stream_index == video_stream_index) 
    {
      logging("AVPacket->pts %" PRId64, pPacket->pts);
      response = decode_packet(pPacket, pCodecContext, pFrame);
      if (response < 0)
        break;
      // остановиться, иначе мы будем сохранять сотни кадров
      if (--how_many_packets_to_process <= 0) 
        break;
    }
    // https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html
    av_packet_unref(pPacket);
  }

  logging("releasing all the resources");

  avformat_close_input(&pFormatContext);
  av_packet_free(&pPacket);
  av_frame_free(&pFrame);
  avcodec_free_context(&pCodecContext);
  return 0;
}

static void logging(const char *fmt, ...)
{
  va_list args;
  fprintf( stderr, "LOG: " );
  va_start( args, fmt );
  vfprintf( stderr, fmt, args );
  va_end( args );
  fprintf( stderr, "\n" );
}

static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame)
{
  // Предоставление необработанных данных пакета в качестве входных данных для декодера
  // https://ffmpeg.org/doxygen/trunk/group__lavc__decoding.html
  int response = avcodec_send_packet(pCodecContext, pPacket);

  if (response < 0) 
  {
    logging("Error while sending a packet to the decoder: %s", 
            av_err2str(response));
    return response;
  }

  while (response >= 0)
  {
    // Вернуть декодированные выходные данные (в кадр) из декодера
    // https://ffmpeg.org/doxygen/trunk/group__lavc__decoding.html
    response = avcodec_receive_frame(pCodecContext, pFrame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) 
    {
      break;
    } 
    else if (response < 0) 
    {
      logging("Error while receiving a frame from the decoder: %s", 
              av_err2str(response));
      return response;
    }

    if (response >= 0) 
    {
      logging(
          "Frame %d (type=%c, size=%d bytes, format=%d) pts %d key_frame %d [DTS %d]",
          pCodecContext->frame_number,
          av_get_picture_type_char(pFrame->pict_type),
          pFrame->pkt_size,
          pFrame->format,
          pFrame->pts,
          pFrame->key_frame,
          pFrame->coded_picture_number
      );

      char frame_filename[1024];
      snprintf(frame_filename, sizeof(frame_filename),
               "%s-%d.pgm", "frame", pCodecContext->frame_number);
      // Проверить, является ли кадр YUV 4:2:0, 12bpp
      // Это формат предоставленного файла .mp4
      // Форматы RGB точно не дадут серого изображения
      // Другое YUV-изображение может делать то же самое,
      // но не протестировано, поэтому выдать предупреждение
      if (pFrame->format != AV_PIX_FMT_YUV420P)
      {
        logging("Warning: the generated file may not be a grayscale image, "
                "but could e.g. be just the R component if the video format is RGB");
      }
      // сохранить кадр в градациях серого в файл .pgm
      save_gray_frame(pFrame->data[0], pFrame->linesize[0],
                      pFrame->width, pFrame->height, frame_filename);
    }
  }
  return 0;
}

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
  FILE *f;
  int i;
  f = fopen(filename,"w");
  // запись минимально необходимого заголовка для формата файла pgm
  // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
  fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

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

Мы собираемся выделить память компоненту AVFormatContext, который будет хранить информацию о формате (контейнере).

AVFormatContext *pFormatContext = avformat_alloc_context();

Теперь мы собираемся открыть файл, прочитать его заголовок и заполнить AVFormatContext минимально необходимой информацией о формате (обратите внимание, что обычно кодеки не открываются). Для этого используется функция avformat_open_input. Она ожидает AVFormatContext, имя файла и два необязательных аргумента: AVInputFormat (если вы передадите NULL, FFmpeg угадает формат) и AVDictionary (который является опциями демультиплексора).

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

Мы можем напечатать имя формата и продолжительность медиаконтента:

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

Чтобы получить доступ к потокам, нам нужно прочитать данные из медиаконтента. Это делает функция avformat_find_stream_info. Теперь pFormatContext->nb_streams будет содержать количество потоков, а pFormatContext->streams[i] даст нам i-ый поток (AVStream).

avformat_find_stream_info(pFormatContext,  NULL);

Теперь пройдёмся по всем потокам.

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

Для каждого потока мы собираемся сохранить AVCodecParameters, который описывает свойства кодека, используемого потоком i.

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

С помощью свойств кодека мы можем найти правильный кодек, запросив функцию avcodec_find_decoder, найти зарегистрированный декодер для идентификатора кодека и вернуть AVCodec, компонент, который знает, как кодировать и декодировать поток.

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

Теперь мы можем распечатать информацию о кодеках.

// конкретно для видео и аудио
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) 
{
  printf("Video Codec: resolution %d x %d", 
         pLocalCodecParameters->width, pLocalCodecParameters->height);
} 
else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) 
{
  printf("Audio Codec: %d channels, sample rate %d", 
         pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}

// общее
printf("\tCodec %s ID %d bit_rate %lld", 
       pLocalCodec->long_name, pLocalCodec->id, pLocalCodecParameters->bit_rate);

С помощью кодека мы можем выделить память для AVCodecContext, который будет содержать контекст для нашего процесса декодирования/кодирования. Затем нам нужно заполнить этот контекст кодека параметрами кодека; мы делаем это с помощью avcodec_parameters_to_context.

После того, как мы заполнили контекст кодека, нам нужно открыть кодек. Мы вызываем функцию avcodec_open2, после чего можем использовать кодек.

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

Теперь мы будем читать пакеты из потока и декодировать их в кадры, но сначала нам нужно выделить память для обоих компонентов, AVPacket и AVFrame.

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

Давайте заполнять наши пакеты из потоков функцией av_read_frame, пока у нее есть пакеты.

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

Давайте отправим пакет необработанных данных (сжатый кадр) в декодер через контекст кодека, используя функцию avcodec_send_packet.

avcodec_send_packet(pCodecContext, pPacket);

И давайте получим кадр необработанных данных (несжатый кадр) из декодера через тот же контекст кодека, используя функцию avcodec_receive_frame.

avcodec_receive_frame(pCodecContext, pFrame);

Мы можем напечатать номер кадра, PTS, DTS, тип кадра и т. д.

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

Наконец, мы можем сохранить наш декодированный кадр в виде простого серого изображения. Этот процесс очень простой, мы будем использовать pFrame->data, где индекс связан с пространствами Y, Cb и Cr, и мы просто выбираем 0 (Y), чтобы сохранить наше серое изображение.

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

static void save_gray_frame(unsigned char *buf, int wrap, 
                            int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // запись минимально необходимого заголовка для формата файла pgm
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // запись построчно
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

И вуаля! Теперь у нас есть изображение в градациях серого размером 2 МБ:

сохраненный кадр

Теги

FFmpeglibavДекодирование видеоОбработка видеоПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.