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