/** * @file llvoiceclient.cpp * @brief Implementation of LLVoiceClient class which is the interface to the voice client process. * * $LicenseInfo:firstyear=2001&license=viewergpl$ * * Copyright (c) 2001-2009, Linden Research, Inc. * * Second Life Viewer Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in doc/GPL-license.txt in this distribution, or * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file doc/FLOSS-exception.txt in this software distribution, or * online at * http://secondlifegrid.net/programs/open_source/licensing/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llvoiceclient.h" #include #include "llsdutil.h" #include "llvoavatar.h" #include "llbufferstream.h" #include "llfile.h" #ifdef LL_STANDALONE # include "expat.h" #else # include "expat/expat.h" #endif #include "llcallbacklist.h" #include "llviewerregion.h" #include "llviewernetwork.h" // for gGridChoice #include "llbase64.h" #include "llviewercontrol.h" #include "llkeyboard.h" #include "llappviewer.h" // for gDisconnected, gDisableVoice #include "llmutelist.h" // to check for muted avatars #include "llagent.h" #include "llcachename.h" #include "llimview.h" // for LLIMMgr #include "llimpanel.h" // for LLVoiceChannel #include "llparcel.h" #include "llviewerparcelmgr.h" #include "llfirstuse.h" #include "llviewerwindow.h" #include "llviewercamera.h" #include "llvoavatarself.h" #include "llfloaterfriends.h" //VIVOX, inorder to refresh communicate panel #include "llfloaterchat.h" // for LLFloaterChat::addChat() // for base64 decoding #include "apr_base64.h" // for SHA1 hash #include "apr_sha1.h" // for MD5 hash #include "llmd5.h" #define USE_SESSION_GROUPS 0 static bool sConnectingToAgni = false; F32 LLVoiceClient::OVERDRIVEN_POWER_LEVEL = 0.7f; const F32 SPEAKING_TIMEOUT = 1.f; const int VOICE_MAJOR_VERSION = 1; const int VOICE_MINOR_VERSION = 0; LLVoiceClient *gVoiceClient = NULL; // Don't retry connecting to the daemon more frequently than this: const F32 CONNECT_THROTTLE_SECONDS = 1.0f; // Don't send positional updates more frequently than this: const F32 UPDATE_THROTTLE_SECONDS = 0.1f; const F32 LOGIN_RETRY_SECONDS = 10.0f; const int MAX_LOGIN_RETRIES = 12; static void setUUIDFromStringHash(LLUUID &uuid, const std::string &str) { LLMD5 md5_uuid; md5_uuid.update((const unsigned char*)str.data(), str.size()); md5_uuid.finalize(); md5_uuid.raw_digest(uuid.mData); } static int scale_mic_volume(float volume) { // incoming volume has the range [0.0 ... 2.0], with 1.0 as the default. // Map it as follows: 0.0 -> 40, 1.0 -> 44, 2.0 -> 75 volume -= 1.0f; // offset volume to the range [-1.0 ... 1.0], with 0 at the default. int scaled_volume = 44; // offset scaled_volume by its default level if(volume < 0.0f) scaled_volume += ((int)(volume * 4.0f)); // (44 - 40) else scaled_volume += ((int)(volume * 31.0f)); // (75 - 44) return scaled_volume; } static int scale_speaker_volume(float volume) { // incoming volume has the range [0.0 ... 1.0], with 0.5 as the default. // Map it as follows: 0.0 -> 0, 0.5 -> 62, 1.0 -> 75 volume -= 0.5f; // offset volume to the range [-0.5 ... 0.5], with 0 at the default. int scaled_volume = 62; // offset scaled_volume by its default level if(volume < 0.0f) scaled_volume += ((int)(volume * 124.0f)); // (62 - 0) * 2 else scaled_volume += ((int)(volume * 26.0f)); // (75 - 62) * 2 return scaled_volume; } class LLViewerVoiceAccountProvisionResponder : public LLHTTPClient::Responder { public: LLViewerVoiceAccountProvisionResponder(int retries) { mRetries = retries; } virtual void error(U32 status, const std::string& reason) { if ( mRetries > 0 ) { LL_WARNS("Voice") << "ProvisionVoiceAccountRequest returned an error, retrying. status = " << status << ", reason = \"" << reason << "\"" << LL_ENDL; if ( gVoiceClient ) gVoiceClient->requestVoiceAccountProvision( mRetries - 1); } else { LL_WARNS("Voice") << "ProvisionVoiceAccountRequest returned an error, too many retries (giving up). status = " << status << ", reason = \"" << reason << "\"" << LL_ENDL; if ( gVoiceClient ) gVoiceClient->giveUp(); } } virtual void result(const LLSD& content) { if ( gVoiceClient ) { std::string voice_sip_uri_hostname; std::string voice_account_server_uri; LL_DEBUGS("Voice") << "ProvisionVoiceAccountRequest response:" << ll_pretty_print_sd(content) << LL_ENDL; if(content.has("voice_sip_uri_hostname")) voice_sip_uri_hostname = content["voice_sip_uri_hostname"].asString(); // this key is actually misnamed -- it will be an entire URI, not just a hostname. if(content.has("voice_account_server_name")) voice_account_server_uri = content["voice_account_server_name"].asString(); gVoiceClient->login( content["username"].asString(), content["password"].asString(), voice_sip_uri_hostname, voice_account_server_uri); } } private: int mRetries; }; /** * @class LLVivoxProtocolParser * @brief This class helps construct new LLIOPipe specializations * @see LLIOPipe * * THOROUGH_DESCRIPTION */ class LLVivoxProtocolParser : public LLIOPipe { LOG_CLASS(LLVivoxProtocolParser); public: LLVivoxProtocolParser(); virtual ~LLVivoxProtocolParser(); protected: /* @name LLIOPipe virtual implementations */ //@{ /** * @brief Process the data in buffer */ virtual EStatus process_impl( const LLChannelDescriptors& channels, buffer_ptr_t& buffer, bool& eos, LLSD& context, LLPumpIO* pump); //@} std::string mInput; // Expat control members XML_Parser parser; int responseDepth; bool ignoringTags; bool isEvent; int ignoreDepth; // Members for processing responses. The values are transient and only valid within a call to processResponse(). bool squelchDebugOutput; int returnCode; int statusCode; std::string statusString; std::string requestId; std::string actionString; std::string connectorHandle; std::string versionID; std::string accountHandle; std::string sessionHandle; std::string sessionGroupHandle; std::string alias; std::string applicationString; // Members for processing events. The values are transient and only valid within a call to processResponse(). std::string eventTypeString; int state; std::string uriString; bool isChannel; bool incoming; bool enabled; std::string nameString; std::string audioMediaString; std::string displayNameString; int participantType; bool isLocallyMuted; bool isModeratorMuted; bool isSpeaking; int volume; F32 energy; std::string messageHeader; std::string messageBody; std::string notificationType; bool hasText; bool hasAudio; bool hasVideo; bool terminated; std::string blockMask; std::string presenceOnly; std::string autoAcceptMask; std::string autoAddAsBuddy; int numberOfAliases; std::string subscriptionHandle; std::string subscriptionType; // Members for processing text between tags std::string textBuffer; bool accumulateText; void reset(); void processResponse(std::string tag); static void XMLCALL ExpatStartTag(void *data, const char *el, const char **attr); static void XMLCALL ExpatEndTag(void *data, const char *el); static void XMLCALL ExpatCharHandler(void *data, const XML_Char *s, int len); void StartTag(const char *tag, const char **attr); void EndTag(const char *tag); void CharData(const char *buffer, int length); }; LLVivoxProtocolParser::LLVivoxProtocolParser() { parser = NULL; parser = XML_ParserCreate(NULL); reset(); } void LLVivoxProtocolParser::reset() { responseDepth = 0; ignoringTags = false; accumulateText = false; energy = 0.f; ignoreDepth = 0; isChannel = false; isEvent = false; isLocallyMuted = false; isModeratorMuted = false; isSpeaking = false; participantType = 0; squelchDebugOutput = false; returnCode = -1; state = 0; statusCode = 0; volume = 0; textBuffer.clear(); alias.clear(); numberOfAliases = 0; applicationString.clear(); } //virtual LLVivoxProtocolParser::~LLVivoxProtocolParser() { if (parser) XML_ParserFree(parser); } // virtual LLIOPipe::EStatus LLVivoxProtocolParser::process_impl( const LLChannelDescriptors& channels, buffer_ptr_t& buffer, bool& eos, LLSD& context, LLPumpIO* pump) { LLBufferStream istr(channels, buffer.get()); std::ostringstream ostr; while (istr.good()) { char buf[1024]; istr.read(buf, sizeof(buf)); mInput.append(buf, istr.gcount()); } // Look for input delimiter(s) in the input buffer. If one is found, send the message to the xml parser. int start = 0; int delim; while((delim = mInput.find("\n\n\n", start)) != std::string::npos) { // Reset internal state of the LLVivoxProtocolParser (no effect on the expat parser) reset(); XML_ParserReset(parser, NULL); XML_SetElementHandler(parser, ExpatStartTag, ExpatEndTag); XML_SetCharacterDataHandler(parser, ExpatCharHandler); XML_SetUserData(parser, this); XML_Parse(parser, mInput.data() + start, delim - start, false); // If this message isn't set to be squelched, output the raw XML received. if(!squelchDebugOutput) { LL_DEBUGS("Voice") << "parsing: " << mInput.substr(start, delim - start) << LL_ENDL; } start = delim + 3; } if(start != 0) mInput = mInput.substr(start); LL_DEBUGS("VivoxProtocolParser") << "at end, mInput is: " << mInput << LL_ENDL; if(!gVoiceClient->mConnected) { // If voice has been disabled, we just want to close the socket. This does so. LL_INFOS("Voice") << "returning STATUS_STOP" << LL_ENDL; return STATUS_STOP; } return STATUS_OK; } void XMLCALL LLVivoxProtocolParser::ExpatStartTag(void *data, const char *el, const char **attr) { if (data) { LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; object->StartTag(el, attr); } } // -------------------------------------------------------------------------------- void XMLCALL LLVivoxProtocolParser::ExpatEndTag(void *data, const char *el) { if (data) { LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; object->EndTag(el); } } // -------------------------------------------------------------------------------- void XMLCALL LLVivoxProtocolParser::ExpatCharHandler(void *data, const XML_Char *s, int len) { if (data) { LLVivoxProtocolParser *object = (LLVivoxProtocolParser*)data; object->CharData(s, len); } } // -------------------------------------------------------------------------------- void LLVivoxProtocolParser::StartTag(const char *tag, const char **attr) { // Reset the text accumulator. We shouldn't have strings that are inturrupted by new tags textBuffer.clear(); // only accumulate text if we're not ignoring tags. accumulateText = !ignoringTags; if (responseDepth == 0) { isEvent = !stricmp("Event", tag); if (!stricmp("Response", tag) || isEvent) { // Grab the attributes while (*attr) { const char *key = *attr++; const char *value = *attr++; if (!stricmp("requestId", key)) { requestId = value; } else if (!stricmp("action", key)) { actionString = value; } else if (!stricmp("type", key)) { eventTypeString = value; } } } LL_DEBUGS("VivoxProtocolParser") << tag << " (" << responseDepth << ")" << LL_ENDL; } else { if (ignoringTags) { LL_DEBUGS("VivoxProtocolParser") << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; } else { LL_DEBUGS("VivoxProtocolParser") << tag << " (" << responseDepth << ")" << LL_ENDL; // Ignore the InputXml stuff so we don't get confused if (!stricmp("InputXml", tag)) { ignoringTags = true; ignoreDepth = responseDepth; accumulateText = false; LL_DEBUGS("VivoxProtocolParser") << "starting ignore, ignoreDepth is " << ignoreDepth << LL_ENDL; } else if (!stricmp("CaptureDevices", tag)) { gVoiceClient->clearCaptureDevices(); } else if (!stricmp("RenderDevices", tag)) { gVoiceClient->clearRenderDevices(); } else if (!stricmp("Buddies", tag)) { gVoiceClient->deleteAllBuddies(); } else if (!stricmp("BlockRules", tag)) { gVoiceClient->deleteAllBlockRules(); } else if (!stricmp("AutoAcceptRules", tag)) { gVoiceClient->deleteAllAutoAcceptRules(); } } } responseDepth++; } // -------------------------------------------------------------------------------- void LLVivoxProtocolParser::EndTag(const char *tag) { const std::string& string = textBuffer; bool clearbuffer = true; responseDepth--; if (ignoringTags) { if (ignoreDepth == responseDepth) { LL_DEBUGS("VivoxProtocolParser") << "end of ignore" << LL_ENDL; ignoringTags = false; } else { LL_DEBUGS("VivoxProtocolParser") << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; } } if (!ignoringTags) { LL_DEBUGS("VivoxProtocolParser") << "processing tag " << tag << " (depth = " << responseDepth << ")" << LL_ENDL; // Closing a tag. Finalize the text we've accumulated and reset if (!stricmp("ReturnCode", tag)) returnCode = strtol(string.c_str(), NULL, 10); else if (!stricmp("SessionHandle", tag)) sessionHandle = string; else if (!stricmp("SessionGroupHandle", tag)) sessionGroupHandle = string; else if (!stricmp("StatusCode", tag)) statusCode = strtol(string.c_str(), NULL, 10); else if (!stricmp("StatusString", tag)) statusString = string; else if (!stricmp("ParticipantURI", tag)) uriString = string; else if (!stricmp("Volume", tag)) volume = strtol(string.c_str(), NULL, 10); else if (!stricmp("Energy", tag)) energy = (F32)strtod(string.c_str(), NULL); else if (!stricmp("IsModeratorMuted", tag)) isModeratorMuted = !stricmp(string.c_str(), "true"); else if (!stricmp("IsSpeaking", tag)) isSpeaking = !stricmp(string.c_str(), "true"); else if (!stricmp("Alias", tag)) alias = string; else if (!stricmp("NumberOfAliases", tag)) numberOfAliases = strtol(string.c_str(), NULL, 10); else if (!stricmp("Application", tag)) applicationString = string; else if (!stricmp("ConnectorHandle", tag)) connectorHandle = string; else if (!stricmp("VersionID", tag)) versionID = string; else if (!stricmp("AccountHandle", tag)) accountHandle = string; else if (!stricmp("State", tag)) state = strtol(string.c_str(), NULL, 10); else if (!stricmp("URI", tag)) uriString = string; else if (!stricmp("IsChannel", tag)) isChannel = !stricmp(string.c_str(), "true"); else if (!stricmp("Incoming", tag)) incoming = !stricmp(string.c_str(), "true"); else if (!stricmp("Enabled", tag)) enabled = !stricmp(string.c_str(), "true"); else if (!stricmp("Name", tag)) nameString = string; else if (!stricmp("AudioMedia", tag)) audioMediaString = string; else if (!stricmp("ChannelName", tag)) nameString = string; else if (!stricmp("DisplayName", tag)) displayNameString = string; else if (!stricmp("AccountName", tag)) nameString = string; else if (!stricmp("ParticipantType", tag)) participantType = strtol(string.c_str(), NULL, 10); else if (!stricmp("IsLocallyMuted", tag)) isLocallyMuted = !stricmp(string.c_str(), "true"); else if (!stricmp("MicEnergy", tag)) energy = (F32)strtod(string.c_str(), NULL); else if (!stricmp("ChannelName", tag)) nameString = string; else if (!stricmp("ChannelURI", tag)) uriString = string; else if (!stricmp("BuddyURI", tag)) uriString = string; else if (!stricmp("Presence", tag)) statusString = string; else if (!stricmp("Device", tag)) { // This closing tag shouldn't clear the accumulated text. clearbuffer = false; } else if (!stricmp("CaptureDevice", tag)) { gVoiceClient->addCaptureDevice(textBuffer); } else if (!stricmp("RenderDevice", tag)) { gVoiceClient->addRenderDevice(textBuffer); } else if (!stricmp("Buddy", tag)) { gVoiceClient->processBuddyListEntry(uriString, displayNameString); } else if (!stricmp("BlockRule", tag)) { gVoiceClient->addBlockRule(blockMask, presenceOnly); } else if (!stricmp("BlockMask", tag)) blockMask = string; else if (!stricmp("PresenceOnly", tag)) presenceOnly = string; else if (!stricmp("AutoAcceptRule", tag)) { gVoiceClient->addAutoAcceptRule(autoAcceptMask, autoAddAsBuddy); } else if (!stricmp("AutoAcceptMask", tag)) autoAcceptMask = string; else if (!stricmp("AutoAddAsBuddy", tag)) autoAddAsBuddy = string; else if (!stricmp("MessageHeader", tag)) messageHeader = string; else if (!stricmp("MessageBody", tag)) messageBody = string; else if (!stricmp("NotificationType", tag)) notificationType = string; else if (!stricmp("HasText", tag)) hasText = !stricmp(string.c_str(), "true"); else if (!stricmp("HasAudio", tag)) hasAudio = !stricmp(string.c_str(), "true"); else if (!stricmp("HasVideo", tag)) hasVideo = !stricmp(string.c_str(), "true"); else if (!stricmp("Terminated", tag)) terminated = !stricmp(string.c_str(), "true"); else if (!stricmp("SubscriptionHandle", tag)) subscriptionHandle = string; else if (!stricmp("SubscriptionType", tag)) subscriptionType = string; if(clearbuffer) { textBuffer.clear(); accumulateText= false; } if (responseDepth == 0) { // We finished all of the XML, process the data processResponse(tag); } } } // -------------------------------------------------------------------------------- void LLVivoxProtocolParser::CharData(const char *buffer, int length) { /* This method is called for anything that isn't a tag, which can be text you want that lies between tags, and a lot of stuff you don't want like file formatting (tabs, spaces, CR/LF, etc). Only copy text if we are in accumulate mode... */ if (accumulateText) textBuffer.append(buffer, length); } // -------------------------------------------------------------------------------- void LLVivoxProtocolParser::processResponse(std::string tag) { LL_DEBUGS("VivoxProtocolParser") << tag << LL_ENDL; // SLIM SDK: the SDK now returns a statusCode of "200" (OK) for success. This is a change vs. previous SDKs. // According to Mike S., "The actual API convention is that responses with return codes of 0 are successful, regardless of the status code returned", // so I believe this will give correct behavior. if(returnCode == 0) statusCode = 0; if (isEvent) { const char *eventTypeCstr = eventTypeString.c_str(); if (!stricmp(eventTypeCstr, "AccountLoginStateChangeEvent")) { gVoiceClient->accountLoginStateChangeEvent(accountHandle, statusCode, statusString, state); } else if (!stricmp(eventTypeCstr, "SessionAddedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 sip:confctl-1408789@bhr.vivox.com true false */ gVoiceClient->sessionAddedEvent(uriString, alias, sessionHandle, sessionGroupHandle, isChannel, incoming, nameString, applicationString); } else if (!stricmp(eventTypeCstr, "SessionRemovedEvent")) { gVoiceClient->sessionRemovedEvent(sessionHandle, sessionGroupHandle); } else if (!stricmp(eventTypeCstr, "SessionGroupAddedEvent")) { gVoiceClient->sessionGroupAddedEvent(sessionGroupHandle); } else if (!stricmp(eventTypeCstr, "MediaStreamUpdatedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 200 OK 2 false */ gVoiceClient->mediaStreamUpdatedEvent(sessionHandle, sessionGroupHandle, statusCode, statusString, state, incoming); } else if (!stricmp(eventTypeCstr, "TextStreamUpdatedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg1 c1_m1000xFnPP04IpREWNkuw1cOXlhw==1 true 1 true */ gVoiceClient->textStreamUpdatedEvent(sessionHandle, sessionGroupHandle, enabled, state, incoming); } else if (!stricmp(eventTypeCstr, "ParticipantAddedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4 c1_m1000xFnPP04IpREWNkuw1cOXlhw==4 sip:xI5auBZ60SJWIk606-1JGRQ==@bhr.vivox.com xI5auBZ60SJWIk606-1JGRQ== 0 */ gVoiceClient->participantAddedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString, displayNameString, participantType); } else if (!stricmp(eventTypeCstr, "ParticipantRemovedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4 c1_m1000xFnPP04IpREWNkuw1cOXlhw==4 sip:xtx7YNV-3SGiG7rA1fo5Ndw==@bhr.vivox.com xtx7YNV-3SGiG7rA1fo5Ndw== */ gVoiceClient->participantRemovedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString); } else if (!stricmp(eventTypeCstr, "ParticipantUpdatedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 sip:xFnPP04IpREWNkuw1cOXlhw==@bhr.vivox.com false true 44 0.0879437 */ // These happen so often that logging them is pretty useless. squelchDebugOutput = true; gVoiceClient->participantUpdatedEvent(sessionHandle, sessionGroupHandle, uriString, alias, isModeratorMuted, isSpeaking, volume, energy); } else if (!stricmp(eventTypeCstr, "AuxAudioPropertiesEvent")) { gVoiceClient->auxAudioPropertiesEvent(energy); } else if (!stricmp(eventTypeCstr, "BuddyPresenceEvent")) { gVoiceClient->buddyPresenceEvent(uriString, alias, statusString, applicationString); } else if (!stricmp(eventTypeCstr, "BuddyAndGroupListChangedEvent")) { // The buddy list was updated during parsing. // Need to recheck against the friends list. gVoiceClient->buddyListChanged(); } else if (!stricmp(eventTypeCstr, "BuddyChangedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw== sip:x9fFHFZjOTN6OESF1DUPrZQ==@bhr.vivox.com Monroe Tester 0 Set */ // TODO: Question: Do we need to process this at all? } else if (!stricmp(eventTypeCstr, "MessageEvent")) { gVoiceClient->messageEvent(sessionHandle, uriString, alias, messageHeader, messageBody, applicationString); } else if (!stricmp(eventTypeCstr, "SessionNotificationEvent")) { gVoiceClient->sessionNotificationEvent(sessionHandle, uriString, notificationType); } else if (!stricmp(eventTypeCstr, "SubscriptionEvent")) { gVoiceClient->subscriptionEvent(uriString, subscriptionHandle, alias, displayNameString, applicationString, subscriptionType); } else if (!stricmp(eventTypeCstr, "SessionUpdatedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 c1_m1000xFnPP04IpREWNkuw1cOXlhw==0 sip:confctl-9@bhd.vivox.com 0 50 1 0 000 0 */ // We don't need to process this, but we also shouldn't warn on it, since that confuses people. } else if (!stricmp(eventTypeCstr, "SessionGroupRemovedEvent")) { /* c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0 */ // We don't need to process this, but we also shouldn't warn on it, since that confuses people. } else { LL_WARNS("VivoxProtocolParser") << "Unknown event type " << eventTypeString << LL_ENDL; } } else { const char *actionCstr = actionString.c_str(); if (!stricmp(actionCstr, "Connector.Create.1")) { gVoiceClient->connectorCreateResponse(statusCode, statusString, connectorHandle, versionID); } else if (!stricmp(actionCstr, "Account.Login.1")) { gVoiceClient->loginResponse(statusCode, statusString, accountHandle, numberOfAliases); } else if (!stricmp(actionCstr, "Session.Create.1")) { gVoiceClient->sessionCreateResponse(requestId, statusCode, statusString, sessionHandle); } else if (!stricmp(actionCstr, "SessionGroup.AddSession.1")) { gVoiceClient->sessionGroupAddSessionResponse(requestId, statusCode, statusString, sessionHandle); } else if (!stricmp(actionCstr, "Session.Connect.1")) { gVoiceClient->sessionConnectResponse(requestId, statusCode, statusString); } else if (!stricmp(actionCstr, "Account.Logout.1")) { gVoiceClient->logoutResponse(statusCode, statusString); } else if (!stricmp(actionCstr, "Connector.InitiateShutdown.1")) { gVoiceClient->connectorShutdownResponse(statusCode, statusString); } else if (!stricmp(actionCstr, "Account.ListBlockRules.1")) { gVoiceClient->accountListBlockRulesResponse(statusCode, statusString); } else if (!stricmp(actionCstr, "Account.ListAutoAcceptRules.1")) { gVoiceClient->accountListAutoAcceptRulesResponse(statusCode, statusString); } else if (!stricmp(actionCstr, "Session.Set3DPosition.1")) { // We don't need to process these, but they're so spammy we don't want to log them. squelchDebugOutput = true; } /* else if (!stricmp(actionCstr, "Account.ChannelGetList.1")) { gVoiceClient->channelGetListResponse(statusCode, statusString); } else if (!stricmp(actionCstr, "Connector.AccountCreate.1")) { } else if (!stricmp(actionCstr, "Connector.MuteLocalMic.1")) { } else if (!stricmp(actionCstr, "Connector.MuteLocalSpeaker.1")) { } else if (!stricmp(actionCstr, "Connector.SetLocalMicVolume.1")) { } else if (!stricmp(actionCstr, "Connector.SetLocalSpeakerVolume.1")) { } else if (!stricmp(actionCstr, "Session.ListenerSetPosition.1")) { } else if (!stricmp(actionCstr, "Session.SpeakerSetPosition.1")) { } else if (!stricmp(actionCstr, "Session.AudioSourceSetPosition.1")) { } else if (!stricmp(actionCstr, "Session.GetChannelParticipants.1")) { } else if (!stricmp(actionCstr, "Account.ChannelCreate.1")) { } else if (!stricmp(actionCstr, "Account.ChannelUpdate.1")) { } else if (!stricmp(actionCstr, "Account.ChannelDelete.1")) { } else if (!stricmp(actionCstr, "Account.ChannelCreateAndInvite.1")) { } else if (!stricmp(actionCstr, "Account.ChannelFolderCreate.1")) { } else if (!stricmp(actionCstr, "Account.ChannelFolderUpdate.1")) { } else if (!stricmp(actionCstr, "Account.ChannelFolderDelete.1")) { } else if (!stricmp(actionCstr, "Account.ChannelAddModerator.1")) { } else if (!stricmp(actionCstr, "Account.ChannelDeleteModerator.1")) { } */ } } /////////////////////////////////////////////////////////////////////////////////////////////// class LLVoiceClientMuteListObserver : public LLMuteListObserver { /* virtual */ void onChange() { gVoiceClient->muteListChanged();} }; class LLVoiceClientFriendsObserver : public LLFriendObserver { public: /* virtual */ void changed(U32 mask) { gVoiceClient->updateFriends(mask);} }; static LLVoiceClientMuteListObserver mutelist_listener; static bool sMuteListListener_listening = false; static LLVoiceClientFriendsObserver *friendslist_listener = NULL; /////////////////////////////////////////////////////////////////////////////////////////////// class LLVoiceClientCapResponder : public LLHTTPClient::Responder { public: LLVoiceClientCapResponder(void){}; virtual void error(U32 status, const std::string& reason); // called with bad status codes virtual void result(const LLSD& content); private: }; void LLVoiceClientCapResponder::error(U32 status, const std::string& reason) { LL_WARNS("Voice") << "LLVoiceClientCapResponder::error(" << status << ": " << reason << ")" << LL_ENDL; } void LLVoiceClientCapResponder::result(const LLSD& content) { LLSD::map_const_iterator iter; LL_DEBUGS("Voice") << "ParcelVoiceInfoRequest response:" << ll_pretty_print_sd(content) << LL_ENDL; if ( content.has("voice_credentials") ) { LLSD voice_credentials = content["voice_credentials"]; std::string uri; std::string credentials; if ( voice_credentials.has("channel_uri") ) { uri = voice_credentials["channel_uri"].asString(); } if ( voice_credentials.has("channel_credentials") ) { credentials = voice_credentials["channel_credentials"].asString(); } gVoiceClient->setSpatialChannel(uri, credentials); } } #if LL_WINDOWS static HANDLE sGatewayHandle = 0; static bool isGatewayRunning() { bool result = false; if(sGatewayHandle != 0) { DWORD waitresult = WaitForSingleObject(sGatewayHandle, 0); if(waitresult != WAIT_OBJECT_0) { result = true; } } return result; } static void killGateway() { if(sGatewayHandle != 0) { TerminateProcess(sGatewayHandle,0); } } #else // Mac and linux static pid_t sGatewayPID = 0; static bool isGatewayRunning() { bool result = false; if(sGatewayPID != 0) { // A kill with signal number 0 has no effect, just does error checking. It should return an error if the process no longer exists. if(kill(sGatewayPID, 0) == 0) { result = true; } } return result; } static void killGateway() { if(sGatewayPID != 0) { kill(sGatewayPID, SIGTERM); } } #endif /////////////////////////////////////////////////////////////////////////////////////////////// LLVoiceClient::LLVoiceClient() : mState(stateDisabled), mSessionTerminateRequested(false), mRelogRequested(false), mConnected(false), mPump(NULL), mTuningMode(false), mTuningEnergy(0.0f), mTuningMicVolume(0), mTuningMicVolumeDirty(true), mTuningSpeakerVolume(0), mTuningSpeakerVolumeDirty(true), mTuningExitState(stateDisabled), mAreaVoiceDisabled(false), mAudioSession(NULL), mAudioSessionChanged(false), mNextAudioSession(NULL), mCurrentParcelLocalID(0), mNumberOfAliases(0), mCommandCookie(0), mLoginRetryCount(0), mBuddyListMapPopulated(false), mBlockRulesListReceived(false), mAutoAcceptRulesListReceived(false), mCaptureDeviceDirty(false), mRenderDeviceDirty(false), mSpatialCoordsDirty(false), mPTTDirty(true), mPTT(true), mUsePTT(true), mPTTIsMiddleMouse(false), mPTTKey(0), mPTTIsToggle(false), mUserPTTState(false), mMuteMic(false), mFriendsListDirty(true), mEarLocation(0), mSpeakerVolumeDirty(true), mSpeakerMuteDirty(true), mSpeakerVolume(0), mMicVolume(0), mMicVolumeDirty(true), mVoiceEnabled(false), mWriteInProgress(false), mLipSyncEnabled(false) { gVoiceClient = this; #if LL_DARWIN || LL_LINUX || LL_SOLARIS // HACK: THIS DOES NOT BELONG HERE // When the vivox daemon dies, the next write attempt on our socket generates a SIGPIPE, which kills us. // This should cause us to ignore SIGPIPE and handle the error through proper channels. // This should really be set up elsewhere. Where should it go? signal(SIGPIPE, SIG_IGN); // Since we're now launching the gateway with fork/exec instead of system(), we need to deal with zombie processes. // Ignoring SIGCHLD should prevent zombies from being created. Alternately, we could use wait(), but I'd rather not do that. signal(SIGCHLD, SIG_IGN); #endif // set up state machine setState(stateDisabled); gIdleCallbacks.addFunction(idle, this); } //--------------------------------------------------- LLVoiceClient::~LLVoiceClient() { } //---------------------------------------------- void LLVoiceClient::init(LLPumpIO *pump) { // constructor will set up gVoiceClient LLVoiceClient::getInstance()->mPump = pump; LLVoiceClient::getInstance()->updateSettings(); } void LLVoiceClient::terminate() { if(gVoiceClient) { // gVoiceClient->leaveAudioSession(); gVoiceClient->logout(); // As of SDK version 4885, this should no longer be necessary. It will linger after the socket close if it needs to. // ms_sleep(2000); gVoiceClient->connectorShutdown(); gVoiceClient->closeSocket(); // Need to do this now -- bad things happen if the destructor does it later. // This will do unpleasant things on windows. // killGateway(); // Don't do this anymore -- LLSingleton will take care of deleting the object. // delete gVoiceClient; // Hint to other code not to access the voice client anymore. gVoiceClient = NULL; } } //--------------------------------------------------- void LLVoiceClient::updateSettings() { setVoiceEnabled(gSavedSettings.getBOOL("EnableVoiceChat")); setUsePTT(gSavedSettings.getBOOL("PTTCurrentlyEnabled")); std::string keyString = gSavedSettings.getString("PushToTalkButton"); setPTTKey(keyString); setPTTIsToggle(gSavedSettings.getBOOL("PushToTalkToggle")); setEarLocation(gSavedSettings.getS32("VoiceEarLocation")); std::string inputDevice = gSavedSettings.getString("VoiceInputAudioDevice"); setCaptureDevice(inputDevice); std::string outputDevice = gSavedSettings.getString("VoiceOutputAudioDevice"); setRenderDevice(outputDevice); F32 mic_level = gSavedSettings.getF32("AudioLevelMic"); setMicGain(mic_level); setLipSyncEnabled(gSavedSettings.getBOOL("LipSyncEnabled")); } ///////////////////////////// // utility functions bool LLVoiceClient::writeString(const std::string &str) { bool result = false; if(mConnected) { apr_status_t err; apr_size_t size = (apr_size_t)str.size(); apr_size_t written = size; //MARK: Turn this on to log outgoing XML // LL_DEBUGS("Voice") << "sending: " << str << LL_ENDL; // check return code - sockets will fail (broken, etc.) err = apr_socket_send( mSocket->getSocket(), (const char*)str.data(), &written); if(err == 0) { // Success. result = true; } // TODO: handle partial writes (written is number of bytes written) // Need to set socket to non-blocking before this will work. // else if(APR_STATUS_IS_EAGAIN(err)) // { // // // } else { // Assume any socket error means something bad. For now, just close the socket. char buf[MAX_STRING]; LL_WARNS("Voice") << "apr error " << err << " ("<< apr_strerror(err, buf, MAX_STRING) << ") sending data to vivox daemon." << LL_ENDL; daemonDied(); } } return result; } ///////////////////////////// // session control messages void LLVoiceClient::connectorCreate() { std::ostringstream stream; std::string logpath = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ""); std::string loglevel = "0"; // Transition to stateConnectorStarted when the connector handle comes back. setState(stateConnectorStarting); std::string savedLogLevel = gSavedSettings.getString("VivoxDebugLevel"); if(savedLogLevel != "-1") { LL_DEBUGS("Voice") << "creating connector with logging enabled" << LL_ENDL; loglevel = "10"; } stream << "" << "V2 SDK" << "" << mVoiceAccountServerURI << "" << "Normal" << "" << "" << logpath << "" << "Connector" << ".log" << "" << loglevel << "" << "" << "SecondLifeViewer.1" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::connectorShutdown() { setState(stateConnectorStopping); if(!mConnectorHandle.empty()) { std::ostringstream stream; stream << "" << "" << mConnectorHandle << "" << "" << "\n\n\n"; mConnectorHandle.clear(); writeString(stream.str()); } } void LLVoiceClient::userAuthorized(const std::string& firstName, const std::string& lastName, const LLUUID &agentID) { mAccountFirstName = firstName; mAccountLastName = lastName; mAccountDisplayName = firstName; mAccountDisplayName += " "; mAccountDisplayName += lastName; LL_INFOS("Voice") << "name \"" << mAccountDisplayName << "\" , ID " << agentID << LL_ENDL; sConnectingToAgni = LLViewerLogin::getInstance()->isInProductionGrid(); mAccountName = nameFromID(agentID); } void LLVoiceClient::requestVoiceAccountProvision(S32 retries) { if ( gAgent.getRegion() && mVoiceEnabled ) { std::string url = gAgent.getRegion()->getCapability( "ProvisionVoiceAccountRequest"); if ( url == "" ) return; LLHTTPClient::post( url, LLSD(), new LLViewerVoiceAccountProvisionResponder(retries)); } } void LLVoiceClient::login( const std::string& account_name, const std::string& password, const std::string& voice_sip_uri_hostname, const std::string& voice_account_server_uri) { mVoiceSIPURIHostName = voice_sip_uri_hostname; mVoiceAccountServerURI = voice_account_server_uri; if(!mAccountHandle.empty()) { // Already logged in. LL_WARNS("Voice") << "Called while already logged in." << LL_ENDL; // Don't process another login. return; } else if ( account_name != mAccountName ) { //TODO: error? LL_WARNS("Voice") << "Wrong account name! " << account_name << " instead of " << mAccountName << LL_ENDL; } else { mAccountPassword = password; } std::string debugSIPURIHostName = gSavedSettings.getString("VivoxDebugSIPURIHostName"); if( !debugSIPURIHostName.empty() ) { mVoiceSIPURIHostName = debugSIPURIHostName; } if( mVoiceSIPURIHostName.empty() ) { // we have an empty account server name // so we fall back to hardcoded defaults if(sConnectingToAgni) { // Use the release account server mVoiceSIPURIHostName = "bhr.vivox.com"; } else { // Use the development account server mVoiceSIPURIHostName = "bhd.vivox.com"; } } std::string debugAccountServerURI = gSavedSettings.getString("VivoxDebugVoiceAccountServerURI"); if( !debugAccountServerURI.empty() ) { mVoiceAccountServerURI = debugAccountServerURI; } if( mVoiceAccountServerURI.empty() ) { // If the account server URI isn't specified, construct it from the SIP URI hostname mVoiceAccountServerURI = "https://www." + mVoiceSIPURIHostName + "/api2/"; } } void LLVoiceClient::idle(void* user_data) { LLVoiceClient* self = (LLVoiceClient*)user_data; self->stateMachine(); } std::string LLVoiceClient::state2string(LLVoiceClient::state inState) { std::string result = "UNKNOWN"; // Prevent copy-paste errors when updating this list... #define CASE(x) case x: result = #x; break switch(inState) { CASE(stateDisableCleanup); CASE(stateDisabled); CASE(stateStart); CASE(stateDaemonLaunched); CASE(stateConnecting); CASE(stateConnected); CASE(stateIdle); CASE(stateMicTuningStart); CASE(stateMicTuningRunning); CASE(stateMicTuningStop); CASE(stateConnectorStart); CASE(stateConnectorStarting); CASE(stateConnectorStarted); CASE(stateLoginRetry); CASE(stateLoginRetryWait); CASE(stateNeedsLogin); CASE(stateLoggingIn); CASE(stateLoggedIn); CASE(stateCreatingSessionGroup); CASE(stateNoChannel); CASE(stateJoiningSession); CASE(stateSessionJoined); CASE(stateRunning); CASE(stateLeavingSession); CASE(stateSessionTerminated); CASE(stateLoggingOut); CASE(stateLoggedOut); CASE(stateConnectorStopping); CASE(stateConnectorStopped); CASE(stateConnectorFailed); CASE(stateConnectorFailedWaiting); CASE(stateLoginFailed); CASE(stateLoginFailedWaiting); CASE(stateJoinSessionFailed); CASE(stateJoinSessionFailedWaiting); CASE(stateJail); } #undef CASE return result; } std::string LLVoiceClientStatusObserver::status2string(LLVoiceClientStatusObserver::EStatusType inStatus) { std::string result = "UNKNOWN"; // Prevent copy-paste errors when updating this list... #define CASE(x) case x: result = #x; break switch(inStatus) { CASE(STATUS_LOGIN_RETRY); CASE(STATUS_LOGGED_IN); CASE(STATUS_JOINING); CASE(STATUS_JOINED); CASE(STATUS_LEFT_CHANNEL); CASE(STATUS_VOICE_DISABLED); CASE(BEGIN_ERROR_STATUS); CASE(ERROR_CHANNEL_FULL); CASE(ERROR_CHANNEL_LOCKED); CASE(ERROR_NOT_AVAILABLE); CASE(ERROR_UNKNOWN); default: break; } #undef CASE return result; } void LLVoiceClient::setState(state inState) { LL_DEBUGS("Voice") << "entering state " << state2string(inState) << LL_ENDL; mState = inState; } void LLVoiceClient::stateMachine() { if(gDisconnected) { // The viewer has been disconnected from the sim. Disable voice. setVoiceEnabled(false); } if(mVoiceEnabled) { updatePosition(); } else if(mTuningMode) { // Tuning mode is special -- it needs to launch SLVoice even if voice is disabled. } else { if((getState() != stateDisabled) && (getState() != stateDisableCleanup)) { // User turned off voice support. Send the cleanup messages, close the socket, and reset. if(!mConnected) { // if voice was turned off after the daemon was launched but before we could connect to it, we may need to issue a kill. LL_INFOS("Voice") << "Disabling voice before connection to daemon, terminating." << LL_ENDL; killGateway(); } logout(); connectorShutdown(); setState(stateDisableCleanup); } } // Check for parcel boundary crossing { LLViewerRegion *region = gAgent.getRegion(); LLParcel *parcel = LLViewerParcelMgr::getInstance()->getAgentParcel(); if(region && parcel) { S32 parcelLocalID = parcel->getLocalID(); std::string regionName = region->getName(); std::string capURI = region->getCapability("ParcelVoiceInfoRequest"); // LL_DEBUGS("Voice") << "Region name = \"" << regionName << "\", parcel local ID = " << parcelLocalID << ", cap URI = \"" << capURI << "\"" << LL_ENDL; // The region name starts out empty and gets filled in later. // Also, the cap gets filled in a short time after the region cross, but a little too late for our purposes. // If either is empty, wait for the next time around. if(!regionName.empty()) { if(!capURI.empty()) { if((parcelLocalID != mCurrentParcelLocalID) || (regionName != mCurrentRegionName)) { // We have changed parcels. Initiate a parcel channel lookup. mCurrentParcelLocalID = parcelLocalID; mCurrentRegionName = regionName; parcelChanged(); } } else { LL_WARNS("Voice") << "region doesn't have ParcelVoiceInfoRequest capability. This is normal for a short time after teleporting, but bad if it persists for very long." << LL_ENDL; } } } } switch(getState()) { //MARK: stateDisableCleanup case stateDisableCleanup: // Clean up and reset everything. closeSocket(); deleteAllSessions(); deleteAllBuddies(); mConnectorHandle.clear(); mAccountHandle.clear(); mAccountPassword.clear(); mVoiceAccountServerURI.clear(); setState(stateDisabled); break; //MARK: stateDisabled case stateDisabled: if(mTuningMode || (mVoiceEnabled && !mAccountName.empty())) { setState(stateStart); } break; //MARK: stateStart case stateStart: if(gSavedSettings.getBOOL("CmdLineDisableVoice")) { // Voice is locked out, we must not launch the vivox daemon. setState(stateJail); } else if(!isGatewayRunning()) { if(true) { // Launch the voice daemon // *FIX:Mani - Using the executable dir instead // of mAppRODataDir, the working directory from which the app // is launched. //std::string exe_path = gDirUtilp->getAppRODataDir(); std::string exe_path = gDirUtilp->getExecutableDir(); exe_path += gDirUtilp->getDirDelimiter(); #if LL_WINDOWS exe_path += "SLVoice.exe"; #elif LL_DARWIN exe_path += "../Resources/SLVoice"; #else exe_path += "SLVoice"; #endif // See if the vivox executable exists llstat s; if(!LLFile::stat(exe_path, &s)) { // vivox executable exists. Build the command line and launch the daemon. // SLIM SDK: these arguments are no longer necessary. // std::string args = " -p tcp -h -c"; std::string args; std::string cmd; std::string loglevel = gSavedSettings.getString("VivoxDebugLevel"); if(loglevel.empty()) { loglevel = "-1"; // turn logging off completely } args += " -ll "; args += loglevel; LL_DEBUGS("Voice") << "Args for SLVoice: " << args << LL_ENDL; #if LL_WINDOWS PROCESS_INFORMATION pinfo; STARTUPINFOA sinfo; memset(&sinfo, 0, sizeof(sinfo)); std::string exe_dir = gDirUtilp->getAppRODataDir(); cmd = "SLVoice.exe"; cmd += args; // So retarded. Windows requires that the second parameter to CreateProcessA be a writable (non-const) string... char *args2 = new char[args.size() + 1]; strcpy(args2, args.c_str()); if(!CreateProcessA(exe_path.c_str(), args2, NULL, NULL, FALSE, 0, NULL, exe_dir.c_str(), &sinfo, &pinfo)) { // DWORD dwErr = GetLastError(); } else { // foo = pinfo.dwProcessId; // get your pid here if you want to use it later on // CloseHandle(pinfo.hProcess); // stops leaks - nothing else sGatewayHandle = pinfo.hProcess; CloseHandle(pinfo.hThread); // stops leaks - nothing else } delete[] args2; #else // LL_WINDOWS // This should be the same for mac and linux { std::vector arglist; arglist.push_back(exe_path); // Split the argument string into separate strings for each argument typedef boost::tokenizer > tokenizer; boost::char_separator sep(" "); tokenizer tokens(args, sep); tokenizer::iterator token_iter; for(token_iter = tokens.begin(); token_iter != tokens.end(); ++token_iter) { arglist.push_back(*token_iter); } // create an argv vector for the child process char **fakeargv = new char*[arglist.size() + 1]; int i; for(i=0; i < arglist.size(); i++) fakeargv[i] = const_cast(arglist[i].c_str()); fakeargv[i] = NULL; fflush(NULL); // flush all buffers before the child inherits them pid_t id = vfork(); if(id == 0) { // child execv(exe_path.c_str(), fakeargv); // If we reach this point, the exec failed. // Use _exit() instead of exit() per the vfork man page. _exit(0); } // parent delete[] fakeargv; sGatewayPID = id; } #endif // LL_WINDOWS mDaemonHost = LLHost(gSavedSettings.getString("VoiceHost").c_str(), gSavedSettings.getU32("VoicePort")); } else { LL_INFOS("Voice") << exe_path << " not found." << LL_ENDL; } } else { // SLIM SDK: port changed from 44124 to 44125. // We can connect to a client gateway running on another host. This is useful for testing. // To do this, launch the gateway on a nearby host like this: // vivox-gw.exe -p tcp -i 0.0.0.0:44125 // and put that host's IP address here. mDaemonHost = LLHost(gSavedSettings.getString("VoiceHost"), gSavedSettings.getU32("VoicePort")); } mUpdateTimer.start(); mUpdateTimer.setTimerExpirySec(CONNECT_THROTTLE_SECONDS); setState(stateDaemonLaunched); // Dirty the states we'll need to sync with the daemon when it comes up. mPTTDirty = true; mMicVolumeDirty = true; mSpeakerVolumeDirty = true; mSpeakerMuteDirty = true; // These only need to be set if they're not default (i.e. empty string). mCaptureDeviceDirty = !mCaptureDevice.empty(); mRenderDeviceDirty = !mRenderDevice.empty(); mMainSessionGroupHandle.clear(); } break; //MARK: stateDaemonLaunched case stateDaemonLaunched: if(mUpdateTimer.hasExpired()) { LL_DEBUGS("Voice") << "Connecting to vivox daemon" << LL_ENDL; mUpdateTimer.setTimerExpirySec(CONNECT_THROTTLE_SECONDS); if(!mSocket) { mSocket = LLSocket::create(gAPRPoolp, LLSocket::STREAM_TCP); } mConnected = mSocket->blockingConnect(mDaemonHost); if(mConnected) { setState(stateConnecting); } else { // If the connect failed, the socket may have been put into a bad state. Delete it. closeSocket(); } } break; //MARK: stateConnecting case stateConnecting: // Can't do this until we have the pump available. if(mPump) { // MBW -- Note to self: pumps and pipes examples in // indra/test/io.cpp // indra/test/llpipeutil.{cpp|h} // Attach the pumps and pipes LLPumpIO::chain_t readChain; readChain.push_back(LLIOPipe::ptr_t(new LLIOSocketReader(mSocket))); readChain.push_back(LLIOPipe::ptr_t(new LLVivoxProtocolParser())); mPump->addChain(readChain, NEVER_CHAIN_EXPIRY_SECS); setState(stateConnected); } break; //MARK: stateConnected case stateConnected: // Initial devices query getCaptureDevicesSendMessage(); getRenderDevicesSendMessage(); mLoginRetryCount = 0; setState(stateIdle); break; //MARK: stateIdle case stateIdle: // This is the idle state where we're connected to the daemon but haven't set up a connector yet. if(mTuningMode) { mTuningExitState = stateIdle; setState(stateMicTuningStart); } else if(!mVoiceEnabled) { // We never started up the connector. This will shut down the daemon. setState(stateConnectorStopped); } else if(!mAccountName.empty()) { LLViewerRegion *region = gAgent.getRegion(); if(region) { if ( region->getCapability("ProvisionVoiceAccountRequest") != "" ) { if ( mAccountPassword.empty() ) { requestVoiceAccountProvision(); } setState(stateConnectorStart); } else { LL_WARNS("Voice") << "region doesn't have ProvisionVoiceAccountRequest capability!" << LL_ENDL; } } } break; //MARK: stateMicTuningStart case stateMicTuningStart: if(mUpdateTimer.hasExpired()) { if(mCaptureDeviceDirty || mRenderDeviceDirty) { // These can't be changed while in tuning mode. Set them before starting. std::ostringstream stream; buildSetCaptureDevice(stream); buildSetRenderDevice(stream); if(!stream.str().empty()) { writeString(stream.str()); } // This will come around again in the same state and start the capture, after the timer expires. mUpdateTimer.start(); mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS); } else { // duration parameter is currently unused, per Mike S. tuningCaptureStartSendMessage(10000); setState(stateMicTuningRunning); } } break; //MARK: stateMicTuningRunning case stateMicTuningRunning: if(!mTuningMode || mCaptureDeviceDirty || mRenderDeviceDirty) { // All of these conditions make us leave tuning mode. setState(stateMicTuningStop); } else { // process mic/speaker volume changes if(mTuningMicVolumeDirty || mTuningSpeakerVolumeDirty) { std::ostringstream stream; if(mTuningMicVolumeDirty) { LL_INFOS("Voice") << "setting tuning mic level to " << mTuningMicVolume << LL_ENDL; stream << "" << "" << mTuningMicVolume << "" << "\n\n\n"; } if(mTuningSpeakerVolumeDirty) { stream << "" << "" << mTuningSpeakerVolume << "" << "\n\n\n"; } mTuningMicVolumeDirty = false; mTuningSpeakerVolumeDirty = false; if(!stream.str().empty()) { writeString(stream.str()); } } } break; //MARK: stateMicTuningStop case stateMicTuningStop: { // transition out of mic tuning tuningCaptureStopSendMessage(); setState(mTuningExitState); // if we exited just to change devices, this will keep us from re-entering too fast. mUpdateTimer.start(); mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS); } break; //MARK: stateConnectorStart case stateConnectorStart: if(!mVoiceEnabled) { // We were never logged in. This will shut down the connector. setState(stateLoggedOut); } else if(!mVoiceAccountServerURI.empty()) { connectorCreate(); } break; //MARK: stateConnectorStarting case stateConnectorStarting: // waiting for connector handle // connectorCreateResponse() will transition from here to stateConnectorStarted. break; //MARK: stateConnectorStarted case stateConnectorStarted: // connector handle received if(!mVoiceEnabled) { // We were never logged in. This will shut down the connector. setState(stateLoggedOut); } else { // The connector is started. Send a login message. setState(stateNeedsLogin); } break; //MARK: stateLoginRetry case stateLoginRetry: if(mLoginRetryCount == 0) { // First retry -- display a message to the user notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGIN_RETRY); } mLoginRetryCount++; if(mLoginRetryCount > MAX_LOGIN_RETRIES) { LL_WARNS("Voice") << "too many login retries, giving up." << LL_ENDL; setState(stateLoginFailed); } else { LL_INFOS("Voice") << "will retry login in " << LOGIN_RETRY_SECONDS << " seconds." << LL_ENDL; mUpdateTimer.start(); mUpdateTimer.setTimerExpirySec(LOGIN_RETRY_SECONDS); setState(stateLoginRetryWait); } break; //MARK: stateLoginRetryWait case stateLoginRetryWait: if(mUpdateTimer.hasExpired()) { setState(stateNeedsLogin); } break; //MARK: stateNeedsLogin case stateNeedsLogin: if(!mAccountPassword.empty()) { setState(stateLoggingIn); loginSendMessage(); } break; //MARK: stateLoggingIn case stateLoggingIn: // waiting for account handle // loginResponse() will transition from here to stateLoggedIn. break; //MARK: stateLoggedIn case stateLoggedIn: // account handle received notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGGED_IN); // request the current set of block rules (we'll need them when updating the friends list) accountListBlockRulesSendMessage(); // request the current set of auto-accept rules accountListAutoAcceptRulesSendMessage(); // Set up the mute list observer if it hasn't been set up already. if((!sMuteListListener_listening)) { LLMuteList::getInstance()->addObserver(&mutelist_listener); sMuteListListener_listening = true; } // Set up the friends list observer if it hasn't been set up already. if(friendslist_listener == NULL) { friendslist_listener = new LLVoiceClientFriendsObserver; LLAvatarTracker::instance().addObserver(friendslist_listener); } // Set the initial state of mic mute, local speaker volume, etc. { std::ostringstream stream; buildLocalAudioUpdates(stream); if(!stream.str().empty()) { writeString(stream.str()); } } #if USE_SESSION_GROUPS // create the main session group sessionGroupCreateSendMessage(); setState(stateCreatingSessionGroup); #else // Not using session groups -- skip the stateCreatingSessionGroup state. setState(stateNoChannel); // Initial kick-off of channel lookup logic parcelChanged(); #endif break; //MARK: stateCreatingSessionGroup case stateCreatingSessionGroup: if(mSessionTerminateRequested || !mVoiceEnabled) { // TODO: Question: is this the right way out of this state setState(stateSessionTerminated); } else if(!mMainSessionGroupHandle.empty()) { setState(stateNoChannel); // Start looped recording (needed for "panic button" anti-griefing tool) recordingLoopStart(); // Initial kick-off of channel lookup logic parcelChanged(); } break; //MARK: stateNoChannel case stateNoChannel: // Do this here as well as inside sendPositionalUpdate(). // Otherwise, if you log in but don't join a proximal channel (such as when your login location has voice disabled), your friends list won't sync. sendFriendsListUpdates(); if(mSessionTerminateRequested || !mVoiceEnabled) { // TODO: Question: Is this the right way out of this state? setState(stateSessionTerminated); } else if(mTuningMode) { mTuningExitState = stateNoChannel; setState(stateMicTuningStart); } else if(sessionNeedsRelog(mNextAudioSession)) { requestRelog(); setState(stateSessionTerminated); } else if(mNextAudioSession) { sessionState *oldSession = mAudioSession; mAudioSession = mNextAudioSession; if(!mAudioSession->mReconnect) { mNextAudioSession = NULL; } // The old session may now need to be deleted. reapSession(oldSession); if(!mAudioSession->mHandle.empty()) { // Connect to a session by session handle sessionMediaConnectSendMessage(mAudioSession); } else { // Connect to a session by URI sessionCreateSendMessage(mAudioSession, true, false); } notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINING); setState(stateJoiningSession); } else if(!mSpatialSessionURI.empty()) { // If we're not headed elsewhere and have a spatial URI, return to spatial. switchChannel(mSpatialSessionURI, true, false, false, mSpatialSessionCredentials); } break; //MARK: stateJoiningSession case stateJoiningSession: // waiting for session handle // joinedAudioSession() will transition from here to stateSessionJoined. if(!mVoiceEnabled) { // User bailed out during connect -- jump straight to teardown. setState(stateSessionTerminated); } else if(mSessionTerminateRequested) { if(mAudioSession && !mAudioSession->mHandle.empty()) { // Only allow direct exits from this state in p2p calls (for cancelling an invite). // Terminating a half-connected session on other types of calls seems to break something in the vivox gateway. if(mAudioSession->mIsP2P) { sessionMediaDisconnectSendMessage(mAudioSession); setState(stateSessionTerminated); } } } break; //MARK: stateSessionJoined case stateSessionJoined: // session handle received // It appears that I need to wait for BOTH the SessionGroup.AddSession response and the SessionStateChangeEvent with state 4 // before continuing from this state. They can happen in either order, and if I don't wait for both, things can get stuck. // For now, the SessionGroup.AddSession response handler sets mSessionHandle and the SessionStateChangeEvent handler transitions to stateSessionJoined. // This is a cheap way to make sure both have happened before proceeding. if(mAudioSession && mAudioSession->mVoiceEnabled) { // Dirty state that may need to be sync'ed with the daemon. mPTTDirty = true; mSpeakerVolumeDirty = true; mSpatialCoordsDirty = true; setState(stateRunning); // Start the throttle timer mUpdateTimer.start(); mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS); // Events that need to happen when a session is joined could go here. // Maybe send initial spatial data? notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINED); } else if(!mVoiceEnabled) { // User bailed out during connect -- jump straight to teardown. setState(stateSessionTerminated); } else if(mSessionTerminateRequested) { // Only allow direct exits from this state in p2p calls (for cancelling an invite). // Terminating a half-connected session on other types of calls seems to break something in the vivox gateway. if(mAudioSession && mAudioSession->mIsP2P) { sessionMediaDisconnectSendMessage(mAudioSession); setState(stateSessionTerminated); } } break; //MARK: stateRunning case stateRunning: // steady state // Disabling voice or disconnect requested. if(!mVoiceEnabled || mSessionTerminateRequested) { leaveAudioSession(); } else { // Figure out whether the PTT state needs to change { bool newPTT; if(mUsePTT) { // If configured to use PTT, track the user state. newPTT = mUserPTTState; } else { // If not configured to use PTT, it should always be true (otherwise the user will be unable to speak). newPTT = true; } if(mMuteMic) { // This always overrides any other PTT setting. newPTT = false; } // Dirty if state changed. if(newPTT != mPTT) { mPTT = newPTT; mPTTDirty = true; } } if(!inSpatialChannel()) { // When in a non-spatial channel, never send positional updates. mSpatialCoordsDirty = false; } else { // Do the calculation that enforces the listener<->speaker tether (and also updates the real camera position) enforceTether(); } // Send an update if the ptt state has changed (which shouldn't be able to happen that often -- the user can only click so fast) // or every 10hz, whichever is sooner. if((mAudioSession && mAudioSession->mVolumeDirty) || mPTTDirty || mSpeakerVolumeDirty || mUpdateTimer.hasExpired()) { mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS); sendPositionalUpdate(); } } break; //MARK: stateLeavingSession case stateLeavingSession: // waiting for terminate session response // The handler for the Session.Terminate response will transition from here to stateSessionTerminated. break; //MARK: stateSessionTerminated case stateSessionTerminated: // Must do this first, since it uses mAudioSession. notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); if(mAudioSession) { sessionState *oldSession = mAudioSession; mAudioSession = NULL; // We just notified status observers about this change. Don't do it again. mAudioSessionChanged = false; // The old session may now need to be deleted. reapSession(oldSession); } else { LL_WARNS("Voice") << "stateSessionTerminated with NULL mAudioSession" << LL_ENDL; } // Always reset the terminate request flag when we get here. mSessionTerminateRequested = false; if(mVoiceEnabled && !mRelogRequested) { // Just leaving a channel, go back to stateNoChannel (the "logged in but have no channel" state). setState(stateNoChannel); } else { // Shutting down voice, continue with disconnecting. logout(); // The state machine will take it from here mRelogRequested = false; } break; //MARK: stateLoggingOut case stateLoggingOut: // waiting for logout response // The handler for the AccountLoginStateChangeEvent will transition from here to stateLoggedOut. break; //MARK: stateLoggedOut case stateLoggedOut: // logout response received // Once we're logged out, all these things are invalid. mAccountHandle.clear(); deleteAllSessions(); deleteAllBuddies(); if(mVoiceEnabled && !mRelogRequested) { // User was logged out, but wants to be logged in. Send a new login request. setState(stateNeedsLogin); } else { // shut down the connector connectorShutdown(); } break; //MARK: stateConnectorStopping case stateConnectorStopping: // waiting for connector stop // The handler for the Connector.InitiateShutdown response will transition from here to stateConnectorStopped. break; //MARK: stateConnectorStopped case stateConnectorStopped: // connector stop received setState(stateDisableCleanup); break; //MARK: stateConnectorFailed case stateConnectorFailed: setState(stateConnectorFailedWaiting); break; //MARK: stateConnectorFailedWaiting case stateConnectorFailedWaiting: if(!mVoiceEnabled) { setState(stateDisableCleanup); } break; //MARK: stateLoginFailed case stateLoginFailed: setState(stateLoginFailedWaiting); break; //MARK: stateLoginFailedWaiting case stateLoginFailedWaiting: if(!mVoiceEnabled) { setState(stateDisableCleanup); } break; //MARK: stateJoinSessionFailed case stateJoinSessionFailed: // Transition to error state. Send out any notifications here. if(mAudioSession) { LL_WARNS("Voice") << "stateJoinSessionFailed: (" << mAudioSession->mErrorStatusCode << "): " << mAudioSession->mErrorStatusString << LL_ENDL; } else { LL_WARNS("Voice") << "stateJoinSessionFailed with no current session" << LL_ENDL; } notifyStatusObservers(LLVoiceClientStatusObserver::ERROR_UNKNOWN); setState(stateJoinSessionFailedWaiting); break; //MARK: stateJoinSessionFailedWaiting case stateJoinSessionFailedWaiting: // Joining a channel failed, either due to a failed channel name -> sip url lookup or an error from the join message. // Region crossings may leave this state and try the join again. if(mSessionTerminateRequested) { setState(stateSessionTerminated); } break; //MARK: stateJail case stateJail: // We have given up. Do nothing. break; } if(mAudioSession && mAudioSession->mParticipantsChanged) { mAudioSession->mParticipantsChanged = false; mAudioSessionChanged = true; } if(mAudioSessionChanged) { mAudioSessionChanged = false; notifyParticipantObservers(); } } void LLVoiceClient::closeSocket(void) { mSocket.reset(); mConnected = false; } void LLVoiceClient::loginSendMessage() { std::ostringstream stream; bool autoPostCrashDumps = gSavedSettings.getBOOL("VivoxAutoPostCrashDumps"); stream << "" << "" << mConnectorHandle << "" << "" << mAccountName << "" << "" << mAccountPassword << "" << "VerifyAnswer" << "true" << "Application" << "5" << (autoPostCrashDumps?"true":"") << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::logout() { // Ensure that we'll re-request provisioning before logging in again mAccountPassword.clear(); mVoiceAccountServerURI.clear(); setState(stateLoggingOut); logoutSendMessage(); } void LLVoiceClient::logoutSendMessage() { if(!mAccountHandle.empty()) { std::ostringstream stream; stream << "" << "" << mAccountHandle << "" << "" << "\n\n\n"; mAccountHandle.clear(); writeString(stream.str()); } } void LLVoiceClient::accountListBlockRulesSendMessage() { if(!mAccountHandle.empty()) { std::ostringstream stream; LL_DEBUGS("Voice") << "requesting block rules" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::accountListAutoAcceptRulesSendMessage() { if(!mAccountHandle.empty()) { std::ostringstream stream; LL_DEBUGS("Voice") << "requesting auto-accept rules" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::sessionGroupCreateSendMessage() { if(!mAccountHandle.empty()) { std::ostringstream stream; LL_DEBUGS("Voice") << "creating session group" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "Normal" << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::sessionCreateSendMessage(sessionState *session, bool startAudio, bool startText) { LL_DEBUGS("Voice") << "requesting create: " << session->mSIPURI << LL_ENDL; session->mCreateInProgress = true; if(startAudio) { session->mMediaConnectInProgress = true; } std::ostringstream stream; stream << "mSIPURI << "\" action=\"Session.Create.1\">" << "" << mAccountHandle << "" << "" << session->mSIPURI << ""; static const std::string allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" "0123456789" "-._~"; if(!session->mHash.empty()) { stream << "" << LLURI::escape(session->mHash, allowed_chars) << "" << "SHA1UserName"; } stream << "" << (startAudio?"true":"false") << "" << "" << (startText?"true":"false") << "" << "" << mChannelName << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionGroupAddSessionSendMessage(sessionState *session, bool startAudio, bool startText) { LL_DEBUGS("Voice") << "requesting create: " << session->mSIPURI << LL_ENDL; session->mCreateInProgress = true; if(startAudio) { session->mMediaConnectInProgress = true; } std::string password; if(!session->mHash.empty()) { static const std::string allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" "0123456789" "-._~" ; password = LLURI::escape(session->mHash, allowed_chars); } std::ostringstream stream; stream << "mSIPURI << "\" action=\"SessionGroup.AddSession.1\">" << "" << session->mGroupHandle << "" << "" << session->mSIPURI << "" << "" << mChannelName << "" << "" << (startAudio?"true":"false") << "" << "" << (startText?"true":"false") << "" << "" << password << "" << "SHA1UserName" << "\n\n\n" ; writeString(stream.str()); } void LLVoiceClient::sessionMediaConnectSendMessage(sessionState *session) { LL_DEBUGS("Voice") << "connecting audio to session handle: " << session->mHandle << LL_ENDL; session->mMediaConnectInProgress = true; std::ostringstream stream; stream << "mHandle << "\" action=\"Session.MediaConnect.1\">" << "" << session->mGroupHandle << "" << "" << session->mHandle << "" << "Audio" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionTextConnectSendMessage(sessionState *session) { LL_DEBUGS("Voice") << "connecting text to session handle: " << session->mHandle << LL_ENDL; std::ostringstream stream; stream << "mHandle << "\" action=\"Session.TextConnect.1\">" << "" << session->mGroupHandle << "" << "" << session->mHandle << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionTerminate() { mSessionTerminateRequested = true; } void LLVoiceClient::requestRelog() { mSessionTerminateRequested = true; mRelogRequested = true; } void LLVoiceClient::leaveAudioSession() { if(mAudioSession) { LL_DEBUGS("Voice") << "leaving session: " << mAudioSession->mSIPURI << LL_ENDL; switch(getState()) { case stateNoChannel: // In this case, we want to pretend the join failed so our state machine doesn't get stuck. // Skip the join failed transition state so we don't send out error notifications. setState(stateJoinSessionFailedWaiting); break; case stateJoiningSession: case stateSessionJoined: case stateRunning: if(!mAudioSession->mHandle.empty()) { #if RECORD_EVERYTHING // HACK: for testing only // Save looped recording std::string savepath("/tmp/vivoxrecording"); { time_t now = time(NULL); const size_t BUF_SIZE = 64; char time_str[BUF_SIZE]; /* Flawfinder: ignore */ strftime(time_str, BUF_SIZE, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now)); savepath += time_str; } recordingLoopSave(savepath); #endif sessionMediaDisconnectSendMessage(mAudioSession); setState(stateLeavingSession); } else { LL_WARNS("Voice") << "called with no session handle" << LL_ENDL; setState(stateSessionTerminated); } break; case stateJoinSessionFailed: case stateJoinSessionFailedWaiting: setState(stateSessionTerminated); break; default: LL_WARNS("Voice") << "called from unknown state" << LL_ENDL; break; } } else { LL_WARNS("Voice") << "called with no active session" << LL_ENDL; setState(stateSessionTerminated); } } void LLVoiceClient::sessionTerminateSendMessage(sessionState *session) { std::ostringstream stream; LL_DEBUGS("Voice") << "Sending Session.Terminate with handle " << session->mHandle << LL_ENDL; stream << "" << "" << session->mHandle << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionGroupTerminateSendMessage(sessionState *session) { std::ostringstream stream; LL_DEBUGS("Voice") << "Sending SessionGroup.Terminate with handle " << session->mGroupHandle << LL_ENDL; stream << "" << "" << session->mGroupHandle << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionMediaDisconnectSendMessage(sessionState *session) { std::ostringstream stream; LL_DEBUGS("Voice") << "Sending Session.MediaDisconnect with handle " << session->mHandle << LL_ENDL; stream << "" << "" << session->mGroupHandle << "" << "" << session->mHandle << "" << "Audio" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::sessionTextDisconnectSendMessage(sessionState *session) { std::ostringstream stream; LL_DEBUGS("Voice") << "Sending Session.TextDisconnect with handle " << session->mHandle << LL_ENDL; stream << "" << "" << session->mGroupHandle << "" << "" << session->mHandle << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::getCaptureDevicesSendMessage() { std::ostringstream stream; stream << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::getRenderDevicesSendMessage() { std::ostringstream stream; stream << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::clearCaptureDevices() { LL_DEBUGS("Voice") << "called" << LL_ENDL; mCaptureDevices.clear(); } void LLVoiceClient::addCaptureDevice(const std::string& name) { LL_DEBUGS("Voice") << name << LL_ENDL; mCaptureDevices.push_back(name); } LLVoiceClient::deviceList *LLVoiceClient::getCaptureDevices() { return &mCaptureDevices; } void LLVoiceClient::setCaptureDevice(const std::string& name) { if(name == "Default") { if(!mCaptureDevice.empty()) { mCaptureDevice.clear(); mCaptureDeviceDirty = true; } } else { if(mCaptureDevice != name) { mCaptureDevice = name; mCaptureDeviceDirty = true; } } } void LLVoiceClient::clearRenderDevices() { LL_DEBUGS("Voice") << "called" << LL_ENDL; mRenderDevices.clear(); } void LLVoiceClient::addRenderDevice(const std::string& name) { LL_DEBUGS("Voice") << name << LL_ENDL; mRenderDevices.push_back(name); } LLVoiceClient::deviceList *LLVoiceClient::getRenderDevices() { return &mRenderDevices; } void LLVoiceClient::setRenderDevice(const std::string& name) { if(name == "Default") { if(!mRenderDevice.empty()) { mRenderDevice.clear(); mRenderDeviceDirty = true; } } else { if(mRenderDevice != name) { mRenderDevice = name; mRenderDeviceDirty = true; } } } void LLVoiceClient::tuningStart() { mTuningMode = true; if(getState() >= stateNoChannel) { sessionTerminate(); } } void LLVoiceClient::tuningStop() { mTuningMode = false; } bool LLVoiceClient::inTuningMode() { bool result = false; switch(getState()) { case stateMicTuningRunning: result = true; break; default: break; } return result; } void LLVoiceClient::tuningRenderStartSendMessage(const std::string& name, bool loop) { mTuningAudioFile = name; std::ostringstream stream; stream << "" << "" << mTuningAudioFile << "" << "" << (loop?"1":"0") << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::tuningRenderStopSendMessage() { std::ostringstream stream; stream << "" << "" << mTuningAudioFile << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::tuningCaptureStartSendMessage(int duration) { LL_DEBUGS("Voice") << "sending CaptureAudioStart" << LL_ENDL; std::ostringstream stream; stream << "" << "" << duration << "" << "\n\n\n"; writeString(stream.str()); } void LLVoiceClient::tuningCaptureStopSendMessage() { LL_DEBUGS("Voice") << "sending CaptureAudioStop" << LL_ENDL; std::ostringstream stream; stream << "" << "\n\n\n"; writeString(stream.str()); mTuningEnergy = 0.0f; } void LLVoiceClient::tuningSetMicVolume(float volume) { int scaled_volume = scale_mic_volume(volume); if(scaled_volume != mTuningMicVolume) { mTuningMicVolume = scaled_volume; mTuningMicVolumeDirty = true; } } void LLVoiceClient::tuningSetSpeakerVolume(float volume) { int scaled_volume = scale_speaker_volume(volume); if(scaled_volume != mTuningSpeakerVolume) { mTuningSpeakerVolume = scaled_volume; mTuningSpeakerVolumeDirty = true; } } float LLVoiceClient::tuningGetEnergy(void) { return mTuningEnergy; } bool LLVoiceClient::deviceSettingsAvailable() { bool result = true; if(!mConnected) result = false; if(mRenderDevices.empty()) result = false; return result; } void LLVoiceClient::refreshDeviceLists(bool clearCurrentList) { if(clearCurrentList) { clearCaptureDevices(); clearRenderDevices(); } getCaptureDevicesSendMessage(); getRenderDevicesSendMessage(); } void LLVoiceClient::daemonDied() { // The daemon died, so the connection is gone. Reset everything and start over. LL_WARNS("Voice") << "Connection to vivox daemon lost. Resetting state."<< LL_ENDL; // Try to relaunch the daemon setState(stateDisableCleanup); } void LLVoiceClient::giveUp() { // All has failed. Clean up and stop trying. closeSocket(); deleteAllSessions(); deleteAllBuddies(); setState(stateJail); } static void oldSDKTransform (LLVector3 &left, LLVector3 &up, LLVector3 &at, LLVector3d &pos, LLVector3 &vel) { F32 nat[3], nup[3], nl[3], nvel[3]; // the new at, up, left vectors and the new position and velocity F64 npos[3]; // The original XML command was sent like this: /* << "" << "" << pos[VX] << "" << "" << pos[VZ] << "" << "" << pos[VY] << "" << "" << "" << "" << mAvatarVelocity[VX] << "" << "" << mAvatarVelocity[VZ] << "" << "" << mAvatarVelocity[VY] << "" << "" << "" << "" << l.mV[VX] << "" << "" << u.mV[VX] << "" << "" << a.mV[VX] << "" << "" << "" << "" << l.mV[VZ] << "" << "" << u.mV[VY] << "" << "" << a.mV[VZ] << "" << "" << "" << "" << l.mV [VY] << "" << "" << u.mV [VZ] << "" << "" << a.mV [VY] << "" << ""; */ #if 1 // This was the original transform done when building the XML command nat[0] = left.mV[VX]; nat[1] = up.mV[VX]; nat[2] = at.mV[VX]; nup[0] = left.mV[VZ]; nup[1] = up.mV[VY]; nup[2] = at.mV[VZ]; nl[0] = left.mV[VY]; nl[1] = up.mV[VZ]; nl[2] = at.mV[VY]; npos[0] = pos.mdV[VX]; npos[1] = pos.mdV[VZ]; npos[2] = pos.mdV[VY]; nvel[0] = vel.mV[VX]; nvel[1] = vel.mV[VZ]; nvel[2] = vel.mV[VY]; for(int i=0;i<3;++i) { at.mV[i] = nat[i]; up.mV[i] = nup[i]; left.mV[i] = nl[i]; pos.mdV[i] = npos[i]; } // This was the original transform done in the SDK nat[0] = at.mV[2]; nat[1] = 0; // y component of at vector is always 0, this was up[2] nat[2] = -1 * left.mV[2]; // We override whatever the application gives us nup[0] = 0; // x component of up vector is always 0 nup[1] = 1; // y component of up vector is always 1 nup[2] = 0; // z component of up vector is always 0 nl[0] = at.mV[0]; nl[1] = 0; // y component of left vector is always zero, this was up[0] nl[2] = -1 * left.mV[0]; npos[2] = pos.mdV[2] * -1.0; npos[1] = pos.mdV[1]; npos[0] = pos.mdV[0]; for(int i=0;i<3;++i) { at.mV[i] = nat[i]; up.mV[i] = nup[i]; left.mV[i] = nl[i]; pos.mdV[i] = npos[i]; } #else // This is the compose of the two transforms (at least, that's what I'm trying for) nat[0] = at.mV[VX]; nat[1] = 0; // y component of at vector is always 0, this was up[2] nat[2] = -1 * up.mV[VZ]; // We override whatever the application gives us nup[0] = 0; // x component of up vector is always 0 nup[1] = 1; // y component of up vector is always 1 nup[2] = 0; // z component of up vector is always 0 nl[0] = left.mV[VX]; nl[1] = 0; // y component of left vector is always zero, this was up[0] nl[2] = -1 * left.mV[VY]; npos[0] = pos.mdV[VX]; npos[1] = pos.mdV[VZ]; npos[2] = pos.mdV[VY] * -1.0; nvel[0] = vel.mV[VX]; nvel[1] = vel.mV[VZ]; nvel[2] = vel.mV[VY]; for(int i=0;i<3;++i) { at.mV[i] = nat[i]; up.mV[i] = nup[i]; left.mV[i] = nl[i]; pos.mdV[i] = npos[i]; } #endif } void LLVoiceClient::sendPositionalUpdate(void) { std::ostringstream stream; if(mSpatialCoordsDirty) { LLVector3 l, u, a, vel; LLVector3d pos; mSpatialCoordsDirty = false; // Always send both speaker and listener positions together. stream << "" << "" << getAudioSessionHandle() << ""; stream << ""; // LL_DEBUGS("Voice") << "Sending speaker position " << mAvatarPosition << LL_ENDL; l = mAvatarRot.getLeftRow(); u = mAvatarRot.getUpRow(); a = mAvatarRot.getFwdRow(); pos = mAvatarPosition; vel = mAvatarVelocity; // SLIM SDK: the old SDK was doing a transform on the passed coordinates that the new one doesn't do anymore. // The old transform is replicated by this function. oldSDKTransform(l, u, a, pos, vel); stream << "" << "" << pos.mdV[VX] << "" << "" << pos.mdV[VY] << "" << "" << pos.mdV[VZ] << "" << "" << "" << "" << vel.mV[VX] << "" << "" << vel.mV[VY] << "" << "" << vel.mV[VZ] << "" << "" << "" << "" << a.mV[VX] << "" << "" << a.mV[VY] << "" << "" << a.mV[VZ] << "" << "" << "" << "" << u.mV[VX] << "" << "" << u.mV[VY] << "" << "" << u.mV[VZ] << "" << "" << "" << "" << l.mV [VX] << "" << "" << l.mV [VY] << "" << "" << l.mV [VZ] << "" << ""; stream << ""; stream << ""; LLVector3d earPosition; LLVector3 earVelocity; LLMatrix3 earRot; switch(mEarLocation) { case earLocCamera: default: earPosition = mCameraPosition; earVelocity = mCameraVelocity; earRot = mCameraRot; break; case earLocAvatar: earPosition = mAvatarPosition; earVelocity = mAvatarVelocity; earRot = mAvatarRot; break; case earLocMixed: earPosition = mAvatarPosition; earVelocity = mAvatarVelocity; earRot = mCameraRot; break; } l = earRot.getLeftRow(); u = earRot.getUpRow(); a = earRot.getFwdRow(); pos = earPosition; vel = earVelocity; // LL_DEBUGS("Voice") << "Sending listener position " << earPosition << LL_ENDL; oldSDKTransform(l, u, a, pos, vel); stream << "" << "" << pos.mdV[VX] << "" << "" << pos.mdV[VY] << "" << "" << pos.mdV[VZ] << "" << "" << "" << "" << vel.mV[VX] << "" << "" << vel.mV[VY] << "" << "" << vel.mV[VZ] << "" << "" << "" << "" << a.mV[VX] << "" << "" << a.mV[VY] << "" << "" << a.mV[VZ] << "" << "" << "" << "" << u.mV[VX] << "" << "" << u.mV[VY] << "" << "" << u.mV[VZ] << "" << "" << "" << "" << l.mV [VX] << "" << "" << l.mV [VY] << "" << "" << l.mV [VZ] << "" << ""; stream << ""; stream << "\n\n\n"; } if(mAudioSession && mAudioSession->mVolumeDirty) { participantMap::iterator iter = mAudioSession->mParticipantsByURI.begin(); mAudioSession->mVolumeDirty = false; for(; iter != mAudioSession->mParticipantsByURI.end(); iter++) { participantState *p = iter->second; if(p->mVolumeDirty) { // Can't set volume/mute for yourself if(!p->mIsSelf) { int volume = 56; // nominal default value bool mute = p->mOnMuteList; if(p->mUserVolume != -1) { // scale from user volume in the range 0-400 (with 100 as "normal") to vivox volume in the range 0-100 (with 56 as "normal") if(p->mUserVolume < 100) volume = (p->mUserVolume * 56) / 100; else volume = (((p->mUserVolume - 100) * (100 - 56)) / 300) + 56; } else if(p->mVolume != -1) { // Use the previously reported internal volume (comes in with a ParticipantUpdatedEvent) volume = p->mVolume; } if(mute) { // SetParticipantMuteForMe doesn't work in p2p sessions. // If we want the user to be muted, set their volume to 0 as well. // This isn't perfect, but it will at least reduce their volume to a minimum. volume = 0; } if(volume == 0) mute = true; LL_DEBUGS("Voice") << "Setting volume/mute for avatar " << p->mAvatarID << " to " << volume << (mute?"/true":"/false") << LL_ENDL; // SLIM SDK: Send both volume and mute commands. // Send a "volume for me" command for the user. stream << "" << "" << getAudioSessionHandle() << "" << "" << p->mURI << "" << "" << volume << "" << "\n\n\n"; // Send a "mute for me" command for the user stream << "" << "" << getAudioSessionHandle() << "" << "" << p->mURI << "" << "" << (mute?"1":"0") << "" << "\n\n\n"; } p->mVolumeDirty = false; } } } buildLocalAudioUpdates(stream); if(!stream.str().empty()) { writeString(stream.str()); } // Friends list updates can be huge, especially on the first voice login of an account with lots of friends. // Batching them all together can choke SLVoice, so send them in separate writes. sendFriendsListUpdates(); } void LLVoiceClient::buildSetCaptureDevice(std::ostringstream &stream) { if(mCaptureDeviceDirty) { LL_DEBUGS("Voice") << "Setting input device = \"" << mCaptureDevice << "\"" << LL_ENDL; stream << "" << "" << mCaptureDevice << "" << "" << "\n\n\n"; mCaptureDeviceDirty = false; } } void LLVoiceClient::buildSetRenderDevice(std::ostringstream &stream) { if(mRenderDeviceDirty) { LL_DEBUGS("Voice") << "Setting output device = \"" << mRenderDevice << "\"" << LL_ENDL; stream << "" << "" << mRenderDevice << "" << "" << "\n\n\n"; mRenderDeviceDirty = false; } } void LLVoiceClient::buildLocalAudioUpdates(std::ostringstream &stream) { buildSetCaptureDevice(stream); buildSetRenderDevice(stream); if(mPTTDirty) { mPTTDirty = false; // Send a local mute command. // NOTE that the state of "PTT" is the inverse of "local mute". // (i.e. when PTT is true, we send a mute command with "false", and vice versa) LL_DEBUGS("Voice") << "Sending MuteLocalMic command with parameter " << (mPTT?"false":"true") << LL_ENDL; stream << "" << "" << mConnectorHandle << "" << "" << (mPTT?"false":"true") << "" << "\n\n\n"; } if(mSpeakerMuteDirty) { const char *muteval = ((mSpeakerVolume == 0)?"true":"false"); mSpeakerMuteDirty = false; LL_INFOS("Voice") << "Setting speaker mute to " << muteval << LL_ENDL; stream << "" << "" << mConnectorHandle << "" << "" << muteval << "" << "\n\n\n"; } if(mSpeakerVolumeDirty) { mSpeakerVolumeDirty = false; LL_INFOS("Voice") << "Setting speaker volume to " << mSpeakerVolume << LL_ENDL; stream << "" << "" << mConnectorHandle << "" << "" << mSpeakerVolume << "" << "\n\n\n"; } if(mMicVolumeDirty) { mMicVolumeDirty = false; LL_INFOS("Voice") << "Setting mic volume to " << mMicVolume << LL_ENDL; stream << "" << "" << mConnectorHandle << "" << "" << mMicVolume << "" << "\n\n\n"; } } void LLVoiceClient::checkFriend(const LLUUID& id) { std::string name; buddyListEntry *buddy = findBuddy(id); // Make sure we don't add a name before it's been looked up. if(gCacheName->getFullName(id, name)) { const LLRelationship* relationInfo = LLAvatarTracker::instance().getBuddyInfo(id); bool canSeeMeOnline = false; if(relationInfo && relationInfo->isRightGrantedTo(LLRelationship::GRANT_ONLINE_STATUS)) canSeeMeOnline = true; // When we get here, mNeedsSend is true and mInSLFriends is false. Change them as necessary. if(buddy) { // This buddy is already in both lists. if(name != buddy->mDisplayName) { // The buddy is in the list with the wrong name. Update it with the correct name. LL_WARNS("Voice") << "Buddy " << id << " has wrong name (\"" << buddy->mDisplayName << "\" should be \"" << name << "\"), updating."<< LL_ENDL; buddy->mDisplayName = name; buddy->mNeedsNameUpdate = true; // This will cause the buddy to be resent. } } else { // This buddy was not in the vivox list, needs to be added. buddy = addBuddy(sipURIFromID(id), name); buddy->mUUID = id; } // In all the above cases, the buddy is in the SL friends list (which is how we got here). buddy->mInSLFriends = true; buddy->mCanSeeMeOnline = canSeeMeOnline; buddy->mNameResolved = true; } else { // This name hasn't been looked up yet. Don't do anything with this buddy list entry until it has. if(buddy) { buddy->mNameResolved = false; } // Initiate a lookup. // The "lookup completed" callback will ensure that the friends list is rechecked after it completes. lookupName(id); } } void LLVoiceClient::clearAllLists() { // FOR TESTING ONLY // This will send the necessary commands to delete ALL buddies, autoaccept rules, and block rules SLVoice tells us about. buddyListMap::iterator buddy_it; for(buddy_it = mBuddyListMap.begin(); buddy_it != mBuddyListMap.end();) { buddyListEntry *buddy = buddy_it->second; buddy_it++; std::ostringstream stream; if(buddy->mInVivoxBuddies) { // delete this entry from the vivox buddy list buddy->mInVivoxBuddies = false; LL_DEBUGS("Voice") << "delete " << buddy->mURI << " (" << buddy->mDisplayName << ")" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } if(buddy->mHasBlockListEntry) { // Delete the associated block list entry (so the block list doesn't fill up with junk) buddy->mHasBlockListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } if(buddy->mHasAutoAcceptListEntry) { // Delete the associated auto-accept list entry (so the auto-accept list doesn't fill up with junk) buddy->mHasAutoAcceptListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } writeString(stream.str()); } } void LLVoiceClient::sendFriendsListUpdates() { if(mBuddyListMapPopulated && mBlockRulesListReceived && mAutoAcceptRulesListReceived && mFriendsListDirty) { mFriendsListDirty = false; if(0) { // FOR TESTING ONLY -- clear all buddy list, block list, and auto-accept list entries. clearAllLists(); return; } LL_INFOS("Voice") << "Checking vivox buddy list against friends list..." << LL_ENDL; buddyListMap::iterator buddy_it; for(buddy_it = mBuddyListMap.begin(); buddy_it != mBuddyListMap.end(); buddy_it++) { // reset the temp flags in the local buddy list buddy_it->second->mInSLFriends = false; } // correlate with the friends list { LLCollectAllBuddies collect; LLAvatarTracker::instance().applyFunctor(collect); LLCollectAllBuddies::buddy_map_t::const_iterator it = collect.mOnline.begin(); LLCollectAllBuddies::buddy_map_t::const_iterator end = collect.mOnline.end(); for ( ; it != end; ++it) { checkFriend(it->second); } it = collect.mOffline.begin(); end = collect.mOffline.end(); for ( ; it != end; ++it) { checkFriend(it->second); } } LL_INFOS("Voice") << "Sending friend list updates..." << LL_ENDL; for(buddy_it = mBuddyListMap.begin(); buddy_it != mBuddyListMap.end();) { buddyListEntry *buddy = buddy_it->second; buddy_it++; // Ignore entries that aren't resolved yet. if(buddy->mNameResolved) { std::ostringstream stream; if(buddy->mInSLFriends && (!buddy->mInVivoxBuddies || buddy->mNeedsNameUpdate)) { if(mNumberOfAliases > 0) { // Add (or update) this entry in the vivox buddy list buddy->mInVivoxBuddies = true; buddy->mNeedsNameUpdate = false; LL_DEBUGS("Voice") << "add/update " << buddy->mURI << " (" << buddy->mDisplayName << ")" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "" << buddy->mDisplayName << "" << "" // Without this, SLVoice doesn't seem to parse the command. << "0" << "\n\n\n"; } } else if(!buddy->mInSLFriends) { // This entry no longer exists in your SL friends list. Remove all traces of it from the Vivox buddy list. if(buddy->mInVivoxBuddies) { // delete this entry from the vivox buddy list buddy->mInVivoxBuddies = false; LL_DEBUGS("Voice") << "delete " << buddy->mURI << " (" << buddy->mDisplayName << ")" << LL_ENDL; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } if(buddy->mHasBlockListEntry) { // Delete the associated block list entry, if any buddy->mHasBlockListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } if(buddy->mHasAutoAcceptListEntry) { // Delete the associated auto-accept list entry, if any buddy->mHasAutoAcceptListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; } } if(buddy->mInSLFriends) { if(buddy->mCanSeeMeOnline) { // Buddy should not be blocked. // If this buddy doesn't already have either a block or autoaccept list entry, we'll update their status when we receive a SubscriptionEvent. // If the buddy has a block list entry, delete it. if(buddy->mHasBlockListEntry) { buddy->mHasBlockListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; // If we just deleted a block list entry, add an auto-accept entry. if(!buddy->mHasAutoAcceptListEntry) { buddy->mHasAutoAcceptListEntry = true; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "0" << "\n\n\n"; } } } else { // Buddy should be blocked. // If this buddy doesn't already have either a block or autoaccept list entry, we'll update their status when we receive a SubscriptionEvent. // If this buddy has an autoaccept entry, delete it if(buddy->mHasAutoAcceptListEntry) { buddy->mHasAutoAcceptListEntry = false; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "\n\n\n"; // If we just deleted an auto-accept entry, add a block list entry. if(!buddy->mHasBlockListEntry) { buddy->mHasBlockListEntry = true; stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "1" << "\n\n\n"; } } } if(!buddy->mInSLFriends && !buddy->mInVivoxBuddies) { // Delete this entry from the local buddy list. This should NOT invalidate the iterator, // since it has already been incremented to the next entry. deleteBuddy(buddy->mURI); } } writeString(stream.str()); } } } } ///////////////////////////// // Response/Event handlers void LLVoiceClient::connectorCreateResponse(int statusCode, std::string &statusString, std::string &connectorHandle, std::string &versionID) { if(statusCode != 0) { LL_WARNS("Voice") << "Connector.Create response failure: " << statusString << LL_ENDL; setState(stateConnectorFailed); } else { // Connector created, move forward. LL_INFOS("Voice") << "Connector.Create succeeded, Vivox SDK version is " << versionID << LL_ENDL; mConnectorHandle = connectorHandle; if(getState() == stateConnectorStarting) { setState(stateConnectorStarted); } } } void LLVoiceClient::loginResponse(int statusCode, std::string &statusString, std::string &accountHandle, int numberOfAliases) { LL_DEBUGS("Voice") << "Account.Login response (" << statusCode << "): " << statusString << LL_ENDL; // Status code of 20200 means "bad password". We may want to special-case that at some point. if ( statusCode == 401 ) { // Login failure which is probably caused by the delay after a user's password being updated. LL_INFOS("Voice") << "Account.Login response failure (" << statusCode << "): " << statusString << LL_ENDL; setState(stateLoginRetry); } else if(statusCode != 0) { LL_WARNS("Voice") << "Account.Login response failure (" << statusCode << "): " << statusString << LL_ENDL; setState(stateLoginFailed); } else { // Login succeeded, move forward. mAccountHandle = accountHandle; mNumberOfAliases = numberOfAliases; // This needs to wait until the AccountLoginStateChangeEvent is received. // if(getState() == stateLoggingIn) // { // setState(stateLoggedIn); // } } } void LLVoiceClient::sessionCreateResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle) { sessionState *session = findSessionBeingCreatedByURI(requestId); if(session) { session->mCreateInProgress = false; } if(statusCode != 0) { LL_WARNS("Voice") << "Session.Create response failure (" << statusCode << "): " << statusString << LL_ENDL; if(session) { session->mErrorStatusCode = statusCode; session->mErrorStatusString = statusString; if(session == mAudioSession) { setState(stateJoinSessionFailed); } else { reapSession(session); } } } else { LL_INFOS("Voice") << "Session.Create response received (success), session handle is " << sessionHandle << LL_ENDL; if(session) { setSessionHandle(session, sessionHandle); } } } void LLVoiceClient::sessionGroupAddSessionResponse(std::string &requestId, int statusCode, std::string &statusString, std::string &sessionHandle) { sessionState *session = findSessionBeingCreatedByURI(requestId); if(session) { session->mCreateInProgress = false; } if(statusCode != 0) { LL_WARNS("Voice") << "SessionGroup.AddSession response failure (" << statusCode << "): " << statusString << LL_ENDL; if(session) { session->mErrorStatusCode = statusCode; session->mErrorStatusString = statusString; if(session == mAudioSession) { setState(stateJoinSessionFailed); } else { reapSession(session); } } } else { LL_DEBUGS("Voice") << "SessionGroup.AddSession response received (success), session handle is " << sessionHandle << LL_ENDL; if(session) { setSessionHandle(session, sessionHandle); } } } void LLVoiceClient::sessionConnectResponse(std::string &requestId, int statusCode, std::string &statusString) { sessionState *session = findSession(requestId); if(statusCode != 0) { LL_WARNS("Voice") << "Session.Connect response failure (" << statusCode << "): " << statusString << LL_ENDL; if(session) { session->mMediaConnectInProgress = false; session->mErrorStatusCode = statusCode; session->mErrorStatusString = statusString; if(session == mAudioSession) setState(stateJoinSessionFailed); } } else { LL_DEBUGS("Voice") << "Session.Connect response received (success)" << LL_ENDL; } } void LLVoiceClient::logoutResponse(int statusCode, std::string &statusString) { if(statusCode != 0) { LL_WARNS("Voice") << "Account.Logout response failure: " << statusString << LL_ENDL; // Should this ever fail? do we care if it does? } } void LLVoiceClient::connectorShutdownResponse(int statusCode, std::string &statusString) { if(statusCode != 0) { LL_WARNS("Voice") << "Connector.InitiateShutdown response failure: " << statusString << LL_ENDL; // Should this ever fail? do we care if it does? } mConnected = false; if(getState() == stateConnectorStopping) { setState(stateConnectorStopped); } } void LLVoiceClient::sessionAddedEvent( std::string &uriString, std::string &alias, std::string &sessionHandle, std::string &sessionGroupHandle, bool isChannel, bool incoming, std::string &nameString, std::string &applicationString) { sessionState *session = NULL; LL_INFOS("Voice") << "session " << uriString << ", alias " << alias << ", name " << nameString << " handle " << sessionHandle << LL_ENDL; session = addSession(uriString, sessionHandle); if(session) { session->mGroupHandle = sessionGroupHandle; session->mIsChannel = isChannel; session->mIncoming = incoming; session->mAlias = alias; // Generate a caller UUID -- don't need to do this for channels if(!session->mIsChannel) { if(IDFromName(session->mSIPURI, session->mCallerID)) { // Normal URI(base64-encoded UUID) } else if(!session->mAlias.empty() && IDFromName(session->mAlias, session->mCallerID)) { // Wrong URI, but an alias is available. Stash the incoming URI as an alternate session->mAlternateSIPURI = session->mSIPURI; // and generate a proper URI from the ID. setSessionURI(session, sipURIFromID(session->mCallerID)); } else { LL_INFOS("Voice") << "Could not generate caller id from uri, using hash of uri " << session->mSIPURI << LL_ENDL; setUUIDFromStringHash(session->mCallerID, session->mSIPURI); session->mSynthesizedCallerID = true; // Can't look up the name in this case -- we have to extract it from the URI. std::string namePortion = nameFromsipURI(session->mSIPURI); if(namePortion.empty()) { // Didn't seem to be a SIP URI, just use the whole provided name. namePortion = nameString; } // Some incoming names may be separated with an underscore instead of a space. Fix this. LLStringUtil::replaceChar(namePortion, '_', ' '); // Act like we just finished resolving the name (this stores it in all the right places) avatarNameResolved(session->mCallerID, namePortion); } LL_INFOS("Voice") << "caller ID: " << session->mCallerID << LL_ENDL; if(!session->mSynthesizedCallerID) { // If we got here, we don't have a proper name. Initiate a lookup. lookupName(session->mCallerID); } } } } void LLVoiceClient::sessionGroupAddedEvent(std::string &sessionGroupHandle) { LL_DEBUGS("Voice") << "handle " << sessionGroupHandle << LL_ENDL; #if USE_SESSION_GROUPS if(mMainSessionGroupHandle.empty()) { // This is the first (i.e. "main") session group. Save its handle. mMainSessionGroupHandle = sessionGroupHandle; } else { LL_DEBUGS("Voice") << "Already had a session group handle " << mMainSessionGroupHandle << LL_ENDL; } #endif } void LLVoiceClient::joinedAudioSession(sessionState *session) { if(mAudioSession != session) { sessionState *oldSession = mAudioSession; mAudioSession = session; mAudioSessionChanged = true; // The old session may now need to be deleted. reapSession(oldSession); } // This is the session we're joining. if(getState() == stateJoiningSession) { setState(stateSessionJoined); // SLIM SDK: we don't always receive a participant state change for ourselves when joining a channel now. // Add the current user as a participant here. participantState *participant = session->addParticipant(sipURIFromName(mAccountName)); if(participant) { participant->mIsSelf = true; lookupName(participant->mAvatarID); LL_INFOS("Voice") << "added self as participant \"" << participant->mAccountName << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; } if(!session->mIsChannel) { // this is a p2p session. Make sure the other end is added as a participant. participantState *participant = session->addParticipant(session->mSIPURI); if(participant) { if(participant->mAvatarIDValid) { lookupName(participant->mAvatarID); } else if(!session->mName.empty()) { participant->mDisplayName = session->mName; avatarNameResolved(participant->mAvatarID, session->mName); } // TODO: Question: Do we need to set up mAvatarID/mAvatarIDValid here? LL_INFOS("Voice") << "added caller as participant \"" << participant->mAccountName << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; } } } } void LLVoiceClient::sessionRemovedEvent( std::string &sessionHandle, std::string &sessionGroupHandle) { LL_INFOS("Voice") << "handle " << sessionHandle << LL_ENDL; sessionState *session = findSession(sessionHandle); if(session) { leftAudioSession(session); // This message invalidates the session's handle. Set it to empty. setSessionHandle(session); // This also means that the session's session group is now empty. // Terminate the session group so it doesn't leak. sessionGroupTerminateSendMessage(session); // Reset the media state (we now have no info) session->mMediaStreamState = streamStateUnknown; session->mTextStreamState = streamStateUnknown; // Conditionally delete the session reapSession(session); } else { LL_WARNS("Voice") << "unknown session " << sessionHandle << " removed" << LL_ENDL; } } void LLVoiceClient::reapSession(sessionState *session) { if(session) { if(!session->mHandle.empty()) { LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (non-null session handle)" << LL_ENDL; } else if(session->mCreateInProgress) { LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (create in progress)" << LL_ENDL; } else if(session->mMediaConnectInProgress) { LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (connect in progress)" << LL_ENDL; } else if(session == mAudioSession) { LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (it's the current session)" << LL_ENDL; } else if(session == mNextAudioSession) { LL_DEBUGS("Voice") << "NOT deleting session " << session->mSIPURI << " (it's the next session)" << LL_ENDL; } else { // TODO: Question: Should we check for queued text messages here? // We don't have a reason to keep tracking this session, so just delete it. LL_DEBUGS("Voice") << "deleting session " << session->mSIPURI << LL_ENDL; deleteSession(session); session = NULL; } } else { // LL_DEBUGS("Voice") << "session is NULL" << LL_ENDL; } } // Returns true if the session seems to indicate we've moved to a region on a different voice server bool LLVoiceClient::sessionNeedsRelog(sessionState *session) { bool result = false; if(session != NULL) { // Only make this check for spatial channels (so it won't happen for group or p2p calls) if(session->mIsSpatial) { std::string::size_type atsign; atsign = session->mSIPURI.find("@"); if(atsign != std::string::npos) { std::string urihost = session->mSIPURI.substr(atsign + 1); if(stricmp(urihost.c_str(), mVoiceSIPURIHostName.c_str())) { // The hostname in this URI is different from what we expect. This probably means we need to relog. // We could make a ProvisionVoiceAccountRequest and compare the result with the current values of // mVoiceSIPURIHostName and mVoiceAccountServerURI to be really sure, but this is a pretty good indicator. result = true; } } } } return result; } void LLVoiceClient::leftAudioSession( sessionState *session) { if(mAudioSession == session) { switch(getState()) { case stateJoiningSession: case stateSessionJoined: case stateRunning: case stateLeavingSession: case stateJoinSessionFailed: case stateJoinSessionFailedWaiting: // normal transition LL_DEBUGS("Voice") << "left session " << session->mHandle << " in state " << state2string(getState()) << LL_ENDL; setState(stateSessionTerminated); break; case stateSessionTerminated: // this will happen sometimes -- there are cases where we send the terminate and then go straight to this state. LL_WARNS("Voice") << "left session " << session->mHandle << " in state " << state2string(getState()) << LL_ENDL; break; default: LL_WARNS("Voice") << "unexpected SessionStateChangeEvent (left session) in state " << state2string(getState()) << LL_ENDL; setState(stateSessionTerminated); break; } } } void LLVoiceClient::accountLoginStateChangeEvent( std::string &accountHandle, int statusCode, std::string &statusString, int state) { LL_DEBUGS("Voice") << "state is " << state << LL_ENDL; /* According to Mike S., status codes for this event are: login_state_logged_out=0, login_state_logged_in = 1, login_state_logging_in = 2, login_state_logging_out = 3, login_state_resetting = 4, login_state_error=100 */ switch(state) { case 1: if(getState() == stateLoggingIn) { setState(stateLoggedIn); } break; case 3: // The user is in the process of logging out. setState(stateLoggingOut); break; case 0: // The user has been logged out. setState(stateLoggedOut); break; default: //Used to be a commented out warning LL_DEBUGS("Voice") << "unknown state: " << state << LL_ENDL; break; } } void LLVoiceClient::mediaStreamUpdatedEvent( std::string &sessionHandle, std::string &sessionGroupHandle, int statusCode, std::string &statusString, int state, bool incoming) { sessionState *session = findSession(sessionHandle); LL_DEBUGS("Voice") << "session " << sessionHandle << ", status code " << statusCode << ", string \"" << statusString << "\"" << LL_ENDL; if(session) { // We know about this session // Save the state for later use session->mMediaStreamState = state; switch(statusCode) { case 0: case 200: // generic success // Don't change the saved error code (it may have been set elsewhere) break; default: // save the status code for later session->mErrorStatusCode = statusCode; break; } switch(state) { case streamStateIdle: // Standard "left audio session" session->mVoiceEnabled = false; session->mMediaConnectInProgress = false; leftAudioSession(session); break; case streamStateConnected: session->mVoiceEnabled = true; session->mMediaConnectInProgress = false; joinedAudioSession(session); break; case streamStateRinging: if(incoming) { // Send the voice chat invite to the GUI layer // TODO: Question: Should we correlate with the mute list here? session->mIMSessionID = LLIMMgr::computeSessionID(IM_SESSION_P2P_INVITE, session->mCallerID); session->mVoiceInvitePending = true; if(session->mName.empty()) { lookupName(session->mCallerID); } else { // Act like we just finished resolving the name avatarNameResolved(session->mCallerID, session->mName); } } break; default: LL_WARNS("Voice") << "unknown state " << state << LL_ENDL; break; } } else { LL_WARNS("Voice") << "session " << sessionHandle << "not found"<< LL_ENDL; } } void LLVoiceClient::textStreamUpdatedEvent( std::string &sessionHandle, std::string &sessionGroupHandle, bool enabled, int state, bool incoming) { sessionState *session = findSession(sessionHandle); if(session) { // Save the state for later use session->mTextStreamState = state; // We know about this session switch(state) { case 0: // We see this when the text stream closes LL_DEBUGS("Voice") << "stream closed" << LL_ENDL; break; case 1: // We see this on an incoming call from the Connector // Try to send any text messages queued for this session. sendQueuedTextMessages(session); // Send the text chat invite to the GUI layer // TODO: Question: Should we correlate with the mute list here? session->mTextInvitePending = true; if(session->mName.empty()) { lookupName(session->mCallerID); } else { // Act like we just finished resolving the name avatarNameResolved(session->mCallerID, session->mName); } break; default: LL_WARNS("Voice") << "unknown state " << state << LL_ENDL; break; } } } void LLVoiceClient::participantAddedEvent( std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, std::string &nameString, std::string &displayNameString, int participantType) { sessionState *session = findSession(sessionHandle); if(session) { participantState *participant = session->addParticipant(uriString); if(participant) { participant->mAccountName = nameString; LL_DEBUGS("Voice") << "added participant \"" << participant->mAccountName << "\" (" << participant->mAvatarID << ")"<< LL_ENDL; if(participant->mAvatarIDValid) { // Initiate a lookup lookupName(participant->mAvatarID); } else { // If we don't have a valid avatar UUID, we need to fill in the display name to make the active speakers floater work. std::string namePortion = nameFromsipURI(uriString); if(namePortion.empty()) { // Problem with the SIP URI, fall back to the display name namePortion = displayNameString; } if(namePortion.empty()) { // Problems with both of the above, fall back to the account name namePortion = nameString; } // Set the display name (which is a hint to the active speakers window not to do its own lookup) participant->mDisplayName = namePortion; avatarNameResolved(participant->mAvatarID, namePortion); } } } } void LLVoiceClient::participantRemovedEvent( std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, std::string &nameString) { sessionState *session = findSession(sessionHandle); if(session) { participantState *participant = session->findParticipant(uriString); if(participant) { session->removeParticipant(participant); } else { LL_DEBUGS("Voice") << "unknown participant " << uriString << LL_ENDL; } } else { LL_DEBUGS("Voice") << "unknown session " << sessionHandle << LL_ENDL; } } void LLVoiceClient::participantUpdatedEvent( std::string &sessionHandle, std::string &sessionGroupHandle, std::string &uriString, std::string &alias, bool isModeratorMuted, bool isSpeaking, int volume, F32 energy) { sessionState *session = findSession(sessionHandle); if(session) { participantState *participant = session->findParticipant(uriString); if(participant) { participant->mIsSpeaking = isSpeaking; participant->mIsModeratorMuted = isModeratorMuted; // SLIM SDK: convert range: ensure that energy is set to zero if is_speaking is false if (isSpeaking) { participant->mSpeakingTimeout.reset(); participant->mPower = energy; } else { participant->mPower = 0.0f; } participant->mVolume = volume; } else { LL_WARNS("Voice") << "unknown participant: " << uriString << LL_ENDL; } } else { LL_INFOS("Voice") << "unknown session " << sessionHandle << LL_ENDL; } } void LLVoiceClient::buddyPresenceEvent( std::string &uriString, std::string &alias, std::string &statusString, std::string &applicationString) { buddyListEntry *buddy = findBuddy(uriString); if(buddy) { LL_DEBUGS("Voice") << "Presence event for " << buddy->mDisplayName << " status \"" << statusString << "\", application \"" << applicationString << "\""<< LL_ENDL; LL_DEBUGS("Voice") << "before: mOnlineSL = " << (buddy->mOnlineSL?"true":"false") << ", mOnlineSLim = " << (buddy->mOnlineSLim?"true":"false") << LL_ENDL; if(applicationString.empty()) { // This presence event is from a client that doesn't set up the Application string. Do things the old-skool way. // NOTE: this will be needed to support people who aren't on the 3010-class SDK yet. if ( stricmp("Unknown", statusString.c_str())== 0) { // User went offline with a non-SLim-enabled viewer. buddy->mOnlineSL = false; } else if ( stricmp("Online", statusString.c_str())== 0) { // User came online with a non-SLim-enabled viewer. buddy->mOnlineSL = true; } else { // If the user is online through SLim, their status will be "Online-slc", "Away", or something else. // NOTE: we should never see this unless someone is running an OLD version of SLim -- the versions that should be in use now all set the application string. buddy->mOnlineSLim = true; } } else if(applicationString.find("SecondLifeViewer") != std::string::npos) { // This presence event is from a viewer that sets the application string if ( stricmp("Unknown", statusString.c_str())== 0) { // Viewer says they're offline buddy->mOnlineSL = false; } else { // Viewer says they're online buddy->mOnlineSL = true; } } else { // This presence event is from something which is NOT the SL viewer (assume it's SLim). if ( stricmp("Unknown", statusString.c_str())== 0) { // SLim says they're offline buddy->mOnlineSLim = false; } else { // SLim says they're online buddy->mOnlineSLim = true; } } LL_DEBUGS("Voice") << "after: mOnlineSL = " << (buddy->mOnlineSL?"true":"false") << ", mOnlineSLim = " << (buddy->mOnlineSLim?"true":"false") << LL_ENDL; // HACK -- increment the internal change serial number in the LLRelationship (without changing the actual status), so the UI notices the change. LLAvatarTracker::instance().setBuddyOnline(buddy->mUUID,LLAvatarTracker::instance().isBuddyOnline(buddy->mUUID)); notifyFriendObservers(); } else { LL_DEBUGS("Voice") << "Presence for unknown buddy " << uriString << LL_ENDL; } } void LLVoiceClient::messageEvent( std::string &sessionHandle, std::string &uriString, std::string &alias, std::string &messageHeader, std::string &messageBody, std::string &applicationString) { LL_DEBUGS("Voice") << "Message event, session " << sessionHandle << " from " << uriString << LL_ENDL; // LL_DEBUGS("Voice") << " header " << messageHeader << ", body: \n" << messageBody << LL_ENDL; if(messageHeader.find("text/html") != std::string::npos) { std::string rawMessage; { const std::string startMarker = ", try looking for a instead. start = messageBody.find(startSpan); start = messageBody.find(startMarker2, start); end = messageBody.find(endSpan); if(start != std::string::npos) { start += startMarker2.size(); if(end != std::string::npos) end -= start; rawMessage.assign(messageBody, start, end); } } } // LL_DEBUGS("Voice") << " raw message = \n" << rawMessage << LL_ENDL; // strip formatting tags { std::string::size_type start; std::string::size_type end; while((start = rawMessage.find('<')) != std::string::npos) { if((end = rawMessage.find('>', start + 1)) != std::string::npos) { // Strip out the tag rawMessage.erase(start, (end + 1) - start); } else { // Avoid an infinite loop break; } } } // Decode ampersand-escaped chars { std::string::size_type mark = 0; // The text may contain text encoded with <, >, and & mark = 0; while((mark = rawMessage.find("<", mark)) != std::string::npos) { rawMessage.replace(mark, 4, "<"); mark += 1; } mark = 0; while((mark = rawMessage.find(">", mark)) != std::string::npos) { rawMessage.replace(mark, 4, ">"); mark += 1; } mark = 0; while((mark = rawMessage.find("&", mark)) != std::string::npos) { rawMessage.replace(mark, 5, "&"); mark += 1; } } // strip leading/trailing whitespace (since we always seem to get a couple newlines) LLStringUtil::trim(rawMessage); // LL_DEBUGS("Voice") << " stripped message = \n" << rawMessage << LL_ENDL; sessionState *session = findSession(sessionHandle); if(session) { bool is_busy = gAgent.getBusy(); bool is_muted = LLMuteList::getInstance()->isMuted(session->mCallerID, session->mName, LLMute::flagTextChat); bool is_linden = LLMuteList::getInstance()->isLinden(session->mName); bool quiet_chat = false; LLChat chat; chat.mMuted = is_muted && !is_linden; if(!chat.mMuted) { chat.mFromID = session->mCallerID; chat.mFromName = session->mName; chat.mSourceType = CHAT_SOURCE_AGENT; if(is_busy && !is_linden) { quiet_chat = true; // TODO: Question: Return busy mode response here? Or maybe when session is started instead? } std::string fullMessage = std::string(": ") + rawMessage; LL_DEBUGS("Voice") << "adding message, name " << session->mName << " session " << session->mIMSessionID << ", target " << session->mCallerID << LL_ENDL; gIMMgr->addMessage(session->mIMSessionID, session->mCallerID, session->mName.c_str(), fullMessage.c_str(), LLStringUtil::null, // default arg IM_NOTHING_SPECIAL, // default arg 0, // default arg LLUUID::null, // default arg LLVector3::zero, // default arg true); // prepend name and make it a link to the user's profile chat.mText = std::string("IM: ") + session->mName + std::string(": ") + rawMessage; // If the chat should come in quietly (i.e. we're in busy mode), pretend it's from a local agent. LLFloaterChat::addChat( chat, TRUE, quiet_chat ); } } } } void LLVoiceClient::sessionNotificationEvent(std::string &sessionHandle, std::string &uriString, std::string ¬ificationType) { sessionState *session = findSession(sessionHandle); if(session) { participantState *participant = session->findParticipant(uriString); if(participant) { if (!stricmp(notificationType.c_str(), "Typing")) { // Other end started typing // TODO: The proper way to add a typing notification seems to be LLIMMgr::processIMTypingStart(). // It requires an LLIMInfo for the message, which we don't have here. } else if (!stricmp(notificationType.c_str(), "NotTyping")) { // Other end stopped typing // TODO: The proper way to remove a typing notification seems to be LLIMMgr::processIMTypingStop(). // It requires an LLIMInfo for the message, which we don't have here. } else { LL_DEBUGS("Voice") << "Unknown notification type " << notificationType << "for participant " << uriString << " in session " << session->mSIPURI << LL_ENDL; } } else { LL_DEBUGS("Voice") << "Unknown participant " << uriString << " in session " << session->mSIPURI << LL_ENDL; } } else { LL_DEBUGS("Voice") << "Unknown session handle " << sessionHandle << LL_ENDL; } } void LLVoiceClient::subscriptionEvent(std::string &buddyURI, std::string &subscriptionHandle, std::string &alias, std::string &displayName, std::string &applicationString, std::string &subscriptionType) { buddyListEntry *buddy = findBuddy(buddyURI); if(!buddy) { // Couldn't find buddy by URI, try converting the alias... if(!alias.empty()) { LLUUID id; if(IDFromName(alias, id)) { buddy = findBuddy(id); } } } if(buddy) { std::ostringstream stream; if(buddy->mCanSeeMeOnline) { // Sending the response will create an auto-accept rule buddy->mHasAutoAcceptListEntry = true; } else { // Sending the response will create a block rule buddy->mHasBlockListEntry = true; } if(buddy->mInSLFriends) { buddy->mInVivoxBuddies = true; } stream << "" << "" << mAccountHandle << "" << "" << buddy->mURI << "" << "" << (buddy->mCanSeeMeOnline?"Allow":"Hide") << "" << ""<< (buddy->mInSLFriends?"1":"0")<< "" << "" << subscriptionHandle << "" << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::auxAudioPropertiesEvent(F32 energy) { LL_DEBUGS("Voice") << "got energy " << energy << LL_ENDL; mTuningEnergy = energy; } void LLVoiceClient::buddyListChanged() { // This is called after we receive a BuddyAndGroupListChangedEvent. mBuddyListMapPopulated = true; mFriendsListDirty = true; } void LLVoiceClient::muteListChanged() { // The user's mute list has been updated. Go through the current participant list and sync it with the mute list. if(mAudioSession) { participantMap::iterator iter = mAudioSession->mParticipantsByURI.begin(); for(; iter != mAudioSession->mParticipantsByURI.end(); iter++) { participantState *p = iter->second; // Check to see if this participant is on the mute list already if(p->updateMuteState()) mAudioSession->mVolumeDirty = true; } } } void LLVoiceClient::updateFriends(U32 mask) { if(mask & (LLFriendObserver::ADD | LLFriendObserver::REMOVE | LLFriendObserver::POWERS)) { // Just resend the whole friend list to the daemon mFriendsListDirty = true; } } ///////////////////////////// // Managing list of participants LLVoiceClient::participantState::participantState(const std::string &uri) : mURI(uri), mPTT(false), mIsSpeaking(false), mIsModeratorMuted(false), mLastSpokeTimestamp(0.f), mPower(0.f), mVolume(-1), mOnMuteList(false), mUserVolume(-1), mVolumeDirty(false), mAvatarIDValid(false), mIsSelf(false) { } LLVoiceClient::participantState *LLVoiceClient::sessionState::addParticipant(const std::string &uri) { participantState *result = NULL; bool useAlternateURI = false; // Note: this is mostly the body of LLVoiceClient::sessionState::findParticipant(), but since we need to know if it // matched the alternate SIP URI (so we can add it properly), we need to reproduce it here. { participantMap::iterator iter = mParticipantsByURI.find(&uri); if(iter == mParticipantsByURI.end()) { if(!mAlternateSIPURI.empty() && (uri == mAlternateSIPURI)) { // This is a p2p session (probably with the SLIM client) with an alternate URI for the other participant. // Use mSIPURI instead, since it will be properly encoded. iter = mParticipantsByURI.find(&(mSIPURI)); useAlternateURI = true; } } if(iter != mParticipantsByURI.end()) { result = iter->second; } } if(!result) { // participant isn't already in one list or the other. result = new participantState(useAlternateURI?mSIPURI:uri); mParticipantsByURI.insert(participantMap::value_type(&(result->mURI), result)); mParticipantsChanged = true; // Try to do a reverse transform on the URI to get the GUID back. { LLUUID id; if(IDFromName(result->mURI, id)) { result->mAvatarIDValid = true; result->mAvatarID = id; if(result->updateMuteState()) mVolumeDirty = true; } else { // Create a UUID by hashing the URI, but do NOT set mAvatarIDValid. // This tells both code in LLVoiceClient and code in llfloateractivespeakers.cpp that the ID will not be in the name cache. setUUIDFromStringHash(result->mAvatarID, uri); } } mParticipantsByUUID.insert(participantUUIDMap::value_type(&(result->mAvatarID), result)); LL_DEBUGS("Voice") << "participant \"" << result->mURI << "\" added." << LL_ENDL; } return result; } bool LLVoiceClient::participantState::updateMuteState() { bool result = false; if(mAvatarIDValid) { bool isMuted = LLMuteList::getInstance()->isMuted(mAvatarID, LLMute::flagVoiceChat); if(mOnMuteList != isMuted) { mOnMuteList = isMuted; mVolumeDirty = true; result = true; } } return result; } bool LLVoiceClient::participantState::isAvatar() { return mAvatarIDValid; } void LLVoiceClient::sessionState::removeParticipant(LLVoiceClient::participantState *participant) { if(participant) { participantMap::iterator iter = mParticipantsByURI.find(&(participant->mURI)); participantUUIDMap::iterator iter2 = mParticipantsByUUID.find(&(participant->mAvatarID)); LL_DEBUGS("Voice") << "participant \"" << participant->mURI << "\" (" << participant->mAvatarID << ") removed." << LL_ENDL; if(iter == mParticipantsByURI.end()) { LL_ERRS("Voice") << "Internal error: participant " << participant->mURI << " not in URI map" << LL_ENDL; } else if(iter2 == mParticipantsByUUID.end()) { LL_ERRS("Voice") << "Internal error: participant ID " << participant->mAvatarID << " not in UUID map" << LL_ENDL; } else if(iter->second != iter2->second) { LL_ERRS("Voice") << "Internal error: participant mismatch!" << LL_ENDL; } else { mParticipantsByURI.erase(iter); mParticipantsByUUID.erase(iter2); delete participant; mParticipantsChanged = true; } } } void LLVoiceClient::sessionState::removeAllParticipants() { LL_DEBUGS("Voice") << "called" << LL_ENDL; while(!mParticipantsByURI.empty()) { removeParticipant(mParticipantsByURI.begin()->second); } if(!mParticipantsByUUID.empty()) { LL_ERRS("Voice") << "Internal error: empty URI map, non-empty UUID map" << LL_ENDL } } LLVoiceClient::participantMap *LLVoiceClient::getParticipantList(void) { participantMap *result = NULL; if(mAudioSession) { result = &(mAudioSession->mParticipantsByURI); } return result; } LLVoiceClient::participantState *LLVoiceClient::sessionState::findParticipant(const std::string &uri) { participantState *result = NULL; participantMap::iterator iter = mParticipantsByURI.find(&uri); if(iter == mParticipantsByURI.end()) { if(!mAlternateSIPURI.empty() && (uri == mAlternateSIPURI)) { // This is a p2p session (probably with the SLIM client) with an alternate URI for the other participant. // Look up the other URI iter = mParticipantsByURI.find(&(mSIPURI)); } } if(iter != mParticipantsByURI.end()) { result = iter->second; } return result; } LLVoiceClient::participantState* LLVoiceClient::sessionState::findParticipantByID(const LLUUID& id) { participantState * result = NULL; participantUUIDMap::iterator iter = mParticipantsByUUID.find(&id); if(iter != mParticipantsByUUID.end()) { result = iter->second; } return result; } LLVoiceClient::participantState* LLVoiceClient::findParticipantByID(const LLUUID& id) { participantState * result = NULL; if(mAudioSession) { result = mAudioSession->findParticipantByID(id); } return result; } void LLVoiceClient::parcelChanged() { if(getState() >= stateNoChannel) { // If the user is logged in, start a channel lookup. LL_DEBUGS("Voice") << "sending ParcelVoiceInfoRequest (" << mCurrentRegionName << ", " << mCurrentParcelLocalID << ")" << LL_ENDL; std::string url = gAgent.getRegion()->getCapability("ParcelVoiceInfoRequest"); LLSD data; LLHTTPClient::post( url, data, new LLVoiceClientCapResponder); } else { // The transition to stateNoChannel needs to kick this off again. LL_INFOS("Voice") << "not logged in yet, deferring" << LL_ENDL; } } void LLVoiceClient::switchChannel( std::string uri, bool spatial, bool no_reconnect, bool is_p2p, std::string hash) { bool needsSwitch = false; LL_DEBUGS("Voice") << "called in state " << state2string(getState()) << " with uri \"" << uri << "\"" << (spatial?", spatial is true":", spatial is false") << LL_ENDL; switch(getState()) { case stateJoinSessionFailed: case stateJoinSessionFailedWaiting: case stateNoChannel: // Always switch to the new URI from these states. needsSwitch = true; break; default: if(mSessionTerminateRequested) { // If a terminate has been requested, we need to compare against where the URI we're already headed to. if(mNextAudioSession) { if(mNextAudioSession->mSIPURI != uri) needsSwitch = true; } else { // mNextAudioSession is null -- this probably means we're on our way back to spatial. if(!uri.empty()) { // We do want to process a switch in this case. needsSwitch = true; } } } else { // Otherwise, compare against the URI we're in now. if(mAudioSession) { if(mAudioSession->mSIPURI != uri) { needsSwitch = true; } } else { if(!uri.empty()) { // mAudioSession is null -- it's not clear what case would cause this. // For now, log it as a warning and see if it ever crops up. LL_WARNS("Voice") << "No current audio session." << LL_ENDL; } } } break; } if(needsSwitch) { if(uri.empty()) { // Leave any channel we may be in LL_DEBUGS("Voice") << "leaving channel" << LL_ENDL; sessionState *oldSession = mNextAudioSession; mNextAudioSession = NULL; // The old session may now need to be deleted. reapSession(oldSession); notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED); } else { LL_DEBUGS("Voice") << "switching to channel " << uri << LL_ENDL; mNextAudioSession = addSession(uri); mNextAudioSession->mHash = hash; mNextAudioSession->mIsSpatial = spatial; mNextAudioSession->mReconnect = !no_reconnect; mNextAudioSession->mIsP2P = is_p2p; } if(getState() <= stateNoChannel) { // We're already set up to join a channel, just needed to fill in the session URI } else { // State machine will come around and rejoin if uri/handle is not empty. sessionTerminate(); } } } void LLVoiceClient::joinSession(sessionState *session) { mNextAudioSession = session; if(getState() <= stateNoChannel) { // We're already set up to join a channel, just needed to fill in the session handle } else { // State machine will come around and rejoin if uri/handle is not empty. sessionTerminate(); } } void LLVoiceClient::setNonSpatialChannel( const std::string &uri, const std::string &credentials) { switchChannel(uri, false, false, false, credentials); } void LLVoiceClient::setSpatialChannel( const std::string &uri, const std::string &credentials) { mSpatialSessionURI = uri; mSpatialSessionCredentials = credentials; mAreaVoiceDisabled = mSpatialSessionURI.empty(); LL_DEBUGS("Voice") << "got spatial channel uri: \"" << uri << "\"" << LL_ENDL; if((mAudioSession && !(mAudioSession->mIsSpatial)) || (mNextAudioSession && !(mNextAudioSession->mIsSpatial))) { // User is in a non-spatial chat or joining a non-spatial chat. Don't switch channels. LL_INFOS("Voice") << "in non-spatial chat, not switching channels" << LL_ENDL; } else { switchChannel(mSpatialSessionURI, true, false, false, mSpatialSessionCredentials); } } void LLVoiceClient::callUser(const LLUUID &uuid) { std::string userURI = sipURIFromID(uuid); switchChannel(userURI, false, true, true); } LLVoiceClient::sessionState* LLVoiceClient::startUserIMSession(const LLUUID &uuid) { // Figure out if a session with the user already exists sessionState *session = findSession(uuid); if(!session) { // No session with user, need to start one. std::string uri = sipURIFromID(uuid); session = addSession(uri); session->mIsSpatial = false; session->mReconnect = false; session->mIsP2P = true; session->mCallerID = uuid; } if(session) { if(session->mHandle.empty()) { // Session isn't active -- start it up. sessionCreateSendMessage(session, false, true); } else { // Session is already active -- start up text. sessionTextConnectSendMessage(session); } } return session; } bool LLVoiceClient::sendTextMessage(const LLUUID& participant_id, const std::string& message) { bool result = false; // Attempt to locate the indicated session sessionState *session = startUserIMSession(participant_id); if(session) { // found the session, attempt to send the message session->mTextMsgQueue.push(message); // Try to send queued messages (will do nothing if the session is not open yet) sendQueuedTextMessages(session); // The message is queued, so we succeed. result = true; } else { LL_DEBUGS("Voice") << "Session not found for participant ID " << participant_id << LL_ENDL; } return result; } void LLVoiceClient::sendQueuedTextMessages(sessionState *session) { if(session->mTextStreamState == 1) { if(!session->mTextMsgQueue.empty()) { std::ostringstream stream; while(!session->mTextMsgQueue.empty()) { std::string message = session->mTextMsgQueue.front(); session->mTextMsgQueue.pop(); stream << "" << "" << session->mHandle << "" << "text/HTML" << "" << message << "" << "" << "\n\n\n"; } writeString(stream.str()); } } else { // Session isn't connected yet, defer until later. } } void LLVoiceClient::endUserIMSession(const LLUUID &uuid) { // Figure out if a session with the user exists sessionState *session = findSession(uuid); if(session) { // found the session if(!session->mHandle.empty()) { sessionTextDisconnectSendMessage(session); } } else { LL_DEBUGS("Voice") << "Session not found for participant ID " << uuid << LL_ENDL; } } bool LLVoiceClient::answerInvite(std::string &sessionHandle) { // this is only ever used to answer incoming p2p call invites. sessionState *session = findSession(sessionHandle); if(session) { session->mIsSpatial = false; session->mReconnect = false; session->mIsP2P = true; joinSession(session); return true; } return false; } bool LLVoiceClient::isOnlineSIP(const LLUUID &id) { bool result = false; buddyListEntry *buddy = findBuddy(id); if(buddy) { result = buddy->mOnlineSLim; LL_DEBUGS("Voice") << "Buddy " << buddy->mDisplayName << " is SIP " << (result?"online":"offline") << LL_ENDL; } if(!result) { // This user isn't on the buddy list or doesn't show online status through the buddy list, but could be a participant in an existing session if they initiated a text IM. sessionState *session = findSession(id); if(session && !session->mHandle.empty()) { if((session->mTextStreamState != streamStateUnknown) || (session->mMediaStreamState > streamStateIdle)) { LL_DEBUGS("Voice") << "Open session with " << id << " found, returning SIP online state" << LL_ENDL; // we have a p2p text session open with this user, so by definition they're online. result = true; } } } return result; } // Returns true if the indicated participant in the current audio session is really an SL avatar. // Currently this will be false only for PSTN callers into group chats, and PSTN p2p calls. bool LLVoiceClient::isParticipantAvatar(const LLUUID &id) { bool result = true; sessionState *session = findSession(id); if(session != NULL) { // this is a p2p session with the indicated caller, or the session with the specified UUID. if(session->mSynthesizedCallerID) result = false; } else { // Didn't find a matching session -- check the current audio session for a matching participant if(mAudioSession != NULL) { participantState *participant = findParticipantByID(id); if(participant != NULL) { result = participant->isAvatar(); } } } return result; } // Returns true if calling back the session URI after the session has closed is possible. // Currently this will be false only for PSTN P2P calls. bool LLVoiceClient::isSessionCallBackPossible(const LLUUID &session_id) { bool result = true; sessionState *session = findSession(session_id); if(session != NULL) { result = session->isCallBackPossible(); } return result; } // Returns true if the session can accepte text IM's. // Currently this will be false only for PSTN P2P calls. bool LLVoiceClient::isSessionTextIMPossible(const LLUUID &session_id) { bool result = true; sessionState *session = findSession(session_id); if(session != NULL) { result = session->isTextIMPossible(); } return result; } void LLVoiceClient::declineInvite(std::string &sessionHandle) { sessionState *session = findSession(sessionHandle); if(session) { sessionMediaDisconnectSendMessage(session); } } void LLVoiceClient::leaveNonSpatialChannel() { LL_DEBUGS("Voice") << "called in state " << state2string(getState()) << LL_ENDL; // Make sure we don't rejoin the current session. sessionState *oldNextSession = mNextAudioSession; mNextAudioSession = NULL; // Most likely this will still be the current session at this point, but check it anyway. reapSession(oldNextSession); verifySessionState(); sessionTerminate(); } std::string LLVoiceClient::getCurrentChannel() { std::string result; if((getState() == stateRunning) && !mSessionTerminateRequested) { result = getAudioSessionURI(); } return result; } bool LLVoiceClient::inProximalChannel() { bool result = false; if((getState() == stateRunning) && !mSessionTerminateRequested) { result = inSpatialChannel(); } return result; } std::string LLVoiceClient::sipURIFromID(const LLUUID &id) { std::string result; result = "sip:"; result += nameFromID(id); result += "@"; result += mVoiceSIPURIHostName; return result; } std::string LLVoiceClient::sipURIFromAvatar(LLVOAvatar *avatar) { std::string result; if(avatar) { result = "sip:"; result += nameFromID(avatar->getID()); result += "@"; result += mVoiceSIPURIHostName; } return result; } std::string LLVoiceClient::nameFromAvatar(LLVOAvatar *avatar) { std::string result; if(avatar) { result = nameFromID(avatar->getID()); } return result; } std::string LLVoiceClient::nameFromID(const LLUUID &uuid) { std::string result; if (uuid.isNull()) { //VIVOX, the uuid emtpy look for the mURIString and return that instead. //result.assign(uuid.mURIStringName); LLStringUtil::replaceChar(result, '_', ' '); return result; } // Prepending this apparently prevents conflicts with reserved names inside the vivox and diamondware code. result = "x"; // Base64 encode and replace the pieces of base64 that are less compatible // with e-mail local-parts. // See RFC-4648 "Base 64 Encoding with URL and Filename Safe Alphabet" result += LLBase64::encode(uuid.mData, UUID_BYTES); LLStringUtil::replaceChar(result, '+', '-'); LLStringUtil::replaceChar(result, '/', '_'); // If you need to transform a GUID to this form on the Mac OS X command line, this will do so: // echo -n x && (echo e669132a-6c43-4ee1-a78d-6c82fff59f32 |xxd -r -p |openssl base64|tr '/+' '_-') // The reverse transform can be done with: // echo 'x5mkTKmxDTuGnjWyC__WfMg==' |cut -b 2- -|tr '_-' '/+' |openssl base64 -d|xxd -p return result; } bool LLVoiceClient::IDFromName(const std::string inName, LLUUID &uuid) { bool result = false; // SLIM SDK: The "name" may actually be a SIP URI such as: "sip:xFnPP04IpREWNkuw1cOXlhw==@bhr.vivox.com" // If it is, convert to a bare name before doing the transform. std::string name = nameFromsipURI(inName); // Doesn't look like a SIP URI, assume it's an actual name. if(name.empty()) name = inName; // This will only work if the name is of the proper form. // As an example, the account name for Monroe Linden (UUID 1673cfd3-8229-4445-8d92-ec3570e5e587) is: // "xFnPP04IpREWNkuw1cOXlhw==" if((name.size() == 25) && (name[0] == 'x') && (name[23] == '=') && (name[24] == '=')) { // The name appears to have the right form. // Reverse the transforms done by nameFromID std::string temp = name; LLStringUtil::replaceChar(temp, '-', '+'); LLStringUtil::replaceChar(temp, '_', '/'); U8 rawuuid[UUID_BYTES + 1]; int len = apr_base64_decode_binary(rawuuid, temp.c_str() + 1); if(len == UUID_BYTES) { // The decode succeeded. Stuff the bits into the result's UUID memcpy(uuid.mData, rawuuid, UUID_BYTES); result = true; } } if(!result) { // VIVOX: not a standard account name, just copy the URI name mURIString field // and hope for the best. bpj uuid.setNull(); // VIVOX, set the uuid field to nulls } return result; } std::string LLVoiceClient::displayNameFromAvatar(LLVOAvatar *avatar) { return avatar->getFullname(); } std::string LLVoiceClient::sipURIFromName(std::string &name) { std::string result; result = "sip:"; result += name; result += "@"; result += mVoiceSIPURIHostName; // LLStringUtil::toLower(result); return result; } std::string LLVoiceClient::nameFromsipURI(const std::string &uri) { std::string result; std::string::size_type sipOffset, atOffset; sipOffset = uri.find("sip:"); atOffset = uri.find("@"); if((sipOffset != std::string::npos) && (atOffset != std::string::npos)) { result = uri.substr(sipOffset + 4, atOffset - (sipOffset + 4)); } return result; } bool LLVoiceClient::inSpatialChannel(void) { bool result = false; if(mAudioSession) result = mAudioSession->mIsSpatial; return result; } std::string LLVoiceClient::getAudioSessionURI() { std::string result; if(mAudioSession) result = mAudioSession->mSIPURI; return result; } std::string LLVoiceClient::getAudioSessionHandle() { std::string result; if(mAudioSession) result = mAudioSession->mHandle; return result; } ///////////////////////////// // Sending updates of current state void LLVoiceClient::enforceTether(void) { LLVector3d tethered = mCameraRequestedPosition; // constrain 'tethered' to within 50m of mAvatarPosition. { F32 max_dist = 50.0f; LLVector3d camera_offset = mCameraRequestedPosition - mAvatarPosition; F32 camera_distance = (F32)camera_offset.magVec(); if(camera_distance > max_dist) { tethered = mAvatarPosition + (max_dist / camera_distance) * camera_offset; } } if(dist_vec(mCameraPosition, tethered) > 0.1) { mCameraPosition = tethered; mSpatialCoordsDirty = true; } } void LLVoiceClient::updatePosition(void) { if(gVoiceClient) { LLVOAvatar *agent = gAgent.getAvatarObject(); LLViewerRegion *region = gAgent.getRegion(); if(region && agent) { LLMatrix3 rot; LLVector3d pos; // TODO: If camera and avatar velocity are actually used by the voice system, we could compute them here... // They're currently always set to zero. // Send the current camera position to the voice code rot.setRows(LLViewerCamera::getInstance()->getAtAxis(), LLViewerCamera::getInstance()->getLeftAxis (), LLViewerCamera::getInstance()->getUpAxis()); pos = gAgent.getRegion()->getPosGlobalFromRegion(LLViewerCamera::getInstance()->getOrigin()); gVoiceClient->setCameraPosition( pos, // position LLVector3::zero, // velocity rot); // rotation matrix // Send the current avatar position to the voice code rot = agent->getRootJoint()->getWorldRotation().getMatrix3(); pos = agent->getPositionGlobal(); // TODO: Can we get the head offset from outside the LLVOAvatar? // pos += LLVector3d(mHeadOffset); pos += LLVector3d(0.f, 0.f, 1.f); gVoiceClient->setAvatarPosition( pos, // position LLVector3::zero, // velocity rot); // rotation matrix } } } void LLVoiceClient::setCameraPosition(const LLVector3d &position, const LLVector3 &velocity, const LLMatrix3 &rot) { mCameraRequestedPosition = position; if(mCameraVelocity != velocity) { mCameraVelocity = velocity; mSpatialCoordsDirty = true; } if(mCameraRot != rot) { mCameraRot = rot; mSpatialCoordsDirty = true; } } void LLVoiceClient::setAvatarPosition(const LLVector3d &position, const LLVector3 &velocity, const LLMatrix3 &rot) { if(dist_vec(mAvatarPosition, position) > 0.1) { mAvatarPosition = position; mSpatialCoordsDirty = true; } if(mAvatarVelocity != velocity) { mAvatarVelocity = velocity; mSpatialCoordsDirty = true; } if(mAvatarRot != rot) { mAvatarRot = rot; mSpatialCoordsDirty = true; } } bool LLVoiceClient::channelFromRegion(LLViewerRegion *region, std::string &name) { bool result = false; if(region) { name = region->getName(); } if(!name.empty()) result = true; return result; } void LLVoiceClient::leaveChannel(void) { if(getState() == stateRunning) { LL_DEBUGS("Voice") << "leaving channel for teleport/logout" << LL_ENDL; mChannelName.clear(); sessionTerminate(); } } void LLVoiceClient::setMuteMic(bool muted) { mMuteMic = muted; } bool LLVoiceClient::getMuteMic() const { return mMuteMic; } void LLVoiceClient::setUserPTTState(bool ptt) { mUserPTTState = ptt; } bool LLVoiceClient::getUserPTTState() { return mUserPTTState; } void LLVoiceClient::toggleUserPTTState(void) { mUserPTTState = !mUserPTTState; } void LLVoiceClient::setVoiceEnabled(bool enabled) { if (enabled != mVoiceEnabled) { mVoiceEnabled = enabled; if (enabled) { LLVoiceChannel::getCurrentVoiceChannel()->activate(); } else { // Turning voice off looses your current channel -- this makes sure the UI isn't out of sync when you re-enable it. LLVoiceChannel::getCurrentVoiceChannel()->deactivate(); } } } bool LLVoiceClient::voiceEnabled() { return gSavedSettings.getBOOL("EnableVoiceChat") && !gSavedSettings.getBOOL("CmdLineDisableVoice"); } void LLVoiceClient::setLipSyncEnabled(BOOL enabled) { mLipSyncEnabled = enabled; } BOOL LLVoiceClient::lipSyncEnabled() { if ( mVoiceEnabled && stateDisabled != getState() ) { return mLipSyncEnabled; } else { return FALSE; } } void LLVoiceClient::setUsePTT(bool usePTT) { if(usePTT && !mUsePTT) { // When the user turns on PTT, reset the current state. mUserPTTState = false; } mUsePTT = usePTT; } void LLVoiceClient::setPTTIsToggle(bool PTTIsToggle) { if(!PTTIsToggle && mPTTIsToggle) { // When the user turns off toggle, reset the current state. mUserPTTState = false; } mPTTIsToggle = PTTIsToggle; } void LLVoiceClient::setPTTKey(std::string &key) { if(key == "MiddleMouse") { mPTTIsMiddleMouse = true; } else { mPTTIsMiddleMouse = false; if(!LLKeyboard::keyFromString(key, &mPTTKey)) { // If the call failed, don't match any key. key = KEY_NONE; } } } void LLVoiceClient::setEarLocation(S32 loc) { if(mEarLocation != loc) { LL_DEBUGS("Voice") << "Setting mEarLocation to " << loc << LL_ENDL; mEarLocation = loc; mSpatialCoordsDirty = true; } } void LLVoiceClient::setVoiceVolume(F32 volume) { int scaled_volume = scale_speaker_volume(volume); if(scaled_volume != mSpeakerVolume) { if((scaled_volume == 0) || (mSpeakerVolume == 0)) { mSpeakerMuteDirty = true; } mSpeakerVolume = scaled_volume; mSpeakerVolumeDirty = true; } } void LLVoiceClient::setMicGain(F32 volume) { int scaled_volume = scale_mic_volume(volume); if(scaled_volume != mMicVolume) { mMicVolume = scaled_volume; mMicVolumeDirty = true; } } void LLVoiceClient::keyDown(KEY key, MASK mask) { // LL_DEBUGS("Voice") << "key is " << LLKeyboard::stringFromKey(key) << LL_ENDL; if (gKeyboard->getKeyRepeated(key)) { // ignore auto-repeat keys return; } if(!mPTTIsMiddleMouse) { if(mPTTIsToggle) { if(key == mPTTKey) { toggleUserPTTState(); } } else if(mPTTKey != KEY_NONE) { setUserPTTState(gKeyboard->getKeyDown(mPTTKey)); } } } void LLVoiceClient::keyUp(KEY key, MASK mask) { if(!mPTTIsMiddleMouse) { if(!mPTTIsToggle && (mPTTKey != KEY_NONE)) { setUserPTTState(gKeyboard->getKeyDown(mPTTKey)); } } } void LLVoiceClient::middleMouseState(bool down) { if(mPTTIsMiddleMouse) { if(mPTTIsToggle) { if(down) { toggleUserPTTState(); } } else { setUserPTTState(down); } } } ///////////////////////////// // Accessors for data related to nearby speakers BOOL LLVoiceClient::getVoiceEnabled(const LLUUID& id) { BOOL result = FALSE; participantState *participant = findParticipantByID(id); if(participant) { // I'm not sure what the semantics of this should be. // For now, if we have any data about the user that came through the chat channel, assume they're voice-enabled. result = TRUE; } return result; } BOOL LLVoiceClient::getIsSpeaking(const LLUUID& id) { BOOL result = FALSE; participantState *participant = findParticipantByID(id); if(participant) { if (participant->mSpeakingTimeout.getElapsedTimeF32() > SPEAKING_TIMEOUT) { participant->mIsSpeaking = FALSE; } result = participant->mIsSpeaking; } return result; } BOOL LLVoiceClient::getIsModeratorMuted(const LLUUID& id) { BOOL result = FALSE; participantState *participant = findParticipantByID(id); if(participant) { result = participant->mIsModeratorMuted; } return result; } F32 LLVoiceClient::getCurrentPower(const LLUUID& id) { F32 result = 0; participantState *participant = findParticipantByID(id); if(participant) { result = participant->mPower; } return result; } std::string LLVoiceClient::getDisplayName(const LLUUID& id) { std::string result; participantState *participant = findParticipantByID(id); if(participant) { result = participant->mDisplayName; } return result; } BOOL LLVoiceClient::getUsingPTT(const LLUUID& id) { BOOL result = FALSE; participantState *participant = findParticipantByID(id); if(participant) { // I'm not sure what the semantics of this should be. // Does "using PTT" mean they're configured with a push-to-talk button? // For now, we know there's no PTT mechanism in place, so nobody is using it. } return result; } BOOL LLVoiceClient::getOnMuteList(const LLUUID& id) { BOOL result = FALSE; participantState *participant = findParticipantByID(id); if(participant) { result = participant->mOnMuteList; } return result; } // External accessiors. Maps 0.0 to 1.0 to internal values 0-400 with .5 == 100 // internal = 400 * external^2 F32 LLVoiceClient::getUserVolume(const LLUUID& id) { F32 result = 0.0f; participantState *participant = findParticipantByID(id); if(participant) { S32 ires = 100; // nominal default volume if(participant->mIsSelf) { // Always make it look like the user's own volume is set at the default. } else if(participant->mUserVolume != -1) { // Use the internal volume ires = participant->mUserVolume; // Enable this when debugging voice slider issues. It's way to spammy even for debug-level logging. // LL_DEBUGS("Voice") << "mapping from mUserVolume " << ires << LL_ENDL; } else if(participant->mVolume != -1) { // Map backwards from vivox volume // Enable this when debugging voice slider issues. It's way to spammy even for debug-level logging. // LL_DEBUGS("Voice") << "mapping from mVolume " << participant->mVolume << LL_ENDL; if(participant->mVolume < 56) { ires = (participant->mVolume * 100) / 56; } else { ires = (((participant->mVolume - 56) * 300) / (100 - 56)) + 100; } } result = sqrtf(((F32)ires) / 400.f); } // Enable this when debugging voice slider issues. It's way to spammy even for debug-level logging. // LL_DEBUGS("Voice") << "returning " << result << LL_ENDL; return result; } void LLVoiceClient::setUserVolume(const LLUUID& id, F32 volume) { if(mAudioSession) { participantState *participant = findParticipantByID(id); if (participant) { // volume can amplify by as much as 4x! S32 ivol = (S32)(400.f * volume * volume); participant->mUserVolume = llclamp(ivol, 0, 400); participant->mVolumeDirty = TRUE; mAudioSession->mVolumeDirty = TRUE; } } } std::string LLVoiceClient::getGroupID(const LLUUID& id) { std::string result; participantState *participant = findParticipantByID(id); if(participant) { result = participant->mGroupID; } return result; } BOOL LLVoiceClient::getAreaVoiceDisabled() { return mAreaVoiceDisabled; } void LLVoiceClient::recordingLoopStart(int seconds, int deltaFramesPerControlFrame) { // LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Start)" << LL_ENDL; if(!mMainSessionGroupHandle.empty()) { std::ostringstream stream; stream << "" << "" << mMainSessionGroupHandle << "" << "Start" << "" << deltaFramesPerControlFrame << "" << "" << "" << "" << "false" << "" << seconds << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::recordingLoopSave(const std::string& filename) { // LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Flush)" << LL_ENDL; if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) { std::ostringstream stream; stream << "" << "" << mMainSessionGroupHandle << "" << "Flush" << "" << filename << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::recordingStop() { // LL_DEBUGS("Voice") << "sending SessionGroup.ControlRecording (Stop)" << LL_ENDL; if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) { std::ostringstream stream; stream << "" << "" << mMainSessionGroupHandle << "" << "Stop" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::filePlaybackStart(const std::string& filename) { // LL_DEBUGS("Voice") << "sending SessionGroup.ControlPlayback (Start)" << LL_ENDL; if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) { std::ostringstream stream; stream << "" << "" << mMainSessionGroupHandle << "" << "Start" << "" << filename << "" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::filePlaybackStop() { // LL_DEBUGS("Voice") << "sending SessionGroup.ControlPlayback (Stop)" << LL_ENDL; if(mAudioSession != NULL && !mAudioSession->mGroupHandle.empty()) { std::ostringstream stream; stream << "" << "" << mMainSessionGroupHandle << "" << "Stop" << "\n\n\n"; writeString(stream.str()); } } void LLVoiceClient::filePlaybackSetPaused(bool paused) { // TODO: Implement once Vivox gives me a sample } void LLVoiceClient::filePlaybackSetMode(bool vox, float speed) { // TODO: Implement once Vivox gives me a sample } LLVoiceClient::sessionState::sessionState() : mMediaStreamState(streamStateUnknown), mTextStreamState(streamStateUnknown), mCreateInProgress(false), mMediaConnectInProgress(false), mVoiceInvitePending(false), mTextInvitePending(false), mSynthesizedCallerID(false), mIsChannel(false), mIsSpatial(false), mIsP2P(false), mIncoming(false), mVoiceEnabled(false), mReconnect(false), mVolumeDirty(false), mParticipantsChanged(false) { } LLVoiceClient::sessionState::~sessionState() { removeAllParticipants(); } bool LLVoiceClient::sessionState::isCallBackPossible() { // This may change to be explicitly specified by vivox in the future... // Currently, only PSTN P2P calls cannot be returned. // Conveniently, this is also the only case where we synthesize a caller UUID. return !mSynthesizedCallerID; } bool LLVoiceClient::sessionState::isTextIMPossible() { // This may change to be explicitly specified by vivox in the future... return !mSynthesizedCallerID; } LLVoiceClient::sessionIterator LLVoiceClient::sessionsBegin(void) { return mSessions.begin(); } LLVoiceClient::sessionIterator LLVoiceClient::sessionsEnd(void) { return mSessions.end(); } LLVoiceClient::sessionState *LLVoiceClient::findSession(const std::string &handle) { sessionState *result = NULL; sessionMap::iterator iter = mSessionsByHandle.find(&handle); if(iter != mSessionsByHandle.end()) { result = iter->second; } return result; } LLVoiceClient::sessionState *LLVoiceClient::findSessionBeingCreatedByURI(const std::string &uri) { sessionState *result = NULL; for(sessionIterator iter = sessionsBegin(); iter != sessionsEnd(); iter++) { sessionState *session = *iter; if(session->mCreateInProgress && (session->mSIPURI == uri)) { result = session; break; } } return result; } LLVoiceClient::sessionState *LLVoiceClient::findSession(const LLUUID &participant_id) { sessionState *result = NULL; for(sessionIterator iter = sessionsBegin(); iter != sessionsEnd(); iter++) { sessionState *session = *iter; if((session->mCallerID == participant_id) || (session->mIMSessionID == participant_id)) { result = session; break; } } return result; } LLVoiceClient::sessionState *LLVoiceClient::addSession(const std::string &uri, const std::string &handle) { sessionState *result = NULL; if(handle.empty()) { // No handle supplied. // Check whether there's already a session with this URI for(sessionIterator iter = sessionsBegin(); iter != sessionsEnd(); iter++) { sessionState *s = *iter; if((s->mSIPURI == uri) || (s->mAlternateSIPURI == uri)) { // TODO: I need to think about this logic... it's possible that this case should raise an internal error. result = s; break; } } } else // (!handle.empty()) { // Check for an existing session with this handle sessionMap::iterator iter = mSessionsByHandle.find(&handle); if(iter != mSessionsByHandle.end()) { result = iter->second; } } if(!result) { // No existing session found. LL_DEBUGS("Voice") << "adding new session: handle " << handle << " URI " << uri << LL_ENDL; result = new sessionState(); result->mSIPURI = uri; result->mHandle = handle; mSessions.insert(result); if(!result->mHandle.empty()) { mSessionsByHandle.insert(sessionMap::value_type(&(result->mHandle), result)); } } else { // Found an existing session if(uri != result->mSIPURI) { // TODO: Should this be an internal error? LL_DEBUGS("Voice") << "changing uri from " << result->mSIPURI << " to " << uri << LL_ENDL; setSessionURI(result, uri); } if(handle != result->mHandle) { if(handle.empty()) { // There's at least one race condition where where addSession was clearing an existing session handle, which caused things to break. LL_DEBUGS("Voice") << "NOT clearing handle " << result->mHandle << LL_ENDL; } else { // TODO: Should this be an internal error? LL_DEBUGS("Voice") << "changing handle from " << result->mHandle << " to " << handle << LL_ENDL; setSessionHandle(result, handle); } } LL_DEBUGS("Voice") << "returning existing session: handle " << handle << " URI " << uri << LL_ENDL; } verifySessionState(); return result; } void LLVoiceClient::setSessionHandle(sessionState *session, const std::string &handle) { // Have to remove the session from the handle-indexed map before changing the handle, or things will break badly. if(!session->mHandle.empty()) { // Remove session from the map if it should have been there. sessionMap::iterator iter = mSessionsByHandle.find(&(session->mHandle)); if(iter != mSessionsByHandle.end()) { if(iter->second != session) { LL_ERRS("Voice") << "Internal error: session mismatch!" << LL_ENDL; } mSessionsByHandle.erase(iter); } else { LL_ERRS("Voice") << "Internal error: session handle not found in map!" << LL_ENDL; } } session->mHandle = handle; if(!handle.empty()) { mSessionsByHandle.insert(sessionMap::value_type(&(session->mHandle), session)); } verifySessionState(); } void LLVoiceClient::setSessionURI(sessionState *session, const std::string &uri) { // There used to be a map of session URIs to sessions, which made this complex.... session->mSIPURI = uri; verifySessionState(); } void LLVoiceClient::deleteSession(sessionState *session) { // Remove the session from the handle map if(!session->mHandle.empty()) { sessionMap::iterator iter = mSessionsByHandle.find(&(session->mHandle)); if(iter != mSessionsByHandle.end()) { if(iter->second != session) { LL_ERRS("Voice") << "Internal error: session mismatch" << LL_ENDL } mSessionsByHandle.erase(iter); } } // Remove the session from the URI map mSessions.erase(session); // At this point, the session should be unhooked from all lists and all state should be consistent. verifySessionState(); // If this is the current audio session, clean up the pointer which will soon be dangling. if(mAudioSession == session) { mAudioSession = NULL; mAudioSessionChanged = true; } // ditto for the next audio session if(mNextAudioSession == session) { mNextAudioSession = NULL; } // delete the session delete session; } void LLVoiceClient::deleteAllSessions() { LL_DEBUGS("Voice") << "called" << LL_ENDL; while(!mSessions.empty()) { deleteSession(*(sessionsBegin())); } if(!mSessionsByHandle.empty()) { LL_ERRS("Voice") << "Internal error: empty session map, non-empty handle map" << LL_ENDL } } void LLVoiceClient::verifySessionState(void) { // This is mostly intended for debugging problems with session state management. LL_DEBUGS("Voice") << "Total session count: " << mSessions.size() << " , session handle map size: " << mSessionsByHandle.size() << LL_ENDL; for(sessionIterator iter = sessionsBegin(); iter != sessionsEnd(); iter++) { sessionState *session = *iter; LL_DEBUGS("Voice") << "session " << session << ": handle " << session->mHandle << ", URI " << session->mSIPURI << LL_ENDL; if(!session->mHandle.empty()) { // every session with a non-empty handle needs to be in the handle map sessionMap::iterator i2 = mSessionsByHandle.find(&(session->mHandle)); if(i2 == mSessionsByHandle.end()) { LL_ERRS("Voice") << "internal error (handle " << session->mHandle << " not found in session map)" << LL_ENDL; } else { if(i2->second != session) { LL_ERRS("Voice") << "internal error (handle " << session->mHandle << " in session map points to another session)" << LL_ENDL; } } } } // check that every entry in the handle map points to a valid session in the session set for(sessionMap::iterator iter = mSessionsByHandle.begin(); iter != mSessionsByHandle.end(); iter++) { sessionState *session = iter->second; sessionIterator i2 = mSessions.find(session); if(i2 == mSessions.end()) { LL_ERRS("Voice") << "internal error (session for handle " << session->mHandle << " not found in session map)" << LL_ENDL; } else { if(session->mHandle != (*i2)->mHandle) { LL_ERRS("Voice") << "internal error (session for handle " << session->mHandle << " points to session with different handle " << (*i2)->mHandle << ")" << LL_ENDL; } } } } LLVoiceClient::buddyListEntry::buddyListEntry(const std::string &uri) : mURI(uri) { mOnlineSL = false; mOnlineSLim = false; mCanSeeMeOnline = true; mHasBlockListEntry = false; mHasAutoAcceptListEntry = false; mNameResolved = false; mInVivoxBuddies = false; mInSLFriends = false; mNeedsNameUpdate = false; } void LLVoiceClient::processBuddyListEntry(const std::string &uri, const std::string &displayName) { buddyListEntry *buddy = addBuddy(uri, displayName); buddy->mInVivoxBuddies = true; } LLVoiceClient::buddyListEntry *LLVoiceClient::addBuddy(const std::string &uri) { std::string empty; buddyListEntry *buddy = addBuddy(uri, empty); if(buddy->mDisplayName.empty()) { buddy->mNameResolved = false; } return buddy; } LLVoiceClient::buddyListEntry *LLVoiceClient::addBuddy(const std::string &uri, const std::string &displayName) { buddyListEntry *result = NULL; buddyListMap::iterator iter = mBuddyListMap.find(&uri); if(iter != mBuddyListMap.end()) { // Found a matching buddy already in the map. LL_DEBUGS("Voice") << "adding existing buddy " << uri << LL_ENDL; result = iter->second; } if(!result) { // participant isn't already in one list or the other. LL_DEBUGS("Voice") << "adding new buddy " << uri << LL_ENDL; result = new buddyListEntry(uri); result->mDisplayName = displayName; if(IDFromName(uri, result->mUUID)) { // Extracted UUID from name successfully. } else { LL_DEBUGS("Voice") << "Couldn't find ID for buddy " << uri << " (\"" << displayName << "\")" << LL_ENDL; } mBuddyListMap.insert(buddyListMap::value_type(&(result->mURI), result)); } return result; } LLVoiceClient::buddyListEntry *LLVoiceClient::findBuddy(const std::string &uri) { buddyListEntry *result = NULL; buddyListMap::iterator iter = mBuddyListMap.find(&uri); if(iter != mBuddyListMap.end()) { result = iter->second; } return result; } LLVoiceClient::buddyListEntry *LLVoiceClient::findBuddy(const LLUUID &id) { buddyListEntry *result = NULL; buddyListMap::iterator iter; for(iter = mBuddyListMap.begin(); iter != mBuddyListMap.end(); iter++) { if(iter->second->mUUID == id) { result = iter->second; break; } } return result; } LLVoiceClient::buddyListEntry *LLVoiceClient::findBuddyByDisplayName(const std::string &name) { buddyListEntry *result = NULL; buddyListMap::iterator iter; for(iter = mBuddyListMap.begin(); iter != mBuddyListMap.end(); iter++) { if(iter->second->mDisplayName == name) { result = iter->second; break; } } return result; } void LLVoiceClient::deleteBuddy(const std::string &uri) { buddyListMap::iterator iter = mBuddyListMap.find(&uri); if(iter != mBuddyListMap.end()) { LL_DEBUGS("Voice") << "deleting buddy " << uri << LL_ENDL; buddyListEntry *buddy = iter->second; mBuddyListMap.erase(iter); delete buddy; } else { LL_DEBUGS("Voice") << "attempt to delete nonexistent buddy " << uri << LL_ENDL; } } void LLVoiceClient::deleteAllBuddies(void) { while(!mBuddyListMap.empty()) { deleteBuddy(*(mBuddyListMap.begin()->first)); } // Don't want to correlate with friends list when we've emptied the buddy list. mBuddyListMapPopulated = false; // Don't want to correlate with friends list when we've reset the block rules. mBlockRulesListReceived = false; mAutoAcceptRulesListReceived = false; } void LLVoiceClient::deleteAllBlockRules(void) { // Clear the block list entry flags from all local buddy list entries buddyListMap::iterator buddy_it; for(buddy_it = mBuddyListMap.begin(); buddy_it != mBuddyListMap.end(); buddy_it++) { buddy_it->second->mHasBlockListEntry = false; } } void LLVoiceClient::deleteAllAutoAcceptRules(void) { // Clear the auto-accept list entry flags from all local buddy list entries buddyListMap::iterator buddy_it; for(buddy_it = mBuddyListMap.begin(); buddy_it != mBuddyListMap.end(); buddy_it++) { buddy_it->second->mHasAutoAcceptListEntry = false; } } void LLVoiceClient::addBlockRule(const std::string &blockMask, const std::string &presenceOnly) { buddyListEntry *buddy = NULL; // blockMask is the SIP URI of a friends list entry buddyListMap::iterator iter = mBuddyListMap.find(&blockMask); if(iter != mBuddyListMap.end()) { LL_DEBUGS("Voice") << "block list entry for " << blockMask << LL_ENDL; buddy = iter->second; } if(buddy == NULL) { LL_DEBUGS("Voice") << "block list entry for unknown buddy " << blockMask << LL_ENDL; buddy = addBuddy(blockMask); } if(buddy != NULL) { buddy->mHasBlockListEntry = true; } } void LLVoiceClient::addAutoAcceptRule(const std::string &autoAcceptMask, const std::string &autoAddAsBuddy) { buddyListEntry *buddy = NULL; // blockMask is the SIP URI of a friends list entry buddyListMap::iterator iter = mBuddyListMap.find(&autoAcceptMask); if(iter != mBuddyListMap.end()) { LL_DEBUGS("Voice") << "auto-accept list entry for " << autoAcceptMask << LL_ENDL; buddy = iter->second; } if(buddy == NULL) { LL_DEBUGS("Voice") << "auto-accept list entry for unknown buddy " << autoAcceptMask << LL_ENDL; buddy = addBuddy(autoAcceptMask); } if(buddy != NULL) { buddy->mHasAutoAcceptListEntry = true; } } void LLVoiceClient::accountListBlockRulesResponse(int statusCode, const std::string &statusString) { // Block list entries were updated via addBlockRule() during parsing. Just flag that we're done. mBlockRulesListReceived = true; } void LLVoiceClient::accountListAutoAcceptRulesResponse(int statusCode, const std::string &statusString) { // Block list entries were updated via addBlockRule() during parsing. Just flag that we're done. mAutoAcceptRulesListReceived = true; } void LLVoiceClient::addObserver(LLVoiceClientParticipantObserver* observer) { mParticipantObservers.insert(observer); } void LLVoiceClient::removeObserver(LLVoiceClientParticipantObserver* observer) { mParticipantObservers.erase(observer); } void LLVoiceClient::notifyParticipantObservers() { for (observer_set_t::iterator it = mParticipantObservers.begin(); it != mParticipantObservers.end(); ) { LLVoiceClientParticipantObserver* observer = *it; observer->onChange(); // In case onChange() deleted an entry. it = mParticipantObservers.upper_bound(observer); } } void LLVoiceClient::addObserver(LLVoiceClientStatusObserver* observer) { mStatusObservers.insert(observer); } void LLVoiceClient::removeObserver(LLVoiceClientStatusObserver* observer) { mStatusObservers.erase(observer); } void LLVoiceClient::notifyStatusObservers(LLVoiceClientStatusObserver::EStatusType status) { if(mAudioSession) { if(status == LLVoiceClientStatusObserver::ERROR_UNKNOWN) { switch(mAudioSession->mErrorStatusCode) { case 20713: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_FULL; break; case 20714: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_LOCKED; break; case 20715: //invalid channel, we may be using a set of poorly cached //info status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; break; case 1009: //invalid username and password status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; break; } // Reset the error code to make sure it won't be reused later by accident. mAudioSession->mErrorStatusCode = 0; } else if(status == LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL) { switch(mAudioSession->mErrorStatusCode) { case 404: // NOT_FOUND case 480: // TEMPORARILY_UNAVAILABLE case 408: // REQUEST_TIMEOUT // call failed because other user was not available // treat this as an error case status = LLVoiceClientStatusObserver::ERROR_NOT_AVAILABLE; // Reset the error code to make sure it won't be reused later by accident. mAudioSession->mErrorStatusCode = 0; break; } } } LL_DEBUGS("Voice") << " " << LLVoiceClientStatusObserver::status2string(status) << ", session URI " << getAudioSessionURI() << (inSpatialChannel()?", proximal is true":", proximal is false") << LL_ENDL; for (status_observer_set_t::iterator it = mStatusObservers.begin(); it != mStatusObservers.end(); ) { LLVoiceClientStatusObserver* observer = *it; observer->onChange(status, getAudioSessionURI(), inSpatialChannel()); // In case onError() deleted an entry. it = mStatusObservers.upper_bound(observer); } } void LLVoiceClient::addObserver(LLFriendObserver* observer) { mFriendObservers.insert(observer); } void LLVoiceClient::removeObserver(LLFriendObserver* observer) { mFriendObservers.erase(observer); } void LLVoiceClient::notifyFriendObservers() { for (friend_observer_set_t::iterator it = mFriendObservers.begin(); it != mFriendObservers.end(); ) { LLFriendObserver* observer = *it; it++; // The only friend-related thing we notify on is online/offline transitions. observer->changed(LLFriendObserver::ONLINE); } } void LLVoiceClient::lookupName(const LLUUID &id) { gCacheName->get(id, FALSE, &LLVoiceClient::onAvatarNameLookup); } //static void LLVoiceClient::onAvatarNameLookup(const LLUUID& id, const std::string& first, const std::string& last, BOOL is_group) { if(gVoiceClient) { std::string name = llformat("%s %s", first.c_str(), last.c_str()); gVoiceClient->avatarNameResolved(id, name); } } void LLVoiceClient::avatarNameResolved(const LLUUID &id, const std::string &name) { // If the avatar whose name just resolved is on our friends list, resync the friends list. if(LLAvatarTracker::instance().getBuddyInfo(id) != NULL) { mFriendsListDirty = true; } // Iterate over all sessions. for(sessionIterator iter = sessionsBegin(); iter != sessionsEnd(); iter++) { sessionState *session = *iter; // Check for this user as a participant in this session participantState *participant = session->findParticipantByID(id); if(participant) { // Found -- fill in the name participant->mAccountName = name; // and post a "participants updated" message to listeners later. session->mParticipantsChanged = true; } // Check whether this is a p2p session whose caller name just resolved if(session->mCallerID == id) { // this session's "caller ID" just resolved. Fill in the name. session->mName = name; if(session->mTextInvitePending) { session->mTextInvitePending = false; // We don't need to call gIMMgr->addP2PSession() here. The first incoming message will create the panel. } if(session->mVoiceInvitePending) { session->mVoiceInvitePending = false; gIMMgr->inviteToSession( session->mIMSessionID, session->mName, session->mCallerID, session->mName, IM_SESSION_P2P_INVITE, LLIMMgr::INVITATION_TYPE_VOICE, session->mHandle, session->mSIPURI); } } } } class LLViewerParcelVoiceInfo : public LLHTTPNode { virtual void post( LLHTTPNode::ResponsePtr response, const LLSD& context, const LLSD& input) const { //the parcel you are in has changed something about its //voice information //this is a misnomer, as it can also be when you are not in //a parcel at all. Should really be something like //LLViewerVoiceInfoChanged..... if ( input.has("body") ) { LLSD body = input["body"]; //body has "region_name" (str), "parcel_local_id"(int), //"voice_credentials" (map). //body["voice_credentials"] has "channel_uri" (str), //body["voice_credentials"] has "channel_credentials" (str) //if we really wanted to be extra careful, //we'd check the supplied //local parcel id to make sure it's for the same parcel //we believe we're in if ( body.has("voice_credentials") ) { LLSD voice_credentials = body["voice_credentials"]; std::string uri; std::string credentials; if ( voice_credentials.has("channel_uri") ) { uri = voice_credentials["channel_uri"].asString(); } if ( voice_credentials.has("channel_credentials") ) { credentials = voice_credentials["channel_credentials"].asString(); } gVoiceClient->setSpatialChannel(uri, credentials); } } } }; class LLViewerRequiredVoiceVersion : public LLHTTPNode { static BOOL sAlertedUser; virtual void post( LLHTTPNode::ResponsePtr response, const LLSD& context, const LLSD& input) const { //You received this messsage (most likely on region cross or //teleport) if ( input.has("body") && input["body"].has("major_version") ) { int major_voice_version = input["body"]["major_version"].asInteger(); // int minor_voice_version = // input["body"]["minor_version"].asInteger(); if (gVoiceClient && (major_voice_version > VOICE_MAJOR_VERSION) ) { if (!sAlertedUser) { //sAlertedUser = TRUE; LLNotifications::instance().add("VoiceVersionMismatch"); gSavedSettings.setBOOL("EnableVoiceChat", FALSE); // toggles listener } } } } }; BOOL LLViewerRequiredVoiceVersion::sAlertedUser = FALSE; LLHTTPRegistration gHTTPRegistrationMessageParcelVoiceInfo( "/message/ParcelVoiceInfo"); LLHTTPRegistration gHTTPRegistrationMessageRequiredVoiceVersion( "/message/RequiredVoiceVersion");