关于ubuntu视频叠加水印问题

项目需求:

        最近需要在ubuntu系统上改进以前Qt做的一个视频监控,需要在原始的实时视频和历史视频窗口上叠加水印,需要将登录的用户名和时间同时叠加到视频上面,原本想通过各个厂家的NVR上面直接叠加OSD字符,这样就不用更改程序,首先海康的NVR上叠加的OSD字符数目受限,也不好控制水印的方向,所以还是自己修改下程序完成新的需求更改。


最初方案

        原本想这是一个很简单的功能,直接在原本的Qt的窗口上面,放一个透明的窗口,将重写透明窗口的paintEvent(QPaintEvetn *evt)的事件,同时将叠加水印的窗口的widget放到窗口的最上面显示。主窗口中初始化水印窗口的widget。这样程序的总体逻辑基本不用改动,但实际情况与想象的还是有点差异。

WaterMarkWidget* pWidget=new WaterMarkWidget(this);
pWidget->raise();   //将水印窗口放到父窗口的最前面显示
pWidget->show();

问题描述

  1. 窗口关系描述:
    1. 运行环境,ubuntu 16.04
    2. 主窗口widget里面有一个QLabel用于视频播放,主窗口必须置顶,同时主窗口不是全屏的,主窗口必须要遮挡主ubuntu左边的菜单栏。
    3. 设置QLabel的背景色为黑色,同时设置文字,无视频信号,同时将QLabel的窗口的句柄,传入给调用视频的SDK(调试时使用了海康的NVR)。
    4. 设置主窗口没有边界,并且置顶显示。
    5. 新需求,新增加一个WaterMarkWidget的水印窗口,放置父窗口最上面。  
  2. 未播放视频前,水印叠加正常,看似一切都很正常,为了能分析出问题,特意将水印窗口与主窗口进行了X方向上的平移。
  3.   播放视频后,就出现问题了,看图,水印窗口的部分总是显示QLabel的背景色,即使不显示QLabel对象,那么水印窗口部分显示的也是QWidget的背景色。那么水印窗口的部分,在播放视频的区域,并不能正确的显示视频,这就是问题的所在,所以导致原本设想的方案就不可行

原因分析:

具体什么原因导致的这种情况,其实还不是很明确,如果大家能知道原因,希望能指出。


解决方案:

方案1:既然使用窗口的句柄的方式播放,无法直接叠加水印,那么可以采用回调视频流的方式,获取转码后的YUV的数据,然后转换成RGB数据,最后转换成QImaeg,然后使用QLabel显示,这种方式效率比较低下,消耗的CPU也比较高。

方案2:在父窗口的中,既然无法完成窗口水印叠加,就需要单独实例一个独立窗口,浮动在原本的视频窗口上,完成水印叠加,视频窗口和水印窗口都需要置顶,那么每次在主窗口视频显示的同时,需要通知水印窗口置顶显示,这样水印窗口才能一直在视频窗口上面。

