[Часть 1/2] Руководство по FFmpeg и SDL или Как написать видеоплеер менее чем в 1000 строк

Автор оригинала: Dranger
  • Перевод

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

И хотя мы старались, в таком объёмном тексте неизбежны трудности перевода. Сообщайте о недочётах (желательно, в личных сообщениях) — вместе сделаем лучше.

Оглавление

EDISON Software - web-development
Статья переведена при поддержке компании EDISON.

В нашем портфолио представлено в том числе и создание программного обеспечения для обработки видео, при этом часто пишем программы для работы с видео на C и C++.

Мы очень любим работать с видео! ;-)

Предисловие


UPD: Данное руководство обновлено по состоянию на февраль 2015 года.

FFmpeg — отличная библиотека для создания видеоприложений, а также утилит общего назначения. FFmpeg берёт на себя всю рутину по обработке видео, выполняя всё декодирование, кодирование, мультиплексирование и демультиплексирование. Что заметно упрощает создание медиа-приложений. Всё достаточно простенько-быстренько, написано на C, можно декодировать практически любой кодек, имеющихся на сегодняшний день, а также кодировать в некоторые другие форматы.

Единственная загвоздка в том, что документация, в основном, отсутствует. Есть один учебник (в оригинале тут ссылка на уже несуществующую веб-страницу — примечание переводчка), в который рассмотрены основы FFmpeg и автоматическое генерирование doxygen-доков. И больше ничего. Поэтому, я решил самостоятельно разобраться с тем как с помощью FFmpeg создавать работающие цифровые видео- и аудио-приложения, а заодно задокументировать процесс и представить его в виде учебника.

Есть программа FFplay, поставляемая с FFmpeg. Она проста, написана на C, реализует полноценный видеоплеер с использованием FFmpeg. Первый мой урок — обновленная версии оригинального урока авторства Мартина Бёме (в оригинале тут ссылка на уже несуществующую веб-страницу — примечание переводчка) — я утащил кое-какие куски оттуда. А также в серии моих уроков покажу процесс создания работающего видеоплеера на основе ffplay.c Фабриса Белларда. В каждом уроке будет представлена новая идея (а то и две) с объяснением её реализации. К каждой главе прилагается листинг на C, который сможете самостоятельно скомпилировать и запустить. Исходные файлы покажут, как работает настоящая программа, как работают её отдельные части, а также продемонстрируют второстепенные технические детали, не затронутые в данном руководстве. Когда закончим, у нас будет рабочий видеоплеер, написанный менее чем в 1000 строк кода!

При создании плеера будем использовать SDL для вывода аудио и видео медиа-файла. SDL — это превосходная кроссплатформенная мультимедийная библиотека, используемая в программах воспроизведения MPEG, эмуляторах и многих видеоиграх. Вам необходимо будет загрузить и установить библиотеки для SDL в вашей системе, чтобы скомпилировать программы из этого руководства.

Этот учебник предназначен для людей с хорошим опытом программирования. По крайней мере, нужно знать C, а также иметь представление о таких понятиях, как очереди, мьютексы и т.д. Должно быть некоторое представление о мультимедиа; например, о таких вещах, как форма волны и тому подобное. Однако быть гуру в этих вопросах необязательно, так как многие концепции получат объяснение по ходу уроков.

Пожалуйста, не стесняйтесь, присылайте мне сообщения об ошибках, вопросы, комментарии, идеи, функции, да что угодно, на Dranger собачка Gmail точка Com.







Читайте также в блоге
компании EDISON:


Руководство по FFmpeg libav






Урок 1: Создание скринкапсов


Полный листинг: tutorial01.c
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101 
// on GCC 4.7.2 in Debian February 2015

// A small sample program that shows how to use libavformat and libavcodec to
// read video from a file.
//
// Use
//
// gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz
//
// to build (assuming libavformat and libavcodec are correctly installed
// your system).
//
// Run using
//
// tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM
// format.

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

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

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);
}

int main(int argc, char *argv[]) {
  // Initalizing these to NULL prevents segfaults!
  AVFormatContext   *pFormatCtx = NULL;
  int               i, videoStream;
  AVCodecContext    *pCodecCtxOrig = NULL;
  AVCodecContext    *pCodecCtx = NULL;
  AVCodec           *pCodec = NULL;
  AVFrame           *pFrame = NULL;
  AVFrame           *pFrameRGB = NULL;
  AVPacket          packet;
  int               frameFinished;
  int               numBytes;
  uint8_t           *buffer = NULL;
  struct SwsContext *sws_ctx = NULL;

  if(argc < 2) {
    printf("Please provide a movie file\n");
    return -1;
  }
  // Register all formats and codecs
  av_register_all();
  
  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // 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
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }
  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();
  
  // Allocate an AVFrame structure
  pFrameRGB=av_frame_alloc();
  if(pFrameRGB==NULL)
    return -1;

  // 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));
  
  // 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);
  
  // initialize SWS context for software scaling
  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);
  }
  
  // Free the RGB image
  av_free(buffer);
  av_frame_free(&pFrameRGB);
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);

  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

Обзор


Файлы фильмов имеют несколько основных компонентов. Во-первых, сам файл называется контейнером, и тип контейнера определяет способ представления данных в файле. Примерами контейнеров являются AVI и Quicktime. Далее, в файле есть несколько потоков; в частности, обычно есть аудиопоток и видеопоток. («Поток» — это забавное слово для «последовательности элементов данных, доступных в соответствии с временно́й шкалой».) Элементы данных в потоке называются кадрами. Каждый поток кодируется тем или иным типом кодека. Кодек определяет, как фактические данные кодируются и декодируются — отсюда и название кодек. Примерами кодеков являются DivX и MP3. Пакеты затем считываются из потока. Пакеты — это фрагменты данных, которые могут содержать биты данных, которые декодируются в необработанные кадры, которыми мы, наконец, можем манипулировать в нашем приложении. Для наших целей каждый пакет содержит полные кадры (или несколько кадров, если это аудио).

Работать с видео и аудио потоками очень просто даже на самом базовом уровне:

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

Работа с мультимедиа с помощью FFmpeg почти настолько же проста, как и в этой программе, хотя в некоторых программах шаг «СДЕЛАТЬ [...]» может оказаться весьма сложным. В этом уроке мы откроем файл, считаем видеопоток внутри него, и наш «СДЕЛАТЬ [...]» будет записывать кадр в файл PPM.

