您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# Qt Onvif云台控制实现指南
## 1. 前言:ONVIF协议与云台控制
ONVIF(Open Network Video Interface Forum)是网络视频设备领域的国际标准化协议,它为网络视频设备之间的通信提供了统一的接口规范。云台控制(PTZ Control)作为ONVIF协议的核心功能之一,允许用户通过网络远程控制摄像机的平移(Pan)、倾斜(Tilt)和变焦(Zoom)操作。
在Qt框架下实现ONVIF云台控制,需要深入理解以下几个关键技术点:
- ONVIF协议架构与WS-Discovery设备发现机制
- SOAP协议在设备通信中的应用
- PTZ服务的WSDL接口定义
- Qt的网络通信与XML处理能力
## 2. 开发环境准备
### 2.1 基础工具链
```bash
# 推荐开发环境
Qt 5.15+ 或 Qt 6.x
支持C++11的编译器(GCC/MSVC/Clang)
Wireshark(用于协议分析)
ONVIF Device Test Tool(设备兼容性测试)
// pro文件配置示例
QT += network xml websockets
LIBS += -lgsoap++ # 推荐使用gSOAP库处理SOAP协议
建议使用以下工具构建测试环境: 1. ONVIF模拟器(如ONVIF Device Simulator) 2. 真实支持ONVIF协议的IPC摄像机 3. 网络抓包工具(验证协议交互)
ONVIF使用WS-Discovery协议进行设备发现,采用UDP多播方式(地址239.255.255.250,端口3702)发送Probe消息。
// 设备发现类头文件
class OnvifDeviceDiscoverer : public QObject {
Q_OBJECT
public:
explicit OnvifDeviceDiscoverer(QObject *parent = nullptr);
void startDiscovery();
private slots:
void readPendingDatagrams();
private:
QUdpSocket *discoverySocket;
QString probeMessage();
};
// 实现代码
void OnvifDeviceDiscoverer::startDiscovery() {
discoverySocket = new QUdpSocket(this);
connect(discoverySocket, &QUdpSocket::readyRead,
this, &OnvifDeviceDiscoverer::readPendingDatagrams);
const QByteArray probe = probeMessage().toUtf8();
discoverySocket->writeDatagram(probe,
QHostAddress("239.255.255.250"), 3702);
}
QString OnvifDeviceDiscoverer::probeMessage() {
return QStringLiteral(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\""
" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\""
" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\""
" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\">"
"<e:Header><w:MessageID>uuid:%1</w:MessageID>"
"<w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>"
"<w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>"
"</e:Header><e:Body><d:Probe><d:Types>dn:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>"
).arg(QUuid::createUuid().toString());
}
获取设备支持的ONVIF服务端点(包括PTZ服务地址)。
QString OnvifController::getCapabilities(const QString &deviceEndpoint) {
QNetworkRequest request;
request.setUrl(QUrl(deviceEndpoint));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/soap+xml");
QString soapTemplate =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\""
" xmlns:wsdl=\"http://www.onvif.org/ver10/device/wsdl\">"
"<soap:Header><wsdl:Security>...</wsdl:Security></soap:Header>"
"<soap:Body><wsdl:GetCapabilities><wsdl:Category>PTZ</wsdl:Category></wsdl:GetCapabilities></soap:Body>"
"</soap:Envelope>";
QNetworkReply *reply = manager->post(request, soapTemplate.toUtf8());
connect(reply, &QNetworkReply::finished, [=]() {
// 解析返回的XML获取PTZ服务端点
});
}
<!-- 设备返回的Capabilities响应示例 -->
<tds:Capabilities>
<ptz:XAddr>http://192.168.1.100/onvif/ptz_service</ptz:XAddr>
<ptz:FixedHomePosition>true</ptz:FixedHomePosition>
<ptz:ContinuousPanTiltVelocitySpace>true</ptz:ContinuousPanTiltVelocitySpace>
</tds:Capabilities>
void OnvifPtzController::absoluteMove(const QString &profileToken,
double pan, double tilt, double zoom) {
QString soapRequest = QString(
"<tptz:AbsoluteMove>"
"<tptz:ProfileToken>%1</tptz:ProfileToken>"
"<tptz:Position>"
"<tt:PanTilt x=\"%2\" y=\"%3\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\"/>"
"<tt:Zoom x=\"%4\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\"/>"
"</tptz:Position>"
"</tptz:AbsoluteMove>"
).arg(profileToken).arg(pan).arg(tilt).arg(zoom);
sendPtzCommand(soapRequest);
}
void OnvifPtzController::continuousMove(const QString &profileToken,
double panSpeed, double tiltSpeed,
double zoomSpeed, int timeout) {
QString velocity = QString(
"<tt:PanTilt x=\"%1\" y=\"%2\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace\"/>"
"<tt:Zoom x=\"%3\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace\"/>"
).arg(panSpeed).arg(tiltSpeed).arg(zoomSpeed);
QString soapRequest = QString(
"<tptz:ContinuousMove>"
"<tptz:ProfileToken>%1</tptz:ProfileToken>"
"<tptz:Velocity>%2</tptz:Velocity>"
"<tptz:Timeout>PT%3S</tptz:Timeout>"
"</tptz:ContinuousMove>"
).arg(profileToken).arg(velocity).arg(timeout);
sendPtzCommand(soapRequest);
}
// 保存预置位
void OnvifPtzController::setPreset(const QString &profileToken,
const QString &presetName,
int presetToken) {
QString soapRequest = QString(
"<tptz:SetPreset>"
"<tptz:ProfileToken>%1</tptz:ProfileToken>"
"<tptz:PresetName>%2</tptz:PresetName>"
"<tptz:PresetToken>%3</tptz:PresetToken>"
"</tptz:SetPreset>"
).arg(profileToken).arg(presetName).arg(presetToken);
sendPtzCommand(soapRequest);
}
// 调用预置位
void OnvifPtzController::gotoPreset(const QString &profileToken,
int presetToken) {
QString soapRequest = QString(
"<tptz:GotoPreset>"
"<tptz:ProfileToken>%1</tptz:ProfileToken>"
"<tptz:PresetToken>%2</tptz:PresetToken>"
"</tptz:GotoPreset>"
).arg(profileToken).arg(presetToken);
sendPtzCommand(soapRequest);
}
void OnvifPtzController::createPatrolScan(const QString &profileToken,
const QList<int> &presetTokens,
int scanTime) {
QString presetList;
for (int token : presetTokens) {
presetList += QString("<tptz:PresetToken>%1</tptz:PresetToken>").arg(token);
}
QString soapRequest = QString(
"<tptz:CreatePatrolScan>"
"<tptz:ProfileToken>%1</tptz:ProfileToken>"
"<tptz:PresetTour>"
"<tt:AutoStart>true</tt:AutoStart>"
"<tt:StartingCondition>"
"<tt:RecurringTime>PT%2S</tt:RecurringTime>"
"</tt:StartingCondition>"
"<tt:TourSpot>%3</tt:TourSpot>"
"</tptz:PresetTour>"
"</tptz:CreatePatrolScan>"
).arg(profileToken).arg(scanTime).arg(presetList);
sendPtzCommand(soapRequest);
}
QString OnvifController::generateSecurityHeader(const QString &username,
const QString &password) {
QDateTime current = QDateTime::currentDateTimeUtc();
QString created = current.toString("yyyy-MM-ddThh:mm:ss.zzzZ");
QString nonce = QUuid::createUuid().toString().mid(1, 36);
QByteArray nonceBA = nonce.toUtf8();
QByteArray createdBA = created.toUtf8();
QByteArray passwordBA = password.toUtf8();
QByteArray combined = nonceBA + createdBA + passwordBA;
QByteArray sha1 = QCryptographicHash::hash(combined,
QCryptographicHash::Sha1);
QString digest = QString::fromLatin1(sha1.toBase64());
return QString(
"<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">"
"<wsse:UsernameToken>"
"<wsse:Username>%1</wsse:Username>"
"<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">%2</wsse:Password>"
"<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">%3</wsse:Nonce>"
"<wsu:Created xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">%4</wsu:Created>"
"</wsse:UsernameToken>"
"</wsse:Security>"
).arg(username).arg(digest).arg(QString::fromLatin1(nonceBA.toBase64())).arg(created);
}
错误代码 | 含义 | 解决方案 |
---|---|---|
400 | 错误请求 | 检查SOAP消息格式 |
401 | 未授权 | 验证WS-Security头 |
404 | 服务未找到 | 确认设备服务端点 |
500 | 内部错误 | 检查设备日志 |
// 启用详细网络调试
QLoggingCategory::setFilterRules("qt.network.ssl.warning=true");
// 打印原始SOAP请求响应
qDebug().noquote() << "Request:" << soapRequest;
qDebug().noquote() << "Response:" << reply->readAll();
OnvifPtzDemo/
├── include/
│ ├── onvifdiscoverer.h
│ ├── onvifptzcontroller.h
│ └── onvifdeviceinfo.h
├── src/
│ ├── main.cpp
│ ├── onvifdiscoverer.cpp
│ └── onvifptzcontroller.cpp
├── resources/
│ └── ptz_icons/
└── OnvifPtzDemo.pro
通过本文介绍的方法,开发者可以在Qt框架下实现完整的ONVIF云台控制功能。实际项目中还可以进一步扩展:
”`
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。