diff --git a/RelayClient/Resources/Raw/wwwroot/index.html b/RelayClient/Resources/Raw/wwwroot/index.html index b67a8ae..af1cf57 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.html +++ b/RelayClient/Resources/Raw/wwwroot/index.html @@ -16,7 +16,7 @@
- +
diff --git a/RelayClient/Resources/Raw/wwwroot/index.js b/RelayClient/Resources/Raw/wwwroot/index.js index 81f13d9..ee060e7 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.js +++ b/RelayClient/Resources/Raw/wwwroot/index.js @@ -96,19 +96,25 @@ async function ensurePeerConnection() { LogMessage("ICE gathering state: " + peerConnection.iceGatheringState); }; } -async function ensureLocalMedia() { - if (localStream) return; - +async function ensureLocalMedia(forceReload = false) { const localMediaStatus = document.getElementById("localMediaStatus"); const localVideoStatus = document.getElementById("localVideoStatus"); + const localVideo = document.getElementById("localVideo"); const cameraSelect = document.getElementById("cameraSelect"); const micSelect = document.getElementById("micSelect"); + if (localStream && !forceReload) { + return; + } + + if (localStream) { + localStream.getTracks().forEach(track => track.stop()); + localStream = null; + } + let selectedCameraId = cameraSelect ? cameraSelect.value : ""; let selectedMicId = micSelect ? micSelect.value : ""; - let mediaError = null; - const videoConstraint = selectedCameraId ? { deviceId: { exact: selectedCameraId } } : false; @@ -125,7 +131,6 @@ async function ensureLocalMedia() { LogMessage("Local media initialized"); } catch (err) { - mediaError = err; LogMessage("selected media failed: " + err); try { @@ -137,28 +142,69 @@ async function ensureLocalMedia() { LogMessage("Local media initialized with audio only fallback"); } catch (audioErr) { LogMessage("audio-only failed: " + audioErr); + if (localMediaStatus) localMediaStatus.textContent = "Local media failed"; if (localVideoStatus) localVideoStatus.textContent = "Local video: unavailable"; - throw mediaError; + if (localVideo) localVideo.srcObject = null; + + throw audioErr; } } - const localVideo = document.getElementById("localVideo"); + const hasVideo = localStream.getVideoTracks().length > 0; + const hasAudio = localStream.getAudioTracks().length > 0; - if (localStream.getVideoTracks().length > 0) { - localVideo.srcObject = localStream; - if (localVideoStatus) localVideoStatus.textContent = "Local video: active"; - if (localMediaStatus) localMediaStatus.textContent = "Local media: audio + video"; - } else { - localVideo.srcObject = null; - if (localVideoStatus) localVideoStatus.textContent = "Local video: unavailable"; - if (localMediaStatus) localMediaStatus.textContent = "Local media: audio only"; - LogMessage("No camera available, continuing without video"); + localVideo.srcObject = hasVideo ? localStream : null; + + if (localVideoStatus) { + localVideoStatus.textContent = hasVideo + ? "Local video: active" + : "Local video: unavailable"; } - for (const track of localStream.getTracks()) { - peerConnection.addTrack(track, localStream); - LogMessage(`Added local track: ${track.kind}`); + if (localMediaStatus) { + localMediaStatus.textContent = `Local media: audio=${hasAudio} video=${hasVideo}`; + } + + if (!hasVideo) { + LogMessage("No camera available, continuing without video"); + } +} + +async function applyLocalStreamToPeerConnection() { + if (!peerConnection || !localStream) return; + + const senders = peerConnection.getSenders(); + + const audioTrack = localStream.getAudioTracks()[0] || null; + const videoTrack = localStream.getVideoTracks()[0] || null; + + const audioSender = senders.find(s => s.track && s.track.kind === "audio"); + const videoSender = senders.find(s => s.track && s.track.kind === "video"); + + if (audioSender) { + await audioSender.replaceTrack(audioTrack); + LogMessage("Replaced audio track on peer connection"); + } else if (audioTrack) { + peerConnection.addTrack(audioTrack, localStream); + LogMessage("Added audio track to peer connection"); + } + + if (videoSender) { + await videoSender.replaceTrack(videoTrack); + LogMessage("Replaced video track on peer connection"); + } else if (videoTrack) { + peerConnection.addTrack(videoTrack, localStream); + LogMessage("Added video track to peer connection"); + } +} + +async function refreshDevicesAndPreview() { + await loadDevices(); + await ensureLocalMedia(true); + + if (peerConnection) { + await applyLocalStreamToPeerConnection(); } } @@ -251,6 +297,7 @@ async function channelCallJoin(activeCall) // LogMessage("Active call: " + activeCall); await ensurePeerConnection2(); await ensureLocalMedia(); + await applyLocalStreamToPeerConnection(); if (activeCall) { @@ -432,6 +479,27 @@ async function loadDevices() { } } +function wireDeviceSelectors() { + const cameraSelect = document.getElementById("cameraSelect"); + const micSelect = document.getElementById("micSelect"); + + if (cameraSelect) { + cameraSelect.onchange = async () => { + LogMessage("Camera changed"); + await ensureLocalMedia(true); + await applyLocalStreamToPeerConnection(); + }; + } + + if (micSelect) { + micSelect.onchange = async () => { + LogMessage("Microphone changed"); + await ensureLocalMedia(true); + await applyLocalStreamToPeerConnection(); + }; + } +} + async function waitForIceGatheringComplete(pc) { if (pc.iceGatheringState === "complete") return; @@ -457,4 +525,6 @@ window.addEventListener("load", async () => { LogMessage("RTC page loaded"); window.HybridWebView.SendRawMessage("rtc_page_ready"); await loadDevices(); + wireDeviceSelectors(); + await ensureLocalMedia(true); }); \ No newline at end of file