From 7a4abf0def0b2d9a89bec354ad175c342e7f2106 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Sat, 11 Jul 2009 08:16:47 +0000 Subject: [PATCH] From: Dr Scofield & Alan Webb this commit finally adds the VivoxVoiceModule: it supports positional as well as conference call type voice (currently only per region server), region and parcel voice, speaker indication (LL client family), direct avtar-to-avatar voice chat. NOTE: you need to obtain an customer admin account from Vivox to be able to use this module --- DON'T ask me about how to about an admin account, i've NO clue, we just wrote this code. --- .../Voice/VivoxVoice/VivoxVoiceModule.cs | 1301 +++++++++++++++++ bin/OpenSim.ini.example | 58 + 2 files changed, 1359 insertions(+) create mode 100644 OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs new file mode 100644 index 0000000000..12ad9b8b32 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs @@ -0,0 +1,1301 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project 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 DEVELOPERS ``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 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.IO; +using System.Net; +using System.Text; +using System.Xml; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using OpenMetaverse; +using log4net; +using Nini.Config; +using Nwc.XmlRpc; +using OpenSim.Framework; +using OpenSim.Framework.Communications.Cache; +using OpenSim.Framework.Communications.Capabilities; +using OpenSim.Framework.Servers; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using Caps = OpenSim.Framework.Communications.Capabilities.Caps; + +namespace OpenSim.Region.OptionalModules.Avatar.Voice.VivoxVoice +{ + public class VivoxVoiceModule : ISharedRegionModule + { + + // channel distance model values + public const int CHAN_DIST_NONE = 0; // no attenuation + public const int CHAN_DIST_INVERSE = 1; // inverse distance attenuation + public const int CHAN_DIST_LINEAR = 2; // linear attenuation + public const int CHAN_DIST_EXPONENT = 3; // exponential attenuation + public const int CHAN_DIST_DEFAULT = CHAN_DIST_LINEAR; + + // channel type values + public static readonly string CHAN_TYPE_POSITIONAL = "positional"; + public static readonly string CHAN_TYPE_CHANNEL = "channel"; + public static readonly string CHAN_TYPE_DEFAULT = CHAN_TYPE_POSITIONAL; + + // channel mode values + public static readonly string CHAN_MODE_OPEN = "open"; + public static readonly string CHAN_MODE_LECTURE = "lecture"; + public static readonly string CHAN_MODE_PRESENTATION = "presentation"; + public static readonly string CHAN_MODE_AUDITORIUM = "auditorium"; + public static readonly string CHAN_MODE_DEFAULT = CHAN_MODE_OPEN; + + // unconstrained default values + public const double CHAN_ROLL_OFF_DEFAULT = 2.0; // rate of attenuation + public const double CHAN_ROLL_OFF_MIN = 1.0; + public const double CHAN_ROLL_OFF_MAX = 4.0; + public const int CHAN_MAX_RANGE_DEFAULT = 80; // distance at which channel is silent + public const int CHAN_MAX_RANGE_MIN = 0; + public const int CHAN_MAX_RANGE_MAX = 160; + public const int CHAN_CLAMPING_DISTANCE_DEFAULT = 10; // distance before attenuation applies + public const int CHAN_CLAMPING_DISTANCE_MIN = 0; + public const int CHAN_CLAMPING_DISTANCE_MAX = 160; + + // Infrastructure + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly Object vlock = new Object(); + + // Capability strings + private static readonly string m_parcelVoiceInfoRequestPath = "0107/"; + private static readonly string m_provisionVoiceAccountRequestPath = "0108/"; + private static readonly string m_chatSessionRequestPath = "0109/"; + + // Control info, e.g. vivox server, admin user, admin password + private static bool m_pluginEnabled = false; + private static bool m_adminConnected = false; + + private static string m_vivoxServer; + private static string m_vivoxSipUri; + private static string m_vivoxVoiceAccountApi; + private static string m_vivoxAdminUser; + private static string m_vivoxAdminPassword; + private static string m_authToken = String.Empty; + + private static int m_vivoxChannelDistanceModel; + private static double m_vivoxChannelRollOff; + private static int m_vivoxChannelMaximumRange; + private static string m_vivoxChannelMode; + private static string m_vivoxChannelType; + private static int m_vivoxChannelClampingDistance; + + private static Dictionary m_parents = new Dictionary(); + private static bool m_dumpXml; + + private IConfig m_config; + + public void Initialise(IConfigSource config) + { + + m_config = config.Configs["VivoxVoice"]; + + if (null == m_config) + { + m_log.Info("[VivoxVoice] no config found, plugin disabled"); + return; + } + + if (!m_config.GetBoolean("enabled", false)) + { + m_log.Info("[VivoxVoice] plugin disabled by configuration"); + return; + } + + try + { + // retrieve configuration variables + m_vivoxServer = m_config.GetString("vivox_server", String.Empty); + m_vivoxSipUri = m_config.GetString("vivox_sip_uri", String.Empty); + m_vivoxAdminUser = m_config.GetString("vivox_admin_user", String.Empty); + m_vivoxAdminPassword = m_config.GetString("vivox_admin_password", String.Empty); + + m_vivoxChannelDistanceModel = m_config.GetInt("vivox_channel_distance_model", CHAN_DIST_DEFAULT); + m_vivoxChannelRollOff = m_config.GetDouble("vivox_channel_roll_off", CHAN_ROLL_OFF_DEFAULT); + m_vivoxChannelMaximumRange = m_config.GetInt("vivox_channel_max_range", CHAN_MAX_RANGE_DEFAULT); + m_vivoxChannelMode = m_config.GetString("vivox_channel_mode", CHAN_MODE_DEFAULT).ToLower(); + m_vivoxChannelType = m_config.GetString("vivox_channel_type", CHAN_TYPE_DEFAULT).ToLower(); + m_vivoxChannelClampingDistance = m_config.GetInt("vivox_channel_clamping_distance", + CHAN_CLAMPING_DISTANCE_DEFAULT); + m_dumpXml = m_config.GetBoolean("dump_xml", false); + + // Validate against constraints and default if necessary + if (m_vivoxChannelRollOff < CHAN_ROLL_OFF_MIN || m_vivoxChannelRollOff > CHAN_ROLL_OFF_MAX) + { + m_log.WarnFormat("[VivoxVoice] Invalid value for roll off ({0}), reset to {1}.", + m_vivoxChannelRollOff, CHAN_ROLL_OFF_DEFAULT); + m_vivoxChannelRollOff = CHAN_ROLL_OFF_DEFAULT; + } + + if (m_vivoxChannelMaximumRange < CHAN_MAX_RANGE_MIN || m_vivoxChannelMaximumRange > CHAN_MAX_RANGE_MAX) + { + m_log.WarnFormat("[VivoxVoice] Invalid value for maximum range ({0}), reset to {1}.", + m_vivoxChannelMaximumRange, CHAN_MAX_RANGE_DEFAULT); + m_vivoxChannelMaximumRange = CHAN_MAX_RANGE_DEFAULT; + } + + if (m_vivoxChannelClampingDistance < CHAN_CLAMPING_DISTANCE_MIN || + m_vivoxChannelClampingDistance > CHAN_CLAMPING_DISTANCE_MAX) + { + m_log.WarnFormat("[VivoxVoice] Invalid value for clamping distance ({0}), reset to {1}.", + m_vivoxChannelClampingDistance, CHAN_CLAMPING_DISTANCE_DEFAULT); + m_vivoxChannelClampingDistance = CHAN_CLAMPING_DISTANCE_DEFAULT; + } + + switch (m_vivoxChannelMode) + { + case "open" : break; + case "lecture" : break; + case "presentation" : break; + case "auditorium" : break; + default : + m_log.WarnFormat("[VivoxVoice] Invalid value for channel mode ({0}), reset to {1}.", + m_vivoxChannelMode, CHAN_MODE_DEFAULT); + m_vivoxChannelMode = CHAN_MODE_DEFAULT; + break; + } + + switch (m_vivoxChannelType) + { + case "positional" : break; + case "channel" : break; + default : + m_log.WarnFormat("[VivoxVoice] Invalid value for channel type ({0}), reset to {1}.", + m_vivoxChannelType, CHAN_TYPE_DEFAULT); + m_vivoxChannelType = CHAN_TYPE_DEFAULT; + break; + } + + m_vivoxVoiceAccountApi = String.Format("http://{0}/api2", m_vivoxServer); + + // Admin interface required values + if (String.IsNullOrEmpty(m_vivoxServer) || + String.IsNullOrEmpty(m_vivoxSipUri) || + String.IsNullOrEmpty(m_vivoxAdminUser) || + String.IsNullOrEmpty(m_vivoxAdminPassword)) + { + m_log.Error("[VivoxVoice] plugin mis-configured"); + m_log.Info("[VivoxVoice] plugin disabled: incomplete configuration"); + return; + } + + m_log.InfoFormat("[VivoxVoice] using vivox server {0}", m_vivoxServer); + + // Get admin rights and cleanup any residual channel definition + + DoAdminLogin(); + + m_pluginEnabled = true; + + m_log.Info("[VivoxVoice] plugin enabled"); + + } + catch (Exception e) + { + m_log.ErrorFormat("[VivoxVoice] plugin initialization failed: {0}", e.Message); + m_log.DebugFormat("[VivoxVoice] plugin initialization failed: {0}", e.ToString()); + return; + } + } + + + // Called to indicate that the module has been added to the region + public void AddRegion(Scene scene) + { + + if (m_pluginEnabled) + { + lock (vlock) + { + + string channelId; + + string sceneUUID = scene.RegionInfo.RegionID.ToString(); + string sceneName = scene.RegionInfo.RegionName; + + // Make sure that all local channels are deleted. + // So we have to search for the children, and then do an + // iteration over the set of chidren identified. + // This assumes that there is just one directory per + // region. + + if (VivoxTryGetDirectory(sceneUUID + "D", out channelId)) + { + m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}", + sceneName, sceneUUID, channelId); + + XmlElement children = VivoxListChildren(channelId); + string count; + + if (XmlFind(children, "response.level0.channel-search.count", out count)) + { + int cnum = Convert.ToInt32(count); + for (int i = 0; i < cnum; i++) + { + string id; + if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) + { + if (!IsOK(VivoxDeleteChannel(channelId, id))) + m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id); + } + } + } + } + else + { + if (!VivoxTryCreateDirectory(sceneUUID + "D", sceneName, out channelId)) + { + m_log.WarnFormat("[VivoxVoice] Create failed <{0}:{1}:{2}>", + "*", sceneUUID, sceneName); + channelId = String.Empty; + } + } + + + // Create a dictionary entry unconditionally. This eliminates the + // need to check for a parent in the core code. The end result is + // the same, if the parent table entry is an empty string, then + // region channels will be created as first-level channels. + + lock (m_parents) + if (m_parents.ContainsKey(sceneUUID)) + { + RemoveRegion(scene); + m_parents.Add(sceneUUID, channelId); + } + else + { + m_parents.Add(sceneUUID, channelId); + } + + } + + // we need to capture scene in an anonymous method + // here as we need it later in the callbacks + scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps) + { + OnRegisterCaps(scene, agentID, caps); + }; + + } + + } + + // Called to indicate that all loadable modules have now been added + public void RegionLoaded(Scene scene) + { + // Do nothing. + } + + // Called to indicate that the region is going away. + public void RemoveRegion(Scene scene) + { + + if (m_pluginEnabled) + { + lock (vlock) + { + + string channelId; + + string sceneUUID = scene.RegionInfo.RegionID.ToString(); + string sceneName = scene.RegionInfo.RegionName; + + // Make sure that all local channels are deleted. + // So we have to search for the children, and then do an + // iteration over the set of chidren identified. + // This assumes that there is just one directory per + // region. + + if (VivoxTryGetDirectory(sceneUUID + "D", out channelId)) + { + + m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}", + sceneName, sceneUUID, channelId); + + XmlElement children = VivoxListChildren(channelId); + string count; + + if (XmlFind(children, "response.level0.channel-search.count", out count)) + { + int cnum = Convert.ToInt32(count); + for (int i = 0; i < cnum; i++) + { + string id; + if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) + { + if (!IsOK(VivoxDeleteChannel(channelId, id))) + m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id); + } + } + } + } + + if (!IsOK(VivoxDeleteChannel(null, channelId))) + m_log.WarnFormat("[VivoxVoice] Parent channel delete failed {0}:{1}:{2}", sceneName, sceneUUID, channelId); + + // Remove the channel umbrella entry + + lock (m_parents) + { + if (m_parents.ContainsKey(sceneUUID)) + { + m_parents.Remove(sceneUUID); + } + } + } + } + } + + public void PostInitialise() + { + // Do nothing. + } + + public void Close() + { + if (m_pluginEnabled) + VivoxLogout(); + } + + public string Name + { + get { return "VivoxVoiceModule"; } + } + + public bool IsSharedModule + { + get { return true; } + } + + // + // OnRegisterCaps is invoked via the scene.EventManager + // everytime OpenSim hands out capabilities to a client + // (login, region crossing). We contribute two capabilities to + // the set of capabilities handed back to the client: + // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest. + // + // ProvisionVoiceAccountRequest allows the client to obtain + // the voice account credentials for the avatar it is + // controlling (e.g., user name, password, etc). + // + // ParcelVoiceInfoRequest is invoked whenever the client + // changes from one region or parcel to another. + // + // Note that OnRegisterCaps is called here via a closure + // delegate containing the scene of the respective region (see + // Initialise()). + // + public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) + { + m_log.DebugFormat("[VivoxVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps); + + string capsBase = "/CAPS/" + caps.CapsObjectPath; + caps.RegisterHandler("ProvisionVoiceAccountRequest", + new RestStreamHandler("POST", capsBase + m_provisionVoiceAccountRequestPath, + delegate(string request, string path, string param, + OSHttpRequest httpRequest, OSHttpResponse httpResponse) + { + return ProvisionVoiceAccountRequest(scene, request, path, param, + agentID, caps); + })); + caps.RegisterHandler("ParcelVoiceInfoRequest", + new RestStreamHandler("POST", capsBase + m_parcelVoiceInfoRequestPath, + delegate(string request, string path, string param, + OSHttpRequest httpRequest, OSHttpResponse httpResponse) + { + return ParcelVoiceInfoRequest(scene, request, path, param, + agentID, caps); + })); + caps.RegisterHandler("ChatSessionRequest", + new RestStreamHandler("POST", capsBase + m_chatSessionRequestPath, + delegate(string request, string path, string param, + OSHttpRequest httpRequest, OSHttpResponse httpResponse) + { + return ChatSessionRequest(scene, request, path, param, + agentID, caps); + })); + } + + /// + /// Callback for a client request for Voice Account Details + /// + /// current scene object of the client + /// + /// + /// + /// + /// + /// + public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param, + UUID agentID, Caps caps) + { + try + { + + ScenePresence avatar = null; + string avatarName = null; + + if (scene == null) throw new Exception("[VivoxVoice][PROVISIONVOICE] Invalid scene"); + + avatar = scene.GetScenePresence(agentID); + while (avatar == null) + { + Thread.Sleep(100); + avatar = scene.GetScenePresence(agentID); + } + + avatarName = avatar.Name; + + m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: scene = {0}, agentID = {1}", scene, agentID); + m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}", + request, path, param); + + XmlElement resp; + bool retry = false; + string agentname = "x" + Convert.ToBase64String(agentID.GetBytes()); + string password = new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16); + string code = String.Empty; + + agentname = agentname.Replace('+', '-').Replace('/', '_'); + + do + { + resp = VivoxGetAccountInfo(agentname); + + if (XmlFind(resp, "response.level0.status", out code)) + { + if (code != "OK") + { + if (XmlFind(resp, "response.level0.body.code", out code)) + { + // If the request was recognized, then this should be set to something + switch (code) + { + case "201" : // Account expired + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : expired credentials", + avatarName); + m_adminConnected = false; + retry = DoAdminLogin(); + break; + + case "202" : // Missing credentials + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : missing credentials", + avatarName); + break; + + case "212" : // Not authorized + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : not authorized", + avatarName); + break; + + case "300" : // Required parameter missing + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : parameter missing", + avatarName); + break; + + case "403" : // Account does not exist + resp = VivoxCreateAccount(agentname,password); + // Note: This REALLY MUST BE status. Create Account does not return code. + if (XmlFind(resp, "response.level0.status", out code)) + { + switch (code) + { + case "201" : // Account expired + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : expired credentials", + avatarName); + m_adminConnected = false; + retry = DoAdminLogin(); + break; + + case "202" : // Missing credentials + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : missing credentials", + avatarName); + break; + + case "212" : // Not authorized + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : not authorized", + avatarName); + break; + + case "300" : // Required parameter missing + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : parameter missing", + avatarName); + break; + + case "400" : // Create failed + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : create failed", + avatarName); + break; + } + } + break; + + case "404" : // Failed to retrieve account + m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : retrieve failed"); + // [AMW] Sleep and retry for a fixed period? Or just abandon? + break; + } + } + } + } + } while (retry); + + if (code != "OK") + { + m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: Get Account Request failed for \"{0}\"", avatarName); + throw new Exception("Unable to execute request"); + } + + // Unconditionally change the password on each request + VivoxPassword(agentname, password); + + LLSDVoiceAccountResponse voiceAccountResponse = + new LLSDVoiceAccountResponse(agentname, password, m_vivoxSipUri, m_vivoxVoiceAccountApi); + + string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse); + + m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r); + + return r; + } + catch (Exception e) + { + m_log.ErrorFormat("[VivoxVoice][PROVISIONVOICE]: : {0}, retry later", e.Message); + m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: : {0} failed", e.ToString()); + return ""; + } + } + + /// + /// Callback for a client request for ParcelVoiceInfo + /// + /// current scene object of the client + /// + /// + /// + /// + /// + /// + public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param, + UUID agentID, Caps caps) + { + ScenePresence avatar = scene.GetScenePresence(agentID); + string avatarName = avatar.Name; + + // - check whether we have a region channel in our cache + // - if not: + // create it and cache it + // - send it to the client + // - send channel_uri: as "sip:regionID@m_sipDomain" + try + { + LLSDParcelVoiceInfoResponse parcelVoiceInfo; + string channel_uri; + + if (null == scene.LandChannel) + throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available", + scene.RegionInfo.RegionName, avatarName)); + + // get channel_uri: check first whether estate + // settings allow voice, then whether parcel allows + // voice, if all do retrieve or obtain the parcel + // voice channel + LandData land = scene.GetLandData(avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y); + + m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}", + scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param); + // m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: avatar \"{0}\": location: {1} {2} {3}", + // avatarName, avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y, avatar.AbsolutePosition.Z); + + // TODO: EstateSettings don't seem to get propagated... + if (!scene.RegionInfo.EstateSettings.AllowVoice) + { + m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings", + scene.RegionInfo.RegionName); + channel_uri = String.Empty; + } + + if ((land.Flags & (uint)Parcel.ParcelFlags.AllowVoiceChat) == 0) + { + m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel", + scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName); + channel_uri = String.Empty; + } + else + { + channel_uri = RegionGetOrCreateChannel(scene, land); + } + + // fill in our response to the client + Hashtable creds = new Hashtable(); + creds["channel_uri"] = channel_uri; + + parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds); + string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo); + + m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}", + scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r); + return r; + } + catch (Exception e) + { + m_log.ErrorFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later", + scene.RegionInfo.RegionName, avatarName, e.Message); + m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed", + scene.RegionInfo.RegionName, avatarName, e.ToString()); + + return ""; + } + } + + + /// + /// Callback for a client request for a private chat channel + /// + /// current scene object of the client + /// + /// + /// + /// + /// + /// + public string ChatSessionRequest(Scene scene, string request, string path, string param, + UUID agentID, Caps caps) + { + ScenePresence avatar = scene.GetScenePresence(agentID); + string avatarName = avatar.Name; + + m_log.DebugFormat("[VivoxVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}", + avatarName, request, path, param); + return "true"; + } + + + private string RegionGetOrCreateChannel(Scene scene, LandData land) + { + + string channelUri = null; + string channelId = null; + + string landUUID; + string landName; + string parentId; + + lock (m_parents) parentId = m_parents[scene.RegionInfo.RegionID.ToString()]; + + // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same + // as the directory ID. Otherwise, it reflects the parcel's ID. + + if (land.LocalID != 1 && (land.Flags & (uint)Parcel.ParcelFlags.UseEstateVoiceChan) == 0) + { + landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name); + landUUID = land.GlobalID.ToString(); + m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", + landName, land.LocalID, landUUID); + } + else + { + landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName); + landUUID = scene.RegionInfo.RegionID.ToString(); + m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", + landName, land.LocalID, landUUID); + } + + lock (vlock) + { + if (!VivoxTryGetChannel(parentId, landUUID, out channelId, out channelUri) && + !VivoxTryCreateChannel(parentId, landUUID, landName, out channelUri)) + throw new Exception("vivox channel uri not available"); + + m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parent channel id {1}: retrieved parcel channel_uri {2} ", + landName, parentId, channelUri); + + + } + + return channelUri; + } + + + private static readonly string m_vivoxLoginPath = "http://{0}/api2/viv_signin.php?userid={1}&pwd={2}"; + + /// + /// Perform administrative login for Vivox. + /// Returns a hash table containing values returned from the request. + /// + private XmlElement VivoxLogin(string name, string password) + { + string requrl = String.Format(m_vivoxLoginPath, m_vivoxServer, name, password); + return VivoxCall(requrl, false); + } + + + private static readonly string m_vivoxLogoutPath = "http://{0}/api2/viv_signout.php?auth_token={1}"; + + /// + /// Perform administrative logout for Vivox. + /// + private XmlElement VivoxLogout() + { + string requrl = String.Format(m_vivoxLogoutPath, m_vivoxServer, m_authToken); + return VivoxCall(requrl, false); + } + + + private static readonly string m_vivoxGetAccountPath = "http://{0}/api2/viv_get_acct.php?auth_token={1}&user_name={2}"; + + /// + /// Retrieve account information for the specified user. + /// Returns a hash table containing values returned from the request. + /// + private XmlElement VivoxGetAccountInfo(string user) + { + string requrl = String.Format(m_vivoxGetAccountPath, m_vivoxServer, m_authToken, user); + return VivoxCall(requrl, true); + } + + + private static readonly string m_vivoxNewAccountPath = "http://{0}/api2/viv_adm_acct_new.php?username={1}&pwd={2}&auth_token={3}"; + + /// + /// Creates a new account. + /// For now we supply the minimum set of values, which + /// is user name and password. We *can* supply a lot more + /// demographic data. + /// + private XmlElement VivoxCreateAccount(string user, string password) + { + string requrl = String.Format(m_vivoxNewAccountPath, m_vivoxServer, user, password, m_authToken); + return VivoxCall(requrl, true); + } + + + private static readonly string m_vivoxPasswordPath = "http://{0}/api2/viv_adm_password.php?user_name={1}&new_pwd={2}&auth_token={3}"; + + /// + /// Change the user's password. + /// + private XmlElement VivoxPassword(string user, string password) + { + string requrl = String.Format(m_vivoxPasswordPath, m_vivoxServer, user, password, m_authToken); + return VivoxCall(requrl, true); + } + + + private static readonly string m_vivoxChannelPath = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_name={2}&auth_token={3}"; + + /// + /// Create a channel. + /// Once again, there a multitude of options possible. In the simplest case + /// we specify only the name and get a non-persistent cannel in return. Non + /// persistent means that the channel gets deleted if no-one uses it for + /// 5 hours. To accomodate future requirements, it may be a good idea to + /// initially create channels under the umbrella of a parent ID based upon + /// the region name. That way we have a context for side channels, if those + /// are required in a later phase. + /// + /// In this case the call handles parent and description as optional values. + /// + + private bool VivoxTryCreateChannel(string parent, string channelId, string description, out string channelUri) + { + string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", channelId, m_authToken); + + if (parent != null && parent != String.Empty) + { + requrl = String.Format("{0}&chan_parent={1}", requrl, parent); + } + if (description != null && description != String.Empty) + { + requrl = String.Format("{0}&chan_desc={1}", requrl, description); + } + + requrl = String.Format("{0}&chan_type={1}", requrl, m_vivoxChannelType); + requrl = String.Format("{0}&chan_mode={1}", requrl, m_vivoxChannelMode); + requrl = String.Format("{0}&chan_roll_off={1}", requrl, m_vivoxChannelRollOff); + requrl = String.Format("{0}&chan_dist_model={1}", requrl, m_vivoxChannelDistanceModel); + requrl = String.Format("{0}&chan_max_range={1}", requrl, m_vivoxChannelMaximumRange); + requrl = String.Format("{0}&chan_ckamping_distance={1}", requrl, m_vivoxChannelClampingDistance); + + XmlElement resp = VivoxCall(requrl, true); + if (XmlFind(resp, "response.level0.body.chan_uri", out channelUri)) + return true; + + channelUri = String.Empty; + return false; + } + + /// + /// Create a directory. + /// Create a channel with an unconditional type of "dir" (indicating directory). + /// This is used to create an arbitrary name tree for partitioning of the + /// channel name space. + /// The parent and description are optional values. + /// + + private bool VivoxTryCreateDirectory(string dirId, string description, out string channelId) + { + string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", dirId, m_authToken); + + // if (parent != null && parent != String.Empty) + // { + // requrl = String.Format("{0}&chan_parent={1}", requrl, parent); + // } + + if (description != null && description != String.Empty) + { + requrl = String.Format("{0}&chan_desc={1}", requrl, description); + } + requrl = String.Format("{0}&chan_type={1}", requrl, "dir"); + + XmlElement resp = VivoxCall(requrl, true); + if (IsOK(resp) && XmlFind(resp, "response.level0.body.chan_id", out channelId)) + return true; + + channelId = String.Empty; + return false; + } + + private static readonly string m_vivoxChannelSearchPath = "http://{0}/api2/viv_chan_search.php?cond_channame={1}&auth_token={2}"; + + /// + /// Retrieve a channel. + /// Once again, there a multitude of options possible. In the simplest case + /// we specify only the name and get a non-persistent cannel in return. Non + /// persistent means that the channel gets deleted if no-one uses it for + /// 5 hours. To accomodate future requirements, it may be a good idea to + /// initially create channels under the umbrella of a parent ID based upon + /// the region name. That way we have a context for side channels, if those + /// are required in a later phase. + /// In this case the call handles parent and description as optional values. + /// + + private bool VivoxTryGetChannel(string channelParent, string channelName, + out string channelId, out string channelUri) + { + string count; + + string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, channelName, m_authToken); + XmlElement resp = VivoxCall(requrl, true); + + if (XmlFind(resp, "response.level0.channel-search.count", out count)) + { + int channels = Convert.ToInt32(count); + for (int i = 0; i < channels; i++) + { + string name; + string id; + string type; + string uri; + string parent; + + // skip if not a channel + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) || + (type != "channel" && type != "positional_M")) + continue; + + // skip if not the name we are looking for + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) || + name != channelName) + continue; + + // skip if parent does not match + if (channelParent != null && !XmlFind(resp, "response.level0.channel-search.channels.channels.level4.parent", i, out parent)) + continue; + + // skip if no channel id available + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id)) + continue; + + // skip if no channel uri available + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.uri", i, out uri)) + continue; + + channelId = id; + channelUri = uri; + + return true; + } + } + + channelId = String.Empty; + channelUri = String.Empty; + return false; + } + + private bool VivoxTryGetDirectory(string directoryName, out string directoryId) + { + string count; + + string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, directoryName, m_authToken); + XmlElement resp = VivoxCall(requrl, true); + + if (XmlFind(resp, "response.level0.channel-search.count", out count)) + { + int channels = Convert.ToInt32(count); + for (int i = 0; i < channels; i++) + { + string name; + string id; + string type; + + // skip if not a directory + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) || + type != "dir") + continue; + + // skip if not the name we are looking for + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) || + name != directoryName) + continue; + + // skip if no channel id available + if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id)) + continue; + + directoryId = id; + return true; + } + } + + directoryId = String.Empty; + return false; + } + + // private static readonly string m_vivoxChannelById = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}"; + + // private XmlElement VivoxGetChannelById(string parent, string channelid) + // { + // string requrl = String.Format(m_vivoxChannelById, m_vivoxServer, "get", channelid, m_authToken); + + // if (parent != null && parent != String.Empty) + // return VivoxGetChild(parent, channelid); + // else + // return VivoxCall(requrl, true); + // } + + /// + /// Delete a channel. + /// Once again, there a multitude of options possible. In the simplest case + /// we specify only the name and get a non-persistent cannel in return. Non + /// persistent means that the channel gets deleted if no-one uses it for + /// 5 hours. To accomodate future requirements, it may be a good idea to + /// initially create channels under the umbrella of a parent ID based upon + /// the region name. That way we have a context for side channels, if those + /// are required in a later phase. + /// In this case the call handles parent and description as optional values. + /// + + private static readonly string m_vivoxChannelDel = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}"; + + private XmlElement VivoxDeleteChannel(string parent, string channelid) + { + string requrl = String.Format(m_vivoxChannelDel, m_vivoxServer, "delete", channelid, m_authToken); + if (parent != null && parent != String.Empty) + { + requrl = String.Format("{0}&chan_parent={1}", requrl, parent); + } + return VivoxCall(requrl, true); + } + + /// + /// Return information on channels in the given directory + /// + + private static readonly string m_vivoxChannelSearch = "http://{0}/api2/viv_chan_search.php?&cond_chanparent={1}&auth_token={2}"; + + private XmlElement VivoxListChildren(string channelid) + { + string requrl = String.Format(m_vivoxChannelSearch, m_vivoxServer, channelid, m_authToken); + return VivoxCall(requrl, true); + } + + // private XmlElement VivoxGetChild(string parent, string child) + // { + + // XmlElement children = VivoxListChildren(parent); + // string count; + + // if (XmlFind(children, "response.level0.channel-search.count", out count)) + // { + // int cnum = Convert.ToInt32(count); + // for (int i = 0; i < cnum; i++) + // { + // string name; + // string id; + // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.name", i, out name)) + // { + // if (name == child) + // { + // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id)) + // { + // return VivoxGetChannelById(null, id); + // } + // } + // } + // } + // } + + // // One we *know* does not exist. + // return VivoxGetChannel(null, Guid.NewGuid().ToString()); + + // } + + /// + /// This method handles the WEB side of making a request over the + /// Vivox interface. The returned values are tansferred to a has + /// table which is returned as the result. + /// The outcome of the call can be determined by examining the + /// status value in the hash table. + /// + + private XmlElement VivoxCall(string requrl, bool admin) + { + + XmlDocument doc = null; + + // If this is an admin call, and admin is not connected, + // and the admin id cannot be connected, then fail. + if (admin && !m_adminConnected && !DoAdminLogin()) + return null; + + doc = new XmlDocument(); + + try + { + // Otherwise prepare the request + m_log.DebugFormat("[VivoxVoice] Sending request <{0}>", requrl); + + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requrl); + HttpWebResponse rsp = null; + + // We are sending just parameters, no content + req.ContentLength = 0; + + // Send request and retrieve the response + rsp = (HttpWebResponse)req.GetResponse(); + + XmlTextReader rdr = new XmlTextReader(rsp.GetResponseStream()); + doc.Load(rdr); + rdr.Close(); + } + catch (Exception e) + { + m_log.ErrorFormat("[VivoxVoice] Error in admin call : {0}", e.Message); + } + + // If we're debugging server responses, dump the whole + // load now + if (m_dumpXml) XmlScanl(doc.DocumentElement,0); + + return doc.DocumentElement; + } + + /// + /// Just say if it worked. + /// + + private bool IsOK(XmlElement resp) + { + string status; + XmlFind(resp, "response.level0.status", out status); + return (status == "OK"); + } + + /// + /// Login has been factored in this way because it gets called + /// from several places in the module, and we want it to work + /// the same way each time. + /// + private bool DoAdminLogin() + { + m_log.Debug("[VivoxVoice] Establishing admin connection"); + + lock (vlock) + { + if (!m_adminConnected) + { + string status = "Unknown"; + XmlElement resp = null; + + resp = VivoxLogin(m_vivoxAdminUser, m_vivoxAdminPassword); + + if (XmlFind(resp, "response.level0.body.status", out status)) + { + if (status == "Ok") + { + m_log.Info("[VivoxVoice] Admin connection established"); + if (XmlFind(resp, "response.level0.body.auth_token", out m_authToken)) + { + if (m_dumpXml) m_log.DebugFormat("[VivoxVoice] Auth Token <{0}>", + m_authToken); + m_adminConnected = true; + } + } + else + { + m_log.WarnFormat("[VivoxVoice] Admin connection failed, status = {0}", + status); + } + } + } + } + + return m_adminConnected; + } + + /// + /// The XmlScan routine is provided to aid in the + /// reverse engineering of incompletely + /// documented packets returned by the Vivox + /// voice server. It is only called if the + /// m_dumpXml switch is set. + /// + private void XmlScanl(XmlElement e, int index) + { + if (e.HasChildNodes) + { + m_log.DebugFormat("<{0}>".PadLeft(index+5), e.Name); + XmlNodeList children = e.ChildNodes; + foreach (XmlNode node in children) + switch (node.NodeType) + { + case XmlNodeType.Element : + XmlScanl((XmlElement)node, index+1); + break; + case XmlNodeType.Text : + m_log.DebugFormat("\"{0}\"".PadLeft(index+5), node.Value); + break; + default : + break; + } + m_log.DebugFormat("".PadLeft(index+6), e.Name); + } + else + { + m_log.DebugFormat("<{0}/>".PadLeft(index+6), e.Name); + } + } + + private static readonly char[] C_POINT = {'.'}; + + /// + /// The Find method is passed an element whose + /// inner text is scanned in an attempt to match + /// the name hierarchy passed in the 'tag' parameter. + /// If the whole hierarchy is resolved, the InnerText + /// value at that point is returned. Note that this + /// may itself be a subhierarchy of the entire + /// document. The function returns a boolean indicator + /// of the search's success. The search is performed + /// by the recursive Search method. + /// + private bool XmlFind(XmlElement root, string tag, int nth, out string result) + { + if (root == null || tag == null || tag == String.Empty) + { + result = String.Empty; + return false; + } + return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result); + } + + private bool XmlFind(XmlElement root, string tag, out string result) + { + int nth = 0; + if (root == null || tag == null || tag == String.Empty) + { + result = String.Empty; + return false; + } + return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result); + } + + /// + /// XmlSearch is initially called by XmlFind, and then + /// recursively called by itself until the document + /// supplied to XmlFind is either exhausted or the name hierarchy + /// is matched. + /// + /// If the hierarchy is matched, the value is returned in + /// result, and true returned as the function's + /// value. Otherwise the result is set to the empty string and + /// false is returned. + /// + private bool XmlSearch(XmlElement e, string[] tags, int index, ref int nth, out string result) + { + if (index == tags.Length || e.Name != tags[index]) + { + result = String.Empty; + return false; + } + + if (tags.Length-index == 1) + { + if (nth == 0) + { + result = e.InnerText; + return true; + } + else + { + nth--; + result = String.Empty; + return false; + } + } + + if (e.HasChildNodes) + { + XmlNodeList children = e.ChildNodes; + foreach (XmlNode node in children) + { + switch (node.NodeType) + { + case XmlNodeType.Element : + if (XmlSearch((XmlElement)node, tags, index+1, ref nth, out result)) + return true; + break; + + default : + break; + } + } + } + + result = String.Empty; + return false; + } + } +} diff --git a/bin/OpenSim.ini.example b/bin/OpenSim.ini.example index 745b49ad19..bd7b5f0b8f 100644 --- a/bin/OpenSim.ini.example +++ b/bin/OpenSim.ini.example @@ -1166,6 +1166,64 @@ ; Keep it false for now. Making it true requires the use of a special client in order to access inventory safemode = false +[VivoxVoice] + + ; The VivoxVoice module will allow you to provide voice on your + ; region(s). It uses the same voice technology as the LL grid and + ; works with recent LL clients (we have tested 1.22.9.110075, so + ; anything later ought to be fine as well). + ; + ; For this to work you need to obtain an admin account from Vivox + ; that allows you to create voice accounts and region channels. + + enabled = false + + ; vivox voice server + vivox_server = www.foobar.vivox.com + + ; vivox SIP URI + vivox_sip_uri = foobar.vivox.com + + ; vivox admin user name + vivox_admin_user = DeepThroat + + ; vivox admin password + vivox_admin_password = VoiceG4te + + ; channel type: "channel" or "positional" + ; - positional: spatial sound (default) + ; - channel: normal "conference call", no spatial sound + ;vivox_channel_type = positional + + ; channel characteristics (unless you know what you are doing, i'd + ; leave them as they are --- now you WILL muck around with them, + ; huh? sigh) + + ; channel distance model: + ; 0 - no attenuation + ; 1 - inverse distance attenuation + ; 2 - linear attenuation (default) + ; 3 - exponential attenuation + ;vivox_channel_distance_model = 2 + + ; channel mode: + ; - "open" (default) + ; - "lecture" + ; - "presentation" + ; - "auditorium" + ;vivox_channel_mode = "open" + + ; channel roll off: rate of attenuation + ; - a value between 1.0 and 4.0, default is 2.0 + ;vivox_channel_roll_off = 2.0 + + ; channel max range: distance at which channel is silent + ; - a value between 0 and 160, default is 80 + ;vivox_channel_max_range = 80 + + ; channel clamping distance: distance before attenuation applies + ; - a value between 0 and 160, default is 10 + ;vivox_channel_clamping_distance = 10 [FreeSwitchVoice] ; In order for this to work you need a functioning freeswitch pbx set