Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a0c1d6876 | |||
| f0bf3b6184 | |||
| a96fc86d42 |
14
server.js
14
server.js
@ -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秒后回到默认视频
|
||||||
|
|
||||||
// 获取视频列表
|
// 获取视频列表
|
||||||
|
|||||||
173
src/index.js
173
src/index.js
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -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
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