/*
* 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
}
}