// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Concurrent;
using System.Text;
using System.Runtime.CompilerServices;

#if UNITY_WSA && !UNITY_EDITOR
using global::Windows.UI.Core;
using global::Windows.Foundation;
using global::Windows.Media.Core;
using global::Windows.Media.Capture;
using global::Windows.ApplicationModel.Core;
#endif

[assembly: InternalsVisibleTo("Microsoft.MixedReality.WebRTC.Unity.Tests.Runtime")]

namespace Microsoft.MixedReality.WebRTC.Unity
{
    /// <summary>
    /// Enumeration of the different types of ICE servers.
    /// </summary>
    public enum IceType
    {
        /// <summary>
        /// Indicates there is no ICE information
        /// </summary>
        /// <remarks>
        /// Under normal use, this should not be used
        /// </remarks>
        None = 0,

        /// <summary>
        /// Indicates ICE information is of type STUN
        /// </summary>
        /// <remarks>
        /// https://en.wikipedia.org/wiki/STUN
        /// </remarks>
        Stun,

        /// <summary>
        /// Indicates ICE information is of type TURN
        /// </summary>
        /// <remarks>
        /// https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT
        /// </remarks>
        Turn
    }

    /// <summary>
    /// ICE server as a serializable data structure for the Unity inspector.
    /// </summary>
    [Serializable]
    public struct ConfigurableIceServer
    {
        /// <summary>
        /// The type of ICE server.
        /// </summary>
        [Tooltip("Type of ICE server")]
        public IceType Type;

        /// <summary>
        /// The unqualified URI of the server.
        /// </summary>
        /// <remarks>
        /// The URI must not have any <c>stun:</c> or <c>turn:</c> prefix.
        /// </remarks>
        [Tooltip("ICE server URI, without any stun: or turn: prefix.")]
        public string Uri;

        /// <summary>
        /// Convert the server to the representation the underlying implementation use.
        /// </summary>
        /// <returns>The stringified server information.</returns>
        public override string ToString()
        {
            return string.Format("{0}:{1}", Type.ToString().ToLowerInvariant(), Uri);
        }
    }

    /// <summary>
    /// A <a href="https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html">UnityEvent</a> that represents a WebRTC error event.
    /// </summary>
    [Serializable]
    public class WebRTCErrorEvent : UnityEvent<string>
    {
    }

    /// <summary>
    /// Exception thrown when an invalid transceiver media kind was detected, generally when trying to pair a
    /// transceiver of one media kind with a media line of a different media kind.
    /// </summary>
    public class InvalidTransceiverMediaKindException : Exception
    {
        /// <inheritdoc/>
        public InvalidTransceiverMediaKindException()
            : base("Invalid transceiver kind.")
        {
        }

        /// <inheritdoc/>
        public InvalidTransceiverMediaKindException(string message)
            : base(message)
        {
        }

        /// <inheritdoc/>
        public InvalidTransceiverMediaKindException(string message, Exception inner)
            : base(message, inner)
        {
        }
    }

    /// <summary>
    /// High-level wrapper for Unity WebRTC functionalities.
    /// This is the API entry point for establishing a connection with a remote peer.
    /// </summary>
    /// <remarks>
    /// The component initializes the underlying <see cref="WebRTC.PeerConnection"/> asynchronously
    /// when enabled, and closes it when disabled. The <see cref="OnInitialized"/> event is called
    /// when the connection object is ready to be used. Call <see cref="StartConnection"/>
    /// to create an offer for a remote peer.
    /// </remarks>
    [AddComponentMenu("MixedReality-WebRTC/Peer Connection")]
    public class PeerConnection : WorkQueue, ISerializationCallbackReceiver
    {
        /// <summary>
        /// Retrieves the underlying peer connection object once initialized.
        /// </summary>
        /// <remarks>
        /// If <see cref="OnInitialized"/> has not fired, this will be <c>null</c>.
        /// </remarks>
        public WebRTC.PeerConnection Peer { get; private set; } = null;


        #region Behavior settings

