diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index a236d66..dc362a7 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -32,7 +32,7 @@ public partial class MainPage : ContentPage } _wsc = new WebSocket("ws://localhost:1337/"); - + _wsc.OnMessage += WscOnMessage; _wsc.Connect(); @@ -42,6 +42,7 @@ public partial class MainPage : ContentPage _wsc.Send("GET_CHANNELS"); hybridWebView.SetInvokeJavaScriptTarget(this); + ServerAPI.setupClient(); } @@ -353,21 +354,58 @@ public partial class MainPage : ContentPage } } - public void JoinRtcChannel() + public async Task JoinRtcChannel() { //TODO: get bool value for if channel ID has an active call //TODO: Join RTC using current channel ID + hybridWebView.SendRawMessage($"Attempting to join RTC Channel {_currentChannelName}"); + bool active = await ServerAPI.GetIsChannelActiveAsync(_currentChannelId); + hybridWebView.SendRawMessage($"Rtc Channel {_currentChannelName} is active: {active}"); + return active; + //await hybridWebView.EvaluateJavaScriptAsync($"window.channelCallJoin({active})"); + + } + public async void WriteRtcOffer(string json) + { + try + { + RtcDescription? description = JsonSerializer.Deserialize(json); + DBOffer offer = new DBOffer + { + ChannelId = _currentChannelId, + Username = _username, + SessionDescription = description + }; + await ServerAPI.PostOfferAsync(offer); + } + catch (Exception ex) + { + hybridWebView.SendRawMessage(ex.Message); + } + + } + public async Task GetRtcOffer() + { + RtcDescription? offer = await ServerAPI.GetOffersForChannelAsync(_currentChannelId); + return JsonSerializer.Serialize(offer); + } + public async void WriteRtcAnswer(string json) + { + RtcDescription? description = JsonSerializer.Deserialize(json); + DBOffer answer = new DBOffer + { + ChannelId = _currentChannelId, + Username = _username, + SessionDescription = description + }; + await ServerAPI.PostAnswerAsync(answer); } - public void WriteRtcOffer(string json) + public async void AnswerCallback(RtcDescription answer) { - RTCOffer? offer = JsonSerializer.Deserialize(json); - } - private class RTCOffer - { - private string type { get; set; } - private string sdp { get; set; } + await hybridWebView.EvaluateJavaScriptAsync($"window.AnswerCallback({answer})"); } + private void OnSendMessageButtonClicked(object sender, EventArgs e) { hybridWebView.SendRawMessage($"Hello from C#!"); diff --git a/RelayClient/Resources/Raw/wwwroot/index.js b/RelayClient/Resources/Raw/wwwroot/index.js index 94484ab..a9d27eb 100644 --- a/RelayClient/Resources/Raw/wwwroot/index.js +++ b/RelayClient/Resources/Raw/wwwroot/index.js @@ -166,33 +166,35 @@ async function ensureLocalMedia() { async function joinChannelCall() { LogMessage("Current username: " + currentUsername); LogMessage("Current channel: " + currentChannelId); - //TODO: Update Server DB to hold bool if channel has an active call - //TODO: First check if channel already has an active offer, if it does join with an answer, otherwise make a new offer - try { - if (!currentChannelId) { - LogMessage("No current channel set."); - return; - } - - await ensurePeerConnection(); - await ensureLocalMedia(); - - LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); - - const payload = { - type: "rtc_join", - from: currentUsername, - channelId: currentChannelId - }; - - LogMessage("Requesting join for channel " + currentChannelId); - await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); - } catch (err) { - LogMessage("joinChannelCall failed: " + err); - } + LogMessage("Joining RTCChannel"); + let active = await window.HybridWebView.InvokeDotNet("JoinRtcChannel"); + await channelCallJoin(active); + LogMessage("Joined RTCChannel"); + // return; + // try { + // if (!currentChannelId) { + // LogMessage("No current channel set."); + // return; + // } + // + // await ensurePeerConnection(); + // await ensureLocalMedia(); + // + // LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); + // + // const payload = { + // type: "rtc_join", + // from: currentUsername, + // channelId: currentChannelId + // }; + // + // LogMessage("Requesting join for channel " + currentChannelId); + // await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); + // } catch (err) { + // LogMessage("joinChannelCall failed: " + err); + // } } - async function ensurePeerConnection2() { if (peerConnection) return; @@ -215,60 +217,36 @@ async function ensurePeerConnection2() console.log( `ICE connection state change: ${peerConnection.iceConnectionState}`); }); - } async function channelCallJoin(activeCall) { + LogMessage("Active call: " + activeCall); await ensurePeerConnection2(); if (activeCall) { - const offer = roomSnapshot.data().offer; //TODO: Replace with active call offer from DB using the active ID for current channel + const rawJson = await window.HybridWebView.InvokeDotNet("GetRtcOffer"); + const offer = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson; await peerConnection.setRemoteDescription(offer); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); - - const roomAnswer = { - answer: { - type: answer.type, - sdp: answer.sdp - } - } - await roomRef.update(roomAnswer); //TODO: Update offer in SurrealDB to include answer + + LogMessage(`Joining call with media answer: ${JSON.stringify(answer)}`); + await window.HybridWebView.InvokeDotNet("WriteRtcAnswer", [JSON.stringify(roomAnswer)]); + //TODO: Update offer in SurrealDB to include answer } else { const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); - const roomOffer = { - offer: { - type: offer.type, - sdp: offer.sdp - } - } - await window.HybridWebView.InvokeDotNet("WriteRtcOffer", [JSON.stringify(offer)]); - - //TODO: Write roomId to surreal DB with channel id as active call - - //TODO: Add callback function for when call is answered to replace following code block - roomRef.onSnapshot(async snapshot => { - console.log('Got updated room:', snapshot.data()); - const data = snapshot.data(); - if (!peerConnection.currentRemoteDescription && data.answer) { - console.log('Set remote description: ', data.answer); - const answer = new RTCSessionDescription(data.answer) - await peerConnection.setRemoteDescription(answer); - } - }); + LogMessage(`Joining call with media offer: ${JSON.stringify(offer)}`); localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); - //TODO: collect ICE candidates - peerConnection.addEventListener('track', event => { LogMessage("Received track: " + event.streams[0]); event.streams[0].getTracks().forEach(track => { @@ -277,9 +255,23 @@ async function channelCallJoin(activeCall) }); }); } +} +async function AnswerCallback(answer) +{ + LogMessage("Answer: " + JSON.stringify(answer)); + if (!peerConnection.currentRemoteDescription && answer.answer) + { + LogMessage("Current answer: " + answer); + const desc = new RTCSessionDescription(answer); + await peerConnection.setRemoteDescription(desc); + } } +async function CollectIceCandidates() +{ + //TODO: collect ICE candidates +} async function handleRtcSignal(rawJson) { try { const msg = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson; diff --git a/RelayClient/ServerAPI.cs b/RelayClient/ServerAPI.cs new file mode 100644 index 0000000..d9699ea --- /dev/null +++ b/RelayClient/ServerAPI.cs @@ -0,0 +1,121 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace RelayClient; + +public class ServerAPI +{ + static HttpClient client = new HttpClient { BaseAddress = new Uri("http://localhost:5000/") }; + + public static void setupClient() + { + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + } + + public static async Task PostOfferAsync(DBOffer offer) + { + HttpResponseMessage response = await client.PostAsJsonAsync( + "api/rtc/offer", offer); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetAllOffersAsync() + { + HttpResponseMessage response = await client.GetAsync("api/rtc/offers"); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetIsChannelActiveAsync(string channelId) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/active/{channelId}"); + response.EnsureSuccessStatusCode(); + return bool.Parse(response.Content.ReadAsStringAsync().Result); + } + + public static async Task GetOffersForChannelAsync(string channelId) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/offers/{channelId}"); + response.EnsureSuccessStatusCode(); + RtcDescription? offer = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); + return offer; + } + + public static async Task PostAnswerAsync(DBOffer answer) + { + HttpResponseMessage response = await client.PostAsJsonAsync("api/rtc/answer", answer); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetAnswersForChannelAsync(string channelId) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/answers/{channelId}"); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetLatestAnswerForChannelAsync(string channelId) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/latest/{channelId}"); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task PostIceCandidateAsync(IceCandidate candidate) + { + HttpResponseMessage response = await client.PostAsJsonAsync("api/rtc/candidate", candidate); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetIceCandidatesForChannelAsync(string channelId) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/candidates/{channelId}"); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task GetIceCandidatesForChannelByUserAsync(string channelId, string userId, string directions) + { + HttpResponseMessage response = await client.GetAsync($"api/rtc/candidates/{channelId}/{userId}/{directions}"); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + + public static async Task PostLeave(RtcLeave leave) + { + HttpResponseMessage response = await client.PostAsJsonAsync("api/rtc/leave", leave); + response.EnsureSuccessStatusCode(); + return response.Headers.Location; + } + +} + +public class RtcDescription +{ + public string type { get; set; } + public string sdp { get; set; } +} + +public class DBOffer +{ + public required string ChannelId { get; set; } + public required string Username { get; set; } + public required RtcDescription SessionDescription { get; set; } +} +public class IceCandidate +{ + public string type { get; set; } + public string sdp { get; set; } +} + +public class RtcLeave +{ + public string ChannelId { get; set; } + public string Username { get; set; } +} \ No newline at end of file