2 * Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
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;
27 namespace Tizen.Multimedia.Remoting
29 internal static class WebRTCLog
31 internal const string Tag = "Tizen.Multimedia.WebRTC";
35 /// Provides the ability to control WebRTC.
37 /// <since_tizen> 9 </since_tizen>
38 public partial class WebRTC : IDisposable
40 private readonly WebRTCHandle _handle;
41 private List<MediaSource> _source;
44 /// Initializes a new instance of the <see cref="WebRTC"/> class.
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>
55 if (!Features.IsSupported(WebRTCFeatures.Wifi) &&
56 !Features.IsSupported(WebRTCFeatures.Telephony) &&
57 !Features.IsSupported(WebRTCFeatures.Ethernet))
59 throw new NotSupportedException("Network features are not supported.");
62 NativeWebRTC.Create(out _handle).ThrowIfFailed("Failed to create webrtc");
64 Debug.Assert(_handle != null);
68 _source = new List<MediaSource>();
71 internal void ValidateWebRTCState(params WebRTCState[] desiredStates)
73 Debug.Assert(desiredStates.Length > 0);
75 ValidateNotDisposed();
77 WebRTCState curState = State;
78 if (!curState.IsAnyOf(desiredStates))
80 throw new InvalidOperationException("The WebRTC is not in a valid state. " +
81 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
85 #region Dispose support
86 private bool _disposed;
89 /// Releases all resources used by the current instance.
91 /// <since_tizen> 9 </since_tizen>
95 GC.SuppressFinalize(this);
99 /// Releases the unmanaged resources used by the <see cref="WebRTC"/>.
101 /// <param name="disposing">
102 /// true to release both managed and unmanaged resources;
103 /// false to release only unmanaged resources.
105 [EditorBrowsable(EditorBrowsableState.Never)]
106 protected virtual void Dispose(bool disposing)
108 if (_disposed || !disposing)
118 internal void ValidateNotDisposed()
122 Log.Warn(WebRTCLog.Tag, "WebRTC was disposed");
123 throw new ObjectDisposedException(nameof(WebRTC));
127 internal bool IsDisposed => _disposed;
131 /// Starts the WebRTC.
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.
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>
146 ValidateWebRTCState(WebRTCState.Idle);
148 NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
152 /// Starts the WebRTC asynchronously.
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.
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()
166 ValidateWebRTCState(WebRTCState.Idle);
168 var tcs = new TaskCompletionSource<bool>();
169 var error = WebRTCError.ConnectionFailed;
171 EventHandler<WebRTCStateChangedEventArgs> stateChangedEventHandler = (s, e) =>
173 if (e.Current == WebRTCState.Negotiating)
175 tcs.TrySetResult(true);
178 EventHandler<WebRTCErrorOccurredEventArgs> errorEventHandler = (s, e) =>
180 Log.Info(WebRTCLog.Tag, e.ToString());
182 tcs.TrySetResult(false);
187 StateChanged += stateChangedEventHandler;
188 ErrorOccurred += errorEventHandler;
190 NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC");
192 var result = await tcs.Task.ConfigureAwait(false);
197 throw new InvalidOperationException(error.ToString());
202 StateChanged -= stateChangedEventHandler;
203 ErrorOccurred -= errorEventHandler;
208 /// Stops the WebRTC.
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.
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>
220 ValidateWebRTCState(WebRTCState.Negotiating, WebRTCState.Playing);
222 NativeWebRTC.Stop(Handle).ThrowIfFailed("Failed to stop the WebRTC");
226 /// Creates SDP offer asynchronously to start a new WebRTC connection to a remote peer.
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()
236 ValidateWebRTCState(WebRTCState.Negotiating);
238 var tcsSdpCreated = new TaskCompletionSource<string>();
240 NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
242 tcsSdpCreated.TrySetResult(sdp);
246 using (var cbKeeper = ObjectKeeper.Get(cb))
248 NativeWebRTC.CreateSDPOfferAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
249 ThrowIfFailed("Failed to create offer asynchronously");
251 offer = await tcsSdpCreated.Task.ConfigureAwait(false);
259 /// Creates SDP answer asynchronously with option to an offer received from a remote peer.
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()
269 ValidateWebRTCState(WebRTCState.Negotiating);
271 var tcsSdpCreated = new TaskCompletionSource<string>();
273 NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) =>
275 tcsSdpCreated.TrySetResult(sdp);
278 string answer = null;
279 using (var cbKeeper = ObjectKeeper.Get(cb))
281 NativeWebRTC.CreateSDPAnswerAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero).
282 ThrowIfFailed("Failed to create answer asynchronously");
284 answer = await tcsSdpCreated.Task.ConfigureAwait(false);
292 /// Sets the session description for a local peer.
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)
305 ValidateWebRTCState(WebRTCState.Negotiating);
307 ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
309 NativeWebRTC.SetLocalDescription(Handle, description).ThrowIfFailed("Failed to set description.");
313 /// Sets the session description of the remote peer's current offer or answer.
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)
326 ValidateWebRTCState(WebRTCState.Negotiating);
328 ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description));
330 NativeWebRTC.SetRemoteDescription(Handle, description).ThrowIfFailed("Failed to set description.");
334 /// Adds a new ICE candidate from the remote peer over its signaling channel.
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)
345 ValidateWebRTCState(WebRTCState.Negotiating);
347 ValidationUtil.ValidateIsNullOrEmpty(iceCandidate, nameof(iceCandidate));
349 NativeWebRTC.AddIceCandidate(Handle, iceCandidate).ThrowIfFailed("Failed to set ICE candidate.");
353 /// Adds new ICE candidates from the remote peer over its signaling channel.
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)
364 ValidateWebRTCState(WebRTCState.Negotiating);
366 ValidationUtil.ValidateIsAny(iceCandidates);
368 #pragma warning disable CA1062
369 foreach (string iceCandidate in iceCandidates)
371 AddIceCandidate(iceCandidate);
373 #pragma warning restore CA1062
377 /// Adds media source.
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/>
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/>
396 /// All or one of <paramref name="source"/> was already detached.
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)
413 throw new ArgumentNullException(nameof(source), "source is null");
416 ValidateWebRTCState(WebRTCState.Idle);
418 source?.AttachTo(this);
424 /// Adds media sources.
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/>
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/>
443 /// All or one of <paramref name="sources"/> was already detached.
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)
460 throw new ArgumentNullException(nameof(sources), "sources are null");
463 foreach (var source in sources)
470 /// Removes media source.
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.
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)
492 throw new ArgumentNullException(nameof(source), "source is null");
495 ValidateWebRTCState(WebRTCState.Idle);
497 source?.DetachFrom(this);
499 _source.Remove(source);
505 /// Removes media sources.
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.
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)
525 foreach (var source in sources)
527 RemoveSource(source);
532 /// Sets a turn server.
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)
539 ValidateNotDisposed();
541 if (turnServer == null)
543 throw new ArgumentNullException(nameof(turnServer), "Turn server name is null.");
546 NativeWebRTC.AddTurnServer(Handle, turnServer).
547 ThrowIfFailed("Failed to add turn server");
551 /// Sets turn servers.
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)
558 ValidateNotDisposed();
560 if (turnServers == null)
562 throw new ArgumentNullException(nameof(turnServers), "Turn server names are null.");
565 foreach (var turnServer in turnServers)
567 SetTurnServer(turnServer);
572 /// Gets all turn servers.
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()
579 ValidateNotDisposed();
581 var list = new List<string>();
583 NativeWebRTC.RetrieveTurnServerCallback callback = (server, _) =>
585 if (!string.IsNullOrWhiteSpace(server))
593 NativeWebRTC.ForeachTurnServer(Handle, callback).ThrowIfFailed("Failed to retrieve turn server");
595 return list.AsReadOnly();