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)