拆分媒体
This commit is contained in:
80
client/public/media-config.js
Normal file
80
client/public/media-config.js
Normal file
@@ -0,0 +1,80 @@
|
||||
export const AUDIO_CONFIG = {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
};
|
||||
|
||||
export const VAD_CONFIG = {
|
||||
threshold: 15,
|
||||
debounceTime: 500,
|
||||
fftSize: 256
|
||||
};
|
||||
|
||||
export const MEDIA_CONSTRAINTS = {
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 },
|
||||
height: { ideal: 1080, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
},
|
||||
audio: AUDIO_CONFIG
|
||||
};
|
||||
|
||||
export const VIDEO_ONLY_CONSTRAINT = {
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 },
|
||||
height: { ideal: 1080, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
const TARGET_RESOLUTION_BITRATE_MAP = {
|
||||
270: 1000000,
|
||||
480: 1500000,
|
||||
720: 2500000,
|
||||
1080: 4000000,
|
||||
1440: 6000000
|
||||
};
|
||||
|
||||
export function buildVideoConstraints(savedResolution) {
|
||||
if (!savedResolution) {
|
||||
return MEDIA_CONSTRAINTS.video;
|
||||
}
|
||||
|
||||
return {
|
||||
width: { ideal: savedResolution.width, max: savedResolution.width },
|
||||
height: { ideal: savedResolution.height, max: savedResolution.height },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
};
|
||||
}
|
||||
|
||||
export function getResolutionLabel(height) {
|
||||
if (height >= 1440) {
|
||||
return '2K 1440p';
|
||||
}
|
||||
if (height >= 1080) {
|
||||
return '1080p 超清';
|
||||
}
|
||||
if (height >= 720) {
|
||||
return '720p 高清';
|
||||
}
|
||||
return '480p 流畅';
|
||||
}
|
||||
|
||||
export function getTargetResolutionBitrate(height) {
|
||||
return TARGET_RESOLUTION_BITRATE_MAP[height] || 2500000;
|
||||
}
|
||||
|
||||
export function getAdaptiveVideoBitrate(height) {
|
||||
const supportedHeights = Object.keys(TARGET_RESOLUTION_BITRATE_MAP).map(Number).sort((a, b) => a - b);
|
||||
let maxBitrate = TARGET_RESOLUTION_BITRATE_MAP[1080];
|
||||
|
||||
for (const supportedHeight of supportedHeights) {
|
||||
if (height <= supportedHeight) {
|
||||
return TARGET_RESOLUTION_BITRATE_MAP[supportedHeight];
|
||||
}
|
||||
maxBitrate = TARGET_RESOLUTION_BITRATE_MAP[supportedHeight];
|
||||
}
|
||||
|
||||
return maxBitrate;
|
||||
}
|
||||
@@ -16,36 +16,16 @@ import {
|
||||
removeParticipant,
|
||||
upsertParticipant
|
||||
} from './participants.js';
|
||||
|
||||
const AUDIO_CONFIG = {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
};
|
||||
|
||||
const VAD_CONFIG = {
|
||||
threshold: 15,
|
||||
debounceTime: 500,
|
||||
fftSize: 256
|
||||
};
|
||||
|
||||
const MEDIA_CONSTRAINTS = {
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 },
|
||||
height: { ideal: 1080, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
},
|
||||
audio: AUDIO_CONFIG
|
||||
};
|
||||
|
||||
const VIDEO_ONLY_CONSTRAINT = {
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 },
|
||||
height: { ideal: 1080, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
import {
|
||||
AUDIO_CONFIG,
|
||||
VAD_CONFIG,
|
||||
VIDEO_ONLY_CONSTRAINT,
|
||||
buildVideoConstraints,
|
||||
getAdaptiveVideoBitrate,
|
||||
getResolutionLabel,
|
||||
getTargetResolutionBitrate
|
||||
} from './media-config.js';
|
||||
import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js';
|
||||
|
||||
class CallStateManager {
|
||||
constructor() {
|
||||
@@ -138,13 +118,7 @@ class CallStateManager {
|
||||
|
||||
// 请求摄像头权限并获取媒体流,启用回声消除
|
||||
// 使用保存的分辨率(如有),否则使用默认约束
|
||||
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 videoConstraints = buildVideoConstraints(this._savedResolution);
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: videoConstraints,
|
||||
audio: AUDIO_CONFIG
|
||||
@@ -1064,23 +1038,7 @@ class CallStateManager {
|
||||
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];
|
||||
}
|
||||
const maxBitrate = getAdaptiveVideoBitrate(height);
|
||||
|
||||
params.encodings[0].maxBitrate = maxBitrate;
|
||||
params.encodings[0].scaleResolutionDownBy = 1.0;
|
||||
@@ -1121,7 +1079,7 @@ class CallStateManager {
|
||||
}
|
||||
|
||||
const track = videoTracks[0];
|
||||
const label = height >= 1440 ? '2K 1440p' : height >= 1080 ? '1080p 超清' : height >= 720 ? '720p 高清' : '480p 流畅';
|
||||
const label = getResolutionLabel(height);
|
||||
|
||||
try {
|
||||
// 使用 applyConstraints 在不重新获取流的情况下调整分辨率
|
||||
@@ -1135,13 +1093,7 @@ class CallStateManager {
|
||||
|
||||
// 根据分辨率调整编码比特率
|
||||
// 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;
|
||||
const maxBitrate = getTargetResolutionBitrate(height);
|
||||
this._applyMaxBitrate(maxBitrate);
|
||||
|
||||
// 保存当前分辨率设置到本地存储
|
||||
@@ -1258,62 +1210,8 @@ class CallStateManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let totalPacketsLost = 0;
|
||||
let totalPacketsReceived = 0;
|
||||
let inboundRTPCount = 0;
|
||||
let jitter = 0;
|
||||
let roundTripTime = 0;
|
||||
|
||||
// 分析统计信息
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||||
inboundRTPCount++;
|
||||
|
||||
// 计算丢包率
|
||||
if (report.packetsLost !== undefined && report.packetsReceived !== undefined) {
|
||||
totalPacketsLost += report.packetsLost;
|
||||
totalPacketsReceived += report.packetsReceived;
|
||||
}
|
||||
|
||||
// 获取抖动
|
||||
if (report.jitter !== undefined) {
|
||||
jitter = Math.max(jitter, report.jitter);
|
||||
}
|
||||
|
||||
// 获取往返时间
|
||||
if (report.roundTripTime !== undefined) {
|
||||
roundTripTime = Math.max(roundTripTime, report.roundTripTime);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 计算网络质量指标
|
||||
let quality = 'excellent';
|
||||
|
||||
if (inboundRTPCount > 0) {
|
||||
// 基于丢包率判断
|
||||
const packetLossRate = totalPacketsReceived > 0 ? (totalPacketsLost / (totalPacketsLost + totalPacketsReceived)) : 0;
|
||||
|
||||
// 基于抖动判断
|
||||
const jitterMs = jitter * 1000;
|
||||
|
||||
// 基于往返时间判断
|
||||
const rttMs = roundTripTime * 1000;
|
||||
|
||||
// 综合评估网络质量
|
||||
if (packetLossRate > 0.05 || jitterMs > 100 || rttMs > 300) {
|
||||
quality = 'poor';
|
||||
} else if (packetLossRate > 0.02 || jitterMs > 50 || rttMs > 150) {
|
||||
quality = 'fair';
|
||||
} else if (packetLossRate > 0.01 || jitterMs > 30 || rttMs > 100) {
|
||||
quality = 'good';
|
||||
} else {
|
||||
quality = 'excellent';
|
||||
}
|
||||
} else {
|
||||
// 没有收到任何RTP包,设置为无信号状态
|
||||
quality = 'no_signal';
|
||||
}
|
||||
const summary = summarizeInboundStats(stats);
|
||||
const quality = getNetworkQualityFromSummary(summary);
|
||||
|
||||
// 更新网络质量状态
|
||||
if (this.state.session.remoteUser.networkQuality !== quality) {
|
||||
@@ -1455,48 +1353,7 @@ class CallStateManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let statsSummary = {
|
||||
video: {
|
||||
packetsLost: 0,
|
||||
packetsReceived: 0,
|
||||
bytesReceived: 0,
|
||||
jitter: 0,
|
||||
roundTripTime: 0,
|
||||
fps: 0,
|
||||
bitrate: 0
|
||||
},
|
||||
audio: {
|
||||
packetsLost: 0,
|
||||
packetsReceived: 0,
|
||||
bytesReceived: 0,
|
||||
jitter: 0
|
||||
}
|
||||
};
|
||||
|
||||
// 分析统计信息
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'inbound-rtp') {
|
||||
if (report.mediaType === 'video') {
|
||||
statsSummary.video.packetsLost = report.packetsLost || 0;
|
||||
statsSummary.video.packetsReceived = report.packetsReceived || 0;
|
||||
statsSummary.video.bytesReceived = report.bytesReceived || 0;
|
||||
statsSummary.video.jitter = report.jitter || 0;
|
||||
statsSummary.video.roundTripTime = report.roundTripTime || 0;
|
||||
statsSummary.video.fps = report.framesPerSecond || 0;
|
||||
|
||||
// 计算视频比特率 (kbps)
|
||||
if (report.bytesReceived && report.timestamp) {
|
||||
const duration = report.timestamp / 1000; // 转换为秒
|
||||
statsSummary.video.bitrate = duration > 0 ? Math.round((report.bytesReceived * 8) / (duration * 1000)) : 0;
|
||||
}
|
||||
} else if (report.mediaType === 'audio') {
|
||||
statsSummary.audio.packetsLost = report.packetsLost || 0;
|
||||
statsSummary.audio.packetsReceived = report.packetsReceived || 0;
|
||||
statsSummary.audio.bytesReceived = report.bytesReceived || 0;
|
||||
statsSummary.audio.jitter = report.jitter || 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
const statsSummary = summarizeInboundStats(stats);
|
||||
|
||||
// 输出详细统计信息
|
||||
console.log('=== WebRTC Statistics ===');
|
||||
|
||||
96
client/public/webrtc-stats.js
Normal file
96
client/public/webrtc-stats.js
Normal file
@@ -0,0 +1,96 @@
|
||||
function createStatsSummary() {
|
||||
return {
|
||||
video: {
|
||||
packetsLost: 0,
|
||||
packetsReceived: 0,
|
||||
bytesReceived: 0,
|
||||
jitter: 0,
|
||||
roundTripTime: 0,
|
||||
fps: 0,
|
||||
bitrate: 0
|
||||
},
|
||||
audio: {
|
||||
packetsLost: 0,
|
||||
packetsReceived: 0,
|
||||
bytesReceived: 0,
|
||||
jitter: 0
|
||||
},
|
||||
network: {
|
||||
totalPacketsLost: 0,
|
||||
totalPacketsReceived: 0,
|
||||
inboundRtpCount: 0,
|
||||
jitter: 0,
|
||||
roundTripTime: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeInboundStats(stats) {
|
||||
const summary = createStatsSummary();
|
||||
|
||||
stats.forEach((report) => {
|
||||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||||
summary.network.inboundRtpCount++;
|
||||
|
||||
if (report.packetsLost !== undefined && report.packetsReceived !== undefined) {
|
||||
summary.network.totalPacketsLost += report.packetsLost;
|
||||
summary.network.totalPacketsReceived += report.packetsReceived;
|
||||
}
|
||||
|
||||
if (report.jitter !== undefined) {
|
||||
summary.network.jitter = Math.max(summary.network.jitter, report.jitter);
|
||||
}
|
||||
|
||||
if (report.roundTripTime !== undefined) {
|
||||
summary.network.roundTripTime = Math.max(summary.network.roundTripTime, report.roundTripTime);
|
||||
}
|
||||
|
||||
summary.video.packetsLost = report.packetsLost || 0;
|
||||
summary.video.packetsReceived = report.packetsReceived || 0;
|
||||
summary.video.bytesReceived = report.bytesReceived || 0;
|
||||
summary.video.jitter = report.jitter || 0;
|
||||
summary.video.roundTripTime = report.roundTripTime || 0;
|
||||
summary.video.fps = report.framesPerSecond || 0;
|
||||
|
||||
if (report.bytesReceived && report.timestamp) {
|
||||
const duration = report.timestamp / 1000;
|
||||
summary.video.bitrate = duration > 0
|
||||
? Math.round((report.bytesReceived * 8) / (duration * 1000))
|
||||
: 0;
|
||||
}
|
||||
} else if (report.type === 'inbound-rtp' && report.mediaType === 'audio') {
|
||||
summary.audio.packetsLost = report.packetsLost || 0;
|
||||
summary.audio.packetsReceived = report.packetsReceived || 0;
|
||||
summary.audio.bytesReceived = report.bytesReceived || 0;
|
||||
summary.audio.jitter = report.jitter || 0;
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
export function getNetworkQualityFromSummary(summary) {
|
||||
const { totalPacketsLost, totalPacketsReceived, inboundRtpCount, jitter, roundTripTime } = summary.network;
|
||||
|
||||
if (inboundRtpCount === 0) {
|
||||
return 'no_signal';
|
||||
}
|
||||
|
||||
const packetLossRate = totalPacketsReceived > 0
|
||||
? totalPacketsLost / (totalPacketsLost + totalPacketsReceived)
|
||||
: 0;
|
||||
const jitterMs = jitter * 1000;
|
||||
const rttMs = roundTripTime * 1000;
|
||||
|
||||
if (packetLossRate > 0.05 || jitterMs > 100 || rttMs > 300) {
|
||||
return 'poor';
|
||||
}
|
||||
if (packetLossRate > 0.02 || jitterMs > 50 || rttMs > 150) {
|
||||
return 'fair';
|
||||
}
|
||||
if (packetLossRate > 0.01 || jitterMs > 30 || rttMs > 100) {
|
||||
return 'good';
|
||||
}
|
||||
|
||||
return 'excellent';
|
||||
}
|
||||
Reference in New Issue
Block a user