This repository has been archived by the owner on Mar 22, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 283
/
PeerConnection.cs
1293 lines (1121 loc) · 63.8 KB
/
PeerConnection.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
[assembly:InternalsVisibleTo("Microsoft.MixedReality.WebRTC.Tests")]
namespace Microsoft.MixedReality.WebRTC
{
/// <summary>
/// Attribute to decorate managed delegates used as native callbacks (reverse P/Invoke).
/// Required by Mono in Ahead-Of-Time (AOT) compiling, and Unity with the IL2CPP backend.
/// </summary>
///
/// This attribute is required by Mono AOT and Unity IL2CPP, but not by .NET Core or Framework.
/// The implementation was copied from the Mono source code (https://github.com/mono/mono).
/// The type argument does not seem to be used anywhere in the code, and a stub implementation
/// like this seems to be enough for IL2CPP to be able to marshal the delegate (untested on Mono).
[AttributeUsage(AttributeTargets.Method)]
sealed class MonoPInvokeCallbackAttribute : Attribute
{
public MonoPInvokeCallbackAttribute(Type t) { }
}
/// <summary>
/// The WebRTC peer connection object is the entry point to using WebRTC.
/// </summary>
public class PeerConnection : IDisposable
{
//public delegate void DataChannelMessageDelegate(byte[] data);
//public delegate void DataChannelBufferingDelegate(ulong previous, ulong current, ulong limit);
//public delegate void DataChannelStateDelegate(WebRTCDataChannel.State state);
public delegate void LocalSdpReadyToSendDelegate(string type, string sdp);
public delegate void IceCandidateReadytoSendDelegate(string candidate, int sdpMlineindex, string sdpMid);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void VideoCaptureDeviceEnumCallback(string id, string name);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void VideoCaptureDeviceEnumCompletedCallback();
/// <summary>
/// Signaler implementation used by this peer connection, as specified in the constructor.
/// </summary>
public ISignaler Signaler { get; }
/// <summary>
/// Identifier of the remote peer for signaling. This must be sent before the connection starts
/// to send messages via the <see cref="Signaler"/>.
/// </summary>
public string RemotePeerId;
/// <summary>
/// List of TURN and/or STUN servers to use for NAT bypass, in order of preference.
/// </summary>
public List<string> Servers = new List<string>();
/// <summary>
/// Optional TURN server username.
/// </summary>
public string UserName = string.Empty;
/// <summary>
/// Optional TURN server credentials.
/// </summary>
public string Credentials = string.Empty;
/// <summary>
/// Name of the preferred audio codec, or empty to let WebRTC decide.
/// See https://en.wikipedia.org/wiki/RTP_audio_video_profile for the standard SDP names.
/// </summary>
public string PreferredAudioCodec = string.Empty;
/// <summary>
/// Name of the preferred video codec, or empty to let WebRTC decide.
/// See https://en.wikipedia.org/wiki/RTP_audio_video_profile for the standard SDP names.
/// </summary>
public string PreferredVideoCodec = string.Empty;
/// <summary>
/// Boolean property indicating whether the peer connection has been initialized.
/// </summary>
public bool Initialized
{
get
{
lock (_openCloseLock)
{
return (_nativePeerhandle != IntPtr.Zero);
}
}
}
public bool IsConnected { get; private set; } = false;
/// <summary>
/// Event fired when a connection is established.
/// </summary>
public event Action Connected;
/// <summary>
/// Event that occurs when a local SDP message is ready to be transmitted.
/// </summary>
public event LocalSdpReadyToSendDelegate LocalSdpReadytoSend;
/// <summary>
/// Event that occurs when a local ICE candidate is ready to be transmitted.
/// </summary>
public event IceCandidateReadytoSendDelegate IceCandidateReadytoSend;
/// <summary>
/// Event that occurs when a renegotiation of the session is needed.
/// This generally occurs as a result of adding or removing tracks,
/// and the user should call <see cref="CreateOffer"/> to actually
/// start a renegotiation.
/// </summary>
public event Action RenegotiationNeeded;
/// <summary>
/// Event that occurs when a remote track is added to the current connection.
/// </summary>
public event Action TrackAdded;
/// <summary>
/// Event that occurs when a remote track is removed from the current connection.
/// </summary>
public event Action TrackRemoved;
/// <summary>
/// Event that occurs when a video frame from a local track has been
/// produced locally and is available for render.
/// </summary>
public event I420VideoFrameDelegate I420LocalVideoFrameReady;
/// <summary>
/// Event that occurs when a video frame from a remote peer has been
/// received and is available for render.
/// </summary>
public event I420VideoFrameDelegate I420RemoteVideoFrameReady;
/// <summary>
/// Event that occurs when a video frame from a local track has been
/// produced locally and is available for render.
/// </summary>
public event ARGBVideoFrameDelegate ARGBLocalVideoFrameReady;
/// <summary>
/// Event that occurs when a video frame from a remote peer has been
/// received and is available for render.
/// </summary>
public event ARGBVideoFrameDelegate ARGBRemoteVideoFrameReady;
/// <summary>
/// Static wrappers for native callbacks to invoke instance methods on objects.
/// </summary>
private static class CallbacksWrappers
{
private delegate void VideoCaptureDeviceEnumDelegate(string id, string name, IntPtr handle);
private delegate void VideoCaptureDeviceEnumCompletedDelegate(IntPtr handle);
private delegate void ConnectedDelegate(IntPtr peer);
private delegate void LocalSdpReadytoSendDelegate(IntPtr peer, string type, string sdp);
private delegate void IceCandidateReadytoSendDelegate(IntPtr peer, string candidate, int sdpMlineindex, string sdpMid);
private delegate void RenegotiationNeededDelegate(IntPtr peer);
private delegate void TrackAddedDelegate(IntPtr peer);
private delegate void TrackRemovedDelegate(IntPtr peer);
private delegate void DataChannelMessageDelegate(IntPtr peer, IntPtr data, ulong size);
private delegate void DataChannelBufferingDelegate(IntPtr peer, ulong previous, ulong current, ulong limit);
private delegate void DataChannelStateDelegate(IntPtr peer, int state, int id);
public class EnumVideoCaptureWrapper
{
public VideoCaptureDeviceEnumCallback callback;
public VideoCaptureDeviceEnumCompletedCallback completedCallback;
}
[MonoPInvokeCallback(typeof(VideoCaptureDeviceEnumDelegate))]
public static void VideoCaptureDeviceEnumCallback(string id, string name, IntPtr userData)
{
var handle = GCHandle.FromIntPtr(userData);
var wrapper = (handle.Target as EnumVideoCaptureWrapper);
wrapper.callback(id, name); // this is mandatory, never null
}
[MonoPInvokeCallback(typeof(VideoCaptureDeviceEnumCompletedDelegate))]
public static void VideoCaptureDeviceEnumCompletedCallback(IntPtr userData)
{
var handle = GCHandle.FromIntPtr(userData);
var wrapper = (handle.Target as EnumVideoCaptureWrapper);
wrapper.completedCallback?.Invoke(); // this is optional, allows to be null
}
/// <summary>
/// Utility to lock all delegates registered with the native plugin and prevent their garbage collection.
/// </summary>
/// <remarks>
/// The delegate don't need to be pinned, just referenced to prevent garbage collection.
/// So referencing them from this class is enough to keep them alive and usable.
/// </remarks>
public class PeerCallbackArgs
{
public PeerConnection Peer;
public NativeMethods.PeerConnectionConnectedCallback ConnectedCallback;
public NativeMethods.PeerConnectionLocalSdpReadytoSendCallback LocalSdpReadytoSendCallback;
public NativeMethods.PeerConnectionIceCandidateReadytoSendCallback IceCandidateReadytoSendCallback;
public NativeMethods.PeerConnectionRenegotiationNeededCallback RenegotiationNeededCallback;
public NativeMethods.PeerConnectionTrackAddedCallback TrackAddedCallback;
public NativeMethods.PeerConnectionTrackRemovedCallback TrackRemovedCallback;
public NativeMethods.PeerConnectionI420VideoFrameCallback I420LocalVideoFrameCallback;
public NativeMethods.PeerConnectionI420VideoFrameCallback I420RemoteVideoFrameCallback;
public NativeMethods.PeerConnectionARGBVideoFrameCallback ARGBLocalVideoFrameCallback;
public NativeMethods.PeerConnectionARGBVideoFrameCallback ARGBRemoteVideoFrameCallback;
}
public class DataChannelCallbackArgs
{
public PeerConnection Peer;
public DataChannel DataChannel;
public NativeMethods.PeerConnectionDataChannelMessageCallback MessageCallback;
public NativeMethods.PeerConnectionDataChannelBufferingCallback BufferingCallback;
public NativeMethods.PeerConnectionDataChannelStateCallback StateCallback;
public static DataChannelCallbackArgs FromIntPtr(IntPtr userData)
{
var handle = GCHandle.FromIntPtr(userData);
return (handle.Target as DataChannelCallbackArgs);
}
}
[MonoPInvokeCallback(typeof(ConnectedDelegate))]
public static void ConnectedCallback(IntPtr userData)
{
var peer = FromIntPtr(userData);
peer.IsConnected = true;
peer.Connected?.Invoke();
}
[MonoPInvokeCallback(typeof(LocalSdpReadytoSendDelegate))]
public static void LocalSdpReadytoSendCallback(IntPtr userData, string type, string sdp)
{
var peer = FromIntPtr(userData);
peer.OnLocalSdpReadytoSend(type, sdp);
}
[MonoPInvokeCallback(typeof(IceCandidateReadytoSendDelegate))]
public static void IceCandidateReadytoSendCallback(IntPtr userData, string candidate, int sdpMlineindex, string sdpMid)
{
var peer = FromIntPtr(userData);
peer.OnIceCandidateReadytoSend(candidate, sdpMlineindex, sdpMid);
}
[MonoPInvokeCallback(typeof(RenegotiationNeededDelegate))]
public static void RenegotiationNeededCallback(IntPtr userData)
{
var peer = FromIntPtr(userData);
peer.RenegotiationNeeded?.Invoke();
}
[MonoPInvokeCallback(typeof(TrackAddedDelegate))]
public static void TrackAddedCallback(IntPtr userData)
{
var peer = FromIntPtr(userData);
peer.TrackAdded?.Invoke();
}
[MonoPInvokeCallback(typeof(TrackRemovedDelegate))]
public static void TrackRemovedCallback(IntPtr userData)
{
var peer = FromIntPtr(userData);
peer.TrackRemoved?.Invoke();
}
[MonoPInvokeCallback(typeof(I420VideoFrameDelegate))]
public static void I420LocalVideoFrameCallback(IntPtr userData,
IntPtr dataY, IntPtr dataU, IntPtr dataV, IntPtr dataA,
int strideY, int strideU, int strideV, int strideA,
int width, int height)
{
var peer = FromIntPtr(userData);
var frame = new I420AVideoFrame()
{
width = (uint)width,
height = (uint)height,
dataY = dataY,
dataU = dataU,
dataV = dataV,
dataA = dataA,
strideY = strideY,
strideU = strideU,
strideV = strideV,
strideA = strideA
};
peer.I420LocalVideoFrameReady?.Invoke(frame);
}
[MonoPInvokeCallback(typeof(I420VideoFrameDelegate))]
public static void I420RemoteVideoFrameCallback(IntPtr userData,
IntPtr dataY, IntPtr dataU, IntPtr dataV, IntPtr dataA,
int strideY, int strideU, int strideV, int strideA,
int width, int height)
{
var peer = FromIntPtr(userData);
var frame = new I420AVideoFrame()
{
width = (uint)width,
height = (uint)height,
dataY = dataY,
dataU = dataU,
dataV = dataV,
dataA = dataA,
strideY = strideY,
strideU = strideU,
strideV = strideV,
strideA = strideA
};
peer.I420RemoteVideoFrameReady?.Invoke(frame);
}
[MonoPInvokeCallback(typeof(ARGBVideoFrameDelegate))]
public static void ARGBLocalVideoFrameCallback(IntPtr userData,
IntPtr data, int stride, int width, int height)
{
var peer = FromIntPtr(userData);
var frame = new ARGBVideoFrame()
{
width = (uint)width,
height = (uint)height,
data = data,
stride = stride
};
peer.ARGBLocalVideoFrameReady?.Invoke(frame);
}
[MonoPInvokeCallback(typeof(ARGBVideoFrameDelegate))]
public static void ARGBRemoteVideoFrameCallback(IntPtr userData,
IntPtr data, int stride, int width, int height)
{
var peer = FromIntPtr(userData);
var frame = new ARGBVideoFrame()
{
width = (uint)width,
height = (uint)height,
data = data,
stride = stride
};
peer.ARGBRemoteVideoFrameReady?.Invoke(frame);
}
[MonoPInvokeCallback(typeof(DataChannelMessageDelegate))]
public static void DataChannelMessageCallback(IntPtr userData, IntPtr data, ulong size)
{
var args = DataChannelCallbackArgs.FromIntPtr(userData);
args.DataChannel.OnMessageReceived(data, size);
}
[MonoPInvokeCallback(typeof(DataChannelBufferingDelegate))]
public static void DataChannelBufferingCallback(IntPtr userData, ulong previous, ulong current, ulong limit)
{
var args = DataChannelCallbackArgs.FromIntPtr(userData);
args.DataChannel.OnBufferingChanged(previous, current, limit);
}
[MonoPInvokeCallback(typeof(DataChannelStateDelegate))]
public static void DataChannelStateCallback(IntPtr userData, int state, int id)
{
var args = DataChannelCallbackArgs.FromIntPtr(userData);
args.DataChannel.OnStateChanged(state, id);
}
}
/// <summary>
/// GCHandle to self for the various native callbacks.
/// This also acts as a marker of a connection created or in the process of being created.
/// </summary>
private GCHandle _selfHandle;
/// <summary>
/// Handle to the native PeerConnection object.
/// </summary>
/// <remarks>
/// In native land this is a <code>Microsoft::MixedReality::WebRTC::PeerConnectionHandle</code>.
/// </remarks>
private IntPtr _nativePeerhandle = IntPtr.Zero;
/// <summary>
/// Initialization task returned by <see cref="InitializeAsync"/>.
/// </summary>
private Task _initTask = null;
private CancellationTokenSource _initCTS = new CancellationTokenSource();
/// <summary>
/// Boolean to indicate if <see cref="Close"/> has been called and is waiting for a pending
/// initializing task <see cref="_initTask"/> to complete or cancel.
/// </summary>
private bool _isClosing = false;
/// <summary>
/// Lock for asynchronous opening and closing of the connection, protecting
/// changes to <see cref="_nativePeerhandle"/>, <see cref="_selfHandle"/>,
/// <see cref="_initTask"/>, and <see cref="_isClosing"/>.
/// </summary>
private object _openCloseLock = new object();
private CallbacksWrappers.PeerCallbackArgs _peerCallbackArgs;
#region Initializing and shutdown
/// <summary>
/// Construct an uninitialized peer connection object which will delegate to the given
/// <see cref="ISignaler"/> implementation for its WebRTC signaling needs.
/// </summary>
/// <param name="signaler">The signaling implementation to use.</param>
public PeerConnection(ISignaler signaler)
{
Signaler = signaler;
Signaler.OnMessage += Signaler_OnMessage;
}
private void Signaler_OnMessage(SignalerMessage message)
{
switch (message.MessageType)
{
case SignalerMessage.WireMessageType.Offer:
SetRemoteDescription("offer", message.Data);
// If we get an offer, we immediately send an answer back
CreateAnswer();
break;
case SignalerMessage.WireMessageType.Answer:
SetRemoteDescription("answer", message.Data);
break;
case SignalerMessage.WireMessageType.Ice:
// TODO - This is NodeDSS-specific
// this "parts" protocol is defined above, in OnIceCandiateReadyToSend listener
var parts = message.Data.Split(new string[] { message.IceDataSeparator }, StringSplitOptions.RemoveEmptyEntries);
// Note the inverted arguments; candidate is last here, but first in OnIceCandiateReadyToSend
AddIceCandidate(parts[2], int.Parse(parts[1]), parts[0]);
break;
default:
throw new InvalidOperationException($"Unhandled signaler message type '{message.MessageType}'");
}
}
/// <summary>
/// Initialize the current peer connection object asynchronously.
/// </summary>
/// <param name="token">Optional cancellation token for the initialize task. This is only used if
/// the singleton task was created by this call, and not a prior call.</param>
/// <returns>The singleton task used to initialize the underlying native peer connection.</returns>
/// <remarks>This method is multi-thread safe, and will always return the same task object
/// from the first call to it until the peer connection object is deinitialized. This allows
/// multiple callers to all execute some action following the initialization, without the need
/// to force a single caller and to synchronize with it.</remarks>
public Task InitializeAsync(CancellationToken token = default)
{
lock (_openCloseLock)
{
// If Close() is waiting for _initTask to finish, do not return it.
if (_isClosing)
{
throw new OperationCanceledException("A closing operation is pending.");
}
// If _initTask has already been created, return it.
if (_initTask != null)
{
return _initTask;
}
// Allocate a GC handle to self for static P/Invoke callbacks to be able to call
// back into methods of this peer connection object. The handle is released when
// the peer connection is closed, to allow this object to be destroyed.
Debug.Assert(!_selfHandle.IsAllocated);
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
// Create and lock in memory delegates for all the static callback wrappers (see below).
// This avoids delegates being garbage-collected, since the P/Invoke mechanism by itself
// does not guarantee their lifetime.
_peerCallbackArgs = new CallbacksWrappers.PeerCallbackArgs()
{
Peer = this,
ConnectedCallback = CallbacksWrappers.ConnectedCallback,
LocalSdpReadytoSendCallback = CallbacksWrappers.LocalSdpReadytoSendCallback,
IceCandidateReadytoSendCallback = CallbacksWrappers.IceCandidateReadytoSendCallback,
RenegotiationNeededCallback = CallbacksWrappers.RenegotiationNeededCallback,
TrackAddedCallback = CallbacksWrappers.TrackAddedCallback,
TrackRemovedCallback = CallbacksWrappers.TrackRemovedCallback,
I420LocalVideoFrameCallback = CallbacksWrappers.I420LocalVideoFrameCallback,
I420RemoteVideoFrameCallback = CallbacksWrappers.I420RemoteVideoFrameCallback,
ARGBLocalVideoFrameCallback = CallbacksWrappers.ARGBLocalVideoFrameCallback,
ARGBRemoteVideoFrameCallback = CallbacksWrappers.ARGBRemoteVideoFrameCallback
};
// Cache values in local variables before starting async task, to avoid any
// subsequent external change from affecting that task.
var servers = Servers;
var username = UserName;
var credentials = Credentials;
if (servers == null)
{
throw new ArgumentNullException("Servers");
}
if (servers.Count == 0)
{
throw new ArgumentException("Servers list is empty");
}
if (username == null)
{
throw new ArgumentNullException("UserName");
}
if (credentials == null)
{
throw new ArgumentNullException("Credentials");
}
// On UWP PeerConnectionCreate() fails on main UI thread, so always initialize the native peer
// connection asynchronously from a background worker thread.
//using (var cancelOrCloseToken = CancellationTokenSource.CreateLinkedTokenSource(_initCTS.Token, token))
//{
_initTask = Task.Run(() =>
{
token.ThrowIfCancellationRequested();
var serversArray = servers.ToArray();
IntPtr nativeHandle = NativeMethods.PeerConnectionCreate(serversArray, serversArray.Length, username, credentials, video: false);
lock (_openCloseLock)
{
// The connection may have been aborted while being created, either via the
// cancellation token, or by calling Close() after the synchronous codepath
// above but before this task had a chance to run in the background.
if (token.IsCancellationRequested)
{
// Cancelled by token
NativeMethods.PeerConnectionClose(ref nativeHandle);
throw new OperationCanceledException(token);
}
if (!_selfHandle.IsAllocated)
{
// Cancelled by calling Close()
NativeMethods.PeerConnectionClose(ref nativeHandle);
throw new OperationCanceledException();
}
_nativePeerhandle = nativeHandle;
// Register all trampoline callbacks. Note that even passing a static managed method
// for the callback is not safe, because the compiler implicitly creates a delegate
// object (a static method is not a delegate itself; it can be wrapped inside one),
// and that delegate object will be garbage collected immediately at the end of this
// block. Instead, a delegate needs to be explicitly created and locked in memory.
// Since the current PeerConnection instance is already locked via _selfHandle,
// and it references all delegates via _peerCallbackArgs, those also can't be GC'd.
var self = GCHandle.ToIntPtr(_selfHandle);
NativeMethods.PeerConnectionRegisterConnectedCallback(
_nativePeerhandle, _peerCallbackArgs.ConnectedCallback, self);
NativeMethods.PeerConnectionRegisterLocalSdpReadytoSendCallback(
_nativePeerhandle, _peerCallbackArgs.LocalSdpReadytoSendCallback, self);
NativeMethods.PeerConnectionRegisterIceCandidateReadytoSendCallback(
_nativePeerhandle, _peerCallbackArgs.IceCandidateReadytoSendCallback, self);
NativeMethods.PeerConnectionRegisterRenegotiationNeededCallback(
_nativePeerhandle, _peerCallbackArgs.RenegotiationNeededCallback, self);
NativeMethods.PeerConnectionRegisterTrackAddedCallback(
_nativePeerhandle, _peerCallbackArgs.TrackAddedCallback, self);
NativeMethods.PeerConnectionRegisterTrackRemovedCallback(
_nativePeerhandle, _peerCallbackArgs.TrackRemovedCallback, self);
NativeMethods.PeerConnectionRegisterI420LocalVideoFrameCallback(
_nativePeerhandle, _peerCallbackArgs.I420LocalVideoFrameCallback, self);
NativeMethods.PeerConnectionRegisterI420RemoteVideoFrameCallback(
_nativePeerhandle, _peerCallbackArgs.I420RemoteVideoFrameCallback, self);
//NativeMethods.PeerConnectionRegisterARGBLocalVideoFrameCallback(
// _nativePeerhandle, _peerCallbackArgs.ARGBLocalVideoFrameCallback, self);
//NativeMethods.PeerConnectionRegisterARGBRemoteVideoFrameCallback(
// _nativePeerhandle, _peerCallbackArgs.ARGBRemoteVideoFrameCallback, self);
}
}, token);
return _initTask;
}
}
/// <summary>
/// Close the peer connection and destroy the underlying native resources.
/// </summary>
public void Close()
{
lock (_openCloseLock)
{
// If the connection is not initialized, return immediately.
if (_initTask == null)
{
return;
}
// Indicate to InitializeAsync() that it should stop returning _initTask,
// as it is about to become invalid.
_isClosing = true;
}
// Wait for any pending initializing to finish.
// This must be outside of the lock because the initialization task will
// eventually need to acquire the lock to complete.
_initTask.Wait();
lock (_openCloseLock)
{
_initTask = null;
// This happens on connected connection only
if (_nativePeerhandle != IntPtr.Zero)
{
// Un-register all static trampoline callbacks
NativeMethods.PeerConnectionRegisterConnectedCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterLocalSdpReadytoSendCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterIceCandidateReadytoSendCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterRenegotiationNeededCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterTrackAddedCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterTrackRemovedCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterI420LocalVideoFrameCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterI420RemoteVideoFrameCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterARGBLocalVideoFrameCallback(_nativePeerhandle, null, IntPtr.Zero);
NativeMethods.PeerConnectionRegisterARGBRemoteVideoFrameCallback(_nativePeerhandle, null, IntPtr.Zero);
// Close the native WebRTC peer connection and destroy the object
NativeMethods.PeerConnectionClose(ref _nativePeerhandle);
_nativePeerhandle = IntPtr.Zero;
}
// This happens on connected or connecting connection
if (_selfHandle.IsAllocated)
{
_peerCallbackArgs = null;
_selfHandle.Free();
}
_isClosing = false;
IsConnected = false;
}
}
/// <summary>
/// Dispose of native resources by closing the peer connection.
/// </summary>
/// <remarks>This is equivalent to <see cref="Close"/>.</remarks>
public void Dispose()
{
Close();
}
#endregion
#region Local audio and video tracks
/// <summary>
/// Add to the current connection a video track from a local video capture device (webcam).
/// </summary>
/// <param name="device">Optional device to use, defaults to first available one.</param>
/// <returns>Asynchronous task completed once the device is capturing and the track is added.</returns>
/// <remarks>
/// On UWP this requires the "webcam" capability.
/// See <see href="https://docs.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations"/>
/// for more details.
/// </remarks>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public Task AddLocalVideoTrackAsync(VideoCaptureDevice device = default(VideoCaptureDevice), bool enableMrc = false)
{
ThrowIfConnectionNotOpen();
return Task.Run(() =>
{
// On UWP this cannot be called from the main UI thread, so always call it from
// a background worker thread.
if (!NativeMethods.PeerConnectionAddLocalVideoTrack(_nativePeerhandle, device.id, enableMrc))
{
throw new Exception();
}
});
}
/// <summary>
/// Remove from the current connection the local video track added with <see cref="AddLocalAudioTrackAsync"/>.
/// </summary>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public void RemoveLocalVideoTrack()
{
ThrowIfConnectionNotOpen();
NativeMethods.PeerConnectionRemoveLocalVideoTrack(_nativePeerhandle);
}
/// <summary>
/// Add to the current connection an audio track from a local audio capture device (microphone).
/// </summary>
/// <returns>Asynchronous task completed once the device is capturing and the track is added.</returns>
/// <remarks>
/// On UWP this requires the "microphone" capability.
/// See <see href="https://docs.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations"/>
/// for more details.
/// </remarks>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public Task AddLocalAudioTrackAsync()
{
ThrowIfConnectionNotOpen();
return Task.Run(() =>
{
// On UWP this cannot be called from the main UI thread, so always call it from
// a background worker thread.
if (!NativeMethods.PeerConnectionAddLocalAudioTrack(_nativePeerhandle))
{
throw new Exception();
}
});
}
/// <summary>
/// Remove from the current connection the local audio track added with <see cref="AddLocalAudioTrackAsync"/>.
/// </summary>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public void RemoveLocalAudioTrack()
{
ThrowIfConnectionNotOpen();
NativeMethods.PeerConnectionRemoveLocalAudioTrack(_nativePeerhandle);
}
#endregion
#region Data tracks
/// <summary>
/// Add a new out-of-band data channel with the given ID.
///
/// A data channel is negotiated out-of-band when the peers agree on an identifier by any mean
/// not known to WebRTC, and both open a data channel with that ID. The WebRTC will match the
/// incoming and outgoing pipes by this ID to allow sending and receiving through that channel.
///
/// This requires some external mechanism to agree on an available identifier not otherwise taken
/// by another channel, and also requires to ensure that both peers explicitly open that channel.
/// </summary>
/// <param name="id">The unique data channel identifier to use.</param>
/// <param name="label">The data channel name.</param>
/// <param name="ordered">Indicates whether data channel messages are ordered (see
/// <see cref="DataChannel.Ordered"/>).</param>
/// <param name="reliable">Indicates whether data channel messages are reliably delivered
/// (see <see cref="DataChannel.Reliable"/>).</param>
/// <returns>Returns a task which completes once the data channel is created.</returns>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
/// <exception cref="InvalidOperationException">SCTP not negotiated.</exception>
/// <exception cref="ArgumentOutOfRangeException">Invalid data channel ID, must be in [0:65535].</exception>
public async Task<DataChannel> AddDataChannelAsync(ushort id, string label, bool ordered, bool reliable)
{
if (id < 0)
{
throw new ArgumentOutOfRangeException("id", id, "Data channel ID must be greater than or equal to zero.");
}
return await AddDataChannelAsyncImpl(id, label, ordered, reliable);
}
/// <summary>
/// Add a new in-band data channel whose ID will be determined by the implementation.
///
/// A data channel is negotiated in-band when one peer requests its creation to the WebRTC core,
/// and the implementation negotiates with the remote peer an appropriate ID by sending some
/// SDP offer message. In that case once accepted the other peer will automatically create the
/// appropriate data channel on its side with that negotiated ID, and the ID will be returned on
/// both sides to the user for information.
///
/// Compares to out-of-band messages, this requires exchanging some SDP messages, but avoids having
/// to determine a common unused ID and having to explicitly open the data channel on both sides.
/// </summary>
/// <param name="label">The data channel name.</param>
/// <param name="ordered">Indicates whether data channel messages are ordered (see
/// <see cref="DataChannel.Ordered"/>).</param>
/// <param name="reliable">Indicates whether data channel messages are reliably delivered
/// (see <see cref="DataChannel.Reliable"/>).</param>
/// <returns>Returns a task which completes once the data channel is created.</returns>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
/// <exception cref="InvalidOperationException">SCTP not negotiated.</exception>
/// <exception cref="ArgumentOutOfRangeException">Invalid data channel ID, must be in [0:65535].</exception>
public async Task<DataChannel> AddDataChannelAsync(string label, bool ordered, bool reliable)
{
return await AddDataChannelAsyncImpl(-1, label, ordered, reliable);
}
/// <summary>
/// Add a new in-band or out-of-band data channel.
/// </summary>
/// <param name="id">Identifier in [0:65535] of the out-of-band data channel, or <c>-1</c> for in-band.</param>
/// <param name="label">The data channel name.</param>
/// <param name="ordered">Indicates whether data channel messages are ordered (see
/// <see cref="DataChannel.Ordered"/>).</param>
/// <param name="reliable">Indicates whether data channel messages are reliably delivered
/// (see <see cref="DataChannel.Reliable"/>).</param>
/// <returns>Returns a task which completes once the data channel is created.</returns>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
/// <exception cref="InvalidOperationException">SCTP not negotiated.</exception>
/// <exception cref="ArgumentOutOfRangeException">Invalid data channel ID, must be in [0:65535].</exception>
private async Task<DataChannel> AddDataChannelAsyncImpl(int id, string label, bool ordered, bool reliable)
{
// Preconditions
ThrowIfConnectionNotOpen();
// Create a new data channel object and its callback args
var dataChannel = new DataChannel(this, id, label, ordered, reliable);
var args = new CallbacksWrappers.DataChannelCallbackArgs()
{
Peer = this,
DataChannel = dataChannel,
MessageCallback = CallbacksWrappers.DataChannelMessageCallback,
BufferingCallback = CallbacksWrappers.DataChannelBufferingCallback,
StateCallback = CallbacksWrappers.DataChannelStateCallback
};
// Pin the args to pin the delegates
var handle = GCHandle.Alloc(args, GCHandleType.Normal);
IntPtr userData = GCHandle.ToIntPtr(handle);
// Create the native channel
return await Task.Run(() =>
{
uint res = NativeMethods.PeerConnectionAddDataChannel(_nativePeerhandle, id, label, ordered, reliable,
args.MessageCallback, userData, args.BufferingCallback, userData, args.StateCallback, userData);
if (res == 0)
{
return dataChannel;
}
handle.Free();
if (res == 0x80000301) // MRS_E_SCTP_NOT_NEGOTIATED
{
throw new InvalidOperationException("Cannot add a first data channel after the connection handshake started. Call AddDataChannelAsync() before calling CreateOffer().");
}
if (res == 0x80000302) // MRS_E_INVALID_DATA_CHANNEL_ID
{
throw new ArgumentOutOfRangeException("id", id, "Invalid ID passed to AddDataChannelAsync().");
}
throw new Exception("AddDataChannelAsync() failed.");
});
}
internal bool RemoveDataChannel(DataChannel dataChannel)
{
ThrowIfConnectionNotOpen();
return NativeMethods.PeerConnectionRemoveDataChannel(_nativePeerhandle, dataChannel.ID);
}
internal void SendDataChannelMessage(int id, byte[] message)
{
ThrowIfConnectionNotOpen();
NativeMethods.PeerConnectionSendDataChannelMessage(_nativePeerhandle, id, message, (ulong)message.LongLength);
}
#endregion
#region Signaling
/// <summary>
/// Inform the WebRTC peer connection of a newly received ICE candidate.
/// </summary>
/// <param name="sdpMid"></param>
/// <param name="sdpMlineindex"></param>
/// <param name="candidate"></param>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public void AddIceCandidate(string sdpMid, int sdpMlineindex, string candidate)
{
ThrowIfConnectionNotOpen();
NativeMethods.PeerConnectionAddIceCandidate(_nativePeerhandle, sdpMid, sdpMlineindex, candidate);
}
/// <summary>
/// Create an SDP offer message as an attempt to establish a connection.
/// </summary>
/// <returns><c>true</c> if the offer was created successfully.</returns>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public bool CreateOffer()
{
ThrowIfConnectionNotOpen();
return NativeMethods.PeerConnectionCreateOffer(_nativePeerhandle);
}
/// <summary>
/// Create an SDP answer message to a previously-received offer, to accept a connection.
/// </summary>
/// <returns><c>true</c> if the offer was created successfully.</returns>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public bool CreateAnswer()
{
ThrowIfConnectionNotOpen();
return NativeMethods.PeerConnectionCreateAnswer(_nativePeerhandle);
}
/// <summary>
/// Pass the given SDP description received from the remote peer via signaling to the
/// underlying WebRTC implementation, which will parse and use it.
///
/// This must be called by the signaler when receiving a message.
/// </summary>
/// <param name="type">The type of SDP message ("offer", "answer", "ice")</param>
/// <param name="sdp">The content of the SDP message</param>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
public void SetRemoteDescription(string type, string sdp)
{
ThrowIfConnectionNotOpen();
NativeMethods.PeerConnectionSetRemoteDescription(_nativePeerhandle, type, sdp);
}
#endregion
/// <summary>
/// Utility to throw an exception if a method is called before the underlying
/// native peer connection has been initialized.
/// </summary>
/// <exception cref="InvalidOperationException">The peer connection is not intialized.</exception>
private void ThrowIfConnectionNotOpen()
{
lock (_openCloseLock)
{
if (_nativePeerhandle == IntPtr.Zero)
{
throw new InvalidOperationException("Cannot invoke native method with invalid peer connection handle.");
}
}
}
/// <summary>
/// Helper to convert between an <see cref="IntPtr"/> holding a <see cref="GCHandle"/> to
/// a <see cref="PeerConnection"/> object and the object itself.
/// </summary>
/// <param name="userData">The <see cref="GCHandle"/> to the <see cref="PeerConnection"/> object,
/// wrapped inside an <see cref="IntPtr"/>.</param>
/// <returns>The corresponding <see cref="PeerConnection"/> object.</returns>
private static PeerConnection FromIntPtr(IntPtr userData)
{
var peerHandle = GCHandle.FromIntPtr(userData);
var peer = (peerHandle.Target as PeerConnection);
return peer;
}
/// <summary>
/// Collection of native P/Invoke static functions.
/// </summary>
private static class NativeMethods
{
#if MR_SHARING_WIN
internal const string dllPath = "Microsoft.MixedReality.WebRTC.Native.dll";
#elif MR_SHARING_ANDROID
internal const string dllPath = "Microsoft.MixedReality.WebRTC.Native.so";
#endif
#region Unmanaged delegates
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void VideoCaptureDeviceEnumCallback(string id, string name, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void VideoCaptureDeviceEnumCompletedCallback(IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionConnectedCallback(IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionLocalSdpReadytoSendCallback(IntPtr userData,
string type, string sdp);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionIceCandidateReadytoSendCallback(IntPtr userData,
string candidate, int sdpMlineindex, string sdpMid);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionRenegotiationNeededCallback(IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionTrackAddedCallback(IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionTrackRemovedCallback(IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionI420VideoFrameCallback(IntPtr userData,
IntPtr ydata, IntPtr udata, IntPtr vdata, IntPtr adata,
int ystride, int ustride, int vstride, int astride,
int frameWidth, int frameHeight);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public delegate void PeerConnectionARGBVideoFrameCallback(IntPtr userData,
IntPtr data, int stride, int frameWidth, int frameHeight);
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]