使用视频帧保证切换问题
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m49s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m49s
				
			This commit is contained in:
		
							parent
							
								
									5f1e50c7e9
								
							
						
					
					
						commit
						35c7e82375
					
				
							
								
								
									
										177
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								src/index.js
									
									
									
									
									
								
							| @ -648,24 +648,67 @@ class WebRTCChat { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // 添加视频帧缓存方法
 | ||||||
|  |     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.zIndex = '10'; | ||||||
|  |         overlay.style.pointerEvents = 'none'; | ||||||
|  |         overlay.id = 'video-frame-overlay'; | ||||||
|  |          | ||||||
|  |         return overlay; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // 新增平滑视频切换方法
 |     // 新增平滑视频切换方法
 | ||||||
|     async switchVideoStreamSmooth(videoFile, type = '', text = '') { |     async switchVideoStreamSmooth(videoFile, type = '', text = '') { | ||||||
|         try { |         try { | ||||||
|             this.logMessage(`开始平滑切换视频流: ${videoFile} (${type})`, 'info'); |             this.logMessage(`开始无闪烁切换视频流: ${videoFile} (${type})`, 'info'); | ||||||
|              |              | ||||||
|             // 检查是否已缓存,如果已缓存则不显示加载指示器
 |             // 1. 捕获当前视频的最后一帧
 | ||||||
|  |             const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer; | ||||||
|  |             const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo; | ||||||
|  |              | ||||||
|  |             let frameData = null; | ||||||
|  |             if (currentVideo.readyState >= 2 && currentVideo.currentTime > 0) { | ||||||
|  |                 frameData = this.captureVideoFrame(currentVideo); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 2. 创建并显示静态帧覆盖层
 | ||||||
|  |             let frameOverlay = null; | ||||||
|  |             if (frameData) { | ||||||
|  |                 frameOverlay = this.createFrameOverlay(frameData); | ||||||
|  |                 currentVideo.parentElement.appendChild(frameOverlay); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 3. 检查是否已缓存
 | ||||||
|             const isCached = this.videoStreams.has(videoFile); |             const isCached = this.videoStreams.has(videoFile); | ||||||
|             if (!isCached) { |             if (!isCached) { | ||||||
|                 this.showVideoLoading(); |                 this.showVideoLoading(); | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 确定当前活跃的视频元素和缓冲元素
 |             // 4. 准备新视频流
 | ||||||
|             const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer; |  | ||||||
|             const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo; |  | ||||||
|              |  | ||||||
|             // 检查是否已缓存
 |  | ||||||
|             let newStream; |             let newStream; | ||||||
|              |  | ||||||
|             if (isCached) { |             if (isCached) { | ||||||
|                 const cachedStream = this.videoStreams.get(videoFile); |                 const cachedStream = this.videoStreams.get(videoFile); | ||||||
|                 if (cachedStream && cachedStream.getTracks().length > 0) { |                 if (cachedStream && cachedStream.getTracks().length > 0) { | ||||||
| @ -678,36 +721,43 @@ class WebRTCChat { | |||||||
|                 newStream = await this.createVideoStream(videoFile); |                 newStream = await this.createVideoStream(videoFile); | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 在缓冲视频元素中预加载新视频
 |             // 5. 在缓冲视频元素中预加载新视频
 | ||||||
|             bufferVideo.srcObject = newStream; |             bufferVideo.srcObject = newStream; | ||||||
|              |              | ||||||
|             // 等待缓冲视频准备就绪
 |             // 6. 等待新视频完全准备好(至少渲染一帧)
 | ||||||
|             await new Promise((resolve, reject) => { |             await new Promise((resolve, reject) => { | ||||||
|                 const timeout = setTimeout(() => { |                 const timeout = setTimeout(() => { | ||||||
|                     reject(new Error('视频加载超时')); |                     reject(new Error('视频加载超时')); | ||||||
|                 }, 5000); |                 }, 5000); | ||||||
|                  |                  | ||||||
|                 const onCanPlay = () => { |                 const onReady = () => { | ||||||
|                     clearTimeout(timeout); |                     clearTimeout(timeout); | ||||||
|                     bufferVideo.removeEventListener('canplay', onCanPlay); |                     bufferVideo.removeEventListener('canplay', onCanPlay); | ||||||
|                     bufferVideo.removeEventListener('error', onError); |  | ||||||
|                      |  | ||||||
|                     // 确保视频渲染了至少一帧
 |  | ||||||
|                     if (bufferVideo.readyState >= 2) { |  | ||||||
|                         resolve(); |  | ||||||
|                     } else { |  | ||||||
|                         // 等待第一帧渲染
 |  | ||||||
|                         const onTimeUpdate = () => { |  | ||||||
|                     bufferVideo.removeEventListener('timeupdate', onTimeUpdate); |                     bufferVideo.removeEventListener('timeupdate', onTimeUpdate); | ||||||
|  |                     bufferVideo.removeEventListener('error', onError); | ||||||
|                     resolve(); |                     resolve(); | ||||||
|                 }; |                 }; | ||||||
|                         bufferVideo.addEventListener('timeupdate', onTimeUpdate, { once: true }); |                  | ||||||
|  |                 const onCanPlay = () => { | ||||||
|  |                     // 开始播放新视频
 | ||||||
|  |                     bufferVideo.play().then(() => { | ||||||
|  |                         // 等待至少一帧渲染
 | ||||||
|  |                         if (bufferVideo.currentTime > 0) { | ||||||
|  |                             onReady(); | ||||||
|  |                         } | ||||||
|  |                     }).catch(reject); | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 const onTimeUpdate = () => { | ||||||
|  |                     if (bufferVideo.currentTime > 0) { | ||||||
|  |                         onReady(); | ||||||
|                     } |                     } | ||||||
|                 }; |                 }; | ||||||
|                  |                  | ||||||
|                 const onError = (error) => { |                 const onError = (error) => { | ||||||
|                     clearTimeout(timeout); |                     clearTimeout(timeout); | ||||||
|                     bufferVideo.removeEventListener('canplay', onCanPlay); |                     bufferVideo.removeEventListener('canplay', onCanPlay); | ||||||
|  |                     bufferVideo.removeEventListener('timeupdate', onTimeUpdate); | ||||||
|                     bufferVideo.removeEventListener('error', onError); |                     bufferVideo.removeEventListener('error', onError); | ||||||
|                     reject(error); |                     reject(error); | ||||||
|                 }; |                 }; | ||||||
| @ -716,27 +766,31 @@ class WebRTCChat { | |||||||
|                     onCanPlay(); |                     onCanPlay(); | ||||||
|                 } else { |                 } else { | ||||||
|                     bufferVideo.addEventListener('canplay', onCanPlay); |                     bufferVideo.addEventListener('canplay', onCanPlay); | ||||||
|  |                     bufferVideo.addEventListener('timeupdate', onTimeUpdate); | ||||||
|                     bufferVideo.addEventListener('error', onError); |                     bufferVideo.addEventListener('error', onError); | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 // 开始播放缓冲视频
 |  | ||||||
|                 bufferVideo.play().catch(onError); |  | ||||||
|             }); |             }); | ||||||
|              |              | ||||||
|             // 只有显示了加载指示器才隐藏
 |             // 7. 立即切换视频显示(无过渡)
 | ||||||
|  |             bufferVideo.style.zIndex = '2'; | ||||||
|  |             currentVideo.style.zIndex = '1'; | ||||||
|  |              | ||||||
|  |             // 8. 移除静态帧覆盖层
 | ||||||
|  |             if (frameOverlay) { | ||||||
|  |                 frameOverlay.remove(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 9. 隐藏加载指示器
 | ||||||
|             if (!isCached) { |             if (!isCached) { | ||||||
|                 this.hideVideoLoading(); |                 this.hideVideoLoading(); | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 执行淡入淡出切换
 |             // 10. 更新状态
 | ||||||
|             // await this.performVideoTransition(currentVideo, bufferVideo);
 |  | ||||||
|              |  | ||||||
|             // 更新当前视频流和活跃元素
 |  | ||||||
|             this.currentVideoStream = newStream; |             this.currentVideoStream = newStream; | ||||||
|             this.currentVideo = videoFile; |             this.currentVideo = videoFile; | ||||||
|             this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main'; |             this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main'; | ||||||
|              |              | ||||||
|             // 停止旧视频流(延迟停止避免闪烁)
 |             // 11. 延迟清理旧视频流
 | ||||||
|             setTimeout(() => { |             setTimeout(() => { | ||||||
|                 if (currentVideo.srcObject && currentVideo.srcObject !== newStream) { |                 if (currentVideo.srcObject && currentVideo.srcObject !== newStream) { | ||||||
|                     currentVideo.srcObject.getTracks().forEach(track => track.stop()); |                     currentVideo.srcObject.getTracks().forEach(track => track.stop()); | ||||||
| @ -744,7 +798,7 @@ class WebRTCChat { | |||||||
|                 } |                 } | ||||||
|             }, 1000); |             }, 1000); | ||||||
|              |              | ||||||
|             // 更新WebRTC连接中的视频轨道
 |             // 12. 更新WebRTC连接
 | ||||||
|             if (this.peerConnection && this.videoSender) { |             if (this.peerConnection && this.videoSender) { | ||||||
|                 const newVideoTrack = newStream.getVideoTracks()[0]; |                 const newVideoTrack = newStream.getVideoTracks()[0]; | ||||||
|                 if (newVideoTrack) { |                 if (newVideoTrack) { | ||||||
| @ -752,7 +806,7 @@ class WebRTCChat { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 更新显示信息
 |             // 13. 更新显示信息
 | ||||||
|             if (text) { |             if (text) { | ||||||
|                 this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`; |                 this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`; | ||||||
|                 this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success'); |                 this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success'); | ||||||
| @ -762,11 +816,17 @@ class WebRTCChat { | |||||||
|             } |             } | ||||||
|              |              | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             this.logMessage(`平滑切换视频流失败: ${error.message}`, 'error'); |             this.logMessage(`视频流切换失败: ${error.message}`, 'error'); | ||||||
|             // 确保隐藏加载指示器
 |              | ||||||
|  |             // 清理可能残留的覆盖层
 | ||||||
|  |             const overlay = document.getElementById('video-frame-overlay'); | ||||||
|  |             if (overlay) { | ||||||
|  |                 overlay.remove(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|             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'); | ||||||
| @ -774,56 +834,7 @@ class WebRTCChat { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 执行视频过渡动画
 |  | ||||||
|     async performVideoTransition(currentVideo, bufferVideo) { |  | ||||||
|         return new Promise((resolve) => { |  | ||||||
|             // 确保初始状态正确
 |  | ||||||
|             bufferVideo.style.opacity = '0'; |  | ||||||
|             bufferVideo.style.zIndex = '3'; |  | ||||||
|             currentVideo.style.opacity = '1'; |  | ||||||
|             currentVideo.style.zIndex = '2'; |  | ||||||
| 
 | 
 | ||||||
|             // 设置过渡属性(如果还没有)
 |  | ||||||
|             if (!bufferVideo.style.transition) { |  | ||||||
|                 bufferVideo.style.transition = 'opacity 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)'; |  | ||||||
|             } |  | ||||||
|             if (!currentVideo.style.transition) { |  | ||||||
|                 currentVideo.style.transition = 'opacity 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)'; |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // 强制重绘以确保样式生效
 |  | ||||||
|             bufferVideo.offsetHeight; |  | ||||||
|              |  | ||||||
|             // 开始过渡
 |  | ||||||
|             requestAnimationFrame(() => { |  | ||||||
|                 bufferVideo.style.opacity = '1'; |  | ||||||
|                 currentVideo.style.opacity = '0'; |  | ||||||
|             }); |  | ||||||
|              |  | ||||||
|             // 监听过渡完成
 |  | ||||||
|             const onTransitionEnd = (e) => { |  | ||||||
|                 if (e.target === bufferVideo && e.propertyName === 'opacity') { |  | ||||||
|                     bufferVideo.removeEventListener('transitionend', onTransitionEnd); |  | ||||||
|                      |  | ||||||
|                     // 重置z-index
 |  | ||||||
|                     currentVideo.style.zIndex = '1'; |  | ||||||
|                     bufferVideo.style.zIndex = '2'; |  | ||||||
|                      |  | ||||||
|                     resolve(); |  | ||||||
|                 } |  | ||||||
|             }; |  | ||||||
|              |  | ||||||
|             bufferVideo.addEventListener('transitionend', onTransitionEnd); |  | ||||||
|              |  | ||||||
|             // 备用超时机制
 |  | ||||||
|             setTimeout(() => { |  | ||||||
|                 bufferVideo.removeEventListener('transitionend', onTransitionEnd); |  | ||||||
|                 currentVideo.style.zIndex = '1'; |  | ||||||
|                 bufferVideo.style.zIndex = '2'; |  | ||||||
|                 resolve(); |  | ||||||
|             }, 400); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // 显示加载指示器
 |     // 显示加载指示器
 | ||||||
|     showVideoLoading() { |     showVideoLoading() { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Song367
						Song367