This commit is contained in:
2026-04-25 19:26:39 +08:00
parent bd3698d508
commit bcd55f9dac
3 changed files with 88 additions and 170 deletions

View File

@@ -180,35 +180,6 @@
</div> </div>
</div> </div>
<!-- 远端信息覆盖层 -->
<div class="absolute top-6 left-6 glass px-4 py-2 rounded-full flex items-center gap-3">
<div class="relative">
<!-- [DATA_FIELD: remoteUser.avatar] [TYPE: string] [URL] [REQUIRED] -->
<img id="remoteAvatar"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
class="w-8 h-8 rounded-full object-cover" data-field="remoteUser.avatar">
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] -->
<div id="remoteSpeakingIndicator"
class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900 hidden"
data-field="remoteUser.isSpeaking"></div>
</div>
<div>
<!-- [DATA_FIELD: remoteUser.name] [TYPE: string] -->
<div class="text-sm font-medium" data-field="remoteUser.name" id="remoteName">Sarah Chen</div>
<div class="text-xs text-gray-400 flex items-center gap-2">
<!-- [DATA_FIELD: remoteUser.status] [TYPE: string] [ENUM: online/offline/connecting] -->
<span id="remoteStatus" data-field="remoteUser.status">正在通话</span>
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true && remoteUser.mediaState.audio === true] -->
<div id="remoteAudioWave" class="audio-wave w-6 hidden"
data-field="remoteUser.audioActivity">
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
</div>
</div>
<!-- 网络状态提示 --> <!-- 网络状态提示 -->
<!-- [CONDITIONAL_RENDER: remoteUser.networkQuality !== 'excellent'] --> <!-- [CONDITIONAL_RENDER: remoteUser.networkQuality !== 'excellent'] -->
<div id="networkStatus" <div id="networkStatus"

View File

@@ -29,12 +29,6 @@ class UIRenderer {
// 远端视频 // 远端视频
remoteVideo: document.getElementById('remoteVideo'), remoteVideo: document.getElementById('remoteVideo'),
remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'), remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'),
remoteAvatar: document.getElementById('remoteAvatar'),
remoteName: document.getElementById('remoteName'),
remoteStatus: document.getElementById('remoteStatus'),
remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'),
remoteAudioWave: document.getElementById('remoteAudioWave'),
remoteInfoOverlay: document.querySelector('.absolute.top-6.left-6.glass'),
networkStatus: document.getElementById('networkStatus'), networkStatus: document.getElementById('networkStatus'),
networkStatusText: document.getElementById('networkStatusText'), networkStatusText: document.getElementById('networkStatusText'),
connectingOverlay: document.getElementById('connectingOverlay'), connectingOverlay: document.getElementById('connectingOverlay'),
@@ -243,43 +237,6 @@ class UIRenderer {
// 渲染远端视频 // 渲染远端视频
renderRemoteVideo(remoteUser) { renderRemoteVideo(remoteUser) {
// 找到远端信息覆盖层元素
const remoteInfoOverlay = this.elements.remoteName?.closest('.absolute.top-6.left-6.glass');
// 未连接时不显示红框部分
if (remoteUser.status === 'offline') {
// 隐藏整个远端信息覆盖层
if (remoteInfoOverlay) {
remoteInfoOverlay.classList.add('hidden');
}
} else {
// 显示远端信息覆盖层
if (remoteInfoOverlay) {
remoteInfoOverlay.classList.remove('hidden');
}
// 连接后更新头像和名称数据
if (this.elements.remoteName) {
this.elements.remoteName.textContent = remoteUser.name;
}
if (this.elements.remoteAvatar) {
this.elements.remoteAvatar.src = remoteUser.avatar;
this.elements.remoteAvatar.classList.remove('hidden');
}
if (this.elements.remoteStatus) {
// 显示与黄框中一致的状态
if (!remoteUser.mediaState.audio) {
this.elements.remoteStatus.textContent = '静音中';
} else if (!remoteUser.mediaState.video) {
this.elements.remoteStatus.textContent = '视频关闭';
} else {
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
}
}
}
// 同步更新侧边栏用户列表 // 同步更新侧边栏用户列表
this.renderUserList(this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants); this.renderUserList(this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants);
@@ -317,15 +274,6 @@ class UIRenderer {
} }
} }
// 渲染说话状态
if (this.elements.remoteSpeakingIndicator) {
toggleElement(this.elements.remoteSpeakingIndicator, remoteUser.mediaState.isSpeaking);
}
if (this.elements.remoteAudioWave) {
toggleElement(this.elements.remoteAudioWave, remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio);
}
// 渲染网络状态 // 渲染网络状态
this.renderNetworkStatus(remoteUser.networkQuality); this.renderNetworkStatus(remoteUser.networkQuality);
@@ -471,8 +419,15 @@ class UIRenderer {
// 视频元素显示participant的远端视频流 // 视频元素显示participant的远端视频流
const video = tile.querySelector('video'); const video = tile.querySelector('video');
if (video && stream) { if (video && stream) {
video.srcObject = stream; // 避免重复设置同一流对象(音频先到、视频后到时流对象相同)
console.log(`Set remote stream for participant tile ${connectionId}`); if (video.srcObject === stream) {
console.log(`Same stream for participant ${connectionId}, ensuring playback`);
video.play().catch(e => console.log('Auto-play prevented:', e.message));
} else {
video.srcObject = stream;
video.play().catch(e => console.log('Auto-play prevented:', e.message));
console.log(`Set remote stream for participant tile ${connectionId}`);
}
} }
// 隐藏单路远端视频和占位符 // 隐藏单路远端视频和占位符
@@ -546,91 +501,66 @@ class UIRenderer {
// 渲染Participant端的单一远端视频Host画面 // 渲染Participant端的单一远端视频Host画面
renderSingleRemoteStream(stream) { renderSingleRemoteStream(stream) {
if (this.elements.remoteVideo && stream) { if (!this.elements.remoteVideo || !stream) {
console.log('Rendering remote stream:', stream);
// 即使流对象相同,也要重新设置,确保视频元素能够识别轨道变化
this.elements.remoteVideo.srcObject = null;
// 延迟设置srcObject确保视频元素能够正确处理
setTimeout(() => {
this.elements.remoteVideo.srcObject = stream;
console.log('Remote stream reset successfully:', stream);
// 确保视频元素的属性正确设置
this.elements.remoteVideo.autoplay = true;
this.elements.remoteVideo.playsinline = true;
this.elements.remoteVideo.muted = false; // 不要静音远程视频,否则听不到对方的声音
// 关键设置:启用硬件加速和最佳质量渲染
this.elements.remoteVideo.style.transform = 'translateZ(0)'; // 启用硬件加速
this.elements.remoteVideo.style.imageRendering = 'pixelated'; // 保持像素清晰
this.elements.remoteVideo.style.objectFit = 'contain'; // 保持比例
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
// 获取视频轨道并处理分辨率
const videoTracks = stream.getVideoTracks();
console.log('Remote video tracks:', videoTracks);
// 检查是否有有效的视频轨道
const hasValidVideoTrack = videoTracks.length > 0 && videoTracks.some(track => {
// 检查轨道是否已停止或被禁用
return track.readyState === 'live';
});
console.log('Has valid video track:', hasValidVideoTrack);
if (hasValidVideoTrack) {
console.log('Found valid video tracks, updating resolution');
const activeVideoTrack = videoTracks.find(track => track.readyState === 'live');
if (activeVideoTrack) {
const resolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, resolution);
// 监听轨道变化,处理分辨率调整
activeVideoTrack.addEventListener('resize', () => {
const newResolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, newResolution);
});
}
// 隐藏连接中提示
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
// 隐藏占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
}
} else {
console.log('No valid video tracks in remote stream');
// 清空视频元素的源
this.elements.remoteVideo.srcObject = null;
// 显示占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
}
}
}, 50); // 增加延迟时间,确保视频元素有足够的时间处理
} else {
console.error('Either remoteVideo element or stream is missing'); console.error('Either remoteVideo element or stream is missing');
return;
}
// 清空视频元素的源 console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(t => `${t.kind}(${t.readyState})`));
if (this.elements.remoteVideo) {
this.elements.remoteVideo.srcObject = null;
}
// 显示占位背景 // 关键修复:避免 srcObject = null 的重置模式
// 如果 srcObject 已经是同一个 stream 对象,说明是同一流的轨道更新(如音频先到,视频后到)
// 浏览器会自动识别新添加的轨道,无需重置 srcObject
if (this.elements.remoteVideo.srcObject === stream) {
console.log('Same stream object, track added - ensuring playback');
this.elements.remoteVideo.play().catch(e => console.log('Auto-play prevented:', e.message));
return;
}
// 首次设置或流对象变化:直接设置 srcObject不使用 null 重置模式)
this.elements.remoteVideo.srcObject = stream;
this.elements.remoteVideo.autoplay = true;
this.elements.remoteVideo.playsinline = true;
this.elements.remoteVideo.muted = false;
// 确保视频开始播放
this.elements.remoteVideo.play().catch(e => {
console.log('Auto-play prevented, will retry on interaction:', e.message);
});
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
// 监听视频轨道变化
const videoTracks = stream.getVideoTracks();
const audioTracks = stream.getAudioTracks();
console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`);
if (videoTracks.length > 0) {
// 有视频轨道:隐藏占位符
if (this.elements.remoteVideoPlaceholder) { if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.remove('hidden'); this.elements.remoteVideoPlaceholder.classList.add('hidden');
} }
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
// 监听视频轨道分辨率变化
const activeVideoTrack = videoTracks.find(track => track.readyState === 'live');
if (activeVideoTrack) {
const resolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, resolution);
activeVideoTrack.addEventListener('resize', () => {
const newResolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, newResolution);
});
}
} else {
// 只有音频轨道(视频轨道尚未到达):不显示占位符,等待视频轨道到达
// 不设置 srcObject = null保持音频播放
console.log('Audio-only stream, waiting for video track...');
} }
} }

View File

@@ -480,13 +480,30 @@ class CallStateManager {
} }
// 通知UI远程流已更新 // 通知UI远程流已更新
this.notify({ // 关键优化:如果是音频轨道先到达且流中尚无视频轨道,
type: 'REMOTE_STREAM_OBTAINED', // 延迟通知UI等待视频轨道到达避免音频先触发的UI更新导致黑屏
stream: targetStream, const notifyStreamUpdate = () => {
connectionId: trackParticipantId, this.notify({
isHost: isHost type: 'REMOTE_STREAM_OBTAINED',
}); stream: targetStream,
console.log('Notified UI about remote stream update'); connectionId: trackParticipantId,
isHost: isHost
});
console.log('Notified UI about remote stream update');
};
if (data.track.kind === 'audio' && targetStream.getVideoTracks().length === 0) {
// 音频先到视频尚未到达延迟200ms通知给视频轨道到达的机会
console.log('Audio track arrived first, delaying stream notification for video track...');
setTimeout(() => {
const nowHasVideo = targetStream.getVideoTracks().length > 0;
console.log(`After delay, stream has video: ${nowHasVideo}`);
notifyStreamUpdate();
}, 200);
} else {
// 视频轨道到达,或音频视频同时存在:立即通知
notifyStreamUpdate();
}
// 只有当收到远程流时才更新远程用户状态为在线 // 只有当收到远程流时才更新远程用户状态为在线
if (this.state.session.remoteUser.status !== 'online') { if (this.state.session.remoteUser.status !== 'online') {
this.updateRemoteUserStatus('online'); this.updateRemoteUserStatus('online');