Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

ffmpeg Tutorial

Оригинал лежит на http://www.dranger.com/ffmpeg/

Архив с примерами к этой статье лежит тут.

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

Вместе с ffmpeg идет утилита ffplay, которая является реализацией видео-плеера. Этот туториал основан на ffplay.c Fabrice Bellard. Мы напишем плеер , в котором будет не более 1000 строк .

Для вывода мы будем использовать кросс-платформенную библиотеку SDL. Если у вас нет библиотеки SDL ее нужно установить .

Tutorial 01: вывод на экран

Код - смотрите в архиве : tutorial01.c

Видео-файлы имеют несколько базовых компонентов. Сам по себе файл - это контейнер , например - AVI. Имеется набор потоков - streams; например audio поток и video поток. Данные внутри потока называются фреймами - frames. Каждый поток закодирован своим кодеком - codec. Пример кодеков - DivX , MP3. Пакеты - Packets - порции данных , читаемые из потока . Пакеты - данные , которые декодируются во фреймы . Пакет равен фрейму или нескольким фреймам.

На практике с потоком происходит примерно следующее :

10 OPEN video_stream FROM video.avi
 20 READ packet FROM video_stream INTO frame
 30 IF frame NOT COMPLETE GOTO 20
 40 DO SOMETHING WITH frame
 50 GOTO 20
 
В нашем первом примере мы откроем видео-файл , прочитаем из него видео-поток и запишем его на диск как PPM file.

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

Вначале нужно проинициализировать библиотеки. (Возможно , прийдется написать <ffmpeg/avcodec.h> и <ffmpeg/avformat.h> instead.)

#include <avcodec.h>
 #include <avformat.h>
 ...
 int main(int argc, charg *argv[]) {
 av_register_all();
 
Происходит регистрация форматов и кодеков . Делается это один раз .

Открываем файд:

AVFormatContext *pFormatCtx;
 
 // Open video file
 if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
   return -1; // Couldn't open file
 
Имя файла получаем с первого аргумента. У файла читается хидер и информация пишется в структуру AVFormatContext . Последние 3 аргумента используются для формата файла, размера буффера,опций .

Проверяем потоки :

// Retrieve stream information
 if(av_find_stream_info(pFormatCtx)<0)
   return -1; // Couldn't find stream information
 
Инициализируем pFormatCtx->streams, которая :
// Dump information about file onto standard error
 dump_format(pFormatCtx, 0, argv[1], 0);
 
pFormatCtx->streams - массив указателей, размера pFormatCtx->nb_streams, поищем тут потоки.
int i;
 AVCodecContext *pCodecCtx;
 
 // найдем первый video stream
 videoStream=-1;
 for(i=0; i<pFormatCtx->nb_streams; i++)
   if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) {
     videoStream=i;
     break;
   }
 if(videoStream==-1)
   return -1; // Didn't find a video stream
 
 // получим кодек
 pCodecCtx=pFormatCtx->streams[videoStream]->codec;
 
Информация о кодеке - это т.н. "codec context." Откроем кодек :
AVCodec *pCodec;
 
 // Find the decoder for the video stream
 pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
 if(pCodec==NULL) {
   fprintf(stderr, "Unsupported codec!\n");
   return -1; // Codec not found
 }
 // Open codec
 if(avcodec_open(pCodecCtx, pCodec)<0)
   return -1; // Could not open codec
 
pCodecCtx->time_base хранит frame rate. time_base это структура , имеющая numerator и denominator (AVRational). frame rate может быть дробным числом (напр. NTSC's 29.97fps).

Данные

AVFrame *pFrame;
 
 // Allocate video frame
 pFrame=avcodec_alloc_frame();
 
Поскольку мы сделаем вывод в PPM файл, который имеет формат 24-bit RGB, нужно сконвертировать наш фрейм в RGB. Выделим память для фрейма.
// Allocate an AVFrame structure
 pFrameRGB=avcodec_alloc_frame();
 if(pFrameRGB==NULL)
   return -1;
 
Также нужно выделить место для т.н. сырых данных .
uint8_t *buffer;
 int numBytes;
 // Determine required buffer size and allocate buffer
 numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                             pCodecCtx->height);
 buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
 
av_malloc - это выделенный кусок памяти в формате ffmpeg .

Используем avpicture_fill для привязки фрейма к этой памяти. AVPicture является частью структурыAVFrame.

// 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);
 
Итак - все готово для чтения данных из потока .

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

Мы прочитаем пакет данных из потока , декодируем его во фрейм , а потом запишем .

int frameFinished;
 AVPacket packet;
 
 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_video(pCodecCtx, pFrame, &frameFinished,
                          packet.data, packet.size);
     
     // Did we get a video frame?
     if(frameFinished) {
     // Convert the image from its native format to RGB
         img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 
             (AVPicture*)pFrame, pCodecCtx->pix_fmt, 
             pCodecCtx->width, pCodecCtx->height);
     
         // 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);
 }
 
Замечание по пакетам

Вообще говоря Ю пакет может содержать дробное число фреймов, но ffmpeg парсит пакеты так , что на выходе получает всегда целое число фреймов .

