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}