Открытие файла


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

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <ffmpeg/swscale.h>
...
int main(int argc, charg *argv[]) {
av_register_all();

Это регистрирует все доступные форматы файлов и кодеки в библиотеке, благодаря чему они будут использоваться автоматически при открытии файла с соответствующим форматом/кодеком. Обратите внимание, нужно вызывать av_register_all() только один раз, поэтому мы делаем это здесь, в main(). При желании можно зарегистрировать только выборочные форматы файлов и кодеки, но обычно нет особых причин, чтобы так делать.

Теперь открываем файл:

AVFormatContext *pFormatCtx = NULL;

// Open video file
if(avformat_open_input(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
  return -1; // Couldn't open file

Получаем имя файла из первого аргумента. Эта функция читает заголовок файла и сохраняет информацию о формате файла в структуре AVFormatContext, которую мы передали. Последние три аргумента используются для указания формата файла, размера буфера и параметров формата. Установив в них значения NULL или 0, libavformat определит всё автоматически.

Эта функция смотрит только на заголовок, так что теперь нам нужно проверить информацию о потоке в файле:

// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
  return -1; // Couldn't find stream information

Эта функция передаёт в pFormatCtx->streams достоверные данные. Мы знакомимся с удобной функцией отладки, показывающей нам, что там внутри:

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

Теперь pFormatCtx->streams — это просто массив указателей размером pFormatCtx->nb_streams. Пройдёмся по нему, пока не обнаружим видеопоток:

int i;
AVCodecContext *pCodecCtxOrig = NULL;
AVCodecContext *pCodecCtx = NULL;

// 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;

Информация о кодеке в потоке находится в месте, которое называем «контекст кодека». Оно содержит всю информацию о кодеке, который использует поток, и теперь у нас есть указатель на него. Но мы всё ещё должны найти реальный кодек и открыть его:

AVCodec *pCodec = 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
}
// Copy context
pCodecCtx = avcodec_alloc_context3(pCodec);
if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
// Open codec
if(avcodec_open2(pCodecCtx, pCodec)<0)
  return -1; // Could not open codec

Обратите внимание, что нельзя напрямую использовать AVCodecContext из видеопотока! Поэтому приходится использовать avcodec_copy_context(), чтобы скопировать контекст в новое место (разумеется, после того как для него выделена память).

Хранение данных


Теперь нам нужно место для хранения кадра:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

Поскольку мы планируем выводить файлы PPM, которые хранятся в 24-битном RGB, нам нужно будет преобразовать наш кадр из его собственного формата в RGB. FFmpeg сделает это за нас. Для большинства проектов (включая и этот) нужно преобразовать начальный кадр в определенный формат. Выделяем кадр для преобразованного кадра:

// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();
if(pFrameRGB==NULL)
  return -1;

Несмотря на то, что мы выделили кадр, нам всё ещё нужно место для размещения необработанных данных при их преобразовании. Мы используем avpicture_get_size, для получения нужных размеров и выделяем необходимое место вручную:

uint8_t *buffer = NULL;
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 — это аналог C-шной функции malloc от FFmpeg, представлящая собой простую обертку вокруг malloc, которая обеспечивает выравнивание адресов памяти и т.п. Кстати, это не защищает от утечек памяти, двойного освобождения или других проблем, возникающих с malloc.

Теперь мы используем avpicture_fill, чтобы связать кадр с нашим вновь выделенным буфером. Что касается AVPicture: структура AVPicture является подмножеством структуры AVFrame — начало структуры AVFrame идентично структуре AVPicture.

// 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);

Мы уже на финишной прямой! Теперь мы готовы считывать с потока!

Чтение данных


Теперь, чтобы прочитать весь видеопоток, читаем очередной пакет, расшифровываем его в нашем кадре, и как только расшифровка будет завершена, конвертируем кадр и сохраняем его:

struct SwsContext *sws_ctx = NULL;
int frameFinished;
AVPacket packet;
// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
    pCodecCtx->height,
    pCodecCtx->pix_fmt,
    pCodecCtx->width,
    pCodecCtx->height,
    PIX_FMT_RGB24,
    SWS_BILINEAR,
    NULL,
    NULL,
    NULL
    );

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);
}

Ничего сложного: av_read_frame() считывает пакет и сохраняет его в структуре AVPacket. Обратите внимание, что мы только распределяем структуру пакета — FFmpeg выделяет нам внутренние данные, на которые указывает packet.data. Это чуть позже освобождает av_free_packet(). avcodec_decode_video() преобразует пакет в кадр. Однако у нас может не быть всей информации, которая нам нужна для кадра после декодирования пакета, поэтому avcodec_decode_video() устанавливает frameFinished, когда у нас будет следующий кадр. Наконец, мы используем sws_scale() для преобразования из собственного формата (pCodecCtx->pix_fmt) в RGB. Помните, что можно привести указатель AVFrame к указателю AVPicture. Наконец, передаём информацию о кадре, высоте и ширине нашей функции SaveFrame.

Кстати, о пакетах. Технически пакет может содержать только часть кадра, а также другие биты данных. Однако парсер FFmpeg гарантирует, что получаемые нами пакеты содержат либо полный кадр либо даже несколько кадров.

Теперь всё, что осталось сделать, это применить функцию SaveFrame для записи информации RGB в файл в формате PPM. Хотя мы поверхностно имеем дело с самим форматом 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);
}

Мы производим стандартное открытие файла и пр. А затем записываем данные RGB. Файл записываем построчно. Файл PPM — это просто файл, в котором информация RGB представлена в виде длинной строки. Если вы знаете цвета HTML, это будет похоже на разметку цвета каждого пикселя от первого конца до последнего, что-то вроде #ff0000#ff0000...., как для красного экрана. (На самом деле там хранится в двоичном формате и без разделителя, но, надеюсь, идею вы уловили.) В заголовке указано, насколько широким и высоким является изображение, а также максимальный размер значений RGB.

Теперь вернемся к нашей функции main(). Как только мы закончим чтение из видеопотока, нам просто нужно все очистить:

// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);

// Free the YUV frame
av_free(pFrame);

// Close the codecs
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

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

return 0;

Как можете заметить, используем av_free для памяти, выделенной с помощью avcode_alloc_frame и av_malloc.

Вот и весь код! Теперь, если вы используете Linux или подобную платформу, то запускаете:

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

Если у вас более древняя версия FFmpeg, может потребоваться удалить -lavutil:

gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lz -lm

Большинство графических программ должны открывать формат PPM. Проверьте это на некоторых файлах фильма, скринкапсы которых сделаны с помощью нашей программы.






Урок 2: Вывод на экран


Полный листинг: tutorial02.c
// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
// 
// gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial02 myvideofile.mpg
//
// to play the video stream on your screen.

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

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

int main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  float           aspect_ratio;
  struct SwsContext *sws_ctx = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // 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
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   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) {
	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);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
      
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codec
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

SDL и видео


Для прорисовки на экране будем использовать SDL. SDL расшифровывается как Simple Direct Layer. Это преотличная кроссплатформенная библиотека для мультимедиа, используемая во многих проектах. Получить библиотеку можно на официальном сайте или загрузить пакет разработчика для своей операционной системы, если таковой есть. Понадобится библиотеки для компиляции кода из этого урока (всех остальных уроков это, кстати, тоже касается).

У SDL есть много методов для рисования на экране. Один из способов отображения фильмов — то, что называется наложением YUV.

Формально, даже не YUV, а YCbCr. У некоторых, между прочим, сильно подгорает, когда «YCbCr» называют как «YUV». Вообще говоря, YUV — это аналоговый формат, а YCbCr — это цифровой формат. FFmpeg и SDL в своём коде и в макросах обозначают YCbCr как YUV, но то такое.

YUV — способ хранения необработанных данных изображения, таких как RGB. Грубо говоря, Y является компонентом яркости, а U и V являются компонентами цвета. (Это более сложно, чем RGB, потому что часть информации о цвете отбрасывается, и можно иметь только 1 замер U и V на каждые 2 замера Y). Наложение YUV в SDL принимает необработанный массив данных YUV и отображает его. Он принимает 4 различных вида форматов YUV, но YV12 является из них самым быстрым. Существует другой формат YUV, называемый YUV420P, который совпадает с YV12, за исключением того, что массивы U и V меняются местами. 420 означает, что он подвергается дискретизации в соотношении 4:2:0, т.е., на каждые 4 замера яркости приходится 1 замер цвета, поэтому информация о цвете распределяется на четверти. Это хороший способ экономии пропускной способности, поскольку человеческий глаз всё равно не замечает этих изменений. Латинская буква «P» в названии говорит о том, что формат «плоскостной» (planar), это попросту означает, что компоненты Y, U и V находятся в отдельных массивах. FFmpeg может конвертировать изображения в YUV420P, что очень кстати, ибо многие видеопотоки уже хранятся в этом формате или легко конвертируются в него.

Таким образом, наш текущий план состоит в том, чтобы заменить функцию SaveFrame() из предыдущего урока и вместо этого вывести наш кадр на экран. Но сначала нужно ознакомиться с базовыми возможностями библиотеки SDL. Для начала подключаем библиотеки и инициализируем SDL:

#include <SDL.h>
#include <SDL_thread.h>

if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
  fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
  exit(1);
}

SDL_Init(), по существу, сообщает библиотеке, какие функции будем использовать. SDL_GetError(), понятное дело, это наша удобная функция для отладки.

Создание дисплея


Теперь нам нужно место на экране, чтобы расположить элементы. Основная область для отображения изображений с SDL называется поверхностью:

SDL_Surface *screen;

screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if(!screen) {
  fprintf(stderr, "SDL: could not set video mode - exiting\n");
  exit(1);
}

Так мы устанавили экран с заданной шириной и высотой. Следующая опция — битовая глубина экрана — 0 — это специальное значение, которое означает «такой же, как и текущий дисплей».

