setup client to make calls to servers api for webrtc data management

This commit is contained in:
2026-04-03 19:45:06 -04:00
parent 63e427a4a1
commit c89a0cf88b
3 changed files with 217 additions and 66 deletions

View File

@@ -42,6 +42,7 @@ public partial class MainPage : ContentPage
_wsc.Send("GET_CHANNELS"); _wsc.Send("GET_CHANNELS");
hybridWebView.SetInvokeJavaScriptTarget(this); hybridWebView.SetInvokeJavaScriptTarget(this);
ServerAPI.setupClient();
} }
@@ -353,21 +354,58 @@ public partial class MainPage : ContentPage
} }
} }
public void JoinRtcChannel() public async Task<bool> JoinRtcChannel()
{ {
//TODO: get bool value for if channel ID has an active call //TODO: get bool value for if channel ID has an active call
//TODO: Join RTC using current channel ID //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<RtcDescription>(json);
DBOffer offer = new DBOffer
{
ChannelId = _currentChannelId,
Username = _username,
SessionDescription = description
};
await ServerAPI.PostOfferAsync(offer);
}
catch (Exception ex)
{
hybridWebView.SendRawMessage(ex.Message);
} }
public void WriteRtcOffer(string json)
{
RTCOffer? offer = JsonSerializer.Deserialize<RTCOffer>(json);
} }
private class RTCOffer public async Task<string> GetRtcOffer()
{ {
private string type { get; set; } RtcDescription? offer = await ServerAPI.GetOffersForChannelAsync(_currentChannelId);
private string sdp { get; set; } return JsonSerializer.Serialize(offer);
} }
public async void WriteRtcAnswer(string json)
{
RtcDescription? description = JsonSerializer.Deserialize<RtcDescription>(json);
DBOffer answer = new DBOffer
{
ChannelId = _currentChannelId,
Username = _username,
SessionDescription = description
};
await ServerAPI.PostAnswerAsync(answer);
}
public async void AnswerCallback(RtcDescription answer)
{
await hybridWebView.EvaluateJavaScriptAsync($"window.AnswerCallback({answer})");
}
private void OnSendMessageButtonClicked(object sender, EventArgs e) private void OnSendMessageButtonClicked(object sender, EventArgs e)
{ {
hybridWebView.SendRawMessage($"Hello from C#!"); hybridWebView.SendRawMessage($"Hello from C#!");

View File

@@ -166,33 +166,35 @@ async function ensureLocalMedia() {
async function joinChannelCall() { async function joinChannelCall() {
LogMessage("Current username: " + currentUsername); LogMessage("Current username: " + currentUsername);
LogMessage("Current channel: " + currentChannelId); LogMessage("Current channel: " + currentChannelId);
//TODO: Update Server DB to hold bool if channel has an active call LogMessage("Joining RTCChannel");
//TODO: First check if channel already has an active offer, if it does join with an answer, otherwise make a new offer let active = await window.HybridWebView.InvokeDotNet("JoinRtcChannel");
try { await channelCallJoin(active);
if (!currentChannelId) { LogMessage("Joined RTCChannel");
LogMessage("No current channel set."); // return;
return; // try {
} // if (!currentChannelId) {
// LogMessage("No current channel set.");
await ensurePeerConnection(); // return;
await ensureLocalMedia(); // }
//
LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`); // await ensurePeerConnection();
// await ensureLocalMedia();
const payload = { //
type: "rtc_join", // LogMessage(`Joining call with media: audio=${hasAudioTrack()} video=${hasVideoTrack()}`);
from: currentUsername, //
channelId: currentChannelId // const payload = {
}; // type: "rtc_join",
// from: currentUsername,
LogMessage("Requesting join for channel " + currentChannelId); // channelId: currentChannelId
await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]); // };
} catch (err) { //
LogMessage("joinChannelCall failed: " + err); // LogMessage("Requesting join for channel " + currentChannelId);
} // await window.HybridWebView.InvokeDotNet("SendRtcSignal", [JSON.stringify(payload)]);
// } catch (err) {
// LogMessage("joinChannelCall failed: " + err);
// }
} }
async function ensurePeerConnection2() async function ensurePeerConnection2()
{ {
if (peerConnection) return; if (peerConnection) return;
@@ -215,60 +217,36 @@ async function ensurePeerConnection2()
console.log( console.log(
`ICE connection state change: ${peerConnection.iceConnectionState}`); `ICE connection state change: ${peerConnection.iceConnectionState}`);
}); });
} }
async function channelCallJoin(activeCall) async function channelCallJoin(activeCall)
{ {
LogMessage("Active call: " + activeCall);
await ensurePeerConnection2(); await ensurePeerConnection2();
if (activeCall) 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); await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer(); const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer); await peerConnection.setLocalDescription(answer);
const roomAnswer = { LogMessage(`Joining call with media answer: ${JSON.stringify(answer)}`);
answer: { await window.HybridWebView.InvokeDotNet("WriteRtcAnswer", [JSON.stringify(roomAnswer)]);
type: answer.type, //TODO: Update offer in SurrealDB to include answer
sdp: answer.sdp
}
}
await roomRef.update(roomAnswer); //TODO: Update offer in SurrealDB to include answer
} }
else else
{ {
const offer = await peerConnection.createOffer(); const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer); await peerConnection.setLocalDescription(offer);
const roomOffer = {
offer: {
type: offer.type,
sdp: offer.sdp
}
}
await window.HybridWebView.InvokeDotNet("WriteRtcOffer", [JSON.stringify(offer)]); await window.HybridWebView.InvokeDotNet("WriteRtcOffer", [JSON.stringify(offer)]);
LogMessage(`Joining call with media offer: ${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);
}
});
localStream.getTracks().forEach(track => { localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream); peerConnection.addTrack(track, localStream);
}); });
//TODO: collect ICE candidates
peerConnection.addEventListener('track', event => { peerConnection.addEventListener('track', event => {
LogMessage("Received track: " + event.streams[0]); LogMessage("Received track: " + event.streams[0]);
event.streams[0].getTracks().forEach(track => { 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) { async function handleRtcSignal(rawJson) {
try { try {
const msg = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson; const msg = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson;

121
RelayClient/ServerAPI.cs Normal file
View File

@@ -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<Uri> PostOfferAsync(DBOffer offer)
{
HttpResponseMessage response = await client.PostAsJsonAsync(
"api/rtc/offer", offer);
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> GetAllOffersAsync()
{
HttpResponseMessage response = await client.GetAsync("api/rtc/offers");
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<bool> 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<RtcDescription> GetOffersForChannelAsync(string channelId)
{
HttpResponseMessage response = await client.GetAsync($"api/rtc/offers/{channelId}");
response.EnsureSuccessStatusCode();
RtcDescription? offer = JsonSerializer.Deserialize<RtcDescription>(await response.Content.ReadAsStringAsync());
return offer;
}
public static async Task<Uri> PostAnswerAsync(DBOffer answer)
{
HttpResponseMessage response = await client.PostAsJsonAsync("api/rtc/answer", answer);
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> GetAnswersForChannelAsync(string channelId)
{
HttpResponseMessage response = await client.GetAsync($"api/rtc/answers/{channelId}");
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> GetLatestAnswerForChannelAsync(string channelId)
{
HttpResponseMessage response = await client.GetAsync($"api/rtc/latest/{channelId}");
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> PostIceCandidateAsync(IceCandidate candidate)
{
HttpResponseMessage response = await client.PostAsJsonAsync("api/rtc/candidate", candidate);
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> GetIceCandidatesForChannelAsync(string channelId)
{
HttpResponseMessage response = await client.GetAsync($"api/rtc/candidates/{channelId}");
response.EnsureSuccessStatusCode();
return response.Headers.Location;
}
public static async Task<Uri> 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<Uri> 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; }
}