Plug-in media transport state listener

IceConnected state (transport state) now includes the state of the
MediaTransport.

This is a first change of two. Second change will add state change
signals to the PeerConnectionInterface informing separately about
ice+media transport vs ice+dtls.

Bug: webrtc:9719
Change-Id: I5731530073e8f26dfc8b188778d268b815da7052
Reviewed-on: https://webrtc-review.googlesource.com/c/108901
Commit-Queue: Peter Slatala <psla@webrtc.org>
Reviewed-by: Bjorn Mellem <mellem@webrtc.org>
Reviewed-by: Anton Sukhanov <sukhanov@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#25473}
diff --git a/api/test/fake_media_transport.h b/api/test/fake_media_transport.h
index 1ef7c1f..730d497 100644
--- a/api/test/fake_media_transport.h
+++ b/api/test/fake_media_transport.h
@@ -57,9 +57,6 @@
   void SetTargetTransferRateObserver(
       webrtc::TargetTransferRateObserver* observer) override {}
 
-  void SetMediaTransportStateCallback(
-      MediaTransportStateCallback* callback) override {}
-
   RTCError SendData(int channel_id,
                     const SendDataParams& params,
                     const rtc::CopyOnWriteBuffer& buffer) override {
@@ -70,8 +67,20 @@
 
   void SetDataSink(DataChannelSink* sink) override {}
 
+  void SetMediaTransportStateCallback(
+      MediaTransportStateCallback* callback) override {
+    state_callback_ = callback;
+  }
+
+  void SetState(webrtc::MediaTransportState state) {
+    if (state_callback_) {
+      state_callback_->OnStateChanged(state);
+    }
+  }
+
  private:
   const MediaTransportSettings settings_;
+  MediaTransportStateCallback* state_callback_;
 };
 
 // Fake media transport factory creates fake media transport.
diff --git a/pc/jseptransport.cc b/pc/jseptransport.cc
index 368b32f..88d20a4 100644
--- a/pc/jseptransport.cc
+++ b/pc/jseptransport.cc
@@ -115,9 +115,17 @@
     RTC_DCHECK(!sdes_transport);
     dtls_srtp_transport_ = std::move(dtls_srtp_transport);
   }
+
+  if (media_transport_) {
+    media_transport_->SetMediaTransportStateCallback(this);
+  }
 }
 
-JsepTransport::~JsepTransport() {}
+JsepTransport::~JsepTransport() {
+  if (media_transport_) {
+    media_transport_->SetMediaTransportStateCallback(nullptr);
+  }
+}
 
 webrtc::RTCError JsepTransport::SetLocalJsepTransportDescription(
     const JsepTransportDescription& jsep_description,
@@ -636,4 +644,12 @@
   return true;
 }
 
+void JsepTransport::OnStateChanged(webrtc::MediaTransportState state) {
+  // TODO(bugs.webrtc.org/9719) This method currently fires on the network
+  // thread, but media transport does not make such guarantees. We need to make
+  // sure this callback is guaranteed to be executed on the network thread.
+  media_transport_state_ = state;
+  SignalMediaTransportStateChanged();
+}
+
 }  // namespace cricket
diff --git a/pc/jseptransport.h b/pc/jseptransport.h
index 8883218..952f2cc 100644
--- a/pc/jseptransport.h
+++ b/pc/jseptransport.h
@@ -71,7 +71,8 @@
 //
 // On Threading: JsepTransport performs work solely on the network thread, and
 // so its methods should only be called on the network thread.
-class JsepTransport : public sigslot::has_slots<> {
+class JsepTransport : public sigslot::has_slots<>,
+                      public webrtc::MediaTransportStateCallback {
  public:
   // |mid| is just used for log statements in order to identify the Transport.
   // Note that |local_certificate| is allowed to be null since a remote
@@ -171,11 +172,19 @@
     return media_transport_.get();
   }
 
+  // Returns the latest media transport state.
+  webrtc::MediaTransportState media_transport_state() const {
+    return media_transport_state_;
+  }
+
   // This is signaled when RTCP-mux becomes active and
   // |rtcp_dtls_transport_| is destroyed. The JsepTransportController will
   // handle the signal and update the aggregate transport states.
   sigslot::signal<> SignalRtcpMuxActive;
 
+  // This is signaled for changes in |media_transport_| state.
+  sigslot::signal<> SignalMediaTransportStateChanged;
+
   // TODO(deadbeef): The methods below are only public for testing. Should make
   // them utility functions or objects so they can be tested independently from
   // this class.
@@ -231,6 +240,9 @@
   bool GetTransportStats(DtlsTransportInternal* dtls_transport,
                          TransportStats* stats);
 
+  // Invoked whenever the state of the media transport changes.
+  void OnStateChanged(webrtc::MediaTransportState state) override;
+
   const std::string mid_;
   // needs-ice-restart bit as described in JSEP.
   bool needs_ice_restart_ = false;
@@ -257,6 +269,11 @@
   // Optional media transport (experimental).
   std::unique_ptr<webrtc::MediaTransportInterface> media_transport_;
 
+  // If |media_transport_| is provided, this variable represents the state of
+  // media transport.
+  webrtc::MediaTransportState media_transport_state_ =
+      webrtc::MediaTransportState::kPending;
+
   RTC_DISALLOW_COPY_AND_ASSIGN(JsepTransport);
 };
 
diff --git a/pc/jseptransportcontroller.cc b/pc/jseptransportcontroller.cc
index 19b5025..1613e1e 100644
--- a/pc/jseptransportcontroller.cc
+++ b/pc/jseptransportcontroller.cc
@@ -1041,6 +1041,8 @@
           std::move(media_transport));
   jsep_transport->SignalRtcpMuxActive.connect(
       this, &JsepTransportController::UpdateAggregateStates_n);
+  jsep_transport->SignalMediaTransportStateChanged.connect(
+      this, &JsepTransportController::UpdateAggregateStates_n);
   SetTransportForMid(content_info.name, jsep_transport.get());
 
   jsep_transports_by_name_[content_info.name] = std::move(jsep_transport);
@@ -1234,6 +1236,10 @@
       PeerConnectionInterface::PeerConnectionState::kNew;
   cricket::IceGatheringState new_gathering_state = cricket::kIceGatheringNew;
   bool any_failed = false;
+
+  // TODO(http://bugs.webrtc.org/9719) If(when) media_transport disables
+  // dtls_transports entirely, the below line will have to be changed to account
+  // for the fact that dtls transports might be absent.
   bool all_connected = !dtls_transports.empty();
   bool all_completed = !dtls_transports.empty();
   bool any_gathering = false;
@@ -1262,6 +1268,31 @@
     dtls_state_counts[dtls->dtls_state()]++;
     ice_state_counts[dtls->ice_transport()->GetIceTransportState()]++;
   }
+
+  for (auto it = jsep_transports_by_name_.begin();
+       it != jsep_transports_by_name_.end(); ++it) {
+    auto jsep_transport = it->second.get();
+    if (!jsep_transport->media_transport()) {
+      continue;
+    }
+
+    // There is no 'kIceConnectionDisconnected', so we only need to handle
+    // connected and completed.
+    // We treat kClosed as failed, because if it happens before shutting down
+    // media transports it means that there was a failure.
+    // MediaTransportInterface allows to flip back and forth between kWritable
+    // and kPending, but there does not exist an implementation that does that,
+    // and the contract of jsep transport controller doesn't quite expect that.
+    // When this happens, we would go from connected to connecting state, but
+    // this may change in future.
+    any_failed |= jsep_transport->media_transport_state() ==
+                  webrtc::MediaTransportState::kClosed;
+    all_completed &= jsep_transport->media_transport_state() ==
+                     webrtc::MediaTransportState::kWritable;
+    all_connected &= jsep_transport->media_transport_state() ==
+                     webrtc::MediaTransportState::kWritable;
+  }
+
   if (any_failed) {
     new_connection_state = cricket::kIceConnectionFailed;
   } else if (all_completed) {
diff --git a/pc/jseptransportcontroller_unittest.cc b/pc/jseptransportcontroller_unittest.cc
index 08b1f9b..1ab5d29 100644
--- a/pc/jseptransportcontroller_unittest.cc
+++ b/pc/jseptransportcontroller_unittest.cc
@@ -730,7 +730,8 @@
   EXPECT_EQ(1, combined_connection_state_signal_count_);
 }
 
