From c3c7178e56e17ce8b1d1dcc98ee59df7da0f97b2 Mon Sep 17 00:00:00 2001 From: boks1971 Date: Mon, 7 Oct 2024 13:26:30 +0530 Subject: [PATCH] Add ice transport api to get selected pair stats In use cases like SFU, it is useful to get just the selected candidate pair stats to have access to current RTT on the peer connection. The standard has a way to do `GetSelectedCandidatePair` on `ICETransport`, but does not have a way to get stats of that pair. Although not in standard, adding a method to `ICETransport` to get selected candidate pair along similar lines of above method. --- icegatherer.go | 58 +++++++++++++++++--------------------------- icetransport.go | 6 +++++ icetransport_test.go | 8 ++++++ stats.go | 40 ++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/icegatherer.go b/icegatherer.go index cd0d8672b82..b772f451d5a 100644 --- a/icegatherer.go +++ b/icegatherer.go @@ -306,46 +306,12 @@ func (g *ICEGatherer) collectStats(collector *statsReportCollector) { for _, candidatePairStats := range agent.GetCandidatePairsStats() { collector.Collecting() - state, err := toStatsICECandidatePairState(candidatePairStats.State) + stats, err := toICECandidatePairStats(candidatePairStats) if err != nil { g.log.Error(err.Error()) + continue } - pairID := newICECandidatePairStatsID(candidatePairStats.LocalCandidateID, - candidatePairStats.RemoteCandidateID) - - stats := ICECandidatePairStats{ - Timestamp: statsTimestampFrom(candidatePairStats.Timestamp), - Type: StatsTypeCandidatePair, - ID: pairID, - // TransportID: - LocalCandidateID: candidatePairStats.LocalCandidateID, - RemoteCandidateID: candidatePairStats.RemoteCandidateID, - State: state, - Nominated: candidatePairStats.Nominated, - PacketsSent: candidatePairStats.PacketsSent, - PacketsReceived: candidatePairStats.PacketsReceived, - BytesSent: candidatePairStats.BytesSent, - BytesReceived: candidatePairStats.BytesReceived, - LastPacketSentTimestamp: statsTimestampFrom(candidatePairStats.LastPacketSentTimestamp), - LastPacketReceivedTimestamp: statsTimestampFrom(candidatePairStats.LastPacketReceivedTimestamp), - FirstRequestTimestamp: statsTimestampFrom(candidatePairStats.FirstRequestTimestamp), - LastRequestTimestamp: statsTimestampFrom(candidatePairStats.LastRequestTimestamp), - LastResponseTimestamp: statsTimestampFrom(candidatePairStats.LastResponseTimestamp), - TotalRoundTripTime: candidatePairStats.TotalRoundTripTime, - CurrentRoundTripTime: candidatePairStats.CurrentRoundTripTime, - AvailableOutgoingBitrate: candidatePairStats.AvailableOutgoingBitrate, - AvailableIncomingBitrate: candidatePairStats.AvailableIncomingBitrate, - CircuitBreakerTriggerCount: candidatePairStats.CircuitBreakerTriggerCount, - RequestsReceived: candidatePairStats.RequestsReceived, - RequestsSent: candidatePairStats.RequestsSent, - ResponsesReceived: candidatePairStats.ResponsesReceived, - ResponsesSent: candidatePairStats.ResponsesSent, - RetransmissionsReceived: candidatePairStats.RetransmissionsReceived, - RetransmissionsSent: candidatePairStats.RetransmissionsSent, - ConsentRequestsSent: candidatePairStats.ConsentRequestsSent, - ConsentExpiredTimestamp: statsTimestampFrom(candidatePairStats.ConsentExpiredTimestamp), - } collector.Collect(stats.ID, stats) } @@ -407,3 +373,23 @@ func (g *ICEGatherer) collectStats(collector *statsReportCollector) { collector.Done() }(collector, agent) } + +func (g *ICEGatherer) getSelectedCandidatePairStats() (ICECandidatePairStats, bool) { + agent := g.getAgent() + if agent == nil { + return ICECandidatePairStats{}, false + } + + selectedCandidatePairStats, isAvailable := agent.GetSelectedCandidatePairStats() + if !isAvailable { + return ICECandidatePairStats{}, false + } + + stats, err := toICECandidatePairStats(selectedCandidatePairStats) + if err != nil { + g.log.Error(err.Error()) + return ICECandidatePairStats{}, false + } + + return stats, true +} diff --git a/icetransport.go b/icetransport.go index 01bbc67c39e..6724a94280f 100644 --- a/icetransport.go +++ b/icetransport.go @@ -70,6 +70,12 @@ func (t *ICETransport) GetSelectedCandidatePair() (*ICECandidatePair, error) { return NewICECandidatePair(&local, &remote), nil } +// GetSelectedCandidatePairStats returns the selected candidate pair stats on which packets are sent +// if there is no selected pair empty stats, false is returned to indicate stats not available +func (t *ICETransport) GetSelectedCandidatePairStats() (ICECandidatePairStats, bool) { + return t.gatherer.getSelectedCandidatePairStats() +} + // NewICETransport creates a new NewICETransport. func NewICETransport(gatherer *ICEGatherer, loggerFactory logging.LoggerFactory) *ICETransport { iceTransport := &ICETransport{ diff --git a/icetransport_test.go b/icetransport_test.go index 55867d39420..4cd60711383 100644 --- a/icetransport_test.go +++ b/icetransport_test.go @@ -98,10 +98,14 @@ func TestICETransport_GetSelectedCandidatePair(t *testing.T) { offererSelectedPair, err := offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.Nil(t, offererSelectedPair) + _, statsAvailable := offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePairStats() + assert.False(t, statsAvailable) answererSelectedPair, err := answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.Nil(t, answererSelectedPair) + _, statsAvailable = answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePairStats() + assert.False(t, statsAvailable) assert.NoError(t, signalPair(offerer, answerer)) peerConnectionConnected.Wait() @@ -109,10 +113,14 @@ func TestICETransport_GetSelectedCandidatePair(t *testing.T) { offererSelectedPair, err = offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.NotNil(t, offererSelectedPair) + _, statsAvailable = offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePairStats() + assert.True(t, statsAvailable) answererSelectedPair, err = answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.NotNil(t, answererSelectedPair) + _, statsAvailable = answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePairStats() + assert.True(t, statsAvailable) closePairNow(t, offerer, answerer) } diff --git a/stats.go b/stats.go index 35cbea5bd80..a0b0795e11b 100644 --- a/stats.go +++ b/stats.go @@ -1696,6 +1696,46 @@ func toStatsICECandidatePairState(state ice.CandidatePairState) (StatsICECandida } } +func toICECandidatePairStats(candidatePairStats ice.CandidatePairStats) (ICECandidatePairStats, error) { + state, err := toStatsICECandidatePairState(candidatePairStats.State) + if err != nil { + return ICECandidatePairStats{}, err + } + + return ICECandidatePairStats{ + Timestamp: statsTimestampFrom(candidatePairStats.Timestamp), + Type: StatsTypeCandidatePair, + ID: newICECandidatePairStatsID(candidatePairStats.LocalCandidateID, candidatePairStats.RemoteCandidateID), + // TransportID: + LocalCandidateID: candidatePairStats.LocalCandidateID, + RemoteCandidateID: candidatePairStats.RemoteCandidateID, + State: state, + Nominated: candidatePairStats.Nominated, + PacketsSent: candidatePairStats.PacketsSent, + PacketsReceived: candidatePairStats.PacketsReceived, + BytesSent: candidatePairStats.BytesSent, + BytesReceived: candidatePairStats.BytesReceived, + LastPacketSentTimestamp: statsTimestampFrom(candidatePairStats.LastPacketSentTimestamp), + LastPacketReceivedTimestamp: statsTimestampFrom(candidatePairStats.LastPacketReceivedTimestamp), + FirstRequestTimestamp: statsTimestampFrom(candidatePairStats.FirstRequestTimestamp), + LastRequestTimestamp: statsTimestampFrom(candidatePairStats.LastRequestTimestamp), + LastResponseTimestamp: statsTimestampFrom(candidatePairStats.LastResponseTimestamp), + TotalRoundTripTime: candidatePairStats.TotalRoundTripTime, + CurrentRoundTripTime: candidatePairStats.CurrentRoundTripTime, + AvailableOutgoingBitrate: candidatePairStats.AvailableOutgoingBitrate, + AvailableIncomingBitrate: candidatePairStats.AvailableIncomingBitrate, + CircuitBreakerTriggerCount: candidatePairStats.CircuitBreakerTriggerCount, + RequestsReceived: candidatePairStats.RequestsReceived, + RequestsSent: candidatePairStats.RequestsSent, + ResponsesReceived: candidatePairStats.ResponsesReceived, + ResponsesSent: candidatePairStats.ResponsesSent, + RetransmissionsReceived: candidatePairStats.RetransmissionsReceived, + RetransmissionsSent: candidatePairStats.RetransmissionsSent, + ConsentRequestsSent: candidatePairStats.ConsentRequestsSent, + ConsentExpiredTimestamp: statsTimestampFrom(candidatePairStats.ConsentExpiredTimestamp), + }, nil +} + const ( // StatsICECandidatePairStateFrozen means a check for this pair hasn't been // performed, and it can't yet be performed until some other check succeeds,