#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(any(doc, test), doc = include_str!("../README.md"))]
#![cfg_attr(not(any(doc, test)), doc = env!("CARGO_PKG_NAME"))]
#![deny(nonstandard_style, rustdoc::all, trivial_casts, trivial_numeric_casts)]
#![forbid(non_ascii_idents, unsafe_code)]
#![warn(
clippy::absolute_paths,
clippy::allow_attributes,
clippy::allow_attributes_without_reason,
clippy::as_conversions,
clippy::as_pointer_underscore,
clippy::as_ptr_cast_mut,
clippy::assertions_on_result_states,
clippy::branches_sharing_code,
clippy::cfg_not_test,
clippy::clear_with_drain,
clippy::clone_on_ref_ptr,
clippy::coerce_container_to_any,
clippy::collection_is_never_read,
clippy::create_dir,
clippy::dbg_macro,
clippy::debug_assert_with_mut_call,
clippy::decimal_literal_representation,
clippy::default_union_representation,
clippy::derive_partial_eq_without_eq,
clippy::doc_include_without_cfg,
clippy::empty_drop,
clippy::empty_structs_with_brackets,
clippy::equatable_if_let,
clippy::empty_enum_variants_with_brackets,
clippy::exit,
clippy::expect_used,
clippy::fallible_impl_from,
clippy::filetype_is_file,
clippy::float_cmp_const,
clippy::fn_to_numeric_cast_any,
clippy::get_unwrap,
clippy::if_then_some_else_none,
clippy::imprecise_flops,
clippy::infinite_loop,
clippy::iter_on_empty_collections,
clippy::iter_on_single_items,
clippy::iter_over_hash_type,
clippy::iter_with_drain,
clippy::large_include_file,
clippy::large_stack_frames,
clippy::let_underscore_untyped,
clippy::literal_string_with_formatting_args,
clippy::lossy_float_literal,
clippy::map_err_ignore,
clippy::map_with_unused_argument_over_ranges,
clippy::mem_forget,
clippy::missing_assert_message,
clippy::missing_asserts_for_indexing,
clippy::missing_const_for_fn,
clippy::missing_docs_in_private_items,
clippy::module_name_repetitions,
clippy::multiple_inherent_impl,
clippy::multiple_unsafe_ops_per_block,
clippy::mutex_atomic,
clippy::mutex_integer,
clippy::needless_collect,
clippy::needless_pass_by_ref_mut,
clippy::needless_raw_strings,
clippy::non_zero_suggestions,
clippy::nonstandard_macro_braces,
clippy::option_if_let_else,
clippy::or_fun_call,
clippy::panic_in_result_fn,
clippy::partial_pub_fields,
clippy::pathbuf_init_then_push,
clippy::pedantic,
clippy::precedence_bits,
clippy::print_stderr,
clippy::print_stdout,
clippy::pub_without_shorthand,
clippy::rc_buffer,
clippy::rc_mutex,
clippy::read_zero_byte_vec,
clippy::redundant_clone,
clippy::redundant_test_prefix,
clippy::redundant_type_annotations,
clippy::renamed_function_params,
clippy::ref_patterns,
clippy::rest_pat_in_fully_bound_structs,
clippy::return_and_then,
clippy::same_name_method,
clippy::semicolon_inside_block,
clippy::set_contains_or_insert,
clippy::shadow_unrelated,
clippy::significant_drop_in_scrutinee,
clippy::significant_drop_tightening,
clippy::single_option_map,
clippy::str_to_string,
clippy::string_add,
clippy::string_lit_as_bytes,
clippy::string_lit_chars_any,
clippy::string_slice,
clippy::suboptimal_flops,
clippy::suspicious_operation_groupings,
clippy::suspicious_xor_used_as_pow,
clippy::tests_outside_test_module,
clippy::todo,
clippy::too_long_first_doc_paragraph,
clippy::trailing_empty_array,
clippy::transmute_undefined_repr,
clippy::trivial_regex,
clippy::try_err,
clippy::undocumented_unsafe_blocks,
clippy::unimplemented,
clippy::uninhabited_references,
clippy::unnecessary_safety_comment,
clippy::unnecessary_safety_doc,
clippy::unnecessary_self_imports,
clippy::unnecessary_struct_initialization,
clippy::unused_peekable,
clippy::unused_result_ok,
clippy::unused_trait_names,
clippy::unwrap_in_result,
clippy::unwrap_used,
clippy::use_debug,
clippy::use_self,
clippy::useless_let_if_seq,
clippy::verbose_file_reads,
clippy::volatile_composites,
clippy::while_float,
clippy::wildcard_enum_match_arm,
ambiguous_negative_literals,
closure_returning_async_block,
future_incompatible,
impl_trait_redundant_captures,
let_underscore_drop,
macro_use_extern_crate,
meta_variable_misuse,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
redundant_lifetimes,
rust_2018_idioms,
single_use_lifetimes,
unit_bindings,
unnameable_types,
unreachable_pub,
unstable_features,
unused,
variant_size_differences
)]
pub mod state;
pub mod stats;
use std::{
collections::HashMap,
hash::{Hash, Hasher},
};
use derive_more::with_trait::{Constructor, Display, From, Into};
use medea_macro::dispatchable;
use rand::{Rng as _, distr::Alphanumeric};
use secrecy::{ExposeSecret as _, SecretString};
use serde::{Deserialize, Serialize, Serializer};
use self::stats::RtcStat;
#[derive(
Clone, Debug, Display, Serialize, Deserialize, Eq, From, Hash, PartialEq,
)]
#[from(forward)]
pub struct RoomId(pub String);
#[derive(
Clone, Debug, Display, Serialize, Deserialize, Eq, From, Hash, PartialEq,
)]
#[from(forward)]
pub struct MemberId(pub String);
#[cfg_attr(feature = "server", derive(Default))]
#[derive(
Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize,
)]
pub struct PeerId(pub u32);
#[cfg_attr(feature = "server", derive(Default))]
#[derive(
Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize,
)]
pub struct TrackId(pub u32);
#[derive(Clone, Debug, Deserialize, From, Into)]
pub struct IcePassword(SecretString);
impl IcePassword {
const RANDOM_LENGTH: usize = 16;
#[must_use]
pub fn expose_str(&self) -> &str {
self.0.expose_secret()
}
#[must_use]
pub fn random() -> Self {
Self(
rand::rng()
.sample_iter(&Alphanumeric)
.take(Self::RANDOM_LENGTH)
.map(char::from)
.collect::<String>()
.into(),
)
}
}
impl Serialize for IcePassword {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.expose_secret().serialize(serializer)
}
}
impl Eq for IcePassword {}
impl PartialEq for IcePassword {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq as _;
self.expose_str().as_bytes().ct_eq(other.expose_str().as_bytes()).into()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Credential(SecretString);
impl Serialize for Credential {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.expose_secret().serialize(serializer)
}
}
impl Credential {
#[must_use]
pub fn expose_str(&self) -> &str {
self.0.expose_secret()
}
}
impl<T> From<T> for Credential
where
T: Into<String>,
{
fn from(value: T) -> Self {
Self(value.into().into())
}
}
impl Hash for Credential {
fn hash<H: Hasher>(&self, state: &mut H) {
self.expose_str().hash(state);
}
}
impl Eq for Credential {}
impl PartialEq for Credential {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq as _;
self.expose_str().as_bytes().ct_eq(other.expose_str().as_bytes()).into()
}
}
#[cfg(feature = "server")]
pub trait Incrementable {
#[must_use]
fn incr(&self) -> Self;
}
#[cfg(feature = "server")]
macro_rules! impl_incrementable {
($name:ty) => {
impl Incrementable for $name {
fn incr(&self) -> Self {
Self(self.0 + 1)
}
}
};
}
#[cfg(feature = "server")]
impl_incrementable!(PeerId);
#[cfg(feature = "server")]
impl_incrementable!(TrackId);
#[cfg_attr(
any(
target_family = "wasm",
all(target_arch = "arm", target_os = "android")
),
expect(variant_size_differences, reason = "`Event` is the most common")
)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[serde(tag = "msg", content = "data")]
pub enum ServerMsg {
Ping(u32),
Event {
room_id: RoomId,
event: Event,
},
RpcSettings(RpcSettings),
}
#[cfg_attr(
any(
target_family = "wasm",
all(target_arch = "arm", target_os = "android")
),
expect(variant_size_differences, reason = "`Command` is the most common")
)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
pub enum ClientMsg {
Pong(u32),
Command {
room_id: RoomId,
command: Command,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RpcSettings {
pub idle_timeout_ms: u32,
pub ping_interval_ms: u32,
}
#[dispatchable]
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Debug, PartialEq)]
#[serde(tag = "command", content = "data")]
pub enum Command {
JoinRoom {
member_id: MemberId,
credential: Credential,
capabilities: Capabilities,
},
LeaveRoom {
member_id: MemberId,
},
MakeSdpOffer {
peer_id: PeerId,
sdp_offer: String,
mids: HashMap<TrackId, String>,
transceivers_statuses: HashMap<TrackId, bool>,
},
MakeSdpAnswer {
peer_id: PeerId,
sdp_answer: String,
transceivers_statuses: HashMap<TrackId, bool>,
},
SetIceCandidate {
peer_id: PeerId,
candidate: IceCandidate,
},
AddPeerConnectionMetrics {
peer_id: PeerId,
metrics: PeerMetrics,
},
UpdateTracks {
peer_id: PeerId,
tracks_patches: Vec<TrackPatchCommand>,
},
SynchronizeMe {
state: state::Room,
},
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub enum PeerMetrics {
IceConnectionState(IceConnectionState),
PeerConnectionState(PeerConnectionState),
PeerConnectionError(PeerConnectionError),
RtcStats(Vec<RtcStat>),
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PeerConnectionError {
IceCandidate(IceCandidateError),
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IceCandidateError {
pub address: Option<String>,
pub port: Option<u32>,
pub url: String,
pub error_code: i32,
pub error_text: String,
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IceConnectionState {
New,
Checking,
Connected,
Completed,
Failed,
Disconnected,
Closed,
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PeerConnectionState {
New,
Connecting,
Connected,
Disconnected,
Failed,
Closed,
}
impl From<IceConnectionState> for PeerConnectionState {
fn from(ice_con_state: IceConnectionState) -> Self {
use IceConnectionState as Ice;
match ice_con_state {
Ice::New => Self::New,
Ice::Checking => Self::Connecting,
Ice::Connected | Ice::Completed => Self::Connected,
Ice::Failed => Self::Failed,
Ice::Disconnected => Self::Disconnected,
Ice::Closed => Self::Closed,
}
}
}
#[derive(
Copy, Clone, Debug, Deserialize, Display, Eq, PartialEq, Serialize,
)]
pub enum CloseReason {
Finished,
Reconnected,
Idle,
Rejected,
InternalError,
Evicted,
}
#[derive(
Clone, Constructor, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
)]
pub struct CloseDescription {
pub reason: CloseReason,
}
#[dispatchable(self: &Self, async_trait(?Send))]
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
#[serde(tag = "event", content = "data")]
pub enum Event {
RoomJoined {
member_id: MemberId,
is_reconnect: bool,
},
RoomLeft {
close_reason: CloseReason,
},
PeerCreated {
peer_id: PeerId,
negotiation_role: NegotiationRole,
connection_mode: ConnectionMode,
tracks: Vec<Track>,
ice_servers: Vec<IceServer>,
force_relay: bool,
stats_scrape_interval_ms: u32,
},
SdpAnswerMade {
peer_id: PeerId,
sdp_answer: String,
},
LocalDescriptionApplied {
peer_id: PeerId,
sdp_offer: String,
},
IceCandidateDiscovered {
peer_id: PeerId,
candidate: IceCandidate,
},
PeersRemoved {
peer_ids: Vec<PeerId>,
},
PeerUpdated {
peer_id: PeerId,
updates: Vec<PeerUpdate>,
negotiation_role: Option<NegotiationRole>,
},
ConnectionQualityUpdated {
partner_member_id: MemberId,
quality_score: ConnectionQualityScore,
},
StateSynchronized {
state: state::Room,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum NegotiationRole {
Offerer,
Answerer(String),
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum ConnectionMode {
Mesh,
Sfu,
}
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PeerUpdate {
Added(Track),
Removed(TrackId),
Updated(TrackPatchEvent),
IceRestart,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct IceCandidate {
pub candidate: String,
pub sdp_m_line_index: Option<u16>,
pub sdp_mid: Option<String>,
}
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Track {
pub id: TrackId,
pub direction: Direction,
pub media_direction: MediaDirection,
pub muted: bool,
pub media_type: MediaType,
}
impl Track {
#[must_use]
pub const fn required(&self) -> bool {
self.media_type.required()
}
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TrackPatchCommand {
pub id: TrackId,
pub enabled: Option<bool>,
pub muted: Option<bool>,
}
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackPatchEvent {
pub id: TrackId,
pub media_direction: Option<MediaDirection>,
pub receivers: Option<Vec<MemberId>>,
pub muted: Option<bool>,
pub encoding_parameters: Option<Vec<EncodingParameters>>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum MediaDirection {
SendRecv = 0,
SendOnly = 1,
RecvOnly = 2,
Inactive = 3,
}
impl MediaDirection {
#[must_use]
pub const fn is_send_enabled(self) -> bool {
matches!(self, Self::SendRecv | Self::SendOnly)
}
#[must_use]
pub const fn is_recv_enabled(self) -> bool {
matches!(self, Self::SendRecv | Self::RecvOnly)
}
#[must_use]
pub const fn is_enabled_general(self) -> bool {
matches!(self, Self::SendRecv)
}
}
impl From<TrackPatchCommand> for TrackPatchEvent {
fn from(from: TrackPatchCommand) -> Self {
Self {
id: from.id,
muted: from.muted,
media_direction: from.enabled.map(|enabled| {
if enabled {
MediaDirection::SendRecv
} else {
MediaDirection::Inactive
}
}),
receivers: None,
encoding_parameters: None,
}
}
}
impl TrackPatchEvent {
#[must_use]
pub const fn new(id: TrackId) -> Self {
Self {
id,
muted: None,
media_direction: None,
receivers: None,
encoding_parameters: None,
}
}
pub fn merge(&mut self, another: &Self) {
if self.id != another.id {
return;
}
if let Some(muted) = another.muted {
self.muted = Some(muted);
}
if let Some(direction) = another.media_direction {
self.media_direction = Some(direction);
}
if let Some(receivers) = &another.receivers {
self.receivers = Some(receivers.clone());
}
if let Some(encodings) = &another.encoding_parameters {
self.encoding_parameters = Some(encodings.clone());
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IceServer {
pub urls: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credential: Option<IcePassword>,
}
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Direction {
Send {
receivers: Vec<MemberId>,
mid: Option<String>,
},
Recv {
sender: MemberId,
mid: Option<String>,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum MediaType {
Audio(AudioSettings),
Video(VideoSettings),
}
impl MediaType {
#[must_use]
pub const fn required(&self) -> bool {
match self {
Self::Audio(audio) => audio.required,
Self::Video(video) => video.required,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AudioSettings {
pub required: bool,
pub source_kind: MediaSourceKind,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VideoSettings {
pub required: bool,
pub source_kind: MediaSourceKind,
pub encoding_parameters: Vec<EncodingParameters>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum MediaSourceKind {
Device,
Display,
}
#[derive(
Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize,
)]
pub enum ScalabilityMode {
#[display("L1T1")]
L1T1,
#[display("L1T2")]
L1T2,
#[display("L1T3")]
L1T3,
#[display("L2T1")]
L2T1,
#[display("L2T2")]
L2T2,
#[display("L2T3")]
L2T3,
#[display("L3T1")]
L3T1,
#[display("L3T2")]
L3T2,
#[display("L3T3")]
L3T3,
#[display("S2T1")]
S2T1,
#[display("S2T2")]
S2T2,
#[display("S2T3")]
S2T3,
#[display("S3T1")]
S3T1,
#[display("S3T2")]
S3T2,
#[display("S3T3")]
S3T3,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct EncodingParameters {
pub rid: String,
pub active: bool,
pub codec: Option<Codec>,
pub max_bitrate: Option<u32>,
pub scale_resolution_down_by: Option<u8>,
pub scalability_mode: Option<ScalabilityMode>,
}
#[cfg_attr(feature = "client", derive(Serialize))]
#[cfg_attr(feature = "server", derive(Deserialize))]
#[derive(Clone, Debug, Eq, Default, PartialEq)]
pub struct Capabilities {
pub audio_tx: Vec<Codec>,
pub audio_rx: Vec<Codec>,
pub video_tx: Vec<Codec>,
pub video_rx: Vec<Codec>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Codec {
pub mime_type: String,
pub clock_rate: u32,
pub channels: Option<u16>,
pub parameters: HashMap<String, String>,
}
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "server", derive(Serialize))]
#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConnectionQualityScore {
Poor = 1,
Low = 2,
Medium = 3,
High = 4,
}