最近看的文章和demo都是把H264文件用RTP協議發送出去後,用VLC的SDP文件測試播放,那麼如果
自己接收到RTP協議的H264包後如何解碼播放呢?
關於FFMPEG解碼播放的示例,一般都是打開本地磁盤的某個文件,比如D:\test.h264,邊讀入數據,邊解碼播放,如果數據是RTP協議傳過來的H264包,如何解碼?因爲avformat_open_input()函數傳入的是文件路徑或URL,我們收到的是一個H264包,沒辦法傳入。
查了幾天資料,搗騰了半天,終於用Jrtplib收發H264文件+ FFMPEG解碼+VFW播放視頻的方式把視頻播放出來了,有些文章只有關鍵代碼,沒有demo可參考,比如這個 DrawDibDraw()我百度了半天,才發現是VFW的東西,非資深人士真是傷不起啊!
發送端和接收端採用的是JrtpLib庫,當然自己寫UDP收發RTP包也可以,只是我參考的文章,原意是用JrtpLib庫傳輸H264文件(接收端只往磁盤寫數據,非播放),發送端不是重點,我原樣採用他人的代碼,只是負責推送數據而已.只是它發送的是帶起始碼(00 00 01 或00 00 00 01)的完整H264包,僞代碼如下:
while((feof(fd)==0))
{
buff[pos++] = fgetc(fd); //逐字節讀取fd文件
if(header_flag == 1)
{ //00 00 00 01
if((buff[pos-1]==1)&&(buff[pos-2]==0)&&(buff[pos-3]==0)&&(buff[pos-4]==0))
{
sender.SendH264Nalu(&sess, buff,pos-4); // 發送一個完整的h264數據
buff[0] = 0x00;
buff[1] = 0x00;
buff[2] = 0x00;
buff[3] = 0x01;
pos = 4;
RTPTime::Wait(0.1); //間隔100毫秒
}
}
else
{
if((buff[pos-1]==1)&&(buff[pos-2]==0)&&(buff[pos-3]==0))
{
sender.SendH264Nalu(&sess, buff, pos-3);
buff[0] = 0x00;
buff[1] = 0x00;
buff[3] = 0x01;
pos = 3;
RTPTime::Wait(0.1);
}
}
}
接收端:接收端原來是控制檯程序,原來只是把收到的H264數據寫入磁盤即可,因爲要顯示視頻,我改造爲MFC的對話框程序,在對話框的OnInitDialog()函數裏,做了相關的初始化,如下:
// TODO: 在此添加額外的初始化代碼
m_DrawDib = DrawDibOpen(); // FVW用
// ffmpeg初始化
av_register_all();
avformat_network_init();
m_picture = av_frame_alloc();
m_pFrameRGB = av_frame_alloc();
if(!m_picture || !m_pFrameRGB)
{
printf("Could not allocate video frame\n");
return FALSE;
}
// --ffmpeg初始化 end ---
// ffmpeg解碼器準備
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if(codec==NULL)
{
printf("Codec not found.(沒有找到解碼器)\n");
return FALSE;
}
codecCtx = avcodec_alloc_context3(codec);
if(avcodec_open2(codecCtx, codec,NULL)<0)
{
printf("Could not open codec.(無法打開解碼器)\n");
return FALSE;
}
// NALU_t數據準備
int buffersize=100000;
h264node = (NALU_t*)calloc (1, sizeof (NALU_t));
if (h264node == NULL)
{
printf("Alloc NALU Error\n");
return 0;
}
h264node->max_size = buffersize;
h264node->buf = (char*)calloc (buffersize, sizeof (char));
if (h264node->buf == NULL)
{
free (h264node);
printf ("AllocNALU: n->buf");
return 0;
}
//-- 準備結束
//網絡初始化
WSADATA dat;
WSAStartup(MAKEWORD(2,2),&dat);
sessparams.SetOwnTimestampUnit(1.0/10.0);
transparams.SetPortbase(12346); //監聽端口
int status = sess.Create(sessparams,&transparams);
checkerror(status);
最後,弄個定時器,收rtp包,顯示視頻
SetTimer(666,5,NULL);
然後在定時器裏收包,檢查是否收到完整的H264包,是就對包進行解碼,然後顯示出來,定時器代碼如下:
void CShowH264_PictureDlg::OnTimer(UINT_PTR nIDEvent)
{
if(666 == nIDEvent)
{
size_t len;
RTPPacket *pack;
int status = sess.Poll(); // 主動收包
checkerror(status);
sess.BeginDataAccess();
if (sess.GotoFirstSourceWithData())
{
do
{
while ((pack = sess.GetNextPacket()) != NULL)
{
//printf(" Get packet-> %d \n ",pack->GetPayloadType());
uint8_t * loaddata = pack->GetPayloadData();
len = pack->GetPayloadLength();
if(pack->GetPayloadType() == 96) //H264
{
if(pack->HasMarker()) // the last packet
{
// printf(" write packet-> %d \n ",pack->GetPayloadType());
memcpy(&buff[pos],loaddata,len);
memcpy(Parsebuff,buff,pos+len); //得到完整的h264 naul包
simplest_h264_parser(Parsebuff,pos+len,nCount); //解析完整h264 naul包
nCount++;
pos = 0;
}
else
{
memcpy(&buff[pos],loaddata,len); //大的naul數據,分幾個包發送過來
pos = pos + len;
}
}else
{
printf("!!! GetPayloadType = %d !!!! \n ",pack->GetPayloadType()); // 非264數據包
}
sess.DeletePacket(pack);
}
} while (sess.GotoNextSourceWithData());
}
sess.EndDataAccess();
}
CDialogEx::OnTimer(nIDEvent);
}
得到完整的H264包後,處理的函數爲simplest_h264_parser(),這個函數 解析了H264包的nal_unit_type和nal_reference_idc屬性,這個功能對解碼和顯示沒什麼意義,是我從雷神的文章裏copy過來的,調試用的而已.
// 前2個參數是 一個完整naul包的buffer地址和長度,第3個參數是計數器而已
void CShowH264_PictureDlg::simplest_h264_parser(unsigned char *Buf,int nLength,int packetIndex)
{
if( 0 == Buf[0] & 0 == Buf[1] && 1 == Buf[2] )
{
h264node->startcodeprefix_len = 3; // 0x000001開頭
}
else if( 0 == Buf[0] & 0 == Buf[1] && 0 == Buf[2] && 1 == Buf[3])
{
h264node->startcodeprefix_len = 4; //0x00000001開頭
}
else
{
printf("---->NALU head Error \n");
return;
// h264node->startcodeprefix_len = 0; //未含起始碼.解析是有風險的
}
h264node->len = nLength - h264node->startcodeprefix_len; //有效buffer的長度
memcpy (h264node->buf, &Buf[h264node->startcodeprefix_len], h264node->len); //拷貝有效值
h264node->forbidden_bit = h264node->buf[0] & 0x80; //1 bit
h264node->nal_reference_idc = h264node->buf[0] & 0x60; // 2 bit
h264node->nal_unit_type = (h264node->buf[0]) & 0x1f;// 5 bit
char type_str[20]={0};
switch(h264node->nal_unit_type)
{
case NALU_TYPE_SLICE:sprintf(type_str,"SLICE");break;
case NALU_TYPE_DPA:sprintf(type_str,"DPA");break;
case NALU_TYPE_DPB:sprintf(type_str,"DPB");break;
case NALU_TYPE_DPC:sprintf(type_str,"DPC");break;
case NALU_TYPE_IDR:sprintf(type_str,"IDR");break;
case NALU_TYPE_SEI:sprintf(type_str,"SEI");break;
case NALU_TYPE_SPS:sprintf(type_str,"SPS");break; //SPS和PPS一般在H264的前2幀
case NALU_TYPE_PPS:sprintf(type_str,"PPS");break;
case NALU_TYPE_AUD:sprintf(type_str,"AUD");break;
case NALU_TYPE_EOSEQ:sprintf(type_str,"EOSEQ");break;
case NALU_TYPE_EOSTREAM:sprintf(type_str,"EOSTREAM");break;
case NALU_TYPE_FILL:sprintf(type_str,"FILL");break;
}
char idc_str[20]={0};
switch(h264node->nal_reference_idc>>5)
{
case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str,"DISPOS");break;
case NALU_PRIRITY_LOW:sprintf(idc_str,"LOW");break;
case NALU_PRIORITY_HIGH:sprintf(idc_str,"HIGH");break;
case NALU_PRIORITY_HIGHEST:sprintf(idc_str,"HIGHEST");break;
}
if(packetIndex<10) //顯示前10幀的類型
{
TRACE("\n %5d| %7s| %6s| %8d|",packetIndex,idc_str,type_str,h264node->len);
}
//經測試,PPS和SPS一般是頭2包必須處理,不然後面所有包都無法解碼,SPS包會穿插在後面的SLICE等包裏面
//if( h264node->nal_unit_type <= NALU_TYPE_PPS) 這個限制條件可以取消
{
// 下面是解碼
int len = nLength;
AVPacket packet;
av_new_packet(&packet, len);
memcpy(packet.data, Buf, len);
int ret, got_picture;
ret = avcodec_decode_video2(codecCtx, m_picture, &got_picture, &packet);
if (ret > 0 )
{
if(got_picture)
{
m_PicBytes = avpicture_get_size(PIX_FMT_BGR24, codecCtx->width, codecCtx->height);
m_PicBuf = new uint8_t[m_PicBytes];
avpicture_fill((AVPicture *)m_pFrameRGB, m_PicBuf, PIX_FMT_BGR24, codecCtx->width, codecCtx->height);
if(!m_pImgCtx)
{
m_pImgCtx = sws_getContext(codecCtx->width, codecCtx->height, codecCtx->pix_fmt, codecCtx->width, codecCtx->height,
PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
}
m_picture->data[0] += m_picture->linesize[0]*(codecCtx->height-1);
m_picture->linesize[0] *= -1;
m_picture->data[1] += m_picture->linesize[1]*(codecCtx->height/2-1);
m_picture->linesize[1] *= -1;
m_picture->data[2] += m_picture->linesize[2]*(codecCtx->height/2-1);
m_picture->linesize[2] *= -1;
sws_scale(m_pImgCtx, (const uint8_t* const*)m_picture->data, m_picture->linesize, 0, codecCtx->height, m_pFrameRGB->data,
m_pFrameRGB->linesize);
display_pic(m_pFrameRGB->data[0], codecCtx->width, codecCtx->height);
delete[] m_PicBuf; //釋放內存.感覺它應該是avpicture_fill內部臨時用的
//TRACE("\n -->準備顯示圖片 %d X %d",codecCtx->width,codecCtx->height);
}
}
av_free_packet(&packet); //釋放包
}
}
顯示視頻的函數是display_pic,如下:
void CShowH264_PictureDlg::display_pic(unsigned char* data, int width, int height)
{
CRect rc;
HDC hdc = GetDC()->GetSafeHdc();
GetClientRect(&rc);
if(m_height != height || m_width != width)
{
m_height = height;
m_width = width;
MoveWindow(0, 0, width, height, 0);
Invalidate();
}
init_bm_head();
//利用VFW來顯示
DrawDibDraw(m_DrawDib,
hdc,
rc.left,
rc.top,
-1, // don't stretch
-1,
&bmiHeader,
(void*)data,
0,
0,
width,
height,
0);
}
注意,原文的發送端和接收端只是處理文件傳輸,並沒有嚴格按照RTP協議來打包(無需用VLC的SDP文件播放視頻),我們把接收到的H264數據自己解碼,自己顯示,都是自己處理,也就無所謂了.
本文demo下載地址如下:
http://download.csdn.net/detail/heker2010/9907427
參考文章:
《FFMPEG 實時解碼網絡H264碼流,RTP封裝》
http://blog.csdn.net/fang437385323/article/details/52336680
《linux 使用jrtplib收發h.264視頻文件》
http://blog.csdn.net/li_wen01/article/details/70435005
《FFMPEG如何解碼播放通過socket接收的網絡碼流(h264)》
https://www.oschina.net/question/217709_34304