Теперь мы создаем YUV-оверлей на этом экране, чтобы мы могли выводить на него видео, и настраиваем наш SWSContext для преобразования данных изображения в YUV420:

SDL_Overlay     *bmp = NULL;
struct SWSContext *sws_ctx = NULL;

bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
                           SDL_YV12_OVERLAY, screen);

// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
                         pCodecCtx->height,
			 pCodecCtx->pix_fmt,
			 pCodecCtx->width,
			 pCodecCtx->height,
			 PIX_FMT_YUV420P,
			 SWS_BILINEAR,
			 NULL,
			 NULL,
			 NULL
			 );

Как упоминали, используем YV12 для показа изображения и получаем данные YUV420 из FFmpeg.

Показ изображения


Ну, это было достаточно просто! Теперь нам просто нужно показать изображение. Давайте пройдем весь путь до того места, где у нас был готовый кадр. Мы можем избавиться от всего того, что у нас было для RGB-кадра и мы собираемся заменить SaveFrame() нашим кодом отображения. Чтобы отобразить изображение, мы собираемся создать структуру AVPicture и установить для неё указатели данных и размер линии для нашего наложения YUV:

  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);

Поначалу мы блокируем оверлей, потому что мы планируем записывать в него. Это хорошая привычка, чтобы позже не возникало проблем. Структура AVPicture, как показано выше, имеет указатель данных, который представляет собой массив из 4-х указателей. Поскольку здесь мы имеем дело с YUV420P, у нас есть только 3 канала и, следовательно, только 3 набора данных. Другие форматы могут иметь четвертый указатель для альфа-канала или чего-то ещё. Размер линии — вот на что это похоже. Аналогичные структуры в нашем наложении YUV — это переменные для пикселей и высот. (Питчи, pitches — если изъясняться в терминах SDL для обозначения ширины заданной строки данных.) Итак, мы указываем три массива pict.data на нашем оверлее, поэтому, когда мы пишем в pict, мы на самом деле производим запись в наше наложение, которое, конечно, уже имеет необходимое пространство, выделенное специально для него. Точно так же мы получаем информацию о размерах линий непосредственно из нашего оверлея. Мы меняем формат преобразования на PIX_FMT_YUV420P и используем sws_scale, как и раньше.

Прорисовка изображения


Но нам все еще нужно указать для SDL, чтобы он действительно показывал данные, которые мы ему предоставили. Мы также передаем в эту функцию прямоугольник, в котором указано, куда должен идти фильм, до какой ширины и высоты он должен масштабироваться. Таким образом, SDL выполняет для нас масштабирование, и это может помочь вашему графическому процессору быстрее масштабировать:

SDL_Rect rect;

  if(frameFinished) {
    /* ... code ... */
    // 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);
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
  }

Вот теперь наше видео отображается!

Давайте покажем ещё одну особенность SDL: систему событий. SDL настроен таким образом, что при вводе или перемещении мыши в приложении SDL или отправке ему сигнала — генерируется событие. Затем ваша программа проверяет эти события, если подразумевается обработка вводимых пользователем данных. Ваша программа также может создавать события для отправки системе событий SDL. Это особенно полезно при многопоточном программировании с SDL, что мы увидим в уроке №4. В нашей программе мы собираемся проверять события сразу после завершения обработки пакета. На данный момент мы собираемся обработать событие SDL_QUIT, чтобы мы могли выйти:

SDL_Event       event;

    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

И вот так и живём! Избавляемся от всего старого мусора и мы готовы к компиляции. Если используете Linux или что-то линуксоподобное, лучший способ компиляции с использованием библиотек SDL:

gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

sdl-config просто выводит нужные флаги для gcc, чтобы корректно включать библиотеки SDL. Возможно, придётся сделать что-то другое, чтобы заставить это скомпилироваться в вашей системе; пожалуйста, на всякий пожарный проверьте документацию SDL для вашей системы. Как только скомпилируется, продолжайте и запускайте.

Что происходит, когда вы запускаете эту программу? Видео как будто бы сходит с ума! Фактически, мы просто отображаем все видеокадры настолько быстро, насколько получается извлечь их из файла фильма. У нас сейчас нет кода, чтобы выяснить, когда нам нужно показывать видео. В конце концов (в уроке №5) мы приступим к синхронизации видео. Но на данный момент мы упускаем кое-что не менее важное: звук!






Урок 3: Воспроизведение звука


Полный листинг: tutorial03.c
// tutorial03.c
// A pedagogical video player that will stream through every video frame as fast as it can
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
//
// gcc -o tutorial03 tutorial03.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial03 myvideofile.mpg
//
// to play the stream on your screen.

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

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

PacketQueue audioq;

int quit = 0;

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;
}
static 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;
}

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 = 0;

  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;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	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;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (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(aCodecCtx, audio_buf, sizeof(audio_buf));
      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 main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream, audioStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  struct SwsContext *sws_ctx = NULL;
  
  AVCodecContext  *aCodecCtxOrig = NULL;
  AVCodecContext  *aCodecCtx = NULL;
  AVCodec         *aCodec = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;
  SDL_AudioSpec   wanted_spec, spec;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
    
  // 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;
   
  aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;
  aCodec = avcodec_find_decoder(aCodecCtxOrig->codec_id);
  if(!aCodec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  // Copy context
  aCodecCtx = avcodec_alloc_context3(aCodec);
  if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Set audio settings from codec info
  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;
  }

  avcodec_open2(aCodecCtx, aCodec, NULL);

  // audio_st = pFormatCtx->streams[index]
  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video

#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   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) {
	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);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
	av_free_packet(&packet);
      }
    } else if(packet.stream_index==audioStream) {
      packet_queue_put(&audioq, &packet);
    } else {
      av_free_packet(&packet);
    }
    // Free the packet that was allocated by av_read_frame
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      quit = 1;
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }

  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtxOrig);
  avcodec_close(pCodecCtx);
  avcodec_close(aCodecCtxOrig);
  avcodec_close(aCodecCtx);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

Аудио


Теперь нам хотелось бы, чтоб в приложении проигрывал звук. SDL также предоставляет нам методы для воспроизведения звука. Функция SDL_OpenAudio() используется для открытия самого аудиоустройства. Он принимает в качестве аргументов структуру SDL_AudioSpec, которая содержит всю информацию об аудио, которое мы собираемся воспроизвести.

Прежде чем покажем, как это настроить, сначала поясним, как вообще компьютер обрабатывает аудио. Цифровое аудио состоит из длинного потока сэмплов, каждый из которых представляет собой конкретное значение звуковой волны. Звуки записываются с определенной частотой дискретизации, которая просто говорит о том, как быстро воспроизводится каждый сэмпл, и измеряется количеством сэмплов в секунду. Примерные частоты дискретизации составляют 22 050 и 44 100 сэмплов в секунду, являющиеся скоростями, используемыми для радио и CD соответственно. Кроме того, большинство аудио может иметь более одного канала для стерео или объемного звучания, поэтому, например, если сэмпл находится в стерео, сэмплы будут приходить по два за раз. Когда мы получаем данные из файла фильма, мы не знаем, сколько сэмплов мы получим, но FFmpeg не выдаёт разбитых на части сэмплов — это также означает, что он не разделит и стерео-сэмпл.

Метод воспроизведения аудио в 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;

Здесь мы можем получить всю информацию, которую хотим от AVCodecContext из потока, точно так же, как мы это делали с видеопотоком:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;

Если помните, в предыдущих уроках, нам всё равно приходится открывать сам аудиокодек. Это просто:

AVCodec         *aCodec;

aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1;
}
// Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
/* set up SDL Audio here */

avcodec_open2(aCodecCtx, aCodec, NULL);

