1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/// Errors returned by [`Phone`](crate::Phone) and [`Call`](crate::Call) methods.
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
/// Operation requires an active SIP registration.
#[error("xphone: not registered")]
NotRegistered,
/// No call matching the given identifier was found.
#[error("xphone: call not found")]
CallNotFound,
/// The call or phone is not in a valid state for the requested operation.
#[error("xphone: invalid state for operation")]
InvalidState,
/// No RTP packets received within the configured media timeout.
#[error("xphone: RTP media timeout")]
MediaTimeout,
/// Outbound dial timed out before the remote party answered.
#[error("xphone: dial timeout exceeded before answer")]
DialTimeout,
/// All ports in the configured RTP port range are in use.
#[error("xphone: RTP port range exhausted")]
NoRtpPortAvailable,
/// SIP REGISTER request was rejected, or all retries were exhausted.
/// `code` is the last SIP status observed across retry attempts (`0` when
/// no response was received, e.g. transport failure). `reason` carries the
/// corresponding reason-phrase, or a transport error description when
/// `code == 0`.
#[error("{}", format_registration_failed(*code, reason))]
RegistrationFailed { code: u16, reason: String },
/// SIP REFER (blind transfer) was rejected or failed.
#[error("xphone: transfer failed")]
TransferFailed,
/// TLS transport was requested but no TLS configuration was provided.
#[error("xphone: TLS transport requires TLSConfig")]
TlsConfigRequired,
/// The supplied character is not a valid DTMF digit (0-9, *, #, A-D).
#[error("xphone: invalid DTMF digit")]
InvalidDtmfDigit,
/// `send_dtmf` called in [`DtmfMode::Rfc4733`](crate::config::DtmfMode)
/// but RFC 4733 telephone-event (PT 101) was not negotiated with the
/// remote. Switch to [`DtmfMode::SipInfo`](crate::config::DtmfMode) or
/// [`DtmfMode::Both`](crate::config::DtmfMode) to fall back to SIP INFO.
#[error("xphone: RFC 4733 DTMF not negotiated with remote")]
DtmfNotNegotiated,
/// Mute was requested but the call is already muted.
#[error("xphone: already muted")]
AlreadyMuted,
/// Unmute was requested but the call is not muted.
#[error("xphone: not muted")]
NotMuted,
/// Video mute was requested but video is already muted.
#[error("xphone: video already muted")]
VideoAlreadyMuted,
/// Video unmute was requested but video is not muted.
#[error("xphone: video not muted")]
VideoNotMuted,
/// Operation requires a video stream but none is active.
#[error("xphone: no video stream")]
NoVideoStream,
/// [`Phone::connect`](crate::Phone) called while already connected.
#[error("xphone: already connected")]
AlreadyConnected,
/// Operation requires an active connection but the phone is disconnected.
#[error("xphone: not connected")]
NotConnected,
/// Configuration is missing the required SIP server host.
#[error("xphone: Host is required")]
HostRequired,
/// SDP parsing or negotiation error with a descriptive message.
#[error("xphone: {0}")]
Sdp(String),
/// Catch-all for errors that do not fit other variants.
#[error("xphone: {0}")]
Other(String),
}
/// Convenience alias for `std::result::Result<T, Error>`.
pub type Result<T> = std::result::Result<T, Error>;
/// Formats [`Error::RegistrationFailed`] suppressing `code` when it is `0`
/// (pure transport failure) and suppressing `reason` when empty.
fn format_registration_failed(code: u16, reason: &str) -> String {
match (code, reason.is_empty()) {
(0, true) => "xphone: registration failed".into(),
(0, false) => format!("xphone: registration failed: {reason}"),
(c, true) => format!("xphone: registration failed: {c}"),
(c, false) => format!("xphone: registration failed: {c} {reason}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display() {
assert_eq!(Error::NotRegistered.to_string(), "xphone: not registered");
assert_eq!(
Error::InvalidState.to_string(),
"xphone: invalid state for operation"
);
assert_eq!(
Error::InvalidDtmfDigit.to_string(),
"xphone: invalid DTMF digit"
);
}
#[test]
fn registration_failed_display_skips_zero_and_empty() {
let full = Error::RegistrationFailed {
code: 403,
reason: "Forbidden".into(),
};
assert_eq!(
full.to_string(),
"xphone: registration failed: 403 Forbidden"
);
let transport = Error::RegistrationFailed {
code: 0,
reason: "transport: timeout".into(),
};
assert_eq!(
transport.to_string(),
"xphone: registration failed: transport: timeout"
);
let bare = Error::RegistrationFailed {
code: 0,
reason: String::new(),
};
assert_eq!(bare.to_string(), "xphone: registration failed");
}
#[test]
fn error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Error>();
}
#[test]
fn result_alias_works() {
let ok: Result<i32> = Ok(42);
assert!(matches!(ok, Ok(42)));
let err: Result<i32> = Err(Error::NotRegistered);
assert!(err.is_err());
}
}