diff --git a/api/uma_metrics.h b/api/uma_metrics.h index 925ba07576..98905abb1a 100644 --- a/api/uma_metrics.h +++ b/api/uma_metrics.h @@ -175,6 +175,44 @@ enum RtcpMuxPolicyUsage { kRtcpMuxPolicyUsageMax }; +// Metrics for SDP munging. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. Keep in sync with SdpMungingType from +// tools/metrics/histograms/metadata/web_rtc/enums.xml +enum SdpMungingType { + kNoModification = 0, + kUnknownModification = 1, + kWithoutCreateAnswer = 2, + kWithoutCreateOffer = 3, + kNumberOfContents = 4, + // Transport-related munging. + kIceOptions = 20, + kIcePwd = 21, + kIceUfrag = 22, + kIceMode = 23, + kDtlsSetup = 24, + kMid = 25, + kSsrcs = 27, + // RTP header extension munging. + kRtpHeaderExtensionRemoved = 40, + kRtpHeaderExtensionAdded = 41, + kRtpHeaderExtensionModified = 42, + // Audio-related munging. + kAudioCodecsRemoved = 60, + kAudioCodecsAdded = 61, + kAudioCodecsReordered = 62, + kAudioCodecsAddedMultiOpus = 63, + kAudioCodecsAddedL16 = 64, + kAudioCodecsFmtpOpusStereo = 68, + // Video-related munging. + kVideoCodecsRemoved = 80, + kVideoCodecsAdded = 81, + kVideoCodecsReordered = 82, + kVideoCodecsLegacySimulcast = 83, + kVideoCodecsFmtpH264SpsPpsIdrInKeyframe = 84, + kMaxValue, +}; + // When adding new metrics please consider using the style described in // https://chromium.googlesource.com/chromium/src.git/+/HEAD/tools/metrics/histograms/README.md#usage // instead of the legacy enums used above. diff --git a/media/base/stream_params.h b/media/base/stream_params.h index 89fc1554cc..e15789b3c2 100644 --- a/media/base/stream_params.h +++ b/media/base/stream_params.h @@ -80,7 +80,7 @@ struct SsrcGroup { std::string ToString() const; - std::string semantics; // e.g FIX, FEC, SIM. + std::string semantics; // e.g FID, FEC-FR, SIM. std::vector ssrcs; // SSRCs of this type. }; diff --git a/pc/BUILD.gn b/pc/BUILD.gn index b77b976992..e0a3336b8a 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -953,6 +953,24 @@ rtc_source_set("rtc_stats_traversal") { ] } +rtc_source_set("sdp_munging_detector") { + visibility = [ ":*" ] + sources = [ + "sdp_munging_detector.cc", + "sdp_munging_detector.h", + ] + deps = [ + ":session_description", + "../api:libjingle_peerconnection_api", + "../media:codec", + "../media:media_constants", + "../media:stream_params", + "../p2p:transport_info", + "../rtc_base:checks", + "../rtc_base:logging", + "//third_party/abseil-cpp/absl/algorithm:container", + ] +} rtc_source_set("sdp_offer_answer") { visibility = [ ":*" ] sources = [ @@ -980,6 +998,7 @@ rtc_source_set("sdp_offer_answer") { ":rtp_sender_proxy", ":rtp_transceiver", ":rtp_transmission_manager", + ":sdp_munging_detector", ":sdp_state_provider", ":session_description", ":simulcast_description", @@ -1009,6 +1028,7 @@ rtc_source_set("sdp_offer_answer") { "../call:payload_type", "../media:codec", "../media:media_channel", + "../media:media_constants", "../media:media_engine", "../media:rid_description", "../media:stream_params", diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc index 3bbeecf4a0..cc27509ce5 100644 --- a/pc/peer_connection.cc +++ b/pc/peer_connection.cc @@ -1947,6 +1947,7 @@ void PeerConnection::Close() { StopRtcEventLog_w(); }); ReportUsagePattern(); + ReportCloseUsageMetrics(); // Signal shutdown to the sdp handler. This invalidates weak pointers for // internal pending callbacks. @@ -2083,6 +2084,54 @@ void PeerConnection::ReportFirstConnectUsageMetrics() { } RTC_HISTOGRAM_ENUMERATION("WebRTC.PeerConnection.RtcpMuxPolicy", rtcp_mux_policy, kRtcpMuxPolicyUsageMax); + switch (local_description()->GetType()) { + case SdpType::kOffer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Offer.ConnectionEstablished", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Answer.ConnectionEstablished", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kPrAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.PrAnswer.ConnectionEstablished", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kRollback: + // Rollback does not have SDP so can not be munged. + break; + } +} + +void PeerConnection::ReportCloseUsageMetrics() { + if (!was_ever_connected_) { + return; + } + RTC_DCHECK(local_description()); + RTC_DCHECK(sdp_handler_); + switch (local_description()->GetType()) { + case SdpType::kOffer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Offer.ConnectionClosed", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Answer.ConnectionClosed", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kPrAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.PrAnswer.ConnectionClosed", + sdp_handler_->sdp_munging_type(), SdpMungingType::kMaxValue); + break; + case SdpType::kRollback: + // Rollback does not have SDP so can not be munged. + break; + } } void PeerConnection::OnIceGatheringChange( diff --git a/pc/peer_connection.h b/pc/peer_connection.h index d3f4457646..bf5b7857f4 100644 --- a/pc/peer_connection.h +++ b/pc/peer_connection.h @@ -385,6 +385,9 @@ class PeerConnection : public PeerConnectionInternal, // Report several UMA metrics on establishing the connection. void ReportFirstConnectUsageMetrics() RTC_RUN_ON(signaling_thread()); + // Report several UMA metrics for established connections when the connection + // is closed. + void ReportCloseUsageMetrics() RTC_RUN_ON(signaling_thread()); // Returns true if the PeerConnection is configured to use Unified Plan // semantics for creating offers/answers and setting local/remote diff --git a/pc/peer_connection_wrapper.cc b/pc/peer_connection_wrapper.cc index de94cc1efc..2f63ce64a4 100644 --- a/pc/peer_connection_wrapper.cc +++ b/pc/peer_connection_wrapper.cc @@ -43,6 +43,7 @@ namespace webrtc { +using ::testing::Eq; using RTCOfferAnswerOptions = PeerConnectionInterface::RTCOfferAnswerOptions; PeerConnectionWrapper::PeerConnectionWrapper( @@ -167,6 +168,20 @@ bool PeerConnectionWrapper::SetLocalDescription( error_out); } +bool PeerConnectionWrapper::SetLocalDescription( + std::unique_ptr desc, + RTCError* error_out) { + auto observer = rtc::make_ref_counted(); + pc()->SetLocalDescription(std::move(desc), observer); + EXPECT_THAT( + WaitUntil([&] { return observer->called(); }, ::testing::IsTrue()), + IsRtcOk()); + bool ok = observer->error().ok(); + if (error_out) + *error_out = std::move(observer->error()); + return ok; +} + bool PeerConnectionWrapper::SetRemoteDescription( std::unique_ptr desc, std::string* error_out) { diff --git a/pc/peer_connection_wrapper.h b/pc/peer_connection_wrapper.h index de8dc471f5..8055c7b16f 100644 --- a/pc/peer_connection_wrapper.h +++ b/pc/peer_connection_wrapper.h @@ -23,6 +23,7 @@ #include "api/media_types.h" #include "api/peer_connection_interface.h" #include "api/rtc_error.h" +#include "api/rtp_parameters.h" #include "api/rtp_sender_interface.h" #include "api/rtp_transceiver_interface.h" #include "api/scoped_refptr.h" @@ -95,6 +96,8 @@ class PeerConnectionWrapper { // Returns true if the description was successfully set. bool SetLocalDescription(std::unique_ptr desc, std::string* error_out = nullptr); + bool SetLocalDescription(std::unique_ptr desc, + RTCError* error_out); // Calls the underlying PeerConnection's SetRemoteDescription method with the // given session description and waits for the success/failure response. // Returns true if the description was successfully set. diff --git a/pc/sdp_munging_detector.cc b/pc/sdp_munging_detector.cc new file mode 100644 index 0000000000..3fc32e8e2a --- /dev/null +++ b/pc/sdp_munging_detector.cc @@ -0,0 +1,349 @@ +/* + * Copyright 2025 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "pc/sdp_munging_detector.h" + +#include +#include + +#include "absl/algorithm/container.h" +#include "api/jsep.h" +#include "api/uma_metrics.h" +#include "media/base/codec.h" +#include "media/base/media_constants.h" +#include "media/base/stream_params.h" +#include "p2p/base/transport_info.h" +#include "pc/session_description.h" +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" + +namespace webrtc { + +namespace { + +SdpMungingType DetermineTransportModification( + const cricket::TransportInfos& last_created_transport_infos, + const cricket::TransportInfos& transport_infos_to_set) { + if (last_created_transport_infos.size() != transport_infos_to_set.size()) { + RTC_LOG(LS_WARNING) << "SDP munging: Number of transport-infos does not " + "match last created description."; + // Number of transports should always match number of contents so this + // should never happen. + return SdpMungingType::kNumberOfContents; + } + for (size_t i = 0; i < last_created_transport_infos.size(); i++) { + if (last_created_transport_infos[i].description.ice_ufrag != + transport_infos_to_set[i].description.ice_ufrag) { + RTC_LOG(LS_WARNING) + << "SDP munging: ice-ufrag does not match last created description."; + return SdpMungingType::kIceUfrag; + } + if (last_created_transport_infos[i].description.ice_pwd != + transport_infos_to_set[i].description.ice_pwd) { + RTC_LOG(LS_WARNING) + << "SDP munging: ice-pwd does not match last created description."; + return SdpMungingType::kIcePwd; + } + if (last_created_transport_infos[i].description.ice_mode != + transport_infos_to_set[i].description.ice_mode) { + RTC_LOG(LS_WARNING) + << "SDP munging: ice mode does not match last created description."; + return SdpMungingType::kIceMode; + } + if (last_created_transport_infos[i].description.connection_role != + transport_infos_to_set[i].description.connection_role) { + RTC_LOG(LS_WARNING) + << "SDP munging: DTLS role does not match last created description."; + return SdpMungingType::kDtlsSetup; + } + if (last_created_transport_infos[i].description.transport_options != + transport_infos_to_set[i].description.transport_options) { + RTC_LOG(LS_WARNING) << "SDP munging: ice_options does not match last " + "created description."; + return SdpMungingType::kIceOptions; + } + } + return SdpMungingType::kNoModification; +} + +SdpMungingType DetermineAudioSdpMungingType( + const cricket::MediaContentDescription* last_created_media_description, + const cricket::MediaContentDescription* media_description_to_set) { + RTC_DCHECK(last_created_media_description); + RTC_DCHECK(media_description_to_set); + // Removing codecs should be done via setCodecPreferences or negotiation, not + // munging. + if (last_created_media_description->codecs().size() > + media_description_to_set->codecs().size()) { + RTC_LOG(LS_WARNING) << "SDP munging: audio codecs removed."; + return SdpMungingType::kAudioCodecsRemoved; + } + // Adding audio codecs is measured after the more specific multiopus and L16 + // checks. + + // Opus stereo modification required to enabled stereo playout for opus. + bool created_opus_stereo = + absl::c_find_if(last_created_media_description->codecs(), + [](const cricket::Codec codec) { + std::string value; + return codec.name == cricket::kOpusCodecName && + codec.GetParam(cricket::kCodecParamStereo, + &value) && + value == cricket::kParamValueTrue; + }) != last_created_media_description->codecs().end(); + bool set_opus_stereo = + absl::c_find_if( + media_description_to_set->codecs(), [](const cricket::Codec codec) { + std::string value; + return codec.name == cricket::kOpusCodecName && + codec.GetParam(cricket::kCodecParamStereo, &value) && + value == cricket::kParamValueTrue; + }) != media_description_to_set->codecs().end(); + if (!created_opus_stereo && set_opus_stereo) { + RTC_LOG(LS_WARNING) << "SDP munging: Opus stereo enabled."; + return SdpMungingType::kAudioCodecsFmtpOpusStereo; + } + + // Nonstandard 5.1/7.1 opus variant. + bool created_multiopus = + absl::c_find_if(last_created_media_description->codecs(), + [](const cricket::Codec codec) { + return codec.name == "multiopus"; + }) != last_created_media_description->codecs().end(); + bool set_multiopus = + absl::c_find_if(media_description_to_set->codecs(), + [](const cricket::Codec codec) { + return codec.name == "multiopus"; + }) != media_description_to_set->codecs().end(); + if (!created_multiopus && set_multiopus) { + RTC_LOG(LS_WARNING) << "SDP munging: multiopus enabled."; + return SdpMungingType::kAudioCodecsAddedMultiOpus; + } + + // L16. + bool created_l16 = + absl::c_find_if(last_created_media_description->codecs(), + [](const cricket::Codec codec) { + return codec.name == cricket::kL16CodecName; + }) != last_created_media_description->codecs().end(); + bool set_l16 = absl::c_find_if(media_description_to_set->codecs(), + [](const cricket::Codec codec) { + return codec.name == cricket::kL16CodecName; + }) != media_description_to_set->codecs().end(); + if (!created_l16 && set_l16) { + RTC_LOG(LS_WARNING) << "SDP munging: L16 enabled."; + return SdpMungingType::kAudioCodecsAddedL16; + } + + if (last_created_media_description->codecs().size() < + media_description_to_set->codecs().size()) { + RTC_LOG(LS_WARNING) << "SDP munging: audio codecs added."; + return SdpMungingType::kAudioCodecsAdded; + } + return SdpMungingType::kNoModification; +} + +SdpMungingType DetermineVideoSdpMungingType( + const cricket::MediaContentDescription* last_created_media_description, + const cricket::MediaContentDescription* media_description_to_set) { + RTC_DCHECK(last_created_media_description); + RTC_DCHECK(media_description_to_set); + // Removing codecs should be done via setCodecPreferences or negotiation, not + // munging. + if (last_created_media_description->codecs().size() > + media_description_to_set->codecs().size()) { + RTC_LOG(LS_WARNING) << "SDP munging: video codecs removed."; + return SdpMungingType::kVideoCodecsRemoved; + } + if (last_created_media_description->codecs().size() < + media_description_to_set->codecs().size()) { + RTC_LOG(LS_WARNING) << "SDP munging: video codecs added."; + return SdpMungingType::kVideoCodecsAdded; + } + + // Simulcast munging. + if (last_created_media_description->streams().size() == 1 && + media_description_to_set->streams().size() == 1) { + bool created_sim = + absl::c_find_if( + last_created_media_description->streams()[0].ssrc_groups, + [](const cricket::SsrcGroup group) { + return group.semantics == cricket::kSimSsrcGroupSemantics; + }) != + last_created_media_description->streams()[0].ssrc_groups.end(); + bool set_sim = + absl::c_find_if( + media_description_to_set->streams()[0].ssrc_groups, + [](const cricket::SsrcGroup group) { + return group.semantics == cricket::kSimSsrcGroupSemantics; + }) != media_description_to_set->streams()[0].ssrc_groups.end(); + if (!created_sim && set_sim) { + RTC_LOG(LS_WARNING) << "SDP munging: legacy simulcast group created."; + return SdpMungingType::kVideoCodecsLegacySimulcast; + } + } + + // sps-pps-idr-in-keyframe. + bool created_sps_pps_idr_in_keyframe = + absl::c_find_if(last_created_media_description->codecs(), + [](const cricket::Codec codec) { + std::string value; + return codec.name == cricket::kH264CodecName && + codec.GetParam( + cricket::kH264FmtpSpsPpsIdrInKeyframe, + &value) && + value == cricket::kParamValueTrue; + }) != last_created_media_description->codecs().end(); + bool set_sps_pps_idr_in_keyframe = + absl::c_find_if( + media_description_to_set->codecs(), [](const cricket::Codec codec) { + std::string value; + return codec.name == cricket::kH264CodecName && + codec.GetParam(cricket::kH264FmtpSpsPpsIdrInKeyframe, + &value) && + value == cricket::kParamValueTrue; + }) != media_description_to_set->codecs().end(); + if (!created_sps_pps_idr_in_keyframe && set_sps_pps_idr_in_keyframe) { + RTC_LOG(LS_WARNING) << "SDP munging: sps-pps-idr-in-keyframe enabled."; + return SdpMungingType::kVideoCodecsFmtpH264SpsPpsIdrInKeyframe; + } + + return SdpMungingType::kNoModification; +} + +} // namespace + +// Determine if the SDP was modified between createOffer and +// setLocalDescription. +SdpMungingType DetermineSdpMungingType( + const SessionDescriptionInterface* sdesc, + const SessionDescriptionInterface* last_created_desc) { + if (!sdesc || !sdesc->description()) { + RTC_LOG(LS_WARNING) << "SDP munging: Failed to parse session description."; + return SdpMungingType::kUnknownModification; + } + + if (!last_created_desc || !last_created_desc->description()) { + RTC_LOG(LS_WARNING) << "SDP munging: SetLocalDescription called without " + "CreateOffer or CreateAnswer."; + if (sdesc->GetType() == SdpType::kOffer) { + return SdpMungingType::kWithoutCreateOffer; + } else { // answer or pranswer. + return SdpMungingType::kWithoutCreateAnswer; + } + } + + // TODO: crbug.com/40567530 - we currently allow answer->pranswer + // so can not check sdesc->GetType() == last_created_desc->GetType(). + + SdpMungingType type; + + // TODO: crbug.com/40567530 - change Chromium so that pointer comparison works + // at least for implicit local description. + if (sdesc->description() == last_created_desc->description()) { + return SdpMungingType::kNoModification; + } + + // Validate contents. + const auto& last_created_contents = + last_created_desc->description()->contents(); + const auto& contents_to_set = sdesc->description()->contents(); + if (last_created_contents.size() != contents_to_set.size()) { + RTC_LOG(LS_WARNING) << "SDP munging: Number of m= sections does not match " + "last created description."; + return SdpMungingType::kNumberOfContents; + } + for (size_t i = 0; i < last_created_contents.size(); i++) { + // TODO: crbug.com/40567530 - more checks are needed here. + if (last_created_contents[i].name != contents_to_set[i].name) { + RTC_LOG(LS_WARNING) << "SDP munging: mid does not match " + "last created description."; + return SdpMungingType::kMid; + } + + auto* last_created_media_description = + last_created_contents[i].media_description(); + auto* media_description_to_set = contents_to_set[i].media_description(); + if (!(last_created_media_description && media_description_to_set)) { + continue; + } + // Validate video and audio contents. + if (last_created_media_description->as_video() != nullptr) { + type = DetermineVideoSdpMungingType(last_created_media_description, + media_description_to_set); + if (type != SdpMungingType::kNoModification) { + return type; + } + } else if (last_created_media_description->as_audio() != nullptr) { + type = DetermineAudioSdpMungingType(last_created_media_description, + media_description_to_set); + if (type != SdpMungingType::kNoModification) { + return type; + } + } + // Validate media streams. + if (last_created_media_description->streams().size() != + media_description_to_set->streams().size()) { + RTC_LOG(LS_WARNING) << "SDP munging: streams size does not match last " + "created description."; + return SdpMungingType::kSsrcs; + } + for (size_t i = 0; i < last_created_media_description->streams().size(); + i++) { + if (last_created_media_description->streams()[i].ssrcs != + media_description_to_set->streams()[i].ssrcs) { + RTC_LOG(LS_WARNING) + << "SDP munging: SSRCs do not match last created description."; + return SdpMungingType::kSsrcs; + } + } + + // Validate RTP header extensions. + auto last_created_extensions = + last_created_media_description->rtp_header_extensions(); + auto extensions_to_set = media_description_to_set->rtp_header_extensions(); + if (last_created_extensions.size() < extensions_to_set.size()) { + RTC_LOG(LS_WARNING) << "SDP munging: RTP header extension added."; + return SdpMungingType::kRtpHeaderExtensionAdded; + } + if (last_created_extensions.size() > extensions_to_set.size()) { + RTC_LOG(LS_WARNING) << "SDP munging: RTP header extension removed."; + return SdpMungingType::kRtpHeaderExtensionRemoved; + } + for (size_t i = 0; i < last_created_extensions.size(); i++) { + if (!(last_created_extensions[i].id == extensions_to_set[i].id)) { + RTC_LOG(LS_WARNING) << "SDP munging: header extension modified."; + return SdpMungingType::kRtpHeaderExtensionModified; + } + } + } + + // Validate transport descriptions. + type = DetermineTransportModification( + last_created_desc->description()->transport_infos(), + sdesc->description()->transport_infos()); + if (type != SdpMungingType::kNoModification) { + return type; + } + + // TODO: crbug.com/40567530 - this serializes the descriptions back to a SDP + // string which is very complex and we not should be be forced to rely on + // string equality. + std::string serialized_description; + std::string serialized_last_description; + if (sdesc->ToString(&serialized_description) && + last_created_desc->ToString(&serialized_last_description) && + serialized_description == serialized_last_description) { + return SdpMungingType::kNoModification; + } + return SdpMungingType::kUnknownModification; +} + +} // namespace webrtc diff --git a/pc/sdp_munging_detector.h b/pc/sdp_munging_detector.h new file mode 100644 index 0000000000..9b630a30dc --- /dev/null +++ b/pc/sdp_munging_detector.h @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef PC_SDP_MUNGING_DETECTOR_H_ +#define PC_SDP_MUNGING_DETECTOR_H_ + +#include "api/jsep.h" +#include "api/uma_metrics.h" + +namespace webrtc { +// Determines if and how the SDP was modified. +SdpMungingType DetermineSdpMungingType( + const SessionDescriptionInterface* sdesc, + const SessionDescriptionInterface* last_created_desc); + +} // namespace webrtc + +#endif // PC_SDP_MUNGING_DETECTOR_H_ diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc index c39b14e629..7ff0aaeabd 100644 --- a/pc/sdp_offer_answer.cc +++ b/pc/sdp_offer_answer.cc @@ -79,6 +79,7 @@ #include "pc/rtp_sender_proxy.h" #include "pc/rtp_transceiver.h" #include "pc/rtp_transmission_manager.h" +#include "pc/sdp_munging_detector.h" #include "pc/session_description.h" #include "pc/simulcast_description.h" #include "pc/stream_collection.h" @@ -1286,6 +1287,31 @@ class CreateSessionDescriptionObserverOperationWrapper std::function operation_complete_callback_; }; +// Wraps a session description observer so a Clone of the last created +// offer/answer can be stored. +class CreateDescriptionObserverWrapperWithCreationCallback + : public CreateSessionDescriptionObserver { + public: + CreateDescriptionObserverWrapperWithCreationCallback( + std::function callback, + rtc::scoped_refptr observer) + : callback_(callback), observer_(observer) { + RTC_DCHECK(observer_); + } + void OnSuccess(SessionDescriptionInterface* desc) override { + callback_(desc); + observer_->OnSuccess(desc); + } + void OnFailure(RTCError error) override { + callback_(nullptr); + observer_->OnFailure(std::move(error)); + } + + private: + std::function callback_; + rtc::scoped_refptr observer_; +}; + // Wrapper for SetSessionDescriptionObserver that invokes the success or failure // callback in a posted message handled by the peer connection. This introduces // a delay that prevents recursive API calls by the observer, but this also @@ -2401,8 +2427,15 @@ void SdpOfferAnswerHandler::DoSetLocalDescription( return; } - // Grab the description type before moving ownership to ApplyLocalDescription, - // which may destroy it before returning. + // Determine if SDP munging was done. This is not yet acted upon. + bool had_local_description = !!local_description(); + SdpMungingType sdp_munging_type = + DetermineSdpMungingType(desc.get(), desc->GetType() == SdpType::kOffer + ? last_created_offer_.get() + : last_created_answer_.get()); + + // Grab the description type before moving ownership to + // ApplyLocalDescription, which may destroy it before returning. const SdpType type = desc->GetType(); error = ApplyLocalDescription(std::move(desc), bundle_groups_by_mid); @@ -2431,12 +2464,40 @@ void SdpOfferAnswerHandler::DoSetLocalDescription( [this] { port_allocator()->DiscardCandidatePool(); }); } + // Clear last created offer/answer and update SDP munging type. + last_created_offer_.reset(nullptr); + last_created_answer_.reset(nullptr); + last_sdp_munging_type_ = sdp_munging_type; + // Report SDP munging of the initial call to setLocalDescription separately. + if (!had_local_description) { + switch (local_description()->GetType()) { + case SdpType::kOffer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Offer.Initial", + last_sdp_munging_type_, SdpMungingType::kMaxValue); + break; + case SdpType::kAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.Answer.Initial", + last_sdp_munging_type_, SdpMungingType::kMaxValue); + break; + case SdpType::kPrAnswer: + RTC_HISTOGRAM_ENUMERATION( + "WebRTC.PeerConnection.SdpMunging.PrAnswer.Initial", + last_sdp_munging_type_, SdpMungingType::kMaxValue); + break; + case SdpType::kRollback: + // Rollback does not have SDP so can not be munged. + break; + } + } + observer->OnSetLocalDescriptionComplete(RTCError::OK()); pc_->NoteUsageEvent(UsageEvent::SET_LOCAL_DESCRIPTION_SUCCEEDED); // Check if negotiation is needed. We must do this after informing the - // observer that SetLocalDescription() has completed to ensure negotiation is - // not needed prior to the promise resolving. + // observer that SetLocalDescription() has completed to ensure negotiation + // is not needed prior to the promise resolving. if (IsUnifiedPlan()) { bool was_negotiation_needed = is_negotiation_needed_; UpdateNegotiationNeeded(); @@ -2449,9 +2510,9 @@ void SdpOfferAnswerHandler::DoSetLocalDescription( } } - // MaybeStartGathering needs to be called after informing the observer so that - // we don't signal any candidates before signaling that SetLocalDescription - // completed. + // MaybeStartGathering needs to be called after informing the observer so + // that we don't signal any candidates before signaling that + // SetLocalDescription completed. transport_controller_s()->MaybeStartGathering(); } @@ -2508,7 +2569,18 @@ void SdpOfferAnswerHandler::DoCreateOffer( cricket::MediaSessionOptions session_options; GetOptionsForOffer(options, &session_options); - webrtc_session_desc_factory_->CreateOffer(observer.get(), options, + auto observer_wrapper = rtc::make_ref_counted< + CreateDescriptionObserverWrapperWithCreationCallback>( + [this](const SessionDescriptionInterface* desc) { + RTC_DCHECK_RUN_ON(signaling_thread()); + if (desc) { + last_created_offer_ = desc->Clone(); + } else { + last_created_offer_.reset(nullptr); + } + }, + std::move(observer)); + webrtc_session_desc_factory_->CreateOffer(observer_wrapper.get(), options, session_options); } @@ -2594,7 +2666,19 @@ void SdpOfferAnswerHandler::DoCreateAnswer( cricket::MediaSessionOptions session_options; GetOptionsForAnswer(options, &session_options); - webrtc_session_desc_factory_->CreateAnswer(observer.get(), session_options); + auto observer_wrapper = rtc::make_ref_counted< + CreateDescriptionObserverWrapperWithCreationCallback>( + [this](const SessionDescriptionInterface* desc) { + RTC_DCHECK_RUN_ON(signaling_thread()); + if (desc) { + last_created_answer_ = desc->Clone(); + } else { + last_created_answer_.reset(nullptr); + } + }, + std::move(observer)); + webrtc_session_desc_factory_->CreateAnswer(observer_wrapper.get(), + session_options); } void SdpOfferAnswerHandler::DoSetRemoteDescription( diff --git a/pc/sdp_offer_answer.h b/pc/sdp_offer_answer.h index 793f2c9770..0c914433a6 100644 --- a/pc/sdp_offer_answer.h +++ b/pc/sdp_offer_answer.h @@ -181,6 +181,8 @@ class SdpOfferAnswerHandler : public SdpStateProvider { return false; } + SdpMungingType sdp_munging_type() const { return last_sdp_munging_type_; } + private: class RemoteDescriptionOperation; class ImplicitCreateSessionDescriptionObserver; @@ -603,6 +605,11 @@ class SdpOfferAnswerHandler : public SdpStateProvider { RTC_GUARDED_BY(signaling_thread()); std::unique_ptr pending_remote_description_ RTC_GUARDED_BY(signaling_thread()); + std::unique_ptr last_created_offer_ + RTC_GUARDED_BY(signaling_thread()); + std::unique_ptr last_created_answer_ + RTC_GUARDED_BY(signaling_thread()); + SdpMungingType last_sdp_munging_type_ = SdpMungingType::kNoModification; PeerConnectionInterface::SignalingState signaling_state_ RTC_GUARDED_BY(signaling_thread()) = PeerConnectionInterface::kStable; diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc index a1bb5d85d6..1546db04d5 100644 --- a/pc/sdp_offer_answer_unittest.cc +++ b/pc/sdp_offer_answer_unittest.cc @@ -18,6 +18,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_replace.h" +#include "api/audio_codecs/audio_format.h" #include "api/audio_codecs/builtin_audio_decoder_factory.h" #include "api/audio_codecs/builtin_audio_encoder_factory.h" #include "api/create_peerconnection_factory.h" @@ -31,6 +32,8 @@ #include "api/rtp_transceiver_direction.h" #include "api/rtp_transceiver_interface.h" #include "api/scoped_refptr.h" +#include "api/uma_metrics.h" +#include "api/video_codecs/sdp_video_format.h" #include "api/video_codecs/video_decoder_factory_template.h" #include "api/video_codecs/video_decoder_factory_template_dav1d_adapter.h" #include "api/video_codecs/video_decoder_factory_template_libvpx_vp8_adapter.h" @@ -44,10 +47,14 @@ #include "media/base/codec.h" #include "media/base/media_constants.h" #include "media/base/stream_params.h" +#include "p2p/base/transport_description.h" #include "pc/peer_connection_wrapper.h" #include "pc/session_description.h" #include "pc/test/fake_audio_capture_module.h" +#include "pc/test/fake_rtc_certificate_generator.h" +#include "pc/test/integration_test_helpers.h" #include "pc/test/mock_peer_connection_observers.h" +#include "rtc_base/gunit.h" #include "rtc_base/string_encode.h" #include "rtc_base/thread.h" #include "system_wrappers/include/metrics.h" @@ -63,6 +70,8 @@ namespace webrtc { using RTCConfiguration = PeerConnectionInterface::RTCConfiguration; +using ::testing::ElementsAre; +using ::testing::Pair; namespace { @@ -196,6 +205,7 @@ TEST_F(SdpOfferAnswerTest, BundleRejectsCodecCollisionsAudioVideo) { pc->SetRemoteDescription(std::move(desc), &error); // There is no error yet but the metrics counter will increase. EXPECT_TRUE(error.ok()); + EXPECT_METRIC_EQ( 1, metrics::NumEvents("WebRTC.PeerConnection.ValidBundledPayloadTypes", false)); @@ -1523,4 +1533,530 @@ TEST_F(SdpOfferAnswerTest, ReducedSizeNotNegotiated) { EXPECT_FALSE(video_send_param.rtcp.reduced_size); } +class SdpOfferAnswerMungingTest : public SdpOfferAnswerTest { + public: + SdpOfferAnswerMungingTest() : SdpOfferAnswerTest() { metrics::Reset(); } +}; + +TEST_F(SdpOfferAnswerMungingTest, DISABLED_ReportUMAMetricsWithNoMunging) { + auto caller = CreatePeerConnection(); + auto callee = CreatePeerConnection(); + + caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO); + caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO); + + // Negotiate, gather candidates, then exchange ICE candidates. + ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get())); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Answer.Initial"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); + + EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout); + EXPECT_TRUE_WAIT(callee->IsIceGatheringDone(), kDefaultTimeout); + for (const auto& candidate : caller->observer()->GetAllCandidates()) { + callee->pc()->AddIceCandidate(candidate); + } + for (const auto& candidate : callee->observer()->GetAllCandidates()) { + caller->pc()->AddIceCandidate(candidate); + } + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected, + caller->pc()->peer_connection_state(), kDefaultTimeout); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected, + callee->pc()->peer_connection_state(), kDefaultTimeout); + + caller->pc()->Close(); + callee->pc()->Close(); + + EXPECT_THAT( + metrics::Samples( + "WebRTC.PeerConnection.SdpMunging.Offer.ConnectionEstablished"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); + EXPECT_THAT( + metrics::Samples( + "WebRTC.PeerConnection.SdpMunging.Answer.ConnectionEstablished"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); + + EXPECT_THAT(metrics::Samples( + "WebRTC.PeerConnection.SdpMunging.Offer.ConnectionClosed"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); + EXPECT_THAT(metrics::Samples( + "WebRTC.PeerConnection.SdpMunging.Answer.ConnectionClosed"), + ElementsAre(Pair(SdpMungingType::kNoModification, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, + InitialSetLocalDescriptionWithoutCreateOffer) { + RTCConfiguration config; + config.certificates.push_back( + FakeRTCCertificateGenerator::GenerateCertificate()); + auto pc = CreatePeerConnection(config, nullptr); + std::string sdp = + "v=0\r\n" + "o=- 0 3 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-1 " + "D9:AB:00:AA:12:7B:62:54:CF:AD:3B:55:F7:60:BC:F3:40:A7:0B:5B\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:ETEn\r\n" + "a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l\r\n"; + auto offer = CreateSessionDescription(SdpType::kOffer, sdp); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kWithoutCreateOffer, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, + InitialSetLocalDescriptionWithoutCreateAnswer) { + RTCConfiguration config; + config.certificates.push_back( + FakeRTCCertificateGenerator::GenerateCertificate()); + auto pc = CreatePeerConnection(config, nullptr); + std::string sdp = + "v=0\r\n" + "o=- 0 3 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=fingerprint:sha-1 " + "D9:AB:00:AA:12:7B:62:54:CF:AD:3B:55:F7:60:BC:F3:40:A7:0B:5B\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:ETEn\r\n" + "a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l\r\n" + "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtcp-mux\r\n" + "a=sendrecv\r\n" + "a=mid:0\r\n" + "a=rtpmap:111 opus/48000/2\r\n"; + auto offer = CreateSessionDescription(SdpType::kOffer, sdp); + EXPECT_TRUE(pc->SetRemoteDescription(std::move(offer))); + + RTCError error; + auto answer = CreateSessionDescription(SdpType::kAnswer, sdp); + answer->description()->transport_infos()[0].description.connection_role = + cricket::CONNECTIONROLE_ACTIVE; + EXPECT_TRUE(pc->SetLocalDescription(std::move(answer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Answer.Initial"), + ElementsAre(Pair(SdpMungingType::kWithoutCreateAnswer, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, IceUfrag) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].description.ice_ufrag = + "amungediceufragthisshouldberejected"; + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kIceUfrag, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, IcePwd) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].description.ice_pwd = "amungedicepwdthisshouldberejected"; + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kIcePwd, 1))); +} +TEST_F(SdpOfferAnswerMungingTest, IceMode) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].description.ice_mode = cricket::ICEMODE_LITE; + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kIceMode, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, IceOptions) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].description.transport_options.push_back( + cricket::ICE_OPTION_RENOMINATION); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kIceOptions, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, DtlsRole) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].description.connection_role = + cricket::CONNECTIONROLE_PASSIVE; + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kDtlsSetup, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, RemoveContent) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + std::string name = contents[0].name; + EXPECT_TRUE(offer->description()->RemoveContentByName(contents[0].name)); + std::string sdp; + offer->ToString(&sdp); + auto modified_offer = CreateSessionDescription( + SdpType::kOffer, + absl::StrReplaceAll(sdp, {{"a=group:BUNDLE " + name, "a=group:BUNDLE"}})); + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(modified_offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kNumberOfContents, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, Mid) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + std::string name = contents[0].name; + contents[0].name = "amungedmid"; + + auto& transport_infos = offer->description()->transport_infos(); + ASSERT_EQ(transport_infos.size(), 1u); + transport_infos[0].content_name = "amungedmid"; + std::string sdp; + offer->ToString(&sdp); + auto modified_offer = CreateSessionDescription( + SdpType::kOffer, + absl::StrReplaceAll( + sdp, {{"a=group:BUNDLE " + name, "a=group:BUNDLE amungedmid"}})); + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(modified_offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kMid, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, LegacySimulcast) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + uint32_t ssrc = media_description->first_ssrc(); + ASSERT_EQ(media_description->streams().size(), 1u); + const std::string& cname = media_description->streams()[0].cname; + + std::string sdp; + offer->ToString(&sdp); + sdp += "a=ssrc-group:SIM " + rtc::ToString(ssrc) + " " + + rtc::ToString(ssrc + 1) + "\r\n" + // + "a=ssrc-group:FID " + rtc::ToString(ssrc + 1) + " " + + rtc::ToString(ssrc + 2) + "\r\n" + // + "a=ssrc:" + rtc::ToString(ssrc + 1) + " msid:- video_track\r\n" + // + "a=ssrc:" + rtc::ToString(ssrc + 1) + " cname:" + cname + "\r\n" + // + "a=ssrc:" + rtc::ToString(ssrc + 2) + " msid:- video_track\r\n" + // + "a=ssrc:" + rtc::ToString(ssrc + 2) + " cname:" + cname + "\r\n"; + auto modified_offer = CreateSessionDescription(SdpType::kOffer, sdp); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(modified_offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kVideoCodecsLegacySimulcast, 1))); +} + +#ifdef WEBRTC_USE_H264 +TEST_F(SdpOfferAnswerMungingTest, H264SpsPpsIdrInKeyFrame) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + for (auto& codec : codecs) { + if (codec.name == cricket::kH264CodecName) { + codec.SetParam(cricket::kH264FmtpSpsPpsIdrInKeyframe, + cricket::kParamValueTrue); + } + } + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre( + Pair(SdpMungingType::kVideoCodecsFmtpH264SpsPpsIdrInKeyframe, 1))); +} +#endif // WEBRTC_USE_H264 + +TEST_F(SdpOfferAnswerMungingTest, OpusStereo) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + for (auto& codec : codecs) { + if (codec.name == cricket::kOpusCodecName) { + codec.SetParam(cricket::kCodecParamStereo, cricket::kParamValueTrue); + } + } + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kAudioCodecsFmtpOpusStereo, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, AudioCodecsRemoved) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + codecs.pop_back(); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kAudioCodecsRemoved, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, AudioCodecsAdded) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + auto codec = cricket::CreateAudioCodec(SdpAudioFormat("pcmu", 8000, 1, {})); + codec.id = 19; // IANA reserved payload type, should not conflict. + codecs.push_back(codec); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kAudioCodecsAdded, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, VideoCodecsRemoved) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + codecs.pop_back(); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kVideoCodecsRemoved, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, VideoCodecsAdded) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + auto codec = cricket::CreateVideoCodec(SdpVideoFormat("VP8", {})); + codec.id = 19; // IANA reserved payload type, should not conflict. + codecs.push_back(codec); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kVideoCodecsAdded, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, MultiOpus) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + auto multiopus = + cricket::CreateAudioCodec(SdpAudioFormat("multiopus", 48000, 4, + {{"channel_mapping", "0,1,2,3"}, + {"coupled_streams", "2"}, + {"num_streams", "2"}})); + multiopus.id = 19; // IANA reserved payload type, should not conflict. + codecs.push_back(multiopus); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kAudioCodecsAddedMultiOpus, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, L16) { + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + std::vector codecs = media_description->codecs(); + auto l16 = cricket::CreateAudioCodec(SdpAudioFormat("L16", 48000, 2, {})); + l16.id = 19; // IANA reserved payload type, should not conflict. + codecs.push_back(l16); + media_description->set_codecs(codecs); + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kAudioCodecsAddedL16, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, AudioSsrc) { + // Note: same applies to video but is harder to write since one needs to + // modify the ssrc-group too. + auto pc = CreatePeerConnection(); + pc->AddAudioTrack("audio_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + ASSERT_EQ(media_description->streams().size(), 1u); + media_description->mutable_streams()[0].ssrcs[0] = 4404; + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kSsrcs, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, HeaderExtensionAdded) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + // VLA is off by default, id=42 should be unused. + media_description->AddRtpHeaderExtension( + {RtpExtension::kVideoLayersAllocationUri, 42}); + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kRtpHeaderExtensionAdded, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, HeaderExtensionRemoved) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + media_description->ClearRtpHeaderExtensions(); + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kRtpHeaderExtensionRemoved, 1))); +} + +TEST_F(SdpOfferAnswerMungingTest, HeaderExtensionModified) { + auto pc = CreatePeerConnection(); + pc->AddVideoTrack("video_track", {}); + + auto offer = pc->CreateOffer(); + auto& contents = offer->description()->contents(); + ASSERT_EQ(contents.size(), 1u); + auto* media_description = contents[0].media_description(); + ASSERT_TRUE(media_description); + auto extensions = media_description->rtp_header_extensions(); + ASSERT_GT(extensions.size(), 0u); + extensions[0].id = 42; // id=42 should be unused. + media_description->set_rtp_header_extensions(extensions); + + RTCError error; + EXPECT_TRUE(pc->SetLocalDescription(std::move(offer), &error)); + EXPECT_THAT( + metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"), + ElementsAre(Pair(SdpMungingType::kRtpHeaderExtensionModified, 1))); +} + } // namespace webrtc