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: u32,
435
436 pub rtc_frames_decoded: u64,
439
440 pub rtc_frames_received: u64,
443
444 pub rtc_frames_per_second: u8, pub rtc_freeze_count: u32,
452
453 pub rtc_jitter_sec: f32,
461
462 pub rtc_keyframes_decoded: u32,
470
471 pub rtc_total_freezes_duration_sec: f32,
473}
474
475#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
477pub struct RtcIceCandidate {
478 pub stats_id: String,
480 pub foundation: String,
482 pub priority: u32,
484 pub address: String,
486 pub protocol: RtcIceProtocol,
488 pub port: u16,
490 pub typ: RtcIceCandidateType,
492 pub component: u16,
494 pub related_address: String,
496 pub related_port: u16,
498 pub tcp_type: String,
500}
501
502#[cfg(feature = "webrtc")]
503impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
504 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
505 Self {
506 stats_id: candidate.stats_id,
507 foundation: candidate.foundation,
508 priority: candidate.priority,
509 address: candidate.address,
510 protocol: candidate.protocol.into(),
511 port: candidate.port,
512 typ: candidate.typ.into(),
513 component: candidate.component,
514 related_address: candidate.related_address,
515 related_port: candidate.related_port,
516 tcp_type: candidate.tcp_type,
517 }
518 }
519}
520
521#[cfg(feature = "webrtc")]
522impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
523 fn from(candidate: RtcIceCandidate) -> Self {
524 Self {
525 stats_id: candidate.stats_id,
526 foundation: candidate.foundation,
527 priority: candidate.priority,
528 address: candidate.address,
529 protocol: candidate.protocol.into(),
530 port: candidate.port,
531 typ: candidate.typ.into(),
532 component: candidate.component,
533 related_address: candidate.related_address,
534 related_port: candidate.related_port,
535 tcp_type: candidate.tcp_type,
536 }
537 }
538}
539
540#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
542#[serde(rename_all = "snake_case")]
543pub enum RtcIceCandidateType {
544 #[default]
546 Unspecified,
547
548 Host,
554
555 Srflx,
562
563 Prflx,
568
569 Relay,
573}
574
575#[cfg(feature = "webrtc")]
576impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
577 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
578 match candidate_type {
579 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
580 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
581 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
582 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
583 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
584 RtcIceCandidateType::Unspecified
585 }
586 }
587 }
588}
589
590#[cfg(feature = "webrtc")]
591impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
592 fn from(candidate_type: RtcIceCandidateType) -> Self {
593 match candidate_type {
594 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
595 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
596 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
597 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
598 RtcIceCandidateType::Unspecified => {
599 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
600 }
601 }
602 }
603}
604
605#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
608#[serde(rename_all = "snake_case")]
609pub enum RtcIceProtocol {
610 #[default]
612 Unspecified,
613
614 Udp,
616
617 Tcp,
619}
620
621#[cfg(feature = "webrtc")]
622impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
623 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
624 match protocol {
625 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
626 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
627 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
628 }
629 }
630}
631
632#[cfg(feature = "webrtc")]
633impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
634 fn from(protocol: RtcIceProtocol) -> Self {
635 match protocol {
636 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
637 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
638 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
639 }
640 }
641}
642
643#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
645#[serde(rename_all = "camelCase")]
646pub struct RtcIceCandidateInit {
648 pub candidate: String,
650 pub sdp_mid: Option<String>,
653 #[serde(rename = "sdpMLineIndex")]
656 pub sdp_mline_index: Option<u16>,
657 pub username_fragment: Option<String>,
660}
661
662#[cfg(feature = "webrtc")]
663impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
664 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
665 Self {
666 candidate: candidate.candidate,
667 sdp_mid: candidate.sdp_mid,
668 sdp_mline_index: candidate.sdp_mline_index,
669 username_fragment: candidate.username_fragment,
670 }
671 }
672}
673
674#[cfg(feature = "webrtc")]
675impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
676 fn from(candidate: RtcIceCandidateInit) -> Self {
677 Self {
678 candidate: candidate.candidate,
679 sdp_mid: candidate.sdp_mid,
680 sdp_mline_index: candidate.sdp_mline_index,
681 username_fragment: candidate.username_fragment,
682 }
683 }
684}
685
686#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
688pub struct RtcSessionDescription {
689 #[serde(rename = "type")]
691 pub sdp_type: RtcSdpType,
692
693 pub sdp: String,
695}
696
697#[cfg(feature = "webrtc")]
698impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
699 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
700 Self {
701 sdp_type: desc.sdp_type.into(),
702 sdp: desc.sdp,
703 }
704 }
705}
706
707#[cfg(feature = "webrtc")]
708impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
709 type Error = anyhow::Error;
710
711 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
712 let result = match desc.sdp_type {
713 RtcSdpType::Offer => {
714 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
715 }
716 RtcSdpType::Pranswer => {
717 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
718 }
719 RtcSdpType::Answer => {
720 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
721 }
722 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
723 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
724 };
725
726 Ok(result)
727 }
728}
729
730#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
732#[serde(rename_all = "snake_case")]
733pub enum RtcSdpType {
734 #[default]
736 Unspecified = 0,
737
738 Offer,
740
741 Pranswer,
746
747 Answer,
752
753 Rollback,
759}
760
761#[cfg(feature = "webrtc")]
762impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
763 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
764 match sdp_type {
765 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
766 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
767 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
768 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
769 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
770 }
771 }
772}
773
774#[cfg(feature = "webrtc")]
775impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
776 fn from(sdp_type: RtcSdpType) -> Self {
777 match sdp_type {
778 RtcSdpType::Offer => Self::Offer,
779 RtcSdpType::Pranswer => Self::Pranswer,
780 RtcSdpType::Answer => Self::Answer,
781 RtcSdpType::Rollback => Self::Rollback,
782 RtcSdpType::Unspecified => Self::Unspecified,
783 }
784 }
785}
786#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
788#[serde(rename_all = "snake_case")]
789pub struct ModelingSessionData {
790 pub api_call_id: String,
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798 use crate::output;
799
800 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
801
802 #[test]
803 fn serialize_websocket_modeling_ok() {
804 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
805 success: true,
806 request_id: Some(REQ_ID),
807 resp: OkWebSocketResponseData::Modeling {
808 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
809 control_points: vec![],
810 }),
811 },
812 });
813 let expected = serde_json::json!({
814 "success": true,
815 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
816 "resp": {
817 "type": "modeling",
818 "data": {
819 "modeling_response": {
820 "type": "curve_get_control_points",
821 "data": { "control_points": [] }
822 }
823 }
824 }
825 });
826 assert_json_eq(actual, expected);
827 }
828
829 #[test]
830 fn serialize_websocket_webrtc_ok() {
831 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
832 success: true,
833 request_id: Some(REQ_ID),
834 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
835 });
836 let expected = serde_json::json!({
837 "success": true,
838 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
839 "resp": {
840 "type": "ice_server_info",
841 "data": {
842 "ice_servers": []
843 }
844 }
845 });
846 assert_json_eq(actual, expected);
847 }
848
849 #[test]
850 fn serialize_websocket_export_ok() {
851 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
852 success: true,
853 request_id: Some(REQ_ID),
854 resp: OkWebSocketResponseData::Export { files: vec![] },
855 });
856 let expected = serde_json::json!({
857 "success": true,
858 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
859 "resp": {
860 "type": "export",
861 "data": {"files": [] }
862 }
863 });
864 assert_json_eq(actual, expected);
865 }
866
867 #[test]
868 fn serialize_websocket_err() {
869 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
870 success: false,
871 request_id: Some(REQ_ID),
872 errors: vec![ApiError {
873 error_code: ErrorCode::InternalApi,
874 message: "you fucked up!".to_owned(),
875 }],
876 });
877 let expected = serde_json::json!({
878 "success": false,
879 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
880 "errors": [
881 {
882 "error_code": "internal_api",
883 "message": "you fucked up!"
884 }
885 ],
886 });
887 assert_json_eq(actual, expected);
888 }
889
890 #[test]
891 fn serialize_websocket_metrics() {
892 let actual = WebSocketRequest::MetricsResponse {
893 metrics: Box::new(ClientMetrics {
894 rtc_frames_dropped: 1,
895 rtc_frames_decoded: 2,
896 rtc_frames_per_second: 3,
897 rtc_frames_received: 4,
898 rtc_freeze_count: 5,
899 rtc_jitter_sec: 6.7,
900 rtc_keyframes_decoded: 8,
901 rtc_total_freezes_duration_sec: 9.1,
902 }),
903 };
904 let expected = serde_json::json!({
905 "type": "metrics_response",
906 "metrics": {
907 "rtc_frames_dropped": 1,
908 "rtc_frames_decoded": 2,
909 "rtc_frames_per_second": 3,
910 "rtc_frames_received": 4,
911 "rtc_freeze_count": 5,
912 "rtc_jitter_sec": 6.7,
913 "rtc_keyframes_decoded": 8,
914 "rtc_total_freezes_duration_sec": 9.1
915 },
916 });
917 assert_json_eq(actual, expected);
918 }
919
920 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
921 let json_str = serde_json::to_string(&actual).unwrap();
922 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
923 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
924 }
925}