Complete first release candidate audit (#107)
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

This commit is contained in:
hugo committed 2026-08-12 14:44:28 +00:00
1 parent dceb394378
commit c9a1170a27
140 files changed
+82175 -27179

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+63 -5
View File
@@ -7,34 +7,92 @@
extern crate self as libremetaverse_voice_webrtc;
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
mod compatibility;
mod generated;
mod native;
pub use compatibility::VoiceLogRecord;
pub use generated::*;
pub use libremetaverse as core;
pub use libremetaverse_structured_data as structured_data;
pub use libremetaverse_types::Error;
pub use native::*;
#[derive(Clone, Debug, PartialEq)]
struct JsonValueInner(serde_json::Value);
/// Project-owned boundary types for external JSON signatures.
pub mod signaling {
pub struct JsonValue;
/// Owned JSON value without exposing the implementation's JSON crate in
/// the public compatibility boundary.
#[derive(Clone, Debug, PartialEq)]
pub struct JsonValue(crate::JsonValueInner);
impl JsonValue {
/// Parses one complete JSON value.
///
/// # Errors
///
/// Returns [`crate::Error::Argument`] when `value` is not valid JSON.
pub fn parse(value: &str) -> Result<Self, crate::Error> {
serde_json::from_str(value)
.map(crate::JsonValueInner)
.map(Self)
.map_err(|_| crate::Error::Argument)
}
/// Serializes the value as compact JSON.
///
/// # Errors
///
/// Returns [`crate::Error::Argument`] if serialization fails.
pub fn to_json(&self) -> Result<String, crate::Error> {
serde_json::to_string(&self.0.0).map_err(|_| crate::Error::Argument)
}
pub(crate) fn into_inner(self) -> serde_json::Value {
self.0.0
}
pub(crate) fn from_inner(value: serde_json::Value) -> Self {
Self(crate::JsonValueInner(value))
}
}
}
/// Project-owned boundary types for external WebRTC transport signatures.
pub mod transport {
pub struct RTCDataChannel;
pub struct RTCPeerConnection;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RTCDataChannel {
pub label: String,
pub ready: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RTCPeerConnection {
pub connected: bool,
}
}
/// Project-owned boundary types for external media signatures.
pub mod media {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AudioSamplingRatesEnum(pub i32);
}
/// Project-owned boundary types for external audio signatures.
pub mod audio {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct NoiseSuppressionLevel(pub i32);
pub struct SoundFlowAudioEndPoint;
pub struct SoundFlowAudioSource;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SoundFlowAudioEndPoint {
pub id: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SoundFlowAudioSource {
pub id: String,
}
}
@@ -453,7 +453,7 @@ struct Playback {
/// Owned native WebRTC voice session.
pub struct WebRtcVoiceSession {
command: mpsc::Sender<Command>,
events: mpsc::Receiver<VoiceEvent>,
events: Option<mpsc::Receiver<VoiceEvent>>,
snapshot: Arc<Mutex<SessionSnapshot>>,
signaling: Arc<dyn VoiceSignaling>,
viewer_session: UUID,
@@ -461,6 +461,8 @@ pub struct WebRtcVoiceSession {
credentials: Option<VoiceSecret>,
task: Option<JoinHandle<()>>,
timeout: Duration,
local_sdp: String,
remote_sdp: String,
#[cfg(feature = "real-audio")]
hardware_audio: Option<RealAudioBridge>,
}
@@ -493,10 +495,11 @@ impl WebRtcVoiceSession {
let (offer, pending) = change.apply().ok_or(WebRtcError::Protocol(
"SDP changes did not produce an offer",
))?;
let local_sdp = offer.to_sdp_string();
let request = ProvisionRequest {
jsep: Jsep {
kind: "offer".into(),
sdp: offer.to_sdp_string(),
sdp: local_sdp.clone(),
},
channel_type: "local".into(),
voice_server_type: "webrtc".into(),
@@ -509,7 +512,8 @@ impl WebRtcVoiceSession {
abandon_provisioned(&signaling, &mut response, config.timeout).await;
return Err(WebRtcError::Protocol("SDP answer exceeds 1 MiB"));
}
let answer = match SdpAnswer::from_sdp_string(&sanitize_remote_sdp(&response.answer_sdp)) {
let remote_sdp = sanitize_remote_sdp(&response.answer_sdp);
let answer = match SdpAnswer::from_sdp_string(&remote_sdp) {
Ok(answer) => answer,
Err(error) => {
abandon_provisioned(&signaling, &mut response, config.timeout).await;
@@ -578,7 +582,7 @@ impl WebRtcVoiceSession {
});
Ok(Self {
command: command_tx,
events: event_rx,
events: Some(event_rx),
snapshot,
signaling,
viewer_session: response.viewer_session,
@@ -586,13 +590,21 @@ impl WebRtcVoiceSession {
credentials: response.credentials,
task: Some(task),
timeout: config.timeout,
local_sdp,
remote_sdp,
#[cfg(feature = "real-audio")]
hardware_audio: Some(hardware_audio),
})
}
pub async fn next_event(&mut self) -> Option<VoiceEvent> {
self.events.recv().await
self.events.as_mut()?.recv().await
}
/// Transfers the event stream to a facade task while retaining the session's
/// command and teardown handles.
pub fn take_events(&mut self) -> Option<mpsc::Receiver<VoiceEvent>> {
self.events.take()
}
#[must_use]
@@ -615,6 +627,49 @@ impl WebRtcVoiceSession {
reply_rx.await.map_err(|_| WebRtcError::Closed)?
}
/// Queues a bounded data-channel message without blocking the caller.
pub fn try_send_data(&self, message: impl Into<String>) -> Result<bool, WebRtcError> {
let message = message.into();
if message.len() > MAX_DATA_BYTES {
return Err(WebRtcError::InvalidInput(
"data-channel message exceeds 64 KiB",
));
}
let (reply_tx, _reply_rx) = oneshot::channel();
match self.command.try_send(Command::Data(message, reply_tx)) {
Ok(()) => Ok(true),
Err(mpsc::error::TrySendError::Full(_)) => Ok(false),
Err(mpsc::error::TrySendError::Closed(_)) => Err(WebRtcError::Closed),
}
}
#[must_use]
pub fn viewer_session(&self) -> UUID {
self.viewer_session
}
#[must_use]
pub fn channel(&self) -> Option<String> {
self.channel.clone()
}
#[must_use]
pub fn credentials(&self) -> Option<String> {
self.credentials
.as_ref()
.map(|credentials| credentials.expose().to_owned())
}
#[must_use]
pub fn local_sdp(&self) -> String {
self.local_sdp.clone()
}
#[must_use]
pub fn remote_sdp(&self) -> String {
self.remote_sdp.clone()
}
pub async fn set_peer_mute(&self, peer: UUID, mute: bool) -> Result<bool, WebRtcError> {
self.send_data(json!({"m": {peer.to_string(): mute}}).to_string())
.await
@@ -0,0 +1,50 @@
//! Behavioral checks for the mutable WebRTC voice data models.
use libremetaverse_types::UUID;
use libremetaverse_voice_webrtc::{
VoiceSessionAvatarPosition, VoiceSessionInt3, VoiceSessionInt4, VoiceSessionPeerAudioState,
};
#[test]
fn peer_audio_state_uses_clr_defaults_and_round_trips_updates() {
let mut state = VoiceSessionPeerAudioState::new().expect("PeerAudioState");
assert_eq!(state.power(), None);
assert_eq!(state.voice_active(), None);
assert_eq!(state.joined_primary(), None);
assert_eq!(state.moderator_muted(), None);
assert!(!state.left());
state.set_power(Some(Some(37)));
state.set_voice_active(Some(Some(true)));
state.set_joined_primary(Some(Some(false)));
state.set_moderator_muted(Some(Some(true)));
state.set_left(true);
assert_eq!(state.power(), Some(Some(37)));
assert_eq!(state.voice_active(), Some(Some(true)));
assert_eq!(state.joined_primary(), Some(Some(false)));
assert_eq!(state.moderator_muted(), Some(Some(true)));
assert!(state.left());
}
#[test]
fn avatar_position_round_trips_typed_coordinates() {
let mut position = VoiceSessionAvatarPosition::new().expect("AvatarPosition");
let agent = UUID::new_with_u_int64(44).expect("UUID");
let xyz = VoiceSessionInt3 { x: 1, y: 2, z: 3 };
let xyzw = VoiceSessionInt4 {
x: 4,
y: 5,
z: 6,
w: 7,
};
assert_eq!(position.agent_id(), UUID::zero());
position.set_agent_id(agent);
position.set_sender_position(Some(Some(xyz)));
position.set_listener_heading(Some(Some(xyzw)));
assert_eq!(position.agent_id(), agent);
assert_eq!(position.sender_position(), Some(Some(xyz)));
assert_eq!(position.listener_heading(), Some(Some(xyzw)));
}