Propagate the corruption_score metric to RTCInboundRtpStreamStats.

Bug: webrtc:358039777
Change-Id: I7e956188a5ef913cbe1647d00ca02b5a46a99b3a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/362083
Reviewed-by: Erik Språng <sprang@webrtc.org>
Commit-Queue: Emil Vardar (xWF) <vardar@google.com>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#43281}
This commit is contained in:
Emil Vardar 2024-10-22 11:46:55 +00:00 committed by WebRTC LUCI CQ
parent 3f1a04acf9
commit a2205e3943
7 changed files with 226 additions and 0 deletions

View File

@ -297,6 +297,10 @@ class RTC_EXPORT RTCInboundRtpStreamStats final
std::optional<uint32_t> pli_count; std::optional<uint32_t> pli_count;
std::optional<uint32_t> nack_count; std::optional<uint32_t> nack_count;
std::optional<uint64_t> qp_sum; std::optional<uint64_t> qp_sum;
// https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/corruption-detection
std::optional<double> corruption_score_sum;
std::optional<double> corruption_score_squared_sum;
std::optional<uint32_t> corruption_score_count;
// This is a remnant of the legacy getStats() API. When the "video-timing" // This is a remnant of the legacy getStats() API. When the "video-timing"
// header extension is used, // header extension is used,
// https://webrtc.github.io/webrtc-org/experiments/rtp-hdrext/video-timing/, // https://webrtc.github.io/webrtc-org/experiments/rtp-hdrext/video-timing/,

View File

