[WebRTC] Handle error when WebRTC is started (#4422)
[platform/core/csapi/tizenfx.git] / src / Tizen.Multimedia.Remoting / WebRTC / WebRTC.cs
1 /*
2  * Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved
3  *
4  * Licensed under the Apache License, Version 2.0 (the License);
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an AS IS BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 using System;
18 using System.Collections.ObjectModel;
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using System.Diagnostics;
22 using System.Threading;
23 using System.Threading.Tasks;
24 using Tizen.Applications;
25 using static Interop;
26
27 namespace Tizen.Multimedia.Remoting
28 {
29     internal static class WebRTCLog
30     {
31         internal const string Tag = "Tizen.Multimedia.WebRTC";
32     }
33
34     /// <summary>
35     /// Provides the ability to control WebRTC.
36     /// </summary>
37     /// <since_tizen> 9 </since_tizen>
38     public partial class WebRTC : IDisposable
39     {
40         private readonly WebRTCHandle _handle;
41         private List<MediaSource> _source;
42
43         /// <summary>
44         /// Initializes a new instance of the <see cref="WebRTC"/> class.
45         /// </summary>
46         /// <feature>http://tizen.org/feature/network.wifi</feature>
47         /// <feature>http://tizen.org/feature/network.telephony</feature>
48         /// <feature>http://tizen.org/feature/network.ethernet</feature>
49         /// <privilege>http://tizen.org/privilege/internet</privilege>
50         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
51         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
52         /// <since_tizen> 9 </since_tizen>
53         public WebRTC()
54         {
55             if (!Features.IsSupported(WebRTCFeatures.Wifi) &&
56                 !Features.IsSupported(WebRTCFeatures.Telephony) &&
57                 !Features.IsSupported(WebRTCFeatures.Ethernet))
58             {
59                 throw new NotSupportedException("Network features are not supported.");
60             }
61
62             NativeWebRTC.Create(out _handle).ThrowIfFailed("Failed to create webrtc");
63
64             Debug.Assert(_handle != null);
65
66             RegisterEvents();
67
68             _source = new List<MediaSource>();
69         }
70
71         internal void ValidateWebRTCState(params WebRTCState[] desiredStates)
72         {
73             Debug.Assert(desiredStates.Length > 0);
74
75             ValidateNotDisposed();
76
77             WebRTCState curState = State;
78             if (!curState.IsAnyOf(desiredStates))
79             {
80                 throw new InvalidOperationException("The WebRTC is not in a valid state. " +
81                     $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
82             }
83         }
84
85         #region Dispose support
86         private bool _disposed;
87
88         /// <summary>
89         /// Releases all resources used by the current instance.
90         /// </summary>
91         /// <since_tizen> 9 </since_tizen>
92         public void Dispose()
93         {
94             Dispose(true);
95             GC.SuppressFinalize(this);
96         }
97
98         /// <summary>
99         /// Releases the unmanaged resources used by the <see cref="WebRTC"/>.
100         /// </summary>
101         /// <param name="disposing">
102         /// true to release both managed and unmanaged resources;
103         /// false to release only unmanaged resources.
104         /// </param>
105         [EditorBrowsable(EditorBrowsableState.Never)]
106         protected virtual void Dispose(bool disposing)
107         {
108             if (_disposed || !disposing)
109                 return;
110
111             if (_handle != null)
112             {
113                 _handle.Dispose();
114                 _disposed = true;
115             }
116         }
117
118         internal void ValidateNotDisposed()
119         {
120             if (_disposed)
121             {
122                 Log.Warn(WebRTCLog.Tag, "WebRTC was disposed");
123                 throw new ObjectDisposedException(nameof(WebRTC));
124             }
125         }
126
127         internal bool IsDisposed => _disposed;
128         #endregion
129
130         /// <summary>
131         /// Starts the WebRTC.
132         /// </summary>
133         /// <remarks>
134         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/> state.<br/>
135         /// The WebRTC state will be <see cref="WebRTCState.Negotiating"/> state.<br/>
136         /// The user should check whether <see cref="State" /> is changed to <see cref="WebRTCState.Negotiating"/> state or not.
137         /// </remarks>
138         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
139         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
140         /// <see also="WebRTCState"/>
141         /// <see also="StateChanged"/>
142         /// <see also="CreateOffer"/>
143         /// <since_tizen> 9 </since_tizen>
144         public void Start()
145         {
146             ValidateWebRTCState(WebRTCState.Idle);
147
148             NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
149         }
150
151         /// <summary>
152         /// Starts the WebRTC asynchronously.
153         /// </summary>
154         /// <remarks>
155         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/> state.<br/>
156         /// The WebRTC state will be <see cref="WebRTCState.Negotiating"/> state.<br/>
157         /// This ensures that <see cref="State" /> is changed to <see cref="WebRTCState.Negotiating"/> state.
158         /// </remarks>
159         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
160         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
161         /// <see also="WebRTCState"/>
162         /// <see also="CreateOffer"/>
163         /// <since_tizen> 9 </since_tizen>
164         public async Task StartAsync()
165         {
166             ValidateWebRTCState(WebRTCState.Idle);
167
168             var tcs = new TaskCompletionSource<bool>();
169             var error = WebRTCError.ConnectionFailed;
170
171             EventHandler<WebRTCStateChangedEventArgs> stateChangedEventHandler = (s, e) =>
172             {
173                 if (e.Current == WebRTCState.Negotiating)
174                 {
175                     tcs.TrySetResult(true);
176                 }
177             };
178             EventHandler<WebRTCErrorOccurredEventArgs> errorEventHandler = (s, e) =>
179             {
180                 Log.Info(WebRTCLog.Tag, e.ToString());
181                 error = e.Error;
182                 tcs.TrySetResult(false);
183             };
184
185             try
186             {
187                 StateChanged += stateChangedEventHandler;
188                 ErrorOccurred += errorEventHandler;
189
190                 NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
191
192                 var result = await tcs.Task.ConfigureAwait(false);
193                 await Task.Yield();
194
195                 if (!result)
196                 {
197                     throw new InvalidOperationException(error.ToString());
198                 }
199             }
200             finally
201             {
202                 StateChanged -= stateChangedEventHandler;
203                 ErrorOccurred -= errorEventHandler;
204             }
205         }
206
207         /// <summary>
208         /// Stops the WebRTC.
209         /// </summary>
210         /// <remarks>
211         /// The WebRTC must be in the <see cref="WebRTCState.Negotiating"/> or <see cref="WebRTCState.Playing"/> state.<br/>
212         /// The WebRTC state will be <see cref="WebRTCState.Idle"/> state.<br/>
213         /// The user should check whether <see cref="State" /> is changed to <see cref="WebRTCState.Idle"/> state or not.
214         /// </remarks>
215         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
216         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
217         /// <since_tizen> 9 </since_tizen>
218         public void Stop()
219         {
220             ValidateWebRTCState(WebRTCState.Negotiating, WebRTCState.Playing);
221
222             NativeWebRTC.Stop(Handle).ThrowIfFailed("Failed to stop the WebRTC");
223         }
224
225         /// <summary>
226         /// Creates SDP offer asynchronously to start a new WebRTC connection to a remote peer.
227         /// </summary>
228         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/></remarks>
229         /// <returns>The SDP offer.</returns>
230         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
231         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
232         /// <seealso cref="CreateAnswerAsync()"/>
233         /// <since_tizen> 9 </since_tizen>
234         public async Task<string> CreateOfferAsync()
235         {
236             ValidateWebRTCState(WebRTCState.Negotiating);
237
238             var tcsSdpCreated = new TaskCompletionSource<string>();
239
240             NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
241             {
242                 tcsSdpCreated.TrySetResult(sdp);
243             };
244
245             string offer = null;
246             using (var cbKeeper = ObjectKeeper.Get(cb))
247             {
248                 NativeWebRTC.CreateSDPOfferAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
249                     ThrowIfFailed("Failed to create offer asynchronously");
250
251                 offer = await tcsSdpCreated.Task.ConfigureAwait(false);
252                 await Task.Yield();
253             }
254
255             return offer;
256         }
257
258         /// <summary>
259         /// Creates SDP answer asynchronously with option to an offer received from a remote peer.
260         /// </summary>
261         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/></remarks>
262         /// <returns>The SDP answer.</returns>
263         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
264         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
265         /// <seealso cref="CreateOfferAsync()"/>
266         /// <since_tizen> 9 </since_tizen>
267         public async Task<string> CreateAnswerAsync()
268         {
269             ValidateWebRTCState(WebRTCState.Negotiating);
270
271             var tcsSdpCreated = new TaskCompletionSource<string>();
272
273             NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
274             {
275                 tcsSdpCreated.TrySetResult(sdp);
276             };
277
278             string answer = null;
279             using (var cbKeeper = ObjectKeeper.Get(cb))
280             {
281                 NativeWebRTC.CreateSDPAnswerAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
282                     ThrowIfFailed("Failed to create answer asynchronously");
283
284                 answer = await tcsSdpCreated.Task.ConfigureAwait(false);
285                 await Task.Yield();
286             }
287
288             return answer;
289         }
290
291         /// <summary>
292         /// Sets the session description for a local peer.
293         /// </summary>
294         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
295         /// <param name="description">The local session description.</param>
296         /// <exception cref="ArgumentException">The description is empty string.</exception>
297         /// <exception cref="ArgumentNullException">The description is null.</exception>
298         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
299         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
300         /// <seealso cref="CreateOfferAsync()"/>
301         /// <seealso cref="CreateAnswerAsync()"/>
302         /// <since_tizen> 9 </since_tizen>
303         public void SetLocalDescription(string description)
304         {
305             ValidateWebRTCState(WebRTCState.Negotiating);
306
307             ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
308
309             NativeWebRTC.SetLocalDescription(Handle, description).ThrowIfFailed("Failed to set description.");
310         }
311
312         /// <summary>
313         /// Sets the session description of the remote peer's current offer or answer.
314         /// </summary>
315         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
316         /// <param name="description">The remote session description.</param>
317         /// <exception cref="ArgumentException">The description is empty string.</exception>
318         /// <exception cref="ArgumentNullException">The description is null.</exception>
319         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
320         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
321         /// <seealso cref="CreateOfferAsync()"/>
322         /// <seealso cref="CreateAnswerAsync()"/>
323         /// <since_tizen> 9 </since_tizen>
324         public void SetRemoteDescription(string description)
325         {
326             ValidateWebRTCState(WebRTCState.Negotiating);
327
328             ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
329
330             NativeWebRTC.SetRemoteDescription(Handle, description).ThrowIfFailed("Failed to set description.");
331         }
332
333         /// <summary>
334         /// Adds a new ICE candidate from the remote peer over its signaling channel.
335         /// </summary>
336         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
337         /// <param name="iceCandidate">The ICE candidate.</param>
338         /// <exception cref="ArgumentException">The ICE candidate is empty string.</exception>
339         /// <exception cref="ArgumentNullException">The ICE candidate is null.</exception>
340         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
341         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
342         /// <since_tizen> 9 </since_tizen>
343         public void AddIceCandidate(string iceCandidate)
344         {
345             ValidateWebRTCState(WebRTCState.Negotiating);
346
347             ValidationUtil.ValidateIsNullOrEmpty(iceCandidate, nameof(iceCandidate));
348
349             NativeWebRTC.AddIceCandidate(Handle, iceCandidate).ThrowIfFailed("Failed to set ICE candidate.");
350         }
351
352         /// <summary>
353         /// Adds new ICE candidates from the remote peer over its signaling channel.
354         /// </summary>
355         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
356         /// <param name="iceCandidates">The ICE candidates.</param>
357         /// <exception cref="ArgumentException">The ICE candidate is empty string.</exception>
358         /// <exception cref="ArgumentNullException">The ICE candidate is null.</exception>
359         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
360         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
361         /// <since_tizen> 9 </since_tizen>
362         public void AddIceCandidates(IEnumerable<string> iceCandidates)
363         {
364             ValidateWebRTCState(WebRTCState.Negotiating);
365
366             ValidationUtil.ValidateIsAny(iceCandidates);
367
368             #pragma warning disable CA1062
369             foreach (string iceCandidate in iceCandidates)
370             {
371                 AddIceCandidate(iceCandidate);
372             }
373             #pragma warning restore CA1062
374         }
375
376         /// <summary>
377         /// Adds media source.
378         /// </summary>
379         /// <remarks>
380         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
381         /// Each MediaSource requires different feature or privilege.<br/>
382         /// <see cref="MediaCameraSource"/> needs camera feature and privilege.<br/>
383         /// <see cref="MediaMicrophoneSource"/> needs microphone feature and recorder privilege.<br/>
384         /// </remarks>
385         /// <param name="source">The media sources to add.</param>
386         /// <feature>http://tizen.org/feature/camera</feature>
387         /// <feature>http://tizen.org/feature/microphone</feature>
388         /// <privilege>http://tizen.org/privilege/camera</privilege>
389         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
390         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
391         /// <privilege>http://tizen.org/privilege/recorder</privilege>
392         /// <exception cref="ArgumentNullException">The media source is null.</exception>
393         /// <exception cref="InvalidOperationException">
394         /// The WebRTC is not in the valid state.<br/>
395         /// - or -<br/>
396         /// All or one of <paramref name="source"/> was already detached.
397         /// </exception>
398         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
399         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
400         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
401         /// <seealso cref="MediaCameraSource"/>
402         /// <seealso cref="MediaMicrophoneSource"/>
403         /// <seealso cref="MediaTestSource"/>
404         /// <seealso cref="MediaPacketSource"/>
405         /// <seealso cref="AddSources"/>
406         /// <seealso cref="RemoveSource"/>
407         /// <seealso cref="RemoveSources"/>
408         /// <since_tizen> 9 </since_tizen>
409         public void AddSource(MediaSource source)
410         {
411             if (source == null)
412             {
413                 throw new ArgumentNullException(nameof(source), "source is null");
414             }
415
416             ValidateWebRTCState(WebRTCState.Idle);
417
418             source?.AttachTo(this);
419
420             _source.Add(source);
421         }
422
423         /// <summary>
424         /// Adds media sources.
425         /// </summary>
426         /// <remarks>
427         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
428         /// Each MediaSource requires different feature or privilege.<br/>
429         /// <see cref="MediaCameraSource"/> needs camera feature and privilege.<br/>
430         /// <see cref="MediaMicrophoneSource"/> needs microphone feature and recorder privilege.<br/>
431         /// </remarks>
432         /// <param name="sources">The media sources to add.</param>
433         /// <feature>http://tizen.org/feature/camera</feature>
434         /// <feature>http://tizen.org/feature/microphone</feature>
435         /// <privilege>http://tizen.org/privilege/camera</privilege>
436         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
437         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
438         /// <privilege>http://tizen.org/privilege/recorder</privilege>
439         /// <exception cref="ArgumentNullException">The media source is null.</exception>
440         /// <exception cref="InvalidOperationException">
441         /// The WebRTC is not in the valid state.<br/>
442         /// - or -<br/>
443         /// All or one of <paramref name="sources"/> was already detached.
444         /// </exception>
445         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
446         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
447         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
448         /// <seealso cref="MediaCameraSource"/>
449         /// <seealso cref="MediaMicrophoneSource"/>
450         /// <seealso cref="MediaTestSource"/>
451         /// <seealso cref="MediaPacketSource"/>
452         /// <seealso cref="AddSource"/>
453         /// <seealso cref="RemoveSource"/>
454         /// <seealso cref="RemoveSources"/>
455         /// <since_tizen> 9 </since_tizen>
456         public void AddSources(params MediaSource[] sources)
457         {
458             if (sources == null)
459             {
460                 throw new ArgumentNullException(nameof(sources), "sources are null");
461             }
462
463             foreach (var source in sources)
464             {
465                 AddSource(source);
466             }
467         }
468
469         /// <summary>
470         /// Removes media source.
471         /// </summary>
472         /// <remarks>
473         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
474         /// If user want to use removed MediaSource again, user should create new instance for it.
475         /// </remarks>
476         /// <param name="source">The media source to remove.</param>
477         /// <exception cref="ArgumentNullException">The media source is null.</exception>
478         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
479         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
480         /// <seealso cref="MediaCameraSource"/>
481         /// <seealso cref="MediaMicrophoneSource"/>
482         /// <seealso cref="MediaTestSource"/>
483         /// <seealso cref="MediaPacketSource"/>
484         /// <seealso cref="AddSource"/>
485         /// <seealso cref="AddSources"/>
486         /// <seealso cref="RemoveSources"/>
487         /// <since_tizen> 9 </since_tizen>
488         public void RemoveSource(MediaSource source)
489         {
490             if (source == null)
491             {
492                 throw new ArgumentNullException(nameof(source), "source is null");
493             }
494
495             ValidateWebRTCState(WebRTCState.Idle);
496
497             source?.DetachFrom(this);
498
499             _source.Remove(source);
500
501             source = null;
502         }
503
504         /// <summary>
505         /// Removes media sources.
506         /// </summary>
507         /// <remarks>
508         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
509         /// If user want to use removed MediaSource again, user should create new instance for it.
510         /// </remarks>
511         /// <param name="sources">The media source to remove.</param>
512         /// <exception cref="ArgumentNullException">The media source is null.</exception>
513         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
514         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
515         /// <seealso cref="MediaCameraSource"/>
516         /// <seealso cref="MediaMicrophoneSource"/>
517         /// <seealso cref="MediaTestSource"/>
518         /// <seealso cref="MediaPacketSource"/>
519         /// <seealso cref="AddSource"/>
520         /// <seealso cref="AddSources"/>
521         /// <seealso cref="RemoveSource"/>
522         /// <since_tizen> 9 </since_tizen>
523         public void RemoveSources(params MediaSource[] sources)
524         {
525             foreach (var source in sources)
526             {
527                 RemoveSource(source);
528             }
529         }
530
531         /// <summary>
532         /// Sets a turn server.
533         /// </summary>
534         /// <exception cref="ArgumentNullException">The <paramref name="turnServer"/> is null.</exception>
535         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
536         /// <since_tizen> 9 </since_tizen>
537         public void SetTurnServer(string turnServer)
538         {
539             ValidateNotDisposed();
540
541             if (turnServer == null)
542             {
543                 throw new ArgumentNullException(nameof(turnServer), "Turn server name is null.");
544             }
545
546             NativeWebRTC.AddTurnServer(Handle, turnServer).
547                 ThrowIfFailed("Failed to add turn server");
548         }
549
550         /// <summary>
551         /// Sets turn servers.
552         /// </summary>
553         /// <exception cref="ArgumentNullException">The one of <paramref name="turnServers"/> is null.</exception>
554         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
555         /// <since_tizen> 9 </since_tizen>
556         public void SetTurnServers(params string[] turnServers)
557         {
558             ValidateNotDisposed();
559
560             if (turnServers == null)
561             {
562                 throw new ArgumentNullException(nameof(turnServers), "Turn server names are null.");
563             }
564
565             foreach (var turnServer in turnServers)
566             {
567                 SetTurnServer(turnServer);
568             }
569         }
570
571         /// <summary>
572         /// Gets all turn servers.
573         /// </summary>
574         /// <returns>The turn server list.</returns>
575         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
576         /// <since_tizen> 9 </since_tizen>
577         public ReadOnlyCollection<string> GetTurnServer()
578         {
579             ValidateNotDisposed();
580
581             var list = new List<string>();
582
583             NativeWebRTC.RetrieveTurnServerCallback callback = (server, _) =>
584             {
585                 if (!string.IsNullOrWhiteSpace(server))
586                 {
587                     list.Add(server);
588                 }
589
590                 return true;
591             };
592
593             NativeWebRTC.ForeachTurnServer(Handle, callback).ThrowIfFailed("Failed to retrieve turn server");
594
595             return list.AsReadOnly();
596         }
597     }
598 }