确保播放第一帧
This commit is contained in:
		
							parent
							
								
									b600114ed9
								
							
						
					
					
						commit
						a27112d6cb
					
				
							
								
								
									
										144
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								src/index.js
									
									
									
									
									
								
							| @ -25,6 +25,10 @@ class WebRTCChat { | |||||||
|         this.videoStreams = new Map(); // 存储不同视频的MediaStream
 |         this.videoStreams = new Map(); // 存储不同视频的MediaStream
 | ||||||
|         this.currentVideoStream = null; |         this.currentVideoStream = null; | ||||||
| 
 | 
 | ||||||
|  |         // 添加视频相关属性
 | ||||||
|  |         this.videoSender = null; // WebRTC视频发送器
 | ||||||
|  |         this.currentVideoStream = null; // 当前视频流
 | ||||||
|  |          | ||||||
|         // 初始化音频处理器
 |         // 初始化音频处理器
 | ||||||
|         console.log('开始初始化音频处理器'); |         console.log('开始初始化音频处理器'); | ||||||
|         // 初始化音频处理器
 |         // 初始化音频处理器
 | ||||||
| @ -289,11 +293,8 @@ class WebRTCChat { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async createVideoStream(videoFile) { |     async createVideoStream(videoFile) { | ||||||
|         // 如果已经缓存了这个视频流,直接返回
 |         // 检查缓存,但为每个视频创建独立的播放实例
 | ||||||
|         if (this.videoStreams.has(videoFile)) { |         const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
 | ||||||
|             this.logMessage(`使用缓存的视频流: ${videoFile}`, 'info'); |  | ||||||
|             return this.videoStreams.get(videoFile); |  | ||||||
|         } |  | ||||||
|          |          | ||||||
|         try { |         try { | ||||||
|             this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); |             this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); | ||||||
| @ -306,26 +307,25 @@ class WebRTCChat { | |||||||
|             video.src = `/videos/${videoFile}`; |             video.src = `/videos/${videoFile}`; | ||||||
|             video.muted = true; |             video.muted = true; | ||||||
|             video.loop = true; |             video.loop = true; | ||||||
|             video.autoplay = true; |             video.autoplay = false; // 手动控制播放
 | ||||||
|             video.crossOrigin = 'anonymous'; // 添加跨域支持
 |             video.crossOrigin = 'anonymous'; | ||||||
|             video.playsInline = true; // 添加playsInline属性
 |             video.playsInline = true; | ||||||
|  |              | ||||||
|  |             // 预加载视频但不播放
 | ||||||
|  |             video.preload = 'auto'; | ||||||
|              |              | ||||||
|             // 等待视频加载完成
 |             // 等待视频加载完成
 | ||||||
|             await new Promise((resolve, reject) => { |             await new Promise((resolve, reject) => { | ||||||
|                 video.onloadedmetadata = () => { |                 video.onloadeddata = () => { | ||||||
|                     this.logMessage(`视频元数据加载完成: ${videoFile}`, 'info'); |                     this.logMessage(`视频数据加载完成: ${videoFile}`, 'info'); | ||||||
|  |                     // 确保从第一帧开始
 | ||||||
|  |                     video.currentTime = 0; | ||||||
|                     resolve(); |                     resolve(); | ||||||
|                 }; |                 }; | ||||||
|                 video.onerror = (error) => { |                 video.onerror = (error) => { | ||||||
|                     this.logMessage(`视频加载失败: ${videoFile}`, 'error'); |                     this.logMessage(`视频加载失败: ${videoFile}`, 'error'); | ||||||
|                     reject(error); |                     reject(error); | ||||||
|                 }; |                 }; | ||||||
|                 video.onloadstart = () => { |  | ||||||
|                     this.logMessage(`开始加载视频: ${videoFile}`, 'info'); |  | ||||||
|                 }; |  | ||||||
|                 video.oncanplay = () => { |  | ||||||
|                     this.logMessage(`视频可以播放: ${videoFile}`, 'info'); |  | ||||||
|                 }; |  | ||||||
|             }); |             }); | ||||||
|              |              | ||||||
|             // 创建MediaStream
 |             // 创建MediaStream
 | ||||||
| @ -338,22 +338,27 @@ class WebRTCChat { | |||||||
|              |              | ||||||
|             this.logMessage(`Canvas尺寸: ${canvas.width}x${canvas.height}`, 'info'); |             this.logMessage(`Canvas尺寸: ${canvas.width}x${canvas.height}`, 'info'); | ||||||
|              |              | ||||||
|  |             // 先绘制第一帧到canvas(避免黑屏)
 | ||||||
|  |             if (video.readyState >= video.HAVE_CURRENT_DATA) { | ||||||
|  |                 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||||||
|  |                 this.logMessage('已绘制第一帧到Canvas', 'info'); | ||||||
|  |             } | ||||||
|  |              | ||||||
|             // 开始播放视频
 |             // 开始播放视频
 | ||||||
|             try { |             try { | ||||||
|                 await video.play(); |                 await video.play(); | ||||||
|                 this.logMessage(`视频开始播放: ${videoFile}`, 'info'); |                 this.logMessage(`视频开始播放: ${videoFile}`, 'info'); | ||||||
|             } catch (playError) { |             } catch (playError) { | ||||||
|                 this.logMessage(`视频播放失败: ${playError.message}`, 'error'); |                 this.logMessage(`视频播放失败: ${playError.message}`, 'error'); | ||||||
|                 // 即使播放失败也继续创建流
 |  | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 等待视频开始播放
 |             // 等待视频真正开始播放
 | ||||||
|             await new Promise(resolve => { |             await new Promise(resolve => { | ||||||
|                 const checkPlay = () => { |                 const checkPlay = () => { | ||||||
|                     if (video.readyState >= video.HAVE_CURRENT_DATA) { |                     if (video.readyState >= video.HAVE_CURRENT_DATA && !video.paused) { | ||||||
|                         resolve(); |                         resolve(); | ||||||
|                     } else { |                     } else { | ||||||
|                         setTimeout(checkPlay, 100); |                         setTimeout(checkPlay, 50); | ||||||
|                     } |                     } | ||||||
|                 }; |                 }; | ||||||
|                 checkPlay(); |                 checkPlay(); | ||||||
| @ -364,8 +369,7 @@ class WebRTCChat { | |||||||
|             let isDrawing = false; |             let isDrawing = false; | ||||||
|             const drawFrame = () => { |             const drawFrame = () => { | ||||||
|                 const now = performance.now(); |                 const now = performance.now(); | ||||||
|                 // 限制绘制频率,确保平滑过渡
 |                 if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { | ||||||
|                 if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { // 约60fps
 |  | ||||||
|                     isDrawing = true; |                     isDrawing = true; | ||||||
|                     lastDrawTime = now; |                     lastDrawTime = now; | ||||||
|                     ctx.drawImage(video, 0, 0, canvas.width, canvas.height); |                     ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||||||
| @ -378,16 +382,25 @@ class WebRTCChat { | |||||||
|             drawFrame(); |             drawFrame(); | ||||||
|              |              | ||||||
|             // 从canvas创建MediaStream
 |             // 从canvas创建MediaStream
 | ||||||
|             const stream = canvas.captureStream(30); // 30fps
 |             const stream = canvas.captureStream(30); | ||||||
|              |              | ||||||
|             // 等待流创建完成并稳定
 |             // 等待流稳定
 | ||||||
|             await new Promise(resolve => { |             await new Promise(resolve => { | ||||||
|                 setTimeout(resolve, 500); // 给更多时间让流稳定
 |                 setTimeout(resolve, 200); // 减少等待时间
 | ||||||
|             }); |             }); | ||||||
|              |              | ||||||
|             this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); |             this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); | ||||||
|              |              | ||||||
|             // 缓存这个视频流
 |             // 使用有限缓存策略(最多缓存3个视频流)
 | ||||||
|  |             if (this.videoStreams.size >= 3) { | ||||||
|  |                 const firstKey = this.videoStreams.keys().next().value; | ||||||
|  |                 const oldStream = this.videoStreams.get(firstKey); | ||||||
|  |                 if (oldStream) { | ||||||
|  |                     oldStream.getTracks().forEach(track => track.stop()); | ||||||
|  |                 } | ||||||
|  |                 this.videoStreams.delete(firstKey); | ||||||
|  |             } | ||||||
|  |              | ||||||
|             this.videoStreams.set(videoFile, stream); |             this.videoStreams.set(videoFile, stream); | ||||||
|              |              | ||||||
|             return stream; |             return stream; | ||||||
| @ -504,6 +517,68 @@ class WebRTCChat { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // 使用replaceTrack方式切换视频
 | ||||||
|  |     async switchVideoWithReplaceTrack(videoFile, type = '', text = '') { | ||||||
|  |         try { | ||||||
|  |             this.logMessage(`开始使用replaceTrack切换视频: ${videoFile}`, 'info'); | ||||||
|  |              | ||||||
|  |             // 创建新的视频流
 | ||||||
|  |             const newVideoStream = await this.createVideoStream(videoFile); | ||||||
|  |             const newVideoTrack = newVideoStream.getVideoTracks()[0]; | ||||||
|  |              | ||||||
|  |             if (!newVideoTrack) { | ||||||
|  |                 throw new Error('新视频流中没有视频轨道'); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 如果有WebRTC连接且有视频发送器,使用replaceTrack
 | ||||||
|  |             if (this.peerConnection && this.videoSender) { | ||||||
|  |                 await this.videoSender.replaceTrack(newVideoTrack); | ||||||
|  |                 this.logMessage('WebRTC视频轨道替换成功', 'success'); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 同时更新本地视频显示
 | ||||||
|  |             if (this.recordedVideo) { | ||||||
|  |                 // 停止当前视频流
 | ||||||
|  |                 if (this.currentVideoStream) { | ||||||
|  |                     this.currentVideoStream.getTracks().forEach(track => track.stop()); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // 设置新的视频流
 | ||||||
|  |                 this.recordedVideo.srcObject = newVideoStream; | ||||||
|  |                 this.currentVideoStream = newVideoStream; | ||||||
|  |                  | ||||||
|  |                 // 确保视频播放
 | ||||||
|  |                 try { | ||||||
|  |                     await this.recordedVideo.play(); | ||||||
|  |                     this.logMessage(`本地视频切换成功: ${videoFile}`, 'success'); | ||||||
|  |                 } catch (playError) { | ||||||
|  |                     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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     bindEvents() { |     bindEvents() { | ||||||
|         // 开始通话按钮
 |         // 开始通话按钮
 | ||||||
|         this.startButton.onclick = () => this.startCall(); |         this.startButton.onclick = () => this.startCall(); | ||||||
| @ -542,7 +617,7 @@ class WebRTCChat { | |||||||
|                 audio: true |                 audio: true | ||||||
|             }); |             }); | ||||||
|              |              | ||||||
|             this.createPeerConnection(); |             await this.createPeerConnection(); | ||||||
|              |              | ||||||
|             this.startButton.disabled = true; |             this.startButton.disabled = true; | ||||||
|             this.stopButton.disabled = false; |             this.stopButton.disabled = false; | ||||||
| @ -593,7 +668,7 @@ class WebRTCChat { | |||||||
|         this.logMessage('音频通话已结束', 'info'); |         this.logMessage('音频通话已结束', 'info'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     createPeerConnection() { |     async createPeerConnection() { | ||||||
|         const configuration = { |         const configuration = { | ||||||
|             iceServers: [ |             iceServers: [ | ||||||
|                 { urls: "stun:stun.qq.com:3478" }, |                 { urls: "stun:stun.qq.com:3478" }, | ||||||
| @ -610,6 +685,19 @@ class WebRTCChat { | |||||||
|             this.peerConnection.addTrack(track, this.localStream); |             this.peerConnection.addTrack(track, this.localStream); | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
|  |         // 添加初始视频流(默认视频)
 | ||||||
|  |         try { | ||||||
|  |             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) => { | ||||||
|             this.remoteVideo.srcObject = event.streams[0]; |             this.remoteVideo.srcObject = event.streams[0]; | ||||||
|  | |||||||
| @ -70,7 +70,7 @@ async function processAudioQueue() { | |||||||
|       if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { |       if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { | ||||||
|         try { |         try { | ||||||
|           console.log('--------------触发视频切换:', sayName); |           console.log('--------------触发视频切换:', sayName); | ||||||
|           await window.webrtcApp.switchVideoStream(targetVideo, 'audio', 'say-5s-m-sw'); |           await window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', 'say-5s-m-sw'); | ||||||
|           isFirstChunk = false; |           isFirstChunk = false; | ||||||
|           window.webrtcApp.currentVideoTag = sayName; |           window.webrtcApp.currentVideoTag = sayName; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -90,7 +90,7 @@ async function processAudioQueue() { | |||||||
|   if (window.webrtcApp.currentVideoTag != text) { |   if (window.webrtcApp.currentVideoTag != text) { | ||||||
|      |      | ||||||
|     window.webrtcApp.currentVideoTag = text |     window.webrtcApp.currentVideoTag = text | ||||||
|     await window.webrtcApp.switchVideoStream(window.webrtcApp.defaultVideo, 'audio', text); |     await window.webrtcApp.switchVideoWithReplaceTrack(window.webrtcApp.defaultVideo, 'audio', text); | ||||||
|   } |   } | ||||||
|   console.log('音频队列处理完成'); |   console.log('音频队列处理完成'); | ||||||
| } | } | ||||||
|  | |||||||
| @ -446,3 +446,47 @@ header p { | |||||||
| #recordedVideo.playing { | #recordedVideo.playing { | ||||||
|     opacity: 1; |     opacity: 1; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #recordedVideo { | ||||||
|  |     transition: opacity 0.2s ease-in-out; | ||||||
|  |     background-color: #1a1a1a; /* 深灰色背景,避免纯黑 */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #recordedVideo.loading { | ||||||
|  |     opacity: 0.8; /* 加载时稍微降低透明度,但不完全隐藏 */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #recordedVideo.playing { | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* 添加加载指示器 */ | ||||||
|  | .video-container { | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .video-container::before { | ||||||
|  |     content: ''; | ||||||
|  |     position: absolute; | ||||||
|  |     top: 50%; | ||||||
|  |     left: 50%; | ||||||
|  |     width: 40px; | ||||||
|  |     height: 40px; | ||||||
|  |     margin: -20px 0 0 -20px; | ||||||
|  |     border: 3px solid #333; | ||||||
|  |     border-top: 3px solid #fff; | ||||||
|  |     border-radius: 50%; | ||||||
|  |     animation: spin 1s linear infinite; | ||||||
|  |     opacity: 0; | ||||||
|  |     z-index: 10; | ||||||
|  |     transition: opacity 0.3s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .video-container.loading::before { | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @keyframes spin { | ||||||
|  |     0% { transform: rotate(0deg); } | ||||||
|  |     100% { transform: rotate(360deg); } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user