/* * 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 #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 "talk/p2p/base/sessionmanager.h" #include "talk/p2p/client/basicportallocator.h" #include "talk/p2p/client/sessionmanagertask.h" #include "talk/session/media/mediamessages.h" #include "talk/session/media/mediasessionclient.h" #include "talk/xmpp/constants.h" #include "talk/xmpp/hangoutpubsubclient.h" #include "talk/xmpp/mucroomconfigtask.h" #include "talk/xmpp/mucroomlookuptask.h" #include "talk/xmpp/pingtask.h" #include "talk/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& words, size_t index, const std::string& def) { if (words.size() > index) { return words[index]; } else { return def; } } int GetInt(const std::vector& 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 words; int start = -1; int state = 0; for (int index = 0; index <= static_cast(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 "; 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* 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& 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& call_sessions = sessions_[call->id()]; for (std::vector::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& calls = media_client_->calls(); for (std::map::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& sessions = sessions_[call_->id()]; for (std::vector::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::const_iterator k; const std::vector* 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& calls = media_client_->calls(); std::map::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 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(task); JoinMuc(config_task->room_jid()); } void CallClient::OnMucInviteReceived(const buzz::Jid& inviter, const buzz::Jid& room, const std::vector& 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::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 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& names) { for (size_t i = 0; i < names.size(); ++i) { console_->PrintLine("%d: %s", static_cast(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::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* 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& video_streams, bool enable) { std::vector::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& sessions = sessions_[call->id()]; std::set 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-@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::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::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); } }