/indra/newview/llvoicevivox.cpp
C++ | 2376 lines | 1661 code | 397 blank | 318 comment | 208 complexity | bf3d28757257273bd32b19557d17e31c MD5 | raw file
Possible License(s): LGPL-2.1
- /**
- * @file LLVivoxVoiceClient.cpp
- * @brief Implementation of LLVivoxVoiceClient class which is the interface to the voice client process.
- *
- * $LicenseInfo:firstyear=2001&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2010, Linden Research, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation;
- * version 2.1 of the License only.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
- * $/LicenseInfo$
- */
- #include "llviewerprecompiledheaders.h"
- #include "llvoicevivox.h"
- #include <boost/tokenizer.hpp>
- #include "llsdutil.h"
- // Linden library includes
- #include "llavatarnamecache.h"
- #include "llvoavatarself.h"
- #include "llbufferstream.h"
- #include "llfile.h"
- #ifdef LL_STANDALONE
- # include "expat.h"
- #else
- # include "expat/expat.h"
- #endif
- #include "llcallbacklist.h"
- #include "llviewerregion.h"
- #include "llviewernetwork.h" // for gGridChoice
- #include "llbase64.h"
- #include "llviewercontrol.h"
- #include "llappviewer.h" // for gDisconnected, gDisableVoice
- // Viewer includes
- #include "llmutelist.h" // to check for muted avatars
- #include "llagent.h"
- #include "llcachename.h"
- #include "llimview.h" // for LLIMMgr
- #include "llparcel.h"
- #include "llviewerparcelmgr.h"
- #include "llfirstuse.h"
- #include "llspeakers.h"
- #include "lltrans.h"
- #include "llviewerwindow.h"
- #include "llviewercamera.h"
- #include "llviewernetwork.h"
- #include "llnotificationsutil.h"
- #include "stringize.h"
- // for base64 decoding
- #include "apr_base64.h"
- #define USE_SESSION_GROUPS 0
- const F32 VOLUME_SCALE_VIVOX = 0.01f;
- const F32 SPEAKING_TIMEOUT = 1.f;
- static const std::string VOICE_SERVER_TYPE = "Vivox";
- // 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;
- // Defines the maximum number of times(in a row) "stateJoiningSession" case for spatial channel is reached in stateMachine()
- // which is treated as normal. If this number is exceeded we suspect there is a problem with connection
- // to voice server (EXT-4313). When voice works correctly, there is from 1 to 15 times. 50 was chosen
- // to make sure we don't make mistake when slight connection problems happen- situation when connection to server is
- // blocked is VERY rare and it's better to sacrifice response time in this situation for the sake of stability.
- const int MAX_NORMAL_JOINING_SPATIAL_NUM = 50;
- // How often to check for expired voice fonts in seconds
- const F32 VOICE_FONT_EXPIRY_INTERVAL = 10.f;
- // Time of day at which Vivox expires voice font subscriptions.
- // Used to replace the time portion of received expiry timestamps.
- static const std::string VOICE_FONT_EXPIRY_TIME = "T05:00:00Z";
- // Maximum length of capture buffer recordings in seconds.
- const F32 CAPTURE_BUFFER_MAX_TIME = 10.f;
- static int scale_mic_volume(float volume)
- {
- // incoming volume has the range [0.0 ... 2.0], with 1.0 as the default.
- // Map it to Vivox levels as follows: 0.0 -> 30, 1.0 -> 50, 2.0 -> 70
- return 30 + (int)(volume * 20.0f);
- }
- static int scale_speaker_volume(float volume)
- {
- // incoming volume has the range [0.0 ... 1.0], with 0.5 as the default.
- // Map it to Vivox levels as follows: 0.0 -> 30, 0.5 -> 50, 1.0 -> 70
- return 30 + (int)(volume * 40.0f);
-
- }
- class LLVivoxVoiceAccountProvisionResponder :
- public LLHTTPClient::Responder
- {
- public:
- LLVivoxVoiceAccountProvisionResponder(int retries)
- {
- mRetries = retries;
- }
- virtual void error(U32 status, const std::string& reason)
- {
- if ( mRetries > 0 )
- {
- LL_WARNS("Voice") << "ProvisionVoiceAccountRequest returned an error, retrying. status = " << status << ", reason = \"" << reason << "\"" << LL_ENDL;
- LLVivoxVoiceClient::getInstance()->requestVoiceAccountProvision(
- mRetries - 1);
- }
- else
- {
- LL_WARNS("Voice") << "ProvisionVoiceAccountRequest returned an error, too many retries (giving up). status = " << status << ", reason = \"" << reason << "\"" << LL_ENDL;
- LLVivoxVoiceClient::getInstance()->giveUp();
- }
- }
- virtual void result(const LLSD& content)
- {
- std::string voice_sip_uri_hostname;
- std::string voice_account_server_uri;
-
- LL_DEBUGS("Voice") << "ProvisionVoiceAccountRequest response:" << ll_pretty_print_sd(content) << LL_ENDL;
-
- if(content.has("voice_sip_uri_hostname"))
- voice_sip_uri_hostname = content["voice_sip_uri_hostname"].asString();
-
- // this key is actually misnamed -- it will be an entire URI, not just a hostname.
- if(content.has("voice_account_server_name"))
- voice_account_server_uri = content["voice_account_server_name"].asString();
-
- LLVivoxVoiceClient::getInstance()->login(
- content["username"].asString(),
- content["password"].asString(),
- voice_sip_uri_hostname,
- voice_account_server_uri);
- }
- private:
- int mRetries;
- };
- ///////////////////////////////////////////////////////////////////////////////////////////////
- class LLVivoxVoiceClientMuteListObserver : public LLMuteListObserver
- {
- /* virtual */ void onChange() { LLVivoxVoiceClient::getInstance()->muteListChanged();}
- };
- class LLVivoxVoiceClientFriendsObserver : public LLFriendObserver
- {
- public:
- /* virtual */ void changed(U32 mask) { LLVivoxVoiceClient::getInstance()->updateFriends(mask);}
- };
- static LLVivoxVoiceClientMuteListObserver mutelist_listener;
- static bool sMuteListListener_listening = false;
- static LLVivoxVoiceClientFriendsObserver *friendslist_listener = NULL;
- ///////////////////////////////////////////////////////////////////////////////////////////////
- class LLVivoxVoiceClientCapResponder : public LLHTTPClient::Responder
- {
- public:
- LLVivoxVoiceClientCapResponder(LLVivoxVoiceClient::state requesting_state) : mRequestingState(requesting_state) {};
- virtual void error(U32 status, const std::string& reason); // called with bad status codes
- virtual void result(const LLSD& content);
- private:
- LLVivoxVoiceClient::state mRequestingState; // state
- };
- void LLVivoxVoiceClientCapResponder::error(U32 status, const std::string& reason)
- {
- LL_WARNS("Voice") << "LLVivoxVoiceClientCapResponder::error("
- << status << ": " << reason << ")"
- << LL_ENDL;
- LLVivoxVoiceClient::getInstance()->sessionTerminate();
- }
- void LLVivoxVoiceClientCapResponder::result(const LLSD& content)
- {
- LLSD::map_const_iterator iter;
-
- LL_DEBUGS("Voice") << "ParcelVoiceInfoRequest response:" << ll_pretty_print_sd(content) << LL_ENDL;
- std::string uri;
- std::string credentials;
-
- if ( content.has("voice_credentials") )
- {
- LLSD voice_credentials = content["voice_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();
- }
- }
-
- // set the spatial channel. If no voice credentials or uri are
- // available, then we simply drop out of voice spatially.
- if(LLVivoxVoiceClient::getInstance()->parcelVoiceInfoReceived(mRequestingState))
- {
- LLVivoxVoiceClient::getInstance()->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
- ///////////////////////////////////////////////////////////////////////////////////////////////
- LLVivoxVoiceClient::LLVivoxVoiceClient() :
- mState(stateDisabled),
- mSessionTerminateRequested(false),
- mRelogRequested(false),
- mConnected(false),
- mPump(NULL),
- mSpatialJoiningNum(0),
- mTuningMode(false),
- mTuningEnergy(0.0f),
- mTuningMicVolume(0),
- mTuningMicVolumeDirty(true),
- mTuningSpeakerVolume(0),
- mTuningSpeakerVolumeDirty(true),
- mTuningExitState(stateDisabled),
- mAreaVoiceDisabled(false),
- mAudioSession(NULL),
- mAudioSessionChanged(false),
- mNextAudioSession(NULL),
- mCurrentParcelLocalID(0),
- mNumberOfAliases(0),
- mCommandCookie(0),
- mLoginRetryCount(0),
- mBuddyListMapPopulated(false),
- mBlockRulesListReceived(false),
- mAutoAcceptRulesListReceived(false),
- mCaptureDeviceDirty(false),
- mRenderDeviceDirty(false),
- mSpatialCoordsDirty(false),
- mMuteMic(false),
- mMuteMicDirty(false),
- mFriendsListDirty(true),
- mEarLocation(0),
- mSpeakerVolumeDirty(true),
- mSpeakerMuteDirty(true),
- mMicVolume(0),
- mMicVolumeDirty(true),
- mVoiceEnabled(false),
- mWriteInProgress(false),
- mLipSyncEnabled(false),
- mVoiceFontsReceived(false),
- mVoiceFontsNew(false),
- mVoiceFontListDirty(false),
- mCaptureBufferMode(false),
- mCaptureBufferRecording(false),
- mCaptureBufferRecorded(false),
- mCaptureBufferPlaying(false),
- mPlayRequestCount(0)
- {
- mSpeakerVolume = scale_speaker_volume(0);
- mVoiceVersion.serverVersion = "";
- mVoiceVersion.serverType = VOICE_SERVER_TYPE;
-
- // gMuteListp isn't set up at this point, so we defer this until later.
- // gMuteListp->addObserver(&mutelist_listener);
-
-
- #if LL_DARWIN || LL_LINUX || LL_SOLARIS
- // HACK: THIS DOES NOT BELONG HERE
- // When the vivox daemon dies, the next write attempt on our socket generates a SIGPIPE, which kills us.
- // This should cause us to ignore SIGPIPE and handle the error through proper channels.
- // This should really be set up elsewhere. Where should it go?
- signal(SIGPIPE, SIG_IGN);
-
- // Since we're now launching the gateway with fork/exec instead of system(), we need to deal with zombie processes.
- // Ignoring SIGCHLD should prevent zombies from being created. Alternately, we could use wait(), but I'd rather not do that.
- signal(SIGCHLD, SIG_IGN);
- #endif
- // set up state machine
- setState(stateDisabled);
-
- gIdleCallbacks.addFunction(idle, this);
- }
- //---------------------------------------------------
- LLVivoxVoiceClient::~LLVivoxVoiceClient()
- {
- }
- //---------------------------------------------------
- void LLVivoxVoiceClient::init(LLPumpIO *pump)
- {
- // constructor will set up LLVoiceClient::getInstance()
- LLVivoxVoiceClient::getInstance()->mPump = pump;
- }
- void LLVivoxVoiceClient::terminate()
- {
- if(mConnected)
- {
- logout();
- connectorShutdown();
- closeSocket(); // Need to do this now -- bad things happen if the destructor does it later.
- cleanUp();
- }
- else
- {
- killGateway();
- }
- }
- //---------------------------------------------------
- void LLVivoxVoiceClient::cleanUp()
- {
- deleteAllSessions();
- deleteAllBuddies();
- deleteAllVoiceFonts();
- deleteVoiceFontTemplates();
- }
- //---------------------------------------------------
- const LLVoiceVersionInfo& LLVivoxVoiceClient::getVersion()
- {
- return mVoiceVersion;
- }
- //---------------------------------------------------
- void LLVivoxVoiceClient::updateSettings()
- {
- setVoiceEnabled(gSavedSettings.getBOOL("EnableVoiceChat"));
- setEarLocation(gSavedSettings.getS32("VoiceEarLocation"));
- std::string inputDevice = gSavedSettings.getString("VoiceInputAudioDevice");
- setCaptureDevice(inputDevice);
- std::string outputDevice = gSavedSettings.getString("VoiceOutputAudioDevice");
- setRenderDevice(outputDevice);
- F32 mic_level = gSavedSettings.getF32("AudioLevelMic");
- setMicGain(mic_level);
- setLipSyncEnabled(gSavedSettings.getBOOL("LipSyncEnabled"));
- }
- /////////////////////////////
- // utility functions
- bool LLVivoxVoiceClient::writeString(const std::string &str)
- {
- bool result = false;
- if(mConnected)
- {
- apr_status_t err;
- apr_size_t size = (apr_size_t)str.size();
- apr_size_t written = size;
-
- //MARK: Turn this on to log outgoing XML
- // LL_DEBUGS("Voice") << "sending: " << str << LL_ENDL;
- // check return code - sockets will fail (broken, etc.)
- err = apr_socket_send(
- mSocket->getSocket(),
- (const char*)str.data(),
- &written);
-
- if(err == 0)
- {
- // Success.
- result = true;
- }
- // TODO: handle partial writes (written is number of bytes written)
- // Need to set socket to non-blocking before this will work.
- // else if(APR_STATUS_IS_EAGAIN(err))
- // {
- // //
- // }
- else
- {
- // Assume any socket error means something bad. For now, just close the socket.
- char buf[MAX_STRING];
- LL_WARNS("Voice") << "apr error " << err << " ("<< apr_strerror(err, buf, MAX_STRING) << ") sending data to vivox daemon." << LL_ENDL;
- daemonDied();
- }
- }
-
- return result;
- }
- /////////////////////////////
- // session control messages
- void LLVivoxVoiceClient::connectorCreate()
- {
- std::ostringstream stream;
- std::string logpath = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "");
- std::string loglevel = "0";
-
- // Transition to stateConnectorStarted when the connector handle comes back.
- setState(stateConnectorStarting);
- std::string savedLogLevel = gSavedSettings.getString("VivoxDebugLevel");
-
- if(savedLogLevel != "-1")
- {
- LL_DEBUGS("Voice") << "creating connector with logging enabled" << LL_ENDL;
- loglevel = "10";
- }
-
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.Create.1\">"
- << "<ClientName>V2 SDK</ClientName>"
- << "<AccountManagementServer>" << mVoiceAccountServerURI << "</AccountManagementServer>"
- << "<Mode>Normal</Mode>"
- << "<Logging>"
- << "<Folder>" << logpath << "</Folder>"
- << "<FileNamePrefix>Connector</FileNamePrefix>"
- << "<FileNameSuffix>.log</FileNameSuffix>"
- << "<LogLevel>" << loglevel << "</LogLevel>"
- << "</Logging>"
- << "<Application>SecondLifeViewer.1</Application>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::connectorShutdown()
- {
- setState(stateConnectorStopping);
-
- if(!mConnectorHandle.empty())
- {
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Connector.InitiateShutdown.1\">"
- << "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
- << "</Request>"
- << "\n\n\n";
-
- mConnectorHandle.clear();
-
- writeString(stream.str());
- }
- }
- void LLVivoxVoiceClient::userAuthorized(const std::string& user_id, const LLUUID &agentID)
- {
- mAccountDisplayName = user_id;
- LL_INFOS("Voice") << "name \"" << mAccountDisplayName << "\" , ID " << agentID << LL_ENDL;
- mAccountName = nameFromID(agentID);
- }
- void LLVivoxVoiceClient::requestVoiceAccountProvision(S32 retries)
- {
- LLViewerRegion *region = gAgent.getRegion();
-
- if ( region && mVoiceEnabled )
- {
- std::string url =
- region->getCapability("ProvisionVoiceAccountRequest");
-
- if ( url.empty() )
- {
- // we've not received the capability yet, so return.
- // the password will remain empty, so we'll remain in
- // stateIdle
- return;
- }
-
- LLHTTPClient::post(
- url,
- LLSD(),
- new LLVivoxVoiceAccountProvisionResponder(retries));
-
- setState(stateConnectorStart);
- }
- }
- void LLVivoxVoiceClient::login(
- const std::string& account_name,
- const std::string& password,
- const std::string& voice_sip_uri_hostname,
- const std::string& voice_account_server_uri)
- {
- mVoiceSIPURIHostName = voice_sip_uri_hostname;
- mVoiceAccountServerURI = voice_account_server_uri;
- if(!mAccountHandle.empty())
- {
- // Already logged in.
- LL_WARNS("Voice") << "Called while already logged in." << LL_ENDL;
-
- // Don't process another login.
- return;
- }
- else if ( account_name != mAccountName )
- {
- //TODO: error?
- LL_WARNS("Voice") << "Wrong account name! " << account_name
- << " instead of " << mAccountName << LL_ENDL;
- }
- else
- {
- mAccountPassword = password;
- }
- std::string debugSIPURIHostName = gSavedSettings.getString("VivoxDebugSIPURIHostName");
-
- if( !debugSIPURIHostName.empty() )
- {
- mVoiceSIPURIHostName = debugSIPURIHostName;
- }
-
- if( mVoiceSIPURIHostName.empty() )
- {
- // we have an empty account server name
- // so we fall back to hardcoded defaults
- if(LLGridManager::getInstance()->isInProductionGrid())
- {
- // Use the release account server
- mVoiceSIPURIHostName = "bhr.vivox.com";
- }
- else
- {
- // Use the development account server
- mVoiceSIPURIHostName = "bhd.vivox.com";
- }
- }
-
- std::string debugAccountServerURI = gSavedSettings.getString("VivoxDebugVoiceAccountServerURI");
- if( !debugAccountServerURI.empty() )
- {
- mVoiceAccountServerURI = debugAccountServerURI;
- }
-
- if( mVoiceAccountServerURI.empty() )
- {
- // If the account server URI isn't specified, construct it from the SIP URI hostname
- mVoiceAccountServerURI = "https://www." + mVoiceSIPURIHostName + "/api2/";
- }
- }
- void LLVivoxVoiceClient::idle(void* user_data)
- {
- LLVivoxVoiceClient* self = (LLVivoxVoiceClient*)user_data;
- self->stateMachine();
- }
- std::string LLVivoxVoiceClient::state2string(LLVivoxVoiceClient::state inState)
- {
- std::string result = "UNKNOWN";
-
- // Prevent copy-paste errors when updating this list...
- #define CASE(x) case x: result = #x; break
- switch(inState)
- {
- CASE(stateDisableCleanup);
- CASE(stateDisabled);
- CASE(stateStart);
- CASE(stateDaemonLaunched);
- CASE(stateConnecting);
- CASE(stateConnected);
- CASE(stateIdle);
- CASE(stateMicTuningStart);
- CASE(stateMicTuningRunning);
- CASE(stateMicTuningStop);
- CASE(stateCaptureBufferPaused);
- CASE(stateCaptureBufferRecStart);
- CASE(stateCaptureBufferRecording);
- CASE(stateCaptureBufferPlayStart);
- CASE(stateCaptureBufferPlaying);
- CASE(stateConnectorStart);
- CASE(stateConnectorStarting);
- CASE(stateConnectorStarted);
- CASE(stateLoginRetry);
- CASE(stateLoginRetryWait);
- CASE(stateNeedsLogin);
- CASE(stateLoggingIn);
- CASE(stateLoggedIn);
- CASE(stateVoiceFontsWait);
- CASE(stateVoiceFontsReceived);
- CASE(stateCreatingSessionGroup);
- CASE(stateNoChannel);
- CASE(stateRetrievingParcelVoiceInfo);
- 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;
- }
- void LLVivoxVoiceClient::setState(state inState)
- {
- LL_DEBUGS("Voice") << "entering state " << state2string(inState) << LL_ENDL;
-
- mState = inState;
- }
- void LLVivoxVoiceClient::stateMachine()
- {
- if(gDisconnected)
- {
- // The viewer has been disconnected from the sim. Disable voice.
- setVoiceEnabled(false);
- }
-
- if(mVoiceEnabled)
- {
- updatePosition();
- }
- else if(mTuningMode)
- {
- // Tuning mode is special -- it needs to launch SLVoice even if voice is disabled.
- }
- else
- {
- if((getState() != stateDisabled) && (getState() != stateDisableCleanup))
- {
- // User turned off voice support. Send the cleanup messages, close the socket, and reset.
- if(!mConnected)
- {
- // if voice was turned off after the daemon was launched but before we could connect to it, we may need to issue a kill.
- LL_INFOS("Voice") << "Disabling voice before connection to daemon, terminating." << LL_ENDL;
- killGateway();
- }
-
- logout();
- connectorShutdown();
-
- setState(stateDisableCleanup);
- }
- }
-
- switch(getState())
- {
- //MARK: stateDisableCleanup
- case stateDisableCleanup:
- // Clean up and reset everything.
- closeSocket();
- cleanUp();
- mAccountHandle.clear();
- mAccountPassword.clear();
- mVoiceAccountServerURI.clear();
-
- setState(stateDisabled);
- break;
-
- //MARK: stateDisabled
- case stateDisabled:
- if(mTuningMode || (mVoiceEnabled && !mAccountName.empty()))
- {
- setState(stateStart);
- }
- break;
-
- //MARK: stateStart
- case stateStart:
- if(gSavedSettings.getBOOL("CmdLineDisableVoice"))
- {
- // Voice is locked out, we must not launch the vivox daemon.
- setState(stateJail);
- }
- else if(!isGatewayRunning())
- {
- if(true)
- {
- // Launch the voice daemon
-
- // *FIX:Mani - Using the executable dir instead
- // of mAppRODataDir, the working directory from which the app
- // is launched.
- //std::string exe_path = gDirUtilp->getAppRODataDir();
- std::string exe_path = gDirUtilp->getExecutableDir();
- exe_path += gDirUtilp->getDirDelimiter();
- #if LL_WINDOWS
- exe_path += "SLVoice.exe";
- #elif LL_DARWIN
- exe_path += "../Resources/SLVoice";
- #else
- exe_path += "SLVoice";
- #endif
- // See if the vivox executable exists
- llstat s;
- if(!LLFile::stat(exe_path, &s))
- {
- // vivox executable exists. Build the command line and launch the daemon.
- // SLIM SDK: these arguments are no longer necessary.
- // std::string args = " -p tcp -h -c";
- std::string args;
- std::string cmd;
- std::string loglevel = gSavedSettings.getString("VivoxDebugLevel");
-
- if(loglevel.empty())
- {
- loglevel = "-1"; // turn logging off completely
- }
-
- args += " -ll ";
- args += loglevel;
-
- LL_DEBUGS("Voice") << "Args for SLVoice: " << args << LL_ENDL;
- #if LL_WINDOWS
- PROCESS_INFORMATION pinfo;
- STARTUPINFOA sinfo;
-
- memset(&sinfo, 0, sizeof(sinfo));
-
- std::string exe_dir = gDirUtilp->getAppRODataDir();
- cmd = "SLVoice.exe";
- cmd += args;
- // So retarded. Windows requires that the second parameter to CreateProcessA be writable (non-const) string...
- char *args2 = new char[args.size() + 1];
- strcpy(args2, args.c_str());
- if(!CreateProcessA(exe_path.c_str(), args2, NULL, NULL, FALSE, 0, NULL, exe_dir.c_str(), &sinfo, &pinfo))
- {
- // DWORD dwErr = GetLastError();
- }
- else
- {
- // foo = pinfo.dwProcessId; // get your pid here if you want to use it later on
- // CloseHandle(pinfo.hProcess); // stops leaks - nothing else
- sGatewayHandle = pinfo.hProcess;
- CloseHandle(pinfo.hThread); // stops leaks - nothing else
- }
-
- delete[] args2;
- #else // LL_WINDOWS
- // This should be the same for mac and linux
- {
- std::vector<std::string> arglist;
- arglist.push_back(exe_path);
-
- // Split the argument string into separate strings for each argument
- typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
- boost::char_separator<char> sep(" ");
- tokenizer tokens(args, sep);
- tokenizer::iterator token_iter;
- for(token_iter = tokens.begin(); token_iter != tokens.end(); ++token_iter)
- {
- arglist.push_back(*token_iter);
- }
-
- // create an argv vector for the child process
- char **fakeargv = new char*[arglist.size() + 1];
- int i;
- for(i=0; i < arglist.size(); i++)
- fakeargv[i] = const_cast<char*>(arglist[i].c_str());
- fakeargv[i] = NULL;
-
- fflush(NULL); // flush all buffers before the child inherits them
- pid_t id = vfork();
- if(id == 0)
- {
- // child
- execv(exe_path.c_str(), fakeargv);
-
- // If we reach this point, the exec failed.
- // Use _exit() instead of exit() per the vfork man page.
- _exit(0);
- }
- // parent
- delete[] fakeargv;
- sGatewayPID = id;
- }
- #endif // LL_WINDOWS
- mDaemonHost = LLHost(gSavedSettings.getString("VivoxVoiceHost").c_str(), gSavedSettings.getU32("VivoxVoicePort"));
- }
- else
- {
- LL_INFOS("Voice") << exe_path << " not found." << LL_ENDL;
- }
- }
- else
- {
- // SLIM SDK: port changed from 44124 to 44125.
- // We can connect to a client gateway running on another host. This is useful for testing.
- // To do this, launch the gateway on a nearby host like this:
- // vivox-gw.exe -p tcp -i 0.0.0.0:44125
- // and put that host's IP address here.
- mDaemonHost = LLHost(gSavedSettings.getString("VivoxVoiceHost"), gSavedSettings.getU32("VivoxVoicePort"));
- }
- mUpdateTimer.start();
- mUpdateTimer.setTimerExpirySec(CONNECT_THROTTLE_SECONDS);
- setState(stateDaemonLaunched);
-
- // Dirty the states we'll need to sync with the daemon when it comes up.
- mMuteMicDirty = true;
- mMicVolumeDirty = true;
- mSpeakerVolumeDirty = true;
- mSpeakerMuteDirty = true;
- // These only need to be set if they're not default (i.e. empty string).
- mCaptureDeviceDirty = !mCaptureDevice.empty();
- mRenderDeviceDirty = !mRenderDevice.empty();
-
- mMainSessionGroupHandle.clear();
- }
- break;
- //MARK: stateDaemonLaunched
- case stateDaemonLaunched:
- if(mUpdateTimer.hasExpired())
- {
- LL_DEBUGS("Voice") << "Connecting to vivox daemon:" << mDaemonHost << LL_ENDL;
- mUpdateTimer.setTimerExpirySec(CONNECT_THROTTLE_SECONDS);
- if(!mSocket)
- {
- mSocket = LLSocket::create(gAPRPoolp, LLSocket::STREAM_TCP);
- }
-
- mConnected = mSocket->blockingConnect(mDaemonHost);
- if(mConnected)
- {
- setState(stateConnecting);
- }
- else
- {
- // If the connect failed, the socket may have been put into a bad state. Delete it.
- closeSocket();
- }
- }
- break;
- //MARK: stateConnecting
- case stateConnecting:
- // Can't do this until we have the pump available.
- if(mPump)
- {
- // MBW -- Note to self: pumps and pipes examples in
- // indra/test/io.cpp
- // indra/test/llpipeutil.{cpp|h}
- // Attach the pumps and pipes
-
- LLPumpIO::chain_t readChain;
- readChain.push_back(LLIOPipe::ptr_t(new LLIOSocketReader(mSocket)));
- readChain.push_back(LLIOPipe::ptr_t(new LLVivoxProtocolParser()));
- mPump->addChain(readChain, NEVER_CHAIN_EXPIRY_SECS);
- setState(stateConnected);
- }
- break;
-
- //MARK: stateConnected
- case stateConnected:
- // Initial devices query
- getCaptureDevicesSendMessage();
- getRenderDevicesSendMessage();
- mLoginRetryCount = 0;
- setState(stateIdle);
- break;
- //MARK: stateIdle
- case stateIdle:
- // This is the idle state where we're connected to the daemon but haven't set up a connector yet.
- if(mTuningMode)
- {
- mTuningExitState = stateIdle;
- setState(stateMicTuningStart);
- }
- else if(!mVoiceEnabled)
- {
- // We never started up the connector. This will shut down the daemon.
- setState(stateConnectorStopped);
- }
- else if(!mAccountName.empty())
- {
- if ( mAccountPassword.empty() )
- {
- requestVoiceAccountProvision();
- }
- }
- break;
- //MARK: stateMicTuningStart
- case stateMicTuningStart:
- if(mUpdateTimer.hasExpired())
- {
- if(mCaptureDeviceDirty || mRenderDeviceDirty)
- {
- // These can't be changed while in tuning mode. Set them before starting.
- std::ostringstream stream;
-
- buildSetCaptureDevice(stream);
- buildSetRenderDevice(stream);
- if(!stream.str().empty())
- {
- writeString(stream.str());
- }
- // This will come around again in the same state and start the capture, after the timer expires.
- mUpdateTimer.start();
- mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS);
- }
- else
- {
- // duration parameter is currently unused, per Mike S.
- tuningCaptureStartSendMessage(10000);
- setState(stateMicTuningRunning);
- }
- }
-
- break;
-
- //MARK: stateMicTuningRunning
- case stateMicTuningRunning:
- if(!mTuningMode || mCaptureDeviceDirty || mRenderDeviceDirty)
- {
- // All of these conditions make us leave tuning mode.
- setState(stateMicTuningStop);
- }
- else
- {
- // process mic/speaker volume changes
- if(mTuningMicVolumeDirty || mTuningSpeakerVolumeDirty)
- {
- std::ostringstream stream;
-
- if(mTuningMicVolumeDirty)
- {
- LL_INFOS("Voice") << "setting tuning mic level to " << mTuningMicVolume << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetMicLevel.1\">"
- << "<Level>" << mTuningMicVolume << "</Level>"
- << "</Request>\n\n\n";
- }
-
- if(mTuningSpeakerVolumeDirty)
- {
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.SetSpeakerLevel.1\">"
- << "<Level>" << mTuningSpeakerVolume << "</Level>"
- << "</Request>\n\n\n";
- }
-
- mTuningMicVolumeDirty = false;
- mTuningSpeakerVolumeDirty = false;
- if(!stream.str().empty())
- {
- writeString(stream.str());
- }
- }
- }
- break;
-
- //MARK: stateMicTuningStop
- case stateMicTuningStop:
- {
- // transition out of mic tuning
- tuningCaptureStopSendMessage();
-
- setState(mTuningExitState);
-
- // if we exited just to change devices, this will keep us from re-entering too fast.
- mUpdateTimer.start();
- mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS);
-
- }
- break;
- //MARK: stateCaptureBufferPaused
- case stateCaptureBufferPaused:
- if (!mCaptureBufferMode)
- {
- // Leaving capture mode.
- mCaptureBufferRecording = false;
- mCaptureBufferRecorded = false;
- mCaptureBufferPlaying = false;
- // Return to stateNoChannel to trigger reconnection to a channel.
- setState(stateNoChannel);
- }
- else if (mCaptureBufferRecording)
- {
- setState(stateCaptureBufferRecStart);
- }
- else if (mCaptureBufferPlaying)
- {
- setState(stateCaptureBufferPlayStart);
- }
- break;
- //MARK: stateCaptureBufferRecStart
- case stateCaptureBufferRecStart:
- captureBufferRecordStartSendMessage();
- // Flag that something is recorded to allow playback.
- mCaptureBufferRecorded = true;
- // Start the timer, recording will be stopped when it expires.
- mCaptureTimer.start();
- mCaptureTimer.setTimerExpirySec(CAPTURE_BUFFER_MAX_TIME);
- // Update UI, should really use a separate callback.
- notifyVoiceFontObservers();
- setState(stateCaptureBufferRecording);
- break;
- //MARK: stateCaptureBufferRecording
- case stateCaptureBufferRecording:
- if (!mCaptureBufferMode || !mCaptureBufferRecording ||
- mCaptureBufferPlaying || mCaptureTimer.hasExpired())
- {
- // Stop recording
- captureBufferRecordStopSendMessage();
- mCaptureBufferRecording = false;
- // Update UI, should really use a separate callback.
- notifyVoiceFontObservers();
- setState(stateCaptureBufferPaused);
- }
- break;
- //MARK: stateCaptureBufferPlayStart
- case stateCaptureBufferPlayStart:
- captureBufferPlayStartSendMessage(mPreviewVoiceFont);
- // Store the voice font being previewed, so that we know to restart if it changes.
- mPreviewVoiceFontLast = mPreviewVoiceFont;
- // Update UI, should really use a separate callback.
- notifyVoiceFontObservers();
- setState(stateCaptureBufferPlaying);
- break;
- //MARK: stateCaptureBufferPlaying
- case stateCaptureBufferPlaying:
- if (mCaptureBufferPlaying && mPreviewVoiceFont != mPreviewVoiceFontLast)
- {
- // If the preview voice font changes, restart playing with the new font.
- setState(stateCaptureBufferPlayStart);
- }
- else if (!mCaptureBufferMode || !mCaptureBufferPlaying || mCaptureBufferRecording)
- {
- // Stop playing.
- captureBufferPlayStopSendMessage();
- mCaptureBufferPlaying = false;
- // Update UI, should really use a separate callback.
- notifyVoiceFontObservers();
- setState(stateCaptureBufferPaused);
- }
- break;
- //MARK: stateConnectorStart
- case stateConnectorStart:
- if(!mVoiceEnabled)
- {
- // We were never logged in. This will shut down the connector.
- setState(stateLoggedOut);
- }
- else if(!mVoiceAccountServerURI.empty())
- {
- connectorCreate();
- }
- break;
-
- //MARK: stateConnectorStarting
- case stateConnectorStarting: // waiting for connector handle
- // connectorCreateResponse() will transition from here to stateConnectorStarted.
- break;
-
- //MARK: stateConnectorStarted
- case stateConnectorStarted: // connector handle received
- if(!mVoiceEnabled)
- {
- // We were never logged in. This will shut down the connector.
- setState(stateLoggedOut);
- }
- else
- {
- // The connector is started. Send a login message.
- setState(stateNeedsLogin);
- }
- break;
-
- //MARK: stateLoginRetry
- case stateLoginRetry:
- if(mLoginRetryCount == 0)
- {
- // First retry -- display a message to the user
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGIN_RETRY);
- }
-
- mLoginRetryCount++;
-
- if(mLoginRetryCount > MAX_LOGIN_RETRIES)
- {
- LL_WARNS("Voice") << "too many login retries, giving up." << LL_ENDL;
- setState(stateLoginFailed);
- LLSD args;
- std::stringstream errs;
- errs << mVoiceAccountServerURI << "\n:UDP: 3478, 3479, 5060, 5062, 12000-17000";
- args["HOSTID"] = errs.str();
- if (LLGridManager::getInstance()->isSystemGrid())
- {
- LLNotificationsUtil::add("NoVoiceConnect", args);
- }
- else
- {
- LLNotificationsUtil::add("NoVoiceConnect-GIAB", args);
- }
- }
- else
- {
- LL_INFOS("Voice") << "will retry login in " << LOGIN_RETRY_SECONDS << " seconds." << LL_ENDL;
- mUpdateTimer.start();
- mUpdateTimer.setTimerExpirySec(LOGIN_RETRY_SECONDS);
- setState(stateLoginRetryWait);
- }
- break;
-
- //MARK: stateLoginRetryWait
- case stateLoginRetryWait:
- if(mUpdateTimer.hasExpired())
- {
- setState(stateNeedsLogin);
- }
- break;
-
- //MARK: stateNeedsLogin
- case stateNeedsLogin:
- if(!mAccountPassword.empty())
- {
- setState(stateLoggingIn);
- loginSendMessage();
- }
- break;
-
- //MARK: stateLoggingIn
- case stateLoggingIn: // waiting for account handle
- // loginResponse() will transition from here to stateLoggedIn.
- break;
-
- //MARK: stateLoggedIn
- case stateLoggedIn: // account handle received
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LOGGED_IN);
- if (LLVoiceClient::instance().getVoiceEffectEnabled())
- {
- // Request the set of available voice fonts.
- setState(stateVoiceFontsWait);
- refreshVoiceEffectLists(true);
- }
- else
- {
- // If voice effects are disabled, pretend we've received them and carry on.
- setState(stateVoiceFontsReceived);
- }
- // request the current set of block rules (we'll need them when updating the friends list)
- accountListBlockRulesSendMessage();
-
- // request the current set of auto-accept rules
- accountListAutoAcceptRulesSendMessage();
-
- // Set up the mute list observer if it hasn't been set up already.
- if((!sMuteListListener_listening))
- {
- LLMuteList::getInstance()->addObserver(&mutelist_listener);
- sMuteListListener_listening = true;
- }
- // Set up the friends list observer if it hasn't been set up already.
- if(friendslist_listener == NULL)
- {
- friendslist_listener = new LLVivoxVoiceClientFriendsObserver;
- LLAvatarTracker::instance().addObserver(friendslist_listener);
- }
-
- // Set the initial state of mic mute, local speaker volume, etc.
- {
- std::ostringstream stream;
-
- buildLocalAudioUpdates(stream);
-
- if(!stream.str().empty())
- {
- writeString(stream.str());
- }
- }
- break;
- //MARK: stateVoiceFontsWait
- case stateVoiceFontsWait: // Await voice font list
- // accountGetSessionFontsResponse() will transition from here to
- // stateVoiceFontsReceived, to ensure we have the voice font list
- // before attempting to create a session.
- break;
-
- //MARK: stateVoiceFontsReceived
- case stateVoiceFontsReceived: // Voice font list received
- // Set up the timer to check for expiring voice fonts
- mVoiceFontExpiryTimer.start();
- mVoiceFontExpiryTimer.setTimerExpirySec(VOICE_FONT_EXPIRY_INTERVAL);
- #if USE_SESSION_GROUPS
- // create the main session group
- setState(stateCreatingSessionGroup);
- sessionGroupCreateSendMessage();
- #else
- setState(stateNoChannel);
- #endif
- break;
-
- //MARK: stateCreatingSessionGroup
- case stateCreatingSessionGroup:
- if(mSessionTerminateRequested || !mVoiceEnabled)
- {
- // *TODO: Question: is this the right way out of this state
- setState(stateSessionTerminated);
- }
- else if(!mMainSessionGroupHandle.empty())
- {
- // Start looped recording (needed for "panic button" anti-griefing tool)
- recordingLoopStart();
- setState(stateNoChannel);
- }
- break;
-
- //MARK: stateRetrievingParcelVoiceInfo
- case stateRetrievingParcelVoiceInfo:
- // wait until parcel voice info is received.
- if(mSessionTerminateRequested || !mVoiceEnabled)
- {
- // if a terminate request has been received,
- // bail and go to the stateSessionTerminated
- // state. If the cap request is still pending,
- // the responder will check to see if we've moved
- // to a new session and won't change any state.
- setState(stateSessionTerminated);
- }
- break;
-
-
- //MARK: stateNoChannel
- case stateNoChannel:
- LL_DEBUGS("Voice") << "State No Channel" << LL_ENDL;
- mSpatialJoiningNum = 0;
- // Do this here as well as inside sendPositionalUpdate().
- // Otherwise, if you log in but don't join a proximal channel (such as when your login location has voice disabled), your friends list won't sync.
- sendFriendsListUpdates();
-
- if(mSessionTerminateRequested || !mVoiceEnabled)
- {
- // TODO: Question: Is this the right way out of this state?
- setState(stateSessionTerminated);
- }
- else if(mTuningMode)
- {
- mTuningExitState = stateNoChannel;
- setState(stateMicTuningStart);
- }
- else if(mCaptureBufferMode)
- {
- setState(stateCaptureBufferPaused);
- }
- else if(checkParcelChanged() || (mNextAudioSession == NULL))
- {
- // the parcel is changed, or we have no pending audio sessions,
- // so try to request the parcel voice info
- // if we have the cap, we move to the appropriate state
- if(requestParcelVoiceInfo())
- {
- setState(stateRetrievingParcelVoiceInfo);
- }
- }
- else if(sessionNeedsRelog(mNextAudioSession))
- {
- requestRelog();
- setState(stateSessionTerminated);
- }
- else if(mNextAudioSession)
- {
- sessionState *oldSession = mAudioSession;
- mAudioSession = mNextAudioSession;
- mAudioSessionChanged = true;
- if(!mAudioSession->mReconnect)
- {
- mNextAudioSession = NULL;
- }
-
- // The old session may now need to be deleted.
- reapSession(oldSession);
-
- if(!mAudioSession->mHandle.empty())
- {
- // Connect to a session by session handle
- sessionMediaConnectSendMessage(mAudioSession);
- }
- else
- {
- // Connect to a session by URI
- sessionCreateSendMessage(mAudioSession, true, false);
- }
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINING);
- setState(stateJoiningSession);
- }
- break;
-
- //MARK: stateJoiningSession
- case stateJoiningSession: // waiting for session handle
-
- // If this is true we have problem with connection to voice server (EXT-4313).
- // See descriptions of mSpatialJoiningNum and MAX_NORMAL_JOINING_SPATIAL_NUM.
- if(mSpatialJoiningNum == MAX_NORMAL_JOINING_SPATIAL_NUM)
- {
- // Notify observers to let them know there is problem with voice
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_VOICE_DISABLED);
- llwarns << "There seems to be problem with connection to voice server. Disabling voice chat abilities." << llendl;
- }
-
- // Increase mSpatialJoiningNum only for spatial sessions- it's normal to reach this case for
- // example for p2p many times while waiting for response, so it can't be used to detect errors
- if(mAudioSession && mAudioSession->mIsSpatial)
- {
-
- mSpatialJoiningNum++;
- }
-
- // joinedAudioSession() will transition from here to stateSessionJoined.
- if(!mVoiceEnabled)
- {
- // User bailed out during connect -- jump straight to teardown.
- setState(stateSessionTerminated);
- }
- else if(mSessionTerminateRequested)
- {
- if(mAudioSession && !mAudioSession->mHandle.empty())
- {
- // Only allow direct exits from this state in p2p calls (for cancelling an invite).
- // Terminating a half-connected session on other types of calls seems to break something in the vivox gateway.
- if(mAudioSession->mIsP2P)
- {
- sessionMediaDisconnectSendMessage(mAudioSession);
- setState(stateSessionTerminated);
- }
- }
- }
- break;
-
- //MARK: stateSessionJoined
- case stateSessionJoined: // session handle received
- mSpatialJoiningNum = 0;
- // It appears that I need to wait for BOTH the SessionGroup.AddSession response and the SessionStateChangeEvent with state 4
- // before continuing from this state. They can happen in either order, and if I don't wait for both, things can get stuck.
- // For now, the SessionGroup.AddSession response handler sets mSessionHandle and the SessionStateChangeEvent handler transitions to stateSessionJoined.
- // This is a cheap way to make sure both have happened before proceeding.
- if(mAudioSession && mAudioSession->mVoiceEnabled)
- {
- // Dirty state that may need to be sync'ed with the daemon.
- mMuteMicDirty = true;
- mSpeakerVolumeDirty = true;
- mSpatialCoordsDirty = true;
-
- setState(stateRunning);
-
- // Start the throttle timer
- mUpdateTimer.start();
- mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS);
- // Events that need to happen when a session is joined could go here.
- // Maybe send initial spatial data?
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_JOINED);
- }
- else if(!mVoiceEnabled)
- {
- // User bailed out during connect -- jump straight to teardown.
- setState(stateSessionTerminated);
- }
- else if(mSessionTerminateRequested)
- {
- // Only allow direct exits from this state in p2p calls (for cancelling an invite).
- // Terminating a half-connected session on other types of calls seems to break something in the vivox gateway.
- if(mAudioSession && mAudioSession->mIsP2P)
- {
- sessionMediaDisconnectSendMessage(mAudioSession);
- setState(stateSessionTerminated);
- }
- }
- break;
-
- //MARK: stateRunning
- case stateRunning: // steady state
- // Disabling voice or disconnect requested.
- if(!mVoiceEnabled || mSessionTerminateRequested)
- {
- leaveAudioSession();
- }
- else
- {
-
- if(!inSpatialChannel())
- {
- // When in a non-spatial channel, never send positional updates.
- mSpatialCoordsDirty = false;
- }
- else
- {
- if(checkParcelChanged())
- {
- // if the parcel has changed, attempted to request the
- // cap for the parcel voice info. If we can't request it
- // then we don't have the cap URL so we do nothing and will
- // recheck next time around
- if(requestParcelVoiceInfo())
- {
- // we did get the cap, and we made the request,
- // so go wait for the response.
- setState(stateRetrievingParcelVoiceInfo);
- }
- }
- // Do the calculation that enforces the listener<->speaker tether (and also updates the real camera position)
- enforceTether();
-
- }
-
- // Do notifications for expiring Voice Fonts.
- if (mVoiceFontExpiryTimer.hasExpired())
- {
- expireVoiceFonts();
- mVoiceFontExpiryTimer.setTimerExpirySec(VOICE_FONT_EXPIRY_INTERVAL);
- }
- // Send an update only if the ptt or mute 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.
- // Sending for every volume update causes an excessive flood of messages whenever a volume slider is dragged.
- if((mAudioSession && mAudioSession->mMuteDirty) || mMuteMicDirty || mUpdateTimer.hasExpired())
- {
- mUpdateTimer.setTimerExpirySec(UPDATE_THROTTLE_SECONDS);
- sendPositionalUpdate();
- }
- }
- break;
-
- //MARK: stateLeavingSession
- case stateLeavingSession: // waiting for terminate session response
- // The handler for the Session.Terminate response will transition from here to stateSessionTerminated.
- break;
- //MARK: stateSessionTerminated
- case stateSessionTerminated:
-
- // Must do this first, since it uses mAudioSession.
- notifyStatusObservers(LLVoiceClientStatusObserver::STATUS_LEFT_CHANNEL);
-
- if(mAudioSession)
- {
- sessionState *oldSession = mAudioSession;
- mAudioSession = NULL;
- // We just notified status observers about this change. Don't do it again.
- mAudioSessionChanged = false;
- // The old session may now need to be deleted.
- reapSession(oldSession);
- }
- else
- {
- LL_WARNS("Voice") << "stateSessionTerminated with NULL mAudioSession" << LL_ENDL;
- }
-
- // Always reset the terminate request flag when we get here.
- mSessionTerminateRequested = false;
- if(mVoiceEnabled && !mRelogRequested)
- {
- // Just leaving a channel, go back to stateNoChannel (the "logged in but have no channel" state).
- setState(stateNoChannel);
- }
- else
- {
- // Shutting down voice, continue with disconnecting.
- logout();
-
- // The state machine will take it from here
- mRelogRequested = false;
- }
-
- break;
-
- //MARK: stateLoggingOut
- case stateLoggingOut: // waiting for logout response
- // The handler for the AccountLoginStateChangeEvent will transition from here to stateLoggedOut.
- break;
-
- //MARK: stateLoggedOut
- case stateLoggedOut: // logout response received
-
- // Once we're logged out, these things are invalid.
- mAccountHandle.clear();
- cleanUp();
- if(mVoiceEnabled && !mRelogRequested)
- {
- // User was logged out, but wants to be logged in. Send a new login request.
- setState(stateNeedsLogin);
- }
- else
- {
- // shut down the connector
- connectorShutdown();
- }
- break;
-
- //MARK: stateConnectorStopping
- case stateConnectorStopping: // waiting for connector stop
- // The handler for the Connector.InitiateShutdown response will transition from here to stateConnectorStopped.
- break;
- //MARK: stateConnectorStopped
- case stateConnectorStopped: // connector stop received
- setState(stateDisableCleanup);
- break;
- //MARK: stateConnectorFailed
- case stateConnectorFailed:
- setState(stateConnectorFailedWaiting);
- break;
- //MARK: stateConnectorFailedWaiting
- case stateConnectorFailedWaiting:
- if(!mVoiceEnabled)
- {
- setState(stateDisableCleanup);
- }
- break;
- //MARK: stateLoginFailed
- case stateLoginFailed:
- setState(stateLoginFailedWaiting);
- break;
- //MARK: stateLoginFailedWaiting
- case stateLoginFailedWaiting:
- if(!mVoiceEnabled)
- {
- setState(stateDisableCleanup);
- }
- break;
- //MARK: stateJoinSessionFailed
- case stateJoinSessionFailed:
- // Transition to error state. Send out any notifications here.
- if(mAudioSession)
- {
- LL_WARNS("Voice") << "stateJoinSessionFailed: (" << mAudioSession->mErrorStatusCode << "): " << mAudioSession->mErrorStatusString << LL_ENDL;
- }
- else
- {
- LL_WARNS("Voice") << "stateJoinSessionFailed with no current session" << LL_ENDL;
- }
-
- notifyStatusObservers(LLVoiceClientStatusObserver::ERROR_UNKNOWN);
- setState(stateJoinSessionFailedWaiting);
- break;
-
- //MARK: stateJoinSessionFailedWaiting
- case stateJoinSessionFailedWaiting:
- // Joining a channel failed, either due to a failed channel name -> sip url lookup or an error from the join message.
- // Region crossings may leave this state and try the join again.
- if(mSessionTerminateRequested)
- {
- setState(stateSessionTerminated);
- }
- break;
-
- //MARK: stateJail
- case stateJail:
- // We have given up. Do nothing.
- break;
- }
-
- if (mAudioSessionChanged)
- {
- mAudioSessionChanged = false;
- notifyParticipantObservers();
- notifyVoiceFontObservers();
- }
- else if (mAudioSession && mAudioSession->mParticipantsChanged)
- {
- mAudioSession->mParticipantsChanged = false;
- notifyParticipantObservers();
- }
- }
- void LLVivoxVoiceClient::closeSocket(void)
- {
- mSocket.reset();
- mConnected = false;
- mConnectorHandle.clear();
- mAccountHandle.clear();
- }
- void LLVivoxVoiceClient::loginSendMessage()
- {
- std::ostringstream stream;
- bool autoPostCrashDumps = gSavedSettings.getBOOL("VivoxAutoPostCrashDumps");
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.Login.1\">"
- << "<ConnectorHandle>" << mConnectorHandle << "</ConnectorHandle>"
- << "<AccountName>" << mAccountName << "</AccountName>"
- << "<AccountPassword>" << mAccountPassword << "</AccountPassword>"
- << "<AudioSessionAnswerMode>VerifyAnswer</AudioSessionAnswerMode>"
- << "<EnableBuddiesAndPresence>true</EnableBuddiesAndPresence>"
- << "<BuddyManagementMode>Application</BuddyManagementMode>"
- << "<ParticipantPropertyFrequency>5</ParticipantPropertyFrequency>"
- << (autoPostCrashDumps?"<AutopostCrashDumps>true</AutopostCrashDumps>":"")
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::logout()
- {
- // Ensure that we'll re-request provisioning before logging in again
- mAccountPassword.clear();
- mVoiceAccountServerURI.clear();
-
- setState(stateLoggingOut);
- logoutSendMessage();
- }
- void LLVivoxVoiceClient::logoutSendMessage()
- {
- if(!mAccountHandle.empty())
- {
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.Logout.1\">"
- << "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
- << "</Request>"
- << "\n\n\n";
- mAccountHandle.clear();
- writeString(stream.str());
- }
- }
- void LLVivoxVoiceClient::accountListBlockRulesSendMessage()
- {
- if(!mAccountHandle.empty())
- {
- std::ostringstream stream;
- LL_DEBUGS("Voice") << "requesting block rules" << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.ListBlockRules.1\">"
- << "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
- << "</Request>"
- << "\n\n\n";
- writeString(stream.str());
- }
- }
- void LLVivoxVoiceClient::accountListAutoAcceptRulesSendMessage()
- {
- if(!mAccountHandle.empty())
- {
- std::ostringstream stream;
- LL_DEBUGS("Voice") << "requesting auto-accept rules" << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Account.ListAutoAcceptRules.1\">"
- << "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
- << "</Request>"
- << "\n\n\n";
- writeString(stream.str());
- }
- }
- void LLVivoxVoiceClient::sessionGroupCreateSendMessage()
- {
- if(!mAccountHandle.empty())
- {
- std::ostringstream stream;
- LL_DEBUGS("Voice") << "creating session group" << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.Create.1\">"
- << "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
- << "<Type>Normal</Type>"
- << "</Request>"
- << "\n\n\n";
- writeString(stream.str());
- }
- }
- void LLVivoxVoiceClient::sessionCreateSendMessage(sessionState *session, bool startAudio, bool startText)
- {
- LL_DEBUGS("Voice") << "Requesting create: " << session->mSIPURI << LL_ENDL;
- S32 font_index = getVoiceFontIndex(session->mVoiceFontID);
- LL_DEBUGS("Voice") << "With voice font: " << session->mVoiceFontID << " (" << font_index << ")" << LL_ENDL;
- session->mCreateInProgress = true;
- if(startAudio)
- {
- session->mMediaConnectInProgress = true;
- }
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << session->mSIPURI << "\" action=\"Session.Create.1\">"
- << "<AccountHandle>" << mAccountHandle << "</AccountHandle>"
- << "<URI>" << session->mSIPURI << "</URI>";
- static const std::string allowed_chars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- "0123456789"
- "-._~";
- if(!session->mHash.empty())
- {
- stream
- << "<Password>" << LLURI::escape(session->mHash, allowed_chars) << "</Password>"
- << "<PasswordHashAlgorithm>SHA1UserName</PasswordHashAlgorithm>";
- }
- stream
- << "<ConnectAudio>" << (startAudio?"true":"false") << "</ConnectAudio>"
- << "<ConnectText>" << (startText?"true":"false") << "</ConnectText>"
- << "<VoiceFontID>" << font_index << "</VoiceFontID>"
- << "<Name>" << mChannelName << "</Name>"
- << "</Request>\n\n\n";
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionGroupAddSessionSendMessage(sessionState *session, bool startAudio, bool startText)
- {
- LL_DEBUGS("Voice") << "Requesting create: " << session->mSIPURI << LL_ENDL;
- S32 font_index = getVoiceFontIndex(session->mVoiceFontID);
- LL_DEBUGS("Voice") << "With voice font: " << session->mVoiceFontID << " (" << font_index << ")" << LL_ENDL;
- session->mCreateInProgress = true;
- if(startAudio)
- {
- session->mMediaConnectInProgress = true;
- }
-
- std::string password;
- if(!session->mHash.empty())
- {
- static const std::string allowed_chars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- "0123456789"
- "-._~"
- ;
- password = LLURI::escape(session->mHash, allowed_chars);
- }
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << session->mSIPURI << "\" action=\"SessionGroup.AddSession.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "<URI>" << session->mSIPURI << "</URI>"
- << "<Name>" << mChannelName << "</Name>"
- << "<ConnectAudio>" << (startAudio?"true":"false") << "</ConnectAudio>"
- << "<ConnectText>" << (startText?"true":"false") << "</ConnectText>"
- << "<VoiceFontID>" << font_index << "</VoiceFontID>"
- << "<Password>" << password << "</Password>"
- << "<PasswordHashAlgorithm>SHA1UserName</PasswordHashAlgorithm>"
- << "</Request>\n\n\n"
- ;
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionMediaConnectSendMessage(sessionState *session)
- {
- LL_DEBUGS("Voice") << "Connecting audio to session handle: " << session->mHandle << LL_ENDL;
- S32 font_index = getVoiceFontIndex(session->mVoiceFontID);
- LL_DEBUGS("Voice") << "With voice font: " << session->mVoiceFontID << " (" << font_index << ")" << LL_ENDL;
- session->mMediaConnectInProgress = true;
-
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << session->mHandle << "\" action=\"Session.MediaConnect.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "<SessionHandle>" << session->mHandle << "</SessionHandle>"
- << "<VoiceFontID>" << font_index << "</VoiceFontID>"
- << "<Media>Audio</Media>"
- << "</Request>\n\n\n";
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionTextConnectSendMessage(sessionState *session)
- {
- LL_DEBUGS("Voice") << "connecting text to session handle: " << session->mHandle << LL_ENDL;
-
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << session->mHandle << "\" action=\"Session.TextConnect.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "<SessionHandle>" << session->mHandle << "</SessionHandle>"
- << "</Request>\n\n\n";
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionTerminate()
- {
- mSessionTerminateRequested = true;
- }
- void LLVivoxVoiceClient::requestRelog()
- {
- mSessionTerminateRequested = true;
- mRelogRequested = true;
- }
- void LLVivoxVoiceClient::leaveAudioSession()
- {
- if(mAudioSession)
- {
- LL_DEBUGS("Voice") << "leaving session: " << mAudioSession->mSIPURI << LL_ENDL;
- switch(getState())
- {
- case stateNoChannel:
- // In this case, we want to pretend the join failed so our state machine doesn't get stuck.
- // Skip the join failed transition state so we don't send out error notifications.
- setState(stateJoinSessionFailedWaiting);
- break;
- case stateJoiningSession:
- case stateSessionJoined:
- case stateRunning:
- if(!mAudioSession->mHandle.empty())
- {
- #if RECORD_EVERYTHING
- // HACK: for testing only
- // Save looped recording
- std::string savepath("/tmp/vivoxrecording");
- {
- time_t now = time(NULL);
- const size_t BUF_SIZE = 64;
- char time_str[BUF_SIZE]; /* Flawfinder: ignore */
-
- strftime(time_str, BUF_SIZE, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));
- savepath += time_str;
- }
- recordingLoopSave(savepath);
- #endif
- sessionMediaDisconnectSendMessage(mAudioSession);
- setState(stateLeavingSession);
- }
- else
- {
- LL_WARNS("Voice") << "called with no session handle" << LL_ENDL;
- setState(stateSessionTerminated);
- }
- break;
- case stateJoinSessionFailed:
- case stateJoinSessionFailedWaiting:
- setState(stateSessionTerminated);
- break;
-
- default:
- LL_WARNS("Voice") << "called from unknown state" << LL_ENDL;
- break;
- }
- }
- else
- {
- LL_WARNS("Voice") << "called with no active session" << LL_ENDL;
- setState(stateSessionTerminated);
- }
- }
- void LLVivoxVoiceClient::sessionTerminateSendMessage(sessionState *session)
- {
- std::ostringstream stream;
-
- LL_DEBUGS("Voice") << "Sending Session.Terminate with handle " << session->mHandle << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.Terminate.1\">"
- << "<SessionHandle>" << session->mHandle << "</SessionHandle>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionGroupTerminateSendMessage(sessionState *session)
- {
- std::ostringstream stream;
-
- LL_DEBUGS("Voice") << "Sending SessionGroup.Terminate with handle " << session->mGroupHandle << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"SessionGroup.Terminate.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::sessionMediaDisconnectSendMessage(sessionState *session)
- {
- std::ostringstream stream;
-
- LL_DEBUGS("Voice") << "Sending Session.MediaDisconnect with handle " << session->mHandle << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.MediaDisconnect.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "<SessionHandle>" << session->mHandle << "</SessionHandle>"
- << "<Media>Audio</Media>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
-
- }
- void LLVivoxVoiceClient::sessionTextDisconnectSendMessage(sessionState *session)
- {
- std::ostringstream stream;
-
- LL_DEBUGS("Voice") << "Sending Session.TextDisconnect with handle " << session->mHandle << LL_ENDL;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Session.TextDisconnect.1\">"
- << "<SessionGroupHandle>" << session->mGroupHandle << "</SessionGroupHandle>"
- << "<SessionHandle>" << session->mHandle << "</SessionHandle>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::getCaptureDevicesSendMessage()
- {
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.GetCaptureDevices.1\">"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::getRenderDevicesSendMessage()
- {
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.GetRenderDevices.1\">"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::clearCaptureDevices()
- {
- LL_DEBUGS("Voice") << "called" << LL_ENDL;
- mCaptureDevices.clear();
- }
- void LLVivoxVoiceClient::addCaptureDevice(const std::string& name)
- {
- LL_DEBUGS("Voice") << name << LL_ENDL;
- mCaptureDevices.push_back(name);
- }
- LLVoiceDeviceList& LLVivoxVoiceClient::getCaptureDevices()
- {
- return mCaptureDevices;
- }
- void LLVivoxVoiceClient::setCaptureDevice(const std::string& name)
- {
- if(name == "Default")
- {
- if(!mCaptureDevice.empty())
- {
- mCaptureDevice.clear();
- mCaptureDeviceDirty = true;
- }
- }
- else
- {
- if(mCaptureDevice != name)
- {
- mCaptureDevice = name;
- mCaptureDeviceDirty = true;
- }
- }
- }
- void LLVivoxVoiceClient::clearRenderDevices()
- {
- LL_DEBUGS("Voice") << "called" << LL_ENDL;
- mRenderDevices.clear();
- }
- void LLVivoxVoiceClient::addRenderDevice(const std::string& name)
- {
- LL_DEBUGS("Voice") << name << LL_ENDL;
- mRenderDevices.push_back(name);
- }
- LLVoiceDeviceList& LLVivoxVoiceClient::getRenderDevices()
- {
- return mRenderDevices;
- }
- void LLVivoxVoiceClient::setRenderDevice(const std::string& name)
- {
- if(name == "Default")
- {
- if(!mRenderDevice.empty())
- {
- mRenderDevice.clear();
- mRenderDeviceDirty = true;
- }
- }
- else
- {
- if(mRenderDevice != name)
- {
- mRenderDevice = name;
- mRenderDeviceDirty = true;
- }
- }
-
- }
- void LLVivoxVoiceClient::tuningStart()
- {
- mTuningMode = true;
- LL_DEBUGS("Voice") << "Starting tuning" << LL_ENDL;
- if(getState() >= stateNoChannel)
- {
- LL_DEBUGS("Voice") << "no channel" << LL_ENDL;
- sessionTerminate();
- }
- }
- void LLVivoxVoiceClient::tuningStop()
- {
- mTuningMode = false;
- }
- bool LLVivoxVoiceClient::inTuningMode()
- {
- bool result = false;
- switch(getState())
- {
- case stateMicTuningRunning:
- result = true;
- break;
- default:
- break;
- }
- return result;
- }
- void LLVivoxVoiceClient::tuningRenderStartSendMessage(const std::string& name, bool loop)
- {
- mTuningAudioFile = name;
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.RenderAudioStart.1\">"
- << "<SoundFilePath>" << mTuningAudioFile << "</SoundFilePath>"
- << "<Loop>" << (loop?"1":"0") << "</Loop>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::tuningRenderStopSendMessage()
- {
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.RenderAudioStop.1\">"
- << "<SoundFilePath>" << mTuningAudioFile << "</SoundFilePath>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::tuningCaptureStartSendMessage(int duration)
- {
- LL_DEBUGS("Voice") << "sending CaptureAudioStart" << LL_ENDL;
-
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.CaptureAudioStart.1\">"
- << "<Duration>" << duration << "</Duration>"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- }
- void LLVivoxVoiceClient::tuningCaptureStopSendMessage()
- {
- LL_DEBUGS("Voice") << "sending CaptureAudioStop" << LL_ENDL;
-
- std::ostringstream stream;
- stream
- << "<Request requestId=\"" << mCommandCookie++ << "\" action=\"Aux.CaptureAudioStop.1\">"
- << "</Request>\n\n\n";
-
- writeString(stream.str());
- mTuningEnergy = 0.0f;
- }
- void LLVivoxVoiceClient::tuningSetMicVolume(float volume)
- {
- int scaled_volume = scale_mic_volume(volume);
- if(scaled_volume != mTuningMicVolume)
- {
- mTuningMicVolume = scaled_volume;
- mTuningMicVolumeDirty = true;
- }
- }
- void LLVivoxVoiceClient::tuningSetSpeakerVolume(float volume)
- {
- int scaled_volume = scale_speaker_volume(volume);
- if(scaled_volume != mTuningSpeakerVolume)
- {
- mTuningSpeakerVolume = scaled_volume;
- mTuningSpeakerVolumeDirty = true;
- }
- }
-
- float LLVivoxVoiceClient::tuningGetEnergy(void)
- {
- return mTuningEnergy;
- }
- bool LLVivoxVoiceClient::deviceSettingsAvailable()
- {
- bool result = true;
-
- if(!mConnected)
- result = false;
-
- if(mRenderDevices.empty())
- result = false;
-
- return result;
- }
- void LLVivoxVoiceClient::refreshDeviceLists(bool clearCurrentList)
- {
- if(clearCurrentList)
- {
- clearCaptureDevices();
- clearRenderDevices();
- }
- getCaptureDevicesSendMessage();
- getRenderDevicesSendMessage();
- }
- void LLVivoxVoiceClient::daemonDied()
- {
- // The daemon died, so the connection is gone. Reset everything and start over.
- LL_WARNS("Voice") << "Connection to vivox daemon lost. Resetting state."<< LL_ENDL;
- // Try to relaunch the daemon
- setState(stateDisableCleanup);
- }
- void LLVivoxVoiceClient::giveUp()
- {
- // All has failed. Clean up and stop trying.
- closeSocket();
- cleanUp();
-
- setState(stateJail);
- }
- static void oldSDKTransform (LLVector3 &left, LLVector3 &up, LLVector3 &at, LLVector3d &pos, LLVector3 &vel)
- {
- F32 nat[3], nup[3], nl[3], nvel[3]; // the new at, up, left vectors and the new position and velocity
- F64 npos[3];
-
- // The original XML command was sent like this:
- /*
- << "<Position>"
- << "<X>" << pos[VX] << "</X>"
- << "<Y>" << pos[VZ] << "</Y>"
- << "<Z>" << pos[VY] << "</Z>"
- << "</Position>"
- << "<Velocity>"
- << "<X>" << mAvatarVelocity[VX] << "</X>"
- << "<Y>" << mAvatarVelocity[VZ] << "</Y>"
- << "<Z>" << mAvatarVelocity[VY] << "</Z>"
- << "</Velocity>"
- << "<AtOrientation>"
- << "<X>" << l.mV[VX] << "</X>"
- << "<Y>" << u.mV[VX] << "</Y>"
- << "<Z>" << a.mV[VX] << "</Z>"
- << "</AtOrientation>"
- << "<UpOrientation>"
- << "<X>" << l.mV[VZ] << "</X>"
- << "<Y>" << u.mV[VY] << "</Y>"
- << "<Z>" << a.mV[VZ] << "</Z>"
- << "</UpOrientation>"
- << "<LeftOrientation>"
- << "<X>" << l.mV [VY] << "</X>"
- << "<Y>" << u.mV [VZ] << "</Y>"
- << "<Z>" << a.mV [VY] << "</Z>"
-