// Copyright 2023 LiveKit, Inc.
//
// 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.

package rtc

import (
	"context"
	"fmt"
	"io"
	"maps"
	"math/rand"
	"net"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/pion/dtls/v3/pkg/crypto/elliptic"
	"github.com/pion/ice/v4"
	"github.com/pion/interceptor"
	"github.com/pion/interceptor/pkg/cc"
	"github.com/pion/interceptor/pkg/gcc"
	"github.com/pion/interceptor/pkg/twcc"
	"github.com/pion/rtcp"
	"github.com/pion/sdp/v3"
	"github.com/pion/webrtc/v4"
	"github.com/pkg/errors"
	"go.uber.org/atomic"
	"go.uber.org/zap/zapcore"
	"google.golang.org/protobuf/proto"

	"github.com/livekit/livekit-server/pkg/config"
	"github.com/livekit/livekit-server/pkg/rtc/transport"
	"github.com/livekit/livekit-server/pkg/rtc/types"
	"github.com/livekit/livekit-server/pkg/sfu/buffer"
	"github.com/livekit/livekit-server/pkg/sfu/bwe"
	"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
	"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
	"github.com/livekit/livekit-server/pkg/sfu/datachannel"
	sfuinterceptor "github.com/livekit/livekit-server/pkg/sfu/interceptor"
	"github.com/livekit/livekit-server/pkg/sfu/pacer"
	pd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/playoutdelay"
	"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
	sfuutils "github.com/livekit/livekit-server/pkg/sfu/utils"
	"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
	"github.com/livekit/livekit-server/pkg/utils"
	lkinterceptor "github.com/livekit/mediatransportutil/pkg/interceptor"
	"github.com/livekit/mediatransportutil/pkg/rtcconfig"
	lktwcc "github.com/livekit/mediatransportutil/pkg/twcc"
	"github.com/livekit/protocol/codecs/mime"
	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/logger"
	"github.com/livekit/protocol/logger/pionlogger"
	lksdp "github.com/livekit/protocol/sdp"
	"github.com/livekit/protocol/utils/mono"
)

const (
	LossyDataChannel     = "_lossy"
	ReliableDataChannel  = "_reliable"
	DataTrackDataChannel = "_data_track"

	fastNegotiationFrequency   = 10 * time.Millisecond
	negotiationFrequency       = 150 * time.Millisecond
	negotiationFailedTimeout   = 15 * time.Second
	dtlsRetransmissionInterval = 100 * time.Millisecond
	dtlsHandshakeTimeout       = time.Minute

	iceDisconnectedTimeout = 10 * time.Second                          // compatible for ice-lite with firefox client
	iceFailedTimeout       = 5 * time.Second                           // time between disconnected and failed
	iceFailedTimeoutTotal  = iceFailedTimeout + iceDisconnectedTimeout // total time between connecting and failure
	iceKeepaliveInterval   = 2 * time.Second                           // pion's default

	minTcpICEConnectTimeout = 5 * time.Second
	maxTcpICEConnectTimeout = 12 * time.Second // js-sdk has a default 15s timeout for first connection, let server detect failure earlier before that

	minConnectTimeoutAfterICE = 10 * time.Second
	maxConnectTimeoutAfterICE = 20 * time.Second // max duration for waiting pc to connect after ICE is connected

	shortConnectionThreshold = 90 * time.Second

	dataChannelBufferSize             = 65535
	lossyDataChannelMinBufferedAmount = 8 * 1024
)

var (
	ErrNoICETransport                    = errors.New("no ICE transport")
	ErrIceRestartWithoutLocalSDP         = errors.New("ICE restart without local SDP settled")
	ErrIceRestartOnClosedPeerConnection  = errors.New("ICE restart on closed peer connection")
	ErrNoTransceiver                     = errors.New("no transceiver")
	ErrNoSender                          = errors.New("no sender")
	ErrMidNotFound                       = errors.New("mid not found")
	ErrNotSynchronousLocalCandidatesMode = errors.New("not using synchronous local candidates mode")
	ErrNoRemoteDescription               = errors.New("no remote description")
	ErrNoLocalDescription                = errors.New("no local description")
	ErrInvalidSDPFragment                = errors.New("invalid sdp fragment")
	ErrNoBundleMid                       = errors.New("could not get bundle mid")
	ErrMidMismatch                       = errors.New("media mid does not match bundle mid")
	ErrICECredentialMismatch             = errors.New("ice credential mismatch")
)

// -------------------------------------------------------------------------

type signal int

const (
	signalICEGatheringComplete signal = iota
	signalLocalICECandidate
	signalRemoteICECandidate
	signalSendOffer
	signalRemoteDescriptionReceived
	signalICERestart
)

func (s signal) String() string {
	switch s {
	case signalICEGatheringComplete:
		return "ICE_GATHERING_COMPLETE"
	case signalLocalICECandidate:
		return "LOCAL_ICE_CANDIDATE"
	case signalRemoteICECandidate:
		return "REMOTE_ICE_CANDIDATE"
	case signalSendOffer:
		return "SEND_OFFER"
	case signalRemoteDescriptionReceived:
		return "REMOTE_DESCRIPTION_RECEIVED"
	case signalICERestart:
		return "ICE_RESTART"
	default:
		return fmt.Sprintf("%d", int(s))
	}
}

// -------------------------------------------------------

type event struct {
	*PCTransport
	signal signal
	data   any
}

func (e event) String() string {
	return fmt.Sprintf("PCTransport:Event{signal: %s, data: %+v}", e.signal, e.data)
}

// -------------------------------------------------------

type wrappedICECandidatePairLogger struct {
	pair *webrtc.ICECandidatePair
}

func (w wrappedICECandidatePairLogger) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if w.pair == nil {
		return nil
	}

	if w.pair.Local != nil {
		e.AddString("localProtocol", w.pair.Local.Protocol.String())
		e.AddString("localCandidateType", w.pair.Local.Typ.String())
		e.AddString("localAddress", w.pair.Local.Address)
		e.AddUint16("localPort", w.pair.Local.Port)
	}
	if w.pair.Remote != nil {
		e.AddString("remoteProtocol", w.pair.Remote.Protocol.String())
		e.AddString("remoteCandidateType", w.pair.Remote.Typ.String())
		e.AddString("remoteAddress", MaybeTruncateIP(w.pair.Remote.Address))
		e.AddUint16("remotePort", w.pair.Remote.Port)
		if w.pair.Remote.RelatedAddress != "" {
			e.AddString("relatedAddress", MaybeTruncateIP(w.pair.Remote.RelatedAddress))
			e.AddUint16("relatedPort", w.pair.Remote.RelatedPort)
		}
	}
	return nil
}

// -------------------------------------------------------------------

type trackDescription struct {
	mid    string
	sender *webrtc.RTPSender
}

func (t trackDescription) MarshalLogObject(e zapcore.ObjectEncoder) error {
	e.AddString("mid", t.mid)
	if t.sender != nil {
		track := t.sender.Track()
		if track != nil {
			e.AddString("trackID", track.ID())
		}
	}
	return nil
}

// -------------------------------------------------------------------

// PCTransport is a wrapper around PeerConnection, with some helper methods
type PCTransport struct {
	params       TransportParams
	pc           *webrtc.PeerConnection
	iceTransport *webrtc.ICETransport
	me           *webrtc.MediaEngine

	lock sync.RWMutex

	firstOfferReceived      bool
	firstOfferNoDataChannel bool
	reliableDC              *datachannel.DataChannelWriter[*webrtc.DataChannel]
	reliableDCOpened        bool
	lossyDC                 *datachannel.DataChannelWriter[*webrtc.DataChannel]
	lossyDCOpened           bool
	dataTrackDC             *datachannel.DataChannelWriter[*webrtc.DataChannel]
	unlabeledDataChannels   []*datachannel.DataChannelWriter[*webrtc.DataChannel]

	iceFirstStartedAt              time.Time
	iceFirstConnectedAt            time.Time
	peerConnectionFirstConnectedAt time.Time
	peerConnectionLastconnectedAt  time.Time
	tcpICETimer                    *time.Timer
	connectAfterICETimer           *time.Timer // timer to wait for pc to connect after ice connected
	resetShortConnOnICERestart     atomic.Bool
	signalingRTT                   atomic.Uint32 // milliseconds

	hasFullyEstablishedRecorded bool

	debouncedNegotiate *sfuutils.Debouncer
	debouncePending    bool
	lastNegotiate      time.Time

	onNegotiationStateChanged func(state transport.NegotiationState)

	rtxInfoExtractorFactory *sfuinterceptor.RTXInfoExtractorFactory

	// stream allocator for subscriber PC
	streamAllocator *streamallocator.StreamAllocator

	// only for subscriber PC
	bwe   bwe.BWE
	pacer pacer.Pacer

	// transceivers (senders) waiting for SetRemoteDescription (offer) to happen before
	// SetCodecPreferences can be invoked on them.
	// Pion adapts codecs/payload types from remote description.
	// If SetCodecPreferences are done before the remote description is processed,
	// it is possible that the transceiver gets payload types from media engine.
	// Subssequently if the peer sends an offer with different payload type for the
	// same codec, there could be two payload types for the same codec and the wrong
	// one could be used in the forwarding path. So, wait for `SetRemoteDescription`
	// to happen so that remote side payload types are adapted.
	sendersPendingConfigMu sync.Mutex
	sendersPendingConfig   []configureSenderParams

	previousAnswer *webrtc.SessionDescription
	// track id -> description map in previous offer sdp
	previousTrackDescription map[string]*trackDescription
	canReuseTransceiver      bool

	preferTCP atomic.Bool
	isClosed  atomic.Bool

	// used to check for offer/answer pairing,
	// i. e. every offer should have an answer before another offer can be sent
	localOfferId   atomic.Uint32
	remoteAnswerId atomic.Uint32

	remoteOfferId atomic.Uint32
	localAnswerId atomic.Uint32

	eventsQueue *utils.TypedOpsQueue[event]

	connectionDetails      *types.ICEConnectionDetails
	selectedPair           atomic.Pointer[webrtc.ICECandidatePair]
	mayFailedICEStats      []iceCandidatePairStats
	mayFailedICEStatsTimer *time.Timer

	numOutstandingAudios uint32
	numRequestSentAudios uint32
	numOutstandingVideos uint32
	numRequestSentVideos uint32

	// the following should be accessed only in event processing go routine
	cacheLocalCandidates      bool
	cachedLocalCandidates     []*webrtc.ICECandidate
	pendingRemoteCandidates   []*webrtc.ICECandidateInit
	restartAfterGathering     bool
	restartAtNextOffer        bool
	negotiationState          transport.NegotiationState
	negotiateCounter          atomic.Int32
	signalStateCheckTimer     *time.Timer
	currentOfferIceCredential string // ice user:pwd, for publish side ice restart checking
	pendingRestartIceOffer    *webrtc.SessionDescription
}

type TransportParams struct {
	Handler                           transport.Handler
	ProtocolVersion                   types.ProtocolVersion
	Config                            *WebRTCConfig
	Twcc                              *lktwcc.Responder
	DirectionConfig                   DirectionConfig
	CongestionControlConfig           config.CongestionControlConfig
	EnabledPublishCodecs              []*livekit.Codec
	EnabledSubscribeCodecs            []*livekit.Codec
	Logger                            logger.Logger
	Transport                         livekit.SignalTarget
	SimTracks                         map[uint32]sfuinterceptor.SimulcastTrackInfo
	ClientInfo                        ClientInfo
	IsOfferer                         bool
	IsSendSide                        bool
	AllowPlayoutDelay                 bool
	UseOneShotSignallingMode          bool
	ExcludeIPv6LocalCandidates        bool
	FireOnTrackBySdp                  bool
	DataChannelMaxBufferedAmount      uint64
	DatachannelSlowThreshold          int
	DatachannelLossyTargetLatency     time.Duration
	DatachannelDataTrackTargetLatency time.Duration

	// for development test
	DatachannelMaxReceiverBufferSize int

	EnableDataTracks bool
	EnableWarp       bool
}

