From b66b639df0494d69fa43e1a0167256c94c09efc9 Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.COM> Date: Thu, 23 Apr 2026 16:08:08 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90m=E3=80=91=E6=B5=8B=E8=AF=95=EF=BC=8C?= =?UTF-8?q?=E4=BE=BF=E4=BA=8E=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebApp/client/public/onebyone/index.html | 10 +- WebApp/client/public/onebyone/renderer.js | 191 +++++++++++++++++++++- WebApp/client/public/onebyone/store.js | 48 ++++-- 3 files changed, 232 insertions(+), 17 deletions(-) diff --git a/WebApp/client/public/onebyone/index.html b/WebApp/client/public/onebyone/index.html index 31a7f25..1dad8eb 100644 --- a/WebApp/client/public/onebyone/index.html +++ b/WebApp/client/public/onebyone/index.html @@ -149,7 +149,15 @@
+ + +
diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index d81271f..869a267 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -16,6 +16,8 @@ class UIRenderer { // 头部和底部 header: document.querySelector('header'), footer: document.querySelector('footer'), + // 多Participant视频网格 + participantGrid: document.getElementById('participantGrid'), // 头部内容 headerTitle: document.getElementById('headerTitle'), callDuration: document.getElementById('callDuration'), @@ -144,10 +146,12 @@ class UIRenderer { // 本地流获取成功 - 更新本地视频显示 this.renderLocalStream(state.localStream); // 渲染本地流 this.renderLocalVideo(state.session.localUser, state.localStream); // 渲染本地视频 + // 如果host端有participant tile,同步更新它们的视频源 + this.updateParticipantGridStreams(state.localStream); break; case 'REMOTE_STREAM_OBTAINED': // 远程流获取成功 - 更新远程视频显示 - this.renderRemoteStream(state.remoteStream); // 渲染远程流 + this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 渲染远程流 // 当获取到远程流时,隐藏连接中提示 if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); @@ -387,7 +391,127 @@ class UIRenderer { } // 渲染远程视频流 - renderRemoteStream(stream) { + renderRemoteStream(stream, connectionId, isHost) { + if (isHost && connectionId) { + // Host端: 渲染到 participant 视频网格 + this.renderParticipantStream(stream, connectionId); + } else { + // Participant端: 渲染到单一远端视频(Host的画面) + this.renderSingleRemoteStream(stream); + } + } + + // 渲染Host端的多Participant视频网格 + // 所有participant tile显示host的本地视频流(host监控自己广播的画面) + renderParticipantStream(stream, connectionId) { + const grid = this.elements.participantGrid; + if (!grid) return; + + // 显示网格,隐藏单路远端视频 + grid.classList.remove('hidden'); + + // 查找或创建该 connectionId 的视频格子 + let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); + if (!tile) { + // 创建新的视频格子 + tile = document.createElement('div'); + tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center'; + tile.dataset.participantId = connectionId; + + const video = document.createElement('video'); + video.className = 'w-full h-full object-contain'; + video.autoplay = true; + video.playsinline = true; + video.muted = true; // 静音,因为显示的是host自己的画面,不需要播放自己的声音 + video.id = `participantVideo_${connectionId}`; + tile.appendChild(video); + + // 添加隐藏的audio元素播放远端音频(participant的声音) + const audio = document.createElement('audio'); + audio.autoplay = true; + audio.id = `participantAudio_${connectionId}`; + audio.style.display = 'none'; + tile.appendChild(audio); + + // 添加参与者名称标签 + const label = document.createElement('div'); + label.className = 'absolute bottom-3 left-3 glass px-3 py-1 rounded-full text-xs flex items-center gap-2'; + label.innerHTML = `Participant ${connectionId.slice(-4)}`; + tile.appendChild(label); + + // 添加"直播中"标识 + const liveTag = document.createElement('div'); + liveTag.className = 'absolute top-3 right-3 bg-red-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1'; + liveTag.innerHTML = `直播`; + tile.appendChild(liveTag); + + grid.appendChild(tile); + console.log(`Created participant video tile for ${connectionId}`); + } + + if (tile) { + // 视频元素显示host的本地流(监控自己广播的画面) + const video = tile.querySelector('video'); + if (video) { + const localStream = this.stateManager.getState().localStream; + if (localStream) { + video.srcObject = localStream; + console.log(`Set host local stream for participant tile ${connectionId}`); + } else { + console.warn('Host local stream not available for participant tile'); + } + } + + // 隐藏audio元素播放远端流(participant的音频) + const audio = tile.querySelector('audio'); + if (audio && stream) { + audio.srcObject = stream; + console.log(`Set remote audio for participant tile ${connectionId}`); + } + + // 隐藏单路远端视频和占位符 + const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); + if (remoteVideoDiv) { + remoteVideoDiv.classList.add('hidden'); + } + } + + // 根据参与者数量调整网格列数 + const tileCount = grid.querySelectorAll('[data-participant-id]').length; + if (tileCount <= 1) { + grid.style.gridTemplateColumns = '1fr'; + } else if (tileCount <= 4) { + grid.style.gridTemplateColumns = 'repeat(2, 1fr)'; + } else { + grid.style.gridTemplateColumns = 'repeat(3, 1fr)'; + } + + // 隐藏连接中提示 + if (this.elements.connectingOverlay) { + this.elements.connectingOverlay.classList.add('hidden'); + } + + // 隐藏远端视频占位符 + if (this.elements.remoteVideoPlaceholder) { + this.elements.remoteVideoPlaceholder.classList.add('hidden'); + } + } + + // 更新participant网格中所有tile的视频源(host本地流变化时调用) + updateParticipantGridStreams(localStream) { + const grid = this.elements.participantGrid; + if (!grid || grid.classList.contains('hidden')) return; + + grid.querySelectorAll('[data-participant-id]').forEach(tile => { + const video = tile.querySelector('video'); + if (video && localStream) { + video.srcObject = localStream; + } + }); + } + + // 渲染Participant端的单一远端视频(Host画面) + renderSingleRemoteStream(stream) { if (this.elements.remoteVideo && stream) { console.log('Rendering remote stream:', stream); @@ -892,6 +1016,20 @@ class UIRenderer { // 渲染通话结束 renderCallEnded() { console.log('Call ended'); + + // 清理participant网格 + const grid = this.elements.participantGrid; + if (grid) { + grid.querySelectorAll('[data-participant-id]').forEach(tile => { + const video = tile.querySelector('video'); + if (video) video.srcObject = null; + const audio = tile.querySelector('audio'); + if (audio) audio.srcObject = null; + tile.remove(); + }); + grid.classList.add('hidden'); + } + // 跳转到结束通话界面 window.location.href = './endcall/endcall.html'; } @@ -899,14 +1037,55 @@ class UIRenderer { // 渲染participant离开(host端,房间仍然存在) renderParticipantLeft(connectionId) { console.log(`Participant left: ${connectionId}, updating UI`); + + // 移除该 participant 的视频格子 + const grid = this.elements.participantGrid; + if (grid) { + const tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); + if (tile) { + // 清理video元素 + const video = tile.querySelector('video'); + if (video) { + video.srcObject = null; + } + // 清理audio元素 + const audio = tile.querySelector('audio'); + if (audio) { + audio.srcObject = null; + } + tile.remove(); + console.log(`Removed participant video tile for ${connectionId}`); + } + + // 如果没有 participant 了,隐藏网格,显示单路远端视频 + const remainingTiles = grid.querySelectorAll('[data-participant-id]'); + if (remainingTiles.length === 0) { + grid.classList.add('hidden'); + // 显示单路远端视频区域(恢复默认) + const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); + if (remoteVideoDiv) { + remoteVideoDiv.classList.remove('hidden'); + } + // 显示远端视频占位符 + if (this.elements.remoteVideoPlaceholder) { + this.elements.remoteVideoPlaceholder.classList.remove('hidden'); + } + } else { + // 调整网格列数 + if (remainingTiles.length <= 1) { + grid.style.gridTemplateColumns = '1fr'; + } else if (remainingTiles.length <= 4) { + grid.style.gridTemplateColumns = 'repeat(2, 1fr)'; + } else { + grid.style.gridTemplateColumns = 'repeat(3, 1fr)'; + } + } + } + // 更新远程用户状态显示为离线 if (this.elements.remoteNetworkIndicator) { this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full'; } - // 显示断开连接的遮罩层(如果存在) - if (this.elements.disconnectedOverlay) { - this.elements.disconnectedOverlay.classList.remove('hidden'); - } } // 获取状态文本 diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index e4db728..6e193e4 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -19,7 +19,8 @@ class CallStateManager { status: 'idle' // 初始状态为空闲 }, localStream: null, // MediaStream 对象 - remoteStream: null // MediaStream 对象 + remoteStream: null, // 单路远端流(兼容旧逻辑,participant端使用) + remoteStreams: {} // 多路远端流 Map: { connectionId: MediaStream }(host端使用) }; // 监听器数组 @@ -380,12 +381,16 @@ this.renderstreaming.onNewPeer = (connectionId) => { this.updateRemoteUserStatus('offline'); this.updateRemoteUserNetworkQuality('no_signal'); showNotification('对方已离开通话', 'warning'); - // 清理远端流,重置Peer连接为新participant加入做准备 + // 清理该 participant 的远端流 + if (this.state.remoteStreams[connectionId]) { + this.state.remoteStreams[connectionId].getTracks().forEach(track => track.stop()); + delete this.state.remoteStreams[connectionId]; + } + // 同时清理单路远端流(兼容) if (this.state.remoteStream) { this.state.remoteStream.getTracks().forEach(track => track.stop()); this.state.remoteStream = null; } - this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: null }); // 通知UI更新 this.notify({ type: 'PARTICIPANT_LEFT', connectionId: connectionId }); }; @@ -394,25 +399,48 @@ this.renderstreaming.onNewPeer = (connectionId) => { this.renderstreaming.onTrackEvent = (data) => { const direction = data.transceiver.direction; if (direction == "sendrecv" || direction == "recvonly") { - if (this.state.remoteStream == null) { - this.state.remoteStream = new MediaStream(); + // 获取当前连接的远端流 + const trackConnectionId = this.connectionId; + // Host端: 每个participant有独立的远端流 + // Participant端: 只有一个host的远端流 + const isHost = this.role === 'host'; + + // 获取或创建对应的远端流 + let targetStream = null; + if (isHost) { + // Host端: 按 connectionId 管理多路远端流 + if (!this.state.remoteStreams[trackConnectionId]) { + this.state.remoteStreams[trackConnectionId] = new MediaStream(); + } + targetStream = this.state.remoteStreams[trackConnectionId]; + } else { + // Participant端: 使用单一远端流 + if (this.state.remoteStream == null) { + this.state.remoteStream = new MediaStream(); + } + targetStream = this.state.remoteStream; } // 检查是否已经有相同类型的轨道 - const existingTracks = this.state.remoteStream.getTracks().filter(track => track.kind === data.track.kind); + const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind); // 移除旧的轨道 existingTracks.forEach(track => { - this.state.remoteStream.removeTrack(track); + targetStream.removeTrack(track); console.log('Removed old track:', track.kind); }); // 添加新的轨道 - this.state.remoteStream.addTrack(data.track); - console.log('Added new track:', data.track.kind); + targetStream.addTrack(data.track); + console.log('Added new track:', data.track.kind, 'to stream:', trackConnectionId); // 通知UI远程流已更新 - this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream }); + this.notify({ + type: 'REMOTE_STREAM_OBTAINED', + stream: targetStream, + connectionId: trackConnectionId, + isHost: isHost + }); console.log('Notified UI about remote stream update'); // 只有当收到远程流时才更新远程用户状态为在线 if (this.state.session.remoteUser.status !== 'online') {