swapping to webview webrtc setup as temp solution

will build a custom C# webrtc implementation later
This commit is contained in:
2026-03-26 03:32:58 -04:00
parent 3d5c35fb15
commit a5772d7579
9 changed files with 266 additions and 149 deletions

View File

@@ -53,6 +53,14 @@
Spacing="8" /> Spacing="8" />
</ScrollView> </ScrollView>
</Border> </Border>
<Border x:Name="RtcView"
Grid.Row="1"
Grid.Column="1"
StrokeThickness="1"
Padding="10"
IsVisible="False">
<WebView Source="test.html"/>
</Border>
<!-- Input --> <!-- Input -->
<Grid Grid.Row="2" <Grid Grid.Row="2"
@@ -70,5 +78,10 @@
Clicked="SendButton_OnClicked" /> Clicked="SendButton_OnClicked" />
</Grid> </Grid>
<!-- Swap View -->
<Button x:Name="ViewSwapped" Grid.Row="2" Grid.Column="0"
Text="Swap to WebView"
Clicked="SwapView_OnClicked" />
</Grid> </Grid>
</ContentPage> </ContentPage>

View File

@@ -2,13 +2,10 @@
using RelayClient.Models; using RelayClient.Models;
using WebSocketSharp; using WebSocketSharp;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.MixedReality.WebRTC;
using System.IO;
using System.Collections.Generic;
namespace RelayClient; namespace RelayClient;
public partial class MainPage : ContentPage public partial class MainPage : ContentPage
{ {
private readonly string _username; private readonly string _username;
@@ -44,7 +41,6 @@ public partial class MainPage : ContentPage
_wsc.Send("GET_SERVER_KEY"); _wsc.Send("GET_SERVER_KEY");
_wsc.Send("GET_CHANNELS"); _wsc.Send("GET_CHANNELS");
_ = webRTCTest();
} }
private void SendButton_OnClicked(object? sender, EventArgs e) private void SendButton_OnClicked(object? sender, EventArgs e)
@@ -293,146 +289,20 @@ public partial class MainPage : ContentPage
await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true); await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true);
} }
private async Task webRTCTest() private void SwapView_OnClicked(object? sender, EventArgs e)
{ {
var path = Path.GetFullPath( if (RtcView.IsVisible)
Path.Combine(AppContext.BaseDirectory, @"..\..\..\debug.txt")
);
void Log(string msg)
{ {
File.AppendAllText(path, msg + Environment.NewLine); MessagesScrollView.IsVisible = true;
Console.WriteLine(msg); RtcView.IsVisible = false;
ViewSwapped.Text = "Swap to Message View";
} }
else
AudioTrackSource? microphoneSource = null;
DeviceVideoTrackSource? webcamSource = null;
Transceiver? audioTransceiver = null;
Transceiver? videoTransceiver = null;
LocalAudioTrack? localAudioTrack = null;
LocalVideoTrack? localVideoTrack = null;
Log("=== WebRTC TEST START ===");
IReadOnlyList<VideoCaptureDevice>? deviceList = null;
try
{ {
Log("Getting video devices..."); MessagesScrollView.IsVisible = false;
RtcView.IsVisible = true;
deviceList = await DeviceVideoTrackSource.GetCaptureDevicesAsync(); ViewSwapped.Text = "Swap to Web View";
foreach (var device in deviceList)
{
Log($"Found device: {device.name} (id: {device.id})");
}
Log("Device enumeration complete.");
}
catch (Exception ex)
{
Log("Device enumeration FAILED:");
Log(ex.ToString());
return;
}
try
{
Log("Creating PeerConnection...");
using var pc = new PeerConnection();
Log("PeerConnection created.");
var config = new PeerConnectionConfiguration
{
IceServers =
{
new IceServer { Urls = { "stun:stun.l.google.com:19302" } }
}
};
Log("Config created.");
Log("Calling InitializeAsync...");
await pc.InitializeAsync(config);
Log("InitializeAsync SUCCESS.");
if (deviceList.Count > 0)
{
try
{
Log($"Getting formats for first device: {deviceList[0].name}");
var formats = await DeviceVideoTrackSource.GetCaptureFormatsAsync(deviceList[0].id);
foreach (var f in formats)
{
Log($"Format: {f.width}x{f.height} @ {f.framerate}");
}
}
catch (Exception ex)
{
Log("Format enumeration FAILED:");
Log(ex.ToString());
}
}
Log("Creating webcam source (default device)...");
webcamSource = await DeviceVideoTrackSource.CreateAsync();
Log("Webcam source created.");
Log("Creating local video track...");
var videoTrackConfig = new LocalVideoTrackInitConfig
{
trackName = "webcam_track"
};
localVideoTrack = LocalVideoTrack.CreateFromSource(webcamSource, videoTrackConfig);
Log("Local video track created.");
Log("Creating microphone source...");
microphoneSource = await DeviceAudioTrackSource.CreateAsync();
Log("Microphone source created.");
Log("Creating local audio track...");
var audioTrackConfig = new LocalAudioTrackInitConfig
{
trackName = "microphone_track"
};
localAudioTrack = LocalAudioTrack.CreateFromSource(microphoneSource, audioTrackConfig);
Log("Local audio track created.");
Log("Adding video transceiver...");
videoTransceiver = pc.AddTransceiver(MediaKind.Video);
videoTransceiver.LocalVideoTrack = localVideoTrack;
videoTransceiver.DesiredDirection = Transceiver.Direction.SendReceive;
Log("Video transceiver configured.");
Log("Adding audio transceiver...");
audioTransceiver = pc.AddTransceiver(MediaKind.Audio);
audioTransceiver.LocalAudioTrack = localAudioTrack;
audioTransceiver.DesiredDirection = Transceiver.Direction.SendReceive;
Log("Audio transceiver configured.");
Log("WebRTC setup complete.");
}
catch (Exception ex)
{
Log("WebRTC setup FAILED:");
Log(ex.ToString());
}
finally
{
Log("Disposing WebRTC resources...");
localAudioTrack?.Dispose();
localVideoTrack?.Dispose();
microphoneSource?.Dispose();
webcamSource?.Dispose();
Log("Resource disposal complete.");
Log("=== WebRTC TEST END ===");
} }
} }
} }

