Compare commits

...

6 Commits

Author SHA1 Message Date
554bb5d9ee 解决乱码问题 2026-05-24 01:54:47 +08:00
68712fba8c ++ 2026-05-24 01:46:57 +08:00
ac16fa85e9 ++ 2026-05-24 01:29:34 +08:00
a30c74f8da ++ 2026-05-24 01:18:27 +08:00
0d8a567c95 拆分媒体 2026-05-24 01:01:28 +08:00
44f4b30313 拆分part 2026-05-24 00:54:58 +08:00
7 changed files with 1953 additions and 1061 deletions

View 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;
}

View File

@@ -0,0 +1,62 @@
export function createAudioAnalyser(stream, fftSize) {
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContextCtor();
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = fftSize;
source.connect(analyser);
return {
audioContext,
analyser,
dataArray: new Uint8Array(analyser.frequencyBinCount)
};
}
export function getAudioLevel(analyser, dataArray) {
analyser.getByteTimeDomainData(dataArray);
let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
const amplitude = dataArray[i] - 128;
sum += amplitude * amplitude;
}
const rms = Math.sqrt(sum / dataArray.length);
return rms / 128;
}
function formatPacketLossRate(packetsLost, packetsReceived) {
if (packetsReceived <= 0) {
return '0%';
}
return `${((packetsLost / (packetsLost + packetsReceived)) * 100).toFixed(2)}%`;
}
export function buildStatsLogPayload(networkQuality, statsSummary) {
return {
networkQuality,
video: {
'Packets Lost': statsSummary.video.packetsLost,
'Packets Received': statsSummary.video.packetsReceived,
'Packet Loss Rate': formatPacketLossRate(
statsSummary.video.packetsLost,
statsSummary.video.packetsReceived
),
'Jitter': `${(statsSummary.video.jitter * 1000).toFixed(2)}ms`,
'Round Trip Time': `${(statsSummary.video.roundTripTime * 1000).toFixed(2)}ms`,
'FPS': statsSummary.video.fps.toFixed(1),
'Bitrate': `${statsSummary.video.bitrate}kbps`
},
audio: {
'Packets Lost': statsSummary.audio.packetsLost,
'Packets Received': statsSummary.audio.packetsReceived,
'Packet Loss Rate': formatPacketLossRate(
statsSummary.audio.packetsLost,
statsSummary.audio.packetsReceived
),
'Jitter': `${(statsSummary.audio.jitter * 1000).toFixed(2)}ms`
}
};
}

View File

@@ -0,0 +1,77 @@
export const DEFAULT_PARTICIPANT_NAME = '参与者';
export const DEFAULT_PARTICIPANT_AVATAR = '/images/p2.png';
const DEFAULT_MEDIA_STATE = Object.freeze({
audio: false,
video: true,
isSpeaking: false
});
function createParticipantRecord(current = {}, patch = {}) {
return {
id: '',
name: DEFAULT_PARTICIPANT_NAME,
avatar: DEFAULT_PARTICIPANT_AVATAR,
mediaState: { ...DEFAULT_MEDIA_STATE },
status: 'online',
...current,
...patch,
mediaState: {
...DEFAULT_MEDIA_STATE,
...(current.mediaState || {}),
...(patch.mediaState || {})
}
};
}
export function upsertParticipant(participants, participantId, patch = {}) {
if (!participantId) {
return null;
}
participants[participantId] = createParticipantRecord(participants[participantId], patch);
return participants[participantId];
}
export function removeParticipant(participants, participantId) {
if (!participantId || !participants[participantId]) {
return false;
}
delete participants[participantId];
return true;
}
export function omitParticipant(participants, excludedParticipantId) {
const filtered = {};
for (const [participantId, participant] of Object.entries(participants || {})) {
if (participantId !== excludedParticipantId) {
filtered[participantId] = participant;
}
}
return filtered;
}
export function buildParticipantsSyncData(localUser, participants) {
const memberList = {
host: {
id: localUser.id,
name: localUser.name,
avatar: localUser.avatar,
mediaState: { ...localUser.mediaState },
status: 'online',
role: 'host'
}
};
for (const [participantId, participant] of Object.entries(participants || {})) {
memberList[participantId] = {
...createParticipantRecord(participant),
role: 'participant'
};
}
return memberList;
}

View File

@@ -0,0 +1,99 @@
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";
const INVITE_EVENT_NAMES = Object.freeze([
'invite-call',
'invite-accepted',
'invite-rejected',
'invite-failed'
]);
const DEFAULT_SOCKET_USER_NAME = '?';
const DEFAULT_SOCKET_USER_AVATAR = '/images/p1.png';
export function createSignalingInstance(useWebSocket) {
return useWebSocket ? new WebSocketSignaling() : new Signaling();
}
export async function ensureSignalingStarted(existingSignaling, useWebSocket) {
if (existingSignaling) {
return {
signaling: existingSignaling,
reused: true
};
}
const signaling = createSignalingInstance(useWebSocket);
await signaling.start();
return {
signaling,
reused: false
};
}
export function bindInviteSocketEvents(signaling, eventHandlers, boundSignaling = null) {
if (!signaling || signaling === boundSignaling || typeof signaling.addEventListener !== 'function') {
return boundSignaling;
}
INVITE_EVENT_NAMES.forEach((eventName) => {
signaling.addEventListener(eventName, (event) => {
const handler = eventHandlers[eventName];
if (typeof handler === 'function') {
handler(event.detail);
}
});
});
return signaling;
}
export function getActiveSignalingInstance(preconnectedSignaling, renderstreaming) {
if (preconnectedSignaling) {
return preconnectedSignaling;
}
if (renderstreaming && renderstreaming._signaling) {
return renderstreaming._signaling;
}
return null;
}
export function sendInviteSignal(signaling, methodName, payload) {
if (!signaling || typeof signaling[methodName] !== 'function') {
throw new Error('Invite signaling is not ready');
}
signaling[methodName](payload);
}
function readStoredSocketUserInfo() {
try {
return JSON.parse(localStorage.getItem('userSettings') || '{}');
} catch (error) {
console.error('Error parsing user settings:', error);
return {};
}
}
export function buildSocketUserInfoPayload(userInfo, localUser) {
const settings = userInfo || readStoredSocketUserInfo();
return {
id: settings.id || settings.userId || localUser.id || '',
name: settings.name || localUser.name || DEFAULT_SOCKET_USER_NAME,
avatar: settings.avatar || localUser.avatar || DEFAULT_SOCKET_USER_AVATAR
};
}
export function sendSocketUserInfo(signaling, payload) {
if (!signaling || typeof signaling.sendMessage !== 'function') {
return;
}
signaling.sendMessage('', {
type: 'user-info',
data: payload
});
}

File diff suppressed because it is too large Load Diff

1013
client/public/store.js.tmp Normal file

File diff suppressed because it is too large Load Diff

View 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';
}