Add an alt-protocol to SDP to indicate which m= sections use a plugin transport.

The plugin transport parameters (a=x-opaque: lines) relate to how to create and
set up a plugin transport.  When SDP bundle is used, the x-opaque line needs to
be copied into the bundled m= section.  This means x-opaque can appear on a
section even if the offerer does not intend to use the transport for the media
described by that section.  Consequently, the answerer cannot currently tell
whether the caller is offering an alternate transport for media, data, or both.

This change adds an a=x-alt-protocol: line to SDP.  The value following this
line matches the <protocol> part of the x-opaque:<protocol>:<params> line.
However, alt-protocol is not bundled--it only ever applies to the m= section
that contains the line.  This allows the offerer to express which m= sections
should actually use an alternate transport, even in the case of bundle.

Note that this is still limited by the available configuration options:
datagram transport can be used for media (audio + video) and/or data.  It is
still not possible to use it for audio but not video, or vice versa.

PeerConnection places an alt-protocol line in each media (audio/video) m=
section if it is configured to use a datagram transport for media.  It places
an alt-protocol line in each data m= section if it is configured to use a
datagram transport for data channels.  PeerConnection leaves alt-protocol in
media (audio/video) m= sections of the answer if it is configured to use a
datagram transport for media, and in data m= sections of the answer if it is
configured to use a datagram transport for data channels.

JsepTransport now negotiates use of the datagram transport independently for
media and data channels.  It only uses it for media if the m= sections for
bundled audio/video have an alt-protocol line matching the x-opaque protocol,
and only uses it for data channels if a bundled m= section for data has an
alt-protocol line matching the x-opaque protocol.

Bug: webrtc:9719
Change-Id: I773e4fc10c57d815afcd76a2a74da38dd0c52b3b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/154763
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Commit-Queue: Bjorn Mellem <mellem@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#29351}
diff --git a/api/test/loopback_media_transport.cc b/api/test/loopback_media_transport.cc
index e341a38..f1bce1c 100644
--- a/api/test/loopback_media_transport.cc
+++ b/api/test/loopback_media_transport.cc
@@ -19,6 +19,8 @@
 
 namespace {
 
+constexpr size_t kLoopbackMaxDatagramSize = 1200;
+
 // Wrapper used to hand out unique_ptrs to loopback media transports without
 // ownership changes.
 class WrapperMediaTransport : public MediaTransportInterface {
@@ -611,10 +613,11 @@
 
 MediaTransportPair::LoopbackDatagramTransport::LoopbackDatagramTransport(
     rtc::Thread* thread)
-    : dc_transport_(thread) {}
+    : thread_(thread), dc_transport_(thread) {}
 
 void MediaTransportPair::LoopbackDatagramTransport::Connect(
     LoopbackDatagramTransport* other) {
+  other_ = other;
   dc_transport_.Connect(&other->dc_transport_);
 }
 
@@ -631,21 +634,43 @@
 }
 
 void MediaTransportPair::LoopbackDatagramTransport::SetTransportStateCallback(
-    MediaTransportStateCallback* callback) {}
+    MediaTransportStateCallback* callback) {
+  RTC_DCHECK_RUN_ON(thread_);
+  state_callback_ = callback;
+  if (state_callback_) {
+    state_callback_->OnStateChanged(state_);
+  }
+}
 
 RTCError MediaTransportPair::LoopbackDatagramTransport::SendDatagram(
     rtc::ArrayView<const uint8_t> data,
     DatagramId datagram_id) {
+  rtc::CopyOnWriteBuffer buffer;
+  buffer.SetData(data.data(), data.size());
+  invoker_.AsyncInvoke<void>(
+      RTC_FROM_HERE, thread_, [this, datagram_id, buffer = std::move(buffer)] {
+        RTC_DCHECK_RUN_ON(thread_);
+        other_->DeliverDatagram(std::move(buffer));
+        if (sink_) {
+          DatagramAck ack;
+          ack.datagram_id = datagram_id;
+          ack.receive_timestamp = Timestamp::us(rtc::TimeMicros());
+          sink_->OnDatagramAcked(ack);
+        }
+      });
   return RTCError::OK();
 }
 
 size_t MediaTransportPair::LoopbackDatagramTransport::GetLargestDatagramSize()
     const {
-  return 0;
+  return kLoopbackMaxDatagramSize;
 }
 
 void MediaTransportPair::LoopbackDatagramTransport::SetDatagramSink(
-    DatagramSinkInterface* sink) {}
+    DatagramSinkInterface* sink) {
+  RTC_DCHECK_RUN_ON(thread_);
+  sink_ = sink;
+}
 
 std::string
 MediaTransportPair::LoopbackDatagramTransport::GetTransportParameters() const {
@@ -680,6 +705,13 @@
 
 void MediaTransportPair::LoopbackDatagramTransport::SetState(
     MediaTransportState state) {
+  invoker_.AsyncInvoke<void>(RTC_FROM_HERE, thread_, [this, state] {
+    RTC_DCHECK_RUN_ON(thread_);
+    state_ = state;
+    if (state_callback_) {
+      state_callback_->OnStateChanged(state_);
+    }
+  });
   dc_transport_.OnReadyToSend(state == MediaTransportState::kWritable);
 }
 
@@ -692,4 +724,12 @@
   dc_transport_.FlushAsyncInvokes();
 }
 
+void MediaTransportPair::LoopbackDatagramTransport::DeliverDatagram(
+    rtc::CopyOnWriteBuffer buffer) {
+  RTC_DCHECK_RUN_ON(thread_);
+  if (sink_) {
+    sink_->OnDatagramReceived(buffer);
+  }
+}
+
 }  // namespace webrtc
diff --git a/api/test/loopback_media_transport.h b/api/test/loopback_media_transport.h
index bacfd5e..475c586 100644
--- a/api/test/loopback_media_transport.h
+++ b/api/test/loopback_media_transport.h
@@ -300,7 +300,6 @@
     void Connect(LoopbackDatagramTransport* other);
 
     // Datagram transport overrides.
-    // TODO(mellem):  Implement these when tests actually need to use them.
     void Connect(rtc::PacketTransportInternal* packet_transport) override;
     CongestionControlInterface* congestion_control() override;
     void SetTransportStateCallback(
@@ -333,11 +332,23 @@
     }
 
    private:
+    void DeliverDatagram(rtc::CopyOnWriteBuffer buffer);
+
+    rtc::Thread* thread_;
     LoopbackDataChannelTransport dc_transport_;
 
