赞
踩
研究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)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。