Compare commits

...

3 Commits

Author SHA1 Message Date
8a0c1d6876 status 2025-07-29 09:53:02 +08:00
f0bf3b6184 优化衔接处bug 2025-07-29 02:50:08 +08:00
a96fc86d42 处理结尾判断 2025-07-29 01:36:40 +08:00
5 changed files with 188 additions and 23 deletions

View File

@ -87,16 +87,16 @@ const connectedClients = new Map();
// 视频映射配置 // 视频映射配置
const videoMapping = { const videoMapping = {
'say-6s-m-e': '1-m.mp4', // 'say-6s-m-e': '1-m.mp4',
'default': '0.mp4', 'default': 'd-3s.mp4',
'say-5s-amplitude': '2.mp4', // 'say-5s-amplitude': '2.mp4',
'say-5s-m-e': '4.mp4', // 'say-5s-m-e': '4.mp4',
'say-5s-m-sw': '5.mp4', // 'say-5s-m-sw': '5.mp4',
'say-3s-m-sw': '6.mp4', 'say-3s-m-sw': 's-1.mp4',
}; };
// 默认视频流配置 // 默认视频流配置
const DEFAULT_VIDEO = '0.mp4'; const DEFAULT_VIDEO = 'd-3s.mp4';
const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频 const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频
// 获取视频列表 // 获取视频列表

View File

