diff --git a/WebApp/src/lib/app/controller.js b/WebApp/src/lib/app/controller.js index b6a65b2..fddc748 100644 --- a/WebApp/src/lib/app/controller.js +++ b/WebApp/src/lib/app/controller.js @@ -98,6 +98,7 @@ let clientVideoElement = null; const peerConnections = new Map(); const remoteStreams = new Map(); +const hiddenClientStreamElements = new Map(); const pendingCandidatesMap = new Map(); const streamTimers = new Map(); const connectedPeers = new Set(); @@ -536,6 +537,33 @@ const attachClientStreamToElement = () => { }); }; +const primeClientStreamPlayback = (streamSessionId, stream) => { + if (!streamSessionId || !stream) return; + + let hiddenVideoElement = hiddenClientStreamElements.get(streamSessionId); + if (!hiddenVideoElement && typeof document !== 'undefined') { + hiddenVideoElement = document.createElement('video'); + hiddenVideoElement.muted = true; + hiddenVideoElement.autoplay = true; + hiddenVideoElement.playsInline = true; + hiddenVideoElement.style.position = 'fixed'; + hiddenVideoElement.style.width = '1px'; + hiddenVideoElement.style.height = '1px'; + hiddenVideoElement.style.opacity = '0'; + hiddenVideoElement.style.pointerEvents = 'none'; + hiddenVideoElement.style.left = '-9999px'; + hiddenVideoElement.style.top = '-9999px'; + document.body.appendChild(hiddenVideoElement); + hiddenClientStreamElements.set(streamSessionId, hiddenVideoElement); + } + + if (!hiddenVideoElement) return; + if (hiddenVideoElement.srcObject !== stream) { + hiddenVideoElement.srcObject = stream; + } + void hiddenVideoElement.play().catch(() => {}); +}; + const startCameraPreview = async (cameraInputId = getAppState().selectedCameraInputId) => { if (!navigator.mediaDevices?.getUserMedia) { pushToast('Camera API is not available in this browser', 'error'); @@ -1121,6 +1149,12 @@ const teardownPeerConnection = (streamSessionId) => { } peerConnections.clear(); remoteStreams.clear(); + for (const hiddenVideoElement of hiddenClientStreamElements.values()) { + hiddenVideoElement.pause(); + hiddenVideoElement.srcObject = null; + hiddenVideoElement.remove(); + } + hiddenClientStreamElements.clear(); pendingCandidatesMap.clear(); connectedPeers.clear(); setConnectedStreamSessionIds(); @@ -1134,6 +1168,15 @@ const teardownPeerConnection = (streamSessionId) => { peerConnections.delete(streamSessionId); } remoteStreams.delete(streamSessionId); + if (hiddenClientStreamElements.has(streamSessionId)) { + const hiddenVideoElement = hiddenClientStreamElements.get(streamSessionId); + hiddenVideoElement?.pause(); + if (hiddenVideoElement) { + hiddenVideoElement.srcObject = null; + hiddenVideoElement.remove(); + } + hiddenClientStreamElements.delete(streamSessionId); + } pendingCandidatesMap.delete(streamSessionId); connectedPeers.delete(streamSessionId); setConnectedStreamSessionIds(); @@ -1236,7 +1279,10 @@ const ensurePeerConnection = async ({ streamSessionId, targetDeviceId, asCamera if (getAppState().activeStreamSessionId === streamSessionId) { attachClientStreamToElement(); setClientStreamMode('video'); + return; } + + primeClientStreamPlayback(streamSessionId, stream); }; if (asCamera) {