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}