        /// <summary>
        /// Automatically create a new offer whenever a renegotiation needed event is received.
        /// </summary>
        /// <remarks>
        /// Note that the renegotiation needed event may be dispatched asynchronously, so it is
        /// discourages to toggle this field ON and OFF. Instead, the user should choose an
        /// approach (manual or automatic) and stick to it.
        ///
        /// In particular, temporarily setting this to <c>false</c> during a batch of changes and
        /// setting it back to <c>true</c> right after the last change may or may not produce an
        /// automatic offer, depending on whether the negotiated event was dispatched while the
        /// property was still <c>false</c> or not.
        /// </remarks>
        [Tooltip("Automatically create a new offer when receiving a renegotiation needed event.")]
        [Editor.ToggleLeft]
        public bool AutoCreateOfferOnRenegotiationNeeded = true;

        /// <summary>
        /// Flag to log all errors to the Unity console automatically.
        /// </summary>
        [Tooltip("Automatically log all errors to the Unity console.")]
        [Editor.ToggleLeft]
        public bool AutoLogErrorsToUnityConsole = true;

        #endregion


        #region Interactive Connectivity Establishment (ICE)

        /// <summary>
        /// Set of ICE servers the WebRTC library will use to try to establish a connection.
        /// </summary>
        [Tooltip("Optional set of ICE servers (STUN and/or TURN)")]
        public List<ConfigurableIceServer> IceServers = new List<ConfigurableIceServer>()
        {
            new ConfigurableIceServer()
            {
                Type = IceType.Stun,
                Uri = "stun.l.google.com:19302"
            }
        };

        /// <summary>
        /// Optional username for the ICE servers.
        /// </summary>
        [Tooltip("Optional username for the ICE servers")]
        public string IceUsername;

        /// <summary>
        /// Optional credential for the ICE servers.
        /// </summary>
        [Tooltip("Optional credential for the ICE servers")]
        public string IceCredential;

        #endregion


        #region Events

        /// <summary>
        /// Event fired after the peer connection is initialized and ready for use.
        /// </summary>
        [Tooltip("Event fired after the peer connection is initialized and ready for use")]
        public UnityEvent OnInitialized = new UnityEvent();

        /// <summary>
        /// Event fired after the peer connection is shut down and cannot be used anymore.
        /// </summary>
        [Tooltip("Event fired after the peer connection is shut down and cannot be used anymore")]
        public UnityEvent OnShutdown = new UnityEvent();

        /// <summary>
        /// Event that occurs when a WebRTC error occurs
        /// </summary>
        [Tooltip("Event that occurs when a WebRTC error occurs")]
        public WebRTCErrorEvent OnError = new WebRTCErrorEvent();

        #endregion


        #region Private variables

        /// <summary>
        /// Underlying native peer connection wrapper.
        /// </summary>
        /// <remarks>
        /// Unlike the public <see cref="Peer"/> property, this is never <c>NULL</c>,
        /// but can be an uninitialized peer.
        /// </remarks>
        private WebRTC.PeerConnection _nativePeer = null;

        /// <summary>
        /// List of transceiver media lines and their associated media sender/receiver components.
        /// </summary>
        [SerializeField]
        private List<MediaLine> _mediaLines = new List<MediaLine>();

        // Indicates if Awake has been called. Used by media lines to figure out whether to
        // invoke callbacks or not.
        internal bool IsAwake { get; private set; }

        #endregion


        #region Public methods

        /// <summary>
        /// Enumerate the video capture devices available as a WebRTC local video feed source.
        /// </summary>
        /// <returns>The list of local video capture devices available to WebRTC.</returns>
        public static Task<IReadOnlyList<VideoCaptureDevice>> GetVideoCaptureDevicesAsync()
        {
            return DeviceVideoTrackSource.GetCaptureDevicesAsync();
        }