Процесс таков : av_read_frame() читает пакет и хранит его в структуре AVPacket. Для этого ffmpeg выделяет память с помощью packet.data. И освобождает с помощью av_free_packet() . avcodec_decode_video() конвертирует пакет во фрейм . avcodec_decode_video() переходит к следующему фрейму. img_convert() конвертирует из сырого формата (pCodecCtx->pix_fmt) в RGB. Нужно привести указатель AVFrame к виду AVPicture . Затем вызываем функцию SaveFrame.

Теперь нужно записать все в файл формата PPM .

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
   FILE *pFile;
   char szFilename[32];
   int  y;
   
   // Open file
   sprintf(szFilename, "frame%d.ppm", iFrame);
   pFile=fopen(szFilename, "wb");
   if(pFile==NULL)
     return;
   
   // Write header
   fprintf(pFile, "P6\n%d %d\n255\n", width, height);
   
   // Write pixel data
   for(y=0; y<height; y++)
     fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
   
   // Close file
   fclose(pFile);
 }
 
PPM-ффайл представляет из себя строку в формате #ff0000#ff0000.... В заголовок мы запишем высоту , ширину, и размер .

Вернемся в функцию main():

// Free the RGB image
 av_free(buffer);
 av_free(pFrameRGB);
 
 // Free the YUV frame
 av_free(pFrame);
 
 // Close the codec
 avcodec_close(pCodecCtx);
 
 // Close the video file
 av_close_input_file(pFormatCtx);
 
 return 0;
 

Теперь собираем :

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm
 
или так :
gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lz -lm
 
Конкретный вариант сборки зависит от вашей системы

Полученный PPM-файл можно открыть с помощью любой программы image viewer.

Tutorial 02: вывод на экран

Код - смотрите в архиве : tutorial02.c

SDL и Video

Для вывода на экран мы будем использовать SDL. SDL - Simple Direct Layer, это кросс-платформенная библиотека . Ее можно взять на www.libsdl.org , в некоторых дистрибутивах она уже идет в комплекте .

В SDL есть методы для вывода на экран - в частности YUV overlay. YUV (technically not YUV but YCbCr) * A Примечание: ffmpeg и SDL оба ссылаются на YCbCr как YUV в своих макросах. Вообще говоря, Y - это яркость , U и V - цветовые компоненты. YUV берет "сырой" массив данных и выводит их. Есть четыре YUV - формата, наиболее шустрый - YV12. YUV420P аналогичен YV12, только U и V работают по-другому. Символ "P" в имени означает , что формат планарный - "planar" , т.е. Y, U, и V компоненты находятся в раздельных массивах. ffmpeg может конвертировать картинки в YUV420P.

Нам нужно вместо функции SaveFrame() вывести фрейм на экран . Для начала подключим библиотеку 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 называется surface:

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);
 }
 
Мы инициализируем экран по высоте и ширине. 3-й параметр - bit depth of the screen - 0 - означает , что это текущий дисплэй .

Создадим YUV overlay для этого экрана :

SDL_Overlay     *bmp;
 
 bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
                            SDL_YV12_OVERLAY, screen);
 
Мы используем YV12.

Вывод изображения

Теперь выведем картинку . Нужно заменить SaveFrame() на наш код. Берем структуру AVPicture и привязываем ее к YUV overlay:

  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
     img_convert(&pict, PIX_FMT_YUV420P,
                     (AVPicture *)pFrame, pCodecCtx->pix_fmt, 
             pCodecCtx->width, pCodecCtx->height);
     
     SDL_UnlockYUVOverlay(bmp);
   }    
 
Вначале мы блокируем оверлей , т.к. собираемся писать в него.. В структуре AVPicture есть указатель data , который является массивом из 4-х указателей . Для YUV420P у нас есть 3 канала . В YUV есть 3 структуры - pixels и pitches. ("pitches" - ширина линейных данных). Мы берем 3 массива из pict.data и привязываем их к overlay, и запись в pict будет записью в overlay. Поменяем формат на PIX_FMT_YUV420P, используя img_convert.

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

В качестве параметра мы передаем в функцию шиирину и высоту прямоугольного экрана :

SDL_Rect rect;
 
   if(frameFinished) {
     /* ... code ... */
     // Convert the image into YUV format that SDL uses
     img_convert(&pict, PIX_FMT_YUV420P,
                     (AVPicture *)pFrame, pCodecCtx->pix_fmt, 
             pCodecCtx->width, pCodecCtx->height);
     
     SDL_UnlockYUVOverlay(bmp);
     rect.x = 0;
     rect.y = 0;
     rect.w = pCodecCtx->width;
     rect.h = pCodecCtx->height;
     SDL_DisplayYUVOverlay(bmp, &rect);
   }
 
Мы вывели видео на экран !

Несколько слов о событийной модели SDL. Когда вы нажимаете на клавиатуру или мышку , SDL генерит событие - event. Практическую пользу от этого мы рассмотрим в Tutorial 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;
     }
 
Компиляция 2-го примера :
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm \
 `sdl-config --cflags --libs`
 

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

Tutorial 03: Звук

Смотрите код в архиве : tutorial03.c

SDL имеет методы для вывода звука. Функция SDL_OpenAudio() открывает audio device. В качестве аргумента выступает структура SDL_AudioSpec.

Звук в формате Digital audio состоит из потока сэмплов. Каждый сэмпл - это экземпляр waveform. Звуки выводятся с определенным рэйтом, или его скоростью проигрывания, или числом сэмплов в секунду. Например , есть рэйты 22,050 и 44,100 сэмплов в секуду , это радио и CD соответственно . В случае со стерео каналом одновременно проигрываются 2 сэмпла .

