using System.Text.Json; 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.SendRaw($"RTC_JOIN_CHANNEL|{_username}|{channelId}"); return Task.CompletedTask; } public void LeaveRtcChannel() { var channelId = _getCurrentChannelId(); if (string.IsNullOrWhiteSpace(channelId)) return; _socket.SendRaw($"RTC_LEAVE_CHANNEL|{_username}|{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; 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) { var currentChannelId = _getCurrentChannelId(); if (payload.ChannelId != currentChannelId) return; if (payload.SenderUsername == _username) 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); } catch (Exception ex) { _sendRawToWebView("RTC signal parse failed: " + ex.Message); return; } if (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(decryptedJson); } 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(string rawJson) { MainThread.BeginInvokeOnMainThread(async () => { try { var jsArg = JsonSerializer.Serialize(rawJson); 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); }} "); } catch (Exception ex) { _sendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message); } }); return Task.CompletedTask; } }