rvoip_client_core/client/media.rs
1// Media module
2
3//! Media operations for the client-core library
4//!
5//! This module contains all media-related operations including mute/unmute,
6//! audio transmission, codec management, SDP handling, and media session lifecycle.
7
8use std::collections::HashMap;
9use chrono::{DateTime, Utc};
10
11// Import session-core APIs
12use rvoip_session_core::api::{
13 SessionControl,
14 MediaControl,
15 SessionCoordinator,
16 SessionId,
17 MediaInfo as SessionMediaInfo,
18 CallStatistics,
19 MediaSessionStats,
20 RtpSessionStats,
21 QualityMetrics,
22 QualityThresholds,
23};
24
25// Import client-core types
26use crate::{
27 ClientResult, ClientError,
28 call::CallId,
29 events::MediaEventInfo,
30};
31
32use super::types::*;
33
34/// Media operations implementation for ClientManager
35impl super::manager::ClientManager {
36 // ===== PRIORITY 4.1: ENHANCED MEDIA INTEGRATION =====
37
38 /// Enhanced microphone mute/unmute with proper session-core integration
39 ///
40 /// Controls the microphone mute state for a specific call. When muted, the local audio
41 /// transmission is stopped, preventing the remote party from hearing your voice.
42 /// This operation validates the call state and emits appropriate media events.
43 ///
44 /// # Arguments
45 ///
46 /// * `call_id` - The unique identifier of the call to mute/unmute
47 /// * `muted` - `true` to mute the microphone, `false` to unmute
48 ///
49 /// # Returns
50 ///
51 /// Returns `Ok(())` if the operation succeeds, or a `ClientError` if:
52 /// - The call is not found
53 /// - The call is in an invalid state (terminated, failed, cancelled)
54 /// - The underlying media session fails to change mute state
55 ///
56 /// # Examples
57 ///
58 /// ```rust
59 /// # use uuid::Uuid;
60 /// # use rvoip_client_core::call::CallId;
61 /// # fn main() {
62 /// // Basic usage - mute the microphone
63 /// let call_id: CallId = Uuid::new_v4();
64 /// println!("Would mute microphone for call {}", call_id);
65 ///
66 /// // Toggle functionality
67 /// let current_state = false; // Simulated current state
68 /// let new_state = !current_state;
69 /// println!("Would toggle microphone from {} to {}", current_state, new_state);
70 /// # }
71 /// ```
72 ///
73 /// ```rust
74 /// # use uuid::Uuid;
75 /// # use rvoip_client_core::call::CallId;
76 /// # fn main() {
77 /// // Privacy mode example
78 /// let call_id: CallId = Uuid::new_v4();
79 /// println!("Enabling privacy mode for call {}", call_id);
80 /// println!("Microphone would be muted");
81 /// # }
82 /// ```
83 ///
84 /// # Side Effects
85 ///
86 /// - Updates call metadata with mute state and timestamp
87 /// - Emits a `MediaEventType::MicrophoneStateChanged` event
88 /// - Calls session-core to actually control audio transmission
89 pub async fn set_microphone_mute(&self, call_id: &CallId, muted: bool) -> ClientResult<()> {
90 let session_id = self.session_mapping.get(call_id)
91 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
92 .clone();
93
94 // Validate call state
95 if let Some(call_info) = self.call_info.get(call_id) {
96 match call_info.state {
97 crate::call::CallState::Connected => {
98 // OK to mute/unmute
99 }
100 crate::call::CallState::Terminated |
101 crate::call::CallState::Failed |
102 crate::call::CallState::Cancelled => {
103 return Err(ClientError::InvalidCallState {
104 call_id: *call_id,
105 current_state: call_info.state.clone()
106 });
107 }
108 _ => {
109 return Err(ClientError::InvalidCallStateGeneric {
110 expected: "Connected".to_string(),
111 actual: format!("{:?}", call_info.state)
112 });
113 }
114 }
115 }
116
117 // Use session-core mute/unmute functionality
118 SessionControl::set_audio_muted(&self.coordinator, &session_id, muted)
119 .await
120 .map_err(|e| ClientError::CallSetupFailed {
121 reason: format!("Failed to set microphone mute: {}", e)
122 })?;
123
124 // Update call metadata
125 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
126 call_info.metadata.insert("microphone_muted".to_string(), muted.to_string());
127 call_info.metadata.insert("mic_mute_changed_at".to_string(), Utc::now().to_rfc3339());
128 }
129
130 // Emit MediaEvent
131 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
132 let media_event = MediaEventInfo {
133 call_id: *call_id,
134 event_type: crate::events::MediaEventType::MicrophoneStateChanged { muted },
135 timestamp: Utc::now(),
136 metadata: {
137 let mut metadata = HashMap::new();
138 metadata.insert("session_id".to_string(), session_id.0.clone());
139 metadata
140 },
141 };
142 handler.on_media_event(media_event).await;
143 }
144
145 tracing::info!("Set microphone muted={} for call {}", muted, call_id);
146 Ok(())
147 }
148
149 /// Enhanced speaker mute/unmute with event emission
150 ///
151 /// Controls the speaker (audio output) mute state for a specific call. When speaker
152 /// is muted, you won't hear audio from the remote party. This is typically handled
153 /// client-side as it controls local audio playback rather than network transmission.
154 ///
155 /// # Arguments
156 ///
157 /// * `call_id` - The unique identifier of the call to mute/unmute
158 /// * `muted` - `true` to mute the speaker, `false` to unmute
159 ///
160 /// # Returns
161 ///
162 /// Returns `Ok(())` if the operation succeeds, or a `ClientError` if the call is not found.
163 ///
164 /// # Examples
165 ///
166 /// ```rust
167 /// # use uuid::Uuid;
168 /// # use rvoip_client_core::call::CallId;
169 /// # fn main() {
170 /// // Basic speaker control
171 /// let call_id: CallId = Uuid::new_v4();
172 /// println!("Would mute speaker for call {}", call_id);
173 ///
174 /// // Unmute speaker
175 /// println!("Would unmute speaker for call {}", call_id);
176 /// # }
177 /// ```
178 ///
179 /// ```rust
180 /// # use uuid::Uuid;
181 /// # use rvoip_client_core::call::CallId;
182 /// # fn main() {
183 /// // Privacy mode: mute both microphone and speaker
184 /// let call_id: CallId = Uuid::new_v4();
185 /// println!("Enabling full privacy mode for call {}", call_id);
186 /// println!("Would mute microphone and speaker");
187 /// # }
188 /// ```
189 ///
190 /// # Implementation Notes
191 ///
192 /// This function handles client-side audio output control and does not affect
193 /// network RTP streams. The mute state is stored in call metadata and can be
194 /// retrieved using `get_speaker_mute_state()`.
195 ///
196 /// # Side Effects
197 ///
198 /// - Updates call metadata with speaker mute state and timestamp
199 /// - Emits a `MediaEventType::SpeakerStateChanged` event
200 pub async fn set_speaker_mute(&self, call_id: &CallId, muted: bool) -> ClientResult<()> {
201 // Validate call exists
202 if !self.call_info.contains_key(call_id) {
203 return Err(ClientError::CallNotFound { call_id: *call_id });
204 }
205
206 let session_id = self.session_mapping.get(call_id)
207 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
208 .clone();
209
210 // Note: Speaker mute is typically handled client-side as session-core
211 // may not have direct speaker control. This is a placeholder implementation.
212
213 // Update call metadata
214 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
215 call_info.metadata.insert("speaker_muted".to_string(), muted.to_string());
216 call_info.metadata.insert("speaker_mute_changed_at".to_string(), Utc::now().to_rfc3339());
217 }
218
219 // Emit MediaEvent
220 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
221 let media_event = MediaEventInfo {
222 call_id: *call_id,
223 event_type: crate::events::MediaEventType::SpeakerStateChanged { muted },
224 timestamp: Utc::now(),
225 metadata: {
226 let mut metadata = HashMap::new();
227 metadata.insert("session_id".to_string(), session_id.0.clone());
228 metadata.insert("client_side_control".to_string(), "true".to_string());
229 metadata
230 },
231 };
232 handler.on_media_event(media_event).await;
233 }
234
235 tracing::info!("Set speaker muted={} for call {} (client-side)", muted, call_id);
236 Ok(())
237 }
238
239 /// Get comprehensive media information for a call using session-core
240 ///
241 /// Retrieves detailed media information about an active call, including SDP negotiation
242 /// details, RTP port assignments, codec information, and current media state.
243 /// This is useful for monitoring call quality, debugging media issues, and displaying
244 /// technical call information to users or administrators.
245 ///
246 /// # Arguments
247 ///
248 /// * `call_id` - The unique identifier of the call to query
249 ///
250 /// # Returns
251 ///
252 /// Returns a `CallMediaInfo` struct containing:
253 /// - Local and remote SDP descriptions
254 /// - RTP port assignments (local and remote)
255 /// - Negotiated audio codec
256 /// - Current mute and hold states
257 /// - Audio direction (send/receive/both/inactive)
258 /// - Quality metrics (if available)
259 ///
260 /// Returns `ClientError::CallNotFound` if the call doesn't exist, or
261 /// `ClientError::InternalError` if media information cannot be retrieved.
262 ///
263 /// # Examples
264 ///
265 /// ```rust
266 /// # use uuid::Uuid;
267 /// # use rvoip_client_core::call::CallId;
268 /// # use rvoip_client_core::client::types::AudioDirection;
269 /// # fn main() {
270 /// // Basic media info retrieval
271 /// let call_id: CallId = Uuid::new_v4();
272 /// println!("Would get media info for call {}", call_id);
273 ///
274 /// // Check audio direction
275 /// let audio_direction = AudioDirection::SendReceive;
276 /// match audio_direction {
277 /// AudioDirection::SendReceive => println!("Full duplex audio"),
278 /// AudioDirection::SendOnly => println!("Send-only (e.g., hold)"),
279 /// AudioDirection::ReceiveOnly => println!("Receive-only"),
280 /// AudioDirection::Inactive => println!("No audio flow"),
281 /// }
282 /// # }
283 /// ```
284 ///
285 /// ```rust
286 /// # use uuid::Uuid;
287 /// # use rvoip_client_core::call::CallId;
288 /// # fn main() {
289 /// // Diagnostic information
290 /// let call_id: CallId = Uuid::new_v4();
291 /// println!("Getting diagnostic info for call {}", call_id);
292 /// println!("This would include SDP, ports, codec, and states");
293 /// # }
294 /// ```
295 pub async fn get_call_media_info(&self, call_id: &CallId) -> ClientResult<CallMediaInfo> {
296 let session_id = self.session_mapping.get(call_id)
297 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
298 .clone();
299
300 // Get media info from session-core
301 let media_info = MediaControl::get_media_info(&self.coordinator, &session_id)
302 .await
303 .map_err(|e| ClientError::InternalError {
304 message: format!("Failed to get media info: {}", e)
305 })?
306 .ok_or_else(|| ClientError::InternalError {
307 message: "No media info available".to_string()
308 })?;
309
310 // Determine audio direction before moving fields
311 let audio_direction = self.determine_audio_direction(&media_info).await;
312
313 // Convert session-core MediaInfo to client-core CallMediaInfo
314 let call_media_info = CallMediaInfo {
315 call_id: *call_id,
316 local_sdp: media_info.local_sdp,
317 remote_sdp: media_info.remote_sdp,
318 local_rtp_port: media_info.local_rtp_port,
319 remote_rtp_port: media_info.remote_rtp_port,
320 codec: media_info.codec,
321 is_muted: self.get_microphone_mute_state(call_id).await.unwrap_or(false),
322 is_on_hold: self.is_call_on_hold(call_id).await.unwrap_or(false),
323 audio_direction,
324 quality_metrics: None, // TODO: Extract quality metrics if available
325 };
326
327 Ok(call_media_info)
328 }
329
330 /// Get the current microphone mute state for a call
331 ///
332 /// Retrieves the current mute state of the microphone for the specified call.
333 /// This state is maintained in the call's metadata and reflects whether local
334 /// audio transmission is currently enabled (false) or disabled (true).
335 ///
336 /// # Arguments
337 ///
338 /// * `call_id` - The unique identifier of the call to query
339 ///
340 /// # Returns
341 ///
342 /// Returns `Ok(true)` if the microphone is muted, `Ok(false)` if unmuted,
343 /// or `ClientError::CallNotFound` if the call doesn't exist.
344 ///
345 /// # Examples
346 ///
347 /// ```rust
348 /// # use uuid::Uuid;
349 /// # use rvoip_client_core::call::CallId;
350 /// # fn main() {
351 /// // Check microphone state
352 /// let call_id: CallId = Uuid::new_v4();
353 /// println!("Would check microphone mute state for call {}", call_id);
354 ///
355 /// // Conditional logic based on mute state
356 /// let is_muted = false; // Simulated state
357 /// if is_muted {
358 /// println!("Microphone is currently muted");
359 /// } else {
360 /// println!("Microphone is active");
361 /// }
362 /// # }
363 /// ```
364 ///
365 /// ```rust
366 /// # use uuid::Uuid;
367 /// # use rvoip_client_core::call::CallId;
368 /// # fn main() {
369 /// // UI indicator logic
370 /// let call_id: CallId = Uuid::new_v4();
371 /// let mute_state = false; // Would get actual state
372 /// let indicator = if mute_state { "🔇" } else { "🔊" };
373 /// println!("Microphone status for call {}: {}", call_id, indicator);
374 /// # }
375 /// ```
376 pub async fn get_microphone_mute_state(&self, call_id: &CallId) -> ClientResult<bool> {
377 if let Some(call_info) = self.call_info.get(call_id) {
378 let muted = call_info.metadata.get("microphone_muted")
379 .map(|s| s == "true")
380 .unwrap_or(false);
381 Ok(muted)
382 } else {
383 Err(ClientError::CallNotFound { call_id: *call_id })
384 }
385 }
386
387 /// Get the current speaker mute state for a call
388 ///
389 /// Retrieves the current mute state of the speaker (audio output) for the specified call.
390 /// This state is maintained in the call's metadata and reflects whether remote
391 /// audio playback is currently enabled (false) or disabled (true).
392 ///
393 /// # Arguments
394 ///
395 /// * `call_id` - The unique identifier of the call to query
396 ///
397 /// # Returns
398 ///
399 /// Returns `Ok(true)` if the speaker is muted, `Ok(false)` if unmuted,
400 /// or `ClientError::CallNotFound` if the call doesn't exist.
401 ///
402 /// # Examples
403 ///
404 /// ```rust
405 /// # use uuid::Uuid;
406 /// # use rvoip_client_core::call::CallId;
407 /// # fn main() {
408 /// // Check speaker state
409 /// let call_id: CallId = Uuid::new_v4();
410 /// println!("Would check speaker mute state for call {}", call_id);
411 ///
412 /// // Audio feedback prevention
413 /// let speaker_muted = true; // Simulated state
414 /// if speaker_muted {
415 /// println!("Safe to use speakerphone mode");
416 /// } else {
417 /// println!("May cause audio feedback");
418 /// }
419 /// # }
420 /// ```
421 ///
422 /// ```rust
423 /// # use uuid::Uuid;
424 /// # use rvoip_client_core::call::CallId;
425 /// # fn main() {
426 /// // Privacy status check
427 /// let call_id: CallId = Uuid::new_v4();
428 /// let mic_muted = true;
429 /// let speaker_muted = true;
430 ///
431 /// if mic_muted && speaker_muted {
432 /// println!("Call {} is in full privacy mode", call_id);
433 /// }
434 /// # }
435 /// ```
436 pub async fn get_speaker_mute_state(&self, call_id: &CallId) -> ClientResult<bool> {
437 if let Some(call_info) = self.call_info.get(call_id) {
438 let muted = call_info.metadata.get("speaker_muted")
439 .map(|s| s == "true")
440 .unwrap_or(false);
441 Ok(muted)
442 } else {
443 Err(ClientError::CallNotFound { call_id: *call_id })
444 }
445 }
446
447 /// Get supported audio codecs with comprehensive information
448 ///
449 /// Returns a complete list of audio codecs supported by this client implementation.
450 /// This is an alias for `get_available_codecs()` provided for API consistency.
451 /// Each codec includes detailed information about capabilities, quality ratings,
452 /// and technical specifications.
453 ///
454 /// # Returns
455 ///
456 /// A vector of `AudioCodecInfo` structures containing:
457 /// - Codec name and payload type
458 /// - Sample rate and channel configuration
459 /// - Quality rating (1-5 scale)
460 /// - Human-readable description
461 ///
462 /// # Examples
463 ///
464 /// ```rust
465 /// # use rvoip_client_core::client::types::AudioCodecInfo;
466 /// # fn main() {
467 /// // Simulate codec information
468 /// let codec = AudioCodecInfo {
469 /// name: "OPUS".to_string(),
470 /// payload_type: 111,
471 /// clock_rate: 48000,
472 /// channels: 2,
473 /// description: "High quality codec".to_string(),
474 /// quality_rating: 5,
475 /// };
476 ///
477 /// println!("Codec: {} (Quality: {}/5)", codec.name, codec.quality_rating);
478 /// # }
479 /// ```
480 ///
481 /// ```rust
482 /// # fn main() {
483 /// // Filter high-quality codecs
484 /// let quality_threshold = 4;
485 /// println!("Looking for codecs with quality >= {}", quality_threshold);
486 /// println!("Would filter codec list by quality rating");
487 /// # }
488 /// ```
489 ///
490 /// Get supported audio codecs (alias for get_available_codecs)
491 pub async fn get_supported_audio_codecs(&self) -> Vec<AudioCodecInfo> {
492 self.get_available_codecs().await
493 }
494
495 /// Get list of available audio codecs with detailed information
496 ///
497 /// Returns a comprehensive list of audio codecs supported by the client,
498 /// including payload types, sample rates, quality ratings, and descriptions.
499 /// This information can be used for codec selection, capability negotiation,
500 /// and display in user interfaces.
501 ///
502 /// # Returns
503 ///
504 /// A vector of `AudioCodecInfo` structures, each containing:
505 /// - Codec name and standard designation
506 /// - RTP payload type number
507 /// - Audio sampling rate and channel count
508 /// - Human-readable description
509 /// - Quality rating (1-5 scale, 5 being highest)
510 ///
511 /// # Examples
512 ///
513 /// ```rust
514 /// # use rvoip_client_core::client::types::AudioCodecInfo;
515 /// # fn main() {
516 /// // Example codec information
517 /// let codecs = vec![
518 /// AudioCodecInfo {
519 /// name: "OPUS".to_string(),
520 /// payload_type: 111,
521 /// clock_rate: 48000,
522 /// channels: 2,
523 /// description: "High quality codec".to_string(),
524 /// quality_rating: 5,
525 /// },
526 /// AudioCodecInfo {
527 /// name: "G722".to_string(),
528 /// payload_type: 9,
529 /// clock_rate: 8000,
530 /// channels: 1,
531 /// description: "Wideband audio".to_string(),
532 /// quality_rating: 4,
533 /// }
534 /// ];
535 ///
536 /// for codec in &codecs {
537 /// println!("Codec: {} (PT: {}, Rate: {}Hz, Quality: {}/5)",
538 /// codec.name, codec.payload_type, codec.clock_rate, codec.quality_rating);
539 /// println!(" Description: {}", codec.description);
540 /// }
541 ///
542 /// // Find high-quality codecs
543 /// let high_quality: Vec<_> = codecs
544 /// .into_iter()
545 /// .filter(|c| c.quality_rating >= 4)
546 /// .collect();
547 /// println!("Found {} high-quality codecs", high_quality.len());
548 /// # }
549 /// ```
550 pub async fn get_available_codecs(&self) -> Vec<AudioCodecInfo> {
551 // Enhanced codec list with quality ratings and detailed information
552 vec![
553 AudioCodecInfo {
554 name: "PCMU".to_string(),
555 payload_type: 0,
556 clock_rate: 8000,
557 channels: 1,
558 description: "G.711 μ-law - Standard quality, widely compatible".to_string(),
559 quality_rating: 3,
560 },
561 AudioCodecInfo {
562 name: "PCMA".to_string(),
563 payload_type: 8,
564 clock_rate: 8000,
565 channels: 1,
566 description: "G.711 A-law - Standard quality, widely compatible".to_string(),
567 quality_rating: 3,
568 },
569 AudioCodecInfo {
570 name: "G722".to_string(),
571 payload_type: 9,
572 clock_rate: 8000,
573 channels: 1,
574 description: "G.722 - Wideband audio, good quality".to_string(),
575 quality_rating: 4,
576 },
577 AudioCodecInfo {
578 name: "G729".to_string(),
579 payload_type: 18,
580 clock_rate: 8000,
581 channels: 1,
582 description: "G.729 - Low bandwidth, compressed".to_string(),
583 quality_rating: 2,
584 },
585 AudioCodecInfo {
586 name: "OPUS".to_string(),
587 payload_type: 111,
588 clock_rate: 48000,
589 channels: 2,
590 description: "Opus - High quality, adaptive bitrate".to_string(),
591 quality_rating: 5,
592 },
593 ]
594 }
595
596 /// Get codec information for a specific active call
597 ///
598 /// Retrieves detailed information about the audio codec currently being used
599 /// for the specified call. This includes technical specifications, quality ratings,
600 /// and capabilities of the negotiated codec. Returns `None` if no codec has been
601 /// negotiated yet or if the call doesn't have active media.
602 ///
603 /// # Arguments
604 ///
605 /// * `call_id` - The unique identifier of the call to query
606 ///
607 /// # Returns
608 ///
609 /// Returns `Ok(Some(AudioCodecInfo))` with codec details if available,
610 /// `Ok(None)` if no codec is negotiated, or `ClientError` if the call is not found
611 /// or media information cannot be retrieved.
612 ///
613 /// # Examples
614 ///
615 /// ```rust
616 /// # use uuid::Uuid;
617 /// # use rvoip_client_core::call::CallId;
618 /// # use rvoip_client_core::client::types::AudioCodecInfo;
619 /// # fn main() {
620 /// // Check call codec
621 /// let call_id: CallId = Uuid::new_v4();
622 /// println!("Would get codec info for call {}", call_id);
623 ///
624 /// // Example codec info handling
625 /// let codec_info = Some(AudioCodecInfo {
626 /// name: "G722".to_string(),
627 /// payload_type: 9,
628 /// clock_rate: 8000,
629 /// channels: 1,
630 /// description: "Wideband audio".to_string(),
631 /// quality_rating: 4,
632 /// });
633 ///
634 /// match codec_info {
635 /// Some(codec) => println!("Using codec: {} ({})", codec.name, codec.description),
636 /// None => println!("No codec negotiated yet"),
637 /// }
638 /// # }
639 /// ```
640 ///
641 /// ```rust
642 /// # use uuid::Uuid;
643 /// # use rvoip_client_core::call::CallId;
644 /// # fn main() {
645 /// // Quality assessment
646 /// let call_id: CallId = Uuid::new_v4();
647 /// println!("Assessing call quality for call {}", call_id);
648 ///
649 /// let quality_rating = 4; // Simulated rating
650 /// match quality_rating {
651 /// 5 => println!("Excellent audio quality"),
652 /// 4 => println!("Good audio quality"),
653 /// 3 => println!("Acceptable audio quality"),
654 /// _ => println!("Poor audio quality"),
655 /// }
656 /// # }
657 /// ```
658 pub async fn get_call_codec_info(&self, call_id: &CallId) -> ClientResult<Option<AudioCodecInfo>> {
659 let media_info = self.get_call_media_info(call_id).await?;
660
661 if let Some(codec_name) = media_info.codec {
662 let codecs = self.get_available_codecs().await;
663 let codec_info = codecs.into_iter()
664 .find(|c| c.name.eq_ignore_ascii_case(&codec_name));
665 Ok(codec_info)
666 } else {
667 Ok(None)
668 }
669 }
670
671 /// Set preferred codec order for future calls
672 ///
673 /// Configures the preferred order of audio codecs for use in future call negotiations.
674 /// The client will attempt to negotiate codecs in the specified order, with the first
675 /// codec in the list being the most preferred. This setting affects SDP generation
676 /// and codec negotiation during call establishment.
677 ///
678 /// # Arguments
679 ///
680 /// * `codec_names` - Vector of codec names in order of preference (e.g., ["OPUS", "G722", "PCMU"])
681 ///
682 /// # Returns
683 ///
684 /// Returns `Ok(())` on success. Currently always succeeds as this stores preferences
685 /// for future use rather than validating codec availability immediately.
686 ///
687 /// # Examples
688 ///
689 /// ```rust
690 /// # fn main() {
691 /// // Set high-quality codec preference
692 /// let high_quality_codecs = vec![
693 /// "OPUS".to_string(),
694 /// "G722".to_string(),
695 /// "PCMU".to_string(),
696 /// ];
697 /// println!("Would set codec preference: {:?}", high_quality_codecs);
698 /// # }
699 /// ```
700 ///
701 /// ```rust
702 /// # fn main() {
703 /// // Low bandwidth preference
704 /// let low_bandwidth_codecs = vec![
705 /// "G729".to_string(),
706 /// "PCMU".to_string(),
707 /// "PCMA".to_string(),
708 /// ];
709 /// println!("Low bandwidth codec order: {:?}", low_bandwidth_codecs);
710 /// # }
711 /// ```
712 ///
713 /// ```rust
714 /// # fn main() {
715 /// // Enterprise compatibility preference
716 /// let enterprise_codecs = vec![
717 /// "G722".to_string(), // Good quality, widely supported
718 /// "PCMU".to_string(), // Universal compatibility
719 /// "PCMA".to_string(), // European preference
720 /// ];
721 /// println!("Enterprise codec preference: {:?}", enterprise_codecs);
722 /// # }
723 /// ```
724 ///
725 /// # Implementation Notes
726 ///
727 /// This setting will be applied to future call negotiations. Active calls will
728 /// continue using their currently negotiated codecs. The codec names should match
729 /// those returned by `get_available_codecs()`.
730 pub async fn set_preferred_codecs(&self, codec_names: Vec<String>) -> ClientResult<()> {
731 // This would typically configure the session manager with preferred codecs
732 // For now, we'll store it in client configuration metadata
733 tracing::info!("Setting preferred codecs: {:?}", codec_names);
734
735 // TODO: Configure session-core with preferred codec order
736 // self.session_manager.set_preferred_codecs(codec_names).await?;
737
738 Ok(())
739 }
740
741 /// Start audio transmission for a call in pass-through mode (default)
742 ///
743 /// Starts audio transmission for the specified call using the default pass-through mode,
744 /// which allows RTP audio packets to flow between endpoints without automatic audio
745 /// generation. This is the recommended mode for most production use cases.
746 ///
747 /// # Arguments
748 ///
749 /// * `call_id` - The unique identifier of the call to start audio transmission for
750 ///
751 /// # Returns
752 ///
753 /// Returns `Ok(())` on success, or a `ClientError` if:
754 /// - The call is not found
755 /// - The call is not in the Connected state
756 /// - The underlying media session fails to start transmission
757 ///
758 /// # Examples
759 ///
760 /// ```rust
761 /// # use uuid::Uuid;
762 /// # use rvoip_client_core::call::CallId;
763 /// # fn main() {
764 /// // Start audio transmission
765 /// let call_id: CallId = Uuid::new_v4();
766 /// println!("Would start audio transmission for call {}", call_id);
767 /// println!("RTP audio packets would begin flowing");
768 /// # }
769 /// ```
770 ///
771 /// # Side Effects
772 ///
773 /// - Updates call metadata with transmission status and timestamp
774 /// - Emits a `MediaEventType::AudioStarted` event
775 /// - Begins RTP packet transmission through session-core
776 ///
777 /// # State Requirements
778 ///
779 /// The call must be in `Connected` state. Calls that are terminated, failed,
780 /// or cancelled cannot have audio transmission started.
781 pub async fn start_audio_transmission(&self, call_id: &CallId) -> ClientResult<()> {
782 let session_id = self.session_mapping.get(call_id)
783 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
784 .clone();
785
786 // Validate call state
787 if let Some(call_info) = self.call_info.get(call_id) {
788 match call_info.state {
789 crate::call::CallState::Connected => {
790 // OK to start transmission
791 }
792 crate::call::CallState::Terminated |
793 crate::call::CallState::Failed |
794 crate::call::CallState::Cancelled => {
795 return Err(ClientError::InvalidCallState {
796 call_id: *call_id,
797 current_state: call_info.state.clone()
798 });
799 }
800 _ => {
801 return Err(ClientError::InvalidCallStateGeneric {
802 expected: "Connected".to_string(),
803 actual: format!("{:?}", call_info.state)
804 });
805 }
806 }
807 }
808
809 // Use session-core to start audio transmission in pass-through mode
810 MediaControl::start_audio_transmission(&self.coordinator, &session_id)
811 .await
812 .map_err(|e| ClientError::CallSetupFailed {
813 reason: format!("Failed to start audio transmission: {}", e)
814 })?;
815
816 // Update call metadata
817 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
818 call_info.metadata.insert("audio_transmission_active".to_string(), "true".to_string());
819 call_info.metadata.insert("audio_transmission_mode".to_string(), "pass_through".to_string());
820 call_info.metadata.insert("transmission_started_at".to_string(), Utc::now().to_rfc3339());
821 }
822
823 // Emit MediaEvent
824 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
825 let media_event = MediaEventInfo {
826 call_id: *call_id,
827 event_type: crate::events::MediaEventType::AudioStarted,
828 timestamp: Utc::now(),
829 metadata: {
830 let mut metadata = HashMap::new();
831 metadata.insert("session_id".to_string(), session_id.0.clone());
832 metadata.insert("mode".to_string(), "pass_through".to_string());
833 metadata
834 },
835 };
836 handler.on_media_event(media_event).await;
837 }
838
839 tracing::info!("Started audio transmission (pass-through mode) for call {}", call_id);
840 Ok(())
841 }
842
843 /// Start audio transmission for a call with tone generation
844 ///
845 /// Starts audio transmission for the specified call using tone generation mode,
846 /// which generates a 440Hz sine wave for testing purposes. This is useful for
847 /// testing audio connectivity without requiring external audio sources.
848 ///
849 /// # Arguments
850 ///
851 /// * `call_id` - The unique identifier of the call to start audio transmission for
852 ///
853 /// # Returns
854 ///
855 /// Returns `Ok(())` on success, or a `ClientError` if:
856 /// - The call is not found
857 /// - The call is not in the Connected state
858 /// - The underlying media session fails to start transmission
859 ///
860 /// # Examples
861 ///
862 /// ```rust
863 /// # use uuid::Uuid;
864 /// # use rvoip_client_core::call::CallId;
865 /// # fn main() {
866 /// // Start audio transmission with test tone
867 /// let call_id: CallId = Uuid::new_v4();
868 /// println!("Would start 440Hz test tone for call {}", call_id);
869 /// # }
870 /// ```
871 pub async fn start_audio_transmission_with_tone(&self, call_id: &CallId) -> ClientResult<()> {
872 let session_id = self.session_mapping.get(call_id)
873 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
874 .clone();
875
876 // Validate call state
877 self.validate_call_state_for_audio(call_id)?;
878
879 // Use session-core to start audio transmission with tone
880 MediaControl::start_audio_transmission_with_tone(&self.coordinator, &session_id)
881 .await
882 .map_err(|e| ClientError::CallSetupFailed {
883 reason: format!("Failed to start audio transmission with tone: {}", e)
884 })?;
885
886 // Update call metadata
887 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
888 call_info.metadata.insert("audio_transmission_active".to_string(), "true".to_string());
889 call_info.metadata.insert("audio_transmission_mode".to_string(), "tone_generation".to_string());
890 call_info.metadata.insert("transmission_started_at".to_string(), Utc::now().to_rfc3339());
891 }
892
893 // Emit MediaEvent
894 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
895 let media_event = MediaEventInfo {
896 call_id: *call_id,
897 event_type: crate::events::MediaEventType::AudioStarted,
898 timestamp: Utc::now(),
899 metadata: {
900 let mut metadata = HashMap::new();
901 metadata.insert("session_id".to_string(), session_id.0.clone());
902 metadata.insert("mode".to_string(), "tone_generation".to_string());
903 metadata.insert("frequency".to_string(), "440".to_string());
904 metadata
905 },
906 };
907 handler.on_media_event(media_event).await;
908 }
909
910 tracing::info!("Started audio transmission with tone generation for call {}", call_id);
911 Ok(())
912 }
913
914 /// Start audio transmission for a call with custom audio samples
915 ///
916 /// Starts audio transmission for the specified call using custom audio samples.
917 /// The samples must be in G.711 μ-law format (8-bit samples at 8kHz).
918 /// This allows playing back custom audio files or any audio data during the call.
919 ///
920 /// # Arguments
921 ///
922 /// * `call_id` - The unique identifier of the call to start audio transmission for
923 /// * `samples` - The audio samples in G.711 μ-law format
924 /// * `repeat` - Whether to repeat the audio samples when they finish
925 ///
926 /// # Returns
927 ///
928 /// Returns `Ok(())` on success, or a `ClientError` if:
929 /// - The call is not found
930 /// - The call is not in the Connected state
931 /// - The samples vector is empty
932 /// - The underlying media session fails to start transmission
933 ///
934 /// # Examples
935 ///
936 /// ```rust
937 /// # use uuid::Uuid;
938 /// # use rvoip_client_core::call::CallId;
939 /// # fn main() {
940 /// // Start audio transmission with custom audio
941 /// let call_id: CallId = Uuid::new_v4();
942 /// let audio_samples = vec![0x7F, 0x80, 0x7F, 0x80]; // Example μ-law samples
943 /// println!("Would start custom audio transmission for call {} ({} samples)",
944 /// call_id, audio_samples.len());
945 /// # }
946 /// ```
947 pub async fn start_audio_transmission_with_custom_audio(&self, call_id: &CallId, samples: Vec<u8>, repeat: bool) -> ClientResult<()> {
948 let session_id = self.session_mapping.get(call_id)
949 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
950 .clone();
951
952 // Validate call state
953 self.validate_call_state_for_audio(call_id)?;
954
955 // Validate samples
956 if samples.is_empty() {
957 return Err(ClientError::InvalidConfiguration {
958 field: "samples".to_string(),
959 reason: "Audio samples cannot be empty".to_string()
960 });
961 }
962
963 // Use session-core to start audio transmission with custom audio
964 MediaControl::start_audio_transmission_with_custom_audio(&self.coordinator, &session_id, samples.clone(), repeat)
965 .await
966 .map_err(|e| ClientError::CallSetupFailed {
967 reason: format!("Failed to start audio transmission with custom audio: {}", e)
968 })?;
969
970 // Update call metadata
971 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
972 call_info.metadata.insert("audio_transmission_active".to_string(), "true".to_string());
973 call_info.metadata.insert("audio_transmission_mode".to_string(), "custom_audio".to_string());
974 call_info.metadata.insert("custom_audio_samples".to_string(), samples.len().to_string());
975 call_info.metadata.insert("custom_audio_repeat".to_string(), repeat.to_string());
976 call_info.metadata.insert("transmission_started_at".to_string(), Utc::now().to_rfc3339());
977 }
978
979 // Emit MediaEvent
980 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
981 let media_event = MediaEventInfo {
982 call_id: *call_id,
983 event_type: crate::events::MediaEventType::AudioStarted,
984 timestamp: Utc::now(),
985 metadata: {
986 let mut metadata = HashMap::new();
987 metadata.insert("session_id".to_string(), session_id.0.clone());
988 metadata.insert("mode".to_string(), "custom_audio".to_string());
989 metadata.insert("samples_count".to_string(), samples.len().to_string());
990 metadata.insert("repeat".to_string(), repeat.to_string());
991 metadata
992 },
993 };
994 handler.on_media_event(media_event).await;
995 }
996
997 tracing::info!("Started audio transmission with custom audio for call {} ({} samples, repeat: {})",
998 call_id, samples.len(), repeat);
999 Ok(())
1000 }
1001
1002 /// Set custom audio samples for an active transmission session
1003 ///
1004 /// Updates the audio samples for an already active transmission session.
1005 /// This allows changing the audio content during an ongoing call without
1006 /// stopping and restarting the transmission.
1007 ///
1008 /// # Arguments
1009 ///
1010 /// * `call_id` - The unique identifier of the call
1011 /// * `samples` - The new audio samples in G.711 μ-law format
1012 /// * `repeat` - Whether to repeat the audio samples when they finish
1013 ///
1014 /// # Returns
1015 ///
1016 /// Returns `Ok(())` on success, or a `ClientError` if:
1017 /// - The call is not found
1018 /// - Audio transmission is not active for this call
1019 /// - The samples vector is empty
1020 ///
1021 /// # Examples
1022 ///
1023 /// ```rust
1024 /// # use uuid::Uuid;
1025 /// # use rvoip_client_core::call::CallId;
1026 /// # fn main() {
1027 /// // Switch to different audio during call
1028 /// let call_id: CallId = Uuid::new_v4();
1029 /// let new_samples = vec![0x7F, 0x80, 0x7F, 0x80]; // New audio content
1030 /// println!("Would update audio for call {} with {} new samples",
1031 /// call_id, new_samples.len());
1032 /// # }
1033 /// ```
1034 pub async fn set_custom_audio(&self, call_id: &CallId, samples: Vec<u8>, repeat: bool) -> ClientResult<()> {
1035 let session_id = self.session_mapping.get(call_id)
1036 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1037 .clone();
1038
1039 // Validate samples
1040 if samples.is_empty() {
1041 return Err(ClientError::InvalidConfiguration {
1042 field: "samples".to_string(),
1043 reason: "Audio samples cannot be empty".to_string()
1044 });
1045 }
1046
1047 // Check if audio transmission is active
1048 if !self.is_audio_transmission_active(call_id).await? {
1049 return Err(ClientError::InvalidCallStateGeneric {
1050 expected: "Active audio transmission".to_string(),
1051 actual: "Audio transmission not active".to_string()
1052 });
1053 }
1054
1055 // Use session-core to set custom audio
1056 MediaControl::set_custom_audio(&self.coordinator, &session_id, samples.clone(), repeat)
1057 .await
1058 .map_err(|e| ClientError::CallSetupFailed {
1059 reason: format!("Failed to set custom audio: {}", e)
1060 })?;
1061
1062 // Update call metadata
1063 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1064 call_info.metadata.insert("audio_transmission_mode".to_string(), "custom_audio".to_string());
1065 call_info.metadata.insert("custom_audio_samples".to_string(), samples.len().to_string());
1066 call_info.metadata.insert("custom_audio_repeat".to_string(), repeat.to_string());
1067 call_info.metadata.insert("audio_updated_at".to_string(), Utc::now().to_rfc3339());
1068 }
1069
1070 tracing::info!("Set custom audio for call {} ({} samples, repeat: {})",
1071 call_id, samples.len(), repeat);
1072 Ok(())
1073 }
1074
1075 /// Set tone generation parameters for an active transmission session
1076 ///
1077 /// Updates the tone generation parameters for an already active transmission session.
1078 /// This allows changing from custom audio or pass-through mode to tone generation
1079 /// during an ongoing call.
1080 ///
1081 /// # Arguments
1082 ///
1083 /// * `call_id` - The unique identifier of the call
1084 /// * `frequency` - The tone frequency in Hz (e.g., 440.0 for A4)
1085 /// * `amplitude` - The tone amplitude (0.0 to 1.0)
1086 ///
1087 /// # Returns
1088 ///
1089 /// Returns `Ok(())` on success, or a `ClientError` if:
1090 /// - The call is not found
1091 /// - Audio transmission is not active for this call
1092 /// - Invalid frequency or amplitude values
1093 pub async fn set_tone_generation(&self, call_id: &CallId, frequency: f64, amplitude: f64) -> ClientResult<()> {
1094 let session_id = self.session_mapping.get(call_id)
1095 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1096 .clone();
1097
1098 // Validate parameters
1099 if frequency <= 0.0 || frequency > 20000.0 {
1100 return Err(ClientError::InvalidConfiguration {
1101 field: "frequency".to_string(),
1102 reason: "Frequency must be between 0 and 20000 Hz".to_string()
1103 });
1104 }
1105
1106 if amplitude < 0.0 || amplitude > 1.0 {
1107 return Err(ClientError::InvalidConfiguration {
1108 field: "amplitude".to_string(),
1109 reason: "Amplitude must be between 0.0 and 1.0".to_string()
1110 });
1111 }
1112
1113 // Check if audio transmission is active
1114 if !self.is_audio_transmission_active(call_id).await? {
1115 return Err(ClientError::InvalidCallStateGeneric {
1116 expected: "Active audio transmission".to_string(),
1117 actual: "Audio transmission not active".to_string()
1118 });
1119 }
1120
1121 // Use session-core to set tone generation
1122 MediaControl::set_tone_generation(&self.coordinator, &session_id, frequency, amplitude)
1123 .await
1124 .map_err(|e| ClientError::CallSetupFailed {
1125 reason: format!("Failed to set tone generation: {}", e)
1126 })?;
1127
1128 // Update call metadata
1129 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1130 call_info.metadata.insert("audio_transmission_mode".to_string(), "tone_generation".to_string());
1131 call_info.metadata.insert("tone_frequency".to_string(), frequency.to_string());
1132 call_info.metadata.insert("tone_amplitude".to_string(), amplitude.to_string());
1133 call_info.metadata.insert("audio_updated_at".to_string(), Utc::now().to_rfc3339());
1134 }
1135
1136 tracing::info!("Set tone generation for call {} ({}Hz, amplitude: {})",
1137 call_id, frequency, amplitude);
1138 Ok(())
1139 }
1140
1141 /// Enable pass-through mode for an active transmission session
1142 ///
1143 /// Switches an active transmission session to pass-through mode, which stops
1144 /// any audio generation (tones or custom audio) and allows normal RTP audio
1145 /// flow between endpoints.
1146 ///
1147 /// # Arguments
1148 ///
1149 /// * `call_id` - The unique identifier of the call
1150 ///
1151 /// # Returns
1152 ///
1153 /// Returns `Ok(())` on success, or a `ClientError` if:
1154 /// - The call is not found
1155 /// - Audio transmission is not active for this call
1156 pub async fn set_pass_through_mode(&self, call_id: &CallId) -> ClientResult<()> {
1157 let session_id = self.session_mapping.get(call_id)
1158 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1159 .clone();
1160
1161 // Check if audio transmission is active
1162 if !self.is_audio_transmission_active(call_id).await? {
1163 return Err(ClientError::InvalidCallStateGeneric {
1164 expected: "Active audio transmission".to_string(),
1165 actual: "Audio transmission not active".to_string()
1166 });
1167 }
1168
1169 // Use session-core to set pass-through mode
1170 MediaControl::set_pass_through_mode(&self.coordinator, &session_id)
1171 .await
1172 .map_err(|e| ClientError::CallSetupFailed {
1173 reason: format!("Failed to set pass-through mode: {}", e)
1174 })?;
1175
1176 // Update call metadata
1177 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1178 call_info.metadata.insert("audio_transmission_mode".to_string(), "pass_through".to_string());
1179 call_info.metadata.insert("audio_updated_at".to_string(), Utc::now().to_rfc3339());
1180 }
1181
1182 tracing::info!("Set pass-through mode for call {}", call_id);
1183 Ok(())
1184 }
1185
1186 /// Helper method to validate call state for audio operations
1187 fn validate_call_state_for_audio(&self, call_id: &CallId) -> ClientResult<()> {
1188 if let Some(call_info) = self.call_info.get(call_id) {
1189 match call_info.state {
1190 crate::call::CallState::Connected => Ok(()),
1191 crate::call::CallState::Terminated |
1192 crate::call::CallState::Failed |
1193 crate::call::CallState::Cancelled => {
1194 Err(ClientError::InvalidCallState {
1195 call_id: *call_id,
1196 current_state: call_info.state.clone()
1197 })
1198 }
1199 _ => {
1200 Err(ClientError::InvalidCallStateGeneric {
1201 expected: "Connected".to_string(),
1202 actual: format!("{:?}", call_info.state)
1203 })
1204 }
1205 }
1206 } else {
1207 Err(ClientError::CallNotFound { call_id: *call_id })
1208 }
1209 }
1210
1211 /// Stop audio transmission for a call
1212 ///
1213 /// Stops audio transmission for the specified call, halting the flow of
1214 /// RTP audio packets between the local client and the remote endpoint. This
1215 /// is typically used when putting a call on hold or during call termination.
1216 ///
1217 /// # Arguments
1218 ///
1219 /// * `call_id` - The unique identifier of the call to stop audio transmission for
1220 ///
1221 /// # Returns
1222 ///
1223 /// Returns `Ok(())` on success, or a `ClientError` if:
1224 /// - The call is not found
1225 /// - The underlying media session fails to stop transmission
1226 ///
1227 /// # Examples
1228 ///
1229 /// ```rust
1230 /// # use uuid::Uuid;
1231 /// # use rvoip_client_core::call::CallId;
1232 /// # fn main() {
1233 /// // Stop audio transmission
1234 /// let call_id: CallId = Uuid::new_v4();
1235 /// println!("Would stop audio transmission for call {}", call_id);
1236 /// println!("RTP audio packets would stop flowing");
1237 /// # }
1238 /// ```
1239 ///
1240 /// ```rust
1241 /// # use uuid::Uuid;
1242 /// # use rvoip_client_core::call::CallId;
1243 /// # fn main() {
1244 /// // Put call on hold
1245 /// let call_id: CallId = Uuid::new_v4();
1246 /// println!("Putting call {} on hold", call_id);
1247 /// println!("Audio transmission would be stopped");
1248 /// # }
1249 /// ```
1250 ///
1251 /// ```rust
1252 /// # use uuid::Uuid;
1253 /// # use rvoip_client_core::call::CallId;
1254 /// # fn main() {
1255 /// // Emergency stop
1256 /// let call_id: CallId = Uuid::new_v4();
1257 /// println!("Emergency stop of audio for call {}", call_id);
1258 /// println!("Immediate halt of RTP transmission");
1259 /// # }
1260 /// ```
1261 ///
1262 /// # Side Effects
1263 ///
1264 /// - Updates call metadata with transmission status and timestamp
1265 /// - Emits a `MediaEventType::AudioStopped` event
1266 /// - Stops RTP packet transmission through session-core
1267 ///
1268 /// # Use Cases
1269 ///
1270 /// - Putting calls on hold
1271 /// - Call termination procedures
1272 /// - Emergency audio cutoff
1273 /// - Bandwidth conservation
1274 pub async fn stop_audio_transmission(&self, call_id: &CallId) -> ClientResult<()> {
1275 let session_id = self.session_mapping.get(call_id)
1276 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1277 .clone();
1278
1279 // Use session-core to stop audio transmission
1280 MediaControl::stop_audio_transmission(&self.coordinator, &session_id)
1281 .await
1282 .map_err(|e| ClientError::CallSetupFailed {
1283 reason: format!("Failed to stop audio transmission: {}", e)
1284 })?;
1285
1286 // Update call metadata
1287 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1288 call_info.metadata.insert("audio_transmission_active".to_string(), "false".to_string());
1289 call_info.metadata.insert("transmission_stopped_at".to_string(), Utc::now().to_rfc3339());
1290 }
1291
1292 // Emit MediaEvent
1293 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
1294 let media_event = MediaEventInfo {
1295 call_id: *call_id,
1296 event_type: crate::events::MediaEventType::AudioStopped,
1297 timestamp: Utc::now(),
1298 metadata: {
1299 let mut metadata = HashMap::new();
1300 metadata.insert("session_id".to_string(), session_id.0.clone());
1301 metadata
1302 },
1303 };
1304 handler.on_media_event(media_event).await;
1305 }
1306
1307 tracing::info!("Stopped audio transmission for call {}", call_id);
1308 Ok(())
1309 }
1310
1311 /// Check if audio transmission is active for a call
1312 ///
1313 /// Determines whether audio transmission is currently active for the specified call.
1314 /// This status is tracked in the call's metadata and reflects the current state
1315 /// of RTP audio packet transmission.
1316 ///
1317 /// # Arguments
1318 ///
1319 /// * `call_id` - The unique identifier of the call to check
1320 ///
1321 /// # Returns
1322 ///
1323 /// Returns `Ok(true)` if audio transmission is active, `Ok(false)` if inactive,
1324 /// or `ClientError::CallNotFound` if the call doesn't exist.
1325 ///
1326 /// # Examples
1327 ///
1328 /// ```rust
1329 /// # use uuid::Uuid;
1330 /// # use rvoip_client_core::call::CallId;
1331 /// # fn main() {
1332 /// // Check transmission status
1333 /// let call_id: CallId = Uuid::new_v4();
1334 /// let is_active = true; // Simulated state
1335 ///
1336 /// if is_active {
1337 /// println!("Call {} has active audio transmission", call_id);
1338 /// } else {
1339 /// println!("Call {} audio transmission is stopped", call_id);
1340 /// }
1341 /// # }
1342 /// ```
1343 ///
1344 /// ```rust
1345 /// # use uuid::Uuid;
1346 /// # use rvoip_client_core::call::CallId;
1347 /// # fn main() {
1348 /// // Conditional UI display
1349 /// let call_id: CallId = Uuid::new_v4();
1350 /// let transmission_active = false; // Simulated
1351 ///
1352 /// let status_icon = if transmission_active { "🔊" } else { "⏸️" };
1353 /// println!("Audio status for call {}: {}", call_id, status_icon);
1354 /// # }
1355 /// ```
1356 pub async fn is_audio_transmission_active(&self, call_id: &CallId) -> ClientResult<bool> {
1357 if let Some(call_info) = self.call_info.get(call_id) {
1358 let active = call_info.metadata.get("audio_transmission_active")
1359 .map(|s| s == "true")
1360 .unwrap_or(false);
1361 Ok(active)
1362 } else {
1363 Err(ClientError::CallNotFound { call_id: *call_id })
1364 }
1365 }
1366
1367 /// Update call media configuration with new SDP
1368 ///
1369 /// Updates the media configuration for an existing call using a new Session Description
1370 /// Protocol (SDP) description. This is typically used for handling re-INVITE scenarios,
1371 /// media parameter changes, or call modifications during an active session.
1372 ///
1373 /// # Arguments
1374 ///
1375 /// * `call_id` - The unique identifier of the call to update
1376 /// * `new_sdp` - The new SDP description to apply
1377 ///
1378 /// # Returns
1379 ///
1380 /// Returns `Ok(())` on success, or a `ClientError` if:
1381 /// - The call is not found
1382 /// - The SDP is empty or invalid
1383 /// - The session-core fails to apply the media update
1384 ///
1385 /// # Examples
1386 ///
1387 /// ```rust
1388 /// # use uuid::Uuid;
1389 /// # use rvoip_client_core::call::CallId;
1390 /// # fn main() {
1391 /// // Update media configuration
1392 /// let call_id: CallId = Uuid::new_v4();
1393 /// let new_sdp = "v=0\r\no=example 123 456 IN IP4 192.168.1.1\r\n";
1394 ///
1395 /// println!("Would update media for call {} with new SDP", call_id);
1396 /// println!("SDP length: {} bytes", new_sdp.len());
1397 /// # }
1398 /// ```
1399 ///
1400 /// ```rust
1401 /// # use uuid::Uuid;
1402 /// # use rvoip_client_core::call::CallId;
1403 /// # fn main() {
1404 /// // Re-INVITE scenario
1405 /// let call_id: CallId = Uuid::new_v4();
1406 /// println!("Processing re-INVITE for call {}", call_id);
1407 /// println!("Would update call media parameters");
1408 /// # }
1409 /// ```
1410 ///
1411 /// # Use Cases
1412 ///
1413 /// - Handling SIP re-INVITE messages
1414 /// - Updating codec parameters mid-call
1415 /// - Changing media endpoints
1416 /// - Modifying bandwidth allocations
1417 pub async fn update_call_media(&self, call_id: &CallId, new_sdp: &str) -> ClientResult<()> {
1418 let session_id = self.session_mapping.get(call_id)
1419 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1420 .clone();
1421
1422 // Validate SDP
1423 if new_sdp.trim().is_empty() {
1424 return Err(ClientError::InvalidConfiguration {
1425 field: "new_sdp".to_string(),
1426 reason: "SDP cannot be empty".to_string()
1427 });
1428 }
1429
1430 // Use session-core to update media
1431 SessionControl::update_media(&self.coordinator, &session_id, new_sdp)
1432 .await
1433 .map_err(|e| ClientError::InternalError {
1434 message: format!("Failed to update call media: {}", e)
1435 })?;
1436
1437 tracing::info!("Updated media for call {}", call_id);
1438 Ok(())
1439 }
1440
1441 /// Get comprehensive media capabilities of the client
1442 ///
1443 /// Returns a detailed description of the media capabilities supported by this client,
1444 /// including available codecs, supported features, and operational limits. This information
1445 /// is useful for capability negotiation, feature detection, and system configuration.
1446 ///
1447 /// # Returns
1448 ///
1449 /// Returns a `MediaCapabilities` struct containing:
1450 /// - List of supported audio codecs with full details
1451 /// - Feature support flags (hold, mute, DTMF, transfer)
1452 /// - Protocol support indicators (SDP, RTP, RTCP)
1453 /// - Operational limits (max concurrent calls)
1454 /// - Supported media types
1455 ///
1456 /// # Examples
1457 ///
1458 /// ```rust
1459 /// # use rvoip_client_core::client::types::{MediaCapabilities, AudioCodecInfo};
1460 /// # fn main() {
1461 /// // Check client capabilities
1462 /// let capabilities = MediaCapabilities {
1463 /// supported_codecs: vec![
1464 /// AudioCodecInfo {
1465 /// name: "OPUS".to_string(),
1466 /// payload_type: 111,
1467 /// clock_rate: 48000,
1468 /// channels: 2,
1469 /// description: "High quality".to_string(),
1470 /// quality_rating: 5,
1471 /// }
1472 /// ],
1473 /// can_hold: true,
1474 /// can_mute_microphone: true,
1475 /// can_mute_speaker: true,
1476 /// can_send_dtmf: true,
1477 /// can_transfer: true,
1478 /// supports_sdp_offer_answer: true,
1479 /// supports_rtp: true,
1480 /// supports_rtcp: true,
1481 /// max_concurrent_calls: 10,
1482 /// supported_media_types: vec!["audio".to_string()],
1483 /// };
1484 ///
1485 /// println!("Client supports {} codecs", capabilities.supported_codecs.len());
1486 /// println!("Max concurrent calls: {}", capabilities.max_concurrent_calls);
1487 /// # }
1488 /// ```
1489 ///
1490 /// ```rust
1491 /// # fn main() {
1492 /// // Feature detection
1493 /// let can_hold = true;
1494 /// let can_transfer = true;
1495 ///
1496 /// if can_hold && can_transfer {
1497 /// println!("Advanced call control features available");
1498 /// }
1499 /// # }
1500 /// ```
1501 ///
1502 /// ```rust
1503 /// # fn main() {
1504 /// // Protocol support check
1505 /// let supports_rtp = true;
1506 /// let supports_rtcp = true;
1507 ///
1508 /// match (supports_rtp, supports_rtcp) {
1509 /// (true, true) => println!("Full RTP/RTCP support"),
1510 /// (true, false) => println!("RTP only support"),
1511 /// _ => println!("Limited media support"),
1512 /// }
1513 /// # }
1514 /// ```
1515 ///
1516 /// # Use Cases
1517 ///
1518 /// - Client capability advertisement
1519 /// - Feature availability checking before operations
1520 /// - System configuration and limits planning
1521 /// - Interoperability assessment
1522 pub async fn get_media_capabilities(&self) -> MediaCapabilities {
1523 MediaCapabilities {
1524 supported_codecs: self.get_available_codecs().await,
1525 can_hold: true,
1526 can_mute_microphone: true,
1527 can_mute_speaker: true,
1528 can_send_dtmf: true,
1529 can_transfer: true,
1530 supports_sdp_offer_answer: true,
1531 supports_rtp: true,
1532 supports_rtcp: true,
1533 max_concurrent_calls: 10, // TODO: Make configurable
1534 supported_media_types: vec!["audio".to_string()], // TODO: Add video support
1535 }
1536 }
1537
1538 /// Helper method to determine audio direction from MediaInfo
1539 async fn determine_audio_direction(&self, media_info: &rvoip_session_core::api::types::MediaInfo) -> crate::client::types::AudioDirection {
1540 // Simple heuristic based on SDP content
1541 if let (Some(local_sdp), Some(remote_sdp)) = (&media_info.local_sdp, &media_info.remote_sdp) {
1542 let local_sendrecv = local_sdp.contains("sendrecv") || (!local_sdp.contains("sendonly") && !local_sdp.contains("recvonly"));
1543 let remote_sendrecv = remote_sdp.contains("sendrecv") || (!remote_sdp.contains("sendonly") && !remote_sdp.contains("recvonly"));
1544
1545 match (local_sendrecv, remote_sendrecv) {
1546 (true, true) => crate::client::types::AudioDirection::SendReceive,
1547 (true, false) => {
1548 if remote_sdp.contains("sendonly") {
1549 crate::client::types::AudioDirection::ReceiveOnly
1550 } else {
1551 crate::client::types::AudioDirection::SendOnly
1552 }
1553 }
1554 (false, true) => {
1555 if local_sdp.contains("sendonly") {
1556 crate::client::types::AudioDirection::SendOnly
1557 } else {
1558 crate::client::types::AudioDirection::ReceiveOnly
1559 }
1560 }
1561 (false, false) => crate::client::types::AudioDirection::Inactive,
1562 }
1563 } else {
1564 crate::client::types::AudioDirection::SendReceive // Default assumption
1565 }
1566 }
1567
1568 // ===== PRIORITY 4.2: MEDIA SESSION COORDINATION =====
1569
1570 /// Generate SDP offer for a call using session-core
1571 ///
1572 /// Creates a Session Description Protocol (SDP) offer for the specified call, which
1573 /// describes the media capabilities and parameters that this client is willing to
1574 /// negotiate. The offer includes codec preferences, RTP port assignments, and other
1575 /// media configuration details required for establishing the call.
1576 ///
1577 /// # Arguments
1578 ///
1579 /// * `call_id` - The unique identifier of the call to generate an SDP offer for
1580 ///
1581 /// # Returns
1582 ///
1583 /// Returns the SDP offer as a string, or a `ClientError` if:
1584 /// - The call is not found
1585 /// - The call is not in an appropriate state (must be Initiating or Connected)
1586 /// - The underlying session-core fails to generate the SDP
1587 ///
1588 /// # Examples
1589 ///
1590 /// ```rust
1591 /// # use uuid::Uuid;
1592 /// # use rvoip_client_core::call::CallId;
1593 /// # fn main() {
1594 /// // Generate SDP offer for outgoing call
1595 /// let call_id: CallId = Uuid::new_v4();
1596 /// println!("Would generate SDP offer for call {}", call_id);
1597 ///
1598 /// // Example SDP structure
1599 /// let sdp_example = "v=0\r\no=- 123456 654321 IN IP4 192.168.1.1\r\n";
1600 /// println!("SDP offer would be {} bytes", sdp_example.len());
1601 /// # }
1602 /// ```
1603 ///
1604 /// ```rust
1605 /// # use uuid::Uuid;
1606 /// # use rvoip_client_core::call::CallId;
1607 /// # fn main() {
1608 /// // SIP call flow context
1609 /// let call_id: CallId = Uuid::new_v4();
1610 /// println!("Generating SDP offer for INVITE to call {}", call_id);
1611 /// println!("This SDP will be included in the SIP INVITE message");
1612 /// # }
1613 /// ```
1614 ///
1615 /// # Side Effects
1616 ///
1617 /// - Updates call metadata with the generated SDP offer and timestamp
1618 /// - Emits a `MediaEventType::SdpOfferGenerated` event
1619 /// - Coordinates with session-core for media session setup
1620 ///
1621 /// # Use Cases
1622 ///
1623 /// - Initiating outbound calls
1624 /// - Re-INVITE scenarios for call modifications
1625 /// - Media renegotiation during active calls
1626 pub async fn generate_sdp_offer(&self, call_id: &CallId) -> ClientResult<String> {
1627 let session_id = self.session_mapping.get(call_id)
1628 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1629 .clone();
1630
1631 // Validate call state
1632 if let Some(call_info) = self.call_info.get(call_id) {
1633 match call_info.state {
1634 crate::call::CallState::Initiating |
1635 crate::call::CallState::Connected => {
1636 // OK to generate offer
1637 }
1638 crate::call::CallState::Terminated |
1639 crate::call::CallState::Failed |
1640 crate::call::CallState::Cancelled => {
1641 return Err(ClientError::InvalidCallState {
1642 call_id: *call_id,
1643 current_state: call_info.state.clone()
1644 });
1645 }
1646 _ => {
1647 return Err(ClientError::InvalidCallStateGeneric {
1648 expected: "Initiating or Connected".to_string(),
1649 actual: format!("{:?}", call_info.state)
1650 });
1651 }
1652 }
1653 }
1654
1655 // Use session-core SDP generation
1656 let sdp_offer = MediaControl::generate_sdp_offer(&self.coordinator, &session_id)
1657 .await
1658 .map_err(|e| ClientError::InternalError {
1659 message: format!("Failed to generate SDP offer: {}", e)
1660 })?;
1661
1662 // Update call metadata
1663 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1664 call_info.metadata.insert("last_sdp_offer".to_string(), sdp_offer.clone());
1665 call_info.metadata.insert("sdp_offer_generated_at".to_string(), Utc::now().to_rfc3339());
1666 }
1667
1668 // Emit MediaEvent
1669 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
1670 let media_event = MediaEventInfo {
1671 call_id: *call_id,
1672 event_type: crate::events::MediaEventType::SdpOfferGenerated { sdp_size: sdp_offer.len() },
1673 timestamp: Utc::now(),
1674 metadata: {
1675 let mut metadata = HashMap::new();
1676 metadata.insert("session_id".to_string(), session_id.0.clone());
1677 metadata
1678 },
1679 };
1680 handler.on_media_event(media_event).await;
1681 }
1682
1683 tracing::info!("Generated SDP offer for call {}: {} bytes", call_id, sdp_offer.len());
1684 Ok(sdp_offer)
1685 }
1686
1687 /// Process SDP answer for a call using session-core
1688 ///
1689 /// Processes a Session Description Protocol (SDP) answer received from the remote party,
1690 /// completing the media negotiation process. This function validates the SDP answer,
1691 /// updates the media session parameters, and establishes the agreed-upon media flow
1692 /// configuration for the call.
1693 ///
1694 /// # Arguments
1695 ///
1696 /// * `call_id` - The unique identifier of the call to process the SDP answer for
1697 /// * `sdp_answer` - The SDP answer string received from the remote party
1698 ///
1699 /// # Returns
1700 ///
1701 /// Returns `Ok(())` on successful processing, or a `ClientError` if:
1702 /// - The call is not found
1703 /// - The SDP answer is empty or malformed
1704 /// - The underlying session-core fails to process the SDP
1705 ///
1706 /// # Examples
1707 ///
1708 /// ```rust
1709 /// # use uuid::Uuid;
1710 /// # use rvoip_client_core::call::CallId;
1711 /// # fn main() {
1712 /// // Process SDP answer from 200 OK response
1713 /// let call_id: CallId = Uuid::new_v4();
1714 /// let sdp_answer = "v=0\r\no=remote 456789 987654 IN IP4 192.168.1.2\r\n";
1715 ///
1716 /// println!("Would process SDP answer for call {}", call_id);
1717 /// println!("SDP answer size: {} bytes", sdp_answer.len());
1718 /// # }
1719 /// ```
1720 ///
1721 /// ```rust
1722 /// # use uuid::Uuid;
1723 /// # use rvoip_client_core::call::CallId;
1724 /// # fn main() {
1725 /// // Media negotiation completion
1726 /// let call_id: CallId = Uuid::new_v4();
1727 /// println!("Completing media negotiation for call {}", call_id);
1728 /// println!("Would establish RTP flow based on negotiated parameters");
1729 /// # }
1730 /// ```
1731 ///
1732 /// # Side Effects
1733 ///
1734 /// - Updates call metadata with the processed SDP answer and timestamp
1735 /// - Emits a `MediaEventType::SdpAnswerProcessed` event
1736 /// - Establishes media flow parameters with session-core
1737 /// - Enables RTP packet transmission/reception
1738 ///
1739 /// # Use Cases
1740 ///
1741 /// - Processing 200 OK responses to INVITE requests
1742 /// - Handling SDP answers in re-INVITE scenarios
1743 /// - Completing media renegotiation processes
1744 pub async fn process_sdp_answer(&self, call_id: &CallId, sdp_answer: &str) -> ClientResult<()> {
1745 // Validate SDP answer is not empty first
1746 if sdp_answer.trim().is_empty() {
1747 return Err(ClientError::InvalidConfiguration {
1748 field: "sdp_answer".to_string(),
1749 reason: "SDP answer cannot be empty".to_string()
1750 });
1751 }
1752
1753 let session_id = self.session_mapping.get(call_id)
1754 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1755 .clone();
1756
1757 // Use session-core SDP processing
1758 MediaControl::update_remote_sdp(&self.coordinator, &session_id, sdp_answer)
1759 .await
1760 .map_err(|e| ClientError::InternalError {
1761 message: format!("Failed to process SDP answer: {}", e)
1762 })?;
1763
1764 // Update call metadata
1765 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1766 call_info.metadata.insert("last_sdp_answer".to_string(), sdp_answer.to_string());
1767 call_info.metadata.insert("sdp_answer_processed_at".to_string(), Utc::now().to_rfc3339());
1768 }
1769
1770 // Emit MediaEvent
1771 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
1772 let media_event = MediaEventInfo {
1773 call_id: *call_id,
1774 event_type: crate::events::MediaEventType::SdpAnswerProcessed { sdp_size: sdp_answer.len() },
1775 timestamp: Utc::now(),
1776 metadata: {
1777 let mut metadata = HashMap::new();
1778 metadata.insert("session_id".to_string(), session_id.0.clone());
1779 metadata
1780 },
1781 };
1782 handler.on_media_event(media_event).await;
1783 }
1784
1785 tracing::info!("Processed SDP answer for call {}: {} bytes", call_id, sdp_answer.len());
1786 Ok(())
1787 }
1788
1789 /// Stop media session for a call
1790 ///
1791 /// Terminates the media session for the specified call, stopping all audio transmission
1792 /// and reception. This function cleanly shuts down the RTP flows, releases media resources,
1793 /// and updates the call state to reflect that media is no longer active.
1794 ///
1795 /// # Arguments
1796 ///
1797 /// * `call_id` - The unique identifier of the call to stop media session for
1798 ///
1799 /// # Returns
1800 ///
1801 /// Returns `Ok(())` on successful termination, or a `ClientError` if:
1802 /// - The call is not found
1803 /// - The underlying media session fails to stop cleanly
1804 ///
1805 /// # Examples
1806 ///
1807 /// ```rust
1808 /// # use uuid::Uuid;
1809 /// # use rvoip_client_core::call::CallId;
1810 /// # fn main() {
1811 /// // Stop media session during call termination
1812 /// let call_id: CallId = Uuid::new_v4();
1813 /// println!("Would stop media session for call {}", call_id);
1814 /// println!("RTP flows would be terminated");
1815 /// # }
1816 /// ```
1817 ///
1818 /// ```rust
1819 /// # use uuid::Uuid;
1820 /// # use rvoip_client_core::call::CallId;
1821 /// # fn main() {
1822 /// // Cleanup during error handling
1823 /// let call_id: CallId = Uuid::new_v4();
1824 /// println!("Emergency media session cleanup for call {}", call_id);
1825 /// println!("Would release all media resources");
1826 /// # }
1827 /// ```
1828 ///
1829 /// # Side Effects
1830 ///
1831 /// - Updates call metadata to mark media session as inactive
1832 /// - Emits a `MediaEventType::MediaSessionStopped` event
1833 /// - Releases RTP ports and media resources
1834 /// - Stops all audio transmission and reception
1835 ///
1836 /// # Use Cases
1837 ///
1838 /// - Call termination procedures
1839 /// - Error recovery and cleanup
1840 /// - Media session reinitiation prep
1841 pub async fn stop_media_session(&self, call_id: &CallId) -> ClientResult<()> {
1842 let session_id = self.session_mapping.get(call_id)
1843 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1844 .clone();
1845
1846 // Stop audio transmission first
1847 MediaControl::stop_audio_transmission(&self.coordinator, &session_id)
1848 .await
1849 .map_err(|e| ClientError::InternalError {
1850 message: format!("Failed to stop media session: {}", e)
1851 })?;
1852
1853 // Update call metadata
1854 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1855 call_info.metadata.insert("media_session_active".to_string(), "false".to_string());
1856 call_info.metadata.insert("media_session_stopped_at".to_string(), Utc::now().to_rfc3339());
1857 }
1858
1859 // Emit MediaEvent
1860 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
1861 let media_event = MediaEventInfo {
1862 call_id: *call_id,
1863 event_type: crate::events::MediaEventType::MediaSessionStopped,
1864 timestamp: Utc::now(),
1865 metadata: {
1866 let mut metadata = HashMap::new();
1867 metadata.insert("session_id".to_string(), session_id.0.clone());
1868 metadata
1869 },
1870 };
1871 handler.on_media_event(media_event).await;
1872 }
1873
1874 tracing::info!("Stopped media session for call {}", call_id);
1875 Ok(())
1876 }
1877
1878 /// Start media session for a call
1879 ///
1880 /// Initiates a new media session for the specified call, creating the necessary RTP flows
1881 /// and establishing audio transmission capabilities. This function coordinates with
1882 /// session-core to set up media parameters and returns detailed information about
1883 /// the created media session.
1884 ///
1885 /// # Arguments
1886 ///
1887 /// * `call_id` - The unique identifier of the call to start media session for
1888 ///
1889 /// # Returns
1890 ///
1891 /// Returns `MediaSessionInfo` containing detailed session information, or a `ClientError` if:
1892 /// - The call is not found
1893 /// - The call is not in Connected state
1894 /// - The underlying session-core fails to create the media session
1895 /// - Media information cannot be retrieved after session creation
1896 ///
1897 /// # Examples
1898 ///
1899 /// ```rust
1900 /// # use uuid::Uuid;
1901 /// # use rvoip_client_core::call::CallId;
1902 /// # use rvoip_client_core::client::types::{MediaSessionInfo, AudioDirection};
1903 /// # use chrono::Utc;
1904 /// # fn main() {
1905 /// // Start media session for connected call
1906 /// let call_id: CallId = Uuid::new_v4();
1907 /// println!("Would start media session for call {}", call_id);
1908 ///
1909 /// // Example session info
1910 /// let session_info = MediaSessionInfo {
1911 /// call_id,
1912 /// session_id: rvoip_session_core::api::SessionId("session-123".to_string()),
1913 /// media_session_id: "media-123".to_string(),
1914 /// local_rtp_port: Some(12000),
1915 /// remote_rtp_port: Some(12001),
1916 /// codec: Some("OPUS".to_string()),
1917 /// media_direction: AudioDirection::SendReceive,
1918 /// quality_metrics: None,
1919 /// is_active: true,
1920 /// created_at: Utc::now(),
1921 /// };
1922 ///
1923 /// println!("Media session {} created on port {}",
1924 /// session_info.media_session_id,
1925 /// session_info.local_rtp_port.unwrap_or(0));
1926 /// # }
1927 /// ```
1928 ///
1929 /// ```rust
1930 /// # use uuid::Uuid;
1931 /// # use rvoip_client_core::call::CallId;
1932 /// # fn main() {
1933 /// // Enterprise call setup
1934 /// let call_id: CallId = Uuid::new_v4();
1935 /// println!("Establishing enterprise media session for call {}", call_id);
1936 /// println!("Would configure high-quality codecs and QoS parameters");
1937 /// # }
1938 /// ```
1939 ///
1940 /// # Side Effects
1941 ///
1942 /// - Creates a new media session in session-core
1943 /// - Updates call metadata with media session details
1944 /// - Emits a `MediaEventType::MediaSessionStarted` event
1945 /// - Allocates RTP ports and resources
1946 ///
1947 /// # State Requirements
1948 ///
1949 /// The call must be in `Connected` state before starting a media session.
1950 pub async fn start_media_session(&self, call_id: &CallId) -> ClientResult<MediaSessionInfo> {
1951 let session_id = self.session_mapping.get(call_id)
1952 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1953 .clone();
1954
1955 // Validate call state
1956 if let Some(call_info) = self.call_info.get(call_id) {
1957 match call_info.state {
1958 crate::call::CallState::Connected => {
1959 // OK to start media
1960 }
1961 _ => {
1962 return Err(ClientError::InvalidCallStateGeneric {
1963 expected: "Connected".to_string(),
1964 actual: format!("{:?}", call_info.state)
1965 });
1966 }
1967 }
1968 }
1969
1970 // Create media session using session-core
1971 MediaControl::create_media_session(&self.coordinator, &session_id)
1972 .await
1973 .map_err(|e| ClientError::InternalError {
1974 message: format!("Failed to start media session: {}", e)
1975 })?;
1976
1977 // Get media info to create MediaSessionInfo
1978 let media_info = MediaControl::get_media_info(&self.coordinator, &session_id)
1979 .await
1980 .map_err(|e| ClientError::InternalError {
1981 message: format!("Failed to get media info: {}", e)
1982 })?
1983 .ok_or_else(|| ClientError::InternalError {
1984 message: "No media info available".to_string()
1985 })?;
1986
1987 let media_session_id = format!("media-{}", session_id.0);
1988 let audio_direction = self.determine_audio_direction(&media_info).await;
1989
1990 let client_media_info = MediaSessionInfo {
1991 call_id: *call_id,
1992 session_id: session_id.clone(),
1993 media_session_id: media_session_id.clone(),
1994 local_rtp_port: media_info.local_rtp_port,
1995 remote_rtp_port: media_info.remote_rtp_port,
1996 codec: media_info.codec,
1997 media_direction: audio_direction,
1998 quality_metrics: None, // TODO: Extract quality metrics
1999 is_active: true,
2000 created_at: Utc::now(),
2001 };
2002
2003 // Update call metadata
2004 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
2005 call_info.metadata.insert("media_session_active".to_string(), "true".to_string());
2006 call_info.metadata.insert("media_session_id".to_string(), media_session_id.clone());
2007 call_info.metadata.insert("media_session_started_at".to_string(), Utc::now().to_rfc3339());
2008 }
2009
2010 // Emit MediaEvent
2011 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
2012 let media_event = MediaEventInfo {
2013 call_id: *call_id,
2014 event_type: crate::events::MediaEventType::MediaSessionStarted {
2015 media_session_id: media_session_id.clone()
2016 },
2017 timestamp: Utc::now(),
2018 metadata: {
2019 let mut metadata = HashMap::new();
2020 metadata.insert("session_id".to_string(), session_id.0.clone());
2021 metadata
2022 },
2023 };
2024 handler.on_media_event(media_event).await;
2025 }
2026
2027 tracing::info!("Started media session for call {}: media_session_id={}",
2028 call_id, media_session_id);
2029 Ok(client_media_info)
2030 }
2031
2032 /// Check if media session is active for a call
2033 ///
2034 /// Determines whether a media session is currently active for the specified call.
2035 /// A media session is considered active if it has been started and not yet stopped,
2036 /// meaning RTP flows are established and audio can be transmitted/received.
2037 ///
2038 /// # Arguments
2039 ///
2040 /// * `call_id` - The unique identifier of the call to check
2041 ///
2042 /// # Returns
2043 ///
2044 /// Returns `Ok(true)` if media session is active, `Ok(false)` if inactive,
2045 /// or `ClientError::CallNotFound` if the call doesn't exist.
2046 ///
2047 /// # Examples
2048 ///
2049 /// ```rust
2050 /// # use uuid::Uuid;
2051 /// # use rvoip_client_core::call::CallId;
2052 /// # fn main() {
2053 /// // Check media session status
2054 /// let call_id: CallId = Uuid::new_v4();
2055 /// let is_active = true; // Simulated state
2056 ///
2057 /// if is_active {
2058 /// println!("Call {} has active media session", call_id);
2059 /// } else {
2060 /// println!("Call {} media session is inactive", call_id);
2061 /// }
2062 /// # }
2063 /// ```
2064 ///
2065 /// ```rust
2066 /// # use uuid::Uuid;
2067 /// # use rvoip_client_core::call::CallId;
2068 /// # fn main() {
2069 /// // Conditional media operations
2070 /// let call_id: CallId = Uuid::new_v4();
2071 /// let media_active = false; // Simulated
2072 ///
2073 /// if !media_active {
2074 /// println!("Need to start media session for call {}", call_id);
2075 /// }
2076 /// # }
2077 /// ```
2078 pub async fn is_media_session_active(&self, call_id: &CallId) -> ClientResult<bool> {
2079 if let Some(call_info) = self.call_info.get(call_id) {
2080 let active = call_info.metadata.get("media_session_active")
2081 .map(|s| s == "true")
2082 .unwrap_or(false);
2083 Ok(active)
2084 } else {
2085 Err(ClientError::CallNotFound { call_id: *call_id })
2086 }
2087 }
2088
2089 /// Get detailed media session information for a call
2090 ///
2091 /// Retrieves comprehensive information about the media session for the specified call,
2092 /// including session identifiers, RTP port assignments, codec details, media direction,
2093 /// and session timestamps. Returns `None` if no active media session exists.
2094 ///
2095 /// # Arguments
2096 ///
2097 /// * `call_id` - The unique identifier of the call to query
2098 ///
2099 /// # Returns
2100 ///
2101 /// Returns `Ok(Some(MediaSessionInfo))` with session details if active,
2102 /// `Ok(None)` if no media session is active, or `ClientError` if the call
2103 /// is not found or media information cannot be retrieved.
2104 ///
2105 /// # Examples
2106 ///
2107 /// ```rust
2108 /// # use uuid::Uuid;
2109 /// # use rvoip_client_core::call::CallId;
2110 /// # use rvoip_client_core::client::types::{MediaSessionInfo, AudioDirection};
2111 /// # use chrono::Utc;
2112 /// # fn main() {
2113 /// // Get current media session info
2114 /// let call_id: CallId = Uuid::new_v4();
2115 /// println!("Would get media session info for call {}", call_id);
2116 ///
2117 /// // Example session info structure
2118 /// let session_info = Some(MediaSessionInfo {
2119 /// call_id,
2120 /// session_id: rvoip_session_core::api::SessionId("session-456".to_string()),
2121 /// media_session_id: "media-456".to_string(),
2122 /// local_rtp_port: Some(13000),
2123 /// remote_rtp_port: Some(13001),
2124 /// codec: Some("G722".to_string()),
2125 /// media_direction: AudioDirection::SendReceive,
2126 /// quality_metrics: None,
2127 /// is_active: true,
2128 /// created_at: Utc::now(),
2129 /// });
2130 ///
2131 /// match session_info {
2132 /// Some(info) => println!("Active session: {} using codec {}",
2133 /// info.media_session_id,
2134 /// info.codec.unwrap_or("Unknown".to_string())),
2135 /// None => println!("No active media session"),
2136 /// }
2137 /// # }
2138 /// ```
2139 ///
2140 /// ```rust
2141 /// # use uuid::Uuid;
2142 /// # use rvoip_client_core::call::CallId;
2143 /// # fn main() {
2144 /// // Session diagnostics
2145 /// let call_id: CallId = Uuid::new_v4();
2146 /// println!("Gathering diagnostic info for call {}", call_id);
2147 /// println!("Would include ports, codecs, and quality metrics");
2148 /// # }
2149 /// ```
2150 pub async fn get_media_session_info(&self, call_id: &CallId) -> ClientResult<Option<MediaSessionInfo>> {
2151 if !self.is_media_session_active(call_id).await? {
2152 return Ok(None);
2153 }
2154
2155 let session_id = self.session_mapping.get(call_id)
2156 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
2157 .clone();
2158
2159 // Get media info from session-core
2160 let media_info = MediaControl::get_media_info(&self.coordinator, &session_id)
2161 .await
2162 .map_err(|e| ClientError::InternalError {
2163 message: format!("Failed to get media info: {}", e)
2164 })?
2165 .ok_or_else(|| ClientError::InternalError {
2166 message: "No media info available".to_string()
2167 })?;
2168
2169 let call_info = self.call_info.get(call_id)
2170 .ok_or(ClientError::CallNotFound { call_id: *call_id })?;
2171
2172 let media_session_id = call_info.metadata.get("media_session_id")
2173 .cloned()
2174 .unwrap_or_else(|| format!("media-{}", session_id.0));
2175
2176 let created_at_str = call_info.metadata.get("media_session_started_at")
2177 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
2178 .map(|dt| dt.with_timezone(&Utc))
2179 .unwrap_or_else(Utc::now);
2180
2181 let audio_direction = self.determine_audio_direction(&media_info).await;
2182
2183 let media_session_info = MediaSessionInfo {
2184 call_id: *call_id,
2185 session_id,
2186 media_session_id,
2187 local_rtp_port: media_info.local_rtp_port,
2188 remote_rtp_port: media_info.remote_rtp_port,
2189 codec: media_info.codec,
2190 media_direction: audio_direction,
2191 quality_metrics: None, // TODO: Extract quality metrics
2192 is_active: true,
2193 created_at: created_at_str,
2194 };
2195
2196 Ok(Some(media_session_info))
2197 }
2198
2199 /// Update media session for a call (e.g., for re-INVITE)
2200 ///
2201 /// Updates an existing media session with new parameters, typically used during
2202 /// SIP re-INVITE scenarios where call parameters need to be modified mid-call.
2203 /// This can include codec changes, hold/unhold operations, or other media modifications.
2204 ///
2205 /// # Arguments
2206 ///
2207 /// * `call_id` - The unique identifier of the call to update
2208 /// * `new_sdp` - The new SDP description with updated media parameters
2209 ///
2210 /// # Returns
2211 ///
2212 /// Returns `Ok(())` on successful update, or a `ClientError` if:
2213 /// - The call is not found
2214 /// - The new SDP is empty or invalid
2215 /// - The session-core fails to apply the media update
2216 ///
2217 /// # Examples
2218 ///
2219 /// ```rust
2220 /// # use uuid::Uuid;
2221 /// # use rvoip_client_core::call::CallId;
2222 /// # fn main() {
2223 /// // Handle re-INVITE with new media parameters
2224 /// let call_id: CallId = Uuid::new_v4();
2225 /// let new_sdp = "v=0\r\no=updated 789 012 IN IP4 192.168.1.3\r\n";
2226 ///
2227 /// println!("Would update media session for call {}", call_id);
2228 /// println!("New SDP size: {} bytes", new_sdp.len());
2229 /// # }
2230 /// ```
2231 ///
2232 /// ```rust
2233 /// # use uuid::Uuid;
2234 /// # use rvoip_client_core::call::CallId;
2235 /// # fn main() {
2236 /// // Codec change during call
2237 /// let call_id: CallId = Uuid::new_v4();
2238 /// println!("Updating codec for call {} due to network conditions", call_id);
2239 /// println!("Would switch to lower bandwidth codec");
2240 /// # }
2241 /// ```
2242 ///
2243 /// # Use Cases
2244 ///
2245 /// - Processing SIP re-INVITE requests
2246 /// - Codec switching for quality adaptation
2247 /// - Hold/unhold operations
2248 /// - Media parameter renegotiation
2249 pub async fn update_media_session(&self, call_id: &CallId, new_sdp: &str) -> ClientResult<()> {
2250 // Validate SDP is not empty first
2251 if new_sdp.trim().is_empty() {
2252 return Err(ClientError::InvalidConfiguration {
2253 field: "new_sdp".to_string(),
2254 reason: "SDP for media update cannot be empty".to_string()
2255 });
2256 }
2257
2258 let session_id = self.session_mapping.get(call_id)
2259 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
2260 .clone();
2261
2262 // Update media session using session-core
2263 SessionControl::update_media(&self.coordinator, &session_id, new_sdp)
2264 .await
2265 .map_err(|e| ClientError::InternalError {
2266 message: format!("Failed to update media session: {}", e)
2267 })?;
2268
2269 // Update call metadata
2270 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
2271 call_info.metadata.insert("media_session_updated_at".to_string(), Utc::now().to_rfc3339());
2272 call_info.metadata.insert("last_media_update_sdp".to_string(), new_sdp.to_string());
2273 }
2274
2275 // Emit MediaEvent
2276 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
2277 let media_event = MediaEventInfo {
2278 call_id: *call_id,
2279 event_type: crate::events::MediaEventType::MediaSessionUpdated { sdp_size: new_sdp.len() },
2280 timestamp: Utc::now(),
2281 metadata: {
2282 let mut metadata = HashMap::new();
2283 metadata.insert("session_id".to_string(), session_id.0.clone());
2284 metadata
2285 },
2286 };
2287 handler.on_media_event(media_event).await;
2288 }
2289
2290 tracing::info!("Updated media session for call {}", call_id);
2291 Ok(())
2292 }
2293
2294 /// Get negotiated media parameters for a call
2295 ///
2296 /// Retrieves the final negotiated media parameters that resulted from the SDP
2297 /// offer/answer exchange. This includes the agreed-upon codec, ports, bandwidth
2298 /// limits, and other media configuration details that both parties have accepted.
2299 ///
2300 /// # Arguments
2301 ///
2302 /// * `call_id` - The unique identifier of the call to get negotiated parameters for
2303 ///
2304 /// # Returns
2305 ///
2306 /// Returns `Ok(Some(NegotiatedMediaParams))` with negotiated parameters if available,
2307 /// `Ok(None)` if negotiation is incomplete, or `ClientError` if the call is not found
2308 /// or parameters cannot be retrieved.
2309 ///
2310 /// # Examples
2311 ///
2312 /// ```rust
2313 /// # use uuid::Uuid;
2314 /// # use rvoip_client_core::call::CallId;
2315 /// # use rvoip_client_core::client::types::{NegotiatedMediaParams, AudioDirection};
2316 /// # use chrono::Utc;
2317 /// # fn main() {
2318 /// // Check negotiated parameters
2319 /// let call_id: CallId = Uuid::new_v4();
2320 /// println!("Would get negotiated media parameters for call {}", call_id);
2321 ///
2322 /// // Example negotiated parameters
2323 /// let params = Some(NegotiatedMediaParams {
2324 /// call_id,
2325 /// negotiated_codec: Some("G722".to_string()),
2326 /// local_rtp_port: Some(14000),
2327 /// remote_rtp_port: Some(14001),
2328 /// audio_direction: AudioDirection::SendReceive,
2329 /// local_sdp: "v=0\r\no=local...".to_string(),
2330 /// remote_sdp: "v=0\r\no=remote...".to_string(),
2331 /// negotiated_at: Utc::now(),
2332 /// supports_dtmf: true,
2333 /// supports_hold: true,
2334 /// bandwidth_kbps: Some(64),
2335 /// encryption_enabled: false,
2336 /// });
2337 ///
2338 /// match params {
2339 /// Some(p) => println!("Negotiated: {} at {}kbps",
2340 /// p.negotiated_codec.unwrap_or("Unknown".to_string()),
2341 /// p.bandwidth_kbps.unwrap_or(0)),
2342 /// None => println!("Negotiation not complete"),
2343 /// }
2344 /// # }
2345 /// ```
2346 ///
2347 /// ```rust
2348 /// # use uuid::Uuid;
2349 /// # use rvoip_client_core::call::CallId;
2350 /// # fn main() {
2351 /// // Compatibility check
2352 /// let call_id: CallId = Uuid::new_v4();
2353 /// println!("Checking feature compatibility for call {}", call_id);
2354 ///
2355 /// let supports_dtmf = true; // From negotiated params
2356 /// let supports_hold = true;
2357 ///
2358 /// println!("DTMF support: {}", if supports_dtmf { "Yes" } else { "No" });
2359 /// println!("Hold support: {}", if supports_hold { "Yes" } else { "No" });
2360 /// # }
2361 /// ```
2362 ///
2363 /// # Use Cases
2364 ///
2365 /// - Verifying successful media negotiation
2366 /// - Feature availability checking
2367 /// - Quality monitoring and optimization
2368 /// - Debugging media setup issues
2369 pub async fn get_negotiated_media_params(&self, call_id: &CallId) -> ClientResult<Option<NegotiatedMediaParams>> {
2370 let media_info = self.get_call_media_info(call_id).await?;
2371
2372 // Only return params if both local and remote SDP are available
2373 if let (Some(local_sdp), Some(remote_sdp)) = (media_info.local_sdp, media_info.remote_sdp) {
2374 let bandwidth_kbps = self.extract_bandwidth_from_sdp(&local_sdp, &remote_sdp).await;
2375
2376 let params = NegotiatedMediaParams {
2377 call_id: *call_id,
2378 negotiated_codec: media_info.codec,
2379 local_rtp_port: media_info.local_rtp_port,
2380 remote_rtp_port: media_info.remote_rtp_port,
2381 audio_direction: media_info.audio_direction,
2382 local_sdp,
2383 remote_sdp,
2384 negotiated_at: Utc::now(),
2385 supports_dtmf: true, // TODO: Parse from SDP
2386 supports_hold: true, // TODO: Parse from SDP
2387 bandwidth_kbps,
2388 encryption_enabled: false, // TODO: Parse SRTP from SDP
2389 };
2390
2391 Ok(Some(params))
2392 } else {
2393 Ok(None)
2394 }
2395 }
2396
2397 /// Get enhanced media capabilities with advanced features
2398 ///
2399 /// Returns an extended set of media capabilities that includes advanced features
2400 /// like session lifecycle management, SDP renegotiation support, early media,
2401 /// and encryption capabilities. This provides a more detailed view of the client's
2402 /// media processing capabilities compared to the basic capabilities.
2403 ///
2404 /// # Returns
2405 ///
2406 /// Returns `EnhancedMediaCapabilities` containing:
2407 /// - Basic media capabilities (codecs, mute, hold, etc.)
2408 /// - Advanced SDP features (offer/answer, renegotiation)
2409 /// - Session lifecycle management capabilities
2410 /// - Encryption and security features
2411 /// - Transport protocol support
2412 /// - Performance and scalability limits
2413 ///
2414 /// # Examples
2415 ///
2416 /// ```rust
2417 /// # use rvoip_client_core::client::types::{EnhancedMediaCapabilities, MediaCapabilities, AudioCodecInfo};
2418 /// # fn main() {
2419 /// // Check advanced capabilities
2420 /// let basic_caps = MediaCapabilities {
2421 /// supported_codecs: vec![],
2422 /// can_hold: true,
2423 /// can_mute_microphone: true,
2424 /// can_mute_speaker: true,
2425 /// can_send_dtmf: true,
2426 /// can_transfer: true,
2427 /// supports_sdp_offer_answer: true,
2428 /// supports_rtp: true,
2429 /// supports_rtcp: true,
2430 /// max_concurrent_calls: 10,
2431 /// supported_media_types: vec!["audio".to_string()],
2432 /// };
2433 ///
2434 /// let enhanced_caps = EnhancedMediaCapabilities {
2435 /// basic_capabilities: basic_caps,
2436 /// supports_sdp_offer_answer: true,
2437 /// supports_media_session_lifecycle: true,
2438 /// supports_sdp_renegotiation: true,
2439 /// supports_early_media: true,
2440 /// supports_media_session_updates: true,
2441 /// supports_codec_negotiation: true,
2442 /// supports_bandwidth_management: false,
2443 /// supports_encryption: false,
2444 /// supported_sdp_version: "0".to_string(),
2445 /// max_media_sessions: 10,
2446 /// preferred_rtp_port_range: (10000, 20000),
2447 /// supported_transport_protocols: vec!["RTP/AVP".to_string()],
2448 /// };
2449 ///
2450 /// println!("SDP renegotiation: {}", enhanced_caps.supports_sdp_renegotiation);
2451 /// println!("Early media: {}", enhanced_caps.supports_early_media);
2452 /// println!("Max sessions: {}", enhanced_caps.max_media_sessions);
2453 /// # }
2454 /// ```
2455 ///
2456 /// ```rust
2457 /// # fn main() {
2458 /// // Feature availability matrix
2459 /// let supports_renegotiation = true;
2460 /// let supports_early_media = true;
2461 /// let supports_encryption = false;
2462 ///
2463 /// println!("Advanced Features:");
2464 /// println!(" SDP Renegotiation: {}", if supports_renegotiation { "✓" } else { "✗" });
2465 /// println!(" Early Media: {}", if supports_early_media { "✓" } else { "✗" });
2466 /// println!(" Encryption: {}", if supports_encryption { "✓" } else { "✗" });
2467 /// # }
2468 /// ```
2469 ///
2470 /// # Use Cases
2471 ///
2472 /// - Advanced capability negotiation
2473 /// - Enterprise feature planning
2474 /// - Integration compatibility assessment
2475 /// - Performance planning and sizing
2476 pub async fn get_enhanced_media_capabilities(&self) -> EnhancedMediaCapabilities {
2477 let basic_capabilities = self.get_media_capabilities().await;
2478
2479 EnhancedMediaCapabilities {
2480 basic_capabilities,
2481 supports_sdp_offer_answer: true,
2482 supports_media_session_lifecycle: true,
2483 supports_sdp_renegotiation: true,
2484 supports_early_media: true, // Set to true to match test expectations
2485 supports_media_session_updates: true,
2486 supports_codec_negotiation: true,
2487 supports_bandwidth_management: false, // TODO: Implement bandwidth management
2488 supports_encryption: false, // TODO: Implement SRTP
2489 supported_sdp_version: "0".to_string(),
2490 max_media_sessions: 10, // TODO: Make configurable
2491 preferred_rtp_port_range: (10000, 20000), // TODO: Make configurable
2492 supported_transport_protocols: vec!["RTP/AVP".to_string()], // TODO: Add SRTP support
2493 }
2494 }
2495
2496 /// Helper method to extract bandwidth information from SDP
2497 ///
2498 /// Parses both local and remote SDP descriptions to extract bandwidth information
2499 /// from standard SDP bandwidth lines (b=AS:). This function searches for bandwidth
2500 /// specifications in either SDP and returns the first valid bandwidth value found.
2501 /// The bandwidth is typically specified in kilobits per second (kbps).
2502 ///
2503 /// # Arguments
2504 ///
2505 /// * `local_sdp` - The local SDP description to search for bandwidth information
2506 /// * `remote_sdp` - The remote SDP description to search for bandwidth information
2507 ///
2508 /// # Returns
2509 ///
2510 /// Returns `Some(bandwidth_kbps)` if a valid bandwidth specification is found,
2511 /// or `None` if no bandwidth information is present in either SDP.
2512 ///
2513 /// # Examples
2514 ///
2515 /// ```rust
2516 /// # fn main() {
2517 /// // SDP with bandwidth specification
2518 /// let local_sdp = "v=0\r\no=- 123 456 IN IP4 192.168.1.1\r\nb=AS:64\r\nm=audio 5004 RTP/AVP 0\r\n";
2519 /// let remote_sdp = "v=0\r\no=- 789 012 IN IP4 192.168.1.2\r\nm=audio 5006 RTP/AVP 0\r\n";
2520 ///
2521 /// // Simulated bandwidth extraction
2522 /// let bandwidth_found = local_sdp.contains("b=AS:");
2523 /// if bandwidth_found {
2524 /// // Extract bandwidth value (64 kbps in this example)
2525 /// let bandwidth = 64;
2526 /// println!("Found bandwidth specification: {}kbps", bandwidth);
2527 /// } else {
2528 /// println!("No bandwidth specification found");
2529 /// }
2530 /// # }
2531 /// ```
2532 ///
2533 /// ```rust
2534 /// # fn main() {
2535 /// // Multiple bandwidth specifications
2536 /// let sdp_with_multiple = r#"v=0
2537 /// o=- 123 456 IN IP4 192.168.1.1
2538 /// b=AS:128
2539 /// m=audio 5004 RTP/AVP 0
2540 /// b=AS:64
2541 /// "#;
2542 ///
2543 /// // Would extract the first valid bandwidth (128 kbps)
2544 /// println!("SDP contains bandwidth specifications");
2545 /// println!("Would extract first valid value: 128kbps");
2546 /// # }
2547 /// ```
2548 ///
2549 /// ```rust
2550 /// # fn main() {
2551 /// // Bandwidth-aware call quality assessment
2552 /// let available_bandwidth = Some(256); // kbps
2553 ///
2554 /// match available_bandwidth {
2555 /// Some(bw) if bw >= 128 => {
2556 /// println!("High bandwidth available: {}kbps", bw);
2557 /// println!("Can use high-quality codecs");
2558 /// }
2559 /// Some(bw) if bw >= 64 => {
2560 /// println!("Medium bandwidth: {}kbps", bw);
2561 /// println!("Standard quality codecs recommended");
2562 /// }
2563 /// Some(bw) => {
2564 /// println!("Low bandwidth: {}kbps", bw);
2565 /// println!("Compressed codecs required");
2566 /// }
2567 /// None => {
2568 /// println!("No bandwidth specification");
2569 /// println!("Using default codec selection");
2570 /// }
2571 /// }
2572 /// # }
2573 /// ```
2574 ///
2575 /// # SDP Bandwidth Format
2576 ///
2577 /// This function specifically looks for lines in the format:
2578 /// - `b=AS:value` - Application-Specific bandwidth in kbps
2579 ///
2580 /// Other bandwidth types (like `b=CT:` for Conference Total) are not currently
2581 /// parsed by this implementation.
2582 ///
2583 /// # Use Cases
2584 ///
2585 /// - Quality of Service (QoS) planning
2586 /// - Codec selection based on available bandwidth
2587 /// - Network capacity monitoring
2588 /// - Adaptive bitrate configuration
2589 /// - Call quality optimization
2590 ///
2591 /// # Implementation Notes
2592 ///
2593 /// The function searches both SDPs and returns the first valid bandwidth found.
2594 /// Priority is given to the local SDP, then the remote SDP. Invalid or malformed
2595 /// bandwidth specifications are ignored.
2596 async fn extract_bandwidth_from_sdp(&self, local_sdp: &str, remote_sdp: &str) -> Option<u32> {
2597 // Simple bandwidth extraction from SDP "b=" lines
2598 for line in local_sdp.lines().chain(remote_sdp.lines()) {
2599 if line.starts_with("b=AS:") {
2600 if let Ok(bandwidth) = line[5..].parse::<u32>() {
2601 return Some(bandwidth);
2602 }
2603 }
2604 }
2605 None
2606 }
2607
2608 /// Generate SDP answer for an incoming call
2609 ///
2610 /// Creates a Session Description Protocol (SDP) answer in response to an incoming
2611 /// SDP offer, typically from a SIP INVITE request. The answer describes the media
2612 /// capabilities and parameters that this client accepts and configures the
2613 /// media session based on the negotiated parameters.
2614 ///
2615 /// # Arguments
2616 ///
2617 /// * `call_id` - The unique identifier of the incoming call
2618 /// * `offer` - The SDP offer string received from the remote party
2619 ///
2620 /// # Returns
2621 ///
2622 /// Returns the SDP answer as a string, or a `ClientError` if:
2623 /// - The call is not found
2624 /// - The SDP offer is empty or malformed
2625 /// - The underlying session-core fails to generate the SDP answer
2626 ///
2627 /// # Examples
2628 ///
2629 /// ```rust
2630 /// # use uuid::Uuid;
2631 /// # use rvoip_client_core::call::CallId;
2632 /// # fn main() {
2633 /// // Generate SDP answer for incoming call
2634 /// let call_id: CallId = Uuid::new_v4();
2635 /// let offer = "v=0\r\no=caller 123456 654321 IN IP4 192.168.1.10\r\n";
2636 ///
2637 /// println!("Would generate SDP answer for call {}", call_id);
2638 /// println!("Processing offer of {} bytes", offer.len());
2639 /// # }
2640 /// ```
2641 ///
2642 /// ```rust
2643 /// # use uuid::Uuid;
2644 /// # use rvoip_client_core::call::CallId;
2645 /// # fn main() {
2646 /// // SIP call flow context
2647 /// let call_id: CallId = Uuid::new_v4();
2648 /// println!("Processing INVITE for call {}", call_id);
2649 /// println!("Generating SDP answer for 200 OK response");
2650 /// # }
2651 /// ```
2652 ///
2653 /// ```rust
2654 /// # use uuid::Uuid;
2655 /// # use rvoip_client_core::call::CallId;
2656 /// # fn main() {
2657 /// // Media negotiation
2658 /// let call_id: CallId = Uuid::new_v4();
2659 /// let offer = "v=0\r\nm=audio 5004 RTP/AVP 0 8\r\n";
2660 ///
2661 /// println!("Negotiating media for call {}", call_id);
2662 /// println!("Offer contains audio on port 5004");
2663 /// println!("Would respond with compatible audio configuration");
2664 /// # }
2665 /// ```
2666 ///
2667 /// # Side Effects
2668 ///
2669 /// - Applies media configuration preferences to the generated SDP
2670 /// - Updates call metadata with the generated SDP answer and timestamp
2671 /// - Coordinates with session-core for media session configuration
2672 /// - May modify SDP with custom attributes, bandwidth limits, and timing preferences
2673 ///
2674 /// # Use Cases
2675 ///
2676 /// - Responding to incoming SIP INVITE requests
2677 /// - Completing media negotiation for incoming calls
2678 /// - Auto-answering systems and IVR applications
2679 /// - Conference bridge incoming call handling
2680 pub async fn generate_sdp_answer(&self, call_id: &CallId, offer: &str) -> ClientResult<String> {
2681 let session_id = self.session_mapping.get(call_id)
2682 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
2683 .clone();
2684
2685 // Validate SDP offer
2686 if offer.trim().is_empty() {
2687 return Err(ClientError::InvalidConfiguration {
2688 field: "sdp_offer".to_string(),
2689 reason: "SDP offer cannot be empty".to_string()
2690 });
2691 }
2692
2693 // Before generating SDP answer, configure session-core with our media preferences
2694 // This ensures the generated SDP reflects our configured codecs and capabilities
2695
2696 // TODO: Once session-core supports setting codec preferences per-session,
2697 // we would do something like:
2698 // MediaControl::set_session_codecs(&self.coordinator, &session_id, &self.media_config.preferred_codecs).await?;
2699
2700 // For now, session-core will use the codecs configured during initialization
2701 // The media config was passed when building the SessionCoordinator
2702
2703 // Use session-core to generate SDP answer
2704 let sdp_answer = MediaControl::generate_sdp_answer(&self.coordinator, &session_id, offer)
2705 .await
2706 .map_err(|e| ClientError::InternalError {
2707 message: format!("Failed to generate SDP answer: {}", e)
2708 })?;
2709
2710 // Post-process the SDP if needed based on media configuration
2711 let sdp_answer = self.apply_media_config_to_sdp(sdp_answer).await;
2712
2713 // Update call metadata
2714 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
2715 call_info.metadata.insert("last_sdp_answer".to_string(), sdp_answer.clone());
2716 call_info.metadata.insert("sdp_answer_generated_at".to_string(), Utc::now().to_rfc3339());
2717 }
2718
2719 tracing::info!("Generated SDP answer for call {}: {} bytes", call_id, sdp_answer.len());
2720 Ok(sdp_answer)
2721 }
2722
2723 /// Apply media configuration to generated SDP
2724 ///
2725 /// Post-processes a generated SDP description by applying the client's media
2726 /// configuration preferences. This includes adding custom SDP attributes,
2727 /// bandwidth constraints, packet time (ptime) preferences, and other
2728 /// media-specific configuration options that customize the SDP for this client.
2729 ///
2730 /// # Arguments
2731 ///
2732 /// * `sdp` - The base SDP string to be modified
2733 ///
2734 /// # Returns
2735 ///
2736 /// Returns the modified SDP string with applied configuration preferences.
2737 /// This function always succeeds and returns a valid SDP.
2738 ///
2739 /// # Examples
2740 ///
2741 /// ```rust
2742 /// # fn main() {
2743 /// // Basic SDP modification
2744 /// let base_sdp = "v=0\r\no=- 123 456 IN IP4 192.168.1.1\r\nm=audio 5004 RTP/AVP 0\r\n";
2745 /// println!("Base SDP: {} bytes", base_sdp.len());
2746 ///
2747 /// // Example configuration applications
2748 /// let has_custom_attrs = true;
2749 /// let has_bandwidth_limit = true;
2750 /// let has_ptime_pref = true;
2751 ///
2752 /// if has_custom_attrs {
2753 /// println!("Would add custom SDP attributes");
2754 /// }
2755 /// if has_bandwidth_limit {
2756 /// println!("Would add bandwidth constraint (b=AS:64)");
2757 /// }
2758 /// if has_ptime_pref {
2759 /// println!("Would add packet time preference (a=ptime:20)");
2760 /// }
2761 /// # }
2762 /// ```
2763 ///
2764 /// ```rust
2765 /// # fn main() {
2766 /// // Enterprise customization example
2767 /// let original_sdp = "v=0\r\nm=audio 8000 RTP/AVP 111\r\na=rtpmap:111 OPUS/48000/2\r\n";
2768 ///
2769 /// // Simulated configuration
2770 /// let max_bandwidth = 128; // kbps
2771 /// let preferred_ptime = 20; // ms
2772 /// let custom_attrs = vec![("a=sendrecv", ""), ("a=tool", "rvoip-client")];
2773 ///
2774 /// println!("Original SDP: {} bytes", original_sdp.len());
2775 /// println!("Applying enterprise configuration:");
2776 /// println!(" Max bandwidth: {}kbps", max_bandwidth);
2777 /// println!(" Packet time: {}ms", preferred_ptime);
2778 /// println!(" Custom attributes: {} items", custom_attrs.len());
2779 /// # }
2780 /// ```
2781 ///
2782 /// ```rust
2783 /// # fn main() {
2784 /// // Quality optimization
2785 /// let sdp = "v=0\r\nm=audio 5004 RTP/AVP 0 8\r\n";
2786 ///
2787 /// // Configuration for different scenarios
2788 /// let scenario = "low_bandwidth";
2789 /// match scenario {
2790 /// "low_bandwidth" => {
2791 /// println!("Applying low bandwidth optimizations");
2792 /// println!("Would add: b=AS:32");
2793 /// }
2794 /// "high_quality" => {
2795 /// println!("Applying high quality settings");
2796 /// println!("Would add: b=AS:256");
2797 /// }
2798 /// _ => println!("Using default settings"),
2799 /// }
2800 /// # }
2801 /// ```
2802 ///
2803 /// # Configuration Applied
2804 ///
2805 /// 1. **Custom SDP Attributes**: Adds configured custom attributes after the media line
2806 /// 2. **Bandwidth Constraints**: Inserts `b=AS:` lines for bandwidth limits
2807 /// 3. **Packet Time**: Adds `a=ptime:` attributes for timing preferences
2808 /// 4. **Format Compliance**: Ensures proper SDP formatting with CRLF line endings
2809 ///
2810 /// # Use Cases
2811 ///
2812 /// - Enterprise policy enforcement in SDP
2813 /// - Network optimization for specific environments
2814 /// - Codec-specific parameter tuning
2815 /// - Quality of Service (QoS) configuration
2816 /// - Compliance with specific SIP server requirements
2817 async fn apply_media_config_to_sdp(&self, mut sdp: String) -> String {
2818 // Add custom attributes if configured
2819 if !self.media_config.custom_sdp_attributes.is_empty() {
2820 let mut lines: Vec<String> = sdp.lines().map(|s| s.to_string()).collect();
2821
2822 // Find where to insert attributes (after the first m= line)
2823 if let Some(m_line_idx) = lines.iter().position(|line| line.starts_with("m=")) {
2824 let mut insert_idx = m_line_idx + 1;
2825
2826 // Insert custom attributes
2827 for (key, value) in &self.media_config.custom_sdp_attributes {
2828 lines.insert(insert_idx, format!("{}:{}", key, value));
2829 insert_idx += 1;
2830 }
2831 }
2832
2833 sdp = lines.join("\r\n");
2834 if !sdp.ends_with("\r\n") {
2835 sdp.push_str("\r\n");
2836 }
2837 }
2838
2839 // Add bandwidth constraint if configured
2840 if let Some(max_bw) = self.media_config.max_bandwidth_kbps {
2841 if !sdp.contains("b=AS:") {
2842 let mut lines: Vec<String> = sdp.lines().map(|s| s.to_string()).collect();
2843
2844 // Insert bandwidth after c= line
2845 if let Some(c_line_idx) = lines.iter().position(|line| line.starts_with("c=")) {
2846 lines.insert(c_line_idx + 1, format!("b=AS:{}", max_bw));
2847 }
2848
2849 sdp = lines.join("\r\n");
2850 if !sdp.ends_with("\r\n") {
2851 sdp.push_str("\r\n");
2852 }
2853 }
2854 }
2855
2856 // Add ptime if configured
2857 if let Some(ptime) = self.media_config.preferred_ptime {
2858 if !sdp.contains("a=ptime:") {
2859 // Add ptime attribute after the last a=rtpmap line
2860 let mut lines: Vec<String> = sdp.lines().map(|s| s.to_string()).collect();
2861
2862 if let Some(last_rtpmap_idx) = lines.iter().rposition(|line| line.starts_with("a=rtpmap:")) {
2863 lines.insert(last_rtpmap_idx + 1, format!("a=ptime:{}", ptime));
2864 }
2865
2866 sdp = lines.join("\r\n");
2867 if !sdp.ends_with("\r\n") {
2868 sdp.push_str("\r\n");
2869 }
2870 }
2871 }
2872
2873 sdp
2874 }
2875
2876 /// Establish media flow to a remote address
2877 ///
2878 /// Establishes the actual media flow (RTP streams) between the local client and
2879 /// a specified remote address. This function configures the media session to
2880 /// begin transmitting and receiving audio packets to/from the designated endpoint.
2881 /// This is typically called after SDP negotiation is complete.
2882 ///
2883 /// # Arguments
2884 ///
2885 /// * `call_id` - The unique identifier of the call to establish media flow for
2886 /// * `remote_addr` - The remote address (IP:port) to establish media flow with
2887 ///
2888 /// # Returns
2889 ///
2890 /// Returns `Ok(())` on successful establishment, or a `ClientError` if:
2891 /// - The call is not found
2892 /// - The remote address is invalid or unreachable
2893 /// - The underlying session-core fails to establish the media flow
2894 ///
2895 /// # Examples
2896 ///
2897 /// ```rust
2898 /// # use uuid::Uuid;
2899 /// # use rvoip_client_core::call::CallId;
2900 /// # fn main() {
2901 /// // Establish media flow after SDP negotiation
2902 /// let call_id: CallId = Uuid::new_v4();
2903 /// let remote_addr = "192.168.1.20:5004";
2904 ///
2905 /// println!("Would establish media flow for call {}", call_id);
2906 /// println!("Target remote address: {}", remote_addr);
2907 /// # }
2908 /// ```
2909 ///
2910 /// ```rust
2911 /// # use uuid::Uuid;
2912 /// # use rvoip_client_core::call::CallId;
2913 /// # fn main() {
2914 /// // Direct media establishment for P2P calls
2915 /// let call_id: CallId = Uuid::new_v4();
2916 /// let peer_endpoint = "10.0.1.100:12000";
2917 ///
2918 /// println!("Establishing direct P2P media to {}", peer_endpoint);
2919 /// println!("Call ID: {}", call_id);
2920 /// # }
2921 /// ```
2922 ///
2923 /// ```rust
2924 /// # use uuid::Uuid;
2925 /// # use rvoip_client_core::call::CallId;
2926 /// # fn main() {
2927 /// // Media relay configuration
2928 /// let call_id: CallId = Uuid::new_v4();
2929 /// let relay_address = "relay.example.com:8000";
2930 ///
2931 /// println!("Configuring media relay for call {}", call_id);
2932 /// println!("Relay endpoint: {}", relay_address);
2933 /// println!("Would establish RTP flows through media relay");
2934 /// # }
2935 /// ```
2936 ///
2937 /// # Side Effects
2938 ///
2939 /// - Updates call metadata with media flow status and remote address
2940 /// - Initiates RTP packet transmission/reception
2941 /// - Configures network routing for media streams
2942 /// - May trigger firewall/NAT traversal procedures
2943 ///
2944 /// # Use Cases
2945 ///
2946 /// - Completing call setup after SDP negotiation
2947 /// - Direct peer-to-peer media establishment
2948 /// - Media relay and proxy configurations
2949 /// - Network topology adaptation
2950 /// - Quality of Service (QoS) path establishment
2951 ///
2952 /// # Network Considerations
2953 ///
2954 /// The remote address should be reachable and the specified port should be
2955 /// available for RTP traffic. This function may trigger network discovery
2956 /// and NAT traversal procedures if required by the network topology.
2957 pub async fn establish_media(&self, call_id: &CallId, remote_addr: &str) -> ClientResult<()> {
2958 let session_id = self.session_mapping.get(call_id)
2959 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
2960 .clone();
2961
2962 // Use session-core to establish media flow
2963 MediaControl::establish_media_flow(&self.coordinator, &session_id, remote_addr)
2964 .await
2965 .map_err(|e| ClientError::InternalError {
2966 message: format!("Failed to establish media flow: {}", e)
2967 })?;
2968
2969 // Update call metadata
2970 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
2971 call_info.metadata.insert("media_flow_established".to_string(), "true".to_string());
2972 call_info.metadata.insert("remote_media_addr".to_string(), remote_addr.to_string());
2973 }
2974
2975 tracing::info!("Established media flow for call {} to {}", call_id, remote_addr);
2976 Ok(())
2977 }
2978
2979 /// Get RTP statistics for a call
2980 ///
2981 /// Retrieves detailed Real-time Transport Protocol (RTP) statistics for the specified call,
2982 /// including packet counts, byte counts, jitter measurements, and packet loss metrics.
2983 /// This information is crucial for monitoring call quality and diagnosing network issues.
2984 ///
2985 /// # Arguments
2986 ///
2987 /// * `call_id` - The unique identifier of the call to get RTP statistics for
2988 ///
2989 /// # Returns
2990 ///
2991 /// Returns `Ok(Some(RtpSessionStats))` with detailed RTP metrics if available,
2992 /// `Ok(None)` if no RTP session exists, or `ClientError` if the call is not found
2993 /// or statistics cannot be retrieved.
2994 ///
2995 /// # Examples
2996 ///
2997 /// ```rust
2998 /// # use uuid::Uuid;
2999 /// # use rvoip_client_core::call::CallId;
3000 /// # fn main() {
3001 /// // Monitor call quality
3002 /// let call_id: CallId = Uuid::new_v4();
3003 /// println!("Would get RTP statistics for call {}", call_id);
3004 ///
3005 /// // Example statistics evaluation
3006 /// let packets_sent = 1000u64;
3007 /// let packets_lost = 5u64;
3008 /// let loss_rate = (packets_lost as f64 / packets_sent as f64) * 100.0;
3009 ///
3010 /// if loss_rate > 5.0 {
3011 /// println!("High packet loss detected: {:.2}%", loss_rate);
3012 /// } else {
3013 /// println!("Good quality: {:.2}% packet loss", loss_rate);
3014 /// }
3015 /// # }
3016 /// ```
3017 ///
3018 /// ```rust
3019 /// # use uuid::Uuid;
3020 /// # use rvoip_client_core::call::CallId;
3021 /// # fn main() {
3022 /// // Network diagnostics
3023 /// let call_id: CallId = Uuid::new_v4();
3024 /// println!("Running network diagnostics for call {}", call_id);
3025 /// println!("Would analyze jitter, latency, and throughput");
3026 /// # }
3027 /// ```
3028 ///
3029 /// # Use Cases
3030 ///
3031 /// - Real-time call quality monitoring
3032 /// - Network performance analysis
3033 /// - Troubleshooting audio issues
3034 /// - Quality of Service (QoS) reporting
3035 pub async fn get_rtp_statistics(&self, call_id: &CallId) -> ClientResult<Option<RtpSessionStats>> {
3036 let session_id = self.session_mapping.get(call_id)
3037 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3038 .clone();
3039
3040 MediaControl::get_rtp_statistics(&self.coordinator, &session_id)
3041 .await
3042 .map_err(|e| ClientError::InternalError {
3043 message: format!("Failed to get RTP statistics: {}", e)
3044 })
3045 }
3046
3047 /// Get comprehensive media statistics for a call
3048 ///
3049 /// Retrieves complete media session statistics including RTP/RTCP metrics, quality
3050 /// measurements, and performance indicators. This provides a holistic view of the
3051 /// media session's health and performance characteristics.
3052 ///
3053 /// # Arguments
3054 ///
3055 /// * `call_id` - The unique identifier of the call to get media statistics for
3056 ///
3057 /// # Returns
3058 ///
3059 /// Returns `Ok(Some(MediaSessionStats))` with comprehensive media metrics if available,
3060 /// `Ok(None)` if no media session exists, or `ClientError` if the call is not found
3061 /// or statistics cannot be retrieved.
3062 ///
3063 /// # Examples
3064 ///
3065 /// ```rust
3066 /// # use uuid::Uuid;
3067 /// # use rvoip_client_core::call::CallId;
3068 /// # fn main() {
3069 /// // Comprehensive call analysis
3070 /// let call_id: CallId = Uuid::new_v4();
3071 /// println!("Would get comprehensive media statistics for call {}", call_id);
3072 ///
3073 /// // Example quality assessment
3074 /// let audio_quality_score = 4.2; // Out of 5.0
3075 /// let network_quality = "Good"; // Based on metrics
3076 ///
3077 /// println!("Audio Quality: {:.1}/5.0", audio_quality_score);
3078 /// println!("Network Quality: {}", network_quality);
3079 /// # }
3080 /// ```
3081 ///
3082 /// ```rust
3083 /// # use uuid::Uuid;
3084 /// # use rvoip_client_core::call::CallId;
3085 /// # fn main() {
3086 /// // Performance monitoring dashboard
3087 /// let call_id: CallId = Uuid::new_v4();
3088 /// println!("Updating performance dashboard for call {}", call_id);
3089 /// println!("Would include RTP, RTCP, jitter, and codec metrics");
3090 /// # }
3091 /// ```
3092 ///
3093 /// # Use Cases
3094 ///
3095 /// - Call quality dashboards
3096 /// - Performance monitoring systems
3097 /// - Troubleshooting complex media issues
3098 /// - Historical call quality analysis
3099 pub async fn get_media_statistics(&self, call_id: &CallId) -> ClientResult<Option<MediaSessionStats>> {
3100 let session_id = self.session_mapping.get(call_id)
3101 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3102 .clone();
3103
3104 MediaControl::get_media_statistics(&self.coordinator, &session_id)
3105 .await
3106 .map_err(|e| ClientError::InternalError {
3107 message: format!("Failed to get media statistics: {}", e)
3108 })
3109 }
3110
3111 /// Get comprehensive call statistics for a call
3112 ///
3113 /// Retrieves complete call statistics encompassing all aspects of the call including
3114 /// RTP metrics, quality measurements, call duration, and detailed performance data.
3115 /// This provides the most comprehensive view of call performance and quality.
3116 ///
3117 /// # Arguments
3118 ///
3119 /// * `call_id` - The unique identifier of the call to get complete statistics for
3120 ///
3121 /// # Returns
3122 ///
3123 /// Returns `Ok(Some(CallStatistics))` with complete call metrics if available,
3124 /// `Ok(None)` if no call statistics exist, or `ClientError` if the call is not found
3125 /// or statistics cannot be retrieved.
3126 ///
3127 /// # Examples
3128 ///
3129 /// ```rust
3130 /// # use uuid::Uuid;
3131 /// # use rvoip_client_core::call::CallId;
3132 /// # use std::time::Duration;
3133 /// # fn main() {
3134 /// // Complete call analysis
3135 /// let call_id: CallId = Uuid::new_v4();
3136 /// println!("Would get complete call statistics for call {}", call_id);
3137 ///
3138 /// // Example comprehensive metrics
3139 /// let call_duration = Duration::from_secs(300); // 5 minutes
3140 /// let avg_jitter = 15; // milliseconds
3141 /// let packet_loss = 0.8; // percent
3142 ///
3143 /// println!("Call Duration: {:?}", call_duration);
3144 /// println!("Average Jitter: {}ms", avg_jitter);
3145 /// println!("Packet Loss: {:.1}%", packet_loss);
3146 /// # }
3147 /// ```
3148 ///
3149 /// ```rust
3150 /// # use uuid::Uuid;
3151 /// # use rvoip_client_core::call::CallId;
3152 /// # fn main() {
3153 /// // Call quality reporting
3154 /// let call_id: CallId = Uuid::new_v4();
3155 /// println!("Generating call quality report for call {}", call_id);
3156 /// println!("Would include all RTP, quality, and performance metrics");
3157 /// # }
3158 /// ```
3159 ///
3160 /// # Use Cases
3161 ///
3162 /// - Post-call quality reports
3163 /// - Billing and usage analytics
3164 /// - Network performance analysis
3165 /// - Customer experience metrics
3166 /// - SLA compliance monitoring
3167 pub async fn get_call_statistics(&self, call_id: &CallId) -> ClientResult<Option<CallStatistics>> {
3168 let session_id = self.session_mapping.get(call_id)
3169 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3170 .clone();
3171
3172 MediaControl::get_call_statistics(&self.coordinator, &session_id)
3173 .await
3174 .map_err(|e| ClientError::InternalError {
3175 message: format!("Failed to get call statistics: {}", e)
3176 })
3177 }
3178
3179
3180
3181 // =============================================================================
3182 // REAL-TIME AUDIO STREAMING API
3183 // =============================================================================
3184
3185 /// Subscribe to audio frames from a call for real-time playback
3186 ///
3187 /// Returns a subscriber that receives decoded audio frames from the RTP stream
3188 /// for the specified call. These frames can be played through speakers or
3189 /// processed for audio analysis.
3190 ///
3191 /// # Arguments
3192 ///
3193 /// * `call_id` - The unique identifier of the call to subscribe to
3194 ///
3195 /// # Returns
3196 ///
3197 /// Returns an `AudioFrameSubscriber` that can be used to receive audio frames.
3198 ///
3199 /// # Examples
3200 ///
3201 /// ```rust
3202 /// # use rvoip_client_core::{ClientManager, call::CallId};
3203 /// # use std::sync::Arc;
3204 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3205 /// // Subscribe to incoming audio frames
3206 /// let subscriber = client.subscribe_to_audio_frames(&call_id).await?;
3207 ///
3208 /// // Process frames in a background task
3209 /// tokio::spawn(async move {
3210 /// while let Ok(frame) = subscriber.recv() {
3211 /// // Play frame through speakers or process it
3212 /// println!("Received audio frame: {} samples at {}Hz",
3213 /// frame.samples.len(), frame.sample_rate);
3214 /// }
3215 /// });
3216 /// # Ok(())
3217 /// # }
3218 /// ```
3219 pub async fn subscribe_to_audio_frames(&self, call_id: &CallId) -> ClientResult<rvoip_session_core::api::types::AudioFrameSubscriber> {
3220 let session_id = self.session_mapping.get(call_id)
3221 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3222 .clone();
3223
3224 // Use session-core to subscribe to audio frames
3225 MediaControl::subscribe_to_audio_frames(&self.coordinator, &session_id)
3226 .await
3227 .map_err(|e| ClientError::CallSetupFailed {
3228 reason: format!("Failed to subscribe to audio frames: {}", e)
3229 })
3230 }
3231
3232 /// Send an audio frame for encoding and transmission
3233 ///
3234 /// Sends an audio frame to be encoded and transmitted via RTP for the specified call.
3235 /// This is typically used for microphone input or generated audio content.
3236 ///
3237 /// # Arguments
3238 ///
3239 /// * `call_id` - The unique identifier of the call to send audio on
3240 /// * `audio_frame` - The audio frame to send
3241 ///
3242 /// # Returns
3243 ///
3244 /// Returns `Ok(())` if the frame was sent successfully.
3245 ///
3246 /// # Examples
3247 ///
3248 /// ```rust
3249 /// # use rvoip_client_core::{ClientManager, call::CallId};
3250 /// # use rvoip_session_core::api::types::AudioFrame;
3251 /// # use std::sync::Arc;
3252 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3253 /// // Create an audio frame (typically from microphone)
3254 /// let samples = vec![0; 160]; // 20ms of silence at 8kHz
3255 /// let frame = AudioFrame::new(samples, 8000, 1, 12345);
3256 ///
3257 /// // Send the frame for transmission
3258 /// client.send_audio_frame(&call_id, frame).await?;
3259 /// # Ok(())
3260 /// # }
3261 /// ```
3262 pub async fn send_audio_frame(&self, call_id: &CallId, audio_frame: rvoip_session_core::api::types::AudioFrame) -> ClientResult<()> {
3263 let session_id = self.session_mapping.get(call_id)
3264 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3265 .clone();
3266
3267 // Use session-core to send audio frame
3268 MediaControl::send_audio_frame(&self.coordinator, &session_id, audio_frame)
3269 .await
3270 .map_err(|e| ClientError::CallSetupFailed {
3271 reason: format!("Failed to send audio frame: {}", e)
3272 })
3273 }
3274
3275 /// Get current audio stream configuration for a call
3276 ///
3277 /// Returns the current audio streaming configuration for the specified call,
3278 /// including sample rate, channels, codec, and processing settings.
3279 ///
3280 /// # Arguments
3281 ///
3282 /// * `call_id` - The unique identifier of the call
3283 ///
3284 /// # Returns
3285 ///
3286 /// Returns the current `AudioStreamConfig` or `None` if no stream is configured.
3287 ///
3288 /// # Examples
3289 ///
3290 /// ```rust
3291 /// # use rvoip_client_core::{ClientManager, call::CallId};
3292 /// # use std::sync::Arc;
3293 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3294 /// if let Some(config) = client.get_audio_stream_config(&call_id).await? {
3295 /// println!("Audio stream: {}Hz, {} channels, codec: {}",
3296 /// config.sample_rate, config.channels, config.codec);
3297 /// } else {
3298 /// println!("No audio stream configured for call");
3299 /// }
3300 /// # Ok(())
3301 /// # }
3302 /// ```
3303 pub async fn get_audio_stream_config(&self, call_id: &CallId) -> ClientResult<Option<rvoip_session_core::api::types::AudioStreamConfig>> {
3304 let session_id = self.session_mapping.get(call_id)
3305 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3306 .clone();
3307
3308 // Use session-core to get audio stream config
3309 MediaControl::get_audio_stream_config(&self.coordinator, &session_id)
3310 .await
3311 .map_err(|e| ClientError::CallSetupFailed {
3312 reason: format!("Failed to get audio stream config: {}", e)
3313 })
3314 }
3315
3316 /// Set audio stream configuration for a call
3317 ///
3318 /// Configures the audio streaming parameters for the specified call,
3319 /// including sample rate, channels, codec preferences, and audio processing settings.
3320 ///
3321 /// # Arguments
3322 ///
3323 /// * `call_id` - The unique identifier of the call
3324 /// * `config` - The audio stream configuration to apply
3325 ///
3326 /// # Returns
3327 ///
3328 /// Returns `Ok(())` if the configuration was applied successfully.
3329 ///
3330 /// # Examples
3331 ///
3332 /// ```rust
3333 /// # use rvoip_client_core::{ClientManager, call::CallId};
3334 /// # use rvoip_session_core::api::types::AudioStreamConfig;
3335 /// # use std::sync::Arc;
3336 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3337 /// // Configure high-quality audio stream
3338 /// let config = AudioStreamConfig {
3339 /// sample_rate: 48000,
3340 /// channels: 1,
3341 /// codec: "Opus".to_string(),
3342 /// frame_size_ms: 20,
3343 /// enable_aec: true,
3344 /// enable_agc: true,
3345 /// enable_vad: true,
3346 /// };
3347 ///
3348 /// client.set_audio_stream_config(&call_id, config).await?;
3349 /// # Ok(())
3350 /// # }
3351 /// ```
3352 pub async fn set_audio_stream_config(&self, call_id: &CallId, config: rvoip_session_core::api::types::AudioStreamConfig) -> ClientResult<()> {
3353 let session_id = self.session_mapping.get(call_id)
3354 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3355 .clone();
3356
3357 // Use session-core to set audio stream config
3358 MediaControl::set_audio_stream_config(&self.coordinator, &session_id, config)
3359 .await
3360 .map_err(|e| ClientError::CallSetupFailed {
3361 reason: format!("Failed to set audio stream config: {}", e)
3362 })
3363 }
3364
3365 /// Start audio streaming for a call
3366 ///
3367 /// Begins the audio streaming pipeline for the specified call, enabling
3368 /// real-time audio frame processing. This must be called before audio frames
3369 /// can be sent or received.
3370 ///
3371 /// # Arguments
3372 ///
3373 /// * `call_id` - The unique identifier of the call
3374 ///
3375 /// # Returns
3376 ///
3377 /// Returns `Ok(())` if the audio stream started successfully.
3378 ///
3379 /// # Examples
3380 ///
3381 /// ```rust
3382 /// # use rvoip_client_core::{ClientManager, call::CallId};
3383 /// # use rvoip_session_core::api::types::AudioStreamConfig;
3384 /// # use std::sync::Arc;
3385 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3386 /// // Configure and start audio streaming
3387 /// let config = AudioStreamConfig {
3388 /// sample_rate: 8000,
3389 /// channels: 1,
3390 /// codec: "PCMU".to_string(),
3391 /// frame_size_ms: 20,
3392 /// enable_aec: true,
3393 /// enable_agc: true,
3394 /// enable_vad: true,
3395 /// };
3396 ///
3397 /// client.set_audio_stream_config(&call_id, config).await?;
3398 /// client.start_audio_stream(&call_id).await?;
3399 /// println!("Audio streaming started for call {}", call_id);
3400 /// # Ok(())
3401 /// # }
3402 /// ```
3403 pub async fn start_audio_stream(&self, call_id: &CallId) -> ClientResult<()> {
3404 let session_id = self.session_mapping.get(call_id)
3405 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3406 .clone();
3407
3408 // Use session-core to start audio stream
3409 MediaControl::start_audio_stream(&self.coordinator, &session_id)
3410 .await
3411 .map_err(|e| ClientError::CallSetupFailed {
3412 reason: format!("Failed to start audio stream: {}", e)
3413 })
3414 }
3415
3416 /// Stop audio streaming for a call
3417 ///
3418 /// Stops the audio streaming pipeline for the specified call, disabling
3419 /// real-time audio frame processing. This cleans up resources and stops
3420 /// audio transmission.
3421 ///
3422 /// # Arguments
3423 ///
3424 /// * `call_id` - The unique identifier of the call
3425 ///
3426 /// # Returns
3427 ///
3428 /// Returns `Ok(())` if the audio stream stopped successfully.
3429 ///
3430 /// # Examples
3431 ///
3432 /// ```rust
3433 /// # use rvoip_client_core::{ClientManager, call::CallId};
3434 /// # use std::sync::Arc;
3435 /// # async fn example(client: Arc<ClientManager>, call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
3436 /// // Stop audio streaming
3437 /// client.stop_audio_stream(&call_id).await?;
3438 /// println!("Audio streaming stopped for call {}", call_id);
3439 /// # Ok(())
3440 /// # }
3441 /// ```
3442 pub async fn stop_audio_stream(&self, call_id: &CallId) -> ClientResult<()> {
3443 let session_id = self.session_mapping.get(call_id)
3444 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
3445 .clone();
3446
3447 // Use session-core to stop audio stream
3448 MediaControl::stop_audio_stream(&self.coordinator, &session_id)
3449 .await
3450 .map_err(|e| ClientError::CallSetupFailed {
3451 reason: format!("Failed to stop audio stream: {}", e)
3452 })
3453 }
3454}