Files
livekit/pkg/rtc/transport.go
T
Raja Subramanian 1ab2bf043b Clean up packet size logging (#4536)
Reverting
- https://github.com/livekit/livekit/pull/4521
- https://github.com/livekit/livekit/pull/4525

There are TWCC feedback packets that are larger than MTU. Seems to
happen under a couple of conditions
1. Bad client data, i. e. severely out-of-order packets, bad sequence
   numbers, etc.
2. On an ICE restart - this is rare, but it seemed to be flaky network
   with some packets arriving and some not and causing a lot of gaps.

Either case, not much to do. If fargmentation/re-assembly back to
publisher works, the feedback will make it through. If not, feedbacks
will be missed and clients have to work with some missing data which is
not unexpected and the protocol is designed to handle.

However, filed pion/interceptor issue just in case - https://github.com/pion/interceptor/issues/416
2026-05-20 23:58:05 +05:30

3289 lines
95 KiB
Go

// 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]
iceStartedAt time.Time
iceConnectedAt time.Time
firstConnectedAt time.Time
connectedAt 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
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
FireOnTrackBySdp bool
DataChannelMaxBufferedAmount uint64
DatachannelSlowThreshold int
DatachannelLossyTargetLatency time.Duration
// for development test
DatachannelMaxReceiverBufferSize int
EnableDataTracks 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)
}
//
// 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, webrtc.ICECandidateTypeHost); 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.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.iceStartedAt.IsZero() {
t.iceStartedAt = 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.iceConnectedAt.IsZero() {
//
// Record initial connection time.
// This prevents reset of connected at time if ICE goes `Connected` -> `Disconnected` -> `Connected`.
//
t.iceConnectedAt = at
// set failure timer for dtls handshake
iceDuration := at.Sub(t.iceStartedAt)
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
}
}
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.iceStartedAt = time.Time{}
t.iceConnectedAt = time.Time{}
t.connectedAt = 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.iceConnectedAt.IsZero() {
return false, 0
}
duration := at.Sub(t.iceConnectedAt)
return duration < shortConnectionThreshold, duration
}
func (t *PCTransport) setConnectedAt(at time.Time) bool {
t.lock.Lock()
t.connectedAt = at
if !t.firstConnectedAt.IsZero() {
t.lock.Unlock()
return false
}
t.firstConnectedAt = at
prometheus.RecordServiceOperationSuccess("peer_connection")
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.setConnectedAt(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, 0, 0)
}
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()
}
}
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.connectedAt.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,
enabledCodecs,
rtcpFeedbackConfig,
!t.params.IsOfferer,
enableAudioStereo,
enableAudioNACK,
}
if !t.params.IsOfferer {
t.sendersPendingConfigMu.Lock()
t.sendersPendingConfig = append(t.sendersPendingConfig, params)
t.sendersPendingConfigMu.Unlock()
return
}
configureSender(params)
}
func (t *PCTransport) processSendersPendingConfig() {
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)
}
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, 0, 0)
}
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) HasEverConnected() bool {
t.lock.RLock()
defer t.lock.RUnlock()
return !t.firstConnectedAt.IsZero()
}
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()
// add local candidates to ICE connection details
parsed, err := cld.Unmarshal()
if err == nil {
addLocalICECandidates := func(attrs []sdp.Attribute) {
for _, a := range attrs {
if a.IsICECandidate() {
c, err := ice.UnmarshalCandidate(a.Value)
if err != nil {
continue
}
t.connectionDetails.AddLocalICECandidate(c, false, false)
}
}
}
addLocalICECandidates(parsed.Attributes)
for _, m := range parsed.MediaDescriptions {
addLocalICECandidates(m.Attributes)
}
}
answerId := t.remoteOfferId.Load()
t.localAnswerId.Store(answerId)
return *cld, 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 crendential from fragment", err,
"sdpFragment", sdpFragment,
)
return ErrInvalidSDPFragment
}
remoteICEUfrag, remoteICEPwd, err := lksdp.ExtractICECredential(parsedRemote)
if err != nil {
t.params.Logger.Warnw("could not get ICE crendential 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)
}
ans, err := t.pc.CreateAnswer(nil)
if err != nil {
t.params.Logger.Warnw("could not create answer", err)
return "", err
}
if err = t.pc.SetLocalDescription(ans); 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()
// add local candidates to ICE connection details
parsedAnswer, err := cld.Unmarshal()
if err != nil {
t.params.Logger.Warnw("could not parse local description", err)
return "", err
}
addLocalICECandidates := func(attrs []sdp.Attribute) {
for _, a := range attrs {
if a.IsICECandidate() {
c, err := ice.UnmarshalCandidate(a.Value)
if err != nil {
continue
}
t.connectionDetails.AddLocalICECandidate(c, false, false)
}
}
}
addLocalICECandidates(parsedAnswer.Attributes)
for _, m := range parsedAnswer.MediaDescriptions {
addLocalICECandidates(m.Attributes)
}
parsedFragmentAnswer, err := lksdp.ExtractSDPFragment(parsedAnswer)
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")
if err := t.setRemoteDescription(offer); err != nil {
return err
}
t.params.Handler.OnSetRemoteDescriptionOffer()
t.processSendersPendingConfig()
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", "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) 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()
if !excluded {
if !t.params.Config.UseMDNS && types.IsICECandidateMDNS(c) {
excluded = true
}
}
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 preferTCP {
t.params.Logger.Debugw("local offer (unfiltered)", "sdp", offer.SDP)
}
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")
}
//
// 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 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 preferTCP {
t.params.Logger.Debugw("remote description (unfiltered)", "type", sd.Type, "sdp", sd.SDP)
}
sd = t.filterCandidates(sd, preferTCP, false)
if 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 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 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("asnwer")
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()
}
if err := t.setRemoteDescription(*sd); err != nil {
return err
}
t.params.Handler.OnSetRemoteDescriptionOffer()
t.processSendersPendingConfig()
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 aleady compelted
// 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) {
configureSenderCodecs(
params.transceiver,
params.enabledCodecs,
params.rtcpFeedbackConfig,
params.filterOutH264HighProfile,
)
if params.transceiver.Kind() == webrtc.RTPCodecTypeAudio {
configureSenderAudio(params.transceiver, params.enableAudioStereo, params.enableAudioNACK)
}
}
// 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) {
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
}
}
}
}
configCodecs = append(configCodecs, c)
}
tr.SetCodecPreferences(configCodecs)
}
// In single peer connection mode, set up enebled 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
}