wrplayer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +413 -0
- package/adapters/react/index.js +272 -0
- package/adapters/vanilla/index.js +392 -0
- package/adapters/vue/index.js +378 -0
- package/core/PTZController.js +462 -0
- package/core/VoiceIntercomController.js +492 -0
- package/core/WRPlayer.js +924 -0
- package/core/ZLMRTCClient-v1.0.1.js +8227 -0
- package/core/ZLMRTCClient-v1.1.2.js +9474 -0
- package/dist/wrplayer.esm.js +11343 -0
- package/dist/wrplayer.esm.js.map +1 -0
- package/dist/wrplayer.umd.js +11351 -0
- package/dist/wrplayer.umd.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# WRPlayer
|
|
2
|
+
|
|
3
|
+
WRPlayer 是一个统一的 Web 视频播放器 SDK,提供一致的 API 来播放多种流媒体协议(WebRTC、HLS、DASH、MP4 等),并可在原生 HTML、Next.js/React 与 Vue 3 环境中复用同一套核心代码。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **多协议支持**: WebRTC、HLS、DASH、MP4 等
|
|
8
|
+
- **自动类型检测**: 可从 URL 自动识别播放类型
|
|
9
|
+
- **统一 API**: `play/pause/stop/on/off` 等一致方法与事件
|
|
10
|
+
- **UMD + ESM 构建**: 一次构建,既可 `<script>` 引入(UMD),也可模块化 `import`(ESM)
|
|
11
|
+
- **SSR 友好**: 核心兼容 SSR,Next.js 适配器按需在客户端初始化
|
|
12
|
+
- **模块化架构**: `core`(核心)与 `adapters`(框架适配)清晰分离
|
|
13
|
+
- **WebRTC 增强**: 支持禁用音频(规避特定浏览器/流的音频解复用问题),并内置防重复协商的状态管理
|
|
14
|
+
|
|
15
|
+
## 项目结构
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
wrplayer/
|
|
19
|
+
├── core/ # 核心播放器逻辑
|
|
20
|
+
│ ├── WRPlayer.js # 播放器主类(直接内置 ZLMRTCClient)
|
|
21
|
+
│ └── ZLMRTCClient.js # WebRTC 客户端库(已作为模块被 WRPlayer 引入)
|
|
22
|
+
├── adapters/
|
|
23
|
+
│ ├── next/ # Next.js / React 适配器组件
|
|
24
|
+
│ ├── react/ # React Hook 适配器
|
|
25
|
+
│ ├── vue/ # Vue 3 组合式 API 适配器
|
|
26
|
+
│ └── vanilla/ # 原生 JS 包装工具
|
|
27
|
+
├── demo/
|
|
28
|
+
│ ├── html/ # 原生 HTML + UMD 示例(可直接双击打开)
|
|
29
|
+
│ ├── next/ # Next.js 示例项目
|
|
30
|
+
│ ├── vue/ # Vue 3 + Vite 示例项目
|
|
31
|
+
│ └── shared/ # 演示页共享样式
|
|
32
|
+
├── dist/ # 构建产物(UMD / ESM)
|
|
33
|
+
├── rollup.config.js # Rollup 构建配置
|
|
34
|
+
├── package.json # 根项目脚本与依赖
|
|
35
|
+
└── README.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 构建
|
|
39
|
+
|
|
40
|
+
在项目根目录执行:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
生成产物:
|
|
48
|
+
- `dist/wrplayer.umd.js`(浏览器 `<script>` 直接引入)
|
|
49
|
+
- `dist/wrplayer.esm.js`(ES 模块)
|
|
50
|
+
|
|
51
|
+
> 提示:若要消除 Node 关于 ESM 的提示,可在根 `package.json` 增加 `"type": "module"`。
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
WRPlayer 目前通过 Git 仓库或本地路径分发(尚未发布到 npm)。接入方需先获取源码并构建:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 方式一:克隆仓库
|
|
59
|
+
git clone https://gitee.com/warom-org/wrplayer.git
|
|
60
|
+
cd wrplayer
|
|
61
|
+
npm install && npm run build
|
|
62
|
+
|
|
63
|
+
# 方式二:在业务项目的 package.json 中引用
|
|
64
|
+
# "wrplayer": "git+https://gitee.com/warom-org/wrplayer.git"
|
|
65
|
+
# 或 "wrplayer": "file:../wrplayer"
|
|
66
|
+
npm install
|
|
67
|
+
cd node_modules/wrplayer && npm install && npm run build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
构建完成后,`dist/wrplayer.esm.js` 与 `dist/wrplayer.umd.js` 即可被引用。
|
|
71
|
+
|
|
72
|
+
## 使用方法
|
|
73
|
+
|
|
74
|
+
### A. 原生 HTML(通过 UMD 包)
|
|
75
|
+
|
|
76
|
+
无需打包工具,只需在页面引入 UMD 文件:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<!-- 1) 引入 UMD 包 -->
|
|
80
|
+
<script src="./dist/wrplayer.umd.js"></script>
|
|
81
|
+
|
|
82
|
+
<!-- 2) 视频与脚本 -->
|
|
83
|
+
<video id="player" controls autoplay muted style="width: 800px; height: 450px;"></video>
|
|
84
|
+
<script>
|
|
85
|
+
const player = new WRPlayer({
|
|
86
|
+
element: document.getElementById('player'),
|
|
87
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
88
|
+
// 可选:明确类型,也可不写走自动识别
|
|
89
|
+
type: 'webrtc',
|
|
90
|
+
autoplay: true,
|
|
91
|
+
debug: true
|
|
92
|
+
// 注意:ZLMRTCClient 已内置到 UMD 包,无需手动传入
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
player.on('ready', () => console.log('ready'));
|
|
96
|
+
player.on('play', () => console.log('play'));
|
|
97
|
+
player.on('connectionstatechange', ({ state }) => console.log('conn:', state));
|
|
98
|
+
player.on('error', (err) => console.error('error', err));
|
|
99
|
+
</script>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**音频控制策略**:为避免常见的 SDP 协商问题,默认禁用音频。若需要音频,可手动启用:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<script>
|
|
106
|
+
// 方案一:默认配置(推荐)- 仅视频,避免 SDP 协商问题
|
|
107
|
+
const player = new WRPlayer({
|
|
108
|
+
element: document.getElementById('player'),
|
|
109
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
110
|
+
autoplay: true,
|
|
111
|
+
debug: true
|
|
112
|
+
// recvAudio 默认为 false,避免 "Failed to set up audio demuxing" 错误
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 方案二:明确禁用音频(与默认行为相同)
|
|
116
|
+
const playerVideoOnly = new WRPlayer({
|
|
117
|
+
element: document.getElementById('player'),
|
|
118
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
119
|
+
autoplay: true,
|
|
120
|
+
debug: true,
|
|
121
|
+
recvAudio: false, // 明确禁用音频
|
|
122
|
+
recvVideo: true // 明确启用视频(默认值)
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 方案三:启用音频(实验性,有自动回退策略)
|
|
126
|
+
const playerWithAudio = new WRPlayer({
|
|
127
|
+
element: document.getElementById('player'),
|
|
128
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
129
|
+
autoplay: true,
|
|
130
|
+
debug: true,
|
|
131
|
+
recvAudio: true // 启用音频,如失败会自动回退到仅视频
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**常见的 WebRTC 音频问题**:
|
|
137
|
+
|
|
138
|
+
如果您遇到类似的错误:
|
|
139
|
+
- `Failed to set up audio demuxing for mid='1'`
|
|
140
|
+
- `Session error description: Failed to set up audio demuxing`
|
|
141
|
+
|
|
142
|
+
这通常是由于客户端与流媒体服务器的音频编解码器不匹配导致的。解决方案:
|
|
143
|
+
|
|
144
|
+
1. **推荐**:使用 `recvAudio: false` 禁用音频接收
|
|
145
|
+
2. **强制方案**:如果仍有问题,使用 `webrtcConfig` 强制禁用:
|
|
146
|
+
```javascript
|
|
147
|
+
const player = new WRPlayer({
|
|
148
|
+
element: document.getElementById('player'),
|
|
149
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
150
|
+
recvAudio: false,
|
|
151
|
+
webrtcConfig: {
|
|
152
|
+
audioEnable: false, // 强制禁用音频 transceiver
|
|
153
|
+
videoEnable: true, // 确保视频启用
|
|
154
|
+
recvOnly: true // 确保仅接收模式
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
3. 检查流媒体服务器是否正确配置音频编码
|
|
159
|
+
4. 如果必须使用音频,播放器会自动进行回退重试
|
|
160
|
+
|
|
161
|
+
### B. Next.js / React
|
|
162
|
+
|
|
163
|
+
示例项目位于 `demo/next/`:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd demo/next
|
|
167
|
+
npm install
|
|
168
|
+
npm run dev # 开发模式
|
|
169
|
+
# 或
|
|
170
|
+
npm run build && npm run start # 生产模式
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
在页面中使用适配器组件:
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
'use client';
|
|
177
|
+
import WRPlayerComponent from '../../../adapters/next';
|
|
178
|
+
|
|
179
|
+
export default function Page() {
|
|
180
|
+
return (
|
|
181
|
+
<WRPlayerComponent
|
|
182
|
+
url="http://your-host/index/api/webrtc?..."
|
|
183
|
+
type="webrtc"
|
|
184
|
+
autoplay={true}
|
|
185
|
+
controls={true}
|
|
186
|
+
muted={true}
|
|
187
|
+
recvAudio={false} // 推荐禁用音频,避免 SDP 协商问题
|
|
188
|
+
recvVideo={true}
|
|
189
|
+
// 如果仍有音频协商问题,使用强制配置
|
|
190
|
+
webrtcConfig={{
|
|
191
|
+
audioEnable: false, // 强制禁用音频
|
|
192
|
+
videoEnable: true, // 确保视频启用
|
|
193
|
+
recvOnly: true // 确保仅接收模式
|
|
194
|
+
}}
|
|
195
|
+
onReady={() => console.log('播放器准备就绪')}
|
|
196
|
+
onError={(err) => console.error('播放错误:', err)}
|
|
197
|
+
debug={true}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
> 说明:适配器已封装客户端初始化流程,SSR 环境下不会触发浏览器 API。
|
|
204
|
+
|
|
205
|
+
### C. Vue 3
|
|
206
|
+
|
|
207
|
+
#### 1. 在现有 Vue 3 项目中安装
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# 假设已通过 Git 或 file: 方式安装 wrplayer(见上方「安装」章节)
|
|
211
|
+
npm install wrplayer
|
|
212
|
+
# 或 pnpm add wrplayer
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
在 `package.json` 中确保 `vue` 版本 ≥ 3.0(wrplayer 将其声明为 optional peerDependency,按需安装即可)。
|
|
216
|
+
|
|
217
|
+
#### 2. 运行官方示例
|
|
218
|
+
|
|
219
|
+
示例项目位于 `demo/vue/`:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
cd demo/vue
|
|
223
|
+
cp .env.example .env.local # 配置 WVP_BASE_URL 与 SIP_DEFAULT_TOKEN
|
|
224
|
+
pnpm install # 或 npm install
|
|
225
|
+
pnpm dev # 开发模式,访问 http://localhost:5173
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### 3. 使用 `useWRPlayer`(推荐)
|
|
229
|
+
|
|
230
|
+
`useWRPlayer` 是 Vue 3 组合式 API,封装了播放器生命周期、PTZ 与语音对讲能力:
|
|
231
|
+
|
|
232
|
+
```vue
|
|
233
|
+
<script setup>
|
|
234
|
+
import { reactive } from 'vue';
|
|
235
|
+
import { useWRPlayer } from 'wrplayer/adapters/vue';
|
|
236
|
+
|
|
237
|
+
const playerConfig = reactive({
|
|
238
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
239
|
+
type: 'webrtc', // 可选,省略时自动检测
|
|
240
|
+
autoplay: true,
|
|
241
|
+
debug: false,
|
|
242
|
+
recvAudio: false, // 推荐 false,避免 WebRTC 音频协商问题
|
|
243
|
+
// 可选:PTZ 云台(useProxy: true 时 apiUrl 走开发代理)
|
|
244
|
+
ptz: {
|
|
245
|
+
apiUrl: '/api/ptz',
|
|
246
|
+
apiKey: 'token',
|
|
247
|
+
deviceId: '34020000001320000009',
|
|
248
|
+
channelId: '34020000001320000009',
|
|
249
|
+
useProxy: true
|
|
250
|
+
},
|
|
251
|
+
// 可选:语音对讲
|
|
252
|
+
voiceIntercom: {
|
|
253
|
+
apiUrl: '/api/voice-intercom',
|
|
254
|
+
apiKey: 'token',
|
|
255
|
+
useProxy: true
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const {
|
|
260
|
+
elementRef, // 绑定到 <video ref="elementRef">
|
|
261
|
+
player, // 底层 WRPlayer 实例(computed)
|
|
262
|
+
isReady, // 是否就绪
|
|
263
|
+
error, // 错误信息
|
|
264
|
+
ptzAvailable, // PTZ 是否可用
|
|
265
|
+
voiceIntercomAvailable, // 语音对讲是否可用
|
|
266
|
+
voiceIntercomStatus, // 对讲状态
|
|
267
|
+
play, pause, stop, // 播放控制
|
|
268
|
+
ptz, // PTZ API(computed,不可用时为 null)
|
|
269
|
+
voiceIntercom // 语音对讲 API(computed,不可用时为 null)
|
|
270
|
+
} = useWRPlayer(playerConfig);
|
|
271
|
+
|
|
272
|
+
// PTZ 示例
|
|
273
|
+
async function moveUp() {
|
|
274
|
+
if (ptz.value) await ptz.value.move('up', 50);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 语音对讲示例
|
|
278
|
+
async function startTalk() {
|
|
279
|
+
if (voiceIntercom.value) {
|
|
280
|
+
await voiceIntercom.value.start('34020000001320000009', '34020000001320000009', false);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<template>
|
|
286
|
+
<video ref="elementRef" controls autoplay muted />
|
|
287
|
+
<button @click="moveUp" :disabled="!ptzAvailable">云台上</button>
|
|
288
|
+
<button @click="startTalk" :disabled="!voiceIntercomAvailable">开始对讲</button>
|
|
289
|
+
</template>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
切换播放地址时,修改 `playerConfig.url` 即可,Hook 会自动销毁并重建播放器:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
playerConfig.url = ''; // 先清空可停止当前流
|
|
296
|
+
playerConfig.url = 'http://new-stream-url'; // 加载新地址
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### 4. 使用 `WRPlayerComponent`
|
|
300
|
+
|
|
301
|
+
适合快速嵌入、通过事件监听 PTZ/对讲回调的场景:
|
|
302
|
+
|
|
303
|
+
```vue
|
|
304
|
+
<script setup>
|
|
305
|
+
import { WRPlayerComponent } from 'wrplayer/adapters/vue';
|
|
306
|
+
|
|
307
|
+
const config = {
|
|
308
|
+
url: 'http://your-host/index/api/webrtc?...',
|
|
309
|
+
type: 'webrtc',
|
|
310
|
+
autoplay: true,
|
|
311
|
+
recvAudio: false
|
|
312
|
+
};
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<template>
|
|
316
|
+
<WRPlayerComponent
|
|
317
|
+
:config="config"
|
|
318
|
+
class-name="my-player"
|
|
319
|
+
@ready="(player) => console.log('ready', player)"
|
|
320
|
+
@error="(err) => console.error('error', err)"
|
|
321
|
+
@ptz-event="({ event, data }) => console.log(event, data)"
|
|
322
|
+
@voice-intercom-event="({ event, data }) => console.log(event, data)"
|
|
323
|
+
/>
|
|
324
|
+
</template>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### 5. Vite 开发代理(PTZ / 语音对讲)
|
|
328
|
+
|
|
329
|
+
浏览器直接请求 SIP 服务器会遇到 CORS,开发环境可在 `vite.config.js` 中配置代理(完整示例见 `demo/vue/vite.config.js`):
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
export default defineConfig({
|
|
333
|
+
server: {
|
|
334
|
+
proxy: {
|
|
335
|
+
'/api/ptz': {
|
|
336
|
+
target: 'http://your-sip-server:18082',
|
|
337
|
+
changeOrigin: true,
|
|
338
|
+
rewrite: (path) => path.replace(/^\/api\/ptz/, '/api/front-end')
|
|
339
|
+
},
|
|
340
|
+
'/api/voice-intercom': {
|
|
341
|
+
target: 'http://your-sip-server:18082',
|
|
342
|
+
changeOrigin: true,
|
|
343
|
+
rewrite: (path) => path.replace(/^\/api\/voice-intercom\/?/, '/')
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
生产环境请用 Nginx 或后端网关实现相同转发,并在请求头注入 `api-key`。
|
|
351
|
+
|
|
352
|
+
#### 6. Vue 3 集成注意事项
|
|
353
|
+
|
|
354
|
+
- `useWRPlayer` 在 `onMounted` 后初始化,`onUnmounted` 时自动 `stop()` 释放资源
|
|
355
|
+
- `config.url` 或 `config.type` 变化时会自动重建实例
|
|
356
|
+
- WebRTC 自动播放需 `<video muted autoplay>`,或用户交互后再播放
|
|
357
|
+
- PTZ / 语音对讲配置建议在首次设置 `url` 前写入 `playerConfig`
|
|
358
|
+
- 也可使用 `WRPlayerVue2` 兼容 Vue 2 Options API(见 `wrplayer/adapters/vue` 导出)
|
|
359
|
+
|
|
360
|
+
## API(核心类 `WRPlayer`)
|
|
361
|
+
|
|
362
|
+
### 构造函数
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
new WRPlayer(options)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
369
|
+
| --- | --- | --- | --- |
|
|
370
|
+
| options.element | HTMLVideoElement | 是 | 用于播放的 `<video>` 元素 |
|
|
371
|
+
| options.url | string | 是 | 流/视频 URL |
|
|
372
|
+
| options.type | string | 否 | `webrtc`/`hls`/`dash`/`mp4`,缺省自动检测 |
|
|
373
|
+
| options.autoplay | boolean | 否 | 默认 `true` |
|
|
374
|
+
| options.debug | boolean | 否 | 默认 `false` |
|
|
375
|
+
| options.dependencies.hlsPath | string | 否 | hls.js CDN 或本地路径(按需) |
|
|
376
|
+
| options.dependencies.dashPath | string | 否 | dash.js CDN 或本地路径(按需) |
|
|
377
|
+
| options.recvAudio | boolean | 否 | WebRTC 是否接收音频(默认 `false`,可设为 `true` 启用,支持回退策略) |
|
|
378
|
+
| options.recvVideo | boolean | 否 | WebRTC 是否接收视频(默认 `true`) |
|
|
379
|
+
| options.webrtcConfig | Object | 否 | 透传给 `ZLMRTCClient.Endpoint` 的高级配置 |
|
|
380
|
+
|
|
381
|
+
> 注意:`ZLMRTCClient` 已在内部以 ES 模块方式引入并随构建打包,无需在外部传入。
|
|
382
|
+
|
|
383
|
+
### 方法
|
|
384
|
+
|
|
385
|
+
| 方法 | 说明 |
|
|
386
|
+
| --- | --- |
|
|
387
|
+
| `play()` | 开始/继续播放(WebRTC 下防重复协商) |
|
|
388
|
+
| `pause()` | 暂停(对 WebRTC 无效) |
|
|
389
|
+
| `stop()` | 停止并清理资源(WebRTC 下会重置内部状态) |
|
|
390
|
+
| `on(event, cb)` | 绑定事件 |
|
|
391
|
+
| `off(event, cb)` | 解绑事件 |
|
|
392
|
+
| `getPlayer()` | 获取底层实例(如 Hls.js) |
|
|
393
|
+
|
|
394
|
+
### 事件
|
|
395
|
+
|
|
396
|
+
| 事件 | 数据 | 说明 |
|
|
397
|
+
| --- | --- | --- |
|
|
398
|
+
| `ready` | `{ streams? }` | 播放器就绪 |
|
|
399
|
+
| `play` | `void` | 开始播放 |
|
|
400
|
+
| `pause` | `void` | 暂停 |
|
|
401
|
+
| `ended` | `void` | 播放结束 |
|
|
402
|
+
| `error` | `{ type, details }` | 错误事件 |
|
|
403
|
+
| `connectionstatechange` | `{ state }` | WebRTC 连接状态:`connecting`/`connected`/`failed`/`disconnected` |
|
|
404
|
+
|
|
405
|
+
## 常见问题
|
|
406
|
+
|
|
407
|
+
- **浏览器自动播放策略**:若需自动播放,建议设置 `muted=true`,或在用户交互后再调用 `play()`。
|
|
408
|
+
- **音频解复用报错(WebRTC)**:默认已禁用音频以避免此问题。如需音频可设 `recvAudio: true`,遇错误时播放器会自动回退到仅视频模式。
|
|
409
|
+
- **CDN 动态依赖**:仅 HLS/DASH 按需加载 `hls.js`/`dash.js`,可通过 `dependencies.hlsPath/dashPath` 指定路径。
|
|
410
|
+
|
|
411
|
+
## 许可证
|
|
412
|
+
|
|
413
|
+
MIT
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import WRPlayer from '../../core/WRPlayer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Hook for WRPlayer with PTZ and Voice Intercom support
|
|
6
|
+
* @param {Object} config - Player configuration
|
|
7
|
+
* @param {string} config.url - Video URL
|
|
8
|
+
* @param {string} [config.type] - Player type
|
|
9
|
+
* @param {boolean} [config.autoplay=true] - Auto play
|
|
10
|
+
* @param {boolean} [config.debug=false] - Debug mode
|
|
11
|
+
* @param {Object} [config.ptz] - PTZ configuration
|
|
12
|
+
* @param {Object} [config.voiceIntercom] - Voice Intercom configuration
|
|
13
|
+
* @returns {Object} Player instance and utilities
|
|
14
|
+
*/
|
|
15
|
+
export function useWRPlayer(config) {
|
|
16
|
+
const elementRef = useRef(null);
|
|
17
|
+
const playerRef = useRef(null);
|
|
18
|
+
const [isReady, setIsReady] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const [ptzAvailable, setPtzAvailable] = useState(false);
|
|
21
|
+
const [voiceIntercomAvailable, setVoiceIntercomAvailable] = useState(false);
|
|
22
|
+
const [voiceIntercomStatus, setVoiceIntercomStatus] = useState(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!elementRef.current || !config.url) return;
|
|
26
|
+
|
|
27
|
+
const player = new WRPlayer({
|
|
28
|
+
element: elementRef.current,
|
|
29
|
+
...config
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
playerRef.current = player;
|
|
33
|
+
setPtzAvailable(!!player.ptz);
|
|
34
|
+
setVoiceIntercomAvailable(!!player.voiceIntercom);
|
|
35
|
+
|
|
36
|
+
// Event listeners
|
|
37
|
+
player.on('ready', () => {
|
|
38
|
+
setIsReady(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
player.on('error', (errorData) => {
|
|
43
|
+
setError(errorData);
|
|
44
|
+
setIsReady(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Voice Intercom event listeners
|
|
48
|
+
if (player.voiceIntercom) {
|
|
49
|
+
player.on('voiceIntercom:statusChange', (data) => {
|
|
50
|
+
setVoiceIntercomStatus(data);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
if (playerRef.current) {
|
|
56
|
+
playerRef.current.stop();
|
|
57
|
+
playerRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
setIsReady(false);
|
|
60
|
+
setError(null);
|
|
61
|
+
setPtzAvailable(false);
|
|
62
|
+
setVoiceIntercomAvailable(false);
|
|
63
|
+
setVoiceIntercomStatus(null);
|
|
64
|
+
};
|
|
65
|
+
}, [config.url, config.type]);
|
|
66
|
+
|
|
67
|
+
const play = useCallback(() => {
|
|
68
|
+
if (playerRef.current) {
|
|
69
|
+
playerRef.current.play();
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const pause = useCallback(() => {
|
|
74
|
+
if (playerRef.current) {
|
|
75
|
+
playerRef.current.pause();
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const stop = useCallback(() => {
|
|
80
|
+
if (playerRef.current) {
|
|
81
|
+
playerRef.current.stop();
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// PTZ control methods
|
|
86
|
+
const ptzMove = useCallback((direction, speed) => {
|
|
87
|
+
if (playerRef.current?.ptz) {
|
|
88
|
+
return playerRef.current.ptz.move(direction, speed);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const ptzZoom = useCallback((direction, speed) => {
|
|
93
|
+
if (playerRef.current?.ptz) {
|
|
94
|
+
return playerRef.current.ptz.zoom(direction, speed);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const ptzFocus = useCallback((direction, speed) => {
|
|
99
|
+
if (playerRef.current?.ptz) {
|
|
100
|
+
return playerRef.current.ptz.focus(direction, speed);
|
|
101
|
+
}
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const ptzStop = useCallback(() => {
|
|
105
|
+
if (playerRef.current?.ptz) {
|
|
106
|
+
return playerRef.current.ptz.stop();
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const ptzSetSpeed = useCallback((speed) => {
|
|
111
|
+
if (playerRef.current?.ptz) {
|
|
112
|
+
return playerRef.current.ptz.setSpeed(speed);
|
|
113
|
+
}
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
// Voice Intercom control methods
|
|
117
|
+
const voiceIntercomStart = useCallback((deviceId, channelId, mode) => {
|
|
118
|
+
if (playerRef.current?.voiceIntercom) {
|
|
119
|
+
return playerRef.current.voiceIntercom.startIntercom(deviceId, channelId, mode);
|
|
120
|
+
}
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const voiceIntercomStop = useCallback(() => {
|
|
124
|
+
if (playerRef.current?.voiceIntercom) {
|
|
125
|
+
return playerRef.current.voiceIntercom.stopIntercom();
|
|
126
|
+
}
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const voiceIntercomSetMode = useCallback((mode) => {
|
|
130
|
+
if (playerRef.current?.voiceIntercom) {
|
|
131
|
+
return playerRef.current.voiceIntercom.setMode(mode);
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const voiceIntercomGetStatus = useCallback(() => {
|
|
136
|
+
if (playerRef.current?.voiceIntercom) {
|
|
137
|
+
return playerRef.current.voiceIntercom.getStatus();
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
elementRef,
|
|
144
|
+
player: playerRef.current,
|
|
145
|
+
isReady,
|
|
146
|
+
error,
|
|
147
|
+
ptzAvailable,
|
|
148
|
+
voiceIntercomAvailable,
|
|
149
|
+
voiceIntercomStatus,
|
|
150
|
+
// Player controls
|
|
151
|
+
play,
|
|
152
|
+
pause,
|
|
153
|
+
stop,
|
|
154
|
+
// PTZ controls
|
|
155
|
+
ptz: ptzAvailable ? {
|
|
156
|
+
move: ptzMove,
|
|
157
|
+
zoom: ptzZoom,
|
|
158
|
+
focus: ptzFocus,
|
|
159
|
+
stop: ptzStop,
|
|
160
|
+
setSpeed: ptzSetSpeed,
|
|
161
|
+
preset: playerRef.current?.ptz?.preset,
|
|
162
|
+
cruise: playerRef.current?.ptz?.cruise,
|
|
163
|
+
scan: playerRef.current?.ptz?.scan
|
|
164
|
+
} : null,
|
|
165
|
+
// Voice Intercom controls
|
|
166
|
+
voiceIntercom: voiceIntercomAvailable ? {
|
|
167
|
+
start: voiceIntercomStart,
|
|
168
|
+
stop: voiceIntercomStop,
|
|
169
|
+
setMode: voiceIntercomSetMode,
|
|
170
|
+
getStatus: voiceIntercomGetStatus,
|
|
171
|
+
status: voiceIntercomStatus
|
|
172
|
+
} : null
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* React Component for WRPlayer with PTZ and Voice Intercom support
|
|
178
|
+
*/
|
|
179
|
+
export function WRPlayerComponent({
|
|
180
|
+
config,
|
|
181
|
+
onReady,
|
|
182
|
+
onError,
|
|
183
|
+
onPtzEvent,
|
|
184
|
+
onVoiceIntercomEvent,
|
|
185
|
+
className = '',
|
|
186
|
+
style = {},
|
|
187
|
+
...videoProps
|
|
188
|
+
}) {
|
|
189
|
+
const {
|
|
190
|
+
elementRef,
|
|
191
|
+
player,
|
|
192
|
+
isReady,
|
|
193
|
+
error,
|
|
194
|
+
ptzAvailable,
|
|
195
|
+
voiceIntercomAvailable,
|
|
196
|
+
ptz,
|
|
197
|
+
voiceIntercom
|
|
198
|
+
} = useWRPlayer(config);
|
|
199
|
+
|
|
200
|
+
// Handle callbacks
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (isReady && onReady) {
|
|
203
|
+
onReady(player);
|
|
204
|
+
}
|
|
205
|
+
}, [isReady, player, onReady]);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (error && onError) {
|
|
209
|
+
onError(error);
|
|
210
|
+
}
|
|
211
|
+
}, [error, onError]);
|
|
212
|
+
|
|
213
|
+
// PTZ event forwarding
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!player || !onPtzEvent) return;
|
|
216
|
+
|
|
217
|
+
const ptzEvents = [
|
|
218
|
+
'ptz:move', 'ptz:zoom', 'ptz:focus', 'ptz:iris', 'ptz:speed:change',
|
|
219
|
+
'ptz:preset:list', 'ptz:preset:saved', 'ptz:preset:called', 'ptz:preset:deleted',
|
|
220
|
+
'ptz:cruise:started', 'ptz:cruise:stopped', 'ptz:cruise:speed:set',
|
|
221
|
+
'ptz:scan:started', 'ptz:scan:stopped', 'ptz:scan:boundary:left', 'ptz:scan:boundary:right',
|
|
222
|
+
'ptz:request:success', 'ptz:request:error'
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const handlers = [];
|
|
226
|
+
ptzEvents.forEach(event => {
|
|
227
|
+
const handler = (data) => onPtzEvent(event, data);
|
|
228
|
+
player.on(event, handler);
|
|
229
|
+
handlers.push({ event, handler });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return () => {
|
|
233
|
+
handlers.forEach(({ event, handler }) => {
|
|
234
|
+
player.off(event, handler);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
}, [player, onPtzEvent]);
|
|
238
|
+
|
|
239
|
+
// Voice Intercom event forwarding
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!player || !onVoiceIntercomEvent) return;
|
|
242
|
+
|
|
243
|
+
const voiceIntercomEvents = [
|
|
244
|
+
'voiceIntercom:statusChange', 'voiceIntercom:modeChange', 'voiceIntercom:error',
|
|
245
|
+
'voiceIntercom:localStream', 'voiceIntercom:remoteStream'
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const handlers = [];
|
|
249
|
+
voiceIntercomEvents.forEach(event => {
|
|
250
|
+
const handler = (data) => onVoiceIntercomEvent(event, data);
|
|
251
|
+
player.on(event, handler);
|
|
252
|
+
handlers.push({ event, handler });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return () => {
|
|
256
|
+
handlers.forEach(({ event, handler }) => {
|
|
257
|
+
player.off(event, handler);
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
}, [player, onVoiceIntercomEvent]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<video
|
|
264
|
+
ref={elementRef}
|
|
265
|
+
className={className}
|
|
266
|
+
style={style}
|
|
267
|
+
{...videoProps}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default { useWRPlayer, WRPlayerComponent };
|