視訊會議中或者錄播中使用RTP協議接收h264視訊

qianbo_0423發表於2012-11-18

此程式文章獻給剛進公司的需要幫助的程式設計師,

       說明:1 該程式碼在windows上執行,用vs2010編譯。

                   2 該程式碼要能解決移植的問題。 

                   3 rtp實時傳輸協議可以使用udp,也可以使用tcp協議

       首先,為了減小程式的難度,說明使用的庫解碼庫為ffmpeg,刷視訊資料的方法可以使用

                  1 SDL庫 ,到sdl的原始碼網站中下載並編譯

                  2 直接使用gdi, 並且解決翻轉問題。

                  3 使用opengl或者direct3d, 或者directdraw。

     基礎知識:

      A  首先RTP 包結構一般為12位元組,傳輸層協議中UDP協議和TCP協議是可選的,都可以用,多數使用了UDP協議,如果要掃盲,請連結到基維百 

          科http://zh.wikipedia.org/wiki/%E5%AE%9E%E6%97%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE,使用tcp協議的好處是和rtsp協議

          相關聯的,涉及到nat轉換 路由方面的知識,我後面會講,而UDP協議在h264等視訊傳送的時候要注意的是分包問題,主要是MTU最大傳輸單元的問題,h264的

          nalu如果超過最大傳輸單元,必須分割傳送。

      B ffmpeg1.0 已經及其優秀,包含ffmpeg庫不要忘了 extern “C”extern "C"{#include #include #include #include }為了使得快速開發出一個原型,使用boost的asio庫,

         可以節省一些時間。並且使用回撥函式來解碼和刷屏,以下是使用asio庫來接收網路的包,預設使用了組播地址,也就是說假設該h264視訊會傳送到組播地址上,傳送到

         組播地址的好處是除錯方便,在區域網內接收都可以。

 

這是網路接收類的一個標頭檔案示例,讀者完全可以不使用boost庫,自行寫出:


#pragma once
#include "CodeReceive.h"

#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>

#include "DrawRGB24.h"
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavutil/mathematics.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
}

#include "h264head/h264-x264.h"

class CodeReceive2:public CBaseReceive
{
friend DWORD WINAPI ThreadProcReceive(LPVOID param);
public:
	CodeReceive2();
	~CodeReceive2(void);
protected:
	void CreateiocpReceiver(const boost::asio::ip::address& listen_address,
      const boost::asio::ip::address& multicast_address,
	  const unsigned short port);
	
	void handle_receive_from_direct(const boost::system::error_code& error,
      size_t bytes_recvd);



	BOOL CreateDecode()
	{
		if(_pDecode==NULL)
		{
			_pDecode= new H264DecoderContext();

			if(_pDecode == NULL)
				return FALSE;
			if(!_pDecode->Initialize())
				return FALSE;
		}
		return TRUE;
	}
	void DeleteDecode()
	{
		if(_pDecode!=NULL)
		{
			delete _pDecode;
			_pDecode = NULL;
		}
	}


public:
	virtual int Pix_Fmt();
	virtual int Width()  ;
	virtual int Height() ;
	virtual BOOL StartReceive(string ip,unsigned short port) ;

    virtual void StopReceive() ;

	//這個畫法是使用了SDL畫法
	virtual void SetFunction(FrameCallback func) ;

	//這個是可以獲取資料自己畫,後面的版本是要用directshow vmr畫法
	virtual void SetFunctionRGB24(FrameCallback_RGB24 func) ;

	//這個是內建的畫法,普通GDI畫,參考OpenCV原始碼,預覽畫像
	virtual void SetDrawhWnd(HWND hWnd0,HWND hWnd1) ;

   // static DWORD ThreadProc_Recv(LPVOID param);

private:

    boost::asio::io_service io_service_;
	boost::asio::ip::udp::socket socket_;
    boost::asio::ip::udp::endpoint sender_endpoint_;
    enum { max_length = 1500 };
    char data_[max_length];
    unsigned short _multicast_port;

	string _multicast_ip;
private:
	H264DecoderContext* _pDecode;

	AVFrame * _pFrameRGB;
	uint8_t * _RGBBuffer;
	struct SwsContext *_img_convert_ctx;
	//同時畫兩個視窗
    CDrawRGB24 _Draw;
   //  HANDLE _ThreadHandle  ;
	HWND _hWnd0;
	HWND _hWnd1;

	FrameCallback_RGB24 _functionRGB24;
};



類的cpp檔案的接收函式的關鍵函式

