/* * Copyright (c) 2006-2014, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Net.Sockets; using System.Text; using System.IO; using System.Xml; using System.Threading; using OpenMetaverse; using OpenMetaverse.StructuredData; using OpenMetaverse.Http; using OpenMetaverse.Interfaces; using OpenMetaverse.Messages.Linden; namespace OpenMetaverse.Utilities { public enum VoiceStatus { StatusLoginRetry, StatusLoggedIn, StatusJoining, StatusJoined, StatusLeftChannel, BeginErrorStatus, ErrorChannelFull, ErrorChannelLocked, ErrorNotAvailable, ErrorUnknown } public enum VoiceServiceType { /// Unknown voice service level Unknown, /// Spatialized local chat TypeA, /// Remote multi-party chat TypeB, /// One-to-one and small group chat TypeC } public partial class VoiceManager { public const int VOICE_MAJOR_VERSION = 1; public const string DAEMON_ARGS = " -p tcp -h -c -ll "; public const int DAEMON_LOG_LEVEL = 1; public const int DAEMON_PORT = 44124; public const string VOICE_RELEASE_SERVER = "bhr.vivox.com"; public const string VOICE_DEBUG_SERVER = "bhd.vivox.com"; public const string REQUEST_TERMINATOR = "\n\n\n"; public delegate void LoginStateChangeCallback(int cookie, string accountHandle, int statusCode, string statusString, int state); public delegate void NewSessionCallback(int cookie, string accountHandle, string eventSessionHandle, int state, string nameString, string uriString); public delegate void SessionStateChangeCallback(int cookie, string uriString, int statusCode, string statusString, string eventSessionHandle, int state, bool isChannel, string nameString); public delegate void ParticipantStateChangeCallback(int cookie, string uriString, int statusCode, string statusString, int state, string nameString, string displayNameString, int participantType); public delegate void ParticipantPropertiesCallback(int cookie, string uriString, int statusCode, string statusString, bool isLocallyMuted, bool isModeratorMuted, bool isSpeaking, int volume, float energy); public delegate void AuxAudioPropertiesCallback(int cookie, float energy); public delegate void BasicActionCallback(int cookie, int statusCode, string statusString); public delegate void ConnectorCreatedCallback(int cookie, int statusCode, string statusString, string connectorHandle); public delegate void LoginCallback(int cookie, int statusCode, string statusString, string accountHandle); public delegate void SessionCreatedCallback(int cookie, int statusCode, string statusString, string sessionHandle); public delegate void DevicesCallback(int cookie, int statusCode, string statusString, string currentDevice); public delegate void ProvisionAccountCallback(string username, string password); public delegate void ParcelVoiceInfoCallback(string regionName, int localID, string channelURI); public event LoginStateChangeCallback OnLoginStateChange; public event NewSessionCallback OnNewSession; public event SessionStateChangeCallback OnSessionStateChange; public event ParticipantStateChangeCallback OnParticipantStateChange; public event ParticipantPropertiesCallback OnParticipantProperties; public event AuxAudioPropertiesCallback OnAuxAudioProperties; public event ConnectorCreatedCallback OnConnectorCreated; public event LoginCallback OnLogin; public event SessionCreatedCallback OnSessionCreated; public event BasicActionCallback OnSessionConnected; public event BasicActionCallback OnAccountLogout; public event BasicActionCallback OnConnectorInitiateShutdown; public event BasicActionCallback OnAccountChannelGetList; public event BasicActionCallback OnSessionTerminated; public event DevicesCallback OnCaptureDevices; public event DevicesCallback OnRenderDevices; public event ProvisionAccountCallback OnProvisionAccount; public event ParcelVoiceInfoCallback OnParcelVoiceInfo; public GridClient Client; public string VoiceServer = VOICE_RELEASE_SERVER; public bool Enabled; protected Voice.TCPPipe _DaemonPipe; protected VoiceStatus _Status; protected int _CommandCookie = 0; protected string _TuningSoundFile = String.Empty; protected Dictionary _ChannelMap = new Dictionary(); protected List _CaptureDevices = new List(); protected List _RenderDevices = new List(); #region Response Processing Variables private bool isEvent = false; private bool isChannel = false; private bool isLocallyMuted = false; private bool isModeratorMuted = false; private bool isSpeaking = false; private int cookie = 0; //private int returnCode = 0; private int statusCode = 0; private int volume = 0; private int state = 0; private int participantType = 0; private float energy = 0f; private string statusString = String.Empty; //private string uuidString = String.Empty; private string actionString = String.Empty; private string connectorHandle = String.Empty; private string accountHandle = String.Empty; private string sessionHandle = String.Empty; private string eventSessionHandle = String.Empty; private string eventTypeString = String.Empty; private string uriString = String.Empty; private string nameString = String.Empty; //private string audioMediaString = String.Empty; private string displayNameString = String.Empty; #endregion Response Processing Variables public VoiceManager(GridClient client) { Client = client; Client.Network.RegisterEventCallback("RequiredVoiceVersion", new Caps.EventQueueCallback(RequiredVoiceVersionEventHandler)); // Register callback handlers for the blocking functions RegisterCallbacks(); Enabled = true; } public bool IsDaemonRunning() { throw new NotImplementedException(); } public bool StartDaemon() { throw new NotImplementedException(); } public void StopDaemon() { throw new NotImplementedException(); } public bool ConnectToDaemon() { if (!Enabled) return false; return ConnectToDaemon("127.0.0.1", DAEMON_PORT); } public bool ConnectToDaemon(string address, int port) { if (!Enabled) return false; _DaemonPipe = new Voice.TCPPipe(); _DaemonPipe.OnDisconnected += new Voice.TCPPipe.OnDisconnectedCallback(_DaemonPipe_OnDisconnected); _DaemonPipe.OnReceiveLine += new Voice.TCPPipe.OnReceiveLineCallback(_DaemonPipe_OnReceiveLine); SocketException se = _DaemonPipe.Connect(address, port); if (se == null) { return true; } else { Console.WriteLine("Connection failed: " + se.Message); return false; } } public Dictionary GetChannelMap() { return new Dictionary(_ChannelMap); } public List CurrentCaptureDevices() { return new List(_CaptureDevices); } public List CurrentRenderDevices() { return new List(_RenderDevices); } public string VoiceAccountFromUUID(UUID id) { string result = "x" + Convert.ToBase64String(id.GetBytes()); return result.Replace('+', '-').Replace('/', '_'); } public UUID UUIDFromVoiceAccount(string accountName) { if (accountName.Length == 25 && accountName[0] == 'x' && accountName[23] == '=' && accountName[24] == '=') { accountName = accountName.Replace('/', '_').Replace('+', '-'); byte[] idBytes = Convert.FromBase64String(accountName); if (idBytes.Length == 16) return new UUID(idBytes, 0); else return UUID.Zero; } else { return UUID.Zero; } } public string SIPURIFromVoiceAccount(string account) { return String.Format("sip:{0}@{1}", account, VoiceServer); } public int RequestCaptureDevices() { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}", _CommandCookie++, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestCaptureDevices() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestRenderDevices() { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}", _CommandCookie++, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestRenderDevices() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestCreateConnector() { return RequestCreateConnector(VoiceServer); } public int RequestCreateConnector(string voiceServer) { if (_DaemonPipe.Connected) { VoiceServer = voiceServer; string accountServer = String.Format("https://www.{0}/api2/", VoiceServer); string logPath = "."; StringBuilder request = new StringBuilder(); request.Append(String.Format("", _CommandCookie++)); request.Append("V2 SDK"); request.Append(String.Format("{0}", accountServer)); request.Append(""); request.Append("false"); request.Append(String.Format("{0}", logPath)); request.Append("vivox-gateway"); request.Append(".log"); request.Append("0"); request.Append(""); request.Append(""); request.Append(REQUEST_TERMINATOR); _DaemonPipe.SendData(Encoding.ASCII.GetBytes(request.ToString())); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.CreateConnector() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } private bool RequestVoiceInternal(string me, CapsClient.CompleteCallback callback, string capsName) { if (Enabled && Client.Network.Connected) { if (Client.Network.CurrentSim != null && Client.Network.CurrentSim.Caps != null) { Uri url = Client.Network.CurrentSim.Caps.CapabilityURI(capsName); if (url != null) { CapsClient request = new CapsClient(url); OSDMap body = new OSDMap(); request.OnComplete += new CapsClient.CompleteCallback(callback); request.BeginGetResponse(body, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); return true; } else { Logger.Log("VoiceManager." + me + "(): " + capsName + " capability is missing", Helpers.LogLevel.Info, Client); return false; } } } Logger.Log("VoiceManager.RequestVoiceInternal(): Voice system is currently disabled", Helpers.LogLevel.Info, Client); return false; } public bool RequestProvisionAccount() { return RequestVoiceInternal("RequestProvisionAccount", ProvisionCapsResponse, "ProvisionVoiceAccountRequest"); } public bool RequestParcelVoiceInfo() { return RequestVoiceInternal("RequestParcelVoiceInfo", ParcelVoiceInfoResponse, "ParcelVoiceInfoRequest"); } public int RequestLogin(string accountName, string password, string connectorHandle) { if (_DaemonPipe.Connected) { StringBuilder request = new StringBuilder(); request.Append(String.Format("", _CommandCookie++)); request.Append(String.Format("{0}", connectorHandle)); request.Append(String.Format("{0}", accountName)); request.Append(String.Format("{0}", password)); request.Append("VerifyAnswer"); request.Append(""); request.Append("10"); request.Append("false"); request.Append(""); request.Append(REQUEST_TERMINATOR); _DaemonPipe.SendData(Encoding.ASCII.GetBytes(request.ToString())); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.Login() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestSetRenderDevice(string deviceName) { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}", _CommandCookie, deviceName, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestSetRenderDevice() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestStartTuningMode(int duration) { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}", _CommandCookie, duration, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestStartTuningMode() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestStopTuningMode() { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}", _CommandCookie, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestStopTuningMode() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return _CommandCookie - 1; } } public int RequestSetSpeakerVolume(int volume) { if (volume < 0 || volume > 100) throw new ArgumentException("volume must be between 0 and 100", "volume"); if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}", _CommandCookie, volume, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestSetSpeakerVolume() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestSetCaptureVolume(int volume) { if (volume < 0 || volume > 100) throw new ArgumentException("volume must be between 0 and 100", "volume"); if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}", _CommandCookie, volume, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestSetCaptureVolume() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } /// /// Does not appear to be working /// /// /// public int RequestRenderAudioStart(string fileName, bool loop) { if (_DaemonPipe.Connected) { _TuningSoundFile = fileName; _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}{3}", _CommandCookie++, _TuningSoundFile, (loop ? "1" : "0"), REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestRenderAudioStart() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } public int RequestRenderAudioStop() { if (_DaemonPipe.Connected) { _DaemonPipe.SendData(Encoding.ASCII.GetBytes(String.Format( "{1}{2}", _CommandCookie++, _TuningSoundFile, REQUEST_TERMINATOR))); return _CommandCookie - 1; } else { Logger.Log("VoiceManager.RequestRenderAudioStop() called when the daemon pipe is disconnected", Helpers.LogLevel.Error, Client); return -1; } } #region Callbacks private void RequiredVoiceVersionEventHandler(string capsKey, IMessage message, Simulator simulator) { RequiredVoiceVersionMessage msg = (RequiredVoiceVersionMessage)message; if (VOICE_MAJOR_VERSION != msg.MajorVersion) { Logger.Log(String.Format("Voice version mismatch! Got {0}, expecting {1}. Disabling the voice manager", msg.MajorVersion, VOICE_MAJOR_VERSION), Helpers.LogLevel.Error, Client); Enabled = false; } else { Logger.DebugLog("Voice version " + msg.MajorVersion + " verified", Client); } } private void ProvisionCapsResponse(CapsClient client, OSD response, Exception error) { if (response is OSDMap) { OSDMap respTable = (OSDMap)response; if (OnProvisionAccount != null) { try { OnProvisionAccount(respTable["username"].AsString(), respTable["password"].AsString()); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e); } } } } private void ParcelVoiceInfoResponse(CapsClient client, OSD response, Exception error) { if (response is OSDMap) { OSDMap respTable = (OSDMap)response; string regionName = respTable["region_name"].AsString(); int localID = (int)respTable["parcel_local_id"].AsInteger(); string channelURI = null; if (respTable["voice_credentials"] is OSDMap) { OSDMap creds = (OSDMap)respTable["voice_credentials"]; channelURI = creds["channel_uri"].AsString(); } if (OnParcelVoiceInfo != null) OnParcelVoiceInfo(regionName, localID, channelURI); } } private void _DaemonPipe_OnDisconnected(SocketException se) { if (se != null) Console.WriteLine("Disconnected! " + se.Message); else Console.WriteLine("Disconnected!"); } private void _DaemonPipe_OnReceiveLine(string line) { XmlTextReader reader = new XmlTextReader(new StringReader(line)); while (reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: { if (reader.Depth == 0) { isEvent = (reader.Name == "Event"); if (isEvent || reader.Name == "Response") { for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); switch (reader.Name) { // case "requestId": // uuidString = reader.Value; // break; case "action": actionString = reader.Value; break; case "type": eventTypeString = reader.Value; break; } } } } else { switch (reader.Name) { case "InputXml": cookie = -1; // Parse through here to get the cookie value reader.Read(); if (reader.Name == "Request") { for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); if (reader.Name == "requestId") { Int32.TryParse(reader.Value, out cookie); break; } } } if (cookie == -1) { Logger.Log("VoiceManager._DaemonPipe_OnReceiveLine(): Failed to parse InputXml for the cookie", Helpers.LogLevel.Warning, Client); } break; case "CaptureDevices": _CaptureDevices.Clear(); break; case "RenderDevices": _RenderDevices.Clear(); break; // case "ReturnCode": // returnCode = reader.ReadElementContentAsInt(); // break; case "StatusCode": statusCode = reader.ReadElementContentAsInt(); break; case "StatusString": statusString = reader.ReadElementContentAsString(); break; case "State": state = reader.ReadElementContentAsInt(); break; case "ConnectorHandle": connectorHandle = reader.ReadElementContentAsString(); break; case "AccountHandle": accountHandle = reader.ReadElementContentAsString(); break; case "SessionHandle": sessionHandle = reader.ReadElementContentAsString(); break; case "URI": uriString = reader.ReadElementContentAsString(); break; case "IsChannel": isChannel = reader.ReadElementContentAsBoolean(); break; case "Name": nameString = reader.ReadElementContentAsString(); break; // case "AudioMedia": // audioMediaString = reader.ReadElementContentAsString(); // break; case "ChannelName": nameString = reader.ReadElementContentAsString(); break; case "ParticipantURI": uriString = reader.ReadElementContentAsString(); break; case "DisplayName": displayNameString = reader.ReadElementContentAsString(); break; case "AccountName": nameString = reader.ReadElementContentAsString(); break; case "ParticipantType": participantType = reader.ReadElementContentAsInt(); break; case "IsLocallyMuted": isLocallyMuted = reader.ReadElementContentAsBoolean(); break; case "IsModeratorMuted": isModeratorMuted = reader.ReadElementContentAsBoolean(); break; case "IsSpeaking": isSpeaking = reader.ReadElementContentAsBoolean(); break; case "Volume": volume = reader.ReadElementContentAsInt(); break; case "Energy": energy = reader.ReadElementContentAsFloat(); break; case "MicEnergy": energy = reader.ReadElementContentAsFloat(); break; case "ChannelURI": uriString = reader.ReadElementContentAsString(); break; case "ChannelListResult": _ChannelMap[nameString] = uriString; break; case "CaptureDevice": reader.Read(); _CaptureDevices.Add(reader.ReadElementContentAsString()); break; case "CurrentCaptureDevice": reader.Read(); nameString = reader.ReadElementContentAsString(); break; case "RenderDevice": reader.Read(); _RenderDevices.Add(reader.ReadElementContentAsString()); break; case "CurrentRenderDevice": reader.Read(); nameString = reader.ReadElementContentAsString(); break; } } break; } case XmlNodeType.EndElement: if (reader.Depth == 0) ProcessEvent(); break; } } if (isEvent) { } //Client.DebugLog("VOICE: " + line); } private void ProcessEvent() { if (isEvent) { switch (eventTypeString) { case "LoginStateChangeEvent": if (OnLoginStateChange != null) OnLoginStateChange(cookie, accountHandle, statusCode, statusString, state); break; case "SessionNewEvent": if (OnNewSession != null) OnNewSession(cookie, accountHandle, eventSessionHandle, state, nameString, uriString); break; case "SessionStateChangeEvent": if (OnSessionStateChange != null) OnSessionStateChange(cookie, uriString, statusCode, statusString, eventSessionHandle, state, isChannel, nameString); break; case "ParticipantStateChangeEvent": if (OnParticipantStateChange != null) OnParticipantStateChange(cookie, uriString, statusCode, statusString, state, nameString, displayNameString, participantType); break; case "ParticipantPropertiesEvent": if (OnParticipantProperties != null) OnParticipantProperties(cookie, uriString, statusCode, statusString, isLocallyMuted, isModeratorMuted, isSpeaking, volume, energy); break; case "AuxAudioPropertiesEvent": if (OnAuxAudioProperties != null) OnAuxAudioProperties(cookie, energy); break; } } else { switch (actionString) { case "Connector.Create.1": if (OnConnectorCreated != null) OnConnectorCreated(cookie, statusCode, statusString, connectorHandle); break; case "Account.Login.1": if (OnLogin != null) OnLogin(cookie, statusCode, statusString, accountHandle); break; case "Session.Create.1": if (OnSessionCreated != null) OnSessionCreated(cookie, statusCode, statusString, sessionHandle); break; case "Session.Connect.1": if (OnSessionConnected != null) OnSessionConnected(cookie, statusCode, statusString); break; case "Session.Terminate.1": if (OnSessionTerminated != null) OnSessionTerminated(cookie, statusCode, statusString); break; case "Account.Logout.1": if (OnAccountLogout != null) OnAccountLogout(cookie, statusCode, statusString); break; case "Connector.InitiateShutdown.1": if (OnConnectorInitiateShutdown != null) OnConnectorInitiateShutdown(cookie, statusCode, statusString); break; case "Account.ChannelGetList.1": if (OnAccountChannelGetList != null) OnAccountChannelGetList(cookie, statusCode, statusString); break; case "Aux.GetCaptureDevices.1": if (OnCaptureDevices != null) OnCaptureDevices(cookie, statusCode, statusString, nameString); break; case "Aux.GetRenderDevices.1": if (OnRenderDevices != null) OnRenderDevices(cookie, statusCode, statusString, nameString); break; } } } #endregion Callbacks } }