Работа с видео / Видеоплеер на базе ffmpeg

в 23:00, , рубрики: ffmpeg, sdl, метки: , ,

В этой статье речь пойдет о разработке простейшего плеера с использованием библиотек из проекта FFmpeg.
Я не нашел на хабре статей на данную тематику, поэтому решил восполнить этот пробел.
Декодирование видео будет осуществляться с помощью библиотек FFmpeg, отображение — с помощью SDL.
Введение

С помощью FFmpeg можно выполнять большое количество задач по обработке видео: кодирование и декодирование, мультиплексирование и демультиплексирование. Это значительно облегчает разработку мультимедиа приложений.
Одна из основных проблем, как и у большинства open source проектов, это документация. Ее очень мало, а та что есть не всегда актуальна, т.к. это быстро развивающийся проект с постоянно меняющимися API. Поэтому основным источником документации является исходный код самой библиотеки.

FFmpeg представляет собой набор утилит и библиотек для работы с различными медиаформатами. Про утилиты, наверное, смысла рассказывать нет — про них все слышали, а вот на библиотеках можно остановиться поподробнее.
libavutil — содержит набор вспомогательных функций, которые включают в себя генераторы случайных чисел, структуры данных, математические процедуры, основные мультимедиа утилиты и многое другое;

libavcodec — содержит энкодеры и декодеры для аудио/видео кодеков (быстро произнесите это словосочетания десять раз подряд);

libavformat — содержит мультиплексоры и демультиплексоры контейнеров мультимедиа;

libavdevice — содержит устройства ввода и вывода для захвата и рендеринга из распространенных мультимедиа фреймворков (Video4Linux, Video4Linux2, VfW, ALSA);

libavfilter — содержит набор фильтров для преобразования;

libswscale — содержит хорошо оптимизированные функции для выполнения масштабирования изображений, преобразования цветовых пространств и форматов пикселов;

libswresample — содержит хорошо оптимизированные функции для выполнения передискретизации аудио и преобразования форматов сэмплов.

Для вывода видео на экран будем использовать SDL. Это удобный и кроссплатформенный фреймворк с довольно простым API.
Опытный читатель может заметить, что подобный плеер уже существует прямо в дистрибутиве FFmpeg, его код доступен в файле ffplay.c, и он тоже использует SDL! Но его код довольно сложен для понимания начинающим разработчикам FFmpeg и содержит много дополнительной функциональности.
Также подобный плеер описывается в [1], но там используются функции, которых уже нет в FFmpeg или они объявлены устаревшими.
Я же постараюсь привести пример минималистичного и понятного плеера с использованием актуального API. Для простоты мы будем отображать только видео, без звука.
Итак, начнем.
Код

Первым делом, подключаем необходимые заголовочные файлы:
#include

#include

#include
#include
#include

В этом небольшом примере весь код будет в main.
Сначала инициализируем библиотеку ffmpeg с помощью av_register_all(). Во время инициализации регистрируются все имеющиеся в библиотеке форматы файлов и кодеков. После этого они будут использоваться автоматически при открытии файлов этого формата и с этими кодеками.
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s filenamen", argv[0]);
return 0;
}

// Register all available file formats and codecs
av_register_all();

Теперь инициализируем SDL. В качестве аргумента функция SDL_Init принимает набор подсистем, которые следует инициализировать (для инициализации нескольких подсистем используется логическое ИЛИ). В этом примере нам достаточно только подсистемы видео.

int err;
// Init SDL with video support
err = SDL_Init(SDL_INIT_VIDEO);
if (err < 0) {
fprintf(stderr, "Unable to init SDL: %sn", SDL_GetError());
return -1;
}

Теперь мы откроем входной файл. Имя файла передается первым аргументом в командной строке.
Функция avformat_open_input читает файловый заголовок и сохраняет информацию о найденных форматах в структуре AVFormatContext. Остальные аргументы могут быть установлены в NULL, в этом случае libavformat использует автоматическое определение параметров.
// Open video file
const char* filename = argv[1];
AVFormatContext* format_context = NULL;
err = avformat_open_input(&format_context, filename, NULL, NULL);
if (err < 0) {
fprintf(stderr, "ffmpeg: Unable to open input filen");
return -1;
}

