diff options
| author | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2020-03-07 13:40:48 +0200 |
|---|---|---|
| committer | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2020-03-07 13:40:48 +0200 |
| commit | 9d18d9ec9658af3716814625b608d58ad3daa14c (patch) | |
| tree | b5b586e6eaf7c9d4df4cbac120e4c6b4c01b1265 /Software/Visual_Studio/Tango.WebRTC/WebRtcClient.cs | |
| parent | 574f075256fbadd2dadb769c5aff5ebfd6dedd56 (diff) | |
| download | Tango-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.cs | 537 |
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 + } +} |