SDL выводит звук так : мы инициализируем опции - рэйт (frequency для SDL), число каналов, callback function и данные. Когда мы начинаем проигрывать данные , SDL будет вызывать callback function для контроля audio buffer. С помощью структуры SDL_AudioSpec мы вызываем SDL_OpenAudio(), инициализируем структуру another 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==CODEC_TYPE_VIDEO
      &&
        videoStream < 0) {
     videoStream=i;
   }
   if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO &&
      audioStream < 0) {
     audioStream=i;
   }
 }
 if(videoStream==-1)
   return -1; // Didn't find a video stream
 if(audioStream==-1)
   return -1;
 
Всю информацию мы можем получить с помощью AVCodecContext из потока :
AVCodecContext *aCodecCtx;
 
 aCodecCtx=pFormatCtx->streams[audioStream]->codec;
 

Инициализация аудио :

wanted_spec.freq = aCodecCtx->sample_rate;
 wanted_spec.format = AUDIO_S16SYS;
 wanted_spec.channels = aCodecCtx->channels;
 wanted_spec.silence = 0;
 wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
 wanted_spec.callback = audio_callback;
 wanted_spec.userdata = aCodecCtx;
 
 if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
   fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
   return -1;
 }
 
  • freq: рэйт.
  • format: формат. "S" в "S16SYS" - это "signed", 16 - 16-битный сигнал, "SYS" - endian-order . Это формат avcodec_decode_audio2 .
  • channels: число аудио-каналов.
  • silence: 0.
  • samples: размер audio - в диапазоне 512 - 8192; ffplay использует 1024.
  • callback: callback function.
  • userdata: SDL будет возвращать указатель на данные
Открываем аудио с помощью SDL_OpenAudio.

Инициализируем audio codec :

AVCodec         *aCodec;
 
 aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
 if(!aCodec) {
   fprintf(stderr, "Unsupported codec!\n");
   return -1;
 }
 avcodec_open(aCodecCtx, aCodec);
 

Queues

Нужно создать глобальную структуру , из которой audio_callback будут извлекать звук. Создадим очередь - queue - пакетов. В ffmpeg уже есть готовая структура : AVPacketList, которая представляет линейный список пакетов:

typedef struct PacketQueue {
   AVPacketList *first_pkt, *last_pkt;
   int nb_packets;
   int size;
   SDL_mutex *mutex;
   SDL_cond *cond;
 } PacketQueue;
 
nb_packets и size - разные вещи. size ссылается байтовый размер , получаемый из packet->size. Мы используем mutex и condtion variable для синхронизации. 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() посылает сигнал о наличии данных , и очередь разблокируется .

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

Есть глобальная переменная quit. Тред будет работать , пока мы не сделаем kill -9. ffmpeg имеет свой callback : url_set_interrupt_cb.

int decode_interrupt_cb(void) {
   return quit;
 }
 ...
 main() {
 ...
   url_set_interrupt_cb(decode_interrupt_cb);  
 ...    
   SDL_PollEvent(&event);
   switch(event.type) {
   case SDL_QUIT:
     quit = 1;
 ...
 

Перед инициализацией очереди нужно :

PacketQueue audioq;
 main() {
 ...
   avcodec_open(aCodecCtx, aCodec);
 
   packet_queue_init(&audioq);
   SDL_PauseAudio(0);
 
SDL_PauseAudio() стартует audio device.

Мы проинициализировали очередь, и готовы к обработке пакетов. Цикл чтения пакетов :

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);
   }
 
Мы не делаем free для пакетов сразу , сделаем это потом .

Сделаем еще одну функцию audio_callback для работы с очередью. callback имеет форму void callback(void *userdata, Uint8 *stream, int len), где userdata - указатель , 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() . Размер audio_buf в полтора раза превышает размер аудио-фрейма.

Сам декодер 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;
 
   int len1, data_size;
 
   for(;;) {
     while(audio_pkt_size > 0) {
       data_size = buf_size;
       len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf, &data_size, 
                   audio_pkt_data, audio_pkt_size);
       if(len1 < 0) {
     /* if error, skip frame */
     audio_pkt_size = 0;
     break;
       }
       audio_pkt_data += len1;
       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(quit) {
       return -1;
     }
 
     if(packet_queue_get(&audioq, &pkt, 1) < 0) {
       return -1;
     }
     audio_pkt_data = pkt.data;
     audio_pkt_size = pkt.size;
   }
 }
 

Компилируем :

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \
 `sdl-config --cflags --libs`
 
Скорость видео по-прежнему не контролируется , а вот аудио работает нормально .

Tutorial 04: Треды

Код - смотрите в архиве : tutorial04.c

По аналогии с тем , что мы сделали со звуком , мы применим для видео треды , что сделает код более управляемым , особенно в плане синхронизации .

