Vue3 :封裝 WebRTC 低延遲視頻流與 WebSocket 實時狀態驅動的大屏可視化
在工業互聯網和智慧安防領域,實時監控大屏是核心業務場景之一。本文將分享在最近的“油罐車作業智能監控系統”中,如何利用 Vue3 + TypeScript 技術棧,實現低延遲的 WebRTC 視頻流播放,以及基於 WebSocket 的全鏈路作業狀態實時同步。
一、 業務背景與要求
我們公司需要開發一個監控大屏,實時展示油罐車在卸油作業過程中的監控畫面,並同步顯示 AI 識別出的作業狀態(如:是否佩戴安全帽、是否連接靜電球、卸油操作步驟等),原本是打算採用 videojs 來實現視頻播放,但是在開發中發現,videojs 的延遲較高(3-10 秒),無法滿足實時風控需求,後來使用了別的一些視頻播放庫,如 hls.js、flv.js 等,但是這些庫的延遲也較高(1-3 秒),無法達到業主要求,最後去了解了下直播用的啥插件,嘗試了了下 webRtc 效果還不錯。
什麼是 WebRTC?
WebRTC (Web Real-Time Communication)是一項開源技術,旨在讓瀏覽器和移動應用通過簡單的 API 實現實時音視頻通信和數據傳輸,而無需安裝任何插件。它由 Google、Mozilla、Opera 等巨頭推動,已成為 W3C 和 IETF 的國際標準。
WebRTC 的核心在於點對點 (P2P)通信能力。不同於傳統的流媒體技術(如 HLS、RTMP)通常需要經過服務器中轉和緩存,WebRTC 允許兩個客户端直接建立連接,從而極大地降低了延遲。
核心用法:
- 信令交換 (Signaling):雖然 WebRTC 是 P2P 的,但在建立連接前,雙方需要通過一個“中間人”(信令服務器,通常使用 WebSocket,用普通的 http 請求也可以)來交換元數據。
- SDP (Session Description Protocol):交換媒體能力信息(如編碼格式、分辨率)。雙方通過
Offer和Answer模式進行協商。 - ICE (Interactive Connectivity Establishment):交換網絡地址候選者 (
ICE Candidates),用於穿越 NAT/防火牆建立連接。
- SDP (Session Description Protocol):交換媒體能力信息(如編碼格式、分辨率)。雙方通過
- 建立連接:通過
RTCPeerConnectionAPI 建立 P2P 通道。 - 媒體流傳輸:連接建立後,音視頻流直接在兩端傳輸,延遲通常控制在 500ms 以內。
- 關於 webRtc 信令交換原理,和更多用途,可參考管網(https://webrtc.org.cn/)。
技術優勢:
- 低延遲:WebRTC 基於 P2P 通信,延遲通常在 500ms 以內,滿足實時監控需求。
- 跨平台:支持所有現代瀏覽器(如 Chrome、Firefox、Safari)和移動應用(如 Android、iOS)。
- 無需插件:無需安裝任何插件,直接在瀏覽器中運行。
- 安全:所有通信均在 HTTPS 環境下進行,確保數據隱私。
二、 WebRTC 播放器的優雅封裝
為了複用邏輯並隔離底層複雜度,我封裝了一個 WebRTCPlayer 類,專門負責與信令服務器交互和流媒體渲染。
1. 核心類設計 (WebRTCPlayer.ts)
我用 WebSocket 作為信令通道,設計了一套信令交互協議。
class WebRTCPlayer {
ws: WebSocket | null = null;
pc: RTCPeerConnection | null = null;
pendingCandidates: any[] = []; // 暫存的 ICE 候選者,等待遠程描述設置完成後添加
isConnecting = false; // 是否正在連接中
videoElement: HTMLVideoElement; // 視頻播放元素
serverUrl: string; // WebSocket 信令服務器地址
taskId: string; // 任務ID,用於標識視頻流
rtcConfig: RTCConfiguration; // WebRTC 配置(STUN/TURN 服務器)
maxRetry =30; // 最大重連次數
retryCount = 0; // 當前重連次數
reconnectTimer: any = null; // 重連定時器
heartbeatTimer: any = null; // 心跳定時器
/**
* 構造函數
* @param videoElement HTMLVideoElement 視頻播放的 DOM 節點
* @param serverIp string 服務器 IP 地址
* @param taskId string 任務 ID
*/
constructor(videoElement: HTMLVideoElement, serverIp: string, taskId: string) {
this.videoElement = videoElement;
this.serverUrl = `ws://${serverIp}:8080/ws`;
this.taskId = taskId;
// 配置 ICE 服務器,包含 Google 的公共 STUN 和自建的 TURN 服務
this.rtcConfig = { iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, // STUN
{
urls: 'turn:192.168.1.111:10002', // ZLMediaKit TURN
username: 'your_username',
credential: 'your_password'
}
]};
}
/**
* 啓動播放
* 重置重連計數並開始連接 WebSocket
*/
start() {
this.retryCount = 0;
this.connectWs();
}
/**
* 連接 WebSocket 信令服務器
*/
connectWs() {
// 如果 WebSocket 已連接,直接發送請求流指令
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.send({ type: 'request_stream', task_id: this.taskId });
return;
}
if (this.isConnecting) return;
this.isConnecting = true;
// 清理舊的 PeerConnection 和 WebSocket
this.cleanupPeer();
if (this.ws) {
try { this.ws.close(); } catch {}
this.ws = null;
}
const ws = new WebSocket(this.serverUrl);
this.ws = ws;
ws.onopen = () => {
this.isConnecting = false;
this.retryCount = 0;
// 連接成功後請求視頻流
this.send({ type: 'request_stream', task_id: this.taskId });
this.startHeartbeat();
};
ws.onmessage = async (event) => {
const msg = JSON.parse(event.data);
await this.handleSignalingMessage(msg);
};
ws.onerror = () => {
this.isConnecting = false;
this.scheduleReconnect();
};
ws.onclose = () => {
this.isConnecting = false;
this.stopHeartbeat();
this.scheduleReconnect();
};
}
/**
* 發送 WebSocket 消息
* @param payload 消息體
*/
send(payload: any) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(payload));
}
}
/**
* 處理信令消息
* @param msg 信令消息對象
*/
async handleSignalingMessage(msg: any) {
if (!this.pc) this.createPeerConnection();
const pc = this.pc!;
switch (msg.type) {
case 'offer': {
// 收到服務器的 Offer,設置遠程描述
await pc.setRemoteDescription({ type: 'offer', sdp: msg.sdp });
// 創建 Answer
const answer = await pc.createAnswer();
// 設置本地描述
await pc.setLocalDescription(answer);
// 發送 Answer 給服務器
this.send({ type: 'answer', sdp: answer.sdp });
// 處理暫存的 ICE 候選者
while (this.pendingCandidates.length) {
const candidate = this.pendingCandidates.shift();
try {
await pc.addIceCandidate(candidate);
} catch (e) {
console.error('Adding pending ICE candidate failed:', e);
}
}
break;
}
case 'ice_candidate': {
// 收到 ICE 候選者
if (msg.candidate) {
const candidate = { candidate: msg.candidate, sdpMLineIndex: msg.sdpMLineIndex };
if (pc.remoteDescription) {
try {
await pc.addIceCandidate(candidate);
} catch (e) {
console.error('添加 ICE 候選失敗:', e);
}
} else {
// 如果遠程描述還沒設置好,先暫存
this.pendingCandidates.push(candidate);
}
}
break;
}
case 'pong':
// 收到心跳回應,不做處理
break;
}
}
/**
* 創建 WebRTC 連接對象
*/
createPeerConnection() {
this.cleanupPeer();
const pc = new RTCPeerConnection(this.rtcConfig);
this.pc = pc;
// 收到遠程流時的回調
pc.ontrack = (event) => {
console.log(`[${this.taskId}] ontrack`, event);
const stream = event.streams[0];
this.videoElement.srcObject = stream;
this.videoElement.play().catch(() => {});
// 監聽流結束事件
stream.getTracks().forEach((t) => {
t.onended = () => this.scheduleReconnect();
});
};
// 收集到本地 ICE 候選者時,發送給服務器
pc.onicecandidate = (event) => {
if (event.candidate) {
this.send({ type: 'ice_candidate', candidate: event.candidate.candidate, sdpMLineIndex: event.candidate.sdpMLineIndex });
}
};
// 連接狀態變化監聽
pc.onconnectionstatechange = () => {
const s = pc.connectionState as any;
if (s === 'failed' || s === 'disconnected') {
this.scheduleReconnect();
}
};
pc.oniceconnectionstatechange = () => {
const s = pc.iceConnectionState as any;
if (s === 'failed' || s === 'disconnected') {
this.scheduleReconnect();
}
};
}
/**
* 調度重連
* 使用指數退避算法計算重連延遲
*/
scheduleReconnect() {
if (this.reconnectTimer) return;
if (this.retryCount >= this.maxRetry) return;
const delay = Math.min(30000, 1000 * Math.pow(2, this.retryCount));
this.retryCount++;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connectWs();
}, delay);
}
/**
* 開始發送心跳
*/
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
this.send({ type: 'ping' });
}, 15000);
}
/**
* 停止心跳
*/
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* 清理 WebRTC 連接資源
*/
cleanupPeer() {
if (this.pc) {
try { this.pc.close(); } catch {}
this.pc = null;
}
}
/**
* 停止播放並清理所有資源
*/
stop() {
this.stopHeartbeat();
if (this.ws) try { this.ws.close(); } catch {}
this.ws = null;
this.cleanupPeer();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}
export default WebRTCPlayer;
2. 頁面使用、信令交互流程
WebRTC 的核心在於 SDP (Session Description Protocol) 的交換。我們的實現流程如下:
使用 video 標籤渲染視頻流
<div class="video-card" v-for="(cfg, index) in playersConfig" :key="index">
<video
:ref="(el) => (videoRefs[index] = el as HTMLVideoElement)"
autoplay
muted
controls
playsinline
webkit-playsinline
class="video-player"
></video>
<div class="video-label">CAM-0{{ index + 1 }}</div>
</div>
<script lang="ts" setup name="AnalysisDashboard">
import { onMounted, ref, onUnmounted, unref, computed } from 'vue';
// 引入 WebRTC 類,根據項目需求,可根據實際情況調整引入路徑
import WebRTCPlayerClass from '/@/components/Ljh/WebRTC/index';
const videoRefs = ref<HTMLVideoElement[]>([]);
// serverIp我配置在環境變量中,可根據需求自行配置在哪,寫在這也行。
const cameraServerIp = import.meta.env.VITE_CAMERA_SERVER_IP as string;
// 配置前端和後端的服務器 serverIp 和任務 taskId(這個是和後端約定傳的參數,這個項目根據這個參數來區分是前端流還是後端流,因為要展示兩個流,如果沒有特殊要求傳參,就不用配)
const playersConfig = ref([
{ serverIp: cameraServerIp, taskId: 'front' },
{ serverIp: cameraServerIp, taskId: 'backend' },
]);
onMounted(() => {
// 初始化 WebRTC 連接
playersConfig.value.forEach((cfg, index) => {
const el = videoRefs.value[index];
if (el) {
const p = new WebRTCPlayerClass(el, cfg.serverIp, cfg.taskId);
players.value[index] = p;
try {
p.start();
} catch (e) {
console.error(e);
}
}
});
});
onUnmounted(() => {
players.value.forEach((p) => {
try {
p.stop();
} catch (e) {
console.error(e);
}
});
});
</script>
<style scoped>
.video-card{
......
// 寫視頻樣式
}
</style>
```
1. 前端發起請求 :連接 WS 成功後,發送 request_stream 指令。
2. 後端響應 Offer :後端創建 WebRTC Peer,發送 offer SDP。
3. 前端應答 Answer :前端收到 Offer,設置 Remote Description,創建 Answer SDP 併發送給後端。
4. ICE 穿透 :雙方交換 ICE Candidate 信息,建立 P2P 連接(或通過 TURN 中轉)。
5. 最終實現效果(https://img2024.cnblogs.com/blog/2819675/202601/2819675-20260108132950357-835871349.png)
總結
通過 WebRTC ,我們將視頻流延遲控制在了 500ms 以內,實現了“所見即所得”的監控體驗;通過 WebSocket + Vue3體系,我們構建了一套高效的狀態同步機制,讓複雜的作業流程數據能夠實時、準確地呈現在用户面前,當然這個需要後端配合,後端需要將傳統流,轉換為 WebRTC 流,具體事項可以參考 WebRTC 官方文檔。
另外這種“實時流 + 實時信令”的架構模式,不僅適用於智慧安防,在遠程醫療、在線教育等對實時性要求極高的場景中也具有廣泛的應用價值。
最後功能實現後,建議可以詳細去官網詳細瞭解下 WebRTC 信令交互流程,上面提供有,代碼裏有註釋,也是根據我自己的理解寫的,不一定準確,而且還有其他一些有意思的功能,像是webRTC實現視頻通話,視頻會議這些。
如有問題,歡迎交流。
```