func newPeerConnection(
	params TransportParams,
	onBandwidthEstimator func(estimator cc.BandwidthEstimator),
) (*webrtc.PeerConnection, *webrtc.MediaEngine, *sfuinterceptor.RTXInfoExtractorFactory, error) {
	directionConfig := params.DirectionConfig
	if params.AllowPlayoutDelay {
		directionConfig.RTPHeaderExtension.Video = append(directionConfig.RTPHeaderExtension.Video, pd.PlayoutDelayURI)
	}

	// Some of the browser clients do not handle H.264 High Profile in signalling properly.
	// They still decode if the actual stream is H.264 High Profile, but do not handle it well in signalling.
	// So, disable H.264 High Profile for SUBSCRIBER peer connection to ensure it is not offered.
	//
	// Single-PC mode registers the union of publish and subscribe codecs so subscriptions
	// can negotiate subscribe-only codecs; per-direction filtering happens at the transceiver
	// level (configureSenderCodecs, restrictReceiverCodecsToPublishList).
	me, err := createMediaEngine(mergeCodecsByMime(params.EnabledPublishCodecs, params.EnabledSubscribeCodecs), directionConfig, params.IsOfferer)
	if err != nil {
		return nil, nil, nil, err
	}

	se := params.Config.SettingEngine
	se.DisableMediaEngineCopy(true)
	// simulcast layer disable/enable signalled via signalling channel,
	// so disable rid pause in SDP
	se.SetIgnoreRidPauseForRecv(true)

	// Change elliptic curve to improve connectivity
	// https://github.com/pion/dtls/pull/474
	se.SetDTLSEllipticCurves(elliptic.X25519, elliptic.P384, elliptic.P256)

	// Disable close by dtls to avoid peerconnection close too early in migration
	// https://github.com/pion/webrtc/pull/2961
	se.DisableCloseByDTLS(true)

	se.DetachDataChannels()
	if params.DatachannelSlowThreshold > 0 {
		se.EnableDataChannelBlockWrite(true)
	}
	if params.DatachannelMaxReceiverBufferSize > 0 {
		se.SetSCTPMaxReceiveBufferSize(uint32(params.DatachannelMaxReceiverBufferSize))
	}
	if params.FireOnTrackBySdp {
		se.SetFireOnTrackBeforeFirstRTP(true)
	}

	if params.ClientInfo.SupportsSctpZeroChecksum() {
		se.EnableSCTPZeroChecksum(true)
	}

	if params.EnableWarp {
		se.EnableSped(true)
		se.EnableSctpSnap(true)
	}

	//
	// Disable SRTP replay protection (https://datatracker.ietf.org/doc/html/rfc3711#page-15).
	// Needed due to lack of RTX stream support in Pion.
	//
	// When clients probe for bandwidth, there are several possible approaches
	//   1. Use padding packet (Chrome uses this)
	//   2. Use an older packet (Firefox uses this)
	// Typically, these are sent over the RTX stream and hence SRTP replay protection will not
	// trigger. As Pion does not support RTX, when firefox uses older packet for probing, they
	// trigger the replay protection.
	//
	// That results in two issues
	//   - Firefox bandwidth probing is not successful
	//   - Pion runs out of read buffer capacity - this potentially looks like a Pion issue
	//
	// NOTE: It is not required to disable RTCP replay protection, but doing it to be symmetric.
	//
	se.DisableSRTPReplayProtection(true)
	se.DisableSRTCPReplayProtection(true)
	if !params.ProtocolVersion.SupportsICELite() || !params.ClientInfo.SupportsPrflxOverRelay() {
		// if client don't support prflx over relay which is only Firefox, disable ICE Lite to ensure that
		// aggressive nomination is handled properly. Firefox does aggressive nomination even if peer is
		// ICE Lite (see comment as to historical reasons: https://github.com/pion/ice/pull/739#issuecomment-2452245066).
		// pion/ice (as of v2.3.37) will accept all use-candidate switches when in ICE Lite mode.
		// That combined with aggressive nomination from Firefox could potentially lead to the two ends
		// ending up with different candidates.
		// As Firefox does not support migration, ICE Lite can be disabled.
		se.SetLite(false)
	}
	se.SetDTLSRetransmissionInterval(dtlsRetransmissionInterval)
	se.SetDTLSConnectContextMaker(func() (context.Context, func()) {
		return context.WithTimeout(context.Background(), dtlsHandshakeTimeout)
	})
	se.SetICETimeouts(iceDisconnectedTimeout, iceFailedTimeout, iceKeepaliveInterval)

	// if client don't support prflx over relay, we should not expose private address to it, use single external ip as host candidate
	if !params.ClientInfo.SupportsPrflxOverRelay() && len(params.Config.NAT1To1IPs) > 0 {
		var nat1to1Ips []string
		var includeIps []string
		for _, mapping := range params.Config.NAT1To1IPs {
			if ips := strings.Split(mapping, "/"); len(ips) == 2 {
				if ips[0] != ips[1] {
					nat1to1Ips = append(nat1to1Ips, mapping)
					includeIps = append(includeIps, ips[1])
				}
			}
		}
		if len(nat1to1Ips) > 0 {
			params.Logger.Infow("client doesn't support prflx over relay, use external ip only as host candidate", "ips", nat1to1Ips)
			if err := rtcconfig.SetNAT1To1AddressRewriteRules(&se, nat1to1Ips, false); err != nil {
				params.Logger.Warnw("failed to set ICE address rewrite rules", err, "ips", nat1to1Ips)
			}
			se.SetIPFilter(func(ip net.IP) bool {
				if ip.To4() == nil {
					return true
				}
				ipstr := ip.String()
				return slices.Contains(includeIps, ipstr)
			})
		}
	}

	se.LoggerFactory = pionlogger.NewLoggerFactory(params.Logger)

	ir := &interceptor.Registry{}
	if params.IsSendSide {
		if params.CongestionControlConfig.UseSendSideBWEInterceptor && !params.CongestionControlConfig.UseSendSideBWE {
			params.Logger.Infow("using send side BWE - interceptor")
			gf, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) {
				return gcc.NewSendSideBWE(
					gcc.SendSideBWEInitialBitrate(1*1000*1000),
					gcc.SendSideBWEPacer(gcc.NewNoOpPacer()),
				)
			})
			if err == nil {
				gf.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) {
					if onBandwidthEstimator != nil {
						onBandwidthEstimator(estimator)
					}
				})
				ir.Add(gf)

				tf, err := twcc.NewHeaderExtensionInterceptor()
				if err == nil {
					ir.Add(tf)
				}
			}
		}
	}
	if !params.IsOfferer {
		// sfu only use interceptor to send XR but don't read response from it (use buffer instead),
		// so use a empty callback here
		ir.Add(lkinterceptor.NewRTTFromXRFactory(func(rtt uint32) {}))
	}
	if len(params.SimTracks) > 0 {
		f, err := sfuinterceptor.NewUnhandleSimulcastInterceptorFactory(sfuinterceptor.UnhandleSimulcastTracks(params.Logger, params.SimTracks))
		if err != nil {
			params.Logger.Warnw("NewUnhandleSimulcastInterceptorFactory failed", err)
		} else {
			ir.Add(f)
		}
	}

	setTWCCForVideo := func(info *interceptor.StreamInfo) {
		if !mime.IsMimeTypeStringVideo(info.MimeType) {
			return
		}
		// rtx stream don't have rtcp feedback, always set twcc for rtx stream
		twccFb := mime.GetMimeTypeCodec(info.MimeType) == mime.MimeTypeCodecRTX
		if !twccFb {
			for _, fb := range info.RTCPFeedback {
				if fb.Type == webrtc.TypeRTCPFBTransportCC {
					twccFb = true
					break
				}
			}
		}
		if !twccFb {
			return
		}

		twccExtID := sfuutils.GetHeaderExtensionID(info.RTPHeaderExtensions, webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI})
		if twccExtID != 0 {
			if buffer := params.Config.BufferFactory.GetBuffer(info.SSRC); buffer != nil {
				params.Logger.Debugw(
					"set twcc and ext id",
					"ssrc", info.SSRC,
					"isRTX", mime.GetMimeTypeCodec(info.MimeType) == mime.MimeTypeCodecRTX,
					"twccExtID", twccExtID,
				)
				buffer.SetTWCCAndExtID(params.Twcc, uint8(twccExtID))
			} else {
				params.Logger.Warnw("failed to get buffer for stream", nil, "ssrc", info.SSRC)
			}
		}
	}
	rtxInfoExtractorFactory := sfuinterceptor.NewRTXInfoExtractorFactory(
		setTWCCForVideo,
		func(repair, base uint32, rsid string) {
			params.Logger.Debugw("rtx pair found from extension", "repair", repair, "base", base, "rsid", rsid)
			params.Config.BufferFactory.SetRTXPair(repair, base, rsid)
		},
		params.Config.BufferFactory,
		params.SimTracks,
		params.Logger,
	)
	// put rtx interceptor behind unhandle simulcast interceptor so it can get the correct mid & rid
	ir.Add(rtxInfoExtractorFactory)

	api := webrtc.NewAPI(
		webrtc.WithMediaEngine(me),
		webrtc.WithSettingEngine(se),
		webrtc.WithInterceptorRegistry(ir),
	)
	pc, err := api.NewPeerConnection(params.Config.Configuration)
	return pc, me, rtxInfoExtractorFactory, err
}

func NewPCTransport(params TransportParams) (*PCTransport, error) {
	if params.Logger == nil {
		params.Logger = logger.GetLogger()
	}
	t := &PCTransport{
		params:             params,
		debouncedNegotiate: sfuutils.NewDebouncer(negotiationFrequency),
		negotiationState:   transport.NegotiationStateNone,
		eventsQueue: utils.NewTypedOpsQueue[event](utils.OpsQueueParams{
			Name:    "transport",
			MinSize: 64,
			Logger:  params.Logger,
		}),
		previousTrackDescription: make(map[string]*trackDescription),
		canReuseTransceiver:      true,
		connectionDetails:        types.NewICEConnectionDetails(params.Transport, params.Logger),
		lastNegotiate:            time.Now(),
	}
	t.localOfferId.Store(uint32(rand.Intn(1<<8) + 1))

	bwe, err := t.createPeerConnection()
	if err != nil {
		return nil, err
	}

	if params.IsSendSide {
		if params.CongestionControlConfig.UseSendSideBWE {
			params.Logger.Infow("using send side BWE", "pacerBehavior", params.CongestionControlConfig.SendSideBWEPacer)
			t.bwe = sendsidebwe.NewSendSideBWE(sendsidebwe.SendSideBWEParams{
				Config: params.CongestionControlConfig.SendSideBWE,
				Logger: params.Logger,
			})
			switch pacer.PacerBehavior(params.CongestionControlConfig.SendSideBWEPacer) {
			case pacer.PacerBehaviorPassThrough:
				t.pacer = pacer.NewPassThrough(params.Logger, t.bwe)
			case pacer.PacerBehaviorNoQueue:
				t.pacer = pacer.NewNoQueue(params.Logger, t.bwe)
			default:
				t.pacer = pacer.NewNoQueue(params.Logger, t.bwe)
			}
		} else {
			t.bwe = remotebwe.NewRemoteBWE(remotebwe.RemoteBWEParams{
				Config: params.CongestionControlConfig.RemoteBWE,
				Logger: params.Logger,
			})
			t.pacer = pacer.NewPassThrough(params.Logger, nil)
		}

		t.streamAllocator = streamallocator.NewStreamAllocator(streamallocator.StreamAllocatorParams{
			Config:    params.CongestionControlConfig.StreamAllocator,
			BWE:       t.bwe,
			Pacer:     t.pacer,
			RTTGetter: t.GetRTT,
			Logger:    params.Logger.WithComponent(utils.ComponentCongestionControl),
		}, params.CongestionControlConfig.Enabled, params.CongestionControlConfig.AllowPause)
		t.streamAllocator.OnStreamStateChange(params.Handler.OnStreamStateChange)
		t.streamAllocator.Start()

		if bwe != nil {
			t.streamAllocator.SetSendSideBWEInterceptor(bwe)
		}
	}

	t.eventsQueue.Start()

	return t, nil
}

func (t *PCTransport) createPeerConnection() (cc.BandwidthEstimator, error) {
	var bwe cc.BandwidthEstimator
	pc, me, rtxInfoExtractorFactory, err := newPeerConnection(t.params, func(estimator cc.BandwidthEstimator) {
		bwe = estimator
	})
	if err != nil {
		return bwe, err
	}

	t.pc = pc
	if !t.params.UseOneShotSignallingMode {
		// one shot signalling mode gathers all candidates and sends in answer
		t.pc.OnICEGatheringStateChange(t.onICEGatheringStateChange)
		t.pc.OnICECandidate(t.onICECandidateTrickle)
	}
	t.pc.OnICEConnectionStateChange(t.onICEConnectionStateChange)
	t.pc.OnConnectionStateChange(t.onPeerConnectionStateChange)

	t.pc.OnDataChannel(t.onDataChannel)
	t.pc.OnTrack(t.params.Handler.OnTrack)

	t.iceTransport = t.pc.SCTP().Transport().ICETransport()
	if t.iceTransport == nil {
		return bwe, ErrNoICETransport
	}
	t.iceTransport.OnSelectedCandidatePairChange(func(pair *webrtc.ICECandidatePair) {
		t.params.Logger.Debugw("selected ICE candidate pair changed", "pair", wrappedICECandidatePairLogger{pair})
		t.connectionDetails.SetSelectedPair(pair)
		existingPair := t.selectedPair.Load()
		if existingPair != nil {
			t.params.Logger.Infow(
				"ice reconnected or switched pair",
				"existingPair", wrappedICECandidatePairLogger{existingPair},
				"newPair", wrappedICECandidatePairLogger{pair})
		}
		t.selectedPair.Store(pair)
	})

	t.me = me

	t.rtxInfoExtractorFactory = rtxInfoExtractorFactory
	return bwe, nil
}

func (t *PCTransport) RTPStreamPublished(ssrc uint32, mid, rid string) {
	t.rtxInfoExtractorFactory.SetStreamInfo(ssrc, mid, rid, "")
}

func (t *PCTransport) GetPacer() pacer.Pacer {
	return t.pacer
}

func (t *PCTransport) SetSignalingRTT(rtt uint32) {
	t.signalingRTT.Store(rtt)
}

func (t *PCTransport) setICEStartedAt(at time.Time) {
	t.lock.Lock()
	if t.iceFirstStartedAt.IsZero() {
		t.iceFirstStartedAt = at

		// checklist of ice agent will be cleared on ice failed, get stats before that
		t.mayFailedICEStatsTimer = time.AfterFunc(iceFailedTimeoutTotal-time.Second, t.logMayFailedICEStats)

		// set failure timer for tcp ice connection based on signaling RTT
		if t.preferTCP.Load() {
			signalingRTT := t.signalingRTT.Load()
			if signalingRTT < 1000 {
				tcpICETimeout := time.Duration(signalingRTT*8) * time.Millisecond
				if tcpICETimeout < minTcpICEConnectTimeout {
					tcpICETimeout = minTcpICEConnectTimeout
				} else if tcpICETimeout > maxTcpICEConnectTimeout {
					tcpICETimeout = maxTcpICEConnectTimeout
				}
				t.params.Logger.Debugw("set TCP ICE connect timer", "timeout", tcpICETimeout, "signalRTT", signalingRTT)
				t.tcpICETimer = time.AfterFunc(tcpICETimeout, func() {
					if t.pc.ICEConnectionState() == webrtc.ICEConnectionStateChecking {
						t.params.Logger.Infow("TCP ICE connect timeout", "timeout", tcpICETimeout, "signalRTT", signalingRTT)
						t.logMayFailedICEStats()
						t.handleConnectionFailed(true)
					}
				})
			}
		}
	}
	t.lock.Unlock()
}

func (t *PCTransport) setICEConnectedAt(at time.Time) {
	t.lock.Lock()
	if t.iceFirstConnectedAt.IsZero() {
		//
		// Record initial connection time.
		// This prevents reset of iceFirstConnectedAt if ICE goes `Connected` -> `Disconnected` -> `Connected`.
		//
		t.iceFirstConnectedAt = at

		// set failure timer for dtls handshake
		iceDuration := at.Sub(t.iceFirstStartedAt)
		connTimeoutAfterICE := min(max(minConnectTimeoutAfterICE, 3*iceDuration), maxConnectTimeoutAfterICE)
		t.params.Logger.Debugw("setting connection timer after ICE connected", "timeout", connTimeoutAfterICE, "iceDuration", iceDuration)
		t.connectAfterICETimer = time.AfterFunc(connTimeoutAfterICE, func() {
			state := t.pc.ConnectionState()
			// if pc is still checking or connected but not fully established after timeout, then fire connection fail
			if state != webrtc.PeerConnectionStateClosed && state != webrtc.PeerConnectionStateFailed && !t.isFullyEstablished() {
				t.params.Logger.Infow("connect timeout after ICE connected", "timeout", connTimeoutAfterICE, "iceDuration", iceDuration)
				t.handleConnectionFailed(false)
			}
		})

		// clear tcp ice connect timer
		if t.tcpICETimer != nil {
			t.tcpICETimer.Stop()
			t.tcpICETimer = nil
		}

		prometheus.RecordPeerConnectionState(t.params.Transport, "ice_connected")
	}

	if t.mayFailedICEStatsTimer != nil {
		t.mayFailedICEStatsTimer.Stop()
		t.mayFailedICEStatsTimer = nil
	}
	t.mayFailedICEStats = nil
	t.lock.Unlock()
}

