Compare commits
	
		
			3 Commits
		
	
	
		
			kehu_femal
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8a0c1d6876 | |||
| f0bf3b6184 | |||
| a96fc86d42 | 
							
								
								
									
										14
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								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秒后回到默认视频
 | ||||
| 
 | ||||
| // 获取视频列表
 | ||||
|  | ||||
							
								
								
									
										173
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								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,6 +403,58 @@ class WebRTCChat { | ||||
|              | ||||
|             this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); | ||||
| 
 | ||||
|             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; | ||||
|                      | ||||
|                     // 检查是否接近结束(最后0.1秒)
 | ||||
|                     if (duration - currentTime <= 0.1) { | ||||
|                         console.log('视频即将播放完成'); | ||||
|                          | ||||
|                         // 检查音频是否正在播放(从minimaxi_stream.js获取isPlaying状态)
 | ||||
|                         const isAudioPlaying = window.isPlaying || false; | ||||
|                          | ||||
|                         // 如果音频没有播放,且当前不是默认视频,则切换到默认视频
 | ||||
|                         if (!isAudioPlaying) { | ||||
|                             const currentVideoFile = this.currentVideo; | ||||
|                              | ||||
|                             if (currentVideoFile !== this.defaultVideo) { | ||||
|                                 console.log('音频已停止,当前视频不是默认视频,准备切换到默认视频'); | ||||
|                                  | ||||
|                                 // 移除监听器
 | ||||
|                                 this.cleanupTimeUpdateListener(); | ||||
|                                  | ||||
|                                 // 停止当前视频的循环
 | ||||
|                                 if (this.recordedVideo) { | ||||
|                                     this.recordedVideo.loop = false; | ||||
|                                 } | ||||
|                                  | ||||
|                                 // 切换到默认视频
 | ||||
|                                 try { | ||||
|                                     await this.switchVideoWithReplaceTrack(this.defaultVideo, 'auto', 'default'); | ||||
|                                     console.log('已自动切换到默认视频'); | ||||
|                                 } catch (error) { | ||||
|                                     console.error('自动切换到默认视频失败:', error); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|                  | ||||
|                 // 保存监听器引用以便后续清理
 | ||||
|                 this.currentTimeUpdateHandler = throttledTimeUpdate; | ||||
|                 video.addEventListener('timeupdate', throttledTimeUpdate); | ||||
|             } | ||||
|              | ||||
|             // 使用有限缓存策略(最多缓存3个视频流)
 | ||||
|             if (this.videoStreams.size >= 3) { | ||||
|                 const firstKey = this.videoStreams.keys().next().value; | ||||
| @ -405,6 +469,8 @@ class WebRTCChat { | ||||
|              | ||||
|             return stream; | ||||
|         } catch (error) { | ||||
|             // 确保在错误情况下也清理资源
 | ||||
|             this.cleanupVideoResources(videoFile); | ||||
|             this.logMessage(`创建视频流失败 ${videoFile}: ${error.message}`, 'error'); | ||||
|             throw error; | ||||
|         } | ||||
| @ -422,7 +488,7 @@ class WebRTCChat { | ||||
|         // });
 | ||||
|          | ||||
|         // 特别确保添加了5.mp4(从日志看这是常用视频)
 | ||||
|         videosToPreload.add('5.mp4'); | ||||
|         videosToPreload.add('s-1.mp4'); | ||||
|          | ||||
|         // 开始预加载
 | ||||
|         for (const videoFile of videosToPreload) { | ||||
| @ -482,6 +548,7 @@ class WebRTCChat { | ||||
|             // 现在停止旧的视频流
 | ||||
|             if (this.currentVideoStream !== newStream) { | ||||
|                 const oldStream = this.currentVideoStream; | ||||
|                 const oldVideo = this.currentVideo; | ||||
|                 setTimeout(() => { | ||||
|                     if (oldStream) { | ||||
|                         oldStream.getTracks().forEach(track => { | ||||
| @ -489,6 +556,10 @@ class WebRTCChat { | ||||
|                             this.logMessage(`已停止旧轨道: ${track.kind}`, 'info'); | ||||
|                         }); | ||||
|                     } | ||||
|                     // 清理旧视频的资源
 | ||||
|                     if (oldVideo) { | ||||
|                         this.cleanupVideoResources(oldVideo); | ||||
|                     } | ||||
|                 }, 1000); // 延迟1秒停止旧流,确保新流已经稳定
 | ||||
|             } | ||||
|              | ||||
| @ -538,9 +609,11 @@ class WebRTCChat { | ||||
|              | ||||
|             // 同时更新本地视频显示
 | ||||
|             if (this.recordedVideo) { | ||||
|                 // 停止当前视频流
 | ||||
|                 // 停止当前视频流和动画循环
 | ||||
|                 if (this.currentVideoStream) { | ||||
|                     this.currentVideoStream.getTracks().forEach(track => track.stop()); | ||||
|                     // 清理当前视频的资源
 | ||||
|                     this.cleanupVideoResources(this.currentVideo); | ||||
|                 } | ||||
|                  | ||||
|                 // 设置新的视频流
 | ||||
| @ -929,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, | ||||
| @ -962,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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 页面加载完成后初始化应用
 | ||||
| @ -973,3 +1131,10 @@ document.addEventListener('DOMContentLoaded', () => { | ||||
|         console.error('WebRTCChat 初始化失败:', error); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| // 在页面卸载时清理资源
 | ||||
| window.addEventListener('beforeunload', () => { | ||||
|     if (window.webrtcApp) { | ||||
|         window.webrtcApp.destroy(); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| // 以流式或非流式方式请求 minimaxi 大模型接口,并打印/返回内容
 | ||||
| 
 | ||||
| // import { text } from "express";
 | ||||
| 
 | ||||
| window.isPlaying = false; | ||||
| // 在文件顶部添加音频播放相关的变量和函数
 | ||||
| let audioContext = null; | ||||
| let audioQueue = []; // 音频队列
 | ||||
| let isPlaying = false; | ||||
| // let isPlaying = false;
 | ||||
| let isProcessingQueue = false; // 队列处理状态
 | ||||
| let nextStartTime = 0; // 添加这行来声明 nextStartTime 变量
 | ||||
| 
 | ||||
| @ -60,17 +60,17 @@ async function processAudioQueue() { | ||||
|   isProcessingQueue = true; | ||||
|   console.log('开始处理音频队列'); | ||||
|   let isFirstChunk = true; | ||||
|   while (audioQueue.length > 0 || isPlaying) { | ||||
|   while (audioQueue.length > 0 || window.isPlaying) { | ||||
|     // 如果当前没有音频在播放,且队列中有音频
 | ||||
|     if (!isPlaying && audioQueue.length > 0) { | ||||
|     if (!window.isPlaying && audioQueue.length > 0) { | ||||
|       const audioItem = audioQueue.shift(); | ||||
|       const sayName = 'say-5s-m-sw' | ||||
|       const targetVideo = '5.mp4' | ||||
|       const sayName = 'say-3s-m-sw' | ||||
|       const targetVideo = 's-1.mp4' | ||||
|       // 如果是第一个音频片段,触发视频切换
 | ||||
|       if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { | ||||
|         try { | ||||
|           console.log('--------------触发视频切换:', sayName); | ||||
|           await window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', 'say-5s-m-sw'); | ||||
|           await window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', 'say-3s-m-sw'); | ||||
|           isFirstChunk = false; | ||||
|           window.webrtcApp.currentVideoTag = sayName; | ||||
|         } catch (error) { | ||||
| @ -104,19 +104,19 @@ function playAudioData(audioData) { | ||||
|       source.buffer = audioData; | ||||
|       source.connect(ctx.destination); | ||||
|        | ||||
|       isPlaying = true; | ||||
|       window.isPlaying = true; | ||||
|        | ||||
|       source.onended = () => { | ||||
|         console.log('音频片段播放完成'); | ||||
|         isPlaying = false; | ||||
|         window.isPlaying = false; | ||||
|         resolve(); | ||||
|       }; | ||||
|        | ||||
|       // 超时保护
 | ||||
|       setTimeout(() => { | ||||
|         if (isPlaying) { | ||||
|         if (window.isPlaying) { | ||||
|           console.log('音频播放超时,强制结束'); | ||||
|           isPlaying = false; | ||||
|           window.isPlaying = false; | ||||
|           resolve(); | ||||
|         } | ||||
|       }, (audioData.duration + 0.5) * 1000); | ||||
| @ -126,7 +126,7 @@ function playAudioData(audioData) { | ||||
|        | ||||
|     } catch (error) { | ||||
|       console.error('播放音频失败:', error); | ||||
|       isPlaying = false; | ||||
|       window.isPlaying = false; | ||||
|       resolve(); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								videos/d-3s.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								videos/d-3s.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								videos/s-1.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								videos/s-1.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user