研究FFmpeg有两三年了,一直没写过这方面的文章,今天记一下。
由于工作关系,需要将化工企业内部的视频发布到一个部署在公网的视频服务器,然后由相关人员浏览。由于是化工企业,企业严禁外部的机器直接访问视频网络,最多提供一个跳板机。因此,两年多前,针对这种情况,基于FFmpeg研发了一个推流系统。
随着接入视频数量的增加,发现不能单纯的使用RTSP协议获取硬盘录像机视频数据了,海康威视的硬盘录像机,最多允许5个用户同时访问,如果使用RTSP的话,每路RTSP都相当于一个访问用户,因此,需要使用海康卫视的SDK将视频流转为FFmpeg帧。
其实海康威视在开发文档中给出了相应示例,这里我贴一下我写的这部分代码:
初始化程序,连接硬盘录像机:
void HKVideo::initInput() {
inputSuccess = false;
NET_DVR_USER_LOGIN_INFO struLoginInfo = { 0 };
NET_DVR_DEVICEINFO_V40 struDeviceInfoV40 = { 0 };
while (true)
{
//---------------------------------------
// 初始化
NET_DVR_Init();
//设置连接时间与重连时间
NET_DVR_SetConnectTime(2000, 1);
NET_DVR_SetReconnect(2000, true);
//---------------------------------------
//设置异常消息回调函数
NET_DVR_SetExceptionCallBack_V30(0, NULL, g_ExceptionCallBack, NULL);
//---------------------------------------
// 注册设备
//登录参数,包括设备地址、登录用户、密码等
struLoginInfo.bUseAsynLogin = 0; //同步登录方式
strcpy(struLoginInfo.sDeviceAddress, ip.c_str()); //设备IP地址
struLoginInfo.wPort = port; //设备服务端口
strcpy(struLoginInfo.sUserName, user.c_str()); //设备登录用户名
strcpy(struLoginInfo.sPassword, psd.c_str()); //设备登录密码
//设备信息, 输出参数
lUserID = NET_DVR_Login_V40(&struLoginInfo, &struDeviceInfoV40);
printf("Login code: %d\n", NET_DVR_GetLastError());
if (lUserID < 0)
{
NET_DVR_Cleanup();
Sleep(3000);
continue;
}
else {
printf("lUserID:%d\n", lUserID);
break;
}
}
int size = rtsp_json["channel"].size();
for (size_t i = 0; i < size; i++)
{
int lChannel = rtsp_json["channel"][i].asInt();
HKVideoPush* push = new HKVideoPush;
stringstream stream;
stream << rtmp;
stream << "_";
stream << lChannel;
string rtmp0;
stream >> rtmp0;
push->init(rtmp0);
printf("Channel:%s\n",push->rtmp);
//---------------------------------------
//启动预览并设置回调数据流
//HWND h = (HWND)cvGetWindowHandle("Mywindow");
push->struPlayInfo.hPlayWnd = NULL; //需要SDK解码时句柄设为有效值,仅取流不解码时可设为空
push->struPlayInfo.lChannel = lChannel; //预览通道号
push->struPlayInfo.dwStreamType = 0; //0-主码流,1-子码流,2-码流3,3-码流4,以此类推
push->struPlayInfo.dwLinkMode = 0; //0- TCP方式,1- UDP方式,2- 多播方式,3- RTP方式,4-RTP/RTSP,5-RSTP/HTTP
push->struPlayInfo.bBlocked = 1; //0- 非阻塞取流,1- 阻塞取流
push->lRealPlayHandle = NET_DVR_RealPlay_V40(lUserID, &push->struPlayInfo, g_RealDataCallBack_V30, push);
if (push->lRealPlayHandle < 0)
{
printf("NET_DVR_RealPlay_V40 error, %d\n", NET_DVR_GetLastError());
NET_DVR_Logout(lUserID);
NET_DVR_Cleanup();
}
HKVideos[lChannel] = push;
HANDLE h = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)StartHKPushThread, (PVOID)push, 1, 0); //创建子线程
ResumeThread(h); //启动子线程
}
inputSuccess = true;
};
回调函数:
void HKVideoPush::AVFrame2AVPacket(unsigned char* pYUV, int srcWidth, int srcHeight) {
//关键函数,将海康威视视频帧转为FFmpeg帧
AVFrame* avframe_tmp = av_frame_alloc();//申请一个新的帧
AVPacket* pkt = av_packet_alloc();//申请一个新的视频包
avframe_tmp->format = AV_PIX_FMT_YUV420P;
avframe_tmp->width = srcWidth;
avframe_tmp->height = srcHeight;
avpicture_fill((AVPicture*)avframe_tmp, pYUV, AV_PIX_FMT_YUV420P, srcWidth, srcHeight);//将海康威视帧数据填充到FFmpeg视频帧中
//下面这一步很重要,如果没有下面三行代码,rtmp看到的视频颜色会失常
uint8_t* ptmp = avframe_tmp->data[1];
avframe_tmp->data[1] = avframe_tmp->data[2];
avframe_tmp->data[2] = ptmp;
this->vpts += 1;
this->lock();//锁定,
avframe_tmp->pts = this->vpts;
int ret = avcodec_send_frame(this->vc, avframe_tmp);
if (ret < 0) {
printf("Error avcodec_send_frame:%d\n", ret);
av_frame_free(&avframe_tmp);
this->unlock();
return;
}
av_frame_free(&avframe_tmp);
ret = avcodec_receive_packet(this->vc, pkt);
if (ret < 0)
{
printf("Error avcodec_receive_packet:%d\n", ret);
this->unlock();
return;
}
//this->lock();
this->Pkts.push_back(pkt);//将最终的到的视频包,推入一个vector中,其他线程会从vector中取出包,推流给rtmp
this->unlock();//解锁
};
void CALLBACK DecCBFun(long nPort, char* pBuf, long nSize, FRAME_INFO* pFrameInfo, long nReserved1, long nReserved2)
{
long lFrameType = pFrameInfo->nType;
if (lFrameType == T_YV12)
{
//将海康威视包转为FFmpeg帧
AVFrame2AVPacket((unsigned char*)pBuf, pFrameInfo->nWidth, pFrameInfo->nHeight);
}
}
void CALLBACK g_RealDataCallBack_V30(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* dwUser)
{
HKVideoPush* push = (HKVideoPush*)dwUser;
int dRet = 0;
switch (dwDataType)
{
case NET_DVR_SYSHEAD: //系统头
//if (push->lPort >= 0)
//{
// break; //该通道取流之前已经获取到句柄,后续接口不需要再调用
//}
if (push->lPort >= 0)
{
break; //该通道取流之前已经获取到句柄,后续接口不需要再调用
}
if (!PlayM4_GetPort(&push->lPort)) //获取播放库未使用的通道号
{
break;
}
//m_iPort = lPort; //第一次回调的是系统头,将获取的播放库port号赋值给全局port,下次回调数据时即使用此port号播放
if (dwBufSize > 0)
{
if (!PlayM4_OpenStream(push->lPort, pBuffer, dwBufSize, 1600 * 1600))
{
dRet = PlayM4_GetLastError(push->lPort);
break;
}
//设置解码回调函数 仅仅解码不显示
if (!PlayM4_SetDecCallBack(push->lPort, DecCBFun))
{
dRet = PlayM4_GetLastError(push->lPort);
break;
}
//设置解码回调函数 解码且显示
//if (!PlayM4_SetDecCallBackEx(nPort,DecCBFun,NULL,NULL))
//{
// dRet=PlayM4_GetLastError(nPort);
// break;
//}
//打开视频解码
if (!PlayM4_Play(push->lPort, push->hWnd))
{
dRet = PlayM4_GetLastError(push->lPort);
break;
}
}
break;
case NET_DVR_STREAMDATA: //码流数据
if (dwBufSize > 0 && push->lPort != -1)
{
BOOL inData = PlayM4_InputData(push->lPort, pBuffer, dwBufSize);
while (!inData)
{
inData = PlayM4_InputData(push->lPort, pBuffer, dwBufSize);
OutputDebugString("PlayM4_InputData failed \n");
}
}
break;
default: //其他数据
if (dwBufSize > 0)
{
}
break;
}
}
void CALLBACK g_ExceptionCallBack(DWORD dwType, LONG lUserID, LONG lHandle, void* pUser)
{
printf("异常回调%X\n", dwType);
switch (dwType)
{
case EXCEPTION_RECONNECT: //预览时重连
break;
default:
break;
}
}
总结:
缺点:这种方式,会造成大量计算,每一帧都会经过计算,我在实际项目使用的时候,使用了一个CPU为J1900的工控机,读取一个硬盘录像机的5路摄像头,CPU占用率达到90% 以上。主要耗费CPU的就是AVFrame2AVPacket函数,它需要大量计算
优点:节省带宽。。。因为海康卫视的SDK解出来的视频包很小,子码流时每路最多200Kb/s(要知道我用RTSP时,子码流每路800Kb)