|
|
@@ -1,46 +1,238 @@
|
|
|
-# cw_web
|
|
|
+# WebRTC 直播组件说明
|
|
|
|
|
|
-This template should help get you started developing with Vue 3 in Vite.
|
|
|
+## 概述
|
|
|
|
|
|
-## Recommended IDE Setup
|
|
|
+`WebRTCVideo` 是一个基于 WebRTC 的实时视频直播 Vue 3 组件,用于从远端摄像头设备接收音视频流并在浏览器中播放。组件通过 WebSocket 信令服务器与设备建立 P2P 连接,支持自动重连、画面冻结检测、截图、扬声器/麦克风控制等功能。
|
|
|
|
|
|
-[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
|
|
+技术栈:Vue 3 Composition API + TypeScript + Element Plus。
|
|
|
|
|
|
-## Type Support for `.vue` Imports in TS
|
|
|
+## 连接流程
|
|
|
|
|
|
-TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
|
|
+整个直播连接分为三个阶段:**信令协商 → ICE 连接 → 媒体播放**。
|
|
|
|
|
|
-If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
|
|
+```
|
|
|
+组件挂载
|
|
|
+ │
|
|
|
+ ├─ 1. 建立 WebSocket 信令连接
|
|
|
+ │ ws://{host}:{port}/wswebclient/{meid}
|
|
|
+ │
|
|
|
+ ├─ 2. WebSocket 打开后发送 __connectto(告诉服务器要连哪个设备)
|
|
|
+ │
|
|
|
+ ├─ 3. 服务器返回 _create(设备在线状态 + ICE 服务器配置)
|
|
|
+ │
|
|
|
+ ├─ 4. 发送 __call(发起呼叫,携带音视频方向、认证信息)
|
|
|
+ │
|
|
|
+ ├─ 5. 服务器返回 _call 或 _offer(SDP 协商开始)
|
|
|
+ │ ├─ 收到 _call → 创建 PeerConnection → createOffer → 发送 __offer
|
|
|
+ │ └─ 收到 _offer → 创建 PeerConnection → createAnswer → 发送 __answer
|
|
|
+ │
|
|
|
+ ├─ 6. 收到 _answer → setRemoteDescription(SDP 协商完成)
|
|
|
+ │
|
|
|
+ ├─ 7. ICE Candidate 交换(__ice_candidate / _ice_candidate)
|
|
|
+ │
|
|
|
+ ├─ 8. ICE 连接建立(checking → connected)
|
|
|
+ │
|
|
|
+ ├─ 9. 远端 Track 到达 → video.srcObject 赋值 → video.play()
|
|
|
+ │
|
|
|
+ └─ 10. 画面显示,进入稳定播放状态
|
|
|
+```
|
|
|
+
|
|
|
+正常情况下,从 WebSocket 打开到画面显示约 100-500ms。
|
|
|
+
|
|
|
+## 信令消息说明
|
|
|
+
|
|
|
+| 方向 | 消息 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| → 发送 | `__connectto` | 请求连接指定设备 |
|
|
|
+| ← 接收 | `_create` | 设备状态(online/offline)及 ICE 配置 |
|
|
|
+| → 发送 | `__call` | 发起呼叫,携带音视频方向和认证信息 |
|
|
|
+| ← 接收 | `_call` / `_offer` | 服务器要求创建 Offer 或直接下发 Offer |
|
|
|
+| → 发送 | `__offer` / `__answer` | SDP 协商 |
|
|
|
+| ← 接收 | `_answer` | 远端 SDP 应答 |
|
|
|
+| ↔ 双向 | `__ice_candidate` / `_ice_candidate` | ICE 候选地址交换 |
|
|
|
+| → 发送 | `__ping` | 心跳保活(每 30 秒) |
|
|
|
+| → 发送 | `__disconnected` | 主动断开通知 |
|
|
|
+| ← 接收 | `_session_failed` | 会话失败,需要重新连接 |
|
|
|
+| ← 接收 | `_session_disconnected` | 远端断开通知 |
|
|
|
+| ← 接收 | `_connectinfo` | 连接信息(调试用) |
|
|
|
+
|
|
|
+## 重要功能
|
|
|
+
|
|
|
+### 自动重连机制
|
|
|
+
|
|
|
+- **心跳保活**:每 2 秒检测 WebSocket 状态,断开后自动重建连接
|
|
|
+- **WebSocket 握手超时**:超过设定时间未连接成功则关闭并重试
|
|
|
+- **Session 失败重试**:收到 `_session_failed` 后立即用新 sessionId 重新发起连接
|
|
|
+- **ICE 断连恢复**:ICE 状态变为 disconnected/failed 时,如果 WebSocket 还活着则立即重新发起信令协商
|
|
|
+- **组件卸载通知**:离开页面或切换路由时主动发送 `__disconnected`,让服务器立即清理旧 session
|
|
|
+
|
|
|
+### 画面冻结检测(Watchdog)
|
|
|
+
|
|
|
+每 3 秒通过 `RTCPeerConnection.getStats()` 检查视频流的 `bytesReceived`:
|
|
|
+- 如果连续 3 次(约 9 秒)没有新数据到达,判定为画面冻结
|
|
|
+- 先尝试 ICE Restart(`createOffer({ iceRestart: true })`)恢复连接
|
|
|
+- ICE Restart 失败则完全重连
|
|
|
+- 同时检测 `<video>` 元素是否意外暂停,自动恢复播放
|
|
|
|
|
|
-1. Disable the built-in TypeScript Extension
|
|
|
- 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
|
|
- 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
|
|
-2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
|
+### 本地媒体按需获取
|
|
|
|
|
|
-## Customize configuration
|
|
|
+组件默认以 `recvonly` 模式连接(只接收远端音视频),不请求本地摄像头/麦克风权限,避免浏览器弹出权限弹窗影响连接速度。只有用户主动点击"开启麦克风"时才调用 `getUserMedia` 获取音频轨道,并动态添加到 PeerConnection。
|
|
|
|
|
|
-See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
|
+### 调试日志
|
|
|
|
|
|
-## Project Setup
|
|
|
+所有关键步骤带有时间戳日志,格式为 `[WebRTC][tag]+XXXms`,从 WebSocket 开始连接计时。示例:
|
|
|
|
|
|
-```sh
|
|
|
-yarn
|
|
|
```
|
|
|
+[WebRTC][ws]+0ms connecting... ws://192.168.1.20:8080/wswebclient/xxx
|
|
|
+[WebRTC][ws]+3ms opened
|
|
|
+[WebRTC][signal]+3ms sendConnect to ANSJ-00-...
|
|
|
+[WebRTC][msg]+5ms _create
|
|
|
+[WebRTC][signal]+5ms callDevice
|
|
|
+[WebRTC][msg]+20ms _offer
|
|
|
+[WebRTC][pc]+21ms PeerConnection created
|
|
|
+[WebRTC][track]+23ms remote track received, kind: video
|
|
|
+[WebRTC][ice]+70ms checking
|
|
|
+[WebRTC][ice]+120ms connected
|
|
|
+[WebRTC][play]+120ms attempting video.play()
|
|
|
+[WebRTC][play]+200ms playing!
|
|
|
+```
|
|
|
+
|
|
|
|
|
|
-### Compile and Hot-Reload for Development
|
|
|
+## 组件 API
|
|
|
+
|
|
|
+### Props
|
|
|
+
|
|
|
+| 属性 | 类型 | 必填 | 默认值 | 说明 |
|
|
|
+|------|------|------|--------|------|
|
|
|
+| `serno` | `string` | ✅ | — | 设备序列号 |
|
|
|
+| `wsUrl` | `string` | — | 自动推导 | 信令 WebSocket 地址,不传则根据环境变量拼接 |
|
|
|
+| `iceServers` | `RTCIceServer[]` | — | Google STUN | ICE 服务器配置,通常由信令服务器下发 |
|
|
|
+| `mode` | `string` | — | `'live'` | 连接模式:`live`(直播)/ `playback`(回放) |
|
|
|
+| `source` | `string` | — | `'MainStream'` | 码流:`MainStream`(主码流)/ `SubStream`(子码流) |
|
|
|
+| `enableDataChannel` | `boolean` | — | `true` | 是否启用 DataChannel |
|
|
|
+| `dragFlag` | `boolean` | — | `false` | 是否允许鼠标拖拽和滚轮缩放 |
|
|
|
+| `sessionType` | `string` | — | `'IE'` | 会话类型标识 |
|
|
|
+| `user` | `string` | — | `'admin'` | 设备认证用户名 |
|
|
|
+| `pwd` | `string` | — | `'admin'` | 设备认证密码 |
|
|
|
+
|
|
|
+### Events
|
|
|
+
|
|
|
+| 事件 | 参数 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `connected` | — | ICE 连接建立成功 |
|
|
|
+| `disconnected` | — | 连接断开 |
|
|
|
+| `error` | `Error \| string` | 连接或播放错误 |
|
|
|
+| `datachannel-message` | `any` | 收到 DataChannel 消息 |
|
|
|
+
|
|
|
+### Expose(通过 ref 调用)
|
|
|
+
|
|
|
+| 方法/属性 | 类型 | 说明 |
|
|
|
+|-----------|------|------|
|
|
|
+| `reconnect()` | `function` | 手动断开并重新连接 |
|
|
|
+| `captureSnapshot()` | `function` | 截取当前画面并下载为 PNG |
|
|
|
+| `sendDataChannelMessage(msg)` | `function` | 通过 DataChannel 发送消息 |
|
|
|
+| `toggleSpeaker()` | `function` | 切换扬声器开关 |
|
|
|
+| `toggleMic()` | `function` | 切换麦克风开关(首次会请求权限) |
|
|
|
+| `isPlaying` | `Ref<boolean>` | 是否正在播放(只读) |
|
|
|
+| `speakerEnabled` | `Ref<boolean>` | 扬声器是否开启(只读) |
|
|
|
+| `micEnabled` | `Ref<boolean>` | 麦克风是否开启(只读) |
|
|
|
+| `connectionStatus` | `Ref<string>` | 连接状态(只读):`idle` / `connecting` / `connected` / `disconnected` / `failed` |
|
|
|
+
|
|
|
+## 使用示例
|
|
|
+
|
|
|
+### 基础用法
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <WebRTCVideo serno="ANSJ-00-I7N3-QINU-00000027" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import WebRTCVideo from '@/components/WebRTCVideo.vue'
|
|
|
+</script>
|
|
|
+```
|
|
|
|
|
|
-```sh
|
|
|
-yarn dev
|
|
|
+### 完整配置
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <WebRTCVideo
|
|
|
+ ref="videoRef"
|
|
|
+ :serno="deviceSerno"
|
|
|
+ mode="live"
|
|
|
+ source="MainStream"
|
|
|
+ session-type="IE"
|
|
|
+ user="admin"
|
|
|
+ pwd="admin123"
|
|
|
+ :drag-flag="true"
|
|
|
+ :enable-data-channel="true"
|
|
|
+ @connected="onConnected"
|
|
|
+ @disconnected="onDisconnected"
|
|
|
+ @error="onError"
|
|
|
+ @datachannel-message="onMessage"
|
|
|
+ />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref } from 'vue'
|
|
|
+import WebRTCVideo from '@/components/WebRTCVideo.vue'
|
|
|
+
|
|
|
+const videoRef = ref()
|
|
|
+const deviceSerno = ref('ANSJ-00-I7N3-QINU-00000027')
|
|
|
+
|
|
|
+function onConnected() {
|
|
|
+ console.log('直播已连接')
|
|
|
+}
|
|
|
+
|
|
|
+function onDisconnected() {
|
|
|
+ console.log('直播已断开')
|
|
|
+}
|
|
|
+
|
|
|
+function onError(err) {
|
|
|
+ console.error('直播错误:', err)
|
|
|
+}
|
|
|
+
|
|
|
+function onMessage(msg) {
|
|
|
+ console.log('DataChannel 消息:', msg)
|
|
|
+}
|
|
|
+
|
|
|
+// 通过 ref 调用组件方法
|
|
|
+function handleScreenshot() {
|
|
|
+ videoRef.value?.captureSnapshot()
|
|
|
+}
|
|
|
+
|
|
|
+function handleReconnect() {
|
|
|
+ videoRef.value?.reconnect()
|
|
|
+}
|
|
|
+</script>
|
|
|
```
|
|
|
|
|
|
-### Type-Check, Compile and Minify for Production
|
|
|
+### 通过 URL 参数指定设备
|
|
|
+
|
|
|
+访问 `http://localhost:3333/preview?serno=YOUR_DEVICE_SERNO` 即可连接指定设备。
|
|
|
+
|
|
|
+## 环境变量
|
|
|
|
|
|
-```sh
|
|
|
-yarn build
|
|
|
+在 `.env.development` 和 `.env.production` 中配置:
|
|
|
+
|
|
|
+```env
|
|
|
+# WebRTC 信令服务器 IP(开发环境使用)
|
|
|
+VITE_WEBRTC_SIGNAL_IP=192.168.1.20
|
|
|
+
|
|
|
+# WebRTC 信令服务器端口
|
|
|
+VITE_WEBRTC_SIGNAL_PORT=8080
|
|
|
```
|
|
|
|
|
|
-### Lint with [ESLint](https://eslint.org/)
|
|
|
+- 开发环境:WebSocket 连接到 `VITE_WEBRTC_SIGNAL_IP:VITE_WEBRTC_SIGNAL_PORT`
|
|
|
+- 生产环境:自动使用当前页面 host + 配置的端口,协议跟随 http/https 自动切换 ws/wss
|
|
|
|
|
|
-```sh
|
|
|
-yarn lint
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+```
|
|
|
+src/
|
|
|
+├── components/
|
|
|
+│ └── WebRTCVideo.vue # WebRTC 直播组件
|
|
|
+├── views/
|
|
|
+│ └── preview/
|
|
|
+│ └── index.vue # 直播预览页面(使用 WebRTCVideo)
|
|
|
```
|