From 117e22047c5752352342d64e3fb7ce00a4eb8113 Mon Sep 17 00:00:00 2001 From: Jacek Antonelli Date: Fri, 15 Aug 2008 23:45:04 -0500 Subject: Second Life viewer sources 1.18.1.2 --- linden/indra/newview/llvoiceclient.cpp | 4076 ++++++++++++++++++++++++++++++++ 1 file changed, 4076 insertions(+) create mode 100644 linden/indra/newview/llvoiceclient.cpp (limited to 'linden/indra/newview/llvoiceclient.cpp') diff --git a/linden/indra/newview/llvoiceclient.cpp b/linden/indra/newview/llvoiceclient.cpp new file mode 100644 index 0000000..0bc42c6 --- /dev/null +++ b/linden/indra/newview/llvoiceclient.cpp @@ -0,0 +1,4076 @@ +/** + * @file llvoiceclient.cpp + * @brief Implementation of LLVoiceClient class which is the interface to the voice client process. + * + * Copyright (c) 2001-2007, 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://secondlife.com/developers/opensource/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://secondlife.com/developers/opensource/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. + */ + +#include + +#include "llviewerprecompiledheaders.h" +#include "llvoiceclient.h" + +#include "llsdutil.h" + +#include "llvoavatar.h" +#include "llbufferstream.h" +#include "llfile.h" +#include "expat/expat.h" +#include "llcallbacklist.h" +#include "llviewerregion.h" +#include "llviewernetwork.h" // for gUserServerChoice +#include "llfloateractivespeakers.h" // for LLSpeakerMgr +#include "llbase64.h" +#include "llviewercontrol.h" +#include "llkeyboard.h" +#include "viewer.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" + +// for base64 decoding +#include "apr-1/apr_base64.h" + +// for SHA1 hash +#include "apr-1/apr_sha1.h" + +// If we are connecting to agni AND the user's last name is "Linden", join this channel instead of looking up the sim name. +// If we are connecting to agni and the user's last name is NOT "Linden", disable voice. +#define AGNI_LINDENS_ONLY_CHANNEL "SL" +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; + +class LLViewerVoiceAccountProvisionResponder : + public LLHTTPClient::Responder +{ +public: + LLViewerVoiceAccountProvisionResponder(int retries) + { + mRetries = retries; + } + + virtual void error(U32 status, const std::string& reason) + { + if ( mRetries > 0 ) + { + if ( gVoiceClient ) gVoiceClient->requestVoiceAccountProvision( + mRetries - 1); + } + else + { + //TODO: throw an error message? + if ( gVoiceClient ) gVoiceClient->giveUp(); + } + } + + virtual void result(const LLSD& content) + { + if ( gVoiceClient ) + { + gVoiceClient->login( + content["username"].asString(), + content["password"].asString()); + } + } + +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(). + int returnCode; + int statusCode; + std::string statusString; + std::string uuidString; + std::string actionString; + std::string connectorHandle; + std::string accountHandle; + std::string sessionHandle; + std::string eventSessionHandle; + + // 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; + std::string nameString; + std::string audioMediaString; + std::string displayNameString; + int participantType; + bool isLocallyMuted; + bool isModeratorMuted; + bool isSpeaking; + int volume; + F32 energy; + + // 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; + textBuffer.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()); + } + + // MBW -- XXX -- This should no longer be necessary. Or even possible. + // We've read all the data out of the buffer. Make sure it doesn't accumulate. +// buffer->clear(); + + // 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) + { + // Turn this on to log incoming XML + if(0) + { + int foo = mInput.find("Set3DPosition", start); + int bar = mInput.find("ParticipantPropertiesEvent", start); + if(foo != std::string::npos && (foo < delim)) + { + // This is a Set3DPosition response. Don't print it, since these are way too spammy. + } + else if(bar != std::string::npos && (bar < delim)) + { + // This is a ParticipantPropertiesEvent response. Don't print it, since these are way too spammy. + } + else + { + llinfos << "parsing: " << mInput.substr(start, delim - start) << llendl; + } + } + + // 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); + + start = delim + 3; + } + + if(start != 0) + mInput = mInput.substr(start); + +// llinfos << "at end, mInput is: " << mInput << llendl; + + if(!gVoiceClient->mConnected) + { + // If voice has been disabled, we just want to close the socket. This does so. + llinfos << "returning STATUS_STOP" << llendl; + 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 = strcmp("Event", tag) == 0; + + if (strcmp("Response", tag) == 0 || isEvent) + { + // Grab the attributes + while (*attr) + { + const char *key = *attr++; + const char *value = *attr++; + + if (strcmp("requestId", key) == 0) + { + uuidString = value; + } + else if (strcmp("action", key) == 0) + { + actionString = value; + } + else if (strcmp("type", key) == 0) + { + eventTypeString = value; + } + } + } + //llinfos << tag << " (" << responseDepth << ")" << llendl; + } + else + { + if (ignoringTags) + { + //llinfos << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << llendl; + } + else + { + //llinfos << tag << " (" << responseDepth << ")" << llendl; + + // Ignore the InputXml stuff so we don't get confused + if (strcmp("InputXml", tag) == 0) + { + ignoringTags = true; + ignoreDepth = responseDepth; + accumulateText = false; + + //llinfos << "starting ignore, ignoreDepth is " << ignoreDepth << llendl; + } + else if (strcmp("CaptureDevices", tag) == 0) + { + gVoiceClient->clearCaptureDevices(); + } + else if (strcmp("RenderDevices", tag) == 0) + { + gVoiceClient->clearRenderDevices(); + } + } + } + responseDepth++; +} + +// -------------------------------------------------------------------------------- + +void LLVivoxProtocolParser::EndTag(const char *tag) +{ + const char *string = textBuffer.c_str(); + bool clearbuffer = true; + + responseDepth--; + + if (ignoringTags) + { + if (ignoreDepth == responseDepth) + { + //llinfos << "end of ignore" << llendl; + ignoringTags = false; + } + else + { + //llinfos << "ignoring tag " << tag << " (depth = " << responseDepth << ")" << llendl; + } + } + + if (!ignoringTags) + { + //llinfos << "processing tag " << tag << " (depth = " << responseDepth << ")" << llendl; + + // Closing a tag. Finalize the text we've accumulated and reset + if (strcmp("ReturnCode", tag) == 0) + returnCode = strtol(string, NULL, 10); + else if (strcmp("StatusCode", tag) == 0) + statusCode = strtol(string, NULL, 10); + else if (strcmp("ConnectorHandle", tag) == 0) + connectorHandle = string; + else if (strcmp("AccountHandle", tag) == 0) + accountHandle = string; + else if (strcmp("SessionHandle", tag) == 0) + { + if (isEvent) + eventSessionHandle = string; + else + sessionHandle = string; + } + else if (strcmp("StatusString", tag) == 0) + statusString = string; + else if (strcmp("State", tag) == 0) + state = strtol(string, NULL, 10); + else if (strcmp("URI", tag) == 0) + uriString = string; + else if (strcmp("IsChannel", tag) == 0) + isChannel = strcmp(string, "true") == 0; + else if (strcmp("Name", tag) == 0) + nameString = string; + else if (strcmp("AudioMedia", tag) == 0) + audioMediaString = string; + else if (strcmp("ChannelName", tag) == 0) + nameString = string; + else if (strcmp("ParticipantURI", tag) == 0) + uriString = string; + else if (strcmp("DisplayName", tag) == 0) + displayNameString = string; + else if (strcmp("AccountName", tag) == 0) + nameString = string; + else if (strcmp("ParticipantTyppe", tag) == 0) + participantType = strtol(string, NULL, 10); + else if (strcmp("IsLocallyMuted", tag) == 0) + isLocallyMuted = strcmp(string, "true") == 0; + else if (strcmp("IsModeratorMuted", tag) == 0) + isModeratorMuted = strcmp(string, "true") == 0; + else if (strcmp("IsSpeaking", tag) == 0) + isSpeaking = strcmp(string, "true") == 0; + else if (strcmp("Volume", tag) == 0) + volume = strtol(string, NULL, 10); + else if (strcmp("Energy", tag) == 0) + energy = (F32)strtod(string, NULL); + else if (strcmp("MicEnergy", tag) == 0) + energy = (F32)strtod(string, NULL); + else if (strcmp("ChannelName", tag) == 0) + nameString = string; + else if (strcmp("ChannelURI", tag) == 0) + uriString = string; + else if (strcmp("ChannelListResult", tag) == 0) + { + gVoiceClient->addChannelMapEntry(nameString, uriString); + } + else if (strcmp("Device", tag) == 0) + { + // This closing tag shouldn't clear the accumulated text. + clearbuffer = false; + } + else if (strcmp("CaptureDevice", tag) == 0) + { + gVoiceClient->addCaptureDevice(textBuffer); + } + else if (strcmp("RenderDevice", tag) == 0) + { + gVoiceClient->addRenderDevice(textBuffer); + } + + 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) +{ +// llinfos << tag << llendl; + + if (isEvent) + { + if (eventTypeString == "LoginStateChangeEvent") + { + gVoiceClient->loginStateChangeEvent(accountHandle, statusCode, statusString, state); + } + else if (eventTypeString == "SessionNewEvent") + { + gVoiceClient->sessionNewEvent(accountHandle, eventSessionHandle, state, nameString, uriString); + } + else if (eventTypeString == "SessionStateChangeEvent") + { + gVoiceClient->sessionStateChangeEvent(uriString, statusCode, statusString, eventSessionHandle, state, isChannel, nameString); + } + else if (eventTypeString == "ParticipantStateChangeEvent") + { + gVoiceClient->participantStateChangeEvent(uriString, statusCode, statusString, state, nameString, displayNameString, participantType); + + } + else if (eventTypeString == "ParticipantPropertiesEvent") + { + gVoiceClient->participantPropertiesEvent(uriString, statusCode, statusString, isLocallyMuted, isModeratorMuted, isSpeaking, volume, energy); + } + else if (eventTypeString == "AuxAudioPropertiesEvent") + { + gVoiceClient->auxAudioPropertiesEvent(energy); + } + } + else + { + if (actionString == "Connector.Create.1") + { + gVoiceClient->connectorCreateResponse(statusCode, statusString, connectorHandle); + } + else if (actionString == "Account.Login.1") + { + gVoiceClient->loginResponse(statusCode, statusString, accountHandle); + } + else if (actionString == "Session.Create.1") + { + gVoiceClient->sessionCreateResponse(statusCode, statusString, sessionHandle); + } + else if (actionString == "Session.Connect.1") + { + gVoiceClient->sessionConnectResponse(statusCode, statusString); + } + else if (actionString == "Session.Terminate.1") + { + gVoiceClient->sessionTerminateResponse(statusCode, statusString); + } + else if (actionString == "Account.Logout.1") + { + gVoiceClient->logoutResponse(statusCode, statusString); + } + else if (actionString == "Connector.InitiateShutdown.1") + { + gVoiceClient->connectorShutdownResponse(statusCode, statusString); + } + else if (actionString == "Account.ChannelGetList.1") + { + gVoiceClient->channelGetListResponse(statusCode, statusString); + } +/* + else if (actionString == "Connector.AccountCreate.1") + { + + } + else if (actionString == "Connector.MuteLocalMic.1") + { + + } + else if (actionString == "Connector.MuteLocalSpeaker.1") + { + + } + else if (actionString == "Connector.SetLocalMicVolume.1") + { + + } + else if (actionString == "Connector.SetLocalSpeakerVolume.1") + { + + } + else if (actionString == "Session.ListenerSetPosition.1") + { + + } + else if (actionString == "Session.SpeakerSetPosition.1") + { + + } + else if (actionString == "Session.Set3DPosition.1") + { + + } + else if (actionString == "Session.AudioSourceSetPosition.1") + { + + } + else if (actionString == "Session.GetChannelParticipants.1") + { + + } + else if (actionString == "Account.ChannelCreate.1") + { + + } + else if (actionString == "Account.ChannelUpdate.1") + { + + } + else if (actionString == "Account.ChannelDelete.1") + { + + } + else if (actionString == "Account.ChannelCreateAndInvite.1") + { + + } + else if (actionString == "Account.ChannelFolderCreate.1") + { + + } + else if (actionString == "Account.ChannelFolderUpdate.1") + { + + } + else if (actionString == "Account.ChannelFolderDelete.1") + { + + } + else if (actionString == "Account.ChannelAddModerator.1") + { + + } + else if (actionString == "Account.ChannelDeleteModerator.1") + { + + } +*/ + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +class LLVoiceClientPrefsListener: public LLSimpleListener +{ + bool handleEvent(LLPointer event, const LLSD& userdata) + { + // Note: Ignore the specific event value, look up the ones we want + + gVoiceClient->setVoiceEnabled(gSavedSettings.getBOOL("EnableVoiceChat")); + gVoiceClient->setUsePTT(gSavedSettings.getBOOL("PTTCurrentlyEnabled")); + std::string keyString = gSavedSettings.getString("PushToTalkButton"); + gVoiceClient->setPTTKey(keyString); + gVoiceClient->setPTTIsToggle(gSavedSettings.getBOOL("PushToTalkToggle")); + gVoiceClient->setEarLocation(gSavedSettings.getS32("VoiceEarLocation")); + std::string serverName = gSavedSettings.getString("VivoxDebugServerName"); + gVoiceClient->setVivoxDebugServerName(serverName); + + std::string inputDevice = gSavedSettings.getString("VoiceInputAudioDevice"); + gVoiceClient->setCaptureDevice(inputDevice); + std::string outputDevice = gSavedSettings.getString("VoiceOutputAudioDevice"); + gVoiceClient->setRenderDevice(outputDevice); + + return true; + } +}; +static LLVoiceClientPrefsListener voice_prefs_listener; + +class LLVoiceClientMuteListObserver : public LLMuteListObserver +{ + /* virtual */ void onChange() { gVoiceClient->muteListChanged();} +}; +static LLVoiceClientMuteListObserver mutelist_listener; +static bool sMuteListListener_listening = false; + +/////////////////////////////////////////////////////////////////////////////////////////////// + +class LLVoiceClientCapResponder : public LLHTTPClient::Responder +{ +public: + LLVoiceClientCapResponder(void){}; + + virtual void error(U32 status, const std::string& reason); // called with bad status codes + virtual void result(const LLSD& content); + +private: +}; + +void LLVoiceClientCapResponder::error(U32 status, const std::string& reason) +{ + llwarns << "LLVoiceClientCapResponder::error(" + << status << ": " << reason << ")" + << llendl; +} + +void LLVoiceClientCapResponder::result(const LLSD& content) +{ + LLSD::map_const_iterator iter; + for(iter = content.beginMap(); iter != content.endMap(); ++iter) + { + llinfos << "LLVoiceClientCapResponder::result got " + << iter->first << llendl; + } + + 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 = true; + mUserPTTState = false; + mMuteMic = false; + mSessionTerminateRequested = false; + mCommandCookie = 0; + mNonSpatialChannel = false; + mNextSessionSpatial = true; + mNextSessionNoReconnect = false; + mSessionP2P = false; + mCurrentParcelLocalID = 0; + mLoginRetryCount = 0; + mVivoxErrorStatusCode = 0; + + mNextSessionResetOnClose = false; + mSessionResetOnClose = false; + mSpeakerVolume = 0; + mMicVolume = 0; + + // Initial dirty state + mSpatialCoordsDirty = false; + mPTTDirty = true; + mVolumeDirty = true; + mSpeakerVolumeDirty = true; + mMicVolumeDirty = true; + mCaptureDeviceDirty = false; + mRenderDeviceDirty = false; + + // Load initial state from prefs. + mVoiceEnabled = gSavedSettings.getBOOL("EnableVoiceChat"); + mUsePTT = gSavedSettings.getBOOL("EnablePushToTalk"); + std::string keyString = gSavedSettings.getString("PushToTalkButton"); + setPTTKey(keyString); + mPTTIsToggle = gSavedSettings.getBOOL("PushToTalkToggle"); + mEarLocation = gSavedSettings.getS32("VoiceEarLocation"); + setVoiceVolume(gSavedSettings.getF32("AudioLevelVoice")); + std::string captureDevice = gSavedSettings.getString("VoiceInputAudioDevice"); + setCaptureDevice(captureDevice); + std::string renderDevice = gSavedSettings.getString("VoiceOutputAudioDevice"); + setRenderDevice(renderDevice); + + // Set up our listener to get updates on all prefs values we care about. + gSavedSettings.getControl("EnableVoiceChat")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("PTTCurrentlyEnabled")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("PushToTalkButton")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("PushToTalkToggle")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("VoiceEarLocation")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("VivoxDebugServerName")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("VoiceInputAudioDevice")->addListener(&voice_prefs_listener); + gSavedSettings.getControl("VoiceOutputAudioDevice")->addListener(&voice_prefs_listener); + + mTuningMode = false; + mTuningEnergy = 0.0f; + mTuningMicVolume = 0; + mTuningMicVolumeDirty = true; + mTuningSpeakerVolume = 0; + mTuningSpeakerVolumeDirty = true; + mTuningCaptureRunning = false; + + // gMuteListp isn't set up at this point, so we defer this until later. +// gMuteListp->addObserver(&mutelist_listener); + + mParticipantMapChanged = false; + + // stash the pump for later use + // This now happens when init() is called instead. + mPump = NULL; + +#if LL_DARWIN || LL_LINUX + // MBW -- XXX -- 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; +} + +void LLVoiceClient::terminate() +{ + if(gVoiceClient) + { + gVoiceClient->sessionTerminateSendMessage(); + gVoiceClient->logout(); + 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; + } +} + + +///////////////////////////// +// 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; + +// llinfos << "sending: " << str << llendl; + + // MBW -- XXX -- 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; + } + // MBW -- XXX -- 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]; + llwarns << "apr error " << err << " ("<< apr_strerror(err, buf, MAX_STRING) << ") sending data to vivox daemon." << llendl; + daemonDied(); + } + } + + return result; +} + + +///////////////////////////// +// session control messages +void LLVoiceClient::connectorCreate() +{ + std::ostringstream stream; + std::string logpath; + std::string loglevel = "0"; + + // Transition to stateConnectorStarted when the connector handle comes back. + setState(stateConnectorStarting); + + std::string savedLogLevel = gSavedSettings.getString("VivoxDebugLevel"); + + if(savedLogLevel != "-1") + { + llinfos << "creating connector with logging enabled" << llendl; + loglevel = "10"; + logpath = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ""); + } + + stream + << "" + << "V2 SDK" + << "" << mAccountServerURI << "" + << "" + << "false" + << "" << logpath << "" + << "Connector" + << ".log" + << "" << loglevel << "" + << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::connectorShutdown() +{ + setState(stateConnectorStopping); + + if(!mConnectorHandle.empty()) + { + std::ostringstream stream; + stream + << "" + << "" << mConnectorHandle << "" + << "" + << "\n\n\n"; + + mConnectorHandle.clear(); + + writeString(stream.str()); + } +} + +void LLVoiceClient::userAuthorized(const std::string& firstName, const std::string& lastName, const LLUUID &agentID) +{ + mAccountFirstName = firstName; + mAccountLastName = lastName; + + mAccountDisplayName = firstName; + mAccountDisplayName += " "; + mAccountDisplayName += lastName; + + llinfos << "name \"" << mAccountDisplayName << "\" , ID " << agentID << llendl; + + std::string userserver = gUserServerName; + LLString::toLower(userserver); + if((gUserServerChoice == USERSERVER_AGNI) || + ((gUserServerChoice == USERSERVER_OTHER) && (userserver.find("agni") != std::string::npos))) + { + sConnectingToAgni = true; + } + + // MBW -- XXX -- Enable this when the bhd.vivox.com server gets a real ssl cert. + if(sConnectingToAgni) + { + // Use the release account server + mAccountServerName = "bhr.vivox.com"; + mAccountServerURI = "https://www." + mAccountServerName + "/api2/"; + } + else + { + // Use the development account server + mAccountServerName = gSavedSettings.getString("VivoxDebugServerName"); + mAccountServerURI = "https://www." + mAccountServerName + "/api2/"; + } + + mAccountName = nameFromID(agentID); +} + +void LLVoiceClient::requestVoiceAccountProvision(S32 retries) +{ + if ( gAgent.getRegion() && mVoiceEnabled ) + { + std::string url = + gAgent.getRegion()->getCapability( + "ProvisionVoiceAccountRequest"); + + if ( url == "" ) return; + + LLHTTPClient::post( + url, + LLSD(), + new LLViewerVoiceAccountProvisionResponder(retries)); + } +} + +void LLVoiceClient::login( + const std::string& accountName, + const std::string &password) +{ + if((getState() >= stateLoggingIn) && (getState() < stateLoggedOut)) + { + // Already logged in. This is an internal error. + llerrs << "called from wrong state." << llendl; + } + else if ( accountName != mAccountName ) + { + //TODO: error? + llinfos << "Wrong account name! " << accountName + << " instead of " << mAccountName << llendl; + } + else + { + mAccountPassword = password; + } +} + +void LLVoiceClient::idle(void* user_data) +{ + LLVoiceClient* self = (LLVoiceClient*)user_data; + self->stateMachine(); +} + +const char *LLVoiceClient::state2string(LLVoiceClient::state inState) +{ + const char *result = "UNKNOWN"; + + // Prevent copy-paste errors when updating this list... +#define CASE(x) case x: result = #x; break + + switch(inState) + { + CASE(stateDisabled); + CASE(stateStart); + CASE(stateDaemonLaunched); + CASE(stateConnecting); + CASE(stateIdle); + CASE(stateConnectorStart); + CASE(stateConnectorStarting); + CASE(stateConnectorStarted); + CASE(stateMicTuningNoLogin); + CASE(stateLoginRetry); + CASE(stateLoginRetryWait); + CASE(stateNeedsLogin); + CASE(stateLoggingIn); + CASE(stateLoggedIn); + CASE(stateNoChannel); + CASE(stateMicTuningLoggedIn); + CASE(stateSessionCreate); + CASE(stateSessionConnect); + 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; +} + +const char *LLVoiceClientStatusObserver::status2string(LLVoiceClientStatusObserver::EStatusType inStatus) +{ + const char *result = "UNKNOWN"; + + // Prevent copy-paste errors when updating this list... +#define CASE(x) case x: result = #x; break + + switch(inStatus) + { + CASE(STATUS_JOINING); + CASE(STATUS_JOINED); + CASE(STATUS_LEFT_CHANNEL); + CASE(ERROR_CHANNEL_FULL); + CASE(ERROR_CHANNEL_LOCKED); + CASE(ERROR_UNKNOWN); + default: + break; + } + +#undef CASE + + return result; +} + +void LLVoiceClient::setState(state inState) +{ + llinfos << "entering state " << state2string(inState) << llendl; + + mState = inState; +} + +void LLVoiceClient::stateMachine() +{ + if(gDisconnected) + { + // The viewer has been disconnected from the sim. Disable voice. + setVoiceEnabled(false); + } + + if(!mVoiceEnabled) + { + if(getState() != stateDisabled) + { + // 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. + llinfos << "Disabling voice before connection to daemon, terminating." << llendl; + killGateway(); + } + + sessionTerminateSendMessage(); + logout(); + connectorShutdown(); + closeSocket(); + removeAllParticipants(); + + setState(stateDisabled); + } + } + + // Check for parcel boundary crossing + { + LLViewerRegion *region = gAgent.getRegion(); + LLParcel *parcel = NULL; + + if(gParcelMgr) + { + parcel = gParcelMgr->getAgentParcel(); + } + + if(region && parcel) + { + S32 parcelLocalID = parcel->getLocalID(); + std::string regionName = region->getName(); + std::string capURI = region->getCapability("ParcelVoiceInfoRequest"); + +// llinfos << "Region name = \"" << regionName <<"\", " << "parcel local ID = " << parcelLocalID << llendl; + + // 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() && !capURI.empty()) + { + if((parcelLocalID != mCurrentParcelLocalID) || (regionName != mCurrentRegionName)) + { + // We have changed parcels. Initiate a parcel channel lookup. + mCurrentParcelLocalID = parcelLocalID; + mCurrentRegionName = regionName; + + parcelChanged(); + } + } + } + } + + switch(getState()) + { + case stateDisabled: + if(mVoiceEnabled && (!mAccountName.empty() || mTuningMode)) + { + setState(stateStart); + } + break; + + case stateStart: + if(gDisableVoice) + { + // Voice is locked out, we must not launch the vivox daemon. + setState(stateJail); + } + else if(!isGatewayRunning()) + { + if(true) + { + // Launch the voice daemon + std::string exe_path = gDirUtilp->getAppRODataDir(); + exe_path += gDirUtilp->getDirDelimiter(); +#if LL_WINDOWS + exe_path += "SLVoice.exe"; +#else + // This will be the same for mac and linux + exe_path += "SLVoice"; +#endif + // See if the vivox executable exists + llstat s; + if(!LLFile::stat(exe_path.c_str(), &s)) + { + // vivox executable exists. Build the command line and launch the daemon. + std::string args = " -p tcp -h -c"; + std::string cmd; + std::string loglevel = gSavedSettings.getString("VivoxDebugLevel"); + + if(loglevel.empty()) + { + loglevel = "-1"; // turn logging off completely + } + + args += " -ll "; + args += loglevel; + +// llinfos << "Args for SLVoice: " << args << llendl; + +#if LL_WINDOWS + PROCESS_INFORMATION pinfo; + STARTUPINFOA sinfo; + memset(&sinfo, 0, sizeof(sinfo)); + std::string exe_dir = gDirUtilp->getAppRODataDir(); + cmd = "SLVoice.exe"; + cmd += args; + + // So retarded. Windows requires that the second parameter to CreateProcessA be a writable (non-const) string... + char *args2 = new char[args.size() + 1]; + strcpy(args2, args.c_str()); + + if(!CreateProcessA(exe_path.c_str(), args2, NULL, NULL, FALSE, 0, NULL, exe_dir.c_str(), &sinfo, &pinfo)) + { +// DWORD dwErr = GetLastError(); + } + else + { + // foo = pinfo.dwProcessId; // get your pid here if you want to use it later on + // CloseHandle(pinfo.hProcess); // stops leaks - nothing else + sGatewayHandle = pinfo.hProcess; + CloseHandle(pinfo.hThread); // stops leaks - nothing else + } + + delete args2; +#else // LL_WINDOWS + // This should be the same for mac and linux + { + std::vector arglist; + arglist.push_back(exe_path.c_str()); + + // Split the argument string into separate strings for each argument + typedef boost::tokenizer > tokenizer; + boost::char_separator sep(" "); + tokenizer tokens(args, sep); + tokenizer::iterator token_iter; + + for(token_iter = tokens.begin(); token_iter != tokens.end(); ++token_iter) + { + arglist.push_back(*token_iter); + } + + // create an argv vector for the child process + char **fakeargv = new char*[arglist.size() + 1]; + int i; + for(i=0; i < arglist.size(); i++) + fakeargv[i] = const_cast(arglist[i].c_str()); + + fakeargv[i] = NULL; + + 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("127.0.0.1", 44124); + } + else + { + llinfos << exe_path << "not found." << llendl + } + } + else + { + // 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:44124 + // and put that host's IP address here. + mDaemonHost = LLHost("127.0.0.1", 44124); + } + + 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; + mSpeakerVolumeDirty = true; + // These only need to be set if they're not default (i.e. empty string). + mCaptureDeviceDirty = !mCaptureDevice.empty(); + mRenderDeviceDirty = !mRenderDevice.empty(); + } + break; + + case stateDaemonLaunched: +// llinfos << "Connecting to vivox daemon" << llendl; + if(mUpdateTimer.hasExpired()) + { + mUpdateTimer.setTimerExpirySec(CONNECT_THROTTLE_SECONDS); + + if(!mSocket) + { + mSocket = LLSocket::create(gAPRPoolp, LLSocket::STREAM_TCP); + } + + mConnected = mSocket->blockingConnect(mDaemonHost); + if(mConnected) + { + setState(stateConnecting); + } + else + { + // If the connect failed, the socket may have been put into a bad state. Delete it. + closeSocket(); + } + } + break; + + 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(stateIdle); + } + + break; + + case stateIdle: + // Initial devices query + getCaptureDevicesSendMessage(); + getRenderDevicesSendMessage(); + + mLoginRetryCount = 0; + + setState(stateConnectorStart); + + break; + + case stateConnectorStart: + if(!mVoiceEnabled) + { + // We were never logged in. This will shut down the connector. + setState(stateLoggedOut); + } + else if(!mAccountServerURI.empty()) + { + connectorCreate(); + } + else if(mTuningMode) + { + setState(stateMicTuningNoLogin); + } + break; + + case stateConnectorStarting: // waiting for connector handle + // connectorCreateResponse() will transition from here to stateConnectorStarted. + break; + + case stateConnectorStarted: // connector handle received + if(!mVoiceEnabled) + { + // We were never logged in. This will shut down the connector. + setState(stateLoggedOut); + } + else if(!mAccountName.empty()) + { + LLViewerRegion *region = gAgent.getRegion(); + + if(region) + { + if ( region->getCapability("ProvisionVoiceAccountRequest") != "" ) + { + if ( mAccountPassword.empty() ) + { + requestVoiceAccountProvision(); + } + setState(stateNeedsLogin); + } + } + } + break; + + case stateMicTuningNoLogin: + case stateMicTuningLoggedIn: + { + // Both of these behave essentially the same. The only difference is where the exit transition goes to. + if(mTuningMode && mVoiceEnabled && !mSessionTerminateRequested) + { + if(!mTuningCaptureRunning) + { + // duration parameter is currently unused, per Mike S. + tuningCaptureStartSendMessage(10000); + } + + if(mTuningMicVolumeDirty || mTuningSpeakerVolumeDirty || mCaptureDeviceDirty || mRenderDeviceDirty) + { + std::ostringstream stream; + + if(mTuningMicVolumeDirty) + { + stream + << "" + << "" << mTuningMicVolume << "" + << "\n\n\n"; + } + + if(mTuningSpeakerVolumeDirty) + { + stream + << "" + << "" << mTuningSpeakerVolume << "" + << "\n\n\n"; + } + + if(mCaptureDeviceDirty) + { + buildSetCaptureDevice(stream); + } + + if(mRenderDeviceDirty) + { + buildSetRenderDevice(stream); + } + + mTuningMicVolumeDirty = false; + mTuningSpeakerVolumeDirty = false; + mCaptureDeviceDirty = false; + mRenderDeviceDirty = false; + + if(!stream.str().empty()) + { + writeString(stream.str()); + } + } + } + else + { + // transition out of mic tuning + if(mTuningCaptureRunning) + { + tuningCaptureStopSendMessage(); + } + + if(getState() == stateMicTuningNoLogin) + { + setState(stateConnectorStart); + } + else + { + setState(stateNoChannel); + } + } + } + break; + + case stateLoginRetry: + if(mLoginRetryCount == 0) + { + // First retry -- display a message to the user + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGIN_RETRY); + } + + mLoginRetryCount++; + + if(mLoginRetryCount > MAX_LOGIN_RETRIES) + { + llinfos << "too many login retries, giving up." << llendl; + setState(stateLoginFailed); + } + else + { + llinfos << "will retry login in " << LOGIN_RETRY_SECONDS << " seconds." << llendl; + mUpdateTimer.start(); + mUpdateTimer.setTimerExpirySec(LOGIN_RETRY_SECONDS); + setState(stateLoginRetryWait); + } + break; + + case stateLoginRetryWait: + if(mUpdateTimer.hasExpired()) + { + setState(stateNeedsLogin); + } + break; + + case stateNeedsLogin: + if(!mAccountPassword.empty()) + { + setState(stateLoggingIn); + loginSendMessage(); + } + break; + + case stateLoggingIn: // waiting for account handle + // loginResponse() will transition from here to stateLoggedIn. + break; + + case stateLoggedIn: // account handle received + // Initial kick-off of channel lookup logic + parcelChanged(); + + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGGED_IN); + + // Set up the mute list observer if it hasn't been set up already. + if((!sMuteListListener_listening) && (gMuteListp)) + { + gMuteListp->addObserver(&mutelist_listener); + sMuteListListener_listening = true; + } + + setState(stateNoChannel); + break; + + case stateNoChannel: + if(mSessionTerminateRequested || !mVoiceEnabled) + { + // MBW -- XXX -- Is this the right way out of this state? + setState(stateSessionTerminated); + } + else if(mTuningMode) + { + setState(stateMicTuningLoggedIn); + } + else if(!mNextSessionHandle.empty()) + { + setState(stateSessionConnect); + } + else if(!mNextSessionURI.empty()) + { + setState(stateSessionCreate); + } + break; + + case stateSessionCreate: + sessionCreateSendMessage(); + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINING); + setState(stateJoiningSession); + break; + + case stateSessionConnect: + sessionConnectSendMessage(); + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINING); + setState(stateJoiningSession); + break; + + case stateJoiningSession: // waiting for session handle + // sessionCreateResponse() will transition from here to stateSessionJoined. + if(!mVoiceEnabled) + { + // User bailed out during connect -- jump straight to teardown. + setState(stateSessionTerminated); + } + else if(mSessionTerminateRequested) + { + if(!mSessionHandle.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(mSessionP2P) + { + sessionTerminateSendMessage(); + setState(stateSessionTerminated); + } + } + } + break; + + case stateSessionJoined: // session handle received + // MBW -- XXX -- It appears that I need to wait for BOTH the Session.Create 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 Session.Create 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(!mSessionHandle.empty()) + { + // Events that need to happen when a session is joined could go here. + // Maybe send initial spatial data? + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINED); + + // 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); + } + 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(mSessionP2P) + { + sessionTerminateSendMessage(); + setState(stateSessionTerminated); + } + } + break; + + case stateRunning: // steady state + // sessionTerminateSendMessage() will transition from here to stateLeavingSession + + // Disabling voice or disconnect requested. + if(!mVoiceEnabled || mSessionTerminateRequested) + { + sessionTerminateSendMessage(); + } + 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(mNonSpatialChannel) + { + // 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(mVolumeDirty || mPTTDirty || mSpeakerVolumeDirty || mUpdateTimer.hasExpired()) + { + mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS); + sendPositionalUpdate(); + } + } + break; + + case stateLeavingSession: // waiting for terminate session response + // The handler for the Session.Terminate response will transition from here to stateSessionTerminated. + break; + + case stateSessionTerminated: + // Always reset the terminate request flag when we get here. + mSessionTerminateRequested = false; + + notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL); + + if(mVoiceEnabled) + { + // SPECIAL CASE: if going back to spatial but in a parcel with an empty URI, transfer the non-spatial flag now. + // This fixes the case where you come out of a group chat in a parcel with voice disabled, and get stuck unable to rejoin spatial chat thereafter. + if(mNextSessionSpatial && mNextSessionURI.empty()) + { + mNonSpatialChannel = !mNextSessionSpatial; + } + + // 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(); + } + + break; + + case stateLoggingOut: // waiting for logout response + // The handler for the Account.Logout response will transition from here to stateLoggedOut. + break; + case stateLoggedOut: // logout response received + // shut down the connector + connectorShutdown(); + break; + + case stateConnectorStopping: // waiting for connector stop + // The handler for the Connector.InitiateShutdown response will transition from here to stateConnectorStopped. + break; + + case stateConnectorStopped: // connector stop received + // Clean up and reset everything. + closeSocket(); + removeAllParticipants(); + setState(stateDisabled); + break; + + case stateConnectorFailed: + setState(stateConnectorFailedWaiting); + break; + case stateConnectorFailedWaiting: + break; + + case stateLoginFailed: + setState(stateLoginFailedWaiting); + break; + case stateLoginFailedWaiting: + // No way to recover from these. Yet. + break; + + case stateJoinSessionFailed: + // Transition to error state. Send out any notifications here. + llwarns << "stateJoinSessionFailed: (" << mVivoxErrorStatusCode << "): " << mVivoxErrorStatusString << llendl; + notifyStatusObservers(LLVoiceClientStatusObserver::ERROR_UNKNOWN); + setState(stateJoinSessionFailedWaiting); + break; + + 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; + + case stateJail: + // We have given up. Do nothing. + break; + } + + if(mParticipantMapChanged) + { + mParticipantMapChanged = false; + notifyObservers(); + } + +} + +void LLVoiceClient::closeSocket(void) +{ + mSocket.reset(); + mConnected = false; +} + +void LLVoiceClient::loginSendMessage() +{ + std::ostringstream stream; + stream + << "" + << "" << mConnectorHandle << "" + << "" << mAccountName << "" + << "" << mAccountPassword << "" + << "VerifyAnswer" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::logout() +{ + mAccountPassword = ""; + setState(stateLoggingOut); + logoutSendMessage(); +} + +void LLVoiceClient::logoutSendMessage() +{ + if(!mAccountHandle.empty()) + { + std::ostringstream stream; + stream + << "" + << "" << mAccountHandle << "" + << "" + << "\n\n\n"; + + mAccountHandle.clear(); + + writeString(stream.str()); + } +} + +void LLVoiceClient::channelGetListSendMessage() +{ + std::ostringstream stream; + stream + << "" + << "" << mAccountHandle << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::sessionCreateSendMessage() +{ + llinfos << "requesting join: " << mNextSessionURI << llendl; + + mSessionURI = mNextSessionURI; + mNonSpatialChannel = !mNextSessionSpatial; + mSessionResetOnClose = mNextSessionResetOnClose; + mNextSessionResetOnClose = false; + if(mNextSessionNoReconnect) + { + // Clear the stashed URI so it can't reconnect + mNextSessionURI.clear(); + } + // Only p2p sessions are created with "no reconnect". + mSessionP2P = mNextSessionNoReconnect; + + std::ostringstream stream; + stream + << "" + << "" << mAccountHandle << "" + << "" << mSessionURI << ""; + + static const std::string allowed_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~"; + + if(!mNextSessionHash.empty()) + { + stream + << "" << LLURI::escape(mNextSessionHash, allowed_chars) << "" + << "SHA1UserName"; + } + + stream + << "" << mChannelName << "" + << "\n\n\n"; + writeString(stream.str()); +} + +void LLVoiceClient::sessionConnectSendMessage() +{ + llinfos << "connecting to session handle: " << mNextSessionHandle << llendl; + + mSessionHandle = mNextSessionHandle; + mSessionURI = mNextP2PSessionURI; + mNextSessionHandle.clear(); // never want to re-use these. + mNextP2PSessionURI.clear(); + mNonSpatialChannel = !mNextSessionSpatial; + mSessionResetOnClose = mNextSessionResetOnClose; + mNextSessionResetOnClose = false; + // Joining by session ID is only used to answer p2p invitations, so we know this is a p2p session. + mSessionP2P = true; + + std::ostringstream stream; + + stream + << "" + << "" << mSessionHandle << "" + << "default" + << "\n\n\n"; + writeString(stream.str()); +} + +void LLVoiceClient::sessionTerminate() +{ + mSessionTerminateRequested = true; +} + +void LLVoiceClient::sessionTerminateSendMessage() +{ + llinfos << "leaving session: " << mSessionURI << llendl; + + 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(!mSessionHandle.empty()) + { + sessionTerminateByHandle(mSessionHandle); + setState(stateLeavingSession); + } + else + { + llwarns << "called with no session handle" << llendl; + setState(stateSessionTerminated); + } + break; + case stateJoinSessionFailed: + case stateJoinSessionFailedWaiting: + setState(stateSessionTerminated); + break; + + default: + llwarns << "called from unknown state" << llendl; + break; + } +} + +void LLVoiceClient::sessionTerminateByHandle(std::string &sessionHandle) +{ + llinfos << "Sending Session.Terminate with handle " << sessionHandle << llendl; + + std::ostringstream stream; + stream + << "" + << "" << sessionHandle << "" + << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::getCaptureDevicesSendMessage() +{ + std::ostringstream stream; + stream + << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::getRenderDevicesSendMessage() +{ + std::ostringstream stream; + stream + << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::clearCaptureDevices() +{ + // MBW -- XXX -- do something here + llinfos << "called" << llendl; + mCaptureDevices.clear(); +} + +void LLVoiceClient::addCaptureDevice(const std::string& name) +{ + // MBW -- XXX -- do something here + llinfos << name << llendl; + + 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() +{ + // MBW -- XXX -- do something here + llinfos << "called" << llendl; + mRenderDevices.clear(); +} + +void LLVoiceClient::addRenderDevice(const std::string& name) +{ + // MBW -- XXX -- do something here + llinfos << name << llendl; + 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 stateMicTuningNoLogin: + case stateMicTuningLoggedIn: + result = true; + default: + break; + } + return result; +} + +void LLVoiceClient::tuningRenderStartSendMessage(const std::string& name, bool loop) +{ + if(!inTuningMode()) + return; + + mTuningAudioFile = name; + std::ostringstream stream; + stream + << "" + << "" << mTuningAudioFile << "" + << "" << (loop?"1":"0") << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::tuningRenderStopSendMessage() +{ + if(!inTuningMode()) + return; + + std::ostringstream stream; + stream + << "" + << "" << mTuningAudioFile << "" + << "\n\n\n"; + + writeString(stream.str()); +} + +void LLVoiceClient::tuningCaptureStartSendMessage(int duration) +{ + if(!inTuningMode()) + return; + + std::ostringstream stream; + stream + << "" + << "" << duration << "" + << "\n\n\n"; + + writeString(stream.str()); + + mTuningCaptureRunning = true; +} + +void LLVoiceClient::tuningCaptureStopSendMessage() +{ + if(!inTuningMode()) + return; + + std::ostringstream stream; + stream + << "" + << "\n\n\n"; + + writeString(stream.str()); + + mTuningCaptureRunning = false; +} + +void LLVoiceClient::tuningSetMicVolume(float volume) +{ + int scaledVolume = ((int)(volume * 100.0f)) - 100; + if(scaledVolume != mTuningMicVolume) + { + mTuningMicVolume = scaledVolume; + mTuningMicVolumeDirty = true; + } +} + +void LLVoiceClient::tuningSetSpeakerVolume(float volume) +{ + int scaledVolume = ((int)(volume * 100.0f)) - 100; + if(scaledVolume != mTuningSpeakerVolume) + { + mTuningSpeakerVolume = ((int)(volume * 100.0f)) - 100; + 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. + llwarns << "Connection to vivox daemon lost. Resetting state."<< llendl; + + closeSocket(); + removeAllParticipants(); + + // Try to relaunch the daemon + setState(stateDisabled); +} + +void LLVoiceClient::giveUp() +{ + // All has failed. Clean up and stop trying. + closeSocket(); + removeAllParticipants(); + + setState(stateJail); +} + +void LLVoiceClient::sendPositionalUpdate(void) +{ + std::ostringstream stream; + + if(mSpatialCoordsDirty) + { + LLVector3 l, u, a; + + // Always send both speaker and listener positions together. + stream << "" + << "" << mSessionHandle << ""; + + stream << ""; + + l = mAvatarRot.getLeftRow(); + u = mAvatarRot.getUpRow(); + a = mAvatarRot.getFwdRow(); + +// llinfos << "Sending speaker position " << mSpeakerPosition << llendl; + + stream + << "" + << "" << mAvatarPosition[VX] << "" + << "" << mAvatarPosition[VZ] << "" + << "" << mAvatarPosition[VY] << "" + << "" + << "" + << "" << mAvatarVelocity[VX] << "" + << "" << mAvatarVelocity[VZ] << "" + << "" << mAvatarVelocity[VY] << "" + << "" + << "" + << "" << l.mV[VX] << "" + << "" << u.mV[VX] << "" + << "" << a.mV[VX] << "" + << "" + << "" + << "" << l.mV[VZ] << "" + << "" << u.mV[VY] << "" + << "" << a.mV[VZ] << "" + << "" + << "" + << "" << l.mV [VY] << "" + << "" << u.mV [VZ] << "" + << "" << a.mV [VY] << "" + << ""; + + stream << ""; + + stream << ""; + + LLVector3d earPosition; + LLVector3 earVelocity; + LLMatrix3 earRot; + + switch(mEarLocation) + { + case earLocCamera: + default: + earPosition = mCameraPosition; + earVelocity = mCameraVelocity; + earRot = mCameraRot; + break; + + case earLocAvatar: + earPosition = mAvatarPosition; + earVelocity = mAvatarVelocity; + earRot = mAvatarRot; + break; + + case earLocMixed: + earPosition = mAvatarPosition; + earVelocity = mAvatarVelocity; + earRot = mCameraRot; + break; + } + + l = earRot.getLeftRow(); + u = earRot.getUpRow(); + a = earRot.getFwdRow(); + +// llinfos << "Sending listener position " << mListenerPosition << llendl; + + stream + << "" + << "" << earPosition[VX] << "" + << "" << earPosition[VZ] << "" + << "" << earPosition[VY] << "" + << "" + << "" + << "" << earVelocity[VX] << "" + << "" << earVelocity[VZ] << "" + << "" << earVelocity[VY] << "" + << "" + << "" + << "" << l.mV[VX] << "" + << "" << u.mV[VX] << "" + << "" << a.mV[VX] << "" + << "" + << "" + << "" << l.mV[VZ] << "" + << "" << u.mV[VY] << "" + << "" << a.mV[VZ] << "" + << "" + << "" + << "" << l.mV [VY] << "" + << "" << u.mV [VZ] << "" + << "" << a.mV [VY] << "" + << ""; + + stream << ""; + + stream << "\n\n\n"; + } + + if(mPTTDirty) + { + // 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) + +// llinfos << "Sending MuteLocalMic command with parameter " << (mPTT?"false":"true") << llendl; + + stream << "" + << "" << mConnectorHandle << "" + << "" << (mPTT?"false":"true") << "" + << "\n\n\n"; + + } + + if(mVolumeDirty) + { + participantMap::iterator iter = mParticipantMap.begin(); + + for(; iter != mParticipantMap.end(); iter++) + { + participantState *p = iter->second; + + if(p->mVolumeDirty) + { + int volume = p->mOnMuteList?0:p->mUserVolume; + + llinfos << "Setting volume for avatar " << p->mAvatarID << " to " << volume << llendl; + + // Send a mute/unumte command for the user (actually "volume for me"). + stream << "" + << "" << mSessionHandle << "" + << "" << p->mURI << "" + << "" << volume << "" + << "\n\n\n"; + + p->mVolumeDirty = false; + } + } + } + + if(mSpeakerMuteDirty) + { + const char *muteval = ((mSpeakerVolume == -100)?"true":"false"); + llinfos << "Setting speaker mute to " << muteval << llendl; + + stream << "" + << "" << mConnectorHandle << "" + << "" << muteval << "" + << "\n\n\n"; + } + + if(mSpeakerVolumeDirty) + { + llinfos << "Setting speaker volume to " << mSpeakerVolume << llendl; + + stream << "" + << "" << mConnectorHandle << "" + << "" << mSpeakerVolume << "" + << "\n\n\n"; + } + + if(mMicVolumeDirty) + { + llinfos << "Setting mic volume to " << mMicVolume << llendl; + + stream << "" + << "" << mConnectorHandle << "" + << "" << mMicVolume << "" + << "\n\n\n"; + } + + + // MBW -- XXX -- Maybe check to make sure the capture/render devices are in the current list here? + if(mCaptureDeviceDirty) + { + buildSetCaptureDevice(stream); + } + + if(mRenderDeviceDirty) + { + buildSetRenderDevice(stream); + } + + mSpatialCoordsDirty = false; + mPTTDirty = false; + mVolumeDirty = false; + mSpeakerVolumeDirty = false; + mMicVolumeDirty = false; + mSpeakerMuteDirty = false; + mCaptureDeviceDirty = false; + mRenderDeviceDirty = false; + + if(!stream.str().empty()) + { + writeString(stream.str()); + } +} + +void LLVoiceClient::buildSetCaptureDevice(std::ostringstream &stream) +{ + llinfos << "Setting input device = \"" << mCaptureDevice << "\"" << llendl; + + stream + << "" + << "" << mCaptureDevice << "" + << "" + << "\n\n\n"; +} + +void LLVoiceClient::buildSetRenderDevice(std::ostringstream &stream) +{ + llinfos << "Setting output device = \"" << mRenderDevice << "\"" << llendl; + + stream + << "" + << "" << mRenderDevice << "" + << "" + << "\n\n\n"; +} + +///////////////////////////// +// Response/Event handlers + +void LLVoiceClient::connectorCreateResponse(int statusCode, std::string &statusString, std::string &connectorHandle) +{ + if(statusCode != 0) + { + llwarns << "Connector.Create response failure: " << statusString << llendl; + setState(stateConnectorFailed); + } + else + { + // Connector created, move forward. + mConnectorHandle = connectorHandle; + if(getState() == stateConnectorStarting) + { + setState(stateConnectorStarted); + } + } +} + +void LLVoiceClient::loginResponse(int statusCode, std::string &statusString, std::string &accountHandle) +{ + llinfos << "Account.Login response (" << statusCode << "): " << statusString << llendl; + + // 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. + llinfos << "Account.Login response failure (" << statusCode << "): " << statusString << llendl; + setState(stateLoginRetry); + } + else if(statusCode != 0) + { + llwarns << "Account.Login response failure (" << statusCode << "): " << statusString << llendl; + setState(stateLoginFailed); + } + else + { + // Login succeeded, move forward. + mAccountHandle = accountHandle; + // MBW -- XXX -- This needs to wait until the LoginStateChangeEvent is received. +// if(getState() == stateLoggingIn) +// { +// setState(stateLoggedIn); +// } + } +} + +void LLVoiceClient::channelGetListResponse(int statusCode, std::string &statusString) +{ + if(statusCode != 0) + { + llwarns << "Account.ChannelGetList response failure: " << statusString << llendl; + switchChannel(); + } + else + { + // Got the channel list, try to do a lookup. + std::string uri = findChannelURI(mChannelName); + if(uri.empty()) + { + // Lookup failed, can't join a channel for this area. + llinfos << "failed to map channel name: " << mChannelName << llendl; + } + else + { + // We have a sip URL for this area. + llinfos << "mapped channel " << mChannelName << " to URI "<< uri << llendl; + } + + // switchChannel with an empty uri string will do the right thing (leave channel and not rejoin) + switchChannel(uri); + } +} + +void LLVoiceClient::sessionCreateResponse(int statusCode, std::string &statusString, std::string &sessionHandle) +{ + if(statusCode != 0) + { + llwarns << "Session.Create response failure (" << statusCode << "): " << statusString << llendl; +// if(statusCode == 1015) +// { +// if(getState() == stateJoiningSession) +// { +// // this happened during a real join. Going to sessionTerminated should cause a retry in appropriate cases. +// llwarns << "session handle \"" << sessionHandle << "\", mSessionStateEventHandle \"" << mSessionStateEventHandle << "\""<< llendl; +// if(!sessionHandle.empty()) +// { +// // This session is bad. Terminate it. +// mSessionHandle = sessionHandle; +// sessionTerminateByHandle(sessionHandle); +// setState(stateLeavingSession); +// } +// else if(!mSessionStateEventHandle.empty()) +// { +// mSessionHandle = mSessionStateEventHandle; +// sessionTerminateByHandle(mSessionStateEventHandle); +// setState(stateLeavingSession); +// } +// else +// { +// setState(stateSessionTerminated); +// } +// } +// else +// { +// // We didn't think we were in the middle of a join. Don't change state. +// llwarns << "Not in stateJoiningSession, ignoring" << llendl; +// } +// } +// else + { + mVivoxErrorStatusCode = statusCode; + mVivoxErrorStatusString = statusString; + setState(stateJoinSessionFailed); + } + } + else + { + llinfos << "Session.Create response received (success), session handle is " << sessionHandle << llendl; + if(getState() == stateJoiningSession) + { + // This is also grabbed in the SessionStateChangeEvent handler, but it might be useful to have it early... + mSessionHandle = sessionHandle; + } + else + { + // We should never get a session.create response in any state except stateJoiningSession. Things are out of sync. Kill this session. + sessionTerminateByHandle(sessionHandle); + } + } +} + +void LLVoiceClient::sessionConnectResponse(int statusCode, std::string &statusString) +{ + if(statusCode != 0) + { + llwarns << "Session.Connect response failure (" << statusCode << "): " << statusString << llendl; +// if(statusCode == 1015) +// { +// llwarns << "terminating existing session" << llendl; +// sessionTerminate(); +// } +// else + { + mVivoxErrorStatusCode = statusCode; + mVivoxErrorStatusString = statusString; + setState(stateJoinSessionFailed); + } + } + else + { + llinfos << "Session.Connect response received (success)" << llendl; + } +} + +void LLVoiceClient::sessionTerminateResponse(int statusCode, std::string &statusString) +{ + if(statusCode != 0) + { + llwarns << "Session.Terminate response failure: (" << statusCode << "): " << statusString << llendl; + if(getState() == stateLeavingSession) + { + // This is probably "(404): Server reporting Failure. Not a member of this conference." + // Do this so we don't get stuck. + setState(stateSessionTerminated); + } + } + +} + +void LLVoiceClient::logoutResponse(int statusCode, std::string &statusString) +{ + if(statusCode != 0) + { + llwarns << "Account.Logout response failure: " << statusString << llendl; + // MBW -- XXX -- Should this ever fail? do we care if it does? + } + + if(getState() == stateLoggingOut) + { + setState(stateLoggedOut); + } +} + +void LLVoiceClient::connectorShutdownResponse(int statusCode, std::string &statusString) +{ + if(statusCode != 0) + { + llwarns << "Connector.InitiateShutdown response failure: " << statusString << llendl; + // MBW -- XXX -- Should this ever fail? do we care if it does? + } + + mConnected = false; + + if(getState() == stateConnectorStopping) + { + setState(stateConnectorStopped); + } +} + +void LLVoiceClient::sessionStateChangeEvent( + std::string &uriString, + int statusCode, + std::string &statusString, + std::string &sessionHandle, + int state, + bool isChannel, + std::string &nameString) +{ + switch(state) + { + case 4: // I see this when joining the session + llinfos << "joined session " << uriString << ", name " << nameString << " handle " << mNextSessionHandle << llendl; + + // Session create succeeded, move forward. + mSessionStateEventHandle = sessionHandle; + mSessionStateEventURI = uriString; + if(sessionHandle == mSessionHandle) + { + // This is the session we're joining. + if(getState() == stateJoiningSession) + { + setState(stateSessionJoined); + //RN: the uriString being returned by vivox here is actually your account uri, not the channel + // you are attempting to join, so ignore it + //llinfos << "received URI " << uriString << "(previously " << mSessionURI << ")" << llendl; + //mSessionURI = uriString; + } + } + else if(sessionHandle == mNextSessionHandle) + { +// llinfos << "received URI " << uriString << ", name " << nameString << " for next session (handle " << mNextSessionHandle << ")" << llendl; + } + else + { + llwarns << "joining unknown session handle " << sessionHandle << ", URI " << uriString << ", name " << nameString << llendl; + // MBW -- XXX -- Should we send a Session.Terminate here? + } + + break; + case 5: // I see this when leaving the session + llinfos << "left session " << uriString << ", name " << nameString << " handle " << mNextSessionHandle << llendl; + + // Set the session handle to the empty string. If we get back to stateJoiningSession, we'll want to wait for the new session handle. + if(sessionHandle == mSessionHandle) + { + // MBW -- XXX -- I think this is no longer necessary, now that we've got mNextSessionURI/mNextSessionHandle + // mSessionURI.clear(); + // clear the session handle here just for sanity. + mSessionHandle.clear(); + if(mSessionResetOnClose) + { + mSessionResetOnClose = false; + mNonSpatialChannel = false; + mNextSessionSpatial = true; + parcelChanged(); + } + + removeAllParticipants(); + + switch(getState()) + { + case stateJoiningSession: + case stateSessionJoined: + case stateRunning: + case stateLeavingSession: + case stateJoinSessionFailed: + case stateJoinSessionFailedWaiting: + // normal transition + llinfos << "left session " << sessionHandle << "in state " << state2string(getState()) << llendl; + setState(stateSessionTerminated); + break; + + case stateSessionTerminated: + // this will happen sometimes -- there are cases where we send the terminate and then go straight to this state. + llwarns << "left session " << sessionHandle << "in state " << state2string(getState()) << llendl; + break; + + default: + llwarns << "unexpected SessionStateChangeEvent (left session) in state " << state2string(getState()) << llendl; + setState(stateSessionTerminated); + break; + } + + // store status values for later notification of observers + mVivoxErrorStatusCode = statusCode; + mVivoxErrorStatusString = statusString; + } + else + { + llinfos << "leaving unknown session handle " << sessionHandle << ", URI " << uriString << ", name " << nameString << llendl; + } + + mSessionStateEventHandle.clear(); + mSessionStateEventURI.clear(); + break; + default: + llwarns << "unknown state: " << state << llendl; + break; + } +} + +void LLVoiceClient::loginStateChangeEvent( + std::string &accountHandle, + int statusCode, + std::string &statusString, + int state) +{ + llinfos << "state is " << state << llendl; + /* + 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; + + default: +// llwarns << "unknown state: " << state << llendl; + break; + } +} + +void LLVoiceClient::sessionNewEvent( + std::string &accountHandle, + std::string &eventSessionHandle, + int state, + std::string &nameString, + std::string &uriString) +{ +// llinfos << "state is " << state << llendl; + + switch(state) + { + case 0: + { + llinfos << "session handle = " << eventSessionHandle << ", name = " << nameString << ", uri = " << uriString << llendl; + + LLUUID caller_id; + if(IDFromName(nameString, caller_id)) + { + gIMMgr->inviteToSession(LLIMMgr::computeSessionID(IM_SESSION_P2P_INVITE, caller_id), + LLString::null, + caller_id, + LLString::null, + IM_SESSION_P2P_INVITE, + eventSessionHandle); + } + else + { + llwarns << "Could not generate caller id from uri " << uriString << llendl; + } + } + break; + + default: + llwarns << "unknown state: " << state << llendl; + break; + } +} + +void LLVoiceClient::participantStateChangeEvent( + std::string &uriString, + int statusCode, + std::string &statusString, + int state, + std::string &nameString, + std::string &displayNameString, + int participantType) +{ + participantState *participant = NULL; + llinfos << "state is " << state << llendl; + + switch(state) + { + case 7: // I see this when a participant joins + participant = addParticipant(uriString); + if(participant) + { + participant->mName = nameString; + llinfos << "added participant \"" << participant->mName + << "\" (" << participant->mAvatarID << ")"<< llendl; + } + break; + case 9: // I see this when a participant leaves + participant = findParticipant(uriString); + if(participant) + { + removeParticipant(participant); + } + break; + default: +// llwarns << "unknown state: " << state << llendl; + break; + } +} + +void LLVoiceClient::participantPropertiesEvent( + std::string &uriString, + int statusCode, + std::string &statusString, + bool isLocallyMuted, + bool isModeratorMuted, + bool isSpeaking, + int volume, + F32 energy) +{ + participantState *participant = findParticipant(uriString); + if(participant) + { + participant->mPTT = !isLocallyMuted; + participant->mIsSpeaking = isSpeaking; + if (isSpeaking) + { + participant->mSpeakingTimeout.reset(); + } + participant->mPower = energy; + participant->mVolume = volume; + } + else + { + llwarns << "unknown participant: " << uriString << llendl; + } +} + +void LLVoiceClient::auxAudioPropertiesEvent(F32 energy) +{ +// llinfos << "got energy " << energy << llendl; + mTuningEnergy = energy; +} + +void LLVoiceClient::muteListChanged() +{ + // The user's mute list has been updated. Go through the current participant list and sync it with the mute list. + + participantMap::iterator iter = mParticipantMap.begin(); + + for(; iter != mParticipantMap.end(); iter++) + { + participantState *p = iter->second; + + // Check to see if this participant is on the mute list already + updateMuteState(p); + } +} + +///////////////////////////// +// Managing list of participants +LLVoiceClient::participantState::participantState(const std::string &uri) : + mURI(uri), mPTT(false), mIsSpeaking(false), mPower(0.0), mServiceType(serviceTypeUnknown), + mOnMuteList(false), mUserVolume(100), mVolumeDirty(false), mAvatarIDValid(false) +{ +} + +LLVoiceClient::participantState *LLVoiceClient::addParticipant(const std::string &uri) +{ + participantState *result = NULL; + + participantMap::iterator iter = mParticipantMap.find(uri); + + if(iter != mParticipantMap.end()) + { + // Found a matching participant already in the map. + result = iter->second; + } + + if(!result) + { + // participant isn't already in one list or the other. + result = new participantState(uri); + mParticipantMap.insert(participantMap::value_type(uri, result)); + mParticipantMapChanged = true; + + // Try to do a reverse transform on the URI to get the GUID back. + { + LLUUID id; + if(IDFromName(uri, id)) + { + result->mAvatarIDValid = true; + result->mAvatarID = id; + + updateMuteState(result); + } + } + + llinfos << "participant \"" << result->mURI << "\" added." << llendl; + } + + return result; +} + +void LLVoiceClient::updateMuteState(participantState *p) +{ + if(p->mAvatarIDValid) + { + bool isMuted = gMuteListp->isMuted(p->mAvatarID, LLMute::flagVoiceChat); + if(p->mOnMuteList != isMuted) + { + p->mOnMuteList = isMuted; + p->mVolumeDirty = true; + mVolumeDirty = true; + } + } +} + +void LLVoiceClient::removeParticipant(LLVoiceClient::participantState *participant) +{ + if(participant) + { + participantMap::iterator iter = mParticipantMap.find(participant->mURI); + + llinfos << "participant \"" << participant->mURI << "\" (" << participant->mAvatarID << ") removed." << llendl; + + mParticipantMap.erase(iter); + delete participant; + mParticipantMapChanged = true; + } +} + +void LLVoiceClient::removeAllParticipants() +{ + llinfos << "called" << llendl; + + while(!mParticipantMap.empty()) + { + removeParticipant(mParticipantMap.begin()->second); + } +} + +LLVoiceClient::participantMap *LLVoiceClient::getParticipantList(void) +{ + return &mParticipantMap; +} + + +LLVoiceClient::participantState *LLVoiceClient::findParticipant(const std::string &uri) +{ + participantState *result = NULL; + + // Don't find any participants if we're not connected. This is so that we don't continue to get stale data + // after the daemon dies. + if(mConnected) + { + participantMap::iterator iter = mParticipantMap.find(uri); + + if(iter != mParticipantMap.end()) + { + result = iter->second; + } + } + + return result; +} + + +LLVoiceClient::participantState *LLVoiceClient::findParticipantByAvatar(LLVOAvatar *avatar) +{ + participantState * result = NULL; + + // You'd think this would work, but it doesn't... +// std::string uri = sipURIFromAvatar(avatar); + + // Currently, the URI is just the account name. + std::string loginName = nameFromAvatar(avatar); + result = findParticipant(loginName); + + if(result != NULL) + { + if(!result->mAvatarIDValid) + { + result->mAvatarID = avatar->getID(); + result->mAvatarIDValid = true; + + // We just figured out the avatar ID, so the participant list has "changed" from the perspective of anyone who uses that to identify participants. + mParticipantMapChanged = true; + + updateMuteState(result); + } + + + } + + return result; +} + +LLVoiceClient::participantState* LLVoiceClient::findParticipantByID(const LLUUID& id) +{ + participantState * result = NULL; + + // Currently, the URI is just the account name. + std::string loginName = nameFromID(id); + result = findParticipant(loginName); + + return result; +} + + +void LLVoiceClient::clearChannelMap(void) +{ + mChannelMap.clear(); +} + +void LLVoiceClient::addChannelMapEntry(std::string &name, std::string &uri) +{ +// llinfos << "Adding channel name mapping: " << name << " -> " << uri << llendl; + mChannelMap.insert(channelMap::value_type(name, uri)); +} + +std::string LLVoiceClient::findChannelURI(std::string &name) +{ + std::string result; + + channelMap::iterator iter = mChannelMap.find(name); + + if(iter != mChannelMap.end()) + { + result = iter->second; + } + + return result; +} + +void LLVoiceClient::parcelChanged() +{ + if(getState() >= stateLoggedIn) + { + // If the user is logged in, start a channel lookup. + llinfos << "sending ParcelVoiceInfoRequest (" << mCurrentRegionName << ", " << mCurrentParcelLocalID << ")" << llendl; + + std::string url = gAgent.getRegion()->getCapability("ParcelVoiceInfoRequest"); + LLSD data; + data["method"] = "call"; + LLHTTPClient::post( + url, + data, + new LLVoiceClientCapResponder); + } + else + { + // The transition to stateLoggedIn needs to kick this off again. + llinfos << "not logged in yet, deferring" << llendl; + } +} + +void LLVoiceClient::switchChannel( + std::string uri, + bool spatial, + bool noReconnect, + std::string hash) +{ + bool needsSwitch = false; + + llinfos << "called in state " << state2string(getState()) << " with uri \"" << uri << "\"" << llendl; + + 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(mNextSessionURI != uri) + needsSwitch = true; + } + else + { + // Otherwise, compare against the URI we're in now. + if(mSessionURI != uri) + needsSwitch = true; + } + break; + } + + if(needsSwitch) + { + mNextSessionURI = uri; + mNextSessionHash = hash; + mNextSessionHandle.clear(); + mNextP2PSessionURI.clear(); + mNextSessionSpatial = spatial; + mNextSessionNoReconnect = noReconnect; + + if(uri.empty()) + { + // Leave any channel we may be in + llinfos << "leaving channel" << llendl; + } + else + { + llinfos << "switching to channel " << uri << llendl; + } + + 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(std::string handle, std::string uri) +{ + mNextSessionURI.clear(); + mNextSessionHash.clear(); + mNextP2PSessionURI = uri; + mNextSessionHandle = handle; + mNextSessionSpatial = false; + mNextSessionNoReconnect = false; + + 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, credentials); +} + +void LLVoiceClient::setSpatialChannel( + const std::string &uri, + const std::string &credentials) +{ + mSpatialSessionURI = uri; + mAreaVoiceDisabled = mSpatialSessionURI.empty(); + + llinfos << "got spatial channel uri: \"" << uri << "\"" << llendl; + + if(mNonSpatialChannel || !mNextSessionSpatial) + { + // User is in a non-spatial chat or joining a non-spatial chat. Don't switch channels. + llinfos << "in non-spatial chat, not switching channels" << llendl; + } + else + { + switchChannel(mSpatialSessionURI, true, false, credentials); + } +} + +void LLVoiceClient::callUser(LLUUID &uuid) +{ + std::string userURI = sipURIFromID(uuid); + + switchChannel(userURI, false, true); +} + +void LLVoiceClient::answerInvite(std::string &sessionHandle, LLUUID& other_user_id) +{ + joinSession(sessionHandle, sipURIFromID(other_user_id)); +} + +void LLVoiceClient::declineInvite(std::string &sessionHandle) +{ + sessionTerminateByHandle(sessionHandle); +} + +void LLVoiceClient::leaveNonSpatialChannel() +{ + switchChannel(mSpatialSessionURI); +} + +std::string LLVoiceClient::getCurrentChannel() +{ + if((getState() == stateRunning) && !mSessionTerminateRequested) + { + return mSessionURI; + } + + return ""; +} + +bool LLVoiceClient::inProximalChannel() +{ + bool result = false; + + if((getState() == stateRunning) && !mSessionTerminateRequested) + { + result = !mNonSpatialChannel; + } + + return result; +} + +std::string LLVoiceClient::sipURIFromID(const LLUUID &id) +{ + std::string result; + result = "sip:"; + result += nameFromID(id); + result += "@"; + result += mAccountServerName; + + return result; +} + +std::string LLVoiceClient::sipURIFromAvatar(LLVOAvatar *avatar) +{ + std::string result; + if(avatar) + { + result = "sip:"; + result += nameFromID(avatar->getID()); + result += "@"; + result += mAccountServerName; + } + + 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; + U8 rawuuid[UUID_BYTES + 1]; + uuid.toCompressedString((char*)rawuuid); + + // 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(rawuuid, UUID_BYTES); + LLString::replaceChar(result, '+', '-'); + LLString::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 '/+' '_-') + + return result; +} + +bool LLVoiceClient::IDFromName(const std::string name, LLUUID &uuid) +{ + bool result = false; + + // 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; + LLString::replaceChar(temp, '-', '+'); + LLString::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 + // MBW -- XXX -- there's no analogue of LLUUID::toCompressedString that allows you to set a UUID from binary data. + // The data field is public, so we cheat thusly: + memcpy(uuid.mData, rawuuid, UUID_BYTES); + result = true; + } + } + + 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 += mAccountServerName; + +// LLString::toLower(result); + + 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::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) + { +// llinfos << "leaving channel for teleport/logout" << llendl; + 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 + { + // for now, leave active channel, to auto join when turning voice back on + //LLVoiceChannel::getCurrentVoiceChannel->deactivate(); + } + } +} + +bool LLVoiceClient::voiceEnabled() +{ + return gSavedSettings.getBOOL("EnableVoiceChat") && !gDisableVoice; +} + +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) + { + llinfos << "Setting mEarLocation to " << loc << llendl; + + mEarLocation = loc; + mSpatialCoordsDirty = true; + } +} + +void LLVoiceClient::setVoiceVolume(F32 volume) +{ + int scaledVolume = ((int)(volume * 100.0f)) - 100; + if(scaledVolume != mSpeakerVolume) + { + if((scaledVolume == -100) || (mSpeakerVolume == -100)) + { + mSpeakerMuteDirty = true; + } + + mSpeakerVolume = scaledVolume; + mSpeakerVolumeDirty = true; + } +} + +void LLVoiceClient::setMicGain(F32 volume) +{ + int scaledVolume = ((int)(volume * 100.0f)) - 100; + if(scaledVolume != mMicVolume) + { + mMicVolume = scaledVolume; + mMicVolumeDirty = true; + } +} + +void LLVoiceClient::setVivoxDebugServerName(std::string &serverName) +{ + if(!mAccountServerName.empty()) + { + // The name has been filled in already, which means we know whether we're connecting to agni or not. + if(!sConnectingToAgni) + { + // Only use the setting if we're connecting to a development grid -- always use bhr when on agni. + mAccountServerName = serverName; + } + } +} + +void LLVoiceClient::keyDown(KEY key, MASK mask) +{ +// llinfos << "key is " << LLKeyboard::stringFromKey(key) << llendl; + + 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; +} + +F32 LLVoiceClient::getCurrentPower(const LLUUID& id) +{ + F32 result = 0; + participantState *participant = findParticipantByID(id); + if(participant) + { + result = participant->mPower; + } + + return result; +} + + +LLString LLVoiceClient::getDisplayName(const LLUUID& id) +{ + LLString 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::getPTTPressed(const LLUUID& id) +{ + BOOL result = FALSE; + + participantState *participant = findParticipantByID(id); + if(participant) + { + result = participant->mPTT; + } + + 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 = participant->mUserVolume; // 0-400 + result = sqrtf(((F32)ires) / 400.f); + } + + return result; +} + +void LLVoiceClient::setUserVolume(const LLUUID& id, F32 volume) +{ + 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; + mVolumeDirty = TRUE; + } +} + + + +LLVoiceClient::serviceType LLVoiceClient::getServiceType(const LLUUID& id) +{ + serviceType result = serviceTypeUnknown; + + participantState *participant = findParticipantByID(id); + if(participant) + { + result = participant->mServiceType; + } + + return result; +} + +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::addObserver(LLVoiceClientParticipantObserver* observer) +{ + mObservers.insert(observer); +} + +void LLVoiceClient::removeObserver(LLVoiceClientParticipantObserver* observer) +{ + mObservers.erase(observer); +} + +void LLVoiceClient::notifyObservers() +{ + for (observer_set_t::iterator it = mObservers.begin(); + it != mObservers.end(); + ) + { + LLVoiceClientParticipantObserver* observer = *it; + observer->onChange(); + // In case onChange() deleted an entry. + it = mObservers.upper_bound(observer); + } +} + +void LLVoiceClient::addStatusObserver(LLVoiceClientStatusObserver* observer) +{ + mStatusObservers.insert(observer); +} + +void LLVoiceClient::removeStatusObserver(LLVoiceClientStatusObserver* observer) +{ + mStatusObservers.erase(observer); +} + +void LLVoiceClient::notifyStatusObservers(LLVoiceClientStatusObserver::EStatusType status) +{ + if(status == LLVoiceClientStatusObserver::ERROR_UNKNOWN) + { + switch(mVivoxErrorStatusCode) + { + case 20713: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_FULL; break; + case 20714: status = LLVoiceClientStatusObserver::ERROR_CHANNEL_LOCKED; break; + } + + // Reset the error code to make sure it won't be reused later by accident. + mVivoxErrorStatusCode = 0; + } + + if (status == LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL + //NOT_FOUND || TEMPORARILY_UNAVAILABLE || REQUEST_TIMEOUT + && (mVivoxErrorStatusCode == 404 || mVivoxErrorStatusCode == 480 || mVivoxErrorStatusCode == 408)) + { + // 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. + mVivoxErrorStatusCode = 0; + } + + llinfos << " " << LLVoiceClientStatusObserver::status2string(status) << ", session URI " << mSessionURI << llendl; + + for (status_observer_set_t::iterator it = mStatusObservers.begin(); + it != mStatusObservers.end(); + ) + { + LLVoiceClientStatusObserver* observer = *it; + observer->onChange(status, mSessionURI, !mNonSpatialChannel); + // In case onError() deleted an entry. + it = mStatusObservers.upper_bound(observer); + } + +} + +//static +void LLVoiceClient::onAvatarNameLookup(const LLUUID& id, const char* first, const char* last, BOOL is_group, void* user_data) +{ + participantState* statep = gVoiceClient->findParticipantByID(id); + + if (statep) + { + statep->mDisplayName = llformat("%s %s", first, last); + } + + gVoiceClient->notifyObservers(); +} + +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 + + 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 ( 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; + gViewerWindow->alertXml("VoiceVersionMismatch"); + gSavedSettings.setBOOL("EnableVoiceChat", FALSE); // toggles listener + } + } + } + } +}; +BOOL LLViewerRequiredVoiceVersion::sAlertedUser = FALSE; + +LLHTTPRegistration + gHTTPRegistrationMessageParcelVoiceInfo( + "/message/ParcelVoiceInfo"); + +LLHTTPRegistration + gHTTPRegistrationMessageRequiredVoiceVersion( + "/message/RequiredVoiceVersion"); -- cgit v1.1