AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

This commit is contained in:
2026-04-01 14:32:23 -04:00
parent bb34b7b0fa
commit 6287f4d19b
2 changed files with 138 additions and 31 deletions

View File

@@ -36,20 +36,10 @@ async function ensurePeerConnection() {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }] iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
}); });
peerConnection.onicecandidate = async (event) => { peerConnection.onicecandidate = (event) => {
if (!event.candidate || !currentChannelId || !currentUsername) return; if (event.candidate) {
LogMessage("ICE candidate gathered");
const payload = { }
type: "rtc_ice_candidate",
from: currentUsername,
channelId: currentChannelId,
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex
};
LogMessage("Sending ICE candidate");
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
}; };
peerConnection.ontrack = (event) => { peerConnection.ontrack = (event) => {
@@ -180,17 +170,13 @@ async function joinChannelCall() {
LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`);
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
const payload = { const payload = {
type: "rtc_offer", type: "rtc_join",
from: currentUsername, from: currentUsername,
channelId: currentChannelId, channelId: currentChannelId
sdp: offer.sdp
}; };
LogMessage("Sending offer to channel " + currentChannelId); LogMessage("Requesting join for channel " + currentChannelId);
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
} catch (err) { } catch (err) {
LogMessage("joinChannelCall failed: " + err); LogMessage("joinChannelCall failed: " + err);
@@ -254,6 +240,30 @@ async function handleRtcSignal(rawJson) {
await ensurePeerConnection(); await ensurePeerConnection();
if (msg.type === "rtc_join_state") {
if (msg.isInitiator) {
LogMessage("No active call found. Becoming initiator.");
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
await waitForIceGatheringComplete(peerConnection);
const payload = {
type: "rtc_offer",
from: currentUsername,
channelId: currentChannelId,
sdp: peerConnection.localDescription.sdp
};
LogMessage("Sending offer to channel " + currentChannelId);
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
} else {
LogMessage("Active call exists. Waiting for stored offer.");
}
return;
}
if (msg.type === "rtc_offer") { if (msg.type === "rtc_offer") {
LogMessage("Incoming channel call offer from " + msg.from); LogMessage("Incoming channel call offer from " + msg.from);
await ensureLocalMedia(); await ensureLocalMedia();
@@ -268,12 +278,13 @@ async function handleRtcSignal(rawJson) {
const answer = await peerConnection.createAnswer(); const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer); await peerConnection.setLocalDescription(answer);
await waitForIceGatheringComplete(peerConnection);
const payload = { const payload = {
type: "rtc_answer", type: "rtc_answer",
from: currentUsername, from: currentUsername,
channelId: msg.channelId, channelId: msg.channelId,
sdp: answer.sdp sdp: peerConnection.localDescription.sdp
}; };
LogMessage("Sending answer to channel " + msg.channelId); LogMessage("Sending answer to channel " + msg.channelId);
@@ -360,6 +371,21 @@ async function loadDevices() {
} }
} }
async function waitForIceGatheringComplete(pc) {
if (pc.iceGatheringState === "complete") return;
await new Promise(resolve => {
function checkState() {
if (pc.iceGatheringState === "complete") {
pc.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
}
pc.addEventListener("icegatheringstatechange", checkState);
});
}
window.handleRtcSignal = handleRtcSignal; window.handleRtcSignal = handleRtcSignal;
window.addEventListener("HybridWebViewMessageReceived", function (e) { window.addEventListener("HybridWebViewMessageReceived", function (e) {

View File

@@ -12,6 +12,8 @@ public class ChatTest : WebSocketBehavior
public static string? ServerPrivateKey { get; set; } public static string? ServerPrivateKey { get; set; }
public static string? ChannelDbKey { get; set; } public static string? ChannelDbKey { get; set; }
public static SurrealDb.Net.SurrealDbClient? Db { get; set; } public static SurrealDb.Net.SurrealDbClient? Db { get; set; }
private static readonly Dictionary<string, string> ActiveRtcOffersByChannel = new();
private static readonly HashSet<string> ActiveRtcChannels = new();
protected override void OnMessage(MessageEventArgs e) protected override void OnMessage(MessageEventArgs e)
{ {
@@ -41,7 +43,7 @@ public class ChatTest : WebSocketBehavior
HandleGetHistory(msg); HandleGetHistory(msg);
return; return;
} }
SocketRtcSignalMessage? rtcProbe = null; SocketRtcSignalMessage? rtcProbe = null;
try try
{ {
@@ -60,7 +62,7 @@ public class ChatTest : WebSocketBehavior
HandleEncryptedClientMessage(msg); HandleEncryptedClientMessage(msg);
} }
private static string ExtractUsernameFromUserId(string senderUserId) private static string ExtractUsernameFromUserId(string senderUserId)
{ {
if (string.IsNullOrWhiteSpace(senderUserId)) if (string.IsNullOrWhiteSpace(senderUserId))
@@ -89,10 +91,8 @@ public class ChatTest : WebSocketBehavior
return; return;
} }
Task.Run(async () => Task.Run(async () => { await ClientKeyService.RegisterOrUpdateKeyAsync(username, publicKey); }).GetAwaiter()
{ .GetResult();
await ClientKeyService.RegisterOrUpdateKeyAsync(username, publicKey);
}).GetAwaiter().GetResult();
Send($"SERVER:REGISTERED_KEY:{username}"); Send($"SERVER:REGISTERED_KEY:{username}");
} }
@@ -125,7 +125,7 @@ public class ChatTest : WebSocketBehavior
Send(JsonSerializer.Serialize(payload)); Send(JsonSerializer.Serialize(payload));
} }
private void HandleGetServerKey() private void HandleGetServerKey()
{ {
if (string.IsNullOrWhiteSpace(ServerPublicKey)) if (string.IsNullOrWhiteSpace(ServerPublicKey))
@@ -322,7 +322,7 @@ public class ChatTest : WebSocketBehavior
Send(JsonSerializer.Serialize(outbound)); Send(JsonSerializer.Serialize(outbound));
} }
} }
private static string GetRecordId(object? id) private static string GetRecordId(object? id)
{ {
if (id is null) if (id is null)
@@ -339,7 +339,7 @@ public class ChatTest : WebSocketBehavior
return $"{table}:{recordId}"; return $"{table}:{recordId}";
} }
private void HandleEncryptedRtcSignal(string msg) private void HandleEncryptedRtcSignal(string msg)
{ {
SocketRtcSignalMessage? clientPayload; SocketRtcSignalMessage? clientPayload;
@@ -384,10 +384,91 @@ public class ChatTest : WebSocketBehavior
return; return;
} }
RtcSignalMessage? rtcSignal;
try
{
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(plainJson);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to parse decrypted RTC signal JSON: {ex.Message}");
return;
}
if (rtcSignal is null)
return;
var allKeys = Task.Run(async () => await ClientKeyService.GetAllAsync()) var allKeys = Task.Run(async () => await ClientKeyService.GetAllAsync())
.GetAwaiter() .GetAwaiter()
.GetResult(); .GetResult();
if (rtcSignal.Type == "rtc_join")
{
var joinState = new
{
type = "rtc_join_state",
from = "server",
channelId = rtcSignal.ChannelId,
isInitiator = !ActiveRtcOffersByChannel.ContainsKey(rtcSignal.ChannelId)
};
var senderClient = allKeys.FirstOrDefault(x => x.Username == clientPayload.SenderUsername);
if (senderClient is null)
{
Console.WriteLine($"No client key found for RTC join sender {clientPayload.SenderUsername}");
return;
}
var joinStateJson = JsonSerializer.Serialize(joinState);
var encryptedJoinState = E2EeHelper.EncryptForRecipient(joinStateJson, senderClient.PublicKey);
var joinStateOutbound = new SocketRtcSignalMessage
{
Type = "encrypted_rtc_signal",
SenderUsername = "server",
ChannelId = clientPayload.ChannelId,
CipherText = encryptedJoinState.CipherText,
Nonce = encryptedJoinState.Nonce,
Tag = encryptedJoinState.Tag,
EncryptedKey = encryptedJoinState.EncryptedKey
};
Send(JsonSerializer.Serialize(joinStateOutbound));
if (ActiveRtcOffersByChannel.TryGetValue(rtcSignal.ChannelId, out var storedOfferJson))
{
var encryptedStoredOffer = E2EeHelper.EncryptForRecipient(storedOfferJson, senderClient.PublicKey);
var storedOfferOutbound = new SocketRtcSignalMessage
{
Type = "encrypted_rtc_signal",
SenderUsername = "server",
ChannelId = clientPayload.ChannelId,
CipherText = encryptedStoredOffer.CipherText,
Nonce = encryptedStoredOffer.Nonce,
Tag = encryptedStoredOffer.Tag,
EncryptedKey = encryptedStoredOffer.EncryptedKey
};
Send(JsonSerializer.Serialize(storedOfferOutbound));
}
return;
}
if (rtcSignal.Type == "rtc_offer")
{
ActiveRtcOffersByChannel[rtcSignal.ChannelId] = plainJson;
ActiveRtcChannels.Add(rtcSignal.ChannelId);
}
if (rtcSignal.Type == "rtc_leave")
{
ActiveRtcOffersByChannel.Remove(rtcSignal.ChannelId);
ActiveRtcChannels.Remove(rtcSignal.ChannelId);
}
foreach (var client in allKeys) foreach (var client in allKeys)
{ {
if (client.Username == clientPayload.SenderUsername) if (client.Username == clientPayload.SenderUsername)