启用webrtc ,喝咖啡不说话视频去水印
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3m57s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3m57s
				
			This commit is contained in:
		
							parent
							
								
									4e256170b0
								
							
						
					
					
						commit
						1d0911e74a
					
				| @ -136,7 +136,7 @@ const scenes = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '喝茶', |     name: '喝茶', | ||||||
|     defaultVideo: '8-5-hc-bd-female.mp4', |     defaultVideo: '8-8-hc-bd-2.mp4', | ||||||
|     interactionVideo: '8-5-hc-sh-female.mp4', |     interactionVideo: '8-5-hc-sh-female.mp4', | ||||||
|     tag: 'tea', |     tag: 'tea', | ||||||
|     apiKey: 'bot-20250805140055-ccdr6' // 喝茶场景的API key
 |     apiKey: 'bot-20250805140055-ccdr6' // 喝茶场景的API key
 | ||||||
|  | |||||||
							
								
								
									
										623
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										623
									
								
								src/index.js
									
									
									
									
									
								
							| @ -27,6 +27,9 @@ class WebRTCChat { | |||||||
|         this.currentVideo = null; |         this.currentVideo = null; | ||||||
|         this.videoStreams = new Map(); // 存储不同视频的MediaStream
 |         this.videoStreams = new Map(); // 存储不同视频的MediaStream
 | ||||||
|         this.currentVideoStream = null; |         this.currentVideoStream = null; | ||||||
|  |         this.precreatedStreams = new Map(); // 预创建的视频流
 | ||||||
|  |         this.importantVideos = ['d-3s.mp4', 's-1.mp4']; // 重要视频列表
 | ||||||
|  |         this.isInitialized = false; | ||||||
| 
 | 
 | ||||||
|         // 添加视频相关属性
 |         // 添加视频相关属性
 | ||||||
|         this.videoSender = null; // WebRTC视频发送器
 |         this.videoSender = null; // WebRTC视频发送器
 | ||||||
| @ -75,6 +78,13 @@ class WebRTCChat { | |||||||
|             }); |             }); | ||||||
|         }, 500); // 延迟2秒开始预加载,避免影响
 |         }, 500); // 延迟2秒开始预加载,避免影响
 | ||||||
| 
 | 
 | ||||||
|  |         // 预创建重要视频流
 | ||||||
|  |         setTimeout(() => { | ||||||
|  |             this.precreateImportantVideos().catch(error => { | ||||||
|  |                 this.logMessage(`预创建重要视频流失败: ${error.message}`, 'error'); | ||||||
|  |             }); | ||||||
|  |         }, 1000); | ||||||
|  | 
 | ||||||
|         window.webrtcApp = this; |         window.webrtcApp = this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -331,6 +341,116 @@ class WebRTCChat { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // 预创建重要视频流
 | ||||||
|  |     async precreateImportantVideos() { | ||||||
|  |         if (this.isInitialized) return; | ||||||
|  |          | ||||||
|  |         this.logMessage('开始预创建重要视频流...', 'info'); | ||||||
|  |          | ||||||
|  |         for (const videoFile of [this.interactionVideo, this.defaultVideo]) { | ||||||
|  |             try { | ||||||
|  |                 const stream = await this.createVideoStreamOptimized(videoFile); | ||||||
|  |                 this.precreatedStreams.set(videoFile, stream); | ||||||
|  |                 this.logMessage(`预创建视频流成功: ${videoFile}`, 'success'); | ||||||
|  |             } catch (error) { | ||||||
|  |                 this.logMessage(`预创建视频流失败: ${videoFile} - ${error.message}`, 'error'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         this.isInitialized = true; | ||||||
|  |         this.logMessage('重要视频流预创建完成', 'success'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 优化的视频流创建方法
 | ||||||
|  |     async createVideoStreamOptimized(videoFile) { | ||||||
|  |         try { | ||||||
|  |             // 先测试视频文件是否存在
 | ||||||
|  |             await this.testVideoFile(videoFile); | ||||||
|  |              | ||||||
|  |             // 创建video元素
 | ||||||
|  |             const video = document.createElement('video'); | ||||||
|  |             video.src = `/videos/${videoFile}`; | ||||||
|  |             video.muted = true; | ||||||
|  |             video.loop = true; | ||||||
|  |             video.autoplay = false; | ||||||
|  |             video.crossOrigin = 'anonymous'; | ||||||
|  |             video.playsInline = true; | ||||||
|  |             video.preload = 'auto'; | ||||||
|  |              | ||||||
|  |             // 等待视频加载
 | ||||||
|  |             await new Promise((resolve, reject) => { | ||||||
|  |                 video.onloadeddata = () => { | ||||||
|  |                     video.currentTime = 0; | ||||||
|  |                     resolve(); | ||||||
|  |                 }; | ||||||
|  |                 video.onerror = reject; | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 创建优化的Canvas
 | ||||||
|  |             const canvas = document.createElement('canvas'); | ||||||
|  |             const ctx = canvas.getContext('2d', {  | ||||||
|  |                 alpha: false, // 禁用透明度以提高性能
 | ||||||
|  |                 willReadFrequently: false // 优化Canvas性能
 | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             canvas.width = video.videoWidth || 640; | ||||||
|  |             canvas.height = video.videoHeight || 480; | ||||||
|  |              | ||||||
|  |             // 绘制第一帧
 | ||||||
|  |             await new Promise((resolve) => { | ||||||
|  |                 const drawFirstFrame = () => { | ||||||
|  |                     if (video.readyState >= video.HAVE_CURRENT_DATA) { | ||||||
|  |                         ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||||||
|  |                         resolve(); | ||||||
|  |                     } else { | ||||||
|  |                         setTimeout(drawFirstFrame, 10); | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 drawFirstFrame(); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 启动视频播放
 | ||||||
|  |             await video.play(); | ||||||
|  |              | ||||||
|  |             // 优化的帧绘制 - 使用更高效的节流
 | ||||||
|  |             let animationId; | ||||||
|  |             let lastFrameTime = 0; | ||||||
|  |             const targetFPS = 30; | ||||||
|  |             const frameInterval = 1000 / targetFPS; | ||||||
|  |              | ||||||
|  |             const drawFrame = (currentTime) => { | ||||||
|  |                 if (currentTime - lastFrameTime >= frameInterval) { | ||||||
|  |                     if (video.readyState >= video.HAVE_CURRENT_DATA) { | ||||||
|  |                         ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||||||
|  |                     } | ||||||
|  |                     lastFrameTime = currentTime; | ||||||
|  |                 } | ||||||
|  |                 animationId = requestAnimationFrame(drawFrame); | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             animationId = requestAnimationFrame(drawFrame); | ||||||
|  |              | ||||||
|  |             // 创建MediaStream
 | ||||||
|  |             const stream = canvas.captureStream(30); | ||||||
|  |              | ||||||
|  |             // 存储清理函数
 | ||||||
|  |             stream._cleanup = () => { | ||||||
|  |                 if (animationId) { | ||||||
|  |                     cancelAnimationFrame(animationId); | ||||||
|  |                 } | ||||||
|  |                 video.pause(); | ||||||
|  |                 video.src = ''; | ||||||
|  |                 video.load(); | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             return stream; | ||||||
|  |              | ||||||
|  |         } catch (error) { | ||||||
|  |             this.logMessage(`优化视频流创建失败 ${videoFile}: ${error.message}`, 'error'); | ||||||
|  |             throw error; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async createVideoStream(videoFile) { |     async createVideoStream(videoFile) { | ||||||
|         // 检查缓存,但为每个视频创建独立的播放实例
 |         // 检查缓存,但为每个视频创建独立的播放实例
 | ||||||
|         const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
 |         const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
 | ||||||
| @ -580,160 +700,50 @@ class WebRTCChat { | |||||||
|     //     }
 |     //     }
 | ||||||
|     // }
 |     // }
 | ||||||
| 
 | 
 | ||||||
|     // 修改原有的switchVideoStream方法,使用新的平滑切换
 |     // 修改视频切换方法,直接使用预加载视频切换,不使用WebRTC传输
 | ||||||
|     async switchVideoStream(videoFile, type = '', text = '') { |     async switchVideoStream(videoFile, type = '', text = '') { | ||||||
|         // 使用平滑切换方法
 |  | ||||||
|         return await this.switchVideoStreamSmooth(videoFile, type, text); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 使用replaceTrack方式切换视频
 |  | ||||||
|     async switchVideoWithReplaceTrack(videoFile, type = '', text = '') { |  | ||||||
|         try { |         try { | ||||||
|             this.logMessage(`开始使用replaceTrack切换视频: ${videoFile}`, 'info'); |             this.logMessage(`开始切换视频: ${videoFile} (${type})`, 'info'); | ||||||
|              |              | ||||||
|             // 创建新的视频流
 |             let newVideoStream; | ||||||
|             const newVideoStream = await this.createVideoStream(videoFile); |             let isUsingPrecreated = false; | ||||||
|             const newVideoTrack = newVideoStream.getVideoTracks()[0]; |  | ||||||
|              |              | ||||||
|             if (!newVideoTrack) { |             // 首先检查预创建的视频流
 | ||||||
|                 throw new Error('新视频流中没有视频轨道'); |             if (this.precreatedStreams.has(videoFile)) { | ||||||
|  |                 newVideoStream = this.precreatedStreams.get(videoFile); | ||||||
|  |                 isUsingPrecreated = true; | ||||||
|  |                 this.logMessage(`使用预创建视频流: ${videoFile}`, 'success'); | ||||||
|  |             } else { | ||||||
|  |                 // 检查缓存
 | ||||||
|  |                 if (this.videoStreams.has(videoFile)) { | ||||||
|  |                     newVideoStream = this.videoStreams.get(videoFile); | ||||||
|  |                     this.logMessage(`使用缓存视频流: ${videoFile}`, 'success'); | ||||||
|  |                 } else { | ||||||
|  |                     // 创建新的视频流
 | ||||||
|  |                     newVideoStream = await this.createVideoStream(videoFile); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 如果有WebRTC连接且有视频发送器,使用replaceTrack
 |             // 直接切换本地视频显示,不使用WebRTC传输
 | ||||||
|             if (this.peerConnection && this.videoSender) { |  | ||||||
|                 await this.videoSender.replaceTrack(newVideoTrack); |  | ||||||
|                 this.logMessage('WebRTC视频轨道替换成功', 'success'); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 同时更新本地视频显示
 |  | ||||||
|             if (this.recordedVideo) { |             if (this.recordedVideo) { | ||||||
|                 // 停止当前视频流
 |                 // 停止当前视频流(但不停止预创建的流)
 | ||||||
|                 if (this.currentVideoStream) { |                 if (this.currentVideoStream && !this.precreatedStreams.has(this.currentVideo)) { | ||||||
|                     this.currentVideoStream.getTracks().forEach(track => track.stop()); |                     this.currentVideoStream.getTracks().forEach(track => track.stop()); | ||||||
|                 } |                 } | ||||||
|                  |                  | ||||||
|                 // 设置新的视频流
 |  | ||||||
|                 this.recordedVideo.srcObject = newVideoStream; |                 this.recordedVideo.srcObject = newVideoStream; | ||||||
|                 this.currentVideoStream = newVideoStream; |                 this.currentVideoStream = newVideoStream; | ||||||
|  |                 this.currentVideo = videoFile; | ||||||
|  |                  | ||||||
|  |                 // 使用预创建流时减少等待时间
 | ||||||
|  |                 const waitTime = isUsingPrecreated ? 10 : 50; | ||||||
|  |                 await new Promise(resolve => setTimeout(resolve, waitTime)); | ||||||
|                  |                  | ||||||
|                 // 确保视频播放
 |  | ||||||
|                 try { |                 try { | ||||||
|                     await this.recordedVideo.play(); |                     await this.recordedVideo.play(); | ||||||
|                     this.logMessage(`本地视频切换成功: ${videoFile}`, 'success'); |                     this.logMessage(`视频切换完成: ${videoFile} (${type})`, 'success'); | ||||||
|                 } catch (playError) { |                 } catch (playError) { | ||||||
|                     this.logMessage(`本地视频播放失败: ${playError.message}`, 'error'); |                     this.logMessage(`视频播放失败: ${playError.message}`, 'error'); | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 记录切换信息
 |  | ||||||
|             if (type && text) { |  | ||||||
|                 this.logMessage(`视频切换完成 - 类型: ${type}, 文本: ${text}`, 'info'); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             return true; |  | ||||||
|              |  | ||||||
|         } catch (error) { |  | ||||||
|             this.logMessage(`replaceTrack视频切换失败: ${error.message}`, 'error'); |  | ||||||
|             console.error('switchVideoWithReplaceTrack error:', error); |  | ||||||
|              |  | ||||||
|             // 回退到原有的切换方式
 |  | ||||||
|             try { |  | ||||||
|                 await this.switchVideoStream(videoFile, type, text); |  | ||||||
|                 this.logMessage('已回退到原有视频切换方式', 'info'); |  | ||||||
|             } catch (fallbackError) { |  | ||||||
|                 this.logMessage(`回退切换也失败: ${fallbackError.message}`, 'error'); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 新增平滑视频切换方法
 |  | ||||||
|     async switchVideoStreamSmooth(videoFile, type = '', text = '') { |  | ||||||
|         try { |  | ||||||
|             this.logMessage(`开始平滑切换视频流: ${videoFile} (${type})`, 'info'); |  | ||||||
|              |  | ||||||
|             // 检查是否已缓存,如果已缓存则不显示加载指示器
 |  | ||||||
|             const isCached = this.videoStreams.has(videoFile); |  | ||||||
|             if (!isCached) { |  | ||||||
|                 this.showVideoLoading(); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 确定当前活跃的视频元素和缓冲元素
 |  | ||||||
|             const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer; |  | ||||||
|             const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo; |  | ||||||
|              |  | ||||||
|             // 检查是否已缓存
 |  | ||||||
|             let newStream; |  | ||||||
|              |  | ||||||
|             if (isCached) { |  | ||||||
|                 const cachedStream = this.videoStreams.get(videoFile); |  | ||||||
|                 if (cachedStream && cachedStream.getTracks().length > 0) { |  | ||||||
|                     newStream = cachedStream; |  | ||||||
|                     this.logMessage(`使用缓存视频流: ${videoFile}`, 'success'); |  | ||||||
|                 } else { |  | ||||||
|                     newStream = await this.createVideoStream(videoFile); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 newStream = await this.createVideoStream(videoFile); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 在缓冲视频元素中预加载新视频
 |  | ||||||
|             bufferVideo.srcObject = newStream; |  | ||||||
|              |  | ||||||
|             // 等待缓冲视频准备就绪
 |  | ||||||
|             await new Promise((resolve, reject) => { |  | ||||||
|                 const timeout = setTimeout(() => { |  | ||||||
|                     reject(new Error('视频加载超时')); |  | ||||||
|                 }, 5000); |  | ||||||
|                  |  | ||||||
|                 const onCanPlay = () => { |  | ||||||
|                     clearTimeout(timeout); |  | ||||||
|                     bufferVideo.removeEventListener('canplay', onCanPlay); |  | ||||||
|                     bufferVideo.removeEventListener('error', onError); |  | ||||||
|                     resolve(); |  | ||||||
|                 }; |  | ||||||
|                  |  | ||||||
|                 const onError = (error) => { |  | ||||||
|                     clearTimeout(timeout); |  | ||||||
|                     bufferVideo.removeEventListener('canplay', onCanPlay); |  | ||||||
|                     bufferVideo.removeEventListener('error', onError); |  | ||||||
|                     reject(error); |  | ||||||
|                 }; |  | ||||||
|                  |  | ||||||
|                 bufferVideo.addEventListener('canplay', onCanPlay); |  | ||||||
|                 bufferVideo.addEventListener('error', onError); |  | ||||||
|                  |  | ||||||
|                 // 开始播放缓冲视频
 |  | ||||||
|                 bufferVideo.play().catch(onError); |  | ||||||
|             }); |  | ||||||
|              |  | ||||||
|             // 只有显示了加载指示器才隐藏
 |  | ||||||
|             if (!isCached) { |  | ||||||
|                 this.hideVideoLoading(); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 执行淡入淡出切换
 |  | ||||||
|             // await this.performVideoTransition(currentVideo, bufferVideo);
 |  | ||||||
|              |  | ||||||
|             // 更新当前视频流和活跃元素
 |  | ||||||
|             this.currentVideoStream = newStream; |  | ||||||
|             this.currentVideo = videoFile; |  | ||||||
|             this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main'; |  | ||||||
|              |  | ||||||
|             // 停止旧视频流(延迟停止避免闪烁)
 |  | ||||||
|             setTimeout(() => { |  | ||||||
|                 if (currentVideo.srcObject && currentVideo.srcObject !== newStream) { |  | ||||||
|                     currentVideo.srcObject.getTracks().forEach(track => track.stop()); |  | ||||||
|                     currentVideo.srcObject = null; |  | ||||||
|                 } |  | ||||||
|             }, 1000); |  | ||||||
|              |  | ||||||
|             // 更新WebRTC连接中的视频轨道
 |  | ||||||
|             if (this.peerConnection && this.videoSender) { |  | ||||||
|                 const newVideoTrack = newStream.getVideoTracks()[0]; |  | ||||||
|                 if (newVideoTrack) { |  | ||||||
|                     await this.videoSender.replaceTrack(newVideoTrack); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |              | ||||||
| @ -746,12 +756,241 @@ class WebRTCChat { | |||||||
|                 this.logMessage(`成功切换到视频流: ${videoFile}`, 'success'); |                 this.logMessage(`成功切换到视频流: ${videoFile}`, 'success'); | ||||||
|             } |             } | ||||||
|              |              | ||||||
|  |             return true; | ||||||
|  |              | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             this.logMessage(`平滑切换视频流失败: ${error.message}`, 'error'); |             this.logMessage(`视频切换失败: ${error.message}`, 'error'); | ||||||
|             // 确保隐藏加载指示器
 |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 添加视频帧缓存方法
 | ||||||
|  |     captureVideoFrame(videoElement) { | ||||||
|  |         const canvas = document.createElement('canvas'); | ||||||
|  |         const ctx = canvas.getContext('2d'); | ||||||
|  |          | ||||||
|  |         canvas.width = videoElement.videoWidth || videoElement.clientWidth; | ||||||
|  |         canvas.height = videoElement.videoHeight || videoElement.clientHeight; | ||||||
|  |          | ||||||
|  |         // 绘制当前视频帧到canvas
 | ||||||
|  |         ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); | ||||||
|  |          | ||||||
|  |         return canvas.toDataURL('image/jpeg', 0.8); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 创建静态帧显示元素
 | ||||||
|  |     createFrameOverlay(frameData) { | ||||||
|  |         const overlay = document.createElement('div'); | ||||||
|  |         overlay.style.position = 'absolute'; | ||||||
|  |         overlay.style.top = '0'; | ||||||
|  |         overlay.style.left = '0'; | ||||||
|  |         overlay.style.width = '100%'; | ||||||
|  |         overlay.style.height = '100%'; | ||||||
|  |         overlay.style.backgroundImage = `url(${frameData})`; | ||||||
|  |         overlay.style.backgroundSize = 'cover'; | ||||||
|  |         overlay.style.backgroundPosition = 'center'; | ||||||
|  |         overlay.style.backgroundRepeat = 'no-repeat'; | ||||||
|  |         overlay.style.zIndex = '10'; | ||||||
|  |         overlay.style.pointerEvents = 'none'; | ||||||
|  |         overlay.style.willChange = 'auto'; // 优化渲染性能
 | ||||||
|  |         overlay.id = 'video-frame-overlay'; | ||||||
|  |          | ||||||
|  |         return overlay; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 新增平滑视频切换方法 - 等待完全传输后切换
 | ||||||
|  |     async switchVideoStreamSmooth(videoFile, type = '', text = '') { | ||||||
|  |         try { | ||||||
|  |             this.logMessage(`开始等待视频流传输完成: ${videoFile} (${type})`, 'info'); | ||||||
|  |              | ||||||
|  |             // 1. 获取当前和缓冲视频元素
 | ||||||
|  |             const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer; | ||||||
|  |             const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo; | ||||||
|  |              | ||||||
|  |             // 2. 检查是否已缓存
 | ||||||
|  |             const isCached = this.videoStreams.has(videoFile); | ||||||
|  |             if (!isCached) { | ||||||
|  |                 this.showVideoLoading(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 3. 准备新视频流
 | ||||||
|  |             let newStream; | ||||||
|  |             if (isCached) { | ||||||
|  |                 const cachedStream = this.videoStreams.get(videoFile); | ||||||
|  |                 if (cachedStream && cachedStream.getTracks().length > 0) { | ||||||
|  |                     newStream = cachedStream; | ||||||
|  |                     this.logMessage(`使用缓存视频流: ${videoFile}`, 'success'); | ||||||
|  |                 } else { | ||||||
|  |                     newStream = await this.createVideoStream(videoFile); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 newStream = await this.createVideoStream(videoFile); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 4. 在缓冲视频元素中预加载新视频
 | ||||||
|  |             bufferVideo.srcObject = newStream; | ||||||
|  |              | ||||||
|  |             // 5. 等待视频流完全传输和准备就绪
 | ||||||
|  |             await new Promise((resolve, reject) => { | ||||||
|  |                 const timeout = setTimeout(() => { | ||||||
|  |                     reject(new Error('视频流传输超时')); | ||||||
|  |                 }, 15000); // 增加超时时间到15秒,确保有足够时间传输
 | ||||||
|  |                  | ||||||
|  |                 let frameCount = 0; | ||||||
|  |                 const minFrames = 10; // 增加最小帧数确保传输完整
 | ||||||
|  |                 let isStreamReady = false; | ||||||
|  |                  | ||||||
|  |                 const onReady = () => { | ||||||
|  |                     clearTimeout(timeout); | ||||||
|  |                     bufferVideo.removeEventListener('canplay', onCanPlay); | ||||||
|  |                     bufferVideo.removeEventListener('canplaythrough', onCanPlayThrough); | ||||||
|  |                     bufferVideo.removeEventListener('timeupdate', onTimeUpdate); | ||||||
|  |                     bufferVideo.removeEventListener('error', onError); | ||||||
|  |                     bufferVideo.removeEventListener('loadeddata', onLoadedData); | ||||||
|  |                     resolve(); | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 // 检查视频数据是否完全加载
 | ||||||
|  |                 const onLoadedData = () => { | ||||||
|  |                     this.logMessage(`视频数据开始加载: ${videoFile}`, 'info'); | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 // 检查视频是否可以流畅播放(数据传输充足)
 | ||||||
|  |                 const onCanPlayThrough = () => { | ||||||
|  |                     this.logMessage(`视频流传输充足,可以流畅播放: ${videoFile}`, 'success'); | ||||||
|  |                     isStreamReady = true; | ||||||
|  |                      | ||||||
|  |                     // 开始播放并等待稳定帧
 | ||||||
|  |                     bufferVideo.play().then(() => { | ||||||
|  |                         const checkFrameStability = () => { | ||||||
|  |                             if (bufferVideo.currentTime > 0) { | ||||||
|  |                                 frameCount++; | ||||||
|  |                                 if (frameCount >= minFrames && isStreamReady) { | ||||||
|  |                                     // 额外等待确保传输稳定
 | ||||||
|  |                                     setTimeout(() => { | ||||||
|  |                                         onReady(); | ||||||
|  |                                     }, 200); // 等待200ms确保传输稳定
 | ||||||
|  |                                 } else { | ||||||
|  |                                     requestAnimationFrame(checkFrameStability); | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 requestAnimationFrame(checkFrameStability); | ||||||
|  |                             } | ||||||
|  |                         }; | ||||||
|  |                         requestAnimationFrame(checkFrameStability); | ||||||
|  |                     }).catch(reject); | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 // 基本播放准备就绪
 | ||||||
|  |                 const onCanPlay = () => { | ||||||
|  |                     if (!isStreamReady) { | ||||||
|  |                         this.logMessage(`视频基本准备就绪,等待完全传输: ${videoFile}`, 'info'); | ||||||
|  |                         // 如果还没有canplaythrough事件,继续等待
 | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 // 时间更新监听(备用检查)
 | ||||||
|  |                 const onTimeUpdate = () => { | ||||||
|  |                     if (bufferVideo.currentTime > 0 && isStreamReady) { | ||||||
|  |                         frameCount++; | ||||||
|  |                         if (frameCount >= minFrames) { | ||||||
|  |                             setTimeout(() => { | ||||||
|  |                                 onReady(); | ||||||
|  |                             }, 200); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 const onError = (error) => { | ||||||
|  |                     clearTimeout(timeout); | ||||||
|  |                     bufferVideo.removeEventListener('canplay', onCanPlay); | ||||||
|  |                     bufferVideo.removeEventListener('canplaythrough', onCanPlayThrough); | ||||||
|  |                     bufferVideo.removeEventListener('timeupdate', onTimeUpdate); | ||||||
|  |                     bufferVideo.removeEventListener('error', onError); | ||||||
|  |                     bufferVideo.removeEventListener('loadeddata', onLoadedData); | ||||||
|  |                     reject(error); | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 // 检查当前状态
 | ||||||
|  |                 if (bufferVideo.readyState >= 4) { // HAVE_ENOUGH_DATA
 | ||||||
|  |                     onCanPlayThrough(); | ||||||
|  |                 } else { | ||||||
|  |                     bufferVideo.addEventListener('loadeddata', onLoadedData); | ||||||
|  |                     bufferVideo.addEventListener('canplay', onCanPlay); | ||||||
|  |                     bufferVideo.addEventListener('canplaythrough', onCanPlayThrough); | ||||||
|  |                     bufferVideo.addEventListener('timeupdate', onTimeUpdate); | ||||||
|  |                     bufferVideo.addEventListener('error', onError); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 6. 最终确认视频流完全准备就绪
 | ||||||
|  |             await new Promise(resolve => { | ||||||
|  |                 let confirmCount = 0; | ||||||
|  |                 const maxConfirms = 3; | ||||||
|  |                  | ||||||
|  |                 const finalCheck = () => { | ||||||
|  |                     if (bufferVideo.readyState >= 4 && bufferVideo.currentTime > 0) { | ||||||
|  |                         confirmCount++; | ||||||
|  |                         if (confirmCount >= maxConfirms) { | ||||||
|  |                             this.logMessage(`视频流传输完成,准备切换: ${videoFile}`, 'success'); | ||||||
|  |                             resolve(); | ||||||
|  |                         } else { | ||||||
|  |                             setTimeout(finalCheck, 50); // 每50ms检查一次
 | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         setTimeout(finalCheck, 50); | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 finalCheck(); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 7. 执行无缝切换(当前视频继续播放直到新视频完全准备好)
 | ||||||
|  |             bufferVideo.style.zIndex = '2'; | ||||||
|  |             currentVideo.style.zIndex = '1'; | ||||||
|  |              | ||||||
|  |             this.logMessage(`视频切换完成: ${videoFile}`, 'success'); | ||||||
|  |              | ||||||
|  |             // 8. 隐藏加载指示器
 | ||||||
|  |             if (!isCached) { | ||||||
|  |                 this.hideVideoLoading(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 9. 更新状态
 | ||||||
|  |             this.currentVideoStream = newStream; | ||||||
|  |             this.currentVideo = videoFile; | ||||||
|  |             this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main'; | ||||||
|  |              | ||||||
|  |             // 10. 延迟清理旧视频流
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 if (currentVideo.srcObject && currentVideo.srcObject !== newStream) { | ||||||
|  |                     currentVideo.srcObject.getTracks().forEach(track => track.stop()); | ||||||
|  |                     currentVideo.srcObject = null; | ||||||
|  |                 } | ||||||
|  |             }, 1000); | ||||||
|  |              | ||||||
|  |             // 11. 更新WebRTC连接
 | ||||||
|  |             if (this.peerConnection && this.videoSender) { | ||||||
|  |                 const newVideoTrack = newStream.getVideoTracks()[0]; | ||||||
|  |                 if (newVideoTrack) { | ||||||
|  |                     await this.videoSender.replaceTrack(newVideoTrack); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 12. 更新显示信息
 | ||||||
|  |             if (text) { | ||||||
|  |                 this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`; | ||||||
|  |                 this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success'); | ||||||
|  |             } else { | ||||||
|  |                 this.currentVideoName.textContent = `视频流: ${videoFile}`; | ||||||
|  |                 this.logMessage(`成功切换到视频流: ${videoFile}`, 'success'); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |         } catch (error) { | ||||||
|  |             this.logMessage(`视频流切换失败: ${error.message}`, 'error'); | ||||||
|  |              | ||||||
|             this.hideVideoLoading(); |             this.hideVideoLoading(); | ||||||
|              |              | ||||||
|             // 如果切换失败,尝试回到默认视频
 |             // 回退到默认视频
 | ||||||
|             if (videoFile !== this.defaultVideo) { |             if (videoFile !== this.defaultVideo) { | ||||||
|                 this.logMessage('尝试回到默认视频', 'info'); |                 this.logMessage('尝试回到默认视频', 'info'); | ||||||
|                 await this.switchVideoStreamSmooth(this.defaultVideo, 'fallback'); |                 await this.switchVideoStreamSmooth(this.defaultVideo, 'fallback'); | ||||||
| @ -759,32 +998,7 @@ class WebRTCChat { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 执行视频过渡动画
 |  | ||||||
|     async performVideoTransition(currentVideo, bufferVideo) { |  | ||||||
|         return new Promise((resolve) => { |  | ||||||
|             // 添加切换类
 |  | ||||||
|             currentVideo.classList.add('switching'); |  | ||||||
|             bufferVideo.classList.add('switching'); |  | ||||||
| 
 | 
 | ||||||
|             // 等待CSS过渡完成
 |  | ||||||
|             setTimeout(() => { |  | ||||||
|                 // 移除切换类
 |  | ||||||
|                 currentVideo.classList.remove('switching'); |  | ||||||
|                 bufferVideo.classList.remove('switching'); |  | ||||||
|                  |  | ||||||
|                 // 交换z-index
 |  | ||||||
|                 if (currentVideo.style.zIndex === '2' || !currentVideo.style.zIndex) { |  | ||||||
|                     currentVideo.style.zIndex = '1'; |  | ||||||
|                     bufferVideo.style.zIndex = '2'; |  | ||||||
|                 } else { |  | ||||||
|                     currentVideo.style.zIndex = '2'; |  | ||||||
|                     bufferVideo.style.zIndex = '1'; |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 resolve(); |  | ||||||
|             }, 500); // 与CSS过渡时间一致
 |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // 显示加载指示器
 |     // 显示加载指示器
 | ||||||
|     showVideoLoading() { |     showVideoLoading() { | ||||||
| @ -804,8 +1018,8 @@ class WebRTCChat { | |||||||
|         // 开始通话按钮
 |         // 开始通话按钮
 | ||||||
|         this.startButton.onclick = () => this.startCall(); |         this.startButton.onclick = () => this.startCall(); | ||||||
|          |          | ||||||
|         // 停止通话按钮
 |         // 停止通话按钮 - 改为调用 userDisconnect
 | ||||||
|         this.stopButton.onclick = () => this.stopCall(); |         this.stopButton.onclick = () => this.userDisconnect(); | ||||||
|          |          | ||||||
|         // 静音按钮
 |         // 静音按钮
 | ||||||
|         // this.muteButton.onclick = () => this.toggleMute();
 |         // this.muteButton.onclick = () => this.toggleMute();
 | ||||||
| @ -855,12 +1069,10 @@ class WebRTCChat { | |||||||
| 
 | 
 | ||||||
|     async startCall() { |     async startCall() { | ||||||
|         try { |         try { | ||||||
|             // 显示等待连接提示
 |  | ||||||
|             this.showConnectionWaiting(); |  | ||||||
| 
 |  | ||||||
|             // 立即隐藏开始通话按钮
 |             // 立即隐藏开始通话按钮
 | ||||||
|             this.startButton.style.display = 'none'; |             this.startButton.style.display = 'none'; | ||||||
|              |             // 显示等待连接提示
 | ||||||
|  |             this.showConnectionWaiting(); | ||||||
|             // 切换到通话中图标
 |             // 切换到通话中图标
 | ||||||
|             this.switchToCallingIcon(); |             this.switchToCallingIcon(); | ||||||
|              |              | ||||||
| @ -900,7 +1112,7 @@ class WebRTCChat { | |||||||
|             this.socket.emit('call-started'); |             this.socket.emit('call-started'); | ||||||
|              |              | ||||||
|             // 开始播放当前场景的默认视频
 |             // 开始播放当前场景的默认视频
 | ||||||
|             await this.startDefaultVideoStream(); |             await this.precreateImportantVideos(); | ||||||
| 
 | 
 | ||||||
|             // 隐藏等待连接提示
 |             // 隐藏等待连接提示
 | ||||||
|             this.hideConnectionWaiting(); |             this.hideConnectionWaiting(); | ||||||
| @ -915,16 +1127,16 @@ class WebRTCChat { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     stopCall() { |     stopCall() { | ||||||
| 
 |  | ||||||
|         // 隐藏等待连接提示
 |         // 隐藏等待连接提示
 | ||||||
|         this.hideConnectionWaiting(); |         this.hideConnectionWaiting(); | ||||||
|         // 恢复到默认图标
 |         // 恢复到默认图标
 | ||||||
|         this.switchToDefaultIcon(); |         this.switchToDefaultIcon(); | ||||||
|          |          | ||||||
|         // 发送用户关闭连接事件到后端
 |         // 只有用户主动点击关闭通话时才发送断开事件
 | ||||||
|         if (this.socket && this.socket.connected) { |         // 移除自动发送 user-disconnect 事件
 | ||||||
|             this.socket.emit('user-disconnect'); |         // if (this.socket && this.socket.connected) {
 | ||||||
|         } |         //     this.socket.emit('user-disconnect');
 | ||||||
|  |         // }
 | ||||||
|          |          | ||||||
|         // 停止音频处理器
 |         // 停止音频处理器
 | ||||||
|         if (this.audioProcessor) { |         if (this.audioProcessor) { | ||||||
| @ -948,24 +1160,37 @@ class WebRTCChat { | |||||||
|         this.stopButton.style.display = 'none'; |         this.stopButton.style.display = 'none'; | ||||||
|         this.stopButton.disabled = true; |         this.stopButton.disabled = true; | ||||||
|          |          | ||||||
|         // // 显示头像,隐藏视频
 |         // 显示开始通话按钮
 | ||||||
|         // if (this.videoContainer) {
 |         this.startButton.style.display = 'block'; | ||||||
|         //     this.videoContainer.classList.remove('calling');
 |         this.startButton.disabled = false; | ||||||
|         // }
 |          | ||||||
|  |         // 移除页面刷新,保持websocket连接
 | ||||||
|  |         // setTimeout(() => {
 | ||||||
|  |         //     window.location.reload();
 | ||||||
|  |         // }, 300);
 | ||||||
|  |          | ||||||
|  |         setTimeout(() => { | ||||||
|  |             // 显示头像,隐藏视频
 | ||||||
|  |             if (this.videoContainer) { | ||||||
|  |                 this.videoContainer.classList.remove('calling'); | ||||||
|  |             } | ||||||
|  |         }, 300); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 新增:用户主动断开连接的方法
 | ||||||
|  |     userDisconnect() { | ||||||
|  |         // 发送用户关闭连接事件到后端
 | ||||||
|  |         if (this.socket && this.socket.connected) { | ||||||
|  |             this.socket.emit('user-disconnect'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 调用停止通话
 | ||||||
|  |         this.stopCall(); | ||||||
|          |          | ||||||
|         // 延迟刷新,确保服务器处理完断开逻辑
 |         // 延迟刷新,确保服务器处理完断开逻辑
 | ||||||
|         console.log('通话已结束,5秒后刷新页面清除缓存...'); |         console.log('用户主动断开,300ms后刷新页面清除缓存...'); | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|             window.location.reload(); |             window.location.reload(); | ||||||
|             this.startButton.style.display = 'block'; |  | ||||||
|             this.startButton.disabled = false; |  | ||||||
|         }, 2000); |  | ||||||
| 
 |  | ||||||
|         setTimeout(() => { |  | ||||||
|         // 显示头像,隐藏视频
 |  | ||||||
|                 if (this.videoContainer) { |  | ||||||
|                     this.videoContainer.classList.remove('calling'); |  | ||||||
|                 } |  | ||||||
|         }, 300); |         }, 300); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -1036,18 +1261,8 @@ class WebRTCChat { | |||||||
|             this.peerConnection.addTrack(track, this.localStream); |             this.peerConnection.addTrack(track, this.localStream); | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
|         // 添加初始视频流(默认视频)
 |         // 移除视频轨道添加逻辑,只使用音频进行WebRTC通信
 | ||||||
|         try { |         // 视频将直接在本地切换,不通过WebRTC传输
 | ||||||
|             const initialVideoStream = await this.createVideoStream(this.defaultVideo); |  | ||||||
|             const videoTrack = initialVideoStream.getVideoTracks()[0]; |  | ||||||
|             if (videoTrack) { |  | ||||||
|                 this.videoSender = this.peerConnection.addTrack(videoTrack, initialVideoStream); |  | ||||||
|                 this.currentVideoStream = initialVideoStream; |  | ||||||
|                 this.logMessage('初始视频轨道已添加到WebRTC连接', 'success'); |  | ||||||
|             } |  | ||||||
|         } catch (error) { |  | ||||||
|             this.logMessage(`添加初始视频轨道失败: ${error.message}`, 'error'); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         // 处理远程流
 |         // 处理远程流
 | ||||||
|         this.peerConnection.ontrack = (event) => { |         this.peerConnection.ontrack = (event) => { | ||||||
|  | |||||||
| @ -68,10 +68,10 @@ async function processAudioQueue() { | |||||||
|       const sayName = '8-4-sh' |       const sayName = '8-4-sh' | ||||||
|       const targetVideo = window.webrtcApp.interactionVideo |       const targetVideo = window.webrtcApp.interactionVideo | ||||||
|       // 如果是第一个音频片段,触发视频切换
 |       // 如果是第一个音频片段,触发视频切换
 | ||||||
|       if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoWithReplaceTrack) { |       if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoStream) { | ||||||
|         try { |         try { | ||||||
|           console.log('--------------触发视频切换:', sayName); |           console.log('--------------触发视频切换:', sayName); | ||||||
|           window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', '8-4-sh'); |           window.webrtcApp.switchVideoStream(targetVideo, 'audio', '8-4-sh'); | ||||||
|           isFirstChunk = false; |           isFirstChunk = false; | ||||||
|           window.webrtcApp.currentVideoTag = sayName; |           window.webrtcApp.currentVideoTag = sayName; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -86,13 +86,18 @@ async function processAudioQueue() { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   isProcessingQueue = false; |   isProcessingQueue = false; | ||||||
|  |    | ||||||
|  |   // 等待当前音频播放完成后再切换回默认视频
 | ||||||
|  |   while (isPlaying) { | ||||||
|  |     await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   const text = 'default' |   const text = 'default' | ||||||
|   // await new Promise(resolve => setTimeout(resolve, 500));
 |  | ||||||
|   console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying) |   console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying) | ||||||
|   if (window.webrtcApp.currentVideoTag != text) { |   if (window.webrtcApp.currentVideoTag != text) { | ||||||
|     isFirstChunk = true |     isFirstChunk = true | ||||||
|     window.webrtcApp.currentVideoTag = text |     window.webrtcApp.currentVideoTag = text | ||||||
|     window.webrtcApp.switchVideoWithReplaceTrack(window.webrtcApp.defaultVideo, 'audio', text); |     window.webrtcApp.switchVideoStream(window.webrtcApp.defaultVideo, 'audio', text); | ||||||
|   } |   } | ||||||
|   console.log('音频队列处理完成'); |   console.log('音频队列处理完成'); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								videos/8-8-hc-bd-2.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								videos/8-8-hc-bd-2.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user