From f0bf3b61843cb5013e4123754fc1f434468c9912 Mon Sep 17 00:00:00 2001 From: songjvcheng Date: Tue, 29 Jul 2025 02:50:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A1=94=E6=8E=A5=E5=A4=84bu?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 14 +-- src/index.js | 211 ++++++++++++++++++++++++++++++----------- src/minimaxi_stream.js | 2 +- 3 files changed, 164 insertions(+), 63 deletions(-) diff --git a/server.js b/server.js index 023e787..944c817 100644 --- a/server.js +++ b/server.js @@ -87,16 +87,16 @@ const connectedClients = new Map(); // 视频映射配置 const videoMapping = { - 'say-6s-m-e': '1-m.mp4', - 'default': '0.mp4', - 'say-5s-amplitude': '2.mp4', - 'say-5s-m-e': '4.mp4', - 'say-5s-m-sw': '5.mp4', - 'say-3s-m-sw': '6.mp4', + // 'say-6s-m-e': '1-m.mp4', + 'default': 'd-3s.mp4', + // 'say-5s-amplitude': '2.mp4', + // 'say-5s-m-e': '4.mp4', + // 'say-5s-m-sw': '5.mp4', + 'say-3s-m-sw': 's-1.mp4', }; // 默认视频流配置 -const DEFAULT_VIDEO = '0.mp4'; +const DEFAULT_VIDEO = 'd-3s.mp4'; const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频 // 获取视频列表 diff --git a/src/index.js b/src/index.js index b672e11..6dfb1a6 100644 --- a/src/index.js +++ b/src/index.js @@ -19,11 +19,14 @@ class WebRTCChat { this.mediaRecorder = null; this.audioChunks = []; this.videoMapping = {}; - this.defaultVideo = '0.mp4'; + this.defaultVideo = 'd-3s.mp4'; this.currentVideoTag = 'default'; this.currentVideo = null; this.videoStreams = new Map(); // 存储不同视频的MediaStream this.currentVideoStream = null; + this.audioEndedListenerAdded = false; // 标志位,避免重复添加监听器 + this.animationFrameIds = new Map(); // 存储每个视频的动画帧ID + this.canvasElements = new Map(); // 存储每个视频的canvas元素 // 添加视频相关属性 this.videoSender = null; // WebRTC视频发送器 @@ -299,6 +302,9 @@ class WebRTCChat { try { this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); + // 清理之前的动画循环和canvas + this.cleanupVideoResources(videoFile); + // 先测试视频文件是否存在 await this.testVideoFile(videoFile); @@ -367,6 +373,8 @@ class WebRTCChat { // 绘制视频到canvas let lastDrawTime = 0; let isDrawing = false; + let animationId; + const drawFrame = () => { const now = performance.now(); if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { @@ -375,12 +383,16 @@ class WebRTCChat { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); isDrawing = false; } - requestAnimationFrame(drawFrame); + animationId = requestAnimationFrame(drawFrame); }; // 开始绘制帧 drawFrame(); + // 存储动画帧ID和canvas引用 + this.animationFrameIds.set(videoFile, animationId); + this.canvasElements.set(videoFile, canvas); + // 从canvas创建MediaStream const stream = canvas.captureStream(30); @@ -391,68 +403,56 @@ class WebRTCChat { this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); - if (videoFile === this.defaultVideo) { - let lastCurrentTime = 0; - video.addEventListener('timeupdate', async () => { + if (videoFile === this.defaultVideo && !this.audioEndedListenerAdded) { + this.audioEndedListenerAdded = true; + + // 清理之前可能存在的监听器 + if (this.currentTimeUpdateHandler) { + video.removeEventListener('timeupdate', this.currentTimeUpdateHandler); + } + // 在handleTimeUpdate中使用节流 + const throttledTimeUpdate = this.throttle(handleTimeUpdate, 100); + const handleTimeUpdate = async () => { const currentTime = video.currentTime; const duration = video.duration; - // 方法2a:检查是否接近结束(最后0.1秒) + // 检查是否接近结束(最后0.1秒) if (duration - currentTime <= 0.1) { console.log('视频即将播放完成'); - // 处理即将结束的逻辑 - if (videoFile === this.defaultVideo) { - let lastCurrentTime = 0; - video.addEventListener('timeupdate', async () => { - const currentTime = video.currentTime; - const duration = video.duration; + + // 检查音频是否正在播放(从minimaxi_stream.js获取isPlaying状态) + const isAudioPlaying = window.isPlaying || false; + + // 如果音频没有播放,且当前不是默认视频,则切换到默认视频 + if (!isAudioPlaying) { + const currentVideoFile = this.currentVideo; + + if (currentVideoFile !== this.defaultVideo) { + console.log('音频已停止,当前视频不是默认视频,准备切换到默认视频'); - // 检查音频是否正在播放(从minimaxi_stream.js获取isPlaying状态) - const isAudioPlaying = window.isPlaying || false; // 需要确保isPlaying是全局可访问的 + // 移除监听器 + this.cleanupTimeUpdateListener(); - // 如果音频没有播放,且当前不是默认视频,则切换到默认视频 - if (!isAudioPlaying) { - const currentVideoFile = this.currentVideo; // 获取当前播放的视频文件名 - - if (currentVideoFile !== this.defaultVideo) { - console.log('音频已停止,当前视频不是默认视频,准备切换到默认视频'); - - // 停止当前视频的循环 - if (this.recordedVideo) { - this.recordedVideo.loop = false; - } - - // 切换到默认视频 - try { - await this.switchVideoWithReplaceTrack(this.defaultVideo, 'auto-switch', 'audio-ended'); - console.log('已自动切换到默认视频'); - } catch (error) { - console.error('自动切换到默认视频失败:', error); - } - } + // 停止当前视频的循环 + if (this.recordedVideo) { + this.recordedVideo.loop = false; } - lastCurrentTime = currentTime; - }); + // 切换到默认视频 + try { + await this.switchVideoWithReplaceTrack(this.defaultVideo, 'auto', 'default'); + console.log('已自动切换到默认视频'); + } catch (error) { + console.error('自动切换到默认视频失败:', error); + } + } } } - - // 方法2b:检查是否已经到达结尾 - if (currentTime >= duration) { - console.log('视频播放完成'); - // 处理播放完成的逻辑 - } - - // 方法2c:检查时间是否停止更新(可能表示播放结束) - if (Math.abs(currentTime - lastCurrentTime) < 0.01) { - // 时间没有更新,可能播放结束或暂停 - if (currentTime >= duration) { - console.log('视频播放完成(通过时间停止检测)'); - } - } - - lastCurrentTime = currentTime; - }); + }; + + // 保存监听器引用以便后续清理 + this.currentTimeUpdateHandler = throttledTimeUpdate; + video.addEventListener('timeupdate', throttledTimeUpdate); } // 使用有限缓存策略(最多缓存3个视频流) @@ -469,6 +469,8 @@ class WebRTCChat { return stream; } catch (error) { + // 确保在错误情况下也清理资源 + this.cleanupVideoResources(videoFile); this.logMessage(`创建视频流失败 ${videoFile}: ${error.message}`, 'error'); throw error; } @@ -486,7 +488,7 @@ class WebRTCChat { // }); // 特别确保添加了5.mp4(从日志看这是常用视频) - videosToPreload.add('6.mp4'); + videosToPreload.add('s-1.mp4'); // 开始预加载 for (const videoFile of videosToPreload) { @@ -546,6 +548,7 @@ class WebRTCChat { // 现在停止旧的视频流 if (this.currentVideoStream !== newStream) { const oldStream = this.currentVideoStream; + const oldVideo = this.currentVideo; setTimeout(() => { if (oldStream) { oldStream.getTracks().forEach(track => { @@ -553,6 +556,10 @@ class WebRTCChat { this.logMessage(`已停止旧轨道: ${track.kind}`, 'info'); }); } + // 清理旧视频的资源 + if (oldVideo) { + this.cleanupVideoResources(oldVideo); + } }, 1000); // 延迟1秒停止旧流,确保新流已经稳定 } @@ -602,9 +609,11 @@ class WebRTCChat { // 同时更新本地视频显示 if (this.recordedVideo) { - // 停止当前视频流 + // 停止当前视频流和动画循环 if (this.currentVideoStream) { this.currentVideoStream.getTracks().forEach(track => track.stop()); + // 清理当前视频的资源 + this.cleanupVideoResources(this.currentVideo); } // 设置新的视频流 @@ -993,6 +1002,29 @@ class WebRTCChat { this.messageLog.scrollTop = this.messageLog.scrollHeight; } + // 添加清理监听器的方法 + cleanupTimeUpdateListener() { + if (this.currentTimeUpdateHandler && this.recordedVideo) { + this.recordedVideo.removeEventListener('timeupdate', this.currentTimeUpdateHandler); + this.currentTimeUpdateHandler = null; + this.audioEndedListenerAdded = false; + } + } + + // 添加节流函数 + throttle(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + } + } + checkVideoStreamStatus() { const status = { hasStream: !!this.currentVideoStream, @@ -1026,6 +1058,68 @@ class WebRTCChat { // 检查当前视频流状态 this.checkVideoStreamStatus(); } + + // 清理视频资源的方法 + cleanupVideoResources(videoFile) { + // 停止动画循环 + if (this.animationFrameIds.has(videoFile)) { + const animationId = this.animationFrameIds.get(videoFile); + if (animationId) { + cancelAnimationFrame(animationId); + } + this.animationFrameIds.delete(videoFile); + } + + // 清理canvas + if (this.canvasElements.has(videoFile)) { + const canvas = this.canvasElements.get(videoFile); + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + this.canvasElements.delete(videoFile); + } + } + + // 清理所有视频资源 + cleanupAllVideoResources() { + // 停止所有动画循环 + for (const [videoFile, animationId] of this.animationFrameIds) { + if (animationId) { + cancelAnimationFrame(animationId); + } + } + this.animationFrameIds.clear(); + + // 清理所有canvas + for (const [videoFile, canvas] of this.canvasElements) { + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + } + this.canvasElements.clear(); + } + + // 添加销毁方法 + destroy() { + this.cleanupAllVideoResources(); + this.cleanupTimeUpdateListener(); + + // 清理其他资源 + if (this.localStream) { + this.localStream.getTracks().forEach(track => track.stop()); + } + if (this.currentVideoStream) { + this.currentVideoStream.getTracks().forEach(track => track.stop()); + } + + // 清理视频流缓存 + for (const [key, stream] of this.videoStreams) { + stream.getTracks().forEach(track => track.stop()); + } + this.videoStreams.clear(); + } } // 页面加载完成后初始化应用 @@ -1037,3 +1131,10 @@ document.addEventListener('DOMContentLoaded', () => { console.error('WebRTCChat 初始化失败:', error); } }); + +// 在页面卸载时清理资源 +window.addEventListener('beforeunload', () => { + if (window.webrtcApp) { + window.webrtcApp.destroy(); + } +}); diff --git a/src/minimaxi_stream.js b/src/minimaxi_stream.js index 17f739b..8399bb1 100644 --- a/src/minimaxi_stream.js +++ b/src/minimaxi_stream.js @@ -65,7 +65,7 @@ async function processAudioQueue() { if (!window.isPlaying && audioQueue.length > 0) { const audioItem = audioQueue.shift(); const sayName = 'say-3s-m-sw' - const targetVideo = '6.mp4' + const targetVideo = 's-1.mp4' // 如果是第一个音频片段,触发视频切换 if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { try {