use std::collections::HashMap;
use std::time::Duration;
use super::proto::models::{self, ErrorCode, WebsocketReconnectStrategy};
pub type Result<T> = std::result::Result<T, RtcError>;
pub type ErrorFromResponse = crate::error::ApiError;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RtcError {
#[error("transport error: {0}")]
Transport(#[source] reqwest::Error),
#[error(transparent)]
Twirp(#[from] TwirpError),
#[error("sfu signal error (code {code}): {message}")]
Signal {
code: i32,
message: String,
should_retry: bool,
},
#[error("protobuf decode error: {0}")]
Decode(#[from] prost::DecodeError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("websocket error: {0}")]
WebSocket(#[source] Box<tokio_tungstenite::tungstenite::Error>),
#[error("invalid url: {0}")]
Url(String),
#[error("connection closed: {0}")]
Closed(String),
#[error("coordinator connection error: {0}")]
Coordinator(String),
#[error(transparent)]
Api(Box<ErrorFromResponse>),
#[error(transparent)]
Join(#[from] SfuJoinError),
#[error(transparent)]
Timeout(#[from] SfuTimeoutError),
#[error(transparent)]
Negotiation(#[from] NegotiationError),
#[error(transparent)]
WsConnection(#[from] WsConnectionError),
#[error("webrtc error: {0}")]
Webrtc(#[source] Box<webrtc::error::Error>),
#[error("illegal state: {0}")]
IllegalState(String),
#[error("permission denied: missing `{capability}` capability")]
PermissionDenied {
capability: &'static str,
},
#[error("media error: {0}")]
Media(String),
#[error("{input} is unsupported for server-managed layered video; use write_i420")]
UnsupportedLayeredInput {
input: &'static str,
},
#[error("server-managed layering is unsupported for {codec} {track_type:?}")]
UnsupportedVideoLayering {
codec: String,
track_type: models::TrackType,
},
#[error(
"pcm queue overflow: dropped {dropped_samples} oldest samples \
(capacity {capacity_samples})"
)]
PcmQueueOverflow {
dropped_samples: usize,
capacity_samples: usize,
},
#[error("token error: {0}")]
Token(String),
#[error(transparent)]
TokenValidation(#[from] crate::error::TokenError),
#[error("{boundary} exceeded {limit} bytes (received at least {actual})")]
SizeLimitExceeded {
boundary: &'static str,
limit: usize,
actual: usize,
},
}
impl From<reqwest::Error> for RtcError {
fn from(e: reqwest::Error) -> Self {
RtcError::Transport(e)
}
}
impl From<tokio_tungstenite::tungstenite::Error> for RtcError {
fn from(e: tokio_tungstenite::tungstenite::Error) -> Self {
RtcError::WebSocket(Box::new(e))
}
}
impl From<webrtc::error::Error> for RtcError {
fn from(e: webrtc::error::Error) -> Self {
RtcError::Webrtc(Box::new(e))
}
}
impl From<ErrorFromResponse> for RtcError {
fn from(e: ErrorFromResponse) -> Self {
RtcError::Api(Box::new(e))
}
}
impl From<crate::error::Error> for RtcError {
fn from(e: crate::error::Error) -> Self {
match e {
crate::error::Error::Api(api) => RtcError::Api(api),
crate::error::Error::Transport(t) => RtcError::Transport(t),
crate::error::Error::Serde(s) => RtcError::Json(s),
crate::error::Error::Token(t) => RtcError::Token(t),
crate::error::Error::TokenValidation(t) => RtcError::TokenValidation(t),
crate::error::Error::ResponseTooLarge { limit, actual } => {
RtcError::SizeLimitExceeded {
boundary: "coordinator HTTP response body",
limit,
actual,
}
}
other => RtcError::Coordinator(other.to_string()),
}
}
}
impl RtcError {
pub(crate) fn from_websocket_with_boundary(
error: tokio_tungstenite::tungstenite::Error,
boundary: &'static str,
) -> Self {
use tokio_tungstenite::tungstenite::error::CapacityError;
match error {
tokio_tungstenite::tungstenite::Error::Capacity(CapacityError::MessageTooLong {
size,
max_size,
}) => Self::SizeLimitExceeded {
boundary,
limit: max_size,
actual: size,
},
other => Self::from(other),
}
}
pub(crate) fn from_signal_error(error: Option<models::Error>) -> Result<()> {
match error {
Some(e) if e.code != ErrorCode::Unspecified as i32 => Err(RtcError::Signal {
code: e.code,
message: e.message,
should_retry: e.should_retry,
}),
_ => Ok(()),
}
}
pub fn is_unrecoverable(&self) -> bool {
match self {
RtcError::Api(e) => e.unrecoverable,
RtcError::Join(e) => e.is_unrecoverable(),
_ => false,
}
}
pub fn is_join_error_code(&self) -> bool {
match self {
RtcError::Join(e) => e.is_join_error_code(),
RtcError::Signal { code, .. } => is_join_error_code(*code),
_ => false,
}
}
pub(crate) fn is_token_expired(&self) -> bool {
match self {
RtcError::Api(error) => error.code == 40,
RtcError::Signal { code, .. } => *code == ErrorCode::Unauthenticated as i32,
RtcError::Twirp(error) => error.code == "unauthenticated",
RtcError::TokenValidation(crate::error::TokenError::Expired { .. })
| RtcError::TokenValidation(crate::error::TokenError::ExpiredByServer) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("sfu join error (code {code}, strategy {reconnect_strategy}): {message}")]
#[non_exhaustive]
pub struct SfuJoinError {
pub code: i32,
pub message: String,
pub should_retry: bool,
pub reconnect_strategy: i32,
}
impl SfuJoinError {
pub fn from_event(error: Option<models::Error>, reconnect_strategy: i32) -> Self {
let error = error.unwrap_or_default();
Self {
code: error.code,
message: error.message,
should_retry: error.should_retry,
reconnect_strategy,
}
}
pub fn is_unrecoverable(&self) -> bool {
self.reconnect_strategy == WebsocketReconnectStrategy::Disconnect as i32
}
pub fn is_join_error_code(&self) -> bool {
is_join_error_code(self.code)
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("sfu timeout waiting for {what} after {}ms", timeout.as_millis())]
pub struct SfuTimeoutError {
pub what: String,
pub timeout: Duration,
}
impl SfuTimeoutError {
pub fn new(what: impl Into<String>, timeout: Duration) -> Self {
Self {
what: what.into(),
timeout,
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("negotiation failed: {0}")]
pub struct NegotiationError(pub String);
#[derive(Debug, Clone, thiserror::Error)]
#[error("websocket connection error (ws_failure={is_ws_failure}): {message}")]
pub struct WsConnectionError {
pub message: String,
pub is_ws_failure: bool,
}
impl WsConnectionError {
pub fn transport(message: impl Into<String>) -> Self {
Self {
message: message.into(),
is_ws_failure: true,
}
}
pub fn permanent(message: impl Into<String>) -> Self {
Self {
message: message.into(),
is_ws_failure: false,
}
}
}
pub fn is_join_error_code(code: i32) -> bool {
code == ErrorCode::SfuFull as i32
|| code == ErrorCode::SfuShuttingDown as i32
|| code == ErrorCode::CallParticipantLimitReached as i32
}
#[derive(Debug, Clone, serde::Deserialize, thiserror::Error)]
#[error("twirp error [{code}]: {msg}")]
#[non_exhaustive]
pub struct TwirpError {
pub code: String,
#[serde(default)]
pub msg: String,
#[serde(default)]
pub meta: HashMap<String, String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_error_codes_match_sfu() {
assert!(is_join_error_code(ErrorCode::SfuFull as i32));
assert!(is_join_error_code(ErrorCode::SfuShuttingDown as i32));
assert!(is_join_error_code(
ErrorCode::CallParticipantLimitReached as i32
));
assert!(!is_join_error_code(ErrorCode::ParticipantSignalLost as i32));
assert!(!is_join_error_code(ErrorCode::Unspecified as i32));
}
#[test]
fn sfu_join_error_unrecoverable_only_on_disconnect() {
let disconnect = SfuJoinError::from_event(
Some(models::Error {
code: ErrorCode::CallParticipantLimitReached as i32,
message: "full".into(),
should_retry: false,
}),
WebsocketReconnectStrategy::Disconnect as i32,
);
assert!(disconnect.is_unrecoverable());
assert!(disconnect.is_join_error_code());
let rejoin = SfuJoinError::from_event(
Some(models::Error {
code: ErrorCode::SfuFull as i32,
message: "full".into(),
should_retry: true,
}),
WebsocketReconnectStrategy::Rejoin as i32,
);
assert!(!rejoin.is_unrecoverable());
assert!(rejoin.is_join_error_code());
}
#[test]
fn api_unrecoverable_flows_through_rtc_error() {
let api = ErrorFromResponse {
unrecoverable: true,
..Default::default()
};
let err: RtcError = api.into();
assert!(err.is_unrecoverable());
}
#[test]
fn ws_failure_flag() {
assert!(WsConnectionError::transport("drop").is_ws_failure);
assert!(!WsConnectionError::permanent("bad").is_ws_failure);
}
#[test]
fn websocket_capacity_failure_maps_to_typed_size_error() {
use tokio_tungstenite::tungstenite::error::CapacityError;
let error =
tokio_tungstenite::tungstenite::Error::Capacity(CapacityError::MessageTooLong {
size: 65,
max_size: 64,
});
assert!(matches!(
RtcError::from_websocket_with_boundary(error, "test WebSocket message"),
RtcError::SizeLimitExceeded {
boundary: "test WebSocket message",
limit: 64,
actual: 65,
}
));
}
}