+    MediaTransportState state_ RTC_GUARDED_BY(thread_) =
+        MediaTransportState::kPending;
+    DatagramSinkInterface* sink_ RTC_GUARDED_BY(thread_) = nullptr;
+    MediaTransportStateCallback* state_callback_ RTC_GUARDED_BY(thread_) =
+        nullptr;
+    LoopbackDatagramTransport* other_;
+
     std::string transport_parameters_;
 
     absl::optional<MediaTransportState> state_after_connect_;
+
+    rtc::AsyncInvoker invoker_;
   };
 
   LoopbackMediaTransport first_;
diff --git a/pc/jsep_transport.cc b/pc/jsep_transport.cc
index 7c83e85..ca44ec8 100644
--- a/pc/jsep_transport.cc
+++ b/pc/jsep_transport.cc
@@ -60,12 +60,16 @@
     const std::vector<CryptoParams>& cryptos,
     const std::vector<int>& encrypted_header_extension_ids,
     int rtp_abs_sendtime_extn_id,
-    const TransportDescription& transport_desc)
+    const TransportDescription& transport_desc,
+    absl::optional<std::string> media_alt_protocol,
+    absl::optional<std::string> data_alt_protocol)
     : rtcp_mux_enabled(rtcp_mux_enabled),
       cryptos(cryptos),
       encrypted_header_extension_ids(encrypted_header_extension_ids),
       rtp_abs_sendtime_extn_id(rtp_abs_sendtime_extn_id),
-      transport_desc(transport_desc) {}
+      transport_desc(transport_desc),
+      media_alt_protocol(media_alt_protocol),
+      data_alt_protocol(data_alt_protocol) {}
 
 JsepTransportDescription::JsepTransportDescription(
     const JsepTransportDescription& from)
@@ -73,7 +77,9 @@
       cryptos(from.cryptos),
       encrypted_header_extension_ids(from.encrypted_header_extension_ids),
       rtp_abs_sendtime_extn_id(from.rtp_abs_sendtime_extn_id),
-      transport_desc(from.transport_desc) {}
+      transport_desc(from.transport_desc),
+      media_alt_protocol(from.media_alt_protocol),
+      data_alt_protocol(from.data_alt_protocol) {}
 
 JsepTransportDescription::~JsepTransportDescription() = default;
 
@@ -87,6 +93,8 @@
   encrypted_header_extension_ids = from.encrypted_header_extension_ids;
   rtp_abs_sendtime_extn_id = from.rtp_abs_sendtime_extn_id;
   transport_desc = from.transport_desc;
+  media_alt_protocol = from.media_alt_protocol;
+  data_alt_protocol = from.data_alt_protocol;
 
   return *this;
 }
@@ -794,34 +802,50 @@
     return;  // No need to negotiate the use of datagram transport.
   }
 
-  bool use_datagram_transport =
+  bool compatible_datagram_transport =
       remote_description_->transport_desc.opaque_parameters &&
       remote_description_->transport_desc.opaque_parameters ==
           local_description_->transport_desc.opaque_parameters;
 
-  RTC_LOG(LS_INFO) << "Negotiating datagram transport, use_datagram_transport="
-                   << use_datagram_transport << " answer type="
-                   << (type == SdpType::kAnswer ? "answer" : "pr_answer");
+  bool use_datagram_transport_for_media =
+      compatible_datagram_transport &&
+      remote_description_->media_alt_protocol ==
+          remote_description_->transport_desc.opaque_parameters->protocol &&
+      remote_description_->media_alt_protocol ==
+          local_description_->media_alt_protocol;
+
+  bool use_datagram_transport_for_data =
+      compatible_datagram_transport &&
+      remote_description_->data_alt_protocol ==
+          remote_description_->transport_desc.opaque_parameters->protocol &&
+      remote_description_->data_alt_protocol ==
+          local_description_->data_alt_protocol;
+
+  RTC_LOG(LS_INFO)
+      << "Negotiating datagram transport, use_datagram_transport_for_media="
+      << use_datagram_transport_for_media
+      << ", use_datagram_transport_for_data=" << use_datagram_transport_for_data
+      << " answer type=" << (type == SdpType::kAnswer ? "answer" : "pr_answer");
 
   // A provisional or full or answer lets the peer start sending on one of the
   // transports.
   if (composite_rtp_transport_) {
     composite_rtp_transport_->SetSendTransport(
-        use_datagram_transport ? datagram_rtp_transport_.get()
-                               : default_rtp_transport());
+        use_datagram_transport_for_media ? datagram_rtp_transport_.get()
+                                         : default_rtp_transport());
   }
   if (composite_data_channel_transport_) {
     composite_data_channel_transport_->SetSendTransport(
-        use_datagram_transport ? data_channel_transport_
-                               : sctp_data_channel_transport_.get());
+        use_datagram_transport_for_data ? data_channel_transport_
+                                        : sctp_data_channel_transport_.get());
   }
 
   if (type != SdpType::kAnswer) {
     return;
   }
 
-  if (use_datagram_transport) {
-    if (composite_rtp_transport_) {
+  if (composite_rtp_transport_) {
+    if (use_datagram_transport_for_media) {
       // Negotiated use of datagram transport for RTP, so remove the
       // non-datagram RTP transport.
       composite_rtp_transport_->RemoveTransport(default_rtp_transport());
@@ -832,30 +856,34 @@
       } else {
         dtls_srtp_transport_ = nullptr;
       }
+    } else {
+      composite_rtp_transport_->RemoveTransport(datagram_rtp_transport_.get());
+      datagram_rtp_transport_ = nullptr;
     }
-    if (composite_data_channel_transport_) {
+  }
+
+  if (composite_data_channel_transport_) {
+    if (use_datagram_transport_for_data) {
       // Negotiated use of datagram transport for data channels, so remove the
       // non-datagram data channel transport.
       composite_data_channel_transport_->RemoveTransport(
           sctp_data_channel_transport_.get());
       sctp_data_channel_transport_ = nullptr;
       sctp_transport_ = nullptr;
-    }
-  } else {
-    // Remove and delete the datagram transport.
-    if (composite_rtp_transport_) {
-      composite_rtp_transport_->RemoveTransport(datagram_rtp_transport_.get());
-    }
-    if (composite_data_channel_transport_) {
+    } else {
       composite_data_channel_transport_->RemoveTransport(
           data_channel_transport_);
-    } else {
-      // If there's no composite data channel transport, we need to signal that
-      // the data channel is about to be deleted.
-      SignalDataChannelTransportNegotiated(this, nullptr);
+      data_channel_transport_ = nullptr;
     }
-    datagram_rtp_transport_ = nullptr;
+  } else if (data_channel_transport_ && !use_datagram_transport_for_data) {
+    // The datagram transport has been rejected without a fallback.  We still
+    // need to inform the application and delete it.
+    SignalDataChannelTransportNegotiated(this, nullptr);
     data_channel_transport_ = nullptr;
+  }
+
+  if (!use_datagram_transport_for_media && !use_datagram_transport_for_data) {
+    // Datagram transport is not being used for anything, so clean it up.
     datagram_transport_ = nullptr;
   }
 }
diff --git a/pc/jsep_transport.h b/pc/jsep_transport.h
index b6199f8..3c63c47 100644
--- a/pc/jsep_transport.h
+++ b/pc/jsep_transport.h
@@ -55,7 +55,9 @@
       const std::vector<CryptoParams>& cryptos,
       const std::vector<int>& encrypted_header_extension_ids,
       int rtp_abs_sendtime_extn_id,
-      const TransportDescription& transport_description);
+      const TransportDescription& transport_description,
+      absl::optional<std::string> media_alt_protocol,
+      absl::optional<std::string> data_alt_protocol);
   JsepTransportDescription(const JsepTransportDescription& from);
   ~JsepTransportDescription();
 
