gstreamer/subprojects/gst-examples/webrtc/sendrecv/gst-sharp/WebRTCSendRecv.cs
Nirbheek Chauhan 033a71e405 webrtc examples: Use webrtc.gstreamer.net
Actually just a CNAME to webrtc.nirbheek.in for now, but it allows
replacement / hosting without my involvement, so reduces the bus
factor.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3802>
2023-02-04 13:37:02 +00:00

309 lines
11 KiB
C#

using System;
using static System.Diagnostics.Debug;
using Gst;
using WebSocketSharp;
using Gst.WebRTC;
using Newtonsoft.Json;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Gst.Sdp;
using System.Text;
using GLib;
namespace GstWebRTCDemo
{
class WebRtcClient : IDisposable
{
const string SERVER = "wss://127.0.0.1:8443";
const string PIPELINE_DESC = @"webrtcbin name=sendrecv bundle-policy=max-bundle
videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay !
queue ! application/x-rtp,media=video,encoding-name=VP8,payload=97 ! sendrecv.
audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay !
queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 ! sendrecv.";
readonly int _id;
readonly int _peerId;
readonly string _server;
readonly WebSocket _conn;
Pipeline pipe;
Element webrtc;
bool terminate;
public WebRtcClient(int id, int peerId, string server = SERVER)
{
_id = id;
_peerId = peerId;
_server = server;
_conn = new WebSocket(_server);
_conn.SslConfiguration.ServerCertificateValidationCallback = validatCert;
_conn.OnOpen += OnOpen;
_conn.OnError += OnError;
_conn.OnMessage += OnMessage;
_conn.OnClose += OnClose;
pipe = (Pipeline)Parse.Launch(PIPELINE_DESC);
}
bool validatCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
}
public void Connect()
{
_conn.ConnectAsync();
}
void SetupCall()
{
_conn.Send($"SESSION {_peerId}");
}
void OnClose(object sender, CloseEventArgs e)
{
Console.WriteLine("Closed: " + e.Reason);
terminate = true;
}
void OnError(object sender, ErrorEventArgs e)
{
Console.WriteLine("Error " + e.Message);
terminate = true;
}
void OnOpen(object sender, System.EventArgs e)
{
var ws = sender as WebSocket;
ws.SendAsync($"HELLO {_id}", (b) => Console.WriteLine($"Opened {b}"));
}
void OnMessage(object sender, MessageEventArgs args)
{
var msg = args.Data;
switch (msg)
{
case "HELLO":
SetupCall();
break;
case "SESSION_OK":
StartPipeline();
break;
default:
if (msg.StartsWith("ERROR")) {
Console.WriteLine(msg);
terminate = true;
} else {
HandleSdp(msg);
}
break;
}
}
void StartPipeline()
{
webrtc = pipe.GetByName("sendrecv");
Assert(webrtc != null);
webrtc.Connect("on-negotiation-needed", OnNegotiationNeeded);
webrtc.Connect("on-ice-candidate", OnIceCandidate);
webrtc.Connect("pad-added", OnIncomingStream);
pipe.SetState(State.Playing);
Console.WriteLine("Playing");
}
#region Webrtc signal handlers
#region Incoming stream
void OnIncomingStream(object o, GLib.SignalArgs args)
{
var pad = args.Args[0] as Pad;
if (pad.Direction != PadDirection.Src)
return;
var decodebin = ElementFactory.Make("decodebin");
decodebin.Connect("pad-added", OnIncomingDecodebinStream);
pipe.Add(decodebin);
decodebin.SyncStateWithParent();
webrtc.Link(decodebin);
}
void OnIncomingDecodebinStream(object o, SignalArgs args)
{
var pad = (Pad)args.Args[0];
if (!pad.HasCurrentCaps)
{
Console.WriteLine($"{pad.Name} has no caps, ignoring");
return;
}
var caps = pad.CurrentCaps;
Assert(!caps.IsEmpty);
Structure s = caps[0];
var name = s.Name;
if (name.StartsWith("video"))
{
var q = ElementFactory.Make("queue");
var conv = ElementFactory.Make("videoconvert");
var sink = ElementFactory.Make("autovideosink");
pipe.Add(q, conv, sink);
pipe.SyncChildrenStates();
pad.Link(q.GetStaticPad("sink"));
Element.Link(q, conv, sink);
}
else if (name.StartsWith("audio"))
{
var q = ElementFactory.Make("queue");
var conv = ElementFactory.Make("audioconvert");
var resample = ElementFactory.Make("audioresample");
var sink = ElementFactory.Make("autoaudiosink");
pipe.Add(q, conv, resample, sink);
pipe.SyncChildrenStates();
pad.Link(q.GetStaticPad("sink"));
Element.Link(q, conv, resample, sink);
}
}
#endregion
void OnIceCandidate(object o, GLib.SignalArgs args)
{
var index = (uint)args.Args[0];
var cand = (string)args.Args[1];
var obj = new { ice = new { sdpMLineIndex = index, candidate = cand } };
var iceMsg = JsonConvert.SerializeObject(obj);
_conn.SendAsync(iceMsg, (b) => { } );
}
void OnNegotiationNeeded(object o, GLib.SignalArgs args)
{
var webRtc = o as Element;
Assert(webRtc != null, "not a webrtc object");
Promise promise = new Promise(OnOfferCreated, webrtc.Handle, null); // webRtc.Handle, null);
Structure structure = new Structure("struct");
webrtc.Emit("create-offer", structure, promise);
}
void OnOfferCreated(Promise promise)
{
promise.Wait();
var reply = promise.RetrieveReply();
var gval = reply.GetValue("offer");
WebRTCSessionDescription offer = (WebRTCSessionDescription)gval.Val;
promise = new Promise();
webrtc.Emit("set-local-description", offer, promise);
promise.Interrupt();
SendSdpOffer(offer) ;
}
#endregion
void SendSdpOffer(WebRTCSessionDescription offer)
{
var text = offer.Sdp.AsText();
var obj = new { sdp = new { type = "offer", sdp = text } };
var json = JsonConvert.SerializeObject(obj);
Console.Write(json);
_conn.SendAsync(json, (b) => Console.WriteLine($"Send offer completed {b}"));
}
void HandleSdp(string message)
{
var msg = JsonConvert.DeserializeObject<dynamic>(message);
if (msg.sdp != null)
{
var sdp = msg.sdp;
if (sdp.type != null && sdp.type != "answer")
{
throw new Exception("Not an answer");
}
string sdpAns = sdp.sdp;
Console.WriteLine($"received answer:\n{sdpAns}");
SDPMessage.New(out SDPMessage sdpMsg);
SDPMessage.ParseBuffer(ASCIIEncoding.Default.GetBytes(sdpAns), (uint)sdpAns.Length, sdpMsg);
var answer = WebRTCSessionDescription.New(WebRTCSDPType.Answer, sdpMsg);
var promise = new Promise();
webrtc.Emit("set-remote-description", answer, promise);
}
else if (msg.ice != null)
{
var ice = msg.ice;
string candidate = ice.candidate;
uint sdpMLineIndex = ice.sdpMLineIndex;
webrtc.Emit("add-ice-candidate", sdpMLineIndex, candidate);
}
}
public void Run()
{
// Wait until error, EOS or State Change
var bus = pipe.Bus;
do {
var msg = bus.TimedPopFiltered (Gst.Constants.SECOND, MessageType.Error | MessageType.Eos | MessageType.StateChanged);
// Parse message
if (msg != null) {
switch (msg.Type) {
case MessageType.Error:
string debug;
GLib.GException exc;
msg.ParseError (out exc, out debug);
Console.WriteLine ("Error received from element {0}: {1}", msg.Src.Name, exc.Message);
Console.WriteLine ("Debugging information: {0}", debug != null ? debug : "none");
terminate = true;
break;
case MessageType.Eos:
Console.WriteLine ("End-Of-Stream reached.\n");
terminate = true;
break;
case MessageType.StateChanged:
// We are only interested in state-changed messages from the pipeline
if (msg.Src == pipe) {
State oldState, newState, pendingState;
msg.ParseStateChanged (out oldState, out newState, out pendingState);
Console.WriteLine ("Pipeline state changed from {0} to {1}:",
Element.StateGetName (oldState), Element.StateGetName (newState));
}
break;
default:
// We should not reach here because we only asked for ERRORs, EOS and STATE_CHANGED
Console.WriteLine ("Unexpected message received.");
break;
}
}
} while (!terminate);
}
public void Dispose()
{
((IDisposable)_conn).Dispose();
pipe.SetState(State.Null);
pipe.Dispose();
}
}
static class WebRtcSendRcv
{
const string SERVER = "wss://webrtc.gstreamer.net:8443";
static Random random = new Random();
public static void Main(string[] args)
{
// Initialize GStreamer
Gst.Application.Init (ref args);
if (args.Length == 0)
throw new Exception("need peerId");
int peerId = Int32.Parse(args[0]);
var server = (args.Length > 1) ? args[1] : SERVER;
var ourId = random.Next(100, 10000);
Console.WriteLine($"PeerId:{peerId} OurId:{ourId} ");
var c = new WebRtcClient(ourId, peerId, server);
c.Connect();
c.Run();
c.Dispose();
}
}
}