Default streams: don't block media even if on different transceiver.

This fixes some edge cases where early media could cause default
stream that block the actual signaled media from beind delivered.

Bug: webrtc:11477
Change-Id: I8b26df63a690861bd19f083102d1395e882f8733
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/183120
Commit-Queue: Taylor <deadbeef@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Reviewed-by: Rasmus Brandt <brandtr@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#32030}
diff --git a/media/base/media_channel.h b/media/base/media_channel.h
index e8400a5..812d492 100644
--- a/media/base/media_channel.h
+++ b/media/base/media_channel.h
@@ -204,7 +204,8 @@
   // ssrc must be the first SSRC of the media stream if the stream uses
   // multiple SSRCs.
   virtual bool RemoveRecvStream(uint32_t ssrc) = 0;
-  // Resets any cached StreamParams for an unsignaled RecvStream.
+  // Resets any cached StreamParams for an unsignaled RecvStream, and removes
+  // any existing unsignaled streams.
   virtual void ResetUnsignaledRecvStream() = 0;
   // Returns the absoulte sendtime extension id value from media channel.
   virtual int GetRtpSendTimeExtnId() const;
diff --git a/media/engine/webrtc_voice_engine.cc b/media/engine/webrtc_voice_engine.cc
index 41308ba..d0d4b15 100644
--- a/media/engine/webrtc_voice_engine.cc
+++ b/media/engine/webrtc_voice_engine.cc
@@ -2028,6 +2028,11 @@
   RTC_DCHECK(worker_thread_checker_.IsCurrent());
   RTC_LOG(LS_INFO) << "ResetUnsignaledRecvStream.";
   unsignaled_stream_params_ = StreamParams();
+  // Create a copy since RemoveRecvStream will modify |unsignaled_recv_ssrcs_|.
+  std::vector<uint32_t> to_remove = unsignaled_recv_ssrcs_;
+  for (uint32_t ssrc : to_remove) {
+    RemoveRecvStream(ssrc);
+  }
 }
 
 bool WebRtcVoiceMediaChannel::SetLocalSource(uint32_t ssrc,
diff --git a/media/engine/webrtc_voice_engine_unittest.cc b/media/engine/webrtc_voice_engine_unittest.cc
index 3010974..f844b50 100644
--- a/media/engine/webrtc_voice_engine_unittest.cc
+++ b/media/engine/webrtc_voice_engine_unittest.cc
@@ -2722,6 +2722,30 @@
   EXPECT_TRUE(GetRecvStream(kSsrc1).GetConfig().sync_group.empty());
 }
 