View File

@@ -36,14 +36,24 @@
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" /> <MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<MauiFont Include="Resources\Fonts\*" /> <MauiFont Include="Resources\Fonts\*" />
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<None Remove="Resources\Raw\test.html" />
<MauiAsset Include="Resources\Raw\test.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</MauiAsset>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="11.0.0-preview.2.26159.112" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageReference Include="Microsoft.MixedReality.WebRTC" Version="2.0.2" />
<PackageReference Include="SurrealDb.Net" Version="0.9.0" /> <PackageReference Include="SurrealDb.Net" Version="0.9.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" /> <PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<MauiCss Update="Resources\Raw\test.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</MauiCss>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,3 @@
#header{
border: black solid 2px;
}

View File

@@ -0,0 +1,9 @@
<html lang="enus">
<head>
<link rel="stylesheet" href="test.css">
<script src="test.js"></script>
</head>
<body>
<Button id="header" onclick="onClicked()">Hello World!</Button>
</body>
</html>

View File

@@ -0,0 +1,10 @@
var toggle = true;
function onClicked()
{
if (toggle)
document.getElementById("header").style.color = "green";
else
document.getElementById("header").style.color = "red";
toggle = !toggle;
}

170
RelayClient/WebRTC.cs Normal file
View File

@@ -0,0 +1,170 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Maui.Dispatching;
using Microsoft.AspNetCore.SignalR.Client;
namespace RelayClient;
public static class NativeWebRtc
{
[DllImport("webrtc_wrapper.dll")]
public static extern IntPtr CreatePeerConnection();
[DllImport("webrtc_wrapper.dll")]
public static extern string CreateOffer(IntPtr pc);
[DllImport("webrtc_wrapper.dll")]
public static extern string CreateAnswer(IntPtr pc);
[DllImport("webrtc_wrapper.dll")]
public static extern void SetLocalDescription(IntPtr pc, string type, string sdp);
[DllImport("webrtc_wrapper.dll")]
public static extern void SetRemoteDescription(IntPtr pc, string type, string sdp);
[DllImport("webrtc_wrapper.dll")]
public static extern void AddIceCandidate(IntPtr pc, string candidate);
}
public enum RTCSdpType { Offer, Answer }
public class RTCSessionDescription
{
public RTCSdpType Type { get; set; }
public string Sdp { get; set; }
}
public class RTCIceCandidate
{
public string Candidate { get; set; }
}
public class PeerConnection
{
private readonly IntPtr _nativeHandle;
public string RemoteId { get; set; }
public PeerConnection()
{
_nativeHandle = NativeWebRtc.CreatePeerConnection();
}
public Task CreateOffer(Action<RTCSessionDescription> onOfferCreated)
{
var sdp = NativeWebRtc.CreateOffer(_nativeHandle);
onOfferCreated?.Invoke(new RTCSessionDescription { Type = RTCSdpType.Offer, Sdp = sdp });
return Task.CompletedTask;
}
public Task CreateAnswer(Action<RTCSessionDescription> onAnswerCreated)
{
var sdp = NativeWebRtc.CreateAnswer(_nativeHandle);
onAnswerCreated?.Invoke(new RTCSessionDescription { Type = RTCSdpType.Answer, Sdp = sdp });
return Task.CompletedTask;
}
public Task SetLocalDescription(RTCSessionDescription desc)
{
NativeWebRtc.SetLocalDescription(_nativeHandle, desc.Type.ToString(), desc.Sdp);
return Task.CompletedTask;
}
public Task SetRemoteDescription(RTCSessionDescription desc)
{
NativeWebRtc.SetRemoteDescription(_nativeHandle, desc.Type.ToString(), desc.Sdp);
return Task.CompletedTask;
}
public Task AddIceCandidate(RTCIceCandidate candidate)
{
NativeWebRtc.AddIceCandidate(_nativeHandle, candidate.Candidate);
return Task.CompletedTask;
}
}
public class WebRtcClient
{
private readonly PeerConnection _peerConnection = new();
private readonly HubConnection _signal;
private string _myId;
public WebRtcClient(string serverUrl)
{
_signal = new HubConnectionBuilder()
.WithUrl($"{serverUrl}/webrtc")
.WithAutomaticReconnect()
.Build();
_signal.On<string, string>("ReceiveOffer", (fromId, sdp) =>
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await HandleOffer(fromId, sdp);
});
});
_signal.On<string, string>("ReceiveAnswer", (fromId, sdp) =>
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await HandleAnswer(sdp);
});
});
_signal.On<string, string>("ReceiveIceCandidate", (fromId, candidate) =>
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await HandleIceCandidate(candidate);
});
});
}
public async Task ConnectAsync()
{
await _signal.StartAsync();
_myId = _signal.ConnectionId;
}
public async Task CallAsync(string targetId)
{
_peerConnection.RemoteId = targetId;
await _peerConnection.CreateOffer(async offer =>
{
await _peerConnection.SetLocalDescription(offer);
await _signal.InvokeAsync("SendOffer", targetId, offer.Sdp);
});
}
public async Task HandleOffer(string fromId, string sdp)
{
_peerConnection.RemoteId = fromId;
var remoteDesc = new RTCSessionDescription { Type = RTCSdpType.Offer, Sdp = sdp };
await _peerConnection.SetRemoteDescription(remoteDesc);
await _peerConnection.CreateAnswer(async answer =>
{
await _peerConnection.SetLocalDescription(answer);
await _signal.InvokeAsync("SendAnswer", fromId, answer.Sdp);
});
}
public async Task HandleAnswer(string sdp)
{
var remoteDesc = new RTCSessionDescription { Type = RTCSdpType.Answer, Sdp = sdp };
await _peerConnection.SetRemoteDescription(remoteDesc);
}
public async Task HandleIceCandidate(string candidate)
{
await _peerConnection.AddIceCandidate(new RTCIceCandidate { Candidate = candidate });
}
public async Task SendIceCandidate(string candidate)
{
if (!string.IsNullOrEmpty(_peerConnection.RemoteId))
{
await _signal.InvokeAsync("SendIceCandidate", _peerConnection.RemoteId, candidate);
}
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.Json; using System.Text.Json;
using RelayServer.Services; using RelayServer.Services;
using WebSocketSharp.Server; using WebSocketSharp.Server;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Builder;
using RelayServer.Models; using RelayServer.Models;
var surrealService = new SurrealService(); var surrealService = new SurrealService();
@@ -12,6 +14,14 @@ await using var db = await surrealService.ConnectAsync();
ChatTest.ClientKeyService = new ClientKeyService(db); ChatTest.ClientKeyService = new ClientKeyService(db);
ChatTest.Db = db; ChatTest.Db = db;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapGet("/", () => "Server Running!");
app.MapHub<WebRtcHub>("/webrtc");
app.Run();
var wssv = new WebSocketServer("ws://localhost:1337"); var wssv = new WebSocketServer("ws://localhost:1337");
wssv.AddWebSocketService<ChatTest>("/"); wssv.AddWebSocketService<ChatTest>("/");
wssv.Start(); wssv.Start();
@@ -147,3 +157,24 @@ static string GetRecordId(object? id)
return $"{table}:{recordId}"; return $"{table}:{recordId}";
} }
public class WebRtcHub : Hub
{
public async Task SendOffer(string targetConnectionId, string sdp)
{
await Clients.Client(targetConnectionId)
.SendAsync("ReceiveOffer", Context.ConnectionId, sdp);
}
public async Task SendAnswer(string targetConnectionId, string sdp)
{
await Clients.Client(targetConnectionId)
.SendAsync("ReceiveAnswer", Context.ConnectionId, sdp);
}
public async Task SendIceCandidate(string targetConnectionId, string candidate)
{
await Clients.Client(targetConnectionId)
.SendAsync("ReceiveIceCandidate", Context.ConnectionId, candidate);
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
<PackageReference Include="SurrealDb.Net" Version="0.9.0" /> <PackageReference Include="SurrealDb.Net" Version="0.9.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" /> <PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
</ItemGroup> </ItemGroup>