确保播放第一帧
This commit is contained in:
parent
b600114ed9
commit
a27112d6cb
144
src/index.js
144
src/index.js
@ -25,6 +25,10 @@ class WebRTCChat {
|
||||
this.videoStreams = new Map(); // 存储不同视频的MediaStream
|
||||
this.currentVideoStream = null;
|
||||
|
||||
// 添加视频相关属性
|
||||
this.videoSender = null; // WebRTC视频发送器
|
||||
this.currentVideoStream = null; // 当前视频流
|
||||
|
||||
// 初始化音频处理器
|
||||
console.log('开始初始化音频处理器');
|
||||
// 初始化音频处理器
|
||||
@ -289,11 +293,8 @@ class WebRTCChat {
|
||||
}
|
||||
|
||||
async createVideoStream(videoFile) {
|
||||
// 如果已经缓存了这个视频流,直接返回
|
||||
if (this.videoStreams.has(videoFile)) {
|
||||
this.logMessage(`使用缓存的视频流: ${videoFile}`, 'info');
|
||||
return this.videoStreams.get(videoFile);
|
||||
}
|
||||
// 检查缓存,但为每个视频创建独立的播放实例
|
||||
const cacheKey = `${videoFile}_${Date.now()}`; // 添加时间戳确保唯一性
|
||||
|
||||
try {
|
||||
this.logMessage(`开始创建视频流: ${videoFile}`, 'info');
|
||||
@ -306,26 +307,25 @@ class WebRTCChat {
|
||||
video.src = `/videos/${videoFile}`;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.autoplay = true;
|
||||
video.crossOrigin = 'anonymous'; // 添加跨域支持
|
||||
video.playsInline = true; // 添加playsInline属性
|
||||
video.autoplay = false; // 手动控制播放
|
||||
video.crossOrigin = 'anonymous';
|
||||
video.playsInline = true;
|
||||
|
||||
// 预加载视频但不播放
|
||||
video.preload = 'auto';
|
||||
|
||||
// 等待视频加载完成
|
||||
await new Promise((resolve, reject) => {
|
||||
video.onloadedmetadata = () => {
|
||||
this.logMessage(`视频元数据加载完成: ${videoFile}`, 'info');
|
||||
video.onloadeddata = () => {
|
||||
this.logMessage(`视频数据加载完成: ${videoFile}`, 'info');
|
||||
// 确保从第一帧开始
|
||||
video.currentTime = 0;
|
||||
resolve();
|
||||
};
|
||||
video.onerror = (error) => {
|
||||
this.logMessage(`视频加载失败: ${videoFile}`, 'error');
|
||||
reject(error);
|
||||
};
|
||||
video.onloadstart = () => {
|
||||
this.logMessage(`开始加载视频: ${videoFile}`, 'info');
|
||||
};
|
||||
video.oncanplay = () => {
|
||||
this.logMessage(`视频可以播放: ${videoFile}`, 'info');
|
||||
};
|
||||
});
|
||||
|
||||
// 创建MediaStream
|
||||
@ -338,22 +338,27 @@ class WebRTCChat {
|
||||
|
||||
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 {
|
||||
await video.play();
|
||||
this.logMessage(`视频开始播放: ${videoFile}`, 'info');
|
||||
} catch (playError) {
|
||||
this.logMessage(`视频播放失败: ${playError.message}`, 'error');
|
||||
// 即使播放失败也继续创建流
|
||||
}
|
||||
|
||||
// 等待视频开始播放
|
||||
// 等待视频真正开始播放
|
||||
await new Promise(resolve => {
|
||||
const checkPlay = () => {
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA && !video.paused) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkPlay, 100);
|
||||
setTimeout(checkPlay, 50);
|
||||
}
|
||||
};
|
||||
checkPlay();
|
||||
@ -364,8 +369,7 @@ class WebRTCChat {
|
||||
let isDrawing = false;
|
||||
const drawFrame = () => {
|
||||
const now = performance.now();
|
||||
// 限制绘制频率,确保平滑过渡
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) { // 约60fps
|
||||
if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing && (now - lastDrawTime > 16)) {
|
||||
isDrawing = true;
|
||||
lastDrawTime = now;
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
@ -378,16 +382,25 @@ class WebRTCChat {
|
||||
drawFrame();
|
||||
|
||||
// 从canvas创建MediaStream
|
||||
const stream = canvas.captureStream(30); // 30fps
|
||||
const stream = canvas.captureStream(30);
|
||||
|
||||
// 等待流创建完成并稳定
|
||||
// 等待流稳定
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500); // 给更多时间让流稳定
|
||||
setTimeout(resolve, 200); // 减少等待时间
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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() {
|
||||
// 开始通话按钮
|
||||
this.startButton.onclick = () => this.startCall();
|
||||
@ -542,7 +617,7 @@ class WebRTCChat {
|
||||
audio: true
|
||||
});
|
||||
|
||||
this.createPeerConnection();
|
||||
await this.createPeerConnection();
|
||||
|
||||
this.startButton.disabled = true;
|
||||
this.stopButton.disabled = false;
|
||||
@ -593,7 +668,7 @@ class WebRTCChat {
|
||||
this.logMessage('音频通话已结束', 'info');
|
||||
}
|
||||
|
||||
createPeerConnection() {
|
||||
async createPeerConnection() {
|
||||
const configuration = {
|
||||
iceServers: [
|
||||
{ urls: "stun:stun.qq.com:3478" },
|
||||
@ -610,6 +685,19 @@ 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');
|
||||
}
|
||||
|
||||
// 处理远程流
|
||||
this.peerConnection.ontrack = (event) => {
|
||||
this.remoteVideo.srcObject = event.streams[0];
|
||||
|
||||
@ -70,7 +70,7 @@ async function processAudioQueue() {
|
||||
if (sayName != window.webrtcApp.currentVideoTag && window.webrtcApp && window.webrtcApp.handleTextInput) {
|
||||
try {
|
||||
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;
|
||||
window.webrtcApp.currentVideoTag = sayName;
|
||||
} catch (error) {
|
||||
@ -90,7 +90,7 @@ async function processAudioQueue() {
|
||||
if (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('音频队列处理完成');
|
||||
}
|
||||
|
||||
@ -446,3 +446,47 @@ header p {
|
||||
#recordedVideo.playing {
|
||||
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); }
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user