【m】占位重构完成
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user