方案1实现:

  1. 本方案主要就是如何获取回调海康的视频流并转换成QImage,下面附代码
  2. 海康播放视频时调用视频接口
  3. //实时视频播放时设置回调函数
        NET_DVR_PREVIEWINFO ClientInfo;
    	ClientInfo.hPlayWnd = hWnd;            //窗口的句柄,windows下是一个void* 指针,linux下是一个unsigned int 类型
    	ClientInfo.lChannel = nChannel;        //设置视频通道 传入参数
    	ClientInfo.dwStreamType = nCodeFlow;   //设置码流模式 通过用户传入的参数决定
        ClientInfo.bBlocked = 0;
        ClientInfo.dwLinkMode=1;
        ClientInfo.byPreviewMode = 0;
        //ClientInfo.bBlocked = 1;   //使用阻塞模式,否则打开音频会失败
    	ClientInfo.bPassbackRecord = 0;
    	ClientInfo.byProtoType = 0;
    	ClientInfo.byVideoCodingType = 0;
        /*
        nLoginID: nvr登录的句柄  long类型
        ClientInfo: 播放参数设置
        RealDataCallBack 设置回调函数
        g_lpContext: 用户参数    void*
        */
        long nPlayID = NET_DVR_RealPlay_V40(nLoginID, &ClientInfo, RealDataCallBack, g_lpContext);
    
    
    //历史视频的回调函数的设置
      /*
        nLoginID: nvr登录的句柄  long类型
        nChannel: 视频通道
        theStart:开始时间 NET_DVR_FILECOND_V40
        theEnd:   结束时间 NET_DVR_FILECOND_V40
       */
      long nPlayID = NET_DVR_PlayBackByTime(nLoginID, nChannel, &theStart, &theEnd, hWnd);
     
     /*
       视频播放成功后,设置回调函数,
       RealDataCallBack 设置回调函数
       g_lpContext: 用户参数    void*  
    */
      NET_DVR_SetPlayDataCallBack_V40(nPlayID,RealDataCallBack,g_lpContext);
    
  4. 回调函数部分
    void CALLBACK DecCBFun(int nPort, char * pBuf, int nSize, FRAME_INFO * pFrameInfo, void* pUserContext, int nReserved2)
    {
    
        long lFrameType = pFrameInfo->nType;
        if (lFrameType == T_AUDIO16)
        {
            //音频流
        }
        else if (lFrameType == T_YV12)
        {
            int nBuffSize=(pFrameInfo->nWidth)*(pFrameInfo->nHeight) * 3 ;
            unsigned char* pRgb = new unsigned char[nBuffSize];
            if(NULL==pRgb)
                return ;
            //yuv12转码成RGB888
            bool bresult=yv12ToRGB888(pBuf,pRgb,pFrameInfo->nWidth,pFrameInfo->nHeight);
        }
    }
    
    void CALLBACK RealDataCallBack(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void *pUser)
    {
       //Player为自定义的一个结构体,默认m_nPort为-1
        Player* pPlayer=static_cast<Player*>(pUser);  
        int nPort=pPlayer->m_nPort;
    	int dRet;
    	BOOL inData = FALSE;
    	switch (dwDataType)
    	{
    		case NET_DVR_SYSHEAD://系统包头部
    		{
    			if (nPort >= 0)
    				break; //同一路码流不需要多次调用开流接口
    			if (!PlayM4_GetPort(&nPort)) //获取未使用的通道号
    				break;
    			if (!PlayM4_OpenStream(nPort, pBuffer, dwBufSize, 1024 * 1024))
    			{
    				dRet = PlayM4_GetLastError(nPort);
    				break;
    			}
    			//设置解码回调函数 解码且显示
                if (!PlayM4_SetDecCallBack(nPort, DecCBFun))
    			{
    				dRet = PlayM4_GetLastError(nPort);
    				break;
    			}
    			if (!PlayM4_Play(nPort, NULL)) //只解码,不显示
    			{
    				dRet = PlayM4_GetLastError(nPort);
    				break;
    			}
    			break;
    		}
    		case NET_DVR_STREAMDATA:
    			inData = PlayM4_InputData(nPort, pBuffer, dwBufSize);
    			while (!inData)
    			{
    				inData = PlayM4_InputData(nPort, pBuffer, dwBufSize);
    			}
    			break;
    		default:
    			inData = PlayM4_InputData(nPort, pBuffer, dwBufSize);
    			while (!inData)
    			{
    				inData = PlayM4_InputData(nPort, pBuffer, dwBufSize);
    			}
    			break;
    	}
        pPlayer->m_nPort=nport;
    }
    
  5. yuv12转RGB代码
    //参考网上的一段代码
    static bool yv12ToRGB888(char *yv12, unsigned char *rgb888, int width, int height)
    {
        if ((width < 1) || (height < 1) || (yv12 == NULL) || (rgb888 == NULL)) {
            return false;
        }
        int len = width * height;
        unsigned char  *yData = (unsigned char*)yv12;
        unsigned char  *vData = &yData[len];
        unsigned char  *uData = &vData[len >> 2];
    
        int rgb[3];
        int yIdx, uIdx, vIdx, idx;
        for (int i = 0; i < height; ++i) {
            for (int j = 0; j < width; ++j) {
                yIdx = i * width + j;
                vIdx = (i / 2) * (width / 2) + (j / 2);
                uIdx = vIdx;
    
                rgb[0] = static_cast<int>(yData[yIdx] + 1.370705 * (vData[uIdx] - 128));
                rgb[1] = static_cast<int>(yData[yIdx] - 0.698001 * (uData[uIdx] - 128) - 0.703125 * (vData[vIdx] - 128));
                rgb[2] = static_cast<int>(yData[yIdx] + 1.732446 * (uData[vIdx] - 128));
                for (int k = 0; k < 3; ++k) {
                    idx = (i * width + j) * 3 + k;
                    if ((rgb[k] >= 0) && (rgb[k] <= 255)) {
                        rgb888[idx] = static_cast<unsigned char>(rgb[k]);
                    } else {
                        rgb888[idx] = (rgb[k] < 0) ? (0) : (255);
                    }
                }
            }
        }
        return true;
    }
    
    //依赖opencv的方式转换成RGB的格式
    void CALLBACK DecCBFunYUV(long nPort, char * pBuf, long nSize, 
                              FRAME_INFO * pFrameInfo, 
                              long nReserved1, long nReserved2)
    {
    	long lFrameType = pFrameInfo->nType;
    	if (lFrameType == T_AUDIO16)
    		cout << "nType =" << pFrameInfo->nType << endl;
    	else if (lFrameType == T_YV12)
    	{
    		
    		IplImage* pImgYCrCb = cvCreateImage(cvSize(pFrameInfo->nWidth,
                                                 pFrameInfo->nHeight), 8, 3);//得到图像的Y分量    
    		yv12toYUV(pImgYCrCb->imageData, pBuf, 
                      pFrameInfo->nWidth, 
                      pFrameInfo->nHeight, 
                      pImgYCrCb->widthStep);//得到全部RGB图像  
    		IplImage* img = cvCreateImage(cvSize(pFrameInfo->nWidth, 
                                          pFrameInfo->nHeight), 8, 3);
    		cvCvtColor(pImgYCrCb, img, CV_YCrCb2RGB);
    		cvReleaseImage(&pImgYCrCb);
            //img就是opencv里面的BRG的数据
    	}
    }
    
    
  6. rgb数据转换成QImage
    //rgb888 转QImage 
    QImage image(pPicBuffer,nWidth,nHeight,QImage::Format_RGB888);
  7. 最后在由QLabel显示图像
    
    //UI注册的回调函数实现
    int pfnRGBCallback(int nWidth, int nHeight, unsigned char *pPicBuffer, int nPicBuffLen,  void *pUserData)
    {
        if(NULL==pUserData || nPicBuffLen<1)
            return -1;
        playWidget* pPlayWidget=(playWidget*)pUserData;
        QImage image(pPicBuffer,nWidth,nHeight,QImage::Format_RGB888);
        pPlayWidget->SetImage(image);
    }
    //回调线程与UI线程不在同一个线程,采用信号与槽的方式,通知UI线程显示,
    void playWidget::SetImage(QImage &image)
    {   
    #if  1 
        emit SendImage(image);
    #else
    //如果直接在回调线程中设置图像,可能会出现一些意想不到的异常,程序莫名奇妙的崩溃。
     QPixmap pix=QPixmap::fromImage(image);
     QPixmap pixScale=pix.scaled(this->m_pPlayVideoLabel->width(),
                                 this->m_pPlayVideoLabel->height(),
                                 Qt::KeepAspectRatio);
     if(pixScale.isNull())
            return ;
      this->m_pPlayVideoLabel->setPixmap(pixScale);
    #endif
    }
    //UI线程响应的槽函数
    void playWidget::GetImage(QImage image)
    {
        QPixmap pix=QPixmap::fromImage(image);
        QPixmap pixScale=pix.scaled(this->m_pPlayVideoLabel->width(),
                                    this->m_pPlayVideoLabel->height(),
                                    Qt::KeepAspectRatio);
        if(pixScale.isNull())
            return ;
        this->m_pPlayVideoLabel->setPixmap(pixScale);
    }
       
    

