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 Headers {
102 headers: HashMap<String, String>,
104 },
105}
106
107#[derive(Serialize, Deserialize, Debug, Clone)]
109#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
110#[serde(rename_all = "snake_case")]
111pub struct ModelingBatch {
112 pub requests: Vec<ModelingCmdReq>,
114 pub batch_id: ModelingCmdId,
118 #[serde(default)]
121 pub responses: bool,
122}
123
124impl std::default::Default for ModelingBatch {
125 fn default() -> Self {
127 Self {
128 requests: Default::default(),
129 batch_id: Uuid::new_v4().into(),
130 responses: false,
131 }
132 }
133}
134
135impl ModelingBatch {
136 pub fn push(&mut self, req: ModelingCmdReq) {
138 self.requests.push(req);
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.requests.is_empty()
144 }
145}
146
147#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone)]
151pub struct IceServer {
152 pub urls: Vec<String>,
156 pub credential: Option<String>,
158 pub username: Option<String>,
160}
161
162#[derive(Serialize, Deserialize, Debug, Clone)]
164#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
165#[serde(tag = "type", content = "data", rename_all = "snake_case")]
166pub enum OkWebSocketResponseData {
167 IceServerInfo {
169 ice_servers: Vec<IceServer>,
171 },
172 TrickleIce {
175 candidate: Box<RtcIceCandidateInit>,
177 },
178 SdpAnswer {
180 answer: Box<RtcSessionDescription>,
182 },
183 Modeling {
185 modeling_response: OkModelingCmdResponse,
187 },
188 ModelingBatch {
190 responses: HashMap<ModelingCmdId, BatchResponse>,
193 },
194 Export {
196 files: Vec<RawFile>,
198 },
199
200 MetricsRequest {},
202
203 ModelingSessionData {
205 session: ModelingSessionData,
207 },
208
209 Pong {},
211}
212
213#[derive(Debug, Serialize, Deserialize, Clone)]
215#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
216#[serde(rename_all = "snake_case")]
217pub struct SuccessWebSocketResponse {
218 pub success: bool,
220 pub request_id: Option<Uuid>,
224 pub resp: OkWebSocketResponseData,
227}
228
229#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
231#[serde(rename_all = "snake_case")]
232pub struct FailureWebSocketResponse {
233 pub success: bool,
235 pub request_id: Option<Uuid>,
239 pub errors: Vec<ApiError>,
241}
242
243#[derive(Debug, Serialize, Deserialize, Clone)]
246#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
247#[serde(rename_all = "snake_case", untagged)]
248pub enum WebSocketResponse {
249 Success(SuccessWebSocketResponse),
251 Failure(FailureWebSocketResponse),
253}
254
255#[derive(Debug, Serialize, Deserialize, Clone)]
258#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
259#[serde(rename_all = "snake_case", untagged)]
260pub enum BatchResponse {
261 Success {
263 response: OkModelingCmdResponse,
265 },
266 Failure {
268 errors: Vec<ApiError>,
270 },
271}
272
273impl WebSocketResponse {
274 pub fn success(request_id: Option<Uuid>, resp: OkWebSocketResponseData) -> Self {
276 Self::Success(SuccessWebSocketResponse {
277 success: true,
278 request_id,
279 resp,
280 })
281 }
282
283 pub fn failure(request_id: Option<Uuid>, errors: Vec<ApiError>) -> Self {
285 Self::Failure(FailureWebSocketResponse {
286 success: false,
287 request_id,
288 errors,
289 })
290 }
291
292 pub fn is_success(&self) -> bool {
294 matches!(self, Self::Success(_))
295 }
296
297 pub fn is_failure(&self) -> bool {
299 matches!(self, Self::Failure(_))
300 }
301
302 pub fn request_id(&self) -> Option<Uuid> {
304 match self {
305 WebSocketResponse::Success(x) => x.request_id,
306 WebSocketResponse::Failure(x) => x.request_id,
307 }
308 }
309}
310
311#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
314pub struct RawFile {
315 pub name: String,
317 #[serde(
319 serialize_with = "serde_bytes::serialize",
320 deserialize_with = "serde_bytes::deserialize"
321 )]
322 pub contents: Vec<u8>,
323}
324
325impl From<ExportFile> for RawFile {
326 fn from(f: ExportFile) -> Self {
327 Self {
328 name: f.name,
329 contents: f.contents.0,
330 }
331 }
332}
333
334#[derive(Debug, Serialize, Deserialize, JsonSchema)]
336pub struct LoggableApiError {
337 pub error: ApiError,
339 pub msg_internal: Option<Cow<'static, str>>,
341}
342
343#[cfg(feature = "slog")]
344impl KV for LoggableApiError {
345 fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
346 if let Some(ref msg_internal) = self.msg_internal {
347 serializer.emit_str("msg_internal", msg_internal)?;
348 }
349 serializer.emit_str("msg_external", &self.error.message)?;
350 serializer.emit_str("error_code", &self.error.error_code.to_string())
351 }
352}
353
354#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
356pub struct ApiError {
357 pub error_code: ErrorCode,
359 pub message: String,
361}
362
363impl ApiError {
364 pub fn no_internal_message(self) -> LoggableApiError {
366 LoggableApiError {
367 error: self,
368 msg_internal: None,
369 }
370 }
371 pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
373 LoggableApiError {
374 error: self,
375 msg_internal: Some(msg_internal),
376 }
377 }
378
379 pub fn should_log_internal_message(&self) -> bool {
381 use ErrorCode as Code;
382 match self.error_code {
383 Code::InternalEngine | Code::InternalApi => true,
385 Code::MessageTypeNotAcceptedForWebRTC
387 | Code::MessageTypeNotAccepted
388 | Code::BadRequest
389 | Code::WrongProtocol
390 | Code::AuthTokenMissing
391 | Code::AuthTokenInvalid
392 | Code::InvalidBson
393 | Code::InvalidJson => false,
394 Code::ConnectionProblem => cfg!(debug_assertions),
396 }
397 }
398}
399
400#[derive(Debug, Serialize, Deserialize, JsonSchema)]
403#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
404pub enum SnakeCaseResult<T, E> {
405 Ok(T),
407 Err(E),
409}
410
411impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
412 fn from(value: SnakeCaseResult<T, E>) -> Self {
413 match value {
414 SnakeCaseResult::Ok(x) => Self::Ok(x),
415 SnakeCaseResult::Err(x) => Self::Err(x),
416 }
417 }
418}
419
420impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
421 fn from(value: Result<T, E>) -> Self {
422 match value {
423 Ok(x) => Self::Ok(x),
424 Err(x) => Self::Err(x),
425 }
426 }
427}
428
429#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
431pub struct ClientMetrics {
432 pub rtc_frames_dropped: Option<u32>,
437
438 pub rtc_frames_decoded: Option<u64>,
443
444 pub rtc_frames_received: Option<u64>,
449
450 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
462
463 pub rtc_jitter_sec: Option<f64>,
473
474 pub rtc_keyframes_decoded: Option<u32>,
484
485 pub rtc_total_freezes_duration_sec: Option<f32>,
489
490 pub rtc_frame_height: Option<u32>,
494
495 pub rtc_frame_width: Option<u32>,
499
500 pub rtc_packets_lost: Option<u32>,
504
505 pub rtc_pli_count: Option<u32>,
509
510 pub rtc_pause_count: Option<u32>,
514
515 pub rtc_total_pauses_duration_sec: Option<f32>,
519
520 pub rtc_stun_rtt_sec: Option<f32>,
528}
529
530#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
532pub struct RtcIceCandidate {
533 pub stats_id: String,
535 pub foundation: String,
537 pub priority: u32,
539 pub address: String,
541 pub protocol: RtcIceProtocol,
543 pub port: u16,
545 pub typ: RtcIceCandidateType,
547 pub component: u16,
549 pub related_address: String,
551 pub related_port: u16,
553 pub tcp_type: String,
555}
556
557#[cfg(feature = "webrtc")]
558impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
559 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
560 Self {
561 stats_id: candidate.stats_id,
562 foundation: candidate.foundation,
563 priority: candidate.priority,
564 address: candidate.address,
565 protocol: candidate.protocol.into(),
566 port: candidate.port,
567 typ: candidate.typ.into(),
568 component: candidate.component,
569 related_address: candidate.related_address,
570 related_port: candidate.related_port,
571 tcp_type: candidate.tcp_type,
572 }
573 }
574}
575
576#[cfg(feature = "webrtc")]
577impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
578 fn from(candidate: RtcIceCandidate) -> Self {
579 Self {
580 stats_id: candidate.stats_id,
581 foundation: candidate.foundation,
582 priority: candidate.priority,
583 address: candidate.address,
584 protocol: candidate.protocol.into(),
585 port: candidate.port,
586 typ: candidate.typ.into(),
587 component: candidate.component,
588 related_address: candidate.related_address,
589 related_port: candidate.related_port,
590 tcp_type: candidate.tcp_type,
591 }
592 }
593}
594
595#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
597#[serde(rename_all = "snake_case")]
598pub enum RtcIceCandidateType {
599 #[default]
601 Unspecified,
602
603 Host,
609
610 Srflx,
617
618 Prflx,
623
624 Relay,
628}
629
630#[cfg(feature = "webrtc")]
631impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
632 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
633 match candidate_type {
634 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
635 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
636 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
637 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
638 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
639 RtcIceCandidateType::Unspecified
640 }
641 }
642 }
643}
644
645#[cfg(feature = "webrtc")]
646impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
647 fn from(candidate_type: RtcIceCandidateType) -> Self {
648 match candidate_type {
649 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
650 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
651 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
652 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
653 RtcIceCandidateType::Unspecified => {
654 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
655 }
656 }
657 }
658}
659
660#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
663#[serde(rename_all = "snake_case")]
664pub enum RtcIceProtocol {
665 #[default]
667 Unspecified,
668
669 Udp,
671
672 Tcp,
674}
675
676#[cfg(feature = "webrtc")]
677impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
678 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
679 match protocol {
680 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
681 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
682 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
683 }
684 }
685}
686
687#[cfg(feature = "webrtc")]
688impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
689 fn from(protocol: RtcIceProtocol) -> Self {
690 match protocol {
691 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
692 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
693 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
694 }
695 }
696}
697
698#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
700#[serde(rename_all = "camelCase")]
701pub struct RtcIceCandidateInit {
703 pub candidate: String,
705 pub sdp_mid: Option<String>,
708 #[serde(rename = "sdpMLineIndex")]
711 pub sdp_mline_index: Option<u16>,
712 pub username_fragment: Option<String>,
715}
716
717#[cfg(feature = "webrtc")]
718impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
719 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
720 Self {
721 candidate: candidate.candidate,
722 sdp_mid: candidate.sdp_mid,
723 sdp_mline_index: candidate.sdp_mline_index,
724 username_fragment: candidate.username_fragment,
725 }
726 }
727}
728
729#[cfg(feature = "webrtc")]
730impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
731 fn from(candidate: RtcIceCandidateInit) -> Self {
732 Self {
733 candidate: candidate.candidate,
734 sdp_mid: candidate.sdp_mid,
735 sdp_mline_index: candidate.sdp_mline_index,
736 username_fragment: candidate.username_fragment,
737 }
738 }
739}
740
741#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct RtcSessionDescription {
744 #[serde(rename = "type")]
746 pub sdp_type: RtcSdpType,
747
748 pub sdp: String,
750}
751
752#[cfg(feature = "webrtc")]
753impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
754 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
755 Self {
756 sdp_type: desc.sdp_type.into(),
757 sdp: desc.sdp,
758 }
759 }
760}
761
762#[cfg(feature = "webrtc")]
763impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
764 type Error = anyhow::Error;
765
766 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
767 let result = match desc.sdp_type {
768 RtcSdpType::Offer => {
769 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
770 }
771 RtcSdpType::Pranswer => {
772 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
773 }
774 RtcSdpType::Answer => {
775 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
776 }
777 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
778 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
779 };
780
781 Ok(result)
782 }
783}
784
785#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
787#[serde(rename_all = "snake_case")]
788pub enum RtcSdpType {
789 #[default]
791 Unspecified = 0,
792
793 Offer,
795
796 Pranswer,
801
802 Answer,
807
808 Rollback,
814}
815
816#[cfg(feature = "webrtc")]
817impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
818 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
819 match sdp_type {
820 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
821 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
822 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
823 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
824 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
825 }
826 }
827}
828
829#[cfg(feature = "webrtc")]
830impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
831 fn from(sdp_type: RtcSdpType) -> Self {
832 match sdp_type {
833 RtcSdpType::Offer => Self::Offer,
834 RtcSdpType::Pranswer => Self::Pranswer,
835 RtcSdpType::Answer => Self::Answer,
836 RtcSdpType::Rollback => Self::Rollback,
837 RtcSdpType::Unspecified => Self::Unspecified,
838 }
839 }
840}
841#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
843#[serde(rename_all = "snake_case")]
844pub struct ModelingSessionData {
845 pub api_call_id: String,
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853 use crate::output;
854
855 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
856
857 #[test]
858 fn serialize_websocket_modeling_ok() {
859 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
860 success: true,
861 request_id: Some(REQ_ID),
862 resp: OkWebSocketResponseData::Modeling {
863 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
864 control_points: vec![],
865 }),
866 },
867 });
868 let expected = serde_json::json!({
869 "success": true,
870 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
871 "resp": {
872 "type": "modeling",
873 "data": {
874 "modeling_response": {
875 "type": "curve_get_control_points",
876 "data": { "control_points": [] }
877 }
878 }
879 }
880 });
881 assert_json_eq(actual, expected);
882 }
883
884 #[test]
885 fn serialize_websocket_webrtc_ok() {
886 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
887 success: true,
888 request_id: Some(REQ_ID),
889 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
890 });
891 let expected = serde_json::json!({
892 "success": true,
893 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
894 "resp": {
895 "type": "ice_server_info",
896 "data": {
897 "ice_servers": []
898 }
899 }
900 });
901 assert_json_eq(actual, expected);
902 }
903
904 #[test]
905 fn serialize_websocket_export_ok() {
906 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
907 success: true,
908 request_id: Some(REQ_ID),
909 resp: OkWebSocketResponseData::Export { files: vec![] },
910 });
911 let expected = serde_json::json!({
912 "success": true,
913 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
914 "resp": {
915 "type": "export",
916 "data": {"files": [] }
917 }
918 });
919 assert_json_eq(actual, expected);
920 }
921
922 #[test]
923 fn serialize_websocket_err() {
924 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
925 success: false,
926 request_id: Some(REQ_ID),
927 errors: vec![ApiError {
928 error_code: ErrorCode::InternalApi,
929 message: "you fucked up!".to_owned(),
930 }],
931 });
932 let expected = serde_json::json!({
933 "success": false,
934 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
935 "errors": [
936 {
937 "error_code": "internal_api",
938 "message": "you fucked up!"
939 }
940 ],
941 });
942 assert_json_eq(actual, expected);
943 }
944
945 #[test]
946 fn serialize_websocket_metrics() {
947 let actual = WebSocketRequest::MetricsResponse {
948 metrics: Box::new(ClientMetrics {
949 rtc_frames_dropped: Some(1),
950 rtc_frames_decoded: Some(2),
951 rtc_frames_per_second: Some(3),
952 rtc_frames_received: Some(4),
953 rtc_freeze_count: Some(5),
954 rtc_jitter_sec: Some(6.7),
955 rtc_keyframes_decoded: Some(8),
956 rtc_total_freezes_duration_sec: Some(9.1),
957 rtc_frame_height: Some(100),
958 rtc_frame_width: Some(100),
959 rtc_packets_lost: Some(0),
960 rtc_pli_count: Some(0),
961 rtc_pause_count: Some(0),
962 rtc_total_pauses_duration_sec: Some(0.0),
963 rtc_stun_rtt_sec: Some(0.005),
964 }),
965 };
966 let expected = serde_json::json!({
967 "type": "metrics_response",
968 "metrics": {
969 "rtc_frames_dropped": 1,
970 "rtc_frames_decoded": 2,
971 "rtc_frames_per_second": 3,
972 "rtc_frames_received": 4,
973 "rtc_freeze_count": 5,
974 "rtc_jitter_sec": 6.7,
975 "rtc_keyframes_decoded": 8,
976 "rtc_total_freezes_duration_sec": 9.1,
977 "rtc_frame_height": 100,
978 "rtc_frame_width": 100,
979 "rtc_packets_lost": 0,
980 "rtc_pli_count": 0,
981 "rtc_pause_count": 0,
982 "rtc_total_pauses_duration_sec": 0.0,
983 "rtc_stun_rtt_sec": 0.005,
984 },
985 });
986 assert_json_eq(actual, expected);
987 }
988
989 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
990 let json_str = serde_json::to_string(&actual).unwrap();
991 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
992 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
993 }
994}