Files
MetaCrate/crates/libremetaverse-voice-vivox/src/gateway.rs
Chili Palmer c9a1170a27
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

1497 lines
52 KiB
Rust

//! Native Vivox gateway using the bounded TCP control protocol.
use crate::*;
use base64::Engine as _;
use libremetaverse_types::compat::{EventHandler, Subscription};
use libremetaverse_types::{UUID, Vector3d};
use roxmltree::{Document, Node};
use std::any::Any;
use std::collections::HashMap;
use std::sync::{
Arc, Mutex, Weak,
atomic::{AtomicBool, AtomicU64, Ordering},
};
const MAX_XML_BYTES: usize = 1024 * 1024;
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum EventKey {
AccountLoginResponse,
AccountLoginState,
AuxAudioProperties,
CaptureDevices,
RenderDevices,
ConnectorCreate,
DaemonConnected,
DaemonCouldntConnect,
DaemonCouldntRun,
DaemonDisconnected,
DaemonExited,
DaemonRunning,
SessionAdded,
SessionCreate,
SessionCreateResponse,
SessionGroupAdded,
SessionMedia,
SessionNew,
ParticipantAdded,
ParticipantProperties,
ParticipantRemoved,
ParticipantState,
ParticipantUpdated,
SessionRemove,
SessionRemoved,
SessionState,
SessionUpdated,
VoiceConnection,
VoiceMicTest,
VoiceResponse,
RawControlLine,
}
type ErasedHandler = Arc<dyn Fn(&dyn Any) + Send + Sync>;
#[derive(Default)]
struct EventBus {
next_id: AtomicU64,
handlers: Arc<Mutex<HashMap<EventKey, HashMap<u64, ErasedHandler>>>>,
}
impl EventBus {
fn subscribe<T>(&self, key: EventKey, handler: Option<EventHandler<T>>) -> Subscription
where
T: Any + Clone + Send + Sync + 'static,
{
let Some(handler) = handler else {
return Subscription::detached();
};
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let erased: ErasedHandler = Arc::new(move |value| {
if let Some(value) = value.downcast_ref::<T>() {
handler(value.clone());
}
});
lock(&self.handlers)
.entry(key)
.or_default()
.insert(id, erased);
let handlers = Arc::downgrade(&self.handlers);
Subscription::new(move || remove_handler(&handlers, key, id))
}
fn emit<T>(&self, key: EventKey, value: T)
where
T: Any + Send + Sync + 'static,
{
let handlers: Vec<_> = lock(&self.handlers)
.get(&key)
.into_iter()
.flat_map(|handlers| handlers.values().cloned())
.collect();
for handler in handlers {
handler(&value);
}
}
}
fn remove_handler(
handlers: &Weak<Mutex<HashMap<EventKey, HashMap<u64, ErasedHandler>>>>,
key: EventKey,
id: u64,
) {
let Some(handlers) = handlers.upgrade() else {
return;
};
let mut handlers = lock(&handlers);
if let Some(event) = handlers.get_mut(&key) {
event.remove(&id);
if event.is_empty() {
handlers.remove(&key);
}
}
}
#[derive(Default)]
struct GatewayState {
active: bool,
current_capture_device: String,
current_playback_device: String,
daemon_connected: bool,
daemon_running: bool,
request_id: i32,
testing: bool,
connector_handle: String,
account_handle: String,
session_handle: String,
sip_server: String,
sessions: HashMap<String, VoiceSession>,
}
struct GatewayInner {
_client: libremetaverse::GridClient,
pipe: TCPPipe,
state: Mutex<GatewayState>,
events: EventBus,
pipe_subscriptions: Mutex<Vec<Subscription>>,
disposed: AtomicBool,
}
#[derive(Clone)]
pub struct VoiceGateway {
inner: Arc<GatewayInner>,
}
macro_rules! event_subscription {
($name:ident, $key:ident, $type:ty) => {
#[must_use]
pub fn $name(&self, handler: Option<EventHandler<$type>>) -> Subscription {
self.inner.events.subscribe(EventKey::$key, handler)
}
};
}
macro_rules! zero_callback_subscription {
($name:ident, $key:ident, $type:ty) => {
#[must_use]
pub fn $name(&self, handler: Option<$type>) -> Subscription {
let handler = handler.map(|handler| {
Arc::new(move |()| {
let _ = handler.invoke();
}) as EventHandler<()>
});
self.inner.events.subscribe(EventKey::$key, handler)
}
};
}
impl VoiceGateway {
pub fn new(c: libremetaverse::GridClient) -> Result<Self, Error> {
let inner = Arc::new(GatewayInner {
_client: c,
pipe: TCPPipe::new()?,
state: Mutex::new(GatewayState::default()),
events: EventBus::default(),
pipe_subscriptions: Mutex::new(Vec::new()),
disposed: AtomicBool::new(false),
});
let weak = Arc::downgrade(&inner);
let received =
inner
.pipe
.subscribe_on_receive_line(Some(TCPPipeOnReceiveLineCallback::from_handler(
move |line| {
weak.upgrade()
.ok_or(Error::InvalidOperation)
.and_then(|inner| inner.handle_line(&line))
},
)));
let weak = Arc::downgrade(&inner);
let disconnected = inner.pipe.subscribe_on_disconnected(Some(
TCPPipeOnDisconnectedCallback::from_handler(move |_| {
let inner = weak.upgrade().ok_or(Error::InvalidOperation)?;
let mut state = lock(&inner.state);
state.daemon_connected = false;
drop(state);
inner.events.emit(EventKey::DaemonDisconnected, ());
Ok(())
}),
));
*lock(&inner.pipe_subscriptions) = vec![received, disconnected];
Ok(Self { inner })
}
/// Feeds one already-framed control line through the same bounded parser
/// used by the TCP reader. This is shared by the legacy manager facade.
#[cfg(test)]
pub(crate) fn process_control_line(&self, line: &str) -> Result<(), Error> {
self.inner.handle_line(line)
}
pub(crate) fn subscribe_control_line(&self, handler: EventHandler<String>) -> Subscription {
self.inner
.events
.subscribe(EventKey::RawControlLine, Some(handler))
}
event_subscription!(
subscribe_on_account_login_response,
AccountLoginResponse,
VoiceGatewayVoiceAccountEventArgs
);
event_subscription!(
subscribe_on_account_login_state_change_event,
AccountLoginState,
VoiceGatewayAccountLoginStateChangeEventArgs
);
event_subscription!(
subscribe_on_aux_audio_properties_event,
AuxAudioProperties,
VoiceGatewayAudioPropertiesEventArgs
);
event_subscription!(
subscribe_on_aux_get_capture_devices_response,
CaptureDevices,
VoiceGatewayVoiceDevicesEventArgs
);
event_subscription!(
subscribe_on_aux_get_render_devices_response,
RenderDevices,
VoiceGatewayVoiceDevicesEventArgs
);
event_subscription!(
subscribe_on_connector_create_response,
ConnectorCreate,
VoiceGatewayVoiceConnectorEventArgs
);
zero_callback_subscription!(
subscribe_on_daemon_connected,
DaemonConnected,
VoiceGatewayDaemonConnectedCallback
);
zero_callback_subscription!(
subscribe_on_daemon_couldnt_connect,
DaemonCouldntConnect,
VoiceGatewayDaemonCouldntConnectCallback
);
zero_callback_subscription!(
subscribe_on_daemon_couldnt_run,
DaemonCouldntRun,
VoiceGatewayDaemonCouldntRunCallback
);
zero_callback_subscription!(
subscribe_on_daemon_disconnected,
DaemonDisconnected,
VoiceGatewayDaemonDisconnectedCallback
);
zero_callback_subscription!(
subscribe_on_daemon_exited,
DaemonExited,
VoiceGatewayDaemonExitedCallback
);
zero_callback_subscription!(
subscribe_on_daemon_running,
DaemonRunning,
VoiceGatewayDaemonRunningCallback
);
event_subscription!(
subscribe_on_session_added_event,
SessionAdded,
VoiceGatewaySessionAddedEventArgs
);
event_subscription!(subscribe_on_session_create, SessionCreate, ());
event_subscription!(
subscribe_on_session_create_response,
SessionCreateResponse,
VoiceGatewayVoiceSessionEventArgs
);
event_subscription!(
subscribe_on_session_group_added_event,
SessionGroupAdded,
VoiceGatewaySessionGroupAddedEventArgs
);
event_subscription!(
subscribe_on_session_media_event,
SessionMedia,
VoiceGatewaySessionMediaEventArgs
);
event_subscription!(
subscribe_on_session_new_event,
SessionNew,
VoiceGatewayNewSessionEventArgs
);
event_subscription!(
subscribe_on_session_participant_added_event,
ParticipantAdded,
VoiceGatewayParticipantAddedEventArgs
);
event_subscription!(
subscribe_on_session_participant_properties_event,
ParticipantProperties,
VoiceGatewayParticipantPropertiesEventArgs
);
event_subscription!(
subscribe_on_session_participant_removed_event,
ParticipantRemoved,
VoiceGatewayParticipantRemovedEventArgs
);
event_subscription!(
subscribe_on_session_participant_state_change_event,
ParticipantState,
VoiceGatewayParticipantStateChangeEventArgs
);
event_subscription!(
subscribe_on_session_participant_updated_event,
ParticipantUpdated,
VoiceGatewayParticipantUpdatedEventArgs
);
event_subscription!(subscribe_on_session_remove, SessionRemove, ());
event_subscription!(
subscribe_on_session_removed_event,
SessionRemoved,
VoiceGatewaySessionRemovedEventArgs
);
event_subscription!(
subscribe_on_session_state_change_event,
SessionState,
VoiceGatewaySessionStateChangeEventArgs
);
event_subscription!(
subscribe_on_session_updated_event,
SessionUpdated,
VoiceGatewaySessionUpdatedEventArgs
);
event_subscription!(
subscribe_on_voice_response,
VoiceResponse,
VoiceGatewayVoiceResponseEventArgs
);
#[must_use]
pub fn subscribe_on_voice_connection_change(
&self,
handler: Option<VoiceGatewayVoiceConnectionChangeCallback>,
) -> Subscription {
let handler = handler.map(|handler| {
Arc::new(move |state| {
let _ = handler.invoke(state);
}) as EventHandler<VoiceGatewayConnectionState>
});
self.inner
.events
.subscribe(EventKey::VoiceConnection, handler)
}
#[must_use]
pub fn subscribe_on_voice_mic_test(
&self,
handler: Option<VoiceGatewayVoiceMicTestCallback>,
) -> Subscription {
let handler = handler.map(|handler| {
Arc::new(move |level| {
let _ = handler.invoke(level);
}) as EventHandler<f32>
});
self.inner.events.subscribe(EventKey::VoiceMicTest, handler)
}
pub fn connect_to_daemon(&self, address: String, port: i32) -> Result<bool, Error> {
self.ensure_active_object()?;
let connected = self.inner.pipe.connect(address, port)?.is_none();
{
let mut state = lock(&self.inner.state);
state.daemon_connected = connected;
state.daemon_running = connected;
}
if connected {
self.inner.events.emit(EventKey::DaemonRunning, ());
self.inner.events.emit(EventKey::DaemonConnected, ());
self.inner.events.emit(
EventKey::VoiceConnection,
VoiceGatewayConnectionState::DaemonConnected,
);
} else {
self.inner.events.emit(EventKey::DaemonCouldntConnect, ());
}
Ok(connected)
}
pub fn start(&self) -> Result<(), Error> {
self.ensure_active_object()?;
lock(&self.inner.state).active = true;
Ok(())
}
pub fn stop(&self) -> Result<(), Error> {
if self.inner.disposed.load(Ordering::Acquire) {
return Ok(());
}
let (session, account, connector, connected) = {
let state = lock(&self.inner.state);
(
state.session_handle.clone(),
state.account_handle.clone(),
state.connector_handle.clone(),
state.daemon_connected,
)
};
if connected {
if !session.is_empty() {
let _ = self.session_terminate(session);
}
if !account.is_empty() {
let _ = self.account_logout(account);
}
if !connector.is_empty() {
let _ = self.connector_initiate_shutdown(connector);
}
}
self.inner.pipe.disconnect()?;
let mut state = lock(&self.inner.state);
state.active = false;
state.daemon_connected = false;
state.daemon_running = false;
state.connector_handle.clear();
state.account_handle.clear();
state.session_handle.clear();
Ok(())
}
pub fn dispose(&self) -> Result<(), Error> {
if self.inner.disposed.swap(true, Ordering::AcqRel) {
return Ok(());
}
self.stop_after_dispose()
}
fn stop_after_dispose(&self) -> Result<(), Error> {
self.inner.pipe.disconnect()?;
let mut state = lock(&self.inner.state);
state.active = false;
state.daemon_connected = false;
state.daemon_running = false;
Ok(())
}
/// Native releases connect to an explicitly supplied service and never
/// spawn a proprietary daemon or child process.
pub fn start_daemon(&self, path: String, args: String) -> Result<(), Error> {
if path.is_empty() || args.len() > 64 * 1024 {
return Err(Error::Argument);
}
self.inner.events.emit(EventKey::DaemonCouldntRun, ());
Err(Error::InvalidOperation)
}
pub fn stop_daemon(&self) -> Result<(), Error> {
let was_running = lock(&self.inner.state).daemon_running;
self.inner.pipe.disconnect()?;
let mut state = lock(&self.inner.state);
state.daemon_running = false;
state.daemon_connected = false;
drop(state);
if was_running {
self.inner.events.emit(EventKey::DaemonExited, ());
}
Ok(())
}
pub fn request_with_string(&self, action: String) -> Result<i32, Error> {
self.request_with_string_string(action, None)
}
pub fn request_with_string_string(
&self,
action: String,
request_xml: Option<String>,
) -> Result<i32, Error> {
self.ensure_active_object()?;
if !valid_xml_name(&action) {
return Err(Error::Argument);
}
let connected = lock(&self.inner.state).daemon_connected;
if !connected {
return Ok(-1);
}
let body = request_xml.unwrap_or_default();
validate_fragment(&body)?;
let request_id = {
let mut state = lock(&self.inner.state);
let id = state.request_id;
state.request_id = state
.request_id
.checked_add(1)
.ok_or(Error::InvalidOperation)?;
id
};
let request = if body.is_empty() {
format!("<Request requestId=\"{request_id}\" action=\"{action}\" />\n\n\n")
} else {
format!(
"<Request requestId=\"{request_id}\" action=\"{action}\">{body}</Request>\n\n\n"
)
};
if request.len() > MAX_XML_BYTES {
return Err(Error::Argument);
}
let bytes = request
.chars()
.map(|character| {
if character.is_ascii() {
character as u8
} else {
b'?'
}
})
.collect();
match self.inner.pipe.send_data(bytes) {
Ok(()) => Ok(request_id),
Err(Error::Socket | Error::InvalidOperation) => Ok(-1),
Err(error) => Err(error),
}
}
pub fn make_xml(name: String, text: String) -> Result<String, Error> {
xml_element(&name, &text)
}
pub fn account_login(
&self,
connector_handle: String,
account_name: String,
account_password: String,
audio_session_answer_mode: String,
account_uri: String,
participant_property_frequency: i32,
enable_buddies_and_presence: bool,
) -> Result<i32, Error> {
let body = xml_fields(&[
("ConnectorHandle", connector_handle),
("AccountName", account_name),
("AccountPassword", account_password),
("AudioSessionAnswerMode", audio_session_answer_mode),
("AccountURI", account_uri),
(
"ParticipantPropertyFrequency",
participant_property_frequency.to_string(),
),
(
"EnableBuddiesAndPresence",
enable_buddies_and_presence.to_string(),
),
("BuddyManagementMode", "Application".into()),
])?;
self.request_with_string_string("Account.Login.1".into(), Some(body))
}
pub fn account_logout(&self, account_handle: String) -> Result<i32, Error> {
self.request_field("Account.Logout.1", "AccountHandle", account_handle)
}
pub fn aux_get_capture_devices(&self) -> Result<i32, Error> {
self.request_with_string("Aux.GetCaptureDevices.1".into())
}
pub fn aux_get_render_devices(&self) -> Result<i32, Error> {
self.request_with_string("Aux.GetRenderDevices.1".into())
}
pub fn aux_set_render_device(&self, value: String) -> Result<i32, Error> {
self.request_field("Aux.SetRenderDevice.1", "RenderDeviceSpecifier", value)
}
pub fn aux_set_capture_device(&self, value: String) -> Result<i32, Error> {
self.request_field("Aux.SetCaptureDevice.1", "CaptureDeviceSpecifier", value)
}
pub fn aux_capture_audio_start(&self, duration: i32) -> Result<i32, Error> {
self.request_field("Aux.CaptureAudioStart.1", "Duration", duration.to_string())
}
pub fn aux_capture_audio_stop(&self) -> Result<i32, Error> {
self.request_with_string("Aux.CaptureAudioStop.1".into())
}
pub fn aux_set_mic_level(&self, level: i32) -> Result<i32, Error> {
self.request_field("Aux.SetMicLevel.1", "Level", level.to_string())
}
pub fn aux_set_speaker_level(&self, level: i32) -> Result<i32, Error> {
self.request_field("Aux.SetSpeakerLevel.1", "Level", level.to_string())
}
pub fn connector_create(
&self,
client_name: String,
account_management_server: String,
minimum_port: u16,
maximum_port: u16,
logging: VoiceGatewayVoiceLoggingSettings,
) -> Result<i32, Error> {
if minimum_port > maximum_port {
return Err(Error::Argument);
}
let logging_xml = xml_fields(&[
("Enabled", logging.enabled.to_string()),
("Folder", logging.folder),
("FileNamePrefix", logging.file_name_prefix),
("FileNameSuffix", logging.file_name_suffix),
("LogLevel", logging.log_level.to_string()),
])?;
let body = format!(
"{}<Logging>{logging_xml}</Logging>",
xml_fields(&[
("ClientName", client_name),
("AccountManagementServer", account_management_server),
("MinimumPort", minimum_port.to_string()),
("MaximumPort", maximum_port.to_string()),
("Mode", "Normal".into()),
])?
);
self.request_with_string_string("Connector.Create.1".into(), Some(body))
}
pub fn connector_initiate_shutdown(&self, handle: String) -> Result<i32, Error> {
self.request_field("Connector.InitiateShutdown.1", "ConnectorHandle", handle)
}
pub fn connector_mute_local_mic(&self, handle: String, mute: bool) -> Result<i32, Error> {
self.connector_value_request("Connector.MuteLocalMic.1", handle, mute.to_string())
}
pub fn connector_mute_local_speaker(&self, handle: String, mute: bool) -> Result<i32, Error> {
self.connector_value_request("Connector.MuteLocalSpeaker.1", handle, mute.to_string())
}
pub fn connector_set_local_mic_volume(&self, handle: String, value: i32) -> Result<i32, Error> {
self.connector_value_request("Connector.SetLocalMicVolume.1", handle, value.to_string())
}
pub fn connector_set_local_speaker_volume(
&self,
handle: String,
value: i32,
) -> Result<i32, Error> {
self.connector_value_request(
"Connector.SetLocalSpeakerVolume.1",
handle,
value.to_string(),
)
}
pub fn session_connect(
&self,
session_handle: String,
audio_media: String,
) -> Result<i32, Error> {
self.request_fields_action(
"Session.Connect.1",
&[
("SessionHandle", session_handle),
("AudioMedia", audio_media),
],
)
}
#[allow(clippy::too_many_arguments)]
pub fn session_create(
&self,
account_handle: String,
session_uri: String,
name: String,
password: String,
join_audio: bool,
join_text: bool,
password_hash_algorithm: String,
) -> Result<i32, Error> {
let mut fields = vec![
("AccountHandle", account_handle),
("URI", session_uri),
("Name", name),
];
if !password.is_empty() {
fields.push(("Password", password));
fields.push(("PasswordHashAlgorithm", password_hash_algorithm));
}
fields.extend([
("ConnectAudio", join_audio.to_string()),
("ConnectText", join_text.to_string()),
("JoinAudio", join_audio.to_string()),
("JoinText", join_text.to_string()),
("VoiceFontID", "0".into()),
]);
self.request_fields_action("Session.Create.1", &fields)
}
pub fn session_render_audio_start(
&self,
sound_file_path: String,
loop_: bool,
) -> Result<i32, Error> {
self.request_fields_action(
"Session.RenderAudioStart.1",
&[
("SoundFilePath", sound_file_path),
("Loop", if loop_ { "1" } else { "0" }.into()),
],
)
}
pub fn session_render_audio_stop(&self, path: String) -> Result<i32, Error> {
self.request_field("Session.RenderAudioStop.1", "SoundFilePath", path)
}
pub fn session_terminate(&self, handle: String) -> Result<i32, Error> {
self.request_field("Session.Terminate.1", "SessionHandle", handle)
}
pub fn session_set_participant_volume_for_me(
&self,
session_handle: String,
participant_uri: String,
volume: i32,
) -> Result<i32, Error> {
self.request_fields_action(
"Session.SetParticipantVolumeForMe.1",
&[
("SessionHandle", session_handle),
("ParticipantURI", participant_uri),
("Volume", volume.to_string()),
],
)
}
pub(crate) fn session_set_participant_mute_for_me(
&self,
session_handle: String,
participant_uri: String,
mute: bool,
) -> Result<i32, Error> {
self.request_fields_action(
"Session.SetParticipantMuteForMe.1",
&[
("SessionHandle", session_handle),
("ParticipantURI", participant_uri),
("Mute", if mute { "1" } else { "0" }.into()),
],
)
}
pub fn session_set3_d_position(
&self,
session_handle: String,
speaker_position: VoicePosition,
listener_position: VoicePosition,
) -> Result<i32, Error> {
let body = format!(
"{}<SpeakerPosition>{}</SpeakerPosition><ListenerPosition>{}</ListenerPosition>",
xml_element("SessionHandle", &session_handle)?,
position_xml(&speaker_position)?,
position_xml(&listener_position)?,
);
self.request_with_string_string("Session.Set3DPosition.1".into(), Some(body))
}
pub fn sip_from_uuid(&self, id: UUID) -> Result<String, Error> {
if id == UUID::zero() {
return Ok(format!("sip:@{}", lock(&self.inner.state).sip_server));
}
let encoded = base64::engine::general_purpose::STANDARD
.encode(id.get_bytes()?)
.replace('+', "-")
.replace('/', "_");
Ok(format!(
"sip:x{encoded}@{}",
lock(&self.inner.state).sip_server
))
}
#[must_use]
pub fn current_capture_device(&self) -> String {
lock(&self.inner.state).current_capture_device.clone()
}
pub fn set_current_capture_device(&mut self, value: String) {
lock(&self.inner.state).current_capture_device = value.clone();
let _ = self.aux_set_capture_device(value);
}
#[must_use]
pub fn playback_device(&self) -> String {
lock(&self.inner.state).current_playback_device.clone()
}
pub fn set_playback_device(&mut self, value: String) {
lock(&self.inner.state).current_playback_device = value.clone();
let _ = self.aux_set_render_device(value);
}
#[must_use]
pub fn daemon_is_connected(&self) -> bool {
lock(&self.inner.state).daemon_connected
}
pub fn set_daemon_is_connected(&mut self, value: bool) {
lock(&self.inner.state).daemon_connected = value;
}
#[must_use]
pub fn daemon_is_running(&self) -> bool {
lock(&self.inner.state).daemon_running
}
pub fn set_daemon_is_running(&mut self, value: bool) {
lock(&self.inner.state).daemon_running = value;
}
#[must_use]
pub fn request_id(&self) -> i32 {
lock(&self.inner.state).request_id
}
pub fn set_request_id(&mut self, value: i32) {
lock(&self.inner.state).request_id = value;
}
pub fn set_mic_level(&mut self, value: i32) {
let handle = lock(&self.inner.state).connector_handle.clone();
let _ = self.connector_set_local_mic_volume(handle, value);
}
pub fn set_spkr_level(&mut self, value: i32) {
let handle = lock(&self.inner.state).connector_handle.clone();
let _ = self.connector_set_local_speaker_volume(handle, value);
}
pub fn set_mic_mute(&mut self, value: bool) {
let handle = lock(&self.inner.state).connector_handle.clone();
let _ = self.connector_mute_local_mic(handle, value);
}
pub fn set_spkr_mute(&mut self, value: bool) {
let handle = lock(&self.inner.state).connector_handle.clone();
let _ = self.connector_mute_local_speaker(handle, value);
}
#[must_use]
pub fn test_mode(&self) -> bool {
lock(&self.inner.state).testing
}
pub fn set_test_mode(&mut self, value: bool) {
lock(&self.inner.state).testing = value;
if value {
let _ = self.aux_capture_audio_start(0);
} else {
let _ = self.aux_capture_audio_stop();
}
}
fn ensure_active_object(&self) -> Result<(), Error> {
if self.inner.disposed.load(Ordering::Acquire) {
Err(Error::InvalidOperation)
} else {
Ok(())
}
}
fn request_field(&self, action: &str, name: &str, value: String) -> Result<i32, Error> {
self.request_fields_action(action, &[(name, value)])
}
fn request_fields_action(&self, action: &str, fields: &[(&str, String)]) -> Result<i32, Error> {
self.request_with_string_string(action.into(), Some(xml_fields(fields)?))
}
fn connector_value_request(
&self,
action: &str,
handle: String,
value: String,
) -> Result<i32, Error> {
self.request_fields_action(action, &[("ConnectorHandle", handle), ("Value", value)])
}
}
impl GatewayInner {
fn handle_line(self: &Arc<Self>, line: &str) -> Result<(), Error> {
if line.len() > MAX_XML_BYTES || line.contains("<!DOCTYPE") || line.contains("<!ENTITY") {
return Err(Error::Argument);
}
let document = Document::parse(line).map_err(|_| Error::Argument)?;
self.events.emit(EventKey::RawControlLine, line.to_owned());
let root = document.root_element();
match root.tag_name().name() {
"Response" => self.handle_response(root),
"Event" => self.handle_event(root),
_ => Err(Error::Argument),
}
}
fn handle_response(self: &Arc<Self>, root: Node<'_, '_>) -> Result<(), Error> {
let action = root.attribute("action").ok_or(Error::Argument)?;
let return_code = field_i32(root, "ReturnCode");
let status_code = field_i32(root, "StatusCode");
let status = field(root, "StatusString");
match action {
"Connector.Create.1" => {
let version = field(root, "VersionID");
let handle = field(root, "ConnectorHandle");
lock(&self.state).connector_handle = handle.clone();
self.events.emit(
EventKey::ConnectorCreate,
VoiceGatewayVoiceConnectorEventArgs::new(
return_code,
status_code,
status,
version,
handle,
)?,
);
}
"Aux.GetCaptureDevices.1" | "Aux.GetRenderDevices.1" => {
let capture = action.contains("Capture");
let item = if capture {
"CaptureDevice"
} else {
"RenderDevice"
};
let current_item = if capture {
"CurrentCaptureDevice"
} else {
"CurrentRenderDevice"
};
let devices = device_values(root, item);
let current = nested_device(root, current_item);
let type_ = if capture {
VoiceGatewayResponseType::GetCaptureDevices
} else {
VoiceGatewayResponseType::GetRenderDevices
};
let key = if capture {
lock(&self.state).current_capture_device = current.clone();
EventKey::CaptureDevices
} else {
lock(&self.state).current_playback_device = current.clone();
EventKey::RenderDevices
};
self.events.emit(
key,
VoiceGatewayVoiceDevicesEventArgs::new(
type_,
return_code,
status_code,
status,
current,
devices,
)?,
);
}
"Account.Login.1" => {
let handle = field(root, "AccountHandle");
lock(&self.state).account_handle = handle.clone();
self.events.emit(
EventKey::AccountLoginResponse,
VoiceGatewayVoiceAccountEventArgs::new(
return_code,
status_code,
status,
handle,
)?,
);
}
"Session.Create.1" => {
let handle = field(root, "SessionHandle");
let mut state = lock(&self.state);
state.session_handle = handle.clone();
if !handle.is_empty() && return_code == 0 && status_code == 0 {
let gateway = VoiceGateway {
inner: Arc::clone(self),
};
state
.sessions
.insert(handle.clone(), VoiceSession::new(gateway, handle.clone())?);
}
drop(state);
self.events.emit(
EventKey::SessionCreateResponse,
VoiceGatewayVoiceSessionEventArgs::new(
return_code,
status_code,
status,
handle,
)?,
);
self.events.emit(EventKey::SessionCreate, ());
}
_ => {
if let Some(type_) = response_type(action) {
self.events.emit(
EventKey::VoiceResponse,
VoiceGatewayVoiceResponseEventArgs::new(
type_,
return_code,
status_code,
status,
)?,
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn handle_event(self: &Arc<Self>, root: Node<'_, '_>) -> Result<(), Error> {
match root.attribute("type").ok_or(Error::Argument)? {
"LoginStateChangeEvent" | "AccountLoginStateChangeEvent" => self.events.emit(
EventKey::AccountLoginState,
VoiceGatewayAccountLoginStateChangeEventArgs::new(
field(root, "AccountHandle"),
field_i32(root, "StatusCode"),
field(root, "StatusString"),
login_state(field_i32(root, "State")),
)?,
),
"SessionNewEvent" => self.events.emit(
EventKey::SessionNew,
VoiceGatewayNewSessionEventArgs::new(
field(root, "AccountHandle"),
field(root, "SessionHandle"),
field(root, "URI"),
field_bool(root, "IsChannel"),
field(root, "Name"),
field(root, "AudioMedia"),
)?,
),
"SessionStateChangeEvent" => self.events.emit(
EventKey::SessionState,
VoiceGatewaySessionStateChangeEventArgs::new(
field(root, "SessionHandle"),
field_i32(root, "StatusCode"),
field(root, "StatusString"),
session_state(field_i32(root, "State")),
field(root, "URI"),
field_bool(root, "IsChannel"),
field(root, "ChannelName"),
)?,
),
"ParticipantAddedEvent" => {
let session_handle = field(root, "SessionHandle");
let participant_uri = field(root, "ParticipantUri");
if let Some(session) = lock(&self.state).sessions.get(&session_handle).cloned() {
session.add_participant(participant_uri.clone())?;
}
self.events.emit(
EventKey::ParticipantAdded,
VoiceGatewayParticipantAddedEventArgs::new(
field(root, "SessionGroupHandle"),
session_handle,
participant_uri,
field(root, "AccountName"),
field(root, "DisplayName"),
participant_type(field_i32(root, "ParticipantType")),
field(root, "Application"),
)?,
);
}
"ParticipantRemovedEvent" => {
let session_handle = field(root, "SessionHandle");
let participant_uri = field(root, "ParticipantUri");
if let Some(session) = lock(&self.state).sessions.get(&session_handle).cloned() {
session.remove_participant(&participant_uri);
}
self.events.emit(
EventKey::ParticipantRemoved,
VoiceGatewayParticipantRemovedEventArgs::new(
field(root, "SessionGroupHandle"),
session_handle,
participant_uri,
field(root, "AccountName"),
field(root, "Reason"),
)?,
);
}
"ParticipantStateChangeEvent" => self.events.emit(
EventKey::ParticipantState,
VoiceGatewayParticipantStateChangeEventArgs::new(
field(root, "SessionHandle"),
field_i32(root, "StatusCode"),
field(root, "StatusString"),
participant_state(field_i32(root, "State")),
field(root, "ParticipantUri"),
field(root, "AccountName"),
field(root, "DisplayName"),
participant_type(field_i32(root, "ParticipantType")),
)?,
),
"ParticipantPropertiesEvent" => {
let session_handle = field(root, "SessionHandle");
let participant_uri = field(root, "ParticipantUri");
let locally_muted = field_bool(root, "IsLocallyMuted");
let moderator_muted = field_bool(root, "IsModeratorMuted");
let speaking = field_bool(root, "IsSpeaking");
let volume = field_i32(root, "Volume");
let energy = field_f32(root, "Energy");
if let Some(session) = lock(&self.state).sessions.get(&session_handle).cloned() {
session.update_participant(
&participant_uri,
locally_muted || moderator_muted,
speaking,
volume,
energy,
);
}
self.events.emit(
EventKey::ParticipantProperties,
VoiceGatewayParticipantPropertiesEventArgs::new(
session_handle,
participant_uri,
locally_muted,
moderator_muted,
speaking,
volume,
energy,
)?,
);
}
"ParticipantUpdatedEvent" => {
let session_handle = field(root, "SessionHandle");
let participant_uri = field(root, "ParticipantUri");
let moderator_muted = field_bool(root, "IsModeratorMuted");
let speaking = field_bool(root, "IsSpeaking");
let volume = field_i32(root, "Volume");
let energy = field_f32(root, "Energy");
if let Some(session) = lock(&self.state).sessions.get(&session_handle).cloned() {
session.update_participant(
&participant_uri,
moderator_muted,
speaking,
volume,
energy,
);
}
self.events.emit(
EventKey::ParticipantUpdated,
VoiceGatewayParticipantUpdatedEventArgs::new(
session_handle,
participant_uri,
moderator_muted,
speaking,
volume,
energy,
)?,
);
}
"SessionGroupAddedEvent" => self.events.emit(
EventKey::SessionGroupAdded,
VoiceGatewaySessionGroupAddedEventArgs::new(
field(root, "AccountHandle"),
field(root, "SessionGroupHandle"),
field(root, "Type"),
)?,
),
"SessionAddedEvent" => self.events.emit(
EventKey::SessionAdded,
VoiceGatewaySessionAddedEventArgs::new(
field(root, "SessionGroupHandle"),
field(root, "SessionHandle"),
field(root, "Uri"),
field_bool(root, "IsChannel"),
field_bool(root, "Incoming"),
)?,
),
"SessionRemovedEvent" => {
let session_handle = field(root, "SessionHandle");
if let Some(session) = lock(&self.state).sessions.remove(&session_handle) {
session.close();
}
self.events.emit(
EventKey::SessionRemoved,
VoiceGatewaySessionRemovedEventArgs::new(
field(root, "SessionGroupHandle"),
session_handle,
field(root, "Uri"),
)?,
);
self.events.emit(EventKey::SessionRemove, ());
}
"SessionUpdatedEvent" => self.events.emit(
EventKey::SessionUpdated,
VoiceGatewaySessionUpdatedEventArgs::new(
field(root, "SessionGroupHandle"),
field(root, "SessionHandle"),
field(root, "Uri"),
field_i32(root, "IsMuted") != 0,
field_i32(root, "Volume"),
field_i32(root, "TransmitEnabled") != 0,
field_i32(root, "IsFocused") != 0,
)?,
),
"AuxAudioPropertiesEvent" => {
let energy = field_f32(root, "MicEnergy");
self.events.emit(
EventKey::AuxAudioProperties,
VoiceGatewayAudioPropertiesEventArgs::new(
field_bool(root, "MicIsActive"),
energy,
field_i32(root, "MicVolume"),
field_i32(root, "SpeakerVolume"),
)?,
);
self.events.emit(EventKey::VoiceMicTest, energy);
}
"SessionMediaEvent" => self.events.emit(
EventKey::SessionMedia,
VoiceGatewaySessionMediaEventArgs::new(
field(root, "SessionHandle"),
field_bool(root, "HasText"),
field_bool(root, "HasAudio"),
field_bool(root, "HasVideo"),
field_bool(root, "Terminated"),
)?,
),
_ => {}
}
Ok(())
}
}
fn valid_xml_name(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
}
fn validate_fragment(fragment: &str) -> Result<(), Error> {
if fragment.len() > MAX_XML_BYTES
|| fragment.contains("<!DOCTYPE")
|| fragment.contains("<!ENTITY")
{
return Err(Error::Argument);
}
if !fragment.is_empty() {
Document::parse(&format!("<Root>{fragment}</Root>")).map_err(|_| Error::Argument)?;
}
Ok(())
}
fn xml_element(name: &str, text: &str) -> Result<String, Error> {
if !valid_xml_name(name) || text.len() > 64 * 1024 {
return Err(Error::Argument);
}
if text.is_empty() {
return Ok(format!("<{name} />"));
}
let mut escaped = String::with_capacity(text.len());
for character in text.chars() {
match character {
'&' => escaped.push_str("&amp;"),
'<' => escaped.push_str("&lt;"),
'>' => escaped.push_str("&gt;"),
'\'' => escaped.push_str("&apos;"),
'"' => escaped.push_str("&quot;"),
'\0'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f' => {
return Err(Error::Argument);
}
_ => escaped.push(character),
}
}
Ok(format!("<{name}>{escaped}</{name}>"))
}
fn xml_fields(fields: &[(&str, String)]) -> Result<String, Error> {
let mut output = String::new();
for (name, value) in fields {
output.push_str(&xml_element(name, value)?);
}
Ok(output)
}
fn position_xml(position: &VoicePosition) -> Result<String, Error> {
fn vector(name: &str, value: Vector3d) -> Result<String, Error> {
Ok(format!(
"<{name}>{}</{name}>",
xml_fields(&[
("X", value.x.to_string()),
("Y", value.y.to_string()),
("Z", value.z.to_string()),
])?
))
}
Ok([
vector("Position", position.position)?,
vector("Velocity", position.velocity)?,
vector("AtOrientation", position.at_orientation)?,
vector("UpOrientation", position.up_orientation)?,
vector("LeftOrientation", position.left_orientation)?,
]
.concat())
}
fn field(root: Node<'_, '_>, name: &str) -> String {
root.descendants()
.find(|node| node.has_tag_name(name))
.and_then(|node| node.text())
.unwrap_or_default()
.to_owned()
}
fn field_i32(root: Node<'_, '_>, name: &str) -> i32 {
field(root, name).parse().unwrap_or_default()
}
fn field_f32(root: Node<'_, '_>, name: &str) -> f32 {
field(root, name).parse().unwrap_or_default()
}
fn field_bool(root: Node<'_, '_>, name: &str) -> bool {
matches!(
field(root, name).to_ascii_lowercase().as_str(),
"true" | "1"
)
}
fn nested_device(root: Node<'_, '_>, name: &str) -> String {
root.descendants()
.find(|node| node.has_tag_name(name))
.map(|node| field(node, "Device"))
.unwrap_or_default()
}
fn device_values(root: Node<'_, '_>, name: &str) -> Vec<String> {
root.descendants()
.filter(|node| node.has_tag_name(name))
.map(|node| field(node, "Device"))
.filter(|value| !value.is_empty())
.collect()
}
fn response_type(action: &str) -> Option<VoiceGatewayResponseType> {
Some(match action {
"Connector.InitiateShutdown.1" => VoiceGatewayResponseType::ConnectorInitiateShutdown,
"Aux.SetRenderDevice.1" => VoiceGatewayResponseType::SetRenderDevice,
"Connector.MuteLocalMic.1" => VoiceGatewayResponseType::MuteLocalMic,
"Connector.MuteLocalSpeaker.1" => VoiceGatewayResponseType::MuteLocalSpeaker,
"Connector.SetLocalMicVolume.1" => VoiceGatewayResponseType::SetLocalMicVolume,
"Connector.SetLocalSpeakerVolume.1" => VoiceGatewayResponseType::SetLocalSpeakerVolume,
"Aux.SetCaptureDevice.1" => VoiceGatewayResponseType::SetCaptureDevice,
"Session.RenderAudioStart.1" => VoiceGatewayResponseType::RenderAudioStart,
"Session.RenderAudioStop.1" => VoiceGatewayResponseType::RenderAudioStop,
"Aux.CaptureAudioStart.1" => VoiceGatewayResponseType::CaptureAudioStart,
"Aux.CaptureAudioStop.1" => VoiceGatewayResponseType::CaptureAudioStop,
"Aux.SetMicLevel.1" => VoiceGatewayResponseType::SetMicLevel,
"Aux.SetSpeakerLevel.1" => VoiceGatewayResponseType::SetSpeakerLevel,
"Account.Logout.1" => VoiceGatewayResponseType::AccountLogout,
"Session.Connect.1" => VoiceGatewayResponseType::SessionConnect,
"Session.Terminate.1" => VoiceGatewayResponseType::SessionTerminate,
"Session.SetParticipantVolumeForMe.1" => {
VoiceGatewayResponseType::SetParticipantVolumeForMe
}
"Session.SetParticipantMuteForMe.1" => VoiceGatewayResponseType::SetParticipantMuteForMe,
"Session.Set3DPosition.1" => VoiceGatewayResponseType::Set3DPosition,
_ => return None,
})
}
fn login_state(value: i32) -> VoiceGatewayLoginState {
match value {
1 => VoiceGatewayLoginState::LoggedIn,
4 => VoiceGatewayLoginState::Error,
_ => VoiceGatewayLoginState::LoggedOut,
}
}
fn session_state(value: i32) -> VoiceGatewaySessionState {
match value {
2 => VoiceGatewaySessionState::Answering,
3 => VoiceGatewaySessionState::InProgress,
4 => VoiceGatewaySessionState::Connected,
5 => VoiceGatewaySessionState::Disconnected,
6 => VoiceGatewaySessionState::Hold,
7 => VoiceGatewaySessionState::Refer,
8 => VoiceGatewaySessionState::Ringing,
_ => VoiceGatewaySessionState::Idle,
}
}
fn participant_state(value: i32) -> VoiceGatewayParticipantState {
match value {
2 => VoiceGatewayParticipantState::Pending,
3 => VoiceGatewayParticipantState::Incoming,
4 => VoiceGatewayParticipantState::Answering,
5 => VoiceGatewayParticipantState::InProgress,
6 => VoiceGatewayParticipantState::Ringing,
7 => VoiceGatewayParticipantState::Connected,
8 => VoiceGatewayParticipantState::Disconnecting,
9 => VoiceGatewayParticipantState::Disconnected,
_ => VoiceGatewayParticipantState::Idle,
}
}
fn participant_type(value: i32) -> VoiceGatewayParticipantType {
match value {
1 => VoiceGatewayParticipantType::Moderator,
2 => VoiceGatewayParticipantType::Focus,
_ => VoiceGatewayParticipantType::User,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
#[test]
fn xml_builder_escapes_values_and_rejects_active_xml() {
assert_eq!(
VoiceGateway::make_xml("Name".into(), "<&>\"'".into()).unwrap(),
"<Name>&lt;&amp;&gt;&quot;&apos;</Name>"
);
assert!(validate_fragment("<!ENTITY x 'bad'>").is_err());
assert!(VoiceGateway::make_xml("bad name".into(), "x".into()).is_err());
}
#[test]
fn response_and_event_dispatch_retain_typed_payloads() {
let client = libremetaverse::GridClient::new().unwrap();
let gateway = VoiceGateway::new(client).unwrap();
let (sender, receiver) = mpsc::channel();
let _subscription =
gateway.subscribe_on_session_participant_updated_event(Some(Arc::new(move |event| {
sender.send(event).unwrap();
})));
gateway.inner.handle_line("<Event type=\"ParticipantUpdatedEvent\"><SessionHandle>s</SessionHandle><ParticipantUri>sip:p@example.test</ParticipantUri><IsModeratorMuted>true</IsModeratorMuted><IsSpeaking>1</IsSpeaking><Volume>42</Volume><Energy>0.75</Energy></Event>").unwrap();
let event = receiver.recv().unwrap();
assert_eq!(event.session_handle, "s");
assert!(event.is_muted);
assert!(event.is_speaking);
assert_eq!(event.volume, 42);
assert_eq!(event.energy, 0.75);
}
#[test]
fn protocol_events_drive_the_session_participant_lifecycle() {
let gateway = VoiceGateway::new(libremetaverse::GridClient::new().unwrap()).unwrap();
gateway.inner.handle_line("<Response requestId=\"3\" action=\"Session.Create.1\"><ReturnCode>0</ReturnCode><Results><StatusCode>0</StatusCode><StatusString>OK</StatusString><SessionHandle>session-3</SessionHandle></Results></Response>").unwrap();
let session = lock(&gateway.inner.state)
.sessions
.get("session-3")
.cloned()
.unwrap();
let (sender, receiver) = mpsc::channel();
let added = sender.clone();
let _added = session.subscribe_on_participant_added(Some(Arc::new(move |()| {
added.send("added").unwrap();
})));
let updated = sender.clone();
let _updated = session.subscribe_on_participant_update(Some(Arc::new(move |()| {
updated.send("updated").unwrap();
})));
let _removed = session.subscribe_on_participant_removed(Some(Arc::new(move |()| {
sender.send("removed").unwrap();
})));
gateway.inner.handle_line("<Event type=\"ParticipantAddedEvent\"><SessionHandle>session-3</SessionHandle><ParticipantUri>sip:p@example.test</ParticipantUri></Event>").unwrap();
gateway.inner.handle_line("<Event type=\"ParticipantUpdatedEvent\"><SessionHandle>session-3</SessionHandle><ParticipantUri>sip:p@example.test</ParticipantUri><IsSpeaking>1</IsSpeaking><Volume>42</Volume><Energy>0.75</Energy></Event>").unwrap();
gateway.inner.handle_line("<Event type=\"ParticipantRemovedEvent\"><SessionHandle>session-3</SessionHandle><ParticipantUri>sip:p@example.test</ParticipantUri></Event>").unwrap();
assert_eq!(receiver.recv().unwrap(), "added");
assert_eq!(receiver.recv().unwrap(), "updated");
assert_eq!(receiver.recv().unwrap(), "removed");
gateway.inner.handle_line("<Event type=\"SessionRemovedEvent\"><SessionHandle>session-3</SessionHandle></Event>").unwrap();
assert!(
!lock(&gateway.inner.state)
.sessions
.contains_key("session-3")
);
}
}