diff --git a/api/stats/rtcstats_objects.h b/api/stats/rtcstats_objects.h index 0645ee6683..67341aab5f 100644 --- a/api/stats/rtcstats_objects.h +++ b/api/stats/rtcstats_objects.h @@ -297,6 +297,10 @@ class RTC_EXPORT RTCInboundRtpStreamStats final std::optional pli_count; std::optional nack_count; std::optional qp_sum; + // https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/corruption-detection + std::optional corruption_score_sum; + std::optional corruption_score_squared_sum; + std::optional corruption_score_count; // This is a remnant of the legacy getStats() API. When the "video-timing" // header extension is used, // https://webrtc.github.io/webrtc-org/experiments/rtp-hdrext/video-timing/, diff --git a/pc/peer_connection_integrationtest.cc b/pc/peer_connection_integrationtest.cc index 777d312829..4e501e0160 100644 --- a/pc/peer_connection_integrationtest.cc +++ b/pc/peer_connection_integrationtest.cc @@ -4065,6 +4065,151 @@ TEST_F(PeerConnectionIntegrationTestUnifiedPlan, 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 report = pair->NewGetStats(); + ASSERT_TRUE(report); + auto inbound_stream_stats = + report->GetStatsOfType(); + 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(*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 report = pair->NewGetStats(); + ASSERT_TRUE(report); + auto inbound_stream_stats = + report->GetStatsOfType(); + 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(*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 report = pair->NewGetStats(); + ASSERT_TRUE(report); + auto inbound_stream_stats = + report->GetStatsOfType(); + 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 webrtc diff --git a/pc/rtc_stats_collector.cc b/pc/rtc_stats_collector.cc index 60741b64ce..47a1a86b7b 100644 --- a/pc/rtc_stats_collector.cc +++ b/pc/rtc_stats_collector.cc @@ -624,6 +624,16 @@ CreateInboundRTPStreamStatsFromVideoReceiverInfo( if (video_receiver_info.qp_sum.has_value()) { 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()) { inbound_video->goog_timing_frame_info = video_receiver_info.timing_frame_info->ToString(); diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc index e7fdb1abd9..1cc3ce5617 100644 --- a/pc/rtc_stats_collector_unittest.cc +++ b/pc/rtc_stats_collector_unittest.cc @@ -2346,6 +2346,8 @@ TEST_F(RTCStatsCollectorTest, CollectRTCInboundRtpStreamStats_Video) { video_media_info.receivers[0].key_frames_decoded = 3; video_media_info.receivers[0].frames_dropped = 13; 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_processing_delay = TimeDelta::Millis(600); 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.frames_dropped = 13; // `expected_video.qp_sum` should be undefined. + // `corruption_score` related metrics should be undefined. expected_video.total_decode_time = 9.0; expected_video.total_processing_delay = 0.6; expected_video.total_assembly_time = 0.5; @@ -2453,6 +2456,12 @@ TEST_F(RTCStatsCollectorTest, CollectRTCInboundRtpStreamStats_Video) { // Set previously undefined values and "GetStats" again. video_media_info.receivers[0].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); expected_video.last_packet_received_timestamp = 1000.0; video_media_info.receivers[0].content_type = VideoContentType::SCREENSHARE; diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc index 779f06bfd5..11f7e5ce49 100644 --- a/pc/rtc_stats_integrationtest.cc +++ b/pc/rtc_stats_integrationtest.cc @@ -560,6 +560,13 @@ class RTCStatsReportVerifier { verifier.TestAttributeIsUndefined(inbound_stream.decoder_implementation); 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( inbound_stream.packets_received); if (inbound_stream.kind.has_value() && *inbound_stream.kind == "audio") { diff --git a/pc/test/integration_test_helpers.h b/pc/test/integration_test_helpers.h index 5d5293bc23..5f10dc5319 100644 --- a/pc/test/integration_test_helpers.h +++ b/pc/test/integration_test_helpers.h @@ -650,6 +650,46 @@ class PeerConnectionIntegrationWrapper : public PeerConnectionObserver, return observer->error().ok(); } + void AddCorruptionDetectionHeader() { + SetGeneratedSdpMunger( + [&](std::unique_ptr& 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 report = NewGetStats(); + auto inbound_stream_stats = + report->GetStatsOfType(); + for (const auto& stat : inbound_stream_stats) { + if (*stat->kind == "video") { + return stat->corruption_score_count.value_or(0); + } + } + return 0; + } + private: // Constructor used by friend class PeerConnectionIntegrationBaseTest. explicit PeerConnectionIntegrationWrapper(const std::string& debug_name) @@ -997,6 +1037,14 @@ class PeerConnectionIntegrationWrapper : public PeerConnectionObserver, data_observers_.push_back( std::make_unique(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_; diff --git a/stats/rtcstats_objects.cc b/stats/rtcstats_objects.cc index 4c58f45f02..4ee9bdcc24 100644 --- a/stats/rtcstats_objects.cc +++ b/stats/rtcstats_objects.cc @@ -267,6 +267,9 @@ WEBRTC_RTCSTATS_IMPL( AttributeInit("pliCount", &pli_count), AttributeInit("nackCount", &nack_count), 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("powerEfficientDecoder", &power_efficient_decoder), AttributeInit("jitterBufferFlushes", &jitter_buffer_flushes),