From 1d10d54af42c2901f7376d7f922d61091c202809 Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.COM> Date: Sat, 25 Apr 2026 22:26:30 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90m=E3=80=91=E8=87=AA=E9=80=82=E5=BA=94?= =?UTF-8?q?=E5=88=86=E8=BE=A8=E7=8E=87=EF=BC=8C=E4=B8=8D=E7=9F=A5=E9=81=93?= =?UTF-8?q?=E6=9C=89=E6=B2=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApp/client/public/onebyone/renderer.js | 2 +- WebApp/client/public/onebyone/store.js | 88 ++++++++++++++++------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index d2197a5..5cf1b76 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -809,7 +809,7 @@ class UIRenderer { container.style.alignItems = 'center'; container.style.justifyContent = 'center'; // 优化图像渲染 - videoElement.style.imageRendering = 'pixelated'; + videoElement.style.imageRendering = 'auto'; // 确保视频元素在容器内正确显示 videoElement.style.maxWidth = '100%'; videoElement.style.maxHeight = '100%'; diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index c520e9f..b5d6978 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -440,6 +440,19 @@ class CallStateManager { this.hangUp(); // 房间已关闭,挂断连接 }; + // SDP Answer 接收回调:重新设置编码参数以保障画质 + this.renderstreaming.onGotAnswer = (connectionId) => { + console.log('SDP Answer received, resetting encoding parameters for connectionId:', connectionId); + if (this.role === 'host') { + const allParticipantIds = Object.keys(this.state.remoteStreams || {}); + for (const pid of allParticipantIds) { + setTimeout(() => { this.setVideoEncodingParameters(pid); }, 50); + } + } else { + setTimeout(() => { this.setVideoEncodingParameters(); }, 50); + } + }; + // participant加入回调(host收到,新participant加入房间) this.renderstreaming.onParticipantJoined = (participantId) => { console.log(`Participant joined: ${participantId}`); @@ -848,43 +861,46 @@ class CallStateManager { * 优先选择 VP9/AV1(更高效的压缩),回退到 H264 High Profile */ setCodecPreferences(participantId) { - const { codecs } = RTCRtpSender.getCapabilities('video'); - if (!codecs || codecs.length === 0) return; + const capabilities = RTCRtpSender.getCapabilities('video'); + if (!capabilities || !capabilities.codecs || capabilities.codecs.length === 0) return; + const { codecs } = capabilities; - let selectedCodecs = null; + // 构建多codec优先级列表(而非只选一个) + let selectedCodecs = []; - // 优先级: AV1 > VP9 > H264 High Profile > H264 const av1Codec = codecs.find(c => c.mimeType === 'video/AV1'); const vp9Codec = codecs.find(c => c.mimeType === 'video/VP9'); - // H264 High Profile 提供比 Baseline/Constrained Baseline 更好的画质 const h264HighCodec = codecs.find(c => c.mimeType === 'video/H264' && c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=6400') ); const h264Codec = codecs.find(c => c.mimeType === 'video/H264'); - if (av1Codec) { - selectedCodecs = [av1Codec]; - console.log('Selected codec: AV1'); - } else if (vp9Codec) { - selectedCodecs = [vp9Codec]; - console.log('Selected codec: VP9'); - } else if (h264HighCodec) { - selectedCodecs = [h264HighCodec]; - console.log('Selected codec: H264 High Profile'); - } else if (h264Codec) { - selectedCodecs = [h264Codec]; - console.log('Selected codec: H264'); - } + if (av1Codec) selectedCodecs.push(av1Codec); + if (vp9Codec) selectedCodecs.push(vp9Codec); + if (h264HighCodec) selectedCodecs.push(h264HighCodec); + if (h264Codec && (!h264HighCodec || h264Codec !== h264HighCodec)) selectedCodecs.push(h264Codec); - if (selectedCodecs == null) return; + if (selectedCodecs.length === 0) return; if (this.renderstreaming) { const transceivers = this.renderstreaming.getTransceivers(participantId); if (transceivers && transceivers.length > 0) { - const videoTransceivers = transceivers.filter(t => t.receiver.track.kind == "video"); + const videoTransceivers = transceivers.filter(t => { + if (t.sender && t.sender.track) { + return t.sender.track.kind === 'video'; + } + return t.mid !== null && t.receiver && t.receiver.track && t.receiver.track.kind === 'video'; + }); if (videoTransceivers && videoTransceivers.length > 0) { - videoTransceivers.forEach(t => t.setCodecPreferences(selectedCodecs)); + videoTransceivers.forEach(t => { + try { + t.setCodecPreferences(selectedCodecs); + } catch(e) { + console.error('Error setting codec preferences:', e); + } + }); + console.log(`Codec preferences set: ${selectedCodecs.map(c => c.mimeType).join(' > ')}`); } } } @@ -914,8 +930,32 @@ class CallStateManager { params.encodings = [{}]; } - // 设置最大比特率为 4Mbps(1080p@30fps 良好画质) - params.encodings[0].maxBitrate = 4000000; // 4 Mbps = 4,000,000 bps + // 根据实际采集分辨率动态设置maxBitrate + const videoTrack = sender.track; + const settings = videoTrack ? videoTrack.getSettings() : {}; + const height = settings.height || 1080; + + const bitrateMap = { + 270: 1000000, + 480: 1500000, + 720: 2500000, + 1080: 4000000, + 1440: 6000000 + }; + // 找到最接近的分辨率对应的比特率 + let maxBitrate = 4000000; + const heights = Object.keys(bitrateMap).map(Number).sort((a, b) => a - b); + for (const h of heights) { + if (height <= h) { + maxBitrate = bitrateMap[h]; + break; + } + maxBitrate = bitrateMap[h]; + } + + params.encodings[0].maxBitrate = maxBitrate; + params.encodings[0].scaleResolutionDownBy = 1.0; + params.encodings[0].xGoogleMinBitrate = Math.floor(maxBitrate * 0.5); // 优先保持分辨率,降低帧率来适应带宽 // 'maintain-resolution' 在带宽不足时保持清晰度 @@ -924,7 +964,7 @@ class CallStateManager { } sender.setParameters(params); - console.log(`Set video encoding: maxBitrate=4Mbps, degradationPreference=maintain-resolution${participantId ? ` for ${participantId}` : ''}`); + console.log(`Set video encoding: maxBitrate=${maxBitrate / 1000000}Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate=${Math.floor(maxBitrate * 0.5)}${participantId ? ` for ${participantId}` : ''}`); } catch (error) { console.error('Error setting video encoding parameters:', error); }