Files
livekit/test/client/client.go
T
Raja Subramanian 972c64a29b Check for ICE connection before closing participant on signal close. (#4780)
* Check for ICE connection before closing participant on signal close.

Only close the participant if ICE has not connected by the time signal
source is closed. If ICE had connected, candidates have been exchanged
and link was established. So, it should be resumable.

Waiting for DTLS closed the participant sometimes in the windowa after
ICE connection, but before DTLS finishes and that unnecessarily closed
the participant forcing a full reconnect.

* variable name
2026-08-20 13:07:28 +05:30

1391 lines
39 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 client
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"math/rand/v2"
"net/http"
"net/url"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
"github.com/thoas/go-funk"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/signalling"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/rtc/transport/transportfakes"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
)
type SignalRequestHandler func(msg *livekit.SignalRequest) error
type SignalRequestInterceptor func(msg *livekit.SignalRequest, next SignalRequestHandler) error
type SignalResponseHandler func(msg *livekit.SignalResponse) error
type SignalResponseInterceptor func(msg *livekit.SignalResponse, next SignalResponseHandler) error
type RTCClient struct {
useSinglePeerConnection bool
id livekit.ParticipantID
conn *websocket.Conn
publisher *rtc.PCTransport
subscriber *rtc.PCTransport
enabledCodecs []*livekit.Codec
forceRelay bool
transportReady chan struct{}
// sid => track
localTracks map[string]webrtc.TrackLocal
trackSenders map[string]*webrtc.RTPSender
lock sync.Mutex
wsLock sync.Mutex
ctx context.Context
cancel context.CancelFunc
me *webrtc.MediaEngine // optional, populated only when receiving tracks
subscribedTracks map[livekit.ParticipantID][]*webrtc.TrackRemote
localParticipant *livekit.ParticipantInfo
remoteParticipants map[livekit.ParticipantID]*livekit.ParticipantInfo
signalRequestInterceptor SignalRequestInterceptor
signalResponseInterceptor SignalResponseInterceptor
icQueue [2]atomic.Pointer[webrtc.ICECandidate]
subscriberAsPrimary atomic.Bool
publisherFullyEstablished atomic.Bool
subscriberFullyEstablished atomic.Bool
pongReceivedAt atomic.Int64
// tracks waiting to be acked, cid => trackInfo
pendingPublishedTracks map[string]*livekit.TrackInfo
// remote tracks waiting to be processed
pendingRemoteTracks []*webrtc.TrackRemote
pendingTrackWriters []TrackWriter
OnConnected func()
OnDataReceived func(data []byte, sid string)
OnDataUnlabeledReceived func(data []byte)
refreshToken string
// map of livekit.ParticipantID and last packet
lastPackets map[livekit.ParticipantID]*rtp.Packet
bytesReceived map[livekit.ParticipantID]uint64
subscriptionResponse atomic.Pointer[livekit.SubscriptionResponse]
nextDataTrackHandle atomic.Uint32
pendingPublishedDataTracks map[uint16]*livekit.DataTrackInfo
pendingDataTrackWriters []TrackWriter
subscribedDataTracks map[livekit.ParticipantID]map[uint16]*DataTrackRemote
}
var (
// minimal settings only with stun server
rtcConf = webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
extMimeMapping = map[string]string{
".ivf": mime.MimeTypeVP8.String(),
".h264": mime.MimeTypeH264.String(),
".ogg": mime.MimeTypeOpus.String(),
}
)
type Options struct {
AutoSubscribe bool
AutoSubscribeDataTrack bool
Publish string
Attributes map[string]string
ClientInfo *livekit.ClientInfo
DisabledCodecs []webrtc.RTPCodecCapability
TokenCustomizer func(token *auth.AccessToken, grants *auth.VideoGrant)
SignalRequestInterceptor SignalRequestInterceptor
SignalResponseInterceptor SignalResponseInterceptor
UseJoinRequestQueryParam bool
RTCServicePath string
ForceRelay bool
}
func NewWebSocketConn(host, token string, opts *Options) (*websocket.Conn, error) {
rtcServicePath := "/rtc"
if opts != nil && opts.RTCServicePath != "" {
rtcServicePath = opts.RTCServicePath
}
parsedURL, err := url.Parse(host + rtcServicePath)
if err != nil {
return nil, err
}
requestHeader := make(http.Header)
SetAuthorizationToken(requestHeader, token)
connectUrl := parsedURL.String()
if opts != nil && opts.UseJoinRequestQueryParam {
clientInfo := &livekit.ClientInfo{
Os: runtime.GOOS,
Sdk: livekit.ClientInfo_GO,
Protocol: int32(types.CurrentProtocol),
}
if opts.ClientInfo != nil {
clientInfo = opts.ClientInfo
}
connectionSettings := &livekit.ConnectionSettings{
AutoSubscribe: opts.AutoSubscribe,
AutoSubscribeDataTrack: &opts.AutoSubscribeDataTrack,
}
joinRequest := &livekit.JoinRequest{
ClientInfo: clientInfo,
ConnectionSettings: connectionSettings,
ParticipantAttributes: opts.Attributes,
}
if marshalled, err := proto.Marshal(joinRequest); err == nil {
wrapped := &livekit.WrappedJoinRequest{
JoinRequest: marshalled,
}
if marshalled, err := proto.Marshal(wrapped); err == nil {
connectUrl += fmt.Sprintf("?join_request=%s", base64.URLEncoding.EncodeToString(marshalled))
}
}
} else {
connectUrl += fmt.Sprintf("?protocol=%d", types.CurrentProtocol)
sdk := "go"
if opts != nil {
connectUrl += fmt.Sprintf("&auto_subscribe=%t", opts.AutoSubscribe)
connectUrl += fmt.Sprintf("&auto_subscribe_data_track=%t", opts.AutoSubscribeDataTrack)
if opts.Publish != "" {
connectUrl += encodeQueryParam("publish", opts.Publish)
}
if len(opts.Attributes) != 0 {
data, err := json.Marshal(opts.Attributes)
if err != nil {
return nil, err
}
connectUrl += encodeQueryParam("attributes", base64.URLEncoding.EncodeToString(data))
}
if opts.ClientInfo != nil {
if opts.ClientInfo.DeviceModel != "" {
connectUrl += encodeQueryParam("device_model", opts.ClientInfo.DeviceModel)
}
if opts.ClientInfo.Os != "" {
connectUrl += encodeQueryParam("os", opts.ClientInfo.Os)
}
if opts.ClientInfo.Sdk != livekit.ClientInfo_UNKNOWN {
sdk = opts.ClientInfo.Sdk.String()
}
}
}
connectUrl += encodeQueryParam("sdk", sdk)
}
logger.Infow("connecting to", "url", parsedURL.String())
conn, _, err := websocket.DefaultDialer.Dial(connectUrl, requestHeader)
return conn, err
}
func SetAuthorizationToken(header http.Header, token string) {
header.Set("Authorization", "Bearer "+token)
}
func NewRTCClient(conn *websocket.Conn, useSinglePeerConnection bool, opts *Options) (*RTCClient, error) {
c := &RTCClient{
useSinglePeerConnection: useSinglePeerConnection,
conn: conn,
localTracks: make(map[string]webrtc.TrackLocal),
trackSenders: make(map[string]*webrtc.RTPSender),
pendingPublishedTracks: make(map[string]*livekit.TrackInfo),
subscribedTracks: make(map[livekit.ParticipantID][]*webrtc.TrackRemote),
remoteParticipants: make(map[livekit.ParticipantID]*livekit.ParticipantInfo),
me: &webrtc.MediaEngine{},
lastPackets: make(map[livekit.ParticipantID]*rtp.Packet),
bytesReceived: make(map[livekit.ParticipantID]uint64),
pendingPublishedDataTracks: make(map[uint16]*livekit.DataTrackInfo),
subscribedDataTracks: make(map[livekit.ParticipantID]map[uint16]*DataTrackRemote),
transportReady: make(chan struct{}),
}
c.nextDataTrackHandle.Store(uint32(rand.IntN(8192)))
c.ctx, c.cancel = context.WithCancel(context.Background())
var codecs []*livekit.Codec
for _, codec := range []*livekit.Codec{
{
Mime: "audio/opus",
},
{
Mime: "video/vp8",
},
{
Mime: "video/h264",
},
} {
var disabled bool
if opts != nil {
for _, dc := range opts.DisabledCodecs {
if mime.IsMimeTypeStringEqual(dc.MimeType, codec.Mime) && (dc.SDPFmtpLine == "" || dc.SDPFmtpLine == codec.FmtpLine) {
disabled = true
break
}
}
}
if !disabled {
codecs = append(codecs, codec)
}
}
c.enabledCodecs = codecs
if opts != nil {
c.signalRequestInterceptor = opts.SignalRequestInterceptor
c.signalResponseInterceptor = opts.SignalResponseInterceptor
c.forceRelay = opts.ForceRelay
}
return c, nil
}
func (c *RTCClient) createTransport(rtcconf webrtc.Configuration) error {
var err error
conf := rtc.WebRTCConfig{
WebRTCConfig: rtcconfig.WebRTCConfig{
Configuration: rtcconf,
},
}
conf.SettingEngine.SetLite(false)
conf.SettingEngine.SetAnsweringDTLSRole(webrtc.DTLSRoleClient)
ff := buffer.NewFactoryOfBufferFactory(500, 200)
conf.SetBufferFactory(ff.CreateBufferFactory())
//
// The signal targets are from point of view of server.
// From client side, they are flipped,
// i. e. the publisher transport on client side has SUBSCRIBER signal target (i. e. publisher is offerer).
// Same applies for subscriber transport also
//
publisherHandler := &transportfakes.FakeHandler{}
c.publisher, err = rtc.NewPCTransport(rtc.TransportParams{
Config: &conf,
DirectionConfig: conf.Subscriber,
EnabledPublishCodecs: c.enabledCodecs,
EnabledSubscribeCodecs: c.enabledCodecs,
IsOfferer: true,
IsSendSide: true,
Handler: publisherHandler,
DatachannelMaxReceiverBufferSize: 1500,
DatachannelSlowThreshold: 1024 * 1024 * 1024,
FireOnTrackBySdp: true,
EnableDataTracks: true,
})
if err != nil {
return err
}
publisherHandler.OnICECandidateCalls(func(ic *webrtc.ICECandidate, t livekit.SignalTarget) error {
return c.SendIceCandidate(ic, livekit.SignalTarget_PUBLISHER)
})
publisherHandler.OnTrackCalls(func(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
go c.processRemoteTrack(track)
})
publisherHandler.OnDataMessageCalls(c.handleDataMessage)
publisherHandler.OnDataMessageUnlabeledCalls(c.handleDataMessageUnlabeled)
publisherHandler.OnDataTrackMessageCalls(c.handleDataTrackMessage)
publisherHandler.OnInitialConnectedCalls(func() {
logger.Debugw("publisher initial connected", "participant", c.localParticipant.Identity)
c.lock.Lock()
defer c.lock.Unlock()
for _, tw := range c.pendingTrackWriters {
if err := tw.Start(); err != nil {
logger.Errorw("track writer error", err)
}
}
c.pendingTrackWriters = nil
for _, dtw := range c.pendingDataTrackWriters {
if err := dtw.Start(); err != nil {
logger.Errorw("data track writer error", err)
}
}
c.pendingDataTrackWriters = nil
if c.OnConnected != nil {
go c.OnConnected()
}
})
publisherHandler.OnOfferCalls(c.onOffer)
publisherHandler.OnFullyEstablishedCalls(func() {
logger.Debugw("publisher fully established", "participant", c.localParticipant.Identity, "participantID", c.localParticipant.Sid)
c.publisherFullyEstablished.Store(true)
})
ordered := true
if err := c.publisher.CreateDataChannel(rtc.ReliableDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
}); err != nil {
return err
}
if err := c.publisher.CreateDataChannel("pubraw", &webrtc.DataChannelInit{
Ordered: &ordered,
}); err != nil {
return err
}
ordered = false
maxRetransmits := uint16(0)
if err := c.publisher.CreateDataChannel(rtc.LossyDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
MaxRetransmits: &maxRetransmits,
}); err != nil {
return err
}
if err := c.publisher.CreateDataChannel(rtc.DataTrackDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
MaxRetransmits: &maxRetransmits,
}); err != nil {
return err
}
if !c.useSinglePeerConnection {
subscriberHandler := &transportfakes.FakeHandler{}
c.subscriber, err = rtc.NewPCTransport(rtc.TransportParams{
Config: &conf,
DirectionConfig: conf.Publisher,
EnabledPublishCodecs: c.enabledCodecs,
EnabledSubscribeCodecs: c.enabledCodecs,
Handler: subscriberHandler,
DatachannelMaxReceiverBufferSize: 1500,
DatachannelSlowThreshold: 1024 * 1024 * 1024,
FireOnTrackBySdp: true,
EnableDataTracks: true,
})
if err != nil {
return err
}
ordered := true
if err := c.subscriber.CreateReadableDataChannel("subraw", &webrtc.DataChannelInit{
Ordered: &ordered,
}); err != nil {
return err
}
subscriberHandler.OnICECandidateCalls(func(ic *webrtc.ICECandidate, t livekit.SignalTarget) error {
if ic == nil {
return nil
}
return c.SendIceCandidate(ic, livekit.SignalTarget_SUBSCRIBER)
})
subscriberHandler.OnTrackCalls(func(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
go c.processRemoteTrack(track)
})
subscriberHandler.OnDataMessageCalls(c.handleDataMessage)
subscriberHandler.OnDataMessageUnlabeledCalls(c.handleDataMessageUnlabeled)
subscriberHandler.OnDataTrackMessageCalls(c.handleDataTrackMessage)
subscriberHandler.OnInitialConnectedCalls(func() {
logger.Debugw("subscriber initial connected", "participant", c.localParticipant.Identity)
c.lock.Lock()
defer c.lock.Unlock()
for _, tw := range c.pendingTrackWriters {
if err := tw.Start(); err != nil {
logger.Errorw("track writer error", err)
}
}
c.pendingTrackWriters = nil
for _, dtw := range c.pendingDataTrackWriters {
if err := dtw.Start(); err != nil {
logger.Errorw("data track writer error", err)
}
}
c.pendingDataTrackWriters = nil
if c.OnConnected != nil {
go c.OnConnected()
}
})
subscriberHandler.OnFullyEstablishedCalls(func() {
logger.Debugw("subscriber fully established", "participant", c.localParticipant.Identity, "participantID", c.localParticipant.Sid)
c.subscriberFullyEstablished.Store(true)
})
subscriberHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
// send remote an answer
logger.Infow(
"sending subscriber answer",
"participant", c.localParticipant.Identity,
"sdp", answer,
)
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_Answer{
Answer: signalling.ToProtoSessionDescription(answer, answerId, nil),
},
})
})
} else {
go c.ensurePublisherConnected()
}
return nil
}
func (c *RTCClient) ID() livekit.ParticipantID {
return c.id
}
// create an offer for the server
func (c *RTCClient) Run() error {
c.conn.SetCloseHandler(func(code int, text string) error {
// when closed, stop connection
logger.Infow("connection closed", "code", code, "text", text)
c.Stop()
return nil
})
// run the session
for {
res, err := c.ReadResponse()
if errors.Is(err, io.EOF) {
return nil
} else if err != nil {
logger.Errorw("error while reading", err)
return err
}
if c.signalResponseInterceptor != nil {
err = c.signalResponseInterceptor(res, c.handleSignalResponse)
} else {
err = c.handleSignalResponse(res)
}
if err != nil {
return err
}
}
}
func (c *RTCClient) handleSignalResponse(res *livekit.SignalResponse) error {
switch msg := res.Message.(type) {
case *livekit.SignalResponse_Join:
c.localParticipant = msg.Join.Participant
c.id = livekit.ParticipantID(msg.Join.Participant.Sid)
c.lock.Lock()
for _, p := range msg.Join.OtherParticipants {
c.remoteParticipants[livekit.ParticipantID(p.Sid)] = p
}
c.lock.Unlock()
var iceServers []webrtc.ICEServer
for _, is := range msg.Join.IceServers {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: is.Urls,
Username: is.Username,
Credential: is.Credential,
})
}
if len(iceServers) == 0 {
iceServers = rtcConf.ICEServers
}
rtcconf := rtcConf
rtcconf.ICEServers = iceServers
if c.forceRelay {
rtcconf.ICETransportPolicy = webrtc.ICETransportPolicyRelay
}
if err := c.createTransport(rtcconf); err != nil {
return err
}
close(c.transportReady)
// if publish only, negotiate
if !msg.Join.SubscriberPrimary {
c.subscriberAsPrimary.Store(false)
c.publisher.Negotiate(false)
} else {
c.subscriberAsPrimary.Store(true)
}
if c.subscriber != nil {
logger.Infow("join accepted, awaiting offer", "participant", msg.Join.Participant.Identity)
} else {
logger.Infow("join accepted", "participant", msg.Join.Participant.Identity)
}
case *livekit.SignalResponse_Answer:
logger.Infow(
"received server answer",
"participant", c.localParticipant.Identity,
"answer", msg.Answer.Sdp,
)
c.handleAnswer(signalling.FromProtoSessionDescription(msg.Answer))
case *livekit.SignalResponse_Offer:
desc, offerId, midToTrackID := signalling.FromProtoSessionDescription(msg.Offer)
logger.Infow(
"received server offer",
"participant", c.localParticipant.Identity,
"sdp", desc,
"offerId", offerId,
"midToTrackID", midToTrackID,
)
c.handleOffer(desc, offerId, midToTrackID)
case *livekit.SignalResponse_Trickle:
candidateInit, err := signalling.FromProtoTrickle(msg.Trickle)
if err != nil {
return err
}
if msg.Trickle.Target == livekit.SignalTarget_PUBLISHER {
c.publisher.AddICECandidate(candidateInit)
} else {
c.subscriber.AddICECandidate(candidateInit)
}
case *livekit.SignalResponse_Update:
c.lock.Lock()
for _, p := range msg.Update.Participants {
if livekit.ParticipantID(p.Sid) != c.id {
if p.State != livekit.ParticipantInfo_DISCONNECTED {
c.remoteParticipants[livekit.ParticipantID(p.Sid)] = p
} else {
delete(c.remoteParticipants, livekit.ParticipantID(p.Sid))
}
}
}
c.lock.Unlock()
case *livekit.SignalResponse_TrackPublished:
logger.Debugw(
"track published",
"participant", c.localParticipant.Identity,
"cid", msg.TrackPublished.Cid,
"trackID", msg.TrackPublished.Track.Sid,
"trackName", msg.TrackPublished.Track.Name,
)
c.lock.Lock()
c.pendingPublishedTracks[msg.TrackPublished.Cid] = msg.TrackPublished.Track
c.lock.Unlock()
case *livekit.SignalResponse_RefreshToken:
c.lock.Lock()
c.refreshToken = msg.RefreshToken
c.lock.Unlock()
case *livekit.SignalResponse_TrackUnpublished:
sid := msg.TrackUnpublished.TrackSid
c.lock.Lock()
if sender := c.trackSenders[sid]; sender != nil {
if err := c.publisher.RemoveTrack(sender); err != nil {
logger.Errorw("Could not unpublish track", err)
}
c.publisher.Negotiate(false)
}
delete(c.trackSenders, sid)
delete(c.localTracks, sid)
c.lock.Unlock()
case *livekit.SignalResponse_Pong:
c.pongReceivedAt.Store(msg.Pong)
case *livekit.SignalResponse_SubscriptionResponse:
c.subscriptionResponse.Store(msg.SubscriptionResponse)
case *livekit.SignalResponse_MediaSectionsRequirement:
logger.Infow(
"received media sections requirement",
"participant", c.localParticipant.Identity,
"numAudios", msg.MediaSectionsRequirement.NumAudios,
"numVideos", msg.MediaSectionsRequirement.NumVideos,
)
c.handleMediaSectionsRequirement(msg.MediaSectionsRequirement)
case *livekit.SignalResponse_PublishDataTrackResponse:
logger.Debugw(
"data track published",
"participant", c.localParticipant.Identity,
"trackID", msg.PublishDataTrackResponse.Info.Sid,
"trackHandle", msg.PublishDataTrackResponse.Info.PubHandle,
"trackName", msg.PublishDataTrackResponse.Info.Name,
)
c.lock.Lock()
c.pendingPublishedDataTracks[uint16(msg.PublishDataTrackResponse.Info.PubHandle)] = msg.PublishDataTrackResponse.Info
c.lock.Unlock()
case *livekit.SignalResponse_DataTrackSubscriberHandles:
logger.Infow(
"received data track subscriber handles",
"participant", c.localParticipant.Identity,
"handles", msg.DataTrackSubscriberHandles.SubHandles,
)
c.lock.Lock()
// create new remote data tracks if one does not exist for a handle
for handle, publishedDataTrack := range msg.DataTrackSubscriberHandles.SubHandles {
publisherID := livekit.ParticipantID(publishedDataTrack.PublisherSid)
tracks := c.subscribedDataTracks[publisherID]
if tracks == nil {
c.subscribedDataTracks[publisherID] = make(map[uint16]*DataTrackRemote)
tracks = c.subscribedDataTracks[publisherID]
}
if tracks[uint16(handle)] == nil {
tracks[uint16(handle)] = NewDataTrackRemote(
livekit.ParticipantIdentity(publishedDataTrack.PublisherIdentity),
livekit.ParticipantID(publishedDataTrack.PublisherSid),
uint16(handle),
livekit.TrackID(publishedDataTrack.TrackSid),
logger.GetLogger().WithValues("participant", c.localParticipant.Identity, "participantID", c.localParticipant.Sid),
)
}
}
// delete remote data tracks that have gone away
for publisherID, tracks := range c.subscribedDataTracks {
for handle, dataTrackRemote := range tracks {
if msg.DataTrackSubscriberHandles.SubHandles[uint32(handle)] == nil {
dataTrackRemote.Close()
delete(tracks, handle)
if len(tracks) == 0 {
delete(c.subscribedDataTracks, publisherID)
}
}
}
}
c.lock.Unlock()
}
return nil
}
func (c *RTCClient) WaitUntilConnected(timeout time.Duration) error {
if timeout == 0 {
timeout = 20 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
for {
select {
case <-ctx.Done():
id := string(c.ID())
if c.localParticipant != nil {
id = c.localParticipant.Identity
}
return fmt.Errorf("%s could not connect after timeout", id)
case <-time.After(10 * time.Millisecond):
if c.subscriberAsPrimary.Load() {
if c.subscriberFullyEstablished.Load() {
return nil
}
} else {
if c.publisherFullyEstablished.Load() {
return nil
}
}
}
}
}
func (c *RTCClient) ReadResponse() (*livekit.SignalResponse, error) {
for {
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, err
}
if c.ctx.Err() != nil {
return nil, c.ctx.Err()
}
msg := &livekit.SignalResponse{}
switch messageType {
case websocket.PingMessage:
_ = c.conn.WriteMessage(websocket.PongMessage, nil)
continue
case websocket.BinaryMessage:
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, err
default:
return nil, fmt.Errorf("unexpected message received: %v", messageType)
}
}
}
func (c *RTCClient) SubscribedTracks() map[livekit.ParticipantID][]*webrtc.TrackRemote {
// create a copy of this
c.lock.Lock()
defer c.lock.Unlock()
tracks := make(map[livekit.ParticipantID][]*webrtc.TrackRemote, len(c.subscribedTracks))
maps.Copy(tracks, c.subscribedTracks)
return tracks
}
func (c *RTCClient) SubscribedDataTracks() map[livekit.ParticipantID]map[uint16]*DataTrackRemote {
// create a copy of this
c.lock.Lock()
defer c.lock.Unlock()
tracks := make(map[livekit.ParticipantID]map[uint16]*DataTrackRemote, len(c.subscribedDataTracks))
for publisherID, sts := range c.subscribedDataTracks {
tracks[publisherID] = make(map[uint16]*DataTrackRemote)
maps.Copy(tracks[publisherID], sts)
}
return tracks
}
func (c *RTCClient) RemoteParticipants() []*livekit.ParticipantInfo {
c.lock.Lock()
defer c.lock.Unlock()
return funk.Values(c.remoteParticipants).([]*livekit.ParticipantInfo)
}
func (c *RTCClient) GetRemoteParticipant(sid livekit.ParticipantID) *livekit.ParticipantInfo {
c.lock.Lock()
defer c.lock.Unlock()
return c.remoteParticipants[sid]
}
func (c *RTCClient) Stop() {
logger.Infow("stopping client", "ID", c.ID())
_ = c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_Leave{
Leave: &livekit.LeaveRequest{
Reason: livekit.DisconnectReason_CLIENT_INITIATED,
Action: livekit.LeaveRequest_DISCONNECT,
},
},
})
c.publisherFullyEstablished.Store(false)
c.subscriberFullyEstablished.Store(false)
_ = c.conn.Close()
if c.publisher != nil {
c.publisher.Close()
}
if c.subscriber != nil {
c.subscriber.Close()
}
c.cancel()
}
func (c *RTCClient) RefreshToken() string {
c.lock.Lock()
defer c.lock.Unlock()
return c.refreshToken
}
func (c *RTCClient) PongReceivedAt() int64 {
return c.pongReceivedAt.Load()
}
func (c *RTCClient) GetSubscriptionResponseAndClear() *livekit.SubscriptionResponse {
return c.subscriptionResponse.Swap(nil)
}
func (c *RTCClient) SendPing() error {
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_Ping{
Ping: time.Now().UnixNano(),
},
})
}
func (c *RTCClient) SendRequest(msg *livekit.SignalRequest) error {
if c.signalRequestInterceptor != nil {
return c.signalRequestInterceptor(msg, c.sendRequest)
} else {
return c.sendRequest(msg)
}
}
func (c *RTCClient) sendRequest(msg *livekit.SignalRequest) error {
payload, err := proto.Marshal(msg)
if err != nil {
return err
}
c.wsLock.Lock()
defer c.wsLock.Unlock()
return c.conn.WriteMessage(websocket.BinaryMessage, payload)
}
func (c *RTCClient) SendIceCandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
prevIC := c.icQueue[target].Swap(ic)
if prevIC == nil {
return nil
}
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_Trickle{
Trickle: signalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil),
},
})
}
func (c *RTCClient) SetAttributes(attrs map[string]string) error {
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_UpdateMetadata{
UpdateMetadata: &livekit.UpdateParticipantMetadata{
Attributes: attrs,
},
},
})
}
func (c *RTCClient) hasPrimaryEverConnected() bool {
if c.subscriberAsPrimary.Load() {
return c.subscriber.PeerConnectionHasEverConnected()
} else {
return c.publisher.PeerConnectionHasEverConnected()
}
}
type AddTrackParams struct {
NoWriter bool
}
type AddTrackOption func(params *AddTrackParams)
func AddTrackNoWriter() AddTrackOption {
return func(params *AddTrackParams) {
params.NoWriter = true
}
}
func (c *RTCClient) AddTrack(track *webrtc.TrackLocalStaticSample, path string, opts ...AddTrackOption) (writer TrackWriter, err error) {
select {
case <-c.transportReady:
case <-c.ctx.Done():
return nil, c.ctx.Err()
}
var params AddTrackParams
for _, opt := range opts {
opt(&params)
}
trackType := livekit.TrackType_AUDIO
if track.Kind() == webrtc.RTPCodecTypeVideo {
trackType = livekit.TrackType_VIDEO
}
sender, _, err := c.publisher.AddTrack(track, types.AddTrackParams{}, nil, rtc.RTCPFeedbackConfig{})
if err != nil {
logger.Errorw(
"add track failed", err,
"participant", c.localParticipant.Identity,
"participantID", c.localParticipant.Sid,
"trackID", track.ID(),
)
return
}
if err = c.SendAddTrack(track.ID(), track.Codec().MimeType, track.StreamID(), trackType); err != nil {
return
}
// wait till track published message is received
timeout := time.After(5 * time.Second)
var ti *livekit.TrackInfo
for {
select {
case <-timeout:
return nil, errors.New("could not publish track after timeout")
default:
c.lock.Lock()
ti = c.pendingPublishedTracks[track.ID()]
if ti != nil {
delete(c.pendingPublishedTracks, track.ID())
c.lock.Unlock()
break
}
c.lock.Unlock()
time.Sleep(50 * time.Millisecond)
}
if ti != nil {
break
}
}
c.lock.Lock()
defer c.lock.Unlock()
c.localTracks[ti.Sid] = track
c.trackSenders[ti.Sid] = sender
c.publisher.Negotiate(false)
if !params.NoWriter {
writer = NewTrackWriter(c.ctx, track, path)
// write tracks only after connection established
if c.hasPrimaryEverConnected() {
err = writer.Start()
} else {
c.pendingTrackWriters = append(c.pendingTrackWriters, writer)
}
}
return
}
func (c *RTCClient) AddStaticTrack(mime string, id string, label string, opts ...AddTrackOption) (writer TrackWriter, err error) {
return c.AddStaticTrackWithCodec(webrtc.RTPCodecCapability{MimeType: mime}, id, label, opts...)
}
func (c *RTCClient) AddStaticTrackWithCodec(codec webrtc.RTPCodecCapability, id string, label string, opts ...AddTrackOption) (writer TrackWriter, err error) {
track, err := webrtc.NewTrackLocalStaticSample(codec, id, label)
if err != nil {
return
}
return c.AddTrack(track, "", opts...)
}
func (c *RTCClient) AddFileTrack(path string, id string, label string) (writer TrackWriter, err error) {
// determine file mime
mime, ok := extMimeMapping[filepath.Ext(path)]
if !ok {
return nil, fmt.Errorf("%s has an unsupported extension", filepath.Base(path))
}
logger.Debugw("adding file track", "mime", mime)
track, err := webrtc.NewTrackLocalStaticSample(
webrtc.RTPCodecCapability{MimeType: mime},
id,
label,
)
if err != nil {
return
}
return c.AddTrack(track, path)
}
// send AddTrack command to server to initiate server-side negotiation
func (c *RTCClient) SendAddTrack(cid string, mimeType string, name string, trackType livekit.TrackType) error {
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_AddTrack{
AddTrack: &livekit.AddTrackRequest{
Cid: cid,
Name: name,
Type: trackType,
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Cid: cid,
Codec: mimeType,
},
},
},
},
})
}
func (c *RTCClient) PublishData(data []byte, kind livekit.DataPacket_Kind) error {
if err := c.ensurePublisherConnected(); err != nil {
return err
}
dpData, err := proto.Marshal(&livekit.DataPacket{
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{Payload: data},
},
})
if err != nil {
return err
}
return c.publisher.SendDataMessage(kind, dpData)
}
func (c *RTCClient) PublishDataUnlabeled(data []byte) error {
if err := c.ensurePublisherConnected(); err != nil {
return err
}
return c.publisher.SendDataMessageUnlabeled(data, true, "test")
}
func (c *RTCClient) PublishDataTrack() (writer TrackWriter, err error) {
if err = c.ensurePublisherConnected(); err != nil {
return
}
dataTrackHandle := uint16(c.nextDataTrackHandle.Inc())
if err = c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_PublishDataTrackRequest{
PublishDataTrackRequest: &livekit.PublishDataTrackRequest{
PubHandle: uint32(dataTrackHandle),
Name: fmt.Sprintf("data_track_%d", dataTrackHandle),
},
},
}); err != nil {
return
}
// wait till data track published message is received
timeout := time.After(5 * time.Second)
var dti *livekit.DataTrackInfo
for {
select {
case <-timeout:
return nil, errors.New("could not publish data track after timeout")
default:
c.lock.Lock()
dti = c.pendingPublishedDataTracks[dataTrackHandle]
if dti != nil {
delete(c.pendingPublishedDataTracks, dataTrackHandle)
c.lock.Unlock()
break
}
c.lock.Unlock()
time.Sleep(50 * time.Millisecond)
}
if dti != nil {
break
}
}
c.lock.Lock()
defer c.lock.Unlock()
writer = NewDataTrackWriter(c.ctx, dataTrackHandle, c.publisher)
// write data tracks only after connection established
if c.hasPrimaryEverConnected() {
err = writer.Start()
} else {
c.pendingDataTrackWriters = append(c.pendingDataTrackWriters, writer)
}
return
}
func (c *RTCClient) GetPublishedTrackIDs() []string {
c.lock.Lock()
defer c.lock.Unlock()
var trackIDs []string
for key := range c.localTracks {
trackIDs = append(trackIDs, key)
}
return trackIDs
}
// LastAnswer return SDP of the last answer for the publisher connection
func (c *RTCClient) LastAnswer() *webrtc.SessionDescription {
return c.publisher.CurrentRemoteDescription()
}
func (c *RTCClient) ensurePublisherConnected() error {
select {
case <-c.transportReady:
case <-c.ctx.Done():
return c.ctx.Err()
}
if c.publisher.PeerConnectionHasEverConnected() {
return nil
}
// start negotiating
c.publisher.Negotiate(false)
// wait until connected, increase wait time since it takes more than 10s sometimes on GH
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
return fmt.Errorf("could not connect publisher after timeout")
case <-time.After(10 * time.Millisecond):
if c.publisherFullyEstablished.Load() {
return nil
}
}
}
}
func (c *RTCClient) handleDataMessage(kind livekit.DataPacket_Kind, data []byte) {
dp := &livekit.DataPacket{}
err := proto.Unmarshal(data, dp)
if err != nil {
return
}
dp.Kind = kind
if val, ok := dp.Value.(*livekit.DataPacket_User); ok {
if c.OnDataReceived != nil {
c.OnDataReceived(val.User.Payload, val.User.ParticipantSid)
}
}
}
func (c *RTCClient) handleDataMessageUnlabeled(data []byte) {
if c.OnDataUnlabeledReceived != nil {
c.OnDataUnlabeledReceived(data)
}
}
func (c *RTCClient) handleDataTrackMessage(data []byte, _arrivalTime int64) {
var packet datatrack.Packet
if err := packet.Unmarshal(data); err != nil {
return
}
var dataTrackRemote *DataTrackRemote
c.lock.Lock()
for _, tracks := range c.subscribedDataTracks {
if tracks[packet.Handle] != nil {
dataTrackRemote = tracks[packet.Handle]
break
}
}
c.lock.Unlock()
if dataTrackRemote != nil {
dataTrackRemote.PacketReceived(&packet)
}
}
// handles a server initiated offer, handle on subscriber PC
func (c *RTCClient) handleOffer(desc webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) {
logger.Infow("handling server offer", "participant", c.localParticipant.Identity)
c.subscriber.HandleRemoteDescription(desc, offerId)
c.processPendingRemoteTracks()
}
// the client handles answer on the publisher PC
func (c *RTCClient) handleAnswer(desc webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) {
logger.Infow("handling server answer", "participant", c.localParticipant.Identity)
// remote answered the offer, establish connection
c.publisher.HandleRemoteDescription(desc, answerId)
c.processPendingRemoteTracks()
}
// the client handles media sections requirement on the publisher PC
func (c *RTCClient) handleMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) {
addTransceivers := func(kind webrtc.RTPCodecType, count uint32) {
for range count {
if _, err := c.publisher.AddTransceiverFromKind(
kind,
webrtc.RTPTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionRecvonly,
},
); err != nil {
logger.Warnw(
"could not add transceiver", err,
"participant", c.localParticipant.Identity,
"kind", kind,
)
} else {
logger.Infow(
"added transceiver of kind",
"participant", c.localParticipant.Identity,
"kind", kind,
)
}
}
}
addTransceivers(webrtc.RTPCodecTypeAudio, mediaSectionsRequirement.NumAudios)
addTransceivers(webrtc.RTPCodecTypeVideo, mediaSectionsRequirement.NumVideos)
c.publisher.Negotiate(false)
}
func (c *RTCClient) onOffer(offer webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
if c.localParticipant != nil {
logger.Infow("starting negotiation", "participant", c.localParticipant.Identity)
logger.Infow(
"sending publisher offer",
"participant", c.localParticipant.Identity,
"offer", offer,
"midToTrackID", midToTrackID,
)
}
return c.SendRequest(&livekit.SignalRequest{
Message: &livekit.SignalRequest_Offer{
Offer: signalling.ToProtoSessionDescription(offer, offerId, nil),
},
})
}
func (c *RTCClient) processPendingRemoteTracks() {
c.lock.Lock()
pendingRemoteTracks := c.pendingRemoteTracks
c.pendingRemoteTracks = nil
c.lock.Unlock()
for _, pendingRemoteTrack := range pendingRemoteTracks {
go c.processRemoteTrack(pendingRemoteTrack)
}
}
func (c *RTCClient) processRemoteTrack(track *webrtc.TrackRemote) {
lastUpdate := time.Time{}
// because of FireOnTrackBySdp, it is possible get an empty streamID
// if media comes before SDP, cache and try later
streamID := track.StreamID()
if streamID == "" {
logger.Infow(
"client caching track",
"participant", c.localParticipant.Identity,
"participantID", c.ID(),
"codec", track.Codec(),
"ssrc", track.SSRC(),
)
c.lock.Lock()
c.pendingRemoteTracks = append(c.pendingRemoteTracks, track)
c.lock.Unlock()
return
}
publisherID, trackID := rtc.UnpackStreamID(streamID)
if trackID == "" {
trackID = livekit.TrackID(track.ID())
}
c.lock.Lock()
c.subscribedTracks[publisherID] = append(c.subscribedTracks[publisherID], track)
c.lock.Unlock()
logger.Infow(
"client added track",
"participant", c.localParticipant.Identity,
"participantID", c.ID(),
"publisherID", publisherID,
"trackID", trackID,
"codec", track.Codec(),
"ssrc", track.SSRC(),
)
defer func() {
c.lock.Lock()
c.subscribedTracks[publisherID] = funk.Without(c.subscribedTracks[publisherID], track).([]*webrtc.TrackRemote)
c.lock.Unlock()
}()
numBytes := 0
for {
pkt, _, err := track.ReadRTP()
if c.ctx.Err() != nil {
break
}
if rtc.IsEOF(err) {
logger.Infow(
"client track removed",
"participant", c.localParticipant.Identity,
"participantID", c.ID(),
"publisherID", publisherID,
"trackID", trackID,
"codec", track.Codec(),
"ssrc", track.SSRC(),
)
break
}
if err != nil {
logger.Warnw("error reading RTP", err)
continue
}
c.lock.Lock()
c.lastPackets[publisherID] = pkt
c.bytesReceived[publisherID] += uint64(pkt.MarshalSize())
c.lock.Unlock()
numBytes += pkt.MarshalSize()
if time.Since(lastUpdate) > 30*time.Second {
logger.Infow(
"consumed from participant",
"participant", c.localParticipant.Identity,
"participantID", c.ID(),
"publisherID", publisherID,
"trackID", trackID,
"size", numBytes,
)
lastUpdate = time.Now()
}
}
}
func (c *RTCClient) BytesReceived() uint64 {
var total uint64
c.lock.Lock()
for _, size := range c.bytesReceived {
total += size
}
c.lock.Unlock()
return total
}
func (c *RTCClient) IsLocalCandidateRelaySelected() bool {
var info *types.ICEConnectionInfo
if c.subscriberAsPrimary.Load() {
if c.subscriber != nil {
info = c.subscriber.GetICEConnectionInfo()
}
} else {
if c.publisher != nil {
info = c.publisher.GetICEConnectionInfo()
}
}
if info == nil {
return false
}
for _, local := range info.Local {
if local.SelectedOrder > 0 && local.Candidate != nil && local.Candidate.Typ == webrtc.ICECandidateTypeRelay {
return true
}
}
return false
}
func (c *RTCClient) SendNacks(count int) {
var packets []rtcp.Packet
c.lock.Lock()
for _, pkt := range c.lastPackets {
seqs := make([]uint16, 0, count)
for i := range count {
seqs = append(seqs, pkt.SequenceNumber-uint16(i))
}
packets = append(packets, &rtcp.TransportLayerNack{
MediaSSRC: pkt.SSRC,
Nacks: rtcp.NackPairsFromSequenceNumbers(seqs),
})
}
c.lock.Unlock()
_ = c.subscriber.WriteRTCP(packets)
}
func encodeQueryParam(key, value string) string {
return fmt.Sprintf("&%s=%s", url.QueryEscape(key), url.QueryEscape(value))
}