@@ -68,6 +70,14 @@
   // TODO(zhihuang): Add the ICE and DTLS related variables and methods from
   // TransportDescription and remove this extra layer of abstraction.
   TransportDescription transport_desc;
+
+  // Alt-protocols that apply to this JsepTransport.  Presence indicates a
+  // request to use an alternative protocol for media and/or data.  The
+  // alt-protocol is handled by a datagram transport.  If one or both of these
+  // values are present, JsepTransport will attempt to negotiate use of the
+  // datagram transport for media and/or data.
+  absl::optional<std::string> media_alt_protocol;
+  absl::optional<std::string> data_alt_protocol;
 };
 
 // Helper class used by JsepTransportController that processes
diff --git a/pc/jsep_transport_controller.cc b/pc/jsep_transport_controller.cc
index c9ff0a7..52ae53c 100644
--- a/pc/jsep_transport_controller.cc
+++ b/pc/jsep_transport_controller.cc
@@ -644,9 +644,16 @@
   }
 
   std::vector<int> merged_encrypted_extension_ids;
+  absl::optional<std::string> bundle_media_alt_protocol;
+  absl::optional<std::string> bundle_data_alt_protocol;
   if (bundle_group_) {
     merged_encrypted_extension_ids =
         MergeEncryptedHeaderExtensionIdsForBundle(description);
+    error = GetAltProtocolsForBundle(description, &bundle_media_alt_protocol,
+                                     &bundle_data_alt_protocol);
+    if (!error.ok()) {
+      return error;
+    }
   }
 
   for (const cricket::ContentInfo& content_info : description->contents()) {
@@ -665,6 +672,8 @@
              description->transport_infos().size());
   for (size_t i = 0; i < description->contents().size(); ++i) {
     const cricket::ContentInfo& content_info = description->contents()[i];
+    const cricket::MediaContentDescription* media_description =
+        content_info.media_description();
     const cricket::TransportInfo& transport_info =
         description->transport_infos()[i];
     if (content_info.rejected) {
@@ -686,10 +695,23 @@
     }
 
     std::vector<int> extension_ids;
+    absl::optional<std::string> media_alt_protocol;
+    absl::optional<std::string> data_alt_protocol;
     if (bundled_mid() && content_info.name == *bundled_mid()) {
       extension_ids = merged_encrypted_extension_ids;
+      media_alt_protocol = bundle_media_alt_protocol;
+      data_alt_protocol = bundle_data_alt_protocol;
     } else {
       extension_ids = GetEncryptedHeaderExtensionIds(content_info);
+      switch (media_description->type()) {
+        case cricket::MEDIA_TYPE_AUDIO:
+        case cricket::MEDIA_TYPE_VIDEO:
+          media_alt_protocol = media_description->alt_protocol();
+          break;
+        case cricket::MEDIA_TYPE_DATA:
+          data_alt_protocol = media_description->alt_protocol();
+          break;
+      }
     }
 
     int rtp_abs_sendtime_extn_id =
@@ -703,7 +725,8 @@
 
     cricket::JsepTransportDescription jsep_description =
         CreateJsepTransportDescription(content_info, transport_info,
-                                       extension_ids, rtp_abs_sendtime_extn_id);
+                                       extension_ids, rtp_abs_sendtime_extn_id,
+                                       media_alt_protocol, data_alt_protocol);
     if (local) {
       error =
           transport->SetLocalJsepTransportDescription(jsep_description, type);
@@ -896,7 +919,9 @@
     const cricket::ContentInfo& content_info,
     const cricket::TransportInfo& transport_info,
     const std::vector<int>& encrypted_extension_ids,
-    int rtp_abs_sendtime_extn_id) {
+    int rtp_abs_sendtime_extn_id,
+    absl::optional<std::string> media_alt_protocol,
+    absl::optional<std::string> data_alt_protocol) {
   const cricket::MediaContentDescription* content_desc =
       content_info.media_description();
   RTC_DCHECK(content_desc);
@@ -906,7 +931,8 @@
 
   return cricket::JsepTransportDescription(
       rtcp_mux_enabled, content_desc->cryptos(), encrypted_extension_ids,
-      rtp_abs_sendtime_extn_id, transport_info.description);
+      rtp_abs_sendtime_extn_id, transport_info.description, media_alt_protocol,
+      data_alt_protocol);
 }
 
 bool JsepTransportController::ShouldUpdateBundleGroup(
@@ -972,6 +998,55 @@
   return merged_ids;
 }
 
