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)]
323pub struct RawFile {
324 pub name: String,
326 #[serde(
328 serialize_with = "serde_bytes::serialize",
329 deserialize_with = "serde_bytes::deserialize"
330 )]
331 pub contents: Vec<u8>,
332}
333
334impl From<ExportFile> for RawFile {
335 fn from(f: ExportFile) -> Self {
336 Self {
337 name: f.name,
338 contents: f.contents.0,
339 }
340 }
341}
342
343#[derive(Debug, Serialize, Deserialize, JsonSchema)]
345pub struct LoggableApiError {
346 pub error: ApiError,
348 pub msg_internal: Option<Cow<'static, str>>,
350}
351
352#[cfg(feature = "slog")]
353impl KV for LoggableApiError {
354 fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
355 if let Some(ref msg_internal) = self.msg_internal {
356 serializer.emit_str("msg_internal", msg_internal)?;
357 }
358 serializer.emit_str("msg_external", &self.error.message)?;
359 serializer.emit_str("error_code", &self.error.error_code.to_string())
360 }
361}
362
363#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
365pub struct ApiError {
366 pub error_code: ErrorCode,
368 pub message: String,
370}
371
372impl ApiError {
373 pub fn no_internal_message(self) -> LoggableApiError {
375 LoggableApiError {
376 error: self,
377 msg_internal: None,
378 }
379 }
380 pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
382 LoggableApiError {
383 error: self,
384 msg_internal: Some(msg_internal),
385 }
386 }
387
388 pub fn should_log_internal_message(&self) -> bool {
390 use ErrorCode as Code;
391 match self.error_code {
392 Code::InternalEngine | Code::InternalApi => true,
394 Code::MessageTypeNotAcceptedForWebRTC
396 | Code::MessageTypeNotAccepted
397 | Code::BadRequest
398 | Code::WrongProtocol
399 | Code::AuthTokenMissing
400 | Code::AuthTokenInvalid
401 | Code::InvalidBson
402 | Code::InvalidJson => false,
403 Code::ConnectionProblem => cfg!(debug_assertions),
405 }
406 }
407}
408
409#[derive(Debug, Serialize, Deserialize, JsonSchema)]
412#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
413pub enum SnakeCaseResult<T, E> {
414 Ok(T),
416 Err(E),
418}
419
420impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
421 fn from(value: SnakeCaseResult<T, E>) -> Self {
422 match value {
423 SnakeCaseResult::Ok(x) => Self::Ok(x),
424 SnakeCaseResult::Err(x) => Self::Err(x),
425 }
426 }
427}
428
429impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
430 fn from(value: Result<T, E>) -> Self {
431 match value {
432 Ok(x) => Self::Ok(x),
433 Err(x) => Self::Err(x),
434 }
435 }
436}
437
438#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
440pub struct ClientMetrics {
441 pub rtc_frames_dropped: Option<u32>,
446
447 pub rtc_frames_decoded: Option<u64>,
452
453 pub rtc_frames_received: Option<u64>,
458
459 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
471
472 pub rtc_jitter_sec: Option<f64>,
482
483 pub rtc_keyframes_decoded: Option<u32>,
493
494 pub rtc_total_freezes_duration_sec: Option<f32>,
498
499 pub rtc_frame_height: Option<u32>,
503
504 pub rtc_frame_width: Option<u32>,
508
509 pub rtc_packets_lost: Option<u32>,
513
514 pub rtc_pli_count: Option<u32>,
518
519 pub rtc_pause_count: Option<u32>,
523
524 pub rtc_total_pauses_duration_sec: Option<f32>,
528
529 pub rtc_stun_rtt_sec: Option<f32>,
537}
538
539#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
541pub struct RtcIceCandidate {
542 pub stats_id: String,
544 pub foundation: String,
546 pub priority: u32,
548 pub address: String,
550 pub protocol: RtcIceProtocol,
552 pub port: u16,
554 pub typ: RtcIceCandidateType,
556 pub component: u16,
558 pub related_address: String,
560 pub related_port: u16,
562 pub tcp_type: String,
564}
565
566#[cfg(feature = "webrtc")]
567impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
568 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
569 Self {
570 stats_id: candidate.stats_id,
571 foundation: candidate.foundation,
572 priority: candidate.priority,
573 address: candidate.address,
574 protocol: candidate.protocol.into(),
575 port: candidate.port,
576 typ: candidate.typ.into(),
577 component: candidate.component,
578 related_address: candidate.related_address,
579 related_port: candidate.related_port,
580 tcp_type: candidate.tcp_type,
581 }
582 }
583}
584
585#[cfg(feature = "webrtc")]
586impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
587 fn from(candidate: RtcIceCandidate) -> Self {
588 Self {
589 stats_id: candidate.stats_id,
590 foundation: candidate.foundation,
591 priority: candidate.priority,
592 address: candidate.address,
593 protocol: candidate.protocol.into(),
594 port: candidate.port,
595 typ: candidate.typ.into(),
596 component: candidate.component,
597 related_address: candidate.related_address,
598 related_port: candidate.related_port,
599 tcp_type: candidate.tcp_type,
600 }
601 }
602}
603
604#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
606#[serde(rename_all = "snake_case")]
607pub enum RtcIceCandidateType {
608 #[default]
610 Unspecified,
611
612 Host,
618
619 Srflx,
626
627 Prflx,
632
633 Relay,
637}
638
639#[cfg(feature = "webrtc")]
640impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
641 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
642 match candidate_type {
643 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
644 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
645 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
646 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
647 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
648 RtcIceCandidateType::Unspecified
649 }
650 }
651 }
652}
653
654#[cfg(feature = "webrtc")]
655impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
656 fn from(candidate_type: RtcIceCandidateType) -> Self {
657 match candidate_type {
658 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
659 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
660 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
661 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
662 RtcIceCandidateType::Unspecified => {
663 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
664 }
665 }
666 }
667}
668
669#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
672#[serde(rename_all = "snake_case")]
673pub enum RtcIceProtocol {
674 #[default]
676 Unspecified,
677
678 Udp,
680
681 Tcp,
683}
684
685#[cfg(feature = "webrtc")]
686impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
687 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
688 match protocol {
689 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
690 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
691 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
692 }
693 }
694}
695
696#[cfg(feature = "webrtc")]
697impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
698 fn from(protocol: RtcIceProtocol) -> Self {
699 match protocol {
700 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
701 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
702 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
703 }
704 }
705}
706
707#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
709#[serde(rename_all = "camelCase")]
710pub struct RtcIceCandidateInit {
712 pub candidate: String,
714 pub sdp_mid: Option<String>,
717 #[serde(rename = "sdpMLineIndex")]
720 pub sdp_mline_index: Option<u16>,
721 pub username_fragment: Option<String>,
724}
725
726#[cfg(feature = "webrtc")]
727impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
728 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
729 Self {
730 candidate: candidate.candidate,
731 sdp_mid: candidate.sdp_mid,
732 sdp_mline_index: candidate.sdp_mline_index,
733 username_fragment: candidate.username_fragment,
734 }
735 }
736}
737
738#[cfg(feature = "webrtc")]
739impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
740 fn from(candidate: RtcIceCandidateInit) -> Self {
741 Self {
742 candidate: candidate.candidate,
743 sdp_mid: candidate.sdp_mid,
744 sdp_mline_index: candidate.sdp_mline_index,
745 username_fragment: candidate.username_fragment,
746 }
747 }
748}
749
750#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
752pub struct RtcSessionDescription {
753 #[serde(rename = "type")]
755 pub sdp_type: RtcSdpType,
756
757 pub sdp: String,
759}
760
761#[cfg(feature = "webrtc")]
762impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
763 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
764 Self {
765 sdp_type: desc.sdp_type.into(),
766 sdp: desc.sdp,
767 }
768 }
769}
770
771#[cfg(feature = "webrtc")]
772impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
773 type Error = anyhow::Error;
774
775 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
776 let result = match desc.sdp_type {
777 RtcSdpType::Offer => {
778 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
779 }
780 RtcSdpType::Pranswer => {
781 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
782 }
783 RtcSdpType::Answer => {
784 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
785 }
786 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
787 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
788 };
789
790 Ok(result)
791 }
792}
793
794#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
796#[serde(rename_all = "snake_case")]
797pub enum RtcSdpType {
798 #[default]
800 Unspecified = 0,
801
802 Offer,
804
805 Pranswer,
810
811 Answer,
816
817 Rollback,
823}
824
825#[cfg(feature = "webrtc")]
826impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
827 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
828 match sdp_type {
829 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
830 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
831 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
832 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
833 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
834 }
835 }
836}
837
838#[cfg(feature = "webrtc")]
839impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
840 fn from(sdp_type: RtcSdpType) -> Self {
841 match sdp_type {
842 RtcSdpType::Offer => Self::Offer,
843 RtcSdpType::Pranswer => Self::Pranswer,
844 RtcSdpType::Answer => Self::Answer,
845 RtcSdpType::Rollback => Self::Rollback,
846 RtcSdpType::Unspecified => Self::Unspecified,
847 }
848 }
849}
850#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
852#[serde(rename_all = "snake_case")]
853pub struct ModelingSessionData {
854 pub api_call_id: String,
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use crate::output;
863
864 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
865
866 #[test]
867 fn serialize_websocket_modeling_ok() {
868 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
869 success: true,
870 request_id: Some(REQ_ID),
871 resp: OkWebSocketResponseData::Modeling {
872 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
873 control_points: vec![],
874 }),
875 },
876 });
877 let expected = serde_json::json!({
878 "success": true,
879 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
880 "resp": {
881 "type": "modeling",
882 "data": {
883 "modeling_response": {
884 "type": "curve_get_control_points",
885 "data": { "control_points": [] }
886 }
887 }
888 }
889 });
890 assert_json_eq(actual, expected);
891 }
892
893 #[test]
894 fn serialize_websocket_webrtc_ok() {
895 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
896 success: true,
897 request_id: Some(REQ_ID),
898 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
899 });
900 let expected = serde_json::json!({
901 "success": true,
902 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
903 "resp": {
904 "type": "ice_server_info",
905 "data": {
906 "ice_servers": []
907 }
908 }
909 });
910 assert_json_eq(actual, expected);
911 }
912
913 #[test]
914 fn serialize_websocket_export_ok() {
915 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
916 success: true,
917 request_id: Some(REQ_ID),
918 resp: OkWebSocketResponseData::Export { files: vec![] },
919 });
920 let expected = serde_json::json!({
921 "success": true,
922 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
923 "resp": {
924 "type": "export",
925 "data": {"files": [] }
926 }
927 });
928 assert_json_eq(actual, expected);
929 }
930
931 #[test]
932 fn serialize_websocket_err() {
933 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
934 success: false,
935 request_id: Some(REQ_ID),
936 errors: vec![ApiError {
937 error_code: ErrorCode::InternalApi,
938 message: "you fucked up!".to_owned(),
939 }],
940 });
941 let expected = serde_json::json!({
942 "success": false,
943 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
944 "errors": [
945 {
946 "error_code": "internal_api",
947 "message": "you fucked up!"
948 }
949 ],
950 });
951 assert_json_eq(actual, expected);
952 }
953
954 #[test]
955 fn serialize_websocket_metrics() {
956 let actual = WebSocketRequest::MetricsResponse {
957 metrics: Box::new(ClientMetrics {
958 rtc_frames_dropped: Some(1),
959 rtc_frames_decoded: Some(2),
960 rtc_frames_per_second: Some(3),
961 rtc_frames_received: Some(4),
962 rtc_freeze_count: Some(5),
963 rtc_jitter_sec: Some(6.7),
964 rtc_keyframes_decoded: Some(8),
965 rtc_total_freezes_duration_sec: Some(9.1),
966 rtc_frame_height: Some(100),
967 rtc_frame_width: Some(100),
968 rtc_packets_lost: Some(0),
969 rtc_pli_count: Some(0),
970 rtc_pause_count: Some(0),
971 rtc_total_pauses_duration_sec: Some(0.0),
972 rtc_stun_rtt_sec: Some(0.005),
973 }),
974 };
975 let expected = serde_json::json!({
976 "type": "metrics_response",
977 "metrics": {
978 "rtc_frames_dropped": 1,
979 "rtc_frames_decoded": 2,
980 "rtc_frames_per_second": 3,
981 "rtc_frames_received": 4,
982 "rtc_freeze_count": 5,
983 "rtc_jitter_sec": 6.7,
984 "rtc_keyframes_decoded": 8,
985 "rtc_total_freezes_duration_sec": 9.1,
986 "rtc_frame_height": 100,
987 "rtc_frame_width": 100,
988 "rtc_packets_lost": 0,
989 "rtc_pli_count": 0,
990 "rtc_pause_count": 0,
991 "rtc_total_pauses_duration_sec": 0.0,
992 "rtc_stun_rtt_sec": 0.005,
993 },
994 });
995 assert_json_eq(actual, expected);
996 }
997
998 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
999 let json_str = serde_json::to_string(&actual).unwrap();
1000 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1001 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
1002 }
1003}