1004 lines
34 KiB
C#
1004 lines
34 KiB
C#
/*
|
|
* 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.
|
|
*/
|
|
//#define DEBUG_VOICE
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.IO;
|
|
using System.Threading;
|
|
|
|
using OpenMetaverse;
|
|
using OpenMetaverse.StructuredData;
|
|
|
|
namespace OpenMetaverse.Voice
|
|
{
|
|
public partial class VoiceGateway : IDisposable
|
|
{
|
|
// These states should be in increasing order of 'completeness'
|
|
// so that the (int) values can drive a progress bar.
|
|
public enum ConnectionState
|
|
{
|
|
None = 0,
|
|
Provisioned,
|
|
DaemonStarted,
|
|
DaemonConnected,
|
|
ConnectorConnected,
|
|
AccountLogin,
|
|
RegionCapAvailable,
|
|
SessionRunning
|
|
}
|
|
|
|
internal string sipServer = "";
|
|
private string acctServer = "https://www.bhr.vivox.com/api2/";
|
|
private string connectionHandle;
|
|
private string accountHandle;
|
|
private string sessionHandle;
|
|
|
|
// Parameters to Vivox daemon
|
|
private string slvoicePath = "";
|
|
private string slvoiceArgs = "-ll 5";
|
|
private string daemonNode = "127.0.0.1";
|
|
private int daemonPort = 37331;
|
|
|
|
private string voiceUser;
|
|
private string voicePassword;
|
|
private string spatialUri;
|
|
private string spatialCredentials;
|
|
|
|
// Session management
|
|
private Dictionary<string, VoiceSession> sessions;
|
|
private VoiceSession spatialSession;
|
|
private Uri currentParcelCap;
|
|
private Uri nextParcelCap;
|
|
private string regionName;
|
|
|
|
// Position update thread
|
|
private Thread posThread;
|
|
private ManualResetEvent posRestart;
|
|
public GridClient Client;
|
|
private VoicePosition position;
|
|
private Vector3d oldPosition;
|
|
private Vector3d oldAt;
|
|
|
|
// Audio interfaces
|
|
private List<string> inputDevices;
|
|
/// <summary>
|
|
/// List of audio input devices
|
|
/// </summary>
|
|
public List<string> CaptureDevices { get { return inputDevices; } }
|
|
private List<string> outputDevices;
|
|
/// <summary>
|
|
/// List of audio output devices
|
|
/// </summary>
|
|
public List<string> PlaybackDevices { get { return outputDevices; } }
|
|
private string currentCaptureDevice;
|
|
private string currentPlaybackDevice;
|
|
private bool testing = false;
|
|
|
|
public event EventHandler OnSessionCreate;
|
|
public event EventHandler OnSessionRemove;
|
|
public delegate void VoiceConnectionChangeCallback(ConnectionState state);
|
|
public event VoiceConnectionChangeCallback OnVoiceConnectionChange;
|
|
public delegate void VoiceMicTestCallback(float level);
|
|
public event VoiceMicTestCallback OnVoiceMicTest;
|
|
|
|
public VoiceGateway(GridClient c)
|
|
{
|
|
Random rand = new Random();
|
|
daemonPort = rand.Next(34000, 44000);
|
|
|
|
Client = c;
|
|
|
|
sessions = new Dictionary<string, VoiceSession>();
|
|
position = new VoicePosition();
|
|
position.UpOrientation = new Vector3d(0.0, 1.0, 0.0);
|
|
position.Velocity = new Vector3d(0.0, 0.0, 0.0);
|
|
oldPosition = new Vector3d(0, 0, 0);
|
|
oldAt = new Vector3d(1, 0, 0);
|
|
|
|
slvoiceArgs = " -ll -1"; // Min logging
|
|
slvoiceArgs += " -i 127.0.0.1:" + daemonPort.ToString();
|
|
// slvoiceArgs += " -lf " + control.instance.ClientDir;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start up the Voice service.
|
|
/// </summary>
|
|
public void Start()
|
|
{
|
|
// Start the background thread
|
|
if (posThread != null && posThread.IsAlive)
|
|
posThread.Abort();
|
|
posThread = new Thread(new ThreadStart(PositionThreadBody));
|
|
posThread.Name = "VoicePositionUpdate";
|
|
posThread.IsBackground = true;
|
|
posRestart = new ManualResetEvent(false);
|
|
posThread.Start();
|
|
|
|
Client.Network.EventQueueRunning += new EventHandler<EventQueueRunningEventArgs>(Network_EventQueueRunning);
|
|
|
|
// Connection events
|
|
OnDaemonRunning +=
|
|
new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning);
|
|
OnDaemonCouldntRun +=
|
|
new VoiceGateway.DaemonCouldntRunCallback(connector_OnDaemonCouldntRun);
|
|
OnConnectorCreateResponse +=
|
|
new EventHandler<VoiceGateway.VoiceConnectorEventArgs>(connector_OnConnectorCreateResponse);
|
|
OnDaemonConnected +=
|
|
new DaemonConnectedCallback(connector_OnDaemonConnected);
|
|
OnDaemonCouldntConnect +=
|
|
new DaemonCouldntConnectCallback(connector_OnDaemonCouldntConnect);
|
|
OnAuxAudioPropertiesEvent +=
|
|
new EventHandler<AudioPropertiesEventArgs>(connector_OnAuxAudioPropertiesEvent);
|
|
|
|
// Session events
|
|
OnSessionStateChangeEvent +=
|
|
new EventHandler<SessionStateChangeEventArgs>(connector_OnSessionStateChangeEvent);
|
|
OnSessionAddedEvent +=
|
|
new EventHandler<SessionAddedEventArgs>(connector_OnSessionAddedEvent);
|
|
|
|
// Session Participants events
|
|
OnSessionParticipantUpdatedEvent +=
|
|
new EventHandler<ParticipantUpdatedEventArgs>(connector_OnSessionParticipantUpdatedEvent);
|
|
OnSessionParticipantAddedEvent +=
|
|
new EventHandler<ParticipantAddedEventArgs>(connector_OnSessionParticipantAddedEvent);
|
|
|
|
// Device events
|
|
OnAuxGetCaptureDevicesResponse +=
|
|
new EventHandler<VoiceDevicesEventArgs>(connector_OnAuxGetCaptureDevicesResponse);
|
|
OnAuxGetRenderDevicesResponse +=
|
|
new EventHandler<VoiceDevicesEventArgs>(connector_OnAuxGetRenderDevicesResponse);
|
|
|
|
// Generic status response
|
|
OnVoiceResponse += new EventHandler<VoiceResponseEventArgs>(connector_OnVoiceResponse);
|
|
|
|
// Account events
|
|
OnAccountLoginResponse +=
|
|
new EventHandler<VoiceAccountEventArgs>(connector_OnAccountLoginResponse);
|
|
|
|
Logger.Log("Voice initialized", Helpers.LogLevel.Info);
|
|
|
|
// If voice provisioning capability is already available,
|
|
// proceed with voice startup. Otherwise the EventQueueRunning
|
|
// event will do it.
|
|
System.Uri vCap =
|
|
Client.Network.CurrentSim.Caps.CapabilityURI("ProvisionVoiceAccountRequest");
|
|
if (vCap != null)
|
|
RequestVoiceProvision(vCap);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle miscellaneous request status
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
/// ///<remarks>If something goes wrong, we log it.</remarks>
|
|
void connector_OnVoiceResponse(object sender, VoiceGateway.VoiceResponseEventArgs e)
|
|
{
|
|
if (e.StatusCode == 0)
|
|
return;
|
|
|
|
Logger.Log(e.Message + " on " + sender as string, Helpers.LogLevel.Error);
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
Client.Network.EventQueueRunning -= new EventHandler<EventQueueRunningEventArgs>(Network_EventQueueRunning);
|
|
|
|
// Connection events
|
|
OnDaemonRunning -=
|
|
new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning);
|
|
OnDaemonCouldntRun -=
|
|
new VoiceGateway.DaemonCouldntRunCallback(connector_OnDaemonCouldntRun);
|
|
OnConnectorCreateResponse -=
|
|
new EventHandler<VoiceGateway.VoiceConnectorEventArgs>(connector_OnConnectorCreateResponse);
|
|
OnDaemonConnected -=
|
|
new VoiceGateway.DaemonConnectedCallback(connector_OnDaemonConnected);
|
|
OnDaemonCouldntConnect -=
|
|
new VoiceGateway.DaemonCouldntConnectCallback(connector_OnDaemonCouldntConnect);
|
|
OnAuxAudioPropertiesEvent -=
|
|
new EventHandler<AudioPropertiesEventArgs>(connector_OnAuxAudioPropertiesEvent);
|
|
|
|
// Session events
|
|
OnSessionStateChangeEvent -=
|
|
new EventHandler<SessionStateChangeEventArgs>(connector_OnSessionStateChangeEvent);
|
|
OnSessionAddedEvent -=
|
|
new EventHandler<SessionAddedEventArgs>(connector_OnSessionAddedEvent);
|
|
|
|
// Session Participants events
|
|
OnSessionParticipantUpdatedEvent -=
|
|
new EventHandler<ParticipantUpdatedEventArgs>(connector_OnSessionParticipantUpdatedEvent);
|
|
OnSessionParticipantAddedEvent -=
|
|
new EventHandler<ParticipantAddedEventArgs>(connector_OnSessionParticipantAddedEvent);
|
|
OnSessionParticipantRemovedEvent -=
|
|
new EventHandler<ParticipantRemovedEventArgs>(connector_OnSessionParticipantRemovedEvent);
|
|
|
|
// Tuning events
|
|
OnAuxGetCaptureDevicesResponse -=
|
|
new EventHandler<VoiceGateway.VoiceDevicesEventArgs>(connector_OnAuxGetCaptureDevicesResponse);
|
|
OnAuxGetRenderDevicesResponse -=
|
|
new EventHandler<VoiceGateway.VoiceDevicesEventArgs>(connector_OnAuxGetRenderDevicesResponse);
|
|
|
|
// Account events
|
|
OnAccountLoginResponse -=
|
|
new EventHandler<VoiceGateway.VoiceAccountEventArgs>(connector_OnAccountLoginResponse);
|
|
|
|
// Stop the background thread
|
|
if (posThread != null)
|
|
{
|
|
PosUpdating(false);
|
|
|
|
if (posThread.IsAlive)
|
|
posThread.Abort();
|
|
posThread = null;
|
|
}
|
|
|
|
// Close all sessions
|
|
foreach (VoiceSession s in sessions.Values)
|
|
{
|
|
if (OnSessionRemove != null)
|
|
OnSessionRemove(s, EventArgs.Empty);
|
|
s.Close();
|
|
}
|
|
|
|
// Clear out lots of state so in case of restart we begin at the beginning.
|
|
currentParcelCap = null;
|
|
sessions.Clear();
|
|
accountHandle = null;
|
|
voiceUser = null;
|
|
voicePassword = null;
|
|
|
|
SessionTerminate(sessionHandle);
|
|
sessionHandle = null;
|
|
AccountLogout(accountHandle);
|
|
accountHandle = null;
|
|
ConnectorInitiateShutdown(connectionHandle);
|
|
connectionHandle = null;
|
|
StopDaemon();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanup oject resources
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
internal string GetVoiceDaemonPath()
|
|
{
|
|
string myDir =
|
|
Path.GetDirectoryName(
|
|
(System.Reflection.Assembly.GetEntryAssembly() ?? typeof (VoiceGateway).Assembly).Location);
|
|
|
|
if (Environment.OSVersion.Platform != PlatformID.MacOSX &&
|
|
Environment.OSVersion.Platform != PlatformID.Unix)
|
|
{
|
|
string localDaemon = Path.Combine(myDir, Path.Combine("voice", "SLVoice.exe"));
|
|
|
|
if (File.Exists(localDaemon))
|
|
return localDaemon;
|
|
|
|
string progFiles;
|
|
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ProgramFiles(x86)")))
|
|
{
|
|
progFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
|
|
}
|
|
else
|
|
{
|
|
progFiles = Environment.GetEnvironmentVariable("ProgramFiles");
|
|
}
|
|
|
|
if (System.IO.File.Exists(Path.Combine(progFiles, @"SecondLife" + Path.DirectorySeparatorChar + @"SLVoice.exe")))
|
|
{
|
|
return Path.Combine(progFiles, @"SecondLife" + Path.DirectorySeparatorChar + @"SLVoice.exe");
|
|
}
|
|
|
|
return Path.Combine(myDir, @"SLVoice.exe");
|
|
|
|
}
|
|
else
|
|
{
|
|
string localDaemon = Path.Combine(myDir, Path.Combine("voice", "SLVoice"));
|
|
|
|
if (File.Exists(localDaemon))
|
|
return localDaemon;
|
|
|
|
return Path.Combine(myDir,"SLVoice");
|
|
}
|
|
}
|
|
|
|
void RequestVoiceProvision(System.Uri cap)
|
|
{
|
|
OpenMetaverse.Http.CapsClient capClient =
|
|
new OpenMetaverse.Http.CapsClient(cap);
|
|
capClient.OnComplete +=
|
|
new OpenMetaverse.Http.CapsClient.CompleteCallback(cClient_OnComplete);
|
|
OSD postData = new OSD();
|
|
|
|
// STEP 0
|
|
Logger.Log("Requesting voice capability", Helpers.LogLevel.Info);
|
|
capClient.BeginGetResponse(postData, OSDFormat.Xml, 10000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request voice cap when changing regions
|
|
/// </summary>
|
|
void Network_EventQueueRunning(object sender, EventQueueRunningEventArgs e)
|
|
{
|
|
// We only care about the sim we are in.
|
|
if (e.Simulator != Client.Network.CurrentSim)
|
|
return;
|
|
|
|
// Did we provision voice login info?
|
|
if (string.IsNullOrEmpty(voiceUser))
|
|
{
|
|
// The startup steps are
|
|
// 0. Get voice account info
|
|
// 1. Start Daemon
|
|
// 2. Create TCP connection
|
|
// 3. Create Connector
|
|
// 4. Account login
|
|
// 5. Create session
|
|
|
|
// Get the voice provisioning data
|
|
System.Uri vCap =
|
|
Client.Network.CurrentSim.Caps.CapabilityURI("ProvisionVoiceAccountRequest");
|
|
|
|
// Do we have voice capability?
|
|
if (vCap == null)
|
|
{
|
|
Logger.Log("Null voice capability after event queue running", Helpers.LogLevel.Warning);
|
|
}
|
|
else
|
|
{
|
|
RequestVoiceProvision(vCap);
|
|
}
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// Change voice session for this region.
|
|
ParcelChanged();
|
|
}
|
|
}
|
|
|
|
|
|
#region Participants
|
|
|
|
void connector_OnSessionParticipantUpdatedEvent(object sender, ParticipantUpdatedEventArgs e)
|
|
{
|
|
VoiceSession s = FindSession(e.SessionHandle, false);
|
|
if (s == null) return;
|
|
s.ParticipantUpdate(e.URI, e.IsMuted, e.IsSpeaking, e.Volume, e.Energy);
|
|
}
|
|
|
|
public string SIPFromUUID(UUID id)
|
|
{
|
|
return "sip:" +
|
|
nameFromID(id) +
|
|
"@" +
|
|
sipServer;
|
|
}
|
|
|
|
private static string nameFromID(UUID id)
|
|
{
|
|
string result = null;
|
|
|
|
if (id == UUID.Zero)
|
|
return result;
|
|
|
|
// Prepending this apparently prevents conflicts with reserved names inside the vivox and diamondware code.
|
|
result = "x";
|
|
|
|
// Base64 encode and replace the pieces of base64 that are less compatible
|
|
// with e-mail local-parts.
|
|
// See RFC-4648 "Base 64 Encoding with URL and Filename Safe Alphabet"
|
|
byte[] encbuff = id.GetBytes();
|
|
result += Convert.ToBase64String(encbuff);
|
|
result = result.Replace('+', '-');
|
|
result = result.Replace('/', '_');
|
|
|
|
return result;
|
|
}
|
|
|
|
void connector_OnSessionParticipantAddedEvent(object sender, ParticipantAddedEventArgs e)
|
|
{
|
|
VoiceSession s = FindSession(e.SessionHandle, false);
|
|
if (s == null)
|
|
{
|
|
Logger.Log("Orphan participant", Helpers.LogLevel.Error);
|
|
return;
|
|
}
|
|
s.AddParticipant(e.URI);
|
|
}
|
|
|
|
void connector_OnSessionParticipantRemovedEvent(object sender, ParticipantRemovedEventArgs e)
|
|
{
|
|
VoiceSession s = FindSession(e.SessionHandle, false);
|
|
if (s == null) return;
|
|
s.RemoveParticipant(e.URI);
|
|
}
|
|
#endregion
|
|
|
|
#region Sessions
|
|
void connector_OnSessionAddedEvent(object sender, SessionAddedEventArgs e)
|
|
{
|
|
sessionHandle = e.SessionHandle;
|
|
|
|
// Create our session context.
|
|
VoiceSession s = FindSession(sessionHandle, true);
|
|
s.RegionName = regionName;
|
|
|
|
spatialSession = s;
|
|
|
|
// Tell any user-facing code.
|
|
if (OnSessionCreate != null)
|
|
OnSessionCreate(s, null);
|
|
|
|
Logger.Log("Added voice session in " + regionName, Helpers.LogLevel.Info);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle a change in session state
|
|
/// </summary>
|
|
void connector_OnSessionStateChangeEvent(object sender, SessionStateChangeEventArgs e)
|
|
{
|
|
VoiceSession s;
|
|
|
|
switch (e.State)
|
|
{
|
|
case VoiceGateway.SessionState.Connected:
|
|
s = FindSession(e.SessionHandle, true);
|
|
sessionHandle = e.SessionHandle;
|
|
s.RegionName = regionName;
|
|
spatialSession = s;
|
|
|
|
Logger.Log("Voice connected in " + regionName, Helpers.LogLevel.Info);
|
|
// Tell any user-facing code.
|
|
if (OnSessionCreate != null)
|
|
OnSessionCreate(s, null);
|
|
break;
|
|
|
|
case VoiceGateway.SessionState.Disconnected:
|
|
s = FindSession(sessionHandle, false);
|
|
sessions.Remove(sessionHandle);
|
|
|
|
if (s != null)
|
|
{
|
|
Logger.Log("Voice disconnected in " + s.RegionName, Helpers.LogLevel.Info);
|
|
|
|
// Inform interested parties
|
|
if (OnSessionRemove != null)
|
|
OnSessionRemove(s, null);
|
|
|
|
if (s == spatialSession)
|
|
spatialSession = null;
|
|
}
|
|
|
|
// The previous session is now ended. Check for a new one and
|
|
// start it going.
|
|
if (nextParcelCap != null)
|
|
{
|
|
currentParcelCap = nextParcelCap;
|
|
nextParcelCap = null;
|
|
RequestParcelInfo(currentParcelCap);
|
|
}
|
|
break;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close a voice session
|
|
/// </summary>
|
|
/// <param name="sessionHandle"></param>
|
|
internal void CloseSession(string sessionHandle)
|
|
{
|
|
if (!sessions.ContainsKey(sessionHandle))
|
|
return;
|
|
|
|
PosUpdating(false);
|
|
ReportConnectionState(ConnectionState.AccountLogin);
|
|
|
|
// Clean up spatial pointers.
|
|
VoiceSession s = sessions[sessionHandle];
|
|
if (s.IsSpatial)
|
|
{
|
|
spatialSession = null;
|
|
currentParcelCap = null;
|
|
}
|
|
|
|
// Remove this session from the master session list
|
|
sessions.Remove(sessionHandle);
|
|
|
|
// Let any user-facing code clean up.
|
|
if (OnSessionRemove != null)
|
|
OnSessionRemove(s, null);
|
|
|
|
// Tell SLVoice to clean it up as well.
|
|
SessionTerminate(sessionHandle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Locate a Session context from its handle
|
|
/// </summary>
|
|
/// <remarks>Creates the session context if it does not exist.</remarks>
|
|
VoiceSession FindSession(string sessionHandle, bool make)
|
|
{
|
|
if (sessions.ContainsKey(sessionHandle))
|
|
return sessions[sessionHandle];
|
|
|
|
if (!make) return null;
|
|
|
|
// Create a new session and add it to the sessions list.
|
|
VoiceSession s = new VoiceSession(this, sessionHandle);
|
|
|
|
// Turn on position updating for spatial sessions
|
|
// (For now, only spatial sessions are supported)
|
|
if (s.IsSpatial)
|
|
PosUpdating(true);
|
|
|
|
// Register the session by its handle
|
|
sessions.Add(sessionHandle, s);
|
|
return s;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MinorResponses
|
|
|
|
void connector_OnAuxAudioPropertiesEvent(object sender, AudioPropertiesEventArgs e)
|
|
{
|
|
if (OnVoiceMicTest != null)
|
|
OnVoiceMicTest(e.MicEnergy);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void ReportConnectionState(ConnectionState s)
|
|
{
|
|
if (OnVoiceConnectionChange == null) return;
|
|
|
|
OnVoiceConnectionChange(s);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle completion of main voice cap request.
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="result"></param>
|
|
/// <param name="error"></param>
|
|
void cClient_OnComplete(OpenMetaverse.Http.CapsClient client,
|
|
OpenMetaverse.StructuredData.OSD result,
|
|
Exception error)
|
|
{
|
|
if (error != null)
|
|
{
|
|
Logger.Log("Voice cap error " + error.Message, Helpers.LogLevel.Error);
|
|
return;
|
|
}
|
|
|
|
Logger.Log("Voice provisioned", Helpers.LogLevel.Info);
|
|
ReportConnectionState(ConnectionState.Provisioned);
|
|
|
|
OpenMetaverse.StructuredData.OSDMap pMap = result as OpenMetaverse.StructuredData.OSDMap;
|
|
|
|
// We can get back 4 interesting values:
|
|
// voice_sip_uri_hostname
|
|
// voice_account_server_name (actually a full URI)
|
|
// username
|
|
// password
|
|
if (pMap.ContainsKey("voice_sip_uri_hostname"))
|
|
sipServer = pMap["voice_sip_uri_hostname"].AsString();
|
|
if (pMap.ContainsKey("voice_account_server_name"))
|
|
acctServer = pMap["voice_account_server_name"].AsString();
|
|
voiceUser = pMap["username"].AsString();
|
|
voicePassword = pMap["password"].AsString();
|
|
|
|
// Start the SLVoice daemon
|
|
slvoicePath = GetVoiceDaemonPath();
|
|
|
|
// Test if the executable exists
|
|
if (!System.IO.File.Exists(slvoicePath))
|
|
{
|
|
Logger.Log("SLVoice is missing", Helpers.LogLevel.Error);
|
|
return;
|
|
}
|
|
|
|
// STEP 1
|
|
StartDaemon(slvoicePath, slvoiceArgs);
|
|
}
|
|
|
|
#region Daemon
|
|
void connector_OnDaemonCouldntConnect()
|
|
{
|
|
Logger.Log("No voice daemon connect", Helpers.LogLevel.Error);
|
|
}
|
|
|
|
void connector_OnDaemonCouldntRun()
|
|
{
|
|
Logger.Log("Daemon not started", Helpers.LogLevel.Error);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Daemon has started so connect to it.
|
|
/// </summary>
|
|
void connector_OnDaemonRunning()
|
|
{
|
|
OnDaemonRunning -=
|
|
new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning);
|
|
|
|
Logger.Log("Daemon started", Helpers.LogLevel.Info);
|
|
ReportConnectionState(ConnectionState.DaemonStarted);
|
|
|
|
// STEP 2
|
|
ConnectToDaemon(daemonNode, daemonPort);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// The daemon TCP connection is open.
|
|
/// </summary>
|
|
void connector_OnDaemonConnected()
|
|
{
|
|
Logger.Log("Daemon connected", Helpers.LogLevel.Info);
|
|
ReportConnectionState(ConnectionState.DaemonConnected);
|
|
|
|
// The connector is what does the logging.
|
|
VoiceGateway.VoiceLoggingSettings vLog =
|
|
new VoiceGateway.VoiceLoggingSettings();
|
|
|
|
#if DEBUG_VOICE
|
|
vLog.Enabled = true;
|
|
vLog.FileNamePrefix = "OpenmetaverseVoice";
|
|
vLog.FileNameSuffix = ".log";
|
|
vLog.LogLevel = 4;
|
|
#endif
|
|
// STEP 3
|
|
int reqId = ConnectorCreate(
|
|
"V2 SDK", // Magic value keeps SLVoice happy
|
|
acctServer, // Account manager server
|
|
30000, 30099, // port range
|
|
vLog);
|
|
if (reqId < 0)
|
|
{
|
|
Logger.Log("No voice connector request", Helpers.LogLevel.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle creation of the Connector.
|
|
/// </summary>
|
|
void connector_OnConnectorCreateResponse(
|
|
object sender,
|
|
VoiceGateway.VoiceConnectorEventArgs e)
|
|
{
|
|
Logger.Log("Voice daemon protocol started " + e.Message, Helpers.LogLevel.Info);
|
|
|
|
connectionHandle = e.Handle;
|
|
|
|
if (e.StatusCode != 0)
|
|
return;
|
|
|
|
// STEP 4
|
|
AccountLogin(
|
|
connectionHandle,
|
|
voiceUser,
|
|
voicePassword,
|
|
"VerifyAnswer", // This can also be "AutoAnswer"
|
|
"", // Default account management server URI
|
|
10, // Throttle state changes
|
|
true); // Enable buddies and presence
|
|
}
|
|
#endregion
|
|
|
|
void connector_OnAccountLoginResponse(
|
|
object sender,
|
|
VoiceGateway.VoiceAccountEventArgs e)
|
|
{
|
|
Logger.Log("Account Login " + e.Message, Helpers.LogLevel.Info);
|
|
accountHandle = e.AccountHandle;
|
|
ReportConnectionState(ConnectionState.AccountLogin);
|
|
ParcelChanged();
|
|
}
|
|
|
|
#region Audio devices
|
|
/// <summary>
|
|
/// Handle response to audio output device query
|
|
/// </summary>
|
|
void connector_OnAuxGetRenderDevicesResponse(
|
|
object sender,
|
|
VoiceGateway.VoiceDevicesEventArgs e)
|
|
{
|
|
outputDevices = e.Devices;
|
|
currentPlaybackDevice = e.CurrentDevice;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle response to audio input device query
|
|
/// </summary>
|
|
void connector_OnAuxGetCaptureDevicesResponse(
|
|
object sender,
|
|
VoiceGateway.VoiceDevicesEventArgs e)
|
|
{
|
|
inputDevices = e.Devices;
|
|
currentCaptureDevice = e.CurrentDevice;
|
|
}
|
|
|
|
public string CurrentCaptureDevice
|
|
{
|
|
get { return currentCaptureDevice; }
|
|
set
|
|
{
|
|
currentCaptureDevice = value;
|
|
AuxSetCaptureDevice(value);
|
|
}
|
|
}
|
|
public string PlaybackDevice
|
|
{
|
|
get { return currentPlaybackDevice; }
|
|
set
|
|
{
|
|
currentPlaybackDevice = value;
|
|
AuxSetRenderDevice(value);
|
|
}
|
|
}
|
|
|
|
public int MicLevel
|
|
{
|
|
set
|
|
{
|
|
ConnectorSetLocalMicVolume(connectionHandle, value);
|
|
}
|
|
}
|
|
public int SpkrLevel
|
|
{
|
|
set
|
|
{
|
|
ConnectorSetLocalSpeakerVolume(connectionHandle, value);
|
|
}
|
|
}
|
|
|
|
public bool MicMute
|
|
{
|
|
set
|
|
{
|
|
ConnectorMuteLocalMic(connectionHandle, value);
|
|
}
|
|
}
|
|
|
|
public bool SpkrMute
|
|
{
|
|
set
|
|
{
|
|
ConnectorMuteLocalSpeaker(connectionHandle, value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set audio test mode
|
|
/// </summary>
|
|
public bool TestMode
|
|
{
|
|
get { return testing; }
|
|
set
|
|
{
|
|
testing = value;
|
|
if (testing)
|
|
{
|
|
if (spatialSession != null)
|
|
{
|
|
spatialSession.Close();
|
|
spatialSession = null;
|
|
}
|
|
AuxCaptureAudioStart(0);
|
|
}
|
|
else
|
|
{
|
|
AuxCaptureAudioStop();
|
|
ParcelChanged();
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Set voice channel for new parcel
|
|
/// </summary>
|
|
///
|
|
internal void ParcelChanged()
|
|
{
|
|
// Get the capability for this parcel.
|
|
Caps c = Client.Network.CurrentSim.Caps;
|
|
System.Uri pCap = c.CapabilityURI("ParcelVoiceInfoRequest");
|
|
|
|
if (pCap == null)
|
|
{
|
|
Logger.Log("Null voice capability", Helpers.LogLevel.Error);
|
|
return;
|
|
}
|
|
|
|
// Parcel has changed. If we were already in a spatial session, we have to close it first.
|
|
if (spatialSession != null)
|
|
{
|
|
nextParcelCap = pCap;
|
|
CloseSession(spatialSession.Handle);
|
|
}
|
|
|
|
// Not already in a session, so can start the new one.
|
|
RequestParcelInfo(pCap);
|
|
}
|
|
|
|
private OpenMetaverse.Http.CapsClient parcelCap;
|
|
|
|
/// <summary>
|
|
/// Request info from a parcel capability Uri.
|
|
/// </summary>
|
|
/// <param name="cap"></param>
|
|
|
|
void RequestParcelInfo(Uri cap)
|
|
{
|
|
Logger.Log("Requesting region voice info", Helpers.LogLevel.Info);
|
|
|
|
parcelCap = new OpenMetaverse.Http.CapsClient(cap);
|
|
parcelCap.OnComplete +=
|
|
new OpenMetaverse.Http.CapsClient.CompleteCallback(pCap_OnComplete);
|
|
OSD postData = new OSD();
|
|
|
|
currentParcelCap = cap;
|
|
parcelCap.BeginGetResponse(postData, OSDFormat.Xml, 10000);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Receive parcel voice cap
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="result"></param>
|
|
/// <param name="error"></param>
|
|
void pCap_OnComplete(OpenMetaverse.Http.CapsClient client,
|
|
OpenMetaverse.StructuredData.OSD result,
|
|
Exception error)
|
|
{
|
|
parcelCap.OnComplete -=
|
|
new OpenMetaverse.Http.CapsClient.CompleteCallback(pCap_OnComplete);
|
|
parcelCap = null;
|
|
|
|
if (error != null)
|
|
{
|
|
Logger.Log("Region voice cap " + error.Message, Helpers.LogLevel.Error);
|
|
return;
|
|
}
|
|
|
|
OpenMetaverse.StructuredData.OSDMap pMap = result as OpenMetaverse.StructuredData.OSDMap;
|
|
|
|
regionName = pMap["region_name"].AsString();
|
|
ReportConnectionState(ConnectionState.RegionCapAvailable);
|
|
|
|
if (pMap.ContainsKey("voice_credentials"))
|
|
{
|
|
OpenMetaverse.StructuredData.OSDMap cred =
|
|
pMap["voice_credentials"] as OpenMetaverse.StructuredData.OSDMap;
|
|
|
|
if (cred.ContainsKey("channel_uri"))
|
|
spatialUri = cred["channel_uri"].AsString();
|
|
if (cred.ContainsKey("channel_credentials"))
|
|
spatialCredentials = cred["channel_credentials"].AsString();
|
|
}
|
|
|
|
if (spatialUri == null || spatialUri == "")
|
|
{
|
|
// "No voice chat allowed here");
|
|
return;
|
|
}
|
|
|
|
Logger.Log("Voice connecting for region " + regionName, Helpers.LogLevel.Info);
|
|
|
|
// STEP 5
|
|
int reqId = SessionCreate(
|
|
accountHandle,
|
|
spatialUri, // uri
|
|
"", // Channel name seems to be always null
|
|
spatialCredentials, // spatialCredentials, // session password
|
|
true, // Join Audio
|
|
false, // Join Text
|
|
"");
|
|
if (reqId < 0)
|
|
{
|
|
Logger.Log("Voice Session ReqID " + reqId.ToString(), Helpers.LogLevel.Error);
|
|
}
|
|
}
|
|
|
|
#region Location Update
|
|
/// <summary>
|
|
/// Tell Vivox where we are standing
|
|
/// </summary>
|
|
/// <remarks>This has to be called when we move or turn.</remarks>
|
|
internal void UpdatePosition(AgentManager self)
|
|
{
|
|
// Get position in Global coordinates
|
|
Vector3d OMVpos = new Vector3d(self.GlobalPosition);
|
|
|
|
// Do not send trivial updates.
|
|
if (OMVpos.ApproxEquals(oldPosition, 1.0))
|
|
return;
|
|
|
|
oldPosition = OMVpos;
|
|
|
|
// Convert to the coordinate space that Vivox uses
|
|
// OMV X is East, Y is North, Z is up
|
|
// VVX X is East, Y is up, Z is South
|
|
position.Position = new Vector3d(OMVpos.X, OMVpos.Z, -OMVpos.Y);
|
|
|
|
// TODO Rotate these two vectors
|
|
|
|
// Get azimuth from the facing Quaternion.
|
|
// By definition, facing.W = Cos( angle/2 )
|
|
double angle = 2.0 * Math.Acos(self.Movement.BodyRotation.W);
|
|
|
|
position.LeftOrientation = new Vector3d(-1.0, 0.0, 0.0);
|
|
position.AtOrientation = new Vector3d((float)Math.Acos(angle), 0.0, -(float)Math.Asin(angle));
|
|
|
|
SessionSet3DPosition(
|
|
sessionHandle,
|
|
position,
|
|
position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start and stop updating out position.
|
|
/// </summary>
|
|
/// <param name="go"></param>
|
|
internal void PosUpdating(bool go)
|
|
{
|
|
if (go)
|
|
posRestart.Set();
|
|
else
|
|
posRestart.Reset();
|
|
}
|
|
|
|
private void PositionThreadBody()
|
|
{
|
|
while (true)
|
|
{
|
|
posRestart.WaitOne();
|
|
Thread.Sleep(1500);
|
|
UpdatePosition(Client.Self);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
}
|
|
}
|