kittycad_modeling_cmds/
websocket.rs

1//! Types for the websocket server.
2
3use std::{borrow::Cow, collections::HashMap};
4
5use parse_display_derive::{Display, FromStr};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "slog")]
9use slog::{Record, Serializer, KV};
10use uuid::Uuid;
11
12use crate::{
13    id::ModelingCmdId,
14    ok_response::OkModelingCmdResponse,
15    shared::{EngineErrorCode, ExportFile},
16    ModelingCmd,
17};
18
19/// The type of error sent by the KittyCAD API.
20#[derive(Display, FromStr, Copy, Eq, PartialEq, Debug, JsonSchema, Deserialize, Serialize, Clone, Ord, PartialOrd)]
21#[serde(rename_all = "snake_case")]
22pub enum ErrorCode {
23    /// Graphics engine failed to complete request, consider retrying
24    InternalEngine,
25    /// API failed to complete request, consider retrying
26    InternalApi,
27    /// User requested something geometrically or graphically impossible.
28    /// Don't retry this request, as it's inherently impossible. Instead, read the error message
29    /// and change your request.
30    BadRequest,
31    /// Auth token is missing from the request
32    AuthTokenMissing,
33    /// Auth token is invalid in some way (expired, incorrect format, etc)
34    AuthTokenInvalid,
35    /// Client sent invalid JSON.
36    InvalidJson,
37    /// Client sent invalid BSON.
38    InvalidBson,
39    /// Client sent a message which is not accepted over this protocol.
40    WrongProtocol,
41    /// Problem sending data between client and KittyCAD API.
42    ConnectionProblem,
43    /// Client sent a Websocket message type which the KittyCAD API does not handle.
44    MessageTypeNotAccepted,
45    /// Client sent a Websocket message intended for WebRTC but it was configured as a WebRTC
46    /// connection.
47    MessageTypeNotAcceptedForWebRTC,
48}
49
50/// Because [`EngineErrorCode`] is a subset of [`ErrorCode`], you can trivially map
51/// each variant of the former to a variant of the latter.
52impl From<EngineErrorCode> for ErrorCode {
53    fn from(value: EngineErrorCode) -> Self {
54        match value {
55            EngineErrorCode::InternalEngine => Self::InternalEngine,
56            EngineErrorCode::BadRequest => Self::BadRequest,
57        }
58    }
59}
60
61/// A graphics command submitted to the KittyCAD engine via the Modeling API.
62#[derive(Debug, Clone, Deserialize, Serialize)]
63#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
64pub struct ModelingCmdReq {
65    /// Which command to submit to the Kittycad engine.
66    pub cmd: ModelingCmd,
67    /// ID of command being submitted.
68    pub cmd_id: ModelingCmdId,
69}
70
71/// The websocket messages the server receives.
72#[derive(Serialize, Deserialize, Debug, Clone)]
73#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
74#[serde(tag = "type", rename_all = "snake_case")]
75pub enum WebSocketRequest {
76    /// The trickle ICE candidate request.
77    // We box these to avoid a huge size difference between variants.
78    TrickleIce {
79        /// Information about the ICE candidate.
80        candidate: Box<RtcIceCandidateInit>,
81    },
82    /// The SDP offer request.
83    SdpOffer {
84        /// The session description.
85        offer: Box<RtcSessionDescription>,
86    },
87    /// The modeling command request.
88    ModelingCmdReq(ModelingCmdReq),
89    /// A sequence of modeling requests. If any request fails, following requests will not be tried.
90    ModelingCmdBatchReq(ModelingBatch),
91    /// The client-to-server Ping to ensure the WebSocket stays alive.
92    Ping {},
93
94    /// The response to a metrics collection request from the server.
95    MetricsResponse {
96        /// Collected metrics from the Client's end of the engine connection.
97        metrics: Box<ClientMetrics>,
98    },
99
100    /// Authentication header request.
101    Headers {
102        /// The authentication header.
103        headers: HashMap<String, String>,
104    },
105}
106
107/// A sequence of modeling requests. If any request fails, following requests will not be tried.
108#[derive(Serialize, Deserialize, Debug, Clone)]
109#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
110#[serde(rename_all = "snake_case")]
111pub struct ModelingBatch {
112    /// A sequence of modeling requests. If any request fails, following requests will not be tried.
113    pub requests: Vec<ModelingCmdReq>,
114    /// ID of batch being submitted.
115    /// Each request has their own individual ModelingCmdId, but this is the
116    /// ID of the overall batch.
117    pub batch_id: ModelingCmdId,
118    /// If false or omitted, responses to each batch command will just be Ok(()).
119    /// If true, responses will be the actual response data for that modeling command.
120    #[serde(default)]
121    pub responses: bool,
122}
123
124impl std::default::Default for ModelingBatch {
125    /// Creates a batch with 0 requests and a random ID.
126    fn default() -> Self {
127        Self {
128            requests: Default::default(),
129            batch_id: Uuid::new_v4().into(),
130            responses: false,
131        }
132    }
133}
134
135impl ModelingBatch {
136    /// Add a new modeling command to the end of this batch.
137    pub fn push(&mut self, req: ModelingCmdReq) {
138        self.requests.push(req);
139    }
140
141    /// Are there any requests in the batch?
142    pub fn is_empty(&self) -> bool {
143        self.requests.is_empty()
144    }
145}
146
147/// Representation of an ICE server used for STUN/TURN
148/// Used to initiate WebRTC connections
149/// based on <https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer>
150#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone)]
151pub struct IceServer {
152    /// URLs for a given STUN/TURN server.
153    /// IceServer urls can either be a string or an array of strings
154    /// But, we choose to always convert to an array of strings for consistency
155    pub urls: Vec<String>,
156    /// Credentials for a given TURN server.
157    pub credential: Option<String>,
158    /// Username for a given TURN server.
159    pub username: Option<String>,
160}
161
162/// The websocket messages this server sends.
163#[derive(Serialize, Deserialize, Debug, Clone)]
164#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
165#[serde(tag = "type", content = "data", rename_all = "snake_case")]
166pub enum OkWebSocketResponseData {
167    /// Information about the ICE servers.
168    IceServerInfo {
169        /// Information about the ICE servers.
170        ice_servers: Vec<IceServer>,
171    },
172    /// The trickle ICE candidate response.
173    // We box these to avoid a huge size difference between variants.
174    TrickleIce {
175        /// Information about the ICE candidate.
176        candidate: Box<RtcIceCandidateInit>,
177    },
178    /// The SDP answer response.
179    SdpAnswer {
180        /// The session description.
181        answer: Box<RtcSessionDescription>,
182    },
183    /// The modeling command response.
184    Modeling {
185        /// The result of the command.
186        modeling_response: OkModelingCmdResponse,
187    },
188    /// Response to a ModelingBatch.
189    ModelingBatch {
190        /// For each request in the batch,
191        /// maps its ID to the request's outcome.
192        responses: HashMap<ModelingCmdId, BatchResponse>,
193    },
194    /// The exported files.
195    Export {
196        /// The exported files
197        files: Vec<RawFile>,
198    },
199
200    /// Request a collection of metrics, to include WebRTC.
201    MetricsRequest {},
202
203    /// Data about the Modeling Session (application-level).
204    ModelingSessionData {
205        /// Data about the Modeling Session (application-level).
206        session: ModelingSessionData,
207    },
208
209    /// Pong response to a Ping message.
210    Pong {},
211}
212
213/// Successful Websocket response.
214#[derive(Debug, Serialize, Deserialize, Clone)]
215#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
216#[serde(rename_all = "snake_case")]
217pub struct SuccessWebSocketResponse {
218    /// Always true
219    pub success: bool,
220    /// Which request this is a response to.
221    /// If the request was a modeling command, this is the modeling command ID.
222    /// If no request ID was sent, this will be null.
223    pub request_id: Option<Uuid>,
224    /// The data sent with a successful response.
225    /// This will be flattened into a 'type' and 'data' field.
226    pub resp: OkWebSocketResponseData,
227}
228
229/// Unsuccessful Websocket response.
230#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
231#[serde(rename_all = "snake_case")]
232pub struct FailureWebSocketResponse {
233    /// Always false
234    pub success: bool,
235    /// Which request this is a response to.
236    /// If the request was a modeling command, this is the modeling command ID.
237    /// If no request ID was sent, this will be null.
238    pub request_id: Option<Uuid>,
239    /// The errors that occurred.
240    pub errors: Vec<ApiError>,
241}
242
243/// Websocket responses can either be successful or unsuccessful.
244/// Slightly different schemas in either case.
245#[derive(Debug, Serialize, Deserialize, Clone)]
246#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
247#[serde(rename_all = "snake_case", untagged)]
248pub enum WebSocketResponse {
249    /// Response sent when a request succeeded.
250    Success(SuccessWebSocketResponse),
251    /// Response sent when a request did not succeed.
252    Failure(FailureWebSocketResponse),
253}
254
255/// Websocket responses can either be successful or unsuccessful.
256/// Slightly different schemas in either case.
257#[derive(Debug, Serialize, Deserialize, Clone)]
258#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
259#[serde(rename_all = "snake_case", untagged)]
260pub enum BatchResponse {
261    /// Response sent when a request succeeded.
262    Success {
263        /// Response to the modeling command.
264        response: OkModelingCmdResponse,
265    },
266    /// Response sent when a request did not succeed.
267    Failure {
268        /// Errors that occurred during the modeling command.
269        errors: Vec<ApiError>,
270    },
271}
272
273impl WebSocketResponse {
274    /// Make a new success response.
275    pub fn success(request_id: Option<Uuid>, resp: OkWebSocketResponseData) -> Self {
276        Self::Success(SuccessWebSocketResponse {
277            success: true,
278            request_id,
279            resp,
280        })
281    }
282
283    /// Make a new failure response.
284    pub fn failure(request_id: Option<Uuid>, errors: Vec<ApiError>) -> Self {
285        Self::Failure(FailureWebSocketResponse {
286            success: false,
287            request_id,
288            errors,
289        })
290    }
291
292    /// Did the request succeed?
293    pub fn is_success(&self) -> bool {
294        matches!(self, Self::Success(_))
295    }
296
297    /// Did the request fail?
298    pub fn is_failure(&self) -> bool {
299        matches!(self, Self::Failure(_))
300    }
301
302    /// Get the ID of whichever request this response is for.
303    pub fn request_id(&self) -> Option<Uuid> {
304        match self {
305            WebSocketResponse::Success(x) => x.request_id,
306            WebSocketResponse::Failure(x) => x.request_id,
307        }
308    }
309}
310
311/// A raw file with unencoded contents to be passed over binary websockets.
312/// When raw files come back for exports it is sent as binary/bson, not text/json.
313#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
314pub struct RawFile {
315    /// The name of the file.
316    pub name: String,
317    /// The contents of the file.
318    #[serde(
319        serialize_with = "serde_bytes::serialize",
320        deserialize_with = "serde_bytes::deserialize"
321    )]
322    pub contents: Vec<u8>,
323}
324
325impl From<ExportFile> for RawFile {
326    fn from(f: ExportFile) -> Self {
327        Self {
328            name: f.name,
329            contents: f.contents.0,
330        }
331    }
332}
333
334/// An error with an internal message for logging.
335#[derive(Debug, Serialize, Deserialize, JsonSchema)]
336pub struct LoggableApiError {
337    /// The error shown to users
338    pub error: ApiError,
339    /// The string logged internally
340    pub msg_internal: Option<Cow<'static, str>>,
341}
342
343#[cfg(feature = "slog")]
344impl KV for LoggableApiError {
345    fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
346        if let Some(ref msg_internal) = self.msg_internal {
347            serializer.emit_str("msg_internal", msg_internal)?;
348        }
349        serializer.emit_str("msg_external", &self.error.message)?;
350        serializer.emit_str("error_code", &self.error.error_code.to_string())
351    }
352}
353
354/// An error.
355#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
356pub struct ApiError {
357    /// The error code.
358    pub error_code: ErrorCode,
359    /// The error message.
360    pub message: String,
361}
362
363impl ApiError {
364    /// Convert to a `LoggableApiError` with no internal message.
365    pub fn no_internal_message(self) -> LoggableApiError {
366        LoggableApiError {
367            error: self,
368            msg_internal: None,
369        }
370    }
371    /// Add an internal log message to this error.
372    pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
373        LoggableApiError {
374            error: self,
375            msg_internal: Some(msg_internal),
376        }
377    }
378
379    /// Should the internal error message be logged?
380    pub fn should_log_internal_message(&self) -> bool {
381        use ErrorCode as Code;
382        match self.error_code {
383            // Internal errors should always be logged, as they're problems with KittyCAD programming
384            Code::InternalEngine | Code::InternalApi => true,
385            // The user did something wrong, no need to log it, as there's nothing KittyCAD programmers can fix
386            Code::MessageTypeNotAcceptedForWebRTC
387            | Code::MessageTypeNotAccepted
388            | Code::BadRequest
389            | Code::WrongProtocol
390            | Code::AuthTokenMissing
391            | Code::AuthTokenInvalid
392            | Code::InvalidBson
393            | Code::InvalidJson => false,
394            // In debug builds, log connection problems, otherwise don't.
395            Code::ConnectionProblem => cfg!(debug_assertions),
396        }
397    }
398}
399
400/// Serde serializes Result into JSON as "Ok" and "Err", but we want "ok" and "err".
401/// So, create a new enum that serializes as lowercase.
402#[derive(Debug, Serialize, Deserialize, JsonSchema)]
403#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
404pub enum SnakeCaseResult<T, E> {
405    /// The result is Ok.
406    Ok(T),
407    /// The result is Err.
408    Err(E),
409}
410
411impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
412    fn from(value: SnakeCaseResult<T, E>) -> Self {
413        match value {
414            SnakeCaseResult::Ok(x) => Self::Ok(x),
415            SnakeCaseResult::Err(x) => Self::Err(x),
416        }
417    }
418}
419
420impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
421    fn from(value: Result<T, E>) -> Self {
422        match value {
423            Ok(x) => Self::Ok(x),
424            Err(x) => Self::Err(x),
425        }
426    }
427}
428
429/// ClientMetrics contains information regarding the state of the peer.
430#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
431pub struct ClientMetrics {
432    /// Counter of the number of WebRTC frames the client has dropped from the
433    /// inbound video stream.
434    ///
435    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-framesdropped
436    pub rtc_frames_dropped: Option<u32>,
437
438    /// Counter of the number of WebRTC frames that the client has decoded
439    /// from the inbound video stream.
440    ///
441    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-freezecount
442    pub rtc_frames_decoded: Option<u64>,
443
444    /// Counter of the number of WebRTC frames that the client has received
445    /// from the inbound video stream.
446    ///
447    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-freezecount
448    pub rtc_frames_received: Option<u64>,
449
450    /// Current number of frames being rendered in the last second. A good target
451    /// is 60 frames per second, but it can fluctuate depending on network
452    /// conditions.
453    ///
454    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-freezecount
455    pub rtc_frames_per_second: Option<u8>, // no way we're more than 255 fps :)
456
457    /// Number of times the inbound video playback has frozen. This is usually due to
458    /// network conditions.
459    ///
460    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-freezecount
461    pub rtc_freeze_count: Option<u32>,
462
463    /// Amount of "jitter" in the inbound video stream. Network latency is the time
464    /// it takes a packet to traverse the network. The amount that the latency
465    /// varies is the jitter. Video latency is the time it takes to render
466    /// a frame sent by the server (including network latency). A low jitter
467    /// means the video latency can be reduced without impacting smooth
468    /// playback. High jitter means clients will increase video latency to
469    /// ensure smooth playback.
470    ///
471    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcreceivedrtpstreamstats-jitter
472    pub rtc_jitter_sec: Option<f64>,
473
474    /// Number of "key frames" decoded in the inbound h.264 stream. A
475    /// key frame is an expensive (bandwidth-wise) "full image" of the video
476    /// frame. Data after the keyframe become -- effectively -- "diff"
477    /// operations on that key frame. The Engine will only send a keyframe if
478    /// required, which is an indication that some of the "diffs" have been
479    /// lost, usually an indication of poor network conditions. We like this
480    /// metric to understand times when the connection has had to recover.
481    ///
482    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-keyframesdecoded
483    pub rtc_keyframes_decoded: Option<u32>,
484
485    /// Number of seconds of frozen video the user has been subjected to.
486    ///
487    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-totalfreezesduration
488    pub rtc_total_freezes_duration_sec: Option<f32>,
489
490    /// The height of the inbound video stream in pixels.
491    ///
492    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-frameheight
493    pub rtc_frame_height: Option<u32>,
494
495    /// The width of the inbound video stream in pixels.
496    ///
497    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-framewidth
498    pub rtc_frame_width: Option<u32>,
499
500    /// Amount of packets lost in the inbound video stream.
501    ///
502    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetslost
503    pub rtc_packets_lost: Option<u32>,
504
505    ///  Count the total number of Picture Loss Indication (PLI) packets.
506    ///
507    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-plicount
508    pub rtc_pli_count: Option<u32>,
509
510    /// Count of the total number of video pauses experienced by this receiver.
511    ///
512    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-pausecount
513    pub rtc_pause_count: Option<u32>,
514
515    /// Count of the total number of video pauses experienced by this receiver.
516    ///
517    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-totalpausesduration
518    pub rtc_total_pauses_duration_sec: Option<f32>,
519
520    /// Total duration of pauses in seconds.
521    ///
522    /// This is the "ping" between the client and the STUN server. Not to be confused with the
523    /// E2E RTT documented
524    /// [here](https://www.w3.org/TR/webrtc-stats/#dom-rtcremoteinboundrtpstreamstats-roundtriptime)
525    ///
526    /// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-currentroundtriptime
527    pub rtc_stun_rtt_sec: Option<f32>,
528}
529
530/// ICECandidate represents a ice candidate
531#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
532pub struct RtcIceCandidate {
533    /// The stats ID.
534    pub stats_id: String,
535    /// The foundation for the address.
536    pub foundation: String,
537    /// The priority of the candidate.
538    pub priority: u32,
539    /// The address of the candidate.
540    pub address: String,
541    /// The protocol used for the candidate.
542    pub protocol: RtcIceProtocol,
543    /// The port used for the candidate.
544    pub port: u16,
545    /// The type of the candidate.
546    pub typ: RtcIceCandidateType,
547    /// The component of the candidate.
548    pub component: u16,
549    /// The related address of the candidate.
550    pub related_address: String,
551    /// The related port of the candidate.
552    pub related_port: u16,
553    /// The TCP type of the candidate.
554    pub tcp_type: String,
555}
556
557#[cfg(feature = "webrtc")]
558impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
559    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
560        Self {
561            stats_id: candidate.stats_id,
562            foundation: candidate.foundation,
563            priority: candidate.priority,
564            address: candidate.address,
565            protocol: candidate.protocol.into(),
566            port: candidate.port,
567            typ: candidate.typ.into(),
568            component: candidate.component,
569            related_address: candidate.related_address,
570            related_port: candidate.related_port,
571            tcp_type: candidate.tcp_type,
572        }
573    }
574}
575
576#[cfg(feature = "webrtc")]
577impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
578    fn from(candidate: RtcIceCandidate) -> Self {
579        Self {
580            stats_id: candidate.stats_id,
581            foundation: candidate.foundation,
582            priority: candidate.priority,
583            address: candidate.address,
584            protocol: candidate.protocol.into(),
585            port: candidate.port,
586            typ: candidate.typ.into(),
587            component: candidate.component,
588            related_address: candidate.related_address,
589            related_port: candidate.related_port,
590            tcp_type: candidate.tcp_type,
591        }
592    }
593}
594
595/// ICECandidateType represents the type of the ICE candidate used.
596#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
597#[serde(rename_all = "snake_case")]
598pub enum RtcIceCandidateType {
599    /// Unspecified indicates that the candidate type is unspecified.
600    #[default]
601    Unspecified,
602
603    /// ICECandidateTypeHost indicates that the candidate is of Host type as
604    /// described in <https://tools.ietf.org/html/rfc8445#section-5.1.1.1>. A
605    /// candidate obtained by binding to a specific port from an IP address on
606    /// the host. This includes IP addresses on physical interfaces and logical
607    /// ones, such as ones obtained through VPNs.
608    Host,
609
610    /// ICECandidateTypeSrflx indicates the the candidate is of Server
611    /// Reflexive type as described
612    /// <https://tools.ietf.org/html/rfc8445#section-5.1.1.2>. A candidate type
613    /// whose IP address and port are a binding allocated by a NAT for an ICE
614    /// agent after it sends a packet through the NAT to a server, such as a
615    /// STUN server.
616    Srflx,
617
618    /// ICECandidateTypePrflx indicates that the candidate is of Peer
619    /// Reflexive type. A candidate type whose IP address and port are a binding
620    /// allocated by a NAT for an ICE agent after it sends a packet through the
621    /// NAT to its peer.
622    Prflx,
623
624    /// ICECandidateTypeRelay indicates the the candidate is of Relay type as
625    /// described in <https://tools.ietf.org/html/rfc8445#section-5.1.1.2>. A
626    /// candidate type obtained from a relay server, such as a TURN server.
627    Relay,
628}
629
630#[cfg(feature = "webrtc")]
631impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
632    fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
633        match candidate_type {
634            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
635            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
636            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
637            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
638            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
639                RtcIceCandidateType::Unspecified
640            }
641        }
642    }
643}
644
645#[cfg(feature = "webrtc")]
646impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
647    fn from(candidate_type: RtcIceCandidateType) -> Self {
648        match candidate_type {
649            RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
650            RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
651            RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
652            RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
653            RtcIceCandidateType::Unspecified => {
654                webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
655            }
656        }
657    }
658}
659
660/// ICEProtocol indicates the transport protocol type that is used in the
661/// ice.URL structure.
662#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
663#[serde(rename_all = "snake_case")]
664pub enum RtcIceProtocol {
665    /// Unspecified indicates that the protocol is unspecified.
666    #[default]
667    Unspecified,
668
669    /// UDP indicates the URL uses a UDP transport.
670    Udp,
671
672    /// TCP indicates the URL uses a TCP transport.
673    Tcp,
674}
675
676#[cfg(feature = "webrtc")]
677impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
678    fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
679        match protocol {
680            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
681            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
682            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
683        }
684    }
685}
686
687#[cfg(feature = "webrtc")]
688impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
689    fn from(protocol: RtcIceProtocol) -> Self {
690        match protocol {
691            RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
692            RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
693            RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
694        }
695    }
696}
697
698/// ICECandidateInit is used to serialize ice candidates
699#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
700#[serde(rename_all = "camelCase")]
701// These HAVE to be camel case as per the RFC.
702pub struct RtcIceCandidateInit {
703    /// The candidate string associated with the object.
704    pub candidate: String,
705    /// The identifier of the "media stream identification" as defined in
706    /// [RFC 8841](https://tools.ietf.org/html/rfc8841).
707    pub sdp_mid: Option<String>,
708    /// The index (starting at zero) of the m-line in the SDP this candidate is
709    /// associated with.
710    #[serde(rename = "sdpMLineIndex")]
711    pub sdp_mline_index: Option<u16>,
712    /// The username fragment (as defined in
713    /// [RFC 8445](https://tools.ietf.org/html/rfc8445#section-5.2.1)) associated with the object.
714    pub username_fragment: Option<String>,
715}
716
717#[cfg(feature = "webrtc")]
718impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
719    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
720        Self {
721            candidate: candidate.candidate,
722            sdp_mid: candidate.sdp_mid,
723            sdp_mline_index: candidate.sdp_mline_index,
724            username_fragment: candidate.username_fragment,
725        }
726    }
727}
728
729#[cfg(feature = "webrtc")]
730impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
731    fn from(candidate: RtcIceCandidateInit) -> Self {
732        Self {
733            candidate: candidate.candidate,
734            sdp_mid: candidate.sdp_mid,
735            sdp_mline_index: candidate.sdp_mline_index,
736            username_fragment: candidate.username_fragment,
737        }
738    }
739}
740
741/// SessionDescription is used to expose local and remote session descriptions.
742#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct RtcSessionDescription {
744    /// SDP type.
745    #[serde(rename = "type")]
746    pub sdp_type: RtcSdpType,
747
748    /// SDP string.
749    pub sdp: String,
750}
751
752#[cfg(feature = "webrtc")]
753impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
754    fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
755        Self {
756            sdp_type: desc.sdp_type.into(),
757            sdp: desc.sdp,
758        }
759    }
760}
761
762#[cfg(feature = "webrtc")]
763impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
764    type Error = anyhow::Error;
765
766    fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
767        let result = match desc.sdp_type {
768            RtcSdpType::Offer => {
769                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
770            }
771            RtcSdpType::Pranswer => {
772                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
773            }
774            RtcSdpType::Answer => {
775                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
776            }
777            RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
778            RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
779        };
780
781        Ok(result)
782    }
783}
784
785/// SDPType describes the type of an SessionDescription.
786#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
787#[serde(rename_all = "snake_case")]
788pub enum RtcSdpType {
789    /// Unspecified indicates that the type is unspecified.
790    #[default]
791    Unspecified = 0,
792
793    /// indicates that a description MUST be treated as an SDP offer.
794    Offer,
795
796    /// indicates that a description MUST be treated as an
797    /// SDP answer, but not a final answer. A description used as an SDP
798    /// pranswer may be applied as a response to an SDP offer, or an update to
799    /// a previously sent SDP pranswer.
800    Pranswer,
801
802    /// indicates that a description MUST be treated as an SDP
803    /// final answer, and the offer-answer exchange MUST be considered complete.
804    /// A description used as an SDP answer may be applied as a response to an
805    /// SDP offer or as an update to a previously sent SDP pranswer.
806    Answer,
807
808    /// indicates that a description MUST be treated as
809    /// canceling the current SDP negotiation and moving the SDP offer and
810    /// answer back to what it was in the previous stable state. Note the
811    /// local or remote SDP descriptions in the previous stable state could be
812    /// null if there has not yet been a successful offer-answer negotiation.
813    Rollback,
814}
815
816#[cfg(feature = "webrtc")]
817impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
818    fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
819        match sdp_type {
820            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
821            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
822            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
823            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
824            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
825        }
826    }
827}
828
829#[cfg(feature = "webrtc")]
830impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
831    fn from(sdp_type: RtcSdpType) -> Self {
832        match sdp_type {
833            RtcSdpType::Offer => Self::Offer,
834            RtcSdpType::Pranswer => Self::Pranswer,
835            RtcSdpType::Answer => Self::Answer,
836            RtcSdpType::Rollback => Self::Rollback,
837            RtcSdpType::Unspecified => Self::Unspecified,
838        }
839    }
840}
841/// Successful Websocket response.
842#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
843#[serde(rename_all = "snake_case")]
844pub struct ModelingSessionData {
845    /// ID of the API call this modeling session is using.
846    /// Useful for tracing and debugging.
847    pub api_call_id: String,
848}
849
850#[cfg(test)]
851mod tests {
852    use super::*;
853    use crate::output;
854
855    const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
856
857    #[test]
858    fn serialize_websocket_modeling_ok() {
859        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
860            success: true,
861            request_id: Some(REQ_ID),
862            resp: OkWebSocketResponseData::Modeling {
863                modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
864                    control_points: vec![],
865                }),
866            },
867        });
868        let expected = serde_json::json!({
869            "success": true,
870            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
871            "resp": {
872                "type": "modeling",
873                "data": {
874                    "modeling_response": {
875                        "type": "curve_get_control_points",
876                        "data": { "control_points": [] }
877                    }
878                }
879            }
880        });
881        assert_json_eq(actual, expected);
882    }
883
884    #[test]
885    fn serialize_websocket_webrtc_ok() {
886        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
887            success: true,
888            request_id: Some(REQ_ID),
889            resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
890        });
891        let expected = serde_json::json!({
892            "success": true,
893            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
894            "resp": {
895                "type": "ice_server_info",
896                "data": {
897                    "ice_servers": []
898                }
899            }
900        });
901        assert_json_eq(actual, expected);
902    }
903
904    #[test]
905    fn serialize_websocket_export_ok() {
906        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
907            success: true,
908            request_id: Some(REQ_ID),
909            resp: OkWebSocketResponseData::Export { files: vec![] },
910        });
911        let expected = serde_json::json!({
912            "success": true,
913            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
914            "resp": {
915                "type": "export",
916                "data": {"files": [] }
917            }
918        });
919        assert_json_eq(actual, expected);
920    }
921
922    #[test]
923    fn serialize_websocket_err() {
924        let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
925            success: false,
926            request_id: Some(REQ_ID),
927            errors: vec![ApiError {
928                error_code: ErrorCode::InternalApi,
929                message: "you fucked up!".to_owned(),
930            }],
931        });
932        let expected = serde_json::json!({
933            "success": false,
934            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
935            "errors": [
936                {
937                    "error_code": "internal_api",
938                    "message": "you fucked up!"
939                }
940            ],
941        });
942        assert_json_eq(actual, expected);
943    }
944
945    #[test]
946    fn serialize_websocket_metrics() {
947        let actual = WebSocketRequest::MetricsResponse {
948            metrics: Box::new(ClientMetrics {
949                rtc_frames_dropped: Some(1),
950                rtc_frames_decoded: Some(2),
951                rtc_frames_per_second: Some(3),
952                rtc_frames_received: Some(4),
953                rtc_freeze_count: Some(5),
954                rtc_jitter_sec: Some(6.7),
955                rtc_keyframes_decoded: Some(8),
956                rtc_total_freezes_duration_sec: Some(9.1),
957                rtc_frame_height: Some(100),
958                rtc_frame_width: Some(100),
959                rtc_packets_lost: Some(0),
960                rtc_pli_count: Some(0),
961                rtc_pause_count: Some(0),
962                rtc_total_pauses_duration_sec: Some(0.0),
963                rtc_stun_rtt_sec: Some(0.005),
964            }),
965        };
966        let expected = serde_json::json!({
967            "type": "metrics_response",
968            "metrics": {
969                "rtc_frames_dropped": 1,
970                "rtc_frames_decoded": 2,
971                "rtc_frames_per_second": 3,
972                "rtc_frames_received": 4,
973                "rtc_freeze_count": 5,
974                "rtc_jitter_sec": 6.7,
975                "rtc_keyframes_decoded": 8,
976                "rtc_total_freezes_duration_sec": 9.1,
977                "rtc_frame_height": 100,
978                "rtc_frame_width": 100,
979                "rtc_packets_lost": 0,
980                "rtc_pli_count": 0,
981                "rtc_pause_count": 0,
982                "rtc_total_pauses_duration_sec": 0.0,
983                "rtc_stun_rtt_sec": 0.005,
984            },
985        });
986        assert_json_eq(actual, expected);
987    }
988
989    fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
990        let json_str = serde_json::to_string(&actual).unwrap();
991        let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
992        assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
993    }
994}