diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index 3ea2921..e4a9544 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -40,12 +40,8 @@ public partial class MainPage : ContentPage _wsc.Send($"REGISTER_KEY|{_username}|{publicKey}"); _wsc.Send("GET_SERVER_KEY"); _wsc.Send("GET_CHANNELS"); - hybridWebView.SetInvokeJavaScriptTarget(this); - Loaded += async (_, _) => - { - await InitializeRtcPageAsync(); - }; + hybridWebView.SetInvokeJavaScriptTarget(this); } @@ -355,6 +351,12 @@ public partial class MainPage : ContentPage private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e) { + if (e.Message == "rtc_page_ready") + { + await InitializeRtcPageAsync(); + return; + } + await DisplayAlertAsync("Raw Message Received", e.Message, "OK"); } @@ -405,8 +407,8 @@ public partial class MainPage : ContentPage private async Task InitializeRtcPageAsync() { - var jsArg = JsonSerializer.Serialize(_username); - await hybridWebView.EvaluateJavaScriptAsync($"window.currentUsername = {jsArg};"); - Console.WriteLine($"[{_username}] RTC page initialized."); + var usernameJson = JsonSerializer.Serialize(_username); + await hybridWebView.EvaluateJavaScriptAsync($"window.setUsername({usernameJson})"); + Console.WriteLine($"[{_username}] pushed username into HybridWebView."); } } \ No newline at end of file diff --git a/RelayClient/RelayClient.csproj b/RelayClient/RelayClient.csproj index 9fbdf2b..a78e60f 100644 --- a/RelayClient/RelayClient.csproj +++ b/RelayClient/RelayClient.csproj @@ -50,10 +50,4 @@ - - - PreserveNewest - - - diff --git a/RelayClient/Resources/Raw/wwwroot/index.html b/RelayClient/Resources/Raw/wwwroot/index.html index 2e562c6..624413a 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.html +++ b/RelayClient/Resources/Raw/wwwroot/index.html @@ -13,10 +13,26 @@ let localStream = null; let currentTarget = null; let currentUsername = null; + let availableCameras = []; + let availableMics = []; + + window.setUsername = function(name) { + currentUsername = name; + LogMessage("Username set to: " + currentUsername); + }; function LogMessage(msg) { const messageLog = document.getElementById("messageLog"); messageLog.value += '\r\n' + msg; + messageLog.scrollTop = messageLog.scrollHeight; + } + + function hasVideoTrack() { + return !!localStream && localStream.getVideoTracks().length > 0; + } + + function hasAudioTrack() { + return !!localStream && localStream.getAudioTracks().length > 0; } async function ensurePeerConnection() { @@ -44,12 +60,38 @@ peerConnection.ontrack = (event) => { LogMessage("Remote track received"); + const remoteVideo = document.getElementById("remoteVideo"); - remoteVideo.srcObject = event.streams[0]; + const remoteVideoStatus = document.getElementById("remoteVideoStatus"); + const remoteMediaStatus = document.getElementById("remoteMediaStatus"); + + const stream = event.streams[0]; + const hasVideo = stream.getVideoTracks().length > 0; + const hasAudio = stream.getAudioTracks().length > 0; + + if (hasVideo) { + remoteVideo.srcObject = stream; + } else { + remoteVideo.srcObject = null; + } + + if (remoteVideoStatus) { + remoteVideoStatus.textContent = hasVideo + ? "Remote video: active" + : "Remote video: unavailable"; + } + + if (remoteMediaStatus) { + remoteMediaStatus.textContent = `Remote media: audio=${hasAudio} video=${hasVideo}`; + } }; peerConnection.onconnectionstatechange = () => { LogMessage("Connection state: " + peerConnection.connectionState); + const remoteMediaStatus = document.getElementById("remoteMediaStatus"); + if (remoteMediaStatus && peerConnection.connectionState === "connected") { + remoteMediaStatus.textContent += " | connected"; + } }; peerConnection.oniceconnectionstatechange = () => { @@ -64,27 +106,71 @@ async function ensureLocalMedia() { if (localStream) return; + const localMediaStatus = document.getElementById("localMediaStatus"); + const localVideoStatus = document.getElementById("localVideoStatus"); + const cameraSelect = document.getElementById("cameraSelect"); + const micSelect = document.getElementById("micSelect"); + + let selectedCameraId = cameraSelect ? cameraSelect.value : ""; + let selectedMicId = micSelect ? micSelect.value : ""; + + let mediaError = null; + + const videoConstraint = selectedCameraId + ? { deviceId: { exact: selectedCameraId } } + : false; + + const audioConstraint = selectedMicId + ? { deviceId: { exact: selectedMicId } } + : true; + try { localStream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: true + video: videoConstraint, + audio: audioConstraint }); - const localVideo = document.getElementById("localVideo"); - localVideo.srcObject = localStream; - - for (const track of localStream.getTracks()) { - peerConnection.addTrack(track, localStream); - } - LogMessage("Local media initialized"); } catch (err) { - LogMessage("getUserMedia failed: " + err); - throw err; + mediaError = err; + LogMessage("selected media failed: " + err); + + try { + localStream = await navigator.mediaDevices.getUserMedia({ + video: false, + audio: audioConstraint + }); + + 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; + } + } + + const localVideo = document.getElementById("localVideo"); + + 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"); + } + + for (const track of localStream.getTracks()) { + peerConnection.addTrack(track, localStream); + LogMessage(`Added local track: ${track.kind}`); } } async function startCall() { + LogMessage("Current username: " + currentUsername); try { currentTarget = document.getElementById("targetUser").value; @@ -95,6 +181,8 @@ await ensurePeerConnection(); await ensureLocalMedia(); + + LogMessage(`Starting call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); @@ -126,6 +214,9 @@ LogMessage("Incoming call from " + msg.from); await ensureLocalMedia(); + LogMessage(`Answering call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); + LogMessage("Applying remote offer"); + await peerConnection.setRemoteDescription({ type: "offer", sdp: msg.sdp @@ -147,48 +238,129 @@ } if (msg.type === "rtc_answer") { + LogMessage("Applying remote answer"); + await peerConnection.setRemoteDescription({ type: "answer", sdp: msg.sdp }); + LogMessage("Remote answer applied"); return; } if (msg.type === "rtc_ice_candidate") { + LogMessage("Applying remote ICE candidate"); + await peerConnection.addIceCandidate({ candidate: msg.candidate, sdpMid: msg.sdpMid, sdpMLineIndex: msg.sdpMLineIndex }); + LogMessage("Remote ICE candidate applied"); + return; } + + LogMessage("Unhandled signal type: " + msg.type); } catch (err) { LogMessage("handleRtcSignal failed: " + err); } } + async function loadDevices() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + + availableCameras = devices.filter(d => d.kind === "videoinput"); + availableMics = devices.filter(d => d.kind === "audioinput"); + + const cameraSelect = document.getElementById("cameraSelect"); + const micSelect = document.getElementById("micSelect"); + + if (!cameraSelect || !micSelect) { + LogMessage("Device dropdowns not found."); + return; + } + + cameraSelect.innerHTML = ""; + micSelect.innerHTML = ""; + + const noCameraOption = document.createElement("option"); + noCameraOption.value = ""; + noCameraOption.text = "No camera / audio-only"; + cameraSelect.appendChild(noCameraOption); + + const noMicOption = document.createElement("option"); + noMicOption.value = ""; + noMicOption.text = "Default microphone"; + micSelect.appendChild(noMicOption); + + for (const cam of availableCameras) { + const option = document.createElement("option"); + option.value = cam.deviceId; + option.text = cam.label || `Camera ${cameraSelect.options.length}`; + cameraSelect.appendChild(option); + } + + for (const mic of availableMics) { + const option = document.createElement("option"); + option.value = mic.deviceId; + option.text = mic.label || `Microphone ${micSelect.options.length + 1}`; + micSelect.appendChild(option); + } + + LogMessage(`Loaded devices: ${availableCameras.length} cameras, ${availableMics.length} mics`); + } catch (err) { + LogMessage("loadDevices failed: " + err); + } + } + window.handleRtcSignal = handleRtcSignal; window.addEventListener("HybridWebViewMessageReceived", function (e) { LogMessage("Raw message: " + e.detail.message); }); + + window.addEventListener("load", async () => { + LogMessage("RTC page loaded"); + window.HybridWebView.SendRawMessage("rtc_page_ready"); + await loadDevices(); + });

Relay RTC Test

- +
+
- + +
+ + + + + +
+
- - +
+ +
Local video: waiting...
+
Waiting for local media...
+
+ +
+ +
Remote video: waiting...
+
Remote media: waiting...
+
diff --git a/RelayServer/Program.cs b/RelayServer/Program.cs index 008e16e..89d08f3 100644 --- a/RelayServer/Program.cs +++ b/RelayServer/Program.cs @@ -20,7 +20,6 @@ builder.Services.AddSignalR(); var app = builder.Build(); app.MapGet("/", () => "Server Running!"); app.MapHub("/webrtc"); -app.Run(); var wssv = new WebSocketServer("ws://localhost:1337"); wssv.AddWebSocketService("/"); @@ -127,9 +126,12 @@ ChatTest.ChannelDbKey = keyBase64; Console.WriteLine("Server encryption key created."); -Console.ReadKey(true); -wssv.Stop(); +await app.StartAsync(); +Console.ReadKey(true); + +wssv.Stop(); +await app.StopAsync(); return; static string ToJsonString(object? obj)