Crates.io | rvoip-client-core |
lib.rs | rvoip-client-core |
version | 0.1.26 |
created_at | 2025-07-03 07:53:48.370221+00 |
updated_at | 2025-08-15 17:51:08.276464+00 |
description | High-level VoIP client library for the rvoip stack |
homepage | https://github.com/eisenzopf/rvoip |
repository | https://github.com/eisenzopf/rvoip |
max_upload_size | |
id | 1735986 |
size | 1,047,995 |
The client-core
library provides high-level SIP client capabilities for building VoIP applications in Rust. It serves as the primary interface for developers creating SIP user agents, providing comprehensive call management, media control, and event handling while abstracting away the complexities of SIP protocol details.
session-core
dialog-core
and transaction-core
media-core
rtp-core
call-engine
The Client Core sits at the application interface layer, providing high-level client functionality while delegating coordination and protocol details to specialized components:
┌─────────────────────────────────────────┐
│ VoIP Application │
├─────────────────────────────────────────┤
│ rvoip-client-core ⬅️ YOU ARE HERE
├─────────────────────────────────────────┤
│ rvoip-call-engine │
├─────────────────────────────────────────┤
│ rvoip-session-core │
├─────────────────────────────────────────┤
│ rvoip-dialog-core │ rvoip-media-core │
├─────────────────────────────────────────┤
│ rvoip-transaction │ rvoip-rtp-core │
│ -core │ │
├─────────────────────────────────────────┤
│ rvoip-sip-core │
├─────────────────────────────────────────┤
│ Network Layer │
└─────────────────────────────────────────┘
Clean separation of concerns across the client interface:
┌─────────────────┐ Client Events ┌─────────────────┐
│ │ ──────────────────────► │ │
│ VoIP App │ │ client-core │
│ (UI/Business) │ ◄──────────────────────── │ (Client API) │
│ │ Call Control API │ │
└─────────────────┘ └─────────────────┘
│
Session Management │ Event Handling
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ session-core │ │ call-engine │
│ (Coordination) │ │ (Business Logic)│
└─────────────────┘ └─────────────────┘
ClientBuilder
patternmake_call()
with automatic session coordinationanswer_call()
and reject_call()
for incoming callshold_call()
, resume_call()
, and terminate_call()
operationstransfer_call()
for blind call transferssend_dtmf()
for DTMF tone transmissionset_microphone_mute()
and set_speaker_mute()
software audio controlsget_media_statistics()
for real-time quality metricsget_call_quality()
for comprehensive call quality reportingsubscribe_to_audio_frames()
- Receive decoded audio frames for playbacksend_audio_frame()
- Send audio frames for encoding and transmissionset_audio_stream_config()
- Configure sample rate, codec, and processingstart_audio_stream()
/ stop_audio_stream()
- Control streaming pipelineClientEvent
enum with all client lifecycle eventsClientBuilder
pattern for easy configurationmanager.rs
- Core lifecycle and coordination (164 lines)calls.rs
- Call operations and state management (246 lines)media.rs
- Media functionality and SDP handling (829 lines)controls.rs
- Advanced call controls and transfers (401 lines)┌─────────────────────────────────────────────────────────────┐
│ Client Application │
├─────────────────────────────────────────────────────────────┤
│ rvoip-client-core │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ manager │ calls │ media │ controls │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ types │ events │ │ │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ rvoip-session-core │
├─────────────────────────────────────────────────────────────┤
│ dialog-core|transaction-core│media-core│rtp-core│sip-core │
└─────────────────────────────────────────────────────────────┘
manager.rs
: Core lifecycle and coordination (164 lines)calls.rs
: Call operations and state management (246 lines)media.rs
: Media functionality and SDP handling (829 lines)controls.rs
: Advanced call controls and transfers (401 lines)events.rs
: Event handling and broadcasting (277 lines)types.rs
: Type definitions and data structures (158 lines)Refactored from a 1980-line monolith to clean, maintainable modules (91.7% size reduction!)
Add to your Cargo.toml
:
[dependencies]
rvoip-client-core = "0.1.0"
tokio = { version = "1.0", features = ["full"] }
use rvoip_client_core::{ClientBuilder, ClientEvent};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new().local_address("127.0.0.1:5060".parse()?).build().await?;
client.start().await?;
let call_id = client.make_call("sip:bob@example.com").await?;
println!("🚀 SIP call initiated to bob@example.com");
tokio::signal::ctrl_c().await?;
Ok(())
}
use rvoip_client_core::{ClientBuilder, ClientEvent, CallState};
use std::sync::Arc;
use tokio::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Production-grade client setup
let client = Arc::new(
ClientBuilder::new()
.local_address("127.0.0.1:5060".parse()?)
.user_agent("MyCompany Softphone v1.0")
.with_media(|m| m
.codecs(vec!["opus", "G722", "PCMU", "PCMA"])
.echo_cancellation(true)
.noise_suppression(true)
.auto_gain_control(true)
.dtmf_enabled(true)
.max_bandwidth_kbps(256)
.preferred_ptime(20)
)
.build()
.await?
);
// Start the client
client.start().await?;
// Event handling for UI integration
let client_clone = client.clone();
tokio::spawn(async move {
let mut events = client_clone.subscribe_to_events().await;
while let Ok(event) = events.recv().await {
match event {
ClientEvent::IncomingCall { call_id, from, to, .. } => {
println!("📞 Incoming call from {} to {}", from, to);
// Show UI notification and auto-answer after 3 seconds
let client_inner = client_clone.clone();
let call_id_inner = call_id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(3)).await;
if let Err(e) = client_inner.answer_call(&call_id_inner).await {
eprintln!("Failed to answer call: {}", e);
}
});
}
ClientEvent::CallStateChanged { call_id, new_state, .. } => {
match new_state {
CallState::Connected => {
println!("✅ Call {} connected - starting quality monitoring", call_id);
start_quality_monitoring(client_clone.clone(), call_id).await;
}
CallState::Terminated => {
println!("📴 Call {} terminated", call_id);
}
_ => println!("📱 Call {} state: {:?}", call_id, new_state),
}
}
ClientEvent::MediaQualityChanged { call_id, mos_score, .. } => {
let quality = match mos_score {
x if x >= 4.0 => "Excellent",
x if x >= 3.5 => "Good",
x if x >= 3.0 => "Fair",
x if x >= 2.5 => "Poor",
_ => "Bad"
};
println!("📊 Call {} quality: {:.1} MOS ({})", call_id, mos_score, quality);
}
ClientEvent::ErrorOccurred { error, .. } => {
eprintln!("❌ Client error: {}", error);
}
_ => {}
}
}
});
// Interactive CLI for demonstration
println!("🎙️ Softphone ready! Commands:");
println!(" call <sip_uri> - Make a call");
println!(" hangup <call_id> - End a call");
println!(" mute <call_id> - Mute microphone");
println!(" unmute <call_id> - Unmute microphone");
println!(" quit - Exit");
// Simple CLI loop (in production, integrate with your UI framework)
let stdin = tokio::io::stdin();
let mut buffer = String::new();
loop {
buffer.clear();
if stdin.read_line(&mut buffer).await? == 0 {
break;
}
let parts: Vec<&str> = buffer.trim().split_whitespace().collect();
match parts.as_slice() {
["call", uri] => {
match client.make_call(uri).await {
Ok(call_id) => println!("📞 Calling {} (ID: {})", uri, call_id),
Err(e) => eprintln!("❌ Call failed: {}", e),
}
}
["hangup", call_id] => {
match client.terminate_call(call_id).await {
Ok(_) => println!("📴 Hanging up call {}", call_id),
Err(e) => eprintln!("❌ Hangup failed: {}", e),
}
}
["mute", call_id] => {
match client.set_microphone_mute(call_id, true).await {
Ok(_) => println!("🔇 Muted call {}", call_id),
Err(e) => eprintln!("❌ Mute failed: {}", e),
}
}
["unmute", call_id] => {
match client.set_microphone_mute(call_id, false).await {
Ok(_) => println!("🔊 Unmuted call {}", call_id),
Err(e) => eprintln!("❌ Unmute failed: {}", e),
}
}
["quit"] => break,
_ => println!("❓ Unknown command. Try: call, hangup, mute, unmute, quit"),
}
}
Ok(())
}
async fn start_quality_monitoring(client: Arc<Client>, call_id: String) {
tokio::spawn(async move {
let mut poor_quality_count = 0;
let mut quality_history = Vec::new();
while let Ok(Some(call_info)) = client.get_call(&call_id).await {
if !call_info.state.is_active() {
break;
}
if let Ok(Some(stats)) = client.get_media_statistics(&call_id).await {
if let Some(quality) = stats.quality_metrics {
let mos = quality.mos_score.unwrap_or(0.0);
quality_history.push(mos);
// Alert on sustained poor quality
if mos < 3.0 {
poor_quality_count += 1;
if poor_quality_count >= 3 {
println!("🚨 Sustained poor quality on call {} (MOS: {:.1})", call_id, mos);
// In production: notify user, attempt codec change, etc.
poor_quality_count = 0;
}
} else {
poor_quality_count = 0;
}
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
// Final quality report
if !quality_history.is_empty() {
let avg_mos = quality_history.iter().sum::<f64>() / quality_history.len() as f64;
println!("📊 Call {} final quality: {:.1} average MOS", call_id, avg_mos);
}
});
}
use rvoip_client_core::{ClientEvent, CallState};
// Event handling loop
tokio::spawn(async move {
let mut events = client.subscribe_to_events().await;
while let Ok(event) = events.recv().await {
match event {
ClientEvent::IncomingCall { call_id, from, .. } => {
println!("Incoming call from: {}", from);
// Auto-answer after 2 seconds
let client_clone = client.clone();
let call_id_clone = call_id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
if let Err(e) = client_clone.answer_call(&call_id_clone).await {
eprintln!("Failed to answer call: {}", e);
}
});
}
ClientEvent::CallStateChanged { call_id, new_state, .. } => {
println!("Call {} state: {:?}", call_id, new_state);
}
_ => {}
}
}
});
use rvoip_client_core::{ClientBuilder, MediaConfig};
use std::collections::HashMap;
let client = ClientBuilder::new()
.local_address("127.0.0.1:5060".parse()?)
.with_media(|m| m
.codecs(vec!["opus", "G722", "PCMU"])
.require_srtp(false)
.echo_cancellation(true)
.noise_suppression(true)
.auto_gain_control(true)
.dtmf_enabled(true)
.max_bandwidth_kbps(256)
.preferred_ptime(20)
.custom_attributes({
let mut attrs = HashMap::new();
attrs.insert("custom-attr".to_string(), "value".to_string());
attrs
})
)
.build()
.await?;
// During an active call
let call_id = client.make_call("sip:alice@example.com").await?;
// Mute microphone
client.set_microphone_mute(&call_id, true).await?;
// Put call on hold
client.hold_call(&call_id).await?;
// Resume call
client.resume_call(&call_id).await?;
// Send DTMF
client.send_dtmf(&call_id, '1').await?;
// Transfer call (blind transfer)
client.transfer_call(&call_id, "sip:charlie@example.com").await?;
// Get call information
let call_info = client.get_call(&call_id).await?;
println!("Call duration: {:?}", call_info.connected_at);
use rvoip_client_core::{AudioFrame, AudioStreamConfig};
// Start real-time audio streaming for a call
let call_id = client.make_call("sip:alice@example.com").await?;
// Configure high-quality audio stream
let config = AudioStreamConfig {
sample_rate: 48000,
channels: 1,
codec: "Opus".to_string(),
frame_size_ms: 20,
enable_aec: true, // Echo cancellation
enable_agc: true, // Auto gain control
enable_vad: true, // Voice activity detection
};
client.set_audio_stream_config(&call_id, config).await?;
client.start_audio_stream(&call_id).await?;
// Subscribe to incoming audio frames (for speaker playback)
let audio_subscriber = client.subscribe_to_audio_frames(&call_id).await?;
tokio::spawn(async move {
while let Ok(frame) = audio_subscriber.recv() {
// Process incoming audio frame (play through speakers)
play_audio_frame_to_speaker(frame).await;
}
});
// Send outgoing audio frames (from microphone)
let client_clone = client.clone();
let call_id_clone = call_id.clone();
tokio::spawn(async move {
loop {
// Capture audio frame from microphone
if let Some(frame) = capture_audio_frame_from_microphone().await {
// Send frame for encoding and transmission
let _ = client_clone.send_audio_frame(&call_id_clone, frame).await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
});
// Audio frame processing example
async fn play_audio_frame_to_speaker(frame: AudioFrame) {
// Send to audio device (speakers/headphones)
println!("Playing {} samples at {}Hz", frame.samples.len(), frame.sample_rate);
// Integrate with audio libraries like cpal, portaudio, etc.
}
async fn capture_audio_frame_from_microphone() -> Option<AudioFrame> {
// Capture from microphone using audio libraries
// Return AudioFrame with captured samples
let samples = vec![0i16; 480]; // 20ms at 24kHz (example)
Some(AudioFrame::new(samples, 24000, 1, get_timestamp()))
}
fn get_timestamp() -> u32 {
// Return current RTP timestamp
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u32
}
When you configure a specific IP address, it propagates through all layers of the stack:
// This ensures 192.168.1.100 is used at all layers (no more 0.0.0.0 defaults)
let client = ClientBuilder::new()
.local_address("192.168.1.100:5060".parse()?)
.media_address("192.168.1.100:0".parse()?) // Same IP, auto port
.build()
.await?;
Key points:
Set media port to 0 for automatic allocation:
// Port 0 = automatic allocation when media session is created
let client = ClientBuilder::new()
.local_address("127.0.0.1:5060".parse()?) // SIP on standard port 5060
.media_address("127.0.0.1:0".parse()?) // Media port auto-allocated
.build()
.await?;
How it works:
rtp_ports()
method// Example 1: Specific bind address with automatic media ports
let client = ClientBuilder::new()
.local_address("173.225.104.102:5060".parse()?) // Your server's public IP
.media_address("173.225.104.102:0".parse()?) // Same IP, auto media port
.rtp_ports(30000, 40000) // Custom RTP range
.build()
.await?;
// Example 2: Different IPs for SIP and media (multi-homed server)
let client = ClientBuilder::new()
.local_address("203.0.113.10:5060".parse()?) // External IP for SIP
.media_address("10.0.1.100:0".parse()?) // Internal IP for media
.build()
.await?;
// Example 3: All interfaces with automatic ports
let client = ClientBuilder::new()
.local_address("0.0.0.0:5060".parse()?) // Bind to all interfaces
.media_address("0.0.0.0:0".parse()?) // Auto-select interface and port
.build()
.await?;
Client-core seamlessly integrates with session-core's enhanced media API:
// Media preferences are automatically applied to all SDP generation
let client = ClientBuilder::new()
.local_address("127.0.0.1:5060".parse()?)
.with_media(|m| m
.codecs(vec!["opus", "G722", "PCMU"]) // Preference order
.echo_cancellation(true) // Audio processing
.max_bandwidth_kbps(128) // Bandwidth limits
)
.build()
.await?;
// When accepting calls, preferences are automatically used
client.accept_call(&call_id).await?; // SDP includes opus, G722, PCMU in order
// When making calls, preferences are automatically used
let call_id = client.make_call("sip:bob@example.com").await?;
Benefits:
Run the comprehensive test suite:
# Run all tests
cargo test -p rvoip-client-core
# Run specific test categories
cargo test -p rvoip-client-core --test client_lifecycle
cargo test -p rvoip-client-core --test call_operations
cargo test -p rvoip-client-core --test media_operations
cargo test -p rvoip-client-core --test controls_tests
# Run with ignored integration tests (requires SIP server)
cargo test -p rvoip-client-core -- --ignored
# Run performance benchmarks
cargo test -p rvoip-client-core --release -- --ignored benchmark
Test Coverage: 20/20 tests passing (100% success rate)
# Basic client example
cargo run --example basic_client
# Client-server demo
cd examples/client-server
cargo run --bin server &
cargo run --bin client
# Integration testing
cd examples/sipp_integration
./run_tests.sh
pub struct ClientConfig {
pub local_sip_addr: SocketAddr, // SIP listen address
pub media: MediaConfig, // Media configuration
pub user_agent: String, // User-Agent header
pub session_timeout_secs: u64, // Session timeout
}
pub struct MediaConfig {
pub preferred_codecs: Vec<String>, // Codec preference order
pub echo_cancellation: bool, // Enable AEC
pub noise_suppression: bool, // Enable NS
pub auto_gain_control: bool, // Enable AGC
pub dtmf_enabled: bool, // Enable DTMF
pub max_bandwidth_kbps: Option<u32>, // Bandwidth limit
pub preferred_ptime: Option<u32>, // Packet time (ms)
pub custom_sdp_attributes: HashMap<String, String>, // Custom SDP
pub rtp_port_start: u16, // RTP port range start
pub rtp_port_end: u16, // RTP port range end
}
The library provides comprehensive error handling with user-friendly error messages:
use rvoip_client_core::{ClientError, ClientBuilder};
match client_result {
Err(ClientError::InvalidSipUri(uri)) => {
log::error!("Invalid SIP URI: {}", uri);
show_user_error("Please check the phone number format");
}
Err(ClientError::CallNotFound(call_id)) => {
log::info!("Call {} not found, may have ended", call_id);
update_ui_call_ended(&call_id).await;
}
Err(ClientError::MediaNotAvailable) => {
log::warn!("Media system unavailable");
show_user_error("Audio system not available - check permissions");
}
Err(ClientError::NetworkError(msg)) => {
log::error!("Network error: {}", msg);
show_user_error("Network connection failed - check internet connectivity");
}
Err(ClientError::ConfigurationError(msg)) => {
log::error!("Configuration error: {}", msg);
show_user_error("Client configuration invalid - please check settings");
}
Ok(client) => {
// Handle successful client creation
start_client_monitoring(&client).await?;
}
}
Run the comprehensive test suite:
# Run all tests
cargo test -p rvoip-client-core
# Run integration tests
cargo test -p rvoip-client-core --test '*'
# Run specific test suites
cargo test -p rvoip-client-core client_lifecycle
cargo test -p rvoip-client-core call_operations
cargo test -p rvoip-client-core media_controls
# Run performance benchmarks
cargo test -p rvoip-client-core --release -- --ignored benchmark
The library includes comprehensive examples demonstrating all features:
# Basic client setup
cargo run --example basic_client
# Production softphone
cargo run --example production_softphone
# Call quality monitoring
cargo run --example quality_monitoring
# Advanced media controls
cargo run --example advanced_media
# Complete client-server demo
cd examples/client-server
cargo run --bin server &
cargo run --bin client
Contributions are welcome! Please see the main rvoip contributing guidelines for details.
For client-core specific contributions:
The modular architecture makes it easy to contribute:
manager.rs
- Client lifecycle and coordinationcalls.rs
- Call operations and state managementmedia.rs
- Media functionality and SDP handlingcontrols.rs
- Advanced call controls and transfersevents.rs
- Event system enhancementsDevelopment Status: ✅ Production-Ready Client Library
Production Readiness: ✅ Ready for VoIP Application Development
Current Limitations: ⚠️ Minor Feature Gaps
This project is licensed under either of
at your option.
Built with ❤️ for the Rust VoIP community - Production-ready SIP client development made simple