+RTCError JsepTransportController::GetAltProtocolsForBundle(
+    const cricket::SessionDescription* description,
+    absl::optional<std::string>* media_alt_protocol,
+    absl::optional<std::string>* data_alt_protocol) {
+  RTC_DCHECK(description);
+  RTC_DCHECK(bundle_group_);
+  RTC_DCHECK(media_alt_protocol);
+  RTC_DCHECK(data_alt_protocol);
+
+  bool found_media = false;
+  bool found_data = false;
+  for (const cricket::ContentInfo& content : description->contents()) {
+    if (bundle_group_->HasContentName(content.name)) {
+      const cricket::MediaContentDescription* media_description =
+          content.media_description();
+      switch (media_description->type()) {
+        case cricket::MEDIA_TYPE_AUDIO:
+        case cricket::MEDIA_TYPE_VIDEO:
+          if (found_media &&
+              *media_alt_protocol != media_description->alt_protocol()) {
+            return RTCError(RTCErrorType::INVALID_PARAMETER,
+                            "The BUNDLE group contains conflicting "
+                            "alt-protocols for media ('" +
+                                media_alt_protocol->value_or("") + "' and '" +
+                                media_description->alt_protocol().value_or("") +
+                                "')");
+          }
+          found_media = true;
+          *media_alt_protocol = media_description->alt_protocol();
+          break;
+        case cricket::MEDIA_TYPE_DATA:
+          if (found_data &&
+              *data_alt_protocol != media_description->alt_protocol()) {
+            return RTCError(RTCErrorType::INVALID_PARAMETER,
+                            "The BUNDLE group contains conflicting "
+                            "alt-protocols for data ('" +
+                                data_alt_protocol->value_or("") + "' and '" +
+                                media_description->alt_protocol().value_or("") +
+                                "')");
+          }
+          found_data = true;
+          *data_alt_protocol = media_description->alt_protocol();
+          break;
+      }
+    }
+  }
+  return RTCError::OK();
+}
+
 int JsepTransportController::GetRtpAbsSendTimeHeaderExtensionId(
     const cricket::ContentInfo& content_info) {
   if (!config_.enable_external_auth) {
diff --git a/pc/jsep_transport_controller.h b/pc/jsep_transport_controller.h
index af3c82c..a46a71e 100644
--- a/pc/jsep_transport_controller.h
+++ b/pc/jsep_transport_controller.h
@@ -308,7 +308,9 @@
       const cricket::ContentInfo& content_info,
       const cricket::TransportInfo& transport_info,
       const std::vector<int>& encrypted_extension_ids,
-      int rtp_abs_sendtime_extn_id);
+      int rtp_abs_sendtime_extn_id,
+      absl::optional<std::string> media_alt_protocol,
+      absl::optional<std::string> data_alt_protocol);
 
   absl::optional<std::string> bundled_mid() const {
     absl::optional<std::string> bundled_mid;
@@ -330,6 +332,12 @@
   std::vector<int> GetEncryptedHeaderExtensionIds(
       const cricket::ContentInfo& content_info);
 
+  // Extracts the alt-protocol settings that apply to the bundle group.
+  RTCError GetAltProtocolsForBundle(
+      const cricket::SessionDescription* description,
+      absl::optional<std::string>* media_alt_protocol,
+      absl::optional<std::string>* data_alt_protocol);
+
   int GetRtpAbsSendTimeHeaderExtensionId(
       const cricket::ContentInfo& content_info);
 
diff --git a/pc/jsep_transport_controller_unittest.cc b/pc/jsep_transport_controller_unittest.cc
index 8461e86..408cb01 100644
--- a/pc/jsep_transport_controller_unittest.cc
+++ b/pc/jsep_transport_controller_unittest.cc
@@ -136,6 +136,21 @@
     return description;
   }
 
+  std::unique_ptr<cricket::SessionDescription>
+  CreateSessionDescriptionWithBundledData() {
+    auto description = CreateSessionDescriptionWithoutBundle();
+    AddDataSection(description.get(), kDataMid1,
+                   cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+                   cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                   nullptr);
+    cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+    bundle_group.AddContentName(kAudioMid1);
+    bundle_group.AddContentName(kVideoMid1);
+    bundle_group.AddContentName(kDataMid1);
+    description->AddGroup(bundle_group);
+    return description;
+  }
+
   void AddAudioSection(cricket::SessionDescription* description,
                        const std::string& mid,
                        const std::string& ufrag,
@@ -474,13 +489,19 @@
   config.use_datagram_transport_for_data_channels = true;
   CreateJsepTransportController(config);
 
-  auto description = CreateSessionDescriptionWithBundleGroup();
+  auto description = CreateSessionDescriptionWithBundledData();
   AddCryptoSettings(description.get());
+
   absl::optional<cricket::OpaqueTransportParameters> params =
       transport_controller_->GetTransportParameters(kAudioMid1);
   for (auto& info : description->transport_infos()) {
     info.description.opaque_parameters = params;
   }
+  for (cricket::ContentInfo& content_info : description->contents()) {
+    if (content_info.media_description()->type() == cricket::MEDIA_TYPE_DATA) {
+      content_info.media_description()->set_alt_protocol(params->protocol);
+    }
+  }
 
   EXPECT_TRUE(transport_controller_
                   ->SetLocalDescription(SdpType::kOffer, description.get())
@@ -513,6 +534,40 @@
                    ->IsWritable(/*rtcp=*/false));
 }
 
+// An offer that bundles different alt-protocols should be rejected.
+TEST_F(JsepTransportControllerTest, CannotBundleDifferentAltProtocols) {
+  FakeMediaTransportFactory fake_media_transport_factory("transport_params");
+  JsepTransportController::Config config;
+  config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxBundle;
+  config.media_transport_factory = &fake_media_transport_factory;
+  config.use_datagram_transport = true;
+  config.use_datagram_transport_for_data_channels = true;
+  CreateJsepTransportController(config);
+
+  auto description = CreateSessionDescriptionWithBundledData();
+  AddCryptoSettings(description.get());
+
+  absl::optional<cricket::OpaqueTransportParameters> params =
+      transport_controller_->GetTransportParameters(kAudioMid1);
+  for (auto& info : description->transport_infos()) {
+    info.description.opaque_parameters = params;
+  }
+
+  // Append a different alt-protocol to each of the sections.
+  for (cricket::ContentInfo& content_info : description->contents()) {
+    content_info.media_description()->set_alt_protocol(params->protocol + "-" +
+                                                       content_info.name);
+  }
+
+  EXPECT_FALSE(transport_controller_
+                   ->SetLocalDescription(SdpType::kOffer, description.get())
+                   .ok());
+  EXPECT_FALSE(transport_controller_
+                   ->SetRemoteDescription(SdpType::kAnswer, description.get())
+                   .ok());
+}
+
 TEST_F(JsepTransportControllerTest, GetMediaTransportInCaller) {
   FakeMediaTransportFactory fake_media_transport_factory;
   JsepTransportController::Config config;
@@ -2214,6 +2269,12 @@
     for (auto& info : description->transport_infos()) {
       info.description.opaque_parameters = transport_params;
     }
+    if (transport_params) {
+      for (auto& content_info : description->contents()) {
+        content_info.media_description()->set_alt_protocol(
+            transport_params->protocol);
+      }
+    }
     return description;
   }
 
diff --git a/pc/media_session.cc b/pc/media_session.cc
index ff9c17b..dd5a814 100644
--- a/pc/media_session.cc
+++ b/pc/media_session.cc
@@ -661,6 +661,8 @@
     }
   }
 
+  offer->set_alt_protocol(media_description_options.alt_protocol);
+
   if (secure_policy == SEC_REQUIRED && offer->cryptos().empty()) {
     return false;
   }
@@ -1179,6 +1181,10 @@
 
   answer->set_direction(NegotiateRtpTransceiverDirection(
       offer->direction(), media_description_options.direction));
+
+  if (offer->alt_protocol() == media_description_options.alt_protocol) {
+    answer->set_alt_protocol(media_description_options.alt_protocol);
+  }
   return true;
 }
 
diff --git a/pc/media_session.h b/pc/media_session.h
index 1de8ed4..f91729a 100644
--- a/pc/media_session.h
+++ b/pc/media_session.h
@@ -78,6 +78,7 @@
   // stream information goes in the local descriptions.
   std::vector<SenderOptions> sender_options;
   std::vector<webrtc::RtpCodecCapability> codec_preferences;
+  absl::optional<std::string> alt_protocol;
 
  private:
   // Doesn't DCHECK on |type|.
diff --git a/pc/media_session_unittest.cc b/pc/media_session_unittest.cc
index e3778d6..d2feb1f 100644
--- a/pc/media_session_unittest.cc
+++ b/pc/media_session_unittest.cc
@@ -3486,6 +3486,124 @@
   TestTransportInfo(/*offer=*/false, options, /*has_current_desc=*/false);
 }
 
+TEST_F(MediaSessionDescriptionFactoryTest, AltProtocolAddedToOffer) {
+  MediaSessionOptions options;
+  AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options);
+  AddDataSection(cricket::DCT_RTP, RtpTransceiverDirection::kRecvOnly,
+                 &options);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "baz";
+
+  std::unique_ptr<SessionDescription> offer = f1_.CreateOffer(options, nullptr);
+
+  EXPECT_EQ(offer->GetContentDescriptionByName("audio")->alt_protocol(), "foo");
+  EXPECT_EQ(offer->GetContentDescriptionByName("video")->alt_protocol(), "bar");
+  EXPECT_EQ(offer->GetContentDescriptionByName("data")->alt_protocol(), "baz");
+}
+
+TEST_F(MediaSessionDescriptionFactoryTest, AltProtocolAddedToAnswer) {
+  MediaSessionOptions options;
+  AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options);
+  AddDataSection(cricket::DCT_SCTP, RtpTransceiverDirection::kRecvOnly,
+                 &options);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "baz";
+
+  std::unique_ptr<SessionDescription> offer = f1_.CreateOffer(options, nullptr);
+  std::unique_ptr<SessionDescription> answer =
+      f1_.CreateAnswer(offer.get(), options, nullptr);
+
+  EXPECT_EQ(answer->GetContentDescriptionByName("audio")->alt_protocol(),
+            "foo");
+  EXPECT_EQ(answer->GetContentDescriptionByName("video")->alt_protocol(),
+            "bar");
+  EXPECT_EQ(answer->GetContentDescriptionByName("data")->alt_protocol(), "baz");
+}
+
+TEST_F(MediaSessionDescriptionFactoryTest, AltProtocolNotInOffer) {
+  MediaSessionOptions options;
+  AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options);
+  AddDataSection(cricket::DCT_SCTP, RtpTransceiverDirection::kRecvOnly,
+                 &options);
+
+  std::unique_ptr<SessionDescription> offer = f1_.CreateOffer(options, nullptr);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "baz";
+
+  std::unique_ptr<SessionDescription> answer =
+      f1_.CreateAnswer(offer.get(), options, nullptr);
+
+  EXPECT_EQ(answer->GetContentDescriptionByName("audio")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("video")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("data")->alt_protocol(),
+            absl::nullopt);
+}
+
+TEST_F(MediaSessionDescriptionFactoryTest, AltProtocolDifferentInOffer) {
+  MediaSessionOptions options;
+  AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options);
+  AddDataSection(cricket::DCT_SCTP, RtpTransceiverDirection::kRecvOnly,
+                 &options);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "not-foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "not-bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "not-baz";
+
+  std::unique_ptr<SessionDescription> offer = f1_.CreateOffer(options, nullptr);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "baz";
+
+  std::unique_ptr<SessionDescription> answer =
+      f1_.CreateAnswer(offer.get(), options, nullptr);
+
+  EXPECT_EQ(answer->GetContentDescriptionByName("audio")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("video")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("data")->alt_protocol(),
+            absl::nullopt);
+}
+
+TEST_F(MediaSessionDescriptionFactoryTest, AltProtocolNotInAnswer) {
+  MediaSessionOptions options;
+  AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options);
+  AddDataSection(cricket::DCT_SCTP, RtpTransceiverDirection::kRecvOnly,
+                 &options);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol = "foo";
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol = "bar";
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol = "baz";
+
+  std::unique_ptr<SessionDescription> offer = f1_.CreateOffer(options, nullptr);
+
+  FindFirstMediaDescriptionByMid("audio", &options)->alt_protocol =
+      absl::nullopt;
+  FindFirstMediaDescriptionByMid("video", &options)->alt_protocol =
+      absl::nullopt;
+  FindFirstMediaDescriptionByMid("data", &options)->alt_protocol =
+      absl::nullopt;
+
+  std::unique_ptr<SessionDescription> answer =
+      f1_.CreateAnswer(offer.get(), options, nullptr);
+
+  EXPECT_EQ(answer->GetContentDescriptionByName("audio")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("video")->alt_protocol(),
+            absl::nullopt);
+  EXPECT_EQ(answer->GetContentDescriptionByName("data")->alt_protocol(),
+            absl::nullopt);
+}
+
 // Create an offer with bundle enabled and verify the crypto parameters are
 // the common set of the available cryptos.
 TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoWithOfferBundle) {
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index 2679800..c2723e7f 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -4501,8 +4501,19 @@
   // If datagram transport is in use, add opaque transport parameters.
   if (use_datagram_transport_ || use_datagram_transport_for_data_channels_) {
     for (auto& options : session_options->media_description_options) {
-      options.transport_options.opaque_parameters =
+      absl::optional<cricket::OpaqueTransportParameters> params =
           transport_controller_->GetTransportParameters(options.mid);
+      if (!params) {
+        continue;
+      }
+      options.transport_options.opaque_parameters = params;
+      if ((use_datagram_transport_ &&
+           (options.type == cricket::MEDIA_TYPE_AUDIO ||
+            options.type == cricket::MEDIA_TYPE_VIDEO)) ||
+          (use_datagram_transport_for_data_channels_ &&
+           options.type == cricket::MEDIA_TYPE_DATA)) {
+        options.alt_protocol = params->protocol;
+      }
     }
   }
 
@@ -4807,8 +4818,19 @@
   // If datagram transport is in use, add opaque transport parameters.
   if (use_datagram_transport_ || use_datagram_transport_for_data_channels_) {
     for (auto& options : session_options->media_description_options) {
-      options.transport_options.opaque_parameters =
+      absl::optional<cricket::OpaqueTransportParameters> params =
           transport_controller_->GetTransportParameters(options.mid);
+      if (!params) {
+        continue;
+      }
+      options.transport_options.opaque_parameters = params;
+      if ((use_datagram_transport_ &&
+           (options.type == cricket::MEDIA_TYPE_AUDIO ||
+            options.type == cricket::MEDIA_TYPE_VIDEO)) ||
+          (use_datagram_transport_for_data_channels_ &&
+           options.type == cricket::MEDIA_TYPE_DATA)) {
+        options.alt_protocol = params->protocol;
+      }
     }
   }
 }
diff --git a/pc/peer_connection_integrationtest.cc b/pc/peer_connection_integrationtest.cc
index 6b2d830..3a0ef0f 100644
--- a/pc/peer_connection_integrationtest.cc
+++ b/pc/peer_connection_integrationtest.cc
@@ -3647,6 +3647,310 @@
   ASSERT_TRUE(ExpectNewFrames(media_expectations));
 }
 
+TEST_P(PeerConnectionIntegrationTest,
+       DatagramTransportDataChannelWithMediaOnCaller) {
+  // Configure the caller to attempt use of datagram transport for media and
+  // data channels.
+  PeerConnectionInterface::RTCConfiguration offerer_config;
+  offerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  offerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  offerer_config.use_datagram_transport_for_data_channels = true;
+  offerer_config.use_datagram_transport = true;
+
+  // Configure the callee to only use datagram transport for data channels.
+  PeerConnectionInterface::RTCConfiguration answerer_config;
+  answerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  answerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  answerer_config.use_datagram_transport_for_data_channels = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfigAndMediaTransportFactory(
+      offerer_config, answerer_config,
+      loopback_media_transports()->first_factory(),
+      loopback_media_transports()->second_factory()));
+  ConnectFakeSignaling();
+
+  // Offer both media and data.
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateDataChannel();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+
+  // Ensure that the data channel transport is ready.
+  loopback_media_transports()->SetState(webrtc::MediaTransportState::kWritable);
+  loopback_media_transports()->FlushAsyncInvokes();
+
+  ASSERT_NE(nullptr, caller()->data_channel());
+  ASSERT_TRUE_WAIT(callee()->data_channel() != nullptr, kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller()->data_observer()->IsOpen(), kDefaultTimeout);
+  EXPECT_TRUE_WAIT(callee()->data_observer()->IsOpen(), kDefaultTimeout);
+
+  // Both endpoints should agree to use datagram transport for data channels.
+  EXPECT_EQ(nullptr, caller()->pc()->GetSctpTransport());
+  EXPECT_EQ(nullptr, callee()->pc()->GetSctpTransport());
+
+  // Ensure data can be sent in both directions.
+  std::string data = "hello world";
+  caller()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, callee()->data_observer()->last_message(),
+                 kDefaultTimeout);
+  callee()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, caller()->data_observer()->last_message(),
+                 kDefaultTimeout);
+
+  // Media flow should not be impacted.
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudioAndVideo();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
+TEST_P(PeerConnectionIntegrationTest,
+       DatagramTransportMediaWithDataChannelOnCaller) {
+  // Configure the caller to attempt use of datagram transport for media and
+  // data channels.
+  PeerConnectionInterface::RTCConfiguration offerer_config;
+  offerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  offerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  offerer_config.use_datagram_transport_for_data_channels = true;
+  offerer_config.use_datagram_transport = true;
+
+  // Configure the callee to only use datagram transport for media.
+  PeerConnectionInterface::RTCConfiguration answerer_config;
+  answerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  answerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  answerer_config.use_datagram_transport = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfigAndMediaTransportFactory(
+      offerer_config, answerer_config,
+      loopback_media_transports()->first_factory(),
+      loopback_media_transports()->second_factory()));
+  ConnectFakeSignaling();
+
+  // Offer both media and data.
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateDataChannel();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+
+  // Ensure that the data channel transport is ready.
+  loopback_media_transports()->SetState(webrtc::MediaTransportState::kWritable);
+  loopback_media_transports()->FlushAsyncInvokes();
+
+  ASSERT_NE(nullptr, caller()->data_channel());
+  ASSERT_TRUE_WAIT(callee()->data_channel() != nullptr, kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller()->data_observer()->IsOpen(), kDefaultTimeout);
+  EXPECT_TRUE_WAIT(callee()->data_observer()->IsOpen(), kDefaultTimeout);
+
+  // Both endpoints should agree to use SCTP for data channels.
+  EXPECT_NE(nullptr, caller()->pc()->GetSctpTransport());
+  EXPECT_NE(nullptr, callee()->pc()->GetSctpTransport());
+
+  // Ensure data can be sent in both directions.
+  std::string data = "hello world";
+  caller()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, callee()->data_observer()->last_message(),
+                 kDefaultTimeout);
+  callee()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, caller()->data_observer()->last_message(),
+                 kDefaultTimeout);
+
+  // Media flow should not be impacted.
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudioAndVideo();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
+TEST_P(PeerConnectionIntegrationTest,
+       DatagramTransportDataChannelWithMediaOnCallee) {
+  // Configure the caller to attempt use of datagram transport for data
+  // channels.
+  PeerConnectionInterface::RTCConfiguration offerer_config;
+  offerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  offerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  offerer_config.use_datagram_transport_for_data_channels = true;
+
+  // Configure the callee to use datagram transport for data channels and media.
+  PeerConnectionInterface::RTCConfiguration answerer_config;
+  answerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  answerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  answerer_config.use_datagram_transport_for_data_channels = true;
+  answerer_config.use_datagram_transport = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfigAndMediaTransportFactory(
+      offerer_config, answerer_config,
+      loopback_media_transports()->first_factory(),
+      loopback_media_transports()->second_factory()));
+  ConnectFakeSignaling();
+
+  // Offer both media and data.
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateDataChannel();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+
+  // Ensure that the data channel transport is ready.
+  loopback_media_transports()->SetState(webrtc::MediaTransportState::kWritable);
+  loopback_media_transports()->FlushAsyncInvokes();
+
+  ASSERT_NE(nullptr, caller()->data_channel());
+  ASSERT_TRUE_WAIT(callee()->data_channel() != nullptr, kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller()->data_observer()->IsOpen(), kDefaultTimeout);
+  EXPECT_TRUE_WAIT(callee()->data_observer()->IsOpen(), kDefaultTimeout);
+
+  // Both endpoints should agree to use datagram transport for data channels.
+  EXPECT_EQ(nullptr, caller()->pc()->GetSctpTransport());
+  EXPECT_EQ(nullptr, callee()->pc()->GetSctpTransport());
+
+  // Ensure data can be sent in both directions.
+  std::string data = "hello world";
+  caller()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, callee()->data_observer()->last_message(),
+                 kDefaultTimeout);
+  callee()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, caller()->data_observer()->last_message(),
+                 kDefaultTimeout);
+
+  // Media flow should not be impacted.
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudioAndVideo();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
+TEST_P(PeerConnectionIntegrationTest,
+       DatagramTransportMediaWithDataChannelOnCallee) {
+  // Configure the caller to attempt use of datagram transport for media.
+  PeerConnectionInterface::RTCConfiguration offerer_config;
+  offerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  offerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  offerer_config.use_datagram_transport = true;
+
+  // Configure the callee to only use datagram transport for media and data
+  // channels.
+  PeerConnectionInterface::RTCConfiguration answerer_config;
+  answerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  answerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  answerer_config.use_datagram_transport = true;
+  answerer_config.use_datagram_transport_for_data_channels = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfigAndMediaTransportFactory(
+      offerer_config, answerer_config,
+      loopback_media_transports()->first_factory(),
+      loopback_media_transports()->second_factory()));
+  ConnectFakeSignaling();
+
+  // Offer both media and data.
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateDataChannel();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+
+  // Ensure that the data channel transport is ready.
+  loopback_media_transports()->SetState(webrtc::MediaTransportState::kWritable);
+  loopback_media_transports()->FlushAsyncInvokes();
+
+  ASSERT_NE(nullptr, caller()->data_channel());
+  ASSERT_TRUE_WAIT(callee()->data_channel() != nullptr, kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller()->data_observer()->IsOpen(), kDefaultTimeout);
+  EXPECT_TRUE_WAIT(callee()->data_observer()->IsOpen(), kDefaultTimeout);
+
+  // Both endpoints should agree to use SCTP for data channels.
+  EXPECT_NE(nullptr, caller()->pc()->GetSctpTransport());
+  EXPECT_NE(nullptr, callee()->pc()->GetSctpTransport());
+
+  // Ensure data can be sent in both directions.
+  std::string data = "hello world";
+  caller()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, callee()->data_observer()->last_message(),
+                 kDefaultTimeout);
+  callee()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, caller()->data_observer()->last_message(),
+                 kDefaultTimeout);
+
+  // Media flow should not be impacted.
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudioAndVideo();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
+TEST_P(PeerConnectionIntegrationTest, DatagramTransportDataChannelAndMedia) {
+  // Configure the caller to use datagram transport for data channels and media.
+  PeerConnectionInterface::RTCConfiguration offerer_config;
+  offerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  offerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  offerer_config.use_datagram_transport_for_data_channels = true;
+  offerer_config.use_datagram_transport = true;
+
+  // Configure the callee to use datagram transport for data channels and media.
+  PeerConnectionInterface::RTCConfiguration answerer_config;
+  answerer_config.rtcp_mux_policy =
+      PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  answerer_config.bundle_policy =
+      PeerConnectionInterface::kBundlePolicyMaxBundle;
+  answerer_config.use_datagram_transport_for_data_channels = true;
+  answerer_config.use_datagram_transport = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfigAndMediaTransportFactory(
+      offerer_config, answerer_config,
+      loopback_media_transports()->first_factory(),
+      loopback_media_transports()->second_factory()));
+  ConnectFakeSignaling();
+
+  // Offer both media and data.
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateDataChannel();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+
+  // Ensure that the data channel transport is ready.
+  loopback_media_transports()->SetState(webrtc::MediaTransportState::kWritable);
+  loopback_media_transports()->FlushAsyncInvokes();
+
+  ASSERT_NE(nullptr, caller()->data_channel());
+  ASSERT_TRUE_WAIT(callee()->data_channel() != nullptr, kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller()->data_observer()->IsOpen(), kDefaultTimeout);
+  EXPECT_TRUE_WAIT(callee()->data_observer()->IsOpen(), kDefaultTimeout);
+
+  // Both endpoints should agree to use datagram transport for data channels.
+  EXPECT_EQ(nullptr, caller()->pc()->GetSctpTransport());
+  EXPECT_EQ(nullptr, callee()->pc()->GetSctpTransport());
+
+  // Ensure data can be sent in both directions.
+  std::string data = "hello world";
+  caller()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, callee()->data_observer()->last_message(),
+                 kDefaultTimeout);
+  callee()->data_channel()->Send(DataBuffer(data));
+  EXPECT_EQ_WAIT(data, caller()->data_observer()->last_message(),
+                 kDefaultTimeout);
+
+  // Media flow should not be impacted.
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudioAndVideo();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
 // Tests that data channels use SCTP instead of datagram transport if datagram
 // transport is configured in receive-only mode on the caller.
 TEST_P(PeerConnectionIntegrationTest,
diff --git a/pc/session_description.h b/pc/session_description.h
index 99e78d8..9856cd6 100644
--- a/pc/session_description.h
+++ b/pc/session_description.h
@@ -249,6 +249,13 @@
     receive_rids_ = rids;
   }
 
