[WebRTC] Add display feature for video APIs (#5442)
[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.Runtime.InteropServices;
23 using System.Threading;
24 using System.Threading.Tasks;
25 using Tizen.Applications;
26 using static Interop;
27
28 namespace Tizen.Multimedia.Remoting
29 {
30     internal static class WebRTCLog
31     {
32         internal const string Tag = "Tizen.Multimedia.WebRTC";
33     }
34
35     /// <summary>
36     /// Provides the ability to control WebRTC.
37     /// </summary>
38     /// <since_tizen> 9 </since_tizen>
39     public partial class WebRTC : IDisposable
40     {
41         private readonly WebRTCHandle _handle;
42         private List<MediaSource> _source;
43
44         /// <summary>
45         /// Initializes a new instance of the <see cref="WebRTC"/> class.
46         /// </summary>
47         /// <feature>http://tizen.org/feature/network.wifi</feature>
48         /// <feature>http://tizen.org/feature/network.telephony</feature>
49         /// <feature>http://tizen.org/feature/network.ethernet</feature>
50         /// <privilege>http://tizen.org/privilege/internet</privilege>
51         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
52         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
53         /// <since_tizen> 9 </since_tizen>
54         public WebRTC()
55         {
56             if (!Features.IsSupported(WebRTCFeatures.Wifi) &&
57                 !Features.IsSupported(WebRTCFeatures.Telephony) &&
58                 !Features.IsSupported(WebRTCFeatures.Ethernet))
59             {
60                 throw new NotSupportedException("Network features are not supported.");
61             }
62
63             NativeWebRTC.Create(out _handle).ThrowIfFailed("Failed to create webrtc");
64
65             Debug.Assert(_handle != null);
66
67             RegisterEvents();
68
69             _source = new List<MediaSource>();
70         }
71
72         internal void ValidateWebRTCState(params WebRTCState[] desiredStates)
73         {
74             Debug.Assert(desiredStates.Length > 0);
75
76             ValidateNotDisposed();
77
78             WebRTCState curState = State;
79             if (!curState.IsAnyOf(desiredStates))
80             {
81                 throw new InvalidOperationException("The WebRTC is not in a valid state. " +
82                     $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
83             }
84         }
85
86         #region Dispose support
87         private bool _disposed;
88
89         /// <summary>
90         /// Releases all resources used by the current instance.
91         /// </summary>
92         /// <since_tizen> 9 </since_tizen>
93         public void Dispose()
94         {
95             Dispose(true);
96             GC.SuppressFinalize(this);
97         }
98
99         /// <summary>
100         /// Releases the unmanaged resources used by the <see cref="WebRTC"/>.
101         /// </summary>
102         /// <param name="disposing">
103         /// true to release both managed and unmanaged resources;
104         /// false to release only unmanaged resources.
105         /// </param>
106         [EditorBrowsable(EditorBrowsableState.Never)]
107         protected virtual void Dispose(bool disposing)
108         {
109             if (_disposed || !disposing)
110                 return;
111
112             if (_handle != null)
113             {
114                 _handle.Dispose();
115                 _disposed = true;
116             }
117         }
118
119         internal void ValidateNotDisposed()
120         {
121             if (_disposed)
122             {
123                 Log.Warn(WebRTCLog.Tag, "WebRTC was disposed");
124                 throw new ObjectDisposedException(nameof(WebRTC));
125             }
126         }
127
128         internal bool IsDisposed => _disposed;
129         #endregion
130
131         /// <summary>
132         /// Starts the WebRTC.
133         /// </summary>
134         /// <remarks>
135         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/> state.<br/>
136         /// The WebRTC state will be <see cref="WebRTCState.Negotiating"/> state.<br/>
137         /// The user should check whether <see cref="State" /> is changed to <see cref="WebRTCState.Negotiating"/> state or not.
138         /// </remarks>
139         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
140         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
141         /// <see also="WebRTCState"/>
142         /// <see also="StateChanged"/>
143         /// <see also="CreateOffer"/>
144         /// <since_tizen> 9 </since_tizen>
145         public void Start()
146         {
147             ValidateWebRTCState(WebRTCState.Idle);
148
149             NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
150         }
151
152         /// <summary>
153         /// Starts the WebRTC asynchronously.
154         /// </summary>
155         /// <remarks>
156         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/> state.<br/>
157         /// The WebRTC state will be <see cref="WebRTCState.Negotiating"/> state.<br/>
158         /// This ensures that <see cref="State" /> is changed to <see cref="WebRTCState.Negotiating"/> state.
159         /// </remarks>
160         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
161         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
162         /// <see also="WebRTCState"/>
163         /// <see also="CreateOffer"/>
164         /// <since_tizen> 9 </since_tizen>
165         public async Task StartAsync()
166         {
167             ValidateWebRTCState(WebRTCState.Idle);
168
169             var tcs = new TaskCompletionSource<bool>();
170             var error = WebRTCError.ConnectionFailed;
171
172             EventHandler<WebRTCStateChangedEventArgs> stateChangedEventHandler = (s, e) =>
173             {
174                 if (e.Current == WebRTCState.Negotiating)
175                 {
176                     tcs.TrySetResult(true);
177                 }
178             };
179             EventHandler<WebRTCErrorOccurredEventArgs> errorEventHandler = (s, e) =>
180             {
181                 Log.Info(WebRTCLog.Tag, e.ToString());
182                 error = e.Error;
183                 tcs.TrySetResult(false);
184             };
185
186             try
187             {
188                 StateChanged += stateChangedEventHandler;
189                 ErrorOccurred += errorEventHandler;
190
191                 NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
192
193                 var result = await tcs.Task.ConfigureAwait(false);
194                 await Task.Yield();
195
196                 if (!result)
197                 {
198                     throw new InvalidOperationException(error.ToString());
199                 }
200             }
201             finally
202             {
203                 StateChanged -= stateChangedEventHandler;
204                 ErrorOccurred -= errorEventHandler;
205             }
206         }
207
208         /// <summary>
209         /// Stops the WebRTC.
210         /// </summary>
211         /// <remarks>
212         /// The WebRTC must be in the <see cref="WebRTCState.Negotiating"/> or <see cref="WebRTCState.Playing"/> state.<br/>
213         /// The WebRTC state will be <see cref="WebRTCState.Idle"/> state.<br/>
214         /// The user should check whether <see cref="State" /> is changed to <see cref="WebRTCState.Idle"/> state or not.
215         /// </remarks>
216         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
217         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
218         /// <since_tizen> 9 </since_tizen>
219         public void Stop()
220         {
221             ValidateWebRTCState(WebRTCState.Negotiating, WebRTCState.Playing);
222
223             NativeWebRTC.Stop(Handle).ThrowIfFailed("Failed to stop the WebRTC");
224         }
225
226         /// <summary>
227         /// Creates SDP offer asynchronously to start a new WebRTC connection to a remote peer.
228         /// </summary>
229         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/></remarks>
230         /// <returns>The SDP offer.</returns>
231         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
232         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
233         /// <seealso cref="CreateAnswerAsync()"/>
234         /// <since_tizen> 9 </since_tizen>
235         public async Task<string> CreateOfferAsync()
236         {
237             ValidateWebRTCState(WebRTCState.Negotiating);
238
239             var tcsSdpCreated = new TaskCompletionSource<string>();
240
241             NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
242             {
243                 tcsSdpCreated.TrySetResult(sdp);
244             };
245
246             string offer = null;
247             using (var cbKeeper = ObjectKeeper.Get(cb))
248             {
249                 NativeWebRTC.CreateSDPOfferAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
250                     ThrowIfFailed("Failed to create offer asynchronously");
251
252                 offer = await tcsSdpCreated.Task.ConfigureAwait(false);
253                 await Task.Yield();
254             }
255
256             return offer;
257         }
258
259         /// <summary>
260         /// Creates SDP answer asynchronously with option to an offer received from a remote peer.
261         /// </summary>
262         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/></remarks>
263         /// <returns>The SDP answer.</returns>
264         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
265         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
266         /// <seealso cref="CreateOfferAsync()"/>
267         /// <since_tizen> 9 </since_tizen>
268         public async Task<string> CreateAnswerAsync()
269         {
270             ValidateWebRTCState(WebRTCState.Negotiating);
271
272             var tcsSdpCreated = new TaskCompletionSource<string>();
273
274             NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
275             {
276                 tcsSdpCreated.TrySetResult(sdp);
277             };
278
279             string answer = null;
280             using (var cbKeeper = ObjectKeeper.Get(cb))
281             {
282                 NativeWebRTC.CreateSDPAnswerAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
283                     ThrowIfFailed("Failed to create answer asynchronously");
284
285                 answer = await tcsSdpCreated.Task.ConfigureAwait(false);
286                 await Task.Yield();
287             }
288
289             return answer;
290         }
291
292         /// <summary>
293         /// Sets the session description for a local peer.
294         /// </summary>
295         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
296         /// <param name="description">The local session description.</param>
297         /// <exception cref="ArgumentException">The description is empty string.</exception>
298         /// <exception cref="ArgumentNullException">The description is null.</exception>
299         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
300         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
301         /// <seealso cref="CreateOfferAsync()"/>
302         /// <seealso cref="CreateAnswerAsync()"/>
303         /// <since_tizen> 9 </since_tizen>
304         public void SetLocalDescription(string description)
305         {
306             ValidateWebRTCState(WebRTCState.Negotiating);
307
308             ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
309
310             NativeWebRTC.SetLocalDescription(Handle, description).ThrowIfFailed("Failed to set description.");
311         }
312
313         /// <summary>
314         /// Sets the session description of the remote peer's current offer or answer.
315         /// </summary>
316         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
317         /// <param name="description">The remote session description.</param>
318         /// <exception cref="ArgumentException">The description is empty string.</exception>
319         /// <exception cref="ArgumentNullException">The description is null.</exception>
320         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
321         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
322         /// <seealso cref="CreateOfferAsync()"/>
323         /// <seealso cref="CreateAnswerAsync()"/>
324         /// <since_tizen> 9 </since_tizen>
325         public void SetRemoteDescription(string description)
326         {
327             ValidateWebRTCState(WebRTCState.Negotiating);
328
329             ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
330
331             NativeWebRTC.SetRemoteDescription(Handle, description).ThrowIfFailed("Failed to set description.");
332         }
333
334         /// <summary>
335         /// Adds a new ICE candidate from the remote peer over its signaling channel.
336         /// </summary>
337         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
338         /// <param name="iceCandidate">The ICE candidate.</param>
339         /// <exception cref="ArgumentException">The ICE candidate is empty string.</exception>
340         /// <exception cref="ArgumentNullException">The ICE candidate is null.</exception>
341         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
342         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
343         /// <since_tizen> 9 </since_tizen>
344         public void AddIceCandidate(string iceCandidate)
345         {
346             ValidateWebRTCState(WebRTCState.Negotiating);
347
348             ValidationUtil.ValidateIsNullOrEmpty(iceCandidate, nameof(iceCandidate));
349
350             NativeWebRTC.AddIceCandidate(Handle, iceCandidate).ThrowIfFailed("Failed to set ICE candidate.");
351         }
352
353         /// <summary>
354         /// Adds new ICE candidates from the remote peer over its signaling channel.
355         /// </summary>
356         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Negotiating"/>.</remarks>
357         /// <param name="iceCandidates">The ICE candidates.</param>
358         /// <exception cref="ArgumentException">The ICE candidate is empty string.</exception>
359         /// <exception cref="ArgumentNullException">The ICE candidate is null.</exception>
360         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
361         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
362         /// <since_tizen> 9 </since_tizen>
363         public void AddIceCandidates(IEnumerable<string> iceCandidates)
364         {
365             ValidateWebRTCState(WebRTCState.Negotiating);
366
367             ValidationUtil.ValidateIsAny(iceCandidates);
368
369             #pragma warning disable CA1062
370             foreach (string iceCandidate in iceCandidates)
371             {
372                 AddIceCandidate(iceCandidate);
373             }
374             #pragma warning restore CA1062
375         }
376
377         /// <summary>
378         /// Adds media source.
379         /// </summary>
380         /// <remarks>
381         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
382         /// Each MediaSource requires different feature or privilege.<br/>
383         /// <see cref="MediaCameraSource"/> needs camera feature and privilege.<br/>
384         /// <see cref="MediaMicrophoneSource"/> needs microphone feature and recorder privilege.<br/>
385         /// </remarks>
386         /// <param name="source">The media sources to add.</param>
387         /// <feature>http://tizen.org/feature/camera</feature>
388         /// <feature>http://tizen.org/feature/microphone</feature>
389         /// <feature>http://tizen.org/feature/display</feature>
390         /// <privilege>http://tizen.org/privilege/camera</privilege>
391         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
392         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
393         /// <privilege>http://tizen.org/privilege/recorder</privilege>
394         /// <exception cref="ArgumentNullException">The media source is null.</exception>
395         /// <exception cref="InvalidOperationException">
396         /// The WebRTC is not in the valid state.<br/>
397         /// - or -<br/>
398         /// All or one of <paramref name="source"/> was already detached.
399         /// </exception>
400         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
401         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
402         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
403         /// <seealso cref="MediaCameraSource"/>
404         /// <seealso cref="MediaMicrophoneSource"/>
405         /// <seealso cref="MediaTestSource"/>
406         /// <seealso cref="MediaPacketSource"/>
407         /// <seealso cref="AddSources"/>
408         /// <seealso cref="RemoveSource"/>
409         /// <seealso cref="RemoveSources"/>
410         /// <since_tizen> 9 </since_tizen>
411         public void AddSource(MediaSource source)
412         {
413             if (source == null)
414             {
415                 throw new ArgumentNullException(nameof(source), "source is null");
416             }
417
418             ValidateWebRTCState(WebRTCState.Idle);
419
420             source?.AttachTo(this);
421
422             _source.Add(source);
423         }
424
425         /// <summary>
426         /// Adds media sources.
427         /// </summary>
428         /// <remarks>
429         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
430         /// Each MediaSource requires different feature or privilege.<br/>
431         /// <see cref="MediaCameraSource"/> needs camera feature and privilege.<br/>
432         /// <see cref="MediaMicrophoneSource"/> needs microphone feature and recorder privilege.<br/>
433         /// </remarks>
434         /// <param name="sources">The media sources to add.</param>
435         /// <feature>http://tizen.org/feature/camera</feature>
436         /// <feature>http://tizen.org/feature/microphone</feature>
437         /// <feature>http://tizen.org/feature/display</feature>
438         /// <privilege>http://tizen.org/privilege/camera</privilege>
439         /// <privilege>http://tizen.org/privilege/mediastorage</privilege>
440         /// <privilege>http://tizen.org/privilege/externalstorage</privilege>
441         /// <privilege>http://tizen.org/privilege/recorder</privilege>
442         /// <exception cref="ArgumentNullException">The media source is null.</exception>
443         /// <exception cref="InvalidOperationException">
444         /// The WebRTC is not in the valid state.<br/>
445         /// - or -<br/>
446         /// All or one of <paramref name="sources"/> was already detached.
447         /// </exception>
448         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
449         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
450         /// <exception cref="UnauthorizedAccessException">Thrown when the permission is denied.</exception>
451         /// <seealso cref="MediaCameraSource"/>
452         /// <seealso cref="MediaMicrophoneSource"/>
453         /// <seealso cref="MediaTestSource"/>
454         /// <seealso cref="MediaPacketSource"/>
455         /// <seealso cref="AddSource"/>
456         /// <seealso cref="RemoveSource"/>
457         /// <seealso cref="RemoveSources"/>
458         /// <since_tizen> 9 </since_tizen>
459         public void AddSources(params MediaSource[] sources)
460         {
461             if (sources == null)
462             {
463                 throw new ArgumentNullException(nameof(sources), "sources are null");
464             }
465
466             foreach (var source in sources)
467             {
468                 AddSource(source);
469             }
470         }
471
472         /// <summary>
473         /// Removes media source.
474         /// </summary>
475         /// <remarks>
476         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
477         /// If user want to use removed MediaSource again, user should create new instance for it.
478         /// </remarks>
479         /// <param name="source">The media source to remove.</param>
480         /// <exception cref="ArgumentNullException">The media source is null.</exception>
481         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
482         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
483         /// <seealso cref="MediaCameraSource"/>
484         /// <seealso cref="MediaMicrophoneSource"/>
485         /// <seealso cref="MediaTestSource"/>
486         /// <seealso cref="MediaPacketSource"/>
487         /// <seealso cref="AddSource"/>
488         /// <seealso cref="AddSources"/>
489         /// <seealso cref="RemoveSources"/>
490         /// <since_tizen> 9 </since_tizen>
491         public void RemoveSource(MediaSource source)
492         {
493             if (source == null)
494             {
495                 throw new ArgumentNullException(nameof(source), "source is null");
496             }
497
498             ValidateWebRTCState(WebRTCState.Idle);
499
500             source?.DetachFrom(this);
501
502             _source.Remove(source);
503
504             source = null;
505         }
506
507         /// <summary>
508         /// Removes media sources.
509         /// </summary>
510         /// <remarks>
511         /// The WebRTC must be in the <see cref="WebRTCState.Idle"/>.<br/>
512         /// If user want to use removed MediaSource again, user should create new instance for it.
513         /// </remarks>
514         /// <param name="sources">The media source to remove.</param>
515         /// <exception cref="ArgumentNullException">The media source is null.</exception>
516         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
517         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
518         /// <seealso cref="MediaCameraSource"/>
519         /// <seealso cref="MediaMicrophoneSource"/>
520         /// <seealso cref="MediaTestSource"/>
521         /// <seealso cref="MediaPacketSource"/>
522         /// <seealso cref="AddSource"/>
523         /// <seealso cref="AddSources"/>
524         /// <seealso cref="RemoveSource"/>
525         /// <since_tizen> 9 </since_tizen>
526         public void RemoveSources(params MediaSource[] sources)
527         {
528             foreach (var source in sources)
529             {
530                 RemoveSource(source);
531             }
532         }
533
534         /// <summary>
535         /// Sets a turn server.
536         /// </summary>
537         /// <exception cref="ArgumentNullException">The <paramref name="turnServer"/> is null.</exception>
538         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
539         /// <since_tizen> 9 </since_tizen>
540         public void SetTurnServer(string turnServer)
541         {
542             ValidateNotDisposed();
543
544             if (turnServer == null)
545             {
546                 throw new ArgumentNullException(nameof(turnServer), "Turn server name is null.");
547             }
548
549             NativeWebRTC.AddTurnServer(Handle, turnServer).
550                 ThrowIfFailed("Failed to add turn server");
551         }
552
553         /// <summary>
554         /// Sets turn servers.
555         /// </summary>
556         /// <exception cref="ArgumentNullException">The one of <paramref name="turnServers"/> is null.</exception>
557         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
558         /// <since_tizen> 9 </since_tizen>
559         public void SetTurnServers(params string[] turnServers)
560         {
561             ValidateNotDisposed();
562
563             if (turnServers == null)
564             {
565                 throw new ArgumentNullException(nameof(turnServers), "Turn server names are null.");
566             }
567
568             foreach (var turnServer in turnServers)
569             {
570                 SetTurnServer(turnServer);
571             }
572         }
573
574         /// <summary>
575         /// Retrieves all turn servers.
576         /// </summary>
577         /// <returns>The turn server list.</returns>
578         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
579         /// <since_tizen> 9 </since_tizen>
580         public ReadOnlyCollection<string> GetTurnServer()
581         {
582             ValidateNotDisposed();
583
584             var list = new List<string>();
585
586             NativeWebRTC.RetrieveTurnServerCallback cb = (server, _) =>
587             {
588                 if (!string.IsNullOrWhiteSpace(server))
589                 {
590                     list.Add(server);
591                 }
592
593                 return true;
594             };
595
596             NativeWebRTC.ForeachTurnServer(Handle, cb).ThrowIfFailed("Failed to retrieve turn server");
597
598             return list.AsReadOnly();
599         }
600
601         /// <summary>
602         /// Retrieves the current statistics information.
603         /// </summary>
604         /// <remarks>The WebRTC must be in the <see cref="WebRTCState.Playing"/></remarks>
605         /// <returns>The WebRTC statistics informations.</returns>
606         /// <param name="category">The category of statistics to get.</param>
607         /// <exception cref="ObjectDisposedException">The WebRTC has already been disposed.</exception>
608         /// <exception cref="InvalidOperationException">The WebRTC is not in the valid state.</exception>
609         /// <since_tizen> 10 </since_tizen>
610         public ReadOnlyCollection<WebRTCStatistics> GetStatistics(WebRTCStatisticsCategory category)
611         {
612             ValidateWebRTCState(WebRTCState.Playing);
613
614             var stats = new List<WebRTCStatistics>();
615             Exception caught = null;
616
617             NativeWebRTC.RetrieveStatsCallback cb = (category_, prop, _) =>
618             {
619                 try
620                 {
621                     stats.Add(new WebRTCStatistics(category_, prop));
622                 }
623                 catch (Exception e)
624                 {
625                     caught = e;
626                     return false;
627                 }
628
629                 return true;
630             };
631
632             using (var cbKeeper = ObjectKeeper.Get(cb))
633             {
634                 NativeWebRTC.ForeachStats(Handle, (int)category, cb, IntPtr.Zero).
635                     ThrowIfFailed("failed to retrieve stats");
636
637                 if (caught != null)
638                 {
639                     throw caught;
640                 }
641             }
642
643             return new ReadOnlyCollection<WebRTCStatistics>(stats);
644         }
645
646         /// <summary>
647         /// Represents WebRTC statistics information.
648         /// </summary>
649         /// <since_tizen> 10 </since_tizen>
650         public class WebRTCStatistics
651         {
652             internal WebRTCStatistics(WebRTCStatisticsCategory type, IntPtr prop)
653             {
654                 var unmanagedStruct = Marshal.PtrToStructure<NativeWebRTC.StatsPropertyStruct>(prop);
655
656                 Category = type;
657                 Name = unmanagedStruct.name;
658                 Property = unmanagedStruct.property;
659
660                 switch (unmanagedStruct.propertyType)
661                 {
662                     case WebRTCStatsPropertyType.TypeBool:
663                         Value = unmanagedStruct.value.@bool;
664                         break;
665                     case WebRTCStatsPropertyType.TypeInt:
666                         Value = unmanagedStruct.value.@int;
667                         break;
668                     case WebRTCStatsPropertyType.TypeUint:
669                         Value = unmanagedStruct.value.@uint;
670                         break;
671                     case WebRTCStatsPropertyType.TypeInt64:
672                         Value = unmanagedStruct.value.@long;
673                         break;
674                     case WebRTCStatsPropertyType.TypeUint64:
675                         Value = unmanagedStruct.value.@ulong;
676                         break;
677                     case WebRTCStatsPropertyType.TypeFloat:
678                         Value = unmanagedStruct.value.@float;
679                         break;
680                     case WebRTCStatsPropertyType.TypeDouble:
681                         Value = unmanagedStruct.value.@double;
682                         break;
683                     case WebRTCStatsPropertyType.TypeString:
684                         Value = Marshal.PtrToStringAnsi(unmanagedStruct.value.@string);
685                         break;
686                     default:
687                         throw new InvalidOperationException($"No matching type [{unmanagedStruct.propertyType}]");
688                 }
689             }
690
691             /// <summary>
692             /// Gets the category of statistics.
693             /// </summary>
694             /// <value>The category of WebRTC statistics information</value>
695             /// <since_tizen> 10 </since_tizen>
696             public WebRTCStatisticsCategory Category { get; }
697
698             /// <summary>
699             /// Gets the name of statistics.
700             /// </summary>
701             /// <value>The name of WebRTC statistics information</value>
702             /// <since_tizen> 10 </since_tizen>
703             public string Name { get; }
704
705             /// <summary>
706             /// Gets the property of statistics.
707             /// </summary>
708             /// <value>The property of WebRTC statistics information</value>
709             /// <since_tizen> 10 </since_tizen>
710             public WebRTCStatisticsProperty Property { get; }
711
712             /// <summary>
713             /// Gets the value of statistics.
714             /// </summary>
715             /// <value>The value of WebRTC statistics information</value>
716             /// <since_tizen> 10 </since_tizen>
717             public object Value { get; }
718
719             /// <summary>
720             /// Returns a string that represents the current object.
721             /// </summary>
722             /// <returns>A string that represents the current object.</returns>
723             /// <since_tizen> 10 </since_tizen>
724             public override string ToString() =>
725                 $"Category={Category}, Name={Name}, Property={Property}, Value={Value}, Type={Value.GetType()}";
726         }
727     }
728 }