一、前言
雲台控制是視頻監控系統中必備的一個功能,對球機進行上下左右的移動,還有焦距的控制,其實核心就是控制XYZ三個坐標軸,為了開發這個模塊,特意研究了各種雲台控制的方法和開源庫比如soap,有些廠家使用自家SDK控制雲台,但是大部分都會選擇onvif來控制,畢竟是國際標准的通用的,只要符合這個標准的都可以使用,onvif協議的解析通常用的開源庫是soap,涵蓋的內容比較全,包括獲取各種設備信息和回控等,缺點就是比較臃腫,使用非常不容易,函數名實在是有點不順手,很多新手都繞在其中不知所措最后放棄,其實onvif官方提供的就是soap,可能要照顧到所有的onvif標准吧,內容特別多,我看過其中的部分源碼,底層機制和我最終自創的解析機制完全一致,為此特意將純Qt網絡通信封裝了一個onvif通信類做成的pri模塊,大致的處理流程如下:
onvif處理流程
- 綁定組播IP(239.255.255.250)和端口(3702),發送固定的xml格式的數據搜索設備。
- 接收到的xml格式的數據解析,得到設備的Onvif地址。
- 對Onvif地址發送對應的數據,收到數據取出對應的節點數據。
- 請求Onvif地址獲取Media地址和Ptz地址,Media地址用來獲取詳細的配置文件,Ptz地址用來雲台控制。
- ptz控制是對Ptz地址發送對應的數據即可。
- 設置了用戶認證的需要組織用戶token信息一塊發送,每次都需要作鑒權處理。
- 接收到的數據不是標准的xml數據,沒法按照正常的節點解析來處理,只能用QXmlQuery來做。
- 每個廠家設備返回的數據未必完全一致,基本上都不一致,需要進行模糊查找節點值。
- 特意采用底層協議解析,因為soap太臃腫函數名稱太另類,特意做的輕量級的。
- 兩個必備工具,Onvif Device Manager 和 Onvif Device Test Tool。
ptz雲台說明
- x、y、z 范圍都在0-1之間。
- x為負數,表示左轉,x為正數,表示右轉。
- y為負數,表示下轉,y為正數,表示上轉。
- z為正數,表示拉近,z為負數,表示拉遠。
- 通過x和y的組合,來實現雲台的控制。
- 通過z的組合,來實現焦距控制。
onvif功能模塊特點
- 廣播搜索設備,支持IPC和NVR,依次返回,可選擇不同的網卡IP。
- 依次獲取Onvif地址、Media地址、Profile文件、Rtsp地址。
- 可對指定的Profile獲取視頻流Rtsp地址,比如主碼流子碼流地址。
- 可對每個設備設置Onvif用戶信息,用於認證獲取詳細信息。
- 可實時預覽攝像機圖像。
- 支持雲台控制,可上下左右調節雲台,支持絕對移動和相對移動,可放到和縮小圖像遠近。
- 支持Qt4和Qt5任意Qt版本,親測Qt4.7.0到Qt5.12.4。
- 支持任意編譯器,親測mingw、msvc、gcc、clang。
- 支持任意操作系統,親測xp、win7、win10、linux、嵌入式linux、樹莓派全志H3等。
- 支持任意Onvif攝像機和NVR,親測海康、大華、宇視、華為、海思芯片內核等,可定制開發。
- 支持對指定IP地址進行單播搜索,比如跨網段情況下非常有用。
- 純Qt編寫,超級小巧輕量,總共約2000行代碼,不依賴任何第三方的庫和組件,跨平台。
- 封裝好了通用的數據發送和接收解析的函數,可以非常方便的自行拓展其他Onvif處理比如修改IP等。
- 工具上提供了收發數據文本框,顯示收發的數據,方便查看和分析。
- 支持所有Onvif設備,代碼工整,接口友好,直接引入pri即可使用。
通用視頻控件開源地址:https://gitee.com/feiyangqingyun/QWidgetDemo https://github.com/feiyangqingyun/QWidgetDemo
文件名稱:videowidget
體驗地址:https://gitee.com/feiyangqingyun/QWidgetExe https://github.com/feiyangqingyun/QWidgetExe
文件名稱:bin_video_system.zip
二、功能特點
- 支持16畫面切換,全屏切換等,包括1+4+6+8+9+13+16畫面切換。
- 支持alt+enter全屏,esc退出全屏。
- 自定義信息框+錯誤框+詢問框+右下角提示框。
- 17套皮膚樣式隨意更換,所有樣式全部統一,包括菜單等。
- 雲台儀表盤鼠標移上去高亮,八個方位精准識別。
- 底部畫面工具欄(畫面分割切換+截圖聲音等設置)移上去高亮。
- 可在配置文件更改左上角logo+中文軟件名稱+英文軟件名稱。
- 封裝了百度地圖,三維切換,設備點位,鼠標按下獲取經緯度等。
- 堆棧窗體,每個窗體都是個單獨的qwidget,方便編寫自己的代碼。
- 頂部鼠標右鍵菜單,可動態控制時間CPU+左上角面板+左下角面板+右上角面板+右下角面板的顯示和隱藏,支持恢復默認布局。
- 工具欄可以放置多個小圖標和關閉圖標。
- 左側右側可拖動拉伸,並自動記憶寬高位置,重啟后恢復。
- 雙擊攝像機節點自動播放視頻,雙擊節點自動依次添加視頻,會自動跳到下一個,雙擊父節點自動添加該節點下的所有視頻。
- 攝像機節點拖曳到對應窗體播放視頻,同時支持拖曳本地文件直接播放。
- 視頻畫面窗體支持拖曳交換,瞬間響應。
- 雙擊節點+拖曳節點+拖曳窗體交換位置,均自動更新url.txt。
- 支持從url.txt中加載16通道視頻播放,自動記憶最后通道對應的視頻,軟件啟動后自動打開播放。
- 右下角音量條控件,失去焦點自動隱藏,音量條帶靜音圖標。
- 集成百度地圖,可以添加設備對應位置,自動生成地圖,支持縮放和三維地圖,提供地圖風格選擇,共12種風格。
- 視頻拖動到通道窗體外自動刪除視頻。
- 鼠標右鍵可刪除當前+所有視頻,截圖當前+所有視頻。
- 錄像機管理、攝像機管理,可添加刪除修改導入導出打印信息,立即應用新的設備信息生成樹狀列表,不需重啟。
- 在pro文件中可以自由開啟是否加載地圖。
- 視頻播放可選四種內核自由切換,vlc+ffmpeg+easyplayer+海康sdk,均可在pro中設置。
- 可設置1+4+9+16畫面輪詢,可設置輪詢間隔以及輪詢碼流類型等,直接在主界面底部工具欄右側單擊啟動輪詢按鈕即可,再次單擊停止輪詢。
- 默認超過10秒鍾未操作自動隱藏鼠標指針。
- 支持onvif搜素設備,支持任意onvif攝像機,包括但不限於海康大華宇視天地偉業華為等,支持onvif雲台控制。
- 高度可定制化,用戶可以很方便的在此基礎上衍生自己的功能,支持linux系統。
三、效果圖


