diff --git a/RelayClient/Resources/Raw/wwwroot/media.js b/RelayClient/Resources/Raw/wwwroot/media.js index c7f9719..2629671 100644 --- a/RelayClient/Resources/Raw/wwwroot/media.js +++ b/RelayClient/Resources/Raw/wwwroot/media.js @@ -11,23 +11,46 @@ const Media = { const cameraSelect = document.getElementById("cameraSelect"); const micSelect = document.getElementById("micSelect"); + if (!cameraSelect || !micSelect) return; + + const selectedCamera = cameraSelect.value; + const selectedMic = micSelect.value; + cameraSelect.innerHTML = ""; micSelect.innerHTML = ""; + const noCamera = document.createElement("option"); + noCamera.value = ""; + noCamera.textContent = "No camera / audio only"; + cameraSelect.appendChild(noCamera); + + const defaultMic = document.createElement("option"); + defaultMic.value = ""; + defaultMic.textContent = "Default microphone"; + micSelect.appendChild(defaultMic); + for (const camera of cameras) { const option = document.createElement("option"); option.value = camera.deviceId; - option.textContent = camera.label || `Camera ${cameraSelect.length + 1}`; + option.textContent = camera.label || `Camera ${cameraSelect.length}`; cameraSelect.appendChild(option); } for (const mic of mics) { const option = document.createElement("option"); option.value = mic.deviceId; - option.textContent = mic.label || `Microphone ${micSelect.length + 1}`; + option.textContent = mic.label || `Microphone ${micSelect.length}`; micSelect.appendChild(option); } + cameraSelect.value = [...cameraSelect.options].some(o => o.value === selectedCamera) + ? selectedCamera + : ""; + + micSelect.value = [...micSelect.options].some(o => o.value === selectedMic) + ? selectedMic + : ""; + LogMessage(`Loaded devices: ${cameras.length} cameras, ${mics.length} mics`); }, @@ -39,8 +62,8 @@ const Media = { return localStream; } - const audioDeviceId = micSelect?.value; - const videoDeviceId = cameraSelect?.value; + const audioDeviceId = micSelect?.value || ""; + const videoDeviceId = cameraSelect?.value || ""; const constraints = { audio: audioDeviceId @@ -48,12 +71,14 @@ const Media = { : true, video: videoDeviceId ? { deviceId: { exact: videoDeviceId } } - : true + : false }; try { localStream = await navigator.mediaDevices.getUserMedia(constraints); - } catch { + } catch (err) { + LogMessage("Selected media failed: " + err); + localStream = await navigator.mediaDevices.getUserMedia({ audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } @@ -75,13 +100,13 @@ const Media = { const localMediaStatus = document.getElementById("localMediaStatus"); const localVideoStatus = document.getElementById("localVideoStatus"); - if (localVideo) { - localVideo.srcObject = stream; - } - const audioTracks = stream.getAudioTracks(); const videoTracks = stream.getVideoTracks(); + if (localVideo) { + localVideo.srcObject = videoTracks.length > 0 ? stream : null; + } + if (localMediaStatus) { localMediaStatus.textContent = audioTracks.length > 0 @@ -97,6 +122,19 @@ const Media = { } }, + async restartLocalMedia() { + if (localStream) { + localStream.getTracks().forEach(track => track.stop()); + localStream = null; + } + + await this.ensureLocalMedia(); + + if (window.RelayRtc?.applyLocalStreamToAllPeerConnections) { + await window.RelayRtc.applyLocalStreamToAllPeerConnections(); + } + }, + async refreshDevicesAndPreview() { if (localStream) { localStream.getTracks().forEach(track => track.stop()); @@ -113,7 +151,6 @@ const Media = { async applyLocalStreamToPeerConnection(pc, username) { const stream = await this.ensureLocalMedia(); - const existingSenders = pc.getSenders(); for (const track of stream.getTracks()) { @@ -123,11 +160,11 @@ const Media = { if (existingSender) { await existingSender.replaceTrack(track); + LogMessage(`Replaced local ${track.kind} track for ${username}`); } else { pc.addTrack(track, stream); + LogMessage(`Added local ${track.kind} track for ${username}`); } - - LogMessage(`Added local ${track.kind} track for ${username}`); } }, @@ -146,21 +183,25 @@ const Media = { const video = tile.querySelector("video"); const status = tile.querySelector(".remote-media-status"); - video.srcObject = stream; + if (video) { + video.srcObject = stream; + } const audioTracks = stream.getAudioTracks(); const videoTracks = stream.getVideoTracks(); - status.textContent = - `${audioTracks.length > 0 ? "Audio" : "No audio"} / ` + - `${videoTracks.length > 0 ? "Video" : "No video"}`; + if (status) { + status.textContent = + `${audioTracks.length > 0 ? "Audio" : "No audio"} / ` + + `${videoTracks.length > 0 ? "Video" : "No video"}`; + } }, ensureRemoteTile(username) { const container = document.getElementById("remoteMediaContainer"); + if (!container) return null; let tile = document.getElementById(`remote-tile-${username}`); - if (tile) return tile; tile = document.createElement("div"); @@ -204,14 +245,14 @@ const Media = { if (cameraSelect) { cameraSelect.addEventListener("change", async () => { LogMessage("Camera changed"); - await this.refreshDevicesAndPreview(); + await this.restartLocalMedia(); }); } if (micSelect) { micSelect.addEventListener("change", async () => { LogMessage("Microphone changed"); - await this.refreshDevicesAndPreview(); + await this.restartLocalMedia(); }); } }