/* * 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/media_types.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].mid() != contents_to_set[i].mid()) { 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. cricket::MediaType media_type = last_created_media_description->type(); if (media_type == cricket::MEDIA_TYPE_VIDEO) { type = DetermineVideoSdpMungingType(last_created_media_description, media_description_to_set); if (type != SdpMungingType::kNoModification) { return type; } } else if (media_type == cricket::MEDIA_TYPE_AUDIO) { type = DetermineAudioSdpMungingType(last_created_media_description, media_description_to_set); if (type != SdpMungingType::kNoModification) { return type; } } // Validate codecs. We should have bailed out earlier if codecs were added // or removed. auto last_created_codecs = last_created_media_description->codecs(); auto codecs_to_set = media_description_to_set->codecs(); if (last_created_codecs.size() == codecs_to_set.size()) { for (size_t i = 0; i < last_created_codecs.size(); i++) { if (last_created_codecs[i] == codecs_to_set[i]) { continue; } // Codec position swapped. for (size_t j = i + 1; j < last_created_codecs.size(); j++) { if (last_created_codecs[i] == codecs_to_set[j]) { return media_type == cricket::MEDIA_TYPE_AUDIO ? SdpMungingType::kAudioCodecsReordered : SdpMungingType::kVideoCodecsReordered; } } // Same codec but id changed. if (last_created_codecs[i].name == codecs_to_set[i].name && last_created_codecs[i].id != codecs_to_set[i].id) { return SdpMungingType::kPayloadTypes; } if (last_created_codecs[i].params != codecs_to_set[i].params) { return media_type == cricket::MEDIA_TYPE_AUDIO ? SdpMungingType::kAudioCodecsFmtp : SdpMungingType::kVideoCodecsFmtp; } if (last_created_codecs[i].feedback_params != codecs_to_set[i].feedback_params) { return media_type == cricket::MEDIA_TYPE_AUDIO ? SdpMungingType::kAudioCodecsRtcpFb : SdpMungingType::kVideoCodecsRtcpFb; } // At this point clockrate or channels changed. This should already be // rejected later in the process so ignore for munging. } } // 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