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

This commit is contained in:
2026-08-12 14:44:28 +00:00
parent dceb394378
commit c9a1170a27
140 changed files with 82175 additions and 27179 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,
}
}

View File

@@ -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

View File

@@ -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)));
}