Наша main function достаточно велика: в ней обрабатывается event loop, читаются пакеты , плюс декодируется видео . Мы сделаем отдельный поток для декодирования пакетов . Пакет будет добавляться в очередь и читаться оттуда соответствующим audio и video потоком. Видео тред будет немного посложнее , чем аудио тред . В главный цикл мы добавим код для дисплея . Видео-дисплей мы встроим в event loop. После декодирования мы пишем фрейм в another очередь, затем создаем custom event (FF_REFRESH_EVENT) , и при его обработке на дисплей будет выводиться очередной фрейм из очереди . Графическая иллюстрация :

 ________ audio  _______      _____
 |        | pkts |       |    |     | to spkr
 | DECODE |----->| AUDIO |--->| SDL |-->
 |________|      |_______|    |_____|
     |  video     _______
     |   pkts    |       |
     +---------->| VIDEO |
  ________       |_______|   _______
 |       |          |       |       |
 | EVENT |          +------>| VIDEO | to mon.
 | LOOP  |----------------->| DISP. |-->
 |_______|<---FF_REFRESH----|_______|
 
Основная идея использования потока SDL_Delay в том , что мы будем контролировать , когда выводить очередной фрейм на экран .

Мы создадим большую структуру , которая будет хранить информацию обо всем и называться она будет VideoState.

typedef struct VideoState {
 
   AVFormatContext *pFormatCtx;
   int             videoStream, audioStream;
   AVStream        *audio_st;
   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;
   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;
 
Базовая информация - это format context и индикаторы для audio и video потоков, и соответствующие обьекты 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() выделяет память и заполняет ее нулями.

Далее мы инициализируем блокировки для display buffer (pictq). Нужно проинициализировать race condition, прежде чем мы запустим threads. Скопируем имя файла в VideoState.

strcpy(is->filename,  argv[1]);
 
 is->pictq_mutex = SDL_CreateMutex();
 is->pictq_cond = SDL_CreateCond();
 

Thread

Управление threads:

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 с интервалом в несколько миллисекунд. Это приводит к вызову refresh function. Теперь глянем на SDL_CreateThread().

SDL_CreateThread() порождает новый thread. При этом происходит вызов decode_thread() со структурой VideoState. Функция открывает файл на диске и находит индексы audio и video потоков. После этого происходит вызов stream_component_open().

stream_component_open() находит кодек , инициализирут опции аудио , сохраняет инфу в структуре , и передает управление в threads.

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;
   }
 
   // Get a pointer to the codec context for the video stream
   codecCtx = pFormatCtx->streams[stream_index]->codec;
 
   if(codecCtx->codec_type == CODEC_TYPE_AUDIO) {
     // Set audio settings from codec info
     wanted_spec.freq = codecCtx->sample_rate;
     /* .... */
     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;
     }
   }
   codec = avcodec_find_decoder(codecCtx->codec_id);
   if(!codec || (avcodec_open(codecCtx, codec) < 0)) {
     fprintf(stderr, "Unsupported codec!\n");
     return -1;
   }
 
   switch(codecCtx->codec_type) {
   case CODEC_TYPE_AUDIO:
     is->audioStream = stream_index;
     is->audio_st = pFormatCtx->streams[stream_index];
     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 CODEC_TYPE_VIDEO:
     is->videoStream = stream_index;
     is->video_st = pFormatCtx->streams[stream_index];
     
     packet_queue_init(&is->videoq);
     is->video_tid = SDL_CreateThread(video_thread, is);
     break;
   default:
     break;
   }
 }
 
Вместо aCodecCtx мы инициализируем нашу большую структуру . Потоки инициализируются как audio_st и video_st. Инициализируем очередь :
    SDL_PauseAudio(0);
     break;
 
 /* ...... */
 
     is->video_tid = SDL_CreateThread(video_thread, is);
 

Вторая половина функции decode_thread() - происходит чтение пакета и укладка его в очередь :

  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(url_ferror(&pFormatCtx->pb) == 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);
     }
   }
 
format context включает в себя структуру ByteIOContext под индексом pb. ByteIOContext - структура , хранящая низко-уровневую инфу о файле . url_ferror проверяет эту структуру на предмет чтения ошибок.

После выхода из цикла :

  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. первому user event должно быть присвоено значение SDL_USEREVENT, следующему - SDL_USEREVENT + 1, и т.д. FF_QUIT_EVENT у нас определено как SDL_USEREVENT + 2. В конце мы вызываем SDL_PushEvent(). Когда мы генерим FF_QUIT_EVENT, мы ловим флаг quit .

Фреймы: video_thread

После того как мы подготовили кодек, мы запускаем видео thread. Он читает пакеты из video queue, декодирует их во фреймы , и вызывает queue_picture для того , чтобы положить фрейм в picture queue:

int video_thread(void *arg) {
   VideoState *is = (VideoState *)arg;
   AVPacket pkt1, *packet = &pkt1;
   int len1, frameFinished;
   AVFrame *pFrame;
 
   pFrame = avcodec_alloc_frame();
 
   for(;;) {
     if(packet_queue_get(&is->videoq, packet, 1) < 0) {
       // means we quit getting packets
       break;
     }
     // Decode video frame
     len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, 
                 packet->data, packet->size);
 
     // Did we get a video frame?
     if(frameFinished) {
       if(queue_picture(is, pFrame) < 0) {
     break;
       }
     }
     av_free_packet(packet);
   }
   av_free(pFrame);
   return 0;
 }
 

Управление очередью фреймов

Структура , в которой хранится фрейм в очереди picture queue :

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

