Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
      Languages 
      Kernels 
      Packages 
      Books 
      Tests 
      OS 
      Forum 
      Математика 
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 Secure Programming for Li...6613 
 Linux Kernel 2.6...5375 
 Trees...1248 
 Максвелл 3...1173 
 Go Web ...1158 
 William Gropp...1149 
 Clickhouse...1035 
 Ethreal 4...1034 
 Ethreal 1...1033 
 Ethreal 3...1032 
 Rodriguez 6...1025 
 Ext4 FS...1023 
 Gary V.Vaughan-> Libtool...1010 
 Steve Pate 1...1000 
 Assembler...978 
 C++ Patterns 3...966 
 Ulrich Drepper...942 
 DevFS...890 
 MySQL & PosgreSQL...874 
 Стивенс 9...850 
 
  01.01.2024 : 3621733 посещений 

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