方案2实现:

  1. 首先要设置窗口置顶,使用Qt常规方法如下,但是这个窗口置顶,无法覆盖住ubuntu左边的标题栏,
    //设置无边框并且窗口置顶
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Window);
  2. 置顶的窗口无法在遮挡住菜单栏
  3. 使用Qt::Tooltip 标志,可以使窗口置顶并且无边框,最重要的一点就是可以覆盖ubuntu窗口的菜单栏,但是这个也有一个问题,在同一个主窗口的程序中,只能设置一次Qt::Tooltip,否则其余窗口设置,则无效了。
    setWindowFlags(Qt::ToolTip);
    
    //水印窗口设置此标志 并且给水印窗口不设置父窗口,独立生成一个窗口
    //此时子窗口无法显示  具体原因也不清楚
    WaterMarkWidget* pWidget=new WaterMarkWidget();
    pWidget->setWindowFlags(Qt::ToolTip | Qt::Window);
    pWidget->show();
    
    
  4. 单独重新写了水印窗口的程序,在给窗口设置Qt::Tooltip 标志,这样当视频窗口和水印窗口,谁在最后面显示,则谁置顶,由于在本项目中视频窗口会不断的show()和hide()。所以重写了视频窗口的show()和hide()方法, 每次视频窗口show完后,通知水印窗口show,并发送视频窗口的坐标位置,每次hide前,通知水印窗口先hide(),这里使用到了多进程间通信方式,根据自己需要选择合适的方式,
    //水印窗口的显示
    void WaterMarkWidget::show(int x,int y,int width,int height)
    {
      setWindowFlags(Qt::ToolTip);   //每次显示都要置顶
      this->setGeometry(x,y,width,height);  //设置视频窗口传过来的坐标位置
      QWidget::show();   //调用父窗口的show显示
    }
    
    void WaterMarkWidget::hide()
    {
      QWidget::hide();  //直接隐藏
    }
    //*************************************************************************//
    //视频窗口也是主窗口程序
    void playWidget::hide()
    {
        HideMaskClient();  //发送消息 通知水印窗口隐藏
        QWidget::hide();    
    }
    void playWidget::show()
    {
        //显示的时候本视频窗口也需要置顶,需要置顶在其余控件上面
        this->setWindowFlags(Qt::ToolTip); 
        QWidget::show();
        //立即通知水印窗口,置顶显示
        ShowMaskClient();
    }
    //发送一个json串到水印窗口
    int playWidget::ShowMaskClient()
    {
        string strWinId =this->winId;
        QRect rect=this->geometry();
        Json::Value root;   //创建一个Json的对象
        root[JSON_NAME_CMDNO]=CMD_WINDOWS_SHOW;    //show
        root[JSON_NAME_VIDEOWNDNAME]= strWinId;    //窗体的标识
        root[JSON_NAME_POS_X]= rect.x();
        root[JSON_NAME_POS_Y]= rect.y();
        root[JSON_NAME_WIN_WIDTH]= rect.width();
        root[JSON_NAME_WIN_HEIGHT]= rect.height();
        string strCmd = root.toStyledString();
        PushMarkWindowsCmd(strCmd);   //发布消息 通知水印窗口
    }
    //通知隐藏
    int playWidget::HideMaskClient()
    {
        string strWinId =this->winId;
        Json::Value root;   //创建一个Json的对象
        root[JSON_NAME_CMDNO]=CMD_WINDOWS_HIDE;    //hide
        root[JSON_NAME_VIDEOWNDNAME]= strWinId;    //窗体的标识
        string strCmd = root.toStyledString();
        PushMaskWindowsCmd(strCmd);
    }
    //当窗口大小发生变化的时候 还需要在通知一次
    void playWidget::resizeEvent(QResizeEvent *event)
    {
        ShowMaskClient();
    }
  5. 通过这种方式,比方案1消耗的资源更小,

总结:

        本项目,由于在ubuntu环境下并且涉及到多窗口的置顶,视频窗口下面还有编译的一个浏览器窗口置顶,所以置顶的程序会稍微麻烦点,正常的可以直接在主窗口中使WaterMarkWidget 设置成一个独立的子窗口的置顶在视频窗口上面。

猜你喜欢

转载自blog.csdn.net/qq_37103755/article/details/128105952
今日推荐