From 85f80d9f59f63bf81e4179d2db4642fbee3ef9fe Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.COM> Date: Sat, 25 Apr 2026 13:29:35 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90m=E3=80=91=E5=8D=A0=E4=BD=8D=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApp/client/public/onebyone/renderer.js | 33 ++-- WebApp/client/public/onebyone/store.js | 180 +++++++++++----------- WebApp/client/src/signaling.js | 4 + WebApp/src/class/websockethandler.ts | 5 +- 4 files changed, 115 insertions(+), 107 deletions(-) diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index b962d4c..34a319d 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -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) { diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 10c7766..41e696b 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -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 }); } diff --git a/WebApp/client/src/signaling.js b/WebApp/client/src/signaling.js index 5127b30..f989113 100644 --- a/WebApp/client/src/signaling.js +++ b/WebApp/client/src/signaling.js @@ -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": diff --git a/WebApp/src/class/websockethandler.ts b/WebApp/src/class/websockethandler.ts index e8d92e1..d235e2d 100644 --- a/WebApp/src/class/websockethandler.ts +++ b/WebApp/src/class/websockethandler.ts @@ -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 })); } } }