В контексте кодека содержится вся информация, необходимая для настройки нашего аудио:

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 (частота): частота дискретизации, как объяснялось ранее.
  • format (формат): это SDL сообщает, какой формат мы будем ему давать. «S» в «S16SYS» означает «подписанный», 16 означает, что каждая выборка имеет длину 16 битов, а «SYS» означает, что порядковый номер будет зависеть от системы, в которой вы находитесь. Это формат, в котором avcodec_decode_audio2 даст нам аудио.
  • channels (каналы): количество аудиоканалов.
  • silence (тишина): это значение, определяющее тишину. По умолчанию 0.
  • samples (сэмплы): это размер аудио буфера, который мы хотели бы, чтобы SDL предоставлял нам, когда он запрашивает больше аудио. Оптимальное значение здесь где-то между 512 и 8192; FFplay, к примеру, использует 1024.
  • callback (обратный вызов): здесь передаём реальную функцию обратного вызова. Подробнее о функции обратного вызова мы поговорим позже.
  • userdata (пользовательские данные): SDL даст нашему обратному вызову пустой указатель на любые пользовательские данные, которые только захотим. Мы хотим дать ему знать о нашем контексте кодека; чуть ниже будет ясно зачем.

И наконец, открываем аудио с SDL_OpenAudio.

Очереди


И надо же! Теперь мы готовы извлекать аудиоинформацию из потока. Но что делать с этой информацией? Мы будем непрерывно получать пакеты из файла фильма, но в то же время SDL будет вызывать функцию обратного вызова! Решением будет создание какой-то глобальной структуры, в которую мы можем вставлять аудиопакеты, чтобы у нашего audio_callback было что-то для получения аудиоданных! Итак, вот что мы сделаем для создания очереди пакетов. FFmpeg даже имеет структуру, которая поможет в этом: AVPacketList, который является просто связанным списком для пакетов. Вот наша структура очереди:

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

Во-первых, мы должны указать, что nb_packets отличается по размеру — размер относится к размеру байта, который мы получаем из packet->size. Заметьте, у нас есть мьютекс и переменная условия. Это связано с тем, что SDL выполняет аудиопроцесс как отдельный поток. Если мы не заблокируем очередь должным образом, мы действительно можем испортить наши данные. Посмотрим, как реализована очередь. Каждый уважающий себя программист должен знать, как создавать очереди, но мы тоже покажем как это делать, чтобы вам было проще изучать функции 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. Обратите внимание, как SDL_CondWait() создает функциональный блок (т.е. делает паузу, пока мы не получим данные), если мы скажем ей это сделать:

int quit = 0;

static 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(). По сути, всё, что делает CondWait, — это ожидает сигнала от SDL_CondSignal() (или SDL_CondBroadcast()) и затем продолжает. Тем не менее, похоже, что мы поймали его в мьютекс — если мы удерживаем блокировку, наша функция put ничего не может поместить в очередь! Однако то, что SDL_CondWait() также делает для нас, — это разблокировывает мьютекс, который мы ему даём, и затем снова пытаться заблокировать его, как только мы получаем сигнал.

На всякий пожарный


Вы также видите, что у нас есть глобальная переменная quit, которую мы проверяем, чтобы убедиться, что не установили в программе сигнал выхода (SDL автоматически обрабатывает сигналы TERM и т.п.). В противном случае поток будет продолжаться вечно, и нам придётся прибить программу посредством kill -9:

  SDL_PollEvent(&event);
  switch(event.type) {
  case SDL_QUIT:
    quit = 1;

Мы обязательно устанавливаем флаг выхода в значение 1.

Скармливаем пакеты


Осталось только настроить нашу очередь:

PacketQueue audioq;
main() {
...
  avcodec_open2(aCodecCtx, aCodec, NULL);

  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 callback(void *userdata, Uint8 *stream, int len)

userdata, является указателем, который мы дали SDL, stream является буфером, в который мы будем записывать аудиоданные, а len это размер этого буфера. Вот код:

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
  int len1, audio_size;

  static uint8_t audio_buf[(AVCODEC_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(aCodecCtx, audio_buf,
                                      sizeof(audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	audio_buf_size = 1024;
	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;
  }
}

По сути, это простой цикл, который извлекает данные из другой функции, нами написанную, audio_decode_frame(), сохраняет результат в промежуточном буфере, пытается записать len байтов в поток и получает больше данных, если их у нас ещё недостаточно или сохраняет его на потом, если у нас что-то остаётся. Размер audio_buf в 1,5 раза больше размера самого большого аудиокадра, который нам даст FFmpeg, что даёт нам хороший запас.

Заключительная расшифровка аудио


Давайте разберёмся с внутренностями декодера audio_decode_frame:

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 = 0;

  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;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	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;
  }
}

Весь процесс фактически начинается ближе к концу функции, где мы вызываем packet_queue_get(). Мы забираем пакет из очереди и сохраняем информацию из него. Затем, когда у нас есть пакет для работы, мы вызываем avcodec_decode_audio4(), который во многом похож на свою функцию-сестру avcodec_decode_video(), за исключением того, что в этом случае пакет может иметь более одного кадра. Поэтому может потребоваться вызвать несколько раз, чтобы получить все данные из пакета. Получив кадр, мы просто копируем его в наш аудио-буфер, убедившись, что data_size меньше нашего аудио-буфера. Кроме того, помните про приведение audio_buf к правильному типу, потому что SDL дает 8-битный int-буфер, а FFmpeg дает нам данные в 16-битном int-буфере. Также следует учитывать разницу между len1 и data_size. len1 — это объём пакета, который мы использовали, а data_size — количество возвращаемых необработанных данных.

Когда у нас есть некоторые данные, мы немедленно возвращаемся, чтобы узнать, нужно ли нам получать больше данных из очереди или мы закончили. Если нам ещё нужно обрабатывать пакет, то придерживаем его. Если завершили с пакетом, то наконец-то его освобождаем.

И это всё! У нас есть аудио, передаваемое из основного цикла чтения в очередь, которое затем считывается функцией audio_callback, которая передаёт эти данные в SDL, а SDL передаёт на вашу звуковую карту. Идём дальше и компилируем:

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

Гип-гип-ура! Видео по-прежнему несётся на максимальной скорости, но звук уже воспроизводится как надо. Почему так? Да потому, что аудиоинформация имеет частоту дискретизации — мы выкачиваем аудиоинформацию настолько быстро, насколько получается, но аудио просто воспроизводится в данном потоке в соответствии со своей частотой дискретизации.

Мы почти созрели для синхронизации видео и аудио, но сначала нужно провести небольшую реорганизацию программы. Метод постановки в очередь звука и воспроизведения его с использованием отдельного потока работал очень хорошо: он делал код более управляемым и более модульным. Прежде чем начнём синхронизировать видео с аудио, нужно упростить код. В следующей серии будем плодить потоки управления!






Урок 4: Множественные треды


Полный листинг tutorial04.c
// tutorial04.c
// A pedagogical video player that will stream through every video frame as fast as it can,
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial04 tutorial04.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

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

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_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;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  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;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

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;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->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;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      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(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Now, normally here goes a ton of code
	 about timing, etc. we're just going to
	 guess at a delay for now. You can
	 increase and decrease this value and hard code
	 the timing - but I don't suggest that ;)
	 We'll learn how to do it for real later.
      */
      schedule_refresh(is, 40);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);
    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }      
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

Обзор


В прошлый раз мы добавили поддержку аудио, используя аудиофункции SDL. SDL запустил поток, делающий обратные вызовы для функции, которую мы определяли каждый раз, когда требовался звук. Теперь мы собираемся сделать то же самое с видеодисплеем. Это делает код более модульным и с ним проще работать — особенно если хотим добавить синхронизацию. Так с чего начнём?

Заметим, что наша основная функция обрабатывает очень много: она проходит через цикл обработки событий, читает пакеты и ​​декодирует видео. Что мы собираемся сделать, так это разделить всё на части: у нас будет поток, отвечающий за декодирование пакетов; затем эти пакеты добавляются в очередь и считываются соответствующими аудио- и видеопотоками. Аудиопоток мы уже настроили так, как требуется; с видеопотоком будет несколько сложнее, так как нам придётся обеспечивать показ видео своими силами. Мы добавим фактический код дисплея в основной цикл. Но вместо того, чтобы показывать видео каждый раз, когда мы выполняем цикл, мы интегрируем отображение видео в цикл обработки событий. Идея состоит в том, чтобы декодировать видео, сохранить полученный кадр в другой очереди, затем создать собственное событие (FF_REFRESH_EVENT), которое мы добавляем в систему событий, затем, когда наш цикл событий увидит это событие, он отобразит следующий кадр в очереди. Вот удобная ASCII-иллюстрация того, что происходит:


Основная причина переместить управление отображением видео через цикл обработки событий состоит в том, что с помощью потока SDL_Delay мы можем точно контролировать, когда следующий видеокадр появляется на экране. Когда мы, наконец-то, синхронизируем видео в следующем уроке, просто добавим код, который будет планировать следующее обновление видео, чтобы на экране отображалось правильное изображение в нужное время.

Упрощаем код


Немного очистим код. У нас есть вся эта информация об аудио- и видеокодеках, и мы собираемся добавлять очереди, буферы, и Бог весть что ещё. Все эти штуки для некой логической единицы, а именно — для фильма. Итак, мы намерены создать большую структуру, содержащую всю эту информацию, называемую VideoState.

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  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;
} VideoState;

