| Index: webrtc/p2p/base/transportcontroller.cc | 
| diff --git a/webrtc/p2p/base/transportcontroller.cc b/webrtc/p2p/base/transportcontroller.cc | 
| index 053388eeb8130d30fec9a6fb5a21e94ec54a8b0a..128d2fc656406d75b7b094baf1e8a57cd32513e1 100644 | 
| --- a/webrtc/p2p/base/transportcontroller.cc | 
| +++ b/webrtc/p2p/base/transportcontroller.cc | 
| @@ -127,6 +127,12 @@ bool TransportController::AddRemoteCandidates(const std::string& transport_name, | 
| transport_name, candidates, err)); | 
| } | 
|  | 
| +bool TransportController::RemoveRemoteCandidates(const Candidates& candidates, | 
| +                                                 std::string* err) { | 
| +  return worker_thread_->Invoke<bool>(rtc::Bind( | 
| +      &TransportController::RemoveRemoteCandidates_w, this, candidates, err)); | 
| +} | 
| + | 
| bool TransportController::ReadyForRemoteCandidates( | 
| const std::string& transport_name) { | 
| return worker_thread_->Invoke<bool>(rtc::Bind( | 
| @@ -162,6 +168,8 @@ TransportChannel* TransportController::CreateTransportChannel_w( | 
| this, &TransportController::OnChannelGatheringState_w); | 
| channel->SignalCandidateGathered.connect( | 
| this, &TransportController::OnChannelCandidateGathered_w); | 
| +  channel->SignalCandidatesRemoved.connect( | 
| +      this, &TransportController::OnChannelCandidatesRemoved_w); | 
| channel->SignalRoleConflict.connect( | 
| this, &TransportController::OnChannelRoleConflict_w); | 
| channel->SignalConnectionRemoved.connect( | 
| @@ -460,6 +468,28 @@ bool TransportController::AddRemoteCandidates_w( | 
| return transport->AddRemoteCandidates(candidates, err); | 
| } | 
|  | 
| +bool TransportController::RemoveRemoteCandidates_w(const Candidates& candidates, | 
| +                                                   std::string* err) { | 
| +  RTC_DCHECK(worker_thread()->IsCurrent()); | 
| +  std::map<std::string, Candidates> candidates_by_transport_name; | 
| +  for (const Candidate& cand : candidates) { | 
| +    RTC_DCHECK(!cand.transport_name().empty()); | 
| +    candidates_by_transport_name[cand.transport_name()].push_back(cand); | 
| +  } | 
| + | 
| +  bool result = true; | 
| +  for (auto kv : candidates_by_transport_name) { | 
| +    Transport* transport = GetTransport_w(kv.first); | 
| +    if (!transport) { | 
| +      // If we didn't find a transport, that's not an error; | 
| +      // it could have been deleted as a result of bundling. | 
| +      continue; | 
| +    } | 
| +    result &= transport->RemoveRemoteCandidates(kv.second, err); | 
| +  } | 
| +  return result; | 
| +} | 
| + | 
| bool TransportController::ReadyForRemoteCandidates_w( | 
| const std::string& transport_name) { | 
| RTC_DCHECK(worker_thread()->IsCurrent()); | 
| @@ -518,6 +548,21 @@ void TransportController::OnChannelCandidateGathered_w( | 
| signaling_thread_->Post(this, MSG_CANDIDATESGATHERED, data); | 
| } | 
|  | 
| +void TransportController::OnChannelCandidatesRemoved_w( | 
| +    TransportChannelImpl* channel, | 
| +    const Candidates& candidates) { | 
| +  invoker_.AsyncInvoke<void>( | 
| +      signaling_thread_, | 
| +      rtc::Bind(&TransportController::OnChannelCandidatesRemoved, this, | 
| +                candidates)); | 
| +} | 
| + | 
| +void TransportController::OnChannelCandidatesRemoved( | 
| +    const Candidates& candidates) { | 
| +  RTC_DCHECK(signaling_thread_->IsCurrent()); | 
| +  SignalCandidatesRemoved(candidates); | 
| +} | 
| + | 
| void TransportController::OnChannelRoleConflict_w( | 
| TransportChannelImpl* channel) { | 
| RTC_DCHECK(worker_thread_->IsCurrent()); | 
|  |