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