diff --git a/client/public/version.js b/client/public/version.js index e338ace..dc268b3 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.25 R265"; +window.CHGRID_WEB_VERSION = "2026.02.25 R266"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/webrtc/peerManager.ts b/client/src/webrtc/peerManager.ts index 1512c50..753bb8b 100644 --- a/client/src/webrtc/peerManager.ts +++ b/client/src/webrtc/peerManager.ts @@ -4,6 +4,7 @@ import type { RemoteUser } from '../network/protocol'; export type PeerRuntime = SpatialPeerRuntime & { id: string; pc: RTCPeerConnection; + initiator: boolean; remoteStream?: MediaStream; }; @@ -38,6 +39,7 @@ export class PeerManager { const peer: PeerRuntime = { id: targetId, + initiator: isInitiator, nickname: userData.nickname ?? 'user...', x: userData.x ?? 20, y: userData.y ?? 20, @@ -117,15 +119,46 @@ export class PeerManager { if (!newTrack) { return; } + const peersToRenegotiate: PeerRuntime[] = []; for (const peer of this.peers.values()) { + let shouldRenegotiate = false; const sender = peer.pc.getSenders().find((candidate) => candidate.track?.kind === 'audio') ?? peer.pc .getTransceivers() .find((transceiver) => transceiver.receiver.track?.kind === 'audio' || transceiver.sender.track?.kind === 'audio') ?.sender; - if (!sender) continue; - await sender.replaceTrack(newTrack); + if (!sender) { + peer.pc.addTrack(newTrack, stream); + shouldRenegotiate = true; + } else { + if (!sender.track) { + shouldRenegotiate = true; + } + await sender.replaceTrack(newTrack); + } + const audioTransceiver = peer.pc + .getTransceivers() + .find((transceiver) => transceiver.receiver.track?.kind === 'audio' || transceiver.sender.track?.kind === 'audio'); + if (audioTransceiver && (audioTransceiver.direction === 'recvonly' || audioTransceiver.direction === 'inactive')) { + audioTransceiver.direction = 'sendrecv'; + shouldRenegotiate = true; + } + if (shouldRenegotiate && peer.initiator) { + peersToRenegotiate.push(peer); + } + } + for (const peer of peersToRenegotiate) { + if (peer.pc.connectionState === 'closed') continue; + if (peer.pc.signalingState !== 'stable') continue; + try { + let offer = await peer.pc.createOffer(); + offer = this.tuneOpus(offer); + await peer.pc.setLocalDescription(offer); + this.sendSignal(peer.id, { sdp: peer.pc.localDescription ?? undefined }); + } catch { + // Best-effort renegotiation; transport-level failures recover on subsequent signaling. + } } }