@ -19,11 +19,14 @@ class WebRTCChat {
this.mediaRecorder = null; this.mediaRecorder = null;
this.audioChunks = []; this.audioChunks = [];
this.videoMapping = {}; this.videoMapping = {};
this.defaultVideo = '0.mp4'; this.defaultVideo = 'd-3s.mp4';
this.currentVideoTag = 'default'; this.currentVideoTag = 'default';
this.currentVideo = null; this.currentVideo = null;
this.videoStreams = new Map(); // 存储不同视频的MediaStream this.videoStreams = new Map(); // 存储不同视频的MediaStream
this.currentVideoStream = null; this.currentVideoStream = null;
this.audioEndedListenerAdded = false; // 标志位,避免重复添加监听器
this.animationFrameIds = new Map(); // 存储每个视频的动画帧ID
this.canvasElements = new Map(); // 存储每个视频的canvas元素
// 添加视频相关属性 // 添加视频相关属性
this.videoSender = null; // WebRTC视频发送器 this.videoSender = null; // WebRTC视频发送器
@ -299,6 +302,9 @@ class WebRTCChat {
try { try {
this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); this.logMessage(`开始创建视频流: ${videoFile}`, 'info');
// 清理之前的动画循环和canvas
this.cleanupVideoResources(videoFile);
// 先测试视频文件是否存在 // 先测试视频文件是否存在
await this.testVideoFile(videoFile); await this.testVideoFile(videoFile);
@ -367,6 +373,8 @@ class WebRTCChat {
// 绘制视频到canvas // 绘制视频到canvas
let lastDrawTime = 0; let lastDrawTime = 0;
let isDrawing = false; let isDrawing = false;
let animationId;
const drawFrame = () => { const drawFrame = () => {
const now = performance.now(); const now = performance.now();
if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { 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); ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
isDrawing = false; isDrawing = false;
} }
requestAnimationFrame(drawFrame); animationId = requestAnimationFrame(drawFrame);
}; };
// 开始绘制帧 // 开始绘制帧
drawFrame(); drawFrame();
// 存储动画帧ID和canvas引用
this.animationFrameIds.set(videoFile, animationId);
this.canvasElements.set(videoFile, canvas);
// 从canvas创建MediaStream // 从canvas创建MediaStream
const stream = canvas.captureStream(30); const stream = canvas.captureStream(30);
@ -391,6 +403,58 @@ class WebRTCChat {
this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); 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个视频流 // 使用有限缓存策略最多缓存3个视频流
if (this.videoStreams.size >= 3) { if (this.videoStreams.size >= 3) {
const firstKey = this.videoStreams.keys().next().value; const firstKey = this.videoStreams.keys().next().value;
@ -405,6 +469,8 @@ class WebRTCChat {
return stream; return stream;
} catch (error) { } catch (error) {
// 确保在错误情况下也清理资源
this.cleanupVideoResources(videoFile);
this.logMessage(`创建视频流失败 ${videoFile}: ${error.message}`, 'error'); this.logMessage(`创建视频流失败 ${videoFile}: ${error.message}`, 'error');
throw error; throw error;
} }
@ -422,7 +488,7 @@ class WebRTCChat {
// }); // });
// 特别确保添加了5.mp4从日志看这是常用视频 // 特别确保添加了5.mp4从日志看这是常用视频
videosToPreload.add('5.mp4'); videosToPreload.add('s-1.mp4');
// 开始预加载 // 开始预加载
for (const videoFile of videosToPreload) { for (const videoFile of videosToPreload) {
@ -482,6 +548,7 @@ class WebRTCChat {
// 现在停止旧的视频流 // 现在停止旧的视频流
if (this.currentVideoStream !== newStream) { if (this.currentVideoStream !== newStream) {
const oldStream = this.currentVideoStream; const oldStream = this.currentVideoStream;
const oldVideo = this.currentVideo;
setTimeout(() => { setTimeout(() => {
if (oldStream) { if (oldStream) {
oldStream.getTracks().forEach(track => { oldStream.getTracks().forEach(track => {
@ -489,6 +556,10 @@ class WebRTCChat {
this.logMessage(`已停止旧轨道: ${track.kind}`, 'info'); this.logMessage(`已停止旧轨道: ${track.kind}`, 'info');
}); });
} }
// 清理旧视频的资源
if (oldVideo) {
this.cleanupVideoResources(oldVideo);
}
}, 1000); // 延迟1秒停止旧流确保新流已经稳定 }, 1000); // 延迟1秒停止旧流确保新流已经稳定
} }
@ -538,9 +609,11 @@ class WebRTCChat {
// 同时更新本地视频显示 // 同时更新本地视频显示
if (this.recordedVideo) { if (this.recordedVideo) {
// 停止当前视频流 // 停止当前视频流和动画循环
if (this.currentVideoStream) { if (this.currentVideoStream) {
this.currentVideoStream.getTracks().forEach(track => track.stop()); this.currentVideoStream.getTracks().forEach(track => track.stop());
// 清理当前视频的资源
this.cleanupVideoResources(this.currentVideo);
} }
// 设置新的视频流 // 设置新的视频流
@ -929,6 +1002,29 @@ class WebRTCChat {
this.messageLog.scrollTop = this.messageLog.scrollHeight; 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() { checkVideoStreamStatus() {
const status = { const status = {
hasStream: !!this.currentVideoStream, hasStream: !!this.currentVideoStream,
@ -962,6 +1058,68 @@ class WebRTCChat {
// 检查当前视频流状态 // 检查当前视频流状态
this.checkVideoStreamStatus(); 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); console.error('WebRTCChat 初始化失败:', error);
} }
}); });
// 在页面卸载时清理资源
window.addEventListener('beforeunload', () => {
if (window.webrtcApp) {
window.webrtcApp.destroy();
}
});

View File

@ -1,11 +1,11 @@
// 以流式或非流式方式请求 minimaxi 大模型接口,并打印/返回内容 // 以流式或非流式方式请求 minimaxi 大模型接口,并打印/返回内容
// import { text } from "express"; // import { text } from "express";
window.isPlaying = false;
// 在文件顶部添加音频播放相关的变量和函数 // 在文件顶部添加音频播放相关的变量和函数
let audioContext = null; let audioContext = null;
let audioQueue = []; // 音频队列 let audioQueue = []; // 音频队列
let isPlaying = false; // let isPlaying = false;
let isProcessingQueue = false; // 队列处理状态 let isProcessingQueue = false; // 队列处理状态
let nextStartTime = 0; // 添加这行来声明 nextStartTime 变量 let nextStartTime = 0; // 添加这行来声明 nextStartTime 变量
@ -60,17 +60,17 @@ async function processAudioQueue() {
isProcessingQueue = true; isProcessingQueue = true;
console.log('开始处理音频队列'); console.log('开始处理音频队列');
let isFirstChunk = true; 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 audioItem = audioQueue.shift();
const sayName = 'say-5s-m-sw' const sayName = 'say-3s-m-sw'
const targetVideo = '5.mp4' const targetVideo = 's-1.mp4'
// 如果是第一个音频片段,触发视频切换 // 如果是第一个音频片段,触发视频切换
if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) { if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) {
try { try {
console.log('--------------触发视频切换:', sayName); 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; isFirstChunk = false;
window.webrtcApp.currentVideoTag = sayName; window.webrtcApp.currentVideoTag = sayName;
} catch (error) { } catch (error) {
@ -104,19 +104,19 @@ function playAudioData(audioData) {
source.buffer = audioData; source.buffer = audioData;
source.connect(ctx.destination); source.connect(ctx.destination);
isPlaying = true; window.isPlaying = true;
source.onended = () => { source.onended = () => {
console.log('音频片段播放完成'); console.log('音频片段播放完成');
isPlaying = false; window.isPlaying = false;
resolve(); resolve();
}; };
// 超时保护 // 超时保护
setTimeout(() => { setTimeout(() => {
if (isPlaying) { if (window.isPlaying) {
console.log('音频播放超时,强制结束'); console.log('音频播放超时,强制结束');
isPlaying = false; window.isPlaying = false;
resolve(); resolve();
} }
}, (audioData.duration + 0.5) * 1000); }, (audioData.duration + 0.5) * 1000);
@ -126,7 +126,7 @@ function playAudioData(audioData) {
} catch (error) { } catch (error) {
console.error('播放音频失败:', error); console.error('播放音频失败:', error);
isPlaying = false; window.isPlaying = false;
resolve(); resolve();
} }
}); });

BIN
videos/d-3s.mp4 Normal file

Binary file not shown.

BIN
videos/s-1.mp4 Normal file

Binary file not shown.