blob: b60a84f7fe1555101405d110b21ea3dfb351dec5 [file] [log] [blame] [edit]
/*
* 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 <cstddef>
#include <string>
#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].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.
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