Здесь мы видим намёки на то, что собираемся получить в итоге. Сначала видим основную информацию — контекст формата и индексы аудио- и видеопотока, а также соответствующие объекты AVStream. Затем мы видим, что некоторые из этих аудиобуферов перемещены в эту структуру. Они (audio_buf, audio_buf_size и т.д.) предназначались для информации об аудио, которое всё ещё находилось там (или же отсутствовало). Мы добавили еще одну очередь для видео и буфер (который будет использоваться в качестве очереди; для этого нам какие-либо экстравагантные очереди не нужны) для декодированных кадров (сохраненных как наложение). Структура VideoPicture — это наше собственное творение (увидим, что в ней будет, когда придём к ней). Также можно заметить, что мы выделили указатели для двух дополнительных потоков, которые мы создадим, а также флаг выхода и имя файла фильма.

Итак, теперь возвращаемся к основной функции, дабы увидеть, как это меняет нашу программу. Давайте настроим нашу структуру VideoState:

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz() — хорошая функция, которая будет выделять нам память и обнулять её.

Затем инициализируем наши блокировки для буфера отображения (pictq), потому что, поскольку цикл обработки событий вызывает нашу функцию отображения — запомните, функция отображения будет извлекать предварительно декодированные кадры из pictq. В то же время наш видеодекодер будет помещать в него информацию — мы не знаем, кто туда доберётся первым. Надеюсь, вы понимаете, что это классическое состояние гонки. Поэтому мы распределяем его сейчас, прежде чем начинать какие-либо темы. Давайте также скопируем имя нашего фильма в VideoState:

av_strlcpy(is->filename, argv[1], sizeof(is->filename));

is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();

av_strlcpy — это функция из FFmpeg, которая выполняет некоторые дополнительные проверки границ помимо strncpy.

Наш первый тред


Давайте уже запустим наши потоки и сделаем что-то реальное:

schedule_refresh(is, 40);

is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
  av_free(is);
  return -1;
}

schedule_refresh — это функция, которую мы определим позже. Что она делает, так это говорит системе произвести FF_REFRESH_EVENT через указанное количество миллисекунд. Это, в свою очередь, вызовет функцию обновления видео, когда увидим её в очереди событий. Но сейчас давайте посмотрим на SDL_CreateThread().

SDL_CreateThread() делает именно это — порождает новый поток, который имеет полный доступ ко всей памяти исходного процесса, и запускает поток, выполняемый функцией, которую мы ему даём. Эта функция также будет передавать данные, определённые пользователем. В этом случае мы вызываем decode_thread() и присоединяем нашу структуру VideoState. В первой половине функции нет ничего нового; она просто выполняет работу по открытию файла и поиску индекса аудио и видеопотоков. Единственное, что мы делаем иначе — это сохраняем форматный контекст в нашей большой структуре. После того, как мы нашли наши потоковые индексы, вызываем другую функцию, которую определяем, stream_component_open(). Это довольно естественный способ разделения, и, поскольку мы делаем много похожих вещей для настройки видео и аудиокодека, то повторно используем некоторый код, делая его функцией.

Функция stream_component_open() — это место, где мы обнаружим наш декодер кодека, настроим параметры звука, сохраним важную информацию в нашей большой структуре и запустим наши аудио и видеопотоки. Здесь мы также вставляем другие параметры, такие как принудительное использование кодека вместо его автоматического определения и т.д. Вот так:

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx;
  AVCodec *codec;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    /* ...etc... */
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height,
				 is->video_st->codec->pix_fmt, is->video_st->codec->width,
				 is->video_st->codec->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

Это практически то же самое, что и код, который был у нас раньше, за исключением того, что теперь он обобщён для аудио и видео. Обратите внимание, что вместо aCodecCtx мы настроили нашу большую структуру в качестве пользовательских данных для нашего звукового обратного вызова. Мы также сохранили сами потоки как audio_st и video_st. Мы также добавили нашу видеоочередь и настроили ее так же, как и нашу аудиоочередь. Суть в том, чтобы запустить видео и аудиопотоки. Эти биты и делают это:

    SDL_PauseAudio(0);
    break;

/* ...... */

    is->video_tid = SDL_CreateThread(video_thread, is);

Вспоминаем SDL_PauseAudio() из прошлого урока. SDL_CreateThread() используется так же. Возвращаемся к нашей функции video_thread().

Перед этим вернёмся ко второй половине нашей функции decode_thread(). По сути, это просто цикл for, который читает пакет и помещает его в нужную очередь:

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if((is->pFormatCtx->pb->error) == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }

Здесь нет ничего действительно нового, кроме того, что у нас теперь есть максимальный размер для нашей аудио- и видеоочереди, и мы добавили проверку на ошибки чтения. Контекст формата имеет внутри структуру ByteIOContext, которая называется pb. ByteIOContext — это структура, которая в основном хранит в себе всю информацию о файлах низкого уровня.

После нашего цикла for у нас есть весь код для ожидания завершения остальной части программы или информирования об этом. Этот код поучителен, потому что показывает, как мы проталкиваем события — то, что нам понадобится позже, чтобы отобразить видео:

  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;

Мы получаем значения для пользовательских событий, используя константу SDL SDL_USEREVENT. Первому пользовательскому событию должно быть присвоено значение SDL_USEREVENT, следующее SDL_USEREVENT + 1 и т.д. FF_QUIT_EVENT определяется в нашей программе как SDL_USEREVENT + 1. Мы также можем передавать пользовательские данные, если нужно, и здесь мы передаём наш указатель на большую структуру. Наконец, мы вызываем SDL_PushEvent(). В нашем переключателе цикла событий мы просто помещаем это в раздел SDL_QUIT_EVENT, который у нас был раньше. Мы увидим наш цикл событий более подробно; на данный момент, просто будьте уверены, что когда мы нажмём FF_QUIT_EVENT, мы словим его позже и переключим флаг выхода.

Получение кадра: video_thread


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

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);

    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_free(pFrame);
  return 0;
}

Большая часть этой функции должна быть понятна к этому моменту. Мы сюда просто скопипастили функцию avcodec_decode_video2, просто заменив некоторые аргументы; например, у нас есть AVStream, хранящийся в нашей большой структуре, поэтому мы получаем наш кодек оттуда. Мы просто продолжаем получать пакеты из нашей видеоочереди, пока кто-нибудь не скажет нам выйти или мы не обнаружим ошибку.

Кадр из очереди


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

typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

Наша большая структура содержит буфер этих файлов, где мы можем их хранить. Однако нам нужно распределить SDL_Overlay самостоятельно (обратите внимание на назначенный флаг, который показывает, сделали ли мы это или нет).

Чтобы использовать эту очередь, у нас есть два указателя — индекс записи и индекс чтения. Мы также отслеживаем, сколько фактических изображений находится в буфере. Для записи в очередь мы сначала подождем, пока очистится наш буфер, чтобы у нас было место для хранения нашего VideoPicture. Затем мы проверяем — установили ли мы же наложение в нашем индексе записи? Если нет, необходимо выделить память. Мы также должны перераспределить буфер, если размер окна изменился!

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_st->codec->width ||
     vp->height != is->video_st->codec->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