Для того , чтобы работать с этой очередью , нужна пара указателей - на пишущий и читающий индекс. Нам также надо знать , сколько картинок в буфере. Нам нужно очистить буфер для 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;
     /* we have to do it in the main thread */
     event.type = FF_ALLOC_EVENT;
     event.user.data1 = is;
     SDL_PushEvent(&event);
 
     /* wait until we have a picture allocated */
     SDL_LockMutex(is->pictq_mutex);
     while(!vp->allocated && !is->quit) {
       SDL_CondWait(is->pictq_cond, is->pictq_mutex);
     }
     SDL_UnlockMutex(is->pictq_mutex);
     if(is->quit) {
       return -1;
     }
   }
 

Цикл event loop:

for(;;) {
   SDL_WaitEvent(&event);
   switch(event.type) {
 /* ... */  
   case FF_ALLOC_EVENT:
     alloc_picture(event.user.data1);
     break;
 
event.user.data1 - это наша главная структура. Посмотрим на 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
   vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
                  is->video_st->codec->height,
                  SDL_YV12_OVERLAY,
                  screen);
   vp->width = is->video_st->codec->width;
   vp->height = is->video_st->codec->height;
   
   SDL_LockMutex(is->pictq_mutex);
   vp->allocated = 1;
   SDL_CondSignal(is->pictq_cond);
   SDL_UnlockMutex(is->pictq_mutex);
 }
 
Функцию SDL_CreateYUVOverlay мы переместили сюда.

У нас есть выделенный YUV overlay , готовый для получения картинки. Вернемся в queue_picture и глянем , как frame копируется в overlay:

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
     img_convert(&pict, dst_pix_fmt,
         (AVPicture *)pFrame, is->video_st->codec->pix_fmt, 
         is->video_st->codec->width, is->video_st->codec->height);
     
     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;
 }
 
В очередь добавляются фреймы до тех пор . пока она не станет полной , и читаются фреймы до тех пор , пока есть что читать .

Вывод видео

Что делает функция 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() - функция , делающая callback и вызывающая функцию через определенной число миллисекунд. Генерится таймер , который создает event, и в функции main() будет происходить вывод на экран.

Глянем на этот event:

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. Когда мы делаем return 0, SDL останавливает таймер и callback.

event loop:

for(;;) {
 
   SDL_WaitEvent(&event);
   switch(event.type) {
   /* ... */
   case FF_REFRESH_EVENT:
     video_refresh_timer(event.user.data1);
     break;
 
и вызываем функцию , которая извлекает данные из picture queue:
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 для вывода на экран, увеличивает счетчик очереди, и уменьшает ее размер.

Функция video_display :

 void video_display(VideoState *is) {
 
   SDL_Rect rect;
   VideoPicture *vp;
   AVPicture pict;
   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_DisplayYUVOverlay(vp->bmp, &rect);
   }
 }
 

Компиляция :

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

Tutorial 05: Синхронизация видео

Код - смотрите исходник : tutorial05.c

PTS и DTS

audio и video потоки имеют информацию о том , когда и с какой скоростью их проигрывать. Audio streams имеют т.н. sample rate, video streams имеют т.н. frames per second value. Если просто выводить фреймы с частотой frame rate, есть шанс , что произойдет рассинхронизация со звуком . пакеты внутри потока имеют 2 характеристики - decoding time stamp (DTS) и presentation time stamp (PTS). Некоторые форматы, такие как MPEG, используют т.н. "B" frames ("bidirectional"). Есть еще 2 типа фреймов - "I" и "P" ("intra" и "predicted"). I frames включают всю картинку . P frames могут включать фрагмент и их содержание зависит от контента предыдущего фрейма. B - то же , что и P , но его содержимое так же зависит от контента следующего фрейма. В зависимости от этого вызов функции avcodec_decode_video может быть различным .

Представим , что в ролике фреймы идут в последовательности : I B B P. Для вывода P мы должны знать содержимое предыдущего B . decoding timestamp tells - время декодирования , presentation time stamp - время вывода . Тогда:

   PTS: 1 4 2 3
    DTS: 1 2 3 4
 Stream: I P B B
 
PTS и DTS различаются тогда , когда в потоке есть фреймы типа B .

При получении пакета av_read_frame(), в нем будет информация о PTS и DTS . Если мы знаем PTS , мы знаем , когда выводить фрейм . Из фрейма , полученного из avcodec_decode_video() , мы получаем AVFrame, в котором нет PTS . ffmpeg сам устанавливает порядок пакетов так , что DTS пакета , пришедший из avcodec_decode_video() , будет всегда таким же, что и PTS .

Мы можем сами установить очередность пакетов . Мы запоминаем PTS первого пакета во фрейме: он будет равен PTS последнего фрейма. Если поток не дает нам DTS, мы используем PTS. Для определения того , какой пакет является первым во фрейме , используем avcodec_decode_video() . Функция avcodec_decode_video() выделяет буфер для фрейма. Мы определим новую функцию , которая запомнит pts пакета.

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

Мы показываем фрейм и рефрешим его до тех пор , пока не наступает PTS следующего фрейма.

При синхронизации есть 3 подхода : сихронизация аудио к видео , сихронизация видео к аудио , сихронизация аудио и видео по внешнему таймеру . Мы выбираем второй вариант .

Вычисление frame PTS

Код нашего video thread - место , в котором пакеты будут укладываться в очередь. Для вычисления PTS фрейма используем avcodec_decode_video. Вначале получим 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_video(is->video_st->codec, 
                                 pFrame, &frameFinished, 
                 packet->data, packet->size);
     if(packet->dts != AV_NOPTS_VALUE) {
       pts = packet->dts;
     } else {
       pts = 0;
     }
     pts *= av_q2d(is->video_st->time_base);
 

Нам также может пригодиться PTS первого пакета во фрейме . Мы создаем собственные функции :

int get_buffer(struct AVCodecContext *c, AVFrame *pic);
 void release_buffer(struct AVCodecContext *c, AVFrame *pic);
 
Каждый раз , когда мы получаем пакет , мы храним PTS в глобальную переменную.
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;
 
 /* These are called whenever we allocate a frame
  * buffer. We use this to store the global_pts in
  * a frame at the time it is allocated.
  */
 int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) {
   int ret = avcodec_default_get_buffer(c, pic);
   uint64_t *pts = av_malloc(sizeof(uint64_t));
   *pts = global_video_pkt_pts;
   pic->opaque = pts;
   return ret;
 }
 void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) {
   if(pic) av_freep(&pic->opaque);
   avcodec_default_release_buffer(c, pic);
 }
 
