确保播放第一帧

This commit is contained in:
宋居成 2025-07-28 23:39:01 +08:00
parent b600114ed9
commit a27112d6cb
3 changed files with 163 additions and 31 deletions

View File

@ -24,6 +24,10 @@ class WebRTCChat {
this.currentVideo = null; this.currentVideo = null;
this.videoStreams = new Map(); // 存储不同视频的MediaStream this.videoStreams = new Map(); // 存储不同视频的MediaStream
this.currentVideoStream = null; this.currentVideoStream = null;
// 添加视频相关属性
this.videoSender = null; // WebRTC视频发送器
this.currentVideoStream = null; // 当前视频流
// 初始化音频处理器 // 初始化音频处理器
console.log('开始初始化音频处理器'); console.log('开始初始化音频处理器');
@ -289,12 +293,9 @@ class WebRTCChat {
} }
async createVideoStream(videoFile) { async createVideoStream(videoFile) {
// 如果已经缓存了这个视频流,直接返回 // 检查缓存,但为每个视频创建独立的播放实例
if (this.videoStreams.has(videoFile)) { const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
this.logMessage(`使用缓存的视频流: ${videoFile}`, 'info');
return this.videoStreams.get(videoFile);
}
try { try {
this.logMessage(`开始创建视频流: ${videoFile}`, 'info'); this.logMessage(`开始创建视频流: ${videoFile}`, 'info');
@ -306,26 +307,25 @@ class WebRTCChat {
video.src = `/videos/${videoFile}`; video.src = `/videos/${videoFile}`;
video.muted = true; video.muted = true;
video.loop = true; video.loop = true;
video.autoplay = true; video.autoplay = false; // 手动控制播放
video.crossOrigin = 'anonymous'; // 添加跨域支持 video.crossOrigin = 'anonymous';
video.playsInline = true; // 添加playsInline属性 video.playsInline = true;
// 预加载视频但不播放
video.preload = 'auto';
// 等待视频加载完成 // 等待视频加载完成
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
video.onloadedmetadata = () => { video.onloadeddata = () => {
this.logMessage(`视频元数据加载完成: ${videoFile}`, 'info'); this.logMessage(`视频数据加载完成: ${videoFile}`, 'info');
// 确保从第一帧开始
video.currentTime = 0;
resolve(); resolve();
}; };
video.onerror = (error) => { video.onerror = (error) => {
this.logMessage(`视频加载失败: ${videoFile}`, 'error'); this.logMessage(`视频加载失败: ${videoFile}`, 'error');
reject(error); reject(error);
}; };
video.onloadstart = () => {
this.logMessage(`开始加载视频: ${videoFile}`, 'info');
};
video.oncanplay = () => {
this.logMessage(`视频可以播放: ${videoFile}`, 'info');
};
}); });
// 创建MediaStream // 创建MediaStream
@ -338,22 +338,27 @@ class WebRTCChat {
this.logMessage(`Canvas尺寸: ${canvas.width}x${canvas.height}`, 'info'); this.logMessage(`Canvas尺寸: ${canvas.width}x${canvas.height}`, 'info');
// 先绘制第一帧到canvas避免黑屏
if (video.readyState >= video.HAVE_CURRENT_DATA) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
this.logMessage('已绘制第一帧到Canvas', 'info');
}
// 开始播放视频 // 开始播放视频
try { try {
await video.play(); await video.play();
this.logMessage(`视频开始播放: ${videoFile}`, 'info'); this.logMessage(`视频开始播放: ${videoFile}`, 'info');
} catch (playError) { } catch (playError) {
this.logMessage(`视频播放失败: ${playError.message}`, 'error'); this.logMessage(`视频播放失败: ${playError.message}`, 'error');
// 即使播放失败也继续创建流
} }
// 等待视频开始播放 // 等待视频真正开始播放
await new Promise(resolve => { await new Promise(resolve => {
const checkPlay = () => { const checkPlay = () => {
if (video.readyState >= video.HAVE_CURRENT_DATA) { if (video.readyState >= video.HAVE_CURRENT_DATA && !video.paused) {
resolve(); resolve();
} else { } else {
setTimeout(checkPlay, 100); setTimeout(checkPlay, 50);
} }
}; };
checkPlay(); checkPlay();
@ -364,8 +369,7 @@ class WebRTCChat {
let isDrawing = false; let isDrawing = false;
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)) { // 约60fps
isDrawing = true; isDrawing = true;
lastDrawTime = now; lastDrawTime = now;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
@ -378,16 +382,25 @@ class WebRTCChat {
drawFrame(); drawFrame();
// 从canvas创建MediaStream // 从canvas创建MediaStream
const stream = canvas.captureStream(30); // 30fps const stream = canvas.captureStream(30);
// 等待流创建完成并稳定 // 等待流稳定
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, 500); // 给更多时间让流稳定 setTimeout(resolve, 200); // 减少等待时间
}); });
this.logMessage(`视频流创建成功: ${videoFile}`, 'success'); this.logMessage(`视频流创建成功: ${videoFile}`, 'success');
// 缓存这个视频流 // 使用有限缓存策略最多缓存3个视频流
if (this.videoStreams.size >= 3) {
const firstKey = this.videoStreams.keys().next().value;
const oldStream = this.videoStreams.get(firstKey);
if (oldStream) {
oldStream.getTracks().forEach(track => track.stop());
}
this.videoStreams.delete(firstKey);
}
this.videoStreams.set(videoFile, stream); this.videoStreams.set(videoFile, stream);
return stream; return stream;
@ -504,6 +517,68 @@ class WebRTCChat {
} }
} }
// 使用replaceTrack方式切换视频
async switchVideoWithReplaceTrack(videoFile, type = '', text = '') {
try {
this.logMessage(`开始使用replaceTrack切换视频: ${videoFile}`, 'info');
// 创建新的视频流
const newVideoStream = await this.createVideoStream(videoFile);
const newVideoTrack = newVideoStream.getVideoTracks()[0];
if (!newVideoTrack) {
throw new Error('新视频流中没有视频轨道');
}
// 如果有WebRTC连接且有视频发送器使用replaceTrack
if (this.peerConnection && this.videoSender) {
await this.videoSender.replaceTrack(newVideoTrack);
this.logMessage('WebRTC视频轨道替换成功', 'success');
}
// 同时更新本地视频显示
if (this.recordedVideo) {
// 停止当前视频流
if (this.currentVideoStream) {
this.currentVideoStream.getTracks().forEach(track => track.stop());
}
// 设置新的视频流
this.recordedVideo.srcObject = newVideoStream;
this.currentVideoStream = newVideoStream;
// 确保视频播放
try {
await this.recordedVideo.play();
this.logMessage(`本地视频切换成功: ${videoFile}`, '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;
}
}
bindEvents() { bindEvents() {
// 开始通话按钮 // 开始通话按钮
this.startButton.onclick = () => this.startCall(); this.startButton.onclick = () => this.startCall();
@ -542,7 +617,7 @@ class WebRTCChat {
audio: true audio: true
}); });
this.createPeerConnection(); await this.createPeerConnection();
this.startButton.disabled = true; this.startButton.disabled = true;
this.stopButton.disabled = false; this.stopButton.disabled = false;
@ -593,7 +668,7 @@ class WebRTCChat {
this.logMessage('音频通话已结束', 'info'); this.logMessage('音频通话已结束', 'info');
} }
createPeerConnection() { async createPeerConnection() {
const configuration = { const configuration = {
iceServers: [ iceServers: [
{ urls: "stun:stun.qq.com:3478" }, { urls: "stun:stun.qq.com:3478" },
@ -610,6 +685,19 @@ class WebRTCChat {
this.peerConnection.addTrack(track, this.localStream); 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');
}
// 处理远程流 // 处理远程流
this.peerConnection.ontrack = (event) => { this.peerConnection.ontrack = (event) => {
this.remoteVideo.srcObject = event.streams[0]; this.remoteVideo.srcObject = event.streams[0];

View File

@ -70,7 +70,7 @@ async function processAudioQueue() {
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.switchVideoStream(targetVideo, 'audio', 'say-5s-m-sw'); await window.webrtcApp.switchVideoWithReplaceTrack(targetVideo, 'audio', 'say-5s-m-sw');
isFirstChunk = false; isFirstChunk = false;
window.webrtcApp.currentVideoTag = sayName; window.webrtcApp.currentVideoTag = sayName;
} catch (error) { } catch (error) {
@ -90,7 +90,7 @@ async function processAudioQueue() {
if (window.webrtcApp.currentVideoTag != text) { if (window.webrtcApp.currentVideoTag != text) {
window.webrtcApp.currentVideoTag = text window.webrtcApp.currentVideoTag = text
await window.webrtcApp.switchVideoStream(window.webrtcApp.defaultVideo, 'audio', text); await window.webrtcApp.switchVideoWithReplaceTrack(window.webrtcApp.defaultVideo, 'audio', text);
} }
console.log('音频队列处理完成'); console.log('音频队列处理完成');
} }

View File

@ -445,4 +445,48 @@ header p {
/* 视频播放时的样式 */ /* 视频播放时的样式 */
#recordedVideo.playing { #recordedVideo.playing {
opacity: 1; opacity: 1;
}
#recordedVideo {
transition: opacity 0.2s ease-in-out;
background-color: #1a1a1a; /* 深灰色背景,避免纯黑 */
}
#recordedVideo.loading {
opacity: 0.8; /* 加载时稍微降低透明度,但不完全隐藏 */
}
#recordedVideo.playing {
opacity: 1;
}
/* 添加加载指示器 */
.video-container {
position: relative;
}
.video-container::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin: -20px 0 0 -20px;
border: 3px solid #333;
border-top: 3px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
opacity: 0;
z-index: 10;
transition: opacity 0.3s;
}
.video-container.loading::before {
opacity: 1;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} }