+  virtual const absl::optional<std::string>& alt_protocol() const {
+    return alt_protocol_;
+  }
+  virtual void set_alt_protocol(const absl::optional<std::string>& protocol) {
+    alt_protocol_ = protocol;
+  }
+
  protected:
   bool rtcp_mux_ = false;
   bool rtcp_reduced_size_ = false;
@@ -270,6 +277,8 @@
 
   SimulcastDescription simulcast_;
   std::vector<RidDescription> receive_rids_;
+
+  absl::optional<std::string> alt_protocol_;
 };
 
 // TODO(bugs.webrtc.org/8620): Remove this alias once downstream projects have
diff --git a/pc/webrtc_sdp.cc b/pc/webrtc_sdp.cc
index ad8fb7e..7a42dca 100644
--- a/pc/webrtc_sdp.cc
+++ b/pc/webrtc_sdp.cc
@@ -239,6 +239,9 @@
 // This is a non-standardized setting for plugin transports.
 static const char kOpaqueTransportParametersLine[] = "x-opaque";
 
+// This is a non-standardized setting for plugin transports.
+static const char kAltProtocolLine[] = "x-alt-protocol";
+
 // RTP payload type is in the 0-127 range. Use -1 to indicate "all" payload
 // types.
 const int kWildcardPayloadType = -1;