avcodec_default_get_buffer и avcodec_default_release_buffer - функции по умолчанию для ffmpeg . av_freep - функции управления памятью

После (stream_component_open), мы говорим ffmpeg :

    codecCtx->get_buffer = our_get_buffer;
     codecCtx->release_buffer = our_release_buffer;
 
Сохраним PTS в глобальную переменную :
  for(;;) {
     if(packet_queue_get(&is->videoq, packet, 1) < 0) {
       // means we quit getting packets
       break;
     }
     pts = 0;
 
     // Save global pts to be stored in pFrame in first call
     global_video_pkt_pts = packet->pts;
     // Decode video frame
     len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, 
                 packet->data, packet->size);
     if(packet->dts == AV_NOPTS_VALUE 
        && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) {
       pts = *(uint64_t *)pFrame->opaque;
     } else if(packet->dts != AV_NOPTS_VALUE) {
       pts = packet->dts;
     } else {
       pts = 0;
     }
     pts *= av_q2d(is->video_st->time_base);
 
Используется тип int64 для PTS. Это т.н. timestamp . Например , если в потоке частота фреймов - frames per second - равна 24, PTS = 42 говорит о том , что это 42-й фрейм.

time_base - это 1/24 , и для получения PTS в секундах, умножаем 42 на time_base.

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

Определим функцию synchronize_video , которая будет менять PTS . Мы используем video_clock value , которую добавим в основную структуру.

typedef struct VideoState {
   double          video_clock; ///
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:

    // Did we get a video frame?
     if(frameFinished) {
       pts = synchronize_video(is, pFrame, pts);
       if(queue_picture(is, pFrame, pts) < 0) {
     break;
       }
     }
 
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 ...
   }
 
Мы получили картинку , теперь посмотрим на video refreshing function. Рефреш делается через 80ms.

Для синхронизации аудио заведем audio clock , чтобы видео не отставало от звука и не забегало вперед.

Функция get_audio_clock дает нам audio clock. Мы добавляем паузу к 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);
   }
 }
 

Синхронизация звука

Мы устанавливаем audio clock равный 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);
 

Функция get_audio_clock :

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 -lz -lm`sdl-config --cflags --libs`
 

Tutorial 06:

Код - смотрите в ахиве : tutorial06.c

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

Реализуем синхронизацию audio к video с использованием внутреннего видео-таймера.

Видео-таймер - это разница во времени для текущего кадра. Фактически он будет равен PTS текущего фрейма . Текущее значение таймера будет равно PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set). Оно похоже на то , что мы делали с помощью get_audio_clock.

Добавим в структуру double video_current_pts и int64_t video_current_pts_time. Изменение таймера будет происходить в функции video_refresh_timer :

