diff --git a/src/index.js b/src/index.js index ec893be..1aa6282 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,10 @@ class WebRTCChat { this.currentVideo = null; this.videoStreams = new Map(); // 存储不同视频的MediaStream this.currentVideoStream = null; + + // 添加视频相关属性 + this.videoSender = null; // WebRTC视频发送器 + this.currentVideoStream = null; // 当前视频流 // 初始化音频处理器 console.log('开始初始化音频处理器'); @@ -289,12 +293,9 @@ class WebRTCChat { } async createVideoStream(videoFile) { - // 如果已经缓存了这个视频流,直接返回 - if (this.videoStreams.has(videoFile)) { - this.logMessage(`使用缓存的视频流: ${videoFile}`, 'info'); - return this.videoStreams.get(videoFile); - } - + // 检查缓存,但为每个视频创建独立的播放实例 + const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性 + try { this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); @@ -306,26 +307,25 @@ class WebRTCChat { video.src = `/videos/${videoFile}`; video.muted = true; video.loop = true; - video.autoplay = true; - video.crossOrigin = 'anonymous'; // 添加跨域支持 - video.playsInline = true; // 添加playsInline属性 + video.autoplay = false; // 手动控制播放 + video.crossOrigin = 'anonymous'; + video.playsInline = true; + + // 预加载视频但不播放 + video.preload = 'auto'; // 等待视频加载完成 await new Promise((resolve, reject) => { - video.onloadedmetadata = () => { - this.logMessage(`视频元数据加载完成: ${videoFile}`, 'info'); + video.onloadeddata = () => { + this.logMessage(`视频数据加载完成: ${videoFile}`, 'info'); + // 确保从第一帧开始 + video.currentTime = 0; resolve(); }; video.onerror = (error) => { this.logMessage(`视频加载失败: ${videoFile}`, 'error'); reject(error); }; - video.onloadstart = () => { - this.logMessage(`开始加载视频: ${videoFile}`, 'info'); - }; - video.oncanplay = () => { - this.logMessage(`视频可以播放: ${videoFile}`, 'info'); - }; }); // 创建MediaStream @@ -338,22 +338,27 @@ class WebRTCChat { 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 { await video.play(); this.logMessage(`视频开始播放: ${videoFile}`, 'info'); } catch (playError) { this.logMessage(`视频播放失败: ${playError.message}`, 'error'); - // 即使播放失败也继续创建流 } - // 等待视频开始播放 + // 等待视频真正开始播放 await new Promise(resolve => { const checkPlay = () => { - if (video.readyState >= video.HAVE_CURRENT_DATA) { + if (video.readyState >= video.HAVE_CURRENT_DATA && !video.paused) { resolve(); } else { - setTimeout(checkPlay, 100); + setTimeout(checkPlay, 50); } }; checkPlay(); @@ -364,8 +369,7 @@ class WebRTCChat { let isDrawing = false; const drawFrame = () => { const now = performance.now(); - // 限制绘制频率,确保平滑过渡 - if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { // 约60fps + if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { isDrawing = true; lastDrawTime = now; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); @@ -378,16 +382,25 @@ class WebRTCChat { drawFrame(); // 从canvas创建MediaStream - const stream = canvas.captureStream(30); // 30fps + const stream = canvas.captureStream(30); - // 等待流创建完成并稳定 + // 等待流稳定 await new Promise(resolve => { - setTimeout(resolve, 500); // 给更多时间让流稳定 + setTimeout(resolve, 200); // 减少等待时间 }); 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); 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() { // 开始通话按钮 this.startButton.onclick = () => this.startCall(); @@ -542,7 +617,7 @@ class WebRTCChat { audio: true }); - this.createPeerConnection(); + await this.createPeerConnection(); this.startButton.disabled = true; this.stopButton.disabled = false; @@ -593,7 +668,7 @@ class WebRTCChat { this.logMessage('音频通话已结束', 'info'); } - createPeerConnection() { + async createPeerConnection() { const configuration = { iceServers: [ { urls: "stun:stun.qq.com:3478" }, @@ -610,6 +685,19 @@ class WebRTCChat { 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.remoteVideo.srcObject = event.streams[0]; diff --git a/src/minimaxi_stream.js b/src/minimaxi_stream.js index 3df0f38..c5e29ad 100644 --- a/src/minimaxi_stream.js +++ b/src/minimaxi_stream.js @@ -70,7 +70,7 @@ async function processAudioQueue() { if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { try { 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; window.webrtcApp.currentVideoTag = sayName; } catch (error) { @@ -90,7 +90,7 @@ async function processAudioQueue() { if (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('音频队列处理完成'); } diff --git a/src/styles.css b/src/styles.css index fc4a402..8392a03 100644 --- a/src/styles.css +++ b/src/styles.css @@ -445,4 +445,48 @@ header p { /* 视频播放时的样式 */ #recordedVideo.playing { 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); } } \ No newline at end of file