        /// <summary>
        /// Initialize the underlying WebRTC peer connection.
        /// </summary>
        /// <remarks>
        /// This method must be called once before using the peer connection. If <see cref="AutoInitializeOnStart"/>
        /// is <c>true</c> then it is automatically called during <a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.Start.html">MonoBehaviour.Start()</a>.
        ///
        /// This method is asynchronous and completes its task when the initializing completed.
        /// On successful completion, it also trigger the <see cref="OnInitialized"/> event.
        /// Note however that this completion is free-threaded and complete immediately when the
        /// underlying peer connection is initialized, whereas any <see cref="OnInitialized"/>
        /// event handler is invoked when control returns to the main Unity app thread. The former
        /// is faster, but does not allow accessing the underlying peer connection because it
        /// returns before <see cref="OnPostInitialize"/> executed. Therefore it is generally
        /// recommended to listen to the <see cref="OnInitialized"/> event, and ignore the returned
        /// <see xref="System.Threading.Tasks.Task"/> object.
        ///
        /// If the peer connection is already initialized, this method returns immediately with
        /// a <see xref="System.Threading.Tasks.Task.CompletedTask"/> object. The caller can check
        /// that the <see cref="Peer"/> property is non-<c>null</c> to confirm that the connection
        /// is in fact initialized.
        /// </remarks>
        private Task InitializeAsync(CancellationToken token = default(CancellationToken))
        {
            CreateNativePeerConnection();

            // Ensure Android binding is initialized before accessing the native implementation
            Android.Initialize();

#if UNITY_WSA && !UNITY_EDITOR
            if (UnityEngine.WSA.Application.RunningOnUIThread())
#endif
            {
                return RequestAccessAndInitAsync(token);
            }
#if UNITY_WSA && !UNITY_EDITOR
            else
            {
                UnityEngine.WSA.Application.InvokeOnUIThread(() => RequestAccessAndInitAsync(token), waitUntilDone: true);
                return Task.CompletedTask;
            }
#endif
        }

        /// <summary>
        /// Add a new media line of the given kind.
        ///
        /// This method creates a media line, which expresses an intent from the user to get a transceiver.
        /// The actual <see xref="WebRTC.Transceiver"/> object creation is delayed until a session
        /// negotiation is completed.
        ///
        /// Once the media line is created, the user can then assign its <see cref="MediaLine.Source"/> and
        /// <see cref="MediaLine.Receiver"/> properties to express their intent to send and/or receive some media
        /// through the transceiver that will be associated with that media line once a session is negotiated.
        /// This information is used in subsequent negotiations to derive a
        /// <see xref="Microsoft.MixedReality.WebRTC.Transceiver.Direction"/> to negotiate. Therefore users
        /// should avoid modifying the <see cref="Transceiver.DesiredDirection"/> property manually when using
        /// the Unity library, and instead modify the <see cref="MediaLine.Source"/> and
        /// <see cref="MediaLine.Receiver"/> properties.
        /// </summary>
        /// <param name="kind">The kind of media (audio or video) for the transceiver.</param>
        /// <returns>A newly created media line, which will be associated with a transceiver once the next session
        /// is negotiated.</returns>
        public MediaLine AddMediaLine(MediaKind kind)
        {
            var ml = new MediaLine(this, kind);
            _mediaLines.Add(ml);
            return ml;
        }

