aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs
diff options
context:
space:
mode:
authorRoy Ben Shabat <Roy.mail.net@gmail.com>2020-03-07 13:40:48 +0200
committerRoy Ben Shabat <Roy.mail.net@gmail.com>2020-03-07 13:40:48 +0200
commit9d18d9ec9658af3716814625b608d58ad3daa14c (patch)
treeb5b586e6eaf7c9d4df4cbac120e4c6b4c01b1265 /Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs
parent574f075256fbadd2dadb769c5aff5ebfd6dedd56 (diff)
downloadTango-9d18d9ec9658af3716814625b608d58ad3daa14c.tar.gz
Tango-9d18d9ec9658af3716814625b608d58ad3daa14c.zip
Added Tango.WebRTC
Diffstat (limited to 'Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs')
-rw-r--r--Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs537
1 files changed, 537 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs b/Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs
new file mode 100644
index 000000000..9866395ae
--- /dev/null
+++ b/Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs
@@ -0,0 +1,537 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WebRtc.NET;
+
+namespace Tango.WebRTC
+{
+ public class WebRtcClient : IDisposable
+ {
+ private ManagedConductor _conductor;
+ private Thread _conductorThread;
+ private TurboJpegEncoder _encoder;
+ private byte[] _bgrBufflocal;
+ private byte[] _imgBuf;
+ private GCHandle _bufHandle;
+ private IntPtr _imgBufPtr = IntPtr.Zero;
+ private bool _isDisposed;
+ private Bitmap _sendFrame;
+ private TaskCompletionSource<bool> _readyCompletionSource;
+
+ #region Events
+
+ public event EventHandler<NewIceCandidateEventArgs> NewIceCandidate;
+ public event EventHandler Ready;
+ public event EventHandler<DataMessageReceivedEventArgs<String>> TextMessageReceived;
+ public event EventHandler<DataMessageReceivedEventArgs<byte[]>> BinaryMessageReceived;
+ public event EventHandler<VideoFrameReceivedEventArgs> FrameReceived;
+ public event EventHandler<ErrorEventArgs> Error;
+ public event EventHandler Disconnected;
+
+ #endregion
+
+ #region Properties
+
+ private int _frameWidth;
+ public int FrameWidth
+ {
+ get { return _frameWidth; }
+ set
+ {
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("The frame height must be set before calling Init();");
+ }
+
+ _frameWidth = value;
+ }
+ }
+
+ private int _frameHeight;
+ public int FrameHeight
+ {
+ get { return _frameHeight; }
+ set
+ {
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("The frame width must be set before calling Init();");
+ }
+
+ _frameHeight = value;
+ }
+ }
+
+ private int _frameRate;
+ public int FrameRate
+ {
+ get { return _frameRate; }
+ set
+ {
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("The frame rate must be set before calling Init();");
+ }
+
+ _frameRate = value;
+ }
+ }
+
+
+ private String _dataChannelName;
+ public String DataChannelName
+ {
+ get { return _dataChannelName; }
+ set
+ {
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("The data channel must be set before calling Init();");
+ }
+
+ _dataChannelName = value;
+ }
+ }
+
+ public bool IsInitialized { get; private set; }
+ public bool IsReady { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ public WebRtcClient()
+ {
+ FrameWidth = 640;
+ FrameHeight = 480;
+ FrameRate = 5;
+ DataChannelName = "DefaultChannelName";
+ }
+
+ public WebRtcClient(int frameWidth, int frameHeight, int frameRate) : this()
+ {
+ FrameWidth = frameWidth;
+ FrameHeight = frameHeight;
+ FrameRate = frameRate;
+ }
+
+ public WebRtcClient(int frameWidth, int frameHeight, int frameRate, String dataChannelName) : this(frameWidth, frameHeight, frameRate)
+ {
+ DataChannelName = dataChannelName;
+ }
+
+ #endregion
+
+ #region Init
+
+ public Task Init()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException("This instance was already disposed.");
+ }
+
+ if (IsInitialized)
+ {
+ throw new InvalidOperationException("This instance was already initialized.");
+ }
+
+ TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
+
+ _conductorThread = new Thread(() =>
+ {
+ Thread.Sleep(5); //Wait for function to return at least!
+
+ _conductor = new ManagedConductor();
+ _encoder = TurboJpegEncoder.CreateEncoder();
+
+ try
+ {
+ ManagedConductor.InitializeSSL();
+
+ _conductor.AddServerConfig("stun:stun.l.google.com:19302", String.Empty, String.Empty);
+ _conductor.AddServerConfig("stun:stun.anyfirewall.com:3478", String.Empty, String.Empty);
+ _conductor.AddServerConfig("stun:stun.stunprotocol.org:3478", String.Empty, String.Empty);
+
+ _conductor.SetAudio(false);
+ _conductor.SetVideoCapturer(FrameWidth, FrameHeight, FrameRate, false);
+
+ if (!_conductor.InitializePeerConnection())
+ {
+ completion.SetException(new ApplicationException("Error initializing peer connection."));
+ }
+
+ _conductor.CreateDataChannel(DataChannelName);
+ _conductor.OnIceCandidate += _conductor_OnIceCandidate;
+ _conductor.OnDataMessage += _conductor_OnDataMessage;
+ _conductor.OnDataBinaryMessage += _conductor_OnDataBinaryMessage;
+ _conductor.OnError += _conductor_OnError;
+ _conductor.OnFailure += _conductor_OnFailure;
+ _conductor.OnIceStateChanged += _conductor_OnIceStateChanged;
+
+ unsafe
+ {
+ _conductor.OnRenderRemote += _conductor_OnRenderRemote;
+ }
+
+ _conductor.ProcessMessages(1000);
+ }
+ catch (Exception ex)
+ {
+ completion.SetException(ex);
+ }
+
+ IsInitialized = true;
+
+ completion.SetResult(true);
+
+ Stopwatch watch = new Stopwatch();
+ watch.Start();
+
+ while (!_isDisposed)
+ {
+ watch.Restart();
+ _conductor.ProcessMessages(1000);
+ Thread.Sleep(Math.Max(10, (int)watch.ElapsedMilliseconds));
+ }
+
+ IsInitialized = false;
+
+ _conductor.OnIceCandidate -= _conductor_OnIceCandidate;
+ _conductor.OnDataMessage -= _conductor_OnDataMessage;
+ _conductor.OnDataBinaryMessage -= _conductor_OnDataBinaryMessage;
+ _conductor.OnError -= _conductor_OnError;
+ _conductor.OnFailure -= _conductor_OnFailure;
+ _conductor.OnIceStateChanged -= _conductor_OnIceStateChanged;
+
+ _conductor.Dispose();
+ });
+
+ _conductorThread.IsBackground = true;
+ _conductorThread.Start();
+
+ return completion.Task;
+ }
+
+ private void _conductor_OnIceStateChanged(IceConnectionStates state)
+ {
+ if (!_isDisposed)
+ {
+ if (state == IceConnectionStates.kIceConnectionConnected)
+ {
+ OnReady();
+ }
+ else if (state == IceConnectionStates.kIceConnectionFailed)
+ {
+ Disconnected?.Invoke(this, new EventArgs());
+ }
+ }
+ }
+
+ #endregion
+
+ #region WebRTC Event Handlers
+
+ private void _conductor_OnIceCandidate(string sdp_mid, int sdp_mline_index, string sdp)
+ {
+ NewIceCandidate?.Invoke(this, new NewIceCandidateEventArgs()
+ {
+ IceCandidate = new IceCandidate()
+ {
+ SdpMid = sdp_mid,
+ SdpMLineIndex = sdp_mline_index,
+ Sdp = sdp
+ }
+ });
+ }
+
+ private void _conductor_OnDataMessage(string text)
+ {
+ OnTextMessageReceived(text);
+ }
+
+ private void _conductor_OnDataBinaryMessage(byte[] data)
+ {
+ OnBinaryMessageReceived(data);
+ }
+
+ unsafe private void _conductor_OnRenderRemote(byte* frame_buffer, uint w, uint h)
+ {
+ if (_isDisposed) return;
+
+ try
+ {
+ if (_encoder.EncodeI420toBGR24(frame_buffer, w, h, ref _bgrBufflocal, true) == 0)
+ {
+ var bufHandle = GCHandle.Alloc(_bgrBufflocal, GCHandleType.Pinned);
+ var bmp = new Bitmap((int)w, (int)h, (int)w * 3, PixelFormat.Format24bppRgb, bufHandle.AddrOfPinnedObject());
+ FrameReceived?.Invoke(this, new VideoFrameReceivedEventArgs()
+ {
+ Bitmap = bmp
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error occurred while receiving the remote video frame.\n{ex.Message}");
+ }
+ }
+
+ private void _conductor_OnFailure(string error)
+ {
+ OnError(error);
+ }
+
+ private void _conductor_OnError()
+ {
+ OnError("Unspecified error.");
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public Task<Offer> CreateOffer()
+ {
+ EnsureInitialized();
+
+ TaskCompletionSource<Offer> completion = new TaskCompletionSource<Offer>();
+
+ ManagedConductor.OnCallbackSdp del = null;
+
+ bool completed = false;
+
+ del = (sdp) =>
+ {
+ if (!completed)
+ {
+ completed = true;
+ _conductor.OnSuccessOffer -= del;
+ completion.SetResult(new Offer() { Sdp = sdp });
+ }
+ };
+
+ _conductor.OnSuccessOffer += del;
+
+ Task.Factory.StartNew(async () =>
+ {
+ await Task.Delay(10000);
+
+ if (!completed)
+ {
+ completed = true;
+ completion.SetException(new TimeoutException("The offer was not created within the given time."));
+ }
+ });
+
+ _conductor.CreateOffer();
+
+ return completion.Task;
+ }
+
+ public Task<Answer> CreateAnswer(Offer offer)
+ {
+ EnsureInitialized();
+
+ TaskCompletionSource<Answer> completion = new TaskCompletionSource<Answer>();
+
+ ManagedConductor.OnCallbackSdp del = null;
+
+ bool completed = false;
+
+ del = (sdp) =>
+ {
+ if (!completed)
+ {
+ completed = true;
+ _conductor.OnSuccessAnswer -= del;
+ completion.SetResult(new Answer() { Sdp = sdp });
+ }
+ };
+
+ _conductor.OnSuccessAnswer += del;
+
+ Task.Factory.StartNew(async () =>
+ {
+ await Task.Delay(10000);
+
+ if (!completed)
+ {
+ completed = true;
+ completion.SetException(new TimeoutException("The answer was not created within the given time."));
+ }
+ });
+
+ _conductor.OnOfferRequest(offer.Sdp);
+
+ return completion.Task;
+ }
+
+ public void SetAnswer(Answer answer)
+ {
+ EnsureInitialized();
+
+ _conductor.OnOfferReply("answer", answer.Sdp);
+ }
+
+ public void SendText(String msg)
+ {
+ EnsureInitialized();
+
+ _conductor.DataChannelSendText(msg);
+ }
+
+ public void AddIceCandidate(IceCandidate ice)
+ {
+ EnsureInitialized();
+
+ Task.Factory.StartNew(() =>
+ {
+ _conductor.AddIceCandidate(ice.SdpMid, ice.SdpMLineIndex, ice.Sdp);
+ });
+ }
+
+ public unsafe void PushFrame(Bitmap bitmap)
+ {
+ if (_isDisposed) return;
+
+ EnsureInitialized();
+
+ try
+ {
+ if (_sendFrame == null)
+ {
+ _imgBuf = new byte[FrameWidth * 3 * FrameHeight];
+ _bufHandle = GCHandle.Alloc(_imgBuf, GCHandleType.Pinned);
+ _imgBufPtr = _bufHandle.AddrOfPinnedObject();
+ _sendFrame = new Bitmap(FrameWidth, FrameHeight, FrameWidth * 3, PixelFormat.Format24bppRgb, _imgBufPtr);
+ }
+
+ using (var g = Graphics.FromImage(_sendFrame))
+ {
+ g.DrawImage(bitmap, new Rectangle(0, 0, _sendFrame.Width, _sendFrame.Height));
+ }
+
+ byte* firstYuv = _conductor.VideoCapturerI420Buffer();
+
+ int yuvSize = _encoder.EncodeI420((byte*)_imgBufPtr.ToPointer(), FrameWidth, FrameHeight, (int)TJPF.TJPF_BGR, 0, true, firstYuv);
+
+ _conductor.PushFrame();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error occurred while pushing the frame.\n{ex.Message}");
+ }
+ }
+
+ public Task WaitForReady(TimeSpan? timeout = null)
+ {
+ if (!IsReady)
+ {
+ if (timeout != null)
+ {
+ Task.Factory.StartNew(async () =>
+ {
+ await Task.Delay(timeout.Value);
+
+ if (!IsReady)
+ {
+ _readyCompletionSource.SetException(new TimeoutException("The connection was not ready within the given time."));
+ _readyCompletionSource = null;
+ }
+ });
+ }
+
+ _readyCompletionSource = new TaskCompletionSource<bool>();
+ return _readyCompletionSource.Task;
+ }
+ else
+ {
+ return Task.FromResult(true);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!_isDisposed)
+ {
+ _isDisposed = true;
+
+ try
+ {
+ if (_sendFrame != null)
+ {
+ _sendFrame.Dispose();
+ }
+
+ }
+ catch { }
+
+
+ try
+ {
+ if (_bufHandle != null)
+ {
+ _bufHandle.Free();
+ }
+ }
+ catch { }
+ }
+ }
+
+ #endregion
+
+ #region Virtual Methods
+
+ protected virtual void OnTextMessageReceived(String text)
+ {
+ TextMessageReceived?.Invoke(this, new DataMessageReceivedEventArgs<string>() { Data = text });
+ }
+
+ protected virtual void OnBinaryMessageReceived(byte[] data)
+ {
+ BinaryMessageReceived?.Invoke(this, new DataMessageReceivedEventArgs<byte[]>() { Data = data });
+ }
+
+ protected virtual void OnReady()
+ {
+ if (!IsReady)
+ {
+ IsReady = true;
+ Ready?.Invoke(this, new EventArgs());
+
+ if (_readyCompletionSource != null)
+ {
+ _readyCompletionSource.SetResult(true);
+ _readyCompletionSource = null;
+ }
+ }
+ }
+
+ protected virtual void OnError(String error)
+ {
+ Error?.Invoke(this, new ErrorEventArgs() { Error = error });
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private void EnsureInitialized()
+ {
+ if (!IsInitialized)
+ {
+ throw new InvalidOperationException("Invalid operation. The instance was not initialized using Init();");
+ }
+ }
+
+ #endregion
+ }
+}