拆分part
This commit is contained in:
77
client/public/participants.js
Normal file
77
client/public/participants.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -8,6 +8,14 @@ import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连
|
|||||||
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
|
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
|
||||||
import { showNotification, generateId } from './utils.js'; // 导入通知函数
|
import { showNotification, generateId } from './utils.js'; // 导入通知函数
|
||||||
import chatMessage from './chatmessage.js';
|
import chatMessage from './chatmessage.js';
|
||||||
|
import {
|
||||||
|
DEFAULT_PARTICIPANT_AVATAR,
|
||||||
|
DEFAULT_PARTICIPANT_NAME,
|
||||||
|
buildParticipantsSyncData,
|
||||||
|
omitParticipant,
|
||||||
|
removeParticipant,
|
||||||
|
upsertParticipant
|
||||||
|
} from './participants.js';
|
||||||
|
|
||||||
const AUDIO_CONFIG = {
|
const AUDIO_CONFIG = {
|
||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
@@ -574,16 +582,8 @@ class CallStateManager {
|
|||||||
// participant加入回调(host收到,新participant加入房间)
|
// participant加入回调(host收到,新participant加入房间)
|
||||||
this.renderstreaming.onParticipantJoined = (participantId) => {
|
this.renderstreaming.onParticipantJoined = (participantId) => {
|
||||||
console.log(`Participant joined: ${participantId}`);
|
console.log(`Participant joined: ${participantId}`);
|
||||||
if (!this.state.participants[participantId]) {
|
this._upsertParticipant(participantId);
|
||||||
this.state.participants[participantId] = {
|
this._notifyParticipantsUpdate();
|
||||||
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();
|
this.broadcastParticipantsList();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -603,256 +603,20 @@ class CallStateManager {
|
|||||||
this.state.remoteStream = null;
|
this.state.remoteStream = null;
|
||||||
}
|
}
|
||||||
// 清理该 participant 的用户信息
|
// 清理该 participant 的用户信息
|
||||||
delete this.state.participants[participantId];
|
this._removeParticipant(participantId);
|
||||||
// 通知UI更新,用participantId作为connectionId传给renderer
|
// 通知UI更新,用participantId作为connectionId传给renderer
|
||||||
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
|
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this._notifyParticipantsUpdate();
|
||||||
this.broadcastParticipantsList();
|
this.broadcastParticipantsList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 轨道事件回调
|
// 轨道事件回调
|
||||||
this.renderstreaming.onTrackEvent = (data) => {
|
this.renderstreaming.onTrackEvent = (data) => {
|
||||||
const direction = data.transceiver.direction;
|
this._handleTrackEvent(data);
|
||||||
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.renderstreaming.onMessage = (data) => {
|
this.renderstreaming.onMessage = (data) => {
|
||||||
console.log('收到消息:', data);
|
this._handleRenderStreamingMessage(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)可以忽略
|
|
||||||
// 来自其他Participant:participantId存在且在participants中,且不是自身
|
|
||||||
// 来自Host:participantId存在但不是自身(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):
|
|
||||||
// 更新remoteUser(Host的视频流是本端远端画面)
|
|
||||||
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端:更新单一remoteUser(Host的信息)
|
|
||||||
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}秒`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,6 +688,256 @@ class CallStateManager {
|
|||||||
this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' });
|
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 - 消息类型
|
* @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
|
||||||
* 包含Host自身信息 + 所有Participant信息
|
* 包含Host自身信息 + 所有Participant信息
|
||||||
@@ -946,25 +975,7 @@ class CallStateManager {
|
|||||||
broadcastParticipantsList() {
|
broadcastParticipantsList() {
|
||||||
if (this.role !== 'host' || !this.renderstreaming) return;
|
if (this.role !== 'host' || !this.renderstreaming) return;
|
||||||
|
|
||||||
const memberList = {};
|
const memberList = buildParticipantsSyncData(this.state.session.localUser, this.state.participants);
|
||||||
|
|
||||||
// 添加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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderstreaming.sendMessage({
|
this.renderstreaming.sendMessage({
|
||||||
type: 'participants-sync',
|
type: 'participants-sync',
|
||||||
|
|||||||
Reference in New Issue
Block a user