void CodeReceive2::handle_receive_from_direct(const boost::system::error_code& error,
      size_t bytes_recvd)
{
    if (!error)
    {
		
		AVFrame * frame =_pDecode->DecodeFrames((const u_char*)data_,bytes_recvd);
		if(frame!=NULL)
		{
			int Width  = this->Width();//_pDecode->GetContext()->width;
			int Height = this->Height();//_pDecode->GetContext()->height;

#if 0  //如果需要用sdl渲染畫面,可以開啟這個
			if(_function )
				_function(frame,_pDecode->GetContext()->pix_fmt,
					_pDecode->GetContext()->width,
					_pDecode->GetContext()->height
		
					);
#endif            
			
			if(_RGBBuffer == NULL)
			{
				int numBytes;

				numBytes=avpicture_get_size(
					//PIX_FMT_RGB24, 
					PIX_FMT_BGR24,
					Width,
					Height);
				_RGBBuffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

				if(_pFrameRGB == NULL)
					_pFrameRGB = avcodec_alloc_frame();
				avpicture_fill((AVPicture *)_pFrameRGB, _RGBBuffer, PIX_FMT_BGR24,   Width, Height); 

				_img_convert_ctx = sws_getContext(Width, Height, 
					_pDecode->GetContext()->pix_fmt,//PIX_FMT_YUV420P, 
					Width, 
					Height, 
					PIX_FMT_BGR24, 
					SWS_BICUBIC, 
					NULL, 
					NULL, 
					NULL);
			}

			sws_scale(_img_convert_ctx, frame->data, frame->linesize, 0, Height, _pFrameRGB->data, _pFrameRGB->linesize);

			if(_hWnd0!=NULL || _hWnd1!=NULL)
				_Draw.Draw2(_hWnd0,_hWnd1,_pFrameRGB->data[0],Width,Height);

			//Sleep(5);
			if(_functionRGB24)
			{

				_functionRGB24(_pFrameRGB->data[0],_pDecode->GetContext()->pix_fmt,Width,Height);
			}
	
		}
	

        socket_.async_receive_from(
          boost::asio::buffer(data_, max_length), sender_endpoint_,
          boost::bind(&CodeReceive2::handle_receive_from_direct, this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));

    }
  }


  這裡有讓剛做ffmpeg或者影象的程式設計師困惑的一些問題,比如影象為什麼接收的是倒立的?RGB實際上是BGR,能不能直接刷屏yuv420等等。影象倒立的問題是直播取到的影象本身就是倒立的,還是過程中倒立了,這個問題比較難以回答,比如攝像頭D70,高清的HD1等等有一個開關撥一下就正,再返回去就倒立,不過拿普通的USB攝像頭,按照正常採集,經過一系列的變換,你發現影象也是倒得,如果拿一個正常錄下的h264視訊,有的程式設計師播放時發現也是倒立的,如果是倒立的,有兩個方法解決,一是ffmepg的sws_scale函式可以解決這個問題,一個是gdi刷屏可以解決這個問題,有的程式設計師會重新memcpy以下,把影象倒過來,這樣也是可以的,但如果是高清720P或者1080P的影象較大,比較費事,最好是直接在過程中就解決掉這個問題。

   用gdi刷屏時,把SrcH負過來就可以讓影象正過來。

   當然,用gdi必然效率不會很好,尤其做畫中畫的時候或者多路影象的時候,不能用這個,windows上可以用directx和較新的dxva。

  用下面這個來倒立影象

m_lpBmpInfo->bmiHeader.biHeight=   -SrcH;


