wrplayer 1.0.0 → 1.1.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 +3 -0
- package/adapters/vue/WRMonitorView.js +303 -0
- package/adapters/vue/index.js +7 -146
- package/adapters/vue/useWRPlayer.js +148 -0
- package/core/streamUrl.js +70 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
WRPlayer 是一个统一的 Web 视频播放器 SDK,提供一致的 API 来播放多种流媒体协议(WebRTC、HLS、DASH、MP4 等),并可在原生 HTML、Next.js/React 与 Vue 3 环境中复用同一套核心代码。
|
|
4
4
|
|
|
5
|
+
> **团队集成文档**(npm 安装、Vue/React 示例、PTZ/对讲、生产部署):见 [docs/INTEGRATION.md](./docs/INTEGRATION.md)
|
|
6
|
+
> **npm 包**:https://www.npmjs.com/package/wrplayer
|
|
7
|
+
|
|
5
8
|
## 特性
|
|
6
9
|
|
|
7
10
|
- **多协议支持**: WebRTC、HLS、DASH、MP4 等
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { reactive, ref, watch, computed, nextTick } from 'vue';
|
|
2
|
+
import WRPlayer from '../../core/WRPlayer.js';
|
|
3
|
+
import { useWRPlayer } from './useWRPlayer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 开箱即用的监控播放组件:内置加载态、错误提示、重连与基础工具栏
|
|
7
|
+
*/
|
|
8
|
+
export const WRMonitorView = {
|
|
9
|
+
name: 'WRMonitorView',
|
|
10
|
+
props: {
|
|
11
|
+
/** 播放地址,传入后自动开始连接 */
|
|
12
|
+
url: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: ''
|
|
15
|
+
},
|
|
16
|
+
/** 流类型,默认 webrtc */
|
|
17
|
+
type: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: 'webrtc'
|
|
20
|
+
},
|
|
21
|
+
/** 顶部标题 */
|
|
22
|
+
title: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: '实时监控'
|
|
25
|
+
},
|
|
26
|
+
/** 是否显示底部工具栏(停止 / 重连 / 静音) */
|
|
27
|
+
showToolbar: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: true
|
|
30
|
+
},
|
|
31
|
+
/** 透传给 WRPlayer 的额外配置(ptz、voiceIntercom 等) */
|
|
32
|
+
playerOptions: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: () => ({})
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
emits: ['ready', 'error', 'stop', 'retry'],
|
|
38
|
+
setup(props, { emit, expose }) {
|
|
39
|
+
const isMuted = ref(true);
|
|
40
|
+
const playerConfig = reactive({
|
|
41
|
+
url: '',
|
|
42
|
+
type: props.type,
|
|
43
|
+
autoplay: true,
|
|
44
|
+
recvAudio: false,
|
|
45
|
+
recvVideo: true,
|
|
46
|
+
debug: false
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const applyPlayerOptions = () => {
|
|
50
|
+
const { url, type, autoplay, recvAudio, recvVideo, debug, ...rest } = props.playerOptions || {};
|
|
51
|
+
Object.assign(playerConfig, {
|
|
52
|
+
type: type ?? props.type,
|
|
53
|
+
autoplay: autoplay ?? true,
|
|
54
|
+
recvAudio: recvAudio ?? false,
|
|
55
|
+
recvVideo: recvVideo ?? true,
|
|
56
|
+
debug: debug ?? false,
|
|
57
|
+
...rest
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const syncUrl = (value) => {
|
|
62
|
+
playerConfig.url = value || '';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
applyPlayerOptions();
|
|
66
|
+
syncUrl(props.url);
|
|
67
|
+
|
|
68
|
+
watch(
|
|
69
|
+
() => props.url,
|
|
70
|
+
(value) => syncUrl(value)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
watch(
|
|
74
|
+
() => props.type,
|
|
75
|
+
(value) => {
|
|
76
|
+
playerConfig.type = value;
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
watch(
|
|
81
|
+
() => props.playerOptions,
|
|
82
|
+
() => applyPlayerOptions(),
|
|
83
|
+
{ deep: true }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
elementRef,
|
|
88
|
+
player,
|
|
89
|
+
isReady,
|
|
90
|
+
error,
|
|
91
|
+
stop: stopPlayer,
|
|
92
|
+
ptzAvailable,
|
|
93
|
+
voiceIntercomAvailable,
|
|
94
|
+
ptz,
|
|
95
|
+
voiceIntercom
|
|
96
|
+
} = useWRPlayer(playerConfig);
|
|
97
|
+
|
|
98
|
+
const status = computed(() => {
|
|
99
|
+
if (error.value) return 'error';
|
|
100
|
+
if (isReady.value) return 'playing';
|
|
101
|
+
if (playerConfig.url) return 'loading';
|
|
102
|
+
return 'idle';
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const statusText = computed(() => {
|
|
106
|
+
switch (status.value) {
|
|
107
|
+
case 'loading': return '正在连接视频流…';
|
|
108
|
+
case 'playing': return '视频流接收正常';
|
|
109
|
+
case 'error': return '视频流连接失败';
|
|
110
|
+
default: return '等待播放地址';
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
watch(isReady, (ready) => {
|
|
115
|
+
if (ready) emit('ready', player.value);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
watch(error, (err) => {
|
|
119
|
+
if (err) emit('error', err);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const handleStop = () => {
|
|
123
|
+
syncUrl('');
|
|
124
|
+
stopPlayer();
|
|
125
|
+
emit('stop');
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleRetry = async () => {
|
|
129
|
+
const current = props.url;
|
|
130
|
+
if (!current) return;
|
|
131
|
+
syncUrl('');
|
|
132
|
+
emit('retry');
|
|
133
|
+
await nextTick();
|
|
134
|
+
syncUrl(current);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const toggleMute = () => {
|
|
138
|
+
isMuted.value = !isMuted.value;
|
|
139
|
+
if (elementRef.value) {
|
|
140
|
+
elementRef.value.muted = isMuted.value;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
expose({
|
|
145
|
+
player,
|
|
146
|
+
ptz,
|
|
147
|
+
voiceIntercom,
|
|
148
|
+
ptzAvailable,
|
|
149
|
+
voiceIntercomAvailable,
|
|
150
|
+
stop: handleStop,
|
|
151
|
+
retry: handleRetry,
|
|
152
|
+
status
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
elementRef,
|
|
157
|
+
isMuted,
|
|
158
|
+
status,
|
|
159
|
+
statusText,
|
|
160
|
+
error,
|
|
161
|
+
handleStop,
|
|
162
|
+
handleRetry,
|
|
163
|
+
toggleMute
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
template: `
|
|
167
|
+
<div class="wr-monitor">
|
|
168
|
+
<div class="wr-monitor__header" v-if="title">
|
|
169
|
+
<span class="wr-monitor__title">{{ title }}</span>
|
|
170
|
+
<span class="wr-monitor__badge" :class="'wr-monitor__badge--' + status">
|
|
171
|
+
{{ statusText }}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="wr-monitor__viewport">
|
|
176
|
+
<video
|
|
177
|
+
ref="elementRef"
|
|
178
|
+
class="wr-monitor__video"
|
|
179
|
+
autoplay
|
|
180
|
+
playsinline
|
|
181
|
+
:muted="isMuted"
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<div v-if="status === 'loading'" class="wr-monitor__overlay">
|
|
185
|
+
<div class="wr-monitor__spinner"></div>
|
|
186
|
+
<p>正在连接…</p>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div v-else-if="status === 'error'" class="wr-monitor__overlay wr-monitor__overlay--error">
|
|
190
|
+
<p>无法连接视频流</p>
|
|
191
|
+
<button type="button" class="wr-monitor__btn" @click="handleRetry">重新连接</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div v-else-if="status === 'idle'" class="wr-monitor__overlay">
|
|
195
|
+
<p>请传入 url 属性开始播放</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div v-if="showToolbar" class="wr-monitor__toolbar">
|
|
200
|
+
<button type="button" class="wr-monitor__btn" @click="toggleMute">
|
|
201
|
+
{{ isMuted ? '取消静音' : '静音' }}
|
|
202
|
+
</button>
|
|
203
|
+
<button type="button" class="wr-monitor__btn" @click="handleRetry" :disabled="!status || status === 'idle'">
|
|
204
|
+
重连
|
|
205
|
+
</button>
|
|
206
|
+
<button type="button" class="wr-monitor__btn wr-monitor__btn--danger" @click="handleStop" :disabled="status === 'idle'">
|
|
207
|
+
停止
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
`,
|
|
212
|
+
styles: `
|
|
213
|
+
.wr-monitor {
|
|
214
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
215
|
+
background: #0f1419;
|
|
216
|
+
border-radius: 8px;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
color: #e6edf3;
|
|
219
|
+
}
|
|
220
|
+
.wr-monitor__header {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: space-between;
|
|
224
|
+
padding: 10px 14px;
|
|
225
|
+
background: #161b22;
|
|
226
|
+
border-bottom: 1px solid #30363d;
|
|
227
|
+
}
|
|
228
|
+
.wr-monitor__title { font-size: 14px; font-weight: 600; }
|
|
229
|
+
.wr-monitor__badge {
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
padding: 2px 8px;
|
|
232
|
+
border-radius: 999px;
|
|
233
|
+
background: #21262d;
|
|
234
|
+
color: #8b949e;
|
|
235
|
+
}
|
|
236
|
+
.wr-monitor__badge--playing { background: #238636; color: #fff; }
|
|
237
|
+
.wr-monitor__badge--loading { background: #9e6a03; color: #fff; }
|
|
238
|
+
.wr-monitor__badge--error { background: #da3633; color: #fff; }
|
|
239
|
+
.wr-monitor__viewport {
|
|
240
|
+
position: relative;
|
|
241
|
+
aspect-ratio: 16 / 9;
|
|
242
|
+
background: #010409;
|
|
243
|
+
}
|
|
244
|
+
.wr-monitor__video {
|
|
245
|
+
width: 100%;
|
|
246
|
+
height: 100%;
|
|
247
|
+
object-fit: contain;
|
|
248
|
+
display: block;
|
|
249
|
+
background: #000;
|
|
250
|
+
}
|
|
251
|
+
.wr-monitor__overlay {
|
|
252
|
+
position: absolute;
|
|
253
|
+
inset: 0;
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: center;
|
|
258
|
+
gap: 12px;
|
|
259
|
+
background: rgba(1, 4, 9, 0.72);
|
|
260
|
+
color: #8b949e;
|
|
261
|
+
font-size: 14px;
|
|
262
|
+
}
|
|
263
|
+
.wr-monitor__overlay--error { color: #f85149; }
|
|
264
|
+
.wr-monitor__spinner {
|
|
265
|
+
width: 32px;
|
|
266
|
+
height: 32px;
|
|
267
|
+
border: 3px solid #30363d;
|
|
268
|
+
border-top-color: #58a6ff;
|
|
269
|
+
border-radius: 50%;
|
|
270
|
+
animation: wr-monitor-spin 0.8s linear infinite;
|
|
271
|
+
}
|
|
272
|
+
@keyframes wr-monitor-spin { to { transform: rotate(360deg); } }
|
|
273
|
+
.wr-monitor__toolbar {
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: 8px;
|
|
276
|
+
padding: 10px 14px;
|
|
277
|
+
background: #161b22;
|
|
278
|
+
border-top: 1px solid #30363d;
|
|
279
|
+
}
|
|
280
|
+
.wr-monitor__btn {
|
|
281
|
+
padding: 6px 12px;
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
border: 1px solid #30363d;
|
|
284
|
+
border-radius: 6px;
|
|
285
|
+
background: #21262d;
|
|
286
|
+
color: #e6edf3;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
}
|
|
289
|
+
.wr-monitor__btn:hover:not(:disabled) { background: #30363d; }
|
|
290
|
+
.wr-monitor__btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
291
|
+
.wr-monitor__btn--danger { border-color: #da3633; color: #ff7b72; }
|
|
292
|
+
`
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// 注入组件样式(仅一次)
|
|
296
|
+
if (typeof document !== 'undefined' && !document.getElementById('wr-monitor-view-styles')) {
|
|
297
|
+
const style = document.createElement('style');
|
|
298
|
+
style.id = 'wr-monitor-view-styles';
|
|
299
|
+
style.textContent = WRMonitorView.styles;
|
|
300
|
+
document.head.appendChild(style);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default WRMonitorView;
|
package/adapters/vue/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { watch } from 'vue';
|
|
2
2
|
import WRPlayer from '../../core/WRPlayer.js';
|
|
3
|
+
import { useWRPlayer } from './useWRPlayer.js';
|
|
4
|
+
import { WRMonitorView } from './WRMonitorView.js';
|
|
5
|
+
|
|
6
|
+
export { useWRPlayer } from './useWRPlayer.js';
|
|
7
|
+
export { WRMonitorView } from './WRMonitorView.js';
|
|
3
8
|
|
|
4
9
|
const PTZ_EVENTS = [
|
|
5
10
|
'ptz:move', 'ptz:zoom', 'ptz:focus', 'ptz:iris', 'ptz:speed:change',
|
|
@@ -14,150 +19,6 @@ const VOICE_INTERCOM_EVENTS = [
|
|
|
14
19
|
'voiceIntercom:localStream', 'voiceIntercom:remoteStream'
|
|
15
20
|
];
|
|
16
21
|
|
|
17
|
-
/**
|
|
18
|
-
* Vue 3 Composition API for WRPlayer with PTZ and Voice Intercom support
|
|
19
|
-
* @param {Object} config - Player configuration
|
|
20
|
-
* @returns {Object} Player instance and utilities
|
|
21
|
-
*/
|
|
22
|
-
export function useWRPlayer(config) {
|
|
23
|
-
const elementRef = ref(null);
|
|
24
|
-
const player = ref(null);
|
|
25
|
-
const isReady = ref(false);
|
|
26
|
-
const error = ref(null);
|
|
27
|
-
const ptzAvailable = ref(false);
|
|
28
|
-
const voiceIntercomAvailable = ref(false);
|
|
29
|
-
const voiceIntercomStatus = ref(null);
|
|
30
|
-
|
|
31
|
-
const initPlayer = () => {
|
|
32
|
-
if (!elementRef.value || !config.url) return;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const wrPlayer = new WRPlayer({
|
|
36
|
-
element: elementRef.value,
|
|
37
|
-
...config
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
player.value = wrPlayer;
|
|
41
|
-
ptzAvailable.value = !!wrPlayer.ptz;
|
|
42
|
-
voiceIntercomAvailable.value = !!wrPlayer.voiceIntercom;
|
|
43
|
-
|
|
44
|
-
wrPlayer.on('ready', () => {
|
|
45
|
-
isReady.value = true;
|
|
46
|
-
error.value = null;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
wrPlayer.on('error', (errorData) => {
|
|
50
|
-
error.value = errorData;
|
|
51
|
-
isReady.value = false;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (wrPlayer.voiceIntercom) {
|
|
55
|
-
wrPlayer.on('voiceIntercom:statusChange', (data) => {
|
|
56
|
-
voiceIntercomStatus.value = data;
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
} catch (err) {
|
|
61
|
-
error.value = { type: 'init_error', details: err };
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const destroyPlayer = () => {
|
|
66
|
-
if (player.value) {
|
|
67
|
-
player.value.stop();
|
|
68
|
-
player.value = null;
|
|
69
|
-
}
|
|
70
|
-
isReady.value = false;
|
|
71
|
-
error.value = null;
|
|
72
|
-
ptzAvailable.value = false;
|
|
73
|
-
voiceIntercomAvailable.value = false;
|
|
74
|
-
voiceIntercomStatus.value = null;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const play = () => {
|
|
78
|
-
if (player.value) {
|
|
79
|
-
player.value.play();
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const pause = () => {
|
|
84
|
-
if (player.value) {
|
|
85
|
-
player.value.pause();
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const stop = () => {
|
|
90
|
-
if (player.value) {
|
|
91
|
-
player.value.stop();
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const ptz = computed(() => {
|
|
96
|
-
if (!ptzAvailable.value || !player.value?.ptz) return null;
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
move: (direction, speed) => player.value.ptz.move(direction, speed),
|
|
100
|
-
zoom: (direction, speed) => player.value.ptz.zoom(direction, speed),
|
|
101
|
-
focus: (direction, speed) => player.value.ptz.focus(direction, speed),
|
|
102
|
-
iris: (direction, speed) => player.value.ptz.iris(direction, speed),
|
|
103
|
-
stop: () => player.value.ptz.stop(),
|
|
104
|
-
setSpeed: (speed) => player.value.ptz.setSpeed(speed),
|
|
105
|
-
getSpeed: () => player.value.ptz.getSpeed(),
|
|
106
|
-
preset: player.value.ptz.preset,
|
|
107
|
-
cruise: player.value.ptz.cruise,
|
|
108
|
-
scan: player.value.ptz.scan
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const voiceIntercom = computed(() => {
|
|
113
|
-
if (!voiceIntercomAvailable.value || !player.value?.voiceIntercom) return null;
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
start: (deviceId, channelId, mode) =>
|
|
117
|
-
player.value.voiceIntercom.startIntercom(deviceId, channelId, mode),
|
|
118
|
-
stop: () => player.value.voiceIntercom.stopIntercom(),
|
|
119
|
-
setMode: (mode) => player.value.voiceIntercom.setMode(mode),
|
|
120
|
-
getStatus: () => player.value.voiceIntercom.getStatus(),
|
|
121
|
-
status: voiceIntercomStatus.value
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
watch(
|
|
126
|
-
() => [config.url, config.type],
|
|
127
|
-
() => {
|
|
128
|
-
destroyPlayer();
|
|
129
|
-
if (config.url) {
|
|
130
|
-
initPlayer();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
onMounted(() => {
|
|
136
|
-
if (config.url) {
|
|
137
|
-
initPlayer();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
onUnmounted(() => {
|
|
142
|
-
destroyPlayer();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
elementRef,
|
|
147
|
-
player: computed(() => player.value),
|
|
148
|
-
isReady: computed(() => isReady.value),
|
|
149
|
-
error: computed(() => error.value),
|
|
150
|
-
ptzAvailable: computed(() => ptzAvailable.value),
|
|
151
|
-
voiceIntercomAvailable: computed(() => voiceIntercomAvailable.value),
|
|
152
|
-
voiceIntercomStatus: computed(() => voiceIntercomStatus.value),
|
|
153
|
-
play,
|
|
154
|
-
pause,
|
|
155
|
-
stop,
|
|
156
|
-
ptz,
|
|
157
|
-
voiceIntercom
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
22
|
/**
|
|
162
23
|
* Vue 3 Component for WRPlayer with PTZ and Voice Intercom support
|
|
163
24
|
*/
|
|
@@ -375,4 +236,4 @@ export const WRPlayerVue2 = {
|
|
|
375
236
|
`
|
|
376
237
|
};
|
|
377
238
|
|
|
378
|
-
export default { useWRPlayer, WRPlayerComponent, WRPlayerVue2 };
|
|
239
|
+
export default { useWRPlayer, WRPlayerComponent, WRPlayerVue2, WRMonitorView };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
|
2
|
+
import WRPlayer from '../../core/WRPlayer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vue 3 Composition API for WRPlayer with PTZ and Voice Intercom support
|
|
6
|
+
* @param {Object} config - Player configuration
|
|
7
|
+
* @returns {Object} Player instance and utilities
|
|
8
|
+
*/
|
|
9
|
+
export function useWRPlayer(config) {
|
|
10
|
+
const elementRef = ref(null);
|
|
11
|
+
const player = ref(null);
|
|
12
|
+
const isReady = ref(false);
|
|
13
|
+
const error = ref(null);
|
|
14
|
+
const ptzAvailable = ref(false);
|
|
15
|
+
const voiceIntercomAvailable = ref(false);
|
|
16
|
+
const voiceIntercomStatus = ref(null);
|
|
17
|
+
|
|
18
|
+
const initPlayer = () => {
|
|
19
|
+
if (!elementRef.value || !config.url) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const wrPlayer = new WRPlayer({
|
|
23
|
+
element: elementRef.value,
|
|
24
|
+
...config
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
player.value = wrPlayer;
|
|
28
|
+
ptzAvailable.value = !!wrPlayer.ptz;
|
|
29
|
+
voiceIntercomAvailable.value = !!wrPlayer.voiceIntercom;
|
|
30
|
+
|
|
31
|
+
wrPlayer.on('ready', () => {
|
|
32
|
+
isReady.value = true;
|
|
33
|
+
error.value = null;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
wrPlayer.on('error', (errorData) => {
|
|
37
|
+
error.value = errorData;
|
|
38
|
+
isReady.value = false;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (wrPlayer.voiceIntercom) {
|
|
42
|
+
wrPlayer.on('voiceIntercom:statusChange', (data) => {
|
|
43
|
+
voiceIntercomStatus.value = data;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
} catch (err) {
|
|
48
|
+
error.value = { type: 'init_error', details: err };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const destroyPlayer = () => {
|
|
53
|
+
if (player.value) {
|
|
54
|
+
player.value.stop();
|
|
55
|
+
player.value = null;
|
|
56
|
+
}
|
|
57
|
+
isReady.value = false;
|
|
58
|
+
error.value = null;
|
|
59
|
+
ptzAvailable.value = false;
|
|
60
|
+
voiceIntercomAvailable.value = false;
|
|
61
|
+
voiceIntercomStatus.value = null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const play = () => {
|
|
65
|
+
if (player.value) {
|
|
66
|
+
player.value.play();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const pause = () => {
|
|
71
|
+
if (player.value) {
|
|
72
|
+
player.value.pause();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const stop = () => {
|
|
77
|
+
if (player.value) {
|
|
78
|
+
player.value.stop();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ptz = computed(() => {
|
|
83
|
+
if (!ptzAvailable.value || !player.value?.ptz) return null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
move: (direction, speed) => player.value.ptz.move(direction, speed),
|
|
87
|
+
zoom: (direction, speed) => player.value.ptz.zoom(direction, speed),
|
|
88
|
+
focus: (direction, speed) => player.value.ptz.focus(direction, speed),
|
|
89
|
+
iris: (direction, speed) => player.value.ptz.iris(direction, speed),
|
|
90
|
+
stop: () => player.value.ptz.stop(),
|
|
91
|
+
setSpeed: (speed) => player.value.ptz.setSpeed(speed),
|
|
92
|
+
getSpeed: () => player.value.ptz.getSpeed(),
|
|
93
|
+
preset: player.value.ptz.preset,
|
|
94
|
+
cruise: player.value.ptz.cruise,
|
|
95
|
+
scan: player.value.ptz.scan
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const voiceIntercom = computed(() => {
|
|
100
|
+
if (!voiceIntercomAvailable.value || !player.value?.voiceIntercom) return null;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
start: (deviceId, channelId, mode) =>
|
|
104
|
+
player.value.voiceIntercom.startIntercom(deviceId, channelId, mode),
|
|
105
|
+
stop: () => player.value.voiceIntercom.stopIntercom(),
|
|
106
|
+
setMode: (mode) => player.value.voiceIntercom.setMode(mode),
|
|
107
|
+
getStatus: () => player.value.voiceIntercom.getStatus(),
|
|
108
|
+
status: voiceIntercomStatus.value
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
watch(
|
|
113
|
+
() => [config.url, config.type],
|
|
114
|
+
() => {
|
|
115
|
+
destroyPlayer();
|
|
116
|
+
if (config.url) {
|
|
117
|
+
initPlayer();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
onMounted(() => {
|
|
123
|
+
if (config.url) {
|
|
124
|
+
initPlayer();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
onUnmounted(() => {
|
|
129
|
+
destroyPlayer();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
elementRef,
|
|
134
|
+
player: computed(() => player.value),
|
|
135
|
+
isReady: computed(() => isReady.value),
|
|
136
|
+
error: computed(() => error.value),
|
|
137
|
+
ptzAvailable: computed(() => ptzAvailable.value),
|
|
138
|
+
voiceIntercomAvailable: computed(() => voiceIntercomAvailable.value),
|
|
139
|
+
voiceIntercomStatus: computed(() => voiceIntercomStatus.value),
|
|
140
|
+
play,
|
|
141
|
+
pause,
|
|
142
|
+
stop,
|
|
143
|
+
ptz,
|
|
144
|
+
voiceIntercom
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default useWRPlayer;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WVP / ZLM WebRTC 流地址拼接工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 拼接 WVP 设备+通道 stream 标识
|
|
7
|
+
* @param {string} deviceId
|
|
8
|
+
* @param {string} channelId
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function buildWvpStreamKey(deviceId, channelId) {
|
|
12
|
+
return `${deviceId}_${channelId}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 拼接 WebRTC 播放地址(WVP / ZLM 标准格式)
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} options.host - 媒体服务器地址,如 222.222.25.198:6080
|
|
19
|
+
* @param {string} options.stream - 流 ID
|
|
20
|
+
* @param {string} [options.app='rtp'] - app 名称
|
|
21
|
+
* @param {boolean} [options.useSsl=false] - 是否 https
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function buildWebRtcUrl({ host, stream, app = 'rtp', useSsl = false }) {
|
|
25
|
+
if (!host || !stream) {
|
|
26
|
+
throw new Error('buildWebRtcUrl: host and stream are required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const base = host.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
30
|
+
const protocol = useSsl ? 'https' : 'http';
|
|
31
|
+
const params = new URLSearchParams({ app, stream });
|
|
32
|
+
|
|
33
|
+
return `${protocol}://${base}/index/api/webrtc?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 根据国标 deviceId + channelId 拼接 WebRTC 地址
|
|
38
|
+
* @param {Object} options
|
|
39
|
+
* @param {string} options.host
|
|
40
|
+
* @param {string} options.deviceId
|
|
41
|
+
* @param {string} options.channelId
|
|
42
|
+
* @param {string} [options.app='rtp']
|
|
43
|
+
* @param {boolean} [options.useSsl=false]
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function buildWebRtcUrlFromDevice({ host, deviceId, channelId, app = 'rtp', useSsl = false }) {
|
|
47
|
+
return buildWebRtcUrl({
|
|
48
|
+
host,
|
|
49
|
+
app,
|
|
50
|
+
useSsl,
|
|
51
|
+
stream: buildWvpStreamKey(deviceId, channelId)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 清理流地址(去除首尾空格、引号)
|
|
57
|
+
* @param {string} url
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeStreamUrl(url) {
|
|
61
|
+
if (!url || typeof url !== 'string') return '';
|
|
62
|
+
return url.trim().replace(/^["']|["']$/g, '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
buildWvpStreamKey,
|
|
67
|
+
buildWebRtcUrl,
|
|
68
|
+
buildWebRtcUrlFromDevice,
|
|
69
|
+
normalizeStreamUrl
|
|
70
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wrplayer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A unified web video player SDK supporting WebRTC, HLS, DASH and other formats with PTZ camera control",
|
|
5
5
|
"main": "dist/wrplayer.umd.js",
|
|
6
6
|
"module": "dist/wrplayer.esm.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"./core": "./core/WRPlayer.js",
|
|
51
51
|
"./core/ptz": "./core/PTZController.js",
|
|
52
|
+
"./core/streamUrl": "./core/streamUrl.js",
|
|
52
53
|
"./adapters/react": "./adapters/react/index.js",
|
|
53
54
|
"./adapters/vue": "./adapters/vue/index.js",
|
|
54
55
|
"./adapters/vanilla": "./adapters/vanilla/index.js"
|