func (t *PCTransport) logMayFailedICEStats() {
	if t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
		return
	}

	var candidatePairStats []webrtc.ICECandidatePairStats
	pairStats := t.pc.GetStats()
	candidateStats := make(map[string]webrtc.ICECandidateStats)
	for _, stat := range pairStats {
		switch stat := stat.(type) {
		case webrtc.ICECandidatePairStats:
			candidatePairStats = append(candidatePairStats, stat)
		case webrtc.ICECandidateStats:
			candidateStats[stat.ID] = stat
		}
	}

	iceStats := make([]iceCandidatePairStats, 0, len(candidatePairStats))
	for _, pairStat := range candidatePairStats {
		iceStat := iceCandidatePairStats{ICECandidatePairStats: pairStat}
		if local, ok := candidateStats[pairStat.LocalCandidateID]; ok {
			iceStat.local = local
		}
		if remote, ok := candidateStats[pairStat.RemoteCandidateID]; ok {
			remote.IP = MaybeTruncateIP(remote.IP)
			iceStat.remote = remote
		}
		iceStats = append(iceStats, iceStat)
	}

	t.lock.Lock()
	t.mayFailedICEStats = iceStats
	t.lock.Unlock()
}

func (t *PCTransport) resetShortConn() {
	t.params.Logger.Infow("resetting short connection on ICE restart")
	t.lock.Lock()
	t.iceFirstStartedAt = time.Time{}
	t.iceFirstConnectedAt = time.Time{}
	t.peerConnectionLastconnectedAt = time.Time{}
	if t.connectAfterICETimer != nil {
		t.connectAfterICETimer.Stop()
		t.connectAfterICETimer = nil
	}
	if t.tcpICETimer != nil {
		t.tcpICETimer.Stop()
		t.tcpICETimer = nil
	}
	t.lock.Unlock()
}

func (t *PCTransport) IsShortConnection(at time.Time) (bool, time.Duration) {
	t.lock.RLock()
	defer t.lock.RUnlock()

	if t.iceFirstConnectedAt.IsZero() {
		return false, 0
	}

	duration := at.Sub(t.iceFirstConnectedAt)
	return duration < shortConnectionThreshold, duration
}

func (t *PCTransport) setPeerConnectionConnectedAt(at time.Time) bool {
	t.lock.Lock()
	t.peerConnectionLastconnectedAt = at
	if !t.peerConnectionFirstConnectedAt.IsZero() {
		t.lock.Unlock()
		return false
	}

	t.peerConnectionFirstConnectedAt = at
	prometheus.RecordServiceOperationSuccess("peer_connection")
	prometheus.RecordPeerConnectionState(t.params.Transport, "connected")
	t.lock.Unlock()
	return true
}

func (t *PCTransport) onICEGatheringStateChange(state webrtc.ICEGatheringState) {
	t.params.Logger.Debugw("ice gathering state change", "state", state.String())
	if state != webrtc.ICEGatheringStateComplete {
		return
	}

	t.postEvent(event{
		signal: signalICEGatheringComplete,
	})
}

func (t *PCTransport) onICECandidateTrickle(c *webrtc.ICECandidate) {
	t.postEvent(event{
		signal: signalLocalICECandidate,
		data:   c,
	})
}

func (t *PCTransport) handleConnectionFailed(forceShortConn bool) {
	isShort := forceShortConn
	if !isShort {
		var duration time.Duration
		isShort, duration = t.IsShortConnection(time.Now())
		if isShort {
			t.params.Logger.Debugw("short ICE connection", "pair", wrappedICECandidatePairLogger{t.selectedPair.Load()}, "duration", duration)
		}
	}

	t.params.Handler.OnFailed(isShort, t.GetICEConnectionInfo())
}

func (t *PCTransport) onICEConnectionStateChange(state webrtc.ICEConnectionState) {
	t.params.Logger.Debugw("ice connection state change", "state", state.String())
	switch state {
	case webrtc.ICEConnectionStateConnected:
		t.setICEConnectedAt(time.Now())

	case webrtc.ICEConnectionStateChecking:
		t.setICEStartedAt(time.Now())
	}
}

func (t *PCTransport) onPeerConnectionStateChange(state webrtc.PeerConnectionState) {
	t.params.Logger.Debugw("peer connection state change", "state", state.String())
	switch state {
	case webrtc.PeerConnectionStateConnected:
		t.clearConnTimer()
		isInitialConnection := t.setPeerConnectionConnectedAt(time.Now())
		if isInitialConnection {
			t.params.Handler.OnInitialConnected()

			t.maybeNotifyFullyEstablished()
		}
	case webrtc.PeerConnectionStateFailed:
		t.clearConnTimer()
		t.handleConnectionFailed(false)
	}
}

func (t *PCTransport) onDataChannel(dc *webrtc.DataChannel) {
	dc.OnOpen(func() {
		t.params.Logger.Debugw(dc.Label() + " data channel open")
		var kind livekit.DataPacket_Kind
		var isDataTrack bool
		var isUnlabeled bool
		switch dc.Label() {
		case ReliableDataChannel:
			kind = livekit.DataPacket_RELIABLE

		case LossyDataChannel:
			kind = livekit.DataPacket_LOSSY

		case DataTrackDataChannel:
			isDataTrack = true

		default:
			t.params.Logger.Infow("unlabeled datachannel added", "label", dc.Label())
			isUnlabeled = true
		}

		rawDC, err := dc.DetachWithDeadline()
		if err != nil {
			t.params.Logger.Errorw("failed to detach data channel", err, "label", dc.Label())
			return
		}

		isHandled := true
		t.lock.Lock()
		switch {
		case isUnlabeled:
			t.unlabeledDataChannels = append(
				t.unlabeledDataChannels,
				datachannel.NewDataChannelWriterReliable(dc, rawDC, t.params.DatachannelSlowThreshold),
			)

		case isDataTrack:
			if !t.params.EnableDataTracks {
				t.params.Logger.Debugw("data tracks not enabled")
				isHandled = false
			} else {
				if t.dataTrackDC != nil {
					t.dataTrackDC.Close()
				}
				t.dataTrackDC = datachannel.NewDataChannelWriterUnreliable(dc, rawDC, t.params.DatachannelDataTrackTargetLatency, uint64(lossyDataChannelMinBufferedAmount))
			}

		case kind == livekit.DataPacket_RELIABLE:
			if t.reliableDC != nil {
				t.reliableDC.Close()
			}
			t.reliableDC = datachannel.NewDataChannelWriterReliable(dc, rawDC, t.params.DatachannelSlowThreshold)
			t.reliableDCOpened = true

		case kind == livekit.DataPacket_LOSSY:
			if t.lossyDC != nil {
				t.lossyDC.Close()
			}
			t.lossyDC = datachannel.NewDataChannelWriterUnreliable(dc, rawDC, t.params.DatachannelLossyTargetLatency, uint64(lossyDataChannelMinBufferedAmount))
			t.lossyDCOpened = true
		}
		t.lock.Unlock()

		if !isHandled {
			rawDC.Close()
			return
		}

		go func() {
			defer rawDC.Close()
			buffer := make([]byte, dataChannelBufferSize)
			for {
				n, _, err := rawDC.ReadDataChannel(buffer)
				if err != nil {
					if !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "state=Closed") {
						t.params.Logger.Warnw("error reading data channel", err, "label", dc.Label())
					}
					return
				}

				switch {
				case isUnlabeled:
					t.params.Handler.OnDataMessageUnlabeled(buffer[:n])

				case isDataTrack:
					t.params.Handler.OnDataTrackMessage(buffer[:n], mono.UnixNano())

				default:
					t.params.Handler.OnDataMessage(kind, buffer[:n])
				}
			}
		}()

		t.maybeNotifyFullyEstablished()
	})
}

func (t *PCTransport) maybeNotifyFullyEstablished() {
	if t.isFullyEstablished() {
		t.params.Handler.OnFullyEstablished()

		t.lock.Lock()
		if !t.hasFullyEstablishedRecorded {
			t.hasFullyEstablishedRecorded = true
			prometheus.RecordPeerConnectionState(t.params.Transport, "fully_established")
		}
		t.lock.Unlock()
	}
}

func (t *PCTransport) isFullyEstablished() bool {
	t.lock.RLock()
	defer t.lock.RUnlock()

	dataChannelReady := t.params.UseOneShotSignallingMode || t.firstOfferNoDataChannel || (t.reliableDCOpened && t.lossyDCOpened)

	return dataChannelReady && !t.peerConnectionLastconnectedAt.IsZero()
}

func (t *PCTransport) SetPreferTCP(preferTCP bool) {
	t.preferTCP.Store(preferTCP)
}

func (t *PCTransport) AddICECandidate(candidate webrtc.ICECandidateInit) {
	t.postEvent(event{
		signal: signalRemoteICECandidate,
		data:   &candidate,
	})
}

func (t *PCTransport) queueOrConfigureSender(
	transceiver *webrtc.RTPTransceiver,
	enabledCodecs []*livekit.Codec,
	rtcpFeedbackConfig RTCPFeedbackConfig,
	enableAudioStereo bool,
	enableAudioNACK bool,
) {
	params := configureSenderParams{
		transceiver:              transceiver,
		enabledCodecs:            enabledCodecs,
		rtcpFeedbackConfig:       rtcpFeedbackConfig,
		filterOutH264HighProfile: !t.params.IsOfferer,
		enableAudioStereo:        enableAudioStereo,
		enableAudioNACK:          enableAudioNACK,
	}
	if !t.params.IsOfferer {
		t.sendersPendingConfigMu.Lock()
		t.sendersPendingConfig = append(t.sendersPendingConfig, params)
		t.sendersPendingConfigMu.Unlock()
		return
	}

	// Offerer: no remote offer to echo payload types from.
	configureSender(params, nil)
}

// processSendersPendingConfig configures the senders queued while answering the
// remote's offer (single peer connection mode). offerAudioPT (mime type -> the
// payload type the offer assigned) is parsed from the offer by the caller before
// SetRemoteDescription, so the answer echoes the offered payload types for audio
// codecs (and stays consistent with the forwarded RTP). It is nil when there is
// nothing to echo.
func (t *PCTransport) processSendersPendingConfig(offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
	t.sendersPendingConfigMu.Lock()
	pending := t.sendersPendingConfig
	t.sendersPendingConfig = nil
	t.sendersPendingConfigMu.Unlock()

	var unprocessed []configureSenderParams
	for _, p := range pending {
		if p.transceiver.Mid() == "" {
			unprocessed = append(unprocessed, p)
			continue
		}

		configureSender(p, offerAudioPT)
	}

	if len(unprocessed) != 0 {
		t.sendersPendingConfigMu.Lock()
		t.sendersPendingConfig = append(t.sendersPendingConfig, unprocessed...)
		t.sendersPendingConfigMu.Unlock()
	}
}

func (t *PCTransport) AddTrack(
	trackLocal webrtc.TrackLocal,
	params types.AddTrackParams,
	enabledCodecs []*livekit.Codec,
	rtcpFeedbackConfig RTCPFeedbackConfig,
) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
	t.lock.Lock()
	canReuse := t.canReuseTransceiver
	td, ok := t.previousTrackDescription[trackLocal.ID()]
	if ok {
		delete(t.previousTrackDescription, trackLocal.ID())
	}
	t.lock.Unlock()

	// keep track use same mid after migration if possible
	if td != nil && td.sender != nil {
		for _, tr := range t.pc.GetTransceivers() {
			if tr.Mid() == td.mid {
				return td.sender, tr, tr.SetSender(td.sender, trackLocal)
			}
		}
	}

	// if never negotiated with client, can't reuse transceiver for track not subscribed before migration
	if !canReuse {
		return t.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
	}

	sender, err = t.pc.AddTrack(trackLocal)
	if err != nil {
		return
	}

	for _, tr := range t.pc.GetTransceivers() {
		if tr.Sender() == sender {
			transceiver = tr
			break
		}
	}

	if transceiver == nil {
		err = ErrNoTransceiver
		return
	}

	t.queueOrConfigureSender(
		transceiver,
		enabledCodecs,
		rtcpFeedbackConfig,
		params.Stereo,
		!params.Red || !t.params.ClientInfo.SupportsAudioRED(),
	)

	t.adjustNumOutstandingMedia(transceiver)
	return
}

func (t *PCTransport) AddTransceiverFromTrack(
	trackLocal webrtc.TrackLocal,
	params types.AddTrackParams,
	enabledCodecs []*livekit.Codec,
	rtcpFeedbackConfig RTCPFeedbackConfig,
) (sender *webrtc.RTPSender, transceiver *webrtc.RTPTransceiver, err error) {
	transceiver, err = t.pc.AddTransceiverFromTrack(trackLocal)
	if err != nil {
		return
	}

	sender = transceiver.Sender()
	if sender == nil {
		err = ErrNoSender
		return
	}

	t.queueOrConfigureSender(
		transceiver,
		enabledCodecs,
		rtcpFeedbackConfig,
		params.Stereo,
		!params.Red || !t.params.ClientInfo.SupportsAudioRED(),
	)

	t.adjustNumOutstandingMedia(transceiver)
	return
}

func (t *PCTransport) AddTransceiverFromKind(
	kind webrtc.RTPCodecType,
	init webrtc.RTPTransceiverInit,
) (*webrtc.RTPTransceiver, error) {
	return t.pc.AddTransceiverFromKind(kind, init)
}

func (t *PCTransport) RemoveTrack(sender *webrtc.RTPSender) error {
	return t.pc.RemoveTrack(sender)
}

func (t *PCTransport) CurrentLocalDescription() *webrtc.SessionDescription {
	cld := t.pc.CurrentLocalDescription()
	if cld == nil {
		return nil
	}

	ld := *cld
	return &ld
}

func (t *PCTransport) CurrentRemoteDescription() *webrtc.SessionDescription {
	crd := t.pc.CurrentRemoteDescription()
	if crd == nil {
		return nil
	}

	rd := *crd
	return &rd
}

func (t *PCTransport) PendingRemoteDescription() *webrtc.SessionDescription {
	prd := t.pc.PendingRemoteDescription()
	if prd == nil {
		return nil
	}

	rd := *prd
	return &rd
}

