1use 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#[derive(Display, FromStr, Copy, Eq, PartialEq, Debug, JsonSchema, Deserialize, Serialize, Clone, Ord, PartialOrd)]
21#[serde(rename_all = "snake_case")]
22pub enum ErrorCode {
23    InternalEngine,
25    InternalApi,
27    BadRequest,
31    AuthTokenMissing,
33    AuthTokenInvalid,
35    InvalidJson,
37    InvalidBson,
39    WrongProtocol,
41    ConnectionProblem,
43    MessageTypeNotAccepted,
45    MessageTypeNotAcceptedForWebRTC,
48}
49
50impl 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#[derive(Debug, Clone, Deserialize, Serialize)]
63#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
64pub struct ModelingCmdReq {
65    pub cmd: ModelingCmd,
67    pub cmd_id: ModelingCmdId,
69}
70
71#[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    TrickleIce {
80        candidate: Box<RtcIceCandidateInit>,
82    },
83    SdpOffer {
85        offer: Box<RtcSessionDescription>,
87    },
88    ModelingCmdReq(ModelingCmdReq),
90    ModelingCmdBatchReq(ModelingBatch),
92    Ping {},
94
95    MetricsResponse {
97        metrics: Box<ClientMetrics>,
99    },
100
101    Debug {},
103
104    Headers {
106        headers: HashMap<String, String>,
108    },
109}
110
111#[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    pub requests: Vec<ModelingCmdReq>,
118    pub batch_id: ModelingCmdId,
122    #[serde(default)]
125    pub responses: bool,
126}
127
128impl std::default::Default for ModelingBatch {
129    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    pub fn push(&mut self, req: ModelingCmdReq) {
142        self.requests.push(req);
143    }
144
145    pub fn is_empty(&self) -> bool {
147        self.requests.is_empty()
148    }
149}
150
151#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone)]
155pub struct IceServer {
156    pub urls: Vec<String>,
160    pub credential: Option<String>,
162    pub username: Option<String>,
164}
165
166#[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    IceServerInfo {
173        ice_servers: Vec<IceServer>,
175    },
176    TrickleIce {
179        candidate: Box<RtcIceCandidateInit>,
181    },
182    SdpAnswer {
184        answer: Box<RtcSessionDescription>,
186    },
187    Modeling {
189        modeling_response: OkModelingCmdResponse,
191    },
192    ModelingBatch {
194        responses: HashMap<ModelingCmdId, BatchResponse>,
197    },
198    Export {
200        files: Vec<RawFile>,
202    },
203
204    MetricsRequest {},
206
207    ModelingSessionData {
209        session: ModelingSessionData,
211    },
212
213    Pong {},
215
216    Debug {
218        name: String,
220    },
221}
222
223#[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    pub success: bool,
230    pub request_id: Option<Uuid>,
234    pub resp: OkWebSocketResponseData,
237}
238
239#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
241#[serde(rename_all = "snake_case")]
242pub struct FailureWebSocketResponse {
243    pub success: bool,
245    pub request_id: Option<Uuid>,
249    pub errors: Vec<ApiError>,
251}
252
253#[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    Success(SuccessWebSocketResponse),
261    Failure(FailureWebSocketResponse),
263}
264
265#[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    Success {
273        response: OkModelingCmdResponse,
275    },
276    Failure {
278        errors: Vec<ApiError>,
280    },
281}
282
283impl WebSocketResponse {
284    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    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    pub fn is_success(&self) -> bool {
304        matches!(self, Self::Success(_))
305    }
306
307    pub fn is_failure(&self) -> bool {
309        matches!(self, Self::Failure(_))
310    }
311
312    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#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
324#[cfg_attr(feature = "python", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
325pub struct RawFile {
326    pub name: String,
328    #[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#[derive(Debug, Serialize, Deserialize, JsonSchema)]
362pub struct LoggableApiError {
363    pub error: ApiError,
365    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        use slog::Key;
373        if let Some(ref msg_internal) = self.msg_internal {
374            serializer.emit_str(Key::from("msg_internal"), msg_internal)?;
375        }
376        serializer.emit_str(Key::from("msg_external"), &self.error.message)?;
377        serializer.emit_str(Key::from("error_code"), &self.error.error_code.to_string())
378    }
379}
380
381#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
383pub struct ApiError {
384    pub error_code: ErrorCode,
386    pub message: String,
388}
389
390impl ApiError {
391    pub fn no_internal_message(self) -> LoggableApiError {
393        LoggableApiError {
394            error: self,
395            msg_internal: None,
396        }
397    }
398    pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
400        LoggableApiError {
401            error: self,
402            msg_internal: Some(msg_internal),
403        }
404    }
405
406    pub fn should_log_internal_message(&self) -> bool {
408        use ErrorCode as Code;
409        match self.error_code {
410            Code::InternalEngine | Code::InternalApi => true,
412            Code::MessageTypeNotAcceptedForWebRTC
414            | Code::MessageTypeNotAccepted
415            | Code::BadRequest
416            | Code::WrongProtocol
417            | Code::AuthTokenMissing
418            | Code::AuthTokenInvalid
419            | Code::InvalidBson
420            | Code::InvalidJson => false,
421            Code::ConnectionProblem => cfg!(debug_assertions),
423        }
424    }
425}
426
427#[derive(Debug, Serialize, Deserialize, JsonSchema)]
430#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
431pub enum SnakeCaseResult<T, E> {
432    Ok(T),
434    Err(E),
436}
437
438impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
439    fn from(value: SnakeCaseResult<T, E>) -> Self {
440        match value {
441            SnakeCaseResult::Ok(x) => Self::Ok(x),
442            SnakeCaseResult::Err(x) => Self::Err(x),
443        }
444    }
445}
446
447impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
448    fn from(value: Result<T, E>) -> Self {
449        match value {
450            Ok(x) => Self::Ok(x),
451            Err(x) => Self::Err(x),
452        }
453    }
454}
455
456#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
458pub struct ClientMetrics {
459    pub rtc_frames_dropped: Option<u32>,
464
465    pub rtc_frames_decoded: Option<u64>,
470
471    pub rtc_frames_received: Option<u64>,
476
477    pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
489
490    pub rtc_jitter_sec: Option<f64>,
500
501    pub rtc_keyframes_decoded: Option<u32>,
511
512    pub rtc_total_freezes_duration_sec: Option<f32>,
516
517    pub rtc_frame_height: Option<u32>,
521
522    pub rtc_frame_width: Option<u32>,
526
527    pub rtc_packets_lost: Option<u32>,
531
532    pub rtc_pli_count: Option<u32>,
536
537    pub rtc_pause_count: Option<u32>,
541
542    pub rtc_total_pauses_duration_sec: Option<f32>,
546
547    pub rtc_stun_rtt_sec: Option<f32>,
555}
556
557#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
559pub struct RtcIceCandidate {
560    pub stats_id: String,
562    pub foundation: String,
564    pub priority: u32,
566    pub address: String,
568    pub protocol: RtcIceProtocol,
570    pub port: u16,
572    pub typ: RtcIceCandidateType,
574    pub component: u16,
576    pub related_address: String,
578    pub related_port: u16,
580    pub tcp_type: String,
582}
583
584#[cfg(feature = "webrtc")]
585impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
586    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
587        Self {
588            stats_id: candidate.stats_id,
589            foundation: candidate.foundation,
590            priority: candidate.priority,
591            address: candidate.address,
592            protocol: candidate.protocol.into(),
593            port: candidate.port,
594            typ: candidate.typ.into(),
595            component: candidate.component,
596            related_address: candidate.related_address,
597            related_port: candidate.related_port,
598            tcp_type: candidate.tcp_type,
599        }
600    }
601}
602
603#[cfg(feature = "webrtc")]
604impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
605    fn from(candidate: RtcIceCandidate) -> Self {
606        Self {
607            stats_id: candidate.stats_id,
608            foundation: candidate.foundation,
609            priority: candidate.priority,
610            address: candidate.address,
611            protocol: candidate.protocol.into(),
612            port: candidate.port,
613            typ: candidate.typ.into(),
614            component: candidate.component,
615            related_address: candidate.related_address,
616            related_port: candidate.related_port,
617            tcp_type: candidate.tcp_type,
618        }
619    }
620}
621
622#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
624#[serde(rename_all = "snake_case")]
625pub enum RtcIceCandidateType {
626    #[default]
628    Unspecified,
629
630    Host,
636
637    Srflx,
644
645    Prflx,
650
651    Relay,
655}
656
657#[cfg(feature = "webrtc")]
658impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
659    fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
660        match candidate_type {
661            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
662            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
663            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
664            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
665            webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
666                RtcIceCandidateType::Unspecified
667            }
668        }
669    }
670}
671
672#[cfg(feature = "webrtc")]
673impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
674    fn from(candidate_type: RtcIceCandidateType) -> Self {
675        match candidate_type {
676            RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
677            RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
678            RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
679            RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
680            RtcIceCandidateType::Unspecified => {
681                webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
682            }
683        }
684    }
685}
686
687#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
690#[serde(rename_all = "snake_case")]
691pub enum RtcIceProtocol {
692    #[default]
694    Unspecified,
695
696    Udp,
698
699    Tcp,
701}
702
703#[cfg(feature = "webrtc")]
704impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
705    fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
706        match protocol {
707            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
708            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
709            webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
710        }
711    }
712}
713
714#[cfg(feature = "webrtc")]
715impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
716    fn from(protocol: RtcIceProtocol) -> Self {
717        match protocol {
718            RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
719            RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
720            RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
721        }
722    }
723}
724
725#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
727#[serde(rename_all = "camelCase")]
728pub struct RtcIceCandidateInit {
730    pub candidate: String,
732    pub sdp_mid: Option<String>,
735    #[serde(rename = "sdpMLineIndex")]
738    pub sdp_mline_index: Option<u16>,
739    pub username_fragment: Option<String>,
742}
743
744#[cfg(feature = "webrtc")]
745impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
746    fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
747        Self {
748            candidate: candidate.candidate,
749            sdp_mid: candidate.sdp_mid,
750            sdp_mline_index: candidate.sdp_mline_index,
751            username_fragment: candidate.username_fragment,
752        }
753    }
754}
755
756#[cfg(feature = "webrtc")]
757impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
758    fn from(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#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
770pub struct RtcSessionDescription {
771    #[serde(rename = "type")]
773    pub sdp_type: RtcSdpType,
774
775    pub sdp: String,
777}
778
779#[cfg(feature = "webrtc")]
780impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
781    fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
782        Self {
783            sdp_type: desc.sdp_type.into(),
784            sdp: desc.sdp,
785        }
786    }
787}
788
789#[cfg(feature = "webrtc")]
790impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
791    type Error = anyhow::Error;
792
793    fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
794        let result = match desc.sdp_type {
795            RtcSdpType::Offer => {
796                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
797            }
798            RtcSdpType::Pranswer => {
799                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
800            }
801            RtcSdpType::Answer => {
802                webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
803            }
804            RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
805            RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
806        };
807
808        Ok(result)
809    }
810}
811
812#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
814#[serde(rename_all = "snake_case")]
815pub enum RtcSdpType {
816    #[default]
818    Unspecified = 0,
819
820    Offer,
822
823    Pranswer,
828
829    Answer,
834
835    Rollback,
841}
842
843#[cfg(feature = "webrtc")]
844impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
845    fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
846        match sdp_type {
847            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
848            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
849            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
850            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
851            webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
852        }
853    }
854}
855
856#[cfg(feature = "webrtc")]
857impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
858    fn from(sdp_type: RtcSdpType) -> Self {
859        match sdp_type {
860            RtcSdpType::Offer => Self::Offer,
861            RtcSdpType::Pranswer => Self::Pranswer,
862            RtcSdpType::Answer => Self::Answer,
863            RtcSdpType::Rollback => Self::Rollback,
864            RtcSdpType::Unspecified => Self::Unspecified,
865        }
866    }
867}
868#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
870#[serde(rename_all = "snake_case")]
871pub struct ModelingSessionData {
872    pub api_call_id: String,
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880    use crate::output;
881
882    const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
883
884    #[test]
885    fn serialize_websocket_modeling_ok() {
886        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
887            success: true,
888            request_id: Some(REQ_ID),
889            resp: OkWebSocketResponseData::Modeling {
890                modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
891                    control_points: vec![],
892                }),
893            },
894        });
895        let expected = serde_json::json!({
896            "success": true,
897            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
898            "resp": {
899                "type": "modeling",
900                "data": {
901                    "modeling_response": {
902                        "type": "curve_get_control_points",
903                        "data": { "control_points": [] }
904                    }
905                }
906            }
907        });
908        assert_json_eq(actual, expected);
909    }
910
911    #[test]
912    fn serialize_websocket_webrtc_ok() {
913        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
914            success: true,
915            request_id: Some(REQ_ID),
916            resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
917        });
918        let expected = serde_json::json!({
919            "success": true,
920            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
921            "resp": {
922                "type": "ice_server_info",
923                "data": {
924                    "ice_servers": []
925                }
926            }
927        });
928        assert_json_eq(actual, expected);
929    }
930
931    #[test]
932    fn serialize_websocket_export_ok() {
933        let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
934            success: true,
935            request_id: Some(REQ_ID),
936            resp: OkWebSocketResponseData::Export { files: vec![] },
937        });
938        let expected = serde_json::json!({
939            "success": true,
940            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
941            "resp": {
942                "type": "export",
943                "data": {"files": [] }
944            }
945        });
946        assert_json_eq(actual, expected);
947    }
948
949    #[test]
950    fn serialize_websocket_err() {
951        let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
952            success: false,
953            request_id: Some(REQ_ID),
954            errors: vec![ApiError {
955                error_code: ErrorCode::InternalApi,
956                message: "you fucked up!".to_owned(),
957            }],
958        });
959        let expected = serde_json::json!({
960            "success": false,
961            "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
962            "errors": [
963                {
964                    "error_code": "internal_api",
965                    "message": "you fucked up!"
966                }
967            ],
968        });
969        assert_json_eq(actual, expected);
970    }
971
972    #[test]
973    fn serialize_websocket_metrics() {
974        let actual = WebSocketRequest::MetricsResponse {
975            metrics: Box::new(ClientMetrics {
976                rtc_frames_dropped: Some(1),
977                rtc_frames_decoded: Some(2),
978                rtc_frames_per_second: Some(3),
979                rtc_frames_received: Some(4),
980                rtc_freeze_count: Some(5),
981                rtc_jitter_sec: Some(6.7),
982                rtc_keyframes_decoded: Some(8),
983                rtc_total_freezes_duration_sec: Some(9.1),
984                rtc_frame_height: Some(100),
985                rtc_frame_width: Some(100),
986                rtc_packets_lost: Some(0),
987                rtc_pli_count: Some(0),
988                rtc_pause_count: Some(0),
989                rtc_total_pauses_duration_sec: Some(0.0),
990                rtc_stun_rtt_sec: Some(0.005),
991            }),
992        };
993        let expected = serde_json::json!({
994            "type": "metrics_response",
995            "metrics": {
996                "rtc_frames_dropped": 1,
997                "rtc_frames_decoded": 2,
998                "rtc_frames_per_second": 3,
999                "rtc_frames_received": 4,
1000                "rtc_freeze_count": 5,
1001                "rtc_jitter_sec": 6.7,
1002                "rtc_keyframes_decoded": 8,
1003                "rtc_total_freezes_duration_sec": 9.1,
1004                "rtc_frame_height": 100,
1005                "rtc_frame_width": 100,
1006                "rtc_packets_lost": 0,
1007                "rtc_pli_count": 0,
1008                "rtc_pause_count": 0,
1009                "rtc_total_pauses_duration_sec": 0.0,
1010                "rtc_stun_rtt_sec": 0.005,
1011            },
1012        });
1013        assert_json_eq(actual, expected);
1014    }
1015
1016    fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
1017        let json_str = serde_json::to_string(&actual).unwrap();
1018        let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1019        assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
1020    }
1021}