diff --git a/cmd/lk/room.go b/cmd/lk/room.go index 2f206ccd..6b84e6c6 100644 --- a/cmd/lk/room.go +++ b/cmd/lk/room.go @@ -23,8 +23,12 @@ import ( "os/signal" "regexp" "strings" + "sync" "syscall" + "time" + "github.com/pion/interceptor" + pionStats "github.com/pion/interceptor/pkg/stats" "github.com/pion/webrtc/v4" "github.com/urfave/cli/v3" "google.golang.org/protobuf/encoding/protojson" @@ -199,6 +203,15 @@ var ( Name: "metadata", Usage: "`JSON` metadata which will be passed to participant", }, + &cli.BoolFlag{ + Name: "stats", + Usage: "Periodically log publisher WebRTC GetStats as one JSON object per line to stderr", + }, + &cli.FloatFlag{ + Name: "stats-interval", + Usage: "Seconds between stats logs when --stats is set", + Value: 5.0, + }, }, }, { @@ -973,6 +986,26 @@ func joinRoom(ctx context.Context, cmd *cli.Command) error { maps.Copy(participantAttributes, fileAttrs) } + var connectOpts []lksdk.ConnectOption + connectOpts = append(connectOpts, lksdk.WithAutoSubscribe(autoSubscribe)) + + var ( + statsGetterMu sync.Mutex + statsGetters []pionStats.Getter + ) + if cmd.Bool("stats") { + statsFactory, err := pionStats.NewInterceptor() + if err != nil { + return err + } + statsFactory.OnNewPeerConnection(func(_ string, g pionStats.Getter) { + statsGetterMu.Lock() + statsGetters = append(statsGetters, g) + statsGetterMu.Unlock() + }) + connectOpts = append(connectOpts, lksdk.WithInterceptors([]interceptor.Factory{statsFactory})) + } + room, err := lksdk.ConnectToRoom(project.URL, lksdk.ConnectInfo{ APIKey: project.APIKey, APISecret: project.APISecret, @@ -980,7 +1013,7 @@ func joinRoom(ctx context.Context, cmd *cli.Command) error { ParticipantIdentity: participantIdentity, ParticipantAttributes: participantAttributes, ParticipantMetadata: cmd.String("metadata"), - }, roomCB, lksdk.WithAutoSubscribe(autoSubscribe)) + }, roomCB, connectOpts...) if err != nil { return err } @@ -1060,6 +1093,61 @@ func joinRoom(ctx context.Context, cmd *cli.Command) error { } } + if cmd.Bool("stats") { + interval := cmd.Float("stats-interval") + if interval <= 0 { + interval = 5.0 + } + ticker := time.NewTicker(time.Duration(interval * float64(time.Second))) + go func() { + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + pc := room.LocalParticipant.GetPublisherPeerConnection() + if pc == nil { + continue + } + combined := make(map[string]interface{}) + for k, v := range pc.GetStats() { + combined[k] = v + } + statsGetterMu.Lock() + getters := make([]pionStats.Getter, len(statsGetters)) + copy(getters, statsGetters) + statsGetterMu.Unlock() + for _, sender := range pc.GetSenders() { + for _, enc := range sender.GetParameters().Encodings { + ssrc := uint32(enc.SSRC) + if ssrc == 0 { + continue + } + for _, g := range getters { + s := g.Get(ssrc) + if s == nil { + continue + } + id := fmt.Sprintf("RTCRemoteInboundRtp_%d", ssrc) + combined[id] = map[string]interface{}{ + "type": "remote-inbound-rtp", + "ssrc": ssrc, + "packetsLost": s.RemoteInboundRTPStreamStats.PacketsLost, + "jitter": s.RemoteInboundRTPStreamStats.Jitter, + "roundTripTime": s.RemoteInboundRTPStreamStats.RoundTripTime.Seconds(), + "fractionLost": s.RemoteInboundRTPStreamStats.FractionLost, + } + break + } + } + } + logger.Infow("stats", "stats", combined) + } + } + }() + } + if cmd.IsSet("open") { switch cmd.String("open") { case string(util.OpenTargetMeet): diff --git a/go.mod b/go.mod index d3f65cdd..c941c351 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/livekit/server-sdk-go/v2 v2.13.3 github.com/moby/patternmatcher v0.6.0 github.com/pelletier/go-toml v1.9.5 + github.com/pion/interceptor v0.1.43 github.com/pion/rtcp v1.2.16 github.com/pion/rtp v1.10.0 github.com/pion/webrtc/v4 v4.2.3 @@ -137,7 +138,6 @@ require ( github.com/pion/datachannel v1.6.0 // indirect github.com/pion/dtls/v3 v3.0.10 // indirect github.com/pion/ice/v4 v4.2.0 // indirect - github.com/pion/interceptor v0.1.43 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns/v2 v2.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect