/* * Copyright (c) 2006-2016, openmetaverse.co * Copyright (c) 2021-2024, Sjofn LLC. * 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.co 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.Threading; using System.Xml; using OpenMetaverse.StructuredData; using OpenMetaverse; using OpenMetaverse.Http; using OpenMetaverse.Interfaces; using OpenMetaverse.Messages.Linden; using System.Net.Http; using System.Threading.Tasks; namespace LibreMetaverse.Voice { 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 string VoiceServer = VOICE_RELEASE_SERVER; private readonly GridClient _client; private bool _enabled; private TCPPipe _daemonPipe; protected VoiceStatus Status; private int _commandCookie; private string _tuningSoundFile = string.Empty; private readonly Dictionary _channelMap = new Dictionary(); private readonly List _captureDevices = new List(); private readonly List _renderDevices = new List(); #region Response Processing Variables private bool _isEvent; private bool _isChannel; private bool _isLocallyMuted; private bool _isModeratorMuted; private bool _isSpeaking; private int _cookie; //private int _returnCode; private int _statusCode; private int _volume; private int _state; private int _participantType; private float _energy; 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", 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() { return _enabled && ConnectToDaemon("127.0.0.1", DAEMON_PORT); } public bool ConnectToDaemon(string address, int port) { if (!_enabled) return false; _daemonPipe = new Voice.TCPPipe(); _daemonPipe.OnDisconnected += _DaemonPipe_OnDisconnected; _daemonPipe.OnReceiveLine += _DaemonPipe_OnReceiveLine; var se = _daemonPipe.Connect(address, port); if (se == null) { return true; } 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) { var 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('+', '-'); var idBytes = Convert.FromBase64String(accountName); return idBytes.Length == 16 ? new UUID(idBytes, 0) : UUID.Zero; } return UUID.Zero; } public string SipuriFromVoiceAccount(string account) { return $"sip:{account}@{VoiceServer}"; } public int RequestCaptureDevices() { if (_daemonPipe.Connected) { _daemonPipe.SendData(Encoding.ASCII.GetBytes( $"{REQUEST_TERMINATOR}")); return _commandCookie - 1; } 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( $"{REQUEST_TERMINATOR}")); return _commandCookie - 1; } 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; var accountServer = $"https://www.{VoiceServer}/api2/"; var logPath = "."; var request = new StringBuilder(); request.Append($""); request.Append("V2 SDK"); request.Append($"{accountServer}"); request.Append(""); request.Append("false"); request.Append($"{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, DownloadCompleteHandler callback, string capsName) { if (_enabled && _client.Network.Connected) { if (_client.Network.CurrentSim?.Caps != null) { var cap = _client.Network.CurrentSim.Caps.CapabilityURI(capsName); if (cap != null) { var req = _client.HttpCapsClient.PostRequestAsync(cap, OSDFormat.Xml, new OSDMap(), CancellationToken.None, callback); return true; } 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 connHandle) { if (_daemonPipe.Connected) { var request = new StringBuilder(); request.Append($""); request.Append($"{connHandle}"); request.Append($"{accountName}"); request.Append($"{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( $"{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( $"{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( $"{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", nameof(volume)); if (_daemonPipe.Connected) { _daemonPipe.SendData(Encoding.ASCII.GetBytes( $"{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", nameof(volume)); if (_daemonPipe.Connected) { _daemonPipe.SendData(Encoding.ASCII.GetBytes( $"{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( $"{_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( $"{_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) { var msg = (RequiredVoiceVersionMessage)message; if (VOICE_MAJOR_VERSION != msg.MajorVersion) { Logger.Log( $"Voice version mismatch! Got {msg.MajorVersion}, expecting {VOICE_MAJOR_VERSION}. Disabling the voice manager", Helpers.LogLevel.Error, _client); _enabled = false; } else { Logger.DebugLog("Voice version " + msg.MajorVersion + " verified", _client); } } private void ProvisionCapsResponse(HttpResponseMessage httpResponse, byte[] responseData, Exception error) { if (error != null) { Logger.Log("Failed to provision voice capability", Helpers.LogLevel.Warning, _client, error); return; } var response = OSDParser.Deserialize(responseData); if (!(response is OSDMap respMap)) return; if (OnProvisionAccount == null) return; try { OnProvisionAccount(respMap["username"].AsString(), respMap["password"].AsString()); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, _client, e); } } private void ParcelVoiceInfoResponse(HttpResponseMessage httpResponse, byte[] responseData, Exception error) { if (error != null) { Logger.Log("Failed to retrieve voice info", Helpers.LogLevel.Warning, _client, error); return; } var response = OSDParser.Deserialize(responseData); if (!(response is OSDMap respMap)) return; var regionName = respMap["region_name"].AsString(); var localId = respMap["parcel_local_id"].AsInteger(); string channelUri = null; if (respMap["voice_credentials"] is OSDMap) { var creds = (OSDMap)respMap["voice_credentials"]; channelUri = creds["channel_uri"].AsString(); } OnParcelVoiceInfo?.Invoke(regionName, localId, channelUri); } private static void _DaemonPipe_OnDisconnected(SocketException se) { if (se != null) Console.WriteLine("Disconnected! " + se.Message); else Console.WriteLine("Disconnected!"); } private void _DaemonPipe_OnReceiveLine(string line) { var 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 (var i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); if (reader.Name == "action") _actionString = reader.Value; else if (reader.Name == "type") _eventTypeString = reader.Value; } } } else { switch (reader.Name) { case "InputXml": _cookie = -1; // Parse through here to get the cookie value reader.Read(); if (reader.Name == "Request") { for (var i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); if (reader.Name != "requestId") continue; int.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; case XmlNodeType.None: break; case XmlNodeType.Attribute: break; case XmlNodeType.Text: break; case XmlNodeType.CDATA: break; case XmlNodeType.EntityReference: break; case XmlNodeType.Entity: break; case XmlNodeType.ProcessingInstruction: break; case XmlNodeType.Comment: break; case XmlNodeType.Document: break; case XmlNodeType.DocumentType: break; case XmlNodeType.DocumentFragment: break; case XmlNodeType.Notation: break; case XmlNodeType.Whitespace: break; case XmlNodeType.SignificantWhitespace: break; case XmlNodeType.EndEntity: break; case XmlNodeType.XmlDeclaration: break; default: throw new ArgumentOutOfRangeException(); } } if (_isEvent) { } //Client.DebugLog("VOICE: " + line); } private void ProcessEvent() { if (_isEvent) { switch (_eventTypeString) { case "LoginStateChangeEvent": OnLoginStateChange?.Invoke(_cookie, _accountHandle, _statusCode, _statusString, _state); break; case "SessionNewEvent": OnNewSession?.Invoke(_cookie, _accountHandle, _eventSessionHandle, _state, _nameString, _uriString); break; case "SessionStateChangeEvent": OnSessionStateChange?.Invoke(_cookie, _uriString, _statusCode, _statusString, _eventSessionHandle, _state, _isChannel, _nameString); break; case "ParticipantStateChangeEvent": OnParticipantStateChange?.Invoke(_cookie, _uriString, _statusCode, _statusString, _state, _nameString, _displayNameString, _participantType); break; case "ParticipantPropertiesEvent": OnParticipantProperties?.Invoke(_cookie, _uriString, _statusCode, _statusString, _isLocallyMuted, _isModeratorMuted, _isSpeaking, _volume, _energy); break; case "AuxAudioPropertiesEvent": OnAuxAudioProperties?.Invoke(_cookie, _energy); break; } } else { switch (_actionString) { case "Connector.Create.1": OnConnectorCreated?.Invoke(_cookie, _statusCode, _statusString, _connectorHandle); break; case "Account.Login.1": OnLogin?.Invoke(_cookie, _statusCode, _statusString, _accountHandle); break; case "Session.Create.1": OnSessionCreated?.Invoke(_cookie, _statusCode, _statusString, _sessionHandle); break; case "Session.Connect.1": OnSessionConnected?.Invoke(_cookie, _statusCode, _statusString); break; case "Session.Terminate.1": OnSessionTerminated?.Invoke(_cookie, _statusCode, _statusString); break; case "Account.Logout.1": OnAccountLogout?.Invoke(_cookie, _statusCode, _statusString); break; case "Connector.InitiateShutdown.1": OnConnectorInitiateShutdown?.Invoke(_cookie, _statusCode, _statusString); break; case "Account.ChannelGetList.1": OnAccountChannelGetList?.Invoke(_cookie, _statusCode, _statusString); break; case "Aux.GetCaptureDevices.1": OnCaptureDevices?.Invoke(_cookie, _statusCode, _statusString, _nameString); break; case "Aux.GetRenderDevices.1": OnRenderDevices?.Invoke(_cookie, _statusCode, _statusString, _nameString); break; } } } #endregion Callbacks } }