Disable PT based demuxing if MID header extension is present.
We want to turn off PT based demux because SSRC-based endpoints that send media prematurely (which is a popular non-standard behavior still heavily in use) can otherwise get incorrect mappings and unsignalled ssrc issues because of the PT demux path. This CL disables PT based demuxing when the MID header extension is present on all m= sections in the SDP for that kind (audio/video), not caring if it was in the offer or answer. However if PT demuxing has been used in the past then it is always allowed. This ensures PT is off by default but that either offer or answer can enable PT and once it has been on it is also possible to get early media with PT. - Want PT-based demux? The MID header extension has to be removed in either the offer or the answer. Follow-up O/As allow PT demuxing if possible. - Want to use MID or SSRC demuxing? Great, you don't need PT-based demux and won't mind that we turned it off for you. The reason for disabling PT demux at offer time (if MID is present) instead of waiting for the SDP answer is because by the time the SDP answer arrives, early media could have triggered PT demux and caused incorrect mappings. The safe thing is to assume a spec-compliant endpoint until proven otherwise. However if PT demux is ever enabled, then from that point on we always allow PT-based demux in follow-up O/A exchanges. This ensures we don't drop packets in follow-up exchanges. The fact that PT-based demux is disabled during the initial offer should not matter because before the initial O/A exchange we don't have fingerprints. This change only affects Unified Plan and bundled groups. Existing test coverage ensuring we do not break legacy endpoints: [1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/pc/peer_connection_integrationtest.cc;l=1156 [2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/external/wpt/webrtc/protocol/rtp-demuxing.html;l=59 UnsignaledStreamTest is also updated to test the interesting setups. A kill-switch is added in case we want to disable this change. Bug: webrtc:12814 Change-Id: I807a82a543325753633aaef698e06cb4c9dfebaa Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/221101 Commit-Queue: Henrik Boström <hbos@webrtc.org> Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org> Reviewed-by: Harald Alvestrand <hta@webrtc.org> Cr-Commit-Position: refs/heads/master@{#34251}
This commit is contained in:
parent
33e75bb874
commit
4ea80f35f1
@ -60,6 +60,7 @@
|
||||
#include "rtc_base/strings/string_builder.h"
|
||||
#include "rtc_base/third_party/sigslot/sigslot.h"
|
||||
#include "rtc_base/trace_event.h"
|
||||
#include "system_wrappers/include/field_trial.h"
|
||||
#include "system_wrappers/include/metrics.h"
|
||||
|
||||
using cricket::ContentInfo;
|
||||
@ -87,6 +88,9 @@ namespace {
|
||||
typedef webrtc::PeerConnectionInterface::RTCOfferAnswerOptions
|
||||
RTCOfferAnswerOptions;
|
||||
|
||||
constexpr const char* kAlwaysAllowPayloadTypeDemuxingFieldTrialName =
|
||||
"WebRTC-AlwaysAllowPayloadTypeDemuxing";
|
||||
|
||||
// Error messages
|
||||
const char kInvalidSdp[] = "Invalid session description.";
|
||||
const char kInvalidCandidates[] = "Description contains invalid candidates.";
|
||||
@ -738,6 +742,17 @@ rtc::scoped_refptr<webrtc::DtlsTransport> LookupDtlsTransportByMid(
|
||||
[controller, &mid] { return controller->LookupDtlsTransportByMid(mid); });
|
||||
}
|
||||
|
||||
bool ContentHasHeaderExtension(const cricket::ContentInfo& content_info,
|
||||
absl::string_view header_extension_uri) {
|
||||
for (const RtpExtension& rtp_header_extension :
|
||||
content_info.media_description()->rtp_header_extensions()) {
|
||||
if (rtp_header_extension.uri == header_extension_uri) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Used by parameterless SetLocalDescription() to create an offer or answer.
|
||||
@ -4840,10 +4855,13 @@ bool SdpOfferAnswerHandler::UpdatePayloadTypeDemuxingState(
|
||||
struct PayloadTypes {
|
||||
std::set<int> audio_payload_types;
|
||||
std::set<int> video_payload_types;
|
||||
bool pt_demuxing_enabled_audio = true;
|
||||
bool pt_demuxing_enabled_video = true;
|
||||
bool pt_demuxing_possible_audio = true;
|
||||
bool pt_demuxing_possible_video = true;
|
||||
};
|
||||
std::map<const cricket::ContentGroup*, PayloadTypes> payload_types_by_bundle;
|
||||
// If the MID is missing from *any* receiving m= section, this is set to true.
|
||||
bool mid_header_extension_missing_audio = false;
|
||||
bool mid_header_extension_missing_video = false;
|
||||
for (auto& content_info : sdesc->description()->contents()) {
|
||||
auto it = bundle_groups_by_mid.find(content_info.name);
|
||||
const cricket::ContentGroup* bundle_group =
|
||||
@ -4867,26 +4885,34 @@ bool SdpOfferAnswerHandler::UpdatePayloadTypeDemuxingState(
|
||||
}
|
||||
switch (content_info.media_description()->type()) {
|
||||
case cricket::MediaType::MEDIA_TYPE_AUDIO: {
|
||||
if (!mid_header_extension_missing_audio) {
|
||||
mid_header_extension_missing_audio =
|
||||
!ContentHasHeaderExtension(content_info, RtpExtension::kMidUri);
|
||||
}
|
||||
const cricket::AudioContentDescription* audio_desc =
|
||||
content_info.media_description()->as_audio();
|
||||
for (const cricket::AudioCodec& audio : audio_desc->codecs()) {
|
||||
if (payload_types->audio_payload_types.count(audio.id)) {
|
||||
// Two m= sections are using the same payload type, thus demuxing
|
||||
// by payload type is not possible.
|
||||
payload_types->pt_demuxing_enabled_audio = false;
|
||||
payload_types->pt_demuxing_possible_audio = false;
|
||||
}
|
||||
payload_types->audio_payload_types.insert(audio.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case cricket::MediaType::MEDIA_TYPE_VIDEO: {
|
||||
if (!mid_header_extension_missing_video) {
|
||||
mid_header_extension_missing_video =
|
||||
!ContentHasHeaderExtension(content_info, RtpExtension::kMidUri);
|
||||
}
|
||||
const cricket::VideoContentDescription* video_desc =
|
||||
content_info.media_description()->as_video();
|
||||
for (const cricket::VideoCodec& video : video_desc->codecs()) {
|
||||
if (payload_types->video_payload_types.count(video.id)) {
|
||||
// Two m= sections are using the same payload type, thus demuxing
|
||||
// by payload type is not possible.
|
||||
payload_types->pt_demuxing_enabled_video = false;
|
||||
payload_types->pt_demuxing_possible_video = false;
|
||||
}
|
||||
payload_types->video_payload_types.insert(video.id);
|
||||
}
|
||||
@ -4920,9 +4946,39 @@ bool SdpOfferAnswerHandler::UpdatePayloadTypeDemuxingState(
|
||||
if (channels_to_update.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In Unified Plan, payload type demuxing is useful for legacy endpoints that
|
||||
// don't support the MID header extension, but it can also cause incorrrect
|
||||
// forwarding of packets when going from one m= section to multiple m=
|
||||
// sections in the same BUNDLE. This only happens if media arrives prior to
|
||||
// negotiation, but this can cause missing video and unsignalled ssrc bugs
|
||||
// severe enough to warrant disabling PT demuxing in such cases. Therefore, if
|
||||
// a MID header extension is present on all m= sections for a given kind
|
||||
// (audio/video) then we use that as an OK to disable payload type demuxing in
|
||||
// BUNDLEs of that kind. However if PT demuxing was ever turned on (e.g. MID
|
||||
// was ever removed on ANY m= section of that kind) then we continue to allow
|
||||
// PT demuxing in order to prevent disabling it in follow-up O/A exchanges and
|
||||
// allowing early media by PT.
|
||||
bool bundled_pt_demux_allowed_audio = !IsUnifiedPlan() ||
|
||||
mid_header_extension_missing_audio ||
|
||||
pt_demuxing_has_been_used_audio_;
|
||||
bool bundled_pt_demux_allowed_video = !IsUnifiedPlan() ||
|
||||
mid_header_extension_missing_video ||
|
||||
pt_demuxing_has_been_used_video_;
|
||||
// Kill switch for the above change.
|
||||
if (field_trial::IsEnabled(kAlwaysAllowPayloadTypeDemuxingFieldTrialName)) {
|
||||
// TODO(https://crbug.com/webrtc/12814): If disabling PT-based demux does
|
||||
// not trigger regressions, remove this kill switch.
|
||||
bundled_pt_demux_allowed_audio = true;
|
||||
bundled_pt_demux_allowed_video = true;
|
||||
}
|
||||
|
||||
return pc_->worker_thread()->Invoke<bool>(
|
||||
RTC_FROM_HERE,
|
||||
[&channels_to_update, &bundle_groups_by_mid, &payload_types_by_bundle]() {
|
||||
[&channels_to_update, &bundle_groups_by_mid, &payload_types_by_bundle,
|
||||
bundled_pt_demux_allowed_audio, bundled_pt_demux_allowed_video,
|
||||
pt_demuxing_has_been_used_audio = &pt_demuxing_has_been_used_audio_,
|
||||
pt_demuxing_has_been_used_video = &pt_demuxing_has_been_used_video_]() {
|
||||
for (const auto& it : channels_to_update) {
|
||||
RtpTransceiverDirection local_direction = it.first;
|
||||
cricket::ChannelInterface* channel = it.second;
|
||||
@ -4932,17 +4988,27 @@ bool SdpOfferAnswerHandler::UpdatePayloadTypeDemuxingState(
|
||||
bundle_it != bundle_groups_by_mid.end() ? bundle_it->second
|
||||
: nullptr;
|
||||
if (media_type == cricket::MediaType::MEDIA_TYPE_AUDIO) {
|
||||
if (!channel->SetPayloadTypeDemuxingEnabled(
|
||||
(!bundle_group || payload_types_by_bundle[bundle_group]
|
||||
.pt_demuxing_enabled_audio) &&
|
||||
RtpTransceiverDirectionHasRecv(local_direction))) {
|
||||
bool pt_demux_enabled =
|
||||
RtpTransceiverDirectionHasRecv(local_direction) &&
|
||||
(!bundle_group || (bundled_pt_demux_allowed_audio &&
|
||||
payload_types_by_bundle[bundle_group]
|
||||
.pt_demuxing_possible_audio));
|
||||
if (pt_demux_enabled) {
|
||||
*pt_demuxing_has_been_used_audio = true;
|
||||
}
|
||||
if (!channel->SetPayloadTypeDemuxingEnabled(pt_demux_enabled)) {
|
||||
return false;
|
||||
}
|
||||
} else if (media_type == cricket::MediaType::MEDIA_TYPE_VIDEO) {
|
||||
if (!channel->SetPayloadTypeDemuxingEnabled(
|
||||
(!bundle_group || payload_types_by_bundle[bundle_group]
|
||||
.pt_demuxing_enabled_video) &&
|
||||
RtpTransceiverDirectionHasRecv(local_direction))) {
|
||||
bool pt_demux_enabled =
|
||||
RtpTransceiverDirectionHasRecv(local_direction) &&
|
||||
(!bundle_group || (bundled_pt_demux_allowed_video &&
|
||||
payload_types_by_bundle[bundle_group]
|
||||
.pt_demuxing_possible_video));
|
||||
if (pt_demux_enabled) {
|
||||
*pt_demuxing_has_been_used_video = true;
|
||||
}
|
||||
if (!channel->SetPayloadTypeDemuxingEnabled(pt_demux_enabled)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,6 +629,11 @@ class SdpOfferAnswerHandler : public SdpStateProvider,
|
||||
uint32_t negotiation_needed_event_id_ = 0;
|
||||
bool update_negotiation_needed_on_empty_chain_
|
||||
RTC_GUARDED_BY(signaling_thread()) = false;
|
||||
// If PT demuxing is successfully negotiated one time we will allow PT
|
||||
// demuxing for the rest of the session so that PT-based apps default to PT
|
||||
// demuxing in follow-up O/A exchanges.
|
||||
bool pt_demuxing_has_been_used_audio_ = false;
|
||||
bool pt_demuxing_has_been_used_video_ = false;
|
||||
|
||||
// In Unified Plan, if we encounter remote SDP that does not contain an a=msid
|
||||
// line we create and use a stream with a random ID for our receivers. This is
|
||||
|
||||
@ -24,6 +24,31 @@ namespace webrtc {
|
||||
namespace test {
|
||||
namespace {
|
||||
|
||||
enum class MidTestConfiguration {
|
||||
// Legacy endpoint setup where PT demuxing is used.
|
||||
kMidNotNegotiated,
|
||||
// MID is negotiated but missing from packets. PT demuxing is disabled, so
|
||||
// SSRCs have to be added to the SDP for WebRTC to forward packets correctly.
|
||||
// Happens when client is spec compliant but the SFU isn't. Popular legacy.
|
||||
kMidNegotiatedButMissingFromPackets,
|
||||
// Fully spec-compliant: MID is present so we can safely drop packets with
|
||||
// unknown MIDs.
|
||||
kMidNegotiatedAndPresentInPackets,
|
||||
};
|
||||
|
||||
// Gives the parameterized test a readable suffix.
|
||||
std::string TestParametersMidTestConfigurationToString(
|
||||
testing::TestParamInfo<MidTestConfiguration> info) {
|
||||
switch (info.param) {
|
||||
case MidTestConfiguration::kMidNotNegotiated:
|
||||
return "MidNotNegotiated";
|
||||
case MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
|
||||
return "MidNegotiatedButMissingFromPackets";
|
||||
case MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
|
||||
return "MidNegotiatedAndPresentInPackets";
|
||||
}
|
||||
}
|
||||
|
||||
class FrameObserver : public rtc::VideoSinkInterface<VideoFrame> {
|
||||
public:
|
||||
FrameObserver() : frame_observed_(false) {}
|
||||
@ -53,19 +78,24 @@ void set_ssrc(SessionDescriptionInterface* offer, size_t index, uint32_t ssrc) {
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
|
||||
class UnsignaledStreamTest
|
||||
: public ::testing::Test,
|
||||
public ::testing::WithParamInterface<MidTestConfiguration> {};
|
||||
|
||||
TEST_P(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
|
||||
// This test covers a scenario that might occur if a remote client starts
|
||||
// sending media packets before negotiation has completed. These packets will
|
||||
// trigger an unsignalled default stream to be created, and connects that to
|
||||
// a default video sink.
|
||||
// In some edge cases using unified plan, the default stream is create in a
|
||||
// different transceiver to where the media SSRC will actually be used.
|
||||
// This test verifies that the default stream is removed properly, and that
|
||||
// packets are demuxed and video frames reach the desired sink.
|
||||
// sending media packets before negotiation has completed. Depending on setup,
|
||||
// these packets either get dropped or trigger an unsignalled default stream
|
||||
// to be created, and connects that to a default video sink.
|
||||
// In some edge cases using Unified Plan and PT demuxing, the default stream
|
||||
// is create in a different transceiver to where the media SSRC will actually
|
||||
// be used. This test verifies that the default stream is removed properly,
|
||||
// and that packets are demuxed and video frames reach the desired sink.
|
||||
const MidTestConfiguration kMidTestConfiguration = GetParam();
|
||||
|
||||
// Defined before PeerScenario so it gets destructed after, to avoid use after
|
||||
// free.
|
||||
PeerScenario s(*test_info_);
|
||||
PeerScenario s(*::testing::UnitTest::GetInstance()->current_test_info());
|
||||
|
||||
PeerScenarioClient::Config config = PeerScenarioClient::Config();
|
||||
// Disable encryption so that we can inject a fake early media packet without
|
||||
@ -93,14 +123,61 @@ TEST(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
|
||||
std::atomic<bool> got_unsignaled_packet(false);
|
||||
|
||||
// We will capture the media ssrc of the first added stream, and preemptively
|
||||
// inject a new media packet using a different ssrc.
|
||||
// This will create "default stream" for the second ssrc and connected it to
|
||||
// the default video sink (not set in this test).
|
||||
// inject a new media packet using a different ssrc. What happens depends on
|
||||
// the test configuration.
|
||||
//
|
||||
// MidTestConfiguration::kMidNotNegotiated:
|
||||
// - MID is not negotiated which means PT-based demuxing is enabled. Because
|
||||
// the packets have no MID, the second ssrc packet gets forwarded to the
|
||||
// first m= section. This will create a "default stream" for the second ssrc
|
||||
// and connect it to the default video sink (not set in this test). The test
|
||||
// verifies we can recover from this when we later get packets for the first
|
||||
// ssrc.
|
||||
//
|
||||
// MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
|
||||
// - MID is negotiated wich means PT-based demuxing is disabled. Because we
|
||||
// modify the packets not to contain the MID anyway (simulating a legacy SFU
|
||||
// that does not negotiate properly) unknown SSRCs are dropped but do not
|
||||
// otherwise cause any issues.
|
||||
//
|
||||
// MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
|
||||
// - MID is negotiated which means PT-based demuxing is enabled. In this case
|
||||
// the packets have the MID so they either get forwarded or dropped
|
||||
// depending on if the MID is known. The spec-compliant way is also the most
|
||||
// straight-forward one.
|
||||
|
||||
uint32_t first_ssrc = 0;
|
||||
uint32_t second_ssrc = 0;
|
||||
absl::optional<int> mid_header_extension_id = absl::nullopt;
|
||||
|
||||
signaling.NegotiateSdp(
|
||||
/* munge_sdp = */ {},
|
||||
/* munge_sdp = */
|
||||
[&](SessionDescriptionInterface* offer) {
|
||||
// Obtain the MID header extension ID and if we want the
|
||||
// MidTestConfiguration::kMidNotNegotiated setup then we remove the MID
|
||||
// header extension through SDP munging (otherwise SDP is not modified).
|
||||
for (cricket::ContentInfo& content_info :
|
||||
offer->description()->contents()) {
|
||||
std::vector<RtpExtension> header_extensions =
|
||||
content_info.media_description()->rtp_header_extensions();
|
||||
for (auto it = header_extensions.begin();
|
||||
it != header_extensions.end(); ++it) {
|
||||
if (it->uri == RtpExtension::kMidUri) {
|
||||
// MID header extension found!
|
||||
mid_header_extension_id = it->id;
|
||||
if (kMidTestConfiguration ==
|
||||
MidTestConfiguration::kMidNotNegotiated) {
|
||||
// Munge away the extension.
|
||||
header_extensions.erase(it);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
content_info.media_description()->set_rtp_header_extensions(
|
||||
std::move(header_extensions));
|
||||
}
|
||||
ASSERT_TRUE(mid_header_extension_id.has_value());
|
||||
},
|
||||
/* modify_sdp = */
|
||||
[&](SessionDescriptionInterface* offer) {
|
||||
first_ssrc = get_ssrc(offer, 0);
|
||||
@ -113,9 +190,40 @@ TEST(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
|
||||
if (ByteReader<uint32_t>::ReadBigEndian(&(packet.cdata()[8])) ==
|
||||
first_ssrc &&
|
||||
!got_unsignaled_packet) {
|
||||
rtc::CopyOnWriteBuffer updated_buffer = packet.data;
|
||||
ByteWriter<uint32_t>::WriteBigEndian(
|
||||
updated_buffer.MutableData() + 8, second_ssrc);
|
||||
// Parse packet and modify the SSRC to simulate a second m=
|
||||
// section that has not been negotiated yet.
|
||||
std::vector<RtpExtension> extensions;
|
||||
extensions.emplace_back(RtpExtension::kMidUri,
|
||||
mid_header_extension_id.value());
|
||||
RtpHeaderExtensionMap extensions_map(extensions);
|
||||
RtpPacket parsed_packet;
|
||||
parsed_packet.IdentifyExtensions(extensions_map);
|
||||
ASSERT_TRUE(parsed_packet.Parse(packet.data));
|
||||
parsed_packet.SetSsrc(second_ssrc);
|
||||
// The MID extension is present if and only if it was negotiated.
|
||||
// If present, we either want to remove it or modify it depending
|
||||
// on setup.
|
||||
switch (kMidTestConfiguration) {
|
||||
case MidTestConfiguration::kMidNotNegotiated:
|
||||
EXPECT_FALSE(parsed_packet.HasExtension<RtpMid>());
|
||||
break;
|
||||
case MidTestConfiguration::kMidNegotiatedButMissingFromPackets:
|
||||
EXPECT_TRUE(parsed_packet.HasExtension<RtpMid>());
|
||||
ASSERT_TRUE(parsed_packet.RemoveExtension(RtpMid::kId));
|
||||
break;
|
||||
case MidTestConfiguration::kMidNegotiatedAndPresentInPackets:
|
||||
EXPECT_TRUE(parsed_packet.HasExtension<RtpMid>());
|
||||
// The simulated second m= section would have a different MID.
|
||||
// If we don't modify it here then |second_ssrc| would end up
|
||||
// being mapped to the first m= section which would cause SSRC
|
||||
// conflicts if we later add the same SSRC to a second m=
|
||||
// section. Hidden assumption: first m= section does not use
|
||||
// MID:1.
|
||||
ASSERT_TRUE(parsed_packet.SetExtension<RtpMid>("1"));
|
||||
break;
|
||||
}
|
||||
// Inject the modified packet.
|
||||
rtc::CopyOnWriteBuffer updated_buffer = parsed_packet.Buffer();
|
||||
EmulatedIpPacket updated_packet(
|
||||
packet.from, packet.to, updated_buffer, packet.arrival_time);
|
||||
send_node->OnPacketReceived(std::move(updated_packet));
|
||||
@ -153,5 +261,13 @@ TEST(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) {
|
||||
EXPECT_TRUE(s.WaitAndProcess(&second_sink.frame_observed_));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
All,
|
||||
UnsignaledStreamTest,
|
||||
::testing::Values(MidTestConfiguration::kMidNotNegotiated,
|
||||
MidTestConfiguration::kMidNegotiatedButMissingFromPackets,
|
||||
MidTestConfiguration::kMidNegotiatedAndPresentInPackets),
|
||||
TestParametersMidTestConfigurationToString);
|
||||
|
||||
} // namespace test
|
||||
} // namespace webrtc
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user