拆分part

This commit is contained in:
2026-05-24 00:54:58 +08:00
parent a413c56a6f
commit 44f4b30313
2 changed files with 357 additions and 269 deletions

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

@@ -8,6 +8,14 @@ import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
import { showNotification, generateId } from './utils.js'; // 导入通知函数
import chatMessage from './chatmessage.js';
import {
DEFAULT_PARTICIPANT_AVATAR,
DEFAULT_PARTICIPANT_NAME,
buildParticipantsSyncData,
omitParticipant,
removeParticipant,
upsertParticipant
} from './participants.js';
const AUDIO_CONFIG = {
echoCancellation: true,
@@ -574,16 +582,8 @@ class CallStateManager {
// participant加入回调host收到新participant加入房间
this.renderstreaming.onParticipantJoined = (participantId) => {
console.log(`Participant joined: ${participantId}`);
if (!this.state.participants[participantId]) {
this.state.participants[participantId] = {
id: '',
name: '参与者',
avatar: '/images/p2.png',
mediaState: { audio: false, video: true, isSpeaking: false },
status: 'online'
};
}
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
this._upsertParticipant(participantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
};
@@ -603,256 +603,20 @@ class CallStateManager {
this.state.remoteStream = null;
}
// 清理该 participant 的用户信息
delete this.state.participants[participantId];
this._removeParticipant(participantId);
// 通知UI更新用participantId作为connectionId传给renderer
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
};
// 轨道事件回调
this.renderstreaming.onTrackEvent = (data) => {
const direction = data.transceiver.direction;
if (direction == "sendrecv" || direction == "recvonly") {
// 使用participantId区分不同participant的流
const trackParticipantId = data.participantId || this.connectionId;
const isHost = this.role === 'host';
let targetStream = null;
if (isHost) {
// Host端: 按 participantId 管理多路远端流
if (!this.state.remoteStreams[trackParticipantId]) {
this.state.remoteStreams[trackParticipantId] = new MediaStream();
}
targetStream = this.state.remoteStreams[trackParticipantId];
} else {
// Participant端: 使用单一远端流
if (this.state.remoteStream == null) {
this.state.remoteStream = new MediaStream();
}
targetStream = this.state.remoteStream;
}
// 检查是否已经有相同类型的轨道
const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind);
existingTracks.forEach(track => {
targetStream.removeTrack(track);
console.log('Removed old track:', track.kind);
});
targetStream.addTrack(data.track);
console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId);
// Host端兜底确保participants中有该participant条目
if (isHost && !this.state.participants[trackParticipantId]) {
this.state.participants[trackParticipantId] = {
id: '',
name: '参与者',
avatar: '/images/p2.png',
mediaState: { audio: false, video: true, isSpeaking: false },
status: 'online'
};
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
this.broadcastParticipantsList();
}
// 通知UI远程流已更新
// 关键优化:如果是音频轨道先到达且流中尚无视频轨道,
// 延迟通知UI等待视频轨道到达避免音频先触发的UI更新导致黑屏
const notifyStreamUpdate = () => {
this.notify({
type: 'REMOTE_STREAM_OBTAINED',
stream: targetStream,
connectionId: trackParticipantId,
isHost: isHost
});
console.log('Notified UI about remote stream update');
};
if (data.track.kind === 'audio' && targetStream.getVideoTracks().length === 0) {
// 音频先到视频尚未到达延迟200ms通知给视频轨道到达的机会
console.log('Audio track arrived first, delaying stream notification for video track...');
setTimeout(() => {
const nowHasVideo = targetStream.getVideoTracks().length > 0;
console.log(`After delay, stream has video: ${nowHasVideo}`);
notifyStreamUpdate();
}, 200);
} else {
// 视频轨道到达,或音频视频同时存在:立即通知
notifyStreamUpdate();
}
// 只有当收到远程流时才更新远程用户状态为在线
if (this.state.session.remoteUser.status !== 'online') {
this.updateRemoteUserStatus('online');
// 更新远程用户网络质量为好
this.updateRemoteUserNetworkQuality('good');
this.sendMessage('user-info', {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar
});
// 启动通话时长计时器(避免重复启动)
if (!this.durationInterval) {
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
}
// 如果是音频轨道,启动远程音频活动检测
if (data.track.kind === 'audio') {
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
}
} else if (direction == "sendonly") {
// 本地发送轨道,启动本地音频活动检测
if (data.track.kind === 'audio') {
this.startActivityDetection(this.state.localStream, { isLocal: true });
}
}
this._handleTrackEvent(data);
};
this.renderstreaming.onMessage = (data) => {
console.log('收到消息:', data);
if (data.type === 'chat-message') {
const chatPayload = data.data || data.message;
if (!chatPayload) {
return;
}
chatMessage.handleChatMessage(chatPayload);
if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) {
this.state.participants[data.participantId].id = chatPayload.senderId;
if (chatPayload.senderName) {
this.state.participants[data.participantId].name = chatPayload.senderName;
}
if (chatPayload.senderAvatar) {
this.state.participants[data.participantId].avatar = chatPayload.senderAvatar;
}
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
this.broadcastParticipantsList();
}
if (!this.role || this.role !== 'host') {
if (data.participantId && this.state.participants[data.participantId]) {
if (chatPayload.senderName) {
this.state.participants[data.participantId].name = chatPayload.senderName;
}
if (chatPayload.senderAvatar) {
this.state.participants[data.participantId].avatar = chatPayload.senderAvatar;
}
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
} else if (chatPayload.senderId !== this.state.session.localUser.id) {
this.state.session.remoteUser = {
...this.state.session.remoteUser,
id: chatPayload.senderId,
name: chatPayload.senderName,
avatar: chatPayload.senderAvatar
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
}
}
} else if (data.type === 'media-state-changed') {
// 处理媒体状态变化
console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId);
if (this.role === 'host') {
// Host端按participantId同步更新participants中对应participant的mediaState
if (data.participantId && this.state.participants[data.participantId]) {
this.state.participants[data.participantId].mediaState = {
...this.state.participants[data.participantId].mediaState,
...data.data
};
}
// 更新远端媒体状态
this.updateRemoteMedia(data.data, data.participantId);
// 通知UI更新participants
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
// Host端广播最新成员列表含媒体状态给所有Participant
this.broadcastParticipantsList();
} else {
// Participant端根据消息来源更新对应条目
// Host的participantId在participants-sync中也会同步所以不能仅靠participants中有无该key判断
// 自身发出的消息回声participantId === selfParticipantId可以忽略
// 来自其他ParticipantparticipantId存在且在participants中且不是自身
// 来自HostparticipantId存在但不是自身Host不在selfParticipantId中
if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) {
// 来自其他Participant的媒体状态变化仅更新participants中对应条目
// 不调用updateRemoteMedia因为Participant端没有其他Participant的视频流
this.state.participants[data.participantId].mediaState = {
...this.state.participants[data.participantId].mediaState,
...data.data
};
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
} else if (data.participantId === this.selfParticipantId) {
// 自身消息回声,忽略
} else {
// 来自Host的媒体状态变化Host的participantId不匹配participants中任何条目或无participantId
// 更新remoteUserHost的视频流是本端远端画面
console.log('Received media-state-changed from Host, updating remoteUser:', data.data);
this.updateRemoteMedia(data.data, data.participantId);
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
}
}
} else if (data.type === 'user-info') {
// 处理用户信息更新
console.log('收到用户信息:', data.data, 'from participant:', data.participantId);
if (data.data) {
if (data.participantId && this.role === 'host') {
// Host端按participantId存储到participants Map
if (!this.state.participants[data.participantId]) {
this.state.participants[data.participantId] = {
id: '',
name: '参与者',
avatar: '/images/p2.png',
mediaState: { audio: false, video: true, isSpeaking: false },
status: 'online'
};
}
this.state.participants[data.participantId].id = data.data.id || '';
this.state.participants[data.participantId].name = data.data.name || '参与者';
this.state.participants[data.participantId].avatar = data.data.avatar || '/images/p2.png';
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
this.broadcastParticipantsList();
} else {
// Participant端更新单一remoteUserHost的信息
this.state.session.remoteUser = {
...this.state.session.remoteUser,
id: data.data.id || this.state.session.remoteUser.id,
name: data.data.name || this.state.session.remoteUser.name,
avatar: data.data.avatar || this.state.session.remoteUser.avatar
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
}
}
} else if (data.type === 'participants-sync') {
// Participant端接收Host广播的完整成员列表
if (this.role !== 'host' && data.data) {
console.log('收到成员列表同步:', data.data);
// 过滤掉自身条目避免在列表中重复显示自身已作为localUser显示
const filtered = {};
for (const [pid, pInfo] of Object.entries(data.data)) {
if (pid !== this.selfParticipantId) {
filtered[pid] = pInfo;
}
}
this.state.participants = filtered;
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
// 同步通话时长仅首次同步将Host的时长作为基准
if (!this.durationSynced && typeof data.callDuration === 'number') {
this.state.session.duration = data.callDuration;
this.durationSynced = true;
// 如果计时器尚未启动(远程流还未到达),先启动计时器
if (!this.durationInterval) {
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
console.log(`通话时长已同步,当前时长: ${data.callDuration}`);
}
}
}
this._handleRenderStreamingMessage(data);
};
}
@@ -924,6 +688,256 @@ class CallStateManager {
this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' });
}
_handleTrackEvent(data) {
const direction = data.transceiver.direction;
if (direction === 'sendrecv' || direction === 'recvonly') {
this._handleIncomingTrack(data);
return;
}
if (direction === 'sendonly' && data.track.kind === 'audio') {
this.startActivityDetection(this.state.localStream, { isLocal: true });
}
}
_handleIncomingTrack(data) {
const trackParticipantId = data.participantId || this.connectionId;
const isHost = this.role === 'host';
const targetStream = this._getOrCreateRemoteStream(trackParticipantId, isHost);
this._replaceTrackOfSameKind(targetStream, data.track);
console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId);
if (isHost && !this.state.participants[trackParticipantId]) {
this._upsertParticipant(trackParticipantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
}
this._notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, data.track.kind);
if (this.state.session.remoteUser.status !== 'online') {
this.updateRemoteUserStatus('online');
this.updateRemoteUserNetworkQuality('good');
this.sendMessage('user-info', {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar
});
this._startDurationTimer();
}
if (data.track.kind === 'audio') {
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
}
}
_getOrCreateRemoteStream(trackParticipantId, isHost) {
if (isHost) {
if (!this.state.remoteStreams[trackParticipantId]) {
this.state.remoteStreams[trackParticipantId] = new MediaStream();
}
return this.state.remoteStreams[trackParticipantId];
}
if (this.state.remoteStream == null) {
this.state.remoteStream = new MediaStream();
}
return this.state.remoteStream;
}
_replaceTrackOfSameKind(targetStream, track) {
const existingTracks = targetStream.getTracks().filter(existingTrack => existingTrack.kind === track.kind);
existingTracks.forEach(existingTrack => {
targetStream.removeTrack(existingTrack);
console.log('Removed old track:', existingTrack.kind);
});
targetStream.addTrack(track);
}
_notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, trackKind) {
const notifyStreamUpdate = () => {
this.notify({
type: 'REMOTE_STREAM_OBTAINED',
stream: targetStream,
connectionId: trackParticipantId,
isHost
});
console.log('Notified UI about remote stream update');
};
if (trackKind === 'audio' && targetStream.getVideoTracks().length === 0) {
console.log('Audio track arrived first, delaying stream notification for video track...');
setTimeout(() => {
const nowHasVideo = targetStream.getVideoTracks().length > 0;
console.log(`After delay, stream has video: ${nowHasVideo}`);
notifyStreamUpdate();
}, 200);
return;
}
notifyStreamUpdate();
}
_handleRenderStreamingMessage(data) {
console.log('收到消息:', data);
switch (data.type) {
case 'chat-message':
this._handleChatMessage(data);
break;
case 'media-state-changed':
this._handleMediaStateChangedMessage(data);
break;
case 'user-info':
this._handleUserInfoMessage(data);
break;
case 'participants-sync':
this._handleParticipantsSyncMessage(data);
break;
default:
break;
}
}
_handleChatMessage(data) {
const chatPayload = data.data || data.message;
if (!chatPayload) {
return;
}
chatMessage.handleChatMessage(chatPayload);
if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
id: chatPayload.senderId,
...(chatPayload.senderName ? { name: chatPayload.senderName } : {}),
...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {})
});
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
if (!this.role || this.role !== 'host') {
if (data.participantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
...(chatPayload.senderName ? { name: chatPayload.senderName } : {}),
...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {})
});
this._notifyParticipantsUpdate();
} else if (chatPayload.senderId !== this.state.session.localUser.id) {
this._updateRemoteUserProfile({
id: chatPayload.senderId,
name: chatPayload.senderName,
avatar: chatPayload.senderAvatar
});
}
}
}
_handleMediaStateChangedMessage(data) {
console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId);
if (this.role === 'host') {
if (data.participantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
mediaState: data.data
});
}
this.updateRemoteMedia(data.data, data.participantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
mediaState: data.data
});
this._notifyParticipantsUpdate();
return;
}
if (data.participantId === this.selfParticipantId) {
return;
}
console.log('Received media-state-changed from Host, updating remoteUser:', data.data);
this.updateRemoteMedia(data.data, data.participantId);
this._notifyParticipantsUpdate();
}
_handleUserInfoMessage(data) {
console.log('收到用户信息:', data.data, 'from participant:', data.participantId);
if (!data.data) {
return;
}
if (data.participantId && this.role === 'host') {
this._upsertParticipant(data.participantId, {
id: data.data.id || '',
name: data.data.name || DEFAULT_PARTICIPANT_NAME,
avatar: data.data.avatar || DEFAULT_PARTICIPANT_AVATAR
});
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
this._updateRemoteUserProfile({
id: data.data.id || this.state.session.remoteUser.id,
name: data.data.name || this.state.session.remoteUser.name,
avatar: data.data.avatar || this.state.session.remoteUser.avatar
});
}
_handleParticipantsSyncMessage(data) {
if (this.role === 'host' || !data.data) {
return;
}
console.log('收到成员列表同步:', data.data);
this.state.participants = omitParticipant(data.data, this.selfParticipantId);
this._notifyParticipantsUpdate();
this._syncCallDuration(data.callDuration);
}
_updateRemoteUserProfile(profile) {
this.state.session.remoteUser = {
...this.state.session.remoteUser,
...profile
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
}
_syncCallDuration(callDuration) {
if (this.durationSynced || typeof callDuration !== 'number') {
return;
}
this.state.session.duration = callDuration;
this.durationSynced = true;
this._startDurationTimer();
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
console.log(`通话时长已同步,当前时长: ${callDuration}`);
}
_startDurationTimer() {
if (this.durationInterval) {
return;
}
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
/**
* 发送消息
* @param {string} type - 消息类型
@@ -938,6 +952,21 @@ class CallStateManager {
}
}
/**
* Participant state helpers
*/
_notifyParticipantsUpdate() {
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
}
_upsertParticipant(participantId, patch = {}) {
return upsertParticipant(this.state.participants, participantId, patch);
}
_removeParticipant(participantId) {
return removeParticipant(this.state.participants, participantId);
}
/**
* Host端广播完整成员列表给所有Participant
* 包含Host自身信息 + 所有Participant信息
@@ -946,25 +975,7 @@ class CallStateManager {
broadcastParticipantsList() {
if (this.role !== 'host' || !this.renderstreaming) return;
const memberList = {};
// 添加Host自身信息
memberList['host'] = {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar,
mediaState: { ...this.state.session.localUser.mediaState },
status: 'online',
role: 'host'
};
// 添加所有Participant信息
for (const [pid, pInfo] of Object.entries(this.state.participants)) {
memberList[pid] = {
...pInfo,
role: 'participant'
};
}
const memberList = buildParticipantsSyncData(this.state.session.localUser, this.state.participants);
this.renderstreaming.sendMessage({
type: 'participants-sync',