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 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 };