Pull to refresh

Видеоплеер на базе ffmpeg

Reading time 8 min
Views 63K
Привет, Хабр!

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


Введение


С помощью FFmpeg можно выполнять большое количество задач по обработке видео: кодирование и декодирование, мультиплексирование и демультиплексирование. Это значительно облегчает разработку мультимедиа приложений.

Одна из основных проблем, как и у большинства open source проектов, это документация. Ее очень мало, а та что есть не всегда актуальна, т.к. это быстро развивающийся проект с постоянно меняющимися API. Поэтому основным источником документации является исходный код самой библиотеки. Из старых статей советую почитать [1] и [2]. Они дают представление о работе с библиотеками в целом.

FFmpeg представляет собой набор утилит и библиотек для работы с различными медиаформатами. Про утилиты, наверное, смысла рассказывать нет — про них все слышали, а вот на библиотеках можно остановиться поподробнее.
  • libavutil — содержит набор вспомогательных функций, которые включают в себя генераторы случайных чисел, структуры данных, математические процедуры, основные мультимедиа утилиты и многое другое;
  • libavcodec — содержит энкодеры и декодеры для аудио/видео кодеков (быстро произнесите это словосочетания десять раз подряд);
  • libavformat — содержит мультиплексоры и демультиплексоры контейнеров мультимедиа;
  • libavdevice — содержит устройства ввода и вывода для захвата и рендеринга из распространенных мультимедиа фреймворков (Video4Linux, Video4Linux2, VfW, ALSA);
  • libavfilter — содержит набор фильтров для преобразования;
  • libswscale — содержит хорошо оптимизированные функции для выполнения масштабирования изображений, преобразования цветовых пространств и форматов пикселов;
  • libswresample — содержит хорошо оптимизированные функции для выполнения передискретизации аудио и преобразования форматов сэмплов.

Для вывода видео на экран будем использовать SDL. Это удобный и кроссплатформенный фреймворк с довольно простым API.

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

Код


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

#include <SDL.h>

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

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

Т.к. avformat_open_input читает только заголовок файла, то следующим шагом нужно получить информацию о потоках в файле. Это делается функцией avformat_find_stream_info.
	// Retrieve stream information
	err = avformat_find_stream_info(format_context, NULL);
	if (err < 0) {
		fprintf(stderr, "ffmpeg: Unable to find stream info\n");
		return -1;
	}

После этого format_context->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 < format_context->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 stream\n");
		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 < 0) {
		fprintf(stderr, "ffmpeg: Unable to open codec\n");
		return -1;
	}

Самое время подготовить окно для вывода видео с помощью SDL (мы знаем размеры видео). В общем случае мы можем создавать окно любого размера, а затем масштабировать видео, используя libswscale. Но для простоты давайте сделаем окно размером с видео.
Кроме самого окна, необходимо также добавить оверлей (overlay), в который наше видео будет выводиться. SDL поддерживает большое количество методов для рисования изображений на экране и один специально предназначенный для отображения видео — он называется YUV оверлей. YUV это цветовое пространство, как и RGB. Y — представляет компонент яркости (luma), а U и V — компоненты цветности. Этот формат более сложный чем RGB т.к. часть цветовой информации отбрасывается и может быть только один U и V сэмпл на каждые 2 Y сэмпла. Оверлей YUV принимает массив YUV данных и отображает их. Он поддерживает 4 различных формата, но самый быстрый из них это YV12. Есть еще другой формат YUV — YUV420P. Он такой же как и YV12 за исключением того что U и V массивы поменяны местами. FFmpeg умеет преобразовывать изображения в YUV420P, а также большинство видео потоков уже содержаться в этом формате или достаточно просто в него преобразуются.
Таким образом мы будем использовать YV12 оверлей из SDL, видео в FFmpeg преобразовывать в формат YUV420P, а при отображении менять местами порядок массивов U и V.
	SDL_Surface* screen = SDL_SetVideoMode(codec_context->width, codec_context->height, 0, 0);
	if (screen == NULL) {
		fprintf(stderr, "Couldn't set video mode\n");
		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 context\n");
		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`

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

Исходный код целиком.

Всем спасибо за внимание!

Продолжение: Доработка видеоплеера ffmpeg

Ссылки


  1. An ffmpeg and SDL Tutorial (en)
  2. Using libavformat and libavcodec (en)
Tags:
Hubs:
+35
Comments 19
Comments Comments 19

Articles