func (t *PCTransport) GetMid(rtpReceiver *webrtc.RTPReceiver) string {
	tr := rtpReceiver.RTPTransceiver()
	if tr != nil {
		return tr.Mid()
	}

	return ""
}

func (t *PCTransport) GetRTPTransceiver(mid string) *webrtc.RTPTransceiver {
	for _, tr := range t.pc.GetTransceivers() {
		if tr.Mid() == mid {
			return tr
		}
	}

	return nil
}

func (t *PCTransport) GetRTPReceiver(mid string) *webrtc.RTPReceiver {
	for _, tr := range t.pc.GetTransceivers() {
		if tr.Mid() == mid {
			return tr.Receiver()
		}
	}

	return nil
}

func (t *PCTransport) getNumUnmatchedTransceivers() (uint32, uint32) {
	if t.isClosed.Load() || t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
		return 0, 0
	}

	numAudios := uint32(0)
	numVideos := uint32(0)
	for _, tr := range t.pc.GetTransceivers() {
		if tr.Mid() != "" {
			continue
		}

		switch tr.Kind() {
		case webrtc.RTPCodecTypeAudio:
			numAudios++

		case webrtc.RTPCodecTypeVideo:
			numVideos++
		}
	}

	return numAudios, numVideos
}

func (t *PCTransport) CreateDataChannel(label string, dci *webrtc.DataChannelInit) error {
	if label == DataTrackDataChannel && !t.params.EnableDataTracks {
		t.params.Logger.Debugw("data tracks not enabled")
		return nil
	}

	dc, err := t.pc.CreateDataChannel(label, dci)
	if err != nil {
		return err
	}
	var (
		dcPtr       **datachannel.DataChannelWriter[*webrtc.DataChannel]
		dcReady     *bool
		isDataTrack bool
		isUnlabeled bool
		kind        livekit.DataPacket_Kind
	)
	switch dc.Label() {
	default:
		isUnlabeled = true
		t.params.Logger.Infow("unlabeled datachannel added", "label", dc.Label())

	case ReliableDataChannel:
		dcPtr = &t.reliableDC
		dcReady = &t.reliableDCOpened
		kind = livekit.DataPacket_RELIABLE

	case LossyDataChannel:
		dcPtr = &t.lossyDC
		dcReady = &t.lossyDCOpened
		kind = livekit.DataPacket_LOSSY

	case DataTrackDataChannel:
		dcPtr = &t.dataTrackDC
		isDataTrack = true
	}

	dc.OnOpen(func() {
		rawDC, err := dc.DetachWithDeadline()
		if err != nil {
			t.params.Logger.Warnw("failed to detach data channel", err)
			return
		}

		var slowThreshold int
		if dc.Label() == ReliableDataChannel || isUnlabeled {
			slowThreshold = t.params.DatachannelSlowThreshold
		}

		t.lock.Lock()
		if isUnlabeled {
			t.unlabeledDataChannels = append(
				t.unlabeledDataChannels,
				datachannel.NewDataChannelWriterReliable(dc, rawDC, slowThreshold),
			)
		} else {
			if *dcPtr != nil {
				(*dcPtr).Close()
			}
			switch {
			case dcPtr == &t.reliableDC:
				*dcPtr = datachannel.NewDataChannelWriterReliable(dc, rawDC, slowThreshold)
			case dcPtr == &t.lossyDC:
				*dcPtr = datachannel.NewDataChannelWriterUnreliable(dc, rawDC, t.params.DatachannelLossyTargetLatency, uint64(lossyDataChannelMinBufferedAmount))
			case dcPtr == &t.dataTrackDC:
				*dcPtr = datachannel.NewDataChannelWriterUnreliable(dc, rawDC, t.params.DatachannelDataTrackTargetLatency, uint64(lossyDataChannelMinBufferedAmount))
			}
			if dcReady != nil {
				*dcReady = true
			}
		}
		t.lock.Unlock()
		t.params.Logger.Debugw(dc.Label() + " data channel open")

		go func() {
			defer rawDC.Close()
			buffer := make([]byte, dataChannelBufferSize)
			for {
				n, _, err := rawDC.ReadDataChannel(buffer)
				if err != nil {
					if !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "state=Closed") {
						t.params.Logger.Warnw("error reading data channel", err, "label", dc.Label())
					}
					return
				}

				switch {
				case isUnlabeled:
					t.params.Handler.OnDataMessageUnlabeled(buffer[:n])

				case isDataTrack:
					t.params.Handler.OnDataTrackMessage(buffer[:n], mono.UnixNano())

				default:
					t.params.Handler.OnDataMessage(kind, buffer[:n])
				}
			}
		}()

		t.maybeNotifyFullyEstablished()
	})

	return nil
}

// for testing only
func (t *PCTransport) CreateReadableDataChannel(label string, dci *webrtc.DataChannelInit) error {
	dc, err := t.pc.CreateDataChannel(label, dci)
	if err != nil {
		return err
	}

	dc.OnOpen(func() {
		t.params.Logger.Debugw(dc.Label() + " data channel open")
		rawDC, err := dc.DetachWithDeadline()
		if err != nil {
			t.params.Logger.Errorw("failed to detach data channel", err, "label", dc.Label())
			return
		}

		t.lock.Lock()
		t.unlabeledDataChannels = append(
			t.unlabeledDataChannels,
			datachannel.NewDataChannelWriterReliable(dc, rawDC, t.params.DatachannelSlowThreshold),
		)
		t.lock.Unlock()

		go func() {
			defer rawDC.Close()
			buffer := make([]byte, dataChannelBufferSize)
			for {
				n, _, err := rawDC.ReadDataChannel(buffer)
				if err != nil {
					if !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "state=Closed") {
						t.params.Logger.Warnw("error reading data channel", err, "label", dc.Label())
					}
					return
				}

				t.params.Handler.OnDataMessageUnlabeled(buffer[:n])
			}
		}()
	})
	return nil
}

func (t *PCTransport) CreateDataChannelIfEmpty(dcLabel string, dci *webrtc.DataChannelInit) (label string, id uint16, existing bool, err error) {
	if dcLabel == DataTrackDataChannel && !t.params.EnableDataTracks {
		t.params.Logger.Debugw("data tracks not enabled")
		err = errors.New("data tracks not enabled")
		return
	}

	t.lock.RLock()
	var dcw *datachannel.DataChannelWriter[*webrtc.DataChannel]
	switch dcLabel {
	case ReliableDataChannel:
		dcw = t.reliableDC
	case LossyDataChannel:
		dcw = t.lossyDC
	case DataTrackDataChannel:
		dcw = t.dataTrackDC
	default:
		t.params.Logger.Warnw("unknown data channel label", nil, "label", label)
		err = errors.New("unknown data channel label")
	}
	t.lock.RUnlock()
	if err != nil {
		return
	}

	if dcw != nil {
		dc := dcw.BufferedAmountGetter()
		return dc.Label(), *dc.ID(), true, nil
	}

	dc, err := t.pc.CreateDataChannel(dcLabel, dci)
	if err != nil {
		return
	}

	t.onDataChannel(dc)
	return dc.Label(), *dc.ID(), false, nil
}

func (t *PCTransport) GetRTT() (float64, bool) {
	scps, ok := t.iceTransport.GetSelectedCandidatePairStats()
	if !ok {
		return 0.0, false
	}

	return scps.CurrentRoundTripTime, true
}

func (t *PCTransport) IsEstablished() bool {
	return t.pc.ConnectionState() != webrtc.PeerConnectionStateNew
}

func (t *PCTransport) ICEHasEverConnected() bool {
	t.lock.RLock()
	defer t.lock.RUnlock()

	return !t.iceFirstConnectedAt.IsZero()
}

func (t *PCTransport) PeerConnectionHasEverConnected() bool {
	t.lock.RLock()
	defer t.lock.RUnlock()

	return !t.peerConnectionFirstConnectedAt.IsZero()
}

func (t *PCTransport) PeerConnectionFirstConnectedAt() time.Time {
	t.lock.RLock()
	defer t.lock.RUnlock()

	return t.peerConnectionFirstConnectedAt
}

func (t *PCTransport) GetICEConnectionInfo() *types.ICEConnectionInfo {
	return t.connectionDetails.GetInfo()
}

func (t *PCTransport) GetICEConnectionType() types.ICEConnectionType {
	return t.connectionDetails.GetConnectionType()
}

func (t *PCTransport) WriteRTCP(pkts []rtcp.Packet) error {
	return t.pc.WriteRTCP(pkts)
}

func (t *PCTransport) SendDataMessage(kind livekit.DataPacket_Kind, data []byte) error {
	convertFromUserPacket := false
	var dc *datachannel.DataChannelWriter[*webrtc.DataChannel]
	t.lock.RLock()
	if t.params.UseOneShotSignallingMode {
		if len(t.unlabeledDataChannels) > 0 {
			// use the first unlabeled to send
			dc = t.unlabeledDataChannels[0]
		}
		convertFromUserPacket = true
	} else {
		if kind == livekit.DataPacket_RELIABLE {
			dc = t.reliableDC
		} else {
			dc = t.lossyDC
		}
	}
	t.lock.RUnlock()

	if convertFromUserPacket {
		dp := &livekit.DataPacket{}
		if err := proto.Unmarshal(data, dp); err != nil {
			return err
		}

		switch payload := dp.Value.(type) {
		case *livekit.DataPacket_User:
			return t.sendDataMessage(dc, payload.User.Payload)
		default:
			return errors.New("cannot forward non user data packet")
		}
	}

	return t.sendDataMessage(dc, data)
}

func (t *PCTransport) SendDataMessageUnlabeled(data []byte, useRaw bool, sender livekit.ParticipantIdentity) error {
	convertToUserPacket := false
	var dc *datachannel.DataChannelWriter[*webrtc.DataChannel]
	t.lock.RLock()
	if t.params.UseOneShotSignallingMode || useRaw {
		if len(t.unlabeledDataChannels) > 0 {
			// use the first unlabeled to send
			dc = t.unlabeledDataChannels[0]
		}
	} else {
		if t.reliableDC != nil {
			dc = t.reliableDC
		} else if t.lossyDC != nil {
			dc = t.lossyDC
		}

		convertToUserPacket = true
	}
	t.lock.RUnlock()

	if convertToUserPacket {
		dpData, err := proto.Marshal(&livekit.DataPacket{
			ParticipantIdentity: string(sender),
			Value: &livekit.DataPacket_User{
				User: &livekit.UserPacket{Payload: data},
			},
		})
		if err != nil {
			return err
		}
		return t.sendDataMessage(dc, dpData)
	}

	return t.sendDataMessage(dc, data)
}

func (t *PCTransport) SendDataTrackMessage(data []byte) error {
	t.lock.RLock()
	dc := t.dataTrackDC
	t.lock.RUnlock()

	return t.sendDataMessage(dc, data)
}

func (t *PCTransport) sendDataMessage(dc *datachannel.DataChannelWriter[*webrtc.DataChannel], data []byte) error {
	if dc == nil {
		return ErrDataChannelUnavailable
	}

	if t.pc.ConnectionState() == webrtc.PeerConnectionStateFailed {
		return ErrTransportFailure
	}

	if t.params.DatachannelSlowThreshold == 0 && t.params.DataChannelMaxBufferedAmount > 0 && dc.BufferedAmountGetter().BufferedAmount() > t.params.DataChannelMaxBufferedAmount {
		return ErrDataChannelBufferFull
	}
	_, err := dc.Write(data)
	return err
}

func (t *PCTransport) Close() {
	if t.isClosed.Swap(true) {
		return
	}

	if err := t.pc.Close(); err != nil {
		t.params.Logger.Warnw("unclean close of peer connection", err)
	}

	<-t.eventsQueue.Stop()
	t.clearSignalStateCheckTimer()

	if t.streamAllocator != nil {
		t.streamAllocator.Stop()
	}

	if t.pacer != nil {
		t.pacer.Stop()
	}

	t.clearConnTimer()

	t.lock.Lock()
	if t.mayFailedICEStatsTimer != nil {
		t.mayFailedICEStatsTimer.Stop()
		t.mayFailedICEStatsTimer = nil
	}

	if t.reliableDC != nil {
		t.reliableDC.Close()
		t.reliableDC = nil
	}

	if t.lossyDC != nil {
		t.lossyDC.Close()
		t.lossyDC = nil
	}

	if t.dataTrackDC != nil {
		t.dataTrackDC.Close()
		t.dataTrackDC = nil
	}

	for _, dc := range t.unlabeledDataChannels {
		dc.Close()
	}
	t.unlabeledDataChannels = nil
	t.lock.Unlock()

	t.outputAndClearICEStats()
}

func (t *PCTransport) clearConnTimer() {
	t.lock.Lock()
	defer t.lock.Unlock()

	if t.connectAfterICETimer != nil {
		t.connectAfterICETimer.Stop()
		t.connectAfterICETimer = nil
	}

	if t.tcpICETimer != nil {
		t.tcpICETimer.Stop()
		t.tcpICETimer = nil
	}
}

func (t *PCTransport) HandleRemoteDescription(sd webrtc.SessionDescription, remoteId uint32) error {
	if t.params.UseOneShotSignallingMode {
		if sd.Type == webrtc.SDPTypeOffer {
			remoteOfferId := t.remoteOfferId.Load()
			if remoteOfferId != 0 && remoteOfferId != t.localAnswerId.Load() {
				t.params.Logger.Warnw(
					"sdp state: multiple offers without answer", nil,
					"remoteOfferId", remoteOfferId,
					"localAnswerId", t.localAnswerId.Load(),
					"receivedRemoteOfferId", remoteId,
				)
			}
			t.remoteOfferId.Store(remoteId)
		} else {
			if remoteId != 0 && remoteId != t.localOfferId.Load() {
				t.params.Logger.Warnw("sdp state: answer id mismatch", nil, "expected", t.localOfferId.Load(), "got", remoteId)
			}
			t.remoteAnswerId.Store(remoteId)
		}

		// add remote candidates to ICE connection details
		parsed, err := sd.Unmarshal()
		if err == nil {
			addRemoteICECandidates := func(attrs []sdp.Attribute) {
				for _, a := range attrs {
					if a.IsICECandidate() {
						c, err := ice.UnmarshalCandidate(a.Value)
						if err != nil {
							continue
						}
						t.connectionDetails.AddRemoteICECandidate(c, false, false, false)
					}
				}
			}

			addRemoteICECandidates(parsed.Attributes)
			for _, m := range parsed.MediaDescriptions {
				addRemoteICECandidates(m.Attributes)
			}
		}

		err = t.pc.SetRemoteDescription(sd)
		if err != nil {
			t.params.Logger.Errorw("could not set remote description on synchronous mode peer connection", err)
			return err
		}

		rtxRepairs := nonSimulcastRTXRepairsFromSDP(parsed, t.params.Logger)
		if len(rtxRepairs) > 0 {
			t.params.Logger.Debugw("rtx pairs found from sdp", "ssrcs", rtxRepairs)
			for repair, base := range rtxRepairs {
				t.params.Config.BufferFactory.SetRTXPair(repair, base, "")
			}
		}
		return nil
	}

	t.postEvent(event{
		signal: signalRemoteDescriptionReceived,
		data: remoteDescriptionData{
			sessionDescription: &sd,
			remoteId:           remoteId,
		},
	})
	return nil
}