void video_refresh_timer(void *userdata) {
 
   /* ... */
 
   if(is->video_st) {
     if(is->pictq_size == 0) {
       schedule_refresh(is, 1);
     } else {
       vp = &is->pictq[is->pictq_rindex];
 
       is->video_current_pts = vp->pts;
       is->video_current_pts_time = av_gettime();
 
Инициализация в stream_component_open:
    is->video_current_pts_time = av_gettime();
 
Считывание:
double get_video_clock(VideoState *is) {
   double delta;
 
   delta = (av_gettime() - is->video_current_pts_time) / 1000000.0;
   return is->video_current_pts + delta;
 }
 

Напишем новую wrapper function, get_master_clock , которая проверяет переменную av_sync_type и вызывает get_audio_clock, get_video_clock. Можно использовать встроенный таймер get_external_clock:

enum {
   AV_SYNC_AUDIO_MASTER,
   AV_SYNC_VIDEO_MASTER,
   AV_SYNC_EXTERNAL_MASTER,
 };
 
 #define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER
 
 double get_master_clock(VideoState *is) {
   if(is->av_sync_type == AV_SYNC_VIDEO_MASTER) {
     return get_video_clock(is);
   } else if(is->av_sync_type == AV_SYNC_AUDIO_MASTER) {
     return get_audio_clock(is);
   } else {
     return get_external_clock(is);
   }
 }
 main() {
 ...
   is->av_sync_type = DEFAULT_AV_SYNC_TYPE;
 ...
 }
 

Синхронизация аудио к видео-таймеру : мы сравниваем текущий фрейм с видео-таймером . Выполняем функцию synchronize_audio для каждого audio samples. Аудио-процесс выполняется гораздо чаще , нежели работа с видео-пакетами. Нужно минимизировать количество вызовов функции synchronize_audio . Первый вызов будет с интервалом в 40ms, следующий в 50ms, и т.д. Введем фрактальный коэффициент c, и суммируем разницу: diff_sum = new_diff + diff_sum*c. Далее вычисляем avg_diff = diff_sum * (1-c).

Функция:

/* Add or subtract samples to get a better sync, return new
    audio buffer size */
 int synchronize_audio(VideoState *is, short *samples,
               int samples_size, double pts) {
   int n;
   double ref_clock;
   
   n = 2 * is->audio_st->codec->channels;
   
   if(is->av_sync_type != AV_SYNC_AUDIO_MASTER) {
     double diff, avg_diff;
     int wanted_size, min_size, max_size, nb_samples;
     
     ref_clock = get_master_clock(is);
     diff = get_audio_clock(is) - ref_clock;
 
     if(diff < AV_NOSYNC_THRESHOLD) {
       // accumulate the diffs
       is->audio_diff_cum = diff + is->audio_diff_avg_coef
     * is->audio_diff_cum;
       if(is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
     is->audio_diff_avg_count++;
       } else {
     avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
 
        /* Shrinking/expanding buffer code.... */
 
       }
     } else {
       /* difference is TOO big; reset diff stuff */
       is->audio_diff_avg_count = 0;
       is->audio_diff_cum = 0;
     }
   }
   return samples_size;
 }
 

Вычислим , сколько audio samples нам нужно добавить :

if(fabs(avg_diff) >= is->audio_diff_threshold) {
   wanted_size = samples_size + 
   ((int)(diff * is->audio_st->codec->sample_rate) * n);
   min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX)
                              / 100);
   max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) 
                              / 100);
   if(wanted_size < min_size) {
     wanted_size = min_size;
   } else if (wanted_size > max_size) {
     wanted_size = max_size;
   }
 
audio_length * (sample_rate * # of channels * 2) - это число сэмплов в audio_length , измеряемых в секундах .

Коррекция числа сэмплов

Функция synchronize_audio возвращает размер сэмпла в байтах . Размер сэмпла мы можем изменить на wanted_size.

if(wanted_size < samples_size) {
   /* remove samples */
   samples_size = wanted_size;
 } else if(wanted_size > samples_size) {
   uint8_t *samples_end, *q;
   int nb;
 
   /* add samples by copying final samples */
   nb = (samples_size - wanted_size);
   samples_end = (uint8_t *)samples + samples_size - n;
   q = samples_end + n;
   while(nb > 0) {
     memcpy(q, samples_end, n);
     q += n;
     nb -= n;
   }
   samples_size = wanted_size;
 }
 
Функция :
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 {
     audio_size = synchronize_audio(is, (int16_t *)is->audio_buf,
                        audio_size, pts);
     is->audio_buf_size = audio_size;
 
Мы вставили вызов synchronize_audio.

if(is->av_sync_type != AV_SYNC_VIDEO_MASTER) {
   ref_clock = get_master_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;
     }
   }
 }
 
