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 | |
| parent | 574f075256fbadd2dadb769c5aff5ebfd6dedd56 (diff) | |
| download | Tango-9d18d9ec9658af3716814625b608d58ad3daa14c.tar.gz Tango-9d18d9ec9658af3716814625b608d58ad3daa14c.zip | |
Added Tango.WebRTC
Diffstat (limited to 'Software/Visual_Studio/Tango.WebRTC')
10 files changed, 742 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Tango.WebRTC/Answer.cs b/Software/Visual_Studio/Tango.WebRTC/Answer.cs new file mode 100644 index 000000000..535162dca --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/Answer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class Answer + { + public String Sdp { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/DataMessageReceivedEventArgs.cs b/Software/Visual_Studio/Tango.WebRTC/DataMessageReceivedEventArgs.cs new file mode 100644 index 000000000..6faa14ccb --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/DataMessageReceivedEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class DataMessageReceivedEventArgs<T> : EventArgs + { + public T Data { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/ErrorEventArgs.cs b/Software/Visual_Studio/Tango.WebRTC/ErrorEventArgs.cs new file mode 100644 index 000000000..658468bf4 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/ErrorEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class ErrorEventArgs : EventArgs + { + public String Error { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/IceCandidate.cs b/Software/Visual_Studio/Tango.WebRTC/IceCandidate.cs new file mode 100644 index 000000000..af9aa19b5 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/IceCandidate.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class IceCandidate + { + /// <summary> + /// Gets or sets the SDP mid string. + /// </summary> + public String SdpMid { get; set; } + + /// <summary> + /// Gets or sets the SDP line index string. + /// </summary> + public int SdpMLineIndex { get; set; } + + /// <summary> + /// Gets or sets the candidate string. + /// </summary> + public String Sdp { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/NewIceCandidateEventArgs.cs b/Software/Visual_Studio/Tango.WebRTC/NewIceCandidateEventArgs.cs new file mode 100644 index 000000000..5ec1591f9 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/NewIceCandidateEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class NewIceCandidateEventArgs : EventArgs + { + public IceCandidate IceCandidate { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/Offer.cs b/Software/Visual_Studio/Tango.WebRTC/Offer.cs new file mode 100644 index 000000000..7e8f736c2 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/Offer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class Offer + { + public String Sdp { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.WebRTC/Properties/AssemblyInfo.cs b/Software/Visual_Studio/Tango.WebRTC/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b4b3f9476 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Tango.WebRTC")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Tango.WebRTC")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("09f81a12-0f77-4336-854d-9e0a74a17f9e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Software/Visual_Studio/Tango.WebRTC/Tango.WebRTC.csproj b/Software/Visual_Studio/Tango.WebRTC/Tango.WebRTC.csproj new file mode 100644 index 000000000..0d85bb902 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/Tango.WebRTC.csproj @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{09F81A12-0F77-4336-854D-9E0A74A17F9E}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Tango.WebRTC</RootNamespace> + <AssemblyName>Tango.WebRTC</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + <Deterministic>true</Deterministic> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Drawing" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="Answer.cs" /> + <Compile Include="DataMessageReceivedEventArgs.cs" /> + <Compile Include="ErrorEventArgs.cs" /> + <Compile Include="IceCandidate.cs" /> + <Compile Include="NewIceCandidateEventArgs.cs" /> + <Compile Include="Offer.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="VideoFrameReceivedEventArgs.cs" /> + <Compile Include="WebRtcClient.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\SideChains\WebRtc.NET\WebRtc.NET.vcxproj"> + <Project>{a07e6cb4-0132-4eb1-9a38-c8c057884dc2}</Project> + <Name>WebRtc.NET</Name> + </ProjectReference> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> +</Project>
\ No newline at end of file diff --git a/Software/Visual_Studio/Tango.WebRTC/VideoFrameReceivedEventArgs.cs b/Software/Visual_Studio/Tango.WebRTC/VideoFrameReceivedEventArgs.cs new file mode 100644 index 000000000..d147b89d4 --- /dev/null +++ b/Software/Visual_Studio/Tango.WebRTC/VideoFrameReceivedEventArgs.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.WebRTC +{ + public class VideoFrameReceivedEventArgs : EventArgs + { + public Bitmap Bitmap { get; set; } + } +} 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 + } +} |