func (t *PCTransport) GetAnswer() (webrtc.SessionDescription, uint32, error) {
	if !t.params.UseOneShotSignallingMode {
		return webrtc.SessionDescription{}, 0, ErrNotSynchronousLocalCandidatesMode
	}

	prd := t.pc.PendingRemoteDescription()
	if prd == nil || prd.Type != webrtc.SDPTypeOffer {
		return webrtc.SessionDescription{}, 0, ErrNoRemoteDescription
	}

	answer, err := t.pc.CreateAnswer(nil)
	if err != nil {
		return webrtc.SessionDescription{}, 0, err
	}

	if err = t.pc.SetLocalDescription(answer); err != nil {
		return webrtc.SessionDescription{}, 0, err
	}

	// wait for gathering to complete to include all candidates in the answer
	<-webrtc.GatheringCompletePromise(t.pc)

	cld := t.pc.CurrentLocalDescription()

	preferTCP := t.preferTCP.Load()
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (unfiltered)", "sdp", cld.SDP)
	}

	//
	// Filter after setting local description as pion expects the answer
	// to match between CreateAnswer and SetLocalDescription.
	// Filtered answer is sent to remote so that remote does not
	// see filtered candidates.
	//
	filteredAnswer := t.filterCandidates(*cld, preferTCP, true)
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (filtered)", "sdp", filteredAnswer.SDP)
	}

	answerId := t.remoteOfferId.Load()
	t.localAnswerId.Store(answerId)

	return filteredAnswer, answerId, nil
}

func (t *PCTransport) GetICESessionUfrag() (string, error) {
	cld := t.pc.CurrentLocalDescription()
	if cld == nil {
		return "", ErrNoLocalDescription
	}

	parsed, err := cld.Unmarshal()
	if err != nil {
		return "", err
	}

	ufrag, _, err := lksdp.ExtractICECredential(parsed)
	if err != nil {
		return "", err
	}

	return ufrag, nil
}

// Handles SDP Fragment for ICE Trickle in WHIP
func (t *PCTransport) HandleICETrickleSDPFragment(sdpFragment string) error {
	if !t.params.UseOneShotSignallingMode {
		return ErrNotSynchronousLocalCandidatesMode
	}

	parsedFragment := &lksdp.SDPFragment{}
	if err := parsedFragment.Unmarshal(sdpFragment); err != nil {
		t.params.Logger.Warnw("could not parse SDP fragment", err, "sdpFragment", sdpFragment)
		return ErrInvalidSDPFragment
	}

	crd := t.pc.CurrentRemoteDescription()
	if crd == nil {
		t.params.Logger.Warnw("no remote description", nil)
		return ErrNoRemoteDescription
	}

	parsedRemote, err := crd.Unmarshal()
	if err != nil {
		t.params.Logger.Warnw("could not parse remote description", err, "offer", crd)
		return err
	}

	// check if BUNDLE mid matches the "mid" in the SDP fragment
	bundleMid, found := lksdp.GetBundleMid(parsedRemote)
	if !found {
		return ErrNoBundleMid
	}

	if parsedFragment.Mid() != bundleMid {
		t.params.Logger.Warnw("incorrect mid", nil, "sdpFragment", sdpFragment)
		return ErrMidMismatch
	}

	fragmentICEUfrag, fragmentICEPwd, err := parsedFragment.ExtractICECredential()
	if err != nil {
		t.params.Logger.Warnw(
			"could not get ICE credential from fragment", err,
			"sdpFragment", sdpFragment,
		)
		return ErrInvalidSDPFragment
	}
	remoteICEUfrag, remoteICEPwd, err := lksdp.ExtractICECredential(parsedRemote)
	if err != nil {
		t.params.Logger.Warnw("could not get ICE credential from remote description", err, "sdpFragment", sdpFragment, "remoteDescription", crd)
		return err
	}
	if fragmentICEUfrag != "" && fragmentICEUfrag != remoteICEUfrag {
		t.params.Logger.Warnw(
			"ice ufrag mismatch", nil,
			"remoteICEUfrag", remoteICEUfrag,
			"fragmentICEUfrag", fragmentICEUfrag,
			"sdpFragment", sdpFragment,
			"remoteDescription", crd,
		)
		return ErrICECredentialMismatch
	}
	if fragmentICEPwd != "" && fragmentICEPwd != remoteICEPwd {
		t.params.Logger.Warnw(
			"ice pwd mismatch", nil,
			"remoteICEPwd", remoteICEPwd,
			"fragmentICEPwd", fragmentICEPwd,
			"sdpFragment", sdpFragment,
			"remoteDescription", crd,
		)
		return ErrICECredentialMismatch
	}

	// add candidates from media description
	for _, ic := range parsedFragment.Candidates() {
		c, err := ice.UnmarshalCandidate(ic)
		if err == nil {
			t.connectionDetails.AddRemoteICECandidate(c, false, false, false)
		}

		candidate := webrtc.ICECandidateInit{
			Candidate: ic,
		}
		if err := t.pc.AddICECandidate(candidate); err != nil {
			t.params.Logger.Warnw("failed to add ICE candidate", err, "candidate", candidate)
		} else {
			t.params.Logger.Debugw("added ICE candidate", "candidate", candidate)
		}
	}
	return nil
}

// Handles SDP Fragment for ICE Restart in WHIP
func (t *PCTransport) HandleICERestartSDPFragment(sdpFragment string) (string, error) {
	if !t.params.UseOneShotSignallingMode {
		return "", ErrNotSynchronousLocalCandidatesMode
	}

	parsedFragment := &lksdp.SDPFragment{}
	if err := parsedFragment.Unmarshal(sdpFragment); err != nil {
		t.params.Logger.Warnw("could not parse SDP fragment", err, "sdpFragment", sdpFragment)
		return "", ErrInvalidSDPFragment
	}

	crd := t.pc.CurrentRemoteDescription()
	if crd == nil {
		t.params.Logger.Warnw("no remote description", nil)
		return "", ErrNoRemoteDescription
	}

	parsedRemote, err := crd.Unmarshal()
	if err != nil {
		t.params.Logger.Warnw("could not parse remote description", err, "offer", crd)
		return "", err
	}

	if err := parsedFragment.PatchICECredentialAndCandidatesIntoSDP(parsedRemote); err != nil {
		t.params.Logger.Warnw("could not patch SDP fragment into remote description", err, "offer", crd, "sdpFragment", sdpFragment)
		return "", err
	}

	bytes, err := parsedRemote.Marshal()
	if err != nil {
		t.params.Logger.Warnw("could not marshal SDP with patched remote", err)
		return "", err
	}
	sd := webrtc.SessionDescription{
		SDP:  string(bytes),
		Type: webrtc.SDPTypeOffer,
	}
	if err := t.pc.SetRemoteDescription(sd); err != nil {
		t.params.Logger.Warnw("could not set remote description", err)
		return "", err
	}

	// clear out connection details on ICE restart and re-populate
	t.connectionDetails.Clear()
	for _, candidate := range parsedFragment.Candidates() {
		c, err := ice.UnmarshalCandidate(candidate)
		if err != nil {
			continue
		}
		t.connectionDetails.AddRemoteICECandidate(c, false, false, false)
	}

	answer, err := t.pc.CreateAnswer(nil)
	if err != nil {
		t.params.Logger.Warnw("could not create answer", err)
		return "", err
	}

	if err = t.pc.SetLocalDescription(answer); err != nil {
		t.params.Logger.Warnw("could not set local description", err)
		return "", err
	}

	// wait for gathering to complete to include all candidates in the answer
	<-webrtc.GatheringCompletePromise(t.pc)

	cld := t.pc.CurrentLocalDescription()

	preferTCP := t.preferTCP.Load()
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (unfiltered)", "sdp", cld.SDP)
	}

	//
	// Filter after setting local description as pion expects the answer
	// to match between CreateAnswer and SetLocalDescription.
	// Filtered answer is sent to remote so that remote does not
	// see filtered candidates.
	//
	filteredAnswer := t.filterCandidates(*cld, preferTCP, true)
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (filtered)", "sdp", filteredAnswer.SDP)
	}

	// add local candidates to ICE connection details
	parsedFilteredAnswer, err := filteredAnswer.Unmarshal()
	if err != nil {
		t.params.Logger.Warnw("could not parse local description", err)
		return "", err
	}

	parsedFragmentAnswer, err := lksdp.ExtractSDPFragment(parsedFilteredAnswer)
	if err != nil {
		t.params.Logger.Warnw("could not extract SDP fragment", err)
		return "", err
	}

	answerFragment, err := parsedFragmentAnswer.Marshal()
	if err != nil {
		t.params.Logger.Warnw("could not marshal answer SDP fragment", err)
		return "", err
	}

	return answerFragment, nil
}

func (t *PCTransport) OnNegotiationStateChanged(f func(state transport.NegotiationState)) {
	t.lock.Lock()
	t.onNegotiationStateChanged = f
	t.lock.Unlock()
}

func (t *PCTransport) getOnNegotiationStateChanged() func(state transport.NegotiationState) {
	t.lock.RLock()
	defer t.lock.RUnlock()

	return t.onNegotiationStateChanged
}

func (t *PCTransport) Negotiate(force bool) {
	if t.isClosed.Load() {
		return
	}

	var postEvent bool
	t.lock.Lock()
	if force {
		t.debouncedNegotiate.Add(func() {
			// no op to cancel pending negotiation
		})
		t.debouncePending = false
		t.updateLastNegotiateLocked()

		postEvent = true
	} else {
		if !t.debouncePending {
			if time.Since(t.lastNegotiate) > negotiationFrequency {
				t.debouncedNegotiate.SetDuration(fastNegotiationFrequency)
			} else {
				t.debouncedNegotiate.SetDuration(negotiationFrequency)
			}

			t.debouncedNegotiate.Add(func() {
				t.lock.Lock()
				t.debouncePending = false
				t.updateLastNegotiateLocked()
				t.lock.Unlock()

				t.postEvent(event{
					signal: signalSendOffer,
				})
			})
			t.debouncePending = true
		}
	}
	t.lock.Unlock()

	if postEvent {
		t.postEvent(event{
			signal: signalSendOffer,
		})
	}
}

func (t *PCTransport) updateLastNegotiateLocked() {
	if now := time.Now(); now.After(t.lastNegotiate) {
		t.lastNegotiate = now
	}
}

func (t *PCTransport) ICERestart() error {
	if t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
		t.params.Logger.Warnw("trying to restart ICE on closed peer connection", nil)
		return ErrIceRestartOnClosedPeerConnection
	}

	t.postEvent(event{
		signal: signalICERestart,
	})
	return nil
}

func (t *PCTransport) ResetShortConnOnICERestart() {
	t.resetShortConnOnICERestart.Store(true)
}

func (t *PCTransport) AddTrackToStreamAllocator(subTrack types.SubscribedTrack) {
	if t.streamAllocator == nil {
		return
	}

	layers := buffer.GetVideoLayersForMimeType(
		subTrack.DownTrack().Mime(),
		subTrack.MediaTrack().ToProto(),
	)
	t.streamAllocator.AddTrack(subTrack.DownTrack(), streamallocator.AddTrackParams{
		Source:         subTrack.MediaTrack().Source(),
		IsMultiLayered: len(layers) > 1,
		PublisherID:    subTrack.MediaTrack().PublisherID(),
	})
}

func (t *PCTransport) RemoveTrackFromStreamAllocator(subTrack types.SubscribedTrack) {
	if t.streamAllocator == nil {
		return
	}

	t.streamAllocator.RemoveTrack(subTrack.DownTrack())
}

func (t *PCTransport) SetAllowPauseOfStreamAllocator(allowPause bool) {
	if t.streamAllocator == nil {
		return
	}

	t.streamAllocator.SetAllowPause(allowPause)
}

func (t *PCTransport) SetChannelCapacityOfStreamAllocator(channelCapacity int64) {
	if t.streamAllocator == nil {
		return
	}

	t.streamAllocator.SetChannelCapacity(channelCapacity)
}

func (t *PCTransport) preparePC(previousAnswer webrtc.SessionDescription) error {
	// sticky data channel to first m-lines, if someday we don't send sdp without media streams to
	// client's subscribe pc after joining, should change this step
	parsed, err := previousAnswer.Unmarshal()
	if err != nil {
		return err
	}
	fp, fpHahs, err := lksdp.ExtractFingerprint(parsed)
	if err != nil {
		return err
	}

	offer, err := t.pc.CreateOffer(nil)
	if err != nil {
		return err
	}
	if err := t.pc.SetLocalDescription(offer); err != nil {
		return err
	}

	//
	// Simulate client side peer connection and set DTLS role from previous answer.
	// Role needs to be set properly (one side needs to be server and the other side
	// needs to be the client) for DTLS connection to form properly. As this is
	// trying to replicate previous setup, read from previous answer and use that role.
	//
	se := webrtc.SettingEngine{}
	_ = se.SetAnsweringDTLSRole(lksdp.ExtractDTLSRole(parsed))
	se.SetIgnoreRidPauseForRecv(true)
	api := webrtc.NewAPI(
		webrtc.WithSettingEngine(se),
		webrtc.WithMediaEngine(t.me),
	)
	pc2, err := api.NewPeerConnection(webrtc.Configuration{
		SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
	})
	if err != nil {
		return err
	}
	defer pc2.Close()

	if err := pc2.SetRemoteDescription(offer); err != nil {
		return err
	}
	ans, err := pc2.CreateAnswer(nil)
	if err != nil {
		return err
	}

	// replace client's fingerprint into dummy pc's answer, for pion's dtls process, it will
	// keep the fingerprint at first call of SetRemoteDescription, if dummy pc and client pc use
	// different fingerprint, that will cause pion denied dtls data after handshake with client
	// complete (can't pass fingerprint change).
	// in this step, we don't established connection with dummy pc(no candidate swap), just use
	// sdp negotiation to sticky data channel and keep client's fingerprint
	parsedAns, _ := ans.Unmarshal()
	fpLine := fpHahs + " " + fp
	replaceFP := func(attrs []sdp.Attribute, fpLine string) {
		for k := range attrs {
			if attrs[k].Key == "fingerprint" {
				attrs[k].Value = fpLine
			}
		}
	}
	replaceFP(parsedAns.Attributes, fpLine)
	for _, m := range parsedAns.MediaDescriptions {
		replaceFP(m.Attributes, fpLine)
	}
	bytes, err := parsedAns.Marshal()
	if err != nil {
		return err
	}
	ans.SDP = string(bytes)

	return t.pc.SetRemoteDescription(ans)
}

