use crate::errors::Result;
use crate::types::CallState;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Instant;
use uuid::Uuid;
pub use rvoip_sip_core::StatusCode;
pub use crate::errors::SessionError as Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionId(pub String);
impl SessionId {
pub fn new() -> Self {
Self(format!("sess_{}", Uuid::new_v4()))
}
pub fn from_string(id: String) -> Self {
Self(id)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionRole {
UAC,
UAS,
}
pub type Session = CallSession;
#[derive(Debug, Clone)]
pub struct PreparedCall {
pub session_id: SessionId,
pub from: String,
pub to: String,
pub sdp_offer: String,
pub local_rtp_port: u16,
}
#[derive(Debug, Clone)]
pub struct CallSession {
pub id: SessionId,
pub from: String,
pub to: String,
pub state: CallState,
pub started_at: Option<Instant>,
pub sip_call_id: Option<String>,
}
impl CallSession {
pub fn id(&self) -> &SessionId {
&self.id
}
pub fn state(&self) -> &CallState {
&self.state
}
pub fn is_active(&self) -> bool {
matches!(self.state, CallState::Active)
}
pub async fn wait_for_answer(&self) -> Result<()> {
Err(crate::errors::SessionError::Other(
"Use SessionManager::wait_for_answer() method instead".to_string(),
))
}
pub async fn hold(&self) -> Result<()> {
Err(crate::errors::SessionError::Other(
"Use SessionManager::hold_session() method instead".to_string(),
))
}
pub async fn resume(&self) -> Result<()> {
Err(crate::errors::SessionError::Other(
"Use SessionManager::resume_session() method instead".to_string(),
))
}
pub async fn transfer(&self, _target: &str) -> Result<()> {
Err(crate::errors::SessionError::Other(
"Use SessionManager::transfer_session() method instead".to_string(),
))
}
pub async fn terminate(&self) -> Result<()> {
Err(crate::errors::SessionError::Other(
"Use SessionManager::terminate_session() method instead".to_string(),
))
}
pub async fn start_recording(&self) -> Result<()> {
Ok(())
}
pub async fn stop_recording(&self) -> Result<()> {
Ok(())
}
pub async fn play_audio(&self, _file: &str) -> Result<()> {
Ok(())
}
pub async fn start_media(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct IncomingCall {
pub id: SessionId,
pub from: String,
pub to: String,
pub sdp: Option<String>,
pub headers: std::collections::HashMap<String, String>,
pub received_at: Instant,
pub sip_call_id: Option<String>,
}
impl IncomingCall {
#[doc(hidden)]
pub fn new_test(
id: SessionId,
from: String,
to: String,
sdp: Option<String>,
headers: std::collections::HashMap<String, String>,
sip_call_id: Option<String>,
) -> Self {
Self {
id,
from,
to,
sdp,
headers,
received_at: Instant::now(),
sip_call_id,
}
}
pub fn caller(&self) -> &str {
&self.from
}
pub fn called(&self) -> &str {
&self.to
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallDecision {
Accept(Option<String>),
Reject(String),
Defer,
Forward(String),
}
impl CallDecision {
pub fn reject_with_code(status_code: StatusCode, reason: Option<String>) -> Self {
CallDecision::Reject(reason.unwrap_or_else(|| status_code.to_string()))
}
pub fn accept(sdp: Option<String>) -> Self {
CallDecision::Accept(sdp)
}
pub fn defer() -> Self {
CallDecision::Defer
}
pub fn forward(destination: impl Into<String>) -> Self {
CallDecision::Forward(destination.into())
}
}
#[derive(Debug, Clone)]
pub struct SessionStats {
pub total_sessions: usize,
pub active_sessions: usize,
pub failed_sessions: usize,
pub average_duration: Option<std::time::Duration>,
}
#[derive(Debug, Clone)]
pub struct MediaInfo {
pub local_sdp: Option<String>,
pub remote_sdp: Option<String>,
pub local_rtp_port: Option<u16>,
pub remote_rtp_port: Option<u16>,
pub codec: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CallDirection {
Outgoing,
Incoming,
}
#[derive(Debug, Clone)]
pub enum TerminationReason {
LocalHangup,
RemoteHangup,
Rejected(String),
Error(String),
Timeout,
}
impl fmt::Display for TerminationReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TerminationReason::LocalHangup => write!(f, "Local hangup"),
TerminationReason::RemoteHangup => write!(f, "Remote hangup"),
TerminationReason::Rejected(reason) => write!(f, "Rejected: {}", reason),
TerminationReason::Error(error) => write!(f, "Error: {}", error),
TerminationReason::Timeout => write!(f, "Timeout"),
}
}
}
#[derive(Debug, Clone)]
pub struct SdpInfo {
pub ip: String,
pub port: u16,
pub codecs: Vec<String>,
}
pub fn parse_sdp_connection(sdp: &str) -> Result<SdpInfo> {
let mut ip = None;
let mut port = None;
let mut codecs = Vec::new();
for line in sdp.lines() {
if line.starts_with("c=IN IP4 ") {
ip = line.strip_prefix("c=IN IP4 ").map(|s| s.to_string());
} else if line.starts_with("m=audio ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() > 1 {
port = parts[1].parse().ok();
}
if parts.len() > 3 {
for codec in &parts[3..] {
codecs.push(codec.to_string());
}
}
} else if line.starts_with("a=rtpmap:") {
if let Some(codec_info) = line.strip_prefix("a=rtpmap:") {
let parts: Vec<&str> = codec_info.split_whitespace().collect();
if parts.len() >= 2 {
if let Some(codec_name) = parts[1].split('/').next() {
codecs.push(codec_name.to_string());
}
}
}
}
}
match (ip, port) {
(Some(ip), Some(port)) => Ok(SdpInfo { ip, port, codecs }),
_ => Err(crate::errors::SessionError::MediaIntegration {
reason: "Failed to parse SDP connection information".to_string(),
}),
}
}
pub use rvoip_media_core::types::AudioFrame;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioStreamConfig {
pub sample_rate: u32,
pub channels: u8,
pub codec: String,
pub frame_size_ms: u32,
pub enable_aec: bool,
pub enable_agc: bool,
pub enable_vad: bool,
}
impl Default for AudioStreamConfig {
fn default() -> Self {
Self {
sample_rate: 8000, channels: 1, codec: "PCMU".to_string(), frame_size_ms: 20, enable_aec: true,
enable_agc: true,
enable_vad: true,
}
}
}
impl AudioStreamConfig {
pub fn new(sample_rate: u32, channels: u8, codec: impl Into<String>) -> Self {
Self {
sample_rate,
channels,
codec: codec.into(),
..Default::default()
}
}
pub fn frame_size_samples(&self) -> usize {
(self.sample_rate as usize * self.frame_size_ms as usize) / 1000
}
pub fn frame_size_bytes(&self) -> usize {
self.frame_size_samples() * self.channels as usize * 2 }
pub fn telephony() -> Self {
Self::default()
}
pub fn wideband() -> Self {
Self {
sample_rate: 16000,
codec: "Opus".to_string(),
..Default::default()
}
}
pub fn high_quality() -> Self {
Self {
sample_rate: 48000,
channels: 2,
codec: "Opus".to_string(),
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct DialogIdentity {
pub call_id: String,
pub local_tag: Option<String>,
pub remote_tag: Option<String>,
}
impl DialogIdentity {
pub fn to_replaces_value(&self) -> Option<String> {
let (local, remote) = (self.local_tag.as_ref()?, self.remote_tag.as_ref()?);
Some(format!(
"{};to-tag={};from-tag={}",
self.call_id, remote, local
))
}
}
#[derive(Debug)]
pub struct AudioFrameSubscriber {
session_id: SessionId,
receiver: tokio::sync::mpsc::Receiver<AudioFrame>,
}
impl AudioFrameSubscriber {
pub fn new(session_id: SessionId, receiver: tokio::sync::mpsc::Receiver<AudioFrame>) -> Self {
Self {
session_id,
receiver,
}
}
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub async fn recv(&mut self) -> Option<AudioFrame> {
self.receiver.recv().await
}
pub fn try_recv(
&mut self,
) -> std::result::Result<AudioFrame, tokio::sync::mpsc::error::TryRecvError> {
self.receiver.try_recv()
}
pub fn is_connected(&self) -> bool {
!self.receiver.is_closed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_id_creation() {
let id1 = SessionId::new();
let id2 = SessionId::new();
assert_ne!(id1, id2);
assert!(id1.0.starts_with("sess_"));
}
#[test]
fn test_call_state_display() {
use crate::types::FailureReason;
assert_eq!(CallState::Active.to_string(), "Active");
assert_eq!(
CallState::Failed(FailureReason::Timeout).to_string(),
"Failed(Timeout)"
);
}
}