@ -4065,6 +4065,151 @@ TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
PeerConnectionInterface::kStable); PeerConnectionInterface::kStable);
} }
TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
OnlyOnePairWantsCorruptionScorePlumbing) {
// In order for corruption score to be logged, encryption of RTP header
// extensions must be allowed.
CryptoOptions crypto_options;
crypto_options.srtp.enable_encrypted_rtp_header_extensions = true;
PeerConnectionInterface::RTCConfiguration config;
config.crypto_options = crypto_options;
config.offer_extmap_allow_mixed = true;
ASSERT_TRUE(CreatePeerConnectionWrappersWithConfig(config, config));
ConnectFakeSignaling();
// Munge the corruption detection header extension into the SDP.
// If caller adds corruption detection header extension to its SDP offer, it
// will receive it from the callee.
caller()->AddCorruptionDetectionHeader();
// Do normal offer/answer and wait for some frames to be received in each
// direction, and `corruption_score` to be aggregated.
caller()->AddAudioVideoTracks();
callee()->AddAudioVideoTracks();
caller()->CreateAndSetAndSignalOffer();
ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
ASSERT_TRUE_WAIT(caller()->GetCorruptionScoreCount() > 0, kMaxWaitForStatsMs);
ASSERT_TRUE_WAIT(callee()->GetCorruptionScoreCount() == 0,
kMaxWaitForStatsMs);
for (const auto& pair : {caller(), callee()}) {
rtc::scoped_refptr<const RTCStatsReport> report = pair->NewGetStats();
ASSERT_TRUE(report);
auto inbound_stream_stats =
report->GetStatsOfType<RTCInboundRtpStreamStats>();
for (const auto& stat : inbound_stream_stats) {
if (*stat->kind == "video") {
if (pair == caller()) {
EXPECT_TRUE(stat->corruption_score_sum.has_value());
EXPECT_TRUE(stat->corruption_score_squared_sum.has_value());
double average_corruption_score =
(*stat->corruption_score_sum) /
static_cast<int32_t>(*stat->corruption_score_count);
EXPECT_GE(average_corruption_score, 0.0);
EXPECT_LE(average_corruption_score, 1.0);
}
if (pair == callee()) {
// Since only `caller` requests corruption score calculation the
// callee should not aggregate it.
EXPECT_FALSE(stat->corruption_score_sum.has_value());
EXPECT_FALSE(stat->corruption_score_squared_sum.has_value());
}
}
}
}
}
TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
BothPairsWantCorruptionScorePlumbing) {
// In order for corruption score to be logged, encryption of RTP header
// extensions must be allowed.
CryptoOptions crypto_options;
crypto_options.srtp.enable_encrypted_rtp_header_extensions = true;
PeerConnectionInterface::RTCConfiguration config;
config.crypto_options = crypto_options;
config.offer_extmap_allow_mixed = true;
ASSERT_TRUE(CreatePeerConnectionWrappersWithConfig(config, config));
ConnectFakeSignaling();
// Munge the corruption detection header extension into the SDP.
// If caller adds corruption detection header extension to its SDP offer, it
// will receive it from the callee.
caller()->AddCorruptionDetectionHeader();
callee()->AddCorruptionDetectionHeader();
// Do normal offer/answer and wait for some frames to be received in each
// direction, and `corruption_score` to be aggregated.
caller()->AddAudioVideoTracks();
callee()->AddAudioVideoTracks();
caller()->CreateAndSetAndSignalOffer();
ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
ASSERT_TRUE_WAIT(caller()->GetCorruptionScoreCount() > 0, kMaxWaitForStatsMs);
ASSERT_TRUE_WAIT(callee()->GetCorruptionScoreCount() > 0, kMaxWaitForStatsMs);
for (const auto& pair : {caller(), callee()}) {
rtc::scoped_refptr<const RTCStatsReport> report = pair->NewGetStats();
ASSERT_TRUE(report);
auto inbound_stream_stats =
report->GetStatsOfType<RTCInboundRtpStreamStats>();
for (const auto& stat : inbound_stream_stats) {
if (*stat->kind == "video") {
EXPECT_TRUE(stat->corruption_score_sum.has_value());
EXPECT_TRUE(stat->corruption_score_squared_sum.has_value());
double average_corruption_score =
(*stat->corruption_score_sum) /
static_cast<int32_t>(*stat->corruption_score_count);
EXPECT_GE(average_corruption_score, 0.0);
EXPECT_LE(average_corruption_score, 1.0);
}
}
}
}
TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
CorruptionScorePlumbingShouldNotWorkWhenEncryptionIsOff) {
// In order for corruption score to be logged, encryption of RTP header
// extensions must be allowed.
CryptoOptions crypto_options;
crypto_options.srtp.enable_encrypted_rtp_header_extensions = false;
PeerConnectionInterface::RTCConfiguration config;
config.crypto_options = crypto_options;
config.offer_extmap_allow_mixed = true;
ASSERT_TRUE(CreatePeerConnectionWrappersWithConfig(config, config));
ConnectFakeSignaling();
// Munge the corruption detection header extension into the SDP.
// If caller adds corruption detection header extension to its SDP offer, it
// will receive it from the callee.
caller()->AddCorruptionDetectionHeader();
callee()->AddCorruptionDetectionHeader();
// Do normal offer/answer and wait for some frames to be received in each
// direction, and `corruption_score` to be aggregated.
caller()->AddAudioVideoTracks();
callee()->AddAudioVideoTracks();
caller()->CreateAndSetAndSignalOffer();
ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
ASSERT_TRUE_WAIT(caller()->GetCorruptionScoreCount() == 0,
kMaxWaitForStatsMs);
ASSERT_TRUE_WAIT(callee()->GetCorruptionScoreCount() == 0,
kMaxWaitForStatsMs);
for (const auto& pair : {caller(), callee()}) {
rtc::scoped_refptr<const RTCStatsReport> report = pair->NewGetStats();
ASSERT_TRUE(report);
auto inbound_stream_stats =
report->GetStatsOfType<RTCInboundRtpStreamStats>();
for (const auto& stat : inbound_stream_stats) {
if (*stat->kind == "video") {
EXPECT_FALSE(stat->corruption_score_sum.has_value());
EXPECT_FALSE(stat->corruption_score_squared_sum.has_value());
}
}
}
}
} // namespace } // namespace
} // namespace webrtc } // namespace webrtc

View File

