前言
本文讲述的内容最后可以实现手机端在浏览器输入ip和端口号就可以直接拿到电脑端(Ubuntu端)的摄像头内容。本文以一个调试者的视角使用自签名证书,来完成音视频开发,中间很多内容会用各种方式去绕过证书的验证,这也是本文的精髓所在。
目录
注意:pc.html和mobile.html中的ip地址要根据websocket的地址变化而改变
核心类QWebEngineView和QWebEnginePage
正文开始之前先讲一下我的开发所用到的一些协议和工具,主要就是项目的方案。
1.webRTC协议,这个协议是实现音视频传输十分重要的协议,该协议采用的点对点协议,支持html5,所以绝大多数浏览器都内置了这个webRTC的库。但是确定就是建立点对点连接之前需要用其他的方式来交换信令,这里用的就是websocket。
2.websocket协议,该协议采用只能传输低延迟和二进制数据,而且是服务器和客户端的连接方式,所以可以很简单的就建立连接,并完成信令转发。
3.nginx服务器:后续代码需要用到HTML5,和JavaScript的语言,所以需要通过浏览器输入ip和端口来拿到服务器端的html文件。
4.https协议:由于要进行视频传输,并且要调用电脑端的摄像头这种敏感设备,所以http往往是不被浏览器支持的,但是作为开发调试不可能去申请ssl证书,所以本文以一个调试者的视角使用自签名证书,来完成音视频开发,中间很多内容会用各种方式去绕过证书的验证,这也是本文的精髓所在
开发前的环境搭建
nginx服务器的搭建和部署
Nginx介绍:Nginx是一个高性能的Web服务器、反向代理服务器、负载均衡器和HTTP缓存。可以处理静态内容(如HTML、CSS、图片、视频等)并将其响应给客户端。将客户端的请求转发给后端服务器,并将响应返回给客户端。
如果不会搭建的可以参考一下我的另一篇文章https://blog.youkuaiyun.com/2403_87069802/article/details/146415603?spm=1001.2014.3001.5502
Ubuntu和qt的安装和部署
版本选择:Ubuntu 20.04,QT5.12.4
先说明一下:小编之前用的是Ubuntu16.04和QT5.5,但是后面开发会遇到各种问题,比如qwebengine很多接口是在qt5.7引入的,后续也有介绍,所以小编也升级了qt和Ubuntu的版本中间也踩了很多的坑,所以这里也把我的安装流程附上,希望能让大家少踩点坑。
Ubuntu20.04的安装
参考这篇文章吧,个人觉得写的非常好。
qt5.12.4的下载
进入qt官网下载https://download.qt.io/official_releases/你需要的版本。
注意千万不要下载online-installer的版本,因为这个是qt官网给的一个下载工具,它运行之后会直接从官网上拉去资源包,但是qt官网在国外,如果Ubuntu网络配置没有搞好翻墙的话会一直显示下载失败,直接下载opensource版,这个可以直接在本地解压部署。
小编就下了两个版本,用online版搞了好几天没搞定,最后道心破碎了。
进入Ubuntu上运行./qt-opensource-linux-x64-5.12.4.run
可以看到如上的画面,根据指引完成登录,下载即可,这里不难就不多说了。
安装qt的特别注意:
要把qt安装到home目录下,不要安装到root目录下,因为后面需要用qt的webengine调用摄像头,而webengine采用的chromium的内核,其中的sandbox(沙盒检查)无法通过root权限调用,即root权限过高了,浏览器内核会认为有危险,所以后面都是以普通用户的身份来打开qt并运行qt,如果你安装到root目录下的话,普通用户就无法使用qt了,这也是小编踩过的坑,因为这个我后面发现不行的时候又重装了一遍qt。
模块的选择
安装最后一步会让你选择需要的模块,小编这里建议全选,不然后面也无法确定是否会用上其他模块,其实qt默认安装是没有webengine模块的,但是我现在开发突然要用到了,要加装的话会很麻烦,所以如果不是内存实在遭不住的话我建议全装了。
在windows下实现视频传输(用于测试)
因为在Ubuntu下用的webengine也就是浏览器的内核,而浏览器支持的是html5,css,javascript.故调用摄像头的核心接口是JavaScript的
navigator.mediaDevices.getUserMedia
所以在用Ubuntu拿到摄像头之前,可以先用windows下的浏览器先做测试,如果windows都无法成功调用摄像头,Ubuntu的qt上有一大堆的要处理的肯定更不行了。
webrtc协议的简单介绍
WebRTC协议涉及多个关键组件,它们共同协作,确保实时通信的顺利进行。
信令(Signaling)
作用:信令用于在WebRTC客户端之间协调、建立通信,包括会话控制(发起和结束)、网络数据(IP和端口)和媒体数据(编解码器、带宽和媒体类型等SDP信息)等元数据的交换。
网络地址转换穿越(NAT Traversal)
必要性:由于大多数用户位于路由器或防火墙后方,使用私网IP地址,为了实现P2P通信,需要穿透NAT和防火墙。
关键协议:
STUN(Session Traversal Utilities for NAT):轻量级服务器,用于帮助客户端了解自己在公网侧的IP地址和端口。对于“锥形NAT”等非对称NAT场景通常能成功。
TURN(Traversal Using Relays around NAT):中继服务器,当P2P连接无法直接建立时,提供数据转发服务,确保通信的可靠性。TURN会增加带宽成本和网络延迟,但可保证几乎所有复杂网络环境下的连接成功率。
ICE(Interactive Connectivity Establishment)框架:用于完成两客户端媒体协商后的网络连接建立。ICE收集所有可能的候选者(Candidate),包括本地IP地址、通过STUN或TURN服务器获得的公网IP地址和中继路径,交换候选者信息后,通过连通性检查确定最佳的媒体路径。
API层
WebRTC API:目前仅有JavaScript版本,提供了一套简单的接口,允许开发者在Web应用中直接调用浏览器提供的实时通信功能。
关键接口:
RTCPeerConnection:用于建立、维护和管理P2P连接,处理网络连接、音视频编解码、带宽管理等任务。
MediaStream:表示一个媒体数据流,包含音频轨道(AudioTrack)和视频轨道(VideoTrack),开发者可以将其添加到RTCPeerConnection中,通过网络发送到另一个WebRTC客户端。
getUserMedia:用于获取用户的音频和视频输入设备(如麦克风和摄像头)的权限,返回一个包含音视频流的对象。
选择用浏览器内置webrtc的原因
到这里很多人就要问了,我要怎么下载并调用webrtc的库呢,其实浏览器内置了webrtc的库,所以不需要自己去下载和配置,直接通过js调用其api接口即可了。当然后续在Ubuntu上开发的时候可以直接下载webrtc的源码库并交叉编译,但是光是下载webrtc的源码就很复杂了,webrtc的官网下载链接小编怀疑已经损坏了,小编试着下载了一个星期都没下成功,更不用说后面要交叉编译什么的 了,最后选择用浏览器的内核间接的调用webrtc即可了,这样省时省力。
webRTC的工作流程
信令交换
双方通过信令服务器交换会话描述(SDP)信息。SDP以键值对的形式描述媒体会话,包括媒体类型、编解码器、分辨率、带宽等。
一端生成Offer(SDP),描述自身希望发送或接收的媒体类型、可用的编码参数等,通过信令服务器发送给另一端。
另一端收到Offer后,生成Answer(SDP),确认可接受的媒体类型与参数,并返回给发起方。
网络协商
双方通过ICE框架收集候选地址,包括主机候选(设备自身IP/端口)、反射候选(通过STUN服务器获取的公网映射地址)、中继候选(通过TURN服务器获取的中继地址)。
双方交换候选地址信息,ICE进行连通性测试,尝试通过不同的候选地址建立连接。
一旦某组候选地址测试成功,即可作为连接的最终通信路径。ICE后续还可动态监测网络状况并进行切换。
媒体传输
使用RTP/SRTP协议传输音视频数据,确保数据的实时性和安全性。
使用RTCP协议监控传输质量,根据网络状况调整传输策略。
通过数据通道传输任意类型的数据,实现低延迟的点对点交互。
开发总流程
- 用websocket搭建信令服务器
- 用js和html完成pc.html调用电脑摄像头
- 用js完成mobile.html在手机端接收视频流
- 将pc.html和mobile.html放在nginx服务器上进行代理,用户可以直接通过手机/电脑浏览器访问到pc.html和mobile.html
用js搭建websocket服务器(server.js)
安装node.js和websocket库
node.js即运行JavaScript的工具,可以让你随时随地运行js程序
npm是Node.js的包管理器,它允许你从npm注册表安装、发布和管理Node.js包,websocket就需要npm来管理
websocket是一个外部的库。
sudo apt install nodejs
sudo apt install npm
npm install ws
代码如下
const WebSocket = require('/usr/local/nodejs/node_modules/ws');
const fs = require('fs');
const https = require('https');
// 加载 SSL 证书和私钥
const server = https.createServer({
cert: fs.readFileSync('/usr/local/nginx/ssl/nginx-selfsigned.crt'),
key: fs.readFileSync('/usr/local/nginx/ssl/nginx-selfsigned.key')
});
// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ server });
//有用户连接时触发
wss.on('connection', (ws) => {
console.log('A new client connected!');
//受到消息时触发
ws.on('message', (message) => {
console.log('Received:', message);
// 假设 message 是 JSON 格式的字符串
const data = JSON.parse(message);
// 广播消息给所有客户端
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data)); // 确保发送的是 JSON 格式的文本
}
});
});
ws.on('close', () => {
console.log('A client disconnected.');
});
});
// 启动 HTTPS 服务器
server.listen(8888, () => {
console.log('WebSocket server is running on wss://localhost:8888');
});
代码很简单,不需要过多的介绍,要注意的就是加载外部模块时要注意路径,而且websocket在本地运行的话,ip就是本地的地址,即虚拟机的地址,用ifconfig可以查询
WebSocket:从指定的路径引入 ws 模块,这是一个实现 WebSocket 协议的库。
fs:引入 Node.js 的文件系统模块,用于读取文件。
https:引入 Node.js 的 HTTPS 模块,用于创建 HTTPS 服务器。
写完代码之后到对应的路径下运行node server.js即可,这样就表示websocket服务器正在工作中了
实现pc.html
核心接口就是getUserMedia,然后配合上信令服务器的一些接口。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC Camera (PC)</title>
<style>
#localVideo
{
background-color:#CF3;
}
</style>
</head>
<body>
<video id="localVideo" autoplay playsinline muted></video>
<script>
//console.log('aklsdhgklhfgkjhkdfgh');
const localVideo = document.getElementById('localVideo');
let localStream;
let peerConnection;
const ws = new WebSocket('wss://192.168.0.121:8888'); // 替换为信令服务器的 IP 和端口
// 初始化 RTCPeerConnection
function createPeerConnection() {
peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 使用 Google 的公共 STUN 服务器
});
// 处理远程视频流
peerConnection.ontrack = (event) => {
console.log('Received track:', event.track);
if (event.track.kind === 'video') {
const video = document.getElementById('remoteVideo');
video.srcObject = event.streams[0];
video.play(); // 确保视频播放
}
};
// 处理 ICE Candidate
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({ type: 'candidate', candidate: event.candidate }));
}
};
}
// WebSocket 消息处理
ws.onmessage = async (message) => {
const data = JSON.parse(message.data);
console.log('Received data:', data);
if (data.type === 'answer') {
// 收到 Answer,设置远程描述
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
} else if (data.type === 'candidate') {
// 收到 ICE Candidate,添加到连接中
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
};
// 启动摄像头
async function startCamera() {
try {
const constraints = {
video: true,
audio: false // 明确禁用音频
};
localStream = await navigator.mediaDevices.getUserMedia(constraints)
localVideo.srcObject = localStream;
console.log('successs open video');
// 创建 RTCPeerConnection
createPeerConnection();
// 添加本地视频流到连接中
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// 创建 Offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 发送 Offer 到信令服务器
ws.send(JSON.stringify({ type: 'offer', offer: offer }));
} catch (error) {
console.error('Error accessing camera:', error);
}
}
// 当 WebSocket 连接成功后,自动启动摄像头
ws.onopen = () => {
startCamera();
};
//C++ 调用showalert函数
function showalert()
{
alert("asdfg")
}
//C++ 调用getJsData函数
function getJsData()
{
return "C++ Call JS demo"
}
</script>
</body>
</html>
实现mobile.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC Camera (Mobile)</title>
</head>
<body>
<video id="remoteVideo" autoplay playsinline></video>
<script>
const remoteVideo = document.getElementById('remoteVideo');
let peerConnection;
const ws = new WebSocket('wss://192.168.0.121:8888'); // 替换为信令服务器的 IP 和端口
// 初始化 RTCPeerConnection
function createPeerConnection() {
peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 使用 Google 的公共 STUN 服务器
});
// 处理远程视频流
peerConnection.ontrack = (event) => {
console.log('Received track:', event.track);
if (event.track.kind === 'video') {
remoteVideo.srcObject = event.streams[0];
remoteVideo.play(); // 确保视频播放
}
};
// 处理 ICE Candidate
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({ type: 'candidate', candidate: event.candidate }));
}
};
}
// WebSocket 消息处理
ws.onmessage = async (message) => {
const data = JSON.parse(message.data);
console.log('Received data:', data);
if (data.type === 'offer') {
// 创建 RTCPeerConnection
createPeerConnection();//这样才能保证在初始化RTCPeerConnection时就设置ontrack
// 设置远程描述
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
// 创建 Answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 发送 Answer 到信令服务器
ws.send(JSON.stringify({ type: 'answer', answer: answer }));
} else if (data.type === 'candidate') {
// 收到 ICE Candidate,添加到连接中
if (peerConnection) {
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
}
};
</script>
</body>
</html>
注意:pc.html和mobile.html中的ip地址要根据websocket的地址变化而改变
将pc.html,mobile.html部署到nginx上
修改nginx.conf,如下图所示,ip为localhost,端口为9300采用的https服务,pc和mobile共用一个端口,输入时只需要:https://192.168.0.111:9300/pc这样即可
效果演示
电脑端:由于是自签名证书,浏览器访问时可能会弹出警告,不过忽略就好了。
手机端:由于只是测试,手机端的视频大小有点溢出,不过后续处理一下就好了,这个就先不管了。
信令服务器端
可以看到有新用户连接和数据的转发。
可能遇到的问题
这个是浏览器的一个保护机制,即用户没有和浏览器交互(play()),只需要点击页面,刷新即可。
至此,最核心的测试基本上就完成了。
在QT上用qt webengine模块实现电脑端功能
qwebengine模块的介绍
最好的学习方式一定是官网,我只摘出该功能需要用到的类和接口,但是要系统学习了解还是要慢慢品官网。
qwebengine指引官网链接:https://doc.qt.io/qt-5/qtwebengine-index.html
进入官网之后找到Qt WebEngine Features,点击之后可以看到目录,然后可以找到我们需要的webRTC索引。小编用了翻译工具,qt的官网是全英文的,所以我截图出来的可能存在翻译错误,这个不用太过于在意。
这里可以找到webRTC的官方指引(如下图),经过一系列的翻找阅读,发现最重要的就QWebEngineView和QWebEnginePage这两个类
核心类QWebEngineView和QWebEnginePage
官方指引:https://doc.qt.io/qt-5/qwebengineview.html
QWebEngineView
QWebEngineView简介
定位:作为视图层组件,直接用于在界面上显示网页内容。
继承自 QWidget,可像普通控件一样嵌入到 Qt 界面布局中。
提供完整的浏览器视图功能,包括导航按钮、地址栏(需自行实现)等。
支持加载本地 HTML、远程 URL 或直接渲染字符串内容。
核心的api:
-
void QWebEngineView::setPage(QWebEnginePage *page)这个接口可以将界面设置成我想要的Page
-
void QWebEngineView::setHtml(const QString &html, const QUrl &baseUrl = QUrl())这个接口可以直接加载网页,后续也是用这个直接调用我的pc.html
-
QWebEngineSettings *QWebEngineView::settings() const;这个接口可以修改设置,类似浏览器的设置功能,后面就是用这个修改设置,使其支持JavaScript和webrtc广域网连接。几乎全部设置都可以通过这个枚举类型来改变,下图只截取了一部分。
QWebEnginePage
QWebEnginePage简介
QWebEnginePage 定位:作为控制层组件,管理网页的加载、渲染和后台逻辑。 功能: 处理网络请求、资源加载、JavaScript 执行和页面生命周期。 支持自定义请求头、Cookie、代理设置和证书验证。 提供与页面 JavaScript 交互的接口(如执行脚本、接收回调)。 控制页面渲染设置(如缩放比例、字体、用户代理字符串)。
核心api
这个信号就是当你要调用摄像头时会触发的一个信号,即向你申请权限,利用这个信号绑定槽函数,就可以申请到摄像头和麦克风的权限。如下图所示,可以看到有几个关联的类,这几个类其实都需要熟悉,他们互相调用,QWebEnginePage::Feature,setFeaturePermission()
void QWebEnginePage::setFeaturePermission(const QUrl &securityOrigin, QWebEnginePage::Feature feature, QWebEnginePage::PermissionPolicy policy)
这个就是设置权限的函数
这个枚举类型就是权限的确定了。
bool QWebEnginePage::certificateError(const QWebEngineCertificateError &certificateError)
这个虚函数就是验证ssl证书的函数,可以看到return true就是忽略错误,但是默认是return false。所以,要忽略证书错误最重要的就是重写这个函数
代码实现
实现流程:和上面在windows的测试一样,唯一不一样的就是pc.html放在qt上用seturl来跑了,页面显示从浏览器页面变成了webengineview了
在.pro中
QT += core gui webenginewidgets quick webengine
然后重写QWebenginePage。
文件名为:debugwebengine.cpp
#include "debugwebengine.h"
debugwebengine::debugwebengine(QObject* parent): QWebEnginePage(parent)
{
//qDebug() << "debugwebengine initialized.";
connect(this, &QWebEnginePage::featurePermissionRequested, this, &debugwebengine::onFeaturePermissionRequested);
}
//重写这个函数以保证它跳过ssl证书验证
bool debugwebengine::certificateError(const QWebEngineCertificateError &certificateError)
{
return true;
}
void debugwebengine::onFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature) {
//qDebug() << "Granted permission for feature:" ;
// 检查请求的功能权限
if (feature == QWebEnginePage::MediaAudioCapture || feature == QWebEnginePage::MediaVideoCapture) {
// 批准摄像头和麦克风的访问权限
setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser);
qDebug() << "Granted permission for feature:" << feature << "from origin:" << securityOrigin.toString();
} else {
setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser);//所有权限都给
qDebug() << "Denied permission for feature:" << feature << "from origin:" << securityOrigin.toString();
}
}
void debugwebengine::javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID) {
// 将 JavaScript 的 console 输出转发到 Qt 的日志系统
QString logMessage = QString("JS Console: %1 (Line: %2, Source: %3)").arg(message).arg(lineNumber).arg(sourceID);
switch (level) {
case InfoMessageLevel:
qInfo() << logMessage;
break;
case WarningMessageLevel:
qWarning() << logMessage;
break;
case ErrorMessageLevel:
qCritical() << logMessage;
break;
default:
qDebug() << logMessage;
break;
}
}
我加入了一些代码可以让qt和js交互,当然这里是简单交互,要让qt调用js函数或者js调用qt需要额外做很多处理,这里就不赘述了。
debugengine.h
#ifndef DEBUGWEBENGINE_H
#define DEBUGWEBENGINE_H
#include <QWebEnginePage>
class debugwebengine:public QWebEnginePage{
Q_OBJECT
public:
explicit debugwebengine(QObject* parent = nullptr);
~debugwebengine()override{}
protected:
virtual bool certificateError(const QWebEngineCertificateError &certificateError)override;
private slots:
void onFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature);
void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID);
};
#endif // DEBUGWEBENGINE_H
main.cpp
#include <QApplication>
#include <QWebEngineView>
#include <QWebEngineSettings>
#include <QUrl>
#include <QDebug>
#include"debugwebengine.h"
#include <cstdlib> // 用于 setenv
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
setenv("QTWEBENGINE_DISABLE_SANDBOX", "1", 1);
//qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--enable-logging --v=1");
// 创建Web视图(局部变量,无需全局变量)
QWebEngineView view;
// 基础配置
view.settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
// view.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);//这个是屏幕捕获(录屏),不是打开摄像头权限
view.settings()->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, false);//修改了WebRTC的设置,使其不仅限于使用公共网络接口,从而可能允许在局域网内的设备之间建立更直接的WebRTC连接。
debugwebengine* page = new debugwebengine(&view);
view.setPage(page);
view.resize(1024, 768);
// 加载本地HTML文件(更安全的方式)
//const QString htmlPath = "/mnt/hgfs/aic_jeff/final_project/2.画面获取显示/pc.html";
// view.setUrl(QUrl::fromLocalFile(htmlPath)); // 自动处理文件路径格式
const QString htmlUrl = "https://192.168.0.111:9300/pc";
view.setUrl(QUrl(htmlUrl));
view.show();
return app.exec();
}
唯一需要讲解的就是
setenv("QTWEBENGINE_DISABLE_SANDBOX", "1", 1);//关闭沙盒模式,关闭沙盒模式可以减少很多bug,因为这个安全检测其实很烦。
效果演示
首先要确保你的摄像头连接到了Ubuntu上,默认虚拟机是不共享摄像头的,小编这里直接外接了一个摄像头,如果要共享主机摄像头的话,建议各位自己去改一下配置什么的,小编没试过。因为这个摄像头踩了无数的坑,之前一直显示打不开摄像头,找了好久发现是虚拟机没有摄像头。
qt运行画面如下
手机端运行画面如下
结语
至此,整个项目就完成了,这个项目结合了js,qt,web服务器,webrtc等模块实现了音视频的开发,整体难度还是不小的,当然这个功能还有很多有问题的地方,小编也还在学习阶段,欢迎大家留言讨论。