void CDrawRGB24::Draw2(HWND hWnd, HWND hWnd2,unsigned char * buffer, int SrcW, int SrcH)
{
	HDC hDCDst1 = NULL;
	HDC hDCDst2 = NULL;
	RECT destRect1;
	RECT destRect2;
	if(hWnd!=NULL)
	{
		hDCDst1 = GetDC(hWnd);
		GetClientRect(hWnd,&destRect1);
	}
	if(hWnd2!=NULL)
	{
		hDCDst2 = GetDC(hWnd2);
		GetClientRect(hWnd2,&destRect2);
	}

	if(!m_bInit)
	{
		m_bInit = true;
		m_lpBmpInfo=new BITMAPINFO;
		m_lpBmpInfo->bmiHeader.biSize  = sizeof(BITMAPINFOHEADER);
		m_lpBmpInfo->bmiHeader.biWidth =   SrcW;
		m_lpBmpInfo->bmiHeader.biHeight=   -SrcH;
		m_lpBmpInfo->bmiHeader.biPlanes= 1;
		m_lpBmpInfo->bmiHeader.biBitCount      = 24;
		m_lpBmpInfo->bmiHeader.biCompression   = 0;
		m_lpBmpInfo->bmiHeader.biSizeImage     = 0;
		m_lpBmpInfo->bmiHeader.biXPelsPerMeter = 0;
		m_lpBmpInfo->bmiHeader.biYPelsPerMeter = 0;
		m_lpBmpInfo->bmiHeader.biClrUsed=0;
		m_lpBmpInfo->bmiHeader.biClrImportant  = 0;

		//CDC * dc =  CDC::FromHandle(hDCDst);
		//m_pMemDC = new CMemDC(*dc,DestRect);
	}

	if(hDCDst1!=NULL)
	{
		int DstWidth  = destRect1.right-destRect1.left;
		int DstHeight = destRect1.bottom- destRect1.top;
		SetStretchBltMode(hDCDst1,STRETCH_HALFTONE);
		::StretchDIBits(
			//m_pMemDC->GetDC().GetSafeHdc(),
			hDCDst1,
			0, 0, DstWidth, DstHeight,
			0, 0, SrcW, SrcH,
			buffer, m_lpBmpInfo, DIB_RGB_COLORS, SRCCOPY );
		ReleaseDC(hWnd,hDCDst1);
	}
	if(hDCDst2!=NULL)
	{
		int DstWidth  = destRect2.right-destRect2.left;
		int DstHeight = destRect2.bottom- destRect2.top;
		SetStretchBltMode(hDCDst2,STRETCH_HALFTONE);
		::StretchDIBits(
			//m_pMemDC->GetDC().GetSafeHdc(),
			hDCDst2,
			0, 0, DstWidth, DstHeight,
			0, 0, SrcW, SrcH,
			buffer, m_lpBmpInfo, DIB_RGB_COLORS, SRCCOPY );
		ReleaseDC(hWnd2,hDCDst2);
	}

}

整個的過程是收包,拿到包頭時間戳等資訊,去掉包頭12位元組,拿到h264 nalu資料,用ffmpeg解碼,時間戳問題主要集中在音訊和視訊同步的上面,而pts和dts是同步最重要的資訊,解碼過程為:

   

AVFrame* H264DecoderContext::DecodeFrames(const u_char * src, unsigned & srcLen)
{

  RTPFrame srcRTP(src, srcLen);
  if (!_rxH264Frame->SetFromRTPFrame(srcRTP, flags)) {
	  _rxH264Frame->BeginNewFrame();
	  //sprintf(dst,"%s\n","setfromrtpframe is not ok!");
	  flags = (_gotAGoodFrame ? requestIFrame : 0);
	  _gotAGoodFrame = false;
	  return NULL;
  }

  if (srcRTP.GetMarker()==0)
  {
		return NULL;
  } 

  if (_rxH264Frame->GetFrameSize()==0)
  {
	  _rxH264Frame->BeginNewFrame();
	  _skippedFrameCounter++;
	  flags = (_gotAGoodFrame ? requestIFrame : 0);
	  _gotAGoodFrame = false;
	  return NULL;
  }
  // look and see if we have read an I frame.
  if (_gotIFrame == 0)
  {
    _gotIFrame = 1;
  }
 

  int gotPicture = 0;
 // uint32_t bytesUsed = 0;

  // int bytesDecoded = avcodec_decode_video(_context,_outputFrame,&gotPicture,_rxH264Frame->GetFramePtr(),_rxH264Frame->GetFrameSize());

  int bytesDecoded = FFMPEGLibraryInstance.AvcodecDecodeVideo(_context, _outputFrame, &gotPicture, _rxH264Frame->GetFramePtr(), _rxH264Frame->GetFrameSize());
  _rxH264Frame->BeginNewFrame();
  if (!gotPicture) 
  {
	  _skippedFrameCounter++;
	  flags = (_gotAGoodFrame ? requestIFrame : 0);

	  _gotAGoodFrame = false;
	  return NULL;
  }

 //得到了一幀
  // w  = _context->width;
  // h  = _context->height;
  flags = 1;
  _frameCounter++;
  _gotAGoodFrame = true;


  return _outputFrame;
}

  程式碼暫時只是為了演示,並不完整,不過基本過程是非常清楚的。過程中其實還需要處理一個比較傲重要的問題就是解析度改變的問題,音視訊同步的問題,播放過快或者過慢的問題,如果要測試傳送的視訊是否正確,可以使用vlc來接收測試。

  這是第一篇基礎,後面再準備比較完整的示例和用d3d,sdl刷屏,並且加入音訊的解碼,屬於第二篇。

  未完待續。。。。。。

相關文章