@ -624,6 +624,16 @@ CreateInboundRTPStreamStatsFromVideoReceiverInfo(
if (video_receiver_info.qp_sum.has_value()) { if (video_receiver_info.qp_sum.has_value()) {
inbound_video->qp_sum = *video_receiver_info.qp_sum; inbound_video->qp_sum = *video_receiver_info.qp_sum;
} }
if (video_receiver_info.corruption_score_sum.has_value()) {
RTC_CHECK(video_receiver_info.corruption_score_squared_sum.has_value());
RTC_CHECK_GT(video_receiver_info.corruption_score_count, 0);
inbound_video->corruption_score_sum =
*video_receiver_info.corruption_score_sum;
inbound_video->corruption_score_squared_sum =
*video_receiver_info.corruption_score_squared_sum;
inbound_video->corruption_score_count =
video_receiver_info.corruption_score_count;
}
if (video_receiver_info.timing_frame_info.has_value()) { if (video_receiver_info.timing_frame_info.has_value()) {
inbound_video->goog_timing_frame_info = inbound_video->goog_timing_frame_info =
video_receiver_info.timing_frame_info->ToString(); video_receiver_info.timing_frame_info->ToString();

View File

@ -2346,6 +2346,8 @@ TEST_F(RTCStatsCollectorTest, CollectRTCInboundRtpStreamStats_Video) {
video_media_info.receivers[0].key_frames_decoded = 3; video_media_info.receivers[0].key_frames_decoded = 3;
video_media_info.receivers[0].frames_dropped = 13; video_media_info.receivers[0].frames_dropped = 13;
video_media_info.receivers[0].qp_sum = std::nullopt; video_media_info.receivers[0].qp_sum = std::nullopt;
video_media_info.receivers[0].corruption_score_sum = std::nullopt;
video_media_info.receivers[0].corruption_score_squared_sum = std::nullopt;
video_media_info.receivers[0].total_decode_time = TimeDelta::Seconds(9); video_media_info.receivers[0].total_decode_time = TimeDelta::Seconds(9);
video_media_info.receivers[0].total_processing_delay = TimeDelta::Millis(600); video_media_info.receivers[0].total_processing_delay = TimeDelta::Millis(600);
video_media_info.receivers[0].total_assembly_time = TimeDelta::Millis(500); video_media_info.receivers[0].total_assembly_time = TimeDelta::Millis(500);
@ -2417,6 +2419,7 @@ TEST_F(RTCStatsCollectorTest, CollectRTCInboundRtpStreamStats_Video) {
expected_video.key_frames_decoded = 3; expected_video.key_frames_decoded = 3;
expected_video.frames_dropped = 13; expected_video.frames_dropped = 13;
// `expected_video.qp_sum` should be undefined. // `expected_video.qp_sum` should be undefined.
// `corruption_score` related metrics should be undefined.
expected_video.total_decode_time = 9.0; expected_video.total_decode_time = 9.0;
expected_video.total_processing_delay = 0.6; expected_video.total_processing_delay = 0.6;
expected_video.total_assembly_time = 0.5; expected_video.total_assembly_time = 0.5;
@ -2453,6 +2456,12 @@ TEST_F(RTCStatsCollectorTest, CollectRTCInboundRtpStreamStats_Video) {
// Set previously undefined values and "GetStats" again. // Set previously undefined values and "GetStats" again.
video_media_info.receivers[0].qp_sum = 9; video_media_info.receivers[0].qp_sum = 9;
expected_video.qp_sum = 9; expected_video.qp_sum = 9;
video_media_info.receivers[0].corruption_score_sum = 0.5;
video_media_info.receivers[0].corruption_score_squared_sum = 0.25;
video_media_info.receivers[0].corruption_score_count = 5;
expected_video.corruption_score_sum = 0.5;
expected_video.corruption_score_squared_sum = 0.25;
expected_video.corruption_score_count = 5;
video_media_info.receivers[0].last_packet_received = Timestamp::Seconds(1); video_media_info.receivers[0].last_packet_received = Timestamp::Seconds(1);
expected_video.last_packet_received_timestamp = 1000.0; expected_video.last_packet_received_timestamp = 1000.0;
video_media_info.receivers[0].content_type = VideoContentType::SCREENSHARE; video_media_info.receivers[0].content_type = VideoContentType::SCREENSHARE;

View File

@ -560,6 +560,13 @@ class RTCStatsReportVerifier {
verifier.TestAttributeIsUndefined(inbound_stream.decoder_implementation); verifier.TestAttributeIsUndefined(inbound_stream.decoder_implementation);
verifier.TestAttributeIsUndefined(inbound_stream.power_efficient_decoder); verifier.TestAttributeIsUndefined(inbound_stream.power_efficient_decoder);
} }
// As long as the corruption detection RTP header extension is not activated
// it should not aggregate any corruption score. The tests where this header
// extension is enabled are located in pc/peer_connection_integrationtest.cc
verifier.TestAttributeIsUndefined(inbound_stream.corruption_score_sum);
verifier.TestAttributeIsUndefined(
inbound_stream.corruption_score_squared_sum);
verifier.TestAttributeIsUndefined(inbound_stream.corruption_score_count);
verifier.TestAttributeIsNonNegative<uint32_t>( verifier.TestAttributeIsNonNegative<uint32_t>(
inbound_stream.packets_received); inbound_stream.packets_received);
if (inbound_stream.kind.has_value() && *inbound_stream.kind == "audio") { if (inbound_stream.kind.has_value() && *inbound_stream.kind == "audio") {

View File

@ -650,6 +650,46 @@ class PeerConnectionIntegrationWrapper : public PeerConnectionObserver,
return observer->error().ok(); return observer->error().ok();
} }
void AddCorruptionDetectionHeader() {
SetGeneratedSdpMunger(
[&](std::unique_ptr<SessionDescriptionInterface>& sdp) {
for (ContentInfo& content : sdp->description()->contents()) {
cricket::MediaContentDescription* media =
content.media_description();
// Corruption detection is only a valid RTP header extension for
// video stream.
if (media->type() != cricket::MediaType::MEDIA_TYPE_VIDEO) {
continue;
}
cricket::RtpHeaderExtensions extensions =
media->rtp_header_extensions();
// Find a valid id.
int id = extensions.size();
while (IdExists(extensions, id)) {
++id;
}
extensions.push_back(RtpExtension(
RtpExtension::kCorruptionDetectionUri, id, /*encrypt=*/true));
media->set_rtp_header_extensions(extensions);
break;
}
});
}
uint32_t GetCorruptionScoreCount() {
rtc::scoped_refptr<const RTCStatsReport> report = NewGetStats();
auto inbound_stream_stats =
report->GetStatsOfType<RTCInboundRtpStreamStats>();
for (const auto& stat : inbound_stream_stats) {
if (*stat->kind == "video") {
return stat->corruption_score_count.value_or(0);
}
}
return 0;
}
private: private:
// Constructor used by friend class PeerConnectionIntegrationBaseTest. // Constructor used by friend class PeerConnectionIntegrationBaseTest.
explicit PeerConnectionIntegrationWrapper(const std::string& debug_name) explicit PeerConnectionIntegrationWrapper(const std::string& debug_name)
@ -997,6 +1037,14 @@ class PeerConnectionIntegrationWrapper : public PeerConnectionObserver,
data_observers_.push_back( data_observers_.push_back(
std::make_unique<MockDataChannelObserver>(data_channel.get())); std::make_unique<MockDataChannelObserver>(data_channel.get()));
} }
bool IdExists(const cricket::RtpHeaderExtensions& extensions, int id) {
for (const auto& extension : extensions) {
if (extension.id == id) {
return true;
}
}
return false;
}
std::string debug_name_; std::string debug_name_;

View File

@ -267,6 +267,9 @@ WEBRTC_RTCSTATS_IMPL(
AttributeInit("pliCount", &pli_count), AttributeInit("pliCount", &pli_count),
AttributeInit("nackCount", &nack_count), AttributeInit("nackCount", &nack_count),
AttributeInit("qpSum", &qp_sum), AttributeInit("qpSum", &qp_sum),
AttributeInit("corruptionScoreSum", &corruption_score_sum),
AttributeInit("corruptionScoreSumSquared", &corruption_score_squared_sum),
AttributeInit("corruptionScoreCount", &corruption_score_count),
AttributeInit("googTimingFrameInfo", &goog_timing_frame_info), AttributeInit("googTimingFrameInfo", &goog_timing_frame_info),
AttributeInit("powerEfficientDecoder", &power_efficient_decoder), AttributeInit("powerEfficientDecoder", &power_efficient_decoder),
AttributeInit("jitterBufferFlushes", &jitter_buffer_flushes), AttributeInit("jitterBufferFlushes", &jitter_buffer_flushes),