From fd06063d83d3fc7f2c7910497bddf13ba5189a31 Mon Sep 17 00:00:00 2001
From: stary <834207172@qq.COM>
Date: Sat, 25 Apr 2026 21:55:47 +0800
Subject: [PATCH] =?UTF-8?q?=E3=80=90m=E3=80=91=E5=A2=9E=E5=8A=A0=E7=94=BB?=
=?UTF-8?q?=E8=B4=A8=E9=80=89=E6=8B=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
WebApp/client/public/onebyone/css/style.css | 31 +++
WebApp/client/public/onebyone/index.html | 55 ++++-
WebApp/client/public/onebyone/main.js | 35 ++++
WebApp/client/public/onebyone/renderer.js | 26 +++
WebApp/client/public/onebyone/store.js | 216 +++++++++++++++++---
5 files changed, 331 insertions(+), 32 deletions(-)
diff --git a/WebApp/client/public/onebyone/css/style.css b/WebApp/client/public/onebyone/css/style.css
index fd20bde..7277e68 100644
--- a/WebApp/client/public/onebyone/css/style.css
+++ b/WebApp/client/public/onebyone/css/style.css
@@ -222,3 +222,34 @@ body {
}
[data-field]:hover::after { opacity: 1; }
[data-field] { position: relative; }*/
+
+/* 分辨率选项样式 */
+.resolution-option {
+ color: rgba(255, 255, 255, 0.8);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.resolution-option:hover {
+ color: white;
+}
+.resolution-option.active {
+ background: rgba(99, 102, 241, 0.3);
+ color: white;
+}
+.resolution-option.active::before {
+ content: '\f00c';
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 900;
+ font-size: 10px;
+ margin-right: 6px;
+ color: #818cf8;
+}
+
+/* 更多选项菜单动画 */
+#moreOptionsMenu {
+ animation: menuFadeIn 0.15s ease-out;
+}
+@keyframes menuFadeIn {
+ from { opacity: 0; transform: translateX(-50%) translateY(8px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
+}
diff --git a/WebApp/client/public/onebyone/index.html b/WebApp/client/public/onebyone/index.html
index 2a0c5a0..e92815b 100644
--- a/WebApp/client/public/onebyone/index.html
+++ b/WebApp/client/public/onebyone/index.html
@@ -468,14 +468,53 @@
-
+
diff --git a/WebApp/client/public/onebyone/main.js b/WebApp/client/public/onebyone/main.js
index 6c5a2f1..3138180 100644
--- a/WebApp/client/public/onebyone/main.js
+++ b/WebApp/client/public/onebyone/main.js
@@ -51,6 +51,24 @@ function bindDomEvents() {
}
};
+ // 更多选项菜单切换
+ window.toggleMoreOptions = function () {
+ const menu = document.getElementById('moreOptionsMenu');
+ if (menu) {
+ menu.classList.toggle('hidden');
+ }
+ };
+
+ // 切换视频分辨率
+ window.changeResolution = function (width, height) {
+ store.changeResolution(width, height);
+ // 关闭菜单
+ const menu = document.getElementById('moreOptionsMenu');
+ if (menu) {
+ menu.classList.add('hidden');
+ }
+ };
+
// 结束通话
window.endCall = function () {
// 显示确认对话框
@@ -130,6 +148,23 @@ function bindDomEvents() {
document.getElementById('cancelEndCall').addEventListener('click', window.cancelEndCall);
document.getElementById('confirmEndCall').addEventListener('click', window.confirmEndCall);
+ // 更多选项按钮事件
+ const moreOptionsBtn = document.getElementById('moreOptionsBtn');
+ if (moreOptionsBtn) {
+ moreOptionsBtn.addEventListener('click', window.toggleMoreOptions);
+ }
+
+ // 点击外部关闭更多选项菜单
+ document.addEventListener('click', function(event) {
+ const moreOptionsMenu = document.getElementById('moreOptionsMenu');
+ const moreOptionsBtnEl = document.getElementById('moreOptionsBtn');
+ if (moreOptionsMenu && moreOptionsBtnEl &&
+ !moreOptionsMenu.contains(event.target) &&
+ !moreOptionsBtnEl.contains(event.target)) {
+ moreOptionsMenu.classList.add('hidden');
+ }
+ });
+
// 绑定通话请求对话框事件
if (document.getElementById('rejectCall')) {
document.getElementById('rejectCall').addEventListener('click', window.rejectCall);
diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js
index b1ea71f..d2197a5 100644
--- a/WebApp/client/public/onebyone/renderer.js
+++ b/WebApp/client/public/onebyone/renderer.js
@@ -206,6 +206,10 @@ class UIRenderer {
// participant离开 - 更新UI但房间仍然存在
this.renderParticipantLeft(changes.connectionId);
break;
+ case 'RESOLUTION_CHANGED':
+ // 分辨率变化 - 更新UI中的分辨率选中状态
+ this.renderResolutionChanged(changes.resolution);
+ break;
}
}
@@ -254,6 +258,28 @@ class UIRenderer {
}
}
+ // 渲染分辨率变化
+ renderResolutionChanged(resolution) {
+ if (!resolution) return;
+
+ // 更新分辨率选项的选中状态
+ const options = document.querySelectorAll('.resolution-option');
+ options.forEach(btn => {
+ const btnRes = btn.dataset.resolution;
+ const isActive = (resolution.height >= 1440 && btnRes === '1440') ||
+ (resolution.height >= 1080 && resolution.height < 1440 && btnRes === '1080') ||
+ (resolution.height >= 720 && resolution.height < 1080 && btnRes === '720') ||
+ (resolution.height < 720 && btnRes === '480');
+ btn.classList.toggle('active', isActive);
+ });
+
+ // 更新当前分辨率文本
+ const currentResText = document.getElementById('currentResolutionText');
+ if (currentResText) {
+ currentResText.textContent = `当前: ${resolution.label}`;
+ }
+ }
+
// 渲染远端视频
renderRemoteVideo(remoteUser) {
// 同步更新侧边栏用户列表
diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js
index 38e9af5..c520e9f 100644
--- a/WebApp/client/public/onebyone/store.js
+++ b/WebApp/client/public/onebyone/store.js
@@ -22,12 +22,20 @@ const VAD_CONFIG = {
};
const MEDIA_CONSTRAINTS = {
- video: true,
+ video: {
+ width: { ideal: 1920, max: 1920 },
+ height: { ideal: 1080, max: 1080 },
+ frameRate: { ideal: 30, max: 30 }
+ },
audio: AUDIO_CONFIG
};
const VIDEO_ONLY_CONSTRAINT = {
- video: true,
+ video: {
+ width: { ideal: 1920, max: 1920 },
+ height: { ideal: 1080, max: 1080 },
+ frameRate: { ideal: 30, max: 30 }
+ },
audio: false
};
@@ -92,6 +100,12 @@ class CallStateManager {
// 通知UI更新
this.notify({ type: 'USER_SETTINGS_UPDATED', user: this.state.session.localUser });
}
+
+ // 恢复保存的分辨率设置
+ if (settings.resolution) {
+ this._savedResolution = settings.resolution;
+ console.log(`已恢复分辨率设置: ${settings.resolution.width}x${settings.resolution.height}`);
+ }
} catch (error) {
console.error('Error loading user settings:', error);
}
@@ -113,7 +127,18 @@ class CallStateManager {
}
// 请求摄像头权限并获取媒体流,启用回声消除
- const stream = await navigator.mediaDevices.getUserMedia(MEDIA_CONSTRAINTS);
+ // 使用保存的分辨率(如有),否则使用默认约束
+ const videoConstraints = this._savedResolution
+ ? {
+ width: { ideal: this._savedResolution.width, max: this._savedResolution.width },
+ height: { ideal: this._savedResolution.height, max: this._savedResolution.height },
+ frameRate: { ideal: 30, max: 30 }
+ }
+ : MEDIA_CONSTRAINTS.video;
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: videoConstraints,
+ audio: AUDIO_CONFIG
+ });
console.log('Stream obtained successfully:', stream);
console.log('Video tracks:', stream.getVideoTracks());
@@ -211,6 +236,7 @@ class CallStateManager {
// 设置编解码器偏好
setTimeout(() => { this.setCodecPreferences(participantId); }, 100);
+ setTimeout(() => { this.setVideoEncodingParameters(participantId); }, 200);
}
} else {
// Participant端:使用单一peer
@@ -239,6 +265,7 @@ class CallStateManager {
}
}
setTimeout(() => { this.setCodecPreferences(); }, 100);
+ setTimeout(() => { this.setVideoEncodingParameters(); }, 200);
}
}
@@ -325,21 +352,6 @@ class CallStateManager {
// 创建信令实例
const signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration(); // 获取RTC配置
- // 优化RTC配置,确保支持高分辨率和良好的音频处理
- config.peerConnectionOptions = {
- optional: [
- { googCpuOveruseDetection: false }, // 禁用CPU过度使用检测
- { googScreencastMinBitrate: 3000 }, // 设置最小比特率
- { googEchoCancellation: true }, // 启用回声消除
- { googEchoCancellation2: true }, // 启用高级回声消除
- { googNoiseSuppression: true }, // 启用噪声抑制
- { googNoiseSuppression2: true }, // 启用高级噪声抑制
- { googAutoGainControl: true }, // 启用自动增益控制
- { googAutoGainControl2: true }, // 启用高级自动增益控制
- { googHighpassFilter: true }, // 启用高通滤波器
- { googTypingNoiseDetection: true } // 启用打字噪声检测
- ]
- };
this.renderstreaming = new RenderStreaming(signaling, config);
}
@@ -366,6 +378,7 @@ class CallStateManager {
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }, participantId);
}
this.setCodecPreferences(participantId);
+ this.setVideoEncodingParameters(participantId);
}
};
@@ -832,16 +845,38 @@ class CallStateManager {
/**
* 设置编解码器偏好
+ * 优先选择 VP9/AV1(更高效的压缩),回退到 H264 High Profile
*/
setCodecPreferences(participantId) {
- let selectedCodecs = null;
const { codecs } = RTCRtpSender.getCapabilities('video');
- if (codecs && codecs.length > 0) {
- const h264Codec = codecs.find(c => c.mimeType === 'video/H264');
- if (h264Codec) {
- selectedCodecs = [h264Codec];
- }
+ if (!codecs || codecs.length === 0) return;
+
+ let selectedCodecs = null;
+
+ // 优先级: 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 (selectedCodecs == null) return;
if (this.renderstreaming) {
@@ -855,6 +890,139 @@ class CallStateManager {
}
}
+ /**
+ * 设置视频发送编码参数
+ * 提升 maxBitrate 以改善远端视频画质
+ * @param {string} [participantId] - 目标participant(host端使用)
+ */
+ setVideoEncodingParameters(participantId) {
+ if (!this.renderstreaming) return;
+
+ const transceivers = this.renderstreaming.getTransceivers(participantId);
+ if (!transceivers || transceivers.length === 0) return;
+
+ const videoTransceivers = transceivers.filter(t =>
+ t.sender && t.sender.track && t.sender.track.kind === 'video'
+ );
+
+ for (const transceiver of videoTransceivers) {
+ try {
+ const sender = transceiver.sender;
+ const params = sender.getParameters();
+
+ if (!params.encodings || params.encodings.length === 0) {
+ params.encodings = [{}];
+ }
+
+ // 设置最大比特率为 4Mbps(1080p@30fps 良好画质)
+ params.encodings[0].maxBitrate = 4000000; // 4 Mbps = 4,000,000 bps
+
+ // 优先保持分辨率,降低帧率来适应带宽
+ // 'maintain-resolution' 在带宽不足时保持清晰度
+ if (params.degradationPreference !== undefined) {
+ params.degradationPreference = 'maintain-resolution';
+ }
+
+ sender.setParameters(params);
+ console.log(`Set video encoding: maxBitrate=4Mbps, degradationPreference=maintain-resolution${participantId ? ` for ${participantId}` : ''}`);
+ } catch (error) {
+ console.error('Error setting video encoding parameters:', error);
+ }
+ }
+ }
+
+
+ /**
+ * 动态切换视频分辨率
+ * 使用 MediaStreamTrack.applyConstraints() 在通话中实时调整
+ * 同时根据分辨率调整编码比特率
+ * @param {number} width - 目标宽度
+ * @param {number} height - 目标高度
+ */
+ async changeResolution(width, height) {
+ if (!this.state.localStream) {
+ showNotification('本地视频流不可用', 'error');
+ return;
+ }
+
+ const videoTracks = this.state.localStream.getVideoTracks();
+ if (videoTracks.length === 0) {
+ showNotification('视频轨道不可用', 'error');
+ return;
+ }
+
+ const track = videoTracks[0];
+ const label = height >= 1440 ? '2K 1440p' : height >= 1080 ? '1080p 超清' : height >= 720 ? '720p 高清' : '480p 流畅';
+
+ try {
+ // 使用 applyConstraints 在不重新获取流的情况下调整分辨率
+ await track.applyConstraints({
+ width: { ideal: width, max: width },
+ height: { ideal: height, max: height },
+ frameRate: { ideal: 30, max: 30 }
+ });
+
+ console.log(`分辨率已切换为 ${width}x${height}`);
+
+ // 根据分辨率调整编码比特率
+ // 480p: ~1Mbps, 720p: ~2.5Mbps, 1080p: ~4Mbps, 2K: ~6Mbps
+ const bitrateMap = {
+ 270: 1000000, // 480p
+ 720: 2500000, // 720p
+ 1080: 4000000, // 1080p
+ 1440: 6000000 // 2K
+ };
+ const maxBitrate = bitrateMap[height] || 2500000;
+ this._applyMaxBitrate(maxBitrate);
+
+ // 保存当前分辨率设置到本地存储
+ const userSettings = JSON.parse(localStorage.getItem('userSettings') || '{}');
+ userSettings.resolution = { width, height };
+ localStorage.setItem('userSettings', JSON.stringify(userSettings));
+
+ // 通知 UI 更新
+ this.notify({ type: 'RESOLUTION_CHANGED', resolution: { width, height, label } });
+ showNotification(`已切换为 ${label}`, 'success');
+ } catch (error) {
+ console.error('切换分辨率失败:', error);
+ showNotification('切换分辨率失败,摄像头不支持该分辨率', 'error');
+ }
+ }
+
+ /**
+ * 对所有视频 sender 应用 maxBitrate
+ * @param {number} maxBitrate - 最大比特率(bps)
+ */
+ _applyMaxBitrate(maxBitrate) {
+ if (!this.renderstreaming) return;
+
+ const isHost = this.role === 'host';
+ const participantIds = isHost ? Object.keys(this.state.participants) : [null];
+
+ for (const pid of participantIds) {
+ const transceivers = this.renderstreaming.getTransceivers(pid);
+ if (!transceivers) continue;
+
+ const videoTransceivers = transceivers.filter(t =>
+ t.sender && t.sender.track && t.sender.track.kind === 'video'
+ );
+
+ for (const transceiver of videoTransceivers) {
+ try {
+ const sender = transceiver.sender;
+ const params = sender.getParameters();
+ if (!params.encodings || params.encodings.length === 0) {
+ params.encodings = [{}];
+ }
+ params.encodings[0].maxBitrate = maxBitrate;
+ sender.setParameters(params);
+ console.log(`Updated maxBitrate to ${maxBitrate} for ${pid || 'self'}`);
+ } catch (error) {
+ console.error('Error updating maxBitrate:', error);
+ }
+ }
+ }
+ }
// 更新远端媒体状态 (由 WebSocket 触发)
updateRemoteMedia(mediaState, participantId) {