切换增加视频过渡效果,避免黑屏。UI大整改
This commit is contained in:
parent
1ebfd472c4
commit
65f17b4a66
@ -88,15 +88,15 @@ const connectedClients = new Map();
|
||||
// 视频映射配置
|
||||
const videoMapping = {
|
||||
// 'say-6s-m-e': '1-m.mp4',
|
||||
'default': '0-2.mp4',
|
||||
'default': 'bd-1.mp4',
|
||||
// 'say-5s-amplitude': '2.mp4',
|
||||
// 'say-5s-m-e': '4.mp4',
|
||||
'say-5s-m-sw': '5.mp4',
|
||||
'say-5s-m-sw': 'd-0.mp4',
|
||||
// 'say-3s-m-sw': '6.mp4',
|
||||
};
|
||||
|
||||
// 默认视频流配置
|
||||
const DEFAULT_VIDEO = '0-2.mp4';
|
||||
const DEFAULT_VIDEO = 'bd-1.mp4';
|
||||
const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频
|
||||
|
||||
// 获取视频列表
|
||||
|
||||
@ -69,22 +69,30 @@ function updateHistoryMessage(userInput, assistantResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存消息到服务端
|
||||
// 保存消息到服务端
|
||||
async function saveMessage(userInput, assistantResponse) {
|
||||
try {
|
||||
// 验证参数是否有效
|
||||
if (!userInput || !userInput.trim() || !assistantResponse || !assistantResponse.trim()) {
|
||||
console.warn('跳过保存消息:用户输入或助手回复为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/messages/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userInput,
|
||||
assistantResponse
|
||||
userInput: userInput.trim(),
|
||||
assistantResponse: assistantResponse.trim()
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('保存消息失败');
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(`保存消息失败: ${response.status} ${errorData.error || response.statusText}`);
|
||||
}
|
||||
|
||||
console.log('消息已保存到服务端');
|
||||
|
||||
257
src/index.html
257
src/index.html
@ -2,69 +2,260 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>WebRTC 音频通话</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
/* 全屏视频样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recorded-video-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 视频容器样式 - 支持双缓冲 */
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#recordedVideo, #recordedVideoBuffer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* 主视频默认显示 */
|
||||
#recordedVideo {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 缓冲视频默认隐藏 */
|
||||
#recordedVideoBuffer {
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 切换状态 */
|
||||
#recordedVideo.switching {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#recordedVideoBuffer.switching {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.video-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.video-loading.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#startButton {
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 25px;
|
||||
min-width: 200px;
|
||||
background: rgba(0, 123, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#stopButton {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: rgba(220, 53, 69, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
#stopButton.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#stopButton:hover:not(:disabled) {
|
||||
background: rgba(200, 35, 51, 0.95);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(220, 53, 69, 0.5);
|
||||
}
|
||||
|
||||
#stopButton svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
#stopButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<!-- 隐藏的header -->
|
||||
<header style="display: none;">
|
||||
<h1>WebRTC 音频通话</h1>
|
||||
<p>实时播放录制视频,支持文本和语音输入</p>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 音频状态显示 -->
|
||||
<div class="audio-status">
|
||||
<!-- 隐藏的音频状态显示 -->
|
||||
<div class="audio-status" style="display: none;">
|
||||
<div class="status-indicator">
|
||||
<span id="audioStatus">未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录制视频播放区域 -->
|
||||
<!-- 录制视频播放区域 - 全屏显示 -->
|
||||
<div class="recorded-video-section">
|
||||
<video id="recordedVideo" autoplay muted>
|
||||
<source src="" type="video/mp4">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div class="video-info">
|
||||
<div class="video-container">
|
||||
<!-- 主视频元素 -->
|
||||
<video id="recordedVideo" autoplay muted>
|
||||
<source src="" type="video/mp4">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
<!-- 缓冲视频元素 -->
|
||||
<video id="recordedVideoBuffer" autoplay muted>
|
||||
<source src="" type="video/mp4">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
<!-- 加载指示器 -->
|
||||
<div class="video-loading" id="videoLoading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>正在切换视频...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-info" style="display: none;">
|
||||
<span id="currentVideoName">未选择视频</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<!-- 控制按钮 - 悬浮在视频上方 -->
|
||||
<div class="controls">
|
||||
<button id="startButton" class="btn btn-primary">开始音频通话</button>
|
||||
<button id="stopButton" class="btn btn-danger" disabled>停止通话</button>
|
||||
<!-- <button id="muteButton" class="btn btn-secondary">静音</button>
|
||||
<button id="defaultVideoButton" class="btn btn-info">回到默认视频</button>
|
||||
<button id="testVideoButton" class="btn btn-warning">测试视频文件</button> -->
|
||||
<button id="stopButton" class="btn btn-danger" disabled title="结束通话">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
|
||||
<path d="M19 12h2c0-4.97-4.03-9-9-9v2c3.87 0 7 3.13 7 7z"/>
|
||||
<path d="M15 12h2c0-2.76-2.24-5-5-5v2c1.66 0 3 1.34 3 3z"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" stroke="white" stroke-width="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-section">
|
||||
<!-- 隐藏的输入区域 -->
|
||||
<div class="input-section" style="display: none;">
|
||||
<div class="text-input-group">
|
||||
<input type="text" id="textInput" placeholder="输入文本内容..." />
|
||||
<button id="sendTextButton" class="btn btn-primary">发送文本</button>
|
||||
</div>
|
||||
|
||||
<div class="voice-input-group">
|
||||
<button id="startVoiceButton" class="btn btn-success">开始语音输入</button>
|
||||
<button id="stopVoiceButton" class="btn btn-warning" disabled>停止语音输入</button>
|
||||
<span id="voiceStatus">点击开始语音输入</span>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的视频选择 -->
|
||||
<div class="video-selection" style="display: none;">
|
||||
<h3>选择要播放的视频</h3>
|
||||
<div id="videoList" class="video-list">
|
||||
<!-- 视频列表将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频选择 -->
|
||||
<!-- <div class="video-selection">
|
||||
<h3>选择要播放的视频</h3>
|
||||
<div id="videoList" class="video-list">
|
||||
视频列表将在这里动态生成 -->
|
||||
<!-- </div>
|
||||
</div> -->
|
||||
|
||||
<!-- 状态显示 -->
|
||||
<div class="status-section">
|
||||
<!-- 隐藏的状态显示 -->
|
||||
<div class="status-section" style="display: none;">
|
||||
<div id="connectionStatus" class="status">未连接</div>
|
||||
<div id="messageLog" class="message-log"></div>
|
||||
</div>
|
||||
@ -78,4 +269,4 @@
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
379
src/index.js
379
src/index.js
@ -19,7 +19,7 @@ class WebRTCChat {
|
||||
this.mediaRecorder = null;
|
||||
this.audioChunks = [];
|
||||
this.videoMapping = {};
|
||||
this.defaultVideo = '0-2.mp4';
|
||||
this.defaultVideo = 'bd-1.mp4';
|
||||
this.currentVideoTag = 'default';
|
||||
this.currentVideo = null;
|
||||
this.videoStreams = new Map(); // 存储不同视频的MediaStream
|
||||
@ -34,7 +34,7 @@ class WebRTCChat {
|
||||
// 初始化音频处理器
|
||||
this.audioProcessor = new AudioProcessor({
|
||||
onSpeechStart: () => {
|
||||
this.voiceStatus.textContent = '检测到语音,开始录音...';
|
||||
// this.voiceStatus.textContent = '检测到语音,开始录音...';
|
||||
this.logMessage('检测到语音,开始录音...', 'info');
|
||||
},
|
||||
onSpeechEnd: () => {
|
||||
@ -42,16 +42,16 @@ class WebRTCChat {
|
||||
},
|
||||
onRecognitionResult: (text) => {
|
||||
// ASRTEXT = text;
|
||||
this.voiceStatus.textContent = '识别完成';
|
||||
// this.voiceStatus.textContent = '识别完成';
|
||||
this.logMessage(`语音识别结果: ${text}`, 'success');
|
||||
this.handleVoiceInput(text);
|
||||
},
|
||||
onError: (error) => {
|
||||
this.voiceStatus.textContent = '识别失败';
|
||||
// this.voiceStatus.textContent = '识别失败';
|
||||
this.logMessage(error, 'error');
|
||||
},
|
||||
onStatusUpdate: (message, status) => {
|
||||
this.voiceStatus.textContent = message;
|
||||
// this.voiceStatus.textContent = message;
|
||||
}
|
||||
});
|
||||
|
||||
@ -80,6 +80,11 @@ class WebRTCChat {
|
||||
this.localVideo = document.getElementById('localVideo');
|
||||
this.remoteVideo = document.getElementById('remoteVideo');
|
||||
this.recordedVideo = document.getElementById('recordedVideo');
|
||||
this.recordedVideoBuffer = document.getElementById('recordedVideoBuffer'); // 新增缓冲视频元素
|
||||
this.videoLoading = document.getElementById('videoLoading'); // 加载指示器
|
||||
|
||||
// 当前活跃的视频元素标识
|
||||
this.activeVideoElement = 'main'; // 'main' 或 'buffer'
|
||||
|
||||
// 音频状态元素
|
||||
this.audioStatus = document.getElementById('audioStatus');
|
||||
@ -89,14 +94,14 @@ class WebRTCChat {
|
||||
this.stopButton = document.getElementById('stopButton');
|
||||
this.muteButton = document.getElementById('muteButton');
|
||||
this.sendTextButton = document.getElementById('sendTextButton');
|
||||
this.startVoiceButton = document.getElementById('startVoiceButton');
|
||||
this.stopVoiceButton = document.getElementById('stopVoiceButton');
|
||||
this.defaultVideoButton = document.getElementById('defaultVideoButton');
|
||||
// this.startVoiceButton = document.getElementById('startVoiceButton');
|
||||
// this.stopVoiceButton = document.getElementById('stopVoiceButton');
|
||||
// this.defaultVideoButton = document.getElementById('defaultVideoButton');
|
||||
// this.testVideoButton = document.getElementById('testVideoButton'); // 新增测试按钮
|
||||
|
||||
// 输入元素
|
||||
this.textInput = document.getElementById('textInput');
|
||||
this.voiceStatus = document.getElementById('voiceStatus');
|
||||
// this.voiceStatus = document.getElementById('voiceStatus');
|
||||
|
||||
// 状态元素
|
||||
this.connectionStatus = document.getElementById('connectionStatus');
|
||||
@ -424,7 +429,7 @@ class WebRTCChat {
|
||||
// 在应用初始化时预加载常用视频
|
||||
async preloadCommonVideos() {
|
||||
// 获取所有可能需要的视频
|
||||
const videosToPreload = new Set(['0-2.mp4']);
|
||||
const videosToPreload = new Set(['bd-1.mp4']);
|
||||
|
||||
// 添加视频映射中的所有视频
|
||||
// Object.values(this.videoMapping).forEach(video => {
|
||||
@ -432,7 +437,7 @@ class WebRTCChat {
|
||||
// });
|
||||
|
||||
// 特别确保添加了5.mp4(从日志看这是常用视频)
|
||||
videosToPreload.add('5.mp4');
|
||||
videosToPreload.add('d-0.mp4');
|
||||
|
||||
// 并行预加载,提高效率
|
||||
const preloadPromises = Array.from(videosToPreload).map(async (videoFile) => {
|
||||
@ -448,99 +453,105 @@ class WebRTCChat {
|
||||
await Promise.allSettled(preloadPromises);
|
||||
}
|
||||
|
||||
async switchVideoStream(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始切换视频流: ${videoFile} (${type})`, 'info');
|
||||
// async switchVideoStream(videoFile, type = '', text = '') {
|
||||
// try {
|
||||
// this.logMessage(`开始切换视频流: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 检查是否已缓存
|
||||
const isCached = this.videoStreams.has(videoFile);
|
||||
// // 检查是否已缓存
|
||||
// const isCached = this.videoStreams.has(videoFile);
|
||||
|
||||
// 如果已缓存,直接使用,避免loading状态
|
||||
if (isCached) {
|
||||
const cachedStream = this.videoStreams.get(videoFile);
|
||||
if (cachedStream && cachedStream.getTracks().length > 0) {
|
||||
// 直接切换到缓存的流
|
||||
this.currentVideoStream = cachedStream;
|
||||
this.recordedVideo.srcObject = cachedStream;
|
||||
this.currentVideo = videoFile;
|
||||
// // 如果已缓存,直接使用,避免loading状态
|
||||
// if (isCached) {
|
||||
// const cachedStream = this.videoStreams.get(videoFile);
|
||||
// if (cachedStream && cachedStream.getTracks().length > 0) {
|
||||
// // 直接切换到缓存的流
|
||||
// this.currentVideoStream = cachedStream;
|
||||
// this.recordedVideo.srcObject = cachedStream;
|
||||
// this.currentVideo = videoFile;
|
||||
|
||||
// 立即播放,无需loading状态
|
||||
await this.recordedVideo.play();
|
||||
this.recordedVideo.classList.add('playing');
|
||||
// // 立即播放,无需loading状态
|
||||
// await this.recordedVideo.play();
|
||||
// this.recordedVideo.classList.add('playing');
|
||||
|
||||
this.logMessage(`使用缓存视频流: ${videoFile}`, 'success');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// this.logMessage(`使用缓存视频流: ${videoFile}`, 'success');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// 未缓存的视频才显示loading状态
|
||||
this.recordedVideo.classList.add('loading');
|
||||
// // 未缓存的视频才显示loading状态
|
||||
// this.recordedVideo.classList.add('loading');
|
||||
|
||||
// 先创建新的视频流
|
||||
const newStream = await this.createVideoStream(videoFile);
|
||||
// // 先创建新的视频流
|
||||
// const newStream = await this.createVideoStream(videoFile);
|
||||
|
||||
// 减少等待时间
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// // 减少等待时间
|
||||
// await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 检查流是否有效
|
||||
if (!newStream || newStream.getTracks().length === 0) {
|
||||
throw new Error('创建的视频流无效');
|
||||
}
|
||||
// // 检查流是否有效
|
||||
// if (!newStream || newStream.getTracks().length === 0) {
|
||||
// throw new Error('创建的视频流无效');
|
||||
// }
|
||||
|
||||
// 设置新的视频流
|
||||
this.currentVideoStream = newStream;
|
||||
this.recordedVideo.srcObject = newStream;
|
||||
this.currentVideo = videoFile;
|
||||
// // 设置新的视频流
|
||||
// this.currentVideoStream = newStream;
|
||||
// this.recordedVideo.srcObject = newStream;
|
||||
// this.currentVideo = videoFile;
|
||||
|
||||
// 确保视频开始播放
|
||||
try {
|
||||
await this.recordedVideo.play();
|
||||
this.logMessage('视频元素开始播放', 'info');
|
||||
// // 确保视频开始播放
|
||||
// try {
|
||||
// await this.recordedVideo.play();
|
||||
// this.logMessage('视频元素开始播放', 'info');
|
||||
|
||||
// 移除加载状态,添加播放状态
|
||||
this.recordedVideo.classList.remove('loading');
|
||||
this.recordedVideo.classList.add('playing');
|
||||
} catch (playError) {
|
||||
this.logMessage(`视频播放失败: ${playError.message}`, 'error');
|
||||
this.recordedVideo.classList.remove('loading');
|
||||
}
|
||||
// // 移除加载状态,添加播放状态
|
||||
// this.recordedVideo.classList.remove('loading');
|
||||
// this.recordedVideo.classList.add('playing');
|
||||
// } catch (playError) {
|
||||
// this.logMessage(`视频播放失败: ${playError.message}`, 'error');
|
||||
// this.recordedVideo.classList.remove('loading');
|
||||
// }
|
||||
|
||||
// 现在停止旧的视频流
|
||||
if (this.currentVideoStream !== newStream) {
|
||||
const oldStream = this.currentVideoStream;
|
||||
setTimeout(() => {
|
||||
if (oldStream) {
|
||||
oldStream.getTracks().forEach(track => {
|
||||
track.stop();
|
||||
this.logMessage(`已停止旧轨道: ${track.kind}`, 'info');
|
||||
});
|
||||
}
|
||||
}, 1000); // 延迟1秒停止旧流,确保新流已经稳定
|
||||
}
|
||||
// // 现在停止旧的视频流
|
||||
// if (this.currentVideoStream !== newStream) {
|
||||
// const oldStream = this.currentVideoStream;
|
||||
// setTimeout(() => {
|
||||
// if (oldStream) {
|
||||
// oldStream.getTracks().forEach(track => {
|
||||
// track.stop();
|
||||
// this.logMessage(`已停止旧轨道: ${track.kind}`, 'info');
|
||||
// });
|
||||
// }
|
||||
// }, 1000); // 延迟1秒停止旧流,确保新流已经稳定
|
||||
// }
|
||||
|
||||
if (text) {
|
||||
this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`;
|
||||
this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success');
|
||||
} else {
|
||||
this.currentVideoName.textContent = `视频流: ${videoFile}`;
|
||||
this.logMessage(`成功切换到视频流: ${videoFile}`, 'success');
|
||||
}
|
||||
// if (text) {
|
||||
// this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`;
|
||||
// this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success');
|
||||
// } else {
|
||||
// this.currentVideoName.textContent = `视频流: ${videoFile}`;
|
||||
// this.logMessage(`成功切换到视频流: ${videoFile}`, 'success');
|
||||
// }
|
||||
|
||||
// 检查切换后的状态
|
||||
setTimeout(() => {
|
||||
this.checkVideoStreamStatus();
|
||||
}, 1000);
|
||||
// // 检查切换后的状态
|
||||
// setTimeout(() => {
|
||||
// this.checkVideoStreamStatus();
|
||||
// }, 1000);
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`切换视频流失败: ${error.message}`, 'error');
|
||||
this.recordedVideo.classList.remove('loading');
|
||||
// } catch (error) {
|
||||
// this.logMessage(`切换视频流失败: ${error.message}`, 'error');
|
||||
// this.recordedVideo.classList.remove('loading');
|
||||
|
||||
// 如果切换失败,尝试回到默认视频
|
||||
if (videoFile !== this.defaultVideo) {
|
||||
this.logMessage('尝试回到默认视频', 'info');
|
||||
await this.switchVideoStream(this.defaultVideo, 'fallback');
|
||||
}
|
||||
}
|
||||
// // 如果切换失败,尝试回到默认视频
|
||||
// if (videoFile !== this.defaultVideo) {
|
||||
// this.logMessage('尝试回到默认视频', 'info');
|
||||
// await this.switchVideoStream(this.defaultVideo, 'fallback');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 修改原有的switchVideoStream方法,使用新的平滑切换
|
||||
async switchVideoStream(videoFile, type = '', text = '') {
|
||||
// 使用平滑切换方法
|
||||
return await this.switchVideoStreamSmooth(videoFile, type, text);
|
||||
}
|
||||
|
||||
// 使用replaceTrack方式切换视频
|
||||
@ -605,6 +616,153 @@ class WebRTCChat {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增平滑视频切换方法
|
||||
async switchVideoStreamSmooth(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始平滑切换视频流: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 显示加载指示器
|
||||
this.showVideoLoading();
|
||||
|
||||
// 确定当前活跃的视频元素和缓冲元素
|
||||
const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer;
|
||||
const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo;
|
||||
|
||||
// 检查是否已缓存
|
||||
const isCached = this.videoStreams.has(videoFile);
|
||||
let newStream;
|
||||
|
||||
if (isCached) {
|
||||
const cachedStream = this.videoStreams.get(videoFile);
|
||||
if (cachedStream && cachedStream.getTracks().length > 0) {
|
||||
newStream = cachedStream;
|
||||
this.logMessage(`使用缓存视频流: ${videoFile}`, 'success');
|
||||
} else {
|
||||
newStream = await this.createVideoStream(videoFile);
|
||||
}
|
||||
} else {
|
||||
newStream = await this.createVideoStream(videoFile);
|
||||
}
|
||||
|
||||
// 在缓冲视频元素中预加载新视频
|
||||
bufferVideo.srcObject = newStream;
|
||||
|
||||
// 等待缓冲视频准备就绪
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('视频加载超时'));
|
||||
}, 5000);
|
||||
|
||||
const onCanPlay = () => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = (error) => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
bufferVideo.addEventListener('canplay', onCanPlay);
|
||||
bufferVideo.addEventListener('error', onError);
|
||||
|
||||
// 开始播放缓冲视频
|
||||
bufferVideo.play().catch(onError);
|
||||
});
|
||||
|
||||
// 隐藏加载指示器
|
||||
this.hideVideoLoading();
|
||||
|
||||
// 执行淡入淡出切换
|
||||
await this.performVideoTransition(currentVideo, bufferVideo);
|
||||
|
||||
// 更新当前视频流和活跃元素
|
||||
this.currentVideoStream = newStream;
|
||||
this.currentVideo = videoFile;
|
||||
this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main';
|
||||
|
||||
// 停止旧视频流(延迟停止避免闪烁)
|
||||
setTimeout(() => {
|
||||
if (currentVideo.srcObject && currentVideo.srcObject !== newStream) {
|
||||
currentVideo.srcObject.getTracks().forEach(track => track.stop());
|
||||
currentVideo.srcObject = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 更新WebRTC连接中的视频轨道
|
||||
if (this.peerConnection && this.videoSender) {
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
if (newVideoTrack) {
|
||||
await this.videoSender.replaceTrack(newVideoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新显示信息
|
||||
if (text) {
|
||||
this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`;
|
||||
this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success');
|
||||
} else {
|
||||
this.currentVideoName.textContent = `视频流: ${videoFile}`;
|
||||
this.logMessage(`成功切换到视频流: ${videoFile}`, 'success');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`平滑切换视频流失败: ${error.message}`, 'error');
|
||||
this.hideVideoLoading();
|
||||
|
||||
// 如果切换失败,尝试回到默认视频
|
||||
if (videoFile !== this.defaultVideo) {
|
||||
this.logMessage('尝试回到默认视频', 'info');
|
||||
await this.switchVideoStreamSmooth(this.defaultVideo, 'fallback');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行视频过渡动画
|
||||
async performVideoTransition(currentVideo, bufferVideo) {
|
||||
return new Promise((resolve) => {
|
||||
// 添加切换类
|
||||
currentVideo.classList.add('switching');
|
||||
bufferVideo.classList.add('switching');
|
||||
|
||||
// 等待CSS过渡完成
|
||||
setTimeout(() => {
|
||||
// 移除切换类
|
||||
currentVideo.classList.remove('switching');
|
||||
bufferVideo.classList.remove('switching');
|
||||
|
||||
// 交换z-index
|
||||
if (currentVideo.style.zIndex === '2' || !currentVideo.style.zIndex) {
|
||||
currentVideo.style.zIndex = '1';
|
||||
bufferVideo.style.zIndex = '2';
|
||||
} else {
|
||||
currentVideo.style.zIndex = '2';
|
||||
bufferVideo.style.zIndex = '1';
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 500); // 与CSS过渡时间一致
|
||||
});
|
||||
}
|
||||
|
||||
// 显示加载指示器
|
||||
showVideoLoading() {
|
||||
if (this.videoLoading) {
|
||||
this.videoLoading.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载指示器
|
||||
hideVideoLoading() {
|
||||
if (this.videoLoading) {
|
||||
this.videoLoading.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 开始通话按钮
|
||||
this.startButton.onclick = () => this.startCall();
|
||||
@ -632,8 +790,8 @@ class WebRTCChat {
|
||||
};
|
||||
|
||||
// 语音输入按钮
|
||||
this.startVoiceButton.onclick = () => this.startVoiceRecording();
|
||||
this.stopVoiceButton.onclick = () => this.stopVoiceRecording();
|
||||
// this.startVoiceButton.onclick = () => this.startVoiceRecording();
|
||||
// this.stopVoiceButton.onclick = () => this.stopVoiceRecording();
|
||||
}
|
||||
|
||||
async startCall() {
|
||||
@ -644,11 +802,15 @@ class WebRTCChat {
|
||||
});
|
||||
|
||||
await this.createPeerConnection();
|
||||
|
||||
this.startVoiceRecording()
|
||||
// this.audioProcessor.startRecording()
|
||||
this.startButton.disabled = true;
|
||||
this.stopButton.disabled = false;
|
||||
|
||||
this.updateAudioStatus('已连接', 'connected');
|
||||
this.stopButton.disabled = false;
|
||||
|
||||
// 显示结束通话按钮
|
||||
this.stopButton.classList.add('show');
|
||||
|
||||
this.updateAudioStatus('已连接', 'connected');
|
||||
this.logMessage('音频通话已开始', 'success');
|
||||
|
||||
// 确保视频映射已加载
|
||||
@ -690,8 +852,14 @@ class WebRTCChat {
|
||||
this.startButton.disabled = false;
|
||||
this.stopButton.disabled = true;
|
||||
|
||||
// 隐藏结束通话按钮
|
||||
this.stopButton.classList.remove('show');
|
||||
|
||||
this.stopVoiceRecording()
|
||||
this.updateAudioStatus('未连接', 'disconnected');
|
||||
this.logMessage('音频通话已结束', 'info');
|
||||
|
||||
|
||||
}
|
||||
|
||||
async createPeerConnection() {
|
||||
@ -864,13 +1032,13 @@ class WebRTCChat {
|
||||
const success = await this.audioProcessor.startRecording();
|
||||
|
||||
if (success) {
|
||||
this.startVoiceButton.disabled = true;
|
||||
this.stopVoiceButton.disabled = false;
|
||||
this.startVoiceButton.classList.add('recording');
|
||||
this.voiceStatus.textContent = '等待语音输入...';
|
||||
// this.startVoiceButton.disabled = true;
|
||||
// this.stopVoiceButton.disabled = false;
|
||||
// this.startVoiceButton.classList.add('recording');
|
||||
// this.voiceStatus.textContent = '等待语音输入...';
|
||||
this.logMessage('高级语音录制已启动', 'success');
|
||||
} else {
|
||||
this.voiceStatus.textContent = '录音启动失败';
|
||||
// this.voiceStatus.textContent = '录音启动失败';
|
||||
}
|
||||
}
|
||||
|
||||
@ -878,16 +1046,21 @@ class WebRTCChat {
|
||||
stopVoiceRecording() {
|
||||
this.audioProcessor.stopRecording();
|
||||
|
||||
this.startVoiceButton.disabled = false;
|
||||
this.stopVoiceButton.disabled = true;
|
||||
this.startVoiceButton.classList.remove('recording');
|
||||
this.voiceStatus.textContent = '点击开始语音输入';
|
||||
// this.startVoiceButton.disabled = false;
|
||||
// this.stopVoiceButton.disabled = true;
|
||||
// this.startVoiceButton.classList.remove('recording');
|
||||
// this.voiceStatus.textContent = '点击开始语音输入';
|
||||
|
||||
this.logMessage('语音录制已停止', 'info');
|
||||
}
|
||||
|
||||
// 处理语音输入结果
|
||||
async handleVoiceInput(text) {
|
||||
if(text == ""){
|
||||
console.log("识别到用户未说话")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据文本查找对应视频
|
||||
let videoFile = this.videoMapping['default'] || this.defaultVideo;
|
||||
for (const [key, value] of Object.entries(this.videoMapping)) {
|
||||
|
||||
@ -87,7 +87,7 @@ async function requestLLMStream({ apiKey, model, messages, onSegment }) {
|
||||
}
|
||||
|
||||
// 如果累积文本长度大于5个字,处理它
|
||||
if (accumulatedText.length > 8 && onSegment) {
|
||||
if (accumulatedText.length > 6 && onSegment) {
|
||||
console.log('检测到完整段落:', accumulatedText);
|
||||
await onSegment(accumulatedText, false);
|
||||
hasProcessed = true;
|
||||
|
||||
@ -66,7 +66,7 @@ async function processAudioQueue() {
|
||||
if (!isPlaying && audioQueue.length > 0) {
|
||||
const audioItem = audioQueue.shift();
|
||||
const sayName = 'say-5s-m-sw'
|
||||
const targetVideo = '5.mp4'
|
||||
const targetVideo = 'd-0.mp4'
|
||||
// 如果是第一个音频片段,触发视频切换
|
||||
if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoWithReplaceTrack) {
|
||||
try {
|
||||
|
||||
BIN
videos/bd-1.mp4
Normal file
BIN
videos/bd-1.mp4
Normal file
Binary file not shown.
BIN
videos/d-0.mp4
Normal file
BIN
videos/d-0.mp4
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user