Files
MetaCrate/crates/libremetaverse-voice-vivox/src/voice_manager.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

1097 lines
39 KiB
Rust

//! Legacy `VoiceManager` facade backed by the native gateway transport.
use crate::*;
use base64::Engine as _;
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{CancellationToken, Subscription};
use roxmltree::{Document, Node};
use std::collections::HashMap;
use std::sync::{
Arc, Condvar, Mutex, Weak,
atomic::{AtomicBool, AtomicU64, Ordering},
};
use std::time::Duration;
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 ManagerEvent {
AccountChannelGetList,
AccountLogout,
AuxAudioProperties,
CaptureDevices,
ConnectorCreated,
ConnectorShutdown,
Login,
LoginState,
NewSession,
ParcelVoiceInfo,
ParticipantProperties,
ParticipantState,
ProvisionAccount,
RenderDevices,
SessionConnected,
SessionCreated,
SessionState,
SessionTerminated,
}
#[derive(Clone, Copy)]
enum CapabilityKind {
Provision,
Parcel,
}
type Erased = Arc<dyn Fn(&dyn std::any::Any) + Send + Sync>;
#[derive(Default)]
struct ManagerEvents {
next_id: AtomicU64,
handlers: Arc<Mutex<HashMap<ManagerEvent, HashMap<u64, Erased>>>>,
}
impl ManagerEvents {
fn subscribe<T>(&self, key: ManagerEvent, handler: Option<T>) -> Subscription
where
T: Clone + Send + Sync + 'static,
{
let Some(handler) = handler else {
return Subscription::detached();
};
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let erased: Erased = Arc::new(move |value| {
if let Some(invoke) = value.downcast_ref::<Box<dyn Fn(&T) + Send + Sync>>() {
invoke(&handler);
}
});
lock(&self.handlers)
.entry(key)
.or_default()
.insert(id, erased);
let handlers = Arc::downgrade(&self.handlers);
Subscription::new(move || remove(&handlers, key, id))
}
fn emit<T, F>(&self, key: ManagerEvent, invoke: F)
where
T: Clone + Send + Sync + 'static,
F: Fn(&T) + Send + Sync + 'static,
{
let handlers: Vec<_> = lock(&self.handlers)
.get(&key)
.into_iter()
.flat_map(|entries| entries.values().cloned())
.collect();
let invoke: Box<dyn Fn(&T) + Send + Sync> = Box::new(invoke);
for handler in handlers {
handler(&invoke);
}
}
}
fn remove(
handlers: &Weak<Mutex<HashMap<ManagerEvent, HashMap<u64, Erased>>>>,
key: ManagerEvent,
id: u64,
) {
if let Some(handlers) = handlers.upgrade() {
let mut handlers = lock(&handlers);
if let Some(event) = handlers.get_mut(&key) {
event.remove(&id);
}
}
}
#[derive(Default)]
struct ManagerState {
capture_devices: Vec<String>,
render_devices: Vec<String>,
channel_map: HashMap<String, String>,
connector_handle: String,
account_handle: String,
status_code: i32,
tuning_sound_file: String,
}
#[derive(Default)]
struct ResponseSignal {
completed: Mutex<bool>,
wake: Condvar,
}
impl ResponseSignal {
fn notify(&self) {
*lock(&self.completed) = true;
self.wake.notify_all();
}
fn wait(&self, timeout: Duration) -> bool {
let completed = lock(&self.completed);
let (completed, _) = self
.wake
.wait_timeout_while(completed, timeout, |value| !*value)
.unwrap_or_else(std::sync::PoisonError::into_inner);
*completed
}
}
struct VoiceManagerInner {
client: libremetaverse::GridClient,
gateway: VoiceGateway,
state: Mutex<ManagerState>,
events: ManagerEvents,
signals: Mutex<HashMap<i32, Arc<ResponseSignal>>>,
subscriptions: Mutex<Vec<Subscription>>,
disposed: AtomicBool,
}
pub struct VoiceManager {
pub blocking_timeout: i32,
pub voice_server: String,
inner: Arc<VoiceManagerInner>,
}
macro_rules! subscription {
($name:ident, $key:ident, $type:ty) => {
#[must_use]
pub fn $name(&self, handler: Option<$type>) -> Subscription {
self.inner.events.subscribe(ManagerEvent::$key, handler)
}
};
}
impl VoiceManager {
pub const DAEMON_ARGS: &'static str = " -p tcp -h -c -ll ";
pub const DAEMON_LOG_LEVEL: i32 = 1;
pub const DAEMON_PORT: i32 = 44_124;
pub const REQUEST_TERMINATOR: &'static str = "\n\n\n";
pub const VOICE_DEBUG_SERVER: &'static str = "bhd.vivox.com";
pub const VOICE_MAJOR_VERSION: i32 = 1;
pub const VOICE_RELEASE_SERVER: &'static str = "bhr.vivox.com";
pub fn new(client: libremetaverse::GridClient) -> Result<Self, Error> {
let gateway = VoiceGateway::new(client.clone())?;
let inner = Arc::new(VoiceManagerInner {
client,
gateway,
state: Mutex::new(ManagerState::default()),
events: ManagerEvents::default(),
signals: Mutex::new(HashMap::new()),
subscriptions: Mutex::new(Vec::new()),
disposed: AtomicBool::new(false),
});
let weak = Arc::downgrade(&inner);
let subscription = inner.gateway.subscribe_control_line(Arc::new(move |line| {
if let Some(inner) = weak.upgrade() {
let facade = VoiceManager {
blocking_timeout: 30_000,
voice_server: VoiceManager::VOICE_RELEASE_SERVER.into(),
inner,
};
let _ = facade.process_line(&line);
}
}));
lock(&inner.subscriptions).push(subscription);
Ok(Self {
blocking_timeout: 30_000,
voice_server: Self::VOICE_RELEASE_SERVER.into(),
inner,
})
}
subscription!(
subscribe_on_account_channel_get_list,
AccountChannelGetList,
VoiceManagerBasicActionCallback
);
subscription!(
subscribe_on_account_logout,
AccountLogout,
VoiceManagerBasicActionCallback
);
subscription!(
subscribe_on_aux_audio_properties,
AuxAudioProperties,
VoiceManagerAuxAudioPropertiesCallback
);
subscription!(
subscribe_on_capture_devices,
CaptureDevices,
VoiceManagerDevicesCallback
);
subscription!(
subscribe_on_connector_created,
ConnectorCreated,
VoiceManagerConnectorCreatedCallback
);
subscription!(
subscribe_on_connector_initiate_shutdown,
ConnectorShutdown,
VoiceManagerBasicActionCallback
);
subscription!(subscribe_on_login, Login, VoiceManagerLoginCallback);
subscription!(
subscribe_on_login_state_change,
LoginState,
VoiceManagerLoginStateChangeCallback
);
subscription!(
subscribe_on_new_session,
NewSession,
VoiceManagerNewSessionCallback
);
subscription!(
subscribe_on_parcel_voice_info,
ParcelVoiceInfo,
VoiceManagerParcelVoiceInfoCallback
);
subscription!(
subscribe_on_participant_properties,
ParticipantProperties,
VoiceManagerParticipantPropertiesCallback
);
subscription!(
subscribe_on_participant_state_change,
ParticipantState,
VoiceManagerParticipantStateChangeCallback
);
subscription!(
subscribe_on_provision_account,
ProvisionAccount,
VoiceManagerProvisionAccountCallback
);
subscription!(
subscribe_on_render_devices,
RenderDevices,
VoiceManagerDevicesCallback
);
subscription!(
subscribe_on_session_connected,
SessionConnected,
VoiceManagerBasicActionCallback
);
subscription!(
subscribe_on_session_created,
SessionCreated,
VoiceManagerSessionCreatedCallback
);
subscription!(
subscribe_on_session_state_change,
SessionState,
VoiceManagerSessionStateChangeCallback
);
subscription!(
subscribe_on_session_terminated,
SessionTerminated,
VoiceManagerBasicActionCallback
);
pub fn connect_to_daemon_with_method(&self) -> Result<bool, Error> {
self.connect_to_daemon_with_string_int32("127.0.0.1".into(), Self::DAEMON_PORT)
}
pub fn connect_to_daemon_with_string_int32(
&self,
address: String,
port: i32,
) -> Result<bool, Error> {
self.ensure_active()?;
self.inner.gateway.connect_to_daemon(address, port)
}
pub fn is_daemon_running(&self) -> Result<bool, Error> {
self.ensure_active()?;
Ok(self.inner.gateway.daemon_is_running())
}
/// Native releases do not bundle or spawn the proprietary daemon.
pub fn start_daemon(&self) -> Result<bool, Error> {
self.ensure_active()?;
Ok(false)
}
pub fn stop_daemon(&self) -> Result<(), Error> {
self.inner.gateway.stop_daemon()
}
pub fn get_channel_map(&self) -> Result<HashMap<String, String>, Error> {
self.ensure_active()?;
Ok(lock(&self.inner.state).channel_map.clone())
}
pub fn current_capture_devices(&self) -> Result<Vec<String>, Error> {
self.ensure_active()?;
Ok(lock(&self.inner.state).capture_devices.clone())
}
pub fn current_render_devices(&self) -> Result<Vec<String>, Error> {
self.ensure_active()?;
Ok(lock(&self.inner.state).render_devices.clone())
}
pub fn voice_account_from_uuid(&self, id: UUID) -> Result<String, Error> {
Ok(format!(
"x{}",
base64::engine::general_purpose::STANDARD
.encode(id.get_bytes()?)
.replace('+', "-")
.replace('/', "_")
))
}
pub fn uuid_from_voice_account(&self, account_name: String) -> Result<UUID, Error> {
VoiceParticipant::id_from_name(account_name)
}
pub fn sipuri_from_voice_account(&self, account: String) -> Result<String, Error> {
if account.len() > 16 * 1024 || account.chars().any(char::is_control) {
return Err(Error::Argument);
}
Ok(format!("sip:{account}@{}", self.voice_server))
}
pub fn request_capture_devices(&self) -> Result<i32, Error> {
self.inner.gateway.aux_get_capture_devices()
}
pub fn request_render_devices(&self) -> Result<i32, Error> {
self.inner.gateway.aux_get_render_devices()
}
pub fn request_create_connector_with_method(&self) -> Result<i32, Error> {
self.request_create_connector_with_string(self.voice_server.clone())
}
pub fn request_create_connector_with_string(&self, voice_server: String) -> Result<i32, Error> {
let mut logging = VoiceGatewayVoiceLoggingSettings::new()?;
logging.folder = ".".into();
logging.file_name_prefix = "vivox-gateway".into();
let account_server = format!("https://www.{voice_server}/api2/");
self.inner
.gateway
.connector_create("V2 SDK".into(), account_server, 0, u16::MAX, logging)
}
pub fn request_login(
&self,
account_name: String,
password: String,
conn_handle: String,
) -> Result<i32, Error> {
self.inner.gateway.account_login(
conn_handle,
account_name,
password,
"VerifyAnswer".into(),
String::new(),
10,
false,
)
}
pub fn request_set_render_device(&self, device_name: String) -> Result<i32, Error> {
self.inner.gateway.aux_set_render_device(device_name)
}
pub fn request_start_tuning_mode(&self, duration: i32) -> Result<i32, Error> {
self.inner.gateway.aux_capture_audio_start(duration)
}
pub fn request_stop_tuning_mode(&self) -> Result<i32, Error> {
self.inner.gateway.aux_capture_audio_stop()
}
pub fn request_set_speaker_volume(&self, volume: i32) -> Result<i32, Error> {
if !(0..=100).contains(&volume) {
return Err(Error::Argument);
}
self.inner.gateway.aux_set_speaker_level(volume)
}
pub fn request_set_capture_volume(&self, volume: i32) -> Result<i32, Error> {
if !(0..=100).contains(&volume) {
return Err(Error::Argument);
}
self.inner.gateway.aux_set_mic_level(volume)
}
pub fn request_render_audio_start(&self, file_name: String, loop_: bool) -> Result<i32, Error> {
lock(&self.inner.state).tuning_sound_file = file_name.clone();
self.inner.gateway.request_with_string_string(
"Aux.RenderAudioStart.1".into(),
Some(format!(
"{}{}",
VoiceGateway::make_xml("SoundFilePath".into(), file_name)?,
VoiceGateway::make_xml("Loop".into(), if loop_ { "1" } else { "0" }.into())?
)),
)
}
pub fn request_render_audio_stop(&self) -> Result<i32, Error> {
let file = lock(&self.inner.state).tuning_sound_file.clone();
self.inner.gateway.request_with_string_string(
"Aux.RenderAudioStop.1".into(),
Some(VoiceGateway::make_xml("SoundFilePath".into(), file)?),
)
}
pub fn request_provision_account(&self) -> Result<bool, Error> {
self.request_capability("ProvisionVoiceAccountRequest", CapabilityKind::Provision)
}
pub fn request_parcel_voice_info(&self) -> Result<bool, Error> {
self.request_capability("ParcelVoiceInfoRequest", CapabilityKind::Parcel)
}
pub fn capture_devices(&self) -> Result<Vec<String>, Error> {
self.blocking_devices(true)
}
pub fn render_devices(&self) -> Result<Vec<String>, Error> {
self.blocking_devices(false)
}
pub fn create_connector(&self, status: &mut i32) -> Result<String, Error> {
let request = self.request_create_connector_with_method()?;
if request < 0 {
*status = 0;
return Ok(String::new());
}
let signal = self.signal(request);
if !signal.wait(self.timeout()) {
*status = 0;
return Ok(String::new());
}
let state = lock(&self.inner.state);
*status = state.status_code;
Ok(if *status == 0 {
state.connector_handle.clone()
} else {
String::new()
})
}
pub fn login(
&self,
account_name: String,
password: String,
connector_handle: String,
status: &mut i32,
) -> Result<String, Error> {
let request = self.request_login(account_name, password, connector_handle)?;
if request < 0 {
*status = 0;
return Ok(String::new());
}
let signal = self.signal(request);
if !signal.wait(self.timeout()) {
*status = 0;
return Ok(String::new());
}
let state = lock(&self.inner.state);
*status = state.status_code;
Ok(if *status == 0 {
state.account_handle.clone()
} else {
String::new()
})
}
pub fn dispose(&self) -> Result<(), Error> {
if self.inner.disposed.swap(true, Ordering::AcqRel) {
return Ok(());
}
self.inner.gateway.dispose()?;
lock(&self.inner.state).channel_map.clear();
lock(&self.inner.state).capture_devices.clear();
lock(&self.inner.state).render_devices.clear();
lock(&self.inner.signals).clear();
lock(&self.inner.subscriptions).clear();
Ok(())
}
fn ensure_active(&self) -> Result<(), Error> {
if self.inner.disposed.load(Ordering::Acquire) {
Err(Error::InvalidOperation)
} else {
Ok(())
}
}
fn request_capability(&self, name: &str, kind: CapabilityKind) -> Result<bool, Error> {
self.ensure_active()?;
let network = self.inner.client.network();
if !network.connected() {
return Ok(false);
}
let Some(simulator) = network.current_sim() else {
return Ok(false);
};
let Some(uri) = simulator.native_capability_uri(name)? else {
return Ok(false);
};
let http = self.inner.client.http_caps_client();
let weak = Arc::downgrade(&self.inner);
std::thread::Builder::new()
.name(format!("vivox-{}-cap", name.to_ascii_lowercase()))
.spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
let response = runtime.block_on(
http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
uri,
OSDFormat::Xml,
OSD::Map(HashMap::new()),
CancellationToken::default(),
None,
),
);
let Some(inner) = weak.upgrade() else {
return;
};
let Ok((status, bytes)) = response else {
return;
};
if !status.is_success_status_code() {
return;
}
let Ok(OSD::Map(map)) = OSDParser::deserialize_with_bytes(bytes) else {
return;
};
match kind {
CapabilityKind::Provision => {
let username = map
.get("username")
.and_then(|value| value.as_string().ok())
.unwrap_or_default();
let password = map
.get("password")
.and_then(|value| value.as_string().ok())
.unwrap_or_default();
inner
.events
.emit::<VoiceManagerProvisionAccountCallback, _>(
ManagerEvent::ProvisionAccount,
move |callback| {
let _ = callback.invoke(username.clone(), password.clone());
},
);
}
CapabilityKind::Parcel => {
let region = map
.get("region_name")
.and_then(|value| value.as_string().ok())
.unwrap_or_default();
let local_id = map
.get("parcel_local_id")
.and_then(|value| value.as_integer().ok())
.unwrap_or_default();
let channel = match map.get("voice_credentials") {
Some(OSD::Map(credentials)) => credentials
.get("channel_uri")
.and_then(|value| value.as_string().ok())
.filter(|value| !value.is_empty()),
_ => None,
};
inner.events.emit::<VoiceManagerParcelVoiceInfoCallback, _>(
ManagerEvent::ParcelVoiceInfo,
move |callback| {
let _ = callback.invoke(region.clone(), local_id, channel.clone());
},
);
}
}
})
.map_err(|_| Error::InvalidOperation)?;
Ok(true)
}
fn timeout(&self) -> Duration {
Duration::from_millis(u64::try_from(self.blocking_timeout.max(0)).unwrap_or_default())
}
fn signal(&self, cookie: i32) -> Arc<ResponseSignal> {
lock(&self.inner.signals).entry(cookie).or_default().clone()
}
fn blocking_devices(&self, capture: bool) -> Result<Vec<String>, Error> {
let request = if capture {
self.request_capture_devices()?
} else {
self.request_render_devices()?
};
if request < 0 {
return Ok(Vec::new());
}
let signal = self.signal(request);
if !signal.wait(self.timeout()) {
return Ok(Vec::new());
}
if capture {
self.current_capture_devices()
} else {
self.current_render_devices()
}
}
#[cfg(test)]
fn process_control_line(&self, line: &str) -> Result<(), Error> {
self.inner.gateway.process_control_line(line)
}
fn process_line(&self, line: &str) -> Result<(), Error> {
if line.len() > 1024 * 1024 || line.contains("<!DOCTYPE") || line.contains("<!ENTITY") {
return Err(Error::Argument);
}
let document = Document::parse(line).map_err(|_| Error::Argument)?;
let root = document.root_element();
let cookie = root
.descendants()
.find(|node| node.has_tag_name("Request"))
.and_then(|node| node.attribute("requestId"))
.or_else(|| root.attribute("requestId"))
.and_then(|value| value.parse().ok())
.unwrap_or(-1);
let status = value_i32(root, "StatusCode");
let status_string = value(root, "StatusString");
if root.has_tag_name("Response") {
let action = root.attribute("action").unwrap_or_default();
let mut state = lock(&self.inner.state);
state.status_code = status;
match action {
"Connector.Create.1" => {
state.connector_handle = value(root, "ConnectorHandle");
let handle = state.connector_handle.clone();
drop(state);
self.inner
.events
.emit::<VoiceManagerConnectorCreatedCallback, _>(
ManagerEvent::ConnectorCreated,
move |callback| {
let _ = callback.invoke(
cookie,
status,
status_string.clone(),
handle.clone(),
);
},
);
}
"Account.Login.1" => {
state.account_handle = value(root, "AccountHandle");
let handle = state.account_handle.clone();
drop(state);
self.inner.events.emit::<VoiceManagerLoginCallback, _>(
ManagerEvent::Login,
move |callback| {
let _ = callback.invoke(
cookie,
status,
status_string.clone(),
handle.clone(),
);
},
);
}
"Aux.GetCaptureDevices.1" | "Aux.GetRenderDevices.1" => {
let capture = action.contains("Capture");
let devices = values(
root,
if capture {
"CaptureDevice"
} else {
"RenderDevice"
},
);
let current = nested(
root,
if capture {
"CurrentCaptureDevice"
} else {
"CurrentRenderDevice"
},
);
if capture {
state.capture_devices = devices;
} else {
state.render_devices = devices;
}
drop(state);
let key = if capture {
ManagerEvent::CaptureDevices
} else {
ManagerEvent::RenderDevices
};
self.inner.events.emit::<VoiceManagerDevicesCallback, _>(
key,
move |callback| {
let _ = callback.invoke(
cookie,
status,
status_string.clone(),
current.clone(),
);
},
);
}
"Session.Create.1" => {
let handle = value(root, "SessionHandle");
drop(state);
self.inner
.events
.emit::<VoiceManagerSessionCreatedCallback, _>(
ManagerEvent::SessionCreated,
move |callback| {
let _ = callback.invoke(
cookie,
status,
status_string.clone(),
handle.clone(),
);
},
);
}
"Account.ChannelGetList.1" => {
state.channel_map = channel_map(root);
drop(state);
self.inner
.events
.emit::<VoiceManagerBasicActionCallback, _>(
ManagerEvent::AccountChannelGetList,
move |callback| {
let _ = callback.invoke(cookie, status, status_string.clone());
},
);
}
_ => {
drop(state);
if let Some(key) = basic_action(action) {
self.inner
.events
.emit::<VoiceManagerBasicActionCallback, _>(key, move |callback| {
let _ = callback.invoke(cookie, status, status_string.clone());
});
}
}
}
lock(&self.inner.signals)
.entry(cookie)
.or_default()
.clone()
.notify();
} else if root.has_tag_name("Event") {
self.process_event(root, cookie, status, status_string);
}
Ok(())
}
fn process_event(&self, root: Node<'_, '_>, cookie: i32, status: i32, status_string: String) {
match root.attribute("type").unwrap_or_default() {
"LoginStateChangeEvent" | "AccountLoginStateChangeEvent" => {
let account = value(root, "AccountHandle");
let state = value_i32(root, "State");
self.inner
.events
.emit::<VoiceManagerLoginStateChangeCallback, _>(
ManagerEvent::LoginState,
move |callback| {
let _ = callback.invoke(
cookie,
account.clone(),
status,
status_string.clone(),
state,
);
},
);
}
"SessionNewEvent" => {
let account = value(root, "AccountHandle");
let session = value(root, "SessionHandle");
let state = value_i32(root, "State");
let name = value_any(root, &["Name", "ChannelName"]);
let uri = value_any(root, &["URI", "Uri"]);
self.inner.events.emit::<VoiceManagerNewSessionCallback, _>(
ManagerEvent::NewSession,
move |callback| {
let _ = callback.invoke(
cookie,
account.clone(),
session.clone(),
state,
name.clone(),
uri.clone(),
);
},
);
}
"SessionStateChangeEvent" => {
let uri = value_any(root, &["URI", "Uri"]);
let session = value(root, "SessionHandle");
let state = value_i32(root, "State");
let is_channel = value_bool(root, "IsChannel");
let name = value_any(root, &["Name", "ChannelName"]);
self.inner
.events
.emit::<VoiceManagerSessionStateChangeCallback, _>(
ManagerEvent::SessionState,
move |callback| {
let _ = callback.invoke(
cookie,
uri.clone(),
status,
status_string.clone(),
session.clone(),
state,
is_channel,
name.clone(),
);
},
);
}
"ParticipantStateChangeEvent" => {
let uri = value_any(root, &["ParticipantUri", "URI", "Uri"]);
let state = value_i32(root, "State");
let name = value(root, "AccountName");
let display_name = value(root, "DisplayName");
let participant_type = value_i32(root, "ParticipantType");
self.inner
.events
.emit::<VoiceManagerParticipantStateChangeCallback, _>(
ManagerEvent::ParticipantState,
move |callback| {
let _ = callback.invoke(
cookie,
uri.clone(),
status,
status_string.clone(),
state,
name.clone(),
display_name.clone(),
participant_type,
);
},
);
}
"ParticipantPropertiesEvent" | "ParticipantUpdatedEvent" => {
let uri = value_any(root, &["ParticipantUri", "URI", "Uri"]);
let locally_muted = value_bool(root, "IsLocallyMuted");
let moderator_muted = value_bool(root, "IsModeratorMuted");
let speaking = value_bool(root, "IsSpeaking");
let volume = value_i32(root, "Volume");
let energy = value_f32(root, "Energy");
self.inner
.events
.emit::<VoiceManagerParticipantPropertiesCallback, _>(
ManagerEvent::ParticipantProperties,
move |callback| {
let _ = callback.invoke(
cookie,
uri.clone(),
status,
status_string.clone(),
locally_muted,
moderator_muted,
speaking,
volume,
energy,
);
},
);
}
"AuxAudioPropertiesEvent" => {
let energy = value_f32(root, "MicEnergy");
self.inner
.events
.emit::<VoiceManagerAuxAudioPropertiesCallback, _>(
ManagerEvent::AuxAudioProperties,
move |callback| {
let _ = callback.invoke(cookie, energy);
},
);
}
_ => {}
}
}
}
fn basic_action(action: &str) -> Option<ManagerEvent> {
Some(match action {
"Session.Connect.1" => ManagerEvent::SessionConnected,
"Session.Terminate.1" => ManagerEvent::SessionTerminated,
"Account.Logout.1" => ManagerEvent::AccountLogout,
"Connector.InitiateShutdown.1" => ManagerEvent::ConnectorShutdown,
_ => return None,
})
}
fn value_any(root: Node<'_, '_>, names: &[&str]) -> String {
names
.iter()
.map(|name| value(root, name))
.find(|value| !value.is_empty())
.unwrap_or_default()
}
fn value_bool(root: Node<'_, '_>, name: &str) -> bool {
matches!(
value(root, name).to_ascii_lowercase().as_str(),
"1" | "true"
)
}
fn channel_map(root: Node<'_, '_>) -> HashMap<String, String> {
root.descendants()
.filter(|node| node.has_tag_name("Channel"))
.filter_map(|node| {
let name = value(node, "Name");
let uri = value_any(node, &["URI", "Uri"]);
(!name.is_empty() && !uri.is_empty()).then_some((name, uri))
})
.collect()
}
fn value(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 value_i32(root: Node<'_, '_>, name: &str) -> i32 {
value(root, name).parse().unwrap_or_default()
}
fn value_f32(root: Node<'_, '_>, name: &str) -> f32 {
value(root, name).parse().unwrap_or_default()
}
fn nested(root: Node<'_, '_>, name: &str) -> String {
root.descendants()
.find(|node| node.has_tag_name(name))
.map(|node| value(node, "Device"))
.unwrap_or_default()
}
fn values(root: Node<'_, '_>, name: &str) -> Vec<String> {
root.descendants()
.filter(|node| node.has_tag_name(name))
.map(|node| value(node, "Device"))
.filter(|value| !value.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
#[test]
fn account_and_sip_conversions_round_trip() {
let client = libremetaverse::GridClient::new().unwrap();
let manager = VoiceManager::new(client).unwrap();
let id = UUID::new_with_string("1673cfd3-8229-4445-8d92-ec3570e5e587".into()).unwrap();
let account = manager.voice_account_from_uuid(id).unwrap();
assert_eq!(
manager.uuid_from_voice_account(account.clone()).unwrap(),
id
);
assert_eq!(
manager
.sipuri_from_voice_account(account)
.unwrap()
.split('@')
.count(),
2
);
}
#[test]
fn response_parser_updates_device_snapshot_and_invokes_callback() {
let client = libremetaverse::GridClient::new().unwrap();
let manager = VoiceManager::new(client).unwrap();
let observed = Arc::new(Mutex::new(String::new()));
let output = Arc::clone(&observed);
let _subscription = manager.subscribe_on_capture_devices(Some(
VoiceManagerDevicesCallback::from_handler(move |_, _, _, current| {
*lock(&output) = current;
Ok(())
}),
));
manager.process_control_line("<Response requestId=\"7\" action=\"Aux.GetCaptureDevices.1\"><ReturnCode>0</ReturnCode><Results><StatusCode>0</StatusCode><StatusString>OK</StatusString><CaptureDevices><CaptureDevice><Device>USB</Device></CaptureDevice></CaptureDevices><CurrentCaptureDevice><Device>USB</Device></CurrentCaptureDevice></Results></Response>").unwrap();
assert_eq!(manager.current_capture_devices().unwrap(), ["USB"]);
assert_eq!(*lock(&observed), "USB");
assert!(manager.signal(7).wait(Duration::ZERO));
}
#[test]
fn session_and_participant_events_preserve_protocol_payloads() {
let manager = VoiceManager::new(libremetaverse::GridClient::new().unwrap()).unwrap();
let (session_sender, session_receiver) = mpsc::channel();
let _session = manager.subscribe_on_session_state_change(Some(
VoiceManagerSessionStateChangeCallback::from_handler(
move |_, uri, status, _, handle, state, channel, name| {
session_sender
.send((uri, status, handle, state, channel, name))
.unwrap();
Ok(())
},
),
));
let (participant_sender, participant_receiver) = mpsc::channel();
let _participant = manager.subscribe_on_participant_properties(Some(
VoiceManagerParticipantPropertiesCallback::from_handler(
move |_, uri, _, _, local, moderator, speaking, volume, energy| {
participant_sender
.send((uri, local, moderator, speaking, volume, energy))
.unwrap();
Ok(())
},
),
));
manager.process_control_line("<Event type=\"SessionStateChangeEvent\"><StatusCode>0</StatusCode><StatusString>OK</StatusString><SessionHandle>session-1</SessionHandle><URI>sip:channel@example.test</URI><State>4</State><IsChannel>true</IsChannel><ChannelName>Town square</ChannelName></Event>").unwrap();
manager.process_control_line("<Event type=\"ParticipantPropertiesEvent\"><ParticipantUri>sip:avatar@example.test</ParticipantUri><IsLocallyMuted>0</IsLocallyMuted><IsModeratorMuted>1</IsModeratorMuted><IsSpeaking>true</IsSpeaking><Volume>61</Volume><Energy>0.5</Energy></Event>").unwrap();
assert_eq!(
session_receiver.recv().unwrap(),
(
"sip:channel@example.test".into(),
0,
"session-1".into(),
4,
true,
"Town square".into()
)
);
assert_eq!(
participant_receiver.recv().unwrap(),
("sip:avatar@example.test".into(), false, true, true, 61, 0.5)
);
}
#[test]
fn channel_list_response_updates_name_to_uri_map() {
let manager = VoiceManager::new(libremetaverse::GridClient::new().unwrap()).unwrap();
manager.process_control_line("<Response requestId=\"9\" action=\"Account.ChannelGetList.1\"><Results><StatusCode>0</StatusCode><StatusString>OK</StatusString><Channels><Channel><Name>Town square</Name><URI>sip:town@example.test</URI></Channel></Channels></Results></Response>").unwrap();
assert_eq!(
manager
.get_channel_map()
.unwrap()
.get("Town square")
.map(String::as_str),
Some("sip:town@example.test")
);
}
}