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"); cameraSelect.innerHTML = ""; micSelect.innerHTML = ""; for (const camera of cameras) { const option = document.createElement("option"); option.value = camera.deviceId; option.textContent = camera.label || `Camera ${cameraSelect.length + 1}`; 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}`; micSelect.appendChild(option); } 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 } } : true }; try { localStream = await navigator.mediaDevices.getUserMedia(constraints); } catch { 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"); if (localVideo) { localVideo.srcObject = stream; } const audioTracks = stream.getAudioTracks(); const videoTracks = stream.getVideoTracks(); 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 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); } 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"); 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"}`; }, ensureRemoteTile(username) { const container = document.getElementById("remoteMediaContainer"); 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.refreshDevicesAndPreview(); }); } if (micSelect) { micSelect.addEventListener("change", async () => { LogMessage("Microphone changed"); await this.refreshDevicesAndPreview(); }); } } }; window.Media = Media;