+TEST_P(WebRtcVoiceEngineTestFake,
+       ResetUnsignaledRecvStreamDeletesAllDefaultStreams) {
+  ASSERT_TRUE(SetupChannel());
+  // No receive streams to start with.
+  ASSERT_TRUE(call_.GetAudioReceiveStreams().empty());
+
+  // Deliver a couple packets with unsignaled SSRCs.
+  unsigned char packet[sizeof(kPcmuFrame)];
+  memcpy(packet, kPcmuFrame, sizeof(kPcmuFrame));
+  rtc::SetBE32(&packet[8], 0x1234);
+  DeliverPacket(packet, sizeof(packet));
+  rtc::SetBE32(&packet[8], 0x5678);
+  DeliverPacket(packet, sizeof(packet));
+
+  // Verify that the receive streams were created.
+  const auto& receivers1 = call_.GetAudioReceiveStreams();
+  ASSERT_EQ(receivers1.size(), 2u);
+
+  // Should remove all default streams.
+  channel_->ResetUnsignaledRecvStream();
+  const auto& receivers2 = call_.GetAudioReceiveStreams();
+  EXPECT_EQ(0u, receivers2.size());
+}
+
 // Test that receiving N unsignaled stream works (streams will be created), and
 // that packets are forwarded to them all.
 TEST_P(WebRtcVoiceEngineTestFake, RecvMultipleUnsignaled) {
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index 712449f..dd35de0 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -115,6 +115,7 @@
     "../rtc_base:rtc_task_queue",
     "../rtc_base:stringutils",
     "../rtc_base/synchronization:mutex",
+    "../rtc_base/synchronization:sequence_checker",
     "../rtc_base/system:file_wrapper",
     "../rtc_base/system:rtc_export",
     "../rtc_base/third_party/base64",
diff --git a/pc/channel.cc b/pc/channel.cc
index eeba19b..accc233 100644
--- a/pc/channel.cc
+++ b/pc/channel.cc
@@ -30,6 +30,7 @@
 #include "rtc_base/logging.h"
 #include "rtc_base/network_route.h"
 #include "rtc_base/strings/string_builder.h"
+#include "rtc_base/synchronization/sequence_checker.h"
 #include "rtc_base/trace_event.h"
 
 namespace cricket {
@@ -206,7 +207,7 @@
 }
 
 void BaseChannel::Deinit() {
-  RTC_DCHECK(worker_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(worker_thread());
   media_channel_->SetInterface(/*iface=*/nullptr);
   // Packets arrive on the network thread, processing packets calls virtual
   // functions, so need to stop this process in Deinit that is called in
@@ -289,6 +290,13 @@
       Bind(&BaseChannel::SetRemoteContent_w, this, content, type, error_desc));
 }
 
+void BaseChannel::SetPayloadTypeDemuxingEnabled(bool enabled) {
+  TRACE_EVENT0("webrtc", "BaseChannel::SetPayloadTypeDemuxingEnabled");
+  InvokeOnWorker<void>(
+      RTC_FROM_HERE,
+      Bind(&BaseChannel::SetPayloadTypeDemuxingEnabled_w, this, enabled));
+}
+
 bool BaseChannel::IsReadyToReceiveMedia_w() const {
   // Receive data if we are enabled and have local content,
   return enabled() &&
@@ -330,7 +338,7 @@
 int BaseChannel::SetOption_n(SocketType type,
                              rtc::Socket::Option opt,
                              int value) {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   RTC_DCHECK(rtp_transport_);
   switch (type) {
     case ST_RTP:
@@ -346,7 +354,7 @@
 }
 
 void BaseChannel::OnWritableState(bool writable) {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   if (writable) {
     ChannelWritable_n();
   } else {
@@ -358,7 +366,7 @@
     absl::optional<rtc::NetworkRoute> network_route) {
   RTC_LOG(LS_INFO) << "Network route for " << ToString() << " was changed.";
 
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   rtc::NetworkRoute new_route;
   if (network_route) {
     new_route = *(network_route);
@@ -479,7 +487,7 @@
 
   invoker_.AsyncInvoke<void>(
       RTC_FROM_HERE, worker_thread_, [this, packet_buffer, packet_time_us] {
-        RTC_DCHECK(worker_thread_->IsCurrent());
+        RTC_DCHECK_RUN_ON(worker_thread());
         media_channel_->OnPacketReceived(packet_buffer, packet_time_us);
       });
 }
@@ -537,7 +545,7 @@
 }
 
 void BaseChannel::ChannelWritable_n() {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   if (writable_) {
     return;
   }
@@ -551,7 +559,7 @@
 }
 
 void BaseChannel::ChannelNotWritable_n() {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   if (!writable_)
     return;
 
@@ -575,6 +583,24 @@
   media_channel()->ResetUnsignaledRecvStream();
 }
 
+void BaseChannel::SetPayloadTypeDemuxingEnabled_w(bool enabled) {
+  RTC_DCHECK_RUN_ON(worker_thread());
+  if (enabled == payload_type_demuxing_enabled_) {
+    return;
+  }
+  if (!enabled) {
+    // TODO(crbug.com/11477): This will remove *all* unsignaled streams (those
+    // without an explicitly signaled SSRC), which may include streams that
+    // were matched to this channel by MID or RID. Ideally we'd remove only the
+    // streams that were matched based on payload type alone, but currently
+    // there is no straightforward way to identify those streams.
+    media_channel()->ResetUnsignaledRecvStream();
+    ClearHandledPayloadTypes();
+    RegisterRtpDemuxerSink();
+  }
+  payload_type_demuxing_enabled_ = enabled;
+}
+
 bool BaseChannel::UpdateLocalStreams_w(const std::vector<StreamParams>& streams,
                                        SdpType type,
                                        std::string* error_desc) {
@@ -741,7 +767,7 @@
   switch (pmsg->message_id) {
     case MSG_SEND_RTP_PACKET:
     case MSG_SEND_RTCP_PACKET: {
-      RTC_DCHECK(network_thread_->IsCurrent());
+      RTC_DCHECK_RUN_ON(network_thread());
       SendPacketMessageData* data =
           static_cast<SendPacketMessageData*>(pmsg->pdata);
       bool rtcp = pmsg->message_id == MSG_SEND_RTCP_PACKET;
@@ -756,8 +782,10 @@
   }
 }
 
-void BaseChannel::AddHandledPayloadType(int payload_type) {
-  demuxer_criteria_.payload_types.insert(static_cast<uint8_t>(payload_type));
+void BaseChannel::MaybeAddHandledPayloadType(int payload_type) {
+  if (payload_type_demuxing_enabled_) {
+    demuxer_criteria_.payload_types.insert(static_cast<uint8_t>(payload_type));
+  }
 }
 
 void BaseChannel::ClearHandledPayloadTypes() {
@@ -767,7 +795,7 @@
 void BaseChannel::FlushRtcpMessages_n() {
   // Flush all remaining RTCP messages. This should only be called in
   // destructor.
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   rtc::MessageList rtcp_messages;
   network_thread_->Clear(this, MSG_SEND_RTCP_PACKET, &rtcp_messages);
   for (const auto& message : rtcp_messages) {
@@ -777,10 +805,10 @@
 }
 
 void BaseChannel::SignalSentPacket_n(const rtc::SentPacket& sent_packet) {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   invoker_.AsyncInvoke<void>(RTC_FROM_HERE, worker_thread_,
                              [this, sent_packet] {
-                               RTC_DCHECK(worker_thread_->IsCurrent());
+                               RTC_DCHECK_RUN_ON(worker_thread());
                                SignalSentPacket(sent_packet);
                              });
 }
@@ -810,7 +838,7 @@
 }
 
 void BaseChannel::UpdateMediaSendRecvState() {
-  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK_RUN_ON(network_thread());
   invoker_.AsyncInvoke<void>(RTC_FROM_HERE, worker_thread_,
                              [this] { UpdateMediaSendRecvState_w(); });
 }
@@ -869,7 +897,7 @@
 
   if (webrtc::RtpTransceiverDirectionHasRecv(audio->direction())) {
     for (const AudioCodec& codec : audio->codecs()) {
-      AddHandledPayloadType(codec.id);
+      MaybeAddHandledPayloadType(codec.id);
     }
     // Need to re-register the sink to update the handled payload.
     if (!RegisterRtpDemuxerSink()) {
@@ -1062,7 +1090,7 @@
 
   if (webrtc::RtpTransceiverDirectionHasRecv(video->direction())) {
     for (const VideoCodec& codec : video->codecs()) {
-      AddHandledPayloadType(codec.id);
+      MaybeAddHandledPayloadType(codec.id);
     }
     // Need to re-register the sink to update the handled payload.
     if (!RegisterRtpDemuxerSink()) {
@@ -1287,7 +1315,7 @@
     return false;
   }
   for (const DataCodec& codec : data->codecs()) {
-    AddHandledPayloadType(codec.id);
+    MaybeAddHandledPayloadType(codec.id);
   }
   // Need to re-register the sink to update the handled payload.
   if (!RegisterRtpDemuxerSink()) {
diff --git a/pc/channel.h b/pc/channel.h
index 44374b1..bda2da4 100644
--- a/pc/channel.h
+++ b/pc/channel.h
@@ -39,7 +39,9 @@
 #include "rtc_base/async_invoker.h"
 #include "rtc_base/async_udp_socket.h"
 #include "rtc_base/network.h"
+#include "rtc_base/synchronization/sequence_checker.h"
 #include "rtc_base/third_party/sigslot/sigslot.h"
+#include "rtc_base/thread_annotations.h"
 #include "rtc_base/unique_id_generator.h"
 
 namespace webrtc {
@@ -124,6 +126,15 @@
   bool SetRemoteContent(const MediaContentDescription* content,
                         webrtc::SdpType type,
                         std::string* error_desc) override;
+  // Controls whether this channel will receive packets on the basis of
+  // matching payload type alone. This is needed for legacy endpoints that
+  // don't signal SSRCs or use MID/RID, but doesn't make sense if there is
+  // more than channel of specific media type, As that creates an ambiguity.
+  //
+  // This method will also remove any existing streams that were bound to this
+  // channel on the basis of payload type, since one of these streams might
+  // actually belong to a new channel. See: crbug.com/webrtc/11477
+  void SetPayloadTypeDemuxingEnabled(bool enabled) override;
 
   bool Enable(bool enable) override;
 
@@ -224,6 +235,7 @@
   bool AddRecvStream_w(const StreamParams& sp);
   bool RemoveRecvStream_w(uint32_t ssrc);
   void ResetUnsignaledRecvStream_w();
+  void SetPayloadTypeDemuxingEnabled_w(bool enabled);
   bool AddSendStream_w(const StreamParams& sp);
   bool RemoveSendStream_w(uint32_t ssrc);
 
@@ -261,9 +273,11 @@
     return worker_thread_->Invoke<T>(posted_from, functor);
   }
 
-  void AddHandledPayloadType(int payload_type);
+  // Add |payload_type| to |demuxer_criteria_| if payload type demuxing is
+  // enabled.
+  void MaybeAddHandledPayloadType(int payload_type) RTC_RUN_ON(worker_thread());
 
-  void ClearHandledPayloadTypes();
+  void ClearHandledPayloadTypes() RTC_RUN_ON(worker_thread());
 
   void UpdateRtpHeaderExtensionMap(
       const RtpHeaderExtensions& header_extensions);
@@ -308,6 +322,7 @@
   // well, but it can be changed only when signaling thread does a synchronous
   // call to the worker thread, so it should be safe.
   bool enabled_ = false;
+  bool payload_type_demuxing_enabled_ RTC_GUARDED_BY(worker_thread()) = true;
   std::vector<StreamParams> local_streams_;
   std::vector<StreamParams> remote_streams_;
   webrtc::RtpTransceiverDirection local_content_direction_ =
diff --git a/pc/channel_interface.h b/pc/channel_interface.h
index cd29ed4..f510c94 100644
--- a/pc/channel_interface.h
+++ b/pc/channel_interface.h
@@ -52,6 +52,7 @@
   virtual bool SetRemoteContent(const MediaContentDescription* content,
                                 webrtc::SdpType type,
                                 std::string* error_desc) = 0;
+  virtual void SetPayloadTypeDemuxingEnabled(bool enabled) = 0;
 
   // Access to the local and remote streams that were set on the channel.
   virtual const std::vector<StreamParams>& local_streams() const = 0;
diff --git a/pc/channel_manager.h b/pc/channel_manager.h
index 8d5fc0a..ba2c260 100644
--- a/pc/channel_manager.h
+++ b/pc/channel_manager.h
@@ -96,16 +96,15 @@
   // call the appropriate Destroy*Channel method when done.
 
   // Creates a voice channel, to be associated with the specified session.
-  VoiceChannel* CreateVoiceChannel(
-      webrtc::Call* call,
-      const cricket::MediaConfig& media_config,
-      webrtc::RtpTransportInternal* rtp_transport,
-      rtc::Thread* signaling_thread,
-      const std::string& content_name,
-      bool srtp_required,
-      const webrtc::CryptoOptions& crypto_options,
-      rtc::UniqueRandomIdGenerator* ssrc_generator,
-      const AudioOptions& options);
+  VoiceChannel* CreateVoiceChannel(webrtc::Call* call,
+                                   const cricket::MediaConfig& media_config,
+                                   webrtc::RtpTransportInternal* rtp_transport,
+                                   rtc::Thread* signaling_thread,
+                                   const std::string& content_name,
+                                   bool srtp_required,
+                                   const webrtc::CryptoOptions& crypto_options,
+                                   rtc::UniqueRandomIdGenerator* ssrc_generator,
+                                   const AudioOptions& options);
   // Destroys a voice channel created by CreateVoiceChannel.
   void DestroyVoiceChannel(VoiceChannel* voice_channel);
 
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index a39d3c6c..71c9de4 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -5982,6 +5982,87 @@
   return RTCError::OK();
 }
 
+void PeerConnection::UpdatePayloadTypeDemuxingState(
+    cricket::ContentSource source) {
+  // We may need to delete any created default streams and disable creation of
+  // new ones on the basis of payload type. This is needed to avoid SSRC
+  // collisions in Call's RtpDemuxer, in the case that a transceiver has
+  // created a default stream, and then some other channel gets the SSRC
+  // signaled in the corresponding Unified Plan "m=" section. For more context
+  // see https://bugs.chromium.org/p/webrtc/issues/detail?id=11477
+  const SessionDescriptionInterface* sdesc =
+      (source == cricket::CS_LOCAL ? local_description()
+                                   : remote_description());
+  size_t num_receiving_video_transceivers = 0;
+  size_t num_receiving_audio_transceivers = 0;
+  for (auto& content_info : sdesc->description()->contents()) {
+    if (content_info.rejected ||
+        (source == cricket::ContentSource::CS_LOCAL &&
+         !RtpTransceiverDirectionHasRecv(
+             content_info.media_description()->direction())) ||
+        (source == cricket::ContentSource::CS_REMOTE &&
+         !RtpTransceiverDirectionHasSend(
+             content_info.media_description()->direction()))) {
+      // Ignore transceivers that are not receiving.
+      continue;
+    }
+    switch (content_info.media_description()->type()) {
+      case cricket::MediaType::MEDIA_TYPE_AUDIO:
+        ++num_receiving_audio_transceivers;
+        break;
+      case cricket::MediaType::MEDIA_TYPE_VIDEO:
+        ++num_receiving_video_transceivers;
+        break;
+      default:
+        // Ignore data channels.
+        continue;
+    }
+  }
+  bool pt_demuxing_enabled_video = num_receiving_video_transceivers <= 1;
+  bool pt_demuxing_enabled_audio = num_receiving_audio_transceivers <= 1;
+
+  // Gather all updates ahead of time so that all channels can be updated in a
+  // single Invoke; necessary due to thread guards.
+  std::vector<std::pair<RtpTransceiverDirection, cricket::ChannelInterface*>>
+      channels_to_update;
+  for (const auto& transceiver : transceivers_) {
+    cricket::ChannelInterface* channel = transceiver->internal()->channel();
+    const ContentInfo* content =
+        FindMediaSectionForTransceiver(transceiver, sdesc);
+    if (!channel || !content) {
+      continue;
+    }
+    RtpTransceiverDirection local_direction =
+        content->media_description()->direction();
+    if (source == cricket::CS_REMOTE) {
+      local_direction = RtpTransceiverDirectionReversed(local_direction);
+    }
+    channels_to_update.emplace_back(local_direction,
+                                    transceiver->internal()->channel());
+  }
+
+  if (!channels_to_update.empty()) {
+    worker_thread()->Invoke<void>(
+        RTC_FROM_HERE, [&channels_to_update, pt_demuxing_enabled_audio,
+                        pt_demuxing_enabled_video]() {
+          for (const auto& it : channels_to_update) {
+            RtpTransceiverDirection local_direction = it.first;
+            cricket::ChannelInterface* channel = it.second;
+            cricket::MediaType media_type = channel->media_type();
+            if (media_type == cricket::MediaType::MEDIA_TYPE_AUDIO) {
+              channel->SetPayloadTypeDemuxingEnabled(
+                  pt_demuxing_enabled_audio &&
+                  RtpTransceiverDirectionHasRecv(local_direction));
+            } else if (media_type == cricket::MediaType::MEDIA_TYPE_VIDEO) {
+              channel->SetPayloadTypeDemuxingEnabled(
+                  pt_demuxing_enabled_video &&
+                  RtpTransceiverDirectionHasRecv(local_direction));
+            }
+          }
+        });
+  }
+}
+
 RTCError PeerConnection::PushdownMediaDescription(
     SdpType type,
     cricket::ContentSource source) {
@@ -5990,6 +6071,8 @@
                                    : remote_description());
   RTC_DCHECK(sdesc);
 
+  UpdatePayloadTypeDemuxingState(source);
+
   // Push down the new SDP media section for each audio/video transceiver.
   for (const auto& transceiver : transceivers_) {
     const ContentInfo* content_info =
diff --git a/pc/peer_connection.h b/pc/peer_connection.h
index 74c4ebe..8e32608 100644
--- a/pc/peer_connection.h
+++ b/pc/peer_connection.h
@@ -936,6 +936,10 @@
   RTCError UpdateSessionState(SdpType type,
                               cricket::ContentSource source,
                               const cricket::SessionDescription* description);
+  // Based on number of transceivers per media type, enabled or disable
+  // payload type based demuxing in the affected channels.
+  void UpdatePayloadTypeDemuxingState(cricket::ContentSource source)
+      RTC_RUN_ON(signaling_thread());
   // Push the media parts of the local or remote session description
   // down to all of the channels.
   RTCError PushdownMediaDescription(SdpType type, cricket::ContentSource source)
diff --git a/pc/test/mock_channel_interface.h b/pc/test/mock_channel_interface.h
index 2df3bae..3c3a4ee 100644
--- a/pc/test/mock_channel_interface.h
+++ b/pc/test/mock_channel_interface.h
@@ -46,6 +46,7 @@
                webrtc::SdpType,
                std::string*),
               (override));
+  MOCK_METHOD(void, SetPayloadTypeDemuxingEnabled, (bool), (override));
   MOCK_METHOD(const std::vector<StreamParams>&,
               local_streams,
               (),
diff --git a/test/peer_scenario/peer_scenario_client.cc b/test/peer_scenario/peer_scenario_client.cc
index 465ee45..1ced030 100644
--- a/test/peer_scenario/peer_scenario_client.cc
+++ b/test/peer_scenario/peer_scenario_client.cc
@@ -228,6 +228,9 @@
   pcf_deps.network_state_predictor_factory = nullptr;
 
   pc_factory_ = CreateModularPeerConnectionFactory(std::move(pcf_deps));
+  PeerConnectionFactoryInterface::Options pc_options;
+  pc_options.disable_encryption = config.disable_encryption;
+  pc_factory_->SetOptions(pc_options);
 
   PeerConnectionDependencies pc_deps(observer_.get());
   pc_deps.allocator =
@@ -285,14 +288,17 @@
 }
 
 void PeerScenarioClient::CreateAndSetSdp(
+    std::function<void(SessionDescriptionInterface*)> munge_offer,
     std::function<void(std::string)> offer_handler) {
   RTC_DCHECK_RUN_ON(signaling_thread_);
   peer_connection_->CreateOffer(
       SdpCreateObserver([=](SessionDescriptionInterface* offer) {
         RTC_DCHECK_RUN_ON(signaling_thread_);
+        if (munge_offer) {
+          munge_offer(offer);
+        }
         std::string sdp_offer;
-        offer->ToString(&sdp_offer);
-        RTC_LOG(LS_INFO) << sdp_offer;
+        RTC_CHECK(offer->ToString(&sdp_offer));
         peer_connection_->SetLocalDescription(
             SdpSetObserver(
                 [sdp_offer, offer_handler]() { offer_handler(sdp_offer); }),
diff --git a/test/peer_scenario/peer_scenario_client.h b/test/peer_scenario/peer_scenario_client.h
index d939d7f..65ad073 100644
--- a/test/peer_scenario/peer_scenario_client.h
+++ b/test/peer_scenario/peer_scenario_client.h
@@ -89,6 +89,7 @@
         {0, EmulatedEndpointConfig()}};
     CallbackHandlers handlers;
     PeerConnectionInterface::RTCConfiguration rtc_config;
+    bool disable_encryption = false;
     Config() { rtc_config.sdp_semantics = SdpSemantics::kUnifiedPlan; }
   };
 
@@ -136,9 +137,13 @@
 
   CallbackHandlers* handlers() { return &handlers_; }
 
-  // Note that there's no provision for munging SDP as that is deprecated
-  // behavior.
-  void CreateAndSetSdp(std::function<void(std::string)> offer_handler);
+  // The |munge_offer| function can be used to munge the SDP, i.e. modify a
+  // local description afer creating it but before setting it. Note that this is
+  // legacy behavior. It's added here only to be able to have test coverage for
+  // scenarios even if they are not spec compliant.
+  void CreateAndSetSdp(
+      std::function<void(SessionDescriptionInterface*)> munge_offer,
+      std::function<void(std::string)> offer_handler);
   void SetSdpOfferAndGetAnswer(std::string remote_offer,
                                std::function<void(std::string)> answer_handler);
   void SetSdpAnswer(
diff --git a/test/peer_scenario/signaling_route.cc b/test/peer_scenario/signaling_route.cc
index 1e5b9aa..2e0213d 100644
--- a/test/peer_scenario/signaling_route.cc
+++ b/test/peer_scenario/signaling_route.cc
@@ -58,9 +58,10 @@
     PeerScenarioClient* callee,
     TrafficRoute* send_route,
     TrafficRoute* ret_route,
+    std::function<void(SessionDescriptionInterface* offer)> munge_offer,
     std::function<void(SessionDescriptionInterface*)> modify_offer,
     std::function<void(const SessionDescriptionInterface&)> exchange_finished) {
-  caller->CreateAndSetSdp([=](std::string sdp_offer) {
+  caller->CreateAndSetSdp(munge_offer, [=](std::string sdp_offer) {
     if (modify_offer) {
       auto offer = CreateSessionDescription(SdpType::kOffer, sdp_offer);
       modify_offer(offer.get());
@@ -92,15 +93,22 @@
 }
 
 void SignalingRoute::NegotiateSdp(
+    std::function<void(SessionDescriptionInterface*)> munge_offer,
     std::function<void(SessionDescriptionInterface*)> modify_offer,
     std::function<void(const SessionDescriptionInterface&)> exchange_finished) {
-  StartSdpNegotiation(caller_, callee_, send_route_, ret_route_, modify_offer,
-                      exchange_finished);
+  StartSdpNegotiation(caller_, callee_, send_route_, ret_route_, munge_offer,
+                      modify_offer, exchange_finished);
+}
+
+void SignalingRoute::NegotiateSdp(
+    std::function<void(SessionDescriptionInterface*)> modify_offer,
+    std::function<void(const SessionDescriptionInterface&)> exchange_finished) {
+  NegotiateSdp({}, modify_offer, exchange_finished);
 }
 
 void SignalingRoute::NegotiateSdp(
     std::function<void(const SessionDescriptionInterface&)> exchange_finished) {
-  NegotiateSdp({}, exchange_finished);
+  NegotiateSdp({}, {}, exchange_finished);
 }
 
 }  // namespace test
diff --git a/test/peer_scenario/signaling_route.h b/test/peer_scenario/signaling_route.h
index 189c4b6..7434551 100644
--- a/test/peer_scenario/signaling_route.h
+++ b/test/peer_scenario/signaling_route.h
@@ -30,8 +30,20 @@
 
   void StartIceSignaling();
 
+  // The |modify_offer| callback is used to modify an offer after the local
+  // description has been set. This is legal (but odd) behavior.
+  // The |munge_offer| callback is used to modify an offer between its creation
+  // and set local description. This behavior is forbidden according to the spec
+  // but available here in order to allow test coverage on corner cases.
+  // The |exchange_finished| callback is called with the answer produced after
+  // SDP negotations has completed.
   // TODO(srte): Handle lossy links.
   void NegotiateSdp(
+      std::function<void(SessionDescriptionInterface* offer)> munge_offer,
+      std::function<void(SessionDescriptionInterface* offer)> modify_offer,
+      std::function<void(const SessionDescriptionInterface& answer)>
+          exchange_finished);
+  void NegotiateSdp(
       std::function<void(SessionDescriptionInterface* offer)> modify_offer,
       std::function<void(const SessionDescriptionInterface& answer)>
           exchange_finished);
diff --git a/test/peer_scenario/tests/BUILD.gn b/test/peer_scenario/tests/BUILD.gn
index 3552862..0cf7cf3 100644
--- a/test/peer_scenario/tests/BUILD.gn
+++ b/test/peer_scenario/tests/BUILD.gn
@@ -14,12 +14,16 @@
     sources = [
       "peer_scenario_quality_test.cc",
       "remote_estimate_test.cc",
+      "unsignaled_stream_test.cc",
     ]
     deps = [
       "..:peer_scenario",
       "../../:field_trial",
+      "../../:rtp_test_utils",
       "../../:test_support",
+      "../../../media:rtc_media_base",
       "../../../modules/rtp_rtcp:rtp_rtcp",
+      "../../../modules/rtp_rtcp:rtp_rtcp_format",
       "../../../pc:rtc_pc_base",
     ]
   }
diff --git a/test/peer_scenario/tests/unsignaled_stream_test.cc b/test/peer_scenario/tests/unsignaled_stream_test.cc
new file mode 100644
index 0000000..5f470a8
--- /dev/null
+++ b/test/peer_scenario/tests/unsignaled_stream_test.cc
@@ -0,0 +1,157 @@
+/*
+ *  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 "pc/media_session.h"
+#include "pc/session_description.h"
+#include "test/field_trial.h"
+#include "test/peer_scenario/peer_scenario.h"
+#include "test/rtp_header_parser.h"
+
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+class FrameObserver : public rtc::VideoSinkInterface<VideoFrame> {
+ public:
+  FrameObserver() : frame_observed_(false) {}
+  void OnFrame(const VideoFrame&) override { frame_observed_ = true; }
+
+  std::atomic<bool> 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
+
+TEST(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.
+
+  // Defined before PeerScenario so it gets destructed after, to avoid use after
+  // free.
+  PeerScenario s(*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<bool> offer_exchange_done(false);
+  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).
+  uint32_t first_ssrc = 0;
+  uint32_t second_ssrc = 0;
+
+  signaling.NegotiateSdp(
+      /* munge_sdp = */ {},
+      /* modify_sdp = */
+      [&](SessionDescriptionInterface* offer) {
+        first_ssrc = get_ssrc(offer, 0);
+        second_ssrc = first_ssrc + 1;
+
+        send_node->router()->SetWatcher([&](const EmulatedIpPacket& packet) {
+          if (packet.size() > 1 && packet.cdata()[0] >> 6 == 2 &&
+              !RtpHeaderParser::IsRtcp(packet.data.cdata(),
+                                       packet.data.size())) {
+            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.data()[8],
+                                                   second_ssrc);
+              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_));
+}
+
+}  // namespace test
+}  // namespace webrtc