using System.Text.Json; using System.Text.Json.Serialization; using RelayClient.Crypto; using RelayShared.Rtc; using RelayShared.Services; namespace RelayClient.Services; public sealed class RtcBridgeService { private readonly string _username; private readonly RelaySocketClient _socket; private readonly HybridWebView _hybridWebView; private readonly Func _getCurrentChannelId; private readonly Action _sendRawToWebView; public RtcBridgeService(string username, RelaySocketClient socket, HybridWebView hybridWebView, Func getCurrentChannelId, Action sendRawToWebView) { _username = username; _socket = socket; _hybridWebView = hybridWebView; _getCurrentChannelId = getCurrentChannelId; _sendRawToWebView = sendRawToWebView; } public Task JoinRtcChannel() { var channelId = _getCurrentChannelId(); if (string.IsNullOrWhiteSpace(channelId)) return Task.CompletedTask; _socket.SendRtcJoinChannel(channelId); return Task.CompletedTask; } public void LeaveRtcChannel() { var channelId = _getCurrentChannelId(); if (string.IsNullOrWhiteSpace(channelId)) return; _socket.SendRtcLeaveChannel(channelId); } public void SendRtcSignal(string json) { if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey)) { _sendRawToWebView("SendRtcSignal failed: server public key not loaded."); return; } RtcSignalMessage? rtcSignal; try { rtcSignal = JsonSerializer.Deserialize(json); } catch (Exception ex) { _sendRawToWebView("SendRtcSignal failed to parse RTC signal: " + ex.Message); return; } if (rtcSignal is null) return; rtcSignal.ChannelId ??= _getCurrentChannelId(); rtcSignal.From ??= _username; // _sendRawToWebView($"RTC_SIGNAL file: {JsonSerializer.Serialize(rtcSignal)}"); if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId)) { _sendRawToWebView("SendRtcSignal failed: missing channel id."); return; } var outgoingJson = JsonSerializer.Serialize(rtcSignal); try { var encrypted = E2EeHelper.EncryptForRecipient(outgoingJson, _socket.ServerPublicKey); var payload = new SocketRtcSignalMessage { Type = SignalType.EncryptedSignal, SenderUsername = _username, ChannelId = rtcSignal.ChannelId, CipherText = encrypted.CipherText, Nonce = encrypted.Nonce, Tag = encrypted.Tag, EncryptedKey = encrypted.EncryptedKey }; _socket.SendJson(payload); _sendRawToWebView($"SendRtcSignal sent: {rtcSignal.Type} -> {rtcSignal.To}"); } catch (Exception ex) { _sendRawToWebView("SendRtcSignal failed: " + ex.Message); } } public async Task GetRtcParticipants() { var channelId = _getCurrentChannelId(); if (string.IsNullOrWhiteSpace(channelId)) return "[]"; var participants = await ServerAPI.GetRtcParticipantsAsync(channelId); return JsonSerializer.Serialize(participants ?? []); } public async Task HandleIncomingRtcSignalAsync(SocketRtcSignalMessage payload) { // _sendRawToWebView("HandleIncomingRtcSignal called"); var currentChannelId = _getCurrentChannelId(); if (payload.ChannelId != currentChannelId) { _sendRawToWebView("Channel id does not match"); return; } if (payload.SenderUsername == _username) { _sendRawToWebView("Received own message"); return; } string decryptedJson; try { var privateKey = KeyStorage.LoadPrivateKey(_username); decryptedJson = E2EeHelper.DecryptForRecipient( new EncryptedPayload { CipherText = payload.CipherText, Nonce = payload.Nonce, Tag = payload.Tag, EncryptedKey = payload.EncryptedKey }, privateKey ); } catch (Exception ex) { _sendRawToWebView("RTC decrypt failed: " + ex.Message); return; } RtcSignalMessage? rtcSignal; try { rtcSignal = JsonSerializer.Deserialize(decryptedJson); // _sendRawToWebView($"Received Encrypted Signal: [{rtcSignal.From}]: {rtcSignal.Offer}"); } catch (Exception ex) { _sendRawToWebView("RTC signal parse failed: " + ex.Message); return; } if (rtcSignal is null) { _sendRawToWebView("rtcSignal is null"); return; } if (!string.IsNullOrWhiteSpace(rtcSignal.To) && !string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase)) { _sendRawToWebView($"Ignoring RTC signal meant for {rtcSignal.To}"); return; } // _sendRawToWebView("Received encrypted RTC signal: " + decryptedJson); await SendRtcSignalToJsAsync(rtcSignal); } public Task PushRtcContextToJsAsync() { MainThread.BeginInvokeOnMainThread(async () => { var usernameJson = JsonSerializer.Serialize(_username); var channelIdJson = JsonSerializer.Serialize(_getCurrentChannelId()); await _hybridWebView.EvaluateJavaScriptAsync($"window.setUsername({usernameJson})"); await _hybridWebView.EvaluateJavaScriptAsync($"window.setChannelId({channelIdJson})"); }); return Task.CompletedTask; } private Task SendRtcSignalToJsAsync(RtcSignalMessage data) { if (data.Type == "rtc_offer" || data.Type == "rtc_answer") { data.Sdp = data.Sdp.Replace("\r\n", "(rn)"); } MainThread.BeginInvokeOnMainThread(async () => { try { // await _hybridWebView.InvokeJavaScriptAsync("testIndex", [JsonSerializer.Serialize(data)], [RtcJsType.Default.String]); await _hybridWebView.InvokeJavaScriptAsync("testIndex", [data], [RtcJsType.Default.RtcSignalMessage]); #region OldDebugger // var jsArg = JsonSerializer.Serialize(data); // // await _hybridWebView.EvaluateJavaScriptAsync($@" // try {{ // window.HybridWebView.SendRawMessage('C# eval entered'); // // if (!window.RelaySocket) {{ // window.HybridWebView.SendRawMessage('window.RelaySocket missing'); // }} else if (typeof window.RelaySocket.receiveRtcSignal !== 'function') {{ // window.HybridWebView.SendRawMessage('RelaySocket.receiveRtcSignal missing'); // }} else {{ // window.HybridWebView.SendRawMessage('Calling RelaySocket.receiveRtcSignal'); // window.RelaySocket.receiveRtcSignal({jsArg}); // }} // }} catch (err) {{ // window.HybridWebView.SendRawMessage('RTC JS dispatch failed: ' + err); // }} // "); #endregion } catch (Exception ex) { _sendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message); } }); return Task.CompletedTask; } } [JsonSourceGenerationOptions(WriteIndented = false)] [JsonSerializable(typeof(RtcDescription))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(RtcSignalMessage))] [JsonSerializable(typeof(IceCandidate))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(string))] internal partial class RtcJsType : JsonSerializerContext { }