let localStream = null; const remoteStreams = {}; const Media = { async loadDevices() { const devices = await navigator.mediaDevices.enumerateDevices(); const cameras = devices.filter(d => d.kind === "videoinput"); const mics = devices.filter(d => d.kind === "audioinput"); 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}`; cameraSelect.appendChild(option); } for (const mic of mics) { const option = document.createElement("option"); option.value = mic.deviceId; 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`); }, async ensureLocalMedia() { const cameraSelect = document.getElementById("cameraSelect"); const micSelect = document.getElementById("micSelect"); if (localStream) { return localStream; } const audioDeviceId = micSelect?.value || ""; const videoDeviceId = cameraSelect?.value || ""; const constraints = { audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true, video: videoDeviceId ? { deviceId: { exact: videoDeviceId } } : false }; try { localStream = await navigator.mediaDevices.getUserMedia(constraints); } catch (err) { LogMessage("Selected media failed: " + err); localStream = await navigator.mediaDevices.getUserMedia({ audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true, video: false }); LogMessage("No camera available, continuing without video"); } this.attachLocalStream(localStream); LogMessage("Local media initialized"); return localStream; }, attachLocalStream(stream) { const localVideo = document.getElementById("localVideo"); const localMediaStatus = document.getElementById("localMediaStatus"); const localVideoStatus = document.getElementById("localVideoStatus"); const audioTracks = stream.getAudioTracks(); const videoTracks = stream.getVideoTracks(); if (localVideo) { localVideo.srcObject = videoTracks.length > 0 ? stream : null; } if (localMediaStatus) { localMediaStatus.textContent = audioTracks.length > 0 ? "Microphone active" : "No microphone"; } if (localVideoStatus) { localVideoStatus.textContent = videoTracks.length > 0 ? "Local video active" : "Local video unavailable"; } }, 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()); localStream = null; } await this.loadDevices(); await this.ensureLocalMedia(); if (window.RelayRtc?.applyLocalStreamToAllPeerConnections) { await window.RelayRtc.applyLocalStreamToAllPeerConnections(); } }, async applyLocalStreamToPeerConnection(pc, username) { const stream = await this.ensureLocalMedia(); const existingSenders = pc.getSenders(); for (const track of stream.getTracks()) { const existingSender = existingSenders.find(sender => sender.track && sender.track.kind === track.kind ); 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}`); } } }, async applyLocalStreamToAllPeerConnections() { if (!window.RelayRtc?.peerConnections) return; for (const [username, pc] of Object.entries(window.RelayRtc.peerConnections)) { await this.applyLocalStreamToPeerConnection(pc, username); } }, attachRemoteStream(username, stream) { remoteStreams[username] = stream; const tile = this.ensureRemoteTile(username); const video = tile.querySelector("video"); const status = tile.querySelector(".remote-media-status"); if (video) { video.srcObject = stream; } const audioTracks = stream.getAudioTracks(); const videoTracks = stream.getVideoTracks(); 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"); tile.id = `remote-tile-${username}`; tile.className = "remote-media-tile"; const title = document.createElement("div"); title.className = "remote-media-title"; title.textContent = username; const video = document.createElement("video"); video.autoplay = true; video.playsInline = true; const status = document.createElement("div"); status.className = "remote-media-status"; status.textContent = "Remote media: waiting..."; tile.appendChild(title); tile.appendChild(video); tile.appendChild(status); container.appendChild(tile); return tile; }, removeRemoteStream(username) { delete remoteStreams[username]; const tile = document.getElementById(`remote-tile-${username}`); if (tile) { tile.remove(); } }, wireDeviceSelectors() { const cameraSelect = document.getElementById("cameraSelect"); const micSelect = document.getElementById("micSelect"); if (cameraSelect) { cameraSelect.addEventListener("change", async () => { LogMessage("Camera changed"); await this.restartLocalMedia(); }); } if (micSelect) { micSelect.addEventListener("change", async () => { LogMessage("Microphone changed"); await this.restartLocalMedia(); }); } } }; window.Media = Media;