Compare commits

...

2 Commits

Author SHA1 Message Date
d7264b9102 视频没有占位符修复 2026-05-16 20:11:36 +08:00
53166b648f 【m】解决远端黑屏问题 2026-05-16 18:16:57 +08:00
5 changed files with 30 additions and 19 deletions

View File

@@ -169,7 +169,7 @@
<!-- 远端未连接时的占位背景 --> <!-- 远端未连接时的占位背景 -->
<div id="remoteVideoPlaceholder" <div id="remoteVideoPlaceholder"
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80"> class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-900 z-10">
<div class="text-center"> <div class="text-center">
<div <div
class="w-32 h-32 rounded-full bg-indigo-700/50 flex items-center justify-center mx-auto mb-4"> class="w-32 h-32 rounded-full bg-indigo-700/50 flex items-center justify-center mx-auto mb-4">

View File

@@ -290,6 +290,15 @@ class UIRenderer {
const shouldShowPlaceholder = !remoteUser.mediaState.video; const shouldShowPlaceholder = !remoteUser.mediaState.video;
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder); toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
// 当远程视频关闭时,隐藏视频元素本身,避免冻结画面透过占位符
if (this.elements.remoteVideo) {
if (shouldShowPlaceholder) {
this.elements.remoteVideo.style.opacity = '0';
} else {
this.elements.remoteVideo.style.opacity = '1';
}
}
// 更新占位符文本内容 // 更新占位符文本内容
if (shouldShowPlaceholder) { if (shouldShowPlaceholder) {
const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center'); const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');

View File

@@ -653,7 +653,11 @@ class CallStateManager {
this.broadcastParticipantsList(); this.broadcastParticipantsList();
} else { } else {
// Participant端根据消息来源更新对应条目 // Participant端根据消息来源更新对应条目
if (data.participantId && this.state.participants[data.participantId]) { // Host的participantId在participants-sync中也会同步所以不能仅靠participants中有无该key判断
// 自身发出的消息回声participantId === selfParticipantId可以忽略
// 来自其他ParticipantparticipantId存在且在participants中且不是自身
// 来自HostparticipantId存在但不是自身Host不在selfParticipantId中
if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) {
// 来自其他Participant的媒体状态变化仅更新participants中对应条目 // 来自其他Participant的媒体状态变化仅更新participants中对应条目
// 不调用updateRemoteMedia因为Participant端没有其他Participant的视频流 // 不调用updateRemoteMedia因为Participant端没有其他Participant的视频流
this.state.participants[data.participantId].mediaState = { this.state.participants[data.participantId].mediaState = {
@@ -661,15 +665,12 @@ class CallStateManager {
...data.data ...data.data
}; };
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
} else if (!data.participantId) { } else if (data.participantId === this.selfParticipantId) {
// 来自Host的媒体状态变化无participantId // 自身消息回声,忽略
// 更新participants中Host条目 + 更新remoteUserHost的视频流是本端远端画面 } else {
if (this.state.participants['host']) { // 来自Host的媒体状态变化Host的participantId不匹配participants中任何条目或无participantId
this.state.participants['host'].mediaState = { // 更新remoteUserHost的视频流是本端远端画面
...this.state.participants['host'].mediaState, console.log('Received media-state-changed from Host, updating remoteUser:', data.data);
...data.data
};
}
this.updateRemoteMedia(data.data, data.participantId); this.updateRemoteMedia(data.data, data.participantId);
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
} }

View File

@@ -75,10 +75,11 @@ export class RenderStreaming {
if (this._isHost) { if (this._isHost) {
// host端为该participant创建或复用peer // host端为该participant创建或复用peer
// host端始终使用polite=falseimpolite确保perfect negotiation中host的offer优先
let peer = this._peers.get(participantId); let peer = this._peers.get(participantId);
if (!peer || (peer.pc && peer.pc.iceConnectionState === 'disconnected')) { if (!peer || (peer.pc && peer.pc.iceConnectionState === 'disconnected')) {
if (peer) peer.close(); if (peer) peer.close();
peer = this._preparePeerConnection(this._connectionId, offer.polite, participantId); peer = this._preparePeerConnection(this._connectionId, false, participantId);
} }
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" }); const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
try { try {
@@ -87,13 +88,13 @@ export class RenderStreaming {
Logger.warn(`Error on GotDescription for participant ${participantId}: ${error}`); Logger.warn(`Error on GotDescription for participant ${participantId}: ${error}`);
} }
} else { } else {
// participant端使用单一peer // participant端使用单一peer始终使用polite=true
if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') { if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') {
this._peer.close(); this._peer.close();
this._peer = null; this._peer = null;
} }
if (!this._peer) { if (!this._peer) {
this._preparePeerConnection(offer.connectionId, offer.polite, null); this._preparePeerConnection(offer.connectionId, true, null);
} }
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" }); const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
try { try {
@@ -171,10 +172,9 @@ export class RenderStreaming {
const participantId = data.participantId; const participantId = data.participantId;
Logger.log(`Participant joined: ${participantId}`); Logger.log(`Participant joined: ${participantId}`);
// host端为新participant创建peer // host端不在此处创建peer等待participant的offer到达后在_onOffer中创建
if (this._isHost && !this._peers.has(participantId)) { // 这样避免host和participant同时发offer导致的glare冲突
this._preparePeerConnection(this._connectionId, false, participantId); // _onOffer会在收到participant的offer时自动创建peer如果不存在
}
this.onParticipantJoined(participantId); this.onParticipantJoined(participantId);
} }

View File

@@ -239,7 +239,8 @@ function onOffer(ws: WebSocket, message: any): void {
} }
} else { } else {
// participant发送offer给host携带该participant的participantId // participant发送offer给host携带该participant的participantId
newOffer.polite = true; // host端应为impolitepolite=false确保perfect negotiation中host优先
newOffer.polite = false;
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer, participantId: senderParticipantId })); group.host.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer, participantId: senderParticipantId }));
} }
} }