【m】视频渲染完成,测试没问题

This commit is contained in:
2026-04-24 23:38:17 +08:00
parent eba0d07c83
commit 4baad207d6
3 changed files with 20 additions and 78 deletions

View File

@@ -143,11 +143,8 @@ class UIRenderer {
this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表 this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表
break; break;
case 'LOCAL_STREAM_OBTAINED': case 'LOCAL_STREAM_OBTAINED':
// 本地流获取成功 - 更新本地视频显示 this.renderLocalStream(state.localStream);
this.renderLocalStream(state.localStream); // 渲染本地流 this.renderLocalVideo(state.session.localUser, state.localStream);
this.renderLocalVideo(state.session.localUser, state.localStream); // 渲染本地视频
// 如果host端有participant tile同步更新它们的视频源
this.updateParticipantGridStreams(state.localStream);
break; break;
case 'REMOTE_STREAM_OBTAINED': case 'REMOTE_STREAM_OBTAINED':
// 远程流获取成功 - 更新远程视频显示 // 远程流获取成功 - 更新远程视频显示
@@ -402,7 +399,7 @@ class UIRenderer {
} }
// 渲染Host端的多Participant视频网格 // 渲染Host端的多Participant视频网格
// 所有participant tile显示host的本地视频流host监控自己广播的画面 // 每个participant tile显示该participant实际的远端视频流
renderParticipantStream(stream, connectionId) { renderParticipantStream(stream, connectionId) {
const grid = this.elements.participantGrid; const grid = this.elements.participantGrid;
if (!grid) return; if (!grid) return;
@@ -413,7 +410,6 @@ class UIRenderer {
// 查找或创建该 connectionId 的视频格子 // 查找或创建该 connectionId 的视频格子
let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`);
if (!tile) { if (!tile) {
// 创建新的视频格子
tile = document.createElement('div'); tile = document.createElement('div');
tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center'; tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center';
tile.dataset.participantId = connectionId; tile.dataset.participantId = connectionId;
@@ -422,27 +418,20 @@ class UIRenderer {
video.className = 'w-full h-full object-contain'; video.className = 'w-full h-full object-contain';
video.autoplay = true; video.autoplay = true;
video.playsinline = true; video.playsinline = true;
video.muted = true; // 静音,因为显示的是host自己的画面不需要播放自己的声音 video.muted = false; // 静音,播放participant的音频
video.id = `participantVideo_${connectionId}`; video.id = `participantVideo_${connectionId}`;
tile.appendChild(video); 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'); 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.className = 'absolute bottom-3 left-3 glass px-3 py-1 rounded-full text-xs flex items-center gap-2';
label.innerHTML = `<i class="fas fa-user text-indigo-400"></i><span>Participant ${connectionId.slice(-4)}</span>`; label.innerHTML = `<i class="fas fa-user text-indigo-400"></i><span>Participant ${connectionId.slice(-4)}</span>`;
tile.appendChild(label); tile.appendChild(label);
// 添加"直播中"标识 // 在线标识
const liveTag = document.createElement('div'); 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.className = 'absolute top-3 right-3 bg-green-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1';
liveTag.innerHTML = `<span class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></span><span>直播</span>`; liveTag.innerHTML = `<span class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></span><span>在线</span>`;
tile.appendChild(liveTag); tile.appendChild(liveTag);
grid.appendChild(tile); grid.appendChild(tile);
@@ -450,23 +439,11 @@ class UIRenderer {
} }
if (tile) { if (tile) {
// 视频元素显示host的本地流监控自己广播的画面 // 视频元素显示participant的远端视频流
const video = tile.querySelector('video'); const video = tile.querySelector('video');
if (video) { if (video && stream) {
const localStream = this.stateManager.getState().localStream; video.srcObject = stream;
if (localStream) { console.log(`Set remote stream for participant tile ${connectionId}`);
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}`);
} }
// 隐藏单路远端视频和占位符 // 隐藏单路远端视频和占位符
@@ -497,19 +474,6 @@ class UIRenderer {
} }
} }
// 更新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画面 // 渲染Participant端的单一远端视频Host画面
renderSingleRemoteStream(stream) { renderSingleRemoteStream(stream) {
if (this.elements.remoteVideo && stream) { if (this.elements.remoteVideo && stream) {
@@ -1023,8 +987,6 @@ class UIRenderer {
grid.querySelectorAll('[data-participant-id]').forEach(tile => { grid.querySelectorAll('[data-participant-id]').forEach(tile => {
const video = tile.querySelector('video'); const video = tile.querySelector('video');
if (video) video.srcObject = null; if (video) video.srcObject = null;
const audio = tile.querySelector('audio');
if (audio) audio.srcObject = null;
tile.remove(); tile.remove();
}); });
grid.classList.add('hidden'); grid.classList.add('hidden');
@@ -1038,40 +1000,27 @@ class UIRenderer {
renderParticipantLeft(connectionId) { renderParticipantLeft(connectionId) {
console.log(`Participant left: ${connectionId}, updating UI`); console.log(`Participant left: ${connectionId}, updating UI`);
// 移除该 participant 的视频格子
const grid = this.elements.participantGrid; const grid = this.elements.participantGrid;
if (grid) { if (grid) {
const tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); const tile = grid.querySelector(`[data-participant-id="${connectionId}"]`);
if (tile) { if (tile) {
// 清理video元素
const video = tile.querySelector('video'); const video = tile.querySelector('video');
if (video) { if (video) video.srcObject = null;
video.srcObject = null;
}
// 清理audio元素
const audio = tile.querySelector('audio');
if (audio) {
audio.srcObject = null;
}
tile.remove(); tile.remove();
console.log(`Removed participant video tile for ${connectionId}`); console.log(`Removed participant video tile for ${connectionId}`);
} }
// 如果没有 participant 了,隐藏网格,显示单路远端视频
const remainingTiles = grid.querySelectorAll('[data-participant-id]'); const remainingTiles = grid.querySelectorAll('[data-participant-id]');
if (remainingTiles.length === 0) { if (remainingTiles.length === 0) {
grid.classList.add('hidden'); grid.classList.add('hidden');
// 显示单路远端视频区域(恢复默认)
const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in');
if (remoteVideoDiv) { if (remoteVideoDiv) {
remoteVideoDiv.classList.remove('hidden'); remoteVideoDiv.classList.remove('hidden');
} }
// 显示远端视频占位符
if (this.elements.remoteVideoPlaceholder) { if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.remove('hidden'); this.elements.remoteVideoPlaceholder.classList.remove('hidden');
} }
} else { } else {
// 调整网格列数
if (remainingTiles.length <= 1) { if (remainingTiles.length <= 1) {
grid.style.gridTemplateColumns = '1fr'; grid.style.gridTemplateColumns = '1fr';
} else if (remainingTiles.length <= 4) { } else if (remainingTiles.length <= 4) {
@@ -1082,7 +1031,6 @@ class UIRenderer {
} }
} }
// 更新远程用户状态显示为离线
if (this.elements.remoteNetworkIndicator) { if (this.elements.remoteNetworkIndicator) {
this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full'; this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full';
} }

View File

@@ -192,7 +192,7 @@ function onDisconnect(ws: WebSocket, connectionId: string): void {
} else { } else {
// participant断开连接从组中移除并通知host使用participant-left类型host不会关闭房间 // participant断开连接从组中移除并通知host使用participant-left类型host不会关闭房间
group.participants.delete(ws); group.participants.delete(ws);
group.host.send(JSON.stringify({ type: "participant-left", connectionId: connectionId })); group.host.send(JSON.stringify({ type: "participant-left", connectionId: connectionId, participantId: (ws as any).participantId }));
console.log(`Participant left connectionId: ${connectionId}, remaining participants: ${group.participants.size}`); console.log(`Participant left connectionId: ${connectionId}, remaining participants: ${group.participants.size}`);
} }
} }

View File

@@ -74,46 +74,40 @@ export default class WSSignaling {
// 根据消息类型处理 // 根据消息类型处理
switch (msg.type) { switch (msg.type) {
case "connect": case "connect":
// 处理连接请求
handler.onConnect(ws, msg.connectionId); handler.onConnect(ws, msg.connectionId);
break; break;
case "disconnect": case "disconnect":
// 处理断开连接请求
handler.onDisconnect(ws, msg.connectionId); handler.onDisconnect(ws, msg.connectionId);
break; break;
case "offer": case "offer":
// 处理offer信令 if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onOffer(ws, msg.data); handler.onOffer(ws, msg.data);
break; break;
case "answer": case "answer":
// 处理answer信令 if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onAnswer(ws, msg.data); handler.onAnswer(ws, msg.data);
break; break;
case "candidate": case "candidate":
// 处理candidate信令 if (msg.participantId !== undefined) msg.data.participantId = msg.participantId;
handler.onCandidate(ws, msg.data); handler.onCandidate(ws, msg.data);
break; break;
case "ping": case "ping":
// 处理心跳请求回复pong
ws.send(JSON.stringify({ type: "pong" })); ws.send(JSON.stringify({ type: "pong" }));
break; break;
case "pong": case "pong":
// 处理心跳响应,更新最后活动时间
(ws as any).lastActivity = Date.now(); (ws as any).lastActivity = Date.now();
break; break;
case "broadcast": case "broadcast":
handler.onBroadcast(ws, msg.data); handler.onBroadcast(ws, msg.data);
break; break;
case 'call-request'://接受连接ConnectionId case 'call-request':
// 处理callConnectionId信令
handler.onCallConnectionId(ws, msg.data); handler.onCallConnectionId(ws, msg.data);
break; break;
case 'on-message'://接受连接ConnectionId case 'on-message':
// 处理chat-message信令 if (msg.from) msg.data.connectionId = msg.from;
handler.onMessage(ws, msg.data); handler.onMessage(ws, msg.data);
break; break;
default: default:
// 忽略未知消息类型
break; break;
} }
}; };