最近看的文章和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.youkuaiyun.com/detail/heker2010/9907427
参考文章:
《FFMPEG 实时解码网络H264码流,RTP封装》
http://blog.youkuaiyun.com/fang437385323/article/details/52336680
《linux 使用jrtplib收发h.264视频文件》
http://blog.youkuaiyun.com/li_wen01/article/details/70435005
《FFMPEG如何解码播放通过socket接收的网络码流(h264)》
https://www.oschina.net/question/217709_34304