Давайте взглянем на функцию alloc_picture():

void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
				 is->video_st->codec->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);
  vp->width = is->video_st->codec->width;
  vp->height = is->video_st->codec->height;  
  vp->allocated = 1;
}

Вы должны узнать функцию SDL_CreateYUVOverlay, которую мы переместили из нашего основного цикла в этот раздел. Этот код должен быть достаточно понятным к настоящему времени. Однако теперь у нас есть блокировка мьютекса, потому что два потока не могут одновременно записывать информацию на экран! Это не позволит нашей функции alloc_picture помешать другой функции, которая будет отображать картинку. (Мы создали эту блокировку как глобальную переменную и инициализировали ее в main(); см. код.) Помните, что мы сохраняем ширину и высоту в структуре VideoPicture, потому что нам нужно убедиться, что размер нашего видео не изменяется по некоторой причине.
Хорошо, мы все уладили, и у нас есть наш оверлей YUV, выделенный и готовый получать изображение. Давайте вернёмся к queue_picture и посмотрим на код, чтобы скопировать кадр в оверлей. Эта часть должна быть вам знакома:

int queue_picture(VideoState *is, AVFrame *pFrame) {

  /* Allocate a frame if we need it... */
  /* ... */
  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_st->codec->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

Здесь большая часть — просто код, который мы использовали ранее, чтобы заполнить оверлей YUV нашим кадром. Последний бит просто «добавляет» наше значение в очередь. Очередь работает, значения в неё добавляются до тех пор, пока она не заполнится, и чтение из неё происходит, пока в ней хоть что-то есть. Поэтому все зависит от значения is->pictq_size, что требует от нас его блокировки. Итак, что мы делаем здесь: увеличиваем указатель записи (а если нужно, начинаем заново), затем блокируем очередь и увеличиваем её размер. Теперь наш считыватель будет знать, что есть больше информации об очереди, и если это сделает нашу очередь заполненной, и наш записыватель будет знать об этом.

Отображение видео


Вот и всё для нашего видеотреда! Теперь мы завершили все свободные треды, кроме одного — припоминаете, как мы давным-давно вызывали функцию schedule_refresh()? Взглянем, что на самом деле произошло:

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

SDL_AddTimer() — это SDL-функция, просто выполняющая обратный вызов пользовательской функции через определенное число миллисекунд (и, если нужно, переносит некоторые пользовательские данные). Будем использовать эту функцию для планирования обновлений видео — каждый раз, когда мы её вызываем, она устанавливает таймер, который будет запускать событие, которое, в свою очередь, заставит нашу функцию main() вызвать функцию, которая извлекает кадр из наша картинка очереди и отображает ее! Уф! Три «который/которое/которая» в одном предложении! Итак, сделаем первое, что нужно сделать — запустим данное событие. Это отправляет нас к:

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

Событие запускает наш старый знакомый. FF_REFRESH_EVENT определяется здесь как SDL_USEREVENT + 1. Следует отметить, что когда мы возвращаем 0, SDL останавливает таймер, поэтому обратный вызов не выполняется снова.

Теперь, когда мы вновь призвали FF_REFRESH_EVENT, нам нужно обработать его в нашем цикле событий:

for(;;) {

  SDL_WaitEvent(&event);
  switch(event.type) {
  /* ... */
  case FF_REFRESH_EVENT:
    video_refresh_timer(event.user.data1);
    break;

что отправляет нас вот к этой функции, которая фактически извлекает данные из нашей очереди изображений:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Timing code goes here */

      schedule_refresh(is, 80);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

На данный момент эта функция довольно-таки проста: обрабатывает очередь, пока у нас что-то есть, устанавливает таймер для отображения следующего видеокадра, вызывает video_display, чтобы фактически показать видео на экране, затем увеличивает счётчик на очереди, уменьшая при этом её размер. Можно заметить, что на самом деле ничего не делаем с vp в этой функции, и вот почему: это впереди. Но несколько позже. Мы собираемся использовать его для доступа к информации о времени, когда начнём синхронизировать видео с аудио. Вот, взгляните на то место кода, где написан комментарий «Код тайминга находится тут» («Timing code goes here»). В этом разделе собираемся выяснить, как скоро мы должны показать следующий видеокадр, а затем ввести это значение в функцию schedule_refresh(). На данный момент мы просто вводим фиктивное значение 80. Технически, можно угадать и проверить это значение, и перекомпилировать его для каждого фильма, но: 1) начнёт тормозить через некоторое время и 2) это довольно глупо. Хотя, в дальнейшем ещё вернёмся к этому моменту.

Мы почти закончили. Осталось сделать только одно: показать видео! Вот эта функция video_display:

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_st->codec->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) *
	is->video_st->codec->width / is->video_st->codec->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_st->codec->width /
	(float)is->video_st->codec->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);
  }
}

Поскольку экран может оказаться любого размера (мы установили 640x480, и есть способы настроить таким образом, чтобы пользователь изменял размеры), необходимо динамически определить, насколько большим должен быть прямоугольная область для нашего фильма. Итак, сначала надо выяснить соотношение сторон нашего фильма, просто ширина, разделённая на высоту. Некоторые кодеки будут иметь нечётное соотношение сторон выборки, которое представляет собой просто ширину/высоту одного пикселя или выборки. Поскольку значения высоты и ширины в нашем контексте кодека измеряются в пикселях, фактическое соотношение сторон равно соотношению сторон, умноженному на соотношение сторон для выборки. Некоторые кодеки будут показывать соотношение сторон 0, и это означает, что каждый пиксель просто имеет размер 1x1. Затем масштабируем фильм таким макаром, чтобы он максимально умещался на экране. Битовый разворот & -3 просто округляет значение до ближайшего, кратного четырём. Затем центрируем фильм и вызываем SDL_DisplayYUVOverlay(), дабы убедиться, что используется экранный мьютекс для доступа к нему.

И это все? Мы закончили? Всё ещё нужно переписать аудиокод, чтобы использовать новый VideoStruct, но это тривиальные изменения, которые можно увидеть в примере кода. Последнее, что нам нужно сделать, это изменить наш обратный вызов для внутренней функции обратного вызова «выход» в FFmpeg:

VideoState *global_video_state;

int decode_interrupt_cb(void) {
  return (global_video_state && global_video_state->quit);
}

Устанавливаем global_video_state в большую структуру в main().

Итак, это всё! Компилируем:

gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

и наслаждайтесь фильмом без синхронизации! В следующем шаге наконец-то создадим действительно работающий видеоплеер!






Урок 5: Синхронизация видео


