Руководство по FFmpeg libav. Печально известный hello world
Продолжение серии статей об основах работы с FFmpeg libav.
Сложный способ изучения FFmpeg libav
Поскольку FFmpeg настолько полезен как инструмент командной строки для выполнения важных задач с медиафайлами, как мы можем использовать его в наших программах?
FFmpeg состоит из нескольких библиотек, которые можно интегрировать в наши собственные программы. Обычно, когда вы устанавливаете FFmpeg, он автоматически устанавливает все эти библиотеки. Я буду называть набор этих библиотек FFmpeg libav.
Глава 0. Печально известный hello world
Этот hello world на самом деле не будет отображать сообщение «hello world» в терминале. Вместо этого мы собираемся распечатать информацию о видео, такую, как его формат (контейнер), продолжительность, разрешение, аудиоканалы и, наконец, мы декодируем некоторые кадры и сохраним их как файлы изображений.
Архитектура библиотеки 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 МБ: