/* * Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.ObjectModel; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Tizen.Applications; using static Interop; namespace Tizen.Multimedia.Remoting { internal static class WebRTCLog { internal const string Tag = "Tizen.Multimedia.WebRTC"; } /// /// Provides the ability to control WebRTC. /// /// 9 public partial class WebRTC : IDisposable { private readonly WebRTCHandle _handle; private List _source; /// /// Initializes a new instance of the class. /// /// http://tizen.org/feature/network.wifi /// http://tizen.org/feature/network.telephony /// http://tizen.org/feature/network.ethernet /// http://tizen.org/privilege/internet /// Thrown when the permission is denied. /// The required feature is not supported. /// 9 public WebRTC() { if (!Features.IsSupported(WebRTCFeatures.Wifi) && !Features.IsSupported(WebRTCFeatures.Telephony) && !Features.IsSupported(WebRTCFeatures.Ethernet)) { throw new NotSupportedException("Network features are not supported."); } NativeWebRTC.Create(out _handle).ThrowIfFailed("Failed to create webrtc"); Debug.Assert(_handle != null); RegisterEvents(); _source = new List(); } internal void ValidateWebRTCState(params WebRTCState[] desiredStates) { Debug.Assert(desiredStates.Length > 0); ValidateNotDisposed(); WebRTCState curState = State; if (!curState.IsAnyOf(desiredStates)) { throw new InvalidOperationException("The WebRTC is not in a valid state. " + $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }."); } } #region Dispose support private bool _disposed; /// /// Releases all resources used by the current instance. /// /// 9 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the . /// /// /// true to release both managed and unmanaged resources; /// false to release only unmanaged resources. /// [EditorBrowsable(EditorBrowsableState.Never)] protected virtual void Dispose(bool disposing) { if (_disposed || !disposing) return; if (_handle != null) { _handle.Dispose(); _disposed = true; } } internal void ValidateNotDisposed() { if (_disposed) { Log.Warn(WebRTCLog.Tag, "WebRTC was disposed"); throw new ObjectDisposedException(nameof(WebRTC)); } } internal bool IsDisposed => _disposed; #endregion /// /// Starts the WebRTC. /// /// /// The WebRTC must be in the state.
/// The WebRTC state will be state.
/// The user should check whether is changed to state or not. ///
/// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// /// 9 public void Start() { ValidateWebRTCState(WebRTCState.Idle); NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC"); } /// /// Starts the WebRTC asynchronously. /// /// /// The WebRTC must be in the state.
/// The WebRTC state will be state.
/// This ensures that is changed to state. ///
/// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// 9 public async Task StartAsync() { ValidateWebRTCState(WebRTCState.Idle); var tcs = new TaskCompletionSource(); var error = WebRTCError.ConnectionFailed; EventHandler stateChangedEventHandler = (s, e) => { if (e.Current == WebRTCState.Negotiating) { tcs.TrySetResult(true); } }; EventHandler errorEventHandler = (s, e) => { Log.Info(WebRTCLog.Tag, e.ToString()); error = e.Error; tcs.TrySetResult(false); }; try { StateChanged += stateChangedEventHandler; ErrorOccurred += errorEventHandler; NativeWebRTC.Start(Handle).ThrowIfFailed("Failed to start the WebRTC"); var result = await tcs.Task.ConfigureAwait(false); await Task.Yield(); if (!result) { throw new InvalidOperationException(error.ToString()); } } finally { StateChanged -= stateChangedEventHandler; ErrorOccurred -= errorEventHandler; } } /// /// Stops the WebRTC. /// /// /// The WebRTC must be in the or state.
/// The WebRTC state will be state.
/// The user should check whether is changed to state or not. ///
/// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// 9 public void Stop() { ValidateWebRTCState(WebRTCState.Negotiating, WebRTCState.Playing); NativeWebRTC.Stop(Handle).ThrowIfFailed("Failed to stop the WebRTC"); } /// /// Creates SDP offer asynchronously to start a new WebRTC connection to a remote peer. /// /// The WebRTC must be in the /// The SDP offer. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// 9 public async Task CreateOfferAsync() { ValidateWebRTCState(WebRTCState.Negotiating); var tcsSdpCreated = new TaskCompletionSource(); NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) => { tcsSdpCreated.TrySetResult(sdp); }; string offer = null; using (var cbKeeper = ObjectKeeper.Get(cb)) { NativeWebRTC.CreateSDPOfferAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero). ThrowIfFailed("Failed to create offer asynchronously"); offer = await tcsSdpCreated.Task.ConfigureAwait(false); await Task.Yield(); } return offer; } /// /// Creates SDP answer asynchronously with option to an offer received from a remote peer. /// /// The WebRTC must be in the /// The SDP answer. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// 9 public async Task CreateAnswerAsync() { ValidateWebRTCState(WebRTCState.Negotiating); var tcsSdpCreated = new TaskCompletionSource(); NativeWebRTC.SdpCreatedCallback cb = (handle, sdp, _) => { tcsSdpCreated.TrySetResult(sdp); }; string answer = null; using (var cbKeeper = ObjectKeeper.Get(cb)) { NativeWebRTC.CreateSDPAnswerAsync(Handle, new SafeBundleHandle(), cb, IntPtr.Zero). ThrowIfFailed("Failed to create answer asynchronously"); answer = await tcsSdpCreated.Task.ConfigureAwait(false); await Task.Yield(); } return answer; } /// /// Sets the session description for a local peer. /// /// The WebRTC must be in the . /// The local session description. /// The description is empty string. /// The description is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// 9 public void SetLocalDescription(string description) { ValidateWebRTCState(WebRTCState.Negotiating); ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description)); NativeWebRTC.SetLocalDescription(Handle, description).ThrowIfFailed("Failed to set description."); } /// /// Sets the session description of the remote peer's current offer or answer. /// /// The WebRTC must be in the . /// The remote session description. /// The description is empty string. /// The description is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// 9 public void SetRemoteDescription(string description) { ValidateWebRTCState(WebRTCState.Negotiating); ValidationUtil.ValidateIsNullOrEmpty(description, nameof(description)); NativeWebRTC.SetRemoteDescription(Handle, description).ThrowIfFailed("Failed to set description."); } /// /// Adds a new ICE candidate from the remote peer over its signaling channel. /// /// The WebRTC must be in the . /// The ICE candidate. /// The ICE candidate is empty string. /// The ICE candidate is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// 9 public void AddIceCandidate(string iceCandidate) { ValidateWebRTCState(WebRTCState.Negotiating); ValidationUtil.ValidateIsNullOrEmpty(iceCandidate, nameof(iceCandidate)); NativeWebRTC.AddIceCandidate(Handle, iceCandidate).ThrowIfFailed("Failed to set ICE candidate."); } /// /// Adds new ICE candidates from the remote peer over its signaling channel. /// /// The WebRTC must be in the . /// The ICE candidates. /// The ICE candidate is empty string. /// The ICE candidate is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// 9 public void AddIceCandidates(IEnumerable iceCandidates) { ValidateWebRTCState(WebRTCState.Negotiating); ValidationUtil.ValidateIsAny(iceCandidates); #pragma warning disable CA1062 foreach (string iceCandidate in iceCandidates) { AddIceCandidate(iceCandidate); } #pragma warning restore CA1062 } /// /// Adds media source. /// /// /// The WebRTC must be in the .
/// Each MediaSource requires different feature or privilege.
/// needs camera feature and privilege.
/// needs microphone feature and recorder privilege.
///
/// The media sources to add. /// http://tizen.org/feature/camera /// http://tizen.org/feature/microphone /// http://tizen.org/feature/display /// http://tizen.org/privilege/camera /// http://tizen.org/privilege/mediastorage /// http://tizen.org/privilege/externalstorage /// http://tizen.org/privilege/recorder /// The media source is null. /// /// The WebRTC is not in the valid state.
/// - or -
/// All or one of was already detached. ///
/// The required feature is not supported. /// The WebRTC has already been disposed. /// Thrown when the permission is denied. /// /// /// /// /// /// /// /// 9 public void AddSource(MediaSource source) { if (source == null) { throw new ArgumentNullException(nameof(source), "source is null"); } ValidateWebRTCState(WebRTCState.Idle); source?.AttachTo(this); _source.Add(source); } /// /// Adds media sources. /// /// /// The WebRTC must be in the .
/// Each MediaSource requires different feature or privilege.
/// needs camera feature and privilege.
/// needs microphone feature and recorder privilege.
///
/// The media sources to add. /// http://tizen.org/feature/camera /// http://tizen.org/feature/microphone /// http://tizen.org/feature/display /// http://tizen.org/privilege/camera /// http://tizen.org/privilege/mediastorage /// http://tizen.org/privilege/externalstorage /// http://tizen.org/privilege/recorder /// The media source is null. /// /// The WebRTC is not in the valid state.
/// - or -
/// All or one of was already detached. ///
/// The required feature is not supported. /// The WebRTC has already been disposed. /// Thrown when the permission is denied. /// /// /// /// /// /// /// /// 9 public void AddSources(params MediaSource[] sources) { if (sources == null) { throw new ArgumentNullException(nameof(sources), "sources are null"); } foreach (var source in sources) { AddSource(source); } } /// /// Removes media source. /// /// /// The WebRTC must be in the .
/// If user want to use removed MediaSource again, user should create new instance for it. ///
/// The media source to remove. /// The media source is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// /// /// /// /// /// 9 public void RemoveSource(MediaSource source) { if (source == null) { throw new ArgumentNullException(nameof(source), "source is null"); } ValidateWebRTCState(WebRTCState.Idle); source?.DetachFrom(this); _source.Remove(source); source = null; } /// /// Removes media sources. /// /// /// The WebRTC must be in the .
/// If user want to use removed MediaSource again, user should create new instance for it. ///
/// The media source to remove. /// The media source is null. /// The WebRTC is not in the valid state. /// The WebRTC has already been disposed. /// /// /// /// /// /// /// /// 9 public void RemoveSources(params MediaSource[] sources) { foreach (var source in sources) { RemoveSource(source); } } /// /// Sets a turn server. /// /// The is null. /// The WebRTC has already been disposed. /// 9 public void SetTurnServer(string turnServer) { ValidateNotDisposed(); if (turnServer == null) { throw new ArgumentNullException(nameof(turnServer), "Turn server name is null."); } NativeWebRTC.AddTurnServer(Handle, turnServer). ThrowIfFailed("Failed to add turn server"); } /// /// Sets turn servers. /// /// The one of is null. /// The WebRTC has already been disposed. /// 9 public void SetTurnServers(params string[] turnServers) { ValidateNotDisposed(); if (turnServers == null) { throw new ArgumentNullException(nameof(turnServers), "Turn server names are null."); } foreach (var turnServer in turnServers) { SetTurnServer(turnServer); } } /// /// Retrieves all turn servers. /// /// The turn server list. /// The WebRTC has already been disposed. /// 9 public ReadOnlyCollection GetTurnServer() { ValidateNotDisposed(); var list = new List(); NativeWebRTC.RetrieveTurnServerCallback cb = (server, _) => { if (!string.IsNullOrWhiteSpace(server)) { list.Add(server); } return true; }; NativeWebRTC.ForeachTurnServer(Handle, cb).ThrowIfFailed("Failed to retrieve turn server"); return list.AsReadOnly(); } /// /// Retrieves the current statistics information. /// /// The WebRTC must be in the /// The WebRTC statistics informations. /// The category of statistics to get. /// The WebRTC has already been disposed. /// The WebRTC is not in the valid state. /// 10 public ReadOnlyCollection GetStatistics(WebRTCStatisticsCategory category) { ValidateWebRTCState(WebRTCState.Playing); var stats = new List(); Exception caught = null; NativeWebRTC.RetrieveStatsCallback cb = (category_, prop, _) => { try { stats.Add(new WebRTCStatistics(category_, prop)); } catch (Exception e) { caught = e; return false; } return true; }; using (var cbKeeper = ObjectKeeper.Get(cb)) { NativeWebRTC.ForeachStats(Handle, (int)category, cb, IntPtr.Zero). ThrowIfFailed("failed to retrieve stats"); if (caught != null) { throw caught; } } return new ReadOnlyCollection(stats); } /// /// Represents WebRTC statistics information. /// /// 10 public class WebRTCStatistics { internal WebRTCStatistics(WebRTCStatisticsCategory type, IntPtr prop) { var unmanagedStruct = Marshal.PtrToStructure(prop); Category = type; Name = unmanagedStruct.name; Property = unmanagedStruct.property; switch (unmanagedStruct.propertyType) { case WebRTCStatsPropertyType.TypeBool: Value = unmanagedStruct.value.@bool; break; case WebRTCStatsPropertyType.TypeInt: Value = unmanagedStruct.value.@int; break; case WebRTCStatsPropertyType.TypeUint: Value = unmanagedStruct.value.@uint; break; case WebRTCStatsPropertyType.TypeInt64: Value = unmanagedStruct.value.@long; break; case WebRTCStatsPropertyType.TypeUint64: Value = unmanagedStruct.value.@ulong; break; case WebRTCStatsPropertyType.TypeFloat: Value = unmanagedStruct.value.@float; break; case WebRTCStatsPropertyType.TypeDouble: Value = unmanagedStruct.value.@double; break; case WebRTCStatsPropertyType.TypeString: Value = Marshal.PtrToStringAnsi(unmanagedStruct.value.@string); break; default: throw new InvalidOperationException($"No matching type [{unmanagedStruct.propertyType}]"); } } /// /// Gets the category of statistics. /// /// The category of WebRTC statistics information /// 10 public WebRTCStatisticsCategory Category { get; } /// /// Gets the name of statistics. /// /// The name of WebRTC statistics information /// 10 public string Name { get; } /// /// Gets the property of statistics. /// /// The property of WebRTC statistics information /// 10 public WebRTCStatisticsProperty Property { get; } /// /// Gets the value of statistics. /// /// The value of WebRTC statistics information /// 10 public object Value { get; } /// /// Returns a string that represents the current object. /// /// A string that represents the current object. /// 10 public override string ToString() => $"Category={Category}, Name={Name}, Property={Property}, Value={Value}, Type={Value.GetType()}"; } } }