Полный листинг tutorial05.c
// tutorial05.c
// A pedagogical video player that really works!
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial05 tutorial05.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

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

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define AV_SYNC_THRESHOLD 0.01
#define AV_NOSYNC_THRESHOLD 10.0

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
  double pts;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;

  double          audio_clock;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_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;
  int             audio_hw_buf_size;  
  double          frame_timer;
  double          frame_last_pts;
  double          frame_last_delay;
  double          video_clock; ///<pts of last decoded frame / predicted pts of next decoded frame
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  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;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

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;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->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;
}

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_ctx->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_ctx->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;
  double pts;
  int n;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_ctx->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_ctx->sample_rate);
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)
      av_free_packet(pkt);

    if(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;
  double pts;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    vp->pts = pts;
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_ctx->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;
  double pts;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;

    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);

    if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE) {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
    is->audio_hw_buf_size = spec.size;
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

ПРЕДОСТЕРЕЖЕНИЕ


Когда я только-только написал это руководство, весь мой код синхронизации был взят из тогдашней версии ffplay.c. Сегодня это совершенно другая программа, и обновления в библиотеках FFmpeg (да и в самом ffplay.c) привели к принципиальным изменениям. Хотя этот код всё ещё работает, он уже морально устарел, и есть много других улучшений, которые могли бы использоваться в данном руководстве.

Как синхронизируется видео


До сего момента у нас был практически бесполезный проигрыватель фильмов. Да, он воспроизводит видео, и да, он воспроизводит аудио, но это ещё не совсем то, что мы бы назвали фильмом. Так что же нам тогда делать?

PTS и DTS


К счастью, аудио- и видеопотоки содержат информацию о том, насколько быстро и в какие моменты времени они должны воспроизводиться. У аудиопотоков есть частота дискретизации, а у видеопотоков количество кадров в секунду. Однако, если мы просто синхронизируем видео, подсчитав количество кадров и умножив на частоту кадров, есть немалая вероятность, что оно не синхронизируется со звуком. Поэтому мы пойдём другим путём. Пакеты из потока могут иметь так называемую метку времени декодирования (DTS — от decoding time stamp) и метку времени представления (PTS — от presentation time stamp). Чтобы понять эти два значения, нужно знать, как хранятся фильмы. Некоторые форматы, вроде MPEG, используют то, что они называют B-кадрами (B означает двунаправленный, по англ. bidirectional). Два других типа кадров называются I-кадрами и P-кадрами (I это внутренний, inner, а P означает прогнозируемый, predicted). I-кадры содержат полное изображение. P-кадры зависят от предыдущих I- и P-кадров и состоят на различий от предыдущих кадров, или можно ещё назвать — дельты. B-кадры похожи на P-кадры, но зависят от информации, содержащейся как в предыдущих, так в последующих кадрах! То что кадр может содержать не само изображение, а различия с другими кадрами — объясняет, почему у нас может не быть готового кадра после вызова avcodec_decode_video2.

Допустим, у нас фильм, в котором 4 кадра в такой последовательности: I B B P. Тогда нам нужно узнать информацию из последнего P-кадра, прежде чем сможем отобразить любой из двух предыдущих B-кадров. Из-за этого кадры могут храниться в последовательности, не совпадающей фактическому порядку отображения: I P B B. Вот для чего нужна временнáя метка декодирования и временнáя метка представления для каждого кадра. Отметка времени декодирования говорит нам, когда нам нужно что-то декодировать, а отметка времени представления указывает нам, когда нам нужно что-то отобразить. Итак, в этом случае наш поток может выглядеть так:

   PTS: 1 4 2 3
   DTS: 1 2 3 4
Поток: I P B B

Как правило, PTS и DTS отличаются только тогда, когда воспроизводимый поток содержит B-кадры.

Когда получаем пакет от av_read_frame(), он содержит значения PTS и DTS для той информации, что находится внутри пакета. Но что нам действительно нужно, так это PTS нашего недавно декодированного необработанного кадра, в этом случае знаем, когда его нужно отображать.

К счастью, FFmpeg предоставляет нам «самую лучшую из всех возможных» временнýю метку, которую можно получить, с помощью функции av_frame_get_best_effort_timestamp().

Синхронизация


Чтобы кадры отображались по очереди, хорошо бы знать, когда нужно показывать определенный видеокадр. Но как именно мы это делаем? Идея такова: после того, как показываем кадр, мы выясняем, когда должен быть показан следующий кадр. Затем просто делаем паузу, после которой обновляем видео по истечении данного промежутка времени. Как и следовало ожидать, мы проверяем значение PTS следующего кадра по системным часам, чтобы увидеть, как долго должно быть наше время ожидания. Этот подход работает, но есть две проблемы, которые необходимо решить.

Во-первых, это вопрос о том, когда будет следующий PTS? Вы скажете, что можно просто добавить частоту видео к текущей PTS — и будете, в принципе, правы. Тем не менее, некоторые разновидности видео потребуют повторения кадров. Это означает, что придётся повторять текущий кадр определённое количество раз. Это может привести к тому, что программа отобразит следующий кадр слишком рано. Это надо учесть.

Вторая проблема состоит в том, что, в той программе, что мы написали на данный момент, видео и аудио, радостно несутся вперёд, пока не удосуживаясь синхронизироваться вообще. Нам не пришлось бы об этом беспокоиться, если бы всё само собой работало идеально. Но ваш компьютер не идеален, равно как и многие видеофайлы. Таким образом, у нас есть три варианта: синхронизировать аудио с видео, синхронизировать видео с аудио или синхронизировать и аудио и видео с внешними часами (например, с вашим компьютером). Сейчас мы собираемся синхронизировать видео с аудио.

Кодинг: получение PTS кадра


Теперь давайте непосредственно что-нибудь напишем. Нам надо добавить ещё несколько частей в нашу большую структуру, и мы сделаем это так, как нам нужно. Сначала давайте взглянем на наш видеотред. Помните, что здесь мы собираем пакеты, которые были помещены в очередь нашим потоком декодирования? В этой части кода нам нужно получить PTS для кадра, который нам дал avcodec_decode_video2. Первый способ, о котором мы говорили, — получение DTS последнего обработанного пакета, что довольно просто:

  double pts;

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;
    // Decode video frame
    len1 = avcodec_decode_video2(is->video_st->codec,
                                pFrame, &frameFinished, packet);
    if(packet->dts != AV_NOPTS_VALUE) {
      pts = av_frame_get_best_effort_timestamp(pFrame);
    } else {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

Мы устанавливаем PTS на ноль, если не можем определить его значение.

Ну, это было легко. Техническая ремарка: как вы можете заметить, мы используем int64 для PTS. Это потому, что PTS хранится как целое число. Это значение является меткой времени, которая соответствует измерению времени в единице времени timebb. Например, если поток имеет 24 кадра в секунду, PTS от 42 будет указывать, что кадр должен использоваться там, где должен быть 42-й кадр, при условии что у нас кадры сменяются каждые 1/24 секунды (конечно, это не обязательно будет так на самом деле).

Мы можем преобразовать это значение в секунды путём деления на частоту кадров. Значение time_base потока будет равно 1 делённое на частоту кадров (для контента с фиксированной частотой кадров), поэтому, чтобы получить PTS в секундах, мы умножаем на time_base.

Кодим дальше: синхронизация и использование PTS


Так что теперь у нас есть все готовые PTS. Теперь позаботимся о тех двух проблемах синхронизации, о которых говорили чуть выше. Мы собираемся определить функцию synchronize_video, которая будет обновлять PTS для синхронизации со всем. Эта функция, наконец, также будет иметь дело со случаями, когда мы не получаем значение PTS для нашего кадра. В то же время нам нужно отслеживать, когда ожидается следующий кадр, чтобы могли правильно установить частоту обновления. Мы можем сделать это, используя внутреннее значение video_clock, которое отслеживает, сколько времени прошло для видео. Мы добавляем это значение в нашу большую структуру:

typedef struct VideoState {
  double          video_clock; // pts of last decoded frame / predicted pts of next decoded frame

Вот функция synchronize_video, которая довольно понятна:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_st->codec->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

Как можно заметить, мы учитываем повторяющиеся кадры в этой функции.

Теперь давайте получим наш правильный PTS и поставим в очередь кадр, используя queue_picture, добавив новый аргумент pts:

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }

Единственное, что изменяется в queue_picture — это то, что мы сохраняем это значение pts в структуре VideoPicture, которую мы ставим в очередь. Таким образом, мы должны добавить переменную pts в структуру и добавить такие строки кода:

typedef struct VideoPicture {
  ...
  double pts;
}
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
  ... stuff ...
  if(vp->bmp) {
    ... convert picture ...
    vp->pts = pts;
    ... alert queue ...
  }

Итак, теперь у нас есть изображения, выстроенные в очередь с правильными значениями PTS, поэтому давайте взглянем на нашу функцию обновления видео. Вы можете вспомнить из прошлого урока, что мы просто подделали его и поставили обновление 80 мс. Что ж, теперь мы собираемся выяснить, что там должно быть на самом деле.

Наша стратегия заключается в том, чтобы предсказать время следующего PTS, просто измеряя время между текущим pts и предыдущим. В то же время нам нужно синхронизировать видео с аудио. Мы собираемся сделать аудиочасы: внутреннее значение, которое отслеживает, в каком положении находится аудио, который мы воспроизводим. Это похоже на цифровое считывание на любом mp3-плеере. Поскольку мы синхронизируем видео со звуком, видеопоток использует это значение, чтобы выяснить, находится ли он слишком далеко впереди или слишком далеко позади.

Мы вернемся к реализации позже; сейчас давайте предположим, что у нас есть функция get_audio_clock, которая даст нам время на аудиочасах. Как только мы получим это значение, что необходимо сделать, если видео и аудио не синхронизированы? Было бы глупо просто попытаться прыгнуть к правильному пакету через поиск или что-то еще. Вместо этого просто настроим значение, которое мы рассчитали для следующего обновления: если PTS слишком сильно отстает от времени аудио, мы удваиваем нашу расчетную задержку. Если PTS слишком сильно опережает время звучания, мы просто обновляем как можно быстрее. Теперь, когда у нас есть настроенное время обновления или задержки, мы собираемся сравнить это с часами нашего компьютера, оставив запущенный frame_timer. Этот таймер кадров суммирует все наши расчётные задержки во время воспроизведения фильма. Другими словами, этот frame_timer — это время, указывающее, когда нужно отображать следующий кадр. Мы просто добавляем новую задержку в таймер кадров, сравниваем её с временем на часах нашего компьютера и используем это значение для планирования следующего обновления. Это может немного сбивать с толку, поэтому внимательно изучите код:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

Мы делаем несколько проверок: во-первых, следим, чтобы задержка между текущей PTS и предыдущей PTS имела смысл. Если надобности в задержке нет, значит, аудио и видео просто совпали на этот момент и просто используем последнюю задержку. Затем удостоверяемся, что у нас выполняется порог синхронизации, ибо идеальной синхронизации не бывает никогда. FFplay использует значение 0,01 для порога. Также следим за тем, чтобы порог синхронизации никогда не был меньше промежутков между значениями PTS. Наконец, устанавливаем минимальное значение обновления в 10 миллисекунд (действительно, здесь вроде как должны пропустить кадр, но не будем об этом беспокоиться).

Мы добавили кучу переменных в большую структуру, поэтому не забываем проверить код. Кроме того, не забудьте инициализировать таймер кадра и начальную задержку предыдущего кадра в stream_component_open:

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;

Синхронизация: аудио часы


Настал час реализовать аудиочасы. Мы можем обновить время в нашей функции audio_decode_frame, где мы декодируем аудио. Теперь помните, что мы не всегда обрабатываем новый пакет каждый раз, когда вызываем эту функцию, поэтому есть два участка, где требуется обновить часы. Первое место, это где мы получаем новый пакет: просто устанавливаем звуковые часы на PTS пакета. Затем, если пакет имеет несколько кадров, сохраняем время воспроизведения звука, считая количество выборок и умножая их на заданную частоту выборок в секунду. Итак, когда у нас есть пакет:

    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }

И как только мы обрабатываем пакет:

      /* Keep audio_clock up-to-date */
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_st->codec->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_st->codec->sample_rate);