        /// <summary>
        /// Create a new connection offer, either for a first connection to the remote peer, or for
        /// renegotiating some new or removed transceivers.
        ///
        /// This method submits an internal task to create an SDP offer message. Once the message is
        /// created, the implementation raises the <see xref="Microsoft.MixedReality.WebRTC.PeerConnection.LocalSdpReadytoSend"/>
        /// event to allow the user to send the message via the chosen signaling solution to the remote
        /// peer.
        ///
        /// <div class="IMPORTANT alert alert-important">
        /// <h5>IMPORTANT</h5>
        /// <p>
        /// This method is very similar to the <c>CreateOffer()</c> method available in the underlying C# library,
        /// and actually calls it. However it also performs additional work in order to pair the transceivers of
        /// the local and remote peer. Therefore Unity applications must call this method instead of the C# library
        /// one to ensure transceiver pairing works as intended.
        /// </p>
        /// </div>
        /// </summary>
        /// <returns>
        /// <c>true</c> if the offer creation task was submitted successfully, and <c>false</c> otherwise.
        /// The offer SDP message is always created asynchronously.
        /// </returns>
        /// <remarks>
        /// This method can only be called from the main Unity application thread, where Unity objects can
        /// be safely accessed.
        /// </remarks>
        public bool StartConnection()
        {
            // MediaLine manipulates some MonoBehaviour objects when managing senders and receivers
            EnsureIsMainAppThread();

            if (Peer == null)
            {
                throw new InvalidOperationException("Cannot create an offer with an uninitialized peer.");
            }

            // Batch all changes into a single offer
            AutoCreateOfferOnRenegotiationNeeded = false;

            // Add all new transceivers for local tracks. Since transceivers are only paired by negotiated mid,
            // we need to know which peer sends the offer before adding the transceivers on the offering side only,
            // and then pair them on the receiving side. Otherwise they are duplicated, as the transceiver mid from
            // locally-created transceivers is not negotiated yet, so ApplyRemoteDescriptionAsync() won't be able
            // to find them and will re-create a new set of transceivers, leading to duplicates.
            // So we wait until we know this peer is the offering side, and add transceivers to it right before
            // creating an offer. The remote peer will then match the transceivers by index after it applied the offer,
            // then add any missing one.

            // Update all transceivers, whether previously existing or just created above
            var transceivers = _nativePeer.Transceivers;
            int index = 0;
            foreach (var mediaLine in _mediaLines)
            {
                // Ensure each media line has a transceiver
                Transceiver tr = mediaLine.Transceiver;
                if (tr != null)
                {
                    // Media line already had a transceiver from a previous session negotiation
                    Debug.Assert(tr.MlineIndex >= 0); // associated
                }
                else
                {
                    // Create new transceivers for a media line added since last session negotiation.

                    // Compute the transceiver desired direction based on what the local peer expects, both in terms
                    // of sending and in terms of receiving. Note that this means the remote peer will not be able to
                    // send any data if the local peer did not add a remote source first.
                    // Tracks are not tested explicitly since the local track can be swapped on-the-fly without renegotiation,
                    // and the remote track is generally not added yet at the beginning of the negotiation, but only when
                    // the remote description is applied (so for the offering side, at the end of the exchange when the
                    // answer is received).
                    bool wantsSend = (mediaLine.Source != null);
                    bool wantsRecv = (mediaLine.Receiver != null);
                    var wantsDir = Transceiver.DirectionFromSendRecv(wantsSend, wantsRecv);
                    var settings = new TransceiverInitSettings
                    {
                        Name = $"mrsw#{index}",
                        InitialDesiredDirection = wantsDir
                    };
                    tr = _nativePeer.AddTransceiver(mediaLine.MediaKind, settings);
                    try
                    {
                        mediaLine.PairTransceiver(tr);
                    }
                    catch (Exception ex)
                    {
                        LogErrorOnMediaLineException(ex, mediaLine, tr);
                    }
                }
                Debug.Assert(tr != null);
                Debug.Assert(transceivers[index] == tr);
                ++index;
            }

            // Create the offer
            AutoCreateOfferOnRenegotiationNeeded = true;
            return _nativePeer.CreateOffer();
        }

        /// <summary>
        /// Call <see cref="StartConnection"/> and discard the result. Can be wired to a <see cref="UnityEvent"/>.
        /// </summary>
        public void StartConnectionIgnoreError()
        {
            _ = StartConnection();
        }

