use crate::identity::PeerIdentity;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CallId(pub Uuid);
impl CallId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for CallId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for CallId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaConstraints {
pub audio: bool,
pub video: bool,
pub screen_share: bool,
}
impl MediaConstraints {
pub fn audio_only() -> Self {
Self {
audio: true,
video: false,
screen_share: false,
}
}
pub fn video_call() -> Self {
Self {
audio: true,
video: true,
screen_share: false,
}
}
pub fn screen_share() -> Self {
Self {
audio: true,
video: false,
screen_share: true,
}
}
pub fn has_audio(&self) -> bool {
self.audio
}
pub fn has_video(&self) -> bool {
self.video
}
pub fn has_screen_share(&self) -> bool {
self.screen_share
}
pub fn to_media_types(&self) -> Vec<MediaType> {
let mut types = Vec::new();
if self.audio {
types.push(MediaType::Audio);
}
if self.video {
types.push(MediaType::Video);
}
if self.screen_share {
types.push(MediaType::ScreenShare);
}
types
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MediaType {
Audio,
Video,
ScreenShare,
DataChannel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "I: PeerIdentity")]
pub struct CallOffer<I: PeerIdentity> {
pub call_id: CallId,
pub caller: I,
pub callee: I,
pub sdp: String,
pub media_types: Vec<MediaType>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallAnswer {
pub call_id: CallId,
pub sdp: String,
pub accepted: bool,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IceCandidate {
pub call_id: CallId,
pub candidate: String,
pub sdp_mid: Option<String>,
pub sdp_mline_index: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CallState {
Idle,
Calling,
Connecting,
Connected,
Ending,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallQualityMetrics {
pub rtt_ms: u32,
pub packet_loss_percent: f32,
pub jitter_ms: u32,
pub bandwidth_kbps: u32,
pub timestamp: DateTime<Utc>,
}
impl CallQualityMetrics {
pub fn is_good_quality(&self) -> bool {
self.rtt_ms < 100
&& self.packet_loss_percent < 1.0
&& self.jitter_ms < 20
&& self.bandwidth_kbps > 500
}
pub fn needs_adaptation(&self) -> bool {
self.rtt_ms > 200
|| self.packet_loss_percent > 3.0
|| self.jitter_ms > 40
|| self.bandwidth_kbps < 300
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "I: PeerIdentity")]
pub struct MultiPartyCall<I: PeerIdentity> {
pub call_id: CallId,
pub participants: Vec<I>,
pub architecture: CallArchitecture,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CallArchitecture {
Mesh,
SFU,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "I: PeerIdentity")]
pub struct RecordingConsent<I: PeerIdentity> {
pub call_id: CallId,
pub requester: I,
pub participants: Vec<I>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConsentStatus {
Pending,
Granted,
Denied,
Revoked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptationSettings {
pub video_bitrate_kbps: u32,
pub video_resolution: VideoResolution,
pub video_fps: u32,
pub audio_bitrate_kbps: u32,
pub enable_dtx: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VideoResolution {
QVGA240,
SD480,
HD720,
HD1080,
}
impl VideoResolution {
pub fn width(&self) -> u32 {
match self {
Self::QVGA240 => 320,
Self::SD480 => 640,
Self::HD720 => 1280,
Self::HD1080 => 1920,
}
}
pub fn height(&self) -> u32 {
match self {
Self::QVGA240 => 240,
Self::SD480 => 480,
Self::HD720 => 720,
Self::HD1080 => 1080,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NativeQuicConfiguration {
pub dht_discovery: bool,
pub hole_punching: bool,
}
impl Default for NativeQuicConfiguration {
fn default() -> Self {
Self {
dht_discovery: true,
hole_punching: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "I: PeerIdentity")]
pub enum SignalingMessage<I: PeerIdentity> {
Offer(CallOffer<I>),
Answer(CallAnswer),
CallEnd {
call_id: CallId,
},
CallReject {
call_id: CallId,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "I: PeerIdentity")]
pub enum CallEvent<I: PeerIdentity> {
IncomingCall {
offer: CallOffer<I>,
},
CallInitiated {
call_id: CallId,
callee: I,
constraints: MediaConstraints,
},
CallAccepted {
call_id: CallId,
answer: CallAnswer,
},
CallRejected {
call_id: CallId,
},
CallEnded {
call_id: CallId,
},
ConnectionEstablished {
call_id: CallId,
},
ConnectionFailed {
call_id: CallId,
error: String,
},
QualityChanged {
call_id: CallId,
metrics: CallQualityMetrics,
},
}
#[derive(Debug, Clone)]
pub struct CallSession<I: PeerIdentity> {
pub call_id: CallId,
pub participants: Vec<I>,
pub state: CallState,
pub media_constraints: MediaConstraints,
pub created_at: DateTime<Utc>,
pub start_time: Option<DateTime<Utc>>,
pub end_time: Option<DateTime<Utc>>,
pub quality_metrics: Vec<CallQualityMetrics>,
}
impl<I: PeerIdentity> CallSession<I> {
pub fn new(call_id: CallId, constraints: MediaConstraints) -> Self {
Self {
call_id,
participants: Vec::new(),
state: CallState::Idle,
media_constraints: constraints,
created_at: Utc::now(),
start_time: None,
end_time: None,
quality_metrics: Vec::new(),
}
}
pub fn duration(&self) -> Option<chrono::Duration> {
if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
Some(end - start)
} else {
self.start_time.map(|start| Utc::now() - start)
}
}
pub fn add_participant(&mut self, participant: I) {
if !self
.participants
.iter()
.any(|p| p.to_string_repr() == participant.to_string_repr())
{
self.participants.push(participant);
}
}
pub fn remove_participant(&mut self, participant: &I) {
self.participants
.retain(|p| p.to_string_repr() != participant.to_string_repr());
}
pub fn add_quality_metric(&mut self, metric: CallQualityMetrics) {
self.quality_metrics.push(metric);
if self.quality_metrics.len() > 100 {
self.quality_metrics.remove(0);
}
}
pub fn latest_quality(&self) -> Option<&CallQualityMetrics> {
self.quality_metrics.last()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::PeerIdentityString;
#[test]
fn test_call_id() {
let id1 = CallId::new();
let id2 = CallId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_media_constraints() {
let audio = MediaConstraints::audio_only();
assert!(audio.has_audio());
assert!(!audio.has_video());
assert!(!audio.has_screen_share());
let video = MediaConstraints::video_call();
assert!(video.has_audio());
assert!(video.has_video());
assert!(!video.has_screen_share());
let screen = MediaConstraints::screen_share();
assert!(screen.has_audio());
assert!(!screen.has_video());
assert!(screen.has_screen_share());
}
#[test]
fn test_call_session() {
let call_id = CallId::new();
let constraints = MediaConstraints::video_call();
let mut session: CallSession<PeerIdentityString> = CallSession::new(call_id, constraints);
assert_eq!(session.call_id, call_id);
assert_eq!(session.state, CallState::Idle);
assert_eq!(session.participants.len(), 0);
let peer = PeerIdentityString::new("alice");
session.add_participant(peer.clone());
assert_eq!(session.participants.len(), 1);
session.add_participant(peer.clone()); assert_eq!(session.participants.len(), 1);
}
#[test]
fn test_quality_metrics() {
let good = CallQualityMetrics {
rtt_ms: 50,
packet_loss_percent: 0.5,
jitter_ms: 10,
bandwidth_kbps: 1000,
timestamp: Utc::now(),
};
assert!(good.is_good_quality());
assert!(!good.needs_adaptation());
let bad = CallQualityMetrics {
rtt_ms: 300,
packet_loss_percent: 5.0,
jitter_ms: 50,
bandwidth_kbps: 200,
timestamp: Utc::now(),
};
assert!(!bad.is_good_quality());
assert!(bad.needs_adaptation());
}
#[test]
fn test_video_resolution() {
let hd720 = VideoResolution::HD720;
assert_eq!(hd720.width(), 1280);
assert_eq!(hd720.height(), 720);
let hd1080 = VideoResolution::HD1080;
assert_eq!(hd1080.width(), 1920);
assert_eq!(hd1080.height(), 1080);
}
}