api: add additional WebRTC statistics (#4795)
rtpPacketsReceived, rtpPacketsSent, rtpPacketsLost, rtpPacketsJitter, rtcpPacketsReceived, rtcpPacketsSent
This commit is contained in:
@@ -1075,6 +1075,24 @@ components:
|
||||
bytesSent:
|
||||
type: integer
|
||||
format: int64
|
||||
rtpPacketsReceived:
|
||||
type: integer
|
||||
format: int64
|
||||
rtpPacketsSent:
|
||||
type: integer
|
||||
format: int64
|
||||
rtpPacketsLost:
|
||||
type: integer
|
||||
format: int64
|
||||
rtpPacketsJitter:
|
||||
type: number
|
||||
format: float64
|
||||
rtcpPacketsReceived:
|
||||
type: integer
|
||||
format: int64
|
||||
rtcpPacketsSent:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
WebRTCSessionList:
|
||||
type: object
|
||||
|
||||
@@ -761,6 +761,12 @@ func TestAPIProtocolListGet(t *testing.T) {
|
||||
"remoteAddr": out1.(map[string]interface{})["items"].([]interface{})[0].(map[string]interface{})["remoteAddr"],
|
||||
"remoteCandidate": out1.(map[string]interface{})["items"].([]interface{})[0].(map[string]interface{})["remoteCandidate"],
|
||||
"state": "read",
|
||||
"rtcpPacketsReceived": float64(0),
|
||||
"rtcpPacketsSent": float64(2),
|
||||
"rtpPacketsJitter": float64(0),
|
||||
"rtpPacketsLost": float64(0),
|
||||
"rtpPacketsReceived": float64(0),
|
||||
"rtpPacketsSent": float64(1),
|
||||
},
|
||||
},
|
||||
}, out1)
|
||||
|
||||
@@ -357,6 +357,12 @@ type APIWebRTCSession struct {
|
||||
Query string `json:"query"`
|
||||
BytesReceived uint64 `json:"bytesReceived"`
|
||||
BytesSent uint64 `json:"bytesSent"`
|
||||
RTPPacketsReceived uint64 `json:"rtpPacketsReceived"`
|
||||
RTPPacketsSent uint64 `json:"rtpPacketsSent"`
|
||||
RTPPacketsLost uint64 `json:"rtpPacketsLost"`
|
||||
RTPPacketsJitter float64 `json:"rtpPacketsJitter"`
|
||||
RTCPPacketsReceived uint64 `json:"rtcpPacketsReceived"`
|
||||
RTCPPacketsSent uint64 `json:"rtcpPacketsSent"`
|
||||
}
|
||||
|
||||
// APIWebRTCSessionList is a list of WebRTC sessions.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver"
|
||||
@@ -244,6 +245,8 @@ type IncomingTrack struct {
|
||||
receiver *webrtc.RTPReceiver
|
||||
writeRTCP func([]rtcp.Packet) error
|
||||
log logger.Writer
|
||||
rtpPacketsReceived *uint64
|
||||
rtpPacketsLost *uint64
|
||||
|
||||
packetsLost *counterdumper.CounterDumper
|
||||
rtcpReceiver *rtcpreceiver.RTCPReceiver
|
||||
@@ -295,7 +298,8 @@ func (t *IncomingTrack) start() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// incoming RTCP packets must always be read to make interceptors work
|
||||
// read incoming RTCP packets.
|
||||
// incoming RTCP packets must always be read to make interceptors work.
|
||||
go func() {
|
||||
buf := make([]byte, 1500)
|
||||
for {
|
||||
@@ -336,7 +340,7 @@ func (t *IncomingTrack) start() {
|
||||
}()
|
||||
}
|
||||
|
||||
// read incoming RTP packets
|
||||
// read incoming RTP packets.
|
||||
go func() {
|
||||
reorderer := &rtpreorderer.Reorderer{}
|
||||
reorderer.Initialize()
|
||||
@@ -349,10 +353,13 @@ func (t *IncomingTrack) start() {
|
||||
|
||||
packets, lost := reorderer.Process(pkt)
|
||||
if lost != 0 {
|
||||
atomic.AddUint64(t.rtpPacketsLost, uint64(lost))
|
||||
t.packetsLost.Add(uint64(lost))
|
||||
// do not return
|
||||
}
|
||||
|
||||
atomic.AddUint64(t.rtpPacketsReceived, uint64(len(packets)))
|
||||
|
||||
err2 = t.rtcpReceiver.ProcessPacket(pkt, time.Now(), true)
|
||||
if err2 != nil {
|
||||
t.log.Log(logger.Warn, err2.Error())
|
||||
|
||||
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/rtcpsender"
|
||||
@@ -23,9 +24,10 @@ var multichannelOpusSDP = map[int]string{
|
||||
type OutgoingTrack struct {
|
||||
Caps webrtc.RTPCodecCapability
|
||||
|
||||
track *webrtc.TrackLocalStaticRTP
|
||||
ssrc uint32
|
||||
rtcpSender *rtcpsender.RTCPSender
|
||||
track *webrtc.TrackLocalStaticRTP
|
||||
ssrc uint32
|
||||
rtcpSender *rtcpsender.RTCPSender
|
||||
rtpPacketsSent *uint64
|
||||
}
|
||||
|
||||
func (t *OutgoingTrack) isVideo() bool {
|
||||
@@ -67,7 +69,7 @@ func (t *OutgoingTrack) setup(p *PeerConnection) error {
|
||||
}
|
||||
t.rtcpSender.Initialize()
|
||||
|
||||
p.wr.GetSenders()
|
||||
t.rtpPacketsSent = p.rtpPacketsSent
|
||||
|
||||
// incoming RTCP packets must always be read to make interceptors work
|
||||
go func() {
|
||||
@@ -106,5 +108,7 @@ func (t *OutgoingTrack) WriteRTPWithNTP(pkt *rtp.Packet, ntp time.Time) error {
|
||||
|
||||
t.rtcpSender.ProcessPacket(pkt, ntp, true)
|
||||
|
||||
atomic.AddUint64(t.rtpPacketsSent, 1)
|
||||
|
||||
return t.track.WriteRTP(pkt)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pion/ice/v4"
|
||||
@@ -31,17 +32,33 @@ func stringInSlice(a string, list []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// skip ConfigureRTCPReports
|
||||
func registerInterceptors(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error {
|
||||
if err := webrtc.ConfigureNack(mediaEngine, interceptorRegistry); err != nil {
|
||||
// * skip ConfigureRTCPReports
|
||||
// * add statsInterceptor
|
||||
func registerInterceptors(
|
||||
mediaEngine *webrtc.MediaEngine,
|
||||
interceptorRegistry *interceptor.Registry,
|
||||
onStatsInterceptor func(s *statsInterceptor),
|
||||
) error {
|
||||
err := webrtc.ConfigureNack(mediaEngine, interceptorRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := webrtc.ConfigureSimulcastExtensionHeaders(mediaEngine); err != nil {
|
||||
err = webrtc.ConfigureSimulcastExtensionHeaders(mediaEngine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return webrtc.ConfigureTWCCSender(mediaEngine, interceptorRegistry)
|
||||
err = webrtc.ConfigureTWCCSender(mediaEngine, interceptorRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interceptorRegistry.Add(&statsInterceptorFactory{
|
||||
onCreate: onStatsInterceptor,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TracksAreValid checks whether tracks in the SDP are valid
|
||||
@@ -97,17 +114,24 @@ type PeerConnection struct {
|
||||
UseAbsoluteTimestamp bool
|
||||
Log logger.Writer
|
||||
|
||||
wr *webrtc.PeerConnection
|
||||
stateChangeMutex sync.Mutex
|
||||
newLocalCandidate chan *webrtc.ICECandidateInit
|
||||
connected chan struct{}
|
||||
failed chan struct{}
|
||||
closed chan struct{}
|
||||
gatheringDone chan struct{}
|
||||
incomingTrack chan trackRecvPair
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
incomingTracks []*IncomingTrack
|
||||
wr *webrtc.PeerConnection
|
||||
stateChangeMutex sync.Mutex
|
||||
newLocalCandidate chan *webrtc.ICECandidateInit
|
||||
connected chan struct{}
|
||||
failed chan struct{}
|
||||
closed chan struct{}
|
||||
gatheringDone chan struct{}
|
||||
incomingTrack chan trackRecvPair
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
incomingTracks []*IncomingTrack
|
||||
startedReading *int64
|
||||
rtpPacketsReceived *uint64
|
||||
rtpPacketsSent *uint64
|
||||
rtpPacketsLost *uint64
|
||||
statsInterceptor *statsInterceptor
|
||||
// rtcpPacketsReceived *uint64
|
||||
// rtcpPacketsSent *uint64
|
||||
}
|
||||
|
||||
// Start starts the peer connection.
|
||||
@@ -216,7 +240,13 @@ func (co *PeerConnection) Start() error {
|
||||
|
||||
interceptorRegistry := &interceptor.Registry{}
|
||||
|
||||
err := registerInterceptors(mediaEngine, interceptorRegistry)
|
||||
err := registerInterceptors(
|
||||
mediaEngine,
|
||||
interceptorRegistry,
|
||||
func(s *statsInterceptor) {
|
||||
co.statsInterceptor = s
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -242,6 +272,11 @@ func (co *PeerConnection) Start() error {
|
||||
|
||||
co.ctx, co.ctxCancel = context.WithCancel(context.Background())
|
||||
|
||||
co.startedReading = new(int64)
|
||||
co.rtpPacketsReceived = new(uint64)
|
||||
co.rtpPacketsSent = new(uint64)
|
||||
co.rtpPacketsLost = new(uint64)
|
||||
|
||||
if co.Publish {
|
||||
for _, tr := range co.OutgoingTracks {
|
||||
err = tr.setup(co)
|
||||
@@ -473,6 +508,8 @@ func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) error {
|
||||
receiver: pair.receiver,
|
||||
writeRTCP: co.wr.WriteRTCP,
|
||||
log: co.Log,
|
||||
rtpPacketsReceived: co.rtpPacketsReceived,
|
||||
rtpPacketsLost: co.rtpPacketsLost,
|
||||
}
|
||||
t.initialize()
|
||||
co.incomingTracks = append(co.incomingTracks, t)
|
||||
@@ -542,6 +579,7 @@ func (co *PeerConnection) StartReading() {
|
||||
for _, track := range co.incomingTracks {
|
||||
track.start()
|
||||
}
|
||||
atomic.StoreInt64(co.startedReading, 1)
|
||||
}
|
||||
|
||||
// RemoteCandidate returns the remote candidate.
|
||||
@@ -566,26 +604,48 @@ func (co *PeerConnection) RemoteCandidate() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// BytesReceived returns received bytes.
|
||||
func (co *PeerConnection) BytesReceived() uint64 {
|
||||
for _, stats := range co.wr.GetStats() {
|
||||
func bytesStats(wr *webrtc.PeerConnection) (uint64, uint64) {
|
||||
for _, stats := range wr.GetStats() {
|
||||
if tstats, ok := stats.(webrtc.TransportStats); ok {
|
||||
if tstats.ID == "iceTransport" {
|
||||
return tstats.BytesReceived
|
||||
return tstats.BytesReceived, tstats.BytesSent
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// BytesSent returns sent bytes.
|
||||
func (co *PeerConnection) BytesSent() uint64 {
|
||||
for _, stats := range co.wr.GetStats() {
|
||||
if tstats, ok := stats.(webrtc.TransportStats); ok {
|
||||
if tstats.ID == "iceTransport" {
|
||||
return tstats.BytesSent
|
||||
// Stats returns statistics.
|
||||
func (co *PeerConnection) Stats() *Stats {
|
||||
bytesReceived, bytesSent := bytesStats(co.wr)
|
||||
|
||||
v := float64(0)
|
||||
n := float64(0)
|
||||
|
||||
if atomic.LoadInt64(co.startedReading) == 1 {
|
||||
for _, tr := range co.incomingTracks {
|
||||
if recvStats := tr.rtcpReceiver.Stats(); recvStats != nil {
|
||||
v += recvStats.Jitter
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
|
||||
var rtpPacketsJitter float64
|
||||
if n != 0 {
|
||||
rtpPacketsJitter = v / n
|
||||
} else {
|
||||
rtpPacketsJitter = 0
|
||||
}
|
||||
|
||||
return &Stats{
|
||||
BytesReceived: bytesReceived,
|
||||
BytesSent: bytesSent,
|
||||
RTPPacketsReceived: atomic.LoadUint64(co.rtpPacketsReceived),
|
||||
RTPPacketsSent: atomic.LoadUint64(co.rtpPacketsSent),
|
||||
RTPPacketsLost: atomic.LoadUint64(co.rtpPacketsLost),
|
||||
RTPPacketsJitter: rtpPacketsJitter,
|
||||
RTCPPacketsReceived: atomic.LoadUint64(co.statsInterceptor.rtcpPacketsReceived),
|
||||
RTCPPacketsSent: atomic.LoadUint64(co.statsInterceptor.rtcpPacketsSent),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package webrtc
|
||||
|
||||
// Stats are WebRTC statistics.
|
||||
type Stats struct {
|
||||
BytesReceived uint64
|
||||
BytesSent uint64
|
||||
RTPPacketsReceived uint64
|
||||
RTPPacketsSent uint64
|
||||
RTPPacketsLost uint64
|
||||
RTPPacketsJitter float64
|
||||
RTCPPacketsReceived uint64
|
||||
RTCPPacketsSent uint64
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/rtcp"
|
||||
)
|
||||
|
||||
type statsInterceptor struct {
|
||||
rtcpPacketsSent *uint64
|
||||
rtcpPacketsReceived *uint64
|
||||
}
|
||||
|
||||
func (*statsInterceptor) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *statsInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader {
|
||||
return interceptor.RTCPReaderFunc(func(bytes []byte,
|
||||
attributes interceptor.Attributes,
|
||||
) (int, interceptor.Attributes, error) {
|
||||
n, attrs, err := reader.Read(bytes, attributes)
|
||||
|
||||
pkts, err2 := attrs.GetRTCPPackets(bytes)
|
||||
if err2 == nil {
|
||||
atomic.AddUint64(s.rtcpPacketsReceived, uint64(len(pkts)))
|
||||
}
|
||||
|
||||
return n, attrs, err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statsInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter {
|
||||
return interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) {
|
||||
atomic.AddUint64(s.rtcpPacketsSent, uint64(len(pkts)))
|
||||
return writer.Write(pkts, attributes)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statsInterceptor) BindLocalStream(_ *interceptor.StreamInfo,
|
||||
writer interceptor.RTPWriter,
|
||||
) interceptor.RTPWriter {
|
||||
return writer
|
||||
}
|
||||
|
||||
func (*statsInterceptor) UnbindLocalStream(_ *interceptor.StreamInfo) {}
|
||||
|
||||
func (s *statsInterceptor) BindRemoteStream(_ *interceptor.StreamInfo,
|
||||
reader interceptor.RTPReader,
|
||||
) interceptor.RTPReader {
|
||||
return reader
|
||||
}
|
||||
|
||||
func (*statsInterceptor) UnbindRemoteStream(_ *interceptor.StreamInfo) {}
|
||||
|
||||
type statsInterceptorFactory struct {
|
||||
onCreate func(s *statsInterceptor)
|
||||
}
|
||||
|
||||
func (f *statsInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) {
|
||||
s := &statsInterceptor{
|
||||
rtcpPacketsSent: new(uint64),
|
||||
rtcpPacketsReceived: new(uint64),
|
||||
}
|
||||
|
||||
f.onCreate(s)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -433,15 +433,13 @@ func (s *session) apiItem() *defs.APIWebRTCSession {
|
||||
peerConnectionEstablished := false
|
||||
localCandidate := ""
|
||||
remoteCandidate := ""
|
||||
bytesReceived := uint64(0)
|
||||
bytesSent := uint64(0)
|
||||
var stats *webrtc.Stats
|
||||
|
||||
if s.pc != nil {
|
||||
peerConnectionEstablished = true
|
||||
localCandidate = s.pc.LocalCandidate()
|
||||
remoteCandidate = s.pc.RemoteCandidate()
|
||||
bytesReceived = s.pc.BytesReceived()
|
||||
bytesSent = s.pc.BytesSent()
|
||||
stats = s.pc.Stats()
|
||||
}
|
||||
|
||||
return &defs.APIWebRTCSession{
|
||||
@@ -457,9 +455,15 @@ func (s *session) apiItem() *defs.APIWebRTCSession {
|
||||
}
|
||||
return defs.APIWebRTCSessionStateRead
|
||||
}(),
|
||||
Path: s.req.pathName,
|
||||
Query: s.req.httpRequest.URL.RawQuery,
|
||||
BytesReceived: bytesReceived,
|
||||
BytesSent: bytesSent,
|
||||
Path: s.req.pathName,
|
||||
Query: s.req.httpRequest.URL.RawQuery,
|
||||
BytesReceived: stats.BytesReceived,
|
||||
BytesSent: stats.BytesSent,
|
||||
RTPPacketsReceived: stats.RTPPacketsReceived,
|
||||
RTPPacketsSent: stats.RTPPacketsSent,
|
||||
RTPPacketsLost: stats.RTPPacketsLost,
|
||||
RTPPacketsJitter: stats.RTPPacketsJitter,
|
||||
RTCPPacketsReceived: stats.RTCPPacketsReceived,
|
||||
RTCPPacketsSent: stats.RTCPPacketsSent,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user