完成
This commit is contained in:
@@ -180,35 +180,6 @@
|
||||
</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'] -->
|
||||
<div id="networkStatus"
|
||||
|
||||
@@ -29,12 +29,6 @@ class UIRenderer {
|
||||
// 远端视频
|
||||
remoteVideo: document.getElementById('remoteVideo'),
|
||||
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'),
|
||||
networkStatusText: document.getElementById('networkStatusText'),
|
||||
connectingOverlay: document.getElementById('connectingOverlay'),
|
||||
@@ -243,43 +237,6 @@ class UIRenderer {
|
||||
|
||||
// 渲染远端视频
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -471,8 +419,15 @@ class UIRenderer {
|
||||
// 视频元素显示participant的远端视频流
|
||||
const video = tile.querySelector('video');
|
||||
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画面)
|
||||
renderSingleRemoteStream(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 {
|
||||
if (!this.elements.remoteVideo || !stream) {
|
||||
console.error('Either remoteVideo element or stream is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空视频元素的源
|
||||
if (this.elements.remoteVideo) {
|
||||
this.elements.remoteVideo.srcObject = null;
|
||||
}
|
||||
console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(t => `${t.kind}(${t.readyState})`));
|
||||
|
||||
// 显示占位背景
|
||||
// 关键修复:避免 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) {
|
||||
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...');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -480,13 +480,30 @@ class CallStateManager {
|
||||
}
|
||||
|
||||
// 通知UI远程流已更新
|
||||
this.notify({
|
||||
type: 'REMOTE_STREAM_OBTAINED',
|
||||
stream: targetStream,
|
||||
connectionId: trackParticipantId,
|
||||
isHost: isHost
|
||||
});
|
||||
console.log('Notified UI about remote stream update');
|
||||
// 关键优化:如果是音频轨道先到达且流中尚无视频轨道,
|
||||
// 延迟通知UI等待视频轨道到达,避免音频先触发的UI更新导致黑屏
|
||||
const notifyStreamUpdate = () => {
|
||||
this.notify({
|
||||
type: 'REMOTE_STREAM_OBTAINED',
|
||||
stream: targetStream,
|
||||
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') {
|
||||
this.updateRemoteUserStatus('online');
|
||||
|
||||
Reference in New Issue
Block a user