普通文本  |  923行  |  30.32 KB

/*
 * libjingle
 * Copyright 2004--2005, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "talk/examples/call/callclient.h"

#include <string>

#include "talk/base/basicpacketsocketfactory.h"
#include "talk/base/helpers.h"
#include "talk/base/logging.h"
#include "talk/base/network.h"
#include "talk/base/socketaddress.h"
#include "talk/base/stringencode.h"
#include "talk/base/stringutils.h"
#include "talk/base/thread.h"
#include "talk/examples/call/console.h"
#include "talk/examples/call/presencepushtask.h"
#include "talk/examples/call/presenceouttask.h"
#include "talk/examples/call/mucinviterecvtask.h"
#include "talk/examples/call/mucinvitesendtask.h"
#include "talk/examples/call/friendinvitesendtask.h"
#include "talk/examples/call/muc.h"
#include "talk/examples/call/voicemailjidrequester.h"
#include "talk/p2p/base/sessionmanager.h"
#include "talk/p2p/client/basicportallocator.h"
#include "talk/p2p/client/sessionmanagertask.h"
#include "talk/session/phone/devicemanager.h"
#include "talk/session/phone/mediaengine.h"
#include "talk/session/phone/mediasessionclient.h"
#include "talk/xmpp/constants.h"


class NullRenderer : public cricket::VideoRenderer {
 public:
  explicit NullRenderer(const char* s) : s_(s) {}
 private:
  bool SetSize(int width, int height, int reserved) {
    LOG(LS_INFO) << "Video size for " << s_ << ": " << width << "x" << height;
    return true;
  }
  bool RenderFrame(const cricket::VideoFrame *frame) {
    return true;
  }
  const char* s_;
};

namespace {

const char* DescribeStatus(buzz::Status::Show show, const std::string& desc) {
  switch (show) {
  case buzz::Status::SHOW_XA:      return desc.c_str();
  case buzz::Status::SHOW_ONLINE:  return "online";
  case buzz::Status::SHOW_AWAY:    return "away";
  case buzz::Status::SHOW_DND:     return "do not disturb";
  case buzz::Status::SHOW_CHAT:    return "ready to chat";
  default:                         return "offline";
  }
}

std::string GetWord(const std::vector<std::string>& words,
                    size_t index, const std::string& def) {
  if (words.size() > index) {
    return words[index];
  } else {
    return def;
  }
}

int GetInt(const std::vector<std::string>& words, size_t index, int def) {
  int val;
  if (words.size() > index && talk_base::FromString(words[index], &val)) {
    return val;
  } else {
    return def;
  }
}


}  // namespace

const char* CALL_COMMANDS =
"Available commands:\n"
"\n"
"  hangup  Ends the call.\n"
"  mute    Stops sending voice.\n"
"  unmute  Re-starts sending voice.\n"
"  dtmf    Sends a DTMF tone.\n"
"  quit    Quits the application.\n"
"";

const char* RECEIVE_COMMANDS =
"Available commands:\n"
"\n"
"  accept [bw] Accepts the incoming call and switches to it.\n"
"  reject  Rejects the incoming call and stays with the current call.\n"
"  quit    Quits the application.\n"
"";

const char* CONSOLE_COMMANDS =
"Available commands:\n"
"\n"
"  roster              Prints the online friends from your roster.\n"
"  friend user         Request to add a user to your roster.\n"
"  call [jid] [bw]     Initiates a call to the user[/room] with the\n"
"                      given JID and with optional bandwidth.\n"
"  vcall [jid] [bw]    Initiates a video call to the user[/room] with\n"
"                      the given JID and with optional bandwidth.\n"
"  voicemail [jid]     Leave a voicemail for the user with the given JID.\n"
"  join [room]         Joins a multi-user-chat.\n"
"  invite user [room]  Invites a friend to a multi-user-chat.\n"
"  leave [room]        Leaves a multi-user-chat.\n"
"  getdevs             Prints the available media devices.\n"
"  quit                Quits the application.\n"
"";

void CallClient::ParseLine(const std::string& line) {
  std::vector<std::string> words;
  int start = -1;
  int state = 0;
  for (int index = 0; index <= static_cast<int>(line.size()); ++index) {
    if (state == 0) {
      if (!isspace(line[index])) {
        start = index;
        state = 1;
      }
    } else {
      ASSERT(state == 1);
      ASSERT(start >= 0);
      if (isspace(line[index])) {
        std::string word(line, start, index - start);
        words.push_back(word);
        start = -1;
        state = 0;
      }
    }
  }

  // Global commands
  const std::string& command = GetWord(words, 0, "");
  if (command == "quit") {
    Quit();
  } else if (call_ && incoming_call_) {
    if (command == "accept") {
      cricket::CallOptions options;
      options.video_bandwidth = GetInt(words, 1, cricket::kAutoBandwidth);
      Accept(options);
    } else if (command == "reject") {
      Reject();
    } else {
      console_->Print(RECEIVE_COMMANDS);
    }
  } else if (call_) {
    if (command == "hangup") {
      // TODO: do more shutdown here, move to Terminate()
      call_->Terminate();
      call_ = NULL;
      session_ = NULL;
      console_->SetPrompt(NULL);
    } else if (command == "mute") {
      call_->Mute(true);
    } else if (command == "unmute") {
      call_->Mute(false);
    } else if ((command == "dtmf") && (words.size() == 2)) {
      int ev = std::string("0123456789*#").find(words[1][0]);
      call_->PressDTMF(ev);
    } else {
      console_->Print(CALL_COMMANDS);
    }
  } else {
    if (command == "roster") {
      PrintRoster();
    } else if (command == "send") {
      buzz::Jid jid(words[1]);
      if (jid.IsValid()) {
        last_sent_to_ = words[1];
        SendChat(words[1], words[2]);
      } else if (!last_sent_to_.empty()) {
        SendChat(last_sent_to_, words[1]);
      } else {
        console_->Printf(
            "Invalid JID. JIDs should be in the form user@domain\n");
      }
    } else if ((words.size() == 2) && (command == "friend")) {
      InviteFriend(words[1]);
    } else if (command == "call") {
      std::string to = GetWord(words, 1, "");
      MakeCallTo(to, cricket::CallOptions());
    } else if (command == "vcall") {
      std::string to = GetWord(words, 1, "");
      int bandwidth = GetInt(words, 2, cricket::kAutoBandwidth);
      cricket::CallOptions options;
      options.is_video = true;
      options.video_bandwidth = bandwidth;
      MakeCallTo(to, options);
    } else if (command == "join") {
      JoinMuc(GetWord(words, 1, ""));
    } else if ((words.size() >= 2) && (command == "invite")) {
      InviteToMuc(words[1], GetWord(words, 2, ""));
    } else if (command == "leave") {
      LeaveMuc(GetWord(words, 1, ""));
    } else if (command == "getdevs") {
      GetDevices();
    } else if ((words.size() == 2) && (command == "setvol")) {
      SetVolume(words[1]);
    } else if (command == "voicemail") {
      CallVoicemail((words.size() >= 2) ? words[1] : "");
    } else {
      console_->Print(CONSOLE_COMMANDS);
    }
  }
}

CallClient::CallClient(buzz::XmppClient* xmpp_client)
    : xmpp_client_(xmpp_client),
      media_engine_(NULL),
      media_client_(NULL),
      call_(NULL),
      incoming_call_(false),
      auto_accept_(false),
      pmuc_domain_("groupchat.google.com"),
      local_renderer_(NULL),
      remote_renderer_(NULL),
      roster_(new RosterMap),
      portallocator_flags_(0),
      allow_local_ips_(false),
      initial_protocol_(cricket::PROTOCOL_HYBRID),
      secure_policy_(cricket::SEC_DISABLED) {
  xmpp_client_->SignalStateChange.connect(this, &CallClient::OnStateChange);
}

CallClient::~CallClient() {
  delete media_client_;
  delete roster_;
}

const std::string CallClient::strerror(buzz::XmppEngine::Error err) {
  switch (err) {
    case  buzz::XmppEngine::ERROR_NONE:
      return "";
    case  buzz::XmppEngine::ERROR_XML:
      return "Malformed XML or encoding error";
    case  buzz::XmppEngine::ERROR_STREAM:
      return "XMPP stream error";
    case  buzz::XmppEngine::ERROR_VERSION:
      return "XMPP version error";
    case  buzz::XmppEngine::ERROR_UNAUTHORIZED:
      return "User is not authorized (Check your username and password)";
    case  buzz::XmppEngine::ERROR_TLS:
      return "TLS could not be negotiated";
    case  buzz::XmppEngine::ERROR_AUTH:
      return "Authentication could not be negotiated";
    case  buzz::XmppEngine::ERROR_BIND:
      return "Resource or session binding could not be negotiated";
    case  buzz::XmppEngine::ERROR_CONNECTION_CLOSED:
      return "Connection closed by output handler.";
    case  buzz::XmppEngine::ERROR_DOCUMENT_CLOSED:
      return "Closed by </stream:stream>";
    case  buzz::XmppEngine::ERROR_SOCKET:
      return "Socket error";
    default:
      return "Unknown error";
  }
}

void CallClient::OnCallDestroy(cricket::Call* call) {
  if (call == call_) {
    if (remote_renderer_) {
      delete remote_renderer_;
      remote_renderer_ = NULL;
    }
    if (local_renderer_) {
      delete local_renderer_;
      local_renderer_ = NULL;
    }
    console_->SetPrompt(NULL);
    console_->Print("call destroyed");
    call_ = NULL;
    session_ = NULL;
  }
}

void CallClient::OnStateChange(buzz::XmppEngine::State state) {
  switch (state) {
  case buzz::XmppEngine::STATE_START:
    console_->Print("connecting...");
    break;

  case buzz::XmppEngine::STATE_OPENING:
    console_->Print("logging in...");
    break;

  case buzz::XmppEngine::STATE_OPEN:
    console_->Print("logged in...");
    InitPhone();
    InitPresence();
    break;

  case buzz::XmppEngine::STATE_CLOSED:
    buzz::XmppEngine::Error error = xmpp_client_->GetError(NULL);
    console_->Print("logged out..." + strerror(error));
    Quit();
  }
}

void CallClient::InitPhone() {
  std::string client_unique = xmpp_client_->jid().Str();
  talk_base::InitRandom(client_unique.c_str(), client_unique.size());

  worker_thread_ = new talk_base::Thread();
  // The worker thread must be started here since initialization of
  // the ChannelManager will generate messages that need to be
  // dispatched by it.
  worker_thread_->Start();

  // TODO: It looks like we are leaking many
  // objects. E.g. |network_manager_| and |socket_factory_| are never
  // deleted.

  network_manager_ = new talk_base::NetworkManager();
  socket_factory_ = new talk_base::BasicPacketSocketFactory(worker_thread_);

  // TODO: Decide if the relay address should be specified here.
  talk_base::SocketAddress stun_addr("stun.l.google.com", 19302);
  port_allocator_ = new cricket::BasicPortAllocator(
      network_manager_, socket_factory_, stun_addr,
      talk_base::SocketAddress(), talk_base::SocketAddress(),
      talk_base::SocketAddress());

  if (portallocator_flags_ != 0) {
    port_allocator_->set_flags(portallocator_flags_);
  }
  session_manager_ = new cricket::SessionManager(
      port_allocator_, worker_thread_);
  session_manager_->SignalRequestSignaling.connect(
      this, &CallClient::OnRequestSignaling);
  session_manager_->SignalSessionCreate.connect(
      this, &CallClient::OnSessionCreate);
  session_manager_->OnSignalingReady();

  session_manager_task_ =
      new cricket::SessionManagerTask(xmpp_client_, session_manager_);
  session_manager_task_->EnableOutgoingMessages();
  session_manager_task_->Start();

  if (!media_engine_) {
    media_engine_ = cricket::MediaEngine::Create();
  }

  media_client_ = new cricket::MediaSessionClient(
      xmpp_client_->jid(),
      session_manager_,
      media_engine_,
      new cricket::DeviceManager());
  media_client_->SignalCallCreate.connect(this, &CallClient::OnCallCreate);
  media_client_->SignalDevicesChange.connect(this,
                                             &CallClient::OnDevicesChange);
  media_client_->set_secure(secure_policy_);
}

void CallClient::OnRequestSignaling() {
  session_manager_->OnSignalingReady();
}

void CallClient::OnSessionCreate(cricket::Session* session, bool initiate) {
  session->set_allow_local_ips(allow_local_ips_);
  session->set_current_protocol(initial_protocol_);
}

void CallClient::OnCallCreate(cricket::Call* call) {
  call->SignalSessionState.connect(this, &CallClient::OnSessionState);
  if (call->video()) {
    local_renderer_ = new NullRenderer("local");
    remote_renderer_ = new NullRenderer("remote");
  }
}

void CallClient::OnSessionState(cricket::Call* call,
                                cricket::BaseSession* session,
                                cricket::BaseSession::State state) {
  if (state == cricket::Session::STATE_RECEIVEDINITIATE) {
    buzz::Jid jid(session->remote_name());
    console_->Printf("Incoming call from '%s'", jid.Str().c_str());
    call_ = call;
    session_ = session;
    incoming_call_ = true;
    cricket::CallOptions options;
    if (auto_accept_) {
      Accept(options);
    }
  } else if (state == cricket::Session::STATE_SENTINITIATE) {
    console_->Print("calling...");
  } else if (state == cricket::Session::STATE_RECEIVEDACCEPT) {
    console_->Print("call answered");
  } else if (state == cricket::Session::STATE_RECEIVEDREJECT) {
    console_->Print("call not answered");
  } else if (state == cricket::Session::STATE_INPROGRESS) {
    console_->Print("call in progress");
  } else if (state == cricket::Session::STATE_RECEIVEDTERMINATE) {
    console_->Print("other side hung up");
  }
}

void CallClient::InitPresence() {
  presence_push_ = new buzz::PresencePushTask(xmpp_client_, this);
  presence_push_->SignalStatusUpdate.connect(
    this, &CallClient::OnStatusUpdate);
  presence_push_->SignalMucJoined.connect(this, &CallClient::OnMucJoined);
  presence_push_->SignalMucLeft.connect(this, &CallClient::OnMucLeft);
  presence_push_->SignalMucStatusUpdate.connect(
    this, &CallClient::OnMucStatusUpdate);
  presence_push_->Start();

  presence_out_ = new buzz::PresenceOutTask(xmpp_client_);
  RefreshStatus();
  presence_out_->Start();

  muc_invite_recv_ = new buzz::MucInviteRecvTask(xmpp_client_);
  muc_invite_recv_->SignalInviteReceived.connect(this,
      &CallClient::OnMucInviteReceived);
  muc_invite_recv_->Start();

  muc_invite_send_ = new buzz::MucInviteSendTask(xmpp_client_);
  muc_invite_send_->Start();

  friend_invite_send_ = new buzz::FriendInviteSendTask(xmpp_client_);
  friend_invite_send_->Start();
}

void CallClient::RefreshStatus() {
  int media_caps = media_client_->GetCapabilities();
  my_status_.set_jid(xmpp_client_->jid());
  my_status_.set_available(true);
  my_status_.set_show(buzz::Status::SHOW_ONLINE);
  my_status_.set_priority(0);
  my_status_.set_know_capabilities(true);
  my_status_.set_pmuc_capability(true);
  my_status_.set_phone_capability(
      (media_caps & cricket::MediaEngine::AUDIO_RECV) != 0);
  my_status_.set_video_capability(
      (media_caps & cricket::MediaEngine::VIDEO_RECV) != 0);
  my_status_.set_camera_capability(
      (media_caps & cricket::MediaEngine::VIDEO_SEND) != 0);
  my_status_.set_is_google_client(true);
  my_status_.set_version("1.0.0.67");
  presence_out_->Send(my_status_);
}

void CallClient::OnStatusUpdate(const buzz::Status& status) {
  RosterItem item;
  item.jid = status.jid();
  item.show = status.show();
  item.status = status.status();

  std::string key = item.jid.Str();

  if (status.available() && status.phone_capability()) {
     console_->Printf("Adding to roster: %s", key.c_str());
    (*roster_)[key] = item;
  } else {
    console_->Printf("Removing from roster: %s", key.c_str());
    RosterMap::iterator iter = roster_->find(key);
    if (iter != roster_->end())
      roster_->erase(iter);
  }
}

void CallClient::PrintRoster() {
  console_->SetPrompting(false);
  console_->Printf("Roster contains %d callable", roster_->size());
  RosterMap::iterator iter = roster_->begin();
  while (iter != roster_->end()) {
    console_->Printf("%s - %s",
                     iter->second.jid.BareJid().Str().c_str(),
                     DescribeStatus(iter->second.show, iter->second.status));
    iter++;
  }
  console_->SetPrompting(true);
}

void CallClient::SendChat(const std::string& to, const std::string msg) {
  buzz::XmlElement* stanza = new buzz::XmlElement(buzz::QN_MESSAGE);
  stanza->AddAttr(buzz::QN_TO, to);
  stanza->AddAttr(buzz::QN_ID, talk_base::CreateRandomString(16));
  stanza->AddAttr(buzz::QN_TYPE, "chat");
  buzz::XmlElement* body = new buzz::XmlElement(buzz::QN_BODY);
  body->SetBodyText(msg);
  stanza->AddElement(body);

  xmpp_client_->SendStanza(stanza);
  delete stanza;
}

void CallClient::InviteFriend(const std::string& name) {
  buzz::Jid jid(name);
  if (!jid.IsValid() || jid.node() == "") {
    console_->Printf("Invalid JID. JIDs should be in the form user@domain\n");
    return;
  }
  // Note: for some reason the Buzz backend does not forward our presence
  // subscription requests to the end user when that user is another call
  // client as opposed to a Smurf user. Thus, in that scenario, you must
  // run the friend command as the other user too to create the linkage
  // (and you won't be notified to do so).
  friend_invite_send_->Send(jid);
  console_->Printf("Requesting to befriend %s.\n", name.c_str());
}

void CallClient::MakeCallTo(const std::string& name,
                            const cricket::CallOptions& given_options) {
  // Copy so we can change .is_muc.
  cricket::CallOptions options = given_options;

  bool found = false;
  options.is_muc = false;
  buzz::Jid callto_jid(name);
  buzz::Jid found_jid;
  if (name.length() == 0 && mucs_.size() > 0) {
    // if no name, and in a MUC, establish audio with the MUC
    found_jid = mucs_.begin()->first;
    found = true;
    options.is_muc = true;
  } else if (name[0] == '+') {
    // if the first character is a +, assume it's a phone number
    found_jid = callto_jid;
    found = true;
  } else if (callto_jid.resource() == "voicemail") {
    // if the resource is /voicemail, allow that
    found_jid = callto_jid;
    found = true;
  } else {
    // otherwise, it's a friend
    for (RosterMap::iterator iter = roster_->begin();
         iter != roster_->end(); ++iter) {
      if (iter->second.jid.BareEquals(callto_jid)) {
        found = true;
        found_jid = iter->second.jid;
        break;
      }
    }

    if (!found) {
      if (mucs_.count(callto_jid) == 1 &&
          mucs_[callto_jid]->state() == buzz::Muc::MUC_JOINED) {
        found = true;
        found_jid = callto_jid;
        options.is_muc = true;
      }
    }
  }

  if (found) {
    console_->Printf("Found %s '%s'", options.is_muc ? "room" : "online friend",
        found_jid.Str().c_str());
    PlaceCall(found_jid, options);
  } else {
    console_->Printf("Could not find online friend '%s'", name.c_str());
  }
}

void CallClient::PlaceCall(const buzz::Jid& jid,
                           const cricket::CallOptions& options) {
  media_client_->SignalCallDestroy.connect(
      this, &CallClient::OnCallDestroy);
  if (!call_) {
    call_ = media_client_->CreateCall();
    console_->SetPrompt(jid.Str().c_str());
    session_ = call_->InitiateSession(jid, options);
    if (options.is_muc) {
      // If people in this room are already in a call, must add all their
      // streams.
      buzz::Muc::MemberMap& members = mucs_[jid]->members();
      for (buzz::Muc::MemberMap::iterator elem = members.begin();
           elem != members.end();
           ++elem) {
        AddStream(elem->second.audio_src_id(), elem->second.video_src_id());
      }
    }
  }
  media_client_->SetFocus(call_);
  if (call_->video()) {
    call_->SetLocalRenderer(local_renderer_);
    // TODO: Call this once for every different remote SSRC
    // once we get to testing multiway video.
    call_->SetVideoRenderer(session_, 0, remote_renderer_);
  }
}

void CallClient::CallVoicemail(const std::string& name) {
  buzz::Jid jid(name);
  if (!jid.IsValid() || jid.node() == "") {
    console_->Printf("Invalid JID. JIDs should be in the form user@domain\n");
    return;
  }
  buzz::VoicemailJidRequester *request =
    new buzz::VoicemailJidRequester(xmpp_client_, jid, my_status_.jid());
  request->SignalGotVoicemailJid.connect(this,
                                         &CallClient::OnFoundVoicemailJid);
  request->SignalVoicemailJidError.connect(this,
                                           &CallClient::OnVoicemailJidError);
  request->Start();
}

void CallClient::OnFoundVoicemailJid(const buzz::Jid& to,
                                     const buzz::Jid& voicemail) {
  console_->Printf("Calling %s's voicemail.\n", to.Str().c_str());
  PlaceCall(voicemail, cricket::CallOptions());
}

void CallClient::OnVoicemailJidError(const buzz::Jid& to) {
  console_->Printf("Unable to voicemail %s.\n", to.Str().c_str());
}

void CallClient::AddStream(uint32 audio_src_id, uint32 video_src_id) {
  if (audio_src_id || video_src_id) {
    console_->Printf("Adding stream (%u, %u)\n", audio_src_id, video_src_id);
    call_->AddStream(session_, audio_src_id, video_src_id);
  }
}

void CallClient::RemoveStream(uint32 audio_src_id, uint32 video_src_id) {
  if (audio_src_id || video_src_id) {
    console_->Printf("Removing stream (%u, %u)\n", audio_src_id, video_src_id);
    call_->RemoveStream(session_, audio_src_id, video_src_id);
  }
}

void CallClient::Accept(const cricket::CallOptions& options) {
  ASSERT(call_ && incoming_call_);
  ASSERT(call_->sessions().size() == 1);
  call_->AcceptSession(call_->sessions()[0], options);
  media_client_->SetFocus(call_);
  if (call_->video()) {
    call_->SetLocalRenderer(local_renderer_);
    // The client never does an accept for multiway, so this must be 1:1,
    // so there's no SSRC.
    call_->SetVideoRenderer(session_, 0, remote_renderer_);
  }
  incoming_call_ = false;
}

void CallClient::Reject() {
  ASSERT(call_ && incoming_call_);
  call_->RejectSession(call_->sessions()[0]);
  incoming_call_ = false;
}

void CallClient::Quit() {
  talk_base::Thread::Current()->Quit();
}

void CallClient::JoinMuc(const std::string& room) {
  buzz::Jid room_jid;
  if (room.length() > 0) {
    room_jid = buzz::Jid(room);
  } else {
    // generate a GUID of the form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX,
    // for an eventual JID of private-chat-<GUID>@groupchat.google.com
    char guid[37], guid_room[256];
    for (size_t i = 0; i < ARRAY_SIZE(guid) - 1;) {
      if (i == 8 || i == 13 || i == 18 || i == 23) {
        guid[i++] = '-';
      } else {
        sprintf(guid + i, "%04x", rand());
        i += 4;
      }
    }

    talk_base::sprintfn(guid_room, ARRAY_SIZE(guid_room),
                        "private-chat-%s@%s", guid, pmuc_domain_.c_str());
    room_jid = buzz::Jid(guid_room);
  }

  if (!room_jid.IsValid()) {
    console_->Printf("Unable to make valid muc endpoint for %s", room.c_str());
    return;
  }

  MucMap::iterator elem = mucs_.find(room_jid);
  if (elem != mucs_.end()) {
    console_->Printf("This MUC already exists.");
    return;
  }

  buzz::Muc* muc = new buzz::Muc(room_jid, xmpp_client_->jid().node());
  mucs_[room_jid] = muc;
  presence_out_->SendDirected(muc->local_jid(), my_status_);
}

void CallClient::OnMucInviteReceived(const buzz::Jid& inviter,
    const buzz::Jid& room,
    const std::vector<buzz::AvailableMediaEntry>& avail) {

  console_->Printf("Invited to join %s by %s.\n", room.Str().c_str(),
      inviter.Str().c_str());
  console_->Printf("Available media:\n");
  if (avail.size() > 0) {
    for (std::vector<buzz::AvailableMediaEntry>::const_iterator i =
            avail.begin();
        i != avail.end();
        ++i) {
      console_->Printf("  %s, %s\n",
          buzz::AvailableMediaEntry::TypeAsString(i->type),
          buzz::AvailableMediaEntry::StatusAsString(i->status));
    }
  } else {
    console_->Printf("  None\n");
  }
  // We automatically join the room.
  JoinMuc(room.Str());
}

void CallClient::OnMucJoined(const buzz::Jid& endpoint) {
  MucMap::iterator elem = mucs_.find(endpoint);
  ASSERT(elem != mucs_.end() &&
         elem->second->state() == buzz::Muc::MUC_JOINING);

  buzz::Muc* muc = elem->second;
  muc->set_state(buzz::Muc::MUC_JOINED);
  console_->Printf("Joined \"%s\"", muc->jid().Str().c_str());
}

void CallClient::OnMucStatusUpdate(const buzz::Jid& jid,
    const buzz::MucStatus& status) {

  // Look up this muc.
  MucMap::iterator elem = mucs_.find(jid);
  ASSERT(elem != mucs_.end() &&
         elem->second->state() == buzz::Muc::MUC_JOINED);

  buzz::Muc* muc = elem->second;

  if (status.jid().IsBare() || status.jid() == muc->local_jid()) {
    // We are only interested in status about other users.
    return;
  }

  if (!status.available()) {
    // User is leaving the room.
    buzz::Muc::MemberMap::iterator elem =
      muc->members().find(status.jid().resource());

    ASSERT(elem != muc->members().end());

    // If user had src-ids, they have the left the room without explicitly
    // hanging-up; must tear down the stream if in a call to this room.
    if (call_ && session_->remote_name() == muc->jid().Str()) {
      RemoveStream(elem->second.audio_src_id(), elem->second.video_src_id());
    }

    // Remove them from the room.
    muc->members().erase(elem);
  } else {
    // Either user has joined or something changed about them.
    // Note: The [] operator here will create a new entry if it does not
    // exist, which is what we want.
    buzz::MucStatus& member_status(
        muc->members()[status.jid().resource()]);
    if (call_ && session_->remote_name() == muc->jid().Str()) {
      // We are in a call to this muc. Must potentially update our streams.
      // The following code will correctly update our streams regardless of
      // whether the SSRCs have been removed, added, or changed and regardless
      // of whether that has been done to both or just one. This relies on the
      // fact that AddStream/RemoveStream do nothing for SSRC arguments that are
      // zero.
      uint32 remove_audio_src_id = 0;
      uint32 remove_video_src_id = 0;
      uint32 add_audio_src_id = 0;
      uint32 add_video_src_id = 0;
      if (member_status.audio_src_id() != status.audio_src_id()) {
        remove_audio_src_id = member_status.audio_src_id();
        add_audio_src_id = status.audio_src_id();
      }
      if (member_status.video_src_id() != status.video_src_id()) {
        remove_video_src_id = member_status.video_src_id();
        add_video_src_id = status.video_src_id();
      }
      // Remove the old SSRCs, if any.
      RemoveStream(remove_audio_src_id, remove_video_src_id);
      // Add the new SSRCs, if any.
      AddStream(add_audio_src_id, add_video_src_id);
    }
    // Update the status. This will use the compiler-generated copy
    // constructor, which is perfectly adequate for this class.
    member_status = status;
  }
}

void CallClient::LeaveMuc(const std::string& room) {
  buzz::Jid room_jid;
  if (room.length() > 0) {
    room_jid = buzz::Jid(room);
  } else if (mucs_.size() > 0) {
    // leave the first MUC if no JID specified
    room_jid = mucs_.begin()->first;
  }

  if (!room_jid.IsValid()) {
    console_->Printf("Invalid MUC JID.");
    return;
  }

  MucMap::iterator elem = mucs_.find(room_jid);
  if (elem == mucs_.end()) {
    console_->Printf("No such MUC.");
    return;
  }

  buzz::Muc* muc = elem->second;
  muc->set_state(buzz::Muc::MUC_LEAVING);

  buzz::Status status;
  status.set_jid(my_status_.jid());
  status.set_available(false);
  status.set_priority(0);
  presence_out_->SendDirected(muc->local_jid(), status);
}

void CallClient::OnMucLeft(const buzz::Jid& endpoint, int error) {
  // We could be kicked from a room from any state.  We would hope this
  // happens While in the MUC_LEAVING state
  MucMap::iterator elem = mucs_.find(endpoint);
  if (elem == mucs_.end())
    return;

  buzz::Muc* muc = elem->second;
  if (muc->state() == buzz::Muc::MUC_JOINING) {
    console_->Printf("Failed to join \"%s\", code=%d",
                     muc->jid().Str().c_str(), error);
  } else if (muc->state() == buzz::Muc::MUC_JOINED) {
    console_->Printf("Kicked from \"%s\"",
                     muc->jid().Str().c_str());
  }

  delete muc;
  mucs_.erase(elem);
}

void CallClient::InviteToMuc(const std::string& user, const std::string& room) {
  // First find the room.
  const buzz::Muc* found_muc;
  if (room.length() == 0) {
    if (mucs_.size() == 0) {
      console_->Printf("Not in a room yet; can't invite.\n");
      return;
    }
    // Invite to the first muc
    found_muc = mucs_.begin()->second;
  } else {
    MucMap::iterator elem = mucs_.find(buzz::Jid(room));
    if (elem == mucs_.end()) {
      console_->Printf("Not in room %s.\n", room.c_str());
      return;
    }
    found_muc = elem->second;
  }
  // Now find the user. We invite all of their resources.
  bool found_user = false;
  buzz::Jid user_jid(user);
  for (RosterMap::iterator iter = roster_->begin();
       iter != roster_->end(); ++iter) {
    if (iter->second.jid.BareEquals(user_jid)) {
      muc_invite_send_->Send(iter->second.jid, *found_muc);
      found_user = true;
    }
  }
  if (!found_user) {
    console_->Printf("No such friend as %s.\n", user.c_str());
    return;
  }
}

void CallClient::GetDevices() {
  std::vector<std::string> names;
  media_client_->GetAudioInputDevices(&names);
  printf("Audio input devices:\n");
  PrintDevices(names);
  media_client_->GetAudioOutputDevices(&names);
  printf("Audio output devices:\n");
  PrintDevices(names);
  media_client_->GetVideoCaptureDevices(&names);
  printf("Video capture devices:\n");
  PrintDevices(names);
}

void CallClient::PrintDevices(const std::vector<std::string>& names) {
  for (size_t i = 0; i < names.size(); ++i) {
    printf("%d: %s\n", static_cast<int>(i), names[i].c_str());
  }
}

void CallClient::OnDevicesChange() {
  printf("Devices changed.\n");
  RefreshStatus();
}

void CallClient::SetVolume(const std::string& level) {
  media_client_->SetOutputVolume(strtol(level.c_str(), NULL, 10));
}