Files
SingularityViewer/indra/newview/llvoiceclient.cpp
Aleric Inglewood 3f1fb9a66e Add improved timeout handling for HTTP transactions.
Introduces AIHTTPTimeoutPolicy objects which do not just
specify a single "timeout" in seconds, but a plethora of
timings related to the life cycle of the average HTTP
transaction.

This knowledge is that moved to the Responder being
used instead of floating constants hardcoded in the
callers of http requests. This assumes that the same
timeout policy is wanted for each transaction that
uses the same Responder, which can be enforced is needed.

I added a AIHTTPTimeoutPolicy for EVERY responder,
only to make it easier later to tune timeout values
and/or to get feedback about which responder runs
into HTTP errors in debug output (especially time outs),
so that they can be tuned later. If we already understood
exactly what we were doing then most responders could
have been left alone and just return the default timeout
policy: by far most timeout policies are just a copy
of the default policy, currently.

This commit is not finished... It's a work in progress
(viewer runs fine with it though).
2012-10-05 15:53:29 +02:00

7085 lines
188 KiB
C++

/**
* @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"
#if LL_LINUX && defined(LL_STANDALONE)
#include <glib.h>
#endif
#include "llvoiceclient.h"
#include <boost/tokenizer.hpp>
#include "llsdutil.h"
#include "llvoavatarself.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 "llnotificationsutil.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
class AIHTTPTimeoutPolicy;
extern AIHTTPTimeoutPolicy viewerVoiceAccountProvisionResponder_timeout;
extern AIHTTPTimeoutPolicy voiceClientCapResponder_timeout;
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);
}
}
virtual AIHTTPTimeoutPolicy const& getHTTPTimeoutPolicy(void) const { return viewerVoiceAccountProvisionResponder_timeout; }
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"))
{
/*
<Event type="SessionAddedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==0</SessionHandle>
<Uri>sip:confctl-1408789@bhr.vivox.com</Uri>
<IsChannel>true</IsChannel>
<Incoming>false</Incoming>
<ChannelName />
</Event>
*/
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"))
{
/*
<Event type="MediaStreamUpdatedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==0</SessionHandle>
<StatusCode>200</StatusCode>
<StatusString>OK</StatusString>
<State>2</State>
<Incoming>false</Incoming>
</Event>
*/
gVoiceClient->mediaStreamUpdatedEvent(sessionHandle, sessionGroupHandle, statusCode, statusString, state, incoming);
}
else if (!stricmp(eventTypeCstr, "TextStreamUpdatedEvent"))
{
/*
<Event type="TextStreamUpdatedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg1</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==1</SessionHandle>
<Enabled>true</Enabled>
<State>1</State>
<Incoming>true</Incoming>
</Event>
*/
gVoiceClient->textStreamUpdatedEvent(sessionHandle, sessionGroupHandle, enabled, state, incoming);
}
else if (!stricmp(eventTypeCstr, "ParticipantAddedEvent"))
{
/*
<Event type="ParticipantAddedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==4</SessionHandle>
<ParticipantUri>sip:xI5auBZ60SJWIk606-1JGRQ==@bhr.vivox.com</ParticipantUri>
<AccountName>xI5auBZ60SJWIk606-1JGRQ==</AccountName>
<DisplayName />
<ParticipantType>0</ParticipantType>
</Event>
*/
gVoiceClient->participantAddedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString, displayNameString, participantType);
}
else if (!stricmp(eventTypeCstr, "ParticipantRemovedEvent"))
{
/*
<Event type="ParticipantRemovedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg4</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==4</SessionHandle>
<ParticipantUri>sip:xtx7YNV-3SGiG7rA1fo5Ndw==@bhr.vivox.com</ParticipantUri>
<AccountName>xtx7YNV-3SGiG7rA1fo5Ndw==</AccountName>
</Event>
*/
gVoiceClient->participantRemovedEvent(sessionHandle, sessionGroupHandle, uriString, alias, nameString);
}
else if (!stricmp(eventTypeCstr, "ParticipantUpdatedEvent"))
{
/*
<Event type="ParticipantUpdatedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==0</SessionHandle>
<ParticipantUri>sip:xFnPP04IpREWNkuw1cOXlhw==@bhr.vivox.com</ParticipantUri>
<IsModeratorMuted>false</IsModeratorMuted>
<IsSpeaking>true</IsSpeaking>
<Volume>44</Volume>
<Energy>0.0879437</Energy>
</Event>
*/
// 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"))
{
/*
<Event type="BuddyChangedEvent">
<AccountHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==</AccountHandle>
<BuddyURI>sip:x9fFHFZjOTN6OESF1DUPrZQ==@bhr.vivox.com</BuddyURI>
<DisplayName>Monroe Tester</DisplayName>
<BuddyData />
<GroupID>0</GroupID>
<ChangeType>Set</ChangeType>
</Event>
*/
// 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"))
{
/*
<Event type="SessionUpdatedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0</SessionGroupHandle>
<SessionHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==0</SessionHandle>
<Uri>sip:confctl-9@bhd.vivox.com</Uri>
<IsMuted>0</IsMuted>
<Volume>50</Volume>
<TransmitEnabled>1</TransmitEnabled>
<IsFocused>0</IsFocused>
<SpeakerPosition><Position><X>0</X><Y>0</Y><Z>0</Z></Position></SpeakerPosition>
<SessionFontID>0</SessionFontID>
</Event>
*/
// We don't need to process this, but we also shouldn't warn on it, since that confuses people.
}
else if (!stricmp(eventTypeCstr, "SessionGroupRemovedEvent"))
{
/*
<Event type="SessionGroupRemovedEvent">
<SessionGroupHandle>c1_m1000xFnPP04IpREWNkuw1cOXlhw==_sg0</SessionGroupHandle>
</Event>
*/
// 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);
virtual AIHTTPTimeoutPolicy const& getHTTPTimeoutPolicy(void) const { return voiceClientCapResponder_timeout; }
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()
{
gVoiceClient = this;
mWriteInProgress = false;
mAreaVoiceDisabled = false;
mPTT = false;
mUserPTTState = false;
mMuteMic = false;
mSessionTerminateRequested = false;
mRelogRequested = false;
mConnected = false;
mCommandCookie = 0;
mCurrentParcelLocalID = 0;
mLoginRetryCount = 0;
mSpeakerVolume = 0;
mMicVolume = 0;
mNextAudioSession = NULL;
mAudioSession = NULL;
mAudioSessionChanged = false;
// Initial dirty state
mSpatialCoordsDirty = false;
mPTTDirty = true;
mFriendsListDirty = true;
mSpeakerVolumeDirty = true;
mMicVolumeDirty = true;
mBuddyListMapPopulated = false;
mBlockRulesListReceived = false;
mAutoAcceptRulesListReceived = false;
mCaptureDeviceDirty = false;
mRenderDeviceDirty = false;
// Use default values for everything then call updateSettings() after preferences are loaded
mVoiceEnabled = false;
mUsePTT = true;
mPTTIsToggle = false;
mEarLocation = 0;
mLipSyncEnabled = false;
mTuningMode = false;
mTuningEnergy = 0.0f;
mTuningMicVolume = 0;
mTuningMicVolumeDirty = true;
mTuningSpeakerVolume = 0;
mTuningSpeakerVolumeDirty = true;
// gMuteListp isn't set up at this point, so we defer this until later.
// gMuteListp->addObserver(&mutelist_listener);
// stash the pump for later use
// This now happens when init() is called instead.
mPump = NULL;
#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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.Create.1\">"
<< "<ClientName>V2 SDK</ClientName>"
<< "<AccountManagementServer>" << mVoiceAccountServerURI << "</AccountManagementServer>"
<< "<Mode>Normal</Mode>"
<< "<Logging>"
<< "<Folder>" << logpath << "</Folder>"
<< "<FileNamePrefix>Connector</FileNamePrefix>"
<< "<FileNameSuffix>.log</FileNameSuffix>"
<< "<LogLevel>" << loglevel << "</LogLevel>"
<< "</Logging>"
<< "<Application>SecondLifeViewer.1</Application>"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::connectorShutdown()
{
setState(stateConnectorStopping);
if(!mConnectorHandle.empty())
{
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.InitiateShutdown.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "</Request>"
<< "\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::post4(
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
if(mVoiceEnabled)
{
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_DEBUGS("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
#if LL_LINUX && defined(LL_STANDALONE)
// Look for the vivox daemon in the executable path list
// using glib first.
char *voice_path = g_find_program_in_path ("SLVoice");
std::string exe_path;
if (voice_path) {
exe_path = llformat("%s", voice_path);
free(voice_path);
} else {
exe_path = gDirUtilp->getExecutableDir() +
gDirUtilp->getDirDelimiter() + "SLVoice";
}
#else
// *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
#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<std::string> arglist;
arglist.push_back(exe_path);
// Split the argument string into separate strings for each argument
typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
boost::char_separator<char> 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<char*>(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(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_DEBUGS("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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetMicLevel.1\">"
<< "<Level>" << mTuningMicVolume << "</Level>"
<< "</Request>\n\n\n";
}
if(mTuningSpeakerVolumeDirty)
{
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetSpeakerLevel.1\">"
<< "<Level>" << mTuningSpeakerVolume << "</Level>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.Login.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "<AccountName>" << mAccountName << "</AccountName>"
<< "<AccountPassword>" << mAccountPassword << "</AccountPassword>"
<< "<AudioSessionAnswerMode>VerifyAnswer</AudioSessionAnswerMode>"
<< "<EnableBuddiesAndPresence>true</EnableBuddiesAndPresence>"
<< "<BuddyManagementMode>Application</BuddyManagementMode>"
<< "<ParticipantPropertyFrequency>5</ParticipantPropertyFrequency>"
<< (autoPostCrashDumps?"<AutopostCrashDumps>true</AutopostCrashDumps>":"")
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.Logout.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "</Request>"
<< "\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.ListBlockRules.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "</Request>"
<< "\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.ListAutoAcceptRules.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "</Request>"
<< "\n\n\n";
writeString(stream.str());
}
}
void LLVoiceClient::sessionGroupCreateSendMessage()
{
if(!mAccountHandle.empty())
{
std::ostringstream stream;
LL_DEBUGS("Voice") << "creating session group" << LL_ENDL;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.Create.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<Type>Normal</Type>"
<< "</Request>"
<< "\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
<< "<Request requestId=\"" << session->mSIPURI << "\" action=\"Session.Create.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<URI>" << session->mSIPURI << "</URI>";
static const std::string allowed_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
"0123456789"
"-._~";
if(!session->mHash.empty())
{
stream
<< "<Password>" << LLURI::escape(session->mHash, allowed_chars) << "</Password>"
<< "<PasswordHashAlgorithm>SHA1UserName</PasswordHashAlgorithm>";
}
stream
<< "<ConnectAudio>" << (startAudio?"true":"false") << "</ConnectAudio>"
<< "<ConnectText>" << (startText?"true":"false") << "</ConnectText>"
<< "<Name>" << mChannelName << "</Name>"
<< "</Request>\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
<< "<Request requestId=\"" << session->mSIPURI << "\" action=\"SessionGroup.AddSession.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "<URI>" << session->mSIPURI << "</URI>"
<< "<Name>" << mChannelName << "</Name>"
<< "<ConnectAudio>" << (startAudio?"true":"false") << "</ConnectAudio>"
<< "<ConnectText>" << (startText?"true":"false") << "</ConnectText>"
<< "<Password>" << password << "</Password>"
<< "<PasswordHashAlgorithm>SHA1UserName</PasswordHashAlgorithm>"
<< "</Request>\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
<< "<Request requestId=\"" << session->mHandle << "\" action=\"Session.MediaConnect.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "<Media>Audio</Media>"
<< "</Request>\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
<< "<Request requestId=\"" << session->mHandle << "\" action=\"Session.TextConnect.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.Terminate.1\">"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.Terminate.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.MediaDisconnect.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "<Media>Audio</Media>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.TextDisconnect.1\">"
<< "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::getCaptureDevicesSendMessage()
{
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.GetCaptureDevices.1\">"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::getRenderDevicesSendMessage()
{
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.GetRenderDevices.1\">"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.RenderAudioStart.1\">"
<< "<SoundFilePath>" << mTuningAudioFile << "</SoundFilePath>"
<< "<Loop>" << (loop?"1":"0") << "</Loop>"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::tuningRenderStopSendMessage()
{
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.RenderAudioStop.1\">"
<< "<SoundFilePath>" << mTuningAudioFile << "</SoundFilePath>"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::tuningCaptureStartSendMessage(int duration)
{
LL_DEBUGS("Voice") << "sending CaptureAudioStart" << LL_ENDL;
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.CaptureAudioStart.1\">"
<< "<Duration>" << duration << "</Duration>"
<< "</Request>\n\n\n";
writeString(stream.str());
}
void LLVoiceClient::tuningCaptureStopSendMessage()
{
LL_DEBUGS("Voice") << "sending CaptureAudioStop" << LL_ENDL;
std::ostringstream stream;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.CaptureAudioStop.1\">"
<< "</Request>\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:
/*
<< "<Position>"
<< "<X>" << pos[VX] << "</X>"
<< "<Y>" << pos[VZ] << "</Y>"
<< "<Z>" << pos[VY] << "</Z>"
<< "</Position>"
<< "<Velocity>"
<< "<X>" << mAvatarVelocity[VX] << "</X>"
<< "<Y>" << mAvatarVelocity[VZ] << "</Y>"
<< "<Z>" << mAvatarVelocity[VY] << "</Z>"
<< "</Velocity>"
<< "<AtOrientation>"
<< "<X>" << l.mV[VX] << "</X>"
<< "<Y>" << u.mV[VX] << "</Y>"
<< "<Z>" << a.mV[VX] << "</Z>"
<< "</AtOrientation>"
<< "<UpOrientation>"
<< "<X>" << l.mV[VZ] << "</X>"
<< "<Y>" << u.mV[VY] << "</Y>"
<< "<Z>" << a.mV[VZ] << "</Z>"
<< "</UpOrientation>"
<< "<LeftOrientation>"
<< "<X>" << l.mV [VY] << "</X>"
<< "<Y>" << u.mV [VZ] << "</Y>"
<< "<Z>" << a.mV [VY] << "</Z>"
<< "</LeftOrientation>";
*/
#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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.Set3DPosition.1\">"
<< "<SessionHandle>" << getAudioSessionHandle() << "</SessionHandle>";
stream << "<SpeakerPosition>";
// 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
<< "<Position>"
<< "<X>" << pos.mdV[VX] << "</X>"
<< "<Y>" << pos.mdV[VY] << "</Y>"
<< "<Z>" << pos.mdV[VZ] << "</Z>"
<< "</Position>"
<< "<Velocity>"
<< "<X>" << vel.mV[VX] << "</X>"
<< "<Y>" << vel.mV[VY] << "</Y>"
<< "<Z>" << vel.mV[VZ] << "</Z>"
<< "</Velocity>"
<< "<AtOrientation>"
<< "<X>" << a.mV[VX] << "</X>"
<< "<Y>" << a.mV[VY] << "</Y>"
<< "<Z>" << a.mV[VZ] << "</Z>"
<< "</AtOrientation>"
<< "<UpOrientation>"
<< "<X>" << u.mV[VX] << "</X>"
<< "<Y>" << u.mV[VY] << "</Y>"
<< "<Z>" << u.mV[VZ] << "</Z>"
<< "</UpOrientation>"
<< "<LeftOrientation>"
<< "<X>" << l.mV [VX] << "</X>"
<< "<Y>" << l.mV [VY] << "</Y>"
<< "<Z>" << l.mV [VZ] << "</Z>"
<< "</LeftOrientation>";
stream << "</SpeakerPosition>";
stream << "<ListenerPosition>";
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
<< "<Position>"
<< "<X>" << pos.mdV[VX] << "</X>"
<< "<Y>" << pos.mdV[VY] << "</Y>"
<< "<Z>" << pos.mdV[VZ] << "</Z>"
<< "</Position>"
<< "<Velocity>"
<< "<X>" << vel.mV[VX] << "</X>"
<< "<Y>" << vel.mV[VY] << "</Y>"
<< "<Z>" << vel.mV[VZ] << "</Z>"
<< "</Velocity>"
<< "<AtOrientation>"
<< "<X>" << a.mV[VX] << "</X>"
<< "<Y>" << a.mV[VY] << "</Y>"
<< "<Z>" << a.mV[VZ] << "</Z>"
<< "</AtOrientation>"
<< "<UpOrientation>"
<< "<X>" << u.mV[VX] << "</X>"
<< "<Y>" << u.mV[VY] << "</Y>"
<< "<Z>" << u.mV[VZ] << "</Z>"
<< "</UpOrientation>"
<< "<LeftOrientation>"
<< "<X>" << l.mV [VX] << "</X>"
<< "<Y>" << l.mV [VY] << "</Y>"
<< "<Z>" << l.mV [VZ] << "</Z>"
<< "</LeftOrientation>";
stream << "</ListenerPosition>";
stream << "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.SetParticipantVolumeForMe.1\">"
<< "<SessionHandle>" << getAudioSessionHandle() << "</SessionHandle>"
<< "<ParticipantURI>" << p->mURI << "</ParticipantURI>"
<< "<Volume>" << volume << "</Volume>"
<< "</Request>\n\n\n";
// Send a "mute for me" command for the user
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.SetParticipantMuteForMe.1\">"
<< "<SessionHandle>" << getAudioSessionHandle() << "</SessionHandle>"
<< "<ParticipantURI>" << p->mURI << "</ParticipantURI>"
<< "<Mute>" << (mute?"1":"0") << "</Mute>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetCaptureDevice.1\">"
<< "<CaptureDeviceSpecifier>" << mCaptureDevice << "</CaptureDeviceSpecifier>"
<< "</Request>"
<< "\n\n\n";
mCaptureDeviceDirty = false;
}
}
void LLVoiceClient::buildSetRenderDevice(std::ostringstream &stream)
{
if(mRenderDeviceDirty)
{
LL_DEBUGS("Voice") << "Setting output device = \"" << mRenderDevice << "\"" << LL_ENDL;
stream
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetRenderDevice.1\">"
<< "<RenderDeviceSpecifier>" << mRenderDevice << "</RenderDeviceSpecifier>"
<< "</Request>"
<< "\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.MuteLocalMic.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "<Value>" << (mPTT?"false":"true") << "</Value>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.MuteLocalSpeaker.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "<Value>" << muteval << "</Value>"
<< "</Request>\n\n\n";
}
if(mSpeakerVolumeDirty)
{
mSpeakerVolumeDirty = false;
LL_INFOS("Voice") << "Setting speaker volume to " << mSpeakerVolume << LL_ENDL;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.SetLocalSpeakerVolume.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "<Value>" << mSpeakerVolume << "</Value>"
<< "</Request>\n\n\n";
}
if(mMicVolumeDirty)
{
mMicVolumeDirty = false;
LL_INFOS("Voice") << "Setting mic volume to " << mMicVolume << LL_ENDL;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.SetLocalMicVolume.1\">"
<< "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
<< "<Value>" << mMicVolume << "</Value>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.BuddyDelete.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BuddyURI>" << buddy->mURI << "</BuddyURI>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteBlockRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BlockMask>" << buddy->mURI << "</BlockMask>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteAutoAcceptRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<AutoAcceptMask>" << buddy->mURI << "</AutoAcceptMask>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.BuddySet.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BuddyURI>" << buddy->mURI << "</BuddyURI>"
<< "<DisplayName>" << buddy->mDisplayName << "</DisplayName>"
<< "<BuddyData></BuddyData>" // Without this, SLVoice doesn't seem to parse the command.
<< "<GroupID>0</GroupID>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.BuddyDelete.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BuddyURI>" << buddy->mURI << "</BuddyURI>"
<< "</Request>\n\n\n";
}
if(buddy->mHasBlockListEntry)
{
// Delete the associated block list entry, if any
buddy->mHasBlockListEntry = false;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteBlockRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BlockMask>" << buddy->mURI << "</BlockMask>"
<< "</Request>\n\n\n";
}
if(buddy->mHasAutoAcceptListEntry)
{
// Delete the associated auto-accept list entry, if any
buddy->mHasAutoAcceptListEntry = false;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteAutoAcceptRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<AutoAcceptMask>" << buddy->mURI << "</AutoAcceptMask>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteBlockRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BlockMask>" << buddy->mURI << "</BlockMask>"
<< "</Request>\n\n\n";
// If we just deleted a block list entry, add an auto-accept entry.
if(!buddy->mHasAutoAcceptListEntry)
{
buddy->mHasAutoAcceptListEntry = true;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.CreateAutoAcceptRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<AutoAcceptMask>" << buddy->mURI << "</AutoAcceptMask>"
<< "<AutoAddAsBuddy>0</AutoAddAsBuddy>"
<< "</Request>\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 << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.DeleteAutoAcceptRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<AutoAcceptMask>" << buddy->mURI << "</AutoAcceptMask>"
<< "</Request>\n\n\n";
// If we just deleted an auto-accept entry, add a block list entry.
if(!buddy->mHasBlockListEntry)
{
buddy->mHasBlockListEntry = true;
stream << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.CreateBlockRule.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BlockMask>" << buddy->mURI << "</BlockMask>"
<< "<PresenceOnly>1</PresenceOnly>"
<< "</Request>\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 = "<body";
const std::string startMarker2 = ">";
const std::string endMarker = "</body>";
const std::string startSpan = "<span";
const std::string endSpan = "</span>";
std::string::size_type start;
std::string::size_type end;
// Default to displaying the raw string, so the message gets through.
rawMessage = messageBody;
// Find the actual message text within the XML fragment
start = messageBody.find(startMarker);
start = messageBody.find(startMarker2, start);
end = messageBody.find(endMarker);
if(start != std::string::npos)
{
start += startMarker2.size();
if(end != std::string::npos)
end -= start;
rawMessage.assign(messageBody, start, end);
}
else
{
// Didn't find a <body>, try looking for a <span> 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 &lt;, &gt;, and &amp;
mark = 0;
while((mark = rawMessage.find("&lt;", mark)) != std::string::npos)
{
rawMessage.replace(mark, 4, "<");
mark += 1;
}
mark = 0;
while((mark = rawMessage.find("&gt;", mark)) != std::string::npos)
{
rawMessage.replace(mark, 4, ">");
mark += 1;
}
mark = 0;
while((mark = rawMessage.find("&amp;", 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 &notificationType)
{
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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.SendSubscriptionReply.1\">"
<< "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
<< "<BuddyURI>" << buddy->mURI << "</BuddyURI>"
<< "<RuleType>" << (buddy->mCanSeeMeOnline?"Allow":"Hide") << "</RuleType>"
<< "<AutoAccept>"<< (buddy->mInSLFriends?"1":"0")<< "</AutoAccept>"
<< "<SubscriptionHandle>" << subscriptionHandle << "</SubscriptionHandle>"
<< "</Request>"
<< "\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::post4(
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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.SendMessage.1\">"
<< "<SessionHandle>" << session->mHandle << "</SessionHandle>"
<< "<MessageHeader>text/HTML</MessageHeader>"
<< "<MessageBody>" << message << "</MessageBody>"
<< "</Request>"
<< "\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 = gAgentAvatarp;
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;
}
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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.ControlRecording.1\">"
<< "<SessionGroupHandle>" << mMainSessionGroupHandle << "</SessionGroupHandle>"
<< "<RecordingControlType>Start</RecordingControlType>"
<< "<DeltaFramesPerControlFrame>" << deltaFramesPerControlFrame << "</DeltaFramesPerControlFrame>"
<< "<Filename>" << "" << "</Filename>"
<< "<EnableAudioRecordingEvents>false</EnableAudioRecordingEvents>"
<< "<LoopModeDurationSeconds>" << seconds << "</LoopModeDurationSeconds>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.ControlRecording.1\">"
<< "<SessionGroupHandle>" << mMainSessionGroupHandle << "</SessionGroupHandle>"
<< "<RecordingControlType>Flush</RecordingControlType>"
<< "<Filename>" << filename << "</Filename>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.ControlRecording.1\">"
<< "<SessionGroupHandle>" << mMainSessionGroupHandle << "</SessionGroupHandle>"
<< "<RecordingControlType>Stop</RecordingControlType>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.ControlPlayback.1\">"
<< "<SessionGroupHandle>" << mMainSessionGroupHandle << "</SessionGroupHandle>"
<< "<RecordingControlType>Start</RecordingControlType>"
<< "<Filename>" << filename << "</Filename>"
<< "</Request>\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
<< "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.ControlPlayback.1\">"
<< "<SessionGroupHandle>" << mMainSessionGroupHandle << "</SessionGroupHandle>"
<< "<RecordingControlType>Stop</RecordingControlType>"
<< "</Request>\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, boost::bind(&LLVoiceClient::onAvatarNameLookup,_1,_2));
}
//static
void LLVoiceClient::onAvatarNameLookup(const LLUUID& id, const std::string& full_name)
{
if(gVoiceClient)
{
gVoiceClient->avatarNameResolved(id, full_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;
LLNotificationsUtil::add("VoiceVersionMismatch");
gSavedSettings.setBOOL("EnableVoiceChat", FALSE); // toggles listener
}
}
}
}
};
BOOL LLViewerRequiredVoiceVersion::sAlertedUser = FALSE;
LLHTTPRegistration<LLViewerParcelVoiceInfo>
gHTTPRegistrationMessageParcelVoiceInfo(
"/message/ParcelVoiceInfo");
LLHTTPRegistration<LLViewerRequiredVoiceVersion>
gHTTPRegistrationMessageRequiredVoiceVersion(
"/message/RequiredVoiceVersion");