func (t *PCTransport) initPCWithPreviousAnswer(previousAnswer webrtc.SessionDescription) (map[string]*webrtc.RTPSender, error) {
	senders := make(map[string]*webrtc.RTPSender)
	parsed, err := previousAnswer.Unmarshal()
	if err != nil {
		return senders, err
	}
	for _, m := range parsed.MediaDescriptions {
		var codecType webrtc.RTPCodecType
		switch m.MediaName.Media {
		case "video":
			codecType = webrtc.RTPCodecTypeVideo
		case "audio":
			codecType = webrtc.RTPCodecTypeAudio
		case "application":
			if t.params.IsOfferer {
				// for pion generate unmatched sdp, it always appends data channel to last m-lines,
				// that not consistent with our previous answer that data channel might at middle-line
				// because sdp can negotiate multi times before migration.(it will sticky to the last m-line at first negotiate)
				// so use a dummy pc to negotiate sdp to fixed the datachannel's mid at same position with previous answer
				if err := t.preparePC(previousAnswer); err != nil {
					t.params.Logger.Warnw("prepare pc for migration failed", err)
					return senders, err
				}
			}
			continue
		default:
			continue
		}

		if !t.params.IsOfferer {
			// `sendrecv` or `sendonly` means this transceiver is used for sending

			// Note that a transceiver previously used to send could be `inactive`.
			// Let those transceivers be created when remote description is set.
			_, ok1 := m.Attribute(webrtc.RTPTransceiverDirectionSendrecv.String())
			_, ok2 := m.Attribute(webrtc.RTPTransceiverDirectionSendonly.String())
			if !ok1 && !ok2 {
				continue
			}
		}

		tr, err := t.pc.AddTransceiverFromKind(
			codecType,
			webrtc.RTPTransceiverInit{
				Direction: webrtc.RTPTransceiverDirectionSendonly,
			},
		)
		if err != nil {
			return senders, err
		}
		mid := lksdp.GetMidValue(m)
		if mid == "" {
			t.params.Logger.Warnw("cannot set up peer connection with previous answer, mid not found", nil, "senders", slices.Collect(maps.Keys(senders)))
			return senders, ErrMidNotFound
		}
		tr.SetMid(mid)

		// save mid -> senders for migration reuse
		sender := tr.Sender()
		senders[mid] = sender

		// set transceiver to inactive
		tr.SetSender(sender, nil)
	}
	t.params.Logger.Debugw("set up peer connection with previous answer", "senders", slices.Collect(maps.Keys(senders)))
	return senders, nil
}

func (t *PCTransport) SetPreviousSdp(localDescription, remoteDescription *webrtc.SessionDescription) {
	// when there is no answer, cannot migrate, force a full reconnect
	if (t.params.IsOfferer && remoteDescription == nil) || (!t.params.IsOfferer && localDescription == nil) {
		t.onNegotiationFailed(true, "no previous answer")
		return
	}

	t.lock.Lock()
	var (
		senders   map[string]*webrtc.RTPSender
		err       error
		parseMids bool
	)
	if t.params.IsOfferer {
		if t.pc.RemoteDescription() == nil && t.previousAnswer == nil {
			t.previousAnswer = remoteDescription
			senders, err = t.initPCWithPreviousAnswer(*remoteDescription)
			parseMids = true
		}
	} else {
		if t.pc.LocalDescription() == nil {
			senders, err = t.initPCWithPreviousAnswer(*localDescription)
			parseMids = true
		}
	}
	if err != nil {
		t.lock.Unlock()
		t.onNegotiationFailed(true, fmt.Sprintf("initPCWithPreviousAnswer failed, error: %s", err))
		return
	}

	if localDescription != nil && parseMids {
		// in migration case, can't reuse transceiver before negotiating expected tracks
		// that were subscribed at previous node
		t.canReuseTransceiver = false
		if err := t.parseTrackMid(*localDescription, senders); err != nil {
			t.params.Logger.Warnw(
				"parse previous local description failed", err,
				"localDescription", localDescription.SDP,
			)
		}
	}

	if t.params.IsOfferer {
		// disable fast negotiation temporarily after migration to avoid sending offer
		// contains part of subscribed tracks before migration, let the subscribed track
		// resume at the same time.
		t.lastNegotiate = time.Now().Add(iceFailedTimeoutTotal)
	}
	t.lock.Unlock()
}

func (t *PCTransport) parseTrackMid(sd webrtc.SessionDescription, senders map[string]*webrtc.RTPSender) error {
	parsed, err := sd.Unmarshal()
	if err != nil {
		return err
	}

	t.previousTrackDescription = make(map[string]*trackDescription)
	for _, m := range parsed.MediaDescriptions {
		msid, ok := m.Attribute(sdp.AttrKeyMsid)
		if !ok {
			continue
		}

		if split := strings.Split(msid, " "); len(split) == 2 {
			trackID := split[1]
			mid := lksdp.GetMidValue(m)
			if mid == "" {
				return ErrMidNotFound
			}
			if sender, ok := senders[mid]; ok {
				t.previousTrackDescription[trackID] = &trackDescription{mid, sender}
			}
		}
	}
	if len(t.previousTrackDescription) != 0 {
		t.params.Logger.Debugw("previous track description", t.previousTrackDescription)
	}
	return nil
}

func (t *PCTransport) postEvent(e event) {
	e.PCTransport = t
	t.eventsQueue.Enqueue(func(e event) {
		var err error
		switch e.signal {
		case signalICEGatheringComplete:
			err = e.handleICEGatheringComplete(e)
		case signalLocalICECandidate:
			err = e.handleLocalICECandidate(e)
		case signalRemoteICECandidate:
			err = e.handleRemoteICECandidate(e)
		case signalSendOffer:
			err = e.handleSendOffer(e)
		case signalRemoteDescriptionReceived:
			err = e.handleRemoteDescriptionReceived(e)
		case signalICERestart:
			err = e.handleICERestart(e)
		}
		if err != nil {
			if !e.isClosed.Load() {
				e.onNegotiationFailed(true, fmt.Sprintf("error handling event. err: %s, event: %s", err, e))
			}
		}
	}, e)
}

func (t *PCTransport) handleICEGatheringComplete(_ event) error {
	if t.params.IsOfferer {
		return t.handleICEGatheringCompleteOfferer()
	} else {
		return t.handleICEGatheringCompleteAnswerer()
	}
}

func (t *PCTransport) handleICEGatheringCompleteOfferer() error {
	if !t.restartAfterGathering {
		return nil
	}

	t.params.Logger.Debugw("restarting ICE after ICE gathering")
	t.restartAfterGathering = false
	return t.doICERestart()
}

func (t *PCTransport) handleICEGatheringCompleteAnswerer() error {
	if t.pendingRestartIceOffer == nil {
		return nil
	}

	offer := *t.pendingRestartIceOffer
	t.pendingRestartIceOffer = nil

	t.params.Logger.Debugw("accept remote restart ice offer after ICE gathering")

	// Parse the offer payload types before SetRemoteDescription so this does not
	// race with pion's use of the same description.
	var offerAudioPT map[mime.MimeType]webrtc.PayloadType
	if parsed, err := offer.Unmarshal(); err == nil {
		offerAudioPT = offerAudioPayloadTypes(parsed)
	}

	if err := t.setRemoteDescription(offer); err != nil {
		return err
	}
	t.params.Handler.OnSetRemoteDescriptionOffer()
	t.processSendersPendingConfig(offerAudioPT)

	return t.createAndSendAnswer()
}

func (t *PCTransport) localDescriptionSent() error {
	if !t.cacheLocalCandidates {
		return nil
	}

	t.cacheLocalCandidates = false

	cachedLocalCandidates := t.cachedLocalCandidates
	t.cachedLocalCandidates = nil

	for _, c := range cachedLocalCandidates {
		if err := t.params.Handler.OnICECandidate(c, t.params.Transport); err != nil {
			t.params.Logger.Warnw("failed to send cached ICE candidate", err, "candidate", c)
			return err
		}
	}
	return nil
}

func (t *PCTransport) clearLocalDescriptionSent() {
	t.cacheLocalCandidates = true
	t.cachedLocalCandidates = nil
	t.connectionDetails.Clear()
}

func (t *PCTransport) handleLocalICECandidate(e event) error {
	c := e.data.(*webrtc.ICECandidate)

	filtered := false
	if c != nil {
		if t.preferTCP.Load() && c.Protocol != webrtc.ICEProtocolTCP {
			t.params.Logger.Debugw("filtering out local candidate, TCP preferred", "candidate", c.String())
			filtered = true
		}
		if !filtered && t.params.ExcludeIPv6LocalCandidates {
			if IsIPv6(c.Address) {
				t.params.Logger.Debugw("filtering out local candidate, IPv6 excluded", "candidate", c.String())
				filtered = true
			}
		}
		t.connectionDetails.AddLocalCandidate(c, filtered, true)
	}

	if filtered {
		return nil
	}

	if t.cacheLocalCandidates {
		t.cachedLocalCandidates = append(t.cachedLocalCandidates, c)
		return nil
	}

	if err := t.params.Handler.OnICECandidate(c, t.params.Transport); err != nil {
		t.params.Logger.Warnw("failed to send ICE candidate", err, "candidate", c)
		return err
	}

	return nil
}

func (t *PCTransport) handleRemoteICECandidate(e event) error {
	c := e.data.(*webrtc.ICECandidateInit)

	filtered := false
	if t.preferTCP.Load() && !strings.Contains(strings.ToLower(c.Candidate), "tcp") {
		t.params.Logger.Debugw("filtering out remote candidate", "candidate", c.Candidate)
		filtered = true
	}

	if !t.params.Config.UseMDNS && types.IsCandidateMDNS(*c) {
		t.params.Logger.Debugw("ignoring mDNS candidate", "candidate", c.Candidate)
		filtered = true
	}

	t.connectionDetails.AddRemoteCandidate(*c, filtered, true, false)
	if filtered {
		return nil
	}

	if t.pc.RemoteDescription() == nil {
		t.pendingRemoteCandidates = append(t.pendingRemoteCandidates, c)
		return nil
	}

	if err := t.pc.AddICECandidate(*c); err != nil {
		t.params.Logger.Warnw("failed to add ICE candidate", err, "candidate", c)
		// ignore ParseAddr error as it does not affect ICE connectivity
		if !strings.Contains(err.Error(), "ParseAddr") {
			return errors.Wrap(err, "add ice candidate failed")
		}
	} else {
		t.params.Logger.Debugw("added ICE candidate", "candidate", c)
	}

	return nil
}

func (t *PCTransport) setNegotiationState(state transport.NegotiationState) {
	t.negotiationState = state
	if onNegotiationStateChanged := t.getOnNegotiationStateChanged(); onNegotiationStateChanged != nil {
		onNegotiationStateChanged(t.negotiationState)
	}
}

func (t *PCTransport) isCandidateFilterActive(preferTCP bool) bool {
	return preferTCP || t.params.ExcludeIPv6LocalCandidates
}

func (t *PCTransport) filterCandidates(sd webrtc.SessionDescription, preferTCP, isLocal bool) webrtc.SessionDescription {
	parsed, err := sd.Unmarshal()
	if err != nil {
		t.params.Logger.Warnw("could not unmarshal SDP to filter candidates", err)
		return sd
	}

	filterAttributes := func(attrs []sdp.Attribute) []sdp.Attribute {
		filteredAttrs := make([]sdp.Attribute, 0, len(attrs))
		for _, a := range attrs {
			if a.IsICECandidate() {
				c, err := ice.UnmarshalCandidate(a.Value)
				if err != nil {
					t.params.Logger.Errorw("failed to unmarshal candidate in sdp", err, "isLocal", isLocal, "sdp", sd.SDP)
					filteredAttrs = append(filteredAttrs, a)
					continue
				}
				excluded :=
					(preferTCP && !c.NetworkType().IsTCP()) ||
						(t.params.ExcludeIPv6LocalCandidates && isLocal && c.NetworkType().IsIPv6()) ||
						(!t.params.Config.UseMDNS && types.IsICECandidateMDNS(c))
				if !excluded {
					filteredAttrs = append(filteredAttrs, a)
				}

				if isLocal {
					t.connectionDetails.AddLocalICECandidate(c, excluded, false)
				} else {
					t.connectionDetails.AddRemoteICECandidate(c, excluded, false, false)
				}
			} else {
				filteredAttrs = append(filteredAttrs, a)
			}
		}

		return filteredAttrs
	}

	parsed.Attributes = filterAttributes(parsed.Attributes)
	for _, m := range parsed.MediaDescriptions {
		m.Attributes = filterAttributes(m.Attributes)
	}

	bytes, err := parsed.Marshal()
	if err != nil {
		t.params.Logger.Warnw("could not marshal SDP to filter candidates", err)
		return sd
	}
	sd.SDP = string(bytes)
	return sd
}

func (t *PCTransport) clearSignalStateCheckTimer() {
	if t.signalStateCheckTimer != nil {
		t.signalStateCheckTimer.Stop()
		t.signalStateCheckTimer = nil
	}
}

func (t *PCTransport) setupSignalStateCheckTimer() {
	t.clearSignalStateCheckTimer()

	negotiateVersion := t.negotiateCounter.Inc()
	t.signalStateCheckTimer = time.AfterFunc(negotiationFailedTimeout, func() {
		t.clearSignalStateCheckTimer()

		failed := t.negotiationState != transport.NegotiationStateNone

		if t.negotiateCounter.Load() == negotiateVersion && failed && t.pc.ConnectionState() == webrtc.PeerConnectionStateConnected {
			t.onNegotiationFailed(false, "negotiation timed out")
		}
	})
}

func (t *PCTransport) adjustNumOutstandingMedia(transceiver *webrtc.RTPTransceiver) {
	if transceiver.Mid() != "" {
		return
	}

	t.lock.Lock()
	if transceiver.Kind() == webrtc.RTPCodecTypeAudio {
		t.numOutstandingAudios++
	} else {
		t.numOutstandingVideos++
	}
	t.lock.Unlock()
}

