Added WebRTC stuff - Needs Testing.
This commit is contained in:
@@ -15,7 +15,7 @@ public partial class MainPage : ContentPage
|
||||
private string? _currentChannelName;
|
||||
|
||||
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
|
||||
private readonly List<ChannelItem> _channels = new();
|
||||
private readonly List<ChannelItem> _channels = [];
|
||||
|
||||
public MainPage(string username)
|
||||
{
|
||||
@@ -41,6 +41,11 @@ public partial class MainPage : ContentPage
|
||||
_wsc.Send("GET_SERVER_KEY");
|
||||
_wsc.Send("GET_CHANNELS");
|
||||
hybridWebView.SetInvokeJavaScriptTarget(this);
|
||||
|
||||
Loaded += async (_, _) =>
|
||||
{
|
||||
await InitializeRtcPageAsync();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -66,6 +71,12 @@ public partial class MainPage : ContentPage
|
||||
Console.WriteLine("Server public key not loaded yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_currentChannelId))
|
||||
{
|
||||
Console.WriteLine("No channel selected yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
|
||||
|
||||
@@ -152,49 +163,79 @@ public partial class MainPage : ContentPage
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "encrypted_rtc_signal")
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(e.Data);
|
||||
if (payload is null)
|
||||
return;
|
||||
|
||||
if (payload.RecipientUsername != _username)
|
||||
return;
|
||||
|
||||
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
||||
|
||||
var decryptedJson = E2EeHelper.DecryptForRecipient(
|
||||
new EncryptedPayload
|
||||
{
|
||||
CipherText = payload.CipherText,
|
||||
Nonce = payload.Nonce,
|
||||
Tag = payload.Tag,
|
||||
EncryptedKey = payload.EncryptedKey
|
||||
},
|
||||
privateKey
|
||||
);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await SendRtcSignalToJsAsync(decryptedJson);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != "encrypted_chat")
|
||||
return;
|
||||
|
||||
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||
if (payload is null)
|
||||
var pyload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
|
||||
if (pyload is null)
|
||||
return;
|
||||
|
||||
if (payload.RecipientUsername != _username)
|
||||
if (pyload.RecipientUsername != _username)
|
||||
return;
|
||||
|
||||
Console.WriteLine($"[{_username}] received encrypted payload for {payload.RecipientUsername}");
|
||||
Console.WriteLine($"[{_username}] received encrypted payload for {pyload.RecipientUsername}");
|
||||
|
||||
var privateKey = KeyStorage.LoadPrivateKey(_username);
|
||||
var privKey = KeyStorage.LoadPrivateKey(_username);
|
||||
|
||||
var decryptedText = E2EeHelper.DecryptForRecipient(
|
||||
new EncryptedPayload
|
||||
{
|
||||
CipherText = payload.CipherText,
|
||||
Nonce = payload.Nonce,
|
||||
Tag = payload.Tag,
|
||||
EncryptedKey = payload.EncryptedKey
|
||||
CipherText = pyload.CipherText,
|
||||
Nonce = pyload.Nonce,
|
||||
Tag = pyload.Tag,
|
||||
EncryptedKey = pyload.EncryptedKey
|
||||
},
|
||||
privateKey
|
||||
privKey
|
||||
);
|
||||
|
||||
Console.WriteLine($"[{_username}] decrypted message from {payload.SenderUsername}: {decryptedText}");
|
||||
Console.WriteLine($"[{_username}] decrypted message from {pyload.SenderUsername}: {decryptedText}");
|
||||
|
||||
var message = new ChatMessage
|
||||
{
|
||||
SenderUsername = payload.SenderUsername,
|
||||
SenderUsername = pyload.SenderUsername,
|
||||
Text = decryptedText,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
|
||||
if (!_messagesByChannel.ContainsKey(payload.ChannelId))
|
||||
if (!_messagesByChannel.ContainsKey(pyload.ChannelId))
|
||||
{
|
||||
_messagesByChannel[payload.ChannelId] = [];
|
||||
_messagesByChannel[pyload.ChannelId] = [];
|
||||
}
|
||||
|
||||
_messagesByChannel[payload.ChannelId].Add(message);
|
||||
_messagesByChannel[pyload.ChannelId].Add(message);
|
||||
|
||||
if (payload.ChannelId == _currentChannelId)
|
||||
if (pyload.ChannelId == _currentChannelId)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
@@ -296,14 +337,14 @@ public partial class MainPage : ContentPage
|
||||
{
|
||||
MessagesScrollView.IsVisible = true;
|
||||
RtcView.IsVisible = false;
|
||||
ViewSwapped.Text = "Swap to Message View";
|
||||
ViewSwapped.Text = "Swap to Web View";
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
MessagesScrollView.IsVisible = false;
|
||||
RtcView.IsVisible = true;
|
||||
ViewSwapped.Text = "Swap to Web View";
|
||||
ViewSwapped.Text = "Swap to Message View";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,62 +357,56 @@ public partial class MainPage : ContentPage
|
||||
{
|
||||
await DisplayAlertAsync("Raw Message Received", e.Message, "OK");
|
||||
}
|
||||
|
||||
#region syncs
|
||||
public async void DoSyncWork()
|
||||
{
|
||||
await DisplayAlertAsync("Sync Work", "Sync Work", "OK");
|
||||
}
|
||||
|
||||
public async void DoSyncWorkParams(int i, string s)
|
||||
{
|
||||
await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK");
|
||||
}
|
||||
|
||||
public string DoSyncWorkReturn()
|
||||
{
|
||||
return "Hello from C#!";
|
||||
}
|
||||
|
||||
public SyncReturn DoSyncWorkParamsReturn(int i, string s)
|
||||
{
|
||||
return new SyncReturn
|
||||
{
|
||||
Message = $"Hello from C#! {s}",
|
||||
Value = i
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region asyncs
|
||||
|
||||
public async Task DoAsyncWork()
|
||||
public void SendRtcSignal(string json)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
public async Task DoAsyncWorkParams(int i, string s)
|
||||
{
|
||||
await DisplayAlertAsync("Sync Work", $"{i}:{s}", "OK");
|
||||
}
|
||||
public async Task<string> DoAsyncWorkReturn()
|
||||
{
|
||||
return "Hello from C#!";
|
||||
}
|
||||
|
||||
public async Task<SyncReturn> DoAsyncWorkParamsReturn(int i, string s)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
return new SyncReturn
|
||||
if (string.IsNullOrWhiteSpace(_serverPublicKey))
|
||||
{
|
||||
Message = $"Hello from C# ASync! {s}",
|
||||
Value = i
|
||||
};
|
||||
}
|
||||
Console.WriteLine("Server public key not loaded yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
#endregion
|
||||
public class SyncReturn
|
||||
RtcSignalMessage? rtcSignal;
|
||||
try
|
||||
{
|
||||
rtcSignal = JsonSerializer.Deserialize<RtcSignalMessage>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to parse RTC signal from JS: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rtcSignal is null)
|
||||
return;
|
||||
|
||||
var encrypted = E2EeHelper.EncryptForRecipient(json, _serverPublicKey);
|
||||
|
||||
var payload = new SocketRtcSignalMessage
|
||||
{
|
||||
Type = "encrypted_rtc_signal",
|
||||
SenderUsername = _username,
|
||||
RecipientUsername = rtcSignal.To,
|
||||
CipherText = encrypted.CipherText,
|
||||
Nonce = encrypted.Nonce,
|
||||
Tag = encrypted.Tag,
|
||||
EncryptedKey = encrypted.EncryptedKey
|
||||
};
|
||||
|
||||
_wsc.Send(JsonSerializer.Serialize(payload));
|
||||
Console.WriteLine($"[{_username}] sent RTC signal: {rtcSignal.Type} -> {rtcSignal.To}");
|
||||
}
|
||||
|
||||
private async Task SendRtcSignalToJsAsync(string rawJson)
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
public int Value { get; set; }
|
||||
var jsArg = JsonSerializer.Serialize(rawJson);
|
||||
await hybridWebView.EvaluateJavaScriptAsync($"window.handleRtcSignal({jsArg})");
|
||||
}
|
||||
|
||||
private async Task InitializeRtcPageAsync()
|
||||
{
|
||||
var jsArg = JsonSerializer.Serialize(_username);
|
||||
await hybridWebView.EvaluateJavaScriptAsync($"window.currentUsername = {jsArg};");
|
||||
Console.WriteLine($"[{_username}] RTC page initialized.");
|
||||
}
|
||||
}
|
||||
13
RelayClient/Models/RtcSignalMessage.cs
Normal file
13
RelayClient/Models/RtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace RelayClient.Models;
|
||||
|
||||
public class RtcSignalMessage
|
||||
{
|
||||
public required string Type { get; set; } // rtc_offer / rtc_answer / rtc_ice_candidate / rtc_call_request / rtc_call_accept / rtc_call_reject
|
||||
public required string From { get; set; }
|
||||
public required string To { get; set; }
|
||||
|
||||
public string? Sdp { get; set; }
|
||||
public string? Candidate { get; set; }
|
||||
public string? SdpMid { get; set; }
|
||||
public int? SdpMLineIndex { get; set; }
|
||||
}
|
||||
13
RelayClient/Models/SocketRtcSignalMessage.cs
Normal file
13
RelayClient/Models/SocketRtcSignalMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace RelayClient.Models;
|
||||
|
||||
public class SocketRtcSignalMessage
|
||||
{
|
||||
public required string Type { get; set; } // encrypted_rtc_signal
|
||||
public required string SenderUsername { get; set; }
|
||||
public required string RecipientUsername { get; set; }
|
||||
|
||||
public required string CipherText { get; set; }
|
||||
public required string Nonce { get; set; }
|
||||
public required string Tag { get; set; }
|
||||
public required string EncryptedKey { get; set; }
|
||||
}
|
||||
@@ -9,116 +9,190 @@
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script src="_framework/hybridwebview.js"></script>
|
||||
<script>
|
||||
let peerConnection = null;
|
||||
let localStream = null;
|
||||
let currentTarget = null;
|
||||
let currentUsername = null;
|
||||
|
||||
function LogMessage(msg) {
|
||||
var messageLog = document.getElementById("messageLog");
|
||||
const messageLog = document.getElementById("messageLog");
|
||||
messageLog.value += '\r\n' + msg;
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"HybridWebViewMessageReceived",
|
||||
function (e) {
|
||||
LogMessage("Raw message: " + e.detail.message);
|
||||
async function ensurePeerConnection() {
|
||||
if (peerConnection) return;
|
||||
|
||||
peerConnection = new RTCPeerConnection({
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
||||
});
|
||||
|
||||
function AddNumbers(a, b) {
|
||||
var result = {
|
||||
"result": a + b,
|
||||
"operationName": "Addition"
|
||||
peerConnection.onicecandidate = async (event) => {
|
||||
if (!event.candidate || !currentTarget || !currentUsername) return;
|
||||
|
||||
const payload = {
|
||||
type: "rtc_ice_candidate",
|
||||
from: currentUsername,
|
||||
to: currentTarget,
|
||||
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) => {
|
||||
LogMessage("Remote track received");
|
||||
const remoteVideo = document.getElementById("remoteVideo");
|
||||
remoteVideo.srcObject = event.streams[0];
|
||||
};
|
||||
|
||||
peerConnection.onconnectionstatechange = () => {
|
||||
LogMessage("Connection state: " + peerConnection.connectionState);
|
||||
};
|
||||
|
||||
peerConnection.oniceconnectionstatechange = () => {
|
||||
LogMessage("ICE connection state: " + peerConnection.iceConnectionState);
|
||||
};
|
||||
|
||||
peerConnection.onicegatheringstatechange = () => {
|
||||
LogMessage("ICE gathering state: " + peerConnection.iceGatheringState);
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
async function ensureLocalMedia() {
|
||||
if (localStream) return;
|
||||
|
||||
async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
|
||||
const response = await fetch("/asyncdata.txt");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error: ${response.status}`);
|
||||
try {
|
||||
localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
var jsonData = await response.json();
|
||||
|
||||
jsonData[s1] = s2;
|
||||
|
||||
const msg = 'JSON data is available: ' + JSON.stringify(jsonData);
|
||||
window.HybridWebView.SendRawMessage(msg)
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
async function InvokeDoSyncWork() {
|
||||
LogMessage("Invoking DoSyncWork");
|
||||
await window.HybridWebView.InvokeDotNet('DoSyncWork');
|
||||
LogMessage("Invoked DoSyncWork");
|
||||
async function startCall() {
|
||||
try {
|
||||
currentTarget = document.getElementById("targetUser").value;
|
||||
|
||||
if (!currentTarget) {
|
||||
LogMessage("No target user set.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ensurePeerConnection();
|
||||
await ensureLocalMedia();
|
||||
|
||||
const offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
const payload = {
|
||||
type: "rtc_offer",
|
||||
from: currentUsername,
|
||||
to: currentTarget,
|
||||
sdp: offer.sdp
|
||||
};
|
||||
|
||||
LogMessage("Sending offer to " + currentTarget);
|
||||
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
|
||||
} catch (err) {
|
||||
LogMessage("startCall failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
async function InvokeDoSyncWorkParams() {
|
||||
LogMessage("Invoking DoSyncWorkParams");
|
||||
await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']);
|
||||
LogMessage("Invoked DoSyncWorkParams");
|
||||
async function handleRtcSignal(rawJson) {
|
||||
try {
|
||||
const msg = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson;
|
||||
|
||||
LogMessage("Received signal: " + msg.type + " from " + msg.from);
|
||||
|
||||
await ensurePeerConnection();
|
||||
|
||||
if (msg.type === "rtc_offer") {
|
||||
currentTarget = msg.from;
|
||||
LogMessage("Incoming call from " + msg.from);
|
||||
await ensureLocalMedia();
|
||||
|
||||
await peerConnection.setRemoteDescription({
|
||||
type: "offer",
|
||||
sdp: msg.sdp
|
||||
});
|
||||
|
||||
const answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
|
||||
const payload = {
|
||||
type: "rtc_answer",
|
||||
from: currentUsername,
|
||||
to: msg.from,
|
||||
sdp: answer.sdp
|
||||
};
|
||||
|
||||
LogMessage("Sending answer to " + msg.from);
|
||||
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "rtc_answer") {
|
||||
await peerConnection.setRemoteDescription({
|
||||
type: "answer",
|
||||
sdp: msg.sdp
|
||||
});
|
||||
LogMessage("Remote answer applied");
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "rtc_ice_candidate") {
|
||||
await peerConnection.addIceCandidate({
|
||||
candidate: msg.candidate,
|
||||
sdpMid: msg.sdpMid,
|
||||
sdpMLineIndex: msg.sdpMLineIndex
|
||||
});
|
||||
LogMessage("Remote ICE candidate applied");
|
||||
}
|
||||
} catch (err) {
|
||||
LogMessage("handleRtcSignal failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
async function InvokeDoSyncWorkReturn() {
|
||||
LogMessage("Invoking DoSyncWorkReturn");
|
||||
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn');
|
||||
LogMessage("Invoked DoSyncWorkReturn, return value: " + retValue);
|
||||
}
|
||||
|
||||
async function InvokeDoSyncWorkParamsReturn() {
|
||||
LogMessage("Invoking DoSyncWorkParamsReturn");
|
||||
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']);
|
||||
LogMessage("Invoked DoSyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
|
||||
}
|
||||
|
||||
async function InvokeDoAsyncWork() {
|
||||
LogMessage("Invoking DoAsyncWork");
|
||||
await window.HybridWebView.InvokeDotNet('DoAsyncWork');
|
||||
LogMessage("Invoked DoAsyncWork");
|
||||
}
|
||||
|
||||
async function InvokeDoAsyncWorkParams() {
|
||||
LogMessage("Invoking DoAsyncWorkParams");
|
||||
await window.HybridWebView.InvokeDotNet('DoAsyncWorkParams', [123, 'hello']);
|
||||
LogMessage("Invoked DoAsyncWorkParams");
|
||||
}
|
||||
|
||||
async function InvokeDoAsyncWorkReturn() {
|
||||
LogMessage("Invoking DoAsyncWorkReturn");
|
||||
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkReturn');
|
||||
LogMessage("Invoked DoAsyncWorkReturn, return value: " + retValue);
|
||||
}
|
||||
|
||||
async function InvokeDoAsyncWorkParamsReturn() {
|
||||
LogMessage("Invoking DoAsyncWorkParamsReturn");
|
||||
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkParamsReturn', [123, 'hello']);
|
||||
LogMessage("Invoked DoAsyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value);
|
||||
}
|
||||
window.handleRtcSignal = handleRtcSignal;
|
||||
|
||||
window.addEventListener("HybridWebViewMessageReceived", function (e) {
|
||||
LogMessage("Raw message: " + e.detail.message);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
Hybrid sample!
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="InvokeDoSyncWork()">Call C# sync method (no params)</button>
|
||||
<button onclick="InvokeDoSyncWorkParams()">Call C# sync method (params)</button>
|
||||
<button onclick="InvokeDoSyncWorkReturn()">Call C# method (no params) and get simple return value</button>
|
||||
<button onclick="InvokeDoSyncWorkParamsReturn()">Call C# method (params) and get complex return value</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="InvokeDoAsyncWork()">Call C# async method (no params)</button>
|
||||
<button onclick="InvokeDoAsyncWorkParams()">Call C# async method (params)</button>
|
||||
<button onclick="InvokeDoAsyncWorkReturn()">Call C# async method (no params) and get simple return value</button>
|
||||
<button onclick="InvokeDoAsyncWorkParamsReturn()">Call C# async method (params) and get complex return value</button>
|
||||
</div>
|
||||
<div>
|
||||
Log: <textarea readonly id="messageLog" style="width: 80%; height: 10em;"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
Consider checking out this PDF: <a href="docs/sample.pdf">sample.pdf</a>
|
||||
</div>
|
||||
</body>
|
||||
<body>
|
||||
<div>
|
||||
<h3>Relay RTC Test</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="targetUser">Target User:</label>
|
||||
<input id="targetUser" type="text" value="Ru_Kira" />
|
||||
<button onclick="startCall()">Start Call</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px;">
|
||||
<video id="localVideo" autoplay playsinline muted style="width: 320px; height: 240px; background: #111;"></video>
|
||||
<video id="remoteVideo" autoplay playsinline style="width: 320px; height: 240px; background: #111;"></video>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px;">
|
||||
<textarea readonly id="messageLog" style="width: 90%; height: 12em;"></textarea>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user