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")]
22#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
23pub enum ErrorCode {
24 InternalEngine,
26 InternalApi,
28 BadRequest,
32 AuthTokenMissing,
34 AuthTokenInvalid,
36 InvalidJson,
38 InvalidBson,
40 WrongProtocol,
42 ConnectionProblem,
44 MessageTypeNotAccepted,
46 MessageTypeNotAcceptedForWebRTC,
49}
50
51impl From<EngineErrorCode> for ErrorCode {
54 fn from(value: EngineErrorCode) -> Self {
55 match value {
56 EngineErrorCode::InternalEngine => Self::InternalEngine,
57 EngineErrorCode::BadRequest => Self::BadRequest,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
64#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
65#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
66pub struct ModelingCmdReq {
67 pub cmd: ModelingCmd,
69 pub cmd_id: ModelingCmdId,
71}
72
73#[allow(clippy::large_enum_variant)]
75#[derive(Serialize, Deserialize, Debug, Clone)]
76#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
77#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
78#[serde(tag = "type", rename_all = "snake_case")]
79#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
80pub enum WebSocketRequest {
81 TrickleIce {
84 candidate: Box<RtcIceCandidateInit>,
86 },
87 SdpOffer {
89 offer: Box<RtcSessionDescription>,
91 },
92 ModelingCmdReq(ModelingCmdReq),
94 ModelingCmdBatchReq(ModelingBatch),
96 Ping {},
98
99 MetricsResponse {
101 metrics: Box<ClientMetrics>,
103 },
104
105 Debug {},
107
108 Headers {
110 headers: HashMap<String, String>,
112 },
113}
114
115#[derive(Serialize, Deserialize, Debug, Clone)]
117#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
118#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
119#[serde(rename_all = "snake_case")]
120pub struct ModelingBatch {
121 pub requests: Vec<ModelingCmdReq>,
123 pub batch_id: ModelingCmdId,
127 #[serde(default)]
130 pub responses: bool,
131}
132
133impl std::default::Default for ModelingBatch {
134 fn default() -> Self {
136 Self {
137 requests: Default::default(),
138 batch_id: Uuid::new_v4().into(),
139 responses: false,
140 }
141 }
142}
143
144impl ModelingBatch {
145 pub fn push(&mut self, req: ModelingCmdReq) {
147 self.requests.push(req);
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.requests.is_empty()
153 }
154}
155
156#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone)]
160pub struct IceServer {
161 pub urls: Vec<String>,
165 pub credential: Option<String>,
167 pub username: Option<String>,
169}
170
171#[derive(Serialize, Deserialize, Debug, Clone)]
173#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
174#[serde(tag = "type", content = "data", rename_all = "snake_case")]
175#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
176pub enum OkWebSocketResponseData {
177 IceServerInfo {
179 ice_servers: Vec<IceServer>,
181 },
182 TrickleIce {
185 candidate: Box<RtcIceCandidateInit>,
187 },
188 SdpAnswer {
190 answer: Box<RtcSessionDescription>,
192 },
193 Modeling {
195 modeling_response: OkModelingCmdResponse,
197 },
198 ModelingBatch {
200 responses: HashMap<ModelingCmdId, BatchResponse>,
203 },
204 Export {
206 files: Vec<RawFile>,
208 },
209
210 MetricsRequest {},
212
213 ModelingSessionData {
215 session: ModelingSessionData,
217 },
218
219 Pong {},
221
222 Debug {
224 name: String,
226 },
227}
228
229#[derive(Debug, Serialize, Deserialize, Clone)]
231#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
232#[serde(rename_all = "snake_case")]
233pub struct SuccessWebSocketResponse {
234 pub success: bool,
236 pub request_id: Option<Uuid>,
240 pub resp: OkWebSocketResponseData,
243}
244
245#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
247#[serde(rename_all = "snake_case")]
248pub struct FailureWebSocketResponse {
249 pub success: bool,
251 pub request_id: Option<Uuid>,
255 pub errors: Vec<ApiError>,
257}
258
259#[derive(Debug, Serialize, Deserialize, Clone)]
262#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
263#[serde(rename_all = "snake_case", untagged)]
264pub enum WebSocketResponse {
265 Success(SuccessWebSocketResponse),
267 Failure(FailureWebSocketResponse),
269}
270
271#[derive(Debug, Serialize, Deserialize, Clone)]
274#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
275#[serde(rename_all = "snake_case", untagged)]
276pub enum BatchResponse {
277 Success {
279 response: OkModelingCmdResponse,
281 },
282 Failure {
284 errors: Vec<ApiError>,
286 },
287}
288
289impl WebSocketResponse {
290 pub fn success(request_id: Option<Uuid>, resp: OkWebSocketResponseData) -> Self {
292 Self::Success(SuccessWebSocketResponse {
293 success: true,
294 request_id,
295 resp,
296 })
297 }
298
299 pub fn failure(request_id: Option<Uuid>, errors: Vec<ApiError>) -> Self {
301 Self::Failure(FailureWebSocketResponse {
302 success: false,
303 request_id,
304 errors,
305 })
306 }
307
308 pub fn is_success(&self) -> bool {
310 matches!(self, Self::Success(_))
311 }
312
313 pub fn is_failure(&self) -> bool {
315 matches!(self, Self::Failure(_))
316 }
317
318 pub fn request_id(&self) -> Option<Uuid> {
320 match self {
321 WebSocketResponse::Success(x) => x.request_id,
322 WebSocketResponse::Failure(x) => x.request_id,
323 }
324 }
325}
326
327#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
330#[cfg_attr(feature = "python", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
331pub struct RawFile {
332 pub name: String,
334 #[serde(
336 serialize_with = "serde_bytes::serialize",
337 deserialize_with = "serde_bytes::deserialize"
338 )]
339 pub contents: Vec<u8>,
340}
341
342#[cfg(feature = "python")]
343#[pyo3_stub_gen::derive::gen_stub_pymethods]
344#[pyo3::pymethods]
345impl RawFile {
346 #[getter]
347 fn contents(&self) -> Vec<u8> {
348 self.contents.clone()
349 }
350
351 #[getter]
352 fn name(&self) -> String {
353 self.name.clone()
354 }
355}
356
357impl From<ExportFile> for RawFile {
358 fn from(f: ExportFile) -> Self {
359 Self {
360 name: f.name,
361 contents: f.contents.0,
362 }
363 }
364}
365
366#[derive(Debug, Serialize, Deserialize, JsonSchema)]
368pub struct LoggableApiError {
369 pub error: ApiError,
371 pub msg_internal: Option<Cow<'static, str>>,
373}
374
375#[cfg(feature = "slog")]
376impl KV for LoggableApiError {
377 fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
378 use slog::Key;
379 if let Some(ref msg_internal) = self.msg_internal {
380 serializer.emit_str(Key::from("msg_internal"), msg_internal)?;
381 }
382 serializer.emit_str(Key::from("msg_external"), &self.error.message)?;
383 serializer.emit_str(Key::from("error_code"), &self.error.error_code.to_string())
384 }
385}
386
387#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
389pub struct ApiError {
390 pub error_code: ErrorCode,
392 pub message: String,
394}
395
396impl ApiError {
397 pub fn no_internal_message(self) -> LoggableApiError {
399 LoggableApiError {
400 error: self,
401 msg_internal: None,
402 }
403 }
404 pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
406 LoggableApiError {
407 error: self,
408 msg_internal: Some(msg_internal),
409 }
410 }
411
412 pub fn should_log_internal_message(&self) -> bool {
414 use ErrorCode as Code;
415 match self.error_code {
416 Code::InternalEngine | Code::InternalApi => true,
418 Code::MessageTypeNotAcceptedForWebRTC
420 | Code::MessageTypeNotAccepted
421 | Code::BadRequest
422 | Code::WrongProtocol
423 | Code::AuthTokenMissing
424 | Code::AuthTokenInvalid
425 | Code::InvalidBson
426 | Code::InvalidJson => false,
427 Code::ConnectionProblem => cfg!(debug_assertions),
429 }
430 }
431}
432
433#[derive(Debug, Serialize, Deserialize, JsonSchema)]
436#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
437pub enum SnakeCaseResult<T, E> {
438 Ok(T),
440 Err(E),
442}
443
444impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
445 fn from(value: SnakeCaseResult<T, E>) -> Self {
446 match value {
447 SnakeCaseResult::Ok(x) => Self::Ok(x),
448 SnakeCaseResult::Err(x) => Self::Err(x),
449 }
450 }
451}
452
453impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
454 fn from(value: Result<T, E>) -> Self {
455 match value {
456 Ok(x) => Self::Ok(x),
457 Err(x) => Self::Err(x),
458 }
459 }
460}
461
462#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
464#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
465pub struct ClientMetrics {
466 pub rtc_frames_dropped: Option<u32>,
471
472 pub rtc_frames_decoded: Option<u64>,
477
478 pub rtc_frames_received: Option<u64>,
483
484 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
496
497 pub rtc_jitter_sec: Option<f64>,
507
508 pub rtc_keyframes_decoded: Option<u32>,
518
519 pub rtc_total_freezes_duration_sec: Option<f32>,
523
524 pub rtc_frame_height: Option<u32>,
528
529 pub rtc_frame_width: Option<u32>,
533
534 pub rtc_packets_lost: Option<u32>,
538
539 pub rtc_pli_count: Option<u32>,
543
544 pub rtc_pause_count: Option<u32>,
548
549 pub rtc_total_pauses_duration_sec: Option<f32>,
553
554 pub rtc_stun_rtt_sec: Option<f32>,
562}
563
564#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
566pub struct RtcIceCandidate {
567 pub stats_id: String,
569 pub foundation: String,
571 pub priority: u32,
573 pub address: String,
575 pub protocol: RtcIceProtocol,
577 pub port: u16,
579 pub typ: RtcIceCandidateType,
581 pub component: u16,
583 pub related_address: String,
585 pub related_port: u16,
587 pub tcp_type: String,
589}
590
591#[cfg(feature = "webrtc")]
592impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
593 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
594 Self {
595 stats_id: candidate.stats_id,
596 foundation: candidate.foundation,
597 priority: candidate.priority,
598 address: candidate.address,
599 protocol: candidate.protocol.into(),
600 port: candidate.port,
601 typ: candidate.typ.into(),
602 component: candidate.component,
603 related_address: candidate.related_address,
604 related_port: candidate.related_port,
605 tcp_type: candidate.tcp_type,
606 }
607 }
608}
609
610#[cfg(feature = "webrtc")]
611impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
612 fn from(candidate: RtcIceCandidate) -> Self {
613 Self {
614 stats_id: candidate.stats_id,
615 foundation: candidate.foundation,
616 priority: candidate.priority,
617 address: candidate.address,
618 protocol: candidate.protocol.into(),
619 port: candidate.port,
620 typ: candidate.typ.into(),
621 component: candidate.component,
622 related_address: candidate.related_address,
623 related_port: candidate.related_port,
624 tcp_type: candidate.tcp_type,
625 }
626 }
627}
628
629#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
631#[serde(rename_all = "snake_case")]
632pub enum RtcIceCandidateType {
633 #[default]
635 Unspecified,
636
637 Host,
643
644 Srflx,
651
652 Prflx,
657
658 Relay,
662}
663
664#[cfg(feature = "webrtc")]
665impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
666 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
667 match candidate_type {
668 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
669 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
670 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
671 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
672 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
673 RtcIceCandidateType::Unspecified
674 }
675 }
676 }
677}
678
679#[cfg(feature = "webrtc")]
680impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
681 fn from(candidate_type: RtcIceCandidateType) -> Self {
682 match candidate_type {
683 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
684 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
685 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
686 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
687 RtcIceCandidateType::Unspecified => {
688 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
689 }
690 }
691 }
692}
693
694#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
697#[serde(rename_all = "snake_case")]
698pub enum RtcIceProtocol {
699 #[default]
701 Unspecified,
702
703 Udp,
705
706 Tcp,
708}
709
710#[cfg(feature = "webrtc")]
711impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
712 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
713 match protocol {
714 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
715 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
716 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
717 }
718 }
719}
720
721#[cfg(feature = "webrtc")]
722impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
723 fn from(protocol: RtcIceProtocol) -> Self {
724 match protocol {
725 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
726 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
727 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
728 }
729 }
730}
731
732#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
734#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
735#[serde(rename_all = "camelCase")]
736pub struct RtcIceCandidateInit {
738 pub candidate: String,
740 pub sdp_mid: Option<String>,
743 #[serde(rename = "sdpMLineIndex")]
746 pub sdp_mline_index: Option<u16>,
747 pub username_fragment: Option<String>,
750}
751
752#[cfg(feature = "webrtc")]
753impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
754 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
755 Self {
756 candidate: candidate.candidate,
757 sdp_mid: candidate.sdp_mid,
758 sdp_mline_index: candidate.sdp_mline_index,
759 username_fragment: candidate.username_fragment,
760 }
761 }
762}
763
764#[cfg(feature = "webrtc")]
765impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
766 fn from(candidate: RtcIceCandidateInit) -> Self {
767 Self {
768 candidate: candidate.candidate,
769 sdp_mid: candidate.sdp_mid,
770 sdp_mline_index: candidate.sdp_mline_index,
771 username_fragment: candidate.username_fragment,
772 }
773 }
774}
775
776#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
778#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
779pub struct RtcSessionDescription {
780 #[serde(rename = "type")]
782 pub sdp_type: RtcSdpType,
783
784 pub sdp: String,
786}
787
788#[cfg(feature = "webrtc")]
789impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
790 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
791 Self {
792 sdp_type: desc.sdp_type.into(),
793 sdp: desc.sdp,
794 }
795 }
796}
797
798#[cfg(feature = "webrtc")]
799impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
800 type Error = anyhow::Error;
801
802 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
803 let result = match desc.sdp_type {
804 RtcSdpType::Offer => {
805 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
806 }
807 RtcSdpType::Pranswer => {
808 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
809 }
810 RtcSdpType::Answer => {
811 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
812 }
813 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
814 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
815 };
816
817 Ok(result)
818 }
819}
820
821#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
823#[serde(rename_all = "snake_case")]
824#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
825pub enum RtcSdpType {
826 #[default]
828 Unspecified = 0,
829
830 Offer,
832
833 Pranswer,
838
839 Answer,
844
845 Rollback,
851}
852
853#[cfg(feature = "webrtc")]
854impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
855 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
856 match sdp_type {
857 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
858 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
859 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
860 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
861 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
862 }
863 }
864}
865
866#[cfg(feature = "webrtc")]
867impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
868 fn from(sdp_type: RtcSdpType) -> Self {
869 match sdp_type {
870 RtcSdpType::Offer => Self::Offer,
871 RtcSdpType::Pranswer => Self::Pranswer,
872 RtcSdpType::Answer => Self::Answer,
873 RtcSdpType::Rollback => Self::Rollback,
874 RtcSdpType::Unspecified => Self::Unspecified,
875 }
876 }
877}
878#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone)]
880#[serde(rename_all = "snake_case")]
881pub struct ModelingSessionData {
882 pub api_call_id: String,
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890 use crate::output;
891
892 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
893
894 #[test]
895 fn serialize_websocket_modeling_ok() {
896 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
897 success: true,
898 request_id: Some(REQ_ID),
899 resp: OkWebSocketResponseData::Modeling {
900 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
901 control_points: vec![],
902 }),
903 },
904 });
905 let expected = serde_json::json!({
906 "success": true,
907 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
908 "resp": {
909 "type": "modeling",
910 "data": {
911 "modeling_response": {
912 "type": "curve_get_control_points",
913 "data": { "control_points": [] }
914 }
915 }
916 }
917 });
918 assert_json_eq(actual, expected);
919 }
920
921 #[test]
922 fn serialize_websocket_webrtc_ok() {
923 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
924 success: true,
925 request_id: Some(REQ_ID),
926 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
927 });
928 let expected = serde_json::json!({
929 "success": true,
930 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
931 "resp": {
932 "type": "ice_server_info",
933 "data": {
934 "ice_servers": []
935 }
936 }
937 });
938 assert_json_eq(actual, expected);
939 }
940
941 #[test]
942 fn serialize_websocket_export_ok() {
943 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
944 success: true,
945 request_id: Some(REQ_ID),
946 resp: OkWebSocketResponseData::Export { files: vec![] },
947 });
948 let expected = serde_json::json!({
949 "success": true,
950 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
951 "resp": {
952 "type": "export",
953 "data": {"files": [] }
954 }
955 });
956 assert_json_eq(actual, expected);
957 }
958
959 #[test]
960 fn serialize_websocket_err() {
961 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
962 success: false,
963 request_id: Some(REQ_ID),
964 errors: vec![ApiError {
965 error_code: ErrorCode::InternalApi,
966 message: "you fucked up!".to_owned(),
967 }],
968 });
969 let expected = serde_json::json!({
970 "success": false,
971 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
972 "errors": [
973 {
974 "error_code": "internal_api",
975 "message": "you fucked up!"
976 }
977 ],
978 });
979 assert_json_eq(actual, expected);
980 }
981
982 #[test]
983 fn serialize_websocket_metrics() {
984 let actual = WebSocketRequest::MetricsResponse {
985 metrics: Box::new(ClientMetrics {
986 rtc_frames_dropped: Some(1),
987 rtc_frames_decoded: Some(2),
988 rtc_frames_per_second: Some(3),
989 rtc_frames_received: Some(4),
990 rtc_freeze_count: Some(5),
991 rtc_jitter_sec: Some(6.7),
992 rtc_keyframes_decoded: Some(8),
993 rtc_total_freezes_duration_sec: Some(9.1),
994 rtc_frame_height: Some(100),
995 rtc_frame_width: Some(100),
996 rtc_packets_lost: Some(0),
997 rtc_pli_count: Some(0),
998 rtc_pause_count: Some(0),
999 rtc_total_pauses_duration_sec: Some(0.0),
1000 rtc_stun_rtt_sec: Some(0.005),
1001 }),
1002 };
1003 let expected = serde_json::json!({
1004 "type": "metrics_response",
1005 "metrics": {
1006 "rtc_frames_dropped": 1,
1007 "rtc_frames_decoded": 2,
1008 "rtc_frames_per_second": 3,
1009 "rtc_frames_received": 4,
1010 "rtc_freeze_count": 5,
1011 "rtc_jitter_sec": 6.7,
1012 "rtc_keyframes_decoded": 8,
1013 "rtc_total_freezes_duration_sec": 9.1,
1014 "rtc_frame_height": 100,
1015 "rtc_frame_width": 100,
1016 "rtc_packets_lost": 0,
1017 "rtc_pli_count": 0,
1018 "rtc_pause_count": 0,
1019 "rtc_total_pauses_duration_sec": 0.0,
1020 "rtc_stun_rtt_sec": 0.005,
1021 },
1022 });
1023 assert_json_eq(actual, expected);
1024 }
1025
1026 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
1027 let json_str = serde_json::to_string(&actual).unwrap();
1028 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1029 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
1030 }
1031}