rvoip_client_core/lib.rs
1//! High-level SIP client library for VoIP applications
2//!
3//! This crate provides a user-friendly API for building SIP/VoIP client applications.
4//! It handles the complexity of SIP signaling, media negotiation, and call management
5//! while presenting a simple, async-first interface.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────┐
11//! │ Your Application │
12//! └───────────┬─────────────┘
13//! │
14//! ┌───────────▼─────────────┐
15//! │ client-core │ ◄── You are here
16//! │ ┌─────────────────────┐ │
17//! │ │ ClientManager │ │ • High-level call control
18//! │ │ Registration │ │ • Event handling
19//! │ │ Media Control │ │ • Clean async API
20//! │ └─────────────────────┘ │
21//! └───────────┬─────────────┘
22//! │
23//! ┌───────────▼─────────────┐
24//! │ session-core │ • Session management
25//! │ │ • Protocol coordination
26//! └───────────┬─────────────┘ • Infrastructure abstraction
27//! │
28//! ┌───────────▼─────────────┐
29//! │ Lower-level crates │ • SIP, RTP, Media
30//! │ (transaction, dialog, │ • Transport layers
31//! │ sip-core, etc.) │ • Codec processing
32//! └─────────────────────────┘
33//! ```
34//!
35//! # Quick Start
36//!
37//! ## Basic Client Setup
38//!
39//! ```rust,no_run
40//! use rvoip_client_core::{ClientBuilder, Client, ClientEvent};
41//! use std::sync::Arc;
42//!
43//! #[tokio::main]
44//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! // Build and start a client
46//! let client = ClientBuilder::new()
47//! .user_agent("MyApp/1.0")
48//! .local_address("0.0.0.0:5060".parse()?)
49//! .build()
50//! .await?;
51//!
52//! client.start().await?;
53//!
54//! // Subscribe to events
55//! let mut events = client.subscribe_events();
56//!
57//! // Make a call
58//! let call_id = client.make_call(
59//! "sip:alice@example.com".to_string(),
60//! "sip:bob@example.com".to_string(),
61//! None, // Let session-core generate SDP
62//! ).await?;
63//!
64//! // Handle events (in real applications, this would run until shutdown)
65//! tokio::select! {
66//! result = events.recv() => {
67//! if let Ok(event) = result {
68//! match event {
69//! ClientEvent::CallStateChanged { info, .. } => {
70//! println!("Call {} state: {:?}", info.call_id, info.new_state);
71//! }
72//! _ => {}
73//! }
74//! }
75//! }
76//! _ = tokio::time::sleep(tokio::time::Duration::from_secs(30)) => {
77//! println!("Application timeout");
78//! }
79//! }
80//!
81//! Ok(())
82//! }
83//! ```
84//!
85//! ## Registration Example
86//!
87//! ```rust
88//! # use rvoip_client_core::{ClientBuilder, registration::RegistrationConfig};
89//! # use std::sync::Arc;
90//! # async fn example(client: Arc<rvoip_client_core::Client>) -> Result<(), Box<dyn std::error::Error>> {
91//! // Register with a SIP server
92//! let config = RegistrationConfig::new(
93//! "sip:registrar.example.com".to_string(),
94//! "sip:alice@example.com".to_string(),
95//! "sip:alice@192.168.1.100:5060".to_string(),
96//! )
97//! .with_credentials("alice".to_string(), "secret123".to_string())
98//! .with_expires(3600); // 1 hour
99//!
100//! let reg_id = client.register(config).await?;
101//!
102//! // Later, refresh the registration
103//! client.refresh_registration(reg_id).await?;
104//!
105//! // Or unregister
106//! client.unregister(reg_id).await?;
107//! # Ok(())
108//! # }
109//! ```
110//!
111//! # Network Configuration
112//!
113//! ## Bind Address Propagation
114//!
115//! When you configure a specific IP address, it propagates through all layers:
116//!
117//! ```rust,no_run
118//! use rvoip_client_core::ClientBuilder;
119//!
120//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
121//! // This ensures 192.168.1.100 is used at all layers
122//! let client = ClientBuilder::new()
123//! .local_address("192.168.1.100:5060".parse()?)
124//! .media_address("192.168.1.100:0".parse()?) // Same IP, auto port
125//! .build()
126//! .await?;
127//! # Ok(())
128//! # }
129//! ```
130//!
131//! The configured IP address propagates to session-core, dialog-core, and transport layers.
132//! No more hardcoded 0.0.0.0 addresses when you specify an IP.
133//!
134//! ## Automatic Port Allocation
135//!
136//! Set media port to 0 for automatic allocation:
137//!
138//! ```rust,no_run
139//! use rvoip_client_core::ClientBuilder;
140//!
141//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
142//! let client = ClientBuilder::new()
143//! .local_address("127.0.0.1:5060".parse()?) // SIP on standard port
144//! .media_address("127.0.0.1:0".parse()?) // Port 0 = auto
145//! .rtp_ports(10000, 20000) // RTP port range
146//! .build()
147//! .await?;
148//! # Ok(())
149//! # }
150//! ```
151//!
152//! When port is set to 0:
153//! - It signals automatic allocation from the RTP port range
154//! - Actual allocation happens when media sessions are created
155//! - Each media session gets unique ports from the pool
156//!
157//! # Features
158//!
159//! - **Call Management**: Make, receive, hold, transfer calls
160//! - **Registration**: SIP REGISTER support with authentication
161//! - **Media Control**: Audio mute/unmute, codec selection, SDP handling
162//! - **Event System**: Async event notifications for all operations
163//! - **Clean Architecture**: All complexity handled through session-core
164//! - **Network Flexibility**: Automatic port allocation and bind address control
165//!
166//! # Error Handling
167//!
168//! All operations return `ClientResult<T>` which wraps `ClientError`:
169//!
170//! ```rust
171//! use rvoip_client_core::{Client, ClientError};
172//! use std::sync::Arc;
173//!
174//! async fn example(client: Arc<Client>) -> Result<(), Box<dyn std::error::Error>> {
175//! // Example error handling pattern
176//! match client.make_call(
177//! "sip:alice@example.com".to_string(),
178//! "sip:bob@example.com".to_string(),
179//! None
180//! ).await {
181//! Ok(call_id) => {
182//! println!("Call started: {}", call_id);
183//! }
184//! Err(ClientError::NetworkError { reason }) => {
185//! eprintln!("Network problem: {}", reason);
186//! // Could retry with exponential backoff
187//! }
188//! Err(ClientError::InvalidConfiguration { field, reason }) => {
189//! eprintln!("Configuration error in {}: {}", field, reason);
190//! // Fix configuration and retry
191//! }
192//! Err(e) => {
193//! eprintln!("Call failed: {}", e);
194//! // Handle other error types
195//! }
196//! }
197//! Ok(())
198//! }
199//! ```
200
201#![warn(missing_docs)]
202#![doc(html_root_url = "https://docs.rs/rvoip-client-core/0.1.0")]
203
204pub mod client;
205pub mod call;
206pub mod registration;
207pub mod events;
208pub mod error;
209
210// Public API exports (only high-level client-core types)
211pub use client::{
212 ClientManager,
213 ClientCallHandler,
214 Client,
215 ClientBuilder,
216 // All types are now re-exported from types module via client::mod
217 ClientStats,
218 CallCapabilities,
219 CallMediaInfo,
220 AudioCodecInfo,
221 AudioQualityMetrics,
222 MediaCapabilities,
223 MediaSessionInfo,
224 NegotiatedMediaParams,
225 EnhancedMediaCapabilities,
226 ClientConfig,
227 MediaConfig,
228 MediaPreset,
229 MediaConfigBuilder,
230};
231pub use call::{CallId, CallInfo, CallDirection, CallState};
232pub use registration::{RegistrationConfig, RegistrationInfo, RegistrationStatus};
233pub use events::{
234 ClientEventHandler,
235 ClientEvent,
236 IncomingCallInfo,
237 CallStatusInfo,
238 RegistrationStatusInfo,
239 CallAction,
240 MediaEventType,
241 MediaEventInfo,
242 EventFilter,
243 EventPriority,
244 EventSubscription,
245 EventEmitter,
246};
247pub use error::{ClientError, ClientResult};
248
249// Re-export commonly used types from session-core (for convenience)
250pub use rvoip_session_core::api::types::SessionId;
251
252// Re-export streaming audio types from session-core
253pub use rvoip_session_core::api::types::{
254 AudioFrame,
255 AudioFrameSubscriber,
256 AudioStreamConfig
257};
258
259/// Client-core version information
260pub const VERSION: &str = env!("CARGO_PKG_VERSION");
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use std::sync::Arc;
266
267 #[tokio::test]
268 async fn test_client_manager_creation() {
269 // Test that we can create a ClientManager
270 let config = ClientConfig::new()
271 .with_sip_addr("127.0.0.1:5080".parse().unwrap())
272 .with_media_addr("127.0.0.1:5081".parse().unwrap());
273
274 let client_result = ClientManager::new(config).await;
275 assert!(client_result.is_ok(), "ClientManager creation should succeed");
276
277 let _client = client_result.unwrap();
278 // Test passes if we can create the client without panicking
279 }
280
281 #[tokio::test]
282 async fn test_client_manager_lifecycle() {
283 // Test start/stop lifecycle
284 let config = ClientConfig::new()
285 .with_sip_addr("127.0.0.1:5082".parse().unwrap())
286 .with_media_addr("127.0.0.1:5083".parse().unwrap());
287
288 let client = ClientManager::new(config).await
289 .expect("Failed to create client");
290
291 // Test start
292 let start_result = client.start().await;
293 assert!(start_result.is_ok(), "Client start should succeed");
294
295 // Test stop
296 let stop_result = client.stop().await;
297 assert!(stop_result.is_ok(), "Client stop should succeed");
298 }
299
300 // Helper event handler for testing
301 struct TestEventHandler;
302
303 #[async_trait::async_trait]
304 impl ClientEventHandler for TestEventHandler {
305 async fn on_incoming_call(&self, _call_info: IncomingCallInfo) -> CallAction {
306 CallAction::Accept
307 }
308
309 async fn on_call_state_changed(&self, _status_info: CallStatusInfo) {
310 // No-op for test
311 }
312
313 async fn on_registration_status_changed(&self, _status_info: RegistrationStatusInfo) {
314 // No-op for test
315 }
316
317 async fn on_media_event(&self, _media_info: MediaEventInfo) {
318 // No-op for test
319 }
320
321 async fn on_client_error(&self, _error: ClientError, _call_id: Option<CallId>) {
322 // No-op for test
323 }
324
325 async fn on_network_event(&self, _connected: bool, _reason: Option<String>) {
326 // No-op for test
327 }
328 }
329
330 #[tokio::test]
331 async fn test_client_core_compiles() {
332 // Basic compilation test
333 assert!(true);
334 }
335
336 #[tokio::test]
337 async fn test_registration_not_implemented() {
338 // Test that registration operations return NotImplemented error
339 let config = ClientConfig::new()
340 .with_sip_addr("127.0.0.1:5084".parse().unwrap())
341 .with_media_addr("127.0.0.1:5085".parse().unwrap());
342
343 let client = ClientManager::new(config).await.unwrap();
344 let reg_config = RegistrationConfig::new(
345 "sip:server.example.com".to_string(),
346 "sip:user@example.com".to_string(),
347 "sip:user@127.0.0.1:5084".to_string(),
348 );
349
350 let result = client.register(reg_config).await;
351 assert!(result.is_err());
352 match result.unwrap_err() {
353 ClientError::NotImplemented { .. } => {
354 // Expected - registration not available in session-core
355 }
356 ClientError::InternalError { message } if message.contains("Registration failed") => {
357 // Also acceptable - session-core might have registration support now
358 }
359 other => panic!("Expected NotImplemented or InternalError, got: {:?}", other),
360 }
361 }
362
363 #[tokio::test]
364 async fn test_priority_4_1_media_integration() {
365 // Test Priority 4.1: Enhanced Media Integration APIs
366 let config = ClientConfig::new()
367 .with_sip_addr("127.0.0.1:5086".parse().unwrap())
368 .with_media_addr("127.0.0.1:5087".parse().unwrap());
369
370 let client = ClientManager::new(config).await.unwrap();
371 let handler = Arc::new(TestEventHandler);
372 client.set_event_handler(handler).await;
373
374 // Start the client
375 client.start().await.unwrap();
376
377 // Test media capabilities API
378 let capabilities = client.get_media_capabilities().await;
379 assert!(capabilities.can_mute_microphone);
380 assert!(capabilities.can_mute_speaker);
381 assert!(capabilities.can_hold);
382 assert!(capabilities.can_send_dtmf);
383 assert!(capabilities.supports_rtp);
384 assert!(!capabilities.supported_codecs.is_empty());
385
386 // Test codec enumeration
387 let codecs = client.get_available_codecs().await;
388 assert!(!codecs.is_empty());
389
390 // Verify standard codecs are available
391 let codec_names: Vec<String> = codecs.iter().map(|c| c.name.clone()).collect();
392 assert!(codec_names.contains(&"PCMU".to_string()));
393 assert!(codec_names.contains(&"PCMA".to_string()));
394 assert!(codec_names.contains(&"OPUS".to_string()));
395
396 // Test codec preference setting
397 let preferred_codecs = vec!["OPUS".to_string(), "PCMU".to_string()];
398 let result = client.set_preferred_codecs(preferred_codecs).await;
399 assert!(result.is_ok());
400
401 // Test with a mock call (this would normally fail without an actual session)
402 // but we're testing the API structure
403 let fake_call_id = CallId::new_v4();
404
405 // Test media info for non-existent call (should fail gracefully)
406 let media_info_result = client.get_call_media_info(&fake_call_id).await;
407 assert!(media_info_result.is_err());
408 match media_info_result.unwrap_err() {
409 ClientError::CallNotFound { .. } => {
410 // Expected for non-existent call
411 }
412 other => panic!("Expected CallNotFound error, got: {:?}", other),
413 }
414
415 // Test mute state for non-existent call
416 let mute_result = client.get_microphone_mute_state(&fake_call_id).await;
417 assert!(mute_result.is_err());
418
419 // Test speaker mute state for non-existent call
420 let speaker_mute_result = client.get_speaker_mute_state(&fake_call_id).await;
421 assert!(speaker_mute_result.is_err());
422
423 // Test audio transmission status for non-existent call
424 let audio_active_result = client.is_audio_transmission_active(&fake_call_id).await;
425 assert!(audio_active_result.is_err());
426
427 client.stop().await.unwrap();
428
429 println!("✅ Priority 4.1 Media Integration APIs validated successfully!");
430 }
431
432 #[tokio::test]
433 async fn test_priority_4_2_media_session_coordination() {
434 // Test Priority 4.2: Media Session Coordination APIs
435 let config = ClientConfig::new()
436 .with_sip_addr("127.0.0.1:5088".parse().unwrap())
437 .with_media_addr("127.0.0.1:5089".parse().unwrap());
438
439 let client = ClientManager::new(config).await.unwrap();
440 let handler = Arc::new(TestEventHandler);
441 client.set_event_handler(handler).await;
442
443 // Start the client
444 client.start().await.unwrap();
445
446 // Test enhanced media capabilities
447 let enhanced_capabilities = client.get_enhanced_media_capabilities().await;
448 assert!(enhanced_capabilities.supports_sdp_offer_answer);
449 assert!(enhanced_capabilities.supports_media_session_lifecycle);
450 assert!(enhanced_capabilities.supports_sdp_renegotiation);
451 assert!(enhanced_capabilities.supports_early_media);
452 assert!(enhanced_capabilities.supports_media_session_updates);
453 assert!(enhanced_capabilities.supports_codec_negotiation);
454 assert_eq!(enhanced_capabilities.supported_sdp_version, "0");
455 assert_eq!(enhanced_capabilities.preferred_rtp_port_range, (10000, 20000));
456 assert!(enhanced_capabilities.supported_transport_protocols.contains(&"RTP/AVP".to_string()));
457
458 // Test with fake call ID (APIs should fail gracefully)
459 let fake_call_id = CallId::new_v4();
460
461 // Test SDP generation for non-existent call
462 let sdp_offer_result = client.generate_sdp_offer(&fake_call_id).await;
463 assert!(sdp_offer_result.is_err());
464 match sdp_offer_result.unwrap_err() {
465 ClientError::CallNotFound { .. } => {
466 // Expected for non-existent call
467 }
468 other => panic!("Expected CallNotFound error, got: {:?}", other),
469 }
470
471 // Test SDP answer processing for non-existent call
472 let sdp_answer_result = client.process_sdp_answer(&fake_call_id, "v=0\r\nm=audio 5004 RTP/AVP 0\r\n").await;
473 assert!(sdp_answer_result.is_err());
474
475 // Test empty SDP answer validation
476 let empty_sdp_result = client.process_sdp_answer(&fake_call_id, "").await;
477 assert!(empty_sdp_result.is_err());
478 match empty_sdp_result.unwrap_err() {
479 ClientError::InvalidConfiguration { field, .. } => {
480 assert_eq!(field, "sdp_answer");
481 }
482 other => panic!("Expected InvalidConfiguration error, got: {:?}", other),
483 }
484
485 // Test media session lifecycle for non-existent call
486 let start_media_result = client.start_media_session(&fake_call_id).await;
487 assert!(start_media_result.is_err());
488
489 let stop_media_result = client.stop_media_session(&fake_call_id).await;
490 assert!(stop_media_result.is_err());
491
492 // Test media session status for non-existent call
493 let is_active_result = client.is_media_session_active(&fake_call_id).await;
494 assert!(is_active_result.is_err());
495
496 // Test media session info for non-existent call
497 let session_info_result = client.get_media_session_info(&fake_call_id).await;
498 assert!(session_info_result.is_err());
499
500 // Test negotiated media params for non-existent call
501 let negotiated_params_result = client.get_negotiated_media_params(&fake_call_id).await;
502 assert!(negotiated_params_result.is_err());
503
504 // Test media session update for non-existent call
505 let update_result = client.update_media_session(&fake_call_id, "v=0\r\nm=audio 5006 RTP/AVP 0\r\n").await;
506 assert!(update_result.is_err());
507
508 // Test empty SDP update validation
509 let empty_update_result = client.update_media_session(&fake_call_id, "").await;
510 assert!(empty_update_result.is_err());
511 match empty_update_result.unwrap_err() {
512 ClientError::InvalidConfiguration { field, .. } => {
513 assert_eq!(field, "new_sdp");
514 }
515 other => panic!("Expected InvalidConfiguration error, got: {:?}", other),
516 }
517
518 client.stop().await.unwrap();
519
520 println!("✅ Priority 4.2 Media Session Coordination APIs validated successfully!");
521 }
522}