用QT中的webengine实现音视频传输(学习笔记)
前言
本文讲述的内容最后可以实现手机端在浏览器输入ip和端口号就可以直接拿到电脑端(Ubuntu端)的摄像头内容。本文以一个调试者的视角使用自签名证书,来完成音视频开发,中间很多内容会用各种方式去绕过证书的验证,这也是本文的精髓所在。
目录
前言
开发前的环境搭建
nginx服务器的搭建和部署
Ubuntu和qt的安装和部署
Ubuntu20.04的安装
qt5.12.4的下载
安装qt的特别注意:
模块的选择
在windows下实现视频传输(用于测试)
webrtc协议的简单介绍
信令(Signaling)
网络地址转换穿越(NAT Traversal)
API层
选择用浏览器内置webrtc的原因
webRTC的工作流程
信令交换
网络协商
媒体传输
开发总流程
编辑
用js搭建websocket服务器(server.js)
安装node.js和websocket库
代码如下
实现pc.html
实现mobile.html
注意:pc.html和mobile.html中的ip地址要根据websocket的地址变化而改变
将pc.html,mobile.html部署到nginx上
效果演示
可能遇到的问题
在QT上用qt webengine模块实现电脑端功能
qwebengine模块的介绍
核心类QWebEngineView和QWebEnginePage
QWebEngineView
QWebEngineView简介
核心的api:
QWebEnginePage
QWebEnginePage简介
核心api
代码实现
效果演示
结语
正文开始之前先讲一下我的开发所用到的一些协议和工具,主要就是项目的方案。
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.csdn.net/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的安装
参考这篇文章吧,个人觉得写的非常好。
https://blog.csdn.net/qq_42417071/article/details/136327674?fromshare=blogdetail&sharetype=blogdetail&sharerId=136327674&sharerefer=PC&sharesource=2403_87069802&sharefrom=from_link
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,然后配合上信令服务器的一些接口。
WebRTC Camera (PC) #localVideo { background-color:#CF3; } //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" }
实现mobile.html
WebRTC Camera (Mobile) 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)); } } };
注意: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()