【m】占位重构完成

This commit is contained in:
2026-04-25 13:29:35 +08:00
parent d80e64ca66
commit 85f80d9f59
4 changed files with 115 additions and 107 deletions

View File

@@ -158,6 +158,10 @@ class UIRenderer {
// 远程媒体状态变化 - 更新远程视频和用户列表
this.renderRemoteVideo(state.session.remoteUser); // 渲染远程视频
this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表
// Host端精准更新发送者participant tile的占位背景
if (changes.participantId) {
this.renderParticipantVideoPlaceholder(changes.participantId, !state.session.remoteUser.mediaState.video);
}
break;
case 'USER_LIST_UPDATE':
// 用户列表更新 - 重新渲染用户列表
@@ -303,20 +307,6 @@ class UIRenderer {
}
}
// Host端同步更新participant grid中tile的占位背景
// 通过 media-state-changed 信令驱动(比视频轨道事件更可靠)
const participantGrid = this.elements.participantGrid;
if (participantGrid && !participantGrid.classList.contains('hidden')) {
const shouldShowParticipantPlaceholder = !remoteUser.mediaState.video;
const tiles = participantGrid.querySelectorAll('[data-participant-id]');
tiles.forEach(tile => {
const placeholder = tile.querySelector('.participant-video-placeholder');
if (placeholder) {
toggleElement(placeholder, shouldShowParticipantPlaceholder);
}
});
}
// 渲染说话状态
if (this.elements.remoteSpeakingIndicator) {
toggleElement(this.elements.remoteSpeakingIndicator, remoteUser.mediaState.isSpeaking);
@@ -501,6 +491,21 @@ class UIRenderer {
}
}
// 精准更新指定participant tile的占位背景
// participantId: 发送media-state-changed的participant的连接ID
// showPlaceholder: 是否显示占位背景视频关闭时为true
renderParticipantVideoPlaceholder(participantId, showPlaceholder) {
const grid = this.elements.participantGrid;
if (!grid) return;
const tile = grid.querySelector(`[data-participant-id="${participantId}"]`);
if (!tile) return;
const placeholder = tile.querySelector('.participant-video-placeholder');
if (placeholder) {
toggleElement(placeholder, showPlaceholder);
console.log(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`);
}
}
// 渲染Participant端的单一远端视频Host画面
renderSingleRemoteStream(stream) {
if (this.elements.remoteVideo && stream) {

View File

@@ -137,108 +137,106 @@ class CallStateManager {
// 如果是开启视频,重新获取摄像头资源
if (mediaType === 'video' && value) {
if (this.state.localStream) {
// 停止当前的媒体流
try {
// 只获取新的视频轨道,不干扰正在工作的音频
const newVideoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
const newVideoTrack = newVideoStream.getVideoTracks()[0];
if (!newVideoTrack) {
throw new Error('Failed to get video track');
}
// 更新本地流中的视频轨道(替换旧的已停止的轨道)
if (this.state.localStream) {
this.state.localStream.getTracks().forEach(track => track.stop());
const oldVideoTracks = this.state.localStream.getVideoTracks();
oldVideoTracks.forEach(track => {
track.stop();
this.state.localStream.removeTrack(track);
});
this.state.localStream.addTrack(newVideoTrack);
} else {
// 本地流不存在时(不应该发生),使用新流
this.state.localStream = newVideoStream;
}
this.state.localStream = null;
}
// 请求摄像头权限并获取媒体流
this.state.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
await this.getLocalStream();
// 更新WebRTC连接中的媒体轨道
if (this.renderstreaming) {
console.log('Updating media tracks in WebRTC connection');
// 更新WebRTC连接中的视频轨道
if (this.renderstreaming) {
console.log('Updating video track in WebRTC connection');
// 获取所有收发器
const transceivers = this.renderstreaming.getTransceivers();
console.log('All transceivers:', transceivers);
if (this.role === 'host') {
// Host端需要遍历所有participant的peer来替换视频轨道
const participantIds = Object.keys(this.state.remoteStreams);
for (const participantId of participantIds) {
const transceivers = this.renderstreaming.getTransceivers(participantId);
if (!transceivers) continue;
// 查找现有的视频和音频收发器
const videoTransceivers = transceivers.filter(t => {
return t.sender && t.sender.track && t.sender.track.kind === 'video';
});
console.log('Found video transceivers:', videoTransceivers);
const videoTransceivers = transceivers.filter(t =>
t.sender && t.sender.track && t.sender.track.kind === 'video'
);
const audioTransceivers = transceivers.filter(t => {
return t.sender && t.sender.track && t.sender.track.kind === 'audio';
});
console.log('Found audio transceivers:', audioTransceivers);
// 获取新的视频和音频轨道
const videoTracks = this.state.localStream.getVideoTracks();
console.log('New video tracks:', videoTracks);
const audioTracks = this.state.localStream.getAudioTracks();
console.log('New audio tracks:', audioTracks);
// 更新音频轨道
if (audioTracks.length > 0) {
const newAudioTrack = audioTracks[0];
console.log('Using new audio track:', newAudioTrack);
if (audioTransceivers.length > 0) {
// 替换现有的音频轨道
for (const transceiver of audioTransceivers) {
try {
console.log('Replacing audio track in transceiver:', transceiver);
await transceiver.sender.replaceTrack(newAudioTrack);
console.log('Successfully replaced audio track');
} catch (error) {
console.error('Error replacing audio track:', error);
if (videoTransceivers.length > 0) {
for (const transceiver of videoTransceivers) {
try {
await transceiver.sender.replaceTrack(newVideoTrack);
console.log(`Replaced video track for participant ${participantId}`);
} catch (error) {
console.error(`Error replacing video track for ${participantId}:`, error);
}
}
} else {
// 没有视频收发器,添加新的
try {
this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId);
console.log(`Added new video transceiver for participant ${participantId}`);
} catch (error) {
console.error(`Error adding video transceiver for ${participantId}:`, error);
}
}
// 设置编解码器偏好
setTimeout(() => { this.setCodecPreferences(participantId); }, 100);
}
} else {
// 添加新的音频收发器
try {
console.log('Adding new audio transceiver');
const transceiver = this.renderstreaming.addTransceiver(newAudioTrack, { direction: 'sendonly' });
console.log('Added new audio transceiver:', transceiver);
} catch (error) {
console.error('Error adding new audio transceiver:', error);
}
}
}
// 更新视频轨道
if (videoTracks.length > 0) {
const newVideoTrack = videoTracks[0];
console.log('Using new video track:', newVideoTrack);
// Participant端使用单一peer
const transceivers = this.renderstreaming.getTransceivers();
if (transceivers) {
const videoTransceivers = transceivers.filter(t =>
t.sender && t.sender.track && t.sender.track.kind === 'video'
);
if (videoTransceivers.length > 0) {
// 替换现有的视频轨道
for (const transceiver of videoTransceivers) {
try {
console.log('Replacing video track in transceiver:', transceiver);
await transceiver.sender.replaceTrack(newVideoTrack);
console.log('Successfully replaced video track');
} catch (error) {
console.error('Error replacing video track:', error);
if (videoTransceivers.length > 0) {
for (const transceiver of videoTransceivers) {
try {
await transceiver.sender.replaceTrack(newVideoTrack);
console.log('Successfully replaced video track');
} catch (error) {
console.error('Error replacing video track:', error);
}
}
} else {
try {
this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' });
console.log('Added new video transceiver');
} catch (error) {
console.error('Error adding video transceiver:', error);
}
}
}
} else {
// 添加新的视频收发器
try {
console.log('Adding new video transceiver');
const transceiver = this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' });
console.log('Added new video transceiver:', transceiver);
} catch (error) {
console.error('Error adding new video transceiver:', error);
}
setTimeout(() => { this.setCodecPreferences(); }, 100);
}
}
// 更新状态和通知UI
this.state.session.localUser.mediaState.video = true;
this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream });
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true });
this.emitMediaStateChange();
this.startLocalActivityDetection();
// 延迟设置编解码器偏好,确保收发器已完全创建
setTimeout(() => {
this.setCodecPreferences();
}, 100);
} catch (error) {
console.error('Error reopening video:', error);
this.state.session.localUser.mediaState.video = false;
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false });
}
} else {
// 直接更新媒体状态
@@ -481,9 +479,9 @@ class CallStateManager {
}
} else if (data.type === 'media-state-changed') {
// 处理媒体状态变化
console.log('收到媒体状态变化:', data.data);
// 更新远程用户的媒体状态
this.updateRemoteMedia(data.data);
console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId);
// 更新远程用户的媒体状态传递participantId以便精准定位
this.updateRemoteMedia(data.data, data.participantId);
} else if (data.type === 'user-info') {
// 处理用户信息更新
console.log('收到用户信息:', data.data);
@@ -601,12 +599,12 @@ class CallStateManager {
// 更新远端媒体状态 (由 WebSocket 触发)
updateRemoteMedia(mediaState) {
updateRemoteMedia(mediaState, participantId) {
this.state.session.remoteUser.mediaState = {
...this.state.session.remoteUser.mediaState,
...mediaState
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState });
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState, participantId });
// 通知UI更新用户列表
this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}

View File

@@ -191,6 +191,10 @@ export class WebSocketSignaling extends EventTarget {
this.dispatchEvent(new CustomEvent('candidate', { detail: { connectionId: msg.from, candidate: msg.data.candidate, sdpMLineIndex: msg.data.sdpMLineIndex, sdpMid: msg.data.sdpMid, participantId: msg.participantId } }));
break;
case "on-message":
// 将participantId附加到消息数据中以便Host识别消息发送者
if (msg.participantId) {
msg.data.participantId = msg.participantId;
}
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.data }));
break;
case "participant-left":

View File

@@ -448,6 +448,7 @@ function onMessage(ws: WebSocket, message: any): void {
// 获取连接ID
const connectionId = message.connectionId;
const chatMessage = message.message;
const senderParticipantId = (ws as any).participantId;
if (connectionGroup.has(connectionId)) {
const group = connectionGroup.get(connectionId);
if (group.host === ws) {
@@ -456,8 +457,8 @@ function onMessage(ws: WebSocket, message: any): void {
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage }));
});
} else {
// participant发送消息转发给host
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage }));
// participant发送消息转发给host附带participantId以便host识别发送者
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage, participantId: senderParticipantId }));
}
}
}