/* * Copyright (c) 2020 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 "media/base/stream_params.h" #include "modules/rtp_rtcp/source/byte_io.h" #include "modules/rtp_rtcp/source/rtp_util.h" #include "pc/media_session.h" #include "pc/session_description.h" #include "test/field_trial.h" #include "test/gmock.h" #include "test/gtest.h" #include "test/peer_scenario/peer_scenario.h" 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 info) { switch (info.param) { case MidTestConfiguration::kMidNotNegotiated: return "MidNotNegotiated"; case MidTestConfiguration::kMidNegotiatedButMissingFromPackets: return "MidNegotiatedButMissingFromPackets"; case MidTestConfiguration::kMidNegotiatedAndPresentInPackets: return "MidNegotiatedAndPresentInPackets"; } } class FrameObserver : public rtc::VideoSinkInterface { public: FrameObserver() : frame_observed_(false) {} void OnFrame(const VideoFrame&) override { frame_observed_ = true; } std::atomic frame_observed_; }; uint32_t get_ssrc(SessionDescriptionInterface* offer, size_t track_index) { EXPECT_LT(track_index, offer->description()->contents().size()); return offer->description() ->contents()[track_index] .media_description() ->streams()[0] .ssrcs[0]; } void set_ssrc(SessionDescriptionInterface* offer, size_t index, uint32_t ssrc) { EXPECT_LT(index, offer->description()->contents().size()); cricket::StreamParams& new_stream_params = offer->description() ->contents()[index] .media_description() ->mutable_streams()[0]; new_stream_params.ssrcs[0] = ssrc; new_stream_params.ssrc_groups[0].ssrcs[0] = ssrc; } } // namespace class UnsignaledStreamTest : public ::testing::Test, public ::testing::WithParamInterface {}; TEST_P(UnsignaledStreamTest, ReplacesUnsignaledStreamOnCompletedSignaling) { // This test covers a scenario that might occur if a remote client starts // 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(*::testing::UnitTest::GetInstance()->current_test_info()); PeerScenarioClient::Config config = PeerScenarioClient::Config(); // Disable encryption so that we can inject a fake early media packet without // triggering srtp failures. config.disable_encryption = true; auto* caller = s.CreateClient(config); auto* callee = s.CreateClient(config); auto send_node = s.net()->NodeBuilder().Build().node; auto ret_node = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(), {send_node}, callee->endpoint()); s.net()->CreateRoute(callee->endpoint(), {ret_node}, caller->endpoint()); auto signaling = s.ConnectSignaling(caller, callee, {send_node}, {ret_node}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 15; auto first_track = caller->CreateVideo("VIDEO", video_conf); FrameObserver first_sink; callee->AddVideoReceiveSink(first_track.track->id(), &first_sink); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); std::atomic 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. 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 mid_header_extension_id = absl::nullopt; signaling.NegotiateSdp( /* 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 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); second_ssrc = first_ssrc + 1; send_node->router()->SetWatcher([&](const EmulatedIpPacket& packet) { if (IsRtpPacket(packet.data) && ByteReader::ReadBigEndian(&(packet.cdata()[8])) == first_ssrc && !got_unsignaled_packet) { // Parse packet and modify the SSRC to simulate a second m= // section that has not been negotiated yet. std::vector 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()); break; case MidTestConfiguration::kMidNegotiatedButMissingFromPackets: EXPECT_TRUE(parsed_packet.HasExtension()); ASSERT_TRUE(parsed_packet.RemoveExtension(RtpMid::kId)); break; case MidTestConfiguration::kMidNegotiatedAndPresentInPackets: EXPECT_TRUE(parsed_packet.HasExtension()); // 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("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)); got_unsignaled_packet = true; } }); }, [&](const SessionDescriptionInterface& answer) { EXPECT_EQ(answer.description()->contents().size(), 1u); offer_exchange_done = true; }); EXPECT_TRUE(s.WaitAndProcess(&offer_exchange_done)); EXPECT_TRUE(s.WaitAndProcess(&got_unsignaled_packet)); EXPECT_TRUE(s.WaitAndProcess(&first_sink.frame_observed_)); auto second_track = caller->CreateVideo("VIDEO2", video_conf); FrameObserver second_sink; callee->AddVideoReceiveSink(second_track.track->id(), &second_sink); // Create a second video stream, munge the sdp to force it to use our fake // early media ssrc. offer_exchange_done = false; signaling.NegotiateSdp( /* munge_sdp = */ [&](SessionDescriptionInterface* offer) { set_ssrc(offer, 1, second_ssrc); }, /* modify_sdp = */ {}, [&](const SessionDescriptionInterface& answer) { EXPECT_EQ(answer.description()->contents().size(), 2u); offer_exchange_done = true; }); EXPECT_TRUE(s.WaitAndProcess(&offer_exchange_done)); 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