medea_client_api_proto/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(any(doc, test), doc = include_str!("../README.md"))]
3#![cfg_attr(not(any(doc, test)), doc = env!("CARGO_PKG_NAME"))]
4#![deny(nonstandard_style, rustdoc::all, trivial_casts, trivial_numeric_casts)]
5#![forbid(non_ascii_idents, unsafe_code)]
6#![warn(
7    clippy::absolute_paths,
8    clippy::allow_attributes,
9    clippy::allow_attributes_without_reason,
10    clippy::as_conversions,
11    clippy::as_pointer_underscore,
12    clippy::as_ptr_cast_mut,
13    clippy::assertions_on_result_states,
14    clippy::branches_sharing_code,
15    clippy::cfg_not_test,
16    clippy::clear_with_drain,
17    clippy::clone_on_ref_ptr,
18    clippy::collection_is_never_read,
19    clippy::create_dir,
20    clippy::dbg_macro,
21    clippy::debug_assert_with_mut_call,
22    clippy::decimal_literal_representation,
23    clippy::default_union_representation,
24    clippy::derive_partial_eq_without_eq,
25    clippy::doc_include_without_cfg,
26    clippy::else_if_without_else,
27    clippy::empty_drop,
28    clippy::empty_structs_with_brackets,
29    clippy::equatable_if_let,
30    clippy::empty_enum_variants_with_brackets,
31    clippy::exit,
32    clippy::expect_used,
33    clippy::fallible_impl_from,
34    clippy::filetype_is_file,
35    clippy::float_cmp_const,
36    clippy::fn_to_numeric_cast_any,
37    clippy::format_push_string,
38    clippy::get_unwrap,
39    clippy::if_then_some_else_none,
40    clippy::imprecise_flops,
41    clippy::infinite_loop,
42    clippy::iter_on_empty_collections,
43    clippy::iter_on_single_items,
44    clippy::iter_over_hash_type,
45    clippy::iter_with_drain,
46    clippy::large_include_file,
47    clippy::large_stack_frames,
48    clippy::let_underscore_untyped,
49    clippy::literal_string_with_formatting_args,
50    clippy::lossy_float_literal,
51    clippy::map_err_ignore,
52    clippy::map_with_unused_argument_over_ranges,
53    clippy::mem_forget,
54    clippy::missing_assert_message,
55    clippy::missing_asserts_for_indexing,
56    clippy::missing_const_for_fn,
57    clippy::missing_docs_in_private_items,
58    clippy::module_name_repetitions,
59    clippy::multiple_inherent_impl,
60    clippy::multiple_unsafe_ops_per_block,
61    clippy::mutex_atomic,
62    clippy::mutex_integer,
63    clippy::needless_collect,
64    clippy::needless_pass_by_ref_mut,
65    clippy::needless_raw_strings,
66    clippy::non_zero_suggestions,
67    clippy::nonstandard_macro_braces,
68    clippy::option_if_let_else,
69    clippy::or_fun_call,
70    clippy::panic_in_result_fn,
71    clippy::partial_pub_fields,
72    clippy::pathbuf_init_then_push,
73    clippy::pedantic,
74    clippy::print_stderr,
75    clippy::print_stdout,
76    clippy::pub_without_shorthand,
77    clippy::rc_buffer,
78    clippy::rc_mutex,
79    clippy::read_zero_byte_vec,
80    clippy::redundant_clone,
81    clippy::redundant_type_annotations,
82    clippy::renamed_function_params,
83    clippy::ref_patterns,
84    clippy::rest_pat_in_fully_bound_structs,
85    clippy::same_name_method,
86    clippy::semicolon_inside_block,
87    clippy::set_contains_or_insert,
88    clippy::shadow_unrelated,
89    clippy::significant_drop_in_scrutinee,
90    clippy::significant_drop_tightening,
91    clippy::str_to_string,
92    clippy::string_add,
93    clippy::string_lit_as_bytes,
94    clippy::string_lit_chars_any,
95    clippy::string_slice,
96    clippy::string_to_string,
97    clippy::suboptimal_flops,
98    clippy::suspicious_operation_groupings,
99    clippy::suspicious_xor_used_as_pow,
100    clippy::tests_outside_test_module,
101    clippy::todo,
102    clippy::too_long_first_doc_paragraph,
103    clippy::trailing_empty_array,
104    clippy::transmute_undefined_repr,
105    clippy::trivial_regex,
106    clippy::try_err,
107    clippy::undocumented_unsafe_blocks,
108    clippy::unimplemented,
109    clippy::uninhabited_references,
110    clippy::unnecessary_safety_comment,
111    clippy::unnecessary_safety_doc,
112    clippy::unnecessary_self_imports,
113    clippy::unnecessary_struct_initialization,
114    clippy::unneeded_field_pattern,
115    clippy::unused_peekable,
116    clippy::unused_result_ok,
117    clippy::unused_trait_names,
118    clippy::unwrap_in_result,
119    clippy::unwrap_used,
120    clippy::use_debug,
121    clippy::use_self,
122    clippy::useless_let_if_seq,
123    clippy::verbose_file_reads,
124    clippy::while_float,
125    clippy::wildcard_enum_match_arm,
126    ambiguous_negative_literals,
127    closure_returning_async_block,
128    future_incompatible,
129    impl_trait_redundant_captures,
130    let_underscore_drop,
131    macro_use_extern_crate,
132    meta_variable_misuse,
133    missing_abi,
134    missing_copy_implementations,
135    missing_debug_implementations,
136    missing_docs,
137    redundant_lifetimes,
138    rust_2018_idioms,
139    single_use_lifetimes,
140    unit_bindings,
141    unnameable_types,
142    unreachable_pub,
143    unstable_features,
144    unused,
145    variant_size_differences
146)]
147
148pub mod state;
149pub mod stats;
150
151use std::{
152    collections::HashMap,
153    hash::{Hash, Hasher},
154};
155
156use derive_more::with_trait::{Constructor, Display, From, Into};
157use medea_macro::dispatchable;
158use rand::{Rng as _, distr::Alphanumeric};
159use secrecy::{ExposeSecret as _, SecretString};
160use serde::{Deserialize, Serialize, Serializer};
161
162use self::stats::RtcStat;
163
164/// ID of a `Room`.
165#[derive(
166    Clone, Debug, Display, Serialize, Deserialize, Eq, From, Hash, PartialEq,
167)]
168#[from(forward)]
169pub struct RoomId(pub String);
170
171/// ID of a `Member`.
172#[derive(
173    Clone, Debug, Display, Serialize, Deserialize, Eq, From, Hash, PartialEq,
174)]
175#[from(forward)]
176pub struct MemberId(pub String);
177
178/// ID of a `Peer`.
179#[cfg_attr(feature = "server", derive(Default))]
180#[derive(
181    Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize,
182)]
183pub struct PeerId(pub u32);
184
185/// ID of a `MediaTrack`.
186#[cfg_attr(feature = "server", derive(Default))]
187#[derive(
188    Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize,
189)]
190pub struct TrackId(pub u32);
191
192/// Secret used for a client authentication on an [`IceServer`].
193#[derive(Clone, Debug, Deserialize, From, Into)]
194pub struct IcePassword(SecretString);
195
196impl IcePassword {
197    /// Length of a randomly generated [`IcePassword`].
198    const RANDOM_LENGTH: usize = 16;
199
200    /// Provides access to the underlying secret [`str`].
201    #[must_use]
202    pub fn expose_str(&self) -> &str {
203        self.0.expose_secret()
204    }
205
206    /// Generates a new random [`IcePassword`].
207    #[must_use]
208    pub fn random() -> Self {
209        Self(
210            rand::rng()
211                .sample_iter(&Alphanumeric)
212                .take(Self::RANDOM_LENGTH)
213                .map(char::from)
214                .collect::<String>()
215                .into(),
216        )
217    }
218}
219impl Serialize for IcePassword {
220    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
221    where
222        S: Serializer,
223    {
224        self.0.expose_secret().serialize(serializer)
225    }
226}
227
228impl Eq for IcePassword {}
229
230impl PartialEq for IcePassword {
231    fn eq(&self, other: &Self) -> bool {
232        use subtle::ConstantTimeEq as _;
233
234        self.expose_str().as_bytes().ct_eq(other.expose_str().as_bytes()).into()
235    }
236}
237
238/// Credential used for a `Member` authentication.
239#[derive(Clone, Debug, Deserialize)]
240pub struct Credential(SecretString);
241
242impl Serialize for Credential {
243    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
244    where
245        S: Serializer,
246    {
247        self.0.expose_secret().serialize(serializer)
248    }
249}
250
251impl Credential {
252    /// Provides access to the underlying secret [`str`].
253    #[must_use]
254    pub fn expose_str(&self) -> &str {
255        self.0.expose_secret()
256    }
257}
258
259impl<T> From<T> for Credential
260where
261    T: Into<String>,
262{
263    fn from(value: T) -> Self {
264        Self(value.into().into())
265    }
266}
267
268impl Hash for Credential {
269    fn hash<H: Hasher>(&self, state: &mut H) {
270        self.expose_str().hash(state);
271    }
272}
273
274impl Eq for Credential {}
275
276impl PartialEq for Credential {
277    fn eq(&self, other: &Self) -> bool {
278        use subtle::ConstantTimeEq as _;
279
280        self.expose_str().as_bytes().ct_eq(other.expose_str().as_bytes()).into()
281    }
282}
283
284#[cfg(feature = "server")]
285/// Value being able to be increment by `1`.
286pub trait Incrementable {
287    /// Returns current value + 1.
288    #[must_use]
289    fn incr(&self) -> Self;
290}
291
292#[cfg(feature = "server")]
293/// Implements [`Incrementable`] trait for a newtype with any numeric type.
294macro_rules! impl_incrementable {
295    ($name:ty) => {
296        impl Incrementable for $name {
297            fn incr(&self) -> Self {
298                Self(self.0 + 1)
299            }
300        }
301    };
302}
303
304#[cfg(feature = "server")]
305impl_incrementable!(PeerId);
306#[cfg(feature = "server")]
307impl_incrementable!(TrackId);
308
309/// Message sent by Media Server to Web Client.
310#[cfg_attr(
311    any(
312        target_family = "wasm",
313        all(target_arch = "arm", target_os = "android")
314    ),
315    expect(variant_size_differences, reason = "`Event` is the most common")
316)]
317#[derive(Clone, Debug, Eq, PartialEq)]
318#[cfg_attr(feature = "client", derive(Deserialize))]
319#[cfg_attr(feature = "server", derive(Serialize))]
320#[serde(tag = "msg", content = "data")]
321pub enum ServerMsg {
322    /// `ping` message that Media Server is expected to send to Web Client
323    /// periodically for probing its aliveness.
324    Ping(u32),
325
326    /// Media Server notifies Web Client about happened facts and it reacts on
327    /// them to reach the proper state.
328    Event {
329        /// ID of the `Room` that this [`Event`] is associated with.
330        room_id: RoomId,
331
332        /// Actual [`Event`] sent to Web Client.
333        event: Event,
334    },
335
336    /// Media Server notifies Web Client about necessity to update its RPC
337    /// settings.
338    RpcSettings(RpcSettings),
339}
340
341/// Message by Web Client to Media Server.
342#[cfg_attr(
343    any(
344        target_family = "wasm",
345        all(target_arch = "arm", target_os = "android")
346    ),
347    expect(variant_size_differences, reason = "`Command` is the most common")
348)]
349#[derive(Clone, Debug, PartialEq)]
350#[cfg_attr(feature = "client", derive(Serialize))]
351#[cfg_attr(feature = "server", derive(Deserialize))]
352pub enum ClientMsg {
353    /// `pong` message that Web Client answers with to Media Server in response
354    /// to received [`ServerMsg::Ping`].
355    Pong(u32),
356
357    /// Request of Web Client to change its state on Media Server.
358    Command {
359        /// ID of the `Room` that this [`Command`] is associated with.
360        room_id: RoomId,
361
362        /// Actual [`Command`] sent to Media Server.
363        command: Command,
364    },
365}
366
367/// RPC settings of Web Client received from Media Server.
368#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
369pub struct RpcSettings {
370    /// Timeout of considering Web Client as lost by Media Server when it
371    /// doesn't receive any [`ClientMsg::Pong`]s.
372    ///
373    /// Unit: millisecond.
374    pub idle_timeout_ms: u32,
375
376    /// Interval that Media Server sends [`ServerMsg::Ping`]s with.
377    ///
378    /// Unit: millisecond.
379    pub ping_interval_ms: u32,
380}
381
382/// Possible commands sent by Web Client to Media Server.
383#[dispatchable]
384#[cfg_attr(feature = "client", derive(Serialize))]
385#[cfg_attr(feature = "server", derive(Deserialize))]
386#[derive(Clone, Debug, PartialEq)]
387#[serde(tag = "command", content = "data")]
388pub enum Command {
389    /// Request to join a `Room`.
390    JoinRoom {
391        /// ID of the `Member` who joins the `Room`.
392        member_id: MemberId,
393
394        /// [`Credential`] of the `Member` to authenticate with.
395        credential: Credential,
396    },
397
398    /// Request to leave a `Room`.
399    LeaveRoom {
400        /// ID of the `Member` who leaves the `Room`.
401        member_id: MemberId,
402    },
403
404    /// Web Client sends SDP Offer.
405    MakeSdpOffer {
406        /// ID of the `Peer` SDP Offer is sent for.
407        peer_id: PeerId,
408
409        /// SDP Offer of the `Peer`.
410        sdp_offer: String,
411
412        /// Associations between [`Track`] and transceiver's
413        /// [media description][1].
414        ///
415        /// `mid` is basically an ID of [`m=<media>` section][1] in SDP.
416        ///
417        /// [1]: https://tools.ietf.org/html/rfc4566#section-5.14
418        mids: HashMap<TrackId, String>,
419
420        /// Statuses of the `Peer` transceivers.
421        transceivers_statuses: HashMap<TrackId, bool>,
422    },
423
424    /// Web Client sends SDP Answer.
425    MakeSdpAnswer {
426        /// ID of the `Peer` SDP Answer is sent for.
427        peer_id: PeerId,
428
429        /// SDP Answer of the `Peer`.
430        sdp_answer: String,
431
432        /// Statuses of the `Peer` transceivers.
433        transceivers_statuses: HashMap<TrackId, bool>,
434    },
435
436    /// Web Client sends an Ice Candidate.
437    SetIceCandidate {
438        /// ID of the `Peer` the Ice Candidate is sent for.
439        peer_id: PeerId,
440
441        /// [`IceCandidate`] sent by the `Peer`.
442        candidate: IceCandidate,
443    },
444
445    /// Web Client sends Peer Connection metrics.
446    AddPeerConnectionMetrics {
447        /// ID of the `Peer` metrics are sent for.
448        peer_id: PeerId,
449
450        /// Metrics of the `Peer`.
451        metrics: PeerMetrics,
452    },
453
454    /// Web Client asks permission to update [`Track`]s in the specified
455    /// `Peer`. Media Server gives permission by sending
456    /// [`Event::PeerUpdated`].
457    UpdateTracks {
458        /// ID of the `Peer` to update [`Track`]s in.
459        peer_id: PeerId,
460
461        /// Patches for updating the [`Track`]s.
462        tracks_patches: Vec<TrackPatchCommand>,
463    },
464
465    /// Web Client asks Media Server to synchronize Client State with a
466    /// Server State.
467    SynchronizeMe {
468        /// Whole Client State of the `Room`.
469        state: state::Room,
470    },
471}
472
473/// Web Client's `PeerConnection` metrics.
474#[cfg_attr(feature = "client", derive(Serialize))]
475#[cfg_attr(feature = "server", derive(Deserialize))]
476#[derive(Clone, Debug, PartialEq)]
477pub enum PeerMetrics {
478    /// `PeerConnection`'s ICE connection state.
479    IceConnectionState(IceConnectionState),
480
481    /// `PeerConnection`'s connection state.
482    PeerConnectionState(PeerConnectionState),
483
484    /// `PeerConnection` related error occurred.
485    PeerConnectionError(PeerConnectionError),
486
487    /// `PeerConnection`'s RTC stats.
488    RtcStats(Vec<RtcStat>),
489}
490
491/// Possible errors related to a `PeerConnection`.
492#[cfg_attr(feature = "client", derive(Serialize))]
493#[cfg_attr(feature = "server", derive(Deserialize))]
494#[derive(Clone, Debug, Eq, PartialEq)]
495pub enum PeerConnectionError {
496    /// Error occurred with ICE candidate from a `PeerConnection`.
497    IceCandidate(IceCandidateError),
498}
499
500/// Error occurred with an [ICE] candidate from a `PeerConnection`.
501///
502/// [ICE]: https://webrtcglossary.com/ice
503#[cfg_attr(feature = "client", derive(Serialize))]
504#[cfg_attr(feature = "server", derive(Deserialize))]
505#[derive(Clone, Debug, Eq, PartialEq)]
506pub struct IceCandidateError {
507    /// Local IP address used to communicate with a [STUN]/[TURN] server.
508    ///
509    /// [STUN]: https://webrtcglossary.com/stun
510    /// [TURN]: https://webrtcglossary.com/turn
511    pub address: Option<String>,
512
513    /// Port used to communicate with a [STUN]/[TURN] server.
514    ///
515    /// [STUN]: https://webrtcglossary.com/stun
516    /// [TURN]: https://webrtcglossary.com/turn
517    pub port: Option<u32>,
518
519    /// URL identifying the [STUN]/[TURN] server for which the failure
520    /// occurred.
521    ///
522    /// [STUN]: https://webrtcglossary.com/stun
523    /// [TURN]: https://webrtcglossary.com/turn
524    pub url: String,
525
526    /// Numeric [STUN] error code returned by the [STUN]/[TURN] server.
527    ///
528    /// If no host candidate can reach the server, this error code will be set
529    /// to the value `701`, which is outside the [STUN] error code range. This
530    /// error is only fired once per server URL while in the
531    /// `RTCIceGatheringState` of "gathering".
532    ///
533    /// [STUN]: https://webrtcglossary.com/stun
534    /// [TURN]: https://webrtcglossary.com/turn
535    pub error_code: i32,
536
537    /// [STUN] reason text returned by the [STUN]/[TURN] server.
538    ///
539    /// If the server could not be reached, this reason test will be set to an
540    /// implementation-specific value providing details about the error.
541    ///
542    /// [STUN]: https://webrtcglossary.com/stun
543    /// [TURN]: https://webrtcglossary.com/turn
544    pub error_text: String,
545}
546
547/// `PeerConnection`'s ICE connection state.
548#[cfg_attr(feature = "client", derive(Serialize))]
549#[cfg_attr(feature = "server", derive(Deserialize))]
550#[derive(Clone, Copy, Debug, Eq, PartialEq)]
551pub enum IceConnectionState {
552    /// ICE agent is gathering addresses or is waiting to be given remote
553    /// candidates.
554    New,
555
556    /// ICE agent has been given one or more remote candidates and is checking
557    /// pairs of local and remote candidates against one another to try to find
558    /// a compatible match, but hasn't yet found a pair which will allow the
559    /// `PeerConnection` to be made. It's possible that gathering of candidates
560    /// is also still underway.
561    Checking,
562
563    /// Usable pairing of local and remote candidates has been found for all
564    /// components of the connection, and the connection has been established.
565    /// It's possible that gathering is still underway, and it's also possible
566    /// that the ICE agent is still checking candidates against one another
567    /// looking for a better connection to use.
568    Connected,
569
570    /// ICE agent has finished gathering candidates, has checked all pairs
571    /// against one another, and has found a connection for all components.
572    Completed,
573
574    /// ICE candidate has checked all candidates pairs against one another and
575    /// has failed to find compatible matches for all components of the
576    /// connection. It is, however, possible that the ICE agent did find
577    /// compatible connections for some components.
578    Failed,
579
580    /// Checks to ensure that components are still connected failed for at
581    /// least one component of the `PeerConnection`. This is a less stringent
582    /// test than [`IceConnectionState::Failed`] and may trigger intermittently
583    /// and resolve just as spontaneously on less reliable networks, or during
584    /// temporary disconnections. When the problem resolves, the connection may
585    /// return to the [`IceConnectionState::Connected`] state.
586    Disconnected,
587
588    /// ICE agent for this `PeerConnection` has shut down and is no longer
589    /// handling requests.
590    Closed,
591}
592
593/// `PeerConnection`'s connection state.
594#[cfg_attr(feature = "client", derive(Serialize))]
595#[cfg_attr(feature = "server", derive(Deserialize))]
596#[derive(Clone, Copy, Debug, Eq, PartialEq)]
597pub enum PeerConnectionState {
598    /// At least one of the connection's ICE transports are in the
599    /// [`IceConnectionState::New`] state, and none of them are in one
600    /// of the following states: [`IceConnectionState::Checking`],
601    /// [`IceConnectionState::Failed`], or
602    /// [`IceConnectionState::Disconnected`], or all of the connection's
603    /// transports are in the [`IceConnectionState::Closed`] state.
604    New,
605
606    /// One or more of the ICE transports are currently in the process of
607    /// establishing a connection; that is, their [`IceConnectionState`] is
608    /// either [`IceConnectionState::Checking`] or
609    /// [`IceConnectionState::Connected`], and no transports are in the
610    /// [`IceConnectionState::Failed`] state.
611    Connecting,
612
613    /// Every ICE transport used by the connection is either in use (state
614    /// [`IceConnectionState::Connected`] or [`IceConnectionState::Completed`])
615    /// or is closed ([`IceConnectionState::Closed`]); in addition,
616    /// at least one transport is either [`IceConnectionState::Connected`] or
617    /// [`IceConnectionState::Completed`].
618    Connected,
619
620    /// At least one of the ICE transports for the connection is in the
621    /// [`IceConnectionState::Disconnected`] state and none of the other
622    /// transports are in the state [`IceConnectionState::Failed`] or
623    /// [`IceConnectionState::Checking`].
624    Disconnected,
625
626    /// One or more of the ICE transports on the connection is in the
627    /// [`IceConnectionState::Failed`] state.
628    Failed,
629
630    /// The `PeerConnection` is closed.
631    Closed,
632}
633
634impl From<IceConnectionState> for PeerConnectionState {
635    fn from(ice_con_state: IceConnectionState) -> Self {
636        use IceConnectionState as Ice;
637
638        match ice_con_state {
639            Ice::New => Self::New,
640            Ice::Checking => Self::Connecting,
641            Ice::Connected | Ice::Completed => Self::Connected,
642            Ice::Failed => Self::Failed,
643            Ice::Disconnected => Self::Disconnected,
644            Ice::Closed => Self::Closed,
645        }
646    }
647}
648
649/// Reason of disconnecting Web Client from Media Server.
650#[derive(
651    Copy, Clone, Debug, Deserialize, Display, Eq, PartialEq, Serialize,
652)]
653pub enum CloseReason {
654    /// Client session was finished on a server side.
655    Finished,
656
657    /// Old connection was closed due to a client reconnection.
658    Reconnected,
659
660    /// Connection has been inactive for a while and thus considered idle
661    /// by a server.
662    Idle,
663
664    /// Establishing of connection with a server was rejected on server side.
665    ///
666    /// Most likely because of incorrect `Member` credentials.
667    Rejected,
668
669    /// Server internal error has occurred while connecting.
670    ///
671    /// This close reason is similar to 500 HTTP status code.
672    InternalError,
673
674    /// Client was evicted on the server side.
675    Evicted,
676}
677
678/// Description which is sent in [Close] WebSocket frame from Media Server to
679/// Web Client.
680///
681/// [Close]: https://tools.ietf.org/html/rfc6455#section-5.5.1
682#[derive(
683    Clone, Constructor, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
684)]
685pub struct CloseDescription {
686    /// Reason of why WebSocket connection has been closed.
687    pub reason: CloseReason,
688}
689
690/// Possible WebSocket messages sent from Media Server to Web Client.
691#[dispatchable(self: &Self, async_trait(?Send))]
692#[cfg_attr(feature = "client", derive(Deserialize))]
693#[cfg_attr(feature = "server", derive(Serialize))]
694#[derive(Clone, Debug, Eq, PartialEq)]
695#[serde(tag = "event", content = "data")]
696pub enum Event {
697    /// Media Server notifies Web Client that a `Member` joined a `Room`.
698    RoomJoined {
699        /// ID of the `Member` who joined the `Room`.
700        member_id: MemberId,
701    },
702
703    /// Media Server notifies Web Client that a `Member` left a `Room`.
704    RoomLeft {
705        /// [`CloseReason`] with which the `Member` left the `Room`.
706        close_reason: CloseReason,
707    },
708
709    /// Media Server notifies Web Client about necessity of RTCPeerConnection
710    /// creation.
711    PeerCreated {
712        /// ID of the `Peer` to create RTCPeerConnection for.
713        peer_id: PeerId,
714
715        /// [`NegotiationRole`] of the `Peer`.
716        negotiation_role: NegotiationRole,
717
718        /// Indicator whether this `Peer` is working in a [P2P mesh] or [SFU]
719        /// mode.
720        ///
721        /// [P2P mesh]: https://webrtcglossary.com/mesh
722        /// [SFU]: https://webrtcglossary.com/sfu
723        connection_mode: ConnectionMode,
724
725        /// [`Track`]s to create RTCPeerConnection with.
726        tracks: Vec<Track>,
727
728        /// [`IceServer`]s to create RTCPeerConnection with.
729        ice_servers: Vec<IceServer>,
730
731        /// Indicator whether the created RTCPeerConnection should be forced to
732        /// use relay [`IceServer`]s only.
733        force_relay: bool,
734    },
735
736    /// Media Server notifies Web Client about necessity to apply the specified
737    /// SDP Answer to Web Client's RTCPeerConnection.
738    SdpAnswerMade {
739        /// ID of the `Peer` to apply SDP Answer to.
740        peer_id: PeerId,
741
742        /// SDP Answer to be applied.
743        sdp_answer: String,
744    },
745
746    /// Media Server notifies Web Client that his SDP offer was applied.
747    LocalDescriptionApplied {
748        /// ID of the `Peer` which SDP offer was applied.
749        peer_id: PeerId,
750
751        /// SDP offer that was applied.
752        sdp_offer: String,
753    },
754
755    /// Media Server notifies Web Client about necessity to apply the specified
756    /// ICE Candidate.
757    IceCandidateDiscovered {
758        /// ID of the `Peer` to apply ICE Candidate to.
759        peer_id: PeerId,
760
761        /// ICE Candidate to be applied.
762        candidate: IceCandidate,
763    },
764
765    /// Media Server notifies Web Client about necessity of RTCPeerConnection
766    /// close.
767    PeersRemoved {
768        /// IDs of `Peer`s to be removed.
769        peer_ids: Vec<PeerId>,
770    },
771
772    /// Media Server notifies about necessity to update [`Track`]s in a `Peer`.
773    PeerUpdated {
774        /// ID of the `Peer` to update [`Track`]s in.
775        peer_id: PeerId,
776
777        /// List of [`PeerUpdate`]s which should be applied.
778        updates: Vec<PeerUpdate>,
779
780        /// Negotiation role basing on which should be sent
781        /// [`Command::MakeSdpOffer`] or [`Command::MakeSdpAnswer`].
782        ///
783        /// If [`None`] then no (re)negotiation should be done.
784        negotiation_role: Option<NegotiationRole>,
785    },
786
787    /// Media Server notifies about connection quality score update.
788    ConnectionQualityUpdated {
789        /// Partner [`MemberId`] of the `Peer`.
790        partner_member_id: MemberId,
791
792        /// Estimated connection quality.
793        quality_score: ConnectionQualityScore,
794    },
795
796    /// Media Server synchronizes Web Client state and reports the proper one.
797    StateSynchronized {
798        /// Proper state that should be assumed by Web Client.
799        state: state::Room,
800    },
801}
802
803/// `Peer`'s negotiation role.
804///
805/// Some [`Event`]s can trigger SDP negotiation:
806/// - If [`Event`] contains [`NegotiationRole::Offerer`], then `Peer` is
807///   expected to create SDP Offer and send it via [`Command::MakeSdpOffer`].
808/// - If [`Event`] contains [`NegotiationRole::Answerer`], then `Peer` is
809///   expected to apply provided SDP Offer and provide its SDP Answer in a
810///   [`Command::MakeSdpAnswer`].
811#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
812pub enum NegotiationRole {
813    /// [`Command::MakeSdpOffer`] should be sent by client.
814    Offerer,
815
816    /// [`Command::MakeSdpAnswer`] should be sent by client.
817    Answerer(String),
818}
819
820/// Indication whether a `Peer` is working in a [P2P mesh] or [SFU] mode.
821///
822/// [P2P mesh]: https://webrtcglossary.com/mesh
823/// [SFU]: https://webrtcglossary.com/sfu
824#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
825pub enum ConnectionMode {
826    /// `Peer` is configured to work in a [P2P mesh] mode.
827    ///
828    /// [P2P mesh]: https://webrtcglossary.com/mesh
829    Mesh,
830
831    /// `Peer` is configured to work in an [SFU] mode.
832    ///
833    /// [SFU]: https://webrtcglossary.com/sfu
834    Sfu,
835}
836
837/// [`Track`] update which should be applied to the `Peer`.
838#[cfg_attr(feature = "client", derive(Deserialize))]
839#[cfg_attr(feature = "server", derive(Serialize))]
840#[derive(Clone, Debug, Eq, PartialEq)]
841pub enum PeerUpdate {
842    /// New [`Track`] should be added to the `Peer`.
843    Added(Track),
844
845    /// [`Track`] with the provided [`TrackId`] should be removed from the
846    /// `Peer`.
847    ///
848    /// Can only refer [`Track`]s already known to the `Peer`.
849    Removed(TrackId),
850
851    /// [`Track`] should be updated by this [`TrackPatchEvent`] in the `Peer`.
852    /// Can only refer tracks already known to the `Peer`.
853    Updated(TrackPatchEvent),
854
855    /// `Peer` should start ICE restart process on the next renegotiation.
856    IceRestart,
857}
858
859/// Representation of [RTCIceCandidateInit][1] object.
860///
861/// [1]: https://w3.org/TR/webrtc#dom-rtcicecandidateinit
862#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
863pub struct IceCandidate {
864    /// [`candidate-attribute`][0] of this [`IceCandidate`].
865    ///
866    /// If this [`IceCandidate`] represents an end-of-candidates indication,
867    /// then it's an empty string.
868    ///
869    /// [0]: https://w3.org/TR/webrtc#dfn-candidate-attribute
870    pub candidate: String,
871
872    /// Index (starting at zero) of the media description in the SDP this
873    /// [`IceCandidate`] is associated with.
874    pub sdp_m_line_index: Option<u16>,
875
876    /// [Media stream "identification-tag"] for the media component this
877    /// [`IceCandidate`] is associated with.
878    ///
879    /// [0]: https://w3.org/TR/webrtc#dfn-media-stream-identification-tag
880    pub sdp_mid: Option<String>,
881}
882
883/// Track with a [`Direction`].
884#[cfg_attr(feature = "client", derive(Deserialize))]
885#[cfg_attr(feature = "server", derive(Serialize))]
886#[derive(Clone, Debug, Eq, PartialEq)]
887pub struct Track {
888    /// ID of this [`Track`].
889    pub id: TrackId,
890
891    /// [`Direction`] of this [`Track`].
892    pub direction: Direction,
893
894    /// [`MediaDirection`] of this [`Track`].
895    pub media_direction: MediaDirection,
896
897    /// [`Track`]'s mute state.
898    pub muted: bool,
899
900    /// [`MediaType`] of this [`Track`].
901    pub media_type: MediaType,
902}
903
904impl Track {
905    /// Indicates whether this [`Track`] is required to call starting.
906    #[must_use]
907    pub const fn required(&self) -> bool {
908        self.media_type.required()
909    }
910}
911
912/// Patch of a [`Track`] which Web Client can request with a
913/// [`Command::UpdateTracks`].
914#[cfg_attr(feature = "client", derive(Serialize))]
915#[cfg_attr(feature = "server", derive(Deserialize))]
916#[derive(Clone, Copy, Debug, Eq, PartialEq)]
917pub struct TrackPatchCommand {
918    /// ID of the [`Track`] this patch is intended for.
919    pub id: TrackId,
920
921    /// [`Track`]'s media exchange state.
922    pub enabled: Option<bool>,
923
924    /// [`Track`]'s mute state.
925    ///
926    /// Muting and unmuting can be performed without adding/removing tracks
927    /// from transceivers, hence renegotiation is not required.
928    pub muted: Option<bool>,
929}
930
931/// Patch of a [`Track`] which Media Server can send with an
932/// [`Event::PeerUpdated`].
933#[cfg_attr(feature = "client", derive(Deserialize))]
934#[cfg_attr(feature = "server", derive(Serialize))]
935#[derive(Clone, Debug, Eq, PartialEq)]
936pub struct TrackPatchEvent {
937    /// ID of the [`Track`] which should be patched.
938    pub id: TrackId,
939
940    /// General media exchange direction of the `Track`.
941    pub media_direction: Option<MediaDirection>,
942
943    /// IDs of the `Member`s who should receive this outgoing [`Track`].
944    ///
945    /// If [`Some`], then it means there are some changes in this outgoing
946    /// [`Track`]'s `receivers` (or we just want to sync this outgoing
947    /// [`Track`]'s `receivers`). It describes not changes, but the actual
948    /// [`Vec<MemberId>`] of this outgoing [`Track`], that have to be reached
949    /// once this [`TrackPatchEvent`] applied.
950    ///
951    /// If [`None`], then it means there is no need to check and recalculate
952    /// this outgoing [`Track`]'s `receivers`.
953    pub receivers: Option<Vec<MemberId>>,
954
955    /// [`Track`]'s mute state.
956    ///
957    /// Muting and unmuting can be performed without adding/removing tracks
958    /// from transceivers, hence renegotiation is not required.
959    pub muted: Option<bool>,
960
961    /// [`EncodingParameters`] for the [`Track`] which should be patched.
962    pub encoding_parameters: Option<Vec<EncodingParameters>>,
963}
964
965/// Media exchange direction of a `Track`.
966#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
967pub enum MediaDirection {
968    /// `Track` is enabled on both receiver and sender sides.
969    SendRecv = 0,
970
971    /// `Track` is enabled on sender side only.
972    SendOnly = 1,
973
974    /// `Track` is enabled on receiver side only.
975    RecvOnly = 2,
976
977    /// `Track` is disabled on both sides.
978    Inactive = 3,
979}
980
981impl MediaDirection {
982    /// Indicates whether a `Track` is enabled on sender side only.
983    #[must_use]
984    pub const fn is_send_enabled(self) -> bool {
985        matches!(self, Self::SendRecv | Self::SendOnly)
986    }
987
988    /// Indicates whether a `Track` is enabled on receiver side only.
989    #[must_use]
990    pub const fn is_recv_enabled(self) -> bool {
991        matches!(self, Self::SendRecv | Self::RecvOnly)
992    }
993
994    /// Indicates whether a `Track` is enabled on both sender and receiver
995    /// sides.
996    #[must_use]
997    pub const fn is_enabled_general(self) -> bool {
998        matches!(self, Self::SendRecv)
999    }
1000}
1001
1002impl From<TrackPatchCommand> for TrackPatchEvent {
1003    fn from(from: TrackPatchCommand) -> Self {
1004        Self {
1005            id: from.id,
1006            muted: from.muted,
1007            media_direction: from.enabled.map(|enabled| {
1008                if enabled {
1009                    MediaDirection::SendRecv
1010                } else {
1011                    MediaDirection::Inactive
1012                }
1013            }),
1014            receivers: None,
1015            encoding_parameters: None,
1016        }
1017    }
1018}
1019
1020impl TrackPatchEvent {
1021    /// Returns a new empty [`TrackPatchEvent`] with the provided [`TrackId`].
1022    #[must_use]
1023    pub const fn new(id: TrackId) -> Self {
1024        Self {
1025            id,
1026            muted: None,
1027            media_direction: None,
1028            receivers: None,
1029            encoding_parameters: None,
1030        }
1031    }
1032
1033    /// Merges this [`TrackPatchEvent`] with the provided one.
1034    ///
1035    /// Does nothing if [`TrackId`] of this [`TrackPatchEvent`] and the
1036    /// provided [`TrackPatchEvent`] are different.
1037    pub fn merge(&mut self, another: &Self) {
1038        if self.id != another.id {
1039            return;
1040        }
1041
1042        if let Some(muted) = another.muted {
1043            self.muted = Some(muted);
1044        }
1045
1046        if let Some(direction) = another.media_direction {
1047            self.media_direction = Some(direction);
1048        }
1049
1050        if let Some(receivers) = &another.receivers {
1051            self.receivers = Some(receivers.clone());
1052        }
1053
1054        if let Some(encodings) = &another.encoding_parameters {
1055            self.encoding_parameters = Some(encodings.clone());
1056        }
1057    }
1058}
1059
1060/// Representation of [RTCIceServer][1] (item of `iceServers` field
1061/// from [RTCConfiguration][2]).
1062///
1063/// [1]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
1064/// [2]: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration
1065#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1066pub struct IceServer {
1067    /// URLs of this [`IceServer`].
1068    pub urls: Vec<String>,
1069
1070    /// Optional username to authenticate on this [`IceServer`] with.
1071    #[serde(skip_serializing_if = "Option::is_none")]
1072    pub username: Option<String>,
1073
1074    /// Optional secret to authenticate on this [`IceServer`] with.
1075    #[serde(skip_serializing_if = "Option::is_none")]
1076    pub credential: Option<IcePassword>,
1077}
1078
1079/// Possible directions of a [`Track`].
1080#[cfg_attr(feature = "client", derive(Deserialize))]
1081#[cfg_attr(feature = "server", derive(Serialize))]
1082#[derive(Clone, Debug, Eq, PartialEq)]
1083// TODO: Use different struct without mids in PeerUpdated event.
1084pub enum Direction {
1085    /// Outgoing direction.
1086    Send {
1087        /// IDs of the `Member`s who should receive this outgoing [`Track`].
1088        receivers: Vec<MemberId>,
1089
1090        /// [Media stream "identification-tag"] of this outgoing [`Track`].
1091        ///
1092        /// [0]: https://w3.org/TR/webrtc#dfn-media-stream-identification-tag
1093        mid: Option<String>,
1094    },
1095
1096    /// Incoming direction.
1097    Recv {
1098        /// IDs of the `Member` this incoming [`Track`] is received from.
1099        sender: MemberId,
1100
1101        /// [Media stream "identification-tag"] of this incoming [`Track`].
1102        ///
1103        /// [0]: https://w3.org/TR/webrtc#dfn-media-stream-identification-tag
1104        mid: Option<String>,
1105    },
1106}
1107
1108/// Possible media types of a [`Track`].
1109#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1110pub enum MediaType {
1111    /// Audio [`Track`].
1112    Audio(AudioSettings),
1113
1114    /// Video [`Track`].
1115    Video(VideoSettings),
1116}
1117
1118impl MediaType {
1119    /// Indicates whether this [`MediaType`] is required to call starting.
1120    #[must_use]
1121    pub const fn required(&self) -> bool {
1122        match self {
1123            Self::Audio(audio) => audio.required,
1124            Self::Video(video) => video.required,
1125        }
1126    }
1127}
1128
1129/// Settings of an audio [`Track`].
1130#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1131pub struct AudioSettings {
1132    /// Importance of the audio.
1133    ///
1134    /// If `false` then audio may be not published.
1135    pub required: bool,
1136}
1137
1138/// Settings of a video [`Track`].
1139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1140pub struct VideoSettings {
1141    /// Importance of the video.
1142    ///
1143    /// If `false` then video may be not published.
1144    pub required: bool,
1145
1146    /// Source kind of these [`VideoSettings`].
1147    pub source_kind: MediaSourceKind,
1148
1149    /// [`EncodingParameters`] of these [`VideoSettings`].
1150    pub encoding_parameters: Vec<EncodingParameters>,
1151
1152    /// [`SvcSettings`] of these [`VideoSettings`].
1153    pub svc_settings: Vec<SvcSettings>,
1154}
1155
1156/// Possible media sources of a video [`Track`].
1157#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1158pub enum MediaSourceKind {
1159    /// Media is sourced by some media device (webcam or microphone).
1160    Device,
1161
1162    /// Media is obtained with screen-capture.
1163    Display,
1164}
1165
1166/// Supported [codecs][0].
1167///
1168/// [0]: https://webrtcglossary.com/codec
1169#[derive(
1170    Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq, Serialize,
1171)]
1172pub enum Codec {
1173    /// [VP8] codec.
1174    ///
1175    /// [VP8]: https://en.wikipedia.org/wiki/VP8
1176    #[display("VP8")]
1177    VP8,
1178
1179    /// [VP9] codec.
1180    ///
1181    /// [VP9]: https://en.wikipedia.org/wiki/VP9
1182    #[display("VP9")]
1183    VP9,
1184
1185    /// [AV1] codec.
1186    ///
1187    /// [AV1]: https://en.wikipedia.org/wiki/AV1
1188    #[display("AV1")]
1189    AV1,
1190}
1191
1192impl Codec {
1193    /// Returns [MIME "type/subtype"] string of this [`Codec`].
1194    ///
1195    /// [MIME "type/subtype"]: https://en.wikipedia.org/wiki/Media_type
1196    #[must_use]
1197    pub const fn mime_type(&self) -> &'static str {
1198        match self {
1199            Self::VP8 => "video/VP8",
1200            Self::VP9 => "video/VP9",
1201            Self::AV1 => "video/AV1",
1202        }
1203    }
1204}
1205
1206/// [Scalability mode] preference for [SVC (Scalable Video Coding)][SVC].
1207///
1208/// In [SVC], the scalability is typically defined in terms of layers (L) and
1209/// temporal (T) and spatial (S) levels.
1210///
1211/// The "L" part refers to the number of layers used in the encoding. Each layer
1212/// contains different information about the video, with higher layers typically
1213/// containing more detail or higher quality representations of the video.
1214///
1215/// The "T" part refers to temporal scalability layers count. Temporal
1216/// scalability allows for different frame rates to be encoded within the same
1217/// video stream, which can be useful for adaptive streaming or supporting
1218/// devices with varying display capabilities.
1219///
1220/// [SVC]: https://webrtcglossary.com/svc
1221/// [0]: https://w3.org/TR/webrtc-svc#scalabilitymodes*
1222#[derive(
1223    Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq, Serialize,
1224)]
1225pub enum ScalabilityMode {
1226    /// [L1T1] mode.
1227    ///
1228    /// [L1T1]: https://w3.org/TR/webrtc-svc#L1T1*
1229    #[display("L1T1")]
1230    L1T1,
1231
1232    /// [L1T2] mode.
1233    ///
1234    /// [L1T2]: https://w3.org/TR/webrtc-svc#L1T2*
1235    #[display("L1T2")]
1236    L1T2,
1237
1238    /// [L1T3] mode.
1239    ///
1240    /// [L1T3]: https://w3.org/TR/webrtc-svc#L1T3*
1241    #[display("L1T3")]
1242    L1T3,
1243
1244    /// [L2T1] mode.
1245    ///
1246    /// [L2T1]: https://w3.org/TR/webrtc-svc#L2T1*
1247    #[display("L2T1")]
1248    L2T1,
1249
1250    /// [L2T2] mode.
1251    ///
1252    /// [L2T2]: https://w3.org/TR/webrtc-svc#L2T2*
1253    #[display("L2T2")]
1254    L2T2,
1255
1256    /// [L2T3] mode.
1257    ///
1258    /// [L2T3]: https://w3.org/TR/webrtc-svc#L2T3*
1259    #[display("L2T3")]
1260    L2T3,
1261
1262    /// [L3T1] mode.
1263    ///
1264    /// [L3T1]: https://w3.org/TR/webrtc-svc#L3T1*
1265    #[display("L3T1")]
1266    L3T1,
1267
1268    /// [L3T2] mode.
1269    ///
1270    /// [L3T2]: https://w3.org/TR/webrtc-svc#L3T2*
1271    #[display("L3T2")]
1272    L3T2,
1273
1274    /// [L3T3] mode.
1275    ///
1276    /// [L3T3]: https://w3.org/TR/webrtc-svc#L3T3*
1277    #[display("L3T3")]
1278    L3T3,
1279
1280    /// [S2T1] mode.
1281    ///
1282    /// [S2T1]: https://w3.org/TR/webrtc-svc#S2T1*
1283    #[display("S2T1")]
1284    S2T1,
1285
1286    /// [S2T2] mode.
1287    ///
1288    /// [S2T2]: https://w3.org/TR/webrtc-svc#S2T2*
1289    #[display("S2T2")]
1290    S2T2,
1291
1292    /// [S2T3] mode.
1293    ///
1294    /// [S2T3]: https://w3.org/TR/webrtc-svc#S2T3*
1295    #[display("S2T3")]
1296    S2T3,
1297
1298    /// [S3T1] mode.
1299    ///
1300    /// [S3T1]: https://w3.org/TR/webrtc-svc#S3T1*
1301    #[display("S3T1")]
1302    S3T1,
1303
1304    /// [S3T2] mode.
1305    ///
1306    /// [S3T2]: https://w3.org/TR/webrtc-svc#S3T2*
1307    #[display("S3T2")]
1308    S3T2,
1309
1310    /// [S3T3] mode.
1311    ///
1312    /// [S3T3]: https://w3.org/TR/webrtc-svc#S3T3*
1313    #[display("S3T3")]
1314    S3T3,
1315}
1316
1317/// Configuration settings for [SVC (Scalable Video Coding)][SVC].
1318///
1319/// [SVC]: https://webrtcglossary.com/svc
1320#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1321pub struct SvcSettings {
1322    /// [`Codec`] these [`SvcSettings`] are configured for.
1323    pub codec: Codec,
1324
1325    /// Preferred [`ScalabilityMode`].
1326    pub scalability_mode: ScalabilityMode,
1327}
1328
1329/// Representation of an [RTCRtpEncodingParameters][0].
1330///
1331/// [0]: https://w3.org/TR/webrtc#dom-rtcrtpencodingparameters
1332#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1333pub struct EncodingParameters {
1334    /// [RTP stream ID (RID)][RID] to be sent using the
1335    /// [RID header extension][0].
1336    ///
1337    /// [RID]: https://webrtcglossary.com/rid
1338    /// [0]: https://tools.ietf.org/html/rfc8852#section-3.3
1339    pub rid: String,
1340
1341    /// Indicator whether this encoding is actively being sent.
1342    ///
1343    /// Being `false` doesn't cause the [SSRC] to be removed, so an `RTCP BYE`
1344    /// is not sent.
1345    ///
1346    /// [SSRC]: https://webrtcglossary.com/ssrc
1347    pub active: bool,
1348
1349    /// Maximum bitrate that can be used to send this encoding.
1350    ///
1351    /// User agent is free to allocate bandwidth between the encodings, as long
1352    /// as this value is not exceeded.
1353    pub max_bitrate: Option<u32>,
1354
1355    /// Factor for scaling down video's resolution in each dimension before
1356    /// sending.
1357    ///
1358    /// Only present for video encodings.
1359    ///
1360    /// For example, if this value is `2`, a video will be scaled down by a
1361    /// factor of 2 in each dimension, resulting in sending a video of one
1362    /// quarter the size. If this value is `1`, the video won't be affected.
1363    ///
1364    /// Must be greater than or equal to `1`.
1365    pub scale_resolution_down_by: Option<u8>,
1366}
1367
1368/// Estimated connection quality.
1369#[cfg_attr(feature = "client", derive(Deserialize))]
1370#[cfg_attr(feature = "server", derive(Serialize))]
1371#[derive(Clone, Copy, Debug, Display, Eq, Ord, PartialEq, PartialOrd)]
1372pub enum ConnectionQualityScore {
1373    /// Nearly all users dissatisfied.
1374    Poor = 1,
1375
1376    /// Many users dissatisfied.
1377    Low = 2,
1378
1379    /// Some users dissatisfied.
1380    Medium = 3,
1381
1382    /// Satisfied.
1383    High = 4,
1384}