Negotiate receiver reference time (RFC 3611 rtcp-xr:rcvr-rtt) in SDP Add SDP support for negotiating non-sender RTT (RRTR/DLRR). A single MediaContentDescription::receive_non_sender_rtt flag is the source of truth; webrtc_sdp.cc is the only place that translates between the wire forms and the flag: - Parse: set the flag from either the standard a=rtcp-xr:rcvr-rtt or the legacy non-standard a=rtcp-fb:<pt> rrtr. - Generate: when the flag is set, emit BOTH forms (rtcp-xr plus, for interim backward-compat, the legacy rtcp-fb rrtr per codec). Advertised in offers/answers gated by the WebRTC-RtcpXrReceiverReferenceTime field trial (default on, IsDisabled to roll back). This CL only adds negotiation; enabling RRTR from the flag is the stacked follow-up. When the legacy rtcp-fb form is dropped later, only the parser/generator need change. Bug: webrtc:516205747 Change-Id: Id7ad1e9b550bdfa372ddcf3dfdb0298310d407eb Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/479240 Reviewed-by: Harald Alvestrand <hta@webrtc.org> Reviewed-by: Danil Chapovalov <danilchap@webrtc.org> Commit-Queue: Harald Alvestrand <hta@webrtc.org> Cr-Commit-Position: refs/heads/main@{#48051}
diff --git a/AUTHORS b/AUTHORS index e9f6f9b..7c4ea2c 100644 --- a/AUTHORS +++ b/AUTHORS
@@ -148,6 +148,7 @@ Tarun Chawla <trnkumarchawla@gmail.com> Todd Wong <todd.wong.ndq@gmail.com> Tomas Popela <tomas.popela@gmail.com> +Tomohiro Matsuzawa <mabtomoh@amazon.com> Trevor Hayes <trevor.axiom@gmail.com> Uladzislau Susha <landby@gmail.com> Vicken Simonian <vsimon@gmail.com>
diff --git a/api/webrtc_sdp.cc b/api/webrtc_sdp.cc index dc0d488..42e0650 100644 --- a/api/webrtc_sdp.cc +++ b/api/webrtc_sdp.cc
@@ -101,6 +101,9 @@ const char kAttributeBundleOnly[] = "bundle-only"; const char kAttributeRtcpMux[] = "rtcp-mux"; const char kAttributeRtcpReducedSize[] = "rtcp-rsize"; +const char kAttributeRtcpXr[] = "rtcp-xr"; +const char kRtcpXrFormatRcvrRtt[] = "rcvr-rtt"; +const char kRtcpXrFormatRcvrRttPrefix[] = "rcvr-rtt="; const char kAttributeSsrc[] = "ssrc"; const char kSsrcAttributeCname[] = "cname"; const char kAttributeExtmapAllowMixed[] = "extmap-allow-mixed"; @@ -1381,6 +1384,25 @@ AddLine(os.str(), message); } + // RFC 3611 + // a=rtcp-xr:rcvr-rtt=all + if (media_desc->receive_non_sender_rtt()) { + InitAttrLine(kAttributeRtcpXr, &os); + os << kSdpDelimiterColon << kRtcpXrFormatRcvrRtt << "=all"; + AddLine(os.str(), message); + // Interim backward-compat: also advertise the non-standard + // a=rtcp-fb:<pt> rrtr for peers that do not understand rtcp-xr. rrtr is + // no longer carried as a codec feedback param (the parser folds it into + // receive_non_sender_rtt), so it is emitted here from the flag rather + // than via AddRtcpFbLines. + for (const Codec& codec : media_desc->codecs()) { + StringBuilder fb_os; + WriteRtcpFbHeader(codec.id, &fb_os); + fb_os << " " << kRtcpFbParamRrtr; + AddLine(fb_os.str(), message); + } + } + if (media_desc->conference_mode()) { InitAttrLine(kAttributeXGoogleFlag, &os); os << kSdpDelimiterColon << kValueConference; @@ -2535,6 +2557,14 @@ } const FeedbackParam feedback_param(id, param); + // The non-standard "rrtr" rtcp-fb is the legacy way of signaling receiver + // reference time reports (RFC 3611). Translate it to the single internal + // flag rather than storing it as a per-codec feedback param. + if (id == kRtcpFbParamRrtr) { + media_desc->set_receive_non_sender_rtt(true); + return true; + } + if (media_type == MediaType::AUDIO || media_type == MediaType::VIDEO) { UpdateCodec(media_desc, payload_type, feedback_param); } @@ -2811,6 +2841,21 @@ media_desc->set_rtcp_mux(true); } else if (HasAttribute(*line, kAttributeRtcpReducedSize)) { media_desc->set_rtcp_reduced_size(true); + } else if (HasAttribute(*line, kAttributeRtcpXr)) { + // RFC 3611: a=rtcp-xr:<format>[ <format>]... Consume the rcvr-rtt + // format (receiver reference time report); accept rcvr-rtt=all and + // rcvr-rtt=sender. Match whole space-separated tokens, not a + // substring, and require the '=' so partial matches are rejected. + std::string xr_value; + if (GetValue(*line, kAttributeRtcpXr, &xr_value, error)) { + for (absl::string_view format : + split(xr_value, kSdpDelimiterSpaceChar)) { + if (absl::StartsWith(format, kRtcpXrFormatRcvrRttPrefix)) { + media_desc->set_receive_non_sender_rtt(true); + break; + } + } + } } else if (HasAttribute(*line, kAttributeRtcpRemoteEstimate)) { media_desc->set_remote_estimate(true); } else if (HasAttribute(*line, kAttributeSframe)) {
diff --git a/api/webrtc_sdp_unittest.cc b/api/webrtc_sdp_unittest.cc index aecc5601..eb3a1d1 100644 --- a/api/webrtc_sdp_unittest.cc +++ b/api/webrtc_sdp_unittest.cc
@@ -62,6 +62,7 @@ using ::testing::ElementsAre; using ::testing::Field; +using ::testing::HasSubstr; using ::testing::IsNull; using ::testing::NotNull; using ::testing::Property; @@ -3367,6 +3368,51 @@ TestSerialize(jdesc_output); } +TEST_F(WebRtcSdpTest, DeserializeSerializeRtcpXrRcvrRtt) { + const char kSdpWithRcvrRtt[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 9 RTP/SAVPF 111\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=rtcp-xr:rcvr-rtt=all\r\n"; + + // Deserialize: rcvr-rtt sets the single receive-non-sender-RTT flag. + std::unique_ptr<SessionDescriptionInterface> jdesc = + SdpDeserialize(kSdpWithRcvrRtt); + ASSERT_THAT(jdesc, NotNull()); + const AudioContentDescription* acd = + GetFirstAudioContentDescription(jdesc->description()); + ASSERT_THAT(acd, NotNull()); + EXPECT_TRUE(acd->receive_non_sender_rtt()); + + // Serialize: the flag emits BOTH the standard rtcp-xr and the legacy + // rtcp-fb rrtr wire forms (interim backward-compat). + std::string reserialized = SdpSerialize(*jdesc); + EXPECT_THAT(reserialized, HasSubstr("a=rtcp-xr:rcvr-rtt=all")); + EXPECT_THAT(reserialized, HasSubstr("a=rtcp-fb:111 rrtr")); +} + +// The legacy non-standard a=rtcp-fb:<pt> rrtr also sets the flag on parse. +TEST_F(WebRtcSdpTest, DeserializeLegacyRtcpFbRrtr) { + const char kSdpWithFbRrtr[] = + "v=0\r\n" + "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "m=audio 9 RTP/SAVPF 111\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=rtcp-fb:111 rrtr\r\n"; + std::unique_ptr<SessionDescriptionInterface> jdesc = + SdpDeserialize(kSdpWithFbRrtr); + ASSERT_THAT(jdesc, NotNull()); + const AudioContentDescription* acd = + GetFirstAudioContentDescription(jdesc->description()); + ASSERT_THAT(acd, NotNull()); + EXPECT_TRUE(acd->receive_non_sender_rtt()); +} + TEST_F(WebRtcSdpTest, DeserializeVideoFmtp) { const char kSdpWithFmtpString[] = "v=0\r\n"
diff --git a/experiments/field_trials.py b/experiments/field_trials.py index 8cf948d..90efd96 100755 --- a/experiments/field_trials.py +++ b/experiments/field_trials.py
@@ -206,6 +206,9 @@ FieldTrial('WebRTC-RtcEventLogEncodeNetEqSetMinimumDelayKillSwitch', 42225058, date(2024, 4, 1)), + FieldTrial('WebRTC-RtcpXrReceiverReferenceTime', + 516205747, + date(2027, 12, 1)), FieldTrial('WebRTC-Sctp-Snap', 426480601, date(2026, 1, 1)),
diff --git a/pc/media_session.cc b/pc/media_session.cc index 4f1a44d..bbad292 100644 --- a/pc/media_session.cc +++ b/pc/media_session.cc
@@ -363,9 +363,12 @@ const RtpHeaderExtensions& rtp_extensions, UniqueRandomIdGenerator* ssrc_generator, StreamParamsVec* current_streams, - MediaContentDescription* offer) { + MediaContentDescription* offer, + const FieldTrialsView& field_trials) { offer->set_rtcp_mux(session_options.rtcp_mux_enabled); offer->set_rtcp_reduced_size(true); + offer->set_receive_non_sender_rtt( + !field_trials.IsDisabled("WebRTC-RtcpXrReceiverReferenceTime")); // Build the vector of header extensions with directions for this // media_description's options. @@ -409,7 +412,7 @@ return CreateContentOffer(media_description_options, session_options, rtp_extensions, ssrc_generator, current_streams, - offer); + offer, field_trials); } // Adds all extensions from `reference_extensions` to `offered_extensions` that @@ -603,7 +606,8 @@ bool bundle_enabled, MediaContentDescription* answer, PayloadTypeSuggester& suggester, - RtpTransceiverIdDomain id_domain) { + RtpTransceiverIdDomain id_domain, + const FieldTrialsView& field_trials) { answer->set_extmap_allow_mixed_level(offer->extmap_allow_mixed_level()); const RtpExtension::Filter extensions_filter = enable_encrypted_rtp_header_extensions @@ -646,6 +650,9 @@ answer->set_rtcp_mux(session_options.rtcp_mux_enabled && offer->rtcp_mux()); answer->set_rtcp_reduced_size(offer->rtcp_reduced_size()); + answer->set_receive_non_sender_rtt( + offer->receive_non_sender_rtt() && + !field_trials.IsDisabled("WebRTC-RtcpXrReceiverReferenceTime")); answer->set_remote_estimate(offer->remote_estimate()); AddSimulcastToMediaDescription(media_description_options, answer); @@ -1328,9 +1335,9 @@ } } - auto error = CreateContentOffer(media_description_options, session_options, - RtpHeaderExtensions(), ssrc_generator(), - current_streams, data.get()); + auto error = CreateContentOffer( + media_description_options, session_options, RtpHeaderExtensions(), + ssrc_generator(), current_streams, data.get(), env_.field_trials()); if (!error.ok()) { return error; } @@ -1475,7 +1482,8 @@ *codec_lookup_helper_->PayloadTypeSuggester(), offer_description->extmap_allow_mixed() ? RtpTransceiverIdDomain::kTwoByteAllowed - : RtpTransceiverIdDomain::kOneByteOnly)) { + : RtpTransceiverIdDomain::kOneByteOnly, + env_.field_trials())) { return RTC_LOG_ERROR(RTCError(RTCErrorType::INTERNAL_ERROR) << "Failed to create answer"); } @@ -1554,7 +1562,8 @@ *codec_lookup_helper_->PayloadTypeSuggester(), offer_description->extmap_allow_mixed() ? RtpTransceiverIdDomain::kTwoByteAllowed - : RtpTransceiverIdDomain::kOneByteOnly)) { + : RtpTransceiverIdDomain::kOneByteOnly, + env_.field_trials())) { return RTC_LOG_ERROR(RTCError(RTCErrorType::INTERNAL_ERROR) << "Failed to create answer"); }
diff --git a/pc/media_session_unittest.cc b/pc/media_session_unittest.cc index 064122a..183ddc0 100644 --- a/pc/media_session_unittest.cc +++ b/pc/media_session_unittest.cc
@@ -1038,6 +1038,40 @@ EXPECT_EQ(acd->protocol(), kMediaProtocolDtlsSavpf); } +// With the RRTR field trial enabled (default), an offer enables non-sender +// RTT on the negotiated description. The wire-format translation (rtcp-xr and +// the legacy rtcp-fb rrtr) is covered in webrtc_sdp_unittest. +TEST_F(MediaSessionDescriptionFactoryTest, + CreateAudioOfferEnablesReceiveNonSenderRtt) { + std::unique_ptr<SessionDescription> offer = + f1_.CreateOfferOrError(CreateAudioMediaSession(), nullptr).MoveValue(); + ASSERT_TRUE(offer.get()); + const MediaContentDescription* acd = + GetFirstAudioContentDescription(offer.get()); + ASSERT_TRUE(acd); + EXPECT_TRUE(acd->receive_non_sender_rtt()); +} + +class MediaSessionDescriptionFactoryRcvrRttDisabledTest + : public MediaSessionDescriptionFactoryTest { + protected: + MediaSessionDescriptionFactoryRcvrRttDisabledTest() + : MediaSessionDescriptionFactoryTest( + "WebRTC-RtcpXrReceiverReferenceTime/Disabled/") {} +}; + +// With the field trial disabled, the offer must not enable non-sender RTT. +TEST_F(MediaSessionDescriptionFactoryRcvrRttDisabledTest, + CreateAudioOfferDoesNotEnableReceiveNonSenderRtt) { + std::unique_ptr<SessionDescription> offer = + f1_.CreateOfferOrError(CreateAudioMediaSession(), nullptr).MoveValue(); + ASSERT_TRUE(offer.get()); + const MediaContentDescription* acd = + GetFirstAudioContentDescription(offer.get()); + ASSERT_TRUE(acd); + EXPECT_FALSE(acd->receive_non_sender_rtt()); +} + // Create an offer with just Opus and RED. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioOfferWithJustOpusAndRed) {
diff --git a/pc/session_description.h b/pc/session_description.h index f8c44ec..fbc1e50 100644 --- a/pc/session_description.h +++ b/pc/session_description.h
@@ -105,6 +105,13 @@ rtcp_reduced_size_ = reduced_size; } + // Whether RFC 3611 rcvr-rtt (receiver reference time report) was + // negotiated, enabling non-sender RTT (RRTR/DLRR) on this m-section. + bool receive_non_sender_rtt() const { return receive_non_sender_rtt_; } + void set_receive_non_sender_rtt(bool enable) { + receive_non_sender_rtt_ = enable; + } + // Indicates support for the remote network estimate packet type. This // functionality is experimental and subject to change without notice. bool remote_estimate() const { return remote_estimate_; } @@ -342,6 +349,7 @@ private: bool rtcp_mux_ = false; bool rtcp_reduced_size_ = false; + bool receive_non_sender_rtt_ = false; bool remote_estimate_ = false; bool rtcp_fb_ack_ccfb_ = false; int bandwidth_ = kAutoBandwidth;