@@ -549,6 +552,14 @@
   AddLine(os.str(), message);
 }
 
+static void AddAltProtocolLine(const std::string& protocol,
+                               std::string* message) {
+  rtc::StringBuilder os;
+  InitAttrLine(kAltProtocolLine, &os);
+  os << kSdpDelimiterColon << protocol;
+  AddLine(os.str(), message);
+}
+
 // Writes a SDP attribute line based on |attribute| and |value| to |message|.
 static void AddAttributeLine(const std::string& attribute,
                              int value,
@@ -1540,6 +1551,10 @@
     }
   }
 
+  if (media_desc->alt_protocol()) {
+    AddAltProtocolLine(*media_desc->alt_protocol(), message);
+  }
+
   // RFC 3388
   // mid-attribute      = "a=mid:" identification-tag
   // identification-tag = token
@@ -2149,6 +2164,12 @@
   return true;
 }
 
+bool ParseAltProtocolLine(const std::string& line,
+                          std::string* protocol,
+                          SdpParseError* error) {
+  return GetValue(line, kAltProtocolLine, protocol, error);
+}
+
 bool ParseSessionDescription(const std::string& message,
                              size_t* pos,
                              std::string* session_id,
@@ -3180,6 +3201,12 @@
               &transport->opaque_parameters->parameters, error)) {
         return false;
       }
+    } else if (HasAttribute(line, kAltProtocolLine)) {
+      std::string alt_protocol;
+      if (!ParseAltProtocolLine(line, &alt_protocol, error)) {
+        return false;
+      }
+      media_desc->set_alt_protocol(alt_protocol);
     } else if (HasAttribute(line, kAttributeFmtp)) {
       if (!ParseFmtpAttributes(line, media_type, media_desc, error)) {
         return false;
diff --git a/pc/webrtc_sdp_unittest.cc b/pc/webrtc_sdp_unittest.cc
index a6182c5..5c7e783 100644
--- a/pc/webrtc_sdp_unittest.cc
+++ b/pc/webrtc_sdp_unittest.cc
@@ -1524,6 +1524,8 @@
       CompareSimulcastDescription(
           c1.media_description()->simulcast_description(),
           c2.media_description()->simulcast_description());
+      EXPECT_EQ(c1.media_description()->alt_protocol(),
+                c2.media_description()->alt_protocol());
     }
 
     // group
@@ -1682,6 +1684,14 @@
     desc_.AddTransportInfo(info);
   }
 
+  void AddAltProtocol(const std::string& content_name,
+                      const std::string& alt_protocol) {
+    ASSERT_TRUE(desc_.GetTransportInfoByName(content_name) != NULL);
+    cricket::MediaContentDescription* description =
+        desc_.GetContentDescriptionByName(content_name);
+    description->set_alt_protocol(alt_protocol);
+  }
+
   void AddFingerprint() {
     desc_.RemoveTransportInfoByName(kAudioContentName);
     desc_.RemoveTransportInfoByName(kVideoContentName);
@@ -2234,6 +2244,22 @@
   EXPECT_EQ(message, sdp_with_transport_parameters);
 }
 
+TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithAltProtocol) {
+  AddAltProtocol(kAudioContentName, "foo");
+  AddAltProtocol(kVideoContentName, "bar");
+
+  ASSERT_TRUE(jdesc_.Initialize(desc_.Clone(), jdesc_.session_id(),
+                                jdesc_.session_version()));
+  std::string message = webrtc::SdpSerialize(jdesc_);
+
+  std::string sdp_with_alt_protocol = kSdpFullString;
+  InjectAfter(kAttributeIcePwdVoice, "a=x-alt-protocol:foo\r\n",
+              &sdp_with_alt_protocol);
+  InjectAfter(kAttributeIcePwdVideo, "a=x-alt-protocol:bar\r\n",
+              &sdp_with_alt_protocol);
+  EXPECT_EQ(message, sdp_with_alt_protocol);
+}
+
 TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithRecvOnlyContent) {
   EXPECT_TRUE(TestSerializeDirection(RtpTransceiverDirection::kRecvOnly));
 }
@@ -2646,6 +2672,24 @@
       CompareSessionDescription(jdesc_, jdesc_with_transport_parameters));
 }
 
+TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithAltProtocol) {
+  std::string sdp_with_alt_protocol = kSdpFullString;
+  InjectAfter(kAttributeIcePwdVoice, "a=x-alt-protocol:foo\r\n",
+              &sdp_with_alt_protocol);
+  InjectAfter(kAttributeIcePwdVideo, "a=x-alt-protocol:bar\r\n",
+              &sdp_with_alt_protocol);
+
+  JsepSessionDescription jdesc_with_alt_protocol(kDummyType);
+  EXPECT_TRUE(SdpDeserialize(sdp_with_alt_protocol, &jdesc_with_alt_protocol));
+
+  AddAltProtocol(kAudioContentName, "foo");
+  AddAltProtocol(kVideoContentName, "bar");
+
+  ASSERT_TRUE(jdesc_.Initialize(desc_.Clone(), jdesc_.session_id(),
+                                jdesc_.session_version()));
+  EXPECT_TRUE(CompareSessionDescription(jdesc_, jdesc_with_alt_protocol));
+}
+
 TEST_F(WebRtcSdpTest, DeserializeSessionDescriptionWithUfragPwd) {
   // Remove the original ice-ufrag and ice-pwd
   JsepSessionDescription jdesc_with_ufrag_pwd(kDummyType);