找回密码
 立即注册
首页 资源区 代码 封装 WebRTC 低延迟视频流与 WebSocket 实时状态驱动的 ...

封装 WebRTC 低延迟视频流与 WebSocket 实时状态驱动的大屏可视化

裸历 前天 15:10
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/防火墙建立连接。

  • 建立连接:通过 RTCPeerConnection API 建立 P2P 通道。
  • 媒体流传输:连接建立后,音视频流直接在两端传输,延迟通常控制在 500ms 以内。
  • 关于 webRtc 信令交换原理,和更多用途,可参考管网(https://webrtc.org.cn/)。
技术优势:

  • 低延迟:WebRTC 基于 P2P 通信,延迟通常在 500ms 以内,满足实时监控需求。
  • 跨平台:支持所有现代浏览器(如 Chrome、Firefox、Safari)和移动应用(如 Android、iOS)。
  • 无需插件:无需安装任何插件,直接在浏览器中运行。
  • 安全:所有通信均在 HTTPS 环境下进行,确保数据隐私。
二、 WebRTC 播放器的优雅封装

为了复用逻辑并隔离底层复杂度,我封装了一个 WebRTCPlayer 类,专门负责与信令服务器交互和流媒体渲染。
1. 核心类设计 (WebRTCPlayer.ts)

我用 WebSocket 作为信令通道,设计了一套信令交互协议。
  1. class WebRTCPlayer {
  2.   ws: WebSocket | null = null;
  3.   pc: RTCPeerConnection | null = null;
  4.   pendingCandidates: any[] = []; // 暂存的 ICE 候选者,等待远程描述设置完成后添加
  5.   isConnecting = false; // 是否正在连接中
  6.   videoElement: HTMLVideoElement; // 视频播放元素
  7.   serverUrl: string; // WebSocket 信令服务器地址
  8.   taskId: string; // 任务ID,用于标识视频流
  9.   rtcConfig: RTCConfiguration; // WebRTC 配置(STUN/TURN 服务器)
  10.   maxRetry =30; // 最大重连次数
  11.   retryCount = 0; // 当前重连次数
  12.   reconnectTimer: any = null; // 重连定时器
  13.   heartbeatTimer: any = null; // 心跳定时器
  14.   /**
  15.    * 构造函数
  16.    * @param videoElement HTMLVideoElement 视频播放的 DOM 节点
  17.    * @param serverIp string 服务器 IP 地址
  18.    * @param taskId string 任务 ID
  19.    */
  20.   constructor(videoElement: HTMLVideoElement, serverIp: string, taskId: string) {
  21.     this.videoElement = videoElement;
  22.     this.serverUrl = `ws://${serverIp}:8080/ws`;
  23.     this.taskId = taskId;
  24.     // 配置 ICE 服务器,包含 Google 的公共 STUN 和自建的 TURN 服务
  25.     this.rtcConfig = { iceServers: [
  26.     { urls: 'stun:stun.l.google.com:19302' },  // STUN
  27.     {
  28.       urls: 'turn:192.168.1.111:10002',  // ZLMediaKit TURN
  29.       username: 'your_username',
  30.       credential: 'your_password'
  31.     }
  32.   ]};
  33.   }
  34.   /**
  35.    * 启动播放
  36.    * 重置重连计数并开始连接 WebSocket
  37.    */
  38.   start() {
  39.     this.retryCount = 0;
  40.     this.connectWs();
  41.   }
  42.   /**
  43.    * 连接 WebSocket 信令服务器
  44.    */
  45.   connectWs() {
  46.     // 如果 WebSocket 已连接,直接发送请求流指令
  47.     if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  48.       this.send({ type: 'request_stream', task_id: this.taskId });
  49.       return;
  50.     }
  51.     if (this.isConnecting) return;
  52.     this.isConnecting = true;
  53.     // 清理旧的 PeerConnection 和 WebSocket
  54.     this.cleanupPeer();
  55.     if (this.ws) {
  56.       try { this.ws.close(); } catch {}
  57.       this.ws = null;
  58.     }
  59.     const ws = new WebSocket(this.serverUrl);
  60.     this.ws = ws;
  61.     ws.onopen = () => {
  62.       this.isConnecting = false;
  63.       this.retryCount = 0;
  64.       // 连接成功后请求视频流
  65.       this.send({ type: 'request_stream', task_id: this.taskId });
  66.       this.startHeartbeat();
  67.     };
  68.     ws.onmessage = async (event) => {
  69.       const msg = JSON.parse(event.data);
  70.       await this.handleSignalingMessage(msg);
  71.     };
  72.     ws.onerror = () => {
  73.       this.isConnecting = false;
  74.       this.scheduleReconnect();
  75.     };
  76.     ws.onclose = () => {
  77.       this.isConnecting = false;
  78.       this.stopHeartbeat();
  79.       this.scheduleReconnect();
  80.     };
  81.   }
  82.   /**
  83.    * 发送 WebSocket 消息
  84.    * @param payload 消息体
  85.    */
  86.   send(payload: any) {
  87.     if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  88.       this.ws.send(JSON.stringify(payload));
  89.     }
  90.   }
  91.   /**
  92.    * 处理信令消息
  93.    * @param msg 信令消息对象
  94.    */
  95.   async handleSignalingMessage(msg: any) {
  96.     if (!this.pc) this.createPeerConnection();
  97.     const pc = this.pc!;
  98.     switch (msg.type) {
  99.       case 'offer': {
  100.         // 收到服务器的 Offer,设置远程描述
  101.         await pc.setRemoteDescription({ type: 'offer', sdp: msg.sdp });
  102.         // 创建 Answer
  103.         const answer = await pc.createAnswer();
  104.         // 设置本地描述
  105.         await pc.setLocalDescription(answer);
  106.         // 发送 Answer 给服务器
  107.         this.send({ type: 'answer', sdp: answer.sdp });
  108.         // 处理暂存的 ICE 候选者
  109.         while (this.pendingCandidates.length) {
  110.           const candidate = this.pendingCandidates.shift();
  111.           try {
  112.             await pc.addIceCandidate(candidate);
  113.           } catch (e) {
  114.             console.error('Adding pending ICE candidate failed:', e);
  115.           }
  116.         }
  117.         break;
  118.       }
  119.       case 'ice_candidate': {
  120.         // 收到 ICE 候选者
  121.         if (msg.candidate) {
  122.            const candidate = { candidate: msg.candidate, sdpMLineIndex: msg.sdpMLineIndex };
  123.            if (pc.remoteDescription) {
  124.              try {
  125.                await pc.addIceCandidate(candidate);
  126.              } catch (e) {
  127.                console.error('添加 ICE 候选失败:', e);
  128.              }
  129.            } else {
  130.              // 如果远程描述还没设置好,先暂存
  131.              this.pendingCandidates.push(candidate);
  132.            }
  133.         }
  134.         break;
  135.       }
  136.       case 'pong':
  137.         // 收到心跳回应,不做处理
  138.         break;
  139.     }
  140.   }
  141.   /**
  142.    * 创建 WebRTC 连接对象
  143.    */
  144.   createPeerConnection() {
  145.     this.cleanupPeer();
  146.     const pc = new RTCPeerConnection(this.rtcConfig);
  147.     this.pc = pc;
  148.     // 收到远程流时的回调
  149.     pc.ontrack = (event) => {
  150.       console.log(`[${this.taskId}] ontrack`, event);
  151.       const stream = event.streams[0];
  152.       this.videoElement.srcObject = stream;
  153.       this.videoElement.play().catch(() => {});
  154.       // 监听流结束事件
  155.       stream.getTracks().forEach((t) => {
  156.          t.onended = () => this.scheduleReconnect();
  157.       });
  158.     };
  159.     // 收集到本地 ICE 候选者时,发送给服务器
  160.     pc.onicecandidate = (event) => {
  161.       if (event.candidate) {
  162.         this.send({ type: 'ice_candidate', candidate: event.candidate.candidate, sdpMLineIndex: event.candidate.sdpMLineIndex });
  163.       }
  164.     };
  165.     // 连接状态变化监听
  166.     pc.onconnectionstatechange = () => {
  167.       const s = pc.connectionState as any;
  168.       if (s === 'failed' || s === 'disconnected') {
  169.         this.scheduleReconnect();
  170.       }
  171.     };
  172.     pc.oniceconnectionstatechange = () => {
  173.       const s = pc.iceConnectionState as any;
  174.       if (s === 'failed' || s === 'disconnected') {
  175.         this.scheduleReconnect();
  176.       }
  177.     };
  178.   }
  179.   /**
  180.    * 调度重连
  181.    * 使用指数退避算法计算重连延迟
  182.    */
  183.   scheduleReconnect() {
  184.     if (this.reconnectTimer) return;
  185.     if (this.retryCount >= this.maxRetry) return;
  186.     const delay = Math.min(30000, 1000 * Math.pow(2, this.retryCount));
  187.     this.retryCount++;
  188.     this.reconnectTimer = setTimeout(() => {
  189.       this.reconnectTimer = null;
  190.       this.connectWs();
  191.     }, delay);
  192.   }
  193.   /**
  194.    * 开始发送心跳
  195.    */
  196.   startHeartbeat() {
  197.     this.stopHeartbeat();
  198.     this.heartbeatTimer = setInterval(() => {
  199.       this.send({ type: 'ping' });
  200.     }, 15000);
  201.   }
  202.   /**
  203.    * 停止心跳
  204.    */
  205.   stopHeartbeat() {
  206.     if (this.heartbeatTimer) {
  207.       clearInterval(this.heartbeatTimer);
  208.       this.heartbeatTimer = null;
  209.     }
  210.   }
  211.   /**
  212.    * 清理 WebRTC 连接资源
  213.    */
  214.   cleanupPeer() {
  215.     if (this.pc) {
  216.       try { this.pc.close(); } catch {}
  217.       this.pc = null;
  218.     }
  219.   }
  220.   /**
  221.    * 停止播放并清理所有资源
  222.    */
  223.   stop() {
  224.     this.stopHeartbeat();
  225.     if (this.ws) try { this.ws.close(); } catch {}
  226.     this.ws = null;
  227.     this.cleanupPeer();
  228.     if (this.reconnectTimer) {
  229.       clearTimeout(this.reconnectTimer);
  230.       this.reconnectTimer = null;
  231.     }
  232.   }
  233. }
  234. export default WebRTCPlayer;
