Skip to main content

ringo_core/
event.rs

1use std::collections::HashMap;
2
3/// Headers of each INVITE seen in a trace, keyed by SIP `Call-ID`. First
4/// INVITE per Call-ID wins (the call-establishing one).
5pub type InviteHeaders = HashMap<String, Vec<(String, String)>>;
6
7/// RTP media quality for a call (backend-neutral). `mos` is an estimated Mean
8/// Opinion Score (1.0 = worst … 4.5 = best).
9#[derive(Debug, Clone, Copy)]
10pub struct MediaStats {
11    /// Round-trip time in milliseconds.
12    pub rtt_ms: f64,
13    /// Receive-side inter-arrival jitter in milliseconds.
14    pub jitter_ms: f64,
15    /// Cumulative received RTP packets lost.
16    pub rx_lost: i32,
17    /// Receive-side packet loss as a percentage.
18    pub packet_loss_pct: f64,
19    /// Estimated MOS (1.0–4.5).
20    pub mos: f64,
21}
22
23/// The negotiated audio codec on a call (backend-neutral).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CodecInfo {
26    /// Codec name as the backend reports it, e.g. `"opus"`, `"PCMU"`.
27    pub name: String,
28    /// Codec sample rate in Hz.
29    pub srate: u32,
30    /// Channel count (1 = mono, 2 = stereo).
31    pub ch: u8,
32}
33
34#[derive(Debug, Clone)]
35pub enum AppEvent {
36    Registering {
37        account: String,
38    },
39    RegisterOk {
40        account: String,
41    },
42    RegisterFailed {
43        reason: String,
44    },
45    Unregistered {
46        account: String,
47    },
48    CallIncoming {
49        call_id: String,
50        number: String,
51        display_name: Option<String>,
52    },
53    CallOutgoing {
54        call_id: String,
55        number: String,
56    },
57    CallRinging {
58        call_id: String,
59    },
60    CallEstablished {
61        call_id: String,
62    },
63    CallClosed {
64        call_id: String,
65        reason: String,
66        error: bool,
67    },
68    CallDeflected {
69        from: String,
70        display_name: Option<String>,
71        target: String,
72    },
73    VoicemailStatus {
74        waiting: bool,
75        new_count: u32,
76    },
77    Response {
78        ok: bool,
79        data: String,
80    },
81    Unknown {
82        class: String,
83        type_: String,
84    },
85    BackendConnectFailed {
86        reason: String,
87    },
88}
89
90/// Whether a call-closed reason indicates an error (not a normal close).
91/// Backend-neutral: shared by all backends that produce SIP reason strings.
92pub fn is_error_reason(reason: &str) -> bool {
93    if reason.is_empty() {
94        return false;
95    }
96    const NORMAL: &[&str] = &[
97        "Connection reset by peer",
98        "Connection closed",
99        "Rejected by user",
100        "Call transfered",
101    ];
102    !NORMAL
103        .iter()
104        .any(|n| reason.to_lowercase().starts_with(&n.to_lowercase()))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    // ── is_error_reason ────────────────────────────────────────────────────────
112
113    #[test]
114    fn empty_reason_is_not_error() {
115        assert!(!is_error_reason(""));
116    }
117
118    #[test]
119    fn connection_reset_is_not_error() {
120        assert!(!is_error_reason("Connection reset by peer"));
121    }
122
123    #[test]
124    fn connection_reset_with_errno_is_not_error() {
125        assert!(!is_error_reason("Connection reset by peer [104]"));
126    }
127
128    #[test]
129    fn connection_closed_is_not_error() {
130        assert!(!is_error_reason("Connection closed"));
131    }
132
133    #[test]
134    fn rejected_by_user_is_not_error() {
135        assert!(!is_error_reason("Rejected by user"));
136    }
137
138    #[test]
139    fn sip_busy_is_error() {
140        assert!(is_error_reason("486 Busy Here"));
141    }
142
143    #[test]
144    fn sip_not_found_is_error() {
145        assert!(is_error_reason("404 Not Found"));
146    }
147}