        /// <summary>
        /// Pass the given SDP description received from the remote peer via signaling to the
        /// underlying WebRTC implementation, which will parse and use it.
        ///
        /// This must be called by the signaler when receiving a message. Once this operation
        /// has completed, it is safe to call <see xref="WebRTC.PeerConnection.CreateAnswer"/>.
        ///
        /// <div class="IMPORTANT alert alert-important">
        /// <h5>IMPORTANT</h5>
        /// <p>
        /// This method is very similar to the <c>SetRemoteDescriptionAsync()</c> method available in the
        /// underlying C# library, and actually calls it. However it also performs additional work in order
        /// to pair the transceivers of the local and remote peer. Therefore Unity applications must call
        /// this method instead of the C# library one to ensure transceiver pairing works as intended.
        /// </p>
        /// </div>
        /// </summary>
        /// <param name="message">The SDP message to handle.</param>
        /// <returns>A task which completes once the remote description has been applied and transceivers
        /// have been updated.</returns>
        /// <exception xref="InvalidOperationException">The peer connection is not intialized.</exception>
        /// <remarks>
        /// This method can only be called from the main Unity application thread, where Unity objects can
        /// be safely accessed.
        /// </remarks>
        public async Task HandleConnectionMessageAsync(SdpMessage message)
        {
            // MediaLine manipulates some MonoBehaviour objects when managing senders and receivers
            EnsureIsMainAppThread();

            if (!isActiveAndEnabled)
            {
                Debug.LogWarning("Message received by disabled PeerConnection");
                return;
            }

            // First apply the remote description
            try
            {
                await Peer.SetRemoteDescriptionAsync(message);
            }
            catch (Exception ex)
            {
                Debug.LogError($"Cannot apply remote description: {ex.Message}");
            }

            // Sort associated transceiver by media line index. The media line index is not the index of
            // the transceiver, but they are both monotonically increasing, so sorting by one or the other
            // yields the same ordered collection, which allows pairing transceivers and media lines.
            // TODO - Ensure PeerConnection.Transceivers is already sorted
            var transceivers = new List<Transceiver>(_nativePeer.AssociatedTransceivers);
            transceivers.Sort((tr1, tr2) => (tr1.MlineIndex - tr2.MlineIndex));
            int numAssociatedTransceivers = transceivers.Count;
            int numMatching = Math.Min(numAssociatedTransceivers, _mediaLines.Count);

            // Once applied, try to pair transceivers and remote tracks with the Unity receiver components
            if (message.Type == SdpMessageType.Offer)
            {
                // Match transceivers with media line, in order
                for (int i = 0; i < numMatching; ++i)
                {
                    var tr = transceivers[i];
                    var mediaLine = _mediaLines[i];
                    if (mediaLine.Transceiver == null)
                    {
                        mediaLine.PairTransceiver(tr);
                    }
                    else
                    {
                        Debug.Assert(tr == mediaLine.Transceiver);
                    }

                    // Associate the transceiver with the media line, if not already done, and associate
                    // the track components of the media line to the tracks of the transceiver.
                    try
                    {
                        mediaLine.UpdateAfterSdpReceived();
                    }
                    catch (Exception ex)
                    {
                        LogErrorOnMediaLineException(ex, mediaLine, tr);
                    }

                    // Check if the remote peer was planning to send something to this peer, but cannot.
                    bool wantsRecv = (mediaLine.Receiver != null);
                    if (!wantsRecv)
                    {
                        var desDir = tr.DesiredDirection;
                        if (Transceiver.HasRecv(desDir))
                        {
                            string peerName = name;
                            int idx = i;
                            InvokeOnAppThread(() => LogWarningOnMissingReceiver(peerName, idx));
                        }
                    }
                }

                // Ignore extra transceivers without a registered component to attach
                if (numMatching < numAssociatedTransceivers)
                {
                    string peerName = name;
                    InvokeOnAppThread(() =>
                    {
                        for (int i = numMatching; i < numAssociatedTransceivers; ++i)
                        {
                            LogWarningOnIgnoredTransceiver(peerName, i);
                        }
                    });
                }
            }
            else if (message.Type == SdpMessageType.Answer)
            {
                // Associate registered media senders/receivers with existing transceivers
                for (int i = 0; i < numMatching; ++i)
                {
                    Transceiver tr = transceivers[i];
                    var mediaLine = _mediaLines[i];
                    Debug.Assert(mediaLine.Transceiver == transceivers[i]);
                    mediaLine.UpdateAfterSdpReceived();
                }

                // Ignore extra transceivers without a registered component to attach
                if (numMatching < numAssociatedTransceivers)
                {
                    string peerName = name;
                    InvokeOnAppThread(() =>
                    {
                        for (int i = numMatching; i < numAssociatedTransceivers; ++i)
                        {
                            LogWarningOnIgnoredTransceiver(peerName, i);
                        }
                    });
                }
            }
        }

        /// <summary>
        /// Uninitialize the underlying WebRTC library, effectively cleaning up the allocated peer connection.
        /// </summary>
        /// <remarks>
        /// <see cref="Peer"/> will be <c>null</c> afterward.
        /// </remarks>
        private void Uninitialize()
        {
            Debug.Assert(_nativePeer.Initialized);
            // Fire signals before doing anything else to allow listeners to clean-up,
            // including un-registering any callback from the connection.
            OnShutdown.Invoke();

            // Prevent publicly accessing the native peer after it has been deinitialized.
            // This does not prevent systems caching a reference from accessing it, but it
            // is their responsibility to check that the peer is initialized.
            Peer = null;

            // Detach all transceivers. This prevents senders/receivers from trying to access
            // them during their clean-up sequence, as transceivers are about to be destroyed
            // by the native implementation.
            foreach (var mediaLine in _mediaLines)
            {
                mediaLine.UnpairTransceiver();
            }

            // Close the connection and release native resources.
            _nativePeer.Dispose();
            _nativePeer = null;
        }

