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