Skip to main content

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