这其中关键的地方在于,把包含起始码的Naul数据分配给AVPacket对象,然后就可以解码了,如下
AVPacket packet;
av_new_packet(&packet,len);
memcpy(packet.data, Buf, len);
其实FFMPEG解码还有另一种模式,通过回调函数,从指定的地方(比如本地文件,D:\testl.flv, 或者存放在内存中的视频数据)读取数据,然后解码。
通过回调函数读本地文件的意义不大,本文主要关注通过回调函数从内存从抓数据的场景.
经测试,ffmpeg应该是先通过多次调用回调函数从内存中预加载一部分数据进行处理,处理完后,再预加载第二批数据,以此类推.
我们先来看发送端,发送端比上一篇的版本更简单,只需打开本地磁盘文件,每次读入1024字节的数据(当然具体大小可自己拟定),然后把读入的数据发送出去即可,不用关心读入的1024字节数据是不是完整的Naul包,读数据的伪代码如下:
FILE *fp_open;
uint8_t buff[1024*5] = {0};
int bufsize = 1024; //每次读1024字节,谁便取,不超过1400就行
fp_open = fopen("cuc_ieschool.flv","rb");
while( !feof(fp_open) )
{
int true_size = fread(buff,1,bufsize,fp_open); //读一次,内部位移
printf("\n--> read file %d bytes",true_size);
sender.SendH264Nalu(&sess, buff,true_size); // 每次发送固定大小的数据
RTPTime::Wait(0.005); //间隔5毫秒
}
发送数据的伪代码如下:
if(buflen <= MAXLEN) //每次读的是1024大小的数据
{
sess->SetDefaultMark(true);
status = sess->SendPacket((void *)&pSendbuf[0],buflen);
checkerror(status);
printf("send_packt 0 len = %d\n",buflen);
}
发送端就这么简单,我们再看接收端,接收端收到的是一个1024大小的buffer,为了存储这些buffer,我定义了如下的结构体
// 处理收到的1024大小的数据包
typedef struct
{
unsigned length; //包大小,除非是最后一个包,不然是1024
uint8_t *buf; // new uint8_t[length]
}PacketNode_t;
以及存储buffer的list容器和同步锁,如下
CCriticalSection m_cs; //同步锁
list<PacketNode_t> m_packetList; //包列表
首先,在对话框初始化的地方,做相应的准备工作,代码如下:
m_DrawDib = DrawDibOpen(); // FVW用
m_timeCount = 0; //定时器延迟触发用
m_bInitFlag = FALSE; //ffmpeg初始化标志位
//网络初始化
WSADATA dat;
WSAStartup(MAKEWORD(2,2),&dat);
sessparams.SetOwnTimestampUnit(1.0/10.0);
transparams.SetPortbase(12346); //监听端口
int status = sess.Create(sessparams,&transparams);
checkerror(status);
SetTimer(666,5,NULL); // 定时器,收rtp包
SetTimer(888,40,NULL); // 定时器,解码和显示视频
其中,定时器666的作用是收rtp, 其实就是一个个1024大小的包,处理逻辑如下:
if(666 == nIDEvent)
{
RTPPacket *pack;
int status = sess.Poll(); // 主动收包
checkerror(status);
sess.BeginDataAccess();
if (sess.GotoFirstSourceWithData())
{
do
{
while ((pack = sess.GetNextPacket()) != NULL)
{
uint8_t * loaddata = pack->GetPayloadData();
size_t len = pack->GetPayloadLength();
// TRACE(" Get packet-> %d \n ",pack->GetPayloadLength());
// 构建包对象
PacketNode_t temNode;
temNode.length = len;
temNode.buf = new uint8_t[len];
memcpy(temNode.buf,loaddata,len);
m_cs.Lock();
m_packetList.push_back(temNode); //存包列表
m_cs.Unlock();
sess.DeletePacket(pack);
}
} while (sess.GotoNextSourceWithData());
}
sess.EndDataAccess();
}
大概逻辑是把收到的1024包,用结构体PacketNode_t保存起来,放入列表,
定时器888的作用是,初始化ffmpeg(当然只执行一次),解码视频,然后播放,代码如下:
if(888 == nIDEvent)
{
m_timeCount++;
if(m_timeCount <200) // 每次40毫秒,8秒后才触发定时器,让list存储足够多的数据
return;
if( FALSE == m_bInitFlag )
{
if( this->InitDecode()<0 ) //ffmpeg初始化
{
PostQuitMessage(0);
}
}
else
{
this->DecodeVideoFrame(); //开始解压播放
}
}
其中要8秒钟后才执行ffmpeg初始化,因为我们要保证先收到一部分视频数据,初始化的时候,在注册回调函数的时候,ffmpeg会预读一部分数据,方便后面解析数据,得到视频的解码器等信息。
ffmpeg初始化函数为InitDecode(),具体可参见源码,其中注册回调函数的代码为
AVIOContext *avio =avio_alloc_context(iobuffer, 1024*MAX_PACKET_COUNT,0,this,ReadNetPacket,NULL,NULL);
回调函数ReadNetPacket内部调用的是对话框的ToReadNetPacket()函数,如下,
int CShowH264_PictureDlg::ToReadNetPacket(uint8_t *buf, int buf_size)
{
int nIndex = 0; //计数器
int nsize = 0;
m_cs.Lock();
if(!m_packetList.empty())
{
list<PacketNode_t>::iterator itr;
for(itr = m_packetList.begin();itr != m_packetList.end();) // 顺序遍历
{
nIndex++;
if( nIndex > MAX_PACKET_COUNT)
{
break; //第31个就跳出循环
}
//PacketNode_t = *itr;
memcpy(buf+nsize, itr->buf, itr->length);
nsize += itr->length;
delete[] itr->buf; //释放内存
m_packetList.erase(itr++); //list删除item
}
}
else
{
nsize = -1; //表示没有数据可读
}
m_cs.Unlock();
TRACE("\n callback read --> %d ", nsize);
return nsize;
}
这个函数就是 ffmpeg内部要预加载数据时,通过回调,从网络包容器里获取数据的方法.
然后,每隔40毫秒,执行一次DecodeVideoFrame()函数,解码视频,如下:
int CShowH264_PictureDlg::DecodeVideoFrame()
{
int ret, got_picture;
if(av_read_frame(pFormatCtx, packet) >= 0)
{
if(packet->stream_index==videoindex)
{
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0)
{
TRACE("Decode Error.(解码错误)\n");
return -1;
}
else if(got_picture)
{
pFrame->data[0] += pFrame->linesize[0]*(pCodecCtx->height-1);
pFrame->linesize[0] *= -1;
pFrame->data[1] += pFrame->linesize[1]*(pCodecCtx->height/2-1);
pFrame->linesize[1] *= -1;
pFrame->data[2] += pFrame->linesize[2]*(pCodecCtx->height/2-1);
pFrame->linesize[2] *= -1;
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
this->display_pic(pFrameYUV->data[0], pCodecCtx->width, pCodecCtx->height); //显示视频
//TRACE("\n ---> 准备播放了 ");
}
}
av_free_packet(packet);
}
return 0;
}
显示视频的方法和上一篇文章的代码是一样,不再赘述.
本demo和上一篇文件比起来,视频格式不限于h264文件,常见的mp4,mkv,rmvb等格式都支持,经测试,如果播放常规的电影,回调方式会比较卡(哪怕回调播放本地视频,非网络传输方式),如果视频比较小,画面不大,效果还行.
本文只是演示了网络发包的情况下, 采用回调播放的一种思路而已.
本文源码下载: http://download.csdn.net/detail/heker2010/9910101
我修改的雷神的本地回调播放的demo:
http://download.csdn.net/detail/heker2010/9910081
在此之前,我写的回调播放h264的demo,本来想删除的,发现有人下载过了,就保留吧
http://download.csdn.net/detail/heker2010/9908914
参考文章:
ffmpeg 从内存中读取数据(或将数据输出到内存)
http://blog.csdn.net/leixiaohua1020/article/details/12980423
FFMPEG实时解码网络视频流(回调方式)
http://blog.csdn.net/fang437385323/article/details/71247065