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