use crate::statistics::stats::ice_candidate_pair::{
RTCIceCandidatePairStats, RTCStatsIceCandidatePairState,
};
use crate::statistics::stats::{RTCStats, RTCStatsType};
use std::time::Instant;
#[derive(Debug, Default)]
pub struct IceCandidatePairAccumulator {
pub transport_id: String,
pub local_candidate_id: String,
pub remote_candidate_id: String,
pub packets_sent: u64,
pub packets_received: u64,
pub bytes_sent: u64,
pub bytes_received: u64,
pub last_packet_sent_timestamp: Option<Instant>,
pub last_packet_received_timestamp: Option<Instant>,
pub total_round_trip_time: f64,
pub current_round_trip_time: f64,
pub requests_sent: u64,
pub requests_received: u64,
pub responses_sent: u64,
pub responses_received: u64,
pub consent_requests_sent: u64,
pub packets_discarded_on_send: u32,
pub bytes_discarded_on_send: u32,
pub available_outgoing_bitrate: f64,
pub available_incoming_bitrate: f64,
pub state: RTCStatsIceCandidatePairState,
pub nominated: bool,
}
impl IceCandidatePairAccumulator {
pub fn on_packet_sent(&mut self, bytes: usize, now: Instant) {
self.packets_sent += 1;
self.bytes_sent += bytes as u64;
self.last_packet_sent_timestamp = Some(now);
}
pub fn on_packet_received(&mut self, bytes: usize, now: Instant) {
self.packets_received += 1;
self.bytes_received += bytes as u64;
self.last_packet_received_timestamp = Some(now);
}
pub fn on_rtt_measured(&mut self, rtt_seconds: f64) {
self.current_round_trip_time = rtt_seconds;
self.total_round_trip_time += rtt_seconds;
}
pub fn on_stun_request_sent(&mut self) {
self.requests_sent += 1;
}
pub fn on_stun_request_received(&mut self) {
self.requests_received += 1;
}
pub fn on_stun_response_sent(&mut self) {
self.responses_sent += 1;
}
pub fn on_stun_response_received(&mut self) {
self.responses_received += 1;
}
pub fn on_consent_request_sent(&mut self) {
self.consent_requests_sent += 1;
}
pub fn on_packet_discarded(&mut self, bytes: usize) {
self.packets_discarded_on_send += 1;
self.bytes_discarded_on_send += bytes as u32;
}
pub fn snapshot(&self, now: Instant, id: &str) -> RTCIceCandidatePairStats {
RTCIceCandidatePairStats {
stats: RTCStats {
timestamp: now,
typ: RTCStatsType::CandidatePair,
id: id.to_string(),
},
transport_id: self.transport_id.clone(),
local_candidate_id: self.local_candidate_id.clone(),
remote_candidate_id: self.remote_candidate_id.clone(),
state: self.state,
nominated: self.nominated,
packets_sent: self.packets_sent,
packets_received: self.packets_received,
bytes_sent: self.bytes_sent,
bytes_received: self.bytes_received,
last_packet_sent_timestamp: self.last_packet_sent_timestamp.unwrap_or(now),
last_packet_received_timestamp: self.last_packet_received_timestamp.unwrap_or(now),
total_round_trip_time: self.total_round_trip_time,
current_round_trip_time: self.current_round_trip_time,
available_outgoing_bitrate: self.available_outgoing_bitrate,
available_incoming_bitrate: self.available_incoming_bitrate,
requests_received: self.requests_received,
requests_sent: self.requests_sent,
responses_received: self.responses_received,
responses_sent: self.responses_sent,
consent_requests_sent: self.consent_requests_sent,
packets_discarded_on_send: self.packets_discarded_on_send,
bytes_discarded_on_send: self.bytes_discarded_on_send,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default() {
let acc = IceCandidatePairAccumulator::default();
assert_eq!(acc.packets_sent, 0);
assert_eq!(acc.packets_received, 0);
assert_eq!(acc.bytes_sent, 0);
assert_eq!(acc.bytes_received, 0);
assert_eq!(acc.requests_sent, 0);
assert_eq!(acc.responses_received, 0);
assert_eq!(acc.total_round_trip_time, 0.0);
assert_eq!(acc.current_round_trip_time, 0.0);
assert!(!acc.nominated);
}
#[test]
fn test_on_packet_sent() {
let mut acc = IceCandidatePairAccumulator::default();
let now = Instant::now();
acc.on_packet_sent(100, now);
assert_eq!(acc.packets_sent, 1);
assert_eq!(acc.bytes_sent, 100);
assert_eq!(acc.last_packet_sent_timestamp, Some(now));
let later = now + std::time::Duration::from_millis(10);
acc.on_packet_sent(200, later);
assert_eq!(acc.packets_sent, 2);
assert_eq!(acc.bytes_sent, 300);
assert_eq!(acc.last_packet_sent_timestamp, Some(later));
}
#[test]
fn test_on_packet_received() {
let mut acc = IceCandidatePairAccumulator::default();
let now = Instant::now();
acc.on_packet_received(150, now);
assert_eq!(acc.packets_received, 1);
assert_eq!(acc.bytes_received, 150);
assert_eq!(acc.last_packet_received_timestamp, Some(now));
}
#[test]
fn test_on_rtt_measured() {
let mut acc = IceCandidatePairAccumulator::default();
acc.on_rtt_measured(0.050); assert_eq!(acc.current_round_trip_time, 0.050);
assert_eq!(acc.total_round_trip_time, 0.050);
acc.on_rtt_measured(0.030); assert_eq!(acc.current_round_trip_time, 0.030);
assert_eq!(acc.total_round_trip_time, 0.080);
acc.on_rtt_measured(0.040); assert_eq!(acc.current_round_trip_time, 0.040);
assert_eq!(acc.total_round_trip_time, 0.120);
}
#[test]
fn test_stun_transactions() {
let mut acc = IceCandidatePairAccumulator::default();
acc.on_stun_request_sent();
acc.on_stun_request_sent();
assert_eq!(acc.requests_sent, 2);
acc.on_stun_request_received();
assert_eq!(acc.requests_received, 1);
acc.on_stun_response_sent();
assert_eq!(acc.responses_sent, 1);
acc.on_stun_response_received();
acc.on_stun_response_received();
assert_eq!(acc.responses_received, 2);
}
#[test]
fn test_consent_requests() {
let mut acc = IceCandidatePairAccumulator::default();
acc.on_consent_request_sent();
acc.on_consent_request_sent();
acc.on_consent_request_sent();
assert_eq!(acc.consent_requests_sent, 3);
}
#[test]
fn test_on_packet_discarded() {
let mut acc = IceCandidatePairAccumulator::default();
acc.on_packet_discarded(100);
assert_eq!(acc.packets_discarded_on_send, 1);
assert_eq!(acc.bytes_discarded_on_send, 100);
acc.on_packet_discarded(200);
assert_eq!(acc.packets_discarded_on_send, 2);
assert_eq!(acc.bytes_discarded_on_send, 300);
}
#[test]
fn test_full_ice_connectivity_check_flow() {
let mut acc = IceCandidatePairAccumulator {
transport_id: "RTCTransport".to_string(),
local_candidate_id: "local_1".to_string(),
remote_candidate_id: "remote_1".to_string(),
..Default::default()
};
let now = Instant::now();
acc.on_stun_request_sent();
acc.on_stun_response_received();
acc.on_rtt_measured(0.025);
acc.state = RTCStatsIceCandidatePairState::Succeeded;
acc.nominated = true;
acc.on_packet_sent(1200, now);
acc.on_packet_received(1000, now);
assert_eq!(acc.requests_sent, 1);
assert_eq!(acc.responses_received, 1);
assert_eq!(acc.current_round_trip_time, 0.025);
assert!(acc.nominated);
assert_eq!(acc.state, RTCStatsIceCandidatePairState::Succeeded);
assert_eq!(acc.packets_sent, 1);
assert_eq!(acc.bytes_sent, 1200);
}
#[test]
fn test_snapshot() {
let now = Instant::now();
let mut acc = IceCandidatePairAccumulator {
transport_id: "RTCTransport_0".to_string(),
local_candidate_id: "local_abc".to_string(),
remote_candidate_id: "remote_xyz".to_string(),
state: RTCStatsIceCandidatePairState::Succeeded,
nominated: true,
..Default::default()
};
acc.on_packet_sent(500, now);
acc.on_packet_received(400, now);
acc.on_rtt_measured(0.020);
let stats = acc.snapshot(now, "RTCIceCandidatePair_abc_xyz");
assert_eq!(stats.stats.id, "RTCIceCandidatePair_abc_xyz");
assert_eq!(stats.stats.typ, RTCStatsType::CandidatePair);
assert_eq!(stats.local_candidate_id, "local_abc");
assert_eq!(stats.remote_candidate_id, "remote_xyz");
assert_eq!(stats.state, RTCStatsIceCandidatePairState::Succeeded);
assert!(stats.nominated);
assert_eq!(stats.packets_sent, 1);
assert_eq!(stats.bytes_sent, 500);
assert_eq!(stats.packets_received, 1);
assert_eq!(stats.bytes_received, 400);
assert_eq!(stats.current_round_trip_time, 0.020);
}
#[test]
fn test_snapshot_json_serialization() {
let now = Instant::now();
let mut acc = IceCandidatePairAccumulator {
state: RTCStatsIceCandidatePairState::Succeeded,
nominated: true,
..Default::default()
};
acc.on_packet_sent(1000, now);
acc.on_rtt_measured(0.050);
let stats = acc.snapshot(now, "pair_1");
let json = serde_json::to_string(&stats).expect("should serialize");
assert!(json.contains("\"packetsSent\":1"));
assert!(json.contains("\"bytesSent\":1000"));
assert!(json.contains("\"nominated\":true"));
assert!(json.contains("\"currentRoundTripTime\":0.05"));
assert!(json.contains("\"type\":\"candidate-pair\""));
}
}