/// <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 = Peer.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 = Peer.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(Peer.CreateOffer());
        }