/*
* 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()}";
}
}
}