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