【m】增加画质选择
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user