Несколько мелких нюансов: шаблон функции был изменен и теперь включает pts_ptr, поэтому обязательно измените его. pts_ptr это указатель, который используем, чтобы сообщить audio_callback аудиопакетный pts. Это будет использоваться в следующий раз для синхронизации аудио с видео.

Теперь мы можем наконец реализовать нашу функцию get_audio_clock. Это не так просто, как получить значение is->audio_clock, если подумать. Обратите внимание, что мы устанавливаем аудио PTS каждый раз, когда обрабатываем его, но если вы посмотрите на функцию audio_callback, потребуется время, чтобы переместить все данные из нашего аудиопакета в наш выходной буфер. Это означает, что значение в наших аудиочасах может слишком далеко уйти вперёд. Поэтому надо проверить, сколько нам осталось записать. Вот полный код:

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_st->codec->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_st->codec->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

Вы должны сейчас уже понимать, почему эта функция работает ;)

Итак, это все! Компилируем:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

Свершилось! Вы можете смотреть фильм на проигрывателе, созданном собственными руками. В следующем уроке рассмотрим синхронизацию аудио, а затем научимся производить поиск.

Руководство по FFmpeg и SDL или Как написать видеоплеер менее чем в 1000 строк — часть 2

Edison
Изобретаем успех: софт и стартапы

Комментарии 11

    +4
    спасибо за статью

    у меня в 2016 как-то получилось из Перла использовать ffpmeg + sdl, sdl использовал через Inline::C, а ffmpeg — через самопальный модуль…
    Прошёл примерно через все эти описанные в статье проблемы…

    программа на Перле что-то своё делала с видеопотоком, и опционально был SDL-превью текущего обрабатываемого кадра…
    там в примерно 1500 строк заключена ядерная смесь из perl, tcl/tk, Inline:: С (значит и С:) :):)

    если у кого есть интерес — могу описать подробнее, может быть даже в виде статьи тут, на хабре.
      +3
      Конечно, напишите! Может не про все полторы тысяч строк, а самые поучительные из них :-)
      +4
      Вы меня простите, но зачем?
      Программист знает английский, IMHO.
        +2
        Какое смелое утверждение о среднестатистическом программисте :-)
          +3

          Дык, если в самом деле решил использовать либав, то после этого туториала по-любому придется читать официальные доки и разбирать исходники ffmpeg

        +3

        Большое спасибо за перевод! Чисто из любопытства: почему не сдали делать 1 туториал=1 статья?


        Отмечу, что вы вводите терминологию пакет -> кадр ("до декодирования" -> "после декодирования"). Однако в нескольких местах для обозначения кадра вы используете слово "фрейм" (т.е. без перевода).


        Также вы, кажется, пропустили в первом туториале сноску


        Тут фишка в том, что для удобства (в том числе и, наверное, чтения кода), в свежем ffmpeg-е функцию avcodec_decode_video2 даже заменили на пару функций:


        • avcodec_send_packet() [скормить пакет в ffmpeg] и
        • avcodec_receive_frame() [получить декодированный кадр].

        И по тогда получается легче обработать случаи, когда мы:


        а) должны ffmpeg-у передать несколько пакетов прежде, чем получим хотя бы один кадр (это вроде раз история с "частичными пакетами" из ссылки)
        б) на один пакет нам возвращают несколько кадров. Кстати подскажите, а вообще возможно ли такое? На ум почему-то приходит только случай, когда мы декодируем видеопоток с камеры, которая "видит" статичную картинку.

          +2
          Не совсем ясно, что Вы подразумеваете под 1 туториал=1 статья?
          Если имеется ввиду, что каждую из 7 глав оформлять отдельно, то маловато информации на каждую статью.
          Если вообще всё дать в одной статье, то показалось что будет уж чересчур длинный текст. Перевод удобно было подготавливать в два этапа, решили и публиковать тоже в два этапа.

          Фреймы в трёх местах заменил на кадры, спасибо за дельное замечание. Заметка про пакеты, похоже, не захватилась, когда копировали текст из первоистоника. Теперь эта ремарка тоже присутствует в переводе.

          Относительно Вашего вопроса про несколько кадров в одном пакете пока ответить не готов 0:-)
            +4

            Б) Вспомните про аудио :)

              +4
              а) должны ffmpeg-у передать несколько пакетов прежде, чем получим хотя бы один кадр (это вроде раз история с "частичными пакетами" из ссылки)

              несколько проще, некоторые кодеки, например h264, по своей природе (или набору установок) хотят сначала несколько сырых кадров, перед тем как выдать первый закодированный. Новый API более лаконично ложится на такие особенности. Плюс у многих аппаратных кодеков схожие API.


              б) на один пакет нам возвращают несколько кадров. Кстати подскажите, а вообще возможно ли такое? На ум почему-то приходит только случай, когда мы декодируем видеопоток с камеры, которая "видит" статичную картинку.

              имхо, такое только с аудио возможно. С видео лично не сталкивался.

              +3
              Я бы с удовольствием почитал статью по ffmpeg без использования SDL, с многопоточным декодером, синхронизацией A/V, пусть даже без вывода кадра куда либо. Для звука бы использовалась любая подходящая библиотека.
                +3
                А я бы с удовольствием собрал этот проект, да вот только устарел он слегка, под SDL2 уже многое изменилось
                идентификатор «SDL_Overlay» не определен
                'av_dup_packet': объявлен deprecate
                И так всю дорогу.

                Могли бы Вы привести код в соответствие с текущими версиями?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое