~ubuntu-branches/debian/experimental/kopete/experimental

« back to all changes in this revision

Viewing changes to protocols/jabber/googletalk/libjingle/talk/app/webrtc/webrtcsession.cc

  • Committer: Package Import Robot
  • Author(s): Maximiliano Curia
  • Date: 2015-02-24 11:32:57 UTC
  • mfrom: (1.1.41 vivid)
  • Revision ID: package-import@ubuntu.com-20150224113257-gnupg4v7lzz18ij0
Tags: 4:14.12.2-1
* New upstream release (14.12.2).
* Bump Standards-Version to 3.9.6, no changes needed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/*
2
 
 * libjingle
3
 
 * Copyright 2012, Google Inc.
4
 
 *
5
 
 * Redistribution and use in source and binary forms, with or without
6
 
 * modification, are permitted provided that the following conditions are met:
7
 
 *
8
 
 *  1. Redistributions of source code must retain the above copyright notice,
9
 
 *     this list of conditions and the following disclaimer.
10
 
 *  2. Redistributions in binary form must reproduce the above copyright notice,
11
 
 *     this list of conditions and the following disclaimer in the documentation
12
 
 *     and/or other materials provided with the distribution.
13
 
 *  3. The name of the author may not be used to endorse or promote products
14
 
 *     derived from this software without specific prior written permission.
15
 
 *
16
 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17
 
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18
 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19
 
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21
 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22
 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23
 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24
 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25
 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 
 */
27
 
 
28
 
#include "talk/app/webrtc/webrtcsession.h"
29
 
 
30
 
#include "talk/app/webrtc/jsepicecandidate.h"
31
 
#include "talk/app/webrtc/jsepsessiondescription.h"
32
 
#include "talk/app/webrtc/mediastreamsignaling.h"
33
 
#include "talk/app/webrtc/mediastreaminterface.h"
34
 
#include "talk/app/webrtc/peerconnection.h"
35
 
#include "talk/base/helpers.h"
36
 
#include "talk/base/logging.h"
37
 
#include "talk/base/stringencode.h"
38
 
#include "talk/session/phone/channel.h"
39
 
#include "talk/session/phone/channelmanager.h"
40
 
#include "talk/session/phone/mediasession.h"
41
 
#include "talk/session/phone/videocapturer.h"
42
 
 
43
 
using cricket::MediaContentDescription;
44
 
 
45
 
namespace webrtc {
46
 
 
47
 
enum {
48
 
  MSG_CANDIDATE_TIMEOUT = 101,
49
 
  MSG_CANDIDATE_DISCOVERY_TIMEOUT = 102,
50
 
};
51
 
 
52
 
// We allow 30 seconds to establish a connection, otherwise it's an error.
53
 
static const int kCallSetupTimeout = 30 * 1000;
54
 
static const int kCandidateDiscoveryTimeout = 2000;
55
 
 
56
 
// Constants for setting the default encoder size.
57
 
// TODO: Implement proper negotiation of video resolution.
58
 
static const int kDefaultVideoCodecId = 100;
59
 
static const int kDefaultVideoCodecFramerate = 30;
60
 
static const char kDefaultVideoCodecName[] = "VP8";
61
 
static const int kDefaultVideoCodecWidth = 640;
62
 
static const int kDefaultVideoCodecHeight = 480;
63
 
 
64
 
static cricket::ContentAction GetContentAction(JsepInterface::Action action) {
65
 
  switch (action) {
66
 
    case JsepInterface::kOffer:
67
 
      return cricket::CA_OFFER;
68
 
    case JsepInterface::kAnswer:
69
 
      return cricket::CA_ANSWER;
70
 
    default:
71
 
      ASSERT(!"Not supported action");
72
 
  };
73
 
  return cricket::CA_OFFER;
74
 
}
75
 
 
76
 
static void CopyCandidatesFromSessionDescription(
77
 
    const SessionDescriptionInterface* source_desc,
78
 
    SessionDescriptionInterface* dest_desc) {
79
 
  if (!source_desc)
80
 
    return;
81
 
  for (size_t m = 0; m < source_desc->number_of_mediasections(); ++m) {
82
 
    const IceCandidateColletion* source_candidates = source_desc->candidates(m);
83
 
    const IceCandidateColletion* desc_candidates = dest_desc->candidates(m);
84
 
    for  (size_t n = 0; n < source_candidates->count(); ++n) {
85
 
      const IceCandidateInterface* new_candidate = source_candidates->at(n);
86
 
      if (!desc_candidates->HasCandidate(new_candidate))
87
 
        dest_desc->AddCandidate(source_candidates->at(n));
88
 
    }
89
 
  }
90
 
}
91
 
 
92
 
WebRtcSession::WebRtcSession(cricket::ChannelManager* channel_manager,
93
 
                             talk_base::Thread* signaling_thread,
94
 
                             talk_base::Thread* worker_thread,
95
 
                             cricket::PortAllocator* port_allocator,
96
 
                             MediaStreamSignaling* mediastream_signaling)
97
 
