Move jingle examples from talk/ into webrtc/libjingle.  This is part of the effor to move Jingle out of WebRTC and into its own repository.

R=juberti@webrtc.org, pbos@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/32849004

git-svn-id: http://webrtc.googlecode.com/svn/trunk/webrtc@7936 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/libjingle/examples/call/Info.plist b/libjingle/examples/call/Info.plist
new file mode 100644
index 0000000..a59cfa5
--- /dev/null
+++ b/libjingle/examples/call/Info.plist
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleIdentifier</key>
+  <string>com.google.call</string>
+  <key>CFBundleName</key>
+  <string>call</string>
+</dict>
+</plist>
+
diff --git a/libjingle/examples/call/call_main.cc b/libjingle/examples/call/call_main.cc
new file mode 100644
index 0000000..7cdea85
--- /dev/null
+++ b/libjingle/examples/call/call_main.cc
@@ -0,0 +1,497 @@
+/*
+ * 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 <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#include <iomanip>
+#include <iostream>
+#include <vector>
+
+#include "webrtc/base/flags.h"
+#include "webrtc/base/logging.h"
+#ifdef OSX
+#include "webrtc/base/maccocoasocketserver.h"
+#endif
+#include "talk/examples/call/callclient.h"
+#include "talk/examples/call/console.h"
+#include "talk/examples/call/mediaenginefactory.h"
+#include "webrtc/p2p/base/constants.h"
+#include "webrtc/libjingle/media/mediasessionclient.h"
+#include "talk/session/media/srtpfilter.h"
+#include "webrtc/libjingle/xmpp/xmppauth.h"
+#include "webrtc/libjingle/xmpp/xmppclientsettings.h"
+#include "webrtc/libjingle/xmpp/xmpppump.h"
+#include "webrtc/libjingle/xmpp/xmppsocket.h"
+#include "webrtc/base/pathutils.h"
+#include "webrtc/base/ssladapter.h"
+#include "webrtc/base/stream.h"
+#include "webrtc/base/win32socketserver.h"
+
+class DebugLog : public sigslot::has_slots<> {
+ public:
+  DebugLog() :
+    debug_input_buf_(NULL), debug_input_len_(0), debug_input_alloc_(0),
+    debug_output_buf_(NULL), debug_output_len_(0), debug_output_alloc_(0),
+    censor_password_(false)
+      {}
+  char * debug_input_buf_;
+  int debug_input_len_;
+  int debug_input_alloc_;
+  char * debug_output_buf_;
+  int debug_output_len_;
+  int debug_output_alloc_;
+  bool censor_password_;
+
+  void Input(const char * data, int len) {
+    if (debug_input_len_ + len > debug_input_alloc_) {
+      char * old_buf = debug_input_buf_;
+      debug_input_alloc_ = 4096;
+      while (debug_input_alloc_ < debug_input_len_ + len) {
+        debug_input_alloc_ *= 2;
+      }
+      debug_input_buf_ = new char[debug_input_alloc_];
+      memcpy(debug_input_buf_, old_buf, debug_input_len_);
+      delete[] old_buf;
+    }
+    memcpy(debug_input_buf_ + debug_input_len_, data, len);
+    debug_input_len_ += len;
+    DebugPrint(debug_input_buf_, &debug_input_len_, false);
+  }
+
+  void Output(const char * data, int len) {
+    if (debug_output_len_ + len > debug_output_alloc_) {
+      char * old_buf = debug_output_buf_;
+      debug_output_alloc_ = 4096;
+      while (debug_output_alloc_ < debug_output_len_ + len) {
+        debug_output_alloc_ *= 2;
+      }
+      debug_output_buf_ = new char[debug_output_alloc_];
+      memcpy(debug_output_buf_, old_buf, debug_output_len_);
+      delete[] old_buf;
+    }
+    memcpy(debug_output_buf_ + debug_output_len_, data, len);
+    debug_output_len_ += len;
+    DebugPrint(debug_output_buf_, &debug_output_len_, true);
+  }
+
+  static bool IsAuthTag(const char * str, size_t len) {
+    if (str[0] == '<' && str[1] == 'a' &&
+                         str[2] == 'u' &&
+                         str[3] == 't' &&
+                         str[4] == 'h' &&
+                         str[5] <= ' ') {
+      std::string tag(str, len);
+
+      if (tag.find("mechanism") != std::string::npos)
+        return true;
+    }
+    return false;
+  }
+
+  void DebugPrint(char * buf, int * plen, bool output) {
+    int len = *plen;
+    if (len > 0) {
+      time_t tim = time(NULL);
+      struct tm * now = localtime(&tim);
+      char *time_string = asctime(now);
+      if (time_string) {
+        size_t time_len = strlen(time_string);
+        if (time_len > 0) {
+          time_string[time_len-1] = 0;    // trim off terminating \n
+        }
+      }
+      LOG(INFO) << (output ? "SEND >>>>>>>>>>>>>>>>" : "RECV <<<<<<<<<<<<<<<<")
+                << " : " << time_string;
+
+      bool indent;
+      int start = 0, nest = 3;
+      for (int i = 0; i < len; i += 1) {
+        if (buf[i] == '>') {
+          if ((i > 0) && (buf[i-1] == '/')) {
+            indent = false;
+          } else if ((start + 1 < len) && (buf[start + 1] == '/')) {
+            indent = false;
+            nest -= 2;
+          } else {
+            indent = true;
+          }
+
+          // Output a tag
+          LOG(INFO) << std::setw(nest) << " "
+                    << std::string(buf + start, i + 1 - start);
+
+          if (indent)
+            nest += 2;
+
+          // Note if it's a PLAIN auth tag
+          if (IsAuthTag(buf + start, i + 1 - start)) {
+            censor_password_ = true;
+          }
+
+          // incr
+          start = i + 1;
+        }
+
+        if (buf[i] == '<' && start < i) {
+          if (censor_password_) {
+            LOG(INFO) << std::setw(nest) << " " << "## TEXT REMOVED ##";
+            censor_password_ = false;
+          } else {
+            LOG(INFO) << std::setw(nest) << " "
+                      << std::string(buf + start, i - start);
+          }
+          start = i;
+        }
+      }
+      len = len - start;
+      memcpy(buf, buf + start, len);
+      *plen = len;
+    }
+  }
+};
+
+static DebugLog debug_log_;
+static const int DEFAULT_PORT = 5222;
+
+#ifdef ANDROID
+static std::vector<cricket::AudioCodec> codecs;
+static const cricket::AudioCodec ISAC(103, "ISAC", 40000, 16000, 1, 0);
+
+cricket::MediaEngineInterface *CreateAndroidMediaEngine() {
+    cricket::FakeMediaEngine *engine = new cricket::FakeMediaEngine();
+
+    codecs.push_back(ISAC);
+    engine->SetAudioCodecs(codecs);
+    return engine;
+}
+#endif
+
+// TODO: Move this into Console.
+void Print(const char* chars) {
+  printf("%s", chars);
+  fflush(stdout);
+}
+
+bool GetSecurePolicy(const std::string& in, cricket::SecurePolicy* out) {
+  if (in == "disable") {
+    *out = cricket::SEC_DISABLED;
+  } else if (in == "enable") {
+    *out = cricket::SEC_ENABLED;
+  } else if (in == "require") {
+    *out = cricket::SEC_REQUIRED;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+int main(int argc, char **argv) {
+  // This app has three threads. The main thread will run the XMPP client,
+  // which will print to the screen in its own thread. A second thread
+  // will get input from the console, parse it, and pass the appropriate
+  // message back to the XMPP client's thread. A third thread is used
+  // by MediaSessionClient as its worker thread.
+
+  // define options
+  DEFINE_string(s, "talk.google.com", "The connection server to use.");
+  DEFINE_string(tls, "require",
+      "Select connection encryption: disable, enable, require.");
+  DEFINE_bool(allowplain, false, "Allow plain authentication.");
+  DEFINE_bool(testserver, false, "Use test server.");
+  DEFINE_string(oauth, "", "OAuth2 access token.");
+  DEFINE_bool(a, false, "Turn on auto accept for incoming calls.");
+  DEFINE_string(signaling, "hybrid",
+      "Initial signaling protocol to use: jingle, gingle, or hybrid.");
+  DEFINE_string(transport, "hybrid",
+      "Initial transport protocol to use: ice, gice, or hybrid.");
+  DEFINE_string(sdes, "enable",
+      "Select SDES media encryption: disable, enable, require.");
+  DEFINE_string(dtls, "disable",
+      "Select DTLS transport encryption: disable, enable, require.");
+  DEFINE_int(portallocator, 0, "Filter out unwanted connection types.");
+  DEFINE_string(pmuc, "groupchat.google.com", "The persistant muc domain.");
+  DEFINE_string(capsnode, "http://code.google.com/p/libjingle/call",
+                "Caps node: A URI identifying the app.");
+  DEFINE_string(capsver, "0.6",
+                "Caps ver: A string identifying the version of the app.");
+  DEFINE_string(voiceinput, NULL, "RTP dump file for voice input.");
+  DEFINE_string(voiceoutput, NULL, "RTP dump file for voice output.");
+  DEFINE_string(videoinput, NULL, "RTP dump file for video input.");
+  DEFINE_string(videooutput, NULL, "RTP dump file for video output.");
+  DEFINE_bool(render, true, "Renders the video.");
+  DEFINE_string(datachannel, "",
+                "Enable a data channel, and choose the type: rtp or sctp.");
+  DEFINE_bool(d, false, "Turn on debugging.");
+  DEFINE_string(log, "", "Turn on debugging to a file.");
+  DEFINE_bool(debugsrtp, false, "Enable debugging for srtp.");
+  DEFINE_bool(help, false, "Prints this message");
+  DEFINE_bool(multisession, false,
+              "Enable support for multiple sessions in calls.");
+  DEFINE_bool(roster, false,
+      "Enable roster messages printed in console.");
+
+  // parse options
+  rtc::FlagList::SetFlagsFromCommandLine(&argc, argv, true);
+  if (FLAG_help) {
+    rtc::FlagList::Print(NULL, false);
+    return 0;
+  }
+
+  bool auto_accept = FLAG_a;
+  bool debug = FLAG_d;
+  std::string log = FLAG_log;
+  std::string signaling = FLAG_signaling;
+  std::string transport = FLAG_transport;
+  bool test_server = FLAG_testserver;
+  bool allow_plain = FLAG_allowplain;
+  std::string tls = FLAG_tls;
+  std::string oauth_token = FLAG_oauth;
+  int32 portallocator_flags = FLAG_portallocator;
+  std::string pmuc_domain = FLAG_pmuc;
+  std::string server = FLAG_s;
+  std::string sdes = FLAG_sdes;
+  std::string dtls = FLAG_dtls;
+  std::string caps_node = FLAG_capsnode;
+  std::string caps_ver = FLAG_capsver;
+  bool debugsrtp = FLAG_debugsrtp;
+  bool render = FLAG_render;
+  std::string data_channel = FLAG_datachannel;
+  bool multisession_enabled = FLAG_multisession;
+  rtc::SSLIdentity* ssl_identity = NULL;
+  bool show_roster_messages = FLAG_roster;
+
+  // Set up debugging.
+  if (debug) {
+    rtc::LogMessage::LogToDebug(rtc::LS_VERBOSE);
+  }
+
+  if (!log.empty()) {
+    rtc::StreamInterface* stream =
+        rtc::Filesystem::OpenFile(log, "a");
+    if (stream) {
+      rtc::LogMessage::LogToStream(stream, rtc::LS_VERBOSE);
+    } else {
+      Print(("Cannot open debug log " + log + "\n").c_str());
+      return 1;
+    }
+  }
+
+  if (debugsrtp) {
+    cricket::EnableSrtpDebugging();
+  }
+
+  // Set up the crypto subsystem.
+  rtc::InitializeSSL();
+
+  // Parse username and password, if present.
+  buzz::Jid jid;
+  std::string username;
+  rtc::InsecureCryptStringImpl pass;
+  if (argc > 1) {
+    username = argv[1];
+    if (argc > 2) {
+      pass.password() = argv[2];
+    }
+  }
+
+  if (username.empty()) {
+    Print("JID: ");
+    std::cin >> username;
+  }
+  if (username.find('@') == std::string::npos) {
+    username.append("@localhost");
+  }
+  jid = buzz::Jid(username);
+  if (!jid.IsValid() || jid.node() == "") {
+    Print("Invalid JID. JIDs should be in the form user@domain\n");
+    return 1;
+  }
+  if (pass.password().empty() && !test_server && oauth_token.empty()) {
+    Console::SetEcho(false);
+    Print("Password: ");
+    std::cin >> pass.password();
+    Console::SetEcho(true);
+    Print("\n");
+  }
+
+  // Decide on the connection settings.
+  buzz::XmppClientSettings xcs;
+  xcs.set_user(jid.node());
+  xcs.set_resource("call");
+  xcs.set_host(jid.domain());
+  xcs.set_allow_plain(allow_plain);
+
+  if (tls == "disable") {
+    xcs.set_use_tls(buzz::TLS_DISABLED);
+  } else if (tls == "enable") {
+    xcs.set_use_tls(buzz::TLS_ENABLED);
+  } else if (tls == "require") {
+    xcs.set_use_tls(buzz::TLS_REQUIRED);
+  } else {
+    Print("Invalid TLS option, must be enable, disable, or require.\n");
+    return 1;
+  }
+
+  if (test_server) {
+    pass.password() = jid.node();
+    xcs.set_allow_plain(true);
+    xcs.set_use_tls(buzz::TLS_DISABLED);
+    xcs.set_test_server_domain("google.com");
+  }
+  xcs.set_pass(rtc::CryptString(pass));
+  if (!oauth_token.empty()) {
+    xcs.set_auth_token(buzz::AUTH_MECHANISM_OAUTH2, oauth_token);
+  }
+
+  std::string host;
+  int port;
+
+  int colon = server.find(':');
+  if (colon == -1) {
+    host = server;
+    port = DEFAULT_PORT;
+  } else {
+    host = server.substr(0, colon);
+    port = atoi(server.substr(colon + 1).c_str());
+  }
+
+  xcs.set_server(rtc::SocketAddress(host, port));
+
+  // Decide on the signaling and crypto settings.
+  cricket::SignalingProtocol signaling_protocol = cricket::PROTOCOL_HYBRID;
+  if (signaling == "jingle") {
+    signaling_protocol = cricket::PROTOCOL_JINGLE;
+  } else if (signaling == "gingle") {
+    signaling_protocol = cricket::PROTOCOL_GINGLE;
+  } else if (signaling == "hybrid") {
+    signaling_protocol = cricket::PROTOCOL_HYBRID;
+  } else {
+    Print("Invalid signaling protocol.  Must be jingle, gingle, or hybrid.\n");
+    return 1;
+  }
+
+  cricket::TransportProtocol transport_protocol = cricket::ICEPROTO_HYBRID;
+  if (transport == "ice") {
+    transport_protocol = cricket::ICEPROTO_RFC5245;
+  } else if (transport == "gice") {
+    transport_protocol = cricket::ICEPROTO_GOOGLE;
+  } else if (transport == "hybrid") {
+    transport_protocol = cricket::ICEPROTO_HYBRID;
+  } else {
+    Print("Invalid transport protocol.  Must be ice, gice, or hybrid.\n");
+    return 1;
+  }
+
+  cricket::DataChannelType data_channel_type = cricket::DCT_NONE;
+  if (data_channel == "rtp") {
+    data_channel_type = cricket::DCT_RTP;
+  } else if (data_channel == "sctp") {
+    data_channel_type = cricket::DCT_SCTP;
+  } else if (!data_channel.empty()) {
+    Print("Invalid data channel type.  Must be rtp or sctp.\n");
+    return 1;
+  }
+
+  cricket::SecurePolicy sdes_policy, dtls_policy;
+  if (!GetSecurePolicy(sdes, &sdes_policy)) {
+    Print("Invalid SDES policy. Must be enable, disable, or require.\n");
+    return 1;
+  }
+  if (!GetSecurePolicy(dtls, &dtls_policy)) {
+    Print("Invalid DTLS policy. Must be enable, disable, or require.\n");
+    return 1;
+  }
+  if (dtls_policy != cricket::SEC_DISABLED) {
+    ssl_identity = rtc::SSLIdentity::Generate(jid.Str());
+    if (!ssl_identity) {
+      Print("Failed to generate identity for DTLS.\n");
+      return 1;
+    }
+  }
+
+#ifdef ANDROID
+  MediaEngineFactory::SetCreateFunction(&CreateAndroidMediaEngine);
+#endif
+
+#if WIN32
+  // Need to pump messages on our main thread on Windows.
+  rtc::Win32Thread w32_thread;
+  rtc::ThreadManager::Instance()->SetCurrentThread(&w32_thread);
+#endif
+  rtc::Thread* main_thread = rtc::Thread::Current();
+#ifdef OSX
+  rtc::MacCocoaSocketServer ss;
+  rtc::SocketServerScope ss_scope(&ss);
+#endif
+
+  buzz::XmppPump pump;
+  CallClient *client = new CallClient(pump.client(), caps_node, caps_ver);
+
+  if (FLAG_voiceinput || FLAG_voiceoutput ||
+      FLAG_videoinput || FLAG_videooutput) {
+    // If any dump file is specified, we use a FileMediaEngine.
+    cricket::MediaEngineInterface* engine =
+        MediaEngineFactory::CreateFileMediaEngine(
+            FLAG_voiceinput, FLAG_voiceoutput,
+            FLAG_videoinput, FLAG_videooutput);
+    client->SetMediaEngine(engine);
+  }
+
+  Console *console = new Console(main_thread, client);
+  client->SetConsole(console);
+  client->SetAutoAccept(auto_accept);
+  client->SetPmucDomain(pmuc_domain);
+  client->SetPortAllocatorFlags(portallocator_flags);
+  client->SetAllowLocalIps(true);
+  client->SetSignalingProtocol(signaling_protocol);
+  client->SetTransportProtocol(transport_protocol);
+  client->SetSecurePolicy(sdes_policy, dtls_policy);
+  client->SetSslIdentity(ssl_identity);
+  client->SetRender(render);
+  client->SetDataChannelType(data_channel_type);
+  client->SetMultiSessionEnabled(multisession_enabled);
+  client->SetShowRosterMessages(show_roster_messages);
+  console->Start();
+
+  if (debug) {
+    pump.client()->SignalLogInput.connect(&debug_log_, &DebugLog::Input);
+    pump.client()->SignalLogOutput.connect(&debug_log_, &DebugLog::Output);
+  }
+
+  Print(("Logging in to " + server + " as " + jid.Str() + "\n").c_str());
+  pump.DoLogin(xcs, new buzz::XmppSocket(buzz::TLS_REQUIRED), new XmppAuth());
+  main_thread->Run();
+  pump.DoDisconnect();
+
+  console->Stop();
+  delete console;
+  delete client;
+
+  return 0;
+}
diff --git a/libjingle/examples/call/call_unittest.cc b/libjingle/examples/call/call_unittest.cc
new file mode 100644
index 0000000..cc2878f
--- /dev/null
+++ b/libjingle/examples/call/call_unittest.cc
@@ -0,0 +1,37 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+// Main function for all unit tests in talk/examples/call
+
+#include "testing/base/public/gunit.h"
+#include "webrtc/base/logging.h"
+
+int main(int argc, char **argv) {
+  rtc::LogMessage::LogToDebug(rtc::LogMessage::NO_LOGGING);
+  testing::ParseGUnitFlags(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/libjingle/examples/call/callclient.cc b/libjingle/examples/call/callclient.cc
new file mode 100644
index 0000000..6eae983
--- /dev/null
+++ b/libjingle/examples/call/callclient.cc
@@ -0,0 +1,1616 @@
+/*
+ * 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/examples/call/console.h"
+#include "talk/examples/call/friendinvitesendtask.h"
+#include "talk/examples/call/muc.h"
+#include "talk/examples/call/mucinviterecvtask.h"
+#include "talk/examples/call/mucinvitesendtask.h"
+#include "talk/examples/call/presencepushtask.h"
+#include "talk/media/base/mediacommon.h"
+#include "talk/media/base/mediaengine.h"
+#include "talk/media/base/rtpdataengine.h"
+#include "talk/media/base/screencastid.h"
+#include "webrtc/base/helpers.h"
+#include "webrtc/base/logging.h"
+#include "webrtc/base/network.h"
+#include "webrtc/base/socketaddress.h"
+#include "webrtc/base/stringencode.h"
+#include "webrtc/base/stringutils.h"
+#include "webrtc/base/thread.h"
+#include "webrtc/base/windowpickerfactory.h"
+#ifdef HAVE_SCTP
+#include "talk/media/sctp/sctpdataengine.h"
+#endif
+#include "talk/media/base/videorenderer.h"
+#include "talk/media/devices/devicemanager.h"
+#include "talk/media/devices/videorendererfactory.h"
+#include "webrtc/libjingle/session/sessionmanager.h"
+#include "webrtc/p2p/client/basicportallocator.h"
+#include "webrtc/p2p/client/sessionmanagertask.h"
+#include "webrtc/libjingle/media/mediamessages.h"
+#include "webrtc/libjingle/media/mediasessionclient.h"
+#include "webrtc/libjingle/xmpp/constants.h"
+#include "webrtc/libjingle/xmpp/hangoutpubsubclient.h"
+#include "webrtc/libjingle/xmpp/mucroomconfigtask.h"
+#include "webrtc/libjingle/xmpp/mucroomlookuptask.h"
+#include "webrtc/libjingle/xmpp/pingtask.h"
+#include "webrtc/libjingle/xmpp/presenceouttask.h"
+
+namespace {
+
+// Must be period >= timeout.
+const uint32 kPingPeriodMillis = 10000;
+const uint32 kPingTimeoutMillis = 10000;
+
+const char* DescribeStatus(buzz::PresenceStatus::Show show,
+                           const std::string& desc) {
+  switch (show) {
+  case buzz::PresenceStatus::SHOW_XA:      return desc.c_str();
+  case buzz::PresenceStatus::SHOW_ONLINE:  return "online";
+  case buzz::PresenceStatus::SHOW_AWAY:    return "away";
+  case buzz::PresenceStatus::SHOW_DND:     return "do not disturb";
+  case buzz::PresenceStatus::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 && rtc::FromString(words[index], &val)) {
+    return val;
+  } else {
+    return def;
+  }
+}
+
+}  // namespace
+
+const char* CALL_COMMANDS =
+"Available commands:\n"
+"\n"
+"  hangup            Ends the call.\n"
+"  hold              Puts the current call on hold\n"
+"  calls             Lists the current calls and their sessions\n"
+"  switch [call_id]  Switch to the specified call\n"
+"  addsession [jid]  Add a new session to the current call.\n"
+"  rmsession [sid]   Remove specified session.\n"
+"  mute              Stops sending voice.\n"
+"  unmute            Re-starts sending voice.\n"
+"  vmute             Stops sending video.\n"
+"  vunmute           Re-starts sending video.\n"
+"  dtmf              Sends a DTMF tone.\n"
+"  stats             Print voice stats for the current call.\n"
+"  quit              Quits the application.\n"
+"";
+
+// TODO: Make present and record really work.
+const char* HANGOUT_COMMANDS =
+"Available MUC commands:\n"
+"\n"
+"  present    Starts presenting (just signalling; not actually presenting.)\n"
+"  unpresent  Stops presenting (just signalling; not actually presenting.)\n"
+"  record     Starts recording (just signalling; not actually recording.)\n"
+"  unrecord   Stops recording (just signalling; not actually recording.)\n"
+"  rmute [nick] Remote mute another participant.\n"
+"  block [nick] Block another participant.\n"
+"  screencast [fps] Starts screencast. \n"
+"  unscreencast Stops screencast. \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"
+"  calls               Lists the current calls\n"
+"  switch [call_id]    Switch to the specified call\n"
+"  join [room_jid]     Joins a multi-user-chat with room JID.\n"
+"  ljoin [room_name]   Joins a MUC by looking up JID from room name.\n"
+"  invite user [room]  Invites a friend to a multi-user-chat.\n"
+"  leave [room]        Leaves a multi-user-chat.\n"
+"  nick [nick]         Sets the nick.\n"
+"  priority [int]      Sets the priority.\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);
+      options.has_video = true;
+      options.data_channel_type = data_channel_type_;
+      Accept(options);
+    } else if (command == "reject") {
+      Reject();
+    } else {
+      console_->PrintLine(RECEIVE_COMMANDS);
+    }
+  } else if (call_) {
+    if (command == "hangup") {
+      call_->Terminate();
+    } else if (command == "hold") {
+      media_client_->SetFocus(NULL);
+      call_ = NULL;
+    } else if (command == "addsession") {
+      std::string to = GetWord(words, 1, "");
+      cricket::CallOptions options;
+      options.has_video = call_->has_video();
+      options.video_bandwidth = cricket::kAutoBandwidth;
+      options.data_channel_type = data_channel_type_;
+      options.AddStream(cricket::MEDIA_TYPE_VIDEO, "", "");
+      if (!InitiateAdditionalSession(to, options)) {
+        console_->PrintLine("Failed to initiate additional session.");
+      }
+    } else if (command == "rmsession") {
+      std::string id = GetWord(words, 1, "");
+      TerminateAndRemoveSession(call_, id);
+    } else if (command == "calls") {
+      PrintCalls();
+    } else if ((words.size() == 2) && (command == "switch")) {
+      SwitchToCall(GetInt(words, 1, -1));
+    } else if (command == "mute") {
+      call_->Mute(true);
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishAudioMuteState(true);
+      }
+    } else if (command == "unmute") {
+      call_->Mute(false);
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishAudioMuteState(false);
+      }
+    } else if (command == "vmute") {
+      call_->MuteVideo(true);
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishVideoMuteState(true);
+      }
+    } else if (command == "vunmute") {
+      call_->MuteVideo(false);
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishVideoMuteState(false);
+      }
+    } else if (command == "screencast") {
+      if (screencast_ssrc_ != 0) {
+        console_->PrintLine("Can't screencast twice.  Unscreencast first.");
+      } else {
+        std::string streamid = "screencast";
+        screencast_ssrc_ = rtc::CreateRandomId();
+        int fps = GetInt(words, 1, 5);  // Default to 5 fps.
+
+        cricket::ScreencastId screencastid;
+        cricket::Session* session = GetFirstSession();
+        if (session && SelectFirstDesktopScreencastId(&screencastid)) {
+          call_->StartScreencast(
+              session, streamid, screencast_ssrc_, screencastid, fps);
+        }
+      }
+    } else if (command == "unscreencast") {
+      // TODO: Use a random ssrc
+      std::string streamid = "screencast";
+
+      cricket::Session* session = GetFirstSession();
+      if (session) {
+        call_->StopScreencast(session, streamid, screencast_ssrc_);
+        screencast_ssrc_ = 0;
+      }
+    } else if (command == "present") {
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishPresenterState(true);
+      }
+    } else if (command == "unpresent") {
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishPresenterState(false);
+      }
+    } else if (command == "record") {
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishRecordingState(true);
+      }
+    } else if (command == "unrecord") {
+      if (InMuc()) {
+        hangout_pubsub_client_->PublishRecordingState(false);
+      }
+    } else if ((command == "rmute") && (words.size() == 2)) {
+      if (InMuc()) {
+        const std::string& nick = words[1];
+        hangout_pubsub_client_->RemoteMute(nick);
+      }
+    } else if ((command == "block") && (words.size() == 2)) {
+      if (InMuc()) {
+        const std::string& nick = words[1];
+        hangout_pubsub_client_->BlockMedia(nick);
+      }
+    } else if (command == "senddata") {
+      // "" is the default streamid.
+      SendData("", words[1]);
+    } else if ((command == "dtmf") && (words.size() == 2)) {
+      int ev = std::string("0123456789*#").find(words[1][0]);
+      call_->PressDTMF(ev);
+    } else if (command == "stats") {
+      PrintStats();
+    } else {
+      console_->PrintLine(CALL_COMMANDS);
+      if (InMuc()) {
+        console_->PrintLine(HANGOUT_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_->PrintLine(
+            "Invalid JID. JIDs should be in the form user@domain");
+      }
+    } else if ((words.size() == 2) && (command == "friend")) {
+      InviteFriend(words[1]);
+    } else if (command == "call") {
+      std::string to = GetWord(words, 1, "");
+      cricket::CallOptions options;
+      options.data_channel_type = data_channel_type_;
+      if (!PlaceCall(to, options)) {
+        console_->PrintLine("Failed to initiate call.");
+      }
+    } else if (command == "vcall") {
+      std::string to = GetWord(words, 1, "");
+      int bandwidth = GetInt(words, 2, cricket::kAutoBandwidth);
+      cricket::CallOptions options;
+      options.has_video = true;
+      options.video_bandwidth = bandwidth;
+      options.data_channel_type = data_channel_type_;
+      if (!PlaceCall(to, options)) {
+        console_->PrintLine("Failed to initiate call.");
+      }
+    } else if (command == "calls") {
+      PrintCalls();
+    } else if ((words.size() == 2) && (command == "switch")) {
+      SwitchToCall(GetInt(words, 1, -1));
+    } else if (command == "join") {
+      JoinMuc(GetWord(words, 1, ""));
+    } else if (command == "ljoin") {
+      LookupAndJoinMuc(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 == "nick") {
+      SetNick(GetWord(words, 1, ""));
+    } else if (command == "priority") {
+      int priority = GetInt(words, 1, 0);
+      SetPriority(priority);
+      SendStatus();
+    } else if (command == "getdevs") {
+      GetDevices();
+    } else if ((words.size() == 2) && (command == "setvol")) {
+      SetVolume(words[1]);
+    } else {
+      console_->PrintLine(CONSOLE_COMMANDS);
+    }
+  }
+}
+
+CallClient::CallClient(buzz::XmppClient* xmpp_client,
+                       const std::string& caps_node, const std::string& version)
+    : xmpp_client_(xmpp_client),
+      worker_thread_(NULL),
+      media_engine_(NULL),
+      data_engine_(NULL),
+      media_client_(NULL),
+      call_(NULL),
+      hangout_pubsub_client_(NULL),
+      incoming_call_(false),
+      auto_accept_(false),
+      pmuc_domain_("groupchat.google.com"),
+      render_(true),
+      data_channel_type_(cricket::DCT_NONE),
+      multisession_enabled_(false),
+      local_renderer_(NULL),
+      static_views_accumulated_count_(0),
+      screencast_ssrc_(0),
+      roster_(new RosterMap),
+      portallocator_flags_(0),
+      allow_local_ips_(false),
+      signaling_protocol_(cricket::PROTOCOL_HYBRID),
+      transport_protocol_(cricket::ICEPROTO_HYBRID),
+      sdes_policy_(cricket::SEC_DISABLED),
+      dtls_policy_(cricket::SEC_DISABLED),
+      ssl_identity_(),
+      show_roster_messages_(false) {
+  xmpp_client_->SignalStateChange.connect(this, &CallClient::OnStateChange);
+  my_status_.set_caps_node(caps_node);
+  my_status_.set_version(version);
+}
+
+CallClient::~CallClient() {
+  delete media_client_;
+  delete roster_;
+  delete worker_thread_;
+}
+
+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) {
+  RemoveCallsStaticRenderedViews(call);
+  if (call == call_) {
+    if (local_renderer_) {
+      delete local_renderer_;
+      local_renderer_ = NULL;
+    }
+    console_->PrintLine("call destroyed");
+    call_ = NULL;
+    delete hangout_pubsub_client_;
+    hangout_pubsub_client_ = NULL;
+  }
+}
+
+void CallClient::OnStateChange(buzz::XmppEngine::State state) {
+  switch (state) {
+    case buzz::XmppEngine::STATE_START:
+      console_->PrintLine("connecting...");
+      break;
+    case buzz::XmppEngine::STATE_OPENING:
+      console_->PrintLine("logging in...");
+      break;
+    case buzz::XmppEngine::STATE_OPEN:
+      console_->PrintLine("logged in...");
+      InitMedia();
+      InitPresence();
+      break;
+    case buzz::XmppEngine::STATE_CLOSED:
+      {
+        buzz::XmppEngine::Error error = xmpp_client_->GetError(NULL);
+        console_->PrintLine("logged out... %s", strerror(error).c_str());
+        Quit();
+      }
+      break;
+    default:
+      break;
+  }
+}
+
+void CallClient::InitMedia() {
+  worker_thread_ = new rtc::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_| is never deleted.
+  network_manager_ = new rtc::BasicNetworkManager();
+
+  // TODO: Decide if the relay address should be specified here.
+  rtc::SocketAddress stun_addr("stun.l.google.com", 19302);
+  cricket::ServerAddresses stun_servers;
+  stun_servers.insert(stun_addr);
+  port_allocator_ =  new cricket::BasicPortAllocator(
+      network_manager_, stun_servers, rtc::SocketAddress(),
+      rtc::SocketAddress(), rtc::SocketAddress());
+
+  if (portallocator_flags_ != 0) {
+    port_allocator_->set_flags(portallocator_flags_);
+  }
+  session_manager_ = new cricket::SessionManager(
+      port_allocator_, worker_thread_);
+  session_manager_->set_secure(dtls_policy_);
+  session_manager_->set_identity(ssl_identity_.get());
+  session_manager_->set_transport_protocol(transport_protocol_);
+  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::MediaEngineFactory::Create();
+  }
+
+  if (!data_engine_) {
+    if (data_channel_type_ == cricket::DCT_SCTP) {
+#ifdef HAVE_SCTP
+      data_engine_ = new cricket::SctpDataEngine();
+#else
+      LOG(LS_WARNING) << "SCTP Data Engine not supported.";
+      data_channel_type_ = cricket::DCT_NONE;
+      data_engine_ = new cricket::RtpDataEngine();
+#endif
+    } else {
+      // Even if we have DCT_NONE, we still have a data engine, just
+      // to make sure it isn't NULL.
+      data_engine_ = new cricket::RtpDataEngine();
+    }
+  }
+
+  media_client_ = new cricket::MediaSessionClient(
+      xmpp_client_->jid(),
+      session_manager_,
+      media_engine_,
+      data_engine_,
+      cricket::DeviceManagerFactory::Create());
+  media_client_->SignalCallCreate.connect(this, &CallClient::OnCallCreate);
+  media_client_->SignalCallDestroy.connect(this, &CallClient::OnCallDestroy);
+  media_client_->SignalDevicesChange.connect(this,
+                                             &CallClient::OnDevicesChange);
+  media_client_->set_secure(sdes_policy_);
+  media_client_->set_multisession_enabled(multisession_enabled_);
+}
+
+void CallClient::OnRequestSignaling() {
+  session_manager_->OnSignalingReady();
+}
+
+void CallClient::OnSessionCreate(cricket::Session* session, bool initiate) {
+  session->set_current_protocol(signaling_protocol_);
+}
+
+void CallClient::OnCallCreate(cricket::Call* call) {
+  call->SignalSessionState.connect(this, &CallClient::OnSessionState);
+  call->SignalMediaStreamsUpdate.connect(
+      this, &CallClient::OnMediaStreamsUpdate);
+}
+
+void CallClient::OnSessionState(cricket::Call* call,
+                                cricket::Session* session,
+                                cricket::Session::State state) {
+  if (state == cricket::Session::STATE_RECEIVEDINITIATE) {
+    buzz::Jid jid(session->remote_name());
+    if (call_ == call && multisession_enabled_) {
+      // We've received an initiate for an existing call. This is actually a
+      // new session for that call.
+      console_->PrintLine("Incoming session from '%s'", jid.Str().c_str());
+      AddSession(session);
+
+      cricket::CallOptions options;
+      options.has_video = call_->has_video();
+      options.data_channel_type = data_channel_type_;
+      call_->AcceptSession(session, options);
+
+      if (call_->has_video() && render_) {
+        RenderAllStreams(call, session, true);
+      }
+    } else {
+      console_->PrintLine("Incoming call from '%s'", jid.Str().c_str());
+      call_ = call;
+      AddSession(session);
+      incoming_call_ = true;
+      if (call->has_video() && render_) {
+        local_renderer_ =
+            cricket::VideoRendererFactory::CreateGuiVideoRenderer(160, 100);
+      }
+      if (auto_accept_) {
+        cricket::CallOptions options;
+        options.has_video = true;
+        options.data_channel_type = data_channel_type_;
+        Accept(options);
+      }
+    }
+  } else if (state == cricket::Session::STATE_SENTINITIATE) {
+    if (call->has_video() && render_) {
+      local_renderer_ =
+          cricket::VideoRendererFactory::CreateGuiVideoRenderer(160, 100);
+    }
+    console_->PrintLine("calling...");
+  } else if (state == cricket::Session::STATE_RECEIVEDACCEPT) {
+    console_->PrintLine("call answered");
+    SetupAcceptedCall();
+  } else if (state == cricket::Session::STATE_RECEIVEDREJECT) {
+    console_->PrintLine("call not answered");
+  } else if (state == cricket::Session::STATE_INPROGRESS) {
+    console_->PrintLine("call in progress");
+    call->SignalSpeakerMonitor.connect(this, &CallClient::OnSpeakerChanged);
+    call->StartSpeakerMonitor(session);
+  } else if (state == cricket::Session::STATE_RECEIVEDTERMINATE) {
+    console_->PrintLine("other side terminated");
+    TerminateAndRemoveSession(call, session->id());
+  }
+}
+
+void CallClient::OnSpeakerChanged(cricket::Call* call,
+                                  cricket::Session* session,
+                                  const cricket::StreamParams& speaker) {
+  if (!speaker.has_ssrcs()) {
+    console_->PrintLine("Session %s has no current speaker.",
+                        session->id().c_str());
+  } else if (speaker.id.empty()) {
+    console_->PrintLine("Session %s speaker change to unknown (%u).",
+                        session->id().c_str(), speaker.first_ssrc());
+  } else {
+    console_->PrintLine("Session %s speaker changed to %s (%u).",
+                        session->id().c_str(), speaker.id.c_str(),
+                        speaker.first_ssrc());
+  }
+}
+
+void SetMediaCaps(int media_caps, buzz::PresenceStatus* status) {
+  status->set_voice_capability((media_caps & cricket::AUDIO_RECV) != 0);
+  status->set_video_capability((media_caps & cricket::VIDEO_RECV) != 0);
+  status->set_camera_capability((media_caps & cricket::VIDEO_SEND) != 0);
+}
+
+void SetCaps(int media_caps, buzz::PresenceStatus* status) {
+  status->set_know_capabilities(true);
+  status->set_pmuc_capability(true);
+  SetMediaCaps(media_caps, status);
+}
+
+void SetAvailable(const buzz::Jid& jid, buzz::PresenceStatus* status) {
+  status->set_jid(jid);
+  status->set_available(true);
+  status->set_show(buzz::PresenceStatus::SHOW_ONLINE);
+}
+
+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_);
+  SetAvailable(xmpp_client_->jid(), &my_status_);
+  SetCaps(media_client_->GetCapabilities(), &my_status_);
+  SendStatus(my_status_);
+  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();
+
+  StartXmppPing();
+}
+
+void CallClient::StartXmppPing() {
+  buzz::PingTask* ping = new buzz::PingTask(
+      xmpp_client_, rtc::Thread::Current(),
+      kPingPeriodMillis, kPingTimeoutMillis);
+  ping->SignalTimeout.connect(this, &CallClient::OnPingTimeout);
+  ping->Start();
+}
+
+void CallClient::OnPingTimeout() {
+  LOG(LS_WARNING) << "XMPP Ping timeout. Will keep trying...";
+  StartXmppPing();
+
+  // Or should we do this instead?
+  // Quit();
+}
+
+void CallClient::SendStatus(const buzz::PresenceStatus& status) {
+  presence_out_->Send(status);
+}
+
+void CallClient::OnStatusUpdate(const buzz::PresenceStatus& 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.voice_capability()) {
+    if (show_roster_messages_) {
+      console_->PrintLine("Adding to roster: %s", key.c_str());
+    }
+    (*roster_)[key] = item;
+    // TODO: Make some of these constants.
+  } else {
+    if (show_roster_messages_) {
+      console_->PrintLine("Removing from roster: %s", key.c_str());
+    }
+    RosterMap::iterator iter = roster_->find(key);
+    if (iter != roster_->end())
+      roster_->erase(iter);
+  }
+}
+
+void CallClient::PrintRoster() {
+  console_->PrintLine("Roster contains %d callable", roster_->size());
+  RosterMap::iterator iter = roster_->begin();
+  while (iter != roster_->end()) {
+    console_->PrintLine("%s - %s",
+                        iter->second.jid.BareJid().Str().c_str(),
+                        DescribeStatus(iter->second.show, iter->second.status));
+    iter++;
+  }
+}
+
+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, rtc::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::SendData(const std::string& streamid,
+                          const std::string& text) {
+  // TODO(mylesj): Support sending data over sessions other than the first.
+  cricket::Session* session = GetFirstSession();
+  if (!call_ || !session) {
+    console_->PrintLine("Must be in a call to send data.");
+    return;
+  }
+  if (!call_->has_data()) {
+    console_->PrintLine("This call doesn't have a data channel.");
+    return;
+  }
+
+  const cricket::DataContentDescription* data =
+      cricket::GetFirstDataContentDescription(session->local_description());
+  if (!data) {
+    console_->PrintLine("This call doesn't have a data content.");
+    return;
+  }
+
+  cricket::StreamParams stream;
+  if (!cricket::GetStreamByIds(
+          data->streams(), "", streamid, &stream)) {
+    LOG(LS_WARNING) << "Could not send data: no such stream: "
+                    << streamid << ".";
+    return;
+  }
+
+  cricket::SendDataParams params;
+  params.ssrc = stream.first_ssrc();
+  rtc::Buffer payload(text.data(), text.length());
+  cricket::SendDataResult result;
+  bool sent = call_->SendData(session, params, payload, &result);
+  if (!sent) {
+    if (result == cricket::SDR_BLOCK) {
+      LOG(LS_WARNING) << "Could not send data because it would block.";
+    } else {
+      LOG(LS_WARNING) << "Could not send data for unknown reason.";
+    }
+  }
+}
+
+void CallClient::InviteFriend(const std::string& name) {
+  buzz::Jid jid(name);
+  if (!jid.IsValid() || jid.node() == "") {
+    console_->PrintLine("Invalid JID. JIDs should be in the form user@domain.");
+    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_->PrintLine("Requesting to befriend %s.", name.c_str());
+}
+
+bool CallClient::FindJid(const std::string& name, buzz::Jid* found_jid,
+                         cricket::CallOptions* options) {
+  bool found = false;
+  options->is_muc = false;
+  buzz::Jid callto_jid(name);
+  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 {
+    // 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_->PrintLine("Found %s '%s'",
+                        options->is_muc ? "room" : "online friend",
+                        found_jid->Str().c_str());
+  } else {
+    console_->PrintLine("Could not find online friend '%s'", name.c_str());
+  }
+
+  return found;
+}
+
+void CallClient::OnDataReceived(cricket::Call*,
+                                const cricket::ReceiveDataParams& params,
+                                const rtc::Buffer& payload) {
+  // TODO(mylesj): Support receiving data on sessions other than the first.
+  cricket::Session* session = GetFirstSession();
+  if (!session)
+    return;
+
+  cricket::StreamParams stream;
+  const std::vector<cricket::StreamParams>* data_streams =
+      call_->GetDataRecvStreams(session);
+  std::string text(payload.data(), payload.length());
+  if (data_streams && GetStreamBySsrc(*data_streams, params.ssrc, &stream)) {
+    console_->PrintLine(
+        "Received data from '%s' on stream '%s' (ssrc=%u): %s",
+        stream.groupid.c_str(), stream.id.c_str(),
+        params.ssrc, text.c_str());
+  } else {
+    console_->PrintLine(
+        "Received data (ssrc=%u): %s",
+        params.ssrc, text.c_str());
+  }
+}
+
+bool CallClient::PlaceCall(const std::string& name,
+                           cricket::CallOptions options) {
+  buzz::Jid jid;
+  if (!FindJid(name, &jid, &options))
+    return false;
+
+  if (!call_) {
+    call_ = media_client_->CreateCall();
+    AddSession(call_->InitiateSession(jid, media_client_->jid(), options));
+  }
+  media_client_->SetFocus(call_);
+  if (call_->has_video() && render_ && !options.is_muc) {
+    // TODO(pthatcher): Hookup local_render_ to the local capturer.
+  }
+  if (options.is_muc) {
+    const std::string& nick = mucs_[jid]->local_jid().resource();
+    hangout_pubsub_client_ =
+        new buzz::HangoutPubSubClient(xmpp_client_, jid, nick);
+    hangout_pubsub_client_->SignalPresenterStateChange.connect(
+        this, &CallClient::OnPresenterStateChange);
+    hangout_pubsub_client_->SignalAudioMuteStateChange.connect(
+        this, &CallClient::OnAudioMuteStateChange);
+    hangout_pubsub_client_->SignalRecordingStateChange.connect(
+        this, &CallClient::OnRecordingStateChange);
+    hangout_pubsub_client_->SignalRemoteMute.connect(
+        this, &CallClient::OnRemoteMuted);
+    hangout_pubsub_client_->SignalMediaBlock.connect(
+        this, &CallClient::OnMediaBlocked);
+    hangout_pubsub_client_->SignalRequestError.connect(
+        this, &CallClient::OnHangoutRequestError);
+    hangout_pubsub_client_->SignalPublishAudioMuteError.connect(
+        this, &CallClient::OnHangoutPublishAudioMuteError);
+    hangout_pubsub_client_->SignalPublishPresenterError.connect(
+        this, &CallClient::OnHangoutPublishPresenterError);
+    hangout_pubsub_client_->SignalPublishRecordingError.connect(
+        this, &CallClient::OnHangoutPublishRecordingError);
+    hangout_pubsub_client_->SignalRemoteMuteError.connect(
+        this, &CallClient::OnHangoutRemoteMuteError);
+    hangout_pubsub_client_->RequestAll();
+  }
+
+  return true;
+}
+
+bool CallClient::InitiateAdditionalSession(const std::string& name,
+                                           cricket::CallOptions options) {
+  // Can't add a session if there is no call yet.
+  if (!call_)
+    return false;
+
+  buzz::Jid jid;
+  if (!FindJid(name, &jid, &options))
+    return false;
+
+  std::vector<cricket::Session*>& call_sessions = sessions_[call_->id()];
+  call_sessions.push_back(
+      call_->InitiateSession(jid,
+                             buzz::Jid(call_sessions[0]->remote_name()),
+                             options));
+
+  return true;
+}
+
+void CallClient::TerminateAndRemoveSession(cricket::Call* call,
+                                           const std::string& id) {
+  std::vector<cricket::Session*>& call_sessions = sessions_[call->id()];
+  for (std::vector<cricket::Session*>::iterator iter = call_sessions.begin();
+       iter != call_sessions.end(); ++iter) {
+    if ((*iter)->id() == id) {
+      RenderAllStreams(call, *iter, false);
+      call_->TerminateSession(*iter);
+      call_sessions.erase(iter);
+      break;
+    }
+  }
+}
+
+void CallClient::PrintCalls() {
+  const std::map<uint32, cricket::Call*>& calls = media_client_->calls();
+  for (std::map<uint32, cricket::Call*>::const_iterator i = calls.begin();
+       i != calls.end(); ++i) {
+    console_->PrintLine("Call (id:%d), is %s",
+                        i->first,
+                        i->second == call_ ? "active" : "on hold");
+    std::vector<cricket::Session *>& sessions = sessions_[call_->id()];
+    for (std::vector<cricket::Session *>::const_iterator j = sessions.begin();
+         j != sessions.end(); ++j) {
+      console_->PrintLine("|--Session (id:%s), to %s", (*j)->id().c_str(),
+                          (*j)->remote_name().c_str());
+
+      std::vector<cricket::StreamParams>::const_iterator k;
+      const std::vector<cricket::StreamParams>* streams =
+          i->second->GetAudioRecvStreams(*j);
+      if (streams)
+        for (k = streams->begin(); k != streams->end(); ++k) {
+          console_->PrintLine("|----Audio Stream: %s", k->ToString().c_str());
+        }
+      streams = i->second->GetVideoRecvStreams(*j);
+      if (streams)
+        for (k = streams->begin(); k != streams->end(); ++k) {
+          console_->PrintLine("|----Video Stream: %s", k->ToString().c_str());
+        }
+      streams = i->second->GetDataRecvStreams(*j);
+      if (streams)
+        for (k = streams->begin(); k != streams->end(); ++k) {
+          console_->PrintLine("|----Data Stream: %s", k->ToString().c_str());
+        }
+    }
+  }
+}
+
+void CallClient::SwitchToCall(uint32 call_id) {
+  const std::map<uint32, cricket::Call*>& calls = media_client_->calls();
+  std::map<uint32, cricket::Call*>::const_iterator call_iter =
+      calls.find(call_id);
+  if (call_iter != calls.end()) {
+    media_client_->SetFocus(call_iter->second);
+    call_ = call_iter->second;
+  } else {
+    console_->PrintLine("Unable to find call: %d", call_id);
+  }
+}
+
+void CallClient::OnPresenterStateChange(
+    const std::string& nick, bool was_presenting, bool is_presenting) {
+  if (!was_presenting && is_presenting) {
+    console_->PrintLine("%s now presenting.", nick.c_str());
+  } else if (was_presenting && !is_presenting) {
+    console_->PrintLine("%s no longer presenting.", nick.c_str());
+  } else if (was_presenting && is_presenting) {
+    console_->PrintLine("%s still presenting.", nick.c_str());
+  } else if (!was_presenting && !is_presenting) {
+    console_->PrintLine("%s still not presenting.", nick.c_str());
+  }
+}
+
+void CallClient::OnAudioMuteStateChange(
+    const std::string& nick, bool was_muted, bool is_muted) {
+  if (!was_muted && is_muted) {
+    console_->PrintLine("%s now muted.", nick.c_str());
+  } else if (was_muted && !is_muted) {
+    console_->PrintLine("%s no longer muted.", nick.c_str());
+  }
+}
+
+void CallClient::OnRecordingStateChange(
+    const std::string& nick, bool was_recording, bool is_recording) {
+  if (!was_recording && is_recording) {
+    console_->PrintLine("%s now recording.", nick.c_str());
+  } else if (was_recording && !is_recording) {
+    console_->PrintLine("%s no longer recording.", nick.c_str());
+  }
+}
+
+void CallClient::OnRemoteMuted(const std::string& mutee_nick,
+                               const std::string& muter_nick,
+                               bool should_mute_locally) {
+  if (should_mute_locally) {
+    call_->Mute(true);
+    console_->PrintLine("Remote muted by %s.", muter_nick.c_str());
+  } else {
+    console_->PrintLine("%s remote muted by %s.",
+                        mutee_nick.c_str(), muter_nick.c_str());
+  }
+}
+
+void CallClient::OnMediaBlocked(const std::string& blockee_nick,
+                                const std::string& blocker_nick) {
+  console_->PrintLine("%s blocked by %s.",
+                      blockee_nick.c_str(), blocker_nick.c_str());
+}
+
+void CallClient::OnHangoutRequestError(const std::string& node,
+                                       const buzz::XmlElement* stanza) {
+  console_->PrintLine("Failed request pub sub items for node %s.",
+                      node.c_str());
+}
+
+void CallClient::OnHangoutPublishAudioMuteError(
+    const std::string& task_id, const buzz::XmlElement* stanza) {
+  console_->PrintLine("Failed to publish audio mute state.");
+}
+
+void CallClient::OnHangoutPublishPresenterError(
+    const std::string& task_id, const buzz::XmlElement* stanza) {
+  console_->PrintLine("Failed to publish presenting state.");
+}
+
+void CallClient::OnHangoutPublishRecordingError(
+    const std::string& task_id, const buzz::XmlElement* stanza) {
+  console_->PrintLine("Failed to publish recording state.");
+}
+
+void CallClient::OnHangoutRemoteMuteError(const std::string& task_id,
+                                          const std::string& mutee_nick,
+                                          const buzz::XmlElement* stanza) {
+  console_->PrintLine("Failed to remote mute.");
+}
+
+void CallClient::Accept(const cricket::CallOptions& options) {
+  ASSERT(call_ && incoming_call_);
+  ASSERT(sessions_[call_->id()].size() == 1);
+  cricket::Session* session = GetFirstSession();
+  call_->AcceptSession(session, options);
+  media_client_->SetFocus(call_);
+  if (call_->has_video() && render_) {
+    // TODO(pthatcher): Hookup local_render_ to the local capturer.
+    RenderAllStreams(call_, session, true);
+  }
+  SetupAcceptedCall();
+  incoming_call_ = false;
+}
+
+void CallClient::SetupAcceptedCall() {
+  if (call_->has_data()) {
+    call_->SignalDataReceived.connect(this, &CallClient::OnDataReceived);
+  }
+}
+
+void CallClient::Reject() {
+  ASSERT(call_ && incoming_call_);
+  call_->RejectSession(call_->sessions()[0]);
+  incoming_call_ = false;
+}
+
+void CallClient::Quit() {
+  rtc::Thread::Current()->Quit();
+}
+
+void CallClient::SetNick(const std::string& muc_nick) {
+  my_status_.set_nick(muc_nick);
+
+  // TODO: We might want to re-send presence, but right
+  // now, it appears to be ignored by the MUC.
+  //
+  // presence_out_->Send(my_status_); for (MucMap::const_iterator itr
+  // = mucs_.begin(); itr != mucs_.end(); ++itr) {
+  // presence_out_->SendDirected(itr->second->local_jid(),
+  // my_status_); }
+
+  console_->PrintLine("Nick set to '%s'.", muc_nick.c_str());
+}
+
+void CallClient::LookupAndJoinMuc(const std::string& room_name) {
+  // The room_name can't be empty for lookup task.
+  if (room_name.empty()) {
+    console_->PrintLine("Please provide a room name or room jid.");
+    return;
+  }
+
+  std::string room = room_name;
+  std::string domain = xmpp_client_->jid().domain();
+  if (room_name.find("@") != std::string::npos) {
+    // Assume the room_name is a fully qualified room name.
+    // We'll find the room name string and domain name string from it.
+    room = room_name.substr(0, room_name.find("@"));
+    domain = room_name.substr(room_name.find("@") + 1);
+  }
+
+  buzz::MucRoomLookupTask* lookup_query_task =
+      buzz::MucRoomLookupTask::CreateLookupTaskForRoomName(
+          xmpp_client_, buzz::Jid(buzz::STR_GOOGLE_MUC_LOOKUP_JID), room,
+          domain);
+  lookup_query_task->SignalResult.connect(this,
+      &CallClient::OnRoomLookupResponse);
+  lookup_query_task->SignalError.connect(this,
+      &CallClient::OnRoomLookupError);
+  lookup_query_task->Start();
+}
+
+void CallClient::JoinMuc(const std::string& room_jid_str) {
+  if (room_jid_str.empty()) {
+    buzz::Jid room_jid = GenerateRandomMucJid();
+    console_->PrintLine("Generated a random room jid: %s",
+                        room_jid.Str().c_str());
+    JoinMuc(room_jid);
+  } else {
+    JoinMuc(buzz::Jid(room_jid_str));
+  }
+}
+
+void CallClient::JoinMuc(const buzz::Jid& room_jid) {
+  if (!room_jid.IsValid()) {
+    console_->PrintLine("Unable to make valid muc endpoint for %s",
+                        room_jid.Str().c_str());
+    return;
+  }
+
+  std::string room_nick = room_jid.resource();
+  if (room_nick.empty()) {
+    room_nick = (xmpp_client_->jid().node()
+                 + "_" + xmpp_client_->jid().resource());
+  }
+
+  MucMap::iterator elem = mucs_.find(room_jid);
+  if (elem != mucs_.end()) {
+    console_->PrintLine("This MUC already exists.");
+    return;
+  }
+
+  buzz::Muc* muc = new buzz::Muc(room_jid.BareJid(), room_nick);
+  mucs_[muc->jid()] = muc;
+  presence_out_->SendDirected(muc->local_jid(), my_status_);
+}
+
+void CallClient::OnRoomLookupResponse(buzz::MucRoomLookupTask* task,
+                                      const buzz::MucRoomInfo& room) {
+  // The server requires the room be "configured" before being used.
+  // We only need to configure it if we create it, but rooms are
+  // auto-created at lookup, so there's currently no way to know if we
+  // created it.  So, we configure it every time, just in case.
+  // Luckily, it appears to be safe to configure a room that's already
+  // configured.  Our current flow is:
+  // 1. Lookup/auto-create
+  // 2. Configure
+  // 3. Join
+  // TODO: In the future, once the server supports it, we
+  // should:
+  // 1. Lookup
+  // 2. Create and Configure if necessary
+  // 3. Join
+  std::vector<std::string> room_features;
+  room_features.push_back(buzz::STR_MUC_ROOM_FEATURE_ENTERPRISE);
+  buzz::MucRoomConfigTask* room_config_task = new buzz::MucRoomConfigTask(
+      xmpp_client_, room.jid, room.full_name(), room_features);
+  room_config_task->SignalResult.connect(this,
+      &CallClient::OnRoomConfigResult);
+  room_config_task->SignalError.connect(this,
+      &CallClient::OnRoomConfigError);
+  room_config_task->Start();
+}
+
+void CallClient::OnRoomLookupError(buzz::IqTask* task,
+                                   const buzz::XmlElement* stanza) {
+  if (stanza == NULL) {
+    console_->PrintLine("Room lookup failed.");
+  } else {
+    console_->PrintLine("Room lookup error: ", stanza->Str().c_str());
+  }
+}
+
+void CallClient::OnRoomConfigResult(buzz::MucRoomConfigTask* task) {
+  JoinMuc(task->room_jid());
+}
+
+void CallClient::OnRoomConfigError(buzz::IqTask* task,
+                                   const buzz::XmlElement* stanza) {
+  console_->PrintLine("Room config failed.");
+  // We join the muc anyway, because if the room is already
+  // configured, the configure will fail, but we still want to join.
+  // Idealy, we'd know why the room config failed and only do this on
+  // "already configured" errors.  But right now all we get back is
+  // "not-allowed".
+  buzz::MucRoomConfigTask* config_task =
+      static_cast<buzz::MucRoomConfigTask*>(task);
+  JoinMuc(config_task->room_jid());
+}
+
+void CallClient::OnMucInviteReceived(const buzz::Jid& inviter,
+    const buzz::Jid& room,
+    const std::vector<buzz::AvailableMediaEntry>& avail) {
+
+  console_->PrintLine("Invited to join %s by %s.", room.Str().c_str(),
+      inviter.Str().c_str());
+  console_->PrintLine("Available media:");
+  if (avail.size() > 0) {
+    for (std::vector<buzz::AvailableMediaEntry>::const_iterator i =
+            avail.begin();
+        i != avail.end();
+        ++i) {
+      console_->PrintLine("  %s, %s",
+                          buzz::AvailableMediaEntry::TypeAsString(i->type),
+                          buzz::AvailableMediaEntry::StatusAsString(i->status));
+    }
+  } else {
+    console_->PrintLine("  None");
+  }
+  // We automatically join the room.
+  JoinMuc(room);
+}
+
+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_->PrintLine("Joined \"%s\"", muc->jid().Str().c_str());
+}
+
+void CallClient::OnMucStatusUpdate(const buzz::Jid& jid,
+    const buzz::MucPresenceStatus& status) {
+
+  // Look up this muc.
+  MucMap::iterator elem = mucs_.find(jid);
+  ASSERT(elem != mucs_.end());
+
+  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()) {
+    muc->members()[status.jid().resource()] = status;
+  } else {
+    muc->members().erase(status.jid().resource());
+  }
+}
+
+bool CallClient::InMuc() {
+  const buzz::Jid* muc_jid = FirstMucJid();
+  if (!muc_jid) return false;
+  return muc_jid->IsValid();
+}
+
+const buzz::Jid* CallClient::FirstMucJid() {
+  if (mucs_.empty()) return NULL;
+  return &(mucs_.begin()->first);
+}
+
+void CallClient::LeaveMuc(const std::string& room) {
+  buzz::Jid room_jid;
+  const buzz::Jid* muc_jid = FirstMucJid();
+  if (room.length() > 0) {
+    room_jid = buzz::Jid(room);
+  } else if (mucs_.size() > 0) {
+    // leave the first MUC if no JID specified
+    if (muc_jid) {
+      room_jid = *(muc_jid);
+    }
+  }
+
+  if (!room_jid.IsValid()) {
+    console_->PrintLine("Invalid MUC JID.");
+    return;
+  }
+
+  MucMap::iterator elem = mucs_.find(room_jid);
+  if (elem == mucs_.end()) {
+    console_->PrintLine("No such MUC.");
+    return;
+  }
+
+  buzz::Muc* muc = elem->second;
+  muc->set_state(buzz::Muc::MUC_LEAVING);
+
+  buzz::PresenceStatus 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_->PrintLine("Failed to join \"%s\", code=%d",
+                        muc->jid().Str().c_str(), error);
+  } else if (muc->state() == buzz::Muc::MUC_JOINED) {
+    console_->PrintLine("Kicked from \"%s\"",
+                        muc->jid().Str().c_str());
+  }
+
+  delete muc;
+  mucs_.erase(elem);
+}
+
+void CallClient::InviteToMuc(const std::string& given_user,
+                             const std::string& room) {
+  std::string user = given_user;
+
+  // First find the room.
+  const buzz::Muc* found_muc;
+  if (room.length() == 0) {
+    if (mucs_.size() == 0) {
+      console_->PrintLine("Not in a room yet; can't invite.");
+      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_->PrintLine("Not in room %s.", room.c_str());
+      return;
+    }
+    found_muc = elem->second;
+  }
+
+  buzz::Jid invite_to = found_muc->jid();
+
+  // 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)) {
+      buzz::Jid invitee = iter->second.jid;
+      muc_invite_send_->Send(invite_to, invitee);
+      found_user = true;
+    }
+  }
+  if (!found_user) {
+    buzz::Jid invitee = user_jid;
+    muc_invite_send_->Send(invite_to, invitee);
+  }
+}
+
+void CallClient::GetDevices() {
+  std::vector<std::string> names;
+  media_client_->GetAudioInputDevices(&names);
+  console_->PrintLine("Audio input devices:");
+  PrintDevices(names);
+  media_client_->GetAudioOutputDevices(&names);
+  console_->PrintLine("Audio output devices:");
+  PrintDevices(names);
+  media_client_->GetVideoCaptureDevices(&names);
+  console_->PrintLine("Video capture devices:");
+  PrintDevices(names);
+}
+
+void CallClient::PrintDevices(const std::vector<std::string>& names) {
+  for (size_t i = 0; i < names.size(); ++i) {
+    console_->PrintLine("%d: %s", static_cast<int>(i), names[i].c_str());
+  }
+}
+
+void CallClient::OnDevicesChange() {
+  console_->PrintLine("Devices changed.");
+  SetMediaCaps(media_client_->GetCapabilities(), &my_status_);
+  SendStatus(my_status_);
+}
+
+void CallClient::SetVolume(const std::string& level) {
+  media_client_->SetOutputVolume(strtol(level.c_str(), NULL, 10));
+}
+
+void CallClient::OnMediaStreamsUpdate(cricket::Call* call,
+                                      cricket::Session* session,
+                                      const cricket::MediaStreams& added,
+                                      const cricket::MediaStreams& removed) {
+  if (call && call->has_video()) {
+    for (std::vector<cricket::StreamParams>::const_iterator
+         it = removed.video().begin(); it != removed.video().end(); ++it) {
+      RemoveStaticRenderedView(it->first_ssrc());
+    }
+
+    if (render_) {
+      RenderStreams(call, session, added.video(), true);
+    }
+    SendViewRequest(call, session);
+  }
+}
+
+void CallClient::RenderAllStreams(cricket::Call* call,
+                                  cricket::Session* session,
+                                  bool enable) {
+  const std::vector<cricket::StreamParams>* video_streams =
+      call->GetVideoRecvStreams(session);
+  if (video_streams) {
+    RenderStreams(call, session, *video_streams, enable);
+  }
+}
+
+void CallClient::RenderStreams(
+    cricket::Call* call,
+    cricket::Session* session,
+    const std::vector<cricket::StreamParams>& video_streams,
+    bool enable) {
+  std::vector<cricket::StreamParams>::const_iterator stream;
+  for (stream = video_streams.begin(); stream != video_streams.end();
+       ++stream) {
+    RenderStream(call, session, *stream, enable);
+  }
+}
+
+void CallClient::RenderStream(cricket::Call* call,
+                              cricket::Session* session,
+                              const cricket::StreamParams& stream,
+                              bool enable) {
+  if (!stream.has_ssrcs()) {
+    // Nothing to see here; move along.
+    return;
+  }
+
+  uint32 ssrc = stream.first_ssrc();
+  StaticRenderedViews::iterator iter =
+      static_rendered_views_.find(std::make_pair(session, ssrc));
+  if (enable) {
+    if (iter == static_rendered_views_.end()) {
+      // TODO(pthatcher): Make dimensions and positions more configurable.
+      int offset = (50 * static_views_accumulated_count_) % 300;
+      AddStaticRenderedView(session, ssrc, 640, 400, 30,
+                            offset, offset);
+      // Should have it now.
+      iter = static_rendered_views_.find(std::make_pair(session, ssrc));
+    }
+    call->SetVideoRenderer(session, ssrc, iter->second.renderer);
+  } else {
+    if (iter != static_rendered_views_.end()) {
+      call->SetVideoRenderer(session, ssrc, NULL);
+      RemoveStaticRenderedView(ssrc);
+    }
+  }
+}
+
+// TODO: Would these methods to add and remove views make
+// more sense in call.cc?  Would other clients use them?
+void CallClient::AddStaticRenderedView(
+    cricket::Session* session,
+    uint32 ssrc, int width, int height, int framerate,
+    int x_offset, int y_offset) {
+  StaticRenderedView rendered_view(
+      cricket::StaticVideoView(
+          cricket::StreamSelector(ssrc), width, height, framerate),
+      cricket::VideoRendererFactory::CreateGuiVideoRenderer(
+          x_offset, y_offset));
+  rendered_view.renderer->SetSize(width, height, 0);
+  static_rendered_views_.insert(std::make_pair(std::make_pair(session, ssrc),
+                                               rendered_view));
+  ++static_views_accumulated_count_;
+  console_->PrintLine("Added renderer for ssrc %d", ssrc);
+}
+
+bool CallClient::RemoveStaticRenderedView(uint32 ssrc) {
+  for (StaticRenderedViews::iterator it = static_rendered_views_.begin();
+       it != static_rendered_views_.end(); ++it) {
+    if (it->second.view.selector.ssrc == ssrc) {
+      delete it->second.renderer;
+      static_rendered_views_.erase(it);
+      console_->PrintLine("Removed renderer for ssrc %d", ssrc);
+      return true;
+    }
+  }
+  return false;
+}
+
+void CallClient::RemoveCallsStaticRenderedViews(cricket::Call* call) {
+  std::vector<cricket::Session*>& sessions = sessions_[call->id()];
+  std::set<cricket::Session*> call_sessions(sessions.begin(), sessions.end());
+  for (StaticRenderedViews::iterator it = static_rendered_views_.begin();
+       it != static_rendered_views_.end(); ) {
+    if (call_sessions.find(it->first.first) != call_sessions.end()) {
+      delete it->second.renderer;
+      static_rendered_views_.erase(it++);
+    } else {
+      ++it;
+    }
+  }
+}
+
+void CallClient::SendViewRequest(cricket::Call* call,
+                                 cricket::Session* session) {
+  cricket::ViewRequest request;
+  for (StaticRenderedViews::iterator it = static_rendered_views_.begin();
+       it != static_rendered_views_.end(); ++it) {
+    if (it->first.first == session) {
+      request.static_video_views.push_back(it->second.view);
+    }
+  }
+  call->SendViewRequest(session, request);
+}
+
+buzz::Jid CallClient::GenerateRandomMucJid() {
+  // 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;
+    }
+  }
+
+  rtc::sprintfn(guid_room,
+                      ARRAY_SIZE(guid_room),
+                      "private-chat-%s@%s",
+                      guid,
+                      pmuc_domain_.c_str());
+  return buzz::Jid(guid_room);
+}
+
+bool CallClient::SelectFirstDesktopScreencastId(
+    cricket::ScreencastId* screencastid) {
+  if (!rtc::WindowPickerFactory::IsSupported()) {
+    LOG(LS_WARNING) << "Window picker not suported on this OS.";
+    return false;
+  }
+
+  rtc::WindowPicker* picker =
+      rtc::WindowPickerFactory::CreateWindowPicker();
+  if (!picker) {
+    LOG(LS_WARNING) << "Could not create a window picker.";
+    return false;
+  }
+
+  rtc::DesktopDescriptionList desktops;
+  if (!picker->GetDesktopList(&desktops) || desktops.empty()) {
+    LOG(LS_WARNING) << "Could not get a list of desktops.";
+    return false;
+  }
+
+  *screencastid = cricket::ScreencastId(desktops[0].id());
+  return true;
+}
+
+void CallClient::PrintStats() const {
+  const cricket::VoiceMediaInfo& vmi = call_->last_voice_media_info();
+
+  for (std::vector<cricket::VoiceSenderInfo>::const_iterator it =
+       vmi.senders.begin(); it != vmi.senders.end(); ++it) {
+    console_->PrintLine("Sender: ssrc=%u codec='%s' bytes=%d packets=%d "
+                        "rtt=%d jitter=%d",
+                        it->ssrc(), it->codec_name.c_str(), it->bytes_sent,
+                        it->packets_sent, it->rtt_ms, it->jitter_ms);
+  }
+
+  for (std::vector<cricket::VoiceReceiverInfo>::const_iterator it =
+       vmi.receivers.begin(); it != vmi.receivers.end(); ++it) {
+    console_->PrintLine("Receiver: ssrc=%u bytes=%d packets=%d "
+                        "jitter=%d loss=%.2f",
+                        it->ssrc(), it->bytes_rcvd, it->packets_rcvd,
+                        it->jitter_ms, it->fraction_lost);
+  }
+}
diff --git a/libjingle/examples/call/callclient.h b/libjingle/examples/call/callclient.h
new file mode 100644
index 0000000..ac427b4
--- /dev/null
+++ b/libjingle/examples/call/callclient.h
@@ -0,0 +1,352 @@
+/*
+ * 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.
+ */
+
+#ifndef TALK_EXAMPLES_CALL_CALLCLIENT_H_
+#define TALK_EXAMPLES_CALL_CALLCLIENT_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "talk/examples/call/console.h"
+#include "talk/media/base/mediachannel.h"
+#include "webrtc/p2p/base/session.h"
+#include "webrtc/libjingle/media/mediamessages.h"
+#include "webrtc/libjingle/media/mediasessionclient.h"
+#include "webrtc/libjingle/xmpp/hangoutpubsubclient.h"
+#include "webrtc/libjingle/xmpp/presencestatus.h"
+#include "webrtc/libjingle/xmpp/xmppclient.h"
+#include "webrtc/base/scoped_ptr.h"
+#include "webrtc/base/sslidentity.h"
+
+namespace buzz {
+class PresencePushTask;
+class PresenceOutTask;
+class MucInviteRecvTask;
+class MucInviteSendTask;
+class FriendInviteSendTask;
+class DiscoInfoQueryTask;
+class Muc;
+class PresenceStatus;
+class IqTask;
+class MucRoomConfigTask;
+class MucRoomLookupTask;
+class MucPresenceStatus;
+class XmlElement;
+class HangoutPubSubClient;
+struct AvailableMediaEntry;
+struct MucRoomInfo;
+}  // namespace buzz
+
+namespace rtc {
+class Thread;
+class NetworkManager;
+}  // namespace rtc
+
+namespace cricket {
+class PortAllocator;
+class MediaEngineInterface;
+class MediaSessionClient;
+class Call;
+class SessionManagerTask;
+struct CallOptions;
+struct MediaStreams;
+struct StreamParams;
+}  // namespace cricket
+
+struct RosterItem {
+  buzz::Jid jid;
+  buzz::PresenceStatus::Show show;
+  std::string status;
+};
+
+struct StaticRenderedView {
+  StaticRenderedView(const cricket::StaticVideoView& view,
+                     cricket::VideoRenderer* renderer) :
+      view(view),
+      renderer(renderer) {
+  }
+
+  cricket::StaticVideoView view;
+  cricket::VideoRenderer* renderer;
+};
+
+// Maintain a mapping of (session, ssrc) to rendered view.
+typedef std::map<std::pair<cricket::Session*, uint32>,
+                 StaticRenderedView> StaticRenderedViews;
+
+class CallClient: public sigslot::has_slots<> {
+ public:
+  CallClient(buzz::XmppClient* xmpp_client,
+             const std::string& caps_node,
+             const std::string& version);
+  ~CallClient();
+
+  cricket::MediaSessionClient* media_client() const { return media_client_; }
+  void SetMediaEngine(cricket::MediaEngineInterface* media_engine) {
+    media_engine_ = media_engine;
+  }
+  void SetAutoAccept(bool auto_accept) {
+    auto_accept_ = auto_accept;
+  }
+  void SetPmucDomain(const std::string &pmuc_domain) {
+    pmuc_domain_ = pmuc_domain;
+  }
+  void SetRender(bool render) {
+    render_ = render;
+  }
+  void SetDataChannelType(cricket::DataChannelType data_channel_type) {
+    data_channel_type_ = data_channel_type;
+  }
+  void SetMultiSessionEnabled(bool multisession_enabled) {
+    multisession_enabled_ = multisession_enabled;
+  }
+  void SetConsole(Console *console) {
+    console_ = console;
+  }
+  void SetPriority(int priority) {
+    my_status_.set_priority(priority);
+  }
+  void SendStatus() {
+    SendStatus(my_status_);
+  }
+  void SendStatus(const buzz::PresenceStatus& status);
+
+  void ParseLine(const std::string &str);
+
+  void SendChat(const std::string& to, const std::string msg);
+  void SendData(const std::string& stream_name,
+                const std::string& text);
+  void InviteFriend(const std::string& user);
+  void JoinMuc(const buzz::Jid& room_jid);
+  void JoinMuc(const std::string& room_jid_str);
+  void LookupAndJoinMuc(const std::string& room_name);
+  void InviteToMuc(const std::string& user, const std::string& room);
+  bool InMuc();
+  const buzz::Jid* FirstMucJid();
+  void LeaveMuc(const std::string& room);
+  void SetNick(const std::string& muc_nick);
+  void SetPortAllocatorFlags(uint32 flags) { portallocator_flags_ = flags; }
+  void SetAllowLocalIps(bool allow_local_ips) {
+    allow_local_ips_ = allow_local_ips;
+  }
+
+  void SetSignalingProtocol(cricket::SignalingProtocol protocol) {
+    signaling_protocol_ = protocol;
+  }
+  void SetTransportProtocol(cricket::TransportProtocol protocol) {
+    transport_protocol_ = protocol;
+  }
+  void SetSecurePolicy(cricket::SecurePolicy sdes_policy,
+                       cricket::SecurePolicy dtls_policy) {
+    sdes_policy_ = sdes_policy;
+    dtls_policy_ = dtls_policy;
+  }
+  void SetSslIdentity(rtc::SSLIdentity* identity) {
+    ssl_identity_.reset(identity);
+  }
+
+  typedef std::map<buzz::Jid, buzz::Muc*> MucMap;
+
+  const MucMap& mucs() const {
+    return mucs_;
+  }
+
+  void SetShowRosterMessages(bool show_roster_messages) {
+    show_roster_messages_ = show_roster_messages;
+  }
+
+ private:
+  void AddStream(uint32 audio_src_id, uint32 video_src_id);
+  void RemoveStream(uint32 audio_src_id, uint32 video_src_id);
+  void OnStateChange(buzz::XmppEngine::State state);
+
+  void InitMedia();
+  void InitPresence();
+  void StartXmppPing();
+  void OnPingTimeout();
+  void OnRequestSignaling();
+  void OnSessionCreate(cricket::Session* session, bool initiate);
+  void OnCallCreate(cricket::Call* call);
+  void OnCallDestroy(cricket::Call* call);
+  void OnSessionState(cricket::Call* call,
+                      cricket::Session* session,
+                      cricket::Session::State state);
+  void OnStatusUpdate(const buzz::PresenceStatus& status);
+  void OnMucInviteReceived(const buzz::Jid& inviter, const buzz::Jid& room,
+      const std::vector<buzz::AvailableMediaEntry>& avail);
+  void OnMucJoined(const buzz::Jid& endpoint);
+  void OnMucStatusUpdate(const buzz::Jid& jid,
+                         const buzz::MucPresenceStatus& status);
+  void OnMucLeft(const buzz::Jid& endpoint, int error);
+  void OnPresenterStateChange(const std::string& nick,
+                              bool was_presenting, bool is_presenting);
+  void OnAudioMuteStateChange(const std::string& nick,
+                              bool was_muted, bool is_muted);
+  void OnRecordingStateChange(const std::string& nick,
+                              bool was_recording, bool is_recording);
+  void OnRemoteMuted(const std::string& mutee_nick,
+                     const std::string& muter_nick,
+                     bool should_mute_locally);
+  void OnMediaBlocked(const std::string& blockee_nick,
+                      const std::string& blocker_nick);
+  void OnHangoutRequestError(const std::string& node,
+                             const buzz::XmlElement* stanza);
+  void OnHangoutPublishAudioMuteError(const std::string& task_id,
+                                      const buzz::XmlElement* stanza);
+  void OnHangoutPublishPresenterError(const std::string& task_id,
+                                      const buzz::XmlElement* stanza);
+  void OnHangoutPublishRecordingError(const std::string& task_id,
+                                      const buzz::XmlElement* stanza);
+  void OnHangoutRemoteMuteError(const std::string& task_id,
+                                const std::string& mutee_nick,
+                                const buzz::XmlElement* stanza);
+  void OnDevicesChange();
+  void OnMediaStreamsUpdate(cricket::Call* call,
+                            cricket::Session* session,
+                            const cricket::MediaStreams& added,
+                            const cricket::MediaStreams& removed);
+  void OnSpeakerChanged(cricket::Call* call,
+                        cricket::Session* session,
+                        const cricket::StreamParams& speaker_stream);
+  void OnRoomLookupResponse(buzz::MucRoomLookupTask* task,
+                            const buzz::MucRoomInfo& room_info);
+  void OnRoomLookupError(buzz::IqTask* task,
+                         const buzz::XmlElement* stanza);
+  void OnRoomConfigResult(buzz::MucRoomConfigTask* task);
+  void OnRoomConfigError(buzz::IqTask* task,
+                         const buzz::XmlElement* stanza);
+  void OnDataReceived(cricket::Call*,
+                      const cricket::ReceiveDataParams& params,
+                      const rtc::Buffer& payload);
+  buzz::Jid GenerateRandomMucJid();
+
+  // Depending on |enable|, render (or don't) all the streams in |session|.
+  void RenderAllStreams(cricket::Call* call,
+                        cricket::Session* session,
+                        bool enable);
+
+  // Depending on |enable|, render (or don't) the streams in |video_streams|.
+  void RenderStreams(cricket::Call* call,
+                     cricket::Session* session,
+                     const std::vector<cricket::StreamParams>& video_streams,
+                     bool enable);
+
+  // Depending on |enable|, render (or don't) the supplied |stream|.
+  void RenderStream(cricket::Call* call,
+                    cricket::Session* session,
+                    const cricket::StreamParams& stream,
+                    bool enable);
+  void AddStaticRenderedView(
+      cricket::Session* session,
+      uint32 ssrc, int width, int height, int framerate,
+      int x_offset, int y_offset);
+  bool RemoveStaticRenderedView(uint32 ssrc);
+  void RemoveCallsStaticRenderedViews(cricket::Call* call);
+  void SendViewRequest(cricket::Call* call, cricket::Session* session);
+  bool SelectFirstDesktopScreencastId(cricket::ScreencastId* screencastid);
+
+  static const std::string strerror(buzz::XmppEngine::Error err);
+
+  void PrintRoster();
+  bool FindJid(const std::string& name,
+               buzz::Jid* found_jid,
+               cricket::CallOptions* options);
+  bool PlaceCall(const std::string& name, cricket::CallOptions options);
+  bool InitiateAdditionalSession(const std::string& name,
+                                 cricket::CallOptions options);
+  void TerminateAndRemoveSession(cricket::Call* call, const std::string& id);
+  void PrintCalls();
+  void SwitchToCall(uint32 call_id);
+  void Accept(const cricket::CallOptions& options);
+  void Reject();
+  void Quit();
+
+  void GetDevices();
+  void PrintDevices(const std::vector<std::string>& names);
+
+  void SetVolume(const std::string& level);
+
+  cricket::Session* GetFirstSession() { return sessions_[call_->id()][0]; }
+  void AddSession(cricket::Session* session) {
+    sessions_[call_->id()].push_back(session);
+  }
+
+  void PrintStats() const;
+  void SetupAcceptedCall();
+
+  typedef std::map<std::string, RosterItem> RosterMap;
+
+  Console *console_;
+  buzz::XmppClient* xmpp_client_;
+  rtc::Thread* worker_thread_;
+  rtc::NetworkManager* network_manager_;
+  cricket::PortAllocator* port_allocator_;
+  cricket::SessionManager* session_manager_;
+  cricket::SessionManagerTask* session_manager_task_;
+  cricket::MediaEngineInterface* media_engine_;
+  cricket::DataEngineInterface* data_engine_;
+  cricket::MediaSessionClient* media_client_;
+  MucMap mucs_;
+
+  cricket::Call* call_;
+  typedef std::map<uint32, std::vector<cricket::Session *> > SessionMap;
+  SessionMap sessions_;
+
+  buzz::HangoutPubSubClient* hangout_pubsub_client_;
+  bool incoming_call_;
+  bool auto_accept_;
+  std::string pmuc_domain_;
+  bool render_;
+  cricket::DataChannelType data_channel_type_;
+  bool multisession_enabled_;
+  cricket::VideoRenderer* local_renderer_;
+  StaticRenderedViews static_rendered_views_;
+  uint32 static_views_accumulated_count_;
+  uint32 screencast_ssrc_;
+
+  buzz::PresenceStatus my_status_;
+  buzz::PresencePushTask* presence_push_;
+  buzz::PresenceOutTask* presence_out_;
+  buzz::MucInviteRecvTask* muc_invite_recv_;
+  buzz::MucInviteSendTask* muc_invite_send_;
+  buzz::FriendInviteSendTask* friend_invite_send_;
+  RosterMap* roster_;
+  uint32 portallocator_flags_;
+
+  bool allow_local_ips_;
+  cricket::SignalingProtocol signaling_protocol_;
+  cricket::TransportProtocol transport_protocol_;
+  cricket::SecurePolicy sdes_policy_;
+  cricket::SecurePolicy dtls_policy_;
+  rtc::scoped_ptr<rtc::SSLIdentity> ssl_identity_;
+  std::string last_sent_to_;
+
+  bool show_roster_messages_;
+};
+
+#endif  // TALK_EXAMPLES_CALL_CALLCLIENT_H_
diff --git a/libjingle/examples/call/callclient_unittest.cc b/libjingle/examples/call/callclient_unittest.cc
new file mode 100644
index 0000000..256853a
--- /dev/null
+++ b/libjingle/examples/call/callclient_unittest.cc
@@ -0,0 +1,47 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+// Unit tests for CallClient
+
+#include "talk/examples/call/callclient.h"
+#include "talk/media/base/filemediaengine.h"
+#include "talk/media/base/mediaengine.h"
+#include "webrtc/libjingle/xmpp/xmppthread.h"
+#include "webrtc/base/gunit.h"
+
+TEST(CallClientTest, CreateCallClientWithDefaultMediaEngine) {
+  buzz::XmppPump pump;
+  CallClient *client = new CallClient(pump.client(), "app", "version");
+  delete client;
+}
+
+TEST(CallClientTest, CreateCallClientWithFileMediaEngine) {
+  buzz::XmppPump pump;
+  CallClient *client = new CallClient(pump.client(), "app", "version");
+  client->SetMediaEngine(new cricket::FileMediaEngine);
+  delete client;
+}
diff --git a/libjingle/examples/call/console.cc b/libjingle/examples/call/console.cc
new file mode 100644
index 0000000..a0da2af
--- /dev/null
+++ b/libjingle/examples/call/console.cc
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+
+#define _CRT_SECURE_NO_DEPRECATE 1
+
+#include <assert.h>
+
+#ifdef POSIX
+#include <signal.h>
+#include <termios.h>
+#include <unistd.h>
+#endif  // POSIX
+
+#include "talk/examples/call/callclient.h"
+#include "talk/examples/call/console.h"
+#include "webrtc/base/logging.h"
+#include "webrtc/base/messagequeue.h"
+#include "webrtc/base/stringutils.h"
+
+#ifdef POSIX
+static void DoNothing(int unused) {}
+#endif
+
+Console::Console(rtc::Thread *thread, CallClient *client) :
+  client_(client),
+  client_thread_(thread),
+  stopped_(false) {}
+
+Console::~Console() {
+  Stop();
+}
+
+void Console::Start() {
+  if (stopped_) {
+    // stdin was closed in Stop(), so we can't restart.
+    LOG(LS_ERROR) << "Cannot re-start";
+    return;
+  }
+  if (console_thread_) {
+    LOG(LS_WARNING) << "Already started";
+    return;
+  }
+  console_thread_.reset(new rtc::Thread());
+  console_thread_->Start();
+  console_thread_->Post(this, MSG_START);
+}
+
+void Console::Stop() {
+  if (console_thread_) {
+#ifdef WIN32
+    CloseHandle(GetStdHandle(STD_INPUT_HANDLE));
+#else
+    close(fileno(stdin));
+    // This forces the read() in fgets() to return with errno = EINTR. fgets()
+    // will retry the read() and fail, thus returning.
+    pthread_kill(console_thread_->GetPThread(), SIGUSR1);
+#endif
+    console_thread_->Stop();
+    console_thread_.reset();
+    stopped_ = true;
+  }
+}
+
+void Console::SetEcho(bool on) {
+#ifdef WIN32
+  HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
+  if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL))
+    return;
+
+  DWORD mode;
+  if (!GetConsoleMode(hIn, &mode))
+    return;
+
+  if (on) {
+    mode = mode | ENABLE_ECHO_INPUT;
+  } else {
+    mode = mode & ~ENABLE_ECHO_INPUT;
+  }
+
+  SetConsoleMode(hIn, mode);
+#else
+  const int fd = fileno(stdin);
+  if (fd == -1)
+   return;
+
+  struct termios tcflags;
+  if (tcgetattr(fd, &tcflags) == -1)
+    return;
+
+  if (on) {
+    tcflags.c_lflag |= ECHO;
+  } else {
+    tcflags.c_lflag &= ~ECHO;
+  }
+
+  tcsetattr(fd, TCSANOW, &tcflags);
+#endif
+}
+
+void Console::PrintLine(const char* format, ...) {
+  va_list ap;
+  va_start(ap, format);
+
+  char buf[4096];
+  int size = vsnprintf(buf, sizeof(buf), format, ap);
+  assert(size >= 0);
+  assert(size < static_cast<int>(sizeof(buf)));
+  buf[size] = '\0';
+  printf("%s\n", buf);
+  fflush(stdout);
+
+  va_end(ap);
+}
+
+void Console::RunConsole() {
+  char input_buffer[128];
+  while (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL) {
+    client_thread_->Post(this, MSG_INPUT,
+        new rtc::TypedMessageData<std::string>(input_buffer));
+  }
+}
+
+void Console::OnMessage(rtc::Message *msg) {
+  switch (msg->message_id) {
+    case MSG_START:
+#ifdef POSIX
+      // Install a no-op signal so that we can abort RunConsole() by raising
+      // SIGUSR1.
+      struct sigaction act;
+      act.sa_handler = &DoNothing;
+      sigemptyset(&act.sa_mask);
+      act.sa_flags = 0;
+      if (sigaction(SIGUSR1, &act, NULL) < 0) {
+        LOG(LS_WARNING) << "Can't install signal";
+      }
+#endif
+      RunConsole();
+      break;
+    case MSG_INPUT:
+      rtc::TypedMessageData<std::string> *data =
+          static_cast<rtc::TypedMessageData<std::string>*>(msg->pdata);
+      client_->ParseLine(data->data());
+      break;
+  }
+}
diff --git a/libjingle/examples/call/console.h b/libjingle/examples/call/console.h
new file mode 100644
index 0000000..abcae99
--- /dev/null
+++ b/libjingle/examples/call/console.h
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#ifndef TALK_EXAMPLES_CALL_CONSOLE_H_
+#define TALK_EXAMPLES_CALL_CONSOLE_H_
+
+#include <stdio.h>
+
+#include "webrtc/base/messagequeue.h"
+#include "webrtc/base/scoped_ptr.h"
+#include "webrtc/base/thread.h"
+
+class CallClient;
+
+class Console : public rtc::MessageHandler {
+ public:
+  Console(rtc::Thread *thread, CallClient *client);
+  ~Console();
+
+  // Starts reading lines from the console and giving them to the CallClient.
+  void Start();
+  // Stops reading lines. Cannot be restarted.
+  void Stop();
+
+  virtual void OnMessage(rtc::Message *msg);
+
+  void PrintLine(const char* format, ...);
+
+  static void SetEcho(bool on);
+
+ private:
+  enum {
+    MSG_START,
+    MSG_INPUT,
+  };
+
+  void RunConsole();
+  void ParseLine(std::string &str);
+
+  CallClient *client_;
+  rtc::Thread *client_thread_;
+  rtc::scoped_ptr<rtc::Thread> console_thread_;
+  bool stopped_;
+};
+
+#endif // TALK_EXAMPLES_CALL_CONSOLE_H_
diff --git a/libjingle/examples/call/friendinvitesendtask.cc b/libjingle/examples/call/friendinvitesendtask.cc
new file mode 100644
index 0000000..e2b8dde
--- /dev/null
+++ b/libjingle/examples/call/friendinvitesendtask.cc
@@ -0,0 +1,76 @@
+/*
+ * 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/friendinvitesendtask.h"
+#include "webrtc/libjingle/xmpp/constants.h"
+
+namespace buzz {
+
+XmppReturnStatus
+FriendInviteSendTask::Send(const Jid& user) {
+  if (GetState() != STATE_INIT && GetState() != STATE_START)
+    return XMPP_RETURN_BADSTATE;
+
+  // Need to first add to roster, then subscribe to presence.
+  XmlElement* iq = new XmlElement(QN_IQ);
+  iq->AddAttr(QN_TYPE, STR_SET);
+  XmlElement* query = new XmlElement(QN_ROSTER_QUERY);
+  XmlElement* item = new XmlElement(QN_ROSTER_ITEM);
+  item->AddAttr(QN_JID, user.Str());
+  item->AddAttr(QN_NAME, user.node());
+  query->AddElement(item);
+  iq->AddElement(query);
+  QueueStanza(iq);
+
+  // Subscribe to presence
+  XmlElement* presence = new XmlElement(QN_PRESENCE);
+  presence->AddAttr(QN_TO, user.Str());
+  presence->AddAttr(QN_TYPE, STR_SUBSCRIBE);
+  XmlElement* invitation = new XmlElement(QN_INVITATION);
+  invitation->AddAttr(QN_INVITE_MESSAGE,
+      "I've been using Google Talk and thought you might like to try it out. "
+      "We can use it to call each other for free over the internet. Here's an "
+      "invitation to download Google Talk. Give it a try!");
+  presence->AddElement(invitation);
+  QueueStanza(presence);
+
+  return XMPP_RETURN_OK;
+}
+
+int
+FriendInviteSendTask::ProcessStart() {
+  const XmlElement* stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+
+  if (SendStanza(stanza) != XMPP_RETURN_OK)
+    return STATE_ERROR;
+
+  return STATE_START;
+}
+
+}
diff --git a/libjingle/examples/call/friendinvitesendtask.h b/libjingle/examples/call/friendinvitesendtask.h
new file mode 100644
index 0000000..2928452
--- /dev/null
+++ b/libjingle/examples/call/friendinvitesendtask.h
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+#ifndef _FRIENDINVITESENDTASK_H_
+#define _FRIENDINVITESENDTASK_H_
+
+#include "webrtc/libjingle/xmpp/xmppengine.h"
+#include "webrtc/libjingle/xmpp/xmpptask.h"
+
+namespace buzz {
+
+class FriendInviteSendTask : public XmppTask {
+public:
+  explicit FriendInviteSendTask(XmppTaskParentInterface* parent)
+    : XmppTask(parent) {}
+  virtual ~FriendInviteSendTask() {}
+
+  XmppReturnStatus Send(const Jid& user);
+
+  virtual int ProcessStart();
+};
+
+}
+
+#endif
diff --git a/libjingle/examples/call/mediaenginefactory.cc b/libjingle/examples/call/mediaenginefactory.cc
new file mode 100644
index 0000000..dd62bf8
--- /dev/null
+++ b/libjingle/examples/call/mediaenginefactory.cc
@@ -0,0 +1,81 @@
+//
+// libjingle
+// Copyright 2004--2007, 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/mediaenginefactory.h"
+
+#include "talk/media/base/fakemediaengine.h"
+#include "talk/media/base/filemediaengine.h"
+#include "talk/media/base/mediaengine.h"
+#include "webrtc/base/stringutils.h"
+
+std::vector<cricket::AudioCodec> RequiredAudioCodecs() {
+  std::vector<cricket::AudioCodec> audio_codecs;
+  audio_codecs.push_back(
+      cricket::AudioCodec(9, "G722", 16000, 0, 1, 0));
+  audio_codecs.push_back(
+      cricket::AudioCodec(0, "PCMU", 8000, 0, 1, 0));
+  audio_codecs.push_back(
+      cricket::AudioCodec(13, "CN", 8000, 0, 1, 0));
+  audio_codecs.push_back(
+      cricket::AudioCodec(105, "CN", 16000, 0, 1, 0));
+  return audio_codecs;
+}
+
+std::vector<cricket::VideoCodec> RequiredVideoCodecs() {
+  std::vector<cricket::VideoCodec> video_codecs;
+  video_codecs.push_back(
+      cricket::VideoCodec(97, "H264", 320, 240, 30, 0));
+  video_codecs.push_back(
+      cricket::VideoCodec(99, "H264-SVC", 640, 360, 30, 0));
+  return video_codecs;
+}
+
+cricket::MediaEngineInterface* MediaEngineFactory::CreateFileMediaEngine(
+    const char* voice_in, const char* voice_out,
+    const char* video_in, const char* video_out) {
+  cricket::FileMediaEngine* file_media_engine = new cricket::FileMediaEngine;
+  // Set the RTP dump file names.
+  if (voice_in) {
+    file_media_engine->set_voice_input_filename(voice_in);
+  }
+  if (voice_out) {
+    file_media_engine->set_voice_output_filename(voice_out);
+  }
+  if (video_in) {
+    file_media_engine->set_video_input_filename(video_in);
+  }
+  if (video_out) {
+    file_media_engine->set_video_output_filename(video_out);
+  }
+
+  // Set voice and video codecs. TODO: The codecs actually depend on
+  // the the input voice and video streams.
+  file_media_engine->set_voice_codecs(RequiredAudioCodecs());
+  file_media_engine->set_video_codecs(RequiredVideoCodecs());
+
+  return file_media_engine;
+}
diff --git a/libjingle/examples/call/mediaenginefactory.h b/libjingle/examples/call/mediaenginefactory.h
new file mode 100644
index 0000000..90407f9
--- /dev/null
+++ b/libjingle/examples/call/mediaenginefactory.h
@@ -0,0 +1,40 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_
+#define TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_
+
+#include "talk/media/base/mediaengine.h"
+
+class MediaEngineFactory {
+ public:
+  static cricket::MediaEngineInterface* CreateFileMediaEngine(
+      const char* voice_in, const char* voice_out,
+      const char* video_in, const char* video_out);
+};
+
+#endif  // TALK_EXAMPLES_CALL_MEDIAENGINEFACTORY_H_
diff --git a/libjingle/examples/call/muc.h b/libjingle/examples/call/muc.h
new file mode 100644
index 0000000..9be192f
--- /dev/null
+++ b/libjingle/examples/call/muc.h
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+#ifndef _MUC_H_
+#define _MUC_H_
+
+#include <map>
+#include "webrtc/libjingle/xmpp/jid.h"
+#include "webrtc/libjingle/xmpp/presencestatus.h"
+
+namespace buzz {
+
+class Muc {
+ public:
+   Muc(const Jid& jid, const std::string& nick) : state_(MUC_JOINING),
+       jid_(jid), local_jid_(Jid(jid.Str() + "/" + nick)) {}
+  ~Muc() {};
+
+  enum State { MUC_JOINING, MUC_JOINED, MUC_LEAVING };
+  State state() const { return state_; }
+  void set_state(State state) { state_ = state; }
+  const Jid & jid() const { return jid_; }
+  const Jid & local_jid() const { return local_jid_; }
+
+  typedef std::map<std::string, MucPresenceStatus> MemberMap;
+
+  // All the intelligence about how to manage the members is in
+  // CallClient, so we completely expose the map.
+  MemberMap& members() {
+    return members_;
+  }
+
+private:
+  State state_;
+  Jid jid_;
+  Jid local_jid_;
+  MemberMap members_;
+};
+
+}
+
+#endif
diff --git a/libjingle/examples/call/mucinviterecvtask.cc b/libjingle/examples/call/mucinviterecvtask.cc
new file mode 100644
index 0000000..32de8fe
--- /dev/null
+++ b/libjingle/examples/call/mucinviterecvtask.cc
@@ -0,0 +1,124 @@
+/*
+ * 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/mucinviterecvtask.h"
+#include "webrtc/libjingle/xmpp/constants.h"
+
+namespace buzz {
+
+const char* types[] = {
+  "unknown",
+  "audio",
+  "video",
+};
+
+const char* statuses[] = {
+  "unknown",
+  "sendrecv",
+  "sendonly",
+  "recvonly",
+  "inactive",
+};
+
+const char*
+AvailableMediaEntry::TypeAsString(type_t type) {
+  // The values of the constants have been chosen such that this is correct.
+  return types[type];
+}
+
+const char*
+AvailableMediaEntry::StatusAsString(status_t status) {
+  // The values of the constants have been chosen such that this is correct.
+  return statuses[status];
+}
+
+int bodytext_to_array_pos(const XmlElement* elem, const char* array[],
+    int len, int defval = -1) {
+  if (elem) {
+    const std::string& body(elem->BodyText());
+    for (int i = 0; i < len; ++i) {
+      if (body == array[i]) {
+        // Found it.
+        return i;
+      }
+    }
+  }
+  // If we get here, it's not any value in the array.
+  return defval;
+}
+
+bool
+MucInviteRecvTask::HandleStanza(const XmlElement* stanza) {
+  // Figuring out that we want to handle this is a lot of the work of
+  // actually handling it, so we handle it right here instead of queueing it.
+  const XmlElement* xstanza;
+  const XmlElement* invite;
+  if (stanza->Name() != QN_MESSAGE) return false;
+  xstanza = stanza->FirstNamed(QN_MUC_USER_X);
+  if (!xstanza) return false;
+  invite = xstanza->FirstNamed(QN_MUC_USER_INVITE);
+  if (!invite) return false;
+  // Else it's an invite and we definitely want to handle it. Parse the
+  // available-media, if any.
+  std::vector<AvailableMediaEntry> v;
+  const XmlElement* avail =
+    invite->FirstNamed(QN_GOOGLE_MUC_USER_AVAILABLE_MEDIA);
+  if (avail) {
+    for (const XmlElement* entry = avail->FirstNamed(QN_GOOGLE_MUC_USER_ENTRY);
+        entry;
+        entry = entry->NextNamed(QN_GOOGLE_MUC_USER_ENTRY)) {
+      AvailableMediaEntry tmp;
+      // In the interest of debugging, we accept as much valid-looking data
+      // as we can.
+      tmp.label = atoi(entry->Attr(QN_LABEL).c_str());
+      tmp.type = static_cast<AvailableMediaEntry::type_t>(
+          bodytext_to_array_pos(
+              entry->FirstNamed(QN_GOOGLE_MUC_USER_TYPE),
+              types,
+              sizeof(types)/sizeof(const char*),
+              AvailableMediaEntry::TYPE_UNKNOWN));
+      tmp.status = static_cast<AvailableMediaEntry::status_t>(
+          bodytext_to_array_pos(
+              entry->FirstNamed(QN_GOOGLE_MUC_USER_STATUS),
+              statuses,
+              sizeof(statuses)/sizeof(const char*),
+              AvailableMediaEntry::STATUS_UNKNOWN));
+      v.push_back(tmp);
+    }
+  }
+  SignalInviteReceived(Jid(invite->Attr(QN_FROM)), Jid(stanza->Attr(QN_FROM)),
+      v);
+  return true;
+}
+
+int
+MucInviteRecvTask::ProcessStart() {
+  // We never queue anything so we are always blocked.
+  return STATE_BLOCKED;
+}
+
+}
diff --git a/libjingle/examples/call/mucinviterecvtask.h b/libjingle/examples/call/mucinviterecvtask.h
new file mode 100644
index 0000000..0466c94
--- /dev/null
+++ b/libjingle/examples/call/mucinviterecvtask.h
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+#ifndef _MUCINVITERECVTASK_H_
+#define _MUCINVITERECVTASK_H_
+
+#include <vector>
+
+#include "webrtc/libjingle/xmpp/xmppengine.h"
+#include "webrtc/libjingle/xmpp/xmpptask.h"
+#include "webrtc/base/sigslot.h"
+
+namespace buzz {
+
+struct AvailableMediaEntry {
+  enum type_t {
+    // SIP defines other media types, but these are the only ones we use in
+    // multiway jingle.
+    // These numbers are important; see .cc file
+    TYPE_UNKNOWN = 0, // indicates invalid string
+    TYPE_AUDIO = 1,
+    TYPE_VIDEO = 2,
+  };
+
+  enum status_t {
+    // These numbers are important; see .cc file
+    STATUS_UNKNOWN = 0, // indicates invalid string
+    STATUS_SENDRECV = 1,
+    STATUS_SENDONLY = 2,
+    STATUS_RECVONLY = 3,
+    STATUS_INACTIVE = 4,
+  };
+
+  uint32 label;
+  type_t type;
+  status_t status;
+
+  static const char* TypeAsString(type_t type);
+  static const char* StatusAsString(status_t status);
+};
+
+class MucInviteRecvTask : public XmppTask {
+ public:
+  explicit MucInviteRecvTask(XmppTaskParentInterface* parent)
+      : XmppTask(parent, XmppEngine::HL_TYPE) {}
+  virtual int ProcessStart();
+
+  // First arg is inviter's JID; second is MUC's JID.
+  sigslot::signal3<const Jid&, const Jid&, const std::vector<AvailableMediaEntry>& > SignalInviteReceived;
+
+ protected:
+  virtual bool HandleStanza(const XmlElement* stanza);
+
+};
+
+}
+
+#endif
diff --git a/libjingle/examples/call/mucinvitesendtask.cc b/libjingle/examples/call/mucinvitesendtask.cc
new file mode 100644
index 0000000..2299b78
--- /dev/null
+++ b/libjingle/examples/call/mucinvitesendtask.cc
@@ -0,0 +1,63 @@
+/*
+ * 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/mucinvitesendtask.h"
+#include "webrtc/libjingle/xmpp/constants.h"
+#include "webrtc/libjingle/xmpp/xmppclient.h"
+
+namespace buzz {
+
+XmppReturnStatus
+MucInviteSendTask::Send(const Jid& to, const Jid& invitee) {
+  if (GetState() != STATE_INIT && GetState() != STATE_START)
+    return XMPP_RETURN_BADSTATE;
+
+  XmlElement* message = new XmlElement(QN_MESSAGE);
+  message->AddAttr(QN_TO, to.Str());
+  XmlElement* xstanza = new XmlElement(QN_MUC_USER_X);
+  XmlElement* invite = new XmlElement(QN_MUC_USER_INVITE);
+  invite->AddAttr(QN_TO, invitee.Str());
+  xstanza->AddElement(invite);
+  message->AddElement(xstanza);
+
+  QueueStanza(message);
+  return XMPP_RETURN_OK;
+}
+
+int
+MucInviteSendTask::ProcessStart() {
+  const XmlElement* stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+
+  if (SendStanza(stanza) != XMPP_RETURN_OK)
+    return STATE_ERROR;
+
+  return STATE_START;
+}
+
+}
diff --git a/libjingle/examples/call/mucinvitesendtask.h b/libjingle/examples/call/mucinvitesendtask.h
new file mode 100644
index 0000000..673efb0
--- /dev/null
+++ b/libjingle/examples/call/mucinvitesendtask.h
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#ifndef _MUCINVITESENDTASK_H_
+#define _MUCINVITESENDTASK_H_
+
+#include "talk/examples/call/muc.h"
+#include "webrtc/libjingle/xmpp/xmppengine.h"
+#include "webrtc/libjingle/xmpp/xmpptask.h"
+
+namespace buzz {
+
+class MucInviteSendTask : public XmppTask {
+public:
+  explicit MucInviteSendTask(XmppTaskParentInterface* parent)
+      : XmppTask(parent) {}
+  virtual ~MucInviteSendTask() {}
+
+  XmppReturnStatus Send(const Jid& to, const Jid& invitee);
+
+  virtual int ProcessStart();
+};
+
+}
+
+#endif
diff --git a/libjingle/examples/call/presencepushtask.cc b/libjingle/examples/call/presencepushtask.cc
new file mode 100644
index 0000000..8d8dca8
--- /dev/null
+++ b/libjingle/examples/call/presencepushtask.cc
@@ -0,0 +1,222 @@
+/*
+ * 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/presencepushtask.h"
+
+#include "talk/examples/call/muc.h"
+#include "webrtc/libjingle/xmpp/constants.h"
+#include "webrtc/base/stringencode.h"
+
+
+
+namespace buzz {
+
+// string helper functions -----------------------------------------------------
+
+static bool
+IsXmlSpace(int ch) {
+  return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
+}
+
+static bool ListContainsToken(const std::string & list,
+                              const std::string & token) {
+  size_t i = list.find(token);
+  if (i == std::string::npos || token.empty())
+    return false;
+  bool boundary_before = (i == 0 || IsXmlSpace(list[i - 1]));
+  bool boundary_after = (i == list.length() - token.length() ||
+                         IsXmlSpace(list[i + token.length()]));
+  return boundary_before && boundary_after;
+}
+
+
+bool PresencePushTask::HandleStanza(const XmlElement * stanza) {
+  if (stanza->Name() != QN_PRESENCE)
+    return false;
+  QueueStanza(stanza);
+  return true;
+}
+
+static bool IsUtf8FirstByte(int c) {
+  return (((c)&0x80)==0) || // is single byte
+    ((unsigned char)((c)-0xc0)<0x3e); // or is lead byte
+}
+
+int PresencePushTask::ProcessStart() {
+  const XmlElement * stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+
+  Jid from(stanza->Attr(QN_FROM));
+  std::map<Jid, buzz::Muc*>::const_iterator elem =
+      client_->mucs().find(from.BareJid());
+  if (elem == client_->mucs().end()) {
+    HandlePresence(from, stanza);
+  } else {
+    HandleMucPresence(elem->second, from, stanza);
+  }
+
+  return STATE_START;
+}
+
+void PresencePushTask::HandlePresence(const Jid& from,
+                                      const XmlElement* stanza) {
+  if (stanza->Attr(QN_TYPE) == STR_ERROR)
+    return;
+
+  PresenceStatus s;
+  FillStatus(from, stanza, &s);
+  SignalStatusUpdate(s);
+}
+
+void PresencePushTask::HandleMucPresence(buzz::Muc* muc,
+                                         const Jid& from,
+                                         const XmlElement* stanza) {
+  if (from == muc->local_jid()) {
+    if (!stanza->HasAttr(QN_TYPE)) {
+      // We joined the MUC.
+      const XmlElement* elem = stanza->FirstNamed(QN_MUC_USER_X);
+      // Status code=110 or 100 is not guaranteed to be present, so we
+      // only check the item element and Muc join status.
+      if (elem) {
+        if (elem->FirstNamed(QN_MUC_USER_ITEM) &&
+            muc->state() == buzz::Muc::MUC_JOINING) {
+          SignalMucJoined(muc->jid());
+        }
+      }
+    } else {
+      // We've been kicked. Bye.
+      int error = 0;
+      if (stanza->Attr(QN_TYPE) == STR_ERROR) {
+        const XmlElement* elem = stanza->FirstNamed(QN_ERROR);
+        if (elem && elem->HasAttr(QN_CODE)) {
+          error = atoi(elem->Attr(QN_CODE).c_str());
+        }
+      }
+      SignalMucLeft(muc->jid(), error);
+    }
+  } else {
+    MucPresenceStatus s;
+    FillMucStatus(from, stanza, &s);
+    SignalMucStatusUpdate(muc->jid(), s);
+  }
+}
+
+void PresencePushTask::FillStatus(const Jid& from, const XmlElement* stanza,
+                                  PresenceStatus* s) {
+  s->set_jid(from);
+  if (stanza->Attr(QN_TYPE) == STR_UNAVAILABLE) {
+    s->set_available(false);
+  } else {
+    s->set_available(true);
+    const XmlElement * status = stanza->FirstNamed(QN_STATUS);
+    if (status != NULL) {
+      s->set_status(status->BodyText());
+
+      // Truncate status messages longer than 300 bytes
+      if (s->status().length() > 300) {
+        size_t len = 300;
+
+        // Be careful not to split legal utf-8 chars in half
+        while (!IsUtf8FirstByte(s->status()[len]) && len > 0) {
+          len -= 1;
+        }
+        std::string truncated(s->status(), 0, len);
+        s->set_status(truncated);
+      }
+    }
+
+    const XmlElement * priority = stanza->FirstNamed(QN_PRIORITY);
+    if (priority != NULL) {
+      int pri;
+      if (rtc::FromString(priority->BodyText(), &pri)) {
+        s->set_priority(pri);
+      }
+    }
+
+    const XmlElement * show = stanza->FirstNamed(QN_SHOW);
+    if (show == NULL || show->FirstChild() == NULL) {
+      s->set_show(PresenceStatus::SHOW_ONLINE);
+    }
+    else {
+      if (show->BodyText() == "away") {
+        s->set_show(PresenceStatus::SHOW_AWAY);
+      }
+      else if (show->BodyText() == "xa") {
+        s->set_show(PresenceStatus::SHOW_XA);
+      }
+      else if (show->BodyText() == "dnd") {
+        s->set_show(PresenceStatus::SHOW_DND);
+      }
+      else if (show->BodyText() == "chat") {
+        s->set_show(PresenceStatus::SHOW_CHAT);
+      }
+      else {
+        s->set_show(PresenceStatus::SHOW_ONLINE);
+      }
+    }
+
+    const XmlElement * caps = stanza->FirstNamed(QN_CAPS_C);
+    if (caps != NULL) {
+      std::string node = caps->Attr(QN_NODE);
+      std::string ver = caps->Attr(QN_VER);
+      std::string exts = caps->Attr(QN_EXT);
+
+      s->set_know_capabilities(true);
+      s->set_caps_node(node);
+      s->set_version(ver);
+
+      if (ListContainsToken(exts, "voice-v1")) {
+        s->set_voice_capability(true);
+      }
+      if (ListContainsToken(exts, "video-v1")) {
+        s->set_video_capability(true);
+      }
+    }
+
+    const XmlElement* delay = stanza->FirstNamed(kQnDelayX);
+    if (delay != NULL) {
+      // Ideally we would parse this according to the Psuedo ISO-8601 rules
+      // that are laid out in JEP-0082:
+      // http://www.jabber.org/jeps/jep-0082.html
+      std::string stamp = delay->Attr(kQnStamp);
+      s->set_sent_time(stamp);
+    }
+
+    const XmlElement* nick = stanza->FirstNamed(QN_NICKNAME);
+    if (nick) {
+      s->set_nick(nick->BodyText());
+    }
+  }
+}
+
+void PresencePushTask::FillMucStatus(const Jid& from, const XmlElement* stanza,
+                                     MucPresenceStatus* s) {
+  FillStatus(from, stanza, s);
+}
+
+}
diff --git a/libjingle/examples/call/presencepushtask.h b/libjingle/examples/call/presencepushtask.h
new file mode 100644
index 0000000..e3d3268
--- /dev/null
+++ b/libjingle/examples/call/presencepushtask.h
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#ifndef _PRESENCEPUSHTASK_H_
+#define _PRESENCEPUSHTASK_H_
+
+#include <vector>
+
+#include "talk/examples/call/callclient.h"
+#include "webrtc/libjingle/xmpp/presencestatus.h"
+#include "webrtc/libjingle/xmpp/xmppengine.h"
+#include "webrtc/libjingle/xmpp/xmpptask.h"
+#include "webrtc/base/sigslot.h"
+
+namespace buzz {
+
+class PresencePushTask : public XmppTask {
+ public:
+  PresencePushTask(XmppTaskParentInterface* parent, CallClient* client)
+    : XmppTask(parent, XmppEngine::HL_TYPE),
+      client_(client) {}
+  virtual int ProcessStart();
+
+  sigslot::signal1<const PresenceStatus&> SignalStatusUpdate;
+  sigslot::signal1<const Jid&> SignalMucJoined;
+  sigslot::signal2<const Jid&, int> SignalMucLeft;
+  sigslot::signal2<const Jid&, const MucPresenceStatus&> SignalMucStatusUpdate;
+
+ protected:
+  virtual bool HandleStanza(const XmlElement * stanza);
+  void HandlePresence(const Jid& from, const XmlElement * stanza);
+  void HandleMucPresence(buzz::Muc* muc,
+                         const Jid& from, const XmlElement * stanza);
+  static void FillStatus(const Jid& from, const XmlElement * stanza,
+                         PresenceStatus* status);
+  static void FillMucStatus(const Jid& from, const XmlElement * stanza,
+                            MucPresenceStatus* status);
+
+ private:
+  CallClient* client_;
+};
+
+
+}
+
+#endif
diff --git a/libjingle/examples/login/login_main.cc b/libjingle/examples/login/login_main.cc
new file mode 100644
index 0000000..581c986
--- /dev/null
+++ b/libjingle/examples/login/login_main.cc
@@ -0,0 +1,67 @@
+/*
+ * 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 <stdio.h>
+
+#include <iostream>
+
+#include "webrtc/libjingle/xmpp/constants.h"
+#include "webrtc/libjingle/xmpp/xmppclientsettings.h"
+#include "webrtc/libjingle/xmpp/xmppengine.h"
+#include "webrtc/libjingle/xmpp/xmppthread.h"
+#include "webrtc/base/thread.h"
+
+int main(int argc, char **argv) {
+  std::cout << "OAuth Access Token: ";
+  std::string auth_token;
+  std::getline(std::cin, auth_token);
+
+  std::cout << "User Name: ";
+  std::string username;
+  std::getline(std::cin, username);
+
+  // Start xmpp on a different thread
+  buzz::XmppThread thread;
+  thread.Start();
+
+  buzz::XmppClientSettings xcs;
+  xcs.set_user(username.c_str());
+  xcs.set_host("gmail.com");
+  xcs.set_use_tls(buzz::TLS_DISABLED);
+  xcs.set_auth_token(buzz::AUTH_MECHANISM_OAUTH2,
+                     auth_token.c_str());
+  xcs.set_server(rtc::SocketAddress("talk.google.com", 5222));
+  thread.Login(xcs);
+
+  // Use main thread for console input
+  std::string line;
+  while (std::getline(std::cin, line)) {
+    if (line == "quit")
+      break;
+  }
+  return 0;
+}
diff --git a/libjingle/libjingle.gyp b/libjingle/libjingle.gyp
new file mode 100644
index 0000000..42b9992
--- /dev/null
+++ b/libjingle/libjingle.gyp
@@ -0,0 +1,92 @@
+# Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+{
+  'includes': ['../build/common.gypi'],
+  'variables': {
+    'talk_root%': '<(webrtc_root)/../talk',
+  },
+  'targets': [
+    {
+      'target_name': 'libjingle_xmpphelp',
+      'type': 'static_library',
+      'dependencies': [
+        '<(DEPTH)/third_party/expat/expat.gyp:expat',
+        '<(talk_root)/libjingle.gyp:libjingle',
+        '<(talk_root)/libjingle.gyp:libjingle_p2p',
+      ],
+      'sources': [
+        'xmpp/jingleinfotask.cc',
+        'xmpp/jingleinfotask.h',
+      ],
+    },  # target libjingle_xmpphelp
+    {
+      'target_name': 'login',
+      'type': 'executable',
+      'dependencies': [
+        'libjingle_xmpphelp',
+        '<(talk_root)/libjingle.gyp:libjingle',
+      ],
+      'sources': [
+        'examples/login/login_main.cc',
+      ],
+    },  # target login
+  ],
+  'conditions': [
+    # TODO(ronghuawu): Reenable building call.
+    # ['OS!="android"', {
+    #   'targets': [
+    #     {
+    #       'target_name': 'call',
+    #       'type': 'executable',
+    #       'dependencies': [
+    #         'libjingle.gyp:libjingle_p2p',
+    #         'libjingle_xmpphelp',
+    #       ],
+    #       'sources': [
+    #         'examples/call/call_main.cc',
+    #         'examples/call/callclient.cc',
+    #         'examples/call/callclient.h',
+    #         'examples/call/console.cc',
+    #         'examples/call/console.h',
+    #         'examples/call/friendinvitesendtask.cc',
+    #         'examples/call/friendinvitesendtask.h',
+    #         'examples/call/mediaenginefactory.cc',
+    #         'examples/call/mediaenginefactory.h',
+    #         'examples/call/muc.h',
+    #         'examples/call/mucinviterecvtask.cc',
+    #         'examples/call/mucinviterecvtask.h',
+    #         'examples/call/mucinvitesendtask.cc',
+    #         'examples/call/mucinvitesendtask.h',
+    #         'examples/call/presencepushtask.cc',
+    #         'examples/call/presencepushtask.h',
+    #       ],
+    #       'conditions': [
+    #         ['OS=="linux"', {
+    #           'link_settings': {
+    #             'libraries': [
+    #               '<!@(pkg-config --libs-only-l gobject-2.0 gthread-2.0'
+    #                   ' gtk+-2.0)',
+    #             ],
+    #           },
+    #         }],
+    #         ['OS=="win"', {
+    #           'msvs_settings': {
+    #             'VCLinkerTool': {
+    #               'AdditionalDependencies': [
+    #                 'strmiids.lib',
+    #               ],
+    #             },
+    #           },
+    #         }],
+    #       ],  # conditions
+    #     },  # target call
+    #   ], # targets
+    # }],  # OS!="android"
+  ],
+}