C++ 使用海康威视SDK将视频推流到rtmp服务器

研究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)

你可能感兴趣的:(c++,ffmpeg)