mod certificate;
mod codec;
mod data_channel;
mod ice_candidate;
mod ice_candidate_pair;
mod media;
mod peer_connection;
mod rtp_stream;
mod transport;
pub(crate) use certificate::CertificateStatsAccumulator;
pub(crate) use codec::{CodecDirection, CodecStatsAccumulator};
pub(crate) use data_channel::DataChannelStatsAccumulator;
pub(crate) use ice_candidate::IceCandidateAccumulator;
pub(crate) use ice_candidate_pair::IceCandidatePairAccumulator;
pub(crate) use media::app_provided::*;
pub(crate) use media::audio_playout::AudioPlayoutStatsAccumulator;
pub(crate) use media::media_source::MediaSourceStatsAccumulator;
pub(crate) use peer_connection::PeerConnectionStatsAccumulator;
pub(crate) use rtp_stream::inbound::InboundRtpStreamAccumulator;
pub(crate) use rtp_stream::outbound::OutboundRtpStreamAccumulator;
pub(crate) use transport::TransportStatsAccumulator;
use crate::data_channel::RTCDataChannelId;
use crate::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind};
use crate::rtp_transceiver::{PayloadType, RTCRtpTransceiverId, SSRC};
use crate::statistics::StatsSelector;
use crate::statistics::report::{RTCStatsReport, RTCStatsReportEntry};
use ice::CandidatePairStats;
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Default)]
pub(crate) struct RTCStatsAccumulator {
pub(crate) peer_connection: PeerConnectionStatsAccumulator,
pub(crate) transport: TransportStatsAccumulator,
pub(crate) ice_candidate_pairs: HashMap<String, IceCandidatePairAccumulator>,
pub(crate) local_candidates: HashMap<String, IceCandidateAccumulator>,
pub(crate) remote_candidates: HashMap<String, IceCandidateAccumulator>,
pub(crate) certificates: HashMap<String, CertificateStatsAccumulator>,
pub(crate) codecs: HashMap<String, CodecStatsAccumulator>,
pub(crate) data_channels: HashMap<RTCDataChannelId, DataChannelStatsAccumulator>,
pub(crate) inbound_rtp_streams: HashMap<SSRC, InboundRtpStreamAccumulator>,
pub(crate) outbound_rtp_streams: HashMap<SSRC, OutboundRtpStreamAccumulator>,
pub(crate) media_sources: HashMap<String, MediaSourceStatsAccumulator>,
pub(crate) audio_playouts: HashMap<String, AudioPlayoutStatsAccumulator>,
rtx_ssrc_to_primary: HashMap<SSRC, SSRC>,
fec_ssrc_to_primary: HashMap<SSRC, SSRC>,
}
#[cfg(test)]
pub(crate) fn assert_stamped_at(stamp: shared::time::SystemInstant, expected: Instant) {
assert_eq!(
stamp.instant(stamp.duration_since_unix_epoch()),
expected,
"stats must be stamped with the caller's instant"
);
}
impl RTCStatsAccumulator {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn snapshot(&self, now: Instant) -> RTCStatsReport {
let mut entries = Vec::new();
entries.push(RTCStatsReportEntry::PeerConnection(
self.peer_connection.snapshot(now),
));
entries.push(RTCStatsReportEntry::Transport(self.transport.snapshot(now)));
for (id, pair) in &self.ice_candidate_pairs {
entries.push(RTCStatsReportEntry::IceCandidatePair(
pair.snapshot(now, id),
));
}
for (id, candidate) in &self.local_candidates {
entries.push(RTCStatsReportEntry::LocalCandidate(
candidate.snapshot_local(now, id),
));
}
for (id, candidate) in &self.remote_candidates {
entries.push(RTCStatsReportEntry::RemoteCandidate(
candidate.snapshot_remote(now, id),
));
}
for (id, cert) in &self.certificates {
entries.push(RTCStatsReportEntry::Certificate(cert.snapshot(now, id)));
}
for (id, codec) in &self.codecs {
entries.push(RTCStatsReportEntry::Codec(codec.snapshot(now, id)));
}
for (id, channel) in &self.data_channels {
entries.push(RTCStatsReportEntry::DataChannel(
channel.snapshot(now, format!("RTCDataChannel_{}", id)),
));
}
for (ssrc, stream) in &self.inbound_rtp_streams {
let id = format!("RTCInboundRTPStream_{}_{}", stream.kind, ssrc);
entries.push(RTCStatsReportEntry::InboundRtp(stream.snapshot(now, &id)));
entries.push(RTCStatsReportEntry::RemoteOutboundRtp(
stream.snapshot_remote(now),
));
}
for (ssrc, stream) in &self.outbound_rtp_streams {
let id = format!("RTCOutboundRTPStream_{}_{}", stream.kind, ssrc);
entries.push(RTCStatsReportEntry::OutboundRtp(stream.snapshot(now, &id)));
entries.push(RTCStatsReportEntry::RemoteInboundRtp(
stream.snapshot_remote(now),
));
}
for (id, source) in &self.media_sources {
match source.kind {
RtpCodecKind::Audio => {
entries.push(RTCStatsReportEntry::AudioSource(
source.snapshot_audio(now, id),
));
}
RtpCodecKind::Video => {
entries.push(RTCStatsReportEntry::VideoSource(
source.snapshot_video(now, id),
));
}
_ => {}
}
}
for (id, playout) in &self.audio_playouts {
entries.push(RTCStatsReportEntry::AudioPlayout(playout.snapshot(now, id)));
}
RTCStatsReport::new(entries)
}
pub(crate) fn snapshot_with_selector(
&self,
now: Instant,
selector: StatsSelector,
) -> RTCStatsReport {
match selector {
StatsSelector::None => self.snapshot(now),
StatsSelector::Sender(sender_id) => self.snapshot_for_sender(now, sender_id.0),
StatsSelector::Receiver(receiver_id) => self.snapshot_for_receiver(now, receiver_id.0),
}
}
fn snapshot_for_sender(
&self,
now: Instant,
transceiver_id: RTCRtpTransceiverId,
) -> RTCStatsReport {
use std::collections::HashSet;
let mut entries = Vec::new();
let mut referenced_codec_ids = HashSet::new();
let mut has_streams = false;
for (ssrc, stream) in &self.outbound_rtp_streams {
if stream.transceiver_id == transceiver_id {
has_streams = true;
let id = format!("RTCOutboundRTPStream_{}_{}", stream.kind, ssrc);
entries.push(RTCStatsReportEntry::OutboundRtp(stream.snapshot(now, &id)));
entries.push(RTCStatsReportEntry::RemoteInboundRtp(
stream.snapshot_remote(now),
));
if !stream.codec_id.is_empty() {
referenced_codec_ids.insert(stream.codec_id.clone());
}
}
}
if has_streams {
entries.push(RTCStatsReportEntry::Transport(self.transport.snapshot(now)));
for (id, codec) in &self.codecs {
if referenced_codec_ids.contains(id) {
entries.push(RTCStatsReportEntry::Codec(codec.snapshot(now, id)));
}
}
for (id, pair) in &self.ice_candidate_pairs {
entries.push(RTCStatsReportEntry::IceCandidatePair(
pair.snapshot(now, id),
));
}
for (id, candidate) in &self.local_candidates {
entries.push(RTCStatsReportEntry::LocalCandidate(
candidate.snapshot_local(now, id),
));
}
for (id, candidate) in &self.remote_candidates {
entries.push(RTCStatsReportEntry::RemoteCandidate(
candidate.snapshot_remote(now, id),
));
}
for (id, cert) in &self.certificates {
entries.push(RTCStatsReportEntry::Certificate(cert.snapshot(now, id)));
}
}
RTCStatsReport::new(entries)
}
fn snapshot_for_receiver(
&self,
now: Instant,
transceiver_id: RTCRtpTransceiverId,
) -> RTCStatsReport {
use std::collections::HashSet;
let mut entries = Vec::new();
let mut referenced_codec_ids = HashSet::new();
let mut has_streams = false;
for (ssrc, stream) in &self.inbound_rtp_streams {
if stream.transceiver_id == transceiver_id {
has_streams = true;
let id = format!("RTCInboundRTPStream_{}_{}", stream.kind, ssrc);
entries.push(RTCStatsReportEntry::InboundRtp(stream.snapshot(now, &id)));
entries.push(RTCStatsReportEntry::RemoteOutboundRtp(
stream.snapshot_remote(now),
));
if !stream.codec_id.is_empty() {
referenced_codec_ids.insert(stream.codec_id.clone());
}
}
}
if has_streams {
entries.push(RTCStatsReportEntry::Transport(self.transport.snapshot(now)));
for (id, codec) in &self.codecs {
if referenced_codec_ids.contains(id) {
entries.push(RTCStatsReportEntry::Codec(codec.snapshot(now, id)));
}
}
for (id, pair) in &self.ice_candidate_pairs {
entries.push(RTCStatsReportEntry::IceCandidatePair(
pair.snapshot(now, id),
));
}
for (id, candidate) in &self.local_candidates {
entries.push(RTCStatsReportEntry::LocalCandidate(
candidate.snapshot_local(now, id),
));
}
for (id, candidate) in &self.remote_candidates {
entries.push(RTCStatsReportEntry::RemoteCandidate(
candidate.snapshot_remote(now, id),
));
}
for (id, cert) in &self.certificates {
entries.push(RTCStatsReportEntry::Certificate(cert.snapshot(now, id)));
}
}
RTCStatsReport::new(entries)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn get_or_create_inbound_rtp_streams(
&mut self,
ssrc: SSRC,
kind: RtpCodecKind,
track_identifier: &str,
mid: &str,
rtx_ssrc: Option<u32>,
fec_ssrc: Option<u32>,
transceiver_id: RTCRtpTransceiverId,
) -> &mut InboundRtpStreamAccumulator {
if let Some(rtx) = rtx_ssrc {
self.rtx_ssrc_to_primary.insert(rtx, ssrc);
}
if let Some(fec) = fec_ssrc {
self.fec_ssrc_to_primary.insert(fec, ssrc);
}
let transport_id = self.transport.transport_id.clone();
self.inbound_rtp_streams
.entry(ssrc)
.or_insert_with(|| InboundRtpStreamAccumulator {
ssrc,
kind,
transport_id,
track_identifier: track_identifier.to_string(),
mid: mid.to_string(),
rtx_ssrc,
fec_ssrc,
transceiver_id,
..Default::default()
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn get_or_create_outbound_rtp_streams(
&mut self,
ssrc: SSRC,
kind: RtpCodecKind,
mid: &str,
rid: &str,
encoding_index: u32,
rtx_ssrc: Option<u32>,
transceiver_id: RTCRtpTransceiverId,
) -> &mut OutboundRtpStreamAccumulator {
if let Some(rtx) = rtx_ssrc {
self.rtx_ssrc_to_primary.insert(rtx, ssrc);
}
self.outbound_rtp_streams
.entry(ssrc)
.or_insert_with(|| OutboundRtpStreamAccumulator {
ssrc,
kind,
transport_id: self.transport.transport_id.clone(),
mid: mid.to_string(),
rid: rid.to_string(),
encoding_index,
rtx_ssrc,
transceiver_id,
active: true,
..Default::default()
})
}
pub(crate) fn get_or_create_data_channel(
&mut self,
id: RTCDataChannelId,
label: &str,
protocol: &str,
) -> &mut DataChannelStatsAccumulator {
self.data_channels
.entry(id)
.or_insert_with(|| DataChannelStatsAccumulator {
data_channel_identifier: id,
label: label.to_string(),
protocol: protocol.to_string(),
..Default::default()
})
}
pub(crate) fn get_or_create_candidate_pair(
&mut self,
local_id: &str,
remote_id: &str,
) -> &mut IceCandidatePairAccumulator {
self.ice_candidate_pairs
.entry(format!("RTCIceCandidatePair_{}_{}", local_id, remote_id))
.or_insert_with(|| IceCandidatePairAccumulator {
transport_id: self.transport.transport_id.clone(),
local_candidate_id: local_id.to_string(),
remote_candidate_id: remote_id.to_string(),
..Default::default()
})
}
pub(crate) fn register_local_candidate(
&mut self,
id: String,
candidate: IceCandidateAccumulator,
) {
self.local_candidates.insert(id, candidate);
}
pub(crate) fn register_remote_candidate(
&mut self,
id: String,
candidate: IceCandidateAccumulator,
) {
self.remote_candidates.insert(id, candidate);
}
pub(crate) fn register_certificate(
&mut self,
fingerprint: String,
cert: CertificateStatsAccumulator,
) {
self.certificates.insert(fingerprint, cert);
}
pub(crate) fn get_or_create_media_source(
&mut self,
track_id: &str,
kind: RtpCodecKind,
) -> &mut MediaSourceStatsAccumulator {
self.media_sources
.entry(track_id.to_string())
.or_insert_with(|| MediaSourceStatsAccumulator {
track_id: track_id.to_string(),
kind,
..Default::default()
})
}
pub(crate) fn get_or_create_audio_playout(
&mut self,
playout_id: &str,
) -> &mut AudioPlayoutStatsAccumulator {
self.audio_playouts
.entry(playout_id.to_string())
.or_insert_with(|| AudioPlayoutStatsAccumulator {
kind: RtpCodecKind::Audio,
..Default::default()
})
}
pub(crate) fn on_rtx_packet_sent_if_rtx(
&mut self,
rtx_ssrc: SSRC,
payload_bytes: usize,
) -> bool {
if let Some(primary_ssrc) = self.rtx_ssrc_to_primary.get(&rtx_ssrc)
&& let Some(stream) = self.outbound_rtp_streams.get_mut(primary_ssrc)
{
stream.on_rtx_sent(payload_bytes);
return true;
}
false
}
pub(crate) fn on_rtx_packet_received_if_rtx(
&mut self,
rtx_ssrc: SSRC,
payload_bytes: usize,
) -> bool {
if let Some(primary_ssrc) = self.rtx_ssrc_to_primary.get(&rtx_ssrc)
&& let Some(stream) = self.inbound_rtp_streams.get_mut(primary_ssrc)
{
stream.on_rtx_received(payload_bytes);
return true;
}
false
}
pub(crate) fn on_fec_packet_received_if_fec(
&mut self,
fec_ssrc: SSRC,
payload_bytes: usize,
) -> bool {
if let Some(primary_ssrc) = self.fec_ssrc_to_primary.get(&fec_ssrc)
&& let Some(stream) = self.inbound_rtp_streams.get_mut(primary_ssrc)
{
stream.on_fec_received(payload_bytes);
return true;
}
false
}
pub(crate) fn update_decoder_stats(&mut self, ssrc: SSRC, stats: DecoderStatsUpdate) {
if let Some(stream) = self.inbound_rtp_streams.get_mut(&ssrc) {
stream.decoder_stats = Some(stats);
}
}
pub(crate) fn update_encoder_stats(&mut self, ssrc: SSRC, stats: EncoderStatsUpdate) {
if let Some(stream) = self.outbound_rtp_streams.get_mut(&ssrc) {
stream.encoder_stats = Some(stats);
}
}
pub(crate) fn update_audio_receiver_stats(
&mut self,
ssrc: SSRC,
stats: AudioReceiverStatsUpdate,
) {
if let Some(stream) = self.inbound_rtp_streams.get_mut(&ssrc) {
stream.audio_receiver_stats = Some(stats);
}
}
pub(crate) fn update_audio_source_stats(
&mut self,
track_id: &str,
stats: AudioSourceStatsUpdate,
) {
if let Some(source) = self.media_sources.get_mut(track_id) {
source.audio_level = Some(stats.audio_level);
source.total_audio_energy = Some(stats.total_audio_energy);
source.total_samples_duration = Some(stats.total_samples_duration);
source.echo_return_loss = Some(stats.echo_return_loss);
source.echo_return_loss_enhancement = Some(stats.echo_return_loss_enhancement);
}
}
pub(crate) fn update_video_source_stats(
&mut self,
track_id: &str,
stats: VideoSourceStatsUpdate,
) {
if let Some(source) = self.media_sources.get_mut(track_id) {
source.width = Some(stats.width);
source.height = Some(stats.height);
source.frames = Some(stats.frames);
source.frames_per_second = Some(stats.frames_per_second);
}
}
pub(crate) fn update_audio_playout_stats(
&mut self,
playout_id: &str,
stats: AudioPlayoutStatsUpdate,
) {
if let Some(playout) = self.audio_playouts.get_mut(playout_id) {
playout.synthesized_samples_duration = stats.synthesized_samples_duration;
playout.synthesized_samples_events = stats.synthesized_samples_events;
playout.total_samples_duration = stats.total_samples_duration;
playout.total_playout_delay = stats.total_playout_delay;
playout.total_samples_count = stats.total_samples_count;
}
}
pub(crate) fn update_ice_agent_stats(
&mut self,
local_id: &str,
remote_id: &str,
cp_stats: &CandidatePairStats,
) {
let pair = self.get_or_create_candidate_pair(local_id, remote_id);
pair.requests_sent = cp_stats.requests_sent;
pair.requests_received = cp_stats.requests_received;
pair.responses_sent = cp_stats.responses_sent;
pair.responses_received = cp_stats.responses_received;
pair.consent_requests_sent = cp_stats.consent_requests_sent;
pair.total_round_trip_time = cp_stats.total_round_trip_time;
pair.current_round_trip_time = cp_stats.current_round_trip_time;
}
pub(crate) fn register_inbound_codec(
&mut self,
ssrc: SSRC,
codec: &RTCRtpCodec,
payload_type: PayloadType,
) {
let transport_id = self.transport.transport_id.clone();
let codec_id = CodecStatsAccumulator::generate_id(
&transport_id,
CodecDirection::Receive,
payload_type,
);
self.codecs
.entry(codec_id.clone())
.or_insert_with(|| CodecStatsAccumulator::from_codec(codec, payload_type));
if let Some(stream) = self.inbound_rtp_streams.get_mut(&ssrc) {
stream.codec_id = codec_id;
}
}
pub(crate) fn register_outbound_codec(
&mut self,
ssrc: SSRC,
codec: &RTCRtpCodec,
payload_type: PayloadType,
) {
let transport_id = self.transport.transport_id.clone();
let codec_id =
CodecStatsAccumulator::generate_id(&transport_id, CodecDirection::Send, payload_type);
self.codecs
.entry(codec_id.clone())
.or_insert_with(|| CodecStatsAccumulator::from_codec(codec, payload_type));
if let Some(stream) = self.outbound_rtp_streams.get_mut(&ssrc) {
stream.codec_id = codec_id;
}
}
pub(crate) fn cleanup_unreferenced_codecs(&mut self) {
let mut referenced: std::collections::HashSet<String> = std::collections::HashSet::new();
for stream in self.inbound_rtp_streams.values() {
if !stream.codec_id.is_empty() {
referenced.insert(stream.codec_id.clone());
}
}
for stream in self.outbound_rtp_streams.values() {
if !stream.codec_id.is_empty() {
referenced.insert(stream.codec_id.clone());
}
}
self.codecs.retain(|id, _| referenced.contains(id));
}
}