diff --git a/WebApp/client/public/onebyone/index.html b/WebApp/client/public/onebyone/index.html index cf418b7..9dd954a 100644 --- a/WebApp/client/public/onebyone/index.html +++ b/WebApp/client/public/onebyone/index.html @@ -155,6 +155,17 @@ data-field="remoteUser.videoStream"> + +
+
+
+ +
+

等待对方连接...

+

请确保对方已加入通话

+
+
+
diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index 7319815..887655c 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -3,7 +3,7 @@ * 负责将状态映射到DOM,与状态管理解耦 */ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js'; -import {mockCallSession } from './models.js'; +import { mockCallSession } from './models.js'; class UIRenderer { constructor(stateManager) { this.stateManager = stateManager; @@ -22,6 +22,7 @@ class UIRenderer { // 远端视频 remoteVideo: document.getElementById('remoteVideo'), + remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'), remoteAvatar: document.getElementById('remoteAvatar'), remoteName: document.getElementById('remoteName'), remoteStatus: document.getElementById('remoteStatus'), @@ -57,6 +58,17 @@ class UIRenderer { this.unsubscribe = stateManager.subscribe(this.render.bind(this)); // 初始化渲染 this.render(this.stateManager.getState(), { type: 'INIT' }); + + window.addEventListener('resize', () => { + if (this.elements.remoteVideo && this.elements.remoteVideo.srcObject) { + const stream = this.elements.remoteVideo.srcObject; + const videoTracks = stream.getVideoTracks(); + if (videoTracks.length > 0) { + const resolution = this.getVideoResolution(videoTracks[0]); + this.adjustVideoSize(this.elements.remoteVideo, resolution); + } + } + }); } // 绑定事件监听器 @@ -75,6 +87,15 @@ class UIRenderer { this.renderControlButtons(state.session.localUser.mediaState); this.renderChatMessages(state.messages); this.renderUserList(state.session.localUser, state.session.remoteUser); + + // 初始化时检查远程流状态,显示或隐藏占位背景 + if (this.elements.remoteVideoPlaceholder) { + if (state.remoteStream) { + this.elements.remoteVideoPlaceholder.classList.add('hidden'); + } else { + this.elements.remoteVideoPlaceholder.classList.remove('hidden'); + } + } break; case 'DURATION_UPDATE': this.renderCallDuration(changes.duration); @@ -215,14 +236,41 @@ class UIRenderer { if (this.elements.remoteVideo && stream) { this.elements.remoteVideo.srcObject = stream; this.elements.remoteVideo.autoplay = true; + + // 关键设置:启用硬件加速和最佳质量渲染 + this.elements.remoteVideo.style.transform = 'translateZ(0)'; // 启用硬件加速 + this.elements.remoteVideo.style.imageRendering = 'pixelated'; // 保持像素清晰 + this.elements.remoteVideo.style.objectFit = 'contain'; // 保持比例 console.log('Remote stream set successfully:', this.elements.remoteVideo.srcObject); // 隐藏断开连接覆盖层 if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } + + // 隐藏占位背景 + if (this.elements.remoteVideoPlaceholder) { + this.elements.remoteVideoPlaceholder.classList.add('hidden'); + } + // 获取视频轨道并处理分辨率 + const videoTracks = stream.getVideoTracks(); + if (videoTracks.length > 0) { + const resolution = this.getVideoResolution(videoTracks[0]); + this.adjustVideoSize(this.elements.remoteVideo, resolution); + + // 监听轨道变化,处理分辨率调整 + videoTracks[0].addEventListener('resize', () => { + const newResolution = this.getVideoResolution(videoTracks[0]); + this.adjustVideoSize(this.elements.remoteVideo, newResolution); + }); + } } else { console.error('Either remoteVideo element or stream is missing'); + + // 显示占位背景 + if (this.elements.remoteVideoPlaceholder) { + this.elements.remoteVideoPlaceholder.classList.remove('hidden'); + } } } @@ -287,7 +335,44 @@ class UIRenderer { } } } + // 在renderer.js中添加方法 + // 获取视频流分辨率 + getVideoResolution(track) { + if (track && track.getSettings) { + const settings = track.getSettings(); + return { + width: settings.width || 640, + height: settings.height || 480 + }; + } + return { width: 640, height: 480 }; // 默认值 + } + // 调整视频元素大小并居中显示 + adjustVideoSize(videoElement, resolution) { + if (!videoElement) return; + + const { width, height } = resolution; + const aspectRatio = width / height; + + // 根据容器大小和视频宽高比调整视频显示 + const container = videoElement.parentElement; + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + // 启用硬件加速 + videoElement.style.transform = 'translateZ(0)'; + videoElement.style.willChange = 'transform'; + // 设置容器为flex布局,使视频居中 + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.justifyContent = 'center'; + // 优化图像渲染 + videoElement.style.imageRendering = 'pixelated'; + // 确保视频元素在容器内正确显示 + videoElement.style.maxWidth = '100%'; + videoElement.style.maxHeight = '100%'; + videoElement.style.objectFit = 'contain'; + } // 渲染控制按钮 renderControlButtons(mediaState) { if (this.elements.micBtn) { diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 40159cb..689d7ad 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -15,9 +15,9 @@ const defaultStreamHeight = 720; class CallStateManager { constructor() { - const renderstreaming=null; // WebRTC连接管理实例 - const useWebSocket=null; // 是否使用WebSocket信令 - const connectionId=null; // 连接ID + const renderstreaming = null; // WebRTC连接管理实例 + const useWebSocket = null; // 是否使用WebSocket信令 + const connectionId = null; // 连接ID // 核心状态 this.state = { session: { @@ -194,14 +194,21 @@ class CallStateManager { // 创建信令实例 const signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling(); const config = getRTCConfiguration(); // 获取RTC配置 + // 优化RTC配置,确保支持高分辨率 + config.peerConnectionOptions = { + optional: [ + { googCpuOveruseDetection: false }, // 禁用CPU过度使用检测 + { googScreencastMinBitrate: 3000 } // 设置最小比特率 + ] + }; this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例 // 连接建立回调 this.renderstreaming.onConnect = () => { // 连接建立后,更新状态为ongoing - this.state.session.status = 'ongoing'; - this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); - + this.state.session.status = 'ongoing'; + this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); + if (this.state.localStream) { const tracks = this.state.localStream.getTracks(); // 获取本地媒体轨道 for (const track of tracks) {