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