1/*
2 * libjingle
3 * Copyright 2004--2005, 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/examples/call/callclient.h"
29
30#include <string>
31
32#include "talk/base/basicpacketsocketfactory.h"
33#include "talk/base/helpers.h"
34#include "talk/base/logging.h"
35#include "talk/base/network.h"
36#include "talk/base/socketaddress.h"
37#include "talk/base/stringencode.h"
38#include "talk/base/stringutils.h"
39#include "talk/base/thread.h"
40#include "talk/examples/call/console.h"
41#include "talk/examples/call/presencepushtask.h"
42#include "talk/examples/call/presenceouttask.h"
43#include "talk/examples/call/mucinviterecvtask.h"
44#include "talk/examples/call/mucinvitesendtask.h"
45#include "talk/examples/call/friendinvitesendtask.h"
46#include "talk/examples/call/muc.h"
47#include "talk/examples/call/voicemailjidrequester.h"
48#include "talk/p2p/base/sessionmanager.h"
49#include "talk/p2p/client/basicportallocator.h"
50#include "talk/p2p/client/sessionmanagertask.h"
51#include "talk/session/phone/devicemanager.h"
52#include "talk/session/phone/mediaengine.h"
53#include "talk/session/phone/mediasessionclient.h"
54#include "talk/xmpp/constants.h"
55
56
57class NullRenderer : public cricket::VideoRenderer {
58 public:
59  explicit NullRenderer(const char* s) : s_(s) {}
60 private:
61  bool SetSize(int width, int height, int reserved) {
62    LOG(LS_INFO) << "Video size for " << s_ << ": " << width << "x" << height;
63    return true;
64  }
65  bool RenderFrame(const cricket::VideoFrame *frame) {
66    return true;
67  }
68  const char* s_;
69};
70
71namespace {
72
73const char* DescribeStatus(buzz::Status::Show show, const std::string& desc) {
74  switch (show) {
75  case buzz::Status::SHOW_XA:      return desc.c_str();
76  case buzz::Status::SHOW_ONLINE:  return "online";
77  case buzz::Status::SHOW_AWAY:    return "away";
78  case buzz::Status::SHOW_DND:     return "do not disturb";
79  case buzz::Status::SHOW_CHAT:    return "ready to chat";
80  default:                         return "offline";
81  }
82}
83
84std::string GetWord(const std::vector<std::string>& words,
85                    size_t index, const std::string& def) {
86  if (words.size() > index) {
87    return words[index];
88  } else {
89    return def;
90  }
91}
92
93int GetInt(const std::vector<std::string>& words, size_t index, int def) {
94  int val;
95  if (words.size() > index && talk_base::FromString(words[index], &val)) {
96    return val;
97  } else {
98    return def;
99  }
100}
101
102
103}  // namespace
104
105const char* CALL_COMMANDS =
106"Available commands:\n"
107"\n"
108"  hangup  Ends the call.\n"
109"  mute    Stops sending voice.\n"
110"  unmute  Re-starts sending voice.\n"
111"  dtmf    Sends a DTMF tone.\n"
112"  quit    Quits the application.\n"
113"";
114
115const char* RECEIVE_COMMANDS =
116"Available commands:\n"
117"\n"
118"  accept [bw] Accepts the incoming call and switches to it.\n"
119"  reject  Rejects the incoming call and stays with the current call.\n"
120"  quit    Quits the application.\n"
121"";
122
123const char* CONSOLE_COMMANDS =
124"Available commands:\n"
125"\n"
126"  roster              Prints the online friends from your roster.\n"
127"  friend user         Request to add a user to your roster.\n"
128"  call [jid] [bw]     Initiates a call to the user[/room] with the\n"
129"                      given JID and with optional bandwidth.\n"
130"  vcall [jid] [bw]    Initiates a video call to the user[/room] with\n"
131"                      the given JID and with optional bandwidth.\n"
132"  voicemail [jid]     Leave a voicemail for the user with the given JID.\n"
133"  join [room]         Joins a multi-user-chat.\n"
134"  invite user [room]  Invites a friend to a multi-user-chat.\n"
135"  leave [room]        Leaves a multi-user-chat.\n"
136"  getdevs             Prints the available media devices.\n"
137"  quit                Quits the application.\n"
138"";
139
140void CallClient::ParseLine(const std::string& line) {
141  std::vector<std::string> words;
142  int start = -1;
143  int state = 0;
144  for (int index = 0; index <= static_cast<int>(line.size()); ++index) {
145    if (state == 0) {
146      if (!isspace(line[index])) {
147        start = index;
148        state = 1;
149      }
150    } else {
151      ASSERT(state == 1);
152      ASSERT(start >= 0);
153      if (isspace(line[index])) {
154        std::string word(line, start, index - start);
155        words.push_back(word);
156        start = -1;
157        state = 0;
158      }
159    }
160  }
161
162  // Global commands
163  const std::string& command = GetWord(words, 0, "");
164  if (command == "quit") {
165    Quit();
166  } else if (call_ && incoming_call_) {
167    if (command == "accept") {
168      cricket::CallOptions options;
169      options.video_bandwidth = GetInt(words, 1, cricket::kAutoBandwidth);
170      Accept(options);
171    } else if (command == "reject") {
172      Reject();
173    } else {
174      console_->Print(RECEIVE_COMMANDS);
175    }
176  } else if (call_) {
177    if (command == "hangup") {
178      // TODO: do more shutdown here, move to Terminate()
179      call_->Terminate();
180      call_ = NULL;
181      session_ = NULL;
182      console_->SetPrompt(NULL);
183    } else if (command == "mute") {
184      call_->Mute(true);
185    } else if (command == "unmute") {
186      call_->Mute(false);
187    } else if ((command == "dtmf") && (words.size() == 2)) {
188      int ev = std::string("0123456789*#").find(words[1][0]);
189      call_->PressDTMF(ev);
190    } else {
191      console_->Print(CALL_COMMANDS);
192    }
193  } else {
194    if (command == "roster") {
195      PrintRoster();
196    } else if (command == "send") {
197      buzz::Jid jid(words[1]);
198      if (jid.IsValid()) {
199        last_sent_to_ = words[1];
200        SendChat(words[1], words[2]);
201      } else if (!last_sent_to_.empty()) {
202        SendChat(last_sent_to_, words[1]);
203      } else {
204        console_->Printf(
205            "Invalid JID. JIDs should be in the form user@domain\n");
206      }
207    } else if ((words.size() == 2) && (command == "friend")) {
208      InviteFriend(words[1]);
209    } else if (command == "call") {
210      std::string to = GetWord(words, 1, "");
211      MakeCallTo(to, cricket::CallOptions());
212    } else if (command == "vcall") {
213      std::string to = GetWord(words, 1, "");
214      int bandwidth = GetInt(words, 2, cricket::kAutoBandwidth);
215      cricket::CallOptions options;
216      options.is_video = true;
217      options.video_bandwidth = bandwidth;
218      MakeCallTo(to, options);
219    } else if (command == "join") {
220      JoinMuc(GetWord(words, 1, ""));
221    } else if ((words.size() >= 2) && (command == "invite")) {
222      InviteToMuc(words[1], GetWord(words, 2, ""));
223    } else if (command == "leave") {
224      LeaveMuc(GetWord(words, 1, ""));
225    } else if (command == "getdevs") {
226      GetDevices();
227    } else if ((words.size() == 2) && (command == "setvol")) {
228      SetVolume(words[1]);
229    } else if (command == "voicemail") {
230      CallVoicemail((words.size() >= 2) ? words[1] : "");
231    } else {
232      console_->Print(CONSOLE_COMMANDS);
233    }
234  }
235}
236
237CallClient::CallClient(buzz::XmppClient* xmpp_client)
238    : xmpp_client_(xmpp_client),
239      media_engine_(NULL),
240      media_client_(NULL),
241      call_(NULL),
242      incoming_call_(false),
243      auto_accept_(false),
244      pmuc_domain_("groupchat.google.com"),
245      local_renderer_(NULL),
246      remote_renderer_(NULL),
247      roster_(new RosterMap),
248      portallocator_flags_(0),
249      allow_local_ips_(false),
250      initial_protocol_(cricket::PROTOCOL_HYBRID),
251      secure_policy_(cricket::SEC_DISABLED) {
252  xmpp_client_->SignalStateChange.connect(this, &CallClient::OnStateChange);
253}
254
255CallClient::~CallClient() {
256  delete media_client_;
257  delete roster_;
258}
259
260const std::string CallClient::strerror(buzz::XmppEngine::Error err) {
261  switch (err) {
262    case  buzz::XmppEngine::ERROR_NONE:
263      return "";
264    case  buzz::XmppEngine::ERROR_XML:
265      return "Malformed XML or encoding error";
266    case  buzz::XmppEngine::ERROR_STREAM:
267      return "XMPP stream error";
268    case  buzz::XmppEngine::ERROR_VERSION:
269      return "XMPP version error";
270    case  buzz::XmppEngine::ERROR_UNAUTHORIZED:
271      return "User is not authorized (Check your username and password)";
272    case  buzz::XmppEngine::ERROR_TLS:
273      return "TLS could not be negotiated";
274    case  buzz::XmppEngine::ERROR_AUTH:
275      return "Authentication could not be negotiated";
276    case  buzz::XmppEngine::ERROR_BIND:
277      return "Resource or session binding could not be negotiated";
278    case  buzz::XmppEngine::ERROR_CONNECTION_CLOSED:
279      return "Connection closed by output handler.";
280    case  buzz::XmppEngine::ERROR_DOCUMENT_CLOSED:
281      return "Closed by </stream:stream>";
282    case  buzz::XmppEngine::ERROR_SOCKET:
283      return "Socket error";
284    default:
285      return "Unknown error";
286  }
287}
288
289void CallClient::OnCallDestroy(cricket::Call* call) {
290  if (call == call_) {
291    if (remote_renderer_) {
292      delete remote_renderer_;
293      remote_renderer_ = NULL;
294    }
295    if (local_renderer_) {
296      delete local_renderer_;
297      local_renderer_ = NULL;
298    }
299    console_->SetPrompt(NULL);
300    console_->Print("call destroyed");
301    call_ = NULL;
302    session_ = NULL;
303  }
304}
305
306void CallClient::OnStateChange(buzz::XmppEngine::State state) {
307  switch (state) {
308  case buzz::XmppEngine::STATE_START:
309    console_->Print("connecting...");
310    break;
311
312  case buzz::XmppEngine::STATE_OPENING:
313    console_->Print("logging in...");
314    break;
315
316  case buzz::XmppEngine::STATE_OPEN:
317    console_->Print("logged in...");
318    InitPhone();
319    InitPresence();
320    break;
321
322  case buzz::XmppEngine::STATE_CLOSED:
323    buzz::XmppEngine::Error error = xmpp_client_->GetError(NULL);
324    console_->Print("logged out..." + strerror(error));
325    Quit();
326  }
327}
328
329void CallClient::InitPhone() {
330  std::string client_unique = xmpp_client_->jid().Str();
331  talk_base::InitRandom(client_unique.c_str(), client_unique.size());
332
333  worker_thread_ = new talk_base::Thread();
334  // The worker thread must be started here since initialization of
335  // the ChannelManager will generate messages that need to be
336  // dispatched by it.
337  worker_thread_->Start();
338
339  // TODO: It looks like we are leaking many
340  // objects. E.g. |network_manager_| and |socket_factory_| are never
341  // deleted.
342
343  network_manager_ = new talk_base::NetworkManager();
344  socket_factory_ = new talk_base::BasicPacketSocketFactory(worker_thread_);
345
346  // TODO: Decide if the relay address should be specified here.
347  talk_base::SocketAddress stun_addr("stun.l.google.com", 19302);
348  port_allocator_ = new cricket::BasicPortAllocator(
349      network_manager_, socket_factory_, stun_addr,
350      talk_base::SocketAddress(), talk_base::SocketAddress(),
351      talk_base::SocketAddress());
352
353  if (portallocator_flags_ != 0) {
354    port_allocator_->set_flags(portallocator_flags_);
355  }
356  session_manager_ = new cricket::SessionManager(
357      port_allocator_, worker_thread_);
358  session_manager_->SignalRequestSignaling.connect(
359      this, &CallClient::OnRequestSignaling);
360  session_manager_->SignalSessionCreate.connect(
361      this, &CallClient::OnSessionCreate);
362  session_manager_->OnSignalingReady();
363
364  session_manager_task_ =
365      new cricket::SessionManagerTask(xmpp_client_, session_manager_);
366  session_manager_task_->EnableOutgoingMessages();
367  session_manager_task_->Start();
368
369  if (!media_engine_) {
370    media_engine_ = cricket::MediaEngine::Create();
371  }
372
373  media_client_ = new cricket::MediaSessionClient(
374      xmpp_client_->jid(),
375      session_manager_,
376      media_engine_,
377      new cricket::DeviceManager());
378  media_client_->SignalCallCreate.connect(this, &CallClient::OnCallCreate);
379  media_client_->SignalDevicesChange.connect(this,
380                                             &CallClient::OnDevicesChange);
381  media_client_->set_secure(secure_policy_);
382}
383
384void CallClient::OnRequestSignaling() {
385  session_manager_->OnSignalingReady();
386}
387
388void CallClient::OnSessionCreate(cricket::Session* session, bool initiate) {
389  session->set_allow_local_ips(allow_local_ips_);
390  session->set_current_protocol(initial_protocol_);
391}
392
393void CallClient::OnCallCreate(cricket::Call* call) {
394  call->SignalSessionState.connect(this, &CallClient::OnSessionState);
395  if (call->video()) {
396    local_renderer_ = new NullRenderer("local");
397    remote_renderer_ = new NullRenderer("remote");
398  }
399}
400
401void CallClient::OnSessionState(cricket::Call* call,
402                                cricket::BaseSession* session,
403                                cricket::BaseSession::State state) {
404  if (state == cricket::Session::STATE_RECEIVEDINITIATE) {
405    buzz::Jid jid(session->remote_name());
406    console_->Printf("Incoming call from '%s'", jid.Str().c_str());
407    call_ = call;
408    session_ = session;
409    incoming_call_ = true;
410    cricket::CallOptions options;
411    if (auto_accept_) {
412      Accept(options);
413    }
414  } else if (state == cricket::Session::STATE_SENTINITIATE) {
415    console_->Print("calling...");
416  } else if (state == cricket::Session::STATE_RECEIVEDACCEPT) {
417    console_->Print("call answered");
418  } else if (state == cricket::Session::STATE_RECEIVEDREJECT) {
419    console_->Print("call not answered");
420  } else if (state == cricket::Session::STATE_INPROGRESS) {
421    console_->Print("call in progress");
422  } else if (state == cricket::Session::STATE_RECEIVEDTERMINATE) {
423    console_->Print("other side hung up");
424  }
425}
426
427void CallClient::InitPresence() {
428  presence_push_ = new buzz::PresencePushTask(xmpp_client_, this);
429  presence_push_->SignalStatusUpdate.connect(
430    this, &CallClient::OnStatusUpdate);
431  presence_push_->SignalMucJoined.connect(this, &CallClient::OnMucJoined);
432  presence_push_->SignalMucLeft.connect(this, &CallClient::OnMucLeft);
433  presence_push_->SignalMucStatusUpdate.connect(
434    this, &CallClient::OnMucStatusUpdate);
435  presence_push_->Start();
436
437  presence_out_ = new buzz::PresenceOutTask(xmpp_client_);
438  RefreshStatus();
439  presence_out_->Start();
440
441  muc_invite_recv_ = new buzz::MucInviteRecvTask(xmpp_client_);
442  muc_invite_recv_->SignalInviteReceived.connect(this,
443      &CallClient::OnMucInviteReceived);
444  muc_invite_recv_->Start();
445
446  muc_invite_send_ = new buzz::MucInviteSendTask(xmpp_client_);
447  muc_invite_send_->Start();
448
449  friend_invite_send_ = new buzz::FriendInviteSendTask(xmpp_client_);
450  friend_invite_send_->Start();
451}
452
453void CallClient::RefreshStatus() {
454  int media_caps = media_client_->GetCapabilities();
455  my_status_.set_jid(xmpp_client_->jid());
456  my_status_.set_available(true);
457  my_status_.set_show(buzz::Status::SHOW_ONLINE);
458  my_status_.set_priority(0);
459  my_status_.set_know_capabilities(true);
460  my_status_.set_pmuc_capability(true);
461  my_status_.set_phone_capability(
462      (media_caps & cricket::MediaEngine::AUDIO_RECV) != 0);
463  my_status_.set_video_capability(
464      (media_caps & cricket::MediaEngine::VIDEO_RECV) != 0);
465  my_status_.set_camera_capability(
466      (media_caps & cricket::MediaEngine::VIDEO_SEND) != 0);
467  my_status_.set_is_google_client(true);
468  my_status_.set_version("1.0.0.67");
469  presence_out_->Send(my_status_);
470}
471
472void CallClient::OnStatusUpdate(const buzz::Status& status) {
473  RosterItem item;
474  item.jid = status.jid();
475  item.show = status.show();
476  item.status = status.status();
477
478  std::string key = item.jid.Str();
479
480  if (status.available() && status.phone_capability()) {
481     console_->Printf("Adding to roster: %s", key.c_str());
482    (*roster_)[key] = item;
483  } else {
484    console_->Printf("Removing from roster: %s", key.c_str());
485    RosterMap::iterator iter = roster_->find(key);
486    if (iter != roster_->end())
487      roster_->erase(iter);
488  }
489}
490
491void CallClient::PrintRoster() {
492  console_->SetPrompting(false);
493  console_->Printf("Roster contains %d callable", roster_->size());
494  RosterMap::iterator iter = roster_->begin();
495  while (iter != roster_->end()) {
496    console_->Printf("%s - %s",
497                     iter->second.jid.BareJid().Str().c_str(),
498                     DescribeStatus(iter->second.show, iter->second.status));
499    iter++;
500  }
501  console_->SetPrompting(true);
502}
503
504void CallClient::SendChat(const std::string& to, const std::string msg) {
505  buzz::XmlElement* stanza = new buzz::XmlElement(buzz::QN_MESSAGE);
506  stanza->AddAttr(buzz::QN_TO, to);
507  stanza->AddAttr(buzz::QN_ID, talk_base::CreateRandomString(16));
508  stanza->AddAttr(buzz::QN_TYPE, "chat");
509  buzz::XmlElement* body = new buzz::XmlElement(buzz::QN_BODY);
510  body->SetBodyText(msg);
511  stanza->AddElement(body);
512
513  xmpp_client_->SendStanza(stanza);
514  delete stanza;
515}
516
517void CallClient::InviteFriend(const std::string& name) {
518  buzz::Jid jid(name);
519  if (!jid.IsValid() || jid.node() == "") {
520    console_->Printf("Invalid JID. JIDs should be in the form user@domain\n");
521    return;
522  }
523  // Note: for some reason the Buzz backend does not forward our presence
524  // subscription requests to the end user when that user is another call
525  // client as opposed to a Smurf user. Thus, in that scenario, you must
526  // run the friend command as the other user too to create the linkage
527  // (and you won't be notified to do so).
528  friend_invite_send_->Send(jid);
529  console_->Printf("Requesting to befriend %s.\n", name.c_str());
530}
531
532void CallClient::MakeCallTo(const std::string& name,
533                            const cricket::CallOptions& given_options) {
534  // Copy so we can change .is_muc.
535  cricket::CallOptions options = given_options;
536
537  bool found = false;
538  options.is_muc = false;
539  buzz::Jid callto_jid(name);
540  buzz::Jid found_jid;
541  if (name.length() == 0 && mucs_.size() > 0) {
542    // if no name, and in a MUC, establish audio with the MUC
543    found_jid = mucs_.begin()->first;
544    found = true;
545    options.is_muc = true;
546  } else if (name[0] == '+') {
547    // if the first character is a +, assume it's a phone number
548    found_jid = callto_jid;
549    found = true;
550  } else if (callto_jid.resource() == "voicemail") {
551    // if the resource is /voicemail, allow that
552    found_jid = callto_jid;
553    found = true;
554  } else {
555    // otherwise, it's a friend
556    for (RosterMap::iterator iter = roster_->begin();
557         iter != roster_->end(); ++iter) {
558      if (iter->second.jid.BareEquals(callto_jid)) {
559        found = true;
560        found_jid = iter->second.jid;
561        break;
562      }
563    }
564
565    if (!found) {
566      if (mucs_.count(callto_jid) == 1 &&
567          mucs_[callto_jid]->state() == buzz::Muc::MUC_JOINED) {
568        found = true;
569        found_jid = callto_jid;
570        options.is_muc = true;
571      }
572    }
573  }
574
575  if (found) {
576    console_->Printf("Found %s '%s'", options.is_muc ? "room" : "online friend",
577        found_jid.Str().c_str());
578    PlaceCall(found_jid, options);
579  } else {
580    console_->Printf("Could not find online friend '%s'", name.c_str());
581  }
582}
583
584void CallClient::PlaceCall(const buzz::Jid& jid,
585                           const cricket::CallOptions& options) {
586  media_client_->SignalCallDestroy.connect(
587      this, &CallClient::OnCallDestroy);
588  if (!call_) {
589    call_ = media_client_->CreateCall();
590    console_->SetPrompt(jid.Str().c_str());
591    session_ = call_->InitiateSession(jid, options);
592    if (options.is_muc) {
593      // If people in this room are already in a call, must add all their
594      // streams.
595      buzz::Muc::MemberMap& members = mucs_[jid]->members();
596      for (buzz::Muc::MemberMap::iterator elem = members.begin();
597           elem != members.end();
598           ++elem) {
599        AddStream(elem->second.audio_src_id(), elem->second.video_src_id());
600      }
601    }
602  }
603  media_client_->SetFocus(call_);
604  if (call_->video()) {
605    call_->SetLocalRenderer(local_renderer_);
606    // TODO: Call this once for every different remote SSRC
607    // once we get to testing multiway video.
608    call_->SetVideoRenderer(session_, 0, remote_renderer_);
609  }
610}
611
612void CallClient::CallVoicemail(const std::string& name) {
613  buzz::Jid jid(name);
614  if (!jid.IsValid() || jid.node() == "") {
615    console_->Printf("Invalid JID. JIDs should be in the form user@domain\n");
616    return;
617  }
618  buzz::VoicemailJidRequester *request =
619    new buzz::VoicemailJidRequester(xmpp_client_, jid, my_status_.jid());
620  request->SignalGotVoicemailJid.connect(this,
621                                         &CallClient::OnFoundVoicemailJid);
622  request->SignalVoicemailJidError.connect(this,
623                                           &CallClient::OnVoicemailJidError);
624  request->Start();
625}
626
627void CallClient::OnFoundVoicemailJid(const buzz::Jid& to,
628                                     const buzz::Jid& voicemail) {
629  console_->Printf("Calling %s's voicemail.\n", to.Str().c_str());
630  PlaceCall(voicemail, cricket::CallOptions());
631}
632
633void CallClient::OnVoicemailJidError(const buzz::Jid& to) {
634  console_->Printf("Unable to voicemail %s.\n", to.Str().c_str());
635}
636
637void CallClient::AddStream(uint32 audio_src_id, uint32 video_src_id) {
638  if (audio_src_id || video_src_id) {
639    console_->Printf("Adding stream (%u, %u)\n", audio_src_id, video_src_id);
640    call_->AddStream(session_, audio_src_id, video_src_id);
641  }
642}
643
644void CallClient::RemoveStream(uint32 audio_src_id, uint32 video_src_id) {
645  if (audio_src_id || video_src_id) {
646    console_->Printf("Removing stream (%u, %u)\n", audio_src_id, video_src_id);
647    call_->RemoveStream(session_, audio_src_id, video_src_id);
648  }
649}
650
651void CallClient::Accept(const cricket::CallOptions& options) {
652  ASSERT(call_ && incoming_call_);
653  ASSERT(call_->sessions().size() == 1);
654  call_->AcceptSession(call_->sessions()[0], options);
655  media_client_->SetFocus(call_);
656  if (call_->video()) {
657    call_->SetLocalRenderer(local_renderer_);
658    // The client never does an accept for multiway, so this must be 1:1,
659    // so there's no SSRC.
660    call_->SetVideoRenderer(session_, 0, remote_renderer_);
661  }
662  incoming_call_ = false;
663}
664
665void CallClient::Reject() {
666  ASSERT(call_ && incoming_call_);
667  call_->RejectSession(call_->sessions()[0]);
668  incoming_call_ = false;
669}
670
671void CallClient::Quit() {
672  talk_base::Thread::Current()->Quit();
673}
674
675void CallClient::JoinMuc(const std::string& room) {
676  buzz::Jid room_jid;
677  if (room.length() > 0) {
678    room_jid = buzz::Jid(room);
679  } else {
680    // generate a GUID of the form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX,
681    // for an eventual JID of private-chat-<GUID>@groupchat.google.com
682    char guid[37], guid_room[256];
683    for (size_t i = 0; i < ARRAY_SIZE(guid) - 1;) {
684      if (i == 8 || i == 13 || i == 18 || i == 23) {
685        guid[i++] = '-';
686      } else {
687        sprintf(guid + i, "%04x", rand());
688        i += 4;
689      }
690    }
691
692    talk_base::sprintfn(guid_room, ARRAY_SIZE(guid_room),
693                        "private-chat-%s@%s", guid, pmuc_domain_.c_str());
694    room_jid = buzz::Jid(guid_room);
695  }
696
697  if (!room_jid.IsValid()) {
698    console_->Printf("Unable to make valid muc endpoint for %s", room.c_str());
699    return;
700  }
701
702  MucMap::iterator elem = mucs_.find(room_jid);
703  if (elem != mucs_.end()) {
704    console_->Printf("This MUC already exists.");
705    return;
706  }
707
708  buzz::Muc* muc = new buzz::Muc(room_jid, xmpp_client_->jid().node());
709  mucs_[room_jid] = muc;
710  presence_out_->SendDirected(muc->local_jid(), my_status_);
711}
712
713void CallClient::OnMucInviteReceived(const buzz::Jid& inviter,
714    const buzz::Jid& room,
715    const std::vector<buzz::AvailableMediaEntry>& avail) {
716
717  console_->Printf("Invited to join %s by %s.\n", room.Str().c_str(),
718      inviter.Str().c_str());
719  console_->Printf("Available media:\n");
720  if (avail.size() > 0) {
721    for (std::vector<buzz::AvailableMediaEntry>::const_iterator i =
722            avail.begin();
723        i != avail.end();
724        ++i) {
725      console_->Printf("  %s, %s\n",
726          buzz::AvailableMediaEntry::TypeAsString(i->type),
727          buzz::AvailableMediaEntry::StatusAsString(i->status));
728    }
729  } else {
730    console_->Printf("  None\n");
731  }
732  // We automatically join the room.
733  JoinMuc(room.Str());
734}
735
736void CallClient::OnMucJoined(const buzz::Jid& endpoint) {
737  MucMap::iterator elem = mucs_.find(endpoint);
738  ASSERT(elem != mucs_.end() &&
739         elem->second->state() == buzz::Muc::MUC_JOINING);
740
741  buzz::Muc* muc = elem->second;
742  muc->set_state(buzz::Muc::MUC_JOINED);
743  console_->Printf("Joined \"%s\"", muc->jid().Str().c_str());
744}
745
746void CallClient::OnMucStatusUpdate(const buzz::Jid& jid,
747    const buzz::MucStatus& status) {
748
749  // Look up this muc.
750  MucMap::iterator elem = mucs_.find(jid);
751  ASSERT(elem != mucs_.end() &&
752         elem->second->state() == buzz::Muc::MUC_JOINED);
753
754  buzz::Muc* muc = elem->second;
755
756  if (status.jid().IsBare() || status.jid() == muc->local_jid()) {
757    // We are only interested in status about other users.
758    return;
759  }
760
761  if (!status.available()) {
762    // User is leaving the room.
763    buzz::Muc::MemberMap::iterator elem =
764      muc->members().find(status.jid().resource());
765
766    ASSERT(elem != muc->members().end());
767
768    // If user had src-ids, they have the left the room without explicitly
769    // hanging-up; must tear down the stream if in a call to this room.
770    if (call_ && session_->remote_name() == muc->jid().Str()) {
771      RemoveStream(elem->second.audio_src_id(), elem->second.video_src_id());
772    }
773
774    // Remove them from the room.
775    muc->members().erase(elem);
776  } else {
777    // Either user has joined or something changed about them.
778    // Note: The [] operator here will create a new entry if it does not
779    // exist, which is what we want.
780    buzz::MucStatus& member_status(
781        muc->members()[status.jid().resource()]);
782    if (call_ && session_->remote_name() == muc->jid().Str()) {
783      // We are in a call to this muc. Must potentially update our streams.
784      // The following code will correctly update our streams regardless of
785      // whether the SSRCs have been removed, added, or changed and regardless
786      // of whether that has been done to both or just one. This relies on the
787      // fact that AddStream/RemoveStream do nothing for SSRC arguments that are
788      // zero.
789      uint32 remove_audio_src_id = 0;
790      uint32 remove_video_src_id = 0;
791      uint32 add_audio_src_id = 0;
792      uint32 add_video_src_id = 0;
793      if (member_status.audio_src_id() != status.audio_src_id()) {
794        remove_audio_src_id = member_status.audio_src_id();
795        add_audio_src_id = status.audio_src_id();
796      }
797      if (member_status.video_src_id() != status.video_src_id()) {
798        remove_video_src_id = member_status.video_src_id();
799        add_video_src_id = status.video_src_id();
800      }
801      // Remove the old SSRCs, if any.
802      RemoveStream(remove_audio_src_id, remove_video_src_id);
803      // Add the new SSRCs, if any.
804      AddStream(add_audio_src_id, add_video_src_id);
805    }
806    // Update the status. This will use the compiler-generated copy
807    // constructor, which is perfectly adequate for this class.
808    member_status = status;
809  }
810}
811
812void CallClient::LeaveMuc(const std::string& room) {
813  buzz::Jid room_jid;
814  if (room.length() > 0) {
815    room_jid = buzz::Jid(room);
816  } else if (mucs_.size() > 0) {
817    // leave the first MUC if no JID specified
818    room_jid = mucs_.begin()->first;
819  }
820
821  if (!room_jid.IsValid()) {
822    console_->Printf("Invalid MUC JID.");
823    return;
824  }
825
826  MucMap::iterator elem = mucs_.find(room_jid);
827  if (elem == mucs_.end()) {
828    console_->Printf("No such MUC.");
829    return;
830  }
831
832  buzz::Muc* muc = elem->second;
833  muc->set_state(buzz::Muc::MUC_LEAVING);
834
835  buzz::Status status;
836  status.set_jid(my_status_.jid());
837  status.set_available(false);
838  status.set_priority(0);
839  presence_out_->SendDirected(muc->local_jid(), status);
840}
841
842void CallClient::OnMucLeft(const buzz::Jid& endpoint, int error) {
843  // We could be kicked from a room from any state.  We would hope this
844  // happens While in the MUC_LEAVING state
845  MucMap::iterator elem = mucs_.find(endpoint);
846  if (elem == mucs_.end())
847    return;
848
849  buzz::Muc* muc = elem->second;
850  if (muc->state() == buzz::Muc::MUC_JOINING) {
851    console_->Printf("Failed to join \"%s\", code=%d",
852                     muc->jid().Str().c_str(), error);
853  } else if (muc->state() == buzz::Muc::MUC_JOINED) {
854    console_->Printf("Kicked from \"%s\"",
855                     muc->jid().Str().c_str());
856  }
857
858  delete muc;
859  mucs_.erase(elem);
860}
861
862void CallClient::InviteToMuc(const std::string& user, const std::string& room) {
863  // First find the room.
864  const buzz::Muc* found_muc;
865  if (room.length() == 0) {
866    if (mucs_.size() == 0) {
867      console_->Printf("Not in a room yet; can't invite.\n");
868      return;
869    }
870    // Invite to the first muc
871    found_muc = mucs_.begin()->second;
872  } else {
873    MucMap::iterator elem = mucs_.find(buzz::Jid(room));
874    if (elem == mucs_.end()) {
875      console_->Printf("Not in room %s.\n", room.c_str());
876      return;
877    }
878    found_muc = elem->second;
879  }
880  // Now find the user. We invite all of their resources.
881  bool found_user = false;
882  buzz::Jid user_jid(user);
883  for (RosterMap::iterator iter = roster_->begin();
884       iter != roster_->end(); ++iter) {
885    if (iter->second.jid.BareEquals(user_jid)) {
886      muc_invite_send_->Send(iter->second.jid, *found_muc);
887      found_user = true;
888    }
889  }
890  if (!found_user) {
891    console_->Printf("No such friend as %s.\n", user.c_str());
892    return;
893  }
894}
895
896void CallClient::GetDevices() {
897  std::vector<std::string> names;
898  media_client_->GetAudioInputDevices(&names);
899  printf("Audio input devices:\n");
900  PrintDevices(names);
901  media_client_->GetAudioOutputDevices(&names);
902  printf("Audio output devices:\n");
903  PrintDevices(names);
904  media_client_->GetVideoCaptureDevices(&names);
905  printf("Video capture devices:\n");
906  PrintDevices(names);
907}
908
909void CallClient::PrintDevices(const std::vector<std::string>& names) {
910  for (size_t i = 0; i < names.size(); ++i) {
911    printf("%d: %s\n", static_cast<int>(i), names[i].c_str());
912  }
913}
914
915void CallClient::OnDevicesChange() {
916  printf("Devices changed.\n");
917  RefreshStatus();
918}
919
920void CallClient::SetVolume(const std::string& level) {
921  media_client_->SetOutputVolume(strtol(level.c_str(), NULL, 10));
922}
923