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 during
433    /// this session.
434    pub rtc_frames_dropped: u32,
435
436    /// Counter of the number of WebRTC frames that the client has decoded
437    /// during this session.
438    pub rtc_frames_decoded: u64,
439
440    /// Counter of the number of WebRTC frames that the client has received
441    /// during this session.
442    pub rtc_frames_received: u64,
443
444    /// Current number of frames being rendered per second. A good target
445    /// is 60 frames per second, but it can fluctuate depending on network
446    /// conditions.
447    pub rtc_frames_per_second: u8, // no way we're more than 255 fps :)
448
449    /// Number of times the WebRTC playback has frozen. This is usually due to
450    /// network conditions.
451    pub rtc_freeze_count: u32,
452
453    /// Amount of "jitter" in the WebRTC session. Network latency is the time
454    /// it takes a packet to traverse the network. The amount that the latency
455    /// varies is the jitter. Video latency is the time it takes to render
456    /// a frame sent by the server (including network latency). A low jitter
457    /// means the video latency can be reduced without impacting smooth
458    /// playback. High jitter means clients will increase video latency to
459    /// ensure smooth playback.
460    pub rtc_jitter_sec: f32,
461
462    /// Number of "key frames" decoded in the underlying h.264 stream. A
463    /// key frame is an expensive (bandwidth-wise) "full image" of the video
464    /// frame. Data after the keyframe become -- effectively -- "diff"
465    /// operations on that key frame. The Engine will only send a keyframe if
466    /// required, which is an indication that some of the "diffs" have been
467    /// lost, usually an indication of poor network conditions. We like this
468    /// metric to understand times when the connection has had to recover.
469    pub rtc_keyframes_decoded: u32,
470
471    /// Number of seconds of frozen video the user has been subjected to.
472    pub rtc_total_freezes_duration_sec: f32,
473}
474
475/// ICECandidate represents a ice candidate
476#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
477pub struct RtcIceCandidate {
478    /// The stats ID.
479    pub stats_id: String,
480    /// The foundation for the address.
481    pub foundation: String,
482    /// The priority of the candidate.
483    pub priority: u32,
484    /// The address of the candidate.
485    pub address: String,
486    /// The protocol used for the candidate.
487    pub protocol: RtcIceProtocol,
488    /// The port used for the candidate.
489    pub port: u16,
490    /// The type of the candidate.
491    pub typ: RtcIceCandidateType,
492    /// The component of the candidate.
493    pub component: u16,
494    /// The related address of the candidate.
495    pub related_address: String,
496    /// The related port of the candidate.
497    pub related_port: u16,
498    /// The TCP type of the candidate.
499    pub tcp_type: String,
500}
501
502#[cfg(feature = "webrtc")]
503impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
504    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
505        Self {
506            stats_id: candidate.stats_id,
507            foundation: candidate.foundation,
508            priority: candidate.priority,
509            address: candidate.address,
510            protocol: candidate.protocol.into(),
511            port: candidate.port,
512            typ: candidate.typ.into(),
513            component: candidate.component,
514            related_address: candidate.related_address,
515            related_port: candidate.related_port,
516            tcp_type: candidate.tcp_type,
517        }
518    }
519}
520
521#[cfg(feature = "webrtc")]
522impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
523    fn from(candidate: RtcIceCandidate) -> Self {
524        Self {
525            stats_id: candidate.stats_id,
526            foundation: candidate.foundation,
527            priority: candidate.priority,
528            address: candidate.address,
529            protocol: candidate.protocol.into(),
530            port: candidate.port,
531            typ: candidate.typ.into(),
532            component: candidate.component,
533            related_address: candidate.related_address,
534            related_port: candidate.related_port,
535            tcp_type: candidate.tcp_type,
536        }
537    }
538}
539
540/// ICECandidateType represents the type of the ICE candidate used.
541#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
542#[serde(rename_all = "snake_case")]
543pub enum RtcIceCandidateType {
544    /// Unspecified indicates that the candidate type is unspecified.
545    #[default]
546    Unspecified,
547
548    /// ICECandidateTypeHost indicates that the candidate is of Host type as
549    /// described in <https://tools.ietf.org/html/rfc8445#section-5.1.1.1>. A
550    /// candidate obtained by binding to a specific port from an IP address on
551    /// the host. This includes IP addresses on physical interfaces and logical
552    /// ones, such as ones obtained through VPNs.
553    Host,
554
555    /// ICECandidateTypeSrflx indicates the the candidate is of Server
556    /// Reflexive type as described
557    /// <https://tools.ietf.org/html/rfc8445#section-5.1.1.2>. A candidate type
558    /// whose IP address and port are a binding allocated by a NAT for an ICE
559    /// agent after it sends a packet through the NAT to a server, such as a
560    /// STUN server.
561    Srflx,
562
563    /// ICECandidateTypePrflx indicates that the candidate is of Peer
564    /// Reflexive type. A candidate type whose IP address and port are a binding
565    /// allocated by a NAT for an ICE agent after it sends a packet through the
566    /// NAT to its peer.
567    Prflx,
568
569    /// ICECandidateTypeRelay indicates the the candidate is of Relay type as
570    /// described in <https://tools.ietf.org/html/rfc8445#section-5.1.1.2>. A
571    /// candidate type obtained from a relay server, such as a TURN server.
572    Relay,
573}
574
575#[cfg(feature = "webrtc")]
576impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
577    fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
578        match candidate_type {
579            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
580            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
581            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
582            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
583            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
584                RtcIceCandidateType::Unspecified
585            }
586        }
587    }
588}
589
590#[cfg(feature = "webrtc")]
591impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
592    fn from(candidate_type: RtcIceCandidateType) -> Self {
593        match candidate_type {
594            RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
595            RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
596            RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
597            RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
598            RtcIceCandidateType::Unspecified => {
599                webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
600            }
601        }
602    }
603}
604
605/// ICEProtocol indicates the transport protocol type that is used in the
606/// ice.URL structure.
607#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
608#[serde(rename_all = "snake_case")]
609pub enum RtcIceProtocol {
610    /// Unspecified indicates that the protocol is unspecified.
611    #[default]
612    Unspecified,
613
614    /// UDP indicates the URL uses a UDP transport.
615    Udp,
616
617    /// TCP indicates the URL uses a TCP transport.
618    Tcp,
619}
620
621#[cfg(feature = "webrtc")]
622impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
623    fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
624        match protocol {
625            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
626            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
627            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
628        }
629    }
630}
631
632#[cfg(feature = "webrtc")]
633impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
634    fn from(protocol: RtcIceProtocol) -> Self {
635        match protocol {
636            RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
637            RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
638            RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
639        }
640    }
641}
642
643/// ICECandidateInit is used to serialize ice candidates
644#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
645#[serde(rename_all = "camelCase")]
646// These HAVE to be camel case as per the RFC.
647pub struct RtcIceCandidateInit {
648    /// The candidate string associated with the object.
649    pub candidate: String,
650    /// The identifier of the "media stream identification" as defined in
651    /// [RFC 8841](https://tools.ietf.org/html/rfc8841).
652    pub sdp_mid: Option<String>,
653    /// The index (starting at zero) of the m-line in the SDP this candidate is
654    /// associated with.
655    #[serde(rename = "sdpMLineIndex")]
656    pub sdp_mline_index: Option<u16>,
657    /// The username fragment (as defined in
658    /// [RFC 8445](https://tools.ietf.org/html/rfc8445#section-5.2.1)) associated with the object.
659    pub username_fragment: Option<String>,
660}
661
662#[cfg(feature = "webrtc")]
663impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
664    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
665        Self {
666            candidate: candidate.candidate,
667            sdp_mid: candidate.sdp_mid,
668            sdp_mline_index: candidate.sdp_mline_index,
669            username_fragment: candidate.username_fragment,
670        }
671    }
672}
673
674#[cfg(feature = "webrtc")]
675impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
676    fn from(candidate: RtcIceCandidateInit) -> Self {
677        Self {
678            candidate: candidate.candidate,
679            sdp_mid: candidate.sdp_mid,
680            sdp_mline_index: candidate.sdp_mline_index,
681            username_fragment: candidate.username_fragment,
682        }
683    }
684}
685
686/// SessionDescription is used to expose local and remote session descriptions.
687#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
688pub struct RtcSessionDescription {
689    /// SDP type.
690    #[serde(rename = "type")]
691    pub sdp_type: RtcSdpType,
692
693    /// SDP string.
694    pub sdp: String,
695}
696
697#[cfg(feature = "webrtc")]
698impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
699    fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
700        Self {
701            sdp_type: desc.sdp_type.into(),
702            sdp: desc.sdp,
703        }
704    }
705}
706
707#[cfg(feature = "webrtc")]
708impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
709    type Error = anyhow::Error;
710
711    fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
712        let result = match desc.sdp_type {
713            RtcSdpType::Offer => {
714                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
715            }
716            RtcSdpType::Pranswer => {
717                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
718            }
719            RtcSdpType::Answer => {
720                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
721            }
722            RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
723            RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
724        };
725
726        Ok(result)
727    }
728}
729
730/// SDPType describes the type of an SessionDescription.
731#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
732#[serde(rename_all = "snake_case")]
733pub enum RtcSdpType {
734    /// Unspecified indicates that the type is unspecified.
735    #[default]
736    Unspecified = 0,
737
738    /// indicates that a description MUST be treated as an SDP offer.
739    Offer,
740
741    /// indicates that a description MUST be treated as an
742    /// SDP answer, but not a final answer. A description used as an SDP
743    /// pranswer may be applied as a response to an SDP offer, or an update to
744    /// a previously sent SDP pranswer.
745    Pranswer,
746
747    /// indicates that a description MUST be treated as an SDP
748    /// final answer, and the offer-answer exchange MUST be considered complete.
749    /// A description used as an SDP answer may be applied as a response to an
750    /// SDP offer or as an update to a previously sent SDP pranswer.
751    Answer,
752
753    /// indicates that a description MUST be treated as
754    /// canceling the current SDP negotiation and moving the SDP offer and
755    /// answer back to what it was in the previous stable state. Note the
756    /// local or remote SDP descriptions in the previous stable state could be
757    /// null if there has not yet been a successful offer-answer negotiation.
758    Rollback,
759}
760
761#[cfg(feature = "webrtc")]
762impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
763    fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
764        match sdp_type {
765            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
766            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
767            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
768            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
769            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
770        }
771    }
772}
773
774#[cfg(feature = "webrtc")]
775impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
776    fn from(sdp_type: RtcSdpType) -> Self {
777        match sdp_type {
778            RtcSdpType::Offer => Self::Offer,
779            RtcSdpType::Pranswer => Self::Pranswer,
780            RtcSdpType::Answer => Self::Answer,
781            RtcSdpType::Rollback => Self::Rollback,
782            RtcSdpType::Unspecified => Self::Unspecified,
783        }
784    }
785}
786/// Successful Websocket response.
787#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
788#[serde(rename_all = "snake_case")]
789pub struct ModelingSessionData {
790    /// ID of the API call this modeling session is using.
791    /// Useful for tracing and debugging.
792    pub api_call_id: String,
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798    use crate::output;
799
800    const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
801
802    #[test]
803    fn serialize_websocket_modeling_ok() {
804        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
805            success: true,
806            request_id: Some(REQ_ID),
807            resp: OkWebSocketResponseData::Modeling {
808                modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
809                    control_points: vec![],
810                }),
811            },
812        });
813        let expected = serde_json::json!({
814            "success": true,
815            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
816            "resp": {
817                "type": "modeling",
818                "data": {
819                    "modeling_response": {
820                        "type": "curve_get_control_points",
821                        "data": { "control_points": [] }
822                    }
823                }
824            }
825        });
826        assert_json_eq(actual, expected);
827    }
828
829    #[test]
830    fn serialize_websocket_webrtc_ok() {
831        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
832            success: true,
833            request_id: Some(REQ_ID),
834            resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
835        });
836        let expected = serde_json::json!({
837            "success": true,
838            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
839            "resp": {
840                "type": "ice_server_info",
841                "data": {
842                    "ice_servers": []
843                }
844            }
845        });
846        assert_json_eq(actual, expected);
847    }
848
849    #[test]
850    fn serialize_websocket_export_ok() {
851        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
852            success: true,
853            request_id: Some(REQ_ID),
854            resp: OkWebSocketResponseData::Export { files: vec![] },
855        });
856        let expected = serde_json::json!({
857            "success": true,
858            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
859            "resp": {
860                "type": "export",
861                "data": {"files": [] }
862            }
863        });
864        assert_json_eq(actual, expected);
865    }
866
867    #[test]
868    fn serialize_websocket_err() {
869        let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
870            success: false,
871            request_id: Some(REQ_ID),
872            errors: vec![ApiError {
873                error_code: ErrorCode::InternalApi,
874                message: "you fucked up!".to_owned(),
875            }],
876        });
877        let expected = serde_json::json!({
878            "success": false,
879            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
880            "errors": [
881                {
882                    "error_code": "internal_api",
883                    "message": "you fucked up!"
884                }
885            ],
886        });
887        assert_json_eq(actual, expected);
888    }
889
890    #[test]
891    fn serialize_websocket_metrics() {
892        let actual = WebSocketRequest::MetricsResponse {
893            metrics: Box::new(ClientMetrics {
894                rtc_frames_dropped: 1,
895                rtc_frames_decoded: 2,
896                rtc_frames_per_second: 3,
897                rtc_frames_received: 4,
898                rtc_freeze_count: 5,
899                rtc_jitter_sec: 6.7,
900                rtc_keyframes_decoded: 8,
901                rtc_total_freezes_duration_sec: 9.1,
902            }),
903        };
904        let expected = serde_json::json!({
905            "type": "metrics_response",
906            "metrics": {
907                "rtc_frames_dropped": 1,
908                "rtc_frames_decoded": 2,
909                "rtc_frames_per_second": 3,
910                "rtc_frames_received": 4,
911                "rtc_freeze_count": 5,
912                "rtc_jitter_sec": 6.7,
913                "rtc_keyframes_decoded": 8,
914                "rtc_total_freezes_duration_sec": 9.1
915            },
916        });
917        assert_json_eq(actual, expected);
918    }
919
920    fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
921        let json_str = serde_json::to_string(&actual).unwrap();
922        let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
923        assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
924    }
925}