使用视频帧保证切换问题
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m49s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m49s
This commit is contained in:
parent
5f1e50c7e9
commit
35c7e82375
183
src/index.js
183
src/index.js
@ -648,24 +648,67 @@ class WebRTCChat {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加视频帧缓存方法
|
||||
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.zIndex = '10';
|
||||
overlay.style.pointerEvents = 'none';
|
||||
overlay.id = 'video-frame-overlay';
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
// 新增平滑视频切换方法
|
||||
async switchVideoStreamSmooth(videoFile, type = '', text = '') {
|
||||
try {
|
||||
this.logMessage(`开始平滑切换视频流: ${videoFile} (${type})`, 'info');
|
||||
this.logMessage(`开始无闪烁切换视频流: ${videoFile} (${type})`, 'info');
|
||||
|
||||
// 检查是否已缓存,如果已缓存则不显示加载指示器
|
||||
// 1. 捕获当前视频的最后一帧
|
||||
const currentVideo = this.activeVideoElement === 'main' ? this.recordedVideo : this.recordedVideoBuffer;
|
||||
const bufferVideo = this.activeVideoElement === 'main' ? this.recordedVideoBuffer : this.recordedVideo;
|
||||
|
||||
let frameData = null;
|
||||
if (currentVideo.readyState >= 2 && currentVideo.currentTime > 0) {
|
||||
frameData = this.captureVideoFrame(currentVideo);
|
||||
}
|
||||
|
||||
// 2. 创建并显示静态帧覆盖层
|
||||
let frameOverlay = null;
|
||||
if (frameData) {
|
||||
frameOverlay = this.createFrameOverlay(frameData);
|
||||
currentVideo.parentElement.appendChild(frameOverlay);
|
||||
}
|
||||
|
||||
// 3. 检查是否已缓存
|
||||
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;
|
||||
|
||||
// 检查是否已缓存
|
||||
// 4. 准备新视频流
|
||||
let newStream;
|
||||
|
||||
if (isCached) {
|
||||
const cachedStream = this.videoStreams.get(videoFile);
|
||||
if (cachedStream && cachedStream.getTracks().length > 0) {
|
||||
@ -678,36 +721,43 @@ class WebRTCChat {
|
||||
newStream = await this.createVideoStream(videoFile);
|
||||
}
|
||||
|
||||
// 在缓冲视频元素中预加载新视频
|
||||
// 5. 在缓冲视频元素中预加载新视频
|
||||
bufferVideo.srcObject = newStream;
|
||||
|
||||
// 等待缓冲视频准备就绪
|
||||
// 6. 等待新视频完全准备好(至少渲染一帧)
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('视频加载超时'));
|
||||
}, 5000);
|
||||
|
||||
const onCanPlay = () => {
|
||||
const onReady = () => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
|
||||
// 确保视频渲染了至少一帧
|
||||
if (bufferVideo.readyState >= 2) {
|
||||
resolve();
|
||||
} else {
|
||||
// 等待第一帧渲染
|
||||
const onTimeUpdate = () => {
|
||||
bufferVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
resolve();
|
||||
};
|
||||
bufferVideo.addEventListener('timeupdate', onTimeUpdate, { once: true });
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onCanPlay = () => {
|
||||
// 开始播放新视频
|
||||
bufferVideo.play().then(() => {
|
||||
// 等待至少一帧渲染
|
||||
if (bufferVideo.currentTime > 0) {
|
||||
onReady();
|
||||
}
|
||||
}).catch(reject);
|
||||
};
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
if (bufferVideo.currentTime > 0) {
|
||||
onReady();
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (error) => {
|
||||
clearTimeout(timeout);
|
||||
bufferVideo.removeEventListener('canplay', onCanPlay);
|
||||
bufferVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.removeEventListener('error', onError);
|
||||
reject(error);
|
||||
};
|
||||
@ -716,27 +766,31 @@ class WebRTCChat {
|
||||
onCanPlay();
|
||||
} else {
|
||||
bufferVideo.addEventListener('canplay', onCanPlay);
|
||||
bufferVideo.addEventListener('timeupdate', onTimeUpdate);
|
||||
bufferVideo.addEventListener('error', onError);
|
||||
}
|
||||
|
||||
// 开始播放缓冲视频
|
||||
bufferVideo.play().catch(onError);
|
||||
});
|
||||
|
||||
// 只有显示了加载指示器才隐藏
|
||||
// 7. 立即切换视频显示(无过渡)
|
||||
bufferVideo.style.zIndex = '2';
|
||||
currentVideo.style.zIndex = '1';
|
||||
|
||||
// 8. 移除静态帧覆盖层
|
||||
if (frameOverlay) {
|
||||
frameOverlay.remove();
|
||||
}
|
||||
|
||||
// 9. 隐藏加载指示器
|
||||
if (!isCached) {
|
||||
this.hideVideoLoading();
|
||||
}
|
||||
|
||||
// 执行淡入淡出切换
|
||||
// await this.performVideoTransition(currentVideo, bufferVideo);
|
||||
|
||||
// 更新当前视频流和活跃元素
|
||||
// 10. 更新状态
|
||||
this.currentVideoStream = newStream;
|
||||
this.currentVideo = videoFile;
|
||||
this.activeVideoElement = this.activeVideoElement === 'main' ? 'buffer' : 'main';
|
||||
|
||||
// 停止旧视频流(延迟停止避免闪烁)
|
||||
// 11. 延迟清理旧视频流
|
||||
setTimeout(() => {
|
||||
if (currentVideo.srcObject && currentVideo.srcObject !== newStream) {
|
||||
currentVideo.srcObject.getTracks().forEach(track => track.stop());
|
||||
@ -744,7 +798,7 @@ class WebRTCChat {
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 更新WebRTC连接中的视频轨道
|
||||
// 12. 更新WebRTC连接
|
||||
if (this.peerConnection && this.videoSender) {
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
if (newVideoTrack) {
|
||||
@ -752,7 +806,7 @@ class WebRTCChat {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新显示信息
|
||||
// 13. 更新显示信息
|
||||
if (text) {
|
||||
this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`;
|
||||
this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success');
|
||||
@ -762,11 +816,17 @@ class WebRTCChat {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logMessage(`平滑切换视频流失败: ${error.message}`, 'error');
|
||||
// 确保隐藏加载指示器
|
||||
this.logMessage(`视频流切换失败: ${error.message}`, 'error');
|
||||
|
||||
// 清理可能残留的覆盖层
|
||||
const overlay = document.getElementById('video-frame-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
this.hideVideoLoading();
|
||||
|
||||
// 如果切换失败,尝试回到默认视频
|
||||
// 回退到默认视频
|
||||
if (videoFile !== this.defaultVideo) {
|
||||
this.logMessage('尝试回到默认视频', 'info');
|
||||
await this.switchVideoStreamSmooth(this.defaultVideo, 'fallback');
|
||||
@ -774,56 +834,7 @@ class WebRTCChat {
|
||||
}
|
||||
}
|
||||
|
||||
// 执行视频过渡动画
|
||||
async performVideoTransition(currentVideo, bufferVideo) {
|
||||
return new Promise((resolve) => {
|
||||
// 确保初始状态正确
|
||||
bufferVideo.style.opacity = '0';
|
||||
bufferVideo.style.zIndex = '3';
|
||||
currentVideo.style.opacity = '1';
|
||||
currentVideo.style.zIndex = '2';
|
||||
|
||||
// 设置过渡属性(如果还没有)
|
||||
if (!bufferVideo.style.transition) {
|
||||
bufferVideo.style.transition = 'opacity 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)';
|
||||
}
|
||||
if (!currentVideo.style.transition) {
|
||||
currentVideo.style.transition = 'opacity 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)';
|
||||
}
|
||||
|
||||
// 强制重绘以确保样式生效
|
||||
bufferVideo.offsetHeight;
|
||||
|
||||
// 开始过渡
|
||||
requestAnimationFrame(() => {
|
||||
bufferVideo.style.opacity = '1';
|
||||
currentVideo.style.opacity = '0';
|
||||
});
|
||||
|
||||
// 监听过渡完成
|
||||
const onTransitionEnd = (e) => {
|
||||
if (e.target === bufferVideo && e.propertyName === 'opacity') {
|
||||
bufferVideo.removeEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
// 重置z-index
|
||||
currentVideo.style.zIndex = '1';
|
||||
bufferVideo.style.zIndex = '2';
|
||||
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
bufferVideo.addEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
// 备用超时机制
|
||||
setTimeout(() => {
|
||||
bufferVideo.removeEventListener('transitionend', onTransitionEnd);
|
||||
currentVideo.style.zIndex = '1';
|
||||
bufferVideo.style.zIndex = '2';
|
||||
resolve();
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 显示加载指示器
|
||||
showVideoLoading() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user