Т.к. avformat_open_input читает только заголовок файла, то следующим шагом нужно получить информацию о потоках в файле. Это делается функцией avformat_find_stream_info.
// Retrieve stream information
err = avformat_find_stream_info(format_context, NULL);
if (err streams содержит все существующие потоки файла. Их количество равно format_context->nb_streams.
Вывести подробную информацию о файле и обо всех потоках можно функцией av_dump_format.

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

Теперь получим номер видео-потока в format_context->streams. По этому номеру мы сможем получить контекст кодека (AVCodecContext), и потом он будет использоваться для определения типа пакета при чтении файла.
// Find the first video stream
int video_stream;
for (video_stream = 0; video_stream nb_streams; ++video_stream) {
if (format_context->streams[video_stream]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
break;
}
}
if (video_stream == format_context->nb_streams) {
fprintf(stderr, "ffmpeg: Unable to find video streamn");
return -1;
}

Информация о кодеке в потоке называется «контекстом кодека» (AVCodecContext). Используя эту информацию, мы можем найти необходимый кодек (AVCodec) и открыть его.
AVCodecContext* codec_context = format_context->streams[video_stream]->codec;
AVCodec* codec = avcodec_find_decoder(codec_context->codec_id);
err = avcodec_open2(codec_context, codec, NULL);
if (err width, codec_context->height, 0, 0);
if (screen == NULL) {
fprintf(stderr, "Couldn't set video moden");
return -1;
}

SDL_Overlay* bmp = SDL_CreateYUVOverlay(codec_context->width, codec_context->height,
SDL_YV12_OVERLAY, screen);

Преобразование форматов пикселов, также как и масштабирование в FFmpeg выполняется с помощью libswscale.
Преобразование выполняется в два этапа. На первом этапе создается контекст преобразования (struct SwsContext). Раньше для этого использовалась функция с понятным названием sws_getContext. Но сейчас она объявлена устаревшей, и создание контекста рекомендуют делать с помощью sws_getCachedContext. Ей и воспользуемся.
struct SwsContext* img_convert_context;
img_convert_context = sws_getCachedContext(NULL,
codec_context->width, codec_context->height,
codec_context->pix_fmt,
codec_context->width, codec_context->height,
PIX_FMT_YUV420P, SWS_BICUBIC,
NULL, NULL, NULL);
if (img_convert_context == NULL) {
fprintf(stderr, "Cannot initialize the conversion contextn");
return -1;
}

Ну вот мы и подошли к самой интересной части, а именно отображению видео.
Данные из файла читаются пакетами (AVPacket), а для отображения используется фрейм (AVFrame).
Нас интересуют только пакеты, относящиеся к видео потоку (помните мы сохранили номер видео потока в переменной video_stream).
Функция avcodec_decode_video2 осуществляет декодирование пакета в фрейм с использованием кодека, который мы получили раньше (codec_context). Функция устанавливает положительное значение frame_finished в случае если фрейм декодирован целиком (то есть один фрейм может занимать несколько пакетов и frame_finished будет установлен только при декодировании последнего пакета).
AVFrame* frame = avcodec_alloc_frame();
AVPacket packet;
while (av_read_frame(format_context, &packet) >= 0) {
if (packet.stream_index == video_stream) {
// Video stream packet
int frame_finished;
avcodec_decode_video2(codec_context, frame, &frame_finished, &packet);

if (frame_finished) {

Теперь нужно подготовить картинку к отображению в окне. Первым делом блокируем наш оверлей, так как мы будем записывать в него данные. Видео в файле может находиться в любом формате, а отображение мы настроили для YV12. На помощь приходит libswscale. Ранее мы настраивали контекст преобразования img_convert_context. Пришло время его применить. Основной метод libswscale это конечно же sws_scale. Он и выполняет требуемое преобразование. Обратите внимание на несоответствие индексов при присвоении массивов. Это не опечатка. Как упоминалось ранее, YUV420P отличается от YV12 только тем что цветовые компоненты находятся в другом порядке. libswscale мы настроили на преобразование в YUV420P, а SDL от нас ждет YV12. Вот здесь мы и сделаем подмену U и V чтобы все было корректно.
SDL_LockYUVOverlay(bmp);

AVPicture pict;
pict.data[0] = bmp->pixels[0];
pict.data[1] = bmp->pixels[2]; // it's because YV12
pict.data[2] = bmp->pixels[1];

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

sws_scale(img_convert_context,
frame->data, frame->linesize,
0, codec_context->height,
pict.data, pict.linesize);
SDL_UnlockYUVOverlay(bmp);

Выводим изображение из оверлея в окно.

SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = codec_context->width;
rect.h = codec_context->height;
SDL_DisplayYUVOverlay(bmp, &rect);

После обработки пакета необходимо освободить память, которую он занимает. Делается это функцией av_free_packet.
}
}

// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);

Чтобы ОС не посчитала наше приложение зависшим, а также для завершения приложения при закрытии окна обрабатываем по одному событию SDL в конце цикла.

// Handling SDL events there
SDL_Event event;
if (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
break;
}
}
}

Ну теперь стандартная процедура очистки всех использованных ресурсов.

sws_freeContext(img_convert_context);

// Free the YUV frame
av_free(frame);

// Close the codec
avcodec_close(codec_context);

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

// Quit SDL
SDL_Quit();
return 0;
}

Переходим к сборке. Самый простой вариант с использованием gcc выглядит примерно так:
gcc player.c -o player -lavutil -lavformat -lavcodec -lswscale -lz -lbz2 `sdl-config --cflags --libs`

Запускаем. И что же мы видим? Видео воспроизводится с огромной скоростью! Если быть точным, то воспроизведение происходит со скоростью чтения и декодирования фреймов из файла. Действительно. Мы же не написали ни строчки кода для управления скоростью смены кадров. А это тема уже для другой статьи. В этом коде много чего можно улучшить. Например, добавить воспроизведение звука, или вынести чтение и отображение файла в другие потоки. Если Хабрасообществу будет интересно, расскажу об этом в следующих статьях.Исходный код целиком.Всем спасибо за внимание!

  1. Pavel:

    сделал, как ты описал, только для android-a, звук нормальный, а вот видео не синхронно воспроизводится(заметно медленнее, h264 потоковое), может подскажешь, куда копать?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js