四、核心代碼
OnvifDevice *frmVideoMain::getCurrentDevice()
{
OnvifDevice *onvifDevice = 0;
//判斷當前url,找出該url對應的ptz地址
if (!App::CurrentUrl.isEmpty()) {
//可能是主碼流也可能是子碼流
int index1 = DBData::IpcInfo_RtspMain.indexOf(App::CurrentUrl);
int index2 = DBData::IpcInfo_RtspSub.indexOf(App::CurrentUrl);
int index = -1;
if (index1 >= 0) {
index = index1;
} else if (index2 >= 0) {
index = index2;
}
if (index >= 0) {
QString userName = DBData::IpcInfo_UserName.at(index);
QString userPwd = DBData::IpcInfo_UserPwd.at(index);
QString onvifAddr = DBData::IpcInfo_OnvifAddr.at(index);
QString mediaAddr = DBData::IpcInfo_MediaAddr.at(index);
QString ptzAddr = DBData::IpcInfo_PtzAddr.at(index);
bool exist = false;
foreach (OnvifDevice *device, devices) {
if (device->getDeviceUrl() == onvifAddr) {
exist = true;
onvifDevice = device;
break;;
}
}
if (!exist) {
onvifDevice = new OnvifDevice(this);
}
onvifDevice->setUser(userName, userPwd);
onvifDevice->setDeviceUrl(onvifAddr);
onvifDevice->setMediaUrl(mediaAddr);
onvifDevice->setPtzUrl(ptzAddr);
if (!exist) {
devices << onvifDevice;
}
}
}
return onvifDevice;
}
void frmVideoMain::moveRelative(double x, double y, double z)
{
OnvifDevice *device = getCurrentDevice();
if (device != 0) {
QString profileToken = device->getProfile();
device->moveRelative(profileToken, x, y, z);
qDebug() << "相對移動" << App::CurrentUrl << profileToken;
}
}
void frmVideoMain::moveAbsolute(double x, double y, double z)
{
OnvifDevice *device = getCurrentDevice();
if (device != 0) {
QString profileToken = device->getProfile();
device->moveAbsolute(profileToken, x, y, z);
qDebug() << "絕對移動" << App::CurrentUrl << profileToken;
}
}
void frmVideoMain::mousePressed(int position)
{
QString str;
if (position == 0) {
str = "底部";
} else if (position == 1) {
str = "左下角";
} else if (position == 2) {
str = "左側";
} else if (position == 3) {
str = "左上角";
} else if (position == 4) {
str = "頂部";
} else if (position == 5) {
str = "右上角";
} else if (position == 6) {
str = "右側";
} else if (position == 7) {
str = "右下角";
} else if (position == 8) {
str = "中間";
}
DeviceHelper::addMsg(QString("按下雲台 %1").arg(str));
}
void frmVideoMain::mouseReleased(int position)
{
QString str;
if (position == 0) {
str = "底部";
} else if (position == 1) {
str = "左下角";
} else if (position == 2) {
str = "左側";
} else if (position == 3) {
str = "左上角";
} else if (position == 4) {
str = "頂部";
} else if (position == 5) {
str = "右上角";
} else if (position == 6) {
str = "右側";
} else if (position == 7) {
str = "右下角";
} else if (position == 8) {
str = "中間";
}
DeviceHelper::addMsg(QString("松開雲台 %1").arg(str));
mousePtz(position);
}
void frmVideoMain::mousePtz(int position)
{
//根據按下的不同部位發送雲台控制命令
//1. x、y、z 范圍都在0-1之間。
//2. x為負數,表示左轉,x為正數,表示右轉。
//3. y為負數,表示下轉,y為正數,表示上轉。
//4. z為正數,表示拉近,z為負數,表示拉遠。
//5. 通過x和y的組合,來實現雲台的控制。
//6. 通過z的組合,來實現焦距控制。
//計算速度,轉為小數
double speed = (double)ui->sliderPtzSpeed->value() / 10;
if (position == 0) {
moveRelative(0.0, -speed, 0.0);
} else if (position == 1) {
moveRelative(-speed, -speed, 0.0);
} else if (position == 2) {
moveRelative(-speed, 0.0, 0.0);
} else if (position == 3) {
moveRelative(-speed, speed, 0.0);
} else if (position == 4) {
moveRelative(0.0, speed, 0.0);
} else if (position == 5) {
moveRelative(speed, speed, 0.0);
} else if (position == 6) {
moveRelative(speed, 0.0, 0.0);
} else if (position == 7) {
moveRelative(speed, -speed, 0.0);
} else if (position == 8) {
moveAbsolute(0.0, 0.0, 0.0);
}
}
