项目需求:
最近需要在ubuntu系统上改进以前Qt做的一个视频监控,需要在原始的实时视频和历史视频窗口上叠加水印,需要将登录的用户名和时间同时叠加到视频上面,原本想通过各个厂家的NVR上面直接叠加OSD字符,这样就不用更改程序,首先海康的NVR上叠加的OSD字符数目受限,也不好控制水印的方向,所以还是自己修改下程序完成新的需求更改。
最初方案
原本想这是一个很简单的功能,直接在原本的Qt的窗口上面,放一个透明的窗口,将重写透明窗口的paintEvent(QPaintEvetn *evt)的事件,同时将叠加水印的窗口的widget放到窗口的最上面显示。主窗口中初始化水印窗口的widget。这样程序的总体逻辑基本不用改动,但实际情况与想象的还是有点差异。
WaterMarkWidget* pWidget=new WaterMarkWidget(this);
pWidget->raise(); //将水印窗口放到父窗口的最前面显示
pWidget->show();
问题描述
- 窗口关系描述:
- 运行环境,ubuntu 16.04
- 主窗口widget里面有一个QLabel用于视频播放,主窗口必须置顶,同时主窗口不是全屏的,主窗口必须要遮挡主ubuntu左边的菜单栏。
- 设置QLabel的背景色为黑色,同时设置文字,无视频信号,同时将QLabel的窗口的句柄,传入给调用视频的SDK(调试时使用了海康的NVR)。
- 设置主窗口没有边界,并且置顶显示。
- 新需求,新增加一个WaterMarkWidget的水印窗口,放置父窗口最上面。
- 未播放视频前,水印叠加正常,看似一切都很正常,为了能分析出问题,特意将水印窗口与主窗口进行了X方向上的平移。
- 播放视频后,就出现问题了,看图,水印窗口的部分总是显示QLabel的背景色,即使不显示QLabel对象,那么水印窗口部分显示的也是QWidget的背景色。那么水印窗口的部分,在播放视频的区域,并不能正确的显示视频,这就是问题的所在,所以导致原本设想的方案就不可行
原因分析:
具体什么原因导致的这种情况,其实还不是很明确,如果大家能知道原因,希望能指出。
解决方案:
方案1:既然使用窗口的句柄的方式播放,无法直接叠加水印,那么可以采用回调视频流的方式,获取转码后的YUV的数据,然后转换成RGB数据,最后转换成QImaeg,然后使用QLabel显示,这种方式效率比较低下,消耗的CPU也比较高。
方案2:在父窗口的中,既然无法完成窗口水印叠加,就需要单独实例一个独立窗口,浮动在原本的视频窗口上,完成水印叠加,视频窗口和水印窗口都需要置顶,那么每次在主窗口视频显示的同时,需要通知水印窗口置顶显示,这样水印窗口才能一直在视频窗口上面。
方案1实现:
- 本方案主要就是如何获取回调海康的视频流并转换成QImage,下面附代码
- 海康播放视频时调用视频接口
-
//实时视频播放时设置回调函数 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);
- 回调函数部分
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; }
- 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的数据 } }
- rgb数据转换成QImage
//rgb888 转QImage QImage image(pPicBuffer,nWidth,nHeight,QImage::Format_RGB888);
- 最后在由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实现:
- 首先要设置窗口置顶,使用Qt常规方法如下,但是这个窗口置顶,无法覆盖住ubuntu左边的标题栏,
//设置无边框并且窗口置顶 setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Window);
- 置顶的窗口无法在遮挡住菜单栏
- 使用Qt::Tooltip 标志,可以使窗口置顶并且无边框,最重要的一点就是可以覆盖ubuntu窗口的菜单栏,但是这个也有一个问题,在同一个主窗口的程序中,只能设置一次Qt::Tooltip,否则其余窗口设置,则无效了。
setWindowFlags(Qt::ToolTip); //水印窗口设置此标志 并且给水印窗口不设置父窗口,独立生成一个窗口 //此时子窗口无法显示 具体原因也不清楚 WaterMarkWidget* pWidget=new WaterMarkWidget(); pWidget->setWindowFlags(Qt::ToolTip | Qt::Window); pWidget->show();
- 单独重新写了水印窗口的程序,在给窗口设置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(); }
- 通过这种方式,比方案1消耗的资源更小,
总结:
本项目,由于在ubuntu环境下并且涉及到多窗口的置顶,视频窗口下面还有编译的一个浏览器窗口置顶,所以置顶的程序会稍微麻烦点,正常的可以直接在主窗口中使WaterMarkWidget 设置成一个独立的子窗口的置顶在视频窗口上面。