    : cricket::BaseSession(signaling_thread, worker_thread, port_allocator,
98
 
                           talk_base::ToString(talk_base::CreateRandomId()),
99
 
                           cricket::NS_JINGLE_RTP, true),
100
 
      channel_manager_(channel_manager),
101
 
      session_desc_factory_(channel_manager),
102
 
      ice_started_(false),
103
 
      mediastream_signaling_(mediastream_signaling),
104
 
      ice_observer_(NULL) {
105
 
}
106
 
 
107
 
WebRtcSession::~WebRtcSession() {
108
 
  if (voice_channel_.get()) {
109
 
    channel_manager_->DestroyVoiceChannel(voice_channel_.release());
110
 
  }
111
 
  if (video_channel_.get()) {
112
 
    channel_manager_->DestroyVideoChannel(video_channel_.release());
113
 
  }
114
 
}
115
 
 
116
 
bool WebRtcSession::Initialize() {
117
 
  // By default SRTP-SDES is enabled in WebRtc.
118
 
  set_secure_policy(cricket::SEC_REQUIRED);
119
 
  // Make sure SessionDescriptions only contains the StreamParams we negotiate.
120
 
  session_desc_factory_.set_add_legacy_streams(false);
121
 
 
122
 
  const cricket::VideoCodec default_codec(kDefaultVideoCodecId,
123
 
      kDefaultVideoCodecName, kDefaultVideoCodecWidth, kDefaultVideoCodecHeight,
124
 
      kDefaultVideoCodecFramerate, 0);
125
 
  channel_manager_->SetDefaultVideoEncoderConfig(
126
 
      cricket::VideoEncoderConfig(default_codec));
127
 
 
128
 
  return CreateChannels();
129
 
}
130
 
 
131
 
bool WebRtcSession::StartIce(IceOptions /*options*/) {
132
 
  if (!local_description()) {
133
 
    LOG(LS_ERROR) << "StartIce called before SetLocalDescription";
134
 
    return false;
135
 
  }
136
 
 
137
 
  // TODO: Take IceOptions into consideration and restart of the
138
 
  // ice agent.
139
 
  if (ice_started_) {
140
 
    return true;
141
 
  }
142
 
  // Try connecting all transport channels. This is necessary to generate
143
 
  // ICE candidates.
144
 
  SpeculativelyConnectAllTransportChannels();
145
 
  signaling_thread()->PostDelayed(
146
 
      kCandidateDiscoveryTimeout, this, MSG_CANDIDATE_DISCOVERY_TIMEOUT);
147
 
 
148
 
  ice_started_ = true;
149
 
  if (!UseCandidatesInSessionDescription(remote_desc_.get())) {
150
 
    LOG(LS_WARNING) << "StartIce: Can't use candidates in remote session"
151
 
                    << " description";
152
 
  }
153
 
  return true;
154
 
}
155
 
 
156
 
void WebRtcSession::set_secure_policy(
157
 
    cricket::SecureMediaPolicy secure_policy) {
158
 
  session_desc_factory_.set_secure(secure_policy);
159
 
}
160
 
 
161
 
SessionDescriptionInterface* WebRtcSession::CreateOffer(
162
 
    const MediaHints& hints) {
163
 
  cricket::MediaSessionOptions options =
164
 
      mediastream_signaling_->GetMediaSessionOptions(hints);
165
 
  cricket::SessionDescription* desc(
166
 
      session_desc_factory_.CreateOffer(options,
167
 
                                        BaseSession::local_description()));
168
 
  SessionDescriptionInterface* offer = new JsepSessionDescription(desc);
169
 
  if (local_description())
170
 
    CopyCandidatesFromSessionDescription(local_description(), offer);
171
 
  return offer;
172
 
}
173
 
 
174
 
SessionDescriptionInterface* WebRtcSession::CreateAnswer(
175
 
    const MediaHints& hints,
176
 
    const SessionDescriptionInterface* offer) {
177
 
  cricket::MediaSessionOptions options =
178
 
      mediastream_signaling_->GetMediaSessionOptions(hints);
179
 
  cricket::SessionDescription* desc(
180
 
      session_desc_factory_.CreateAnswer(offer->description(), options,
181
 
                                         BaseSession::local_description()));
182
 
  SessionDescriptionInterface* answer = new JsepSessionDescription(desc);
183
 
  if (local_description())
184
 
    CopyCandidatesFromSessionDescription(local_description(), answer);
185
 
  return answer;
186
 
}
187
 
 
188
 
bool WebRtcSession::SetLocalDescription(Action action,
189
 
                                        SessionDescriptionInterface* desc) {
190
 
  cricket::ContentAction type = GetContentAction(action);
191
 
  if ((type == cricket::CA_ANSWER &&
192
 
       state() != STATE_RECEIVEDINITIATE) ||
193
 
      (type == cricket::CA_OFFER &&
194
 
               (state() == STATE_RECEIVEDINITIATE ||
195
 
                state() == STATE_SENTINITIATE))) {
196
 
    LOG(LS_ERROR) << "SetLocalDescription called with action in wrong state, "
197
 
                  << "action: " << type << " state: " << state();
198
 
    return false;
199
 
  }
200
 
  if (!desc || !desc->description()) {
201
 
    LOG(LS_ERROR) << "SetLocalDescription called with an invalid session"
202
 
                  <<" description";
203
 
    return false;
204
 
  }
205
 
 
206
 
  set_local_description(desc->description()->Copy());
207
 
  local_desc_.reset(desc);
208
 
 
209
 
  if (type == cricket::CA_ANSWER) {
210
 
    EnableChannels();
211
 
    SetState(STATE_SENTACCEPT);
212
 
  } else {
213
 
    SetState(STATE_SENTINITIATE);
214
 
  }
215
 
  return true;
216
 
}
217
 
 
218
 
bool WebRtcSession::SetRemoteDescription(Action action,
219
 
                                         SessionDescriptionInterface* desc) {
220
 
  cricket::ContentAction type = GetContentAction(action);
221
 
  if ((type == cricket::CA_ANSWER &&
222
 
       state() != STATE_SENTINITIATE) ||
223
 
      (type == cricket::CA_OFFER &&
224
 
               (state() == STATE_RECEIVEDINITIATE ||
225
 
                state() == STATE_SENTINITIATE))) {
226
 
    LOG(LS_ERROR) << "SetRemoteDescription called with action in wrong state, "
227
 
                  << "action: " << type << " state: " << state();
228
 
    return false;
229
 
  }
230
 
  if (!desc || !desc->description()) {
231
 
    LOG(LS_ERROR) << "SetRemoteDescription called with an invalid session"
232
 
                  <<" description";
233
 
    return false;
234
 
  }
235
 
 
236
 
  set_remote_description(desc->description()->Copy());
237
 
  if (type  == cricket::CA_ANSWER) {
238
 
    EnableChannels();
239
 
    SetState(STATE_RECEIVEDACCEPT);
240
 
  } else {
241
 
    SetState(STATE_RECEIVEDINITIATE);
242
 
  }
243
 
  // Update remote MediaStreams.
244
 
  mediastream_signaling_->UpdateRemoteStreams(desc);
245
 
 
246
 
  // Use all candidates in this new session description if ice is started.
247
 
  if (ice_started_ && !UseCandidatesInSessionDescription(desc)) {
248
 
    LOG(LS_ERROR) << "SetRemoteDescription: Argument |desc| contains "
249
 
                  << "invalid candidates";
250
 
    return false;
251
 
  }
252
 
  // We retain all received candidates.
253
 
  CopyCandidatesFromSessionDescription(remote_desc_.get(), desc);
254
 
  remote_desc_.reset(desc);
255
 
  return true;
256
 
}
257
 
 
258
 
bool WebRtcSession::ProcessIceMessage(const IceCandidateInterface* candidate) {
259
 
  if (!remote_description()) {
260
 
    LOG(LS_ERROR) << "Remote description not set";
261
 
    return false;
262
 
  }
263
 
 
264
 
  if (!candidate) {
265
 
    LOG(LS_ERROR) << "ProcessIceMessage: Candidate is NULL";
266
 
    return false;
267
 
  }
268
 
 
269
 
  // Add this candidate to the remote session description.
270
 
  if (!remote_desc_->AddCandidate(candidate)) {
271
 
    LOG(LS_ERROR) << "ProcessIceMessage: Candidate cannot be used";
272
 
    return false;
273
 
  }
274
 
 
275
 
  if (ice_started_) {  // Use this candidate now if we have started ice.
276
 
    return UseCandidate(candidate);
277
 
  }
278
 
  return true;
279
 
}
280
 
 
281
 
void WebRtcSession::OnMessage(talk_base::Message* msg) {
282
 
  switch (msg->message_id) {
283
 
    case MSG_CANDIDATE_TIMEOUT:
284
 
      LOG(LS_ERROR) << "Transport is not in writable state.";
285
 
      SignalError();
286
 
      break;
287
 
    case MSG_CANDIDATE_DISCOVERY_TIMEOUT:
288
 
      if (ice_observer_)
289
 
         ice_observer_->OnIceComplete();
290
 
      break;
291
 
    default:
292
 
      break;
293
 
  }
294
 
}
295
 
 
296
 
bool WebRtcSession::SetCaptureDevice(const std::string& name,
297
 
                                     cricket::VideoCapturer* camera) {
298
 
  // should be called from a signaling thread
299
 
  ASSERT(signaling_thread()->IsCurrent());
300
 
 
301
 
  // TODO: Refactor this when there is support for multiple cameras.
302
 
  const uint32 dummy_ssrc = 0;
303
 
  if (!channel_manager_->SetVideoCapturer(camera, dummy_ssrc)) {
304
 
    LOG(LS_ERROR) << "Failed to set capture device.";
305
 
    return false;
306
 
  }
307
 
 
308
 
  const bool start_capture = (camera != NULL);
309
 
  cricket::CaptureResult ret = channel_manager_->SetVideoCapture(start_capture);
310
 
  if (ret != cricket::CR_SUCCESS && ret != cricket::CR_PENDING) {
311
 
    LOG(LS_ERROR) << "Failed to start the capture device.";
312
 
    return false;
313
 
  }
314
 
 
315
 
  return true;
316
 
}
317
 
 
318
 
void WebRtcSession::SetLocalRenderer(const std::string& name,
319
 
                                     cricket::VideoRenderer* renderer) {
320
 
  ASSERT(signaling_thread()->IsCurrent());
321
 
  // TODO: Fix SetLocalRenderer.
322
 
  // video_channel_->SetLocalRenderer(0, renderer);
323
 
}
324
 
 
325
 
void WebRtcSession::SetRemoteRenderer(const std::string& name,
326
 
                                      cricket::VideoRenderer* renderer) {
327
 
  ASSERT(signaling_thread()->IsCurrent());
328
 
 
329
 
  const cricket::ContentInfo* video_info =
330
 
      cricket::GetFirstVideoContent(BaseSession::remote_description());
331
 
  if (!video_info) {
332
 
    LOG(LS_ERROR) << "Video not received in this call";
333
 
  }
334
 
 
335
 
  const cricket::MediaContentDescription* video_content =
336
 
      static_cast<const cricket::MediaContentDescription*>(
337
 
          video_info->description);
338
 
  cricket::StreamParams stream;
339
 
  if (cricket::GetStreamByNickAndName(video_content->streams(), "", name,
340
 
                                      &stream)) {
341
 
    video_channel_->SetRenderer(stream.first_ssrc(), renderer);
342
 
  } else {
343
 
    // Allow that |stream| does not exist if renderer is null but assert
344
 
    // otherwise.
345
 
    VERIFY(renderer == NULL);
346
 
  }
347
 
}
348
 
 
349
 
void WebRtcSession::OnTransportRequestSignaling(
350
 
    cricket::Transport* transport) {
351
 
  ASSERT(signaling_thread()->IsCurrent());
352
 
  transport->OnSignalingReady();
353
 
}
354
 
 
355
 
void WebRtcSession::OnTransportConnecting(cricket::Transport* transport) {
356
 
  ASSERT(signaling_thread()->IsCurrent());
357
 
  // start monitoring for the write state of the transport.
358
 
  OnTransportWritable(transport);
359
 
}
360
 
 
361
 
void WebRtcSession::OnTransportWritable(cricket::Transport* transport) {
362
 
  ASSERT(signaling_thread()->IsCurrent());
363
 
  // If the transport is not in writable state, start a timer to monitor
364
 
  // the state. If the transport doesn't become writable state in 30 seconds
365
 
  // then we are assuming call can't be continued.
366
 
  signaling_thread()->Clear(this, MSG_CANDIDATE_TIMEOUT);
367
 
  if (transport->HasChannels() && !transport->writable()) {
368
 
    signaling_thread()->PostDelayed(
369
 
        kCallSetupTimeout, this, MSG_CANDIDATE_TIMEOUT);
370
 
  }
371
 
}
372
 
 
373
 
void WebRtcSession::OnTransportCandidatesReady(
374
 
    cricket::Transport* transport, const cricket::Candidates& candidates) {
375
 
  ASSERT(signaling_thread()->IsCurrent());
376
 
 
377
 
  cricket::TransportProxy* proxy = GetTransportProxy(transport);
378
 
  if (!VERIFY(proxy != NULL)) {
379
 
    LOG(LS_ERROR) << "No Proxy found";
380
 
    return;
381
 
  }
382
 
  ProcessNewLocalCandidate(proxy->content_name(), candidates);
383
 
}
384
 
 
385
 
void WebRtcSession::OnTransportChannelGone(cricket::Transport* transport,
386
 
                                           const std::string& name) {
387
 
  ASSERT(signaling_thread()->IsCurrent());
388
 
}
389
 
 
390
 
bool WebRtcSession::CreateChannels() {
391
 
  voice_channel_.reset(channel_manager_->CreateVoiceChannel(
392
 
      this, cricket::CN_AUDIO, true));
393
 
  if (!voice_channel_.get()) {
394
 
    LOG(LS_ERROR) << "Failed to create voice channel";
395
 
    return false;
396
 
  }
397
 
 
398
 
  video_channel_.reset(channel_manager_->CreateVideoChannel(
399
 
      this, cricket::CN_VIDEO, true, voice_channel_.get()));
400
 
  if (!video_channel_.get()) {
401
 
    LOG(LS_ERROR) << "Failed to create video channel";
402
 
    return false;
403
 
  }
404
 
 
405
 
  // TransportProxies and TransportChannels will be created when
406
 
  // CreateVoiceChannel and CreateVideoChannel are called.
407
 
  return true;
408
 
}
409
 
 
410
 
// Enabling voice and video channel.
411
 
void WebRtcSession::EnableChannels() {
412
 
  if (!voice_channel_->enabled())
413
 
    voice_channel_->Enable(true);
414
 
 
415
 
  if (!video_channel_->enabled())
416
 
    video_channel_->Enable(true);
417
 
}
418
 
 
419
 
void WebRtcSession::ProcessNewLocalCandidate(
420
 
    const std::string& content_name,
421
 
    const cricket::Candidates& candidates) {
422
 
  std::string candidate_label;
423
 
 
424
 
  if (!GetLocalCandidateLabel(content_name, &candidate_label)) {
425
 
    LOG(LS_ERROR) << "ProcessNewLocalCandidate: content name "
426
 
                  << content_name << " not found";
427
 
    return;
428
 
  }
429
 
 
430
 
  for (cricket::Candidates::const_iterator citer = candidates.begin();
431
 
      citer != candidates.end(); ++citer) {
432
 
    JsepIceCandidate candidate(candidate_label, *citer);
433
 
    if (ice_observer_) {
434
 
      ice_observer_->OnIceCandidate(&candidate);
435
 
    }
436
 
    if (local_desc_.get()) {
437
 
      local_desc_->AddCandidate(&candidate);
438
 
    }
439
 
  }
440
 
}
441
 
 
442
 
// Returns a label for a local ice candidate given the content name.
443
 
bool WebRtcSession::GetLocalCandidateLabel(const std::string& content_name,
444
 
                                           std::string* label) {
445
 
  if (!BaseSession::local_description() || !label)
446
 
    return false;
447
 
 
448
 
  bool content_found = false;
449
 
  const cricket::ContentInfos& contents =
450
 
      BaseSession::local_description()->contents();
451
 
  for (size_t index = 0; index < contents.size(); ++index) {
452
 
    if (contents[index].name == content_name) {
453
 
      *label = talk_base::ToString(index);
454
 
      content_found = true;
455
 
      break;
456
 
    }
457
 
  }
458
 
  return content_found;
459
 
}
460
 
 
461
 
bool WebRtcSession::UseCandidatesInSessionDescription(
462
 
    const SessionDescriptionInterface* remote_desc) {
463
 
  if (!remote_desc)
464
 
    return true;
465
 
  bool ret = true;
466
 
  for (size_t m = 0; m < remote_desc->number_of_mediasections(); ++m) {
467
 
    const IceCandidateColletion* candidates = remote_desc->candidates(m);
468
 
    for  (size_t n = 0; n < candidates->count(); ++n) {
469
 
      ret = UseCandidate(candidates->at(n));
470
 
      if (!ret)
471
 
        break;
472
 
    }
473
 
  }
474
 
  return ret;
475
 
}
476
 
 
477
 
bool WebRtcSession::UseCandidate(
478
 
    const IceCandidateInterface* candidate) {
479
 
 
480
 
  size_t mediacontent_index;
481
 
  size_t remote_content_size =
482
 
      BaseSession::remote_description()->contents().size();
483
 
  if ((!talk_base::FromString<size_t>(candidate->label(),
484
 
                                      &mediacontent_index)) ||
485
 
      (mediacontent_index >= remote_content_size)) {
486
 
    LOG(LS_ERROR) << "UseRemoteCandidateInSession: Invalid candidate label";
487
 
    return false;
488
 
  }
489
 
 
490
 
  cricket::ContentInfo content =
491
 
      BaseSession::remote_description()->contents()[mediacontent_index];
492
 
 
493
 
  std::string local_content_name;
494
 
  if (cricket::IsAudioContent(&content)) {
495
 
    local_content_name = cricket::CN_AUDIO;
496
 
  } else if (cricket::IsVideoContent(&content)) {
497
 
    local_content_name = cricket::CN_VIDEO;
498
 
  }
499
 
 
500
 
  // TODO: Justins comment:This is bad encapsulation, suggest we add a
501
 
  // helper to BaseSession to allow us to
502
 
  // pass in candidates without touching the transport proxies.
503
 
  cricket::TransportProxy* proxy = GetTransportProxy(local_content_name);
504
 
  if (!proxy) {
505
 
    LOG(LS_ERROR) << "No TransportProxy exists with name "
506
 
                  << local_content_name;
507
 
    return false;
508
 
  }
509
 
  // CompleteNegotiation will set actual impl's in Proxy.
510
 
  if (!proxy->negotiated())
511
 
    proxy->CompleteNegotiation();
512
 
 
513
 
  // TODO - Add a interface to TransportProxy to accept
514
 
  // a remote candidate.
515
 
  std::vector<cricket::Candidate> candidates;
516
 
  candidates.push_back(candidate->candidate());
517
 
  proxy->impl()->OnRemoteCandidates(candidates);
518
 
  return true;
519
 
}
520
 
 
521
 
}  // namespace webrtc