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#[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 TrickleIce {
79 candidate: Box<RtcIceCandidateInit>,
81 },
82 SdpOffer {
84 offer: Box<RtcSessionDescription>,
86 },
87 ModelingCmdReq(ModelingCmdReq),
89 ModelingCmdBatchReq(ModelingBatch),
91 Ping {},
93
94 MetricsResponse {
96 metrics: Box<ClientMetrics>,
98 },
99
100 Debug {},
102
103 Headers {
105 headers: HashMap<String, String>,
107 },
108}
109
110#[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 pub requests: Vec<ModelingCmdReq>,
117 pub batch_id: ModelingCmdId,
121 #[serde(default)]
124 pub responses: bool,
125}
126
127impl std::default::Default for ModelingBatch {
128 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 pub fn push(&mut self, req: ModelingCmdReq) {
141 self.requests.push(req);
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.requests.is_empty()
147 }
148}
149
150#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone)]
154pub struct IceServer {
155 pub urls: Vec<String>,
159 pub credential: Option<String>,
161 pub username: Option<String>,
163}
164
165#[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 IceServerInfo {
172 ice_servers: Vec<IceServer>,
174 },
175 TrickleIce {
178 candidate: Box<RtcIceCandidateInit>,
180 },
181 SdpAnswer {
183 answer: Box<RtcSessionDescription>,
185 },
186 Modeling {
188 modeling_response: OkModelingCmdResponse,
190 },
191 ModelingBatch {
193 responses: HashMap<ModelingCmdId, BatchResponse>,
196 },
197 Export {
199 files: Vec<RawFile>,
201 },
202
203 MetricsRequest {},
205
206 ModelingSessionData {
208 session: ModelingSessionData,
210 },
211
212 Pong {},
214
215 Debug {
217 name: String,
219 },
220}
221
222#[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 pub success: bool,
229 pub request_id: Option<Uuid>,
233 pub resp: OkWebSocketResponseData,
236}
237
238#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
240#[serde(rename_all = "snake_case")]
241pub struct FailureWebSocketResponse {
242 pub success: bool,
244 pub request_id: Option<Uuid>,
248 pub errors: Vec<ApiError>,
250}
251
252#[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 Success(SuccessWebSocketResponse),
260 Failure(FailureWebSocketResponse),
262}
263
264#[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 Success {
272 response: OkModelingCmdResponse,
274 },
275 Failure {
277 errors: Vec<ApiError>,
279 },
280}
281
282impl WebSocketResponse {
283 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 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 pub fn is_success(&self) -> bool {
303 matches!(self, Self::Success(_))
304 }
305
306 pub fn is_failure(&self) -> bool {
308 matches!(self, Self::Failure(_))
309 }
310
311 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#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
323#[cfg_attr(feature = "python", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
324pub struct RawFile {
325 pub name: String,
327 #[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#[derive(Debug, Serialize, Deserialize, JsonSchema)]
361pub struct LoggableApiError {
362 pub error: ApiError,
364 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#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
381pub struct ApiError {
382 pub error_code: ErrorCode,
384 pub message: String,
386}
387
388impl ApiError {
389 pub fn no_internal_message(self) -> LoggableApiError {
391 LoggableApiError {
392 error: self,
393 msg_internal: None,
394 }
395 }
396 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 pub fn should_log_internal_message(&self) -> bool {
406 use ErrorCode as Code;
407 match self.error_code {
408 Code::InternalEngine | Code::InternalApi => true,
410 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 Code::ConnectionProblem => cfg!(debug_assertions),
421 }
422 }
423}
424
425#[derive(Debug, Serialize, Deserialize, JsonSchema)]
428#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
429pub enum SnakeCaseResult<T, E> {
430 Ok(T),
432 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#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
456pub struct ClientMetrics {
457 pub rtc_frames_dropped: Option<u32>,
462
463 pub rtc_frames_decoded: Option<u64>,
468
469 pub rtc_frames_received: Option<u64>,
474
475 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
487
488 pub rtc_jitter_sec: Option<f64>,
498
499 pub rtc_keyframes_decoded: Option<u32>,
509
510 pub rtc_total_freezes_duration_sec: Option<f32>,
514
515 pub rtc_frame_height: Option<u32>,
519
520 pub rtc_frame_width: Option<u32>,
524
525 pub rtc_packets_lost: Option<u32>,
529
530 pub rtc_pli_count: Option<u32>,
534
535 pub rtc_pause_count: Option<u32>,
539
540 pub rtc_total_pauses_duration_sec: Option<f32>,
544
545 pub rtc_stun_rtt_sec: Option<f32>,
553}
554
555#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
557pub struct RtcIceCandidate {
558 pub stats_id: String,
560 pub foundation: String,
562 pub priority: u32,
564 pub address: String,
566 pub protocol: RtcIceProtocol,
568 pub port: u16,
570 pub typ: RtcIceCandidateType,
572 pub component: u16,
574 pub related_address: String,
576 pub related_port: u16,
578 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#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
622#[serde(rename_all = "snake_case")]
623pub enum RtcIceCandidateType {
624 #[default]
626 Unspecified,
627
628 Host,
634
635 Srflx,
642
643 Prflx,
648
649 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#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
688#[serde(rename_all = "snake_case")]
689pub enum RtcIceProtocol {
690 #[default]
692 Unspecified,
693
694 Udp,
696
697 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#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
725#[serde(rename_all = "camelCase")]
726pub struct RtcIceCandidateInit {
728 pub candidate: String,
730 pub sdp_mid: Option<String>,
733 #[serde(rename = "sdpMLineIndex")]
736 pub sdp_mline_index: Option<u16>,
737 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#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
768pub struct RtcSessionDescription {
769 #[serde(rename = "type")]
771 pub sdp_type: RtcSdpType,
772
773 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#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
812#[serde(rename_all = "snake_case")]
813pub enum RtcSdpType {
814 #[default]
816 Unspecified = 0,
817
818 Offer,
820
821 Pranswer,
826
827 Answer,
832
833 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#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
868#[serde(rename_all = "snake_case")]
869pub struct ModelingSessionData {
870 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}