kittycad_modeling_cmds/
websocket.rs

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