复制代码
2. 页面使用、信令交互流程

WebRTC 的核心在于 SDP (Session Description Protocol) 的交换。我们的实现流程如下:
使用 video 标签渲染视频流
  1.         <video
  2.           :ref="(el) => (videoRefs[index] = el as HTMLVideoElement)"
  3.           autoplay
  4.           muted
  5.           controls
  6.           playsinline
  7.           webkit-playsinline
  8.          
  9.         ></video>
  10.         CAM-0{{ index + 1 }}
  11. ```
  12. 1. 前端发起请求 :连接 WS 成功后,发送 request_stream 指令。
  13. 2. 后端响应 Offer :后端创建 WebRTC Peer,发送 offer SDP。
  14. 3. 前端应答 Answer :前端收到 Offer,设置 Remote Description,创建 Answer SDP 并发送给后端。
  15. 4. ICE 穿透 :双方交换 ICE Candidate 信息,建立 P2P 连接(或通过 TURN 中转)。
  16. 5. 最终实现效果(https://img2024.cnblogs.com/blog/2819675/202601/2819675-20260108132950357-835871349.png)
  17. 总结
  18. 通过 WebRTC ,我们将视频流延迟控制在了 500ms 以内,实现了“所见即所得”的监控体验;通过 WebSocket + Vue3体系,我们构建了一套高效的状态同步机制,让复杂的作业流程数据能够实时、准确地呈现在用户面前,当然这个需要后端配合,后端需要将传统流,转换为 WebRTC 流,具体事项可以参考 WebRTC 官方文档。
  19. 另外这种“实时流 + 实时信令”的架构模式,不仅适用于智慧安防,在远程医疗、在线教育等对实时性要求极高的场景中也具有广泛的应用价值。
  20. 最后功能实现后,建议可以详细去官网详细了解下 WebRTC 信令交互流程,上面提供有,代码里有注释,也是根据我自己的理解写的,不一定准确,而且还有其他一些有意思的功能,像是webRTC实现视频通话,视频会议这些。
  21. 如有问题,欢迎交流。
  22. ```
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册