func (t *PCTransport) sendUnmatchedMediaRequirement(force bool) error {
	// if there are unmatched media sections, notify remote peer to generate offer with
	// enough media section in subsequent offers
	t.lock.Lock()
	numAudios := t.numOutstandingAudios - t.numRequestSentAudios
	t.numRequestSentAudios += numAudios

	numVideos := t.numOutstandingVideos - t.numRequestSentVideos
	t.numRequestSentVideos += numVideos
	t.lock.Unlock()

	if force || (numAudios+numVideos) != 0 {
		if err := t.params.Handler.OnUnmatchedMedia(numAudios, numVideos); err != nil {
			return errors.Wrap(err, "could not send unmatched media requirements")
		}
	}

	return nil
}

func (t *PCTransport) createAndSendOffer(options *webrtc.OfferOptions) error {
	if t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
		t.params.Logger.Warnw("trying to send offer on closed peer connection", nil)
		return nil
	}

	// when there's an ongoing negotiation, let it finish and not disrupt its state
	switch t.negotiationState {
	case transport.NegotiationStateRemote:
		t.params.Logger.Debugw("skipping negotiation, trying again later")
		t.setNegotiationState(transport.NegotiationStateRetry)
		return nil
	case transport.NegotiationStateRetry:
		// already set to retry, we can safely skip this attempt
		return nil
	}

	ensureICERestart := func(options *webrtc.OfferOptions) *webrtc.OfferOptions {
		if options == nil {
			options = &webrtc.OfferOptions{}
		}
		options.ICERestart = true
		return options
	}

	t.lock.Lock()
	if t.previousAnswer != nil {
		t.previousAnswer = nil
		options = ensureICERestart(options)
		t.params.Logger.Infow("ice restart due to previous answer")
	}
	t.lock.Unlock()

	if t.restartAtNextOffer {
		t.restartAtNextOffer = false
		options = ensureICERestart(options)
		t.params.Logger.Infow("ice restart at next offer")
	}

	if options != nil && options.ICERestart {
		t.clearLocalDescriptionSent()
	}

	offer, err := t.pc.CreateOffer(options)
	if err != nil {
		if errors.Is(err, webrtc.ErrConnectionClosed) {
			t.params.Logger.Warnw("trying to create offer on closed peer connection", nil)
			return nil
		}

		prometheus.RecordServiceOperationError("offer", "create")
		return errors.Wrap(err, "create offer failed")
	}

	preferTCP := t.preferTCP.Load()
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local offer (unfiltered)", "sdp", offer.SDP)
	}

	isStartOfConnectionSequence := t.pc.LocalDescription() == nil

	err = t.pc.SetLocalDescription(offer)
	if err != nil {
		if errors.Is(err, webrtc.ErrConnectionClosed) {
			t.params.Logger.Warnw("trying to set local description on closed peer connection", nil)
			return nil
		}

		prometheus.RecordServiceOperationError("offer", "local_description")
		return errors.Wrap(err, "setting local description failed")
	}

	if isStartOfConnectionSequence {
		prometheus.RecordPeerConnectionState(t.params.Transport, "started")
	}

	//
	// Filter after setting local description as pion expects the offer
	// to match between CreateOffer and SetLocalDescription.
	// Filtered offer is sent to remote so that remote does not
	// see filtered candidates.
	//
	offer = t.filterCandidates(offer, preferTCP, true)
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local offer (filtered)", "sdp", offer.SDP)
	}

	// indicate waiting for remote
	t.setNegotiationState(transport.NegotiationStateRemote)

	t.setupSignalStateCheckTimer()

	remoteAnswerId := t.remoteAnswerId.Load()
	if remoteAnswerId != 0 && remoteAnswerId != t.localOfferId.Load() {
		if options == nil || !options.ICERestart {
			t.params.Logger.Warnw(
				"sdp state: sending offer before receiving answer", nil,
				"localOfferId", t.localOfferId.Load(),
				"remoteAnswerId", remoteAnswerId,
			)
		}
	}

	if err := t.params.Handler.OnOffer(offer, t.localOfferId.Inc(), t.getMidToTrackIDMapping()); err != nil {
		prometheus.RecordServiceOperationError("offer", "write_message")
		return errors.Wrap(err, "could not send offer")
	}
	prometheus.RecordServiceOperationSuccess("offer")

	return t.localDescriptionSent()
}

func (t *PCTransport) handleSendOffer(_ event) error {
	if !t.params.IsOfferer {
		return t.sendUnmatchedMediaRequirement(true)
	}

	return t.createAndSendOffer(nil)
}

type remoteDescriptionData struct {
	sessionDescription *webrtc.SessionDescription
	remoteId           uint32
}

func (t *PCTransport) handleRemoteDescriptionReceived(e event) error {
	rdd := e.data.(remoteDescriptionData)
	if rdd.sessionDescription.Type == webrtc.SDPTypeOffer {
		return t.handleRemoteOfferReceived(rdd.sessionDescription, rdd.remoteId)
	} else {
		return t.handleRemoteAnswerReceived(rdd.sessionDescription, rdd.remoteId)
	}
}

func (t *PCTransport) isRemoteOfferRestartICE(parsed *sdp.SessionDescription) (string, bool, error) {
	user, pwd, err := lksdp.ExtractICECredential(parsed)
	if err != nil {
		return "", false, err
	}

	credential := fmt.Sprintf("%s:%s", user, pwd)
	// ice credential changed, remote offer restart ice
	restartICE := t.currentOfferIceCredential != "" && t.currentOfferIceCredential != credential
	return credential, restartICE, nil
}

func (t *PCTransport) setRemoteDescription(sd webrtc.SessionDescription) error {
	// filter before setting remote description so that pion does not see filtered remote candidates
	preferTCP := t.preferTCP.Load()
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("remote description (unfiltered)", "type", sd.Type, "sdp", sd.SDP)
	}
	sd = t.filterCandidates(sd, preferTCP, false)
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("remote description (filtered)", "type", sd.Type, "sdp", sd.SDP)
	}

	if err := t.pc.SetRemoteDescription(sd); err != nil {
		if errors.Is(err, webrtc.ErrConnectionClosed) {
			t.params.Logger.Warnw("trying to set remote description on closed peer connection", nil)
			return nil
		}

		sdpType := "offer"
		if sd.Type == webrtc.SDPTypeAnswer {
			sdpType = "answer"
		}
		prometheus.RecordServiceOperationError(sdpType, "remote_description")
		return errors.Wrap(err, "setting remote description failed")
	} else if sd.Type == webrtc.SDPTypeAnswer {
		t.lock.Lock()
		if !t.canReuseTransceiver {
			t.canReuseTransceiver = true
			t.previousTrackDescription = make(map[string]*trackDescription)
			t.params.Logger.Debugw("enabling transceiver reuse")
		}
		t.lock.Unlock()
	}

	for _, c := range t.pendingRemoteCandidates {
		if err := t.pc.AddICECandidate(*c); err != nil {
			t.params.Logger.Warnw("failed to add cached ICE candidate", err, "candidate", c)
			return errors.Wrap(err, "add ice candidate failed")
		} else {
			t.params.Logger.Debugw("added cached ICE candidate", "candidate", c)
		}
	}
	t.pendingRemoteCandidates = nil

	return nil
}

func (t *PCTransport) createAndSendAnswer() error {
	numOutstandingAudios, numOutstandingVideos := t.getNumUnmatchedTransceivers()
	t.lock.Lock()
	t.numOutstandingAudios, t.numOutstandingVideos = numOutstandingAudios, numOutstandingVideos
	t.numRequestSentAudios, t.numRequestSentVideos = 0, 0
	t.lock.Unlock()

	t.restrictReceiverCodecsToPublishList()

	answer, err := t.pc.CreateAnswer(nil)
	if err != nil {
		if errors.Is(err, webrtc.ErrConnectionClosed) {
			t.params.Logger.Warnw("trying to create answer on closed peer connection", nil)
			return nil
		}

		prometheus.RecordServiceOperationError("answer", "create")
		return errors.Wrap(err, "create answer failed")
	}

	preferTCP := t.preferTCP.Load()
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (unfiltered)", "sdp", answer.SDP)
	}

	if err = t.pc.SetLocalDescription(answer); err != nil {
		prometheus.RecordServiceOperationError("answer", "local_description")
		return errors.Wrap(err, "setting local description failed")
	}

	//
	// Filter after setting local description as pion expects the answer
	// to match between CreateAnswer and SetLocalDescription.
	// Filtered answer is sent to remote so that remote does not
	// see filtered candidates.
	//
	answer = t.filterCandidates(answer, preferTCP, true)
	if t.isCandidateFilterActive(preferTCP) {
		t.params.Logger.Debugw("local answer (filtered)", "sdp", answer.SDP)
	}

	localAnswerId := t.localAnswerId.Load()
	if localAnswerId != 0 && localAnswerId >= t.remoteOfferId.Load() {
		t.params.Logger.Warnw(
			"sdp state: duplicate answer", nil,
			"localAnswerId", localAnswerId,
			"remoteOfferId", t.remoteOfferId.Load(),
		)
	}

	answerId := t.remoteOfferId.Load()

	if err := t.params.Handler.OnAnswer(answer, answerId, t.getMidToTrackIDMapping()); err != nil {
		prometheus.RecordServiceOperationError("answer", "write_message")
		return errors.Wrap(err, "could not send answer")
	}
	t.localAnswerId.Store(answerId)
	prometheus.RecordServiceOperationSuccess("answer")

	if err := t.sendUnmatchedMediaRequirement(false); err != nil {
		return err
	}

	t.lock.Lock()
	if !t.canReuseTransceiver {
		t.canReuseTransceiver = true
		t.previousTrackDescription = make(map[string]*trackDescription)
		t.params.Logger.Debugw("enabling transceiver reuse")
	}
	t.lock.Unlock()

	return t.localDescriptionSent()
}

func (t *PCTransport) handleRemoteOfferReceived(sd *webrtc.SessionDescription, offerId uint32) error {
	t.params.Logger.Debugw("processing offer", "offerId", offerId)
	remoteOfferId := t.remoteOfferId.Load()
	if remoteOfferId != 0 && remoteOfferId != t.localAnswerId.Load() {
		t.params.Logger.Warnw(
			"sdp state: multiple offers without answer", nil,
			"remoteOfferId", remoteOfferId,
			"localAnswerId", t.localAnswerId.Load(),
			"receivedRemoteOfferId", offerId,
		)
	}
	t.remoteOfferId.Store(offerId)

	parsed, err := sd.Unmarshal()
	if err != nil {
		return err
	}

	t.lock.Lock()
	if !t.firstOfferReceived {
		t.firstOfferReceived = true
		var dataChannelFound bool
		for _, media := range parsed.MediaDescriptions {
			if strings.EqualFold(media.MediaName.Media, "application") {
				dataChannelFound = true
				break
			}
		}
		t.firstOfferNoDataChannel = !dataChannelFound
	}
	t.lock.Unlock()

	iceCredential, offerRestartICE, err := t.isRemoteOfferRestartICE(parsed)
	if err != nil {
		return errors.Wrap(err, "check remote offer restart ice failed")
	}

	if offerRestartICE && t.pendingRestartIceOffer == nil {
		t.clearLocalDescriptionSent()
	}

	if offerRestartICE && t.pc.ICEGatheringState() == webrtc.ICEGatheringStateGathering {
		t.params.Logger.Debugw("remote offer restart ice while ice gathering")
		t.pendingRestartIceOffer = sd
		return nil
	}

	if offerRestartICE && t.resetShortConnOnICERestart.CompareAndSwap(true, false) {
		t.resetShortConn()
	}

	if offerRestartICE {
		t.outputAndClearICEStats()
	}

	isStartOfConnectionSequence := t.pc.RemoteDescription() == nil

	if err := t.setRemoteDescription(*sd); err != nil {
		return err
	}

	if isStartOfConnectionSequence {
		prometheus.RecordPeerConnectionState(t.params.Transport, "started")
	}

	t.params.Handler.OnSetRemoteDescriptionOffer()
	t.processSendersPendingConfig(offerAudioPayloadTypes(parsed))

	rtxRepairs := nonSimulcastRTXRepairsFromSDP(parsed, t.params.Logger)
	if len(rtxRepairs) > 0 {
		t.params.Logger.Debugw("rtx pairs found from sdp", "ssrcs", rtxRepairs)
		for repair, base := range rtxRepairs {
			t.params.Config.BufferFactory.SetRTXPair(repair, base, "")
		}
	}

	if t.currentOfferIceCredential == "" || offerRestartICE {
		t.currentOfferIceCredential = iceCredential
	}

	return t.createAndSendAnswer()
}

func (t *PCTransport) handleRemoteAnswerReceived(sd *webrtc.SessionDescription, answerId uint32) error {
	t.params.Logger.Debugw("processing answer", "answerId", answerId)
	if answerId != 0 && answerId != t.localOfferId.Load() {
		t.params.Logger.Warnw(
			"sdp state: answer id mismatch", nil,
			"expected", t.localOfferId.Load(),
			"got", answerId,
		)
	}
	t.remoteAnswerId.Store(answerId)

	t.clearSignalStateCheckTimer()

	if err := t.setRemoteDescription(*sd); err != nil {
		// Pion will call RTPSender.Send method for each new added Downtrack, and return error if the DownTrack.Bind
		// returns error. In case of Downtrack.Bind returns ErrUnsupportedCodec, the signal state will be stable as negotiation is already completed
		// before startRTPSenders, and the peerconnection state can be recovered by next negotiation which will be triggered
		// by the SubscriptionManager unsubscribe the failure DownTrack. So don't treat this error as negotiation failure.
		if !errors.Is(err, webrtc.ErrUnsupportedCodec) {
			return err
		}
	}

	if t.negotiationState == transport.NegotiationStateRetry {
		t.setNegotiationState(transport.NegotiationStateNone)

		t.params.Logger.Debugw("re-negotiate after receiving answer")
		return t.createAndSendOffer(nil)
	}

	t.setNegotiationState(transport.NegotiationStateNone)
	return nil
}

