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/core/WRPlayer.js
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import ZLMRTCClient from './ZLMRTCClient-v1.1.2.js';
|
|
2
|
+
import PTZController from './PTZController.js';
|
|
3
|
+
import VoiceIntercomController from './VoiceIntercomController.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WRPlayer - A unified web video player SDK
|
|
7
|
+
* Supports WebRTC, HLS, DASH and other formats with PTZ camera control
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class WRPlayer {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new player instance
|
|
13
|
+
* @param {Object} options - Player configuration
|
|
14
|
+
* @param {HTMLVideoElement} options.element - Video element for playback
|
|
15
|
+
* @param {string} options.url - URL for the stream/video
|
|
16
|
+
* @param {string} [options.type] - Type of player ('webrtc', 'hls', 'dash', 'mp4'), if not provided will auto-detect from URL
|
|
17
|
+
* @param {boolean} options.autoplay - Whether to start playing automatically
|
|
18
|
+
* @param {boolean} options.debug - Enable debug logging
|
|
19
|
+
* @param {Object} [options.dependencies] - External dependencies configuration
|
|
20
|
+
* @param {string} [options.dependencies.hlsPath] - Path to hls.js if needs to be loaded dynamically
|
|
21
|
+
* @param {string} [options.dependencies.dashPath] - Path to dash.js if needs to be loaded dynamically
|
|
22
|
+
* @param {Object} [options.ptz] - PTZ camera control configuration
|
|
23
|
+
* @param {string} [options.ptz.apiUrl] - SIP server address for PTZ control
|
|
24
|
+
* @param {string} [options.ptz.apiKey] - API key for PTZ authentication
|
|
25
|
+
* @param {string} [options.ptz.deviceId] - PTZ device ID
|
|
26
|
+
* @param {string} [options.ptz.channelId] - PTZ channel ID
|
|
27
|
+
* @param {boolean} [options.ptz.useProxy=true] - Whether to use proxy for PTZ requests
|
|
28
|
+
* @param {Object} [options.voiceIntercom] - Voice intercom configuration
|
|
29
|
+
* @param {string} [options.voiceIntercom.apiUrl] - SIP server address for voice intercom
|
|
30
|
+
* @param {string} [options.voiceIntercom.apiKey] - API key for voice intercom authentication
|
|
31
|
+
* @param {string} [options.voiceIntercom.deviceId] - Voice intercom device ID
|
|
32
|
+
* @param {string} [options.voiceIntercom.channelId] - Voice intercom channel ID
|
|
33
|
+
* @param {number} [options.voiceIntercom.timeout=30] - Voice intercom connection timeout
|
|
34
|
+
* @param {boolean} [options.voiceIntercom.useProxy=true] - Whether to use proxy for voice intercom requests
|
|
35
|
+
*/
|
|
36
|
+
constructor(options) {
|
|
37
|
+
// Check if we're in a browser environment
|
|
38
|
+
this.isBrowser = typeof window !== 'undefined';
|
|
39
|
+
|
|
40
|
+
this.options = {
|
|
41
|
+
autoplay: true,
|
|
42
|
+
debug: false,
|
|
43
|
+
dependencies: {
|
|
44
|
+
hlsPath: 'https://cdn.jsdelivr.net/npm/hls.js@latest',
|
|
45
|
+
dashPath: 'https://cdn.dashjs.org/latest/dash.all.min.js'
|
|
46
|
+
},
|
|
47
|
+
...options
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Deep merge dependencies
|
|
51
|
+
if (options.dependencies) {
|
|
52
|
+
this.options.dependencies = {
|
|
53
|
+
...this.options.dependencies,
|
|
54
|
+
...options.dependencies
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Skip initialization if not in browser (SSR)
|
|
59
|
+
if (!this.isBrowser) {
|
|
60
|
+
if (this.options.debug) {
|
|
61
|
+
console.log('WRPlayer: Running in SSR mode, skipping initialization');
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.element = this.options.element;
|
|
67
|
+
if (!this.element) {
|
|
68
|
+
throw new Error('WRPlayer: Video element is required');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.player = null;
|
|
72
|
+
this.eventHandlers = {};
|
|
73
|
+
this.loadedDependencies = {};
|
|
74
|
+
|
|
75
|
+
// Initialize PTZ controller if PTZ configuration is provided
|
|
76
|
+
this.ptz = null;
|
|
77
|
+
if (this.options.ptz && this.options.ptz.apiUrl) {
|
|
78
|
+
try {
|
|
79
|
+
this.ptz = new PTZController({
|
|
80
|
+
debug: this.options.debug,
|
|
81
|
+
...this.options.ptz
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Forward PTZ events to the main player event system
|
|
85
|
+
this._setupPTZEventForwarding();
|
|
86
|
+
|
|
87
|
+
if (this.options.debug) {
|
|
88
|
+
console.log('WRPlayer: PTZ controller initialized');
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn('WRPlayer: Failed to initialize PTZ controller:', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initialize Voice Intercom controller if voice intercom configuration is provided
|
|
96
|
+
this.voiceIntercom = null;
|
|
97
|
+
if (this.options.voiceIntercom && this.options.voiceIntercom.apiUrl) {
|
|
98
|
+
try {
|
|
99
|
+
this.voiceIntercom = new VoiceIntercomController({
|
|
100
|
+
debug: this.options.debug,
|
|
101
|
+
...this.options.voiceIntercom
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Forward Voice Intercom events to the main player event system
|
|
105
|
+
this._setupVoiceIntercomEventForwarding();
|
|
106
|
+
|
|
107
|
+
if (this.options.debug) {
|
|
108
|
+
console.log('WRPlayer: Voice Intercom controller initialized');
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn('WRPlayer: Failed to initialize Voice Intercom controller:', error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// WebRTC 状态管理,防止重复播放
|
|
116
|
+
this.webrtcState = {
|
|
117
|
+
isConnecting: false,
|
|
118
|
+
isConnected: false,
|
|
119
|
+
hasStartedReceiving: false,
|
|
120
|
+
isRetryWithVideoOnly: false, // 回退策略标志
|
|
121
|
+
retryCount: 0 // 重试计数
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// 超时检测 ID
|
|
125
|
+
this.webrtcTimeoutId = null;
|
|
126
|
+
|
|
127
|
+
// Detect player type from URL if not specified
|
|
128
|
+
if (!this.options.type && this.options.url) {
|
|
129
|
+
this.options.type = this._detectTypeFromUrl(this.options.url);
|
|
130
|
+
if (this.options.debug) {
|
|
131
|
+
console.log(`WRPlayer: Auto-detected type: ${this.options.type} from URL: ${this.options.url}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Initialize the player based on the type
|
|
136
|
+
this._initPlayer();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load external JavaScript dependency if needed
|
|
141
|
+
* @private
|
|
142
|
+
* @param {string} type - Type of dependency ('hls', 'dash')
|
|
143
|
+
* @returns {Promise} - Promise that resolves when dependency is loaded
|
|
144
|
+
*/
|
|
145
|
+
_loadDependency(type) {
|
|
146
|
+
// Skip if not in browser
|
|
147
|
+
if (!this.isBrowser) {
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If already loaded, return resolved promise
|
|
152
|
+
if (this.loadedDependencies[type]) {
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if the dependency is already available globally
|
|
157
|
+
if (type === 'hls' && typeof Hls !== 'undefined') {
|
|
158
|
+
this.loadedDependencies[type] = true;
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
} else if (type === 'dash' && typeof dashjs !== 'undefined') {
|
|
161
|
+
this.loadedDependencies[type] = true;
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Need to dynamically load the script
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const script = document.createElement('script');
|
|
168
|
+
let path;
|
|
169
|
+
|
|
170
|
+
switch (type) {
|
|
171
|
+
case 'hls':
|
|
172
|
+
path = this.options.dependencies.hlsPath;
|
|
173
|
+
break;
|
|
174
|
+
case 'dash':
|
|
175
|
+
path = this.options.dependencies.dashPath;
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
reject(new Error(`WRPlayer: Unknown dependency type ${type}`));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.options.debug) {
|
|
183
|
+
console.log(`WRPlayer: Attempting to load ${type} from ${path}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
script.src = path;
|
|
187
|
+
script.async = true;
|
|
188
|
+
|
|
189
|
+
script.onload = () => {
|
|
190
|
+
if (this.options.debug) {
|
|
191
|
+
console.log(`WRPlayer: Successfully loaded ${type} from ${path}`);
|
|
192
|
+
}
|
|
193
|
+
this.loadedDependencies[type] = true;
|
|
194
|
+
resolve();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
script.onerror = (error) => {
|
|
198
|
+
console.error(`WRPlayer: Failed to load ${type} from ${path}`, error);
|
|
199
|
+
reject(new Error(`WRPlayer: Failed to load ${type} from ${path}`));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
document.head.appendChild(script);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Detect the stream type based on URL format
|
|
208
|
+
* @private
|
|
209
|
+
* @param {string} url - The URL to analyze
|
|
210
|
+
* @returns {string} The detected type ('webrtc', 'hls', 'dash', 'mp4', etc.)
|
|
211
|
+
*/
|
|
212
|
+
_detectTypeFromUrl(url) {
|
|
213
|
+
// Skip if not in browser
|
|
214
|
+
if (!this.isBrowser) {
|
|
215
|
+
return 'unknown';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Create URL object for easier parsing
|
|
219
|
+
let urlObj;
|
|
220
|
+
try {
|
|
221
|
+
urlObj = new URL(url);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
// If not a valid URL, try to extract extension from string
|
|
224
|
+
const extension = url.split('.').pop().toLowerCase();
|
|
225
|
+
return this._getTypeFromExtension(extension);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check for specific patterns in the URL
|
|
229
|
+
|
|
230
|
+
// Check for WebRTC patterns
|
|
231
|
+
if (
|
|
232
|
+
urlObj.pathname.includes('webrtc') ||
|
|
233
|
+
urlObj.searchParams.has('type') && urlObj.searchParams.get('type').includes('webrtc') ||
|
|
234
|
+
urlObj.searchParams.has('protocol') && urlObj.searchParams.get('protocol').includes('webrtc') ||
|
|
235
|
+
url.includes('sdp') ||
|
|
236
|
+
url.includes('rtc')
|
|
237
|
+
) {
|
|
238
|
+
return 'webrtc';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Extract file extension from path
|
|
242
|
+
const pathname = urlObj.pathname;
|
|
243
|
+
const extension = pathname.split('.').pop().toLowerCase();
|
|
244
|
+
|
|
245
|
+
return this._getTypeFromExtension(extension);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get media type from file extension
|
|
250
|
+
* @private
|
|
251
|
+
* @param {string} extension - The file extension
|
|
252
|
+
* @returns {string} The media type
|
|
253
|
+
*/
|
|
254
|
+
_getTypeFromExtension(extension) {
|
|
255
|
+
switch (extension) {
|
|
256
|
+
case 'm3u8':
|
|
257
|
+
case 'm3u':
|
|
258
|
+
return 'hls';
|
|
259
|
+
case 'mpd':
|
|
260
|
+
return 'dash';
|
|
261
|
+
case 'mp4':
|
|
262
|
+
return 'mp4';
|
|
263
|
+
case 'webm':
|
|
264
|
+
return 'webm';
|
|
265
|
+
case 'ogg':
|
|
266
|
+
case 'ogv':
|
|
267
|
+
return 'ogg';
|
|
268
|
+
default:
|
|
269
|
+
// Default to HTML5 player for unknown types
|
|
270
|
+
return 'mp4';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Initialize the appropriate player based on the type
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_initPlayer() {
|
|
279
|
+
// Skip if not in browser
|
|
280
|
+
if (!this.isBrowser) {
|
|
281
|
+
return Promise.resolve();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { type, url } = this.options;
|
|
285
|
+
|
|
286
|
+
if (!url) {
|
|
287
|
+
throw new Error('WRPlayer: URL is required');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!type) {
|
|
291
|
+
throw new Error('WRPlayer: Could not determine player type');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Use a Promise chain to handle dependency loading and player initialization
|
|
295
|
+
let initPromise;
|
|
296
|
+
|
|
297
|
+
switch (type.toLowerCase()) {
|
|
298
|
+
case 'webrtc':
|
|
299
|
+
// ZLMRTCClient is now imported directly, so it's always available.
|
|
300
|
+
this._initWebRTCPlayer();
|
|
301
|
+
initPromise = Promise.resolve();
|
|
302
|
+
break;
|
|
303
|
+
case 'hls':
|
|
304
|
+
initPromise = this._loadDependency('hls')
|
|
305
|
+
.then(() => this._initHLSPlayer())
|
|
306
|
+
.catch(err => {
|
|
307
|
+
console.error(err);
|
|
308
|
+
throw new Error('WRPlayer: Failed to initialize HLS player. Make sure hls.js is available.');
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
case 'dash':
|
|
312
|
+
initPromise = this._loadDependency('dash')
|
|
313
|
+
.then(() => this._initDASHPlayer())
|
|
314
|
+
.catch(err => {
|
|
315
|
+
console.error(err);
|
|
316
|
+
throw new Error('WRPlayer: Failed to initialize DASH player. Make sure dash.js is available.');
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
319
|
+
case 'mp4':
|
|
320
|
+
case 'webm':
|
|
321
|
+
case 'ogg':
|
|
322
|
+
default:
|
|
323
|
+
// HTML5 player doesn't need external dependencies
|
|
324
|
+
this._initHTMLPlayer();
|
|
325
|
+
initPromise = Promise.resolve();
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Start playback if autoplay is enabled, after initialization.
|
|
330
|
+
// WebRTC recvOnly mode starts receiving in ZLMRTCClient.Endpoint's constructor,
|
|
331
|
+
// so calling play() again only produces a duplicate receive() attempt.
|
|
332
|
+
if (this.options.autoplay && this.options.type !== 'webrtc') {
|
|
333
|
+
initPromise
|
|
334
|
+
.then(() => {
|
|
335
|
+
// Small timeout to ensure everything is ready
|
|
336
|
+
setTimeout(() => this.play(), 100);
|
|
337
|
+
})
|
|
338
|
+
.catch(err => {
|
|
339
|
+
this._dispatchEvent('error', { type: 'init_failed', details: err });
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return initPromise;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Initialize a WebRTC player using ZLMRTCClient
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
_initWebRTCPlayer() {
|
|
351
|
+
if (!ZLMRTCClient) {
|
|
352
|
+
throw new Error('WRPlayer: ZLMRTCClient is not available. Build might have failed.');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 灵活的音频控制:优先级 回退策略 > webrtcConfig > 用户设置 > 默认值
|
|
356
|
+
let audioEnable;
|
|
357
|
+
if (this.webrtcState.isRetryWithVideoOnly) {
|
|
358
|
+
// 回退模式:强制禁用音频,优先级最高
|
|
359
|
+
audioEnable = false;
|
|
360
|
+
} else if (this.options.webrtcConfig && this.options.webrtcConfig.audioEnable !== undefined) {
|
|
361
|
+
// webrtcConfig 中明确设置 - 最高优先级
|
|
362
|
+
audioEnable = this.options.webrtcConfig.audioEnable;
|
|
363
|
+
} else if (this.options.recvAudio !== undefined) {
|
|
364
|
+
// 用户明确设置
|
|
365
|
+
audioEnable = this.options.recvAudio;
|
|
366
|
+
} else {
|
|
367
|
+
// 默认启用音频(与 web 项目保持一致)
|
|
368
|
+
audioEnable = true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// WebRTC 配置 - 可以通过 options.webrtcConfig 来自定义
|
|
372
|
+
const defaultWebRTCConfig = {
|
|
373
|
+
element: this.element,
|
|
374
|
+
zlmsdpUrl: this.options.url,
|
|
375
|
+
debug: this.options.debug,
|
|
376
|
+
recvOnly: true,
|
|
377
|
+
audioEnable: audioEnable,
|
|
378
|
+
videoEnable: this.options.recvVideo !== false, // 默认启用视频
|
|
379
|
+
// 添加 web 项目中兼容 1.0.1 版本的默认配置
|
|
380
|
+
simulcast: false, // 关键:禁用多路视频编码,避免额外的媒体流
|
|
381
|
+
useCamera: false,
|
|
382
|
+
usedatachannel: false
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// 合并用户自定义的 WebRTC 配置
|
|
386
|
+
const webrtcConfig = {
|
|
387
|
+
...defaultWebRTCConfig,
|
|
388
|
+
...(this.options.webrtcConfig || {})
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (this.webrtcState.isRetryWithVideoOnly) {
|
|
392
|
+
webrtcConfig.audioEnable = false;
|
|
393
|
+
webrtcConfig.recvAudio = false;
|
|
394
|
+
webrtcConfig.audioConstraints = false;
|
|
395
|
+
webrtcConfig._forceVideoOnly = true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 强制确保音频完全禁用 - 防止 ZLMRTCClient 内部逻辑问题
|
|
399
|
+
if (webrtcConfig.audioEnable === false) {
|
|
400
|
+
webrtcConfig.audioEnable = false;
|
|
401
|
+
// 确保不会有任何音频相关的 transceiver
|
|
402
|
+
webrtcConfig.audioConstraints = false;
|
|
403
|
+
|
|
404
|
+
// 添加特殊标记,用于后续的 SDP 处理
|
|
405
|
+
webrtcConfig._forceVideoOnly = true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.options.debug) {
|
|
409
|
+
let strategy = '';
|
|
410
|
+
if (this.webrtcState.isRetryWithVideoOnly) {
|
|
411
|
+
strategy = '回退模式(禁用音频)';
|
|
412
|
+
} else if (this.options.webrtcConfig && this.options.webrtcConfig.audioEnable !== undefined) {
|
|
413
|
+
strategy = this.options.webrtcConfig.audioEnable ? 'webrtcConfig启用音频' : 'webrtcConfig禁用音频';
|
|
414
|
+
} else if (this.options.recvAudio === true) {
|
|
415
|
+
strategy = '用户启用音频';
|
|
416
|
+
} else if (this.options.recvAudio === false) {
|
|
417
|
+
strategy = '用户禁用音频';
|
|
418
|
+
} else {
|
|
419
|
+
strategy = '默认启用音频(与web项目一致)';
|
|
420
|
+
}
|
|
421
|
+
console.log('WebRPlayer: Audio strategy -', strategy);
|
|
422
|
+
console.log('WebRTC Config:', webrtcConfig);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.player = new ZLMRTCClient.Endpoint(webrtcConfig);
|
|
426
|
+
|
|
427
|
+
// 重要:ZLM 在 recvOnly=true 时会在构造器里直接开始 receive()
|
|
428
|
+
// 为避免后续 play() 再次调用 receive() 导致重复添加 transceiver/多余 m-line,
|
|
429
|
+
// 这里立即标记为连接中,并启动超时监控。
|
|
430
|
+
this.webrtcState.isConnecting = true;
|
|
431
|
+
if (!this.webrtcTimeoutId) {
|
|
432
|
+
this.webrtcTimeoutId = setTimeout(() => {
|
|
433
|
+
if (this.webrtcState.isConnecting && !this.webrtcState.hasStartedReceiving) {
|
|
434
|
+
console.log('WebRTC: 连接超时,可能发生 SDP 协商错误');
|
|
435
|
+
this._handleWebRTCError({
|
|
436
|
+
type: 'connection_timeout',
|
|
437
|
+
details: 'WebRTC connection timeout, possibly due to SDP negotiation failure'
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}, 10000);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 添加特殊的错误监听器,快速检测 mid='1' 音频解复用错误
|
|
444
|
+
this.player.on('error', (error) => {
|
|
445
|
+
if (error && error.message &&
|
|
446
|
+
(error.message.includes("Failed to set up audio demuxing for mid='1'") ||
|
|
447
|
+
error.message.includes("audio demuxing"))) {
|
|
448
|
+
console.warn('WebRTC: 检测到 mid=1 音频解复用错误,这通常是服务器配置问题');
|
|
449
|
+
// 立即触发回退机制
|
|
450
|
+
this._handleWebRTCError({
|
|
451
|
+
type: 'sdp_audio_demux_error',
|
|
452
|
+
details: 'Mid=1 audio demuxing failed - server configuration issue'
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Map events
|
|
459
|
+
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => {
|
|
460
|
+
this.webrtcState.hasStartedReceiving = true;
|
|
461
|
+
|
|
462
|
+
// 清除超时检测
|
|
463
|
+
if (this.webrtcTimeoutId) {
|
|
464
|
+
clearTimeout(this.webrtcTimeoutId);
|
|
465
|
+
this.webrtcTimeoutId = null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this._dispatchEvent('ready', { streams: e.streams });
|
|
469
|
+
this._dispatchEvent('play');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
this.player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {
|
|
473
|
+
this._handleWebRTCError({
|
|
474
|
+
type: 'exchange_failed',
|
|
475
|
+
details: e
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
this.player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (state) => {
|
|
480
|
+
if (this.options.debug) {
|
|
481
|
+
console.log('WebRTC Connection state changed:', state);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (state === 'connecting') {
|
|
485
|
+
this.webrtcState.isConnecting = true;
|
|
486
|
+
this.webrtcState.isConnected = false;
|
|
487
|
+
} else if (state === 'connected') {
|
|
488
|
+
this.webrtcState.isConnecting = false;
|
|
489
|
+
this.webrtcState.isConnected = true;
|
|
490
|
+
} else if (state === 'failed' || state === 'disconnected') {
|
|
491
|
+
this.webrtcState.isConnecting = false;
|
|
492
|
+
this.webrtcState.isConnected = false;
|
|
493
|
+
this.webrtcState.hasStartedReceiving = false;
|
|
494
|
+
|
|
495
|
+
// 如果连接失败且用户启用了音频,尝试回退策略
|
|
496
|
+
if (state === 'failed' && !this.webrtcState.isRetryWithVideoOnly) {
|
|
497
|
+
this._handleWebRTCError({
|
|
498
|
+
type: 'connection_failed',
|
|
499
|
+
details: 'WebRTC connection state changed to failed'
|
|
500
|
+
});
|
|
501
|
+
return; // 不继续分发事件,让回退策略处理
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this._dispatchEvent('connectionstatechange', { state });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// 监听 ICE 候选错误
|
|
509
|
+
this.player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {
|
|
510
|
+
if (this.options.debug) {
|
|
511
|
+
console.log('WebRTC ICE Candidate Error:', e);
|
|
512
|
+
}
|
|
513
|
+
this._handleWebRTCError({
|
|
514
|
+
type: 'ice_candidate_error',
|
|
515
|
+
details: e
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Initialize an HLS player
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
_initHLSPlayer() {
|
|
525
|
+
// Check if Hls.js is available
|
|
526
|
+
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
|
527
|
+
const hls = new Hls({
|
|
528
|
+
debug: this.options.debug
|
|
529
|
+
});
|
|
530
|
+
hls.loadSource(this.options.url);
|
|
531
|
+
hls.attachMedia(this.element);
|
|
532
|
+
|
|
533
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
534
|
+
this._dispatchEvent('ready');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
538
|
+
this._dispatchEvent('error', { type: 'hls_error', details: data });
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
this.player = hls;
|
|
542
|
+
}
|
|
543
|
+
else if (this.element.canPlayType('application/vnd.apple.mpegurl')) {
|
|
544
|
+
// Native HLS support (Safari)
|
|
545
|
+
this.element.src = this.options.url;
|
|
546
|
+
this._setupHTMLVideoEvents();
|
|
547
|
+
this.player = this.element;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
throw new Error('WRPlayer: HLS is not supported in this browser');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Initialize a DASH player
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
_initDASHPlayer() {
|
|
559
|
+
// Check if dash.js is available
|
|
560
|
+
if (typeof dashjs !== 'undefined') {
|
|
561
|
+
const dashPlayer = dashjs.MediaPlayer().create();
|
|
562
|
+
dashPlayer.initialize(this.element, this.options.url, this.options.autoplay);
|
|
563
|
+
|
|
564
|
+
if (this.options.debug) {
|
|
565
|
+
dashPlayer.updateSettings({ 'debug': { 'logLevel': dashjs.Debug.LOG_LEVEL_DEBUG } });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
this.player = dashPlayer;
|
|
569
|
+
this._setupHTMLVideoEvents();
|
|
570
|
+
} else {
|
|
571
|
+
throw new Error('WRPlayer: DASH is not supported, please include dash.js');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Initialize a regular HTML5 video player
|
|
577
|
+
* @private
|
|
578
|
+
*/
|
|
579
|
+
_initHTMLPlayer() {
|
|
580
|
+
this.element.src = this.options.url;
|
|
581
|
+
if (this.options.autoplay) {
|
|
582
|
+
this.element.autoplay = true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this._setupHTMLVideoEvents();
|
|
586
|
+
this.player = this.element;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Set up PTZ event forwarding to the main player event system
|
|
591
|
+
* @private
|
|
592
|
+
*/
|
|
593
|
+
_setupPTZEventForwarding() {
|
|
594
|
+
if (!this.ptz) return;
|
|
595
|
+
|
|
596
|
+
// Forward all PTZ events with 'ptz:' prefix
|
|
597
|
+
const ptzEvents = [
|
|
598
|
+
'move', 'zoom', 'focus', 'iris', 'speed:change',
|
|
599
|
+
'preset:list', 'preset:saved', 'preset:called', 'preset:deleted',
|
|
600
|
+
'cruise:started', 'cruise:stopped', 'cruise:speed:set',
|
|
601
|
+
'scan:started', 'scan:stopped', 'scan:boundary:left', 'scan:boundary:right',
|
|
602
|
+
'request:success', 'request:error'
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
ptzEvents.forEach(event => {
|
|
606
|
+
this.ptz.on(event, (data) => {
|
|
607
|
+
this._dispatchEvent(`ptz:${event}`, data);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Set up Voice Intercom event forwarding to the main player event system
|
|
614
|
+
* @private
|
|
615
|
+
*/
|
|
616
|
+
_setupVoiceIntercomEventForwarding() {
|
|
617
|
+
if (!this.voiceIntercom) return;
|
|
618
|
+
|
|
619
|
+
// Forward all Voice Intercom events with 'voiceIntercom:' prefix
|
|
620
|
+
const voiceIntercomEvents = [
|
|
621
|
+
'statusChange', 'modeChange', 'error', 'localStream', 'remoteStream'
|
|
622
|
+
];
|
|
623
|
+
|
|
624
|
+
voiceIntercomEvents.forEach(event => {
|
|
625
|
+
this.voiceIntercom.on(event, (data) => {
|
|
626
|
+
this._dispatchEvent(`voiceIntercom:${event}`, data);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Set up event listeners for HTML5 video elements
|
|
633
|
+
* @private
|
|
634
|
+
*/
|
|
635
|
+
_setupHTMLVideoEvents() {
|
|
636
|
+
// Map standard HTML5 video events to our event system
|
|
637
|
+
const events = ['play', 'pause', 'ended', 'timeupdate', 'seeking', 'seeked', 'waiting', 'loadedmetadata', 'loadeddata'];
|
|
638
|
+
|
|
639
|
+
events.forEach(eventName => {
|
|
640
|
+
this.element.addEventListener(eventName, () => {
|
|
641
|
+
this._dispatchEvent(eventName);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
this.element.addEventListener('canplay', () => {
|
|
646
|
+
this._dispatchEvent('ready');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
this.element.addEventListener('error', () => {
|
|
650
|
+
this._dispatchEvent('error', {
|
|
651
|
+
type: 'video_error',
|
|
652
|
+
details: this.element.error
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Start playing the video
|
|
659
|
+
*/
|
|
660
|
+
play() {
|
|
661
|
+
const type = this.options.type.toLowerCase();
|
|
662
|
+
|
|
663
|
+
if (type === 'webrtc') {
|
|
664
|
+
if (this.player) {
|
|
665
|
+
// 防止重复播放:如果已经在连接中或已经开始接收,不再重复调用
|
|
666
|
+
if (this.webrtcState.isConnecting || this.webrtcState.hasStartedReceiving) {
|
|
667
|
+
console.log('WebRTC: Already connecting or receiving, skipping duplicate play() call');
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
console.log('WebRTC: Starting receive...');
|
|
672
|
+
this.webrtcState.isConnecting = true;
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
this.player.receive();
|
|
676
|
+
|
|
677
|
+
// 设置一个超时来检测是否连接成功
|
|
678
|
+
this.webrtcTimeoutId = setTimeout(() => {
|
|
679
|
+
if (this.webrtcState.isConnecting && !this.webrtcState.hasStartedReceiving) {
|
|
680
|
+
console.log('WebRTC: 连接超时,可能发生 SDP 协商错误');
|
|
681
|
+
this._handleWebRTCError({
|
|
682
|
+
type: 'connection_timeout',
|
|
683
|
+
details: 'WebRTC connection timeout, possibly due to SDP negotiation failure'
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}, 10000); // 10秒超时
|
|
687
|
+
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error('WebRTC: receive() 调用失败:', error);
|
|
690
|
+
this._handleWebRTCError({
|
|
691
|
+
type: 'receive_failed',
|
|
692
|
+
details: error
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
const playPromise = this.element.play();
|
|
698
|
+
if (playPromise !== undefined) {
|
|
699
|
+
playPromise.catch(error => {
|
|
700
|
+
console.error('WRPlayer: Play error', error);
|
|
701
|
+
this._dispatchEvent('error', { type: 'play_failed', details: error });
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Pause the video
|
|
709
|
+
*/
|
|
710
|
+
pause() {
|
|
711
|
+
const type = this.options.type.toLowerCase();
|
|
712
|
+
|
|
713
|
+
if (type !== 'webrtc') {
|
|
714
|
+
this.element.pause();
|
|
715
|
+
}
|
|
716
|
+
// WebRTC streams typically can't be paused
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Stop and close the player
|
|
721
|
+
*/
|
|
722
|
+
stop() {
|
|
723
|
+
// 检查 this.options.type 是否存在
|
|
724
|
+
const type = this.options.type ? this.options.type.toLowerCase() : '';
|
|
725
|
+
|
|
726
|
+
if (type === 'webrtc') {
|
|
727
|
+
if (this.player) {
|
|
728
|
+
this.player.close();
|
|
729
|
+
}
|
|
730
|
+
// 重置 WebRTC 状态
|
|
731
|
+
this.webrtcState = {
|
|
732
|
+
isConnecting: false,
|
|
733
|
+
isConnected: false,
|
|
734
|
+
hasStartedReceiving: false,
|
|
735
|
+
isRetryWithVideoOnly: false,
|
|
736
|
+
retryCount: 0
|
|
737
|
+
};
|
|
738
|
+
} else if (type === 'hls' && typeof Hls !== 'undefined' && this.player instanceof Hls) {
|
|
739
|
+
this.player.destroy();
|
|
740
|
+
} else if (type === 'dash' && typeof dashjs !== 'undefined') {
|
|
741
|
+
this.player.destroy();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Clean up PTZ controller
|
|
745
|
+
if (this.ptz) {
|
|
746
|
+
this.ptz.destroy();
|
|
747
|
+
this.ptz = null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Clean up Voice Intercom controller
|
|
751
|
+
if (this.voiceIntercom) {
|
|
752
|
+
this.voiceIntercom.destroy();
|
|
753
|
+
this.voiceIntercom = null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Clean up the video element
|
|
757
|
+
if (this.element) {
|
|
758
|
+
this.element.src = '';
|
|
759
|
+
this.element.srcObject = null;
|
|
760
|
+
this.element.load();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* 回退策略:使用仅视频模式重试连接
|
|
766
|
+
* @private
|
|
767
|
+
*/
|
|
768
|
+
_retryWithVideoOnly() {
|
|
769
|
+
console.log('WebRTC: 开始回退策略,停止当前连接...');
|
|
770
|
+
|
|
771
|
+
// 增加重试计数
|
|
772
|
+
this.webrtcState.retryCount++;
|
|
773
|
+
this.webrtcState.isRetryWithVideoOnly = true;
|
|
774
|
+
|
|
775
|
+
// 停止当前播放器
|
|
776
|
+
if (this.player) {
|
|
777
|
+
this.player.close();
|
|
778
|
+
this.player = null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 重置连接状态但保留重试标志
|
|
782
|
+
this.webrtcState.isConnecting = false;
|
|
783
|
+
this.webrtcState.isConnected = false;
|
|
784
|
+
this.webrtcState.hasStartedReceiving = false;
|
|
785
|
+
|
|
786
|
+
// 延迟重新初始化,确保资源完全释放
|
|
787
|
+
setTimeout(() => {
|
|
788
|
+
try {
|
|
789
|
+
console.log('WebRTC: 重新初始化播放器(仅视频模式)...');
|
|
790
|
+
this._initWebRTCPlayer();
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error('WebRTC: 回退策略失败:', error);
|
|
793
|
+
this._dispatchEvent('error', {
|
|
794
|
+
type: 'retry_failed',
|
|
795
|
+
details: { originalError: error, retryMode: 'video_only' }
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}, 1000);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* 处理 WebRTC 错误并尝试回退策略
|
|
803
|
+
* @private
|
|
804
|
+
*/
|
|
805
|
+
_handleWebRTCError(errorInfo) {
|
|
806
|
+
console.log('WebRTC: 处理错误:', errorInfo);
|
|
807
|
+
|
|
808
|
+
const errorString = JSON.stringify(errorInfo.details || errorInfo);
|
|
809
|
+
const isSdpError = errorString.includes('setRemoteDescription') ||
|
|
810
|
+
errorString.includes('m-lines') ||
|
|
811
|
+
errorString.includes('demuxing') ||
|
|
812
|
+
errorInfo.type === 'connection_timeout';
|
|
813
|
+
|
|
814
|
+
if (isSdpError && !this.webrtcState.isRetryWithVideoOnly && this.webrtcState.retryCount < 2) {
|
|
815
|
+
console.log('WebRTC: 检测到 SDP 相关错误,尝试回退到仅视频模式');
|
|
816
|
+
this._retryWithVideoOnly();
|
|
817
|
+
} else {
|
|
818
|
+
// 重置连接状态
|
|
819
|
+
this.webrtcState.isConnecting = false;
|
|
820
|
+
this._dispatchEvent('error', {
|
|
821
|
+
type: errorInfo.type || 'webrtc_error',
|
|
822
|
+
details: errorInfo.details
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Register an event handler
|
|
829
|
+
* @param {string} event - Event name
|
|
830
|
+
* @param {Function} callback - Event handler function
|
|
831
|
+
*/
|
|
832
|
+
on(event, callback) {
|
|
833
|
+
if (!this.eventHandlers[event]) {
|
|
834
|
+
this.eventHandlers[event] = [];
|
|
835
|
+
}
|
|
836
|
+
this.eventHandlers[event].push(callback);
|
|
837
|
+
return this;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Remove an event handler
|
|
842
|
+
* @param {string} event - Event name
|
|
843
|
+
* @param {Function} callback - Event handler function to remove
|
|
844
|
+
*/
|
|
845
|
+
off(event, callback) {
|
|
846
|
+
if (!this.eventHandlers[event]) return this;
|
|
847
|
+
|
|
848
|
+
if (callback) {
|
|
849
|
+
const index = this.eventHandlers[event].indexOf(callback);
|
|
850
|
+
if (index !== -1) {
|
|
851
|
+
this.eventHandlers[event].splice(index, 1);
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
this.eventHandlers[event] = [];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return this;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Dispatch an event to registered handlers
|
|
862
|
+
* @private
|
|
863
|
+
* @param {string} event - Event name
|
|
864
|
+
* @param {Object} data - Event data
|
|
865
|
+
*/
|
|
866
|
+
_dispatchEvent(event, data = {}) {
|
|
867
|
+
if (this.options.debug) {
|
|
868
|
+
console.log(`WRPlayer Event: ${event}`, data);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (this.eventHandlers[event]) {
|
|
872
|
+
this.eventHandlers[event].forEach(handler => {
|
|
873
|
+
handler(data);
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Get the current player instance
|
|
880
|
+
* @returns {Object} The underlying player instance
|
|
881
|
+
*/
|
|
882
|
+
getPlayer() {
|
|
883
|
+
return this.player;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Check if player supports a given media type
|
|
888
|
+
* @param {string} type - Media type to check
|
|
889
|
+
* @returns {boolean} Whether the type is supported
|
|
890
|
+
*/
|
|
891
|
+
static isSupported(type) {
|
|
892
|
+
// Check if we're in a browser environment
|
|
893
|
+
if (typeof window === 'undefined') {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
switch (type.toLowerCase()) {
|
|
898
|
+
case 'webrtc':
|
|
899
|
+
// Since ZLMRTCClient is now a direct import, we assume it's supported if this code runs.
|
|
900
|
+
return true;
|
|
901
|
+
case 'hls':
|
|
902
|
+
return (typeof Hls !== 'undefined' && Hls.isSupported()) ||
|
|
903
|
+
document.createElement('video').canPlayType('application/vnd.apple.mpegurl');
|
|
904
|
+
case 'dash':
|
|
905
|
+
return typeof dashjs !== 'undefined';
|
|
906
|
+
case 'mp4':
|
|
907
|
+
return document.createElement('video').canPlayType('video/mp4') !== '';
|
|
908
|
+
case 'webm':
|
|
909
|
+
return document.createElement('video').canPlayType('video/webm') !== '';
|
|
910
|
+
default:
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Export to make available in different module systems
|
|
917
|
+
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
|
918
|
+
module.exports = WRPlayer;
|
|
919
|
+
} else if (typeof window !== 'undefined') {
|
|
920
|
+
window.WRPlayer = WRPlayer;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Add ESM export for modern bundlers
|
|
924
|
+
export default WRPlayer;
|