Компиляция:
gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs`
 

Tutorial 07: Поиск

Код - смотрите в архиве : tutorial07.c

Для поиска мы воспользуемся функцией av_seek_frame

Реализуем "перемотку" вперед-назад в пределах от 10 до 60 секунд. Вызов av_seek_frame будет происходить непосредственно в главном цикле decode_thread . Для поиска в главную структуру добавим поля :

  int             seek_req;
   int             seek_flags;
   int64_t         seek_pos;
 

Добавим обработку клавиатуры :

  for(;;) {
     double incr, pos;
 
     SDL_WaitEvent(&event);
     switch(event.type) {
     case SDL_KEYDOWN:
       switch(event.key.keysym.sym) {
       case SDLK_LEFT:
     incr = -10.0;
     goto do_seek;
       case SDLK_RIGHT:
     incr = 10.0;
     goto do_seek;
       case SDLK_UP:
     incr = 60.0;
     goto do_seek;
       case SDLK_DOWN:
     incr = -60.0;
     goto do_seek;
       do_seek:
     if(global_video_state) {
       pos = get_master_clock(global_video_state);
       pos += incr;
       stream_seek(global_video_state, 
                       (int64_t)(pos * AV_TIME_BASE), incr);
     }
     break;
       default:
     break;
       }
       break;
 
Используется событие SDL_KEYDOWN . Новое время вычисляется с помощью функции get_master_clock . Далее вызываем функцию stream_seek для установки seek_pos . Полученное время конвертится в т.н. внутренний формат avcodec timestamp . Позиция pos = 2 секундам будет соответствовать timestamp = 2000000.

Функция stream_seek :

void stream_seek(VideoState *is, int64_t pos, int rel) {
 
   if(!is->seek_req) {
     is->seek_pos = pos;
     is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0;
     is->seek_req = 1;
   }
 }
 
Функция av_seek_frame берет в качестве параметров format context, stream, timestamp, а также набор флагов . Поиск будет осуществляться по заданному timestamp . timestamp - это time_base , который берется из stream , передаваемого в функцию.

if(is->seek_req) {
   int stream_index= -1;
   int64_t seek_target = is->seek_pos;
 
   if     (is->videoStream >= 0) stream_index = is->videoStream;
   else if(is->audioStream >= 0) stream_index = is->audioStream;
 
   if(stream_index>=0){
     seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q,
                       pFormatCtx->streams[stream_index]->time_base);
   }
   if(av_seek_frame(is->pFormatCtx, stream_index, 
                     seek_target, is->seek_flags) < 0) {
     fprintf(stderr, "%s: error while seeking\n",
             is->pFormatCtx->filename);
   } else {
      /* handle packet queues... more later... */
 
 
av_rescale_q(a,b,c) - функция , переводящая timestamp из одного формата в другой по формуле a*b/c . AV_TIME_BASE_Q - фрактальная версия AV_TIME_BASE.

AV_TIME_BASE * time_in_seconds = avcodec_timestamp и AV_TIME_BASE_Q * avcodec_timestamp = time_in_seconds где AV_TIME_BASE_Q - это обьект AVRational .

Очистка буферов

avcodec имеет внутренние буфера , которые нужно очищать.

Напишем функцию для очистки очереди пакетов - packet queue. В очередь ложится спец-пакет , при достижении которого все очищается :

static void packet_queue_flush(PacketQueue *q) {
   AVPacketList *pkt, *pkt1;
 
   SDL_LockMutex(q->mutex);
   for(pkt = q->first_pkt; pkt != NULL; pkt = pkt1) {
     pkt1 = pkt->next;
     av_free_packet(&pkt->pkt);
     av_freep(&pkt);
   }
   q->last_pkt = NULL;
   q->first_pkt = NULL;
   q->nb_packets = 0;
   q->size = 0;
   SDL_UnlockMutex(q->mutex);
 }
 

Создадим спец-пакет :

AVPacket flush_pkt;
 
 main() {
   ...
   av_init_packet(&flush_pkt);
   flush_pkt.data = "FLUSH";
   ...
 }
 
Помещаем его в очередь :
  } else {
     if(is->audioStream >= 0) {
       packet_queue_flush(&is->audioq);
       packet_queue_put(&is->audioq, &flush_pkt);
     }
     if(is->videoStream >= 0) {
       packet_queue_flush(&is->videoq);
       packet_queue_put(&is->videoq, &flush_pkt);
     }
   }
   is->seek_req = 0;
 }
 
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
 
   AVPacketList *pkt1;
   if(pkt != &flush_pkt && av_dup_packet(pkt) < 0) {
     return -1;
   }
 
Делаем вызов avcodec_flush_buffers сразу после packet_queue_get:
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
       return -1;
     }
     if(packet->data == flush_pkt.data) {
       avcodec_flush_buffers(is->audio_st->codec);
       continue;
     }
 

Компиляция :

gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs`
 

Tutorial 08: Масштабирование

Код - смотрите в архиве : tutorial08.c

libswscale

libswscale - библиотека интерфейсов для масштабирования картинками. Вместо img_convert мы будем использовать новый интерфейс.

Базовая функция называется sws_scale. Вначале нужно проинициализировать обьект SwsContext. Манипуляции с этим обьектом напоминают выражения SQL или regexp в Python. Для подготовки context, используем функцию sws_getContext , передаем в нее высоту и ширину , формат и т.д :

#include <ffmpeg/swscale.h> // include the header!
 
 int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
 
   static struct SwsContext *img_convert_ctx;
   ...
 
   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
     if(img_convert_ctx == NULL) {
       int w = is->video_st->codec->width;
       int h = is->video_st->codec->height;
       img_convert_ctx = sws_getContext(w, h, 
                         is->video_st->codec->pix_fmt, 
                         w, h, dst_pix_fmt, SWS_BICUBIC, 
                         NULL, NULL, NULL);
       if(img_convert_ctx == NULL) {
     fprintf(stderr, "Cannot initialize the conversion context!\n");
     exit(1);
       }
     }
     sws_scale(img_convert_ctx, pFrame->data, 
               pFrame->linesize, 0, 
               is->video_st->codec->height, 
               pict.data, pict.linesize);
 

Компиляция :

gcc -o tutorial08 tutorial08.c -lavutil -lavformat -lavcodec -lz -lm `sdl-config --cflags --libs`
 
Итак , мы написали видео-плеер , в котором менее 1000 строк на C .
Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье
Владимир
  Спасибо за тутор, печально только, что он сильно устарел :(
2012-01-31 17:05:57
Денис
  Здравствуйте. Нужно помочь с автоматизацией добавления логотипа в видео. Далее видео отправляем в Make.com Кто может помочь с такой задачей ? Моя почта 7418823@mail.ru
2024-12-12 12:59:53
Яковлев Сергей
  ffmpeg -i in.mp4 -framerate 300001001 -loop 1 -i logo.png -filter_complex
  "[1:v] fade=out:st=30:d=1:alpha=1 [ov]; [0:v][ov] overlay=10:10 [v]" -map "[v]"
  -map 0:a -c:v libx264 -c:a copy -shortest out.mp4

Здесь:
  in.mp4 - исходное видео
  out.mp4 - видео после обработки
  logo.png - картинка
  Если вместо картинки видео, то параметр -framerate не нужен
  overlay=10:10 - координаты, в которые будет вставлена картинка

2024-12-12 13:07:52