From 35c7e82375b26109e3dce3128481f36648504395 Mon Sep 17 00:00:00 2001 From: Song367 <601337784@qq.com> Date: Wed, 6 Aug 2025 19:23:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=A7=86=E9=A2=91=E5=B8=A7?= =?UTF-8?q?=E4=BF=9D=E8=AF=81=E5=88=87=E6=8D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 183 +++++++++++++++++++++++++++------------------------ 1 file changed, 97 insertions(+), 86 deletions(-) diff --git a/src/index.js b/src/index.js index c925e12..a346d0d 100644 --- a/src/index.js +++ b/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 = '') { 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); if (!isCached) { this.showVideoLoading(); } - // 确定当前活跃的视频元素和缓冲元素 - const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer; - const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo; - - // 检查是否已缓存 + // 4. 准备新视频流 let newStream; - if (isCached) { const cachedStream = this.videoStreams.get(videoFile); if (cachedStream && cachedStream.getTracks().length > 0) { @@ -678,36 +721,43 @@ class WebRTCChat { newStream = await this.createVideoStream(videoFile); } - // 在缓冲视频元素中预加载新视频 + // 5. 在缓冲视频元素中预加载新视频 bufferVideo.srcObject = newStream; - // 等待缓冲视频准备就绪 + // 6. 等待新视频完全准备好(至少渲染一帧) await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('视频加载超时')); }, 5000); - const onCanPlay = () => { + const onReady = () => { clearTimeout(timeout); bufferVideo.removeEventListener('canplay', onCanPlay); + bufferVideo.removeEventListener('timeupdate', onTimeUpdate); bufferVideo.removeEventListener('error', onError); - - // 确保视频渲染了至少一帧 - if (bufferVideo.readyState >= 2) { - resolve(); - } else { - // 等待第一帧渲染 - const onTimeUpdate = () => { - bufferVideo.removeEventListener('timeupdate', onTimeUpdate); - resolve(); - }; - bufferVideo.addEventListener('timeupdate', onTimeUpdate, { once: true }); + resolve(); + }; + + const onCanPlay = () => { + // 开始播放新视频 + bufferVideo.play().then(() => { + // 等待至少一帧渲染 + if (bufferVideo.currentTime > 0) { + onReady(); + } + }).catch(reject); + }; + + const onTimeUpdate = () => { + if (bufferVideo.currentTime > 0) { + onReady(); } }; const onError = (error) => { clearTimeout(timeout); bufferVideo.removeEventListener('canplay', onCanPlay); + bufferVideo.removeEventListener('timeupdate', onTimeUpdate); bufferVideo.removeEventListener('error', onError); reject(error); }; @@ -716,27 +766,31 @@ class WebRTCChat { onCanPlay(); } else { bufferVideo.addEventListener('canplay', onCanPlay); + bufferVideo.addEventListener('timeupdate', onTimeUpdate); 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) { this.hideVideoLoading(); } - // 执行淡入淡出切换 - // await this.performVideoTransition(currentVideo, bufferVideo); - - // 更新当前视频流和活跃元素 + // 10. 更新状态 this.currentVideoStream = newStream; this.currentVideo = videoFile; this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main'; - // 停止旧视频流(延迟停止避免闪烁) + // 11. 延迟清理旧视频流 setTimeout(() => { if (currentVideo.srcObject && currentVideo.srcObject !== newStream) { currentVideo.srcObject.getTracks().forEach(track => track.stop()); @@ -744,7 +798,7 @@ class WebRTCChat { } }, 1000); - // 更新WebRTC连接中的视频轨道 + // 12. 更新WebRTC连接 if (this.peerConnection && this.videoSender) { const newVideoTrack = newStream.getVideoTracks()[0]; if (newVideoTrack) { @@ -752,7 +806,7 @@ class WebRTCChat { } } - // 更新显示信息 + // 13. 更新显示信息 if (text) { this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`; this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success'); @@ -762,11 +816,17 @@ class WebRTCChat { } } 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(); - // 如果切换失败,尝试回到默认视频 + // 回退到默认视频 if (videoFile !== this.defaultVideo) { this.logMessage('尝试回到默认视频', 'info'); 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() {