启用webrtc ,喝咖啡不说话视频去水印
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3m57s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3m57s
This commit is contained in:
parent
4e256170b0
commit
1d0911e74a
@ -136,7 +136,7 @@ const scenes = [
|
||||
},
|
||||
{
|
||||
name: '喝茶',
|
||||
defaultVideo: '8-5-hc-bd-female.mp4',
|
||||
defaultVideo: '8-8-hc-bd-2.mp4',
|
||||
interactionVideo: '8-5-hc-sh-female.mp4',
|
||||
tag: 'tea',
|
||||
apiKey: 'bot-20250805140055-ccdr6' // 喝茶场景的API key
|
||||
|
||||
623
src/index.js
623
src/index.js
@ -27,6 +27,9 @@ class WebRTCChat {
|
||||
this.currentVideo = null;
|
||||
this.videoStreams = new Map(); // 存储不同视频的MediaStream
|
||||
this.currentVideoStream = null;
|
||||
this.precreatedStreams = new Map(); // 预创建的视频流
|
||||
this.importantVideos = ['d-3s.mp4', 's-1.mp4']; // 重要视频列表
|
||||
this.isInitialized = false;
|
||||
|
||||
// 添加视频相关属性
|
||||
this.videoSender = null; // WebRTC视频发送器
|
||||
@ -75,6 +78,13 @@ class WebRTCChat {
|
||||
});
|
||||
}, 500); // 延迟2秒开始预加载,避免影响
|
||||
|
||||
// 预创建重要视频流
|
||||
setTimeout(() => {
|
||||
this.precreateImportantVideos().catch(error => {
|
||||
this.logMessage(`预创建重要视频流失败: ${error.message}`, 'error');
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
window.webrtcApp = this;
|
||||
}
|
||||
|
||||
@ -331,6 +341,116 @@ class WebRTCChat {
|
||||
});
|
||||
}
|
||||
|
||||
// 预创建重要视频流
|
||||
async precreateImportantVideos() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
this.logMessage('开始预创建重要视频流...', 'info');
|
||||
|
||||
for (const videoFile of [this.interactionVideo, this.defaultVideo]) {
|
||||
try {
|
||||
const stream = await this.createVideoStreamOptimized(videoFile);
|
||||
this.precreatedStreams.set(videoFile, stream);
|
||||
this.logMessage(`预创建视频流成功: ${videoFile}`, 'success');
|
||||
} catch (error) {
|
||||
this.logMessage(`预创建视频流失败: ${videoFile} - ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
this.logMessage('重要视频流预创建完成', 'success');
|
||||
}
|
||||
|
||||
// 优化的视频流创建方法
|
||||
async createVideoStreamOptimized(videoFile) {
|
||||
try {
|
||||
// 先测试视频文件是否存在
|
||||
await this.testVideoFile(videoFile);
|
||||
|
||||
// 创建video元素
|
||||
const video = document.createElement('video');
|
||||
video.src = `/videos/${videoFile}`;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.autoplay = false;
|
||||
video.crossOrigin = 'anonymous';
|
||||
video.playsInline = true;
|
||||
video.preload = 'auto';
|
||||
|
||||
// 等待视频加载
|
||||
await new Promise((resolve, reject) => {
|
||||
video.onloadeddata = () => {
|
||||
video.currentTime = 0;
|
||||
resolve();
|
||||
};
|
||||
video.onerror = reject;
|
||||
});
|
||||
|
||||
// 创建优化的Canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d', {
|
||||
alpha: false, // 禁用透明度以提高性能
|
||||
willReadFrequently: false // 优化Canvas性能
|
||||
});
|
||||
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 480;
|
||||
|
||||
// 绘制第一帧
|
||||
await new Promise((resolve) => {
|
||||
const drawFirstFrame = () => {
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(drawFirstFrame, 10);
|
||||
}
|
||||
};
|
||||
drawFirstFrame();
|
||||
});
|
||||
|
||||
// 启动视频播放
|
||||
await video.play();
|
||||
|
||||
// 优化的帧绘制 - 使用更高效的节流
|
||||
let animationId;
|
||||
let lastFrameTime = 0;
|
||||
const targetFPS = 30;
|
||||
const frameInterval = 1000 / targetFPS;
|
||||
|
||||
const drawFrame = (currentTime) => {
|
||||
if (currentTime - lastFrameTime >= frameInterval) {
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
lastFrameTime = currentTime;
|
||||
}
|
||||
animationId = requestAnimationFrame(drawFrame);
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(drawFrame);
|
||||
|
||||
// 创建MediaStream
|
||||
const stream = canvas.captureStream(30);
|
||||
|
||||
// 存储清理函数
|
||||
stream._cleanup = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
video.pause();
|
||||
video.src = '';
|
||||
video.load();
|
||||
};
|
||||
|
||||
return stream;
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`优化视频流创建失败 ${videoFile}: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createVideoStream(videoFile) {
|
||||
// 检查缓存,但为每个视频创建独立的播放实例
|
||||
const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
|
||||
@ -580,160 +700,50 @@ class WebRTCChat {
|
||||
// }
|
||||
// }
|
||||
|
||||
// 修改原有的switchVideoStream方法,使用新的平滑切换
|
||||
// 修改视频切换方法,直接使用预加载视频切换,不使用WebRTC传输
|
||||
async switchVideoStream(videoFile, type = '', text = '') {
|
||||
// 使用平滑切换方法
|
||||
return await this.switchVideoStreamSmooth(videoFile, type, text);
|
||||
}
|
||||
|
||||
// 使用replaceTrack方式切换视频
|
||||
async switchVideoWithReplaceTrack(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始使用replaceTrack切换视频: ${videoFile}`, 'info');
|
||||
this.logMessage(`开始切换视频: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 创建新的视频流
|
||||
const newVideoStream = await this.createVideoStream(videoFile);
|
||||
const newVideoTrack = newVideoStream.getVideoTracks()[0];
|
||||
let newVideoStream;
|
||||
let isUsingPrecreated = false;
|
||||
|
||||
if (!newVideoTrack) {
|
||||
throw new Error('新视频流中没有视频轨道');
|
||||
// 首先检查预创建的视频流
|
||||
if (this.precreatedStreams.has(videoFile)) {
|
||||
newVideoStream = this.precreatedStreams.get(videoFile);
|
||||
isUsingPrecreated = true;
|
||||
this.logMessage(`使用预创建视频流: ${videoFile}`, 'success');
|
||||
} else {
|
||||
// 检查缓存
|
||||
if (this.videoStreams.has(videoFile)) {
|
||||
newVideoStream = this.videoStreams.get(videoFile);
|
||||
this.logMessage(`使用缓存视频流: ${videoFile}`, 'success');
|
||||
} else {
|
||||
// 创建新的视频流
|
||||
newVideoStream = await this.createVideoStream(videoFile);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有WebRTC连接且有视频发送器,使用replaceTrack
|
||||
if (this.peerConnection && this.videoSender) {
|
||||
await this.videoSender.replaceTrack(newVideoTrack);
|
||||
this.logMessage('WebRTC视频轨道替换成功', 'success');
|
||||
}
|
||||
|
||||
// 同时更新本地视频显示
|
||||
// 直接切换本地视频显示,不使用WebRTC传输
|
||||
if (this.recordedVideo) {
|
||||
// 停止当前视频流
|
||||
if (this.currentVideoStream) {
|
||||
// 停止当前视频流(但不停止预创建的流)
|
||||
if (this.currentVideoStream && !this.precreatedStreams.has(this.currentVideo)) {
|
||||
this.currentVideoStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// 设置新的视频流
|
||||
this.recordedVideo.srcObject = newVideoStream;
|
||||
this.currentVideoStream = newVideoStream;
|
||||
this.currentVideo = videoFile;
|
||||
|
||||
// 使用预创建流时减少等待时间
|
||||
const waitTime = isUsingPrecreated ? 10 : 50;
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
|
||||
// 确保视频播放
|
||||
try {
|
||||
await this.recordedVideo.play();
|
||||
this.logMessage(`本地视频切换成功: ${videoFile}`, 'success');
|
||||
this.logMessage(`视频切换完成: ${videoFile} (${type})`, 'success');
|
||||
} catch (playError) {
|
||||
this.logMessage(`本地视频播放失败: ${playError.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 记录切换信息
|
||||
if (type && text) {
|
||||
this.logMessage(`视频切换完成 - 类型: ${type}, 文本: ${text}`, 'info');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`replaceTrack视频切换失败: ${error.message}`, 'error');
|
||||
console.error('switchVideoWithReplaceTrack error:', error);
|
||||
|
||||
// 回退到原有的切换方式
|
||||
try {
|
||||
await this.switchVideoStream(videoFile, type, text);
|
||||
this.logMessage('已回退到原有视频切换方式', 'info');
|
||||
} catch (fallbackError) {
|
||||
this.logMessage(`回退切换也失败: ${fallbackError.message}`, 'error');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增平滑视频切换方法
|
||||
async switchVideoStreamSmooth(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始平滑切换视频流: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 检查是否已缓存,如果已缓存则不显示加载指示器
|
||||
const isCached = this.videoStreams.has(videoFile);
|
||||
if (!isCached) {
|
||||
this.showVideoLoading();
|
||||
}
|
||||
|
||||
// 确定当前活跃的视频元素和缓冲元素
|
||||
const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer;
|
||||
const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo;
|
||||
|
||||
// 检查是否已缓存
|
||||
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);
|
||||
});
|
||||
|
||||
// 只有显示了加载指示器才隐藏
|
||||
if (!isCached) {
|
||||
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);
|
||||
this.logMessage(`视频播放失败: ${playError.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@ -746,12 +756,241 @@ class WebRTCChat {
|
||||
this.logMessage(`成功切换到视频流: ${videoFile}`, 'success');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`平滑切换视频流失败: ${error.message}`, 'error');
|
||||
// 确保隐藏加载指示器
|
||||
this.logMessage(`视频切换失败: ${error.message}`, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加视频帧缓存方法
|
||||
captureVideoFrame(videoElement) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = videoElement.videoWidth || videoElement.clientWidth;
|
||||
canvas.height = videoElement.videoHeight || videoElement.clientHeight;
|
||||
|
||||
// 绘制当前视频帧到canvas
|
||||
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.8);
|
||||
}
|
||||
|
||||
// 创建静态帧显示元素
|
||||
createFrameOverlay(frameData) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundImage = `url(${frameData})`;
|
||||
overlay.style.backgroundSize = 'cover';
|
||||
overlay.style.backgroundPosition = 'center';
|
||||
overlay.style.backgroundRepeat = 'no-repeat';
|
||||
overlay.style.zIndex = '10';
|
||||
overlay.style.pointerEvents = 'none';
|
||||
overlay.style.willChange = 'auto'; // 优化渲染性能
|
||||
overlay.id = 'video-frame-overlay';
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
// 新增平滑视频切换方法 - 等待完全传输后切换
|
||||
async switchVideoStreamSmooth(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始等待视频流传输完成: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 1. 获取当前和缓冲视频元素
|
||||
const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer;
|
||||
const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo;
|
||||
|
||||
// 2. 检查是否已缓存
|
||||
const isCached = this.videoStreams.has(videoFile);
|
||||
if (!isCached) {
|
||||
this.showVideoLoading();
|
||||
}
|
||||
|
||||
// 3. 准备新视频流
|
||||
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);
|
||||
}
|
||||
|
||||
// 4. 在缓冲视频元素中预加载新视频
|
||||
bufferVideo.srcObject = newStream;
|
||||
|
||||
// 5. 等待视频流完全传输和准备就绪
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('视频流传输超时'));
|
||||
}, 15000); // 增加超时时间到15秒,确保有足够时间传输
|
||||
|
||||
let frameCount = 0;
|
||||
const minFrames = 10; // 增加最小帧数确保传输完整
|
||||
let isStreamReady = false;
|
||||
|
||||
const onReady = () => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('canplaythrough', onCanPlayThrough);
|
||||
bufferVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
bufferVideo.removeEventListener('loadeddata', onLoadedData);
|
||||
resolve();
|
||||
};
|
||||
|
||||
// 检查视频数据是否完全加载
|
||||
const onLoadedData = () => {
|
||||
this.logMessage(`视频数据开始加载: ${videoFile}`, 'info');
|
||||
};
|
||||
|
||||
// 检查视频是否可以流畅播放(数据传输充足)
|
||||
const onCanPlayThrough = () => {
|
||||
this.logMessage(`视频流传输充足,可以流畅播放: ${videoFile}`, 'success');
|
||||
isStreamReady = true;
|
||||
|
||||
// 开始播放并等待稳定帧
|
||||
bufferVideo.play().then(() => {
|
||||
const checkFrameStability = () => {
|
||||
if (bufferVideo.currentTime > 0) {
|
||||
frameCount++;
|
||||
if (frameCount >= minFrames && isStreamReady) {
|
||||
// 额外等待确保传输稳定
|
||||
setTimeout(() => {
|
||||
onReady();
|
||||
}, 200); // 等待200ms确保传输稳定
|
||||
} else {
|
||||
requestAnimationFrame(checkFrameStability);
|
||||
}
|
||||
} else {
|
||||
requestAnimationFrame(checkFrameStability);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(checkFrameStability);
|
||||
}).catch(reject);
|
||||
};
|
||||
|
||||
// 基本播放准备就绪
|
||||
const onCanPlay = () => {
|
||||
if (!isStreamReady) {
|
||||
this.logMessage(`视频基本准备就绪,等待完全传输: ${videoFile}`, 'info');
|
||||
// 如果还没有canplaythrough事件,继续等待
|
||||
}
|
||||
};
|
||||
|
||||
// 时间更新监听(备用检查)
|
||||
const onTimeUpdate = () => {
|
||||
if (bufferVideo.currentTime > 0 && isStreamReady) {
|
||||
frameCount++;
|
||||
if (frameCount >= minFrames) {
|
||||
setTimeout(() => {
|
||||
onReady();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (error) => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('canplaythrough', onCanPlayThrough);
|
||||
bufferVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
bufferVideo.removeEventListener('loadeddata', onLoadedData);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
// 检查当前状态
|
||||
if (bufferVideo.readyState >= 4) { // HAVE_ENOUGH_DATA
|
||||
onCanPlayThrough();
|
||||
} else {
|
||||
bufferVideo.addEventListener('loadeddata', onLoadedData);
|
||||
bufferVideo.addEventListener('canplay', onCanPlay);
|
||||
bufferVideo.addEventListener('canplaythrough', onCanPlayThrough);
|
||||
bufferVideo.addEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.addEventListener('error', onError);
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 最终确认视频流完全准备就绪
|
||||
await new Promise(resolve => {
|
||||
let confirmCount = 0;
|
||||
const maxConfirms = 3;
|
||||
|
||||
const finalCheck = () => {
|
||||
if (bufferVideo.readyState >= 4 && bufferVideo.currentTime > 0) {
|
||||
confirmCount++;
|
||||
if (confirmCount >= maxConfirms) {
|
||||
this.logMessage(`视频流传输完成,准备切换: ${videoFile}`, 'success');
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(finalCheck, 50); // 每50ms检查一次
|
||||
}
|
||||
} else {
|
||||
setTimeout(finalCheck, 50);
|
||||
}
|
||||
};
|
||||
finalCheck();
|
||||
});
|
||||
|
||||
// 7. 执行无缝切换(当前视频继续播放直到新视频完全准备好)
|
||||
bufferVideo.style.zIndex = '2';
|
||||
currentVideo.style.zIndex = '1';
|
||||
|
||||
this.logMessage(`视频切换完成: ${videoFile}`, 'success');
|
||||
|
||||
// 8. 隐藏加载指示器
|
||||
if (!isCached) {
|
||||
this.hideVideoLoading();
|
||||
}
|
||||
|
||||
// 9. 更新状态
|
||||
this.currentVideoStream = newStream;
|
||||
this.currentVideo = videoFile;
|
||||
this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main';
|
||||
|
||||
// 10. 延迟清理旧视频流
|
||||
setTimeout(() => {
|
||||
if (currentVideo.srcObject && currentVideo.srcObject !== newStream) {
|
||||
currentVideo.srcObject.getTracks().forEach(track => track.stop());
|
||||
currentVideo.srcObject = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 11. 更新WebRTC连接
|
||||
if (this.peerConnection && this.videoSender) {
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
if (newVideoTrack) {
|
||||
await this.videoSender.replaceTrack(newVideoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
// 12. 更新显示信息
|
||||
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');
|
||||
@ -759,32 +998,7 @@ class WebRTCChat {
|
||||
}
|
||||
}
|
||||
|
||||
// 执行视频过渡动画
|
||||
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() {
|
||||
@ -804,8 +1018,8 @@ class WebRTCChat {
|
||||
// 开始通话按钮
|
||||
this.startButton.onclick = () => this.startCall();
|
||||
|
||||
// 停止通话按钮
|
||||
this.stopButton.onclick = () => this.stopCall();
|
||||
// 停止通话按钮 - 改为调用 userDisconnect
|
||||
this.stopButton.onclick = () => this.userDisconnect();
|
||||
|
||||
// 静音按钮
|
||||
// this.muteButton.onclick = () => this.toggleMute();
|
||||
@ -855,12 +1069,10 @@ class WebRTCChat {
|
||||
|
||||
async startCall() {
|
||||
try {
|
||||
// 显示等待连接提示
|
||||
this.showConnectionWaiting();
|
||||
|
||||
// 立即隐藏开始通话按钮
|
||||
this.startButton.style.display = 'none';
|
||||
|
||||
// 显示等待连接提示
|
||||
this.showConnectionWaiting();
|
||||
// 切换到通话中图标
|
||||
this.switchToCallingIcon();
|
||||
|
||||
@ -900,7 +1112,7 @@ class WebRTCChat {
|
||||
this.socket.emit('call-started');
|
||||
|
||||
// 开始播放当前场景的默认视频
|
||||
await this.startDefaultVideoStream();
|
||||
await this.precreateImportantVideos();
|
||||
|
||||
// 隐藏等待连接提示
|
||||
this.hideConnectionWaiting();
|
||||
@ -915,16 +1127,16 @@ class WebRTCChat {
|
||||
}
|
||||
|
||||
stopCall() {
|
||||
|
||||
// 隐藏等待连接提示
|
||||
this.hideConnectionWaiting();
|
||||
// 恢复到默认图标
|
||||
this.switchToDefaultIcon();
|
||||
|
||||
// 发送用户关闭连接事件到后端
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('user-disconnect');
|
||||
}
|
||||
// 只有用户主动点击关闭通话时才发送断开事件
|
||||
// 移除自动发送 user-disconnect 事件
|
||||
// if (this.socket && this.socket.connected) {
|
||||
// this.socket.emit('user-disconnect');
|
||||
// }
|
||||
|
||||
// 停止音频处理器
|
||||
if (this.audioProcessor) {
|
||||
@ -948,24 +1160,37 @@ class WebRTCChat {
|
||||
this.stopButton.style.display = 'none';
|
||||
this.stopButton.disabled = true;
|
||||
|
||||
// // 显示头像,隐藏视频
|
||||
// if (this.videoContainer) {
|
||||
// this.videoContainer.classList.remove('calling');
|
||||
// }
|
||||
// 显示开始通话按钮
|
||||
this.startButton.style.display = 'block';
|
||||
this.startButton.disabled = false;
|
||||
|
||||
// 移除页面刷新,保持websocket连接
|
||||
// setTimeout(() => {
|
||||
// window.location.reload();
|
||||
// }, 300);
|
||||
|
||||
setTimeout(() => {
|
||||
// 显示头像,隐藏视频
|
||||
if (this.videoContainer) {
|
||||
this.videoContainer.classList.remove('calling');
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 新增:用户主动断开连接的方法
|
||||
userDisconnect() {
|
||||
// 发送用户关闭连接事件到后端
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('user-disconnect');
|
||||
}
|
||||
|
||||
// 调用停止通话
|
||||
this.stopCall();
|
||||
|
||||
// 延迟刷新,确保服务器处理完断开逻辑
|
||||
console.log('通话已结束,5秒后刷新页面清除缓存...');
|
||||
console.log('用户主动断开,300ms后刷新页面清除缓存...');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
this.startButton.style.display = 'block';
|
||||
this.startButton.disabled = false;
|
||||
}, 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
// 显示头像,隐藏视频
|
||||
if (this.videoContainer) {
|
||||
this.videoContainer.classList.remove('calling');
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@ -1036,18 +1261,8 @@ class WebRTCChat {
|
||||
this.peerConnection.addTrack(track, this.localStream);
|
||||
});
|
||||
|
||||
// 添加初始视频流(默认视频)
|
||||
try {
|
||||
const initialVideoStream = await this.createVideoStream(this.defaultVideo);
|
||||
const videoTrack = initialVideoStream.getVideoTracks()[0];
|
||||
if (videoTrack) {
|
||||
this.videoSender = this.peerConnection.addTrack(videoTrack, initialVideoStream);
|
||||
this.currentVideoStream = initialVideoStream;
|
||||
this.logMessage('初始视频轨道已添加到WebRTC连接', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logMessage(`添加初始视频轨道失败: ${error.message}`, 'error');
|
||||
}
|
||||
// 移除视频轨道添加逻辑,只使用音频进行WebRTC通信
|
||||
// 视频将直接在本地切换,不通过WebRTC传输
|
||||
|
||||
// 处理远程流
|
||||
this.peerConnection.ontrack = (event) => {
|
||||
|
||||
@ -68,10 +68,10 @@ async function processAudioQueue() {
|
||||
const sayName = '8-4-sh'
|
||||
const targetVideo = window.webrtcApp.interactionVideo
|
||||
// 如果是第一个音频片段,触发视频切换
|
||||
if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoWithReplaceTrack) {
|
||||
if (isFirstChunk && sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.switchVideoStream) {
|
||||
try {
|
||||
console.log('--------------触发视频切换:', sayName);
|
||||
window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', '8-4-sh');
|
||||
window.webrtcApp.switchVideoStream(targetVideo, 'audio', '8-4-sh');
|
||||
isFirstChunk = false;
|
||||
window.webrtcApp.currentVideoTag = sayName;
|
||||
} catch (error) {
|
||||
@ -86,13 +86,18 @@ async function processAudioQueue() {
|
||||
}
|
||||
|
||||
isProcessingQueue = false;
|
||||
|
||||
// 等待当前音频播放完成后再切换回默认视频
|
||||
while (isPlaying) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const text = 'default'
|
||||
// await new Promise(resolve => setTimeout(resolve, 500));
|
||||
console.log("音频结束------------------------:", window.webrtcApp.currentVideoTag, isPlaying)
|
||||
if (window.webrtcApp.currentVideoTag != text) {
|
||||
isFirstChunk = true
|
||||
window.webrtcApp.currentVideoTag = text
|
||||
window.webrtcApp.switchVideoWithReplaceTrack(window.webrtcApp.defaultVideo, 'audio', text);
|
||||
window.webrtcApp.switchVideoStream(window.webrtcApp.defaultVideo, 'audio', text);
|
||||
}
|
||||
console.log('音频队列处理完成');
|
||||
}
|
||||
|
||||
BIN
videos/8-8-hc-bd-2.mp4
Normal file
BIN
videos/8-8-hc-bd-2.mp4
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user