-TEST_F(JsepTransportControllerTest, SignalConnectionStateConnected) {
+TEST_F(JsepTransportControllerTest,
+       SignalConnectionStateConnectedNoMediaTransport) {
   CreateJsepTransportController(JsepTransportController::Config());
   auto description = CreateSessionDescriptionWithoutBundle();
   EXPECT_TRUE(transport_controller_
@@ -778,6 +779,93 @@
   EXPECT_EQ(2, combined_connection_state_signal_count_);
 }
 
+TEST_F(JsepTransportControllerTest,
+       SignalConnectionStateConnectedWithMediaTransport) {
+  FakeMediaTransportFactory fake_media_transport_factory;
+  JsepTransportController::Config config;
+  config.media_transport_factory = &fake_media_transport_factory;
+  CreateJsepTransportController(config);
+  auto description = CreateSessionDescriptionWithoutBundle();
+  AddCryptoSettings(description.get());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+  fake_audio_dtls->SetWritable(true);
+  fake_video_dtls->SetWritable(true);
+  // Decreasing connection count from 2 to 1 triggers connection state event.
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(2);
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(2);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_audio_dtls->SetDtlsState(cricket::DTLS_TRANSPORT_CONNECTED);
+  fake_video_dtls->SetDtlsState(cricket::DTLS_TRANSPORT_CONNECTED);
+
+  // Still not connected, because we are waiting for media transport.
+  EXPECT_EQ_WAIT(cricket::kIceConnectionConnecting, connection_state_,
+                 kTimeout);
+
+  FakeMediaTransport* media_transport = static_cast<FakeMediaTransport*>(
+      transport_controller_->GetMediaTransport(kAudioMid1));
+
+  media_transport->SetState(webrtc::MediaTransportState::kWritable);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionConnecting, connection_state_,
+                 kTimeout);
+
+  // Still waiting for the second media transport.
+  media_transport = static_cast<FakeMediaTransport*>(
+      transport_controller_->GetMediaTransport(kVideoMid1));
+  media_transport->SetState(webrtc::MediaTransportState::kWritable);
+
+  EXPECT_EQ_WAIT(cricket::kIceConnectionConnected, connection_state_, kTimeout);
+}
+
+TEST_F(JsepTransportControllerTest,
+       SignalConnectionStateFailedWhenMediaTransportClosed) {
+  FakeMediaTransportFactory fake_media_transport_factory;
+  JsepTransportController::Config config;
+  config.media_transport_factory = &fake_media_transport_factory;
+  CreateJsepTransportController(config);
+  auto description = CreateSessionDescriptionWithoutBundle();
+  AddCryptoSettings(description.get());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+  fake_audio_dtls->SetWritable(true);
+  fake_video_dtls->SetWritable(true);
+  // Decreasing connection count from 2 to 1 triggers connection state event.
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(2);
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(2);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_audio_dtls->SetDtlsState(cricket::DTLS_TRANSPORT_CONNECTED);
+  fake_video_dtls->SetDtlsState(cricket::DTLS_TRANSPORT_CONNECTED);
+
+  FakeMediaTransport* media_transport = static_cast<FakeMediaTransport*>(
+      transport_controller_->GetMediaTransport(kAudioMid1));
+
+  media_transport->SetState(webrtc::MediaTransportState::kWritable);
+
+  media_transport = static_cast<FakeMediaTransport*>(
+      transport_controller_->GetMediaTransport(kVideoMid1));
+
+  media_transport->SetState(webrtc::MediaTransportState::kWritable);
+
+  EXPECT_EQ_WAIT(cricket::kIceConnectionConnected, connection_state_, kTimeout);
+
+  media_transport->SetState(webrtc::MediaTransportState::kClosed);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout);
+}
+
 TEST_F(JsepTransportControllerTest, SignalConnectionStateComplete) {
   CreateJsepTransportController(JsepTransportController::Config());
   auto description = CreateSessionDescriptionWithoutBundle();