【m】测试,便于回退
This commit is contained in:
@@ -149,7 +149,15 @@
|
|||||||
<div class="flex-1 relative bg-black/40 overflow-hidden" id="videoArea">
|
<div class="flex-1 relative bg-black/40 overflow-hidden" id="videoArea">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
子区域: 远端视频 (Remote Video)
|
子区域: 多Participant视频网格(Host端显示)
|
||||||
|
动态生成,每个participant一个视频格子
|
||||||
|
-->
|
||||||
|
<div id="participantGrid" class="hidden absolute inset-0 grid gap-3 p-3 auto-rows-fr" style="grid-template-columns: 1fr;">
|
||||||
|
<!-- 动态生成的 participant 视频格子将插入这里 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
子区域: 远端视频 (Remote Video) - 单路(Participant端显示Host画面)
|
||||||
数据源: RemoteUser
|
数据源: RemoteUser
|
||||||
-->
|
-->
|
||||||
<div class="absolute inset-0 video-fade-in">
|
<div class="absolute inset-0 video-fade-in">
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class UIRenderer {
|
|||||||
// 头部和底部
|
// 头部和底部
|
||||||
header: document.querySelector('header'),
|
header: document.querySelector('header'),
|
||||||
footer: document.querySelector('footer'),
|
footer: document.querySelector('footer'),
|
||||||
|
// 多Participant视频网格
|
||||||
|
participantGrid: document.getElementById('participantGrid'),
|
||||||
// 头部内容
|
// 头部内容
|
||||||
headerTitle: document.getElementById('headerTitle'),
|
headerTitle: document.getElementById('headerTitle'),
|
||||||
callDuration: document.getElementById('callDuration'),
|
callDuration: document.getElementById('callDuration'),
|
||||||
@@ -144,10 +146,12 @@ class UIRenderer {
|
|||||||
// 本地流获取成功 - 更新本地视频显示
|
// 本地流获取成功 - 更新本地视频显示
|
||||||
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':
|
||||||
// 远程流获取成功 - 更新远程视频显示
|
// 远程流获取成功 - 更新远程视频显示
|
||||||
this.renderRemoteStream(state.remoteStream); // 渲染远程流
|
this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 渲染远程流
|
||||||
// 当获取到远程流时,隐藏连接中提示
|
// 当获取到远程流时,隐藏连接中提示
|
||||||
if (this.elements.connectingOverlay) {
|
if (this.elements.connectingOverlay) {
|
||||||
this.elements.connectingOverlay.classList.add('hidden');
|
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 = `<i class="fas fa-user text-indigo-400"></i><span>Participant ${connectionId.slice(-4)}</span>`;
|
||||||
|
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 = `<span class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></span><span>直播</span>`;
|
||||||
|
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) {
|
if (this.elements.remoteVideo && stream) {
|
||||||
console.log('Rendering remote stream:', stream);
|
console.log('Rendering remote stream:', stream);
|
||||||
|
|
||||||
@@ -892,6 +1016,20 @@ class UIRenderer {
|
|||||||
// 渲染通话结束
|
// 渲染通话结束
|
||||||
renderCallEnded() {
|
renderCallEnded() {
|
||||||
console.log('Call ended');
|
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';
|
window.location.href = './endcall/endcall.html';
|
||||||
}
|
}
|
||||||
@@ -899,14 +1037,55 @@ class UIRenderer {
|
|||||||
// 渲染participant离开(host端,房间仍然存在)
|
// 渲染participant离开(host端,房间仍然存在)
|
||||||
renderParticipantLeft(connectionId) {
|
renderParticipantLeft(connectionId) {
|
||||||
console.log(`Participant left: ${connectionId}, updating UI`);
|
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) {
|
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';
|
||||||
}
|
}
|
||||||
// 显示断开连接的遮罩层(如果存在)
|
|
||||||
if (this.elements.disconnectedOverlay) {
|
|
||||||
this.elements.disconnectedOverlay.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class CallStateManager {
|
|||||||
status: 'idle' // 初始状态为空闲
|
status: 'idle' // 初始状态为空闲
|
||||||
},
|
},
|
||||||
localStream: null, // MediaStream 对象
|
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.updateRemoteUserStatus('offline');
|
||||||
this.updateRemoteUserNetworkQuality('no_signal');
|
this.updateRemoteUserNetworkQuality('no_signal');
|
||||||
showNotification('对方已离开通话', 'warning');
|
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) {
|
if (this.state.remoteStream) {
|
||||||
this.state.remoteStream.getTracks().forEach(track => track.stop());
|
this.state.remoteStream.getTracks().forEach(track => track.stop());
|
||||||
this.state.remoteStream = null;
|
this.state.remoteStream = null;
|
||||||
}
|
}
|
||||||
this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: null });
|
|
||||||
// 通知UI更新
|
// 通知UI更新
|
||||||
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: connectionId });
|
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: connectionId });
|
||||||
};
|
};
|
||||||
@@ -394,25 +399,48 @@ this.renderstreaming.onNewPeer = (connectionId) => {
|
|||||||
this.renderstreaming.onTrackEvent = (data) => {
|
this.renderstreaming.onTrackEvent = (data) => {
|
||||||
const direction = data.transceiver.direction;
|
const direction = data.transceiver.direction;
|
||||||
if (direction == "sendrecv" || direction == "recvonly") {
|
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 => {
|
existingTracks.forEach(track => {
|
||||||
this.state.remoteStream.removeTrack(track);
|
targetStream.removeTrack(track);
|
||||||
console.log('Removed old track:', track.kind);
|
console.log('Removed old track:', track.kind);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加新的轨道
|
// 添加新的轨道
|
||||||
this.state.remoteStream.addTrack(data.track);
|
targetStream.addTrack(data.track);
|
||||||
console.log('Added new track:', data.track.kind);
|
console.log('Added new track:', data.track.kind, 'to stream:', trackConnectionId);
|
||||||
|
|
||||||
// 通知UI远程流已更新
|
// 通知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');
|
console.log('Notified UI about remote stream update');
|
||||||
// 只有当收到远程流时才更新远程用户状态为在线
|
// 只有当收到远程流时才更新远程用户状态为在线
|
||||||
if (this.state.session.remoteUser.status !== 'online') {
|
if (this.state.session.remoteUser.status !== 'online') {
|
||||||
|
|||||||
Reference in New Issue
Block a user