        #endregion


        #region Unity MonoBehaviour methods

        protected override void Awake()
        {
            base.Awake();
            IsAwake = true;
            foreach (var ml in _mediaLines)
            {
                ml.Awake();
            }
        }

        /// <summary>
        /// Unity Engine OnEnable() hook
        /// </summary>
        /// <remarks>
        /// See <see href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnEnable.html"/>
        /// </remarks>
        private void OnEnable()
        {
            if (AutoLogErrorsToUnityConsole)
            {
                OnError.AddListener(OnError_Listener);
            }
            InitializeAsync();
        }

        /// <summary>
        /// Unity Engine OnDisable() hook
        /// </summary>
        /// <remarks>
        /// https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnDisable.html
        /// </remarks>
        private void OnDisable()
        {
            Uninitialize();
            OnError.RemoveListener(OnError_Listener);
        }

        private void OnDestroy()
        {
            foreach (var ml in _mediaLines)
            {
                ml.OnDestroy();
            }
        }

        #endregion


        #region Private implementation

        public void OnBeforeSerialize() { }

        public void OnAfterDeserialize()
        {
            foreach (var ml in _mediaLines)
            {
                ml.Peer = this;
            }
        }

        /// <summary>
        /// Create a new native peer connection and register event handlers to it.
        /// This does not initialize the peer connection yet.
        /// </summary>
        private void CreateNativePeerConnection()
        {
            // Create the peer connection managed wrapper and its native implementation
            _nativePeer = new WebRTC.PeerConnection();

            _nativePeer.AudioTrackAdded +=
                (RemoteAudioTrack track) =>
                {
                    // Tracks will be output by AudioReceivers, so avoid outputting them twice.
                    track.OutputToDevice(false);
                };
        }

        /// <summary>
        /// Internal helper to ensure device access and continue initialization.
        /// </summary>
        /// <remarks>
        /// On UWP this must be called from the main UI thread.
        /// </remarks>
        private Task RequestAccessAndInitAsync(CancellationToken token)
        {
#if UNITY_WSA && !UNITY_EDITOR
            // FIXME - Use ADM2 instead, this /maybe/ avoids this.
            // On UWP the app must have the "microphone" capability, and the user must allow microphone
            // access. This is due to the audio module (ADM1) being initialized at startup, even if no audio
            // track is used. Preventing access to audio crashes the ADM1 at startup and the entire application.
            var mediaAccessRequester = new MediaCapture();
            var mediaSettings = new MediaCaptureInitializationSettings();
            mediaSettings.AudioDeviceId = "";
            mediaSettings.VideoDeviceId = "";
            mediaSettings.StreamingCaptureMode = StreamingCaptureMode.Audio;
            mediaSettings.PhotoCaptureSource = PhotoCaptureSource.VideoPreview;
            mediaSettings.SharingMode = MediaCaptureSharingMode.SharedReadOnly; // for MRC and lower res camera
            var accessTask = mediaAccessRequester.InitializeAsync(mediaSettings).AsTask(token);
            return accessTask.ContinueWith(prevTask =>
            {
                token.ThrowIfCancellationRequested();

                if (prevTask.Exception == null)
                {
                    InitializePluginAsync(token);
                }
                else
                {
                    var ex = prevTask.Exception;
                    InvokeOnAppThread(() => OnError.Invoke($"Audio access failure: {ex.Message}."));
                }
            }, token);
#else
            return InitializePluginAsync(token);
#endif
        }

        /// <summary>
        /// Internal handler to actually initialize the plugin.
        /// </summary>
        private Task InitializePluginAsync(CancellationToken token)
        {
            Debug.Log("Initializing WebRTC plugin...");
            var config = new PeerConnectionConfiguration();
            foreach (var server in IceServers)
            {
                config.IceServers.Add(new IceServer
                {
                    Urls = { server.ToString() },
                    TurnUserName = IceUsername,
                    TurnPassword = IceCredential
                });
            }
            return _nativePeer.InitializeAsync(config, token).ContinueWith((initTask) =>
            {
                token.ThrowIfCancellationRequested();

                Exception ex = initTask.Exception;
                if (ex != null)
                {
                    InvokeOnAppThread(() =>
                    {
                        var errorMessage = new StringBuilder();
                        errorMessage.Append("WebRTC plugin initializing failed. See full log for exception details.\n");
                        while (ex is AggregateException ae)
                        {
                            errorMessage.Append($"AggregationException: {ae.Message}\n");
                            ex = ae.InnerException;
                        }
                        errorMessage.Append($"Exception: {ex.Message}");
                        OnError.Invoke(errorMessage.ToString());
                    });
                    throw initTask.Exception;
                }

                InvokeOnAppThread(OnPostInitialize);
            }, token);
        }

        /// <summary>
        /// Callback fired on the main Unity app thread once the WebRTC plugin was initialized successfully.
        /// </summary>
        private void OnPostInitialize()
        {
            Debug.Log("WebRTC plugin initialized successfully.");

            if (AutoCreateOfferOnRenegotiationNeeded)
            {
                _nativePeer.RenegotiationNeeded += Peer_RenegotiationNeeded;
            }

            // Once the peer is initialized, it becomes publicly accessible.
            // This prevent scripts from accessing it before it is initialized.
            Peer = _nativePeer;

            OnInitialized.Invoke();
        }

        private void Peer_RenegotiationNeeded()
        {
            // If already connected, update the connection on the fly.
            // If not, wait for user action and don't automatically connect.
            if (AutoCreateOfferOnRenegotiationNeeded && _nativePeer.IsConnected)
            {
                // Defer to the main app thread, because this implementation likely will
                // again trigger the renegotiation needed event, which is not re-entrant.
                // This also allows accessing Unity objects, and makes it safer in general
                // for other objects.
                InvokeOnAppThread(() => StartConnection());
            }
        }

        /// <summary>
        /// Internal handler for on-error, if <see cref="AutoLogErrorsToUnityConsole"/> is <c>true</c>
        /// </summary>
        /// <param name="error">The error message</param>
        private void OnError_Listener(string error)
        {
            Debug.LogError(error);
        }

        /// <summary>
        /// Log an error when receiving an exception related to a media line and transceiver pairing.
        /// </summary>
        /// <param name="ex">The exception to log.</param>
        /// <param name="mediaLine">The media line associated with the exception.</param>
        /// <param name="transceiver">The transceiver associated with the exception.</param>
        private void LogErrorOnMediaLineException(Exception ex, MediaLine mediaLine, Transceiver transceiver)
        {
            // Dispatch to main thread to access Unity objects to get their names
            InvokeOnAppThread(() =>
            {
                string msg;
                if (ex is InvalidTransceiverMediaKindException)
                {
                    msg = $"Peer connection \"{name}\" received {transceiver.MediaKind} transceiver #{transceiver.MlineIndex} \"{transceiver.Name}\", but local peer expected some {mediaLine.MediaKind} transceiver instead.";
                    if (mediaLine.Source != null)
                    {
                        msg += $" Sender \"{(mediaLine.Source as MonoBehaviour).name}\" will be ignored.";
                    }
                    if (mediaLine.Receiver != null)
                    {
                        msg += $" Receiver \"{(mediaLine.Receiver as MonoBehaviour).name}\" will be ignored.";
                    }
                }
                else
                {
                    // Generic exception, log its message
                    msg = ex.Message;
                }
                Debug.LogError(msg);
            });
        }

        private void LogWarningOnMissingReceiver(string peerName, int trIndex)
        {
            Debug.LogWarning($"The remote peer connected to the local peer connection '{peerName}' offered to send some media"
                + $" through transceiver #{trIndex}, but the local peer connection '{peerName}' has no receiver component to"
                + " process this media. The remote peer's media will be ignored. To be able to receive that media, ensure that"
                + $" the local peer connection '{peerName}' has a receiver component associated with its transceiver #{trIndex}.");
        }

        private void LogWarningOnIgnoredTransceiver(string peerName, int trIndex)
        {
            Debug.LogWarning($"The remote peer connected to the local peer connection '{peerName}' has transceiver #{trIndex},"
                + " but the local peer connection doesn't have a local transceiver to pair with it. The remote peer's media for"
                + " this transceiver will be ignored. To be able to receive that media, ensure that the local peer connection"
                + $" '{peerName}' has transceiver #{trIndex} and a receiver component associated with it.");
        }

        #endregion
    }
}