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.
@@ -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;