func (t *PCTransport) doICERestart() error {
	if t.pc.ConnectionState() == webrtc.PeerConnectionStateClosed {
		t.params.Logger.Warnw("trying to restart ICE on closed peer connection", nil)
		return nil
	}

	// if restart is requested, but negotiation never started
	iceGatheringState := t.pc.ICEGatheringState()
	if iceGatheringState == webrtc.ICEGatheringStateNew {
		t.params.Logger.Debugw("skipping ICE restart on not yet started peer connection")
		return nil
	}

	// if restart is requested, and we are not ready, then continue afterwards
	if iceGatheringState == webrtc.ICEGatheringStateGathering {
		t.params.Logger.Debugw("deferring ICE restart to after gathering")
		t.restartAfterGathering = true
		return nil
	}

	if t.resetShortConnOnICERestart.CompareAndSwap(true, false) {
		t.resetShortConn()
	}

	if t.negotiationState == transport.NegotiationStateNone {
		t.outputAndClearICEStats()
		return t.createAndSendOffer(&webrtc.OfferOptions{ICERestart: true})
	}

	currentRemoteDescription := t.pc.CurrentRemoteDescription()
	if currentRemoteDescription == nil {
		// restart without current remote description, send current local description again to try recover
		offer := t.pc.LocalDescription()
		if offer == nil {
			// it should not happen, log just in case
			t.params.Logger.Warnw("ice restart without local offer", nil)
			return ErrIceRestartWithoutLocalSDP
		} else {
			t.params.Logger.Infow("deferring ice restart to next offer")
			t.setNegotiationState(transport.NegotiationStateRetry)
			t.restartAtNextOffer = true

			remoteAnswerId := t.remoteAnswerId.Load()
			if remoteAnswerId != 0 && remoteAnswerId != t.localOfferId.Load() {
				t.params.Logger.Warnw(
					"sdp state: answer not received in ICE restart", nil,
					"localOfferId", t.localOfferId.Load(),
					"remoteAnswerId", remoteAnswerId,
				)
			}

			err := t.params.Handler.OnOffer(*offer, t.localOfferId.Inc(), t.getMidToTrackIDMapping())
			if err != nil {
				prometheus.RecordServiceOperationError("offer", "write_message")
			} else {
				prometheus.RecordServiceOperationSuccess("offer")
			}
			return err
		}
	} else {
		// recover by re-applying the last answer
		t.params.Logger.Infow("recovering from client negotiation state on ICE restart")
		if err := t.pc.SetRemoteDescription(*currentRemoteDescription); err != nil {
			prometheus.RecordServiceOperationError("offer", "remote_description")
			return errors.Wrap(err, "set remote description failed")
		} else {
			t.setNegotiationState(transport.NegotiationStateNone)
			t.outputAndClearICEStats()
			return t.createAndSendOffer(&webrtc.OfferOptions{ICERestart: true})
		}
	}
}

func (t *PCTransport) handleICERestart(_ event) error {
	return t.doICERestart()
}

func (t *PCTransport) onNegotiationFailed(warning bool, reason string) {
	logFields := []any{
		"reason", reason,
		"localCurrent", t.pc.CurrentLocalDescription(),
		"localPending", t.pc.PendingLocalDescription(),
		"remoteCurrent", t.pc.CurrentRemoteDescription(),
		"remotePending", t.pc.PendingRemoteDescription(),
	}
	if warning {
		t.params.Logger.Warnw(
			"negotiation failed",
			nil,
			logFields...,
		)
	} else {
		t.params.Logger.Infow("negotiation failed", logFields...)
	}
	t.params.Handler.OnNegotiationFailed()
}

func (t *PCTransport) outputAndClearICEStats() {
	t.lock.Lock()
	stats := t.mayFailedICEStats
	t.mayFailedICEStats = nil
	t.lock.Unlock()

	if len(stats) > 0 {
		t.params.Logger.Infow("ICE candidate pair stats", "stats", iceCandidatePairStatsEncoder{stats})
	}
}

func (t *PCTransport) getMidToTrackIDMapping() map[string]string {
	transceivers := t.pc.GetTransceivers()
	midToTrackID := make(map[string]string, len(transceivers))
	for _, tr := range transceivers {
		if mid := tr.Mid(); mid != "" {
			if sender := tr.Sender(); sender != nil {
				if track := sender.Track(); track != nil {
					midToTrackID[mid] = track.ID()
				}
			}
		}
	}
	return midToTrackID
}

// ----------------------

type configureSenderParams struct {
	transceiver              *webrtc.RTPTransceiver
	enabledCodecs            []*livekit.Codec
	rtcpFeedbackConfig       RTCPFeedbackConfig
	filterOutH264HighProfile bool
	enableAudioStereo        bool
	enableAudioNACK          bool
}

func configureSender(params configureSenderParams, offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
	configureSenderCodecs(
		params.transceiver,
		params.enabledCodecs,
		params.rtcpFeedbackConfig,
		params.filterOutH264HighProfile,
	)

	if params.transceiver.Kind() == webrtc.RTPCodecTypeAudio {
		configureSenderAudio(params.transceiver, params.enableAudioStereo, params.enableAudioNACK, offerAudioPT)
	}
}

// configure subscriber transceiver for audio stereo and nack
// pion doesn't support per transciver codec configuration, so the nack of this session will be disabled
// forever once it is first disabled by a transceiver.
func configureSenderAudio(tr *webrtc.RTPTransceiver, stereo bool, nack bool, offerAudioPT map[mime.MimeType]webrtc.PayloadType) {
	sender := tr.Sender()
	if sender == nil {
		return
	}

	// enable stereo
	codecs := sender.GetParameters().Codecs
	configCodecs := make([]webrtc.RTPCodecParameters, 0, len(codecs))
	for _, c := range codecs {
		if mime.IsMimeTypeStringOpus(c.MimeType) {
			c.SDPFmtpLine = strings.ReplaceAll(c.SDPFmtpLine, ";sprop-stereo=1", "")
			if stereo {
				c.SDPFmtpLine += ";sprop-stereo=1"
			}
			if !nack {
				for i, fb := range c.RTCPFeedback {
					if fb.Type == webrtc.TypeRTCPFBNACK {
						c.RTCPFeedback = append(c.RTCPFeedback[:i], c.RTCPFeedback[i+1:]...)
						break
					}
				}
			}
		}
		// When answering a subscriber's offer (single peer connection mode), echo
		// the payload type the offer assigned for this codec instead of the server's
		// MediaEngine payload type. Otherwise the answer can advertise e.g. Opus on a
		// PT that was never offered, which Firefox rejects (received packets decode to
		// 0 samples / silence). The forwarded RTP already uses the offered PT.
		if len(offerAudioPT) > 0 {
			if pt, ok := offerAudioPT[mime.NormalizeMimeType(c.MimeType)]; ok {
				c.PayloadType = pt
			}
		}
		configCodecs = append(configCodecs, c)
	}

	tr.SetCodecPreferences(configCodecs)
}

// offerAudioPayloadTypes returns mime type -> payload type for the audio codecs
// in a remote offer, so the subscriber answer can echo the offered payload types
// (RFC 3264 6.1). The caller parses the offer before SetRemoteDescription, so this
// does not race with pion's use of the same description.
func offerAudioPayloadTypes(parsed *sdp.SessionDescription) map[mime.MimeType]webrtc.PayloadType {
	if parsed == nil {
		return nil
	}
	out := map[mime.MimeType]webrtc.PayloadType{}
	for _, md := range parsed.MediaDescriptions {
		if !strings.EqualFold(md.MediaName.Media, "audio") {
			continue
		}
		for _, a := range md.Attributes {
			if a.Key != "rtpmap" {
				continue
			}
			// value e.g. "109 opus/48000/2"
			fields := strings.Fields(a.Value)
			if len(fields) < 2 {
				continue
			}
			pt, err := strconv.ParseUint(fields[0], 10, 8)
			if err != nil {
				continue
			}
			codecName := fields[1]
			if i := strings.Index(codecName, "/"); i >= 0 {
				codecName = codecName[:i]
			}
			mt := mime.NormalizeMimeTypeCodec(codecName).ToMimeType()
			if mt == mime.MimeTypeUnknown {
				continue
			}
			out[mt] = webrtc.PayloadType(pt)
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

// In single peer connection mode, set up enabled codecs for sender.
// The config provides config of direction.
// For publisher peer connection those are publish enabled codecs
// and for subscriber peer connection those are subscribe enabled codecs.
//
// But, in single peer connection mode, if setting up a transceiver where the media is
// flowing in the other direction, the other direction codec config needs to be set.
func configureSenderCodecs(
	tr *webrtc.RTPTransceiver,
	enabledCodecs []*livekit.Codec,
	rtcpFeedbackConfig RTCPFeedbackConfig,
	filterOutH264HighProfile bool,
) {
	if len(enabledCodecs) == 0 {
		return
	}

	sender := tr.Sender()
	if sender == nil {
		return
	}

	filteredCodecs := filterCodecs(
		sender.GetParameters().Codecs,
		enabledCodecs,
		rtcpFeedbackConfig,
		filterOutH264HighProfile,
	)
	tr.SetCodecPreferences(filteredCodecs)
}

// restrictReceiverCodecsToPublishList narrows recv-side transceiver codec
// preferences to the publish list, so the answer doesn't advertise
// subscribe-only codecs as receivable. No-op in dual-PC mode.
func (t *PCTransport) restrictReceiverCodecsToPublishList() {
	for _, tr := range t.pc.GetTransceivers() {
		if tr.Direction() != webrtc.RTPTransceiverDirectionRecvonly &&
			tr.Direction() != webrtc.RTPTransceiverDirectionSendrecv {
			continue
		}
		receiver := tr.Receiver()
		if receiver == nil {
			continue
		}
		filtered := filterCodecs(
			receiver.GetParameters().Codecs,
			t.params.EnabledPublishCodecs,
			t.params.DirectionConfig.RTCPFeedback,
			false,
		)
		if len(filtered) == 0 {
			continue
		}
		if err := tr.SetCodecPreferences(filtered); err != nil {
			t.params.Logger.Warnw("failed to set recv codec preferences", err, "mid", tr.Mid())
		}
	}
}

func configureReceiverCodecs(
	tr *webrtc.RTPTransceiver,
	preferredMimeType string,
	compliesWithCodecOrderInSDPAnswer bool,
) {
	receiver := tr.Receiver()
	if receiver == nil {
		return
	}

	var preferredCodecs, leftCodecs []webrtc.RTPCodecParameters
	for _, c := range receiver.GetParameters().Codecs {
		if tr.Kind() == webrtc.RTPCodecTypeAudio {
			nackFound := false
			for _, fb := range c.RTCPFeedback {
				if fb.Type == webrtc.TypeRTCPFBNACK {
					nackFound = true
					break
				}
			}

			if !nackFound {
				c.RTCPFeedback = append(c.RTCPFeedback, webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBNACK})
			}
		}

		if mime.GetMimeTypeCodec(preferredMimeType) == mime.GetMimeTypeCodec(c.RTPCodecCapability.MimeType) {
			preferredCodecs = append(preferredCodecs, c)
		} else {
			leftCodecs = append(leftCodecs, c)
		}
	}
	if len(preferredCodecs) == 0 {
		return
	}

	reorderedCodecs := append([]webrtc.RTPCodecParameters{}, preferredCodecs...)
	if tr.Kind() == webrtc.RTPCodecTypeVideo {
		// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
		if compliesWithCodecOrderInSDPAnswer {
			reorderedCodecs = append(reorderedCodecs, leftCodecs...)
		}
	} else {
		reorderedCodecs = append(reorderedCodecs, leftCodecs...)
	}
	tr.SetCodecPreferences(reorderedCodecs)
}

func nonSimulcastRTXRepairsFromSDP(s *sdp.SessionDescription, logger logger.Logger) map[uint32]uint32 {
	rtxRepairFlows := map[uint32]uint32{}
	for _, media := range s.MediaDescriptions {
		// extract rtx repair flows from the media section for non-simulcast stream,
		// pion will handle simulcast streams by rid probe, don't need handle it here.
		var ridFound bool
		rtxPairs := make(map[uint32]uint32)
	findRTX:
		for _, attr := range media.Attributes {
			switch attr.Key {
			case "rid":
				ridFound = true
				break findRTX
			case sdp.AttrKeySSRCGroup:
				split := strings.Split(attr.Value, " ")
				if split[0] == sdp.SemanticTokenFlowIdentification {
					// Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section
					// as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first
					// (2231627014) as specified in RFC5576
					if len(split) == 3 {
						baseSsrc, err := strconv.ParseUint(split[1], 10, 32)
						if err != nil {
							logger.Warnw("Failed to parse SSRC", err, "ssrc", split[1])
							continue
						}
						rtxRepairFlow, err := strconv.ParseUint(split[2], 10, 32)
						if err != nil {
							logger.Warnw("Failed to parse SSRC", err, "ssrc", split[2])
							continue
						}
						rtxPairs[uint32(rtxRepairFlow)] = uint32(baseSsrc)
					}
				}
			}
		}
		if !ridFound {
			maps.Copy(rtxRepairFlows, rtxPairs)
		}
	}

	return rtxRepairFlows
}

// ----------------------

type iceCandidatePairStatsEncoder struct {
	stats []iceCandidatePairStats
}

func (e iceCandidatePairStatsEncoder) MarshalLogArray(arr zapcore.ArrayEncoder) error {
	for _, s := range e.stats {
		if err := arr.AppendObject(s); err != nil {
			return err
		}
	}
	return nil
}

type iceCandidatePairStats struct {
	webrtc.ICECandidatePairStats
	local, remote webrtc.ICECandidateStats
}

func (r iceCandidatePairStats) MarshalLogObject(e zapcore.ObjectEncoder) error {
	candidateToString := func(c webrtc.ICECandidateStats) string {
		return fmt.Sprintf("%s:%d %s type(%s/%s), priority(%d)", c.IP, c.Port, c.Protocol, c.CandidateType, c.RelayProtocol, c.Priority)
	}
	e.AddString("state", string(r.State))
	e.AddBool("nominated", r.Nominated)
	e.AddString("local", candidateToString(r.local))
	e.AddString("remote", candidateToString(r.remote))
	e.AddUint64("requestsSent", r.RequestsSent)
	e.AddUint64("responsesReceived", r.ResponsesReceived)
	e.AddUint64("requestsReceived", r.RequestsReceived)
	e.AddUint64("responsesSent", r.ResponsesSent)
	e.AddTime("firstRequestSentAt", r.FirstRequestTimestamp.Time())
	e.AddTime("lastRequestSentAt", r.LastRequestTimestamp.Time())
	e.AddTime("firstResponseReceivedAt", r.FirstResponseTimestamp.Time())
	e.AddTime("lastResponseReceivedAt", r.LastResponseTimestamp.Time())
	e.AddTime("firstRequestReceivedAt", r.FirstRequestReceivedTimestamp.Time())
	e.AddTime("lastRequestReceivedAt", r.LastRequestReceivedTimestamp.Time())

	return nil
}
