iridiumtechru.github.io:$ cd ~

Реализация видео в VoIP приложении с использованием ffmpeg

Использование видео кодеков ffmpeg на С++.

Зачем?

Обычно в SIP софтфонах используются специализированые библиотеки (pjsip, linphone), но я когдато принял решение делать самостоятельную реализацию и однажды пришло время передавать видео.

Инициализация кодеков

  • Подключим заголовки
extern "C" {
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/rtpdec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include "libavutil/opt.h"
}
  • Функция открытия кодека
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

avctx - контекст кодека. Создается для кодера и под декодера отдельно.

codec - энкодер или декордер. ищется в списке поддерживаемых.

options - опции для инициализации контекста.

  • Создание декодера
AVCodec* decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext* context = avcodec_alloc_context3(decoder);
avcodec_open2(context, decoder, NULL);
  • Сразу все работать не будет нужно указать параметры контексту.
/**
   Context init
*/
void InitContext(AVCodecContext** out_context_ptr, AVCodec* in_codec, int bitrate, int width, int height)
{
   AVCodecContext* context = avcodec_alloc_context3(in_codec);

   l_pContext->codec_type = AVMEDIA_TYPE_VIDEO;
   l_pContext->codec_id = in_pCodec->id;

   l_pContext->bit_rate = bitrate; // default bitrate = 90000

   l_pContext->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
   l_pContext->workaround_bugs = FF_BUG_AUTODETECT;

   l_pContext->pix_fmt = AV_PIX_FMT_YUV420P;

   // 176 × 144, 352 × 288, 704 × 288, 704 × 576, 1408 × 1152
   l_pContext->width     = width;
   l_pContext->height    = height;

   *in_pCodecContext = l_pContext;
}

AVCodec* decoder = avcodec_find_decoder(AV_CODEC_ID_H264);

if(!decoder)
    return false;

AVCodecContext* context = NULL;
InitContext(&context, decoder, 90000, 352, 288);
if(context && (avcodec_open2(context, decoder, NULL) < 0)
    return true;// initalize ok

FF_COMPLIANCE_UNOFFICIAL - позволяет энкодерам генерировать данные не по стандарту.

FF_BUG_AUTODETECT - позволяет автоматически искать ошибки в данных.

bit_rate - битрейт задается приблизительно, если он динамический (vbr), или жестко, если постоянный (cbr). Битрейт определяется энкодерам.

  • Создание rtp демуксера.

Можно разбирать rtp пакеты самому, но я использовал встроенные функции.

    AVStream* st = (AVStream*)av_mallocz(sizeof(AVStream));
    st->start_time = AV_NOPTS_VALUE;
    st->duration = AV_NOPTS_VALUE;
    st->index = 0;
    st->codec = decoder_context;

    // демуксер RTP пакетов
    rtp_demuxer = ff_rtp_parse_open(NULL, st, payload_type, 0);

    // поиск хендлера
    const char* handler_name = "";
    switch(l_eCID)
    {
    case AV_CODEC_ID_H263P:
    {
        handler_name = "H263-1998";
        rtp_demuxer->handler = ff_rtp_handler_find_by_name(handler_name, AVMEDIA_TYPE_VIDEO);
        break;
    }
    case AV_CODEC_ID_H264:
    {
        handler_name = "H264";
        rtp_demuxer->handler = ff_rtp_handler_find_by_name(handler_name, AVMEDIA_TYPE_VIDEO);
        break;
    }

Декодирование пакета

Пакеты сохраняются в массив и обрабатываются в главном потоке.

for(int i = 0; i < packets.size(); i++)
{
    int got_frame  = 0;
    raw_rtp_packet_t* rtp_packet = packets.at(i);

    AVPacket packet;
    memset(&packet, 0, sizeof(packet));
    av_init_packet(&packet);

    // парсим rtp пакет
    if(m_rtp_demuxer && -1 != ff_rtp_parse_packet(rtp_demuxer, &packet, &rtp_packet->data, rtp_packet->size))
    {
        // проверим изменения метки времени. и обработаем данные уже помещенные в буфер
         if(last_ts && rtp_demuxer->timestamp != last_ts)
         {
            AVPacket frame_packet;
            memset(&frame_packet, 0, sizeof(AVPacket));
            av_init_packet(&frame_packet);

            frame_packet.data = m_jitter_buf->GetDataPtr();
            frame_packet.size = m_jitter_buf->Size();
            
            int decoded_data_size = avcodec_decode_video2(m_decoder_context, m_frame, &got_frame, &frame_packet);
            
            // пробуем декодировать фрейм
            if(m_frame && 0 < decoded_data_size)
            {
               if(got_frame)
               {
                   // обработка изображения
                   // ...
                   //sws_scale(m_pSWSContext, m_pFrame->data, m_pFrame->linesize, 0, m_decoder_context->height, l_aSource, l_aStride);
               }
            }

            av_frame_unref(m_frame);
            av_free_packet(&frame_packet);
            m_jitter_buffer->Clear();           // очищаем буфер
         }

         if(m_rtp_demuxer->seq != m_last_ts + 1)
            printf("Packets is mixed %d %d", m_rtp_demuxer->seq, m_last_seq);
         m_last_seq = m_pRTPDemuxer->seq;

         // один фрейм может состоять из нескольких пакетов. добавим пакет в буфер 
         m_jitter_buffer->Add(packet.data, packet.size);
         // сохраняем timestamp
         m_last_ts = rtp_demuxer->timestamp;
      }

      av_free_packet(&packet);
    }
}

Кодирование пакета

Изображения приходят в “сыром” виде с камеры или другого устройства. Вероятно в отдельном потоке.

#define RTP_VIDEO_PAYLOAD_SIZE   1368 // MTU
#define FPS 25

void SendFrame(unsigned int in_u32Width, unsigned int in_u32Height, unsigned int in_u32Pitch, void* in_pBuffer)
{
    m_lock->Lock();
    av_image_alloc(m_packet_frame->data, m_packet_frame->linesize, m_encoder_context->width, m_encoder_context->height, m_encoder_context->pix_fmt, 32);

    AVFrame* picture_frame = av_frame_alloc();

    // заполняем фрейм пришедшими данными
    avpicture_fill((AVPicture*)picture_frame, (u8*)in_pBuffer, l_eFmt, in_u32Width, in_u32Height);
    picture_frame->width = m_encoder_context->width;
    picture_frame->height = m_encoder_context->height;
    picture_frame->format = l_eFmt;

    // скалирование изображения под формат передачи
    sws_scale(m_pSWSContext2, picture_frame->data, picture_frame->linesize, 0, in_u32Height, m_packet_frame->data, m_packet_frame->linesize);

    m_packet_frame->width = m_decoder_width;
    m_packet_frame->height = m_decoder_height;
    m_packet_frame->format = AV_PIX_FMT_YUV420P;
    m_packet_frame->pts = m_encoder_pts++;        // инкрементируем счетчик кадров

    AVPacket packet;
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    int got_packet = 0;
    int l_s32Encoded = avcodec_encode_video2(m_encoder_context, &packet, m_packet_frame, &got_packet);
    if(0 < l_s32Encoded)
    {
        if(got_packet)
        {
            int whole_size = packet.size;                       // размер всего фрейма
            unsigned int delta_ts = m_bitrate / FPS;            // время между кадрами
            unsigned char* marker = 0;                          // конец пакета для rtp
            unsigned char* data = (unsigned char*)packet.data;  // указатель на данные фрейма
            u8* end = data + whole_size;                        // указатель на конец данных фрейма

            u8 packet_buf[RTP_VIDEO_PAYLOAD_SIZE * 2];          // буфер для одного пакета
            memset(packet_buf, 0 , sizeof(packet_buf));
            size_t size = 0;                                    // размер данных помещенных в буфер пакетизатором
            size_t shift = 0;                                   // смещение в буфере фрейма до следующего пакета

            do
            {
                if(m_encoder_context->codec_id == AV_CODEC_ID_H263)
                    shift = h263_rfc2190_packetization(data, whole_size, packet_buf, &size);
                else if(m_encoder_context->codec_id == AV_CODEC_ID_H263P)
                    shift = h263_rfc4629_packetization(data, whole_size, packet_buf, &size);
                else if(m_encoder_context->codec_id == AV_CODEC_ID_H264)
                    shift = h264_rfc6185_packetization(data, whole_size, packet_buf, &size);
                else
                    break;

                // если пакет последний в последовательности
                marker = (whole_size == shift) ? 1 : 0;

                if(size)
                {
                    // отправляем пакет по rtp
                    // ...
                    // m_device->SendVideoPacket(packet_buf, size, marker, delta_ts);
                }

                if(!shift)
                {
                    printf("Bad shift size");
                    shift = whole_size;
                }

                delta_ts = 0;       // все последующие пакеты этого фрейма идут с тем же timestamp
                data += shift;
                whole_size = end - data;
            } while(whole_size > 0);

            av_free_packet(&packet);
        }
    }

    av_freep(&m_packet_frame->data[0]);
    av_frame_unref(m_packet_frame);
    av_frame_free(&picture_frame);
    m_lock->UnLock();
}

  • Пакетизаторы были честно выдраны из pjsip и адаптированы под себя.
size_t h263_rfc2190_packetization(unsigned char* data_buf, size_t buf_size, unsigned char* out_data, size_t* out_size)
{
   size_t size = (buf_size < RTP_VIDEO_PAYLOAD_SIZE) ? buf_size : RTP_VIDEO_PAYLOAD_SIZE;

   out_data[0] = 0x00;
   out_data[1] = 0x70;
   out_data[2] = 0x00;
   out_data[3] = 0x00;

   memcpy(out_data + 4, data_buf, size);
   *out_size = size + 4;

   return size;
}
size_t h263_rfc4629_packetization(unsigned char* data_buf, size_t buf_size, unsigned char* out_data, size_t* out_size)
{
   size_t size = (buf_size < RTP_VIDEO_PAYLOAD_SIZE) ? buf_size : RTP_VIDEO_PAYLOAD_SIZE;

   if(data_buf[0] == 0x00 && data_buf[1] == 0x00)
   {
      memcpy(out_data, data_buf, size);
      out_data[0] = 0x04;
      *out_size = size;
   }
   else
   {
      out_data[0] = 0x00;
      out_data[1] = 0x00;

      memcpy(out_data + 2, data_buf, size);
      *out_size = size + 2;
   }

   return size;
}
#define TYPE_FU_A             0x1C        // код FU-A заголовка для h264 пакетизатора
#define TYPE_STAP_A           0x18        // код STAP-A заголовка для h264 пакетизатора
#define HEADER_SIZE_FU_A      2           // размер FU-A заголовка
#define HEADER_SIZE_STAP_A    3           // размер STAP-A заголовка
#define MAX_NALS_IN_AGGR      32          // максимальное количество NAL заголовков в пакете

static u8* find_next_nal_unit(u8* start, u8* end)
{
   u8 *p = start;

   // Simply lookup "0x000001" pattern
   while(p <= end - 3 && (p[0] || p[1] || p[2] != 1))
      ++p;

   if(p > end - 3)
      // No more NAL unit in this bitstream
      return NULL;

   // Include 8 bits leading zero
   if(p > start && *(p - 1) == 0)
      return (p - 1);

   return p;
}

size_t h264_rfc6185_packetization(unsigned char* data_buf, size_t buf_size, unsigned char* out_data, size_t* out_size)
{
   u8* p = data_buf;
   u8* nal_octet = NULL;
   u8* nal_start = NULL;
   u8* nal_end = NULL;
   u8* end = data_buf + buf_size;

   u8 NRI, TYPE;
   u32 l_u32Size = 0;

   unsigned int nal_cnt = 0;

   if(buf_size > 4)
      nal_start = find_next_nal_unit(p, p + 4);
   if(nal_start)
   {
      // Get NAL unit octet pointer
      while(*nal_start++ == 0);
      nal_octet = nal_start;
   }
   else
      nal_start = p; // This NAL unit is being fragmented

   // Get end of NAL unit
   p = nal_start + RTP_VIDEO_PAYLOAD_SIZE + 1;
   if(p > end)
      p = end;

   nal_end = find_next_nal_unit(nal_start, p);
   if(!nal_end)
      nal_end = p;

   if(!nal_octet || nal_end - nal_start > RTP_VIDEO_PAYLOAD_SIZE)
   {
      if(nal_octet)
      {
         // We have NAL unit octet, so this is the first fragment
         NRI = (*nal_octet & 0x60) >> 5;
         TYPE = *nal_octet & 0x1F;

         // Skip nal_octet in nal_start to be overriden by FU header
         ++nal_start;
      }
      else
      {
         // Not the first fragment, get NRI and NAL unit type from the previous fragment.
         p = nal_start - RTP_VIDEO_PAYLOAD_SIZE;
         NRI = (*p & 0x60) >> 5;
         TYPE = *(p + 1) & 0x1F;
      }

      p = nal_start - HEADER_SIZE_FU_A;
      *p = (NRI << 5) | TYPE_FU_A;
      ++p;
      *p = TYPE;

      l_u32Size = (nal_end - nal_start + HEADER_SIZE_FU_A);

      if(nal_octet)
         *p |= (1 << 7); // S bit flag = start of fragmentation
      if(l_u32Size <= RTP_VIDEO_PAYLOAD_SIZE)
         *p |= (1 << 6); // E bit flag = end of fragmentation

      l_u32Size = (l_u32Size > RTP_VIDEO_PAYLOAD_SIZE) ? RTP_VIDEO_PAYLOAD_SIZE : l_u32Size;
      memcpy(out_data, nal_start - HEADER_SIZE_FU_A, l_u32Size);
      *out_size = l_u32Size;

      return ((nal_start - HEADER_SIZE_FU_A) + l_u32Size) - data_buf;
   }
   else
   {
      if((nal_end != end) && (nal_end - nal_start + HEADER_SIZE_STAP_A) < RTP_VIDEO_PAYLOAD_SIZE)
      {
         int total_size;
         //unsigned int nal_cnt = 1;
         nal_cnt = 1;
         u8* nal[MAX_NALS_IN_AGGR];
         size_t nal_size[MAX_NALS_IN_AGGR];
         u8 NRI;

         // Init the first NAL unit in the packet
         nal[0] = nal_start;
         nal_size[0] = nal_end - nal_start;
         total_size = (int)nal_size[0] + HEADER_SIZE_STAP_A;
         NRI = (*nal_octet & 0x60) >> 5;

         while(nal_cnt < MAX_NALS_IN_AGGR) 
         {
            u8* tmp_end = NULL;

            // Find start address of the next NAL unit
            p = nal[nal_cnt - 1] + nal_size[nal_cnt - 1];
            while(*p++ == 0);
            nal[nal_cnt] = p;

            // Find end address of the next NAL unit
            tmp_end = p + (RTP_VIDEO_PAYLOAD_SIZE - total_size);
            if(tmp_end > end)
               tmp_end = end;
            p = find_next_nal_unit(p + 1, tmp_end);
            if(p)
               nal_size[nal_cnt] = p - nal[nal_cnt];
            else
               break;

            // Update total payload size (2 octet NAL size + NAL)
            total_size += (2 + (int)nal_size[nal_cnt]);
            if(total_size <= RTP_VIDEO_PAYLOAD_SIZE) 
            {
               u8 tmp_nri;

               // Get maximum NRI of the aggregated NAL units
               tmp_nri = (*(nal[nal_cnt] - 1) & 0x60) >> 5;
               if(tmp_nri > NRI)
                  NRI = tmp_nri;
            }
            else
               break;

            ++nal_cnt;
         }

         // Only use STAP-A when we found more than one NAL units
         if(nal_cnt > 1)
         {
            unsigned int i;

            // Init STAP-A NAL header (F+NRI+TYPE)
            p = nal[0] - HEADER_SIZE_STAP_A;
            *p++ = (NRI << 5) | TYPE_STAP_A;

            // Append all populated NAL units into payload (SIZE+NAL)
            for(i = 0; i < nal_cnt; ++i) {
               // Put size (2 octets in network order)
               *p++ = (u8)(nal_size[i] >> 8);
               *p++ = (u8)(nal_size[i] & 0xFF);

               // Append NAL unit, watchout memmove()-ing bitstream!
               if(p != nal[i])
                  memmove(p, nal[i], nal_size[i]);
               p += nal_size[i];
            }

            // 
            u8* payload = nal[0] - HEADER_SIZE_STAP_A;
            l_u32Size = p - payload;
            memcpy(out_data, payload, l_u32Size);
            *out_size = l_u32Size;
            return (u32)(nal[nal_cnt - 1] + nal_size[nal_cnt - 1] - data_buf);;
         }
      }
   }
   
   // Single NAL unit packet
   l_u32Size = nal_end - nal_start;
   memcpy(out_data, nal_start, l_u32Size);
   *out_size = l_u32Size;
   return (u32)(nal_end - data_buf);
}
  • Флаги и настройки контекста h264

Для работы кодека h264 опытным путем добыты дополнительные настройки. Без этих флагов изображение течет и ломается.


AVCodecContext* m_decoder_context;
AVCodecContext* m_encoder_context;

AVCodecID codec_id = AV_CODEC_ID_H264;
int bitrate = 90000;
int w = 352;
int h = 288;
decoder = avcodec_find_decoder(codec_id);
encoder = avcodec_find_encoder(codec_id);

InitContext(&m_decoder_context, decoder, bitrate, w, h);
InitContext(&m_encoder_context, encoder, bitrate, w, h);

if(m_decoder_context)
{
    if(codec_id == AV_CODEC_ID_H264)
    {
        m_decoder_context->flags |= CODEC_FLAG_UNALIGNED;       // обрабатывает невыровнянные кадры
                
        if(decoder->capabilities & CODEC_CAP_TRUNCATED)         // обрабатывает "битые" кадры
            m_decoder_context->flags |= CODEC_FLAG_TRUNCATED;

        m_decoder_context->time_base= av_make_q(1, bitrate);     
    }
}

if(m_encoder_context)
{
    int level = 13;
    m_encoder_context->time_base= av_make_q(1, FPS);      // фпс
    m_encoder_context->gop_size = 10;                     // частота опорных кадров
    
    if(codec_id == AV_CODEC_ID_H264)
    {
        m_encoder_context->max_b_frames = 0;            // не отправлять B фреймы
        m_encoder_context->level = level;               // уровень качества
        m_encoder_context->refs = 1;                    // количество кадров "ссылок"
        m_encoder_context->thread_count = 0;            // количество потоков для кодирования. можно увеличить выставив thread_type
        //m_encoder_context->thread_type = FF_THREAD_SLICE;

        // https://ru.wikipedia.org/wiki/H.264
        added = av_opt_set(m_encoder_context->priv_data, "profile", "baseline", 0);      // профиль базовый для мобильных устройств
        added = av_opt_set(m_encoder_context->priv_data, "preset", "fast", 0);           // скорость кодирования. обратна пропорциональна качеству
        added = av_opt_set(m_encoder_context->priv_data, "tune", "zerolatency", 0);      // минимальная задержка. обязательно для sip 
    }
}

Также могут быть проблемы с размером буфера на UDP сокете. Если размер фрейма большой, то при получении по UDP весь фрейм может не поместиться в системный буфер и часть пакетов будут сброшены. При этом изображение будет искажаться.

int val = 65535;
if(setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (const char*)&val, sizeof(val)) != SOCKET_ERROR)
    status = true;

Ссылки

  1. pjsip
  2. linphone


Оставить комментарий через Telegram