My experiments with ffmpeg.
Как пишут на официальном сайте:
FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.
И это утверждение, похоже, соответствует действительности. FFmpeg используют такие проекты, как MPlayer, VLC, YouTube, Chrome и др.
Несколько лет назад (в 2011 году) внутри проекта случились какие-то разногласия между разработчиками, появился форк libav, однако на деле ffmpeg "пилится" активнее (подробности тут и тут).
- ffmpeg -- конвертирует видео из одного формата в другой
- ffplay -- простой плеер на SDL и библиотеках ffmpeg
- ffserver -- потоковый сервер для видео- или радиовещания
- ffprobe -- простой анализатор мультимедиа
- libavutil -- вспомогательная библиотека общих для ffmpeg функций
- libavcodec -- библиотека со всеми аудио/видеокодеками
- libavformat -- библиотка ввода/вывода и мультиплексирования/демультиплексирования
- libavdevice -- библиотека устройств ввода/вывода для захвата и рендеринга мультимедиа
- libavfilter -- позволяет изменять видеопоток между декодером и кодером "на лету"
- libswscale -- библиотека для масштабирования видео
- libswresample -- библиотека передискретизации аудио (ресамплинга)
Видеофайл штука довольно сложная. Во-первых, файл сам по себе является контейнером (container), и тип контейнера определяет, где находится информация внутри файла. В качестве примера контейнеров можно привести форматы AVI и QuickTime. Внутри файла может быть несколько потоков (streams). Например, обычно в фильме есть аудиопоток и видеопоток. "Поток" означает непрерывное поступление данных, доступных по мере течения времени. Процесс "разделения файла" на потоки называется демультиплексированием (demuxing). Элементы данных в потоке называют кадрами (frames). Каждый поток закодирован (сжат) каким-либо кодеком (codec). В качестве примера кодека можно привести DivX и MP3. Пакет (packet) содержит одни или несколько раскодированных кадров.
Очень грубо обработку видео и аудиопотоков можно описать так:
10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20
В этом туториале мы собираемся открыть файл, прочитать видеопоток внутри него и сохранить первый кадр в PPM файл.
Для начала установим необходимые зависимости $ sudo apt-get install libavutil-dev libavcodec-deb libavformat-dev
А для экспериментов скачаем короткиий анимационный фильм Big Buck Bunny (210M).
Во-первых, инициализируем библиотеку.
#include <libavformat/avformat.h>
int main(int argc, char *argv[]) {
// Register all file formats and codecs
av_register_all();
return 0;
}
av_register_all() регистрирует все доступные форматы файлов и кодеков для автоматического использования в случае открытия соответствующего файла. Функцию нужно вызвать только раз, поэтому мы делаем это в main(). Возможно зарегистрировать форматы файлов и кодеков индивидуально, но в большинстве случаев в этом нет необходимости.
Теперь мы можем открыть файл.
// Open video file
AVFormatContext *pFormatCtx = NULL;
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
return -1; // Couldn't open file
}
Функция читает заголовок файла и сохраняет информацию в структуре AVFileContext.
Читаем информацию о потоках
// Retrieve stream information
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
return -1; // Couldn't find stream information
}
Функция заполняет FormatCtx->streams необходимой информацией. Выведем ее с помощью удобной функции
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, argv[1], 0);
gcc -o tutorial01 tutorial01.c -lavformat
$ ./tutorial01 big_buck_bunny_480p_surround-fix.avi
Input #0, avi, from 'big_buck_bunny_480p_surround-fix.avi':
Duration: 00:09:56.45, start: 0.000000, bitrate: 2957 kb/s
Stream #0.0: Video: mpeg4 (Simple Profile), yuv420p, 854x480 [PAR 1:1 DAR 427:240], 24 tbn, 24 tbc
Stream #0.1: Audio: ac3, 48000 Hz, 5.1, fltp, 448 kb/s
Пройдемся по массиву указателей pFormatCtx->streams
(размер массива
pFormatCtx->nb_streams
), пока не найдем видеопоток.
AVCodecContext *pCodecCtx = NULL;
int i, videoStream;
// Find the first video stream
videoStream=-1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
return -1; // Didn't find a video stream
}
// Get a pointer to the codec context for the video stream
pCodecCtx = pFormatCtx->streams[videoStream]->codec;
Обратите внимание, что при сборке необходимо слинковаться с libavcodec
AVCodec *pCodec = NULL;
AVDictionary *optionsDict = NULL;
// Find the decoder for the video stream
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if (avcodec_open2(pCodecCtx, pCodec, &optionsDict) < 0) {
return -1; // Could not open codec
}
Теперь нам необходимо место для хранения кадра
AVFrame *pFrame = NULL;
// Allocate video frame
pFrame = avcodec_alloc_frame();
Поскольку мы планируем сохранить кадр в PPM файл, который хранит 25-битный RGB, нам нужно сконвертировать кадр из его родного формата в RGB. ffmpeg сделает это преобразование за нас. Выделим место под преобразованный кадр.
// Allocate an AVFrame structure
AVFrame *pFrameRGB = NULL;
pFrameRGB = avcodec_alloc_frame();
if (pFrameRGB == NULL) {
return -1;
}
Несмотря на то, что мы выделили место для фрейма, нам все еще необходимо место
для хранения данных при конвертации. Мы используем avpicture_get_size()
для
выяснения необходимого размера, и выделим место вручную.
uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer = (uint8_t *) av_malloc(numBytes*sizeof(uint8_t));
av_malloc()
-- это обёртка ffmpeg вокруг malloc, которая проверяет
выравнивание и всё такое. Она не защищает от утечек, двойного освобождения и
других проблем.
Для использования av_malloc
приложение необходимо слинковать с libavutil
Теперь используем avpicture_fill()
для связывания кадра с нашим буфером.
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB. buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
Теперь мы готовы прочитать поток.
Теперь мы собираемся прочитать пакет из потока, декодировать его в наш кадр, после чего сконвертировать и сохранить этот кадр.
Структура SwsContext и функции sws_scale и sws_getContext определены в
libswscale. Не забудьте подключить соответствующий заголовочный файл #include <libswscale/swscale.h>
и слинковать с libswscale
.
int frameFinished;
AVPacket packet;
struct SwsContext *sws_ctx = NULL;
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
// Read frames and save first five frames to disk
i = 0;
while (av_read_frame(pFormatCtx, &packet) >= 0) {
// Is this a packet from the video stream?
if (packet.stream_index == videoStream) {
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
// Did we get a video frame?
if (frameFinished) {
// Convert the image from its native format to RGB
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
pFrame->linesize, 0, pCodecCtx->height,
pFrameRGB->data, pFrameRGB->linesize);
// Save the frame to disk
if (++i <= 5) {
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
Теперь реализуем функцию SaveFrame
(про ppm формат можно почитать
здесь):
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile = fopen(szFilename, "wb");
if (pFile == NULL) {
return;
}
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for (y = 0; y < height; y++) {
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
}
// Close file
fclose(pFile);
}
Осталось только все почистить:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
avformat_close_input(&pFormatCtx);
Для отрисовки на экране мы используем SDL (Simple
DirectMedia Layer). В данном туториале используем библиотеку из репозитория. $ sudo apt-get install libsdl1.2-dev
SDL в целом имеет много методов для отрисовки изображений на экран, и в частности, один, вполне подходящий для показа видео -- YUV overlay. YUV -- это цветовая модель, в которой цвет представляется как 3 компоненты (яркость Y и две цветоразностных U и V).
Наш план состоит в том, чтобы заменить функцию SaveFrame() из предыдущего туториала, а вместо этого показывать кадры на экране.
Но сперва посмотрим, как использовать SDL.
#include <SDL.h>
int main(int argc, char *argv[]) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
return 0;
}
Для компиляции используем утилиту sdl-config
, которая просто возвращает
правильные флаги:
$ sdl-config --cflags
-I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT
$ sdl-config --libs
-L/usr/lib/x86_64-linux-gnu -lSDL
$ gcc -o tutorial02 tutorial02.c `sdl-config --cflags --libs`
Теперь нам необходимо место на экране для показа изображений (surface).
// Make a screen to put our video
SDL_Surface *screen = NULL;
screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if (!screen) {
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
Этот код настраивает экран. В качестве параметров выступают ширина и высота. Следущий аргумент устанавливает битовую глубину экрана (0 означает "такая же как у текущего экрана", не работает для OS X).
Теперь создадим YUV оверлей на этом экране. В SDL есть 4 разных YUV-формата, но самый быстрый из них YV12.
// Allocate a place to put our YUV image on that screen
SDL_Overlay *bmp = NULL;
bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
SDL_YV12_OVERLAY, screen);
Для показа изображения используем структуру AVPicture.
if (frameFinished) {
SDL_LockYUVOverlay(bmp);
AVPicture pict;
pict.data[0] = bmp->pixels[0];
pict.data[1] = bmp->pixels[2];
pict.data[2] = bmp->pixels[1];
pict.linesize[0] = bmp->pitches[0];
pict.linesize[1] = bmp->pitches[2];
pict.linesize[2] = bmp->pitches[1];
// Convert the image into YUV format that SDL uses
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
pFrame->linesize, 0, pCodecCtx->height,
pict.data, pict.linesize);
SDL_UnlockYUVOverlay(bmp);
}
Теперь скажем SDL показать отмасштабированное изображение.
SDL_Rect rect;
/* ... code ... */
SDL_UnlockYUVOverlay(bmp);
rect.x = 0;
rect.y = 0;
rect.w = pCodecCtx->width;
rect.h = pCodecCtx->height;
SDL_DisplayYUVOverlay(bmp, &rect);
Теперь добавим ещё обработку события выхода.
SDL_Event event;
/* ... code ... */
av_free_packet(&packet);
SDL_PollEvent(&event);
switch (event.type) {
case SDL_QUIT:
SDL_Quit();
exit(0);
break;
default:
break;
}
К чему приведет запуск программы? Видео воспроизводится с сумасшедшей скоростью! На самом деле, все кадры показываются с той же скоростью, с какой извлекаются из файла. В туториале 5 мы синхронизуем видео. Но сначала более важная задача: звук!
В SDL есть методы для вывода звука. Для открытия аудиоустройства используется функция SDL_OpenAudio(). Она принимает в качестве аргумента структуру SDL_AudioSpec, которая содержит всю информацию об аудио, которое мы хотим воспроизвести.
Цифровой звук состоит из потока сэмплов (samples). Звуки записаны с определенной частотой дискретизации (sample rate), что просто означает, как быстро играть каждый сэмпл (измеряется в числе сэмплов в секунду). Например, 22050 и 44100 -- частоты, используемые для радио и CD, соответственно. Большая часть аудио содержит более одного канала для стерео или объемного звука. Когда мы получаем данные из файла фильма, мы не знаем как много сэмплов мы получим. SDL предлагает следующий метод воспроизведения аудио: выставляем настройки аудио (частоту, число каналов и т.д.) и устанавливаем колбек. Когда мы начинаем воспроизводить аудио, SDL постоянно вызывает эту функцию для заполнения буфера некоторым числом байт. После получения данных в структуре SDL_AudioSpec, вызываем функцию SDL_OpenAudio(), которая откроет аудиоустройство и вернет другую структуру AudioSpec. Последнюю мы в действительности и используем, но нет гарантии, что она та, которую мы запрашивали.
// Find the first video stream
videoStream = -1;
audioStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO &&
videoStream < 0) {
videoStream = i;
}
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO &&
audioStream < 0) {
audioStream = i;
}
}
if (videoStream == -1) {
return -1; // Didn't find a video stream
}
if (audioStream == -1)
return -1; // Didn't find an audio stream
Теперь получим желаемые параметры аудио:
#define SDL_AUDIO_BUFFER_SIZE 1024
AVCodecContext *aCodecCtx = NULL;
SDL_AudioSpec wanted_spec, spec;
// Seta audio settings from codec info
aCodecCtx = pFormatCtx->streams[audioStream]->codec;
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
Пройдемся по параметрам:
- freq: частота дискретизации (sample rate)
- format: параметр сообщает SDL, какой формат использовать. S -- signed (знаковое), 16 -- размер каждого сэмпла в битах, SYS -- порядок расположения байт зависит от системы.
- channels: число каналов
- silence: тишина
- samples: размер буфера, по превышении которого SDL запросит следующую порцию данных. Хорошее значение лежит между 512 и 8192, ffplay использует 1024
- callback: наш колбек
- userdata: SDL передаст колбеку в виде указателя на void* любые пользовательские данные. Мы хотим, чтобы был известен кодек.
Далее мы окрываем аудиоустройство с помощью SDL_OpenAudio(). Также нам нужно открыть и сам кодек:
AVCodec *aCodec = NULL;
AVDictionary *audioOptionsDict = NULL;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if (!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
avcodec_open2(aCodecCtx, aCodec, &audioOptionsDict);
Теперь мы готовы дергать аудиоинформацию из потока. Но что делать с этой информацией? Мы собираемся непрерывно получать пакеты из файла, но в то же время SDL собирается вызывать колбек. Решением может быть создание некоей глобальной структуры, которую мы можем заполнять аудиопакетами, так что нашему audio_callback будет откуда брать данные. Так что создадим очередь (queue) пакетов. ffmpeg содержит структуру AVPacketList, которая поможет нам в этом.
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
PacketQueue audioq;
Отметим, что nb_packets не то же самое, что size (size относится к размеру в байтах, который мы получили из packet->size). Также мы имеем мьютекс и условную переменную, т.к. SDL исполняет обработку аудио в отдельном потоке. Если мы не заблокируем очередь правильно, мы можем реально испортить наши данные. Сначала мы сделаем функцию для инициализации очереди.
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
Затем сделаем функцию для добавления пакетов в очередь:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if (av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1) {
return -1;
}
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt) {
q->first_pkt = pkt1;
} else {
q->last_pkt->next = pkt1;
}
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
SDL_LockMutex() блокирует мьютекс в очереди, так что мы можем добавить что-либо в нее. Затем SDL_CondSignal() посылает сигнал нашей get-функции (если та в ожидании) через условную переменную, что доступны данные для обработки, затем разблокирует мьютекс.
Вот соответствующая get-функция:
int quit = 0;
int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for (;;) {
if (quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt) {
q->last_pkt = NULL;
}
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
SDL_CondWait() приостанавливает выполнение до получения данных.
Вы можете заметить, что мы проверяем глобальную переменную quit для того, чтобы
быть уверенными, что поток не будет выполняться бесконечно, и не придется
посылать kill -9
программе.
Осталось только настроить очередь
PacketQueue audioq;
main() {
...
avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
SDL_PauseAudio() запускает аудиоустройство. Оно играет тишину, если не получает данных. Это не совсем то поведение, которое мы хотим. Снабдим очередь пакетами.
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
Нет необходимости освобождать пакет после добавления в очередь. Мы освободим его после декодирования.
Реализуем audio_callback()
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodeCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while (len > 0) {
if (audio_buf_index >= audio_buf_size) {
// we have already sent all our data; get more
audio_size = audio_decode_frame(aCodeCtx, audio_buf, audio_buf_size);
if (audio_size < 0) {
// If error, output silence
audio_buf_size = 1024; // arbitrary?
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if (len1 > len) {
len1 = len;
}
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
int buf_size) {
static AVPacket pkt;
static uint8_t *audio_pkt_data = NULL;
static int audio_pkt_size = 0;
static AVFrame frame;
int len1, data_size;
for (;;) {
while (audio_pkt_size > 0) {
int got_frame = 0;
len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
if (len1 < 0) {
// if error, skip frame
audio_pkt_size = 0;
break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
if (got_frame) {
data_size = av_samples_get_buffer_size(NULL,
aCodecCtx->channels,
frame.nb_samples,
aCodecCtx->sample_fmt,
1);
memcpy(audio_buf, frame.data[0], data_size);
}
if (data_size <= 0) {
// No data yet, get more frames
continue;
}
// We have data, return it and come back for more later
return data_size;
}
if (pkt.data) {
av_free_packet(&pkt);
}
if (quit) {
return -1;
}
if (packet_queue_get(&audioq, &pkt, 1) < 0) {
return -1;
}
audio_pkt_data = pkt.data;
audio_pkt_size = pkt.size;
}
}
Наша main-функция делает слишком много всего: пробегает по циклу событий, читает пакеты и декодирует видео. Разделим эти события: создадим тред, отвечающий за декодирование пакетов. Также при декодировании видео мы будем сохранять кадры в другой очереди.
Для начала сделаем код более наглядным. Создадим структуру VideoState.
typedef struct VideoState {
AVFormatContext *pFormatCtx;
int videoStream, audioStream;
AVStream *audio_st;
PacketQueue audioq;
uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2];
unsigned int audio_buf_size;
unsigned int audio_buf_index;
AVFrame audio_frame;
AVPacket audio_pkt;
uint8_t *audio_pkt_data;
int audio_pkt_size;
AVStream *video_st;
PacketQueue videoq;
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;
SDL_mutex *pictq_mutex;
SDL_cond *pictq_cond;
SDL_Thread *parse_tid;
SDL_Thread *video_tid;
char filename[1024];
int quit;
AVIOContext *io_context;
struct SwsContext *sws_ctx;
} VideoState;