Add more stats to allow for refined loss analysis

Co-authored-by: adel-signal <adel@signal.org>
This commit is contained in:
Jim Gustafson 2026-06-04 10:01:35 -07:00 committed by GitHub
parent a755cb746f
commit d98700a26a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 291 additions and 154 deletions

View File

@ -35,8 +35,7 @@ pub enum ChartDimension {
AudioReceiveAudioEnergy,
AudioReceiveJitterBufferDelay,
AudioReceiveJitterBufferTargetDelay,
AudioReceiveTotalSamplesReceived,
AudioReceiveConcealedSamples,
AudioReceiveConcealedSamplesPct,
AudioReceiveFecPacketsReceived,
VideoSendPacketsPerSecond,
@ -93,11 +92,8 @@ impl ChartDimension {
ChartDimension::AudioReceiveJitterBufferTargetDelay => {
("Audio Received Jitter Buffer Target Delay", "milliseconds")
}
ChartDimension::AudioReceiveTotalSamplesReceived => {
("Audio Received Total Samples", "Samples")
}
ChartDimension::AudioReceiveConcealedSamples => {
("Audio Received Concealed Samples", "Samples")
ChartDimension::AudioReceiveConcealedSamplesPct => {
("Audio Received Concealed Samples", "%")
}
ChartDimension::AudioReceiveFecPacketsReceived => {
("Audio Received FEC Packets", "Packets")
@ -157,10 +153,9 @@ impl ChartDimension {
ChartDimension::AudioReceiveJitterBufferTargetDelay => {
"audio_receive_jitter_buffer_target_delay"
}
ChartDimension::AudioReceiveTotalSamplesReceived => {
"audio_receive_total_samples_received"
ChartDimension::AudioReceiveConcealedSamplesPct => {
"audio_receive_concealed_samples_pct"
}
ChartDimension::AudioReceiveConcealedSamples => "audio_receive_concealed_samples",
ChartDimension::AudioReceiveFecPacketsReceived => "audio_receive_fec_packets_received",
ChartDimension::VideoSendPacketsPerSecond => "video_send_pps",
ChartDimension::VideoSendPacketSize => "video_send_packet_size",

View File

@ -469,9 +469,10 @@ pub struct AudioReceiveStatsTransfer {
pub audio_energy: StatsData,
pub jitter_buffer_delay: StatsData,
pub jitter_buffer_target_delay: StatsData,
pub total_samples_received: StatsData,
pub concealed_samples: StatsData,
pub jitter_buffer_flushes: StatsData,
pub concealed_samples_pct: StatsData,
pub fec_packets_received: StatsData,
pub relative_arrival_delay_per_packet: StatsData,
}
#[derive(Debug, Default)]
@ -538,9 +539,10 @@ pub struct AudioReceiveStats {
pub audio_energy_stats: Stats,
pub jitter_buffer_delay_stats: Stats,
pub jitter_buffer_target_delay_stats: Stats,
pub total_samples_received_stats: Stats,
pub concealed_samples_stats: Stats,
pub jitter_buffer_flushes_stats: Stats,
pub concealed_samples_pct_stats: Stats,
pub fec_packets_received_stats: Stats,
pub relative_arrival_delay_per_packet_stats: Stats,
}
#[derive(Debug, Default)]
@ -600,9 +602,9 @@ impl ClientLogReport {
r".*ringrtc_stats!,video,send,(?P<ssrc>\d+),(?P<packets_per_second>[-+]?[0-9]*\.?[0-9]+),(?P<average_packet_size>[-+]?[0-9]*\.?[0-9]+),(?P<bitrate>[-+]?[0-9]*\.?[0-9]+)bps,(?P<framerate>[0-9]*\.?[0-9]+)fps,(?P<key_frames_encoded>\d+),(?P<encode_time_per_frame>[0-9]*\.?[0-9]+)ms,(?P<resolution>\d+x\d+),(?P<retransmitted_packets_sent>\d+),(?P<retransmitted_bitrate>[0-9]*\.?[0-9]+)bps,(?P<send_delay_per_packet>[0-9]*\.?[0-9]+)ms,(?P<nack_count>\d+),(?P<pli_count>\d+),(?P<quality_limitation_reason>\w+),(?P<quality_limitation_resolution_changes>\d+),(?P<remote_packet_loss>[-+]?[0-9]*\.?[0-9]+)%,(?P<remote_jitter>[0-9]*\.?[0-9]+)ms,(?P<remote_round_trip_time>[0-9]*\.?[0-9]+)ms",
)?;
// Example: ringrtc_stats!,audio,recv,1002,40.0,0.0%,32000.0bps,0ms,0.000,50ms,40ms,48000,0,0
// Example: ringrtc_stats!,audio,recv,1002,40.0,0.00%,32000.0bps,0ms,0.000,50ms,40ms,0,0.00%,0,0ms
let re_audio_receive_line = Regex::new(
r".*ringrtc_stats!,audio,recv,(?P<ssrc>\d+),(?P<packets_per_second>[-+]?[0-9]*\.?[0-9]+),(?P<packet_loss>[-+]?[0-9]*\.?[0-9]+)%,(?P<bitrate>[-+]?[0-9]*\.?[0-9]+)bps,(?P<jitter>\d+)ms,(?P<audio_energy>[-+]?[0-9]*\.?[0-9]+),(?P<jitter_buffer_delay>\d+)ms,(?P<jitter_buffer_target_delay>\d+)ms,(?P<total_samples_received>\d+),(?P<concealed_samples>\d+),(?P<fec_packets_received>\d+)",
r".*ringrtc_stats!,audio,recv,(?P<ssrc>\d+),(?P<packets_per_second>[-+]?[0-9]*\.?[0-9]+),(?P<packet_loss>[-+]?[0-9]*\.?[0-9]+)%,(?P<bitrate>[-+]?[0-9]*\.?[0-9]+)bps,(?P<jitter>\d+)ms,(?P<audio_energy>[-+]?[0-9]*\.?[0-9]+),(?P<jitter_buffer_delay>\d+)ms,(?P<jitter_buffer_target_delay>\d+)ms,(?P<jitter_buffer_flushes>\d+),(?P<concealed_samples_pct>[-+]?[0-9]*\.?[0-9]+)%,(?P<fec_packets_received>\d+),(?P<relative_arrival_delay_per_packet>\d+)ms",
)?;
// Example: ringrtc_stats!,video,recv,2003,7.0,0.0%,61305bps,1.0fps,1,3.3ms,1280x720
@ -748,14 +750,17 @@ impl ClientLogReport {
.jitter_buffer_target_delay
.push(f32::from_str(&cap["jitter_buffer_target_delay"])?);
audio_receive_stats
.total_samples_received
.push(f32::from_str(&cap["total_samples_received"])?);
.jitter_buffer_flushes
.push(f32::from_str(&cap["jitter_buffer_flushes"])?);
audio_receive_stats
.concealed_samples
.push(f32::from_str(&cap["concealed_samples"])?);
.concealed_samples_pct
.push(f32::from_str(&cap["concealed_samples_pct"])?);
audio_receive_stats
.fec_packets_received
.push(f32::from_str(&cap["fec_packets_received"])?);
audio_receive_stats
.relative_arrival_delay_per_packet
.push(f32::from_str(&cap["relative_arrival_delay_per_packet"])?);
continue;
}
@ -1209,31 +1214,33 @@ impl ClientLogReport {
data: audio_receive_stats.jitter_buffer_target_delay,
};
let total_samples_received_stats = Stats {
let jitter_buffer_flushes_stats = Stats {
config: StatsConfig {
title: format!("Audio Receive Total Samples Received (ssrc={ssrc})"),
title: format!("Audio Receive Jitter Buffer Flushes (ssrc={ssrc})"),
chart_name: format!(
"{}.log.audio.receive.total_samples_received.svg",
"{}.log.audio.receive.jitter_buffer_flushes.svg",
client_name
),
x_label: "Test Seconds".to_string(),
y_label: "Samples".to_string(),
y_label: "Flushes".to_string(),
show_total: true,
..Default::default()
},
data: audio_receive_stats.total_samples_received,
data: audio_receive_stats.jitter_buffer_flushes,
};
let concealed_samples_stats = Stats {
let concealed_samples_pct_stats = Stats {
config: StatsConfig {
title: format!("Audio Receive Concealed Samples (ssrc={ssrc})"),
chart_name: format!("{}.log.audio.receive.concealed_samples.svg", client_name),
chart_name: format!(
"{}.log.audio.receive.concealed_samples_pct.svg",
client_name
),
x_label: "Test Seconds".to_string(),
y_label: "Samples".to_string(),
show_total: true,
y_label: "%".to_string(),
..Default::default()
},
data: audio_receive_stats.concealed_samples,
data: audio_receive_stats.concealed_samples_pct,
};
let fec_packets_received_stats = Stats {
@ -1251,6 +1258,20 @@ impl ClientLogReport {
data: audio_receive_stats.fec_packets_received,
};
let relative_arrival_delay_per_packet_stats = Stats {
config: StatsConfig {
title: format!("Audio Receive Relative Arrival Delay Per Packet (ssrc={ssrc})"),
chart_name: format!(
"{}.log.audio.receive.relative_arrival_delay_per_packet.svg",
client_name
),
x_label: "Test Seconds".to_string(),
y_label: "milliseconds".to_string(),
..Default::default()
},
data: audio_receive_stats.relative_arrival_delay_per_packet,
};
audio_receive_stats_list.push(AudioReceiveStats {
ssrc: audio_receive_stats.ssrc,
packets_per_second_stats,
@ -1260,9 +1281,10 @@ impl ClientLogReport {
audio_energy_stats,
jitter_buffer_delay_stats,
jitter_buffer_target_delay_stats,
total_samples_received_stats,
concealed_samples_stats,
jitter_buffer_flushes_stats,
concealed_samples_pct_stats,
fec_packets_received_stats,
relative_arrival_delay_per_packet_stats,
});
}
@ -1645,9 +1667,10 @@ impl Report {
&per_ssrc.audio_energy_stats,
&per_ssrc.jitter_buffer_delay_stats,
&per_ssrc.jitter_buffer_target_delay_stats,
&per_ssrc.total_samples_received_stats,
&per_ssrc.concealed_samples_stats,
&per_ssrc.jitter_buffer_flushes_stats,
&per_ssrc.concealed_samples_pct_stats,
&per_ssrc.fec_packets_received_stats,
&per_ssrc.relative_arrival_delay_per_packet_stats,
]
}));
@ -1901,10 +1924,11 @@ impl Report {
&audio_receive_stats.jitter_stats,
&audio_receive_stats.jitter_buffer_delay_stats,
&audio_receive_stats.jitter_buffer_target_delay_stats,
&audio_receive_stats.jitter_buffer_flushes_stats,
&audio_receive_stats.audio_energy_stats,
&audio_receive_stats.total_samples_received_stats,
&audio_receive_stats.concealed_samples_stats,
&audio_receive_stats.concealed_samples_pct_stats,
&audio_receive_stats.fec_packets_received_stats,
&audio_receive_stats.relative_arrival_delay_per_packet_stats,
],
);
buf.extend_from_slice(
@ -2169,20 +2193,12 @@ impl Report {
.map(|stats| stats.jitter_buffer_target_delay_stats.data.ave)
.collect_vec(),
),
ChartDimension::AudioReceiveTotalSamplesReceived => average(
ChartDimension::AudioReceiveConcealedSamplesPct => average(
&report
.client_log_report
.audio_receive_stats_list
.iter()
.map(|stats| stats.total_samples_received_stats.data.ave)
.collect_vec(),
),
ChartDimension::AudioReceiveConcealedSamples => average(
&report
.client_log_report
.audio_receive_stats_list
.iter()
.map(|stats| stats.concealed_samples_stats.data.ave)
.map(|stats| stats.concealed_samples_pct_stats.data.ave)
.collect_vec(),
),
ChartDimension::AudioReceiveFecPacketsReceived => average(
@ -2536,7 +2552,6 @@ struct SummaryRow {
pub audio_receive_packet_rate: f32,
pub audio_receive_bitrate: f32,
pub audio_receive_loss: f32,
pub concealed_samples_total: f32,
pub concealed_samples_pct: f32,
pub fec_packets_received_total: f32,
@ -2569,28 +2584,21 @@ impl SummaryRow {
audio_receive_packet_rate,
audio_receive_bitrate,
audio_receive_loss,
concealed_samples_total,
total_samples_received_total,
concealed_samples_pct,
fec_packets_received_total,
) = report
.client_log_report
.audio_receive_stats_list
.iter()
.fold((0.0, 0.0, 0.0, 0.0, 0.0, 0.0), |acc, stats| {
.fold((0.0, 0.0, 0.0, 0.0, 0.0), |acc, stats| {
(
acc.0 + stats.packets_per_second_stats.data.ave,
acc.1 + stats.bitrate_stats.data.ave,
acc.2 + stats.packet_loss_stats.data.ave,
acc.3 + stats.concealed_samples_stats.data.total,
acc.4 + stats.total_samples_received_stats.data.total,
acc.5 + stats.fec_packets_received_stats.data.total,
acc.3 + stats.concealed_samples_pct_stats.data.ave,
acc.4 + stats.fec_packets_received_stats.data.total,
)
});
let concealed_samples_pct = if total_samples_received_total > 0.0 {
100.0 * concealed_samples_total / total_samples_received_total
} else {
0.0
};
Self {
audio_send_packet_size: report
.client_log_report
@ -2613,8 +2621,7 @@ impl SummaryRow {
audio_receive_packet_rate,
audio_receive_bitrate,
audio_receive_loss,
concealed_samples_total: concealed_samples_total as f32,
concealed_samples_pct: concealed_samples_pct as f32,
concealed_samples_pct,
fec_packets_received_total: fec_packets_received_total as f32,
container_cpu: report.docker_stats_report.cpu_usage.data.ave,
container_memory: report.docker_stats_report.mem_usage.data.ave,
@ -2644,22 +2651,30 @@ impl SummaryRow {
vmaf: report.analysis_report.as_ref().and_then(|ar| ar.vmaf),
row_type: SummaryRowType::Single,
row_index: 0,
video_send_resolution: report.client_log_report.video_send_stats[0]
.resolution
.data
.ave,
video_send_framerate: report.client_log_report.video_send_stats[0]
.framerate_stats
.data
.ave,
video_recv_resolution: report.client_log_report.video_receive_stats_list[0]
.resolution
.data
.ave,
video_recv_framerate: report.client_log_report.video_receive_stats_list[0]
.framerate_stats
.data
.ave,
video_send_resolution: report
.client_log_report
.video_send_stats
.first()
.map(|s| s.resolution.data.ave)
.unwrap_or(0.0),
video_send_framerate: report
.client_log_report
.video_send_stats
.first()
.map(|s| s.framerate_stats.data.ave)
.unwrap_or(0.0),
video_recv_resolution: report
.client_log_report
.video_receive_stats_list
.first()
.map(|s| s.resolution.data.ave)
.unwrap_or(0.0),
video_recv_framerate: report
.client_log_report
.video_receive_stats_list
.first()
.map(|s| s.framerate_stats.data.ave)
.unwrap_or(0.0),
}
}
@ -2693,8 +2708,6 @@ impl SummaryRow {
self.audio_receive_bitrate =
new_average(self.audio_receive_bitrate, new.audio_receive_bitrate);
self.audio_receive_loss = new_average(self.audio_receive_loss, new.audio_receive_loss);
self.concealed_samples_total =
new_average(self.concealed_samples_total, new.concealed_samples_total);
self.concealed_samples_pct =
new_average(self.concealed_samples_pct, new.concealed_samples_pct);
self.fec_packets_received_total = new_average(
@ -3333,17 +3346,19 @@ impl Html {
indent, summary_row.audio_send_bitrate
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_send_resolution
);
if group_config.summary_report_columns.show_video {
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_send_resolution
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_send_framerate
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_send_framerate
);
}
}
if group_config.summary_report_columns.show_receive_stats {
@ -3363,17 +3378,19 @@ impl Html {
indent, summary_row.audio_receive_loss
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_recv_resolution
);
if group_config.summary_report_columns.show_video {
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_recv_resolution
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_recv_framerate
);
let _ = writeln!(
buf,
"<td>{}{:.2}</td>",
indent, summary_row.video_recv_framerate
);
}
}
if group_config.summary_report_columns.show_receive_stats
@ -3381,12 +3398,7 @@ impl Html {
{
let _ = writeln!(
buf,
"<td>{}{:.0}</td>",
indent, summary_row.concealed_samples_total
);
let _ = writeln!(
buf,
"<td>{}{:.3}%</td>",
"<td>{}{:.2}</td>",
indent, summary_row.concealed_samples_pct
);
let _ = writeln!(
@ -3489,13 +3501,25 @@ impl Html {
buf.push_str("<th colspan=\"3\" style=\"width: 33%\">Test Case</th>\n");
}
if group_config.summary_report_columns.show_send_stats {
buf.push_str("<th colspan=\"5\">Client Send Stats (average)</th>\n");
let send_colspan = if group_config.summary_report_columns.show_video {
5
} else {
3
};
let _ = writeln!(
buf,
"<th colspan=\"{send_colspan}\">Client Send Stats (average)</th>"
);
}
if group_config.summary_report_columns.show_receive_stats {
let recv_colspan = if group_config.summary_report_columns.show_dred_stats {
8
} else {
5
let recv_colspan = match (
group_config.summary_report_columns.show_video,
group_config.summary_report_columns.show_dred_stats,
) {
(true, true) => 7,
(true, false) => 5,
(false, true) => 5,
(false, false) => 3,
};
let _ = writeln!(
buf,
@ -3547,18 +3571,21 @@ impl Html {
buf.push_str("<th>Packet Size</th>\n");
buf.push_str("<th>Packet Rate</th>\n");
buf.push_str("<th>Bitrate</th>\n");
buf.push_str("<th>Resolution</th>\n");
buf.push_str("<th>Framerate</th>\n");
if group_config.summary_report_columns.show_video {
buf.push_str("<th>Resolution</th>\n");
buf.push_str("<th>Framerate</th>\n");
}
}
if group_config.summary_report_columns.show_receive_stats {
buf.push_str("<th>Packet Rate</th>\n");
buf.push_str("<th>Bitrate</th>\n");
buf.push_str("<th>Loss</th>\n");
buf.push_str("<th>Resolution</th>\n");
buf.push_str("<th>Framerate</th>\n");
if group_config.summary_report_columns.show_video {
buf.push_str("<th>Resolution</th>\n");
buf.push_str("<th>Framerate</th>\n");
}
if group_config.summary_report_columns.show_dred_stats {
buf.push_str("<th>Concealed</th>\n");
buf.push_str("<th>Concealed %</th>\n");
buf.push_str("<th>FEC</th>\n");
}
}

View File

@ -24,18 +24,20 @@ enum VideoCodec {
}
message StreamSummary {
optional DistributionSummary bitrate = 1;
optional DistributionSummary packet_loss_pct = 2;
optional DistributionSummary jitter = 3;
optional DistributionSummary bitrate = 1;
optional DistributionSummary packet_loss_pct = 2;
optional DistributionSummary jitter = 3;
// Only present for received video streams
optional DistributionSummary freeze_count = 4;
optional VideoCodec video_codec = 5;
optional DistributionSummary freeze_count = 4;
optional VideoCodec video_codec = 5;
// Always present
optional uint32 ssrc = 6;
optional string codec_implementation = 7;
optional uint32 ssrc = 6;
optional string codec_implementation = 7;
// Only present for received audio streams
optional float audio_concealment_pct = 8;
optional uint64 audio_redundant_packets = 9;
optional DistributionSummary audio_concealment_pct = 8;
optional uint64 audio_redundant_packets = 9;
optional uint64 audio_jitter_buffer_flushes = 10;
optional DistributionSummary audio_relative_arrival_delay = 11;
}
message StreamSummaries {

View File

@ -64,6 +64,8 @@ impl CallStateHandler for CallEndpoint {
&& let Some(connected_sender) = &state.event_sync.connected
{
let _ = connected_sender.send(());
} else if let CallState::Ended(_, summary) = call_state {
info!("{}", summary);
}
});
Ok(())

View File

@ -5,7 +5,7 @@
use std::{
collections::{HashMap, VecDeque},
fmt::Debug,
fmt::{self, Debug, Display},
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
sync::{Arc, MutexGuard},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
@ -13,6 +13,7 @@ use std::{
use anyhow::anyhow;
use prost::Message;
use serde_json::json;
use sketches_ddsketch::{Config, DDSketch};
use crate::{
@ -106,6 +107,64 @@ impl Timestamp {
}
}
impl Display for CallSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let call_id_hash_hex = self.call_id_hash.as_ref().map(|hash| {
hash.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
});
let raw_stats_text_json = self.raw_stats_text.as_ref().and_then(|text| {
match serde_json::from_str::<serde_json::Value>(text) {
Ok(v) => Some(v),
Err(e) => {
error!("Failed to parse raw_stats_text as JSON: {}", e);
None
}
}
});
let summary_json = json!({
"CallSummary": {
"call_id_hash": call_id_hash_hex,
"start_time": u64::from(self.start_time),
"end_time": u64::from(self.end_time),
"is_survey_candidate": self.is_survey_candidate,
"call_end_reason_text": &self.call_end_reason_text,
"quality_stats": {
"rtt_median_connection": self.quality_stats.rtt_median_connection,
"audio_stats": {
"rtt_median": self.quality_stats.audio_stats.rtt_median,
"jitter_median_send": self.quality_stats.audio_stats.jitter_median_send,
"jitter_median_recv": self.quality_stats.audio_stats.jitter_median_recv,
"packet_loss_fraction_send": self.quality_stats.audio_stats.packet_loss_fraction_send,
"packet_loss_fraction_recv": self.quality_stats.audio_stats.packet_loss_fraction_recv
},
"video_stats": {
"rtt_median": self.quality_stats.video_stats.rtt_median,
"jitter_median_send": self.quality_stats.video_stats.jitter_median_send,
"jitter_median_recv": self.quality_stats.video_stats.jitter_median_recv,
"packet_loss_fraction_send": self.quality_stats.video_stats.packet_loss_fraction_send,
"packet_loss_fraction_recv": self.quality_stats.video_stats.packet_loss_fraction_recv
}
},
// Just showing the length for the blob rather than a byte array.
"raw_stats": self.raw_stats.as_ref().map(|blob| blob.len()),
// We show the telemetry/stats blob json string inline.
"raw_stats_text": raw_stats_text_json,
}
});
let summary = serde_json::to_string_pretty(&summary_json).map_err(|e| {
error!("Failed to serialize CallSummary JSON: {}", e);
fmt::Error
})?;
f.write_str(&summary)
}
}
/// We need to be able to perform basic arithmetic operations on sample values
/// and have the ability to compare them. We also want to be able to create them
/// from counter values, and calculate their square roots.
@ -217,9 +276,10 @@ struct StreamSummary {
video_codec: StatsVideoCodecType,
codec_implementation: Option<String>,
// The most recent audio receive stream redundancy statistics.
audio_samples_received: Option<u64>,
audio_samples_concealed: Option<u64>,
audio_concealed_samples: DistributionSummary<f32>,
audio_redundant_packets: Option<u64>,
audio_jitter_buffer_flushes: Option<u64>,
audio_relative_arrival_delay: DistributionSummary<f32>,
}
/// `StreamSummaries` is converted into `protobuf::call_summary::StreamSummaries`
@ -550,12 +610,10 @@ impl From<(&u32, &StreamSummary)> for protobuf::call_summary::StreamSummary {
},
codec_implementation: summary.codec_implementation.clone(),
ssrc: Some(*ssrc),
audio_concealment_pct: summary
.audio_samples_received
.zip(summary.audio_samples_concealed)
.filter(|(received, _)| *received > 0)
.map(|(received, concealed)| (concealed as f32 / received as f32) * 100.0),
audio_concealment_pct: summary.audio_concealed_samples.to_proto(),
audio_redundant_packets: summary.audio_redundant_packets,
audio_jitter_buffer_flushes: summary.audio_jitter_buffer_flushes,
audio_relative_arrival_delay: summary.audio_relative_arrival_delay.to_proto(),
}
}
}
@ -756,12 +814,16 @@ impl CallInfo {
summary.bitrate.push(snapshot.bitrate);
summary.packet_loss.push(snapshot.packets_lost_pct);
summary.jitter.push(snapshot.jitter as f32);
*summary.audio_samples_received.get_or_insert(0) +=
snapshot.total_samples_received;
*summary.audio_samples_concealed.get_or_insert(0) +=
snapshot.concealed_samples;
summary
.audio_concealed_samples
.push(snapshot.concealed_samples_pct);
*summary.audio_redundant_packets.get_or_insert(0) +=
snapshot.fec_packets_received;
*summary.audio_jitter_buffer_flushes.get_or_insert(0) +=
snapshot.jitter_buffer_flushes;
summary
.audio_relative_arrival_delay
.push(snapshot.relative_arrival_delay_per_packet as f32);
});
self.stats_sets
.push_audio_recv_stream_stats(snapshot.into());
@ -1489,10 +1551,18 @@ mod test {
}),
freeze_count: None,
video_codec: None,
codec_implementation: None,
ssrc: Some(v),
audio_concealment_pct: None,
codec_implementation: None,
audio_concealment_pct: Some(protobuf::call_summary::DistributionSummary {
mean: Some(100.0),
std_dev: Some(0.0),
min_val: Some(100.0),
max_val: Some(100.0),
sample_count: Some(50000),
}),
audio_redundant_packets: None,
audio_jitter_buffer_flushes: None,
audio_relative_arrival_delay: None,
})
.collect::<Vec<protobuf::call_summary::StreamSummary>>()
}

View File

@ -99,6 +99,20 @@ macro_rules! delta {
};
}
fn signed_delta_fn<T, F, V>(lhs: &T, rhs: &T, extract: F) -> V
where
F: Fn(&T) -> V,
V: Sub<Output = V>,
{
extract(lhs) - extract(rhs)
}
macro_rules! signed_delta {
($lhs:tt, $rhs:tt, $field:tt) => {
signed_delta_fn($lhs, $rhs, |stats| stats.$field)
};
}
fn compute_packets_lost_pct(packets_lost: u32, packets_total: u32) -> f32 {
(packets_lost as f32 * 100.0)
.naive_checked_div(packets_total as f32)
@ -280,10 +294,11 @@ pub struct AudioReceiverStatsSnapshot {
pub jitter: f64,
pub jitter_buffer_delay: f64,
pub jitter_buffer_target_delay: f64,
pub jitter_buffer_flushes: u64,
pub audio_energy: f64,
pub total_samples_received: u64,
pub concealed_samples: u64,
pub concealed_samples_pct: f32,
pub fec_packets_received: u64,
pub relative_arrival_delay_per_packet: f64,
}
impl Display for AudioReceiverStatsSnapshot {
@ -296,25 +311,27 @@ impl Display for AudioReceiverStatsSnapshot {
jitter,
jitter_buffer_delay,
jitter_buffer_target_delay,
jitter_buffer_flushes,
audio_energy,
total_samples_received,
concealed_samples,
concealed_samples_pct,
fec_packets_received,
relative_arrival_delay_per_packet,
} = self;
write!(
f,
"{},\
{ssrc},\
{packets_per_second:.1},\
{packets_lost_pct:.1}%,\
{packets_lost_pct:.2}%,\
{bitrate:.1}bps,\
{jitter:.0}ms,\
{audio_energy:.3},\
{jitter_buffer_delay:.0}ms,\
{jitter_buffer_target_delay:.0}ms,\
{total_samples_received},\
{concealed_samples},\
{fec_packets_received}",
{jitter_buffer_flushes},\
{concealed_samples_pct:.2}%,\
{fec_packets_received},\
{relative_arrival_delay_per_packet:.0}ms",
Self::LOG_MARKER
)
}
@ -331,9 +348,10 @@ impl AudioReceiverStatsSnapshot {
audio_energy,\
jitter_buffer_delay,\
jitter_buffer_target_delay,\
total_samples_received,\
concealed_samples,\
fec_packets_received";
jitter_buffer_flushes,\
concealed_samples_pct,\
fec_packets_received,\
relative_arrival_delay_per_packet";
fn derive(
curr_stats: &AudioReceiverStatistics,
@ -342,7 +360,7 @@ impl AudioReceiverStatsSnapshot {
prev_jitter_buffer_delay: f64,
prev_jitter_buffer_target_delay: f64,
) -> Self {
let packets_lost_delta = delta!(curr_stats, prev_stats, packets_lost);
let signed_packets_lost_delta = signed_delta!(curr_stats, prev_stats, packets_lost);
let packets_received_delta = delta!(curr_stats, prev_stats, packets_received);
let jitter_buffer_delay_delta = delta!(curr_stats, prev_stats, jitter_buffer_delay);
let jitter_buffer_target_delay_delta =
@ -353,13 +371,25 @@ impl AudioReceiverStatsSnapshot {
let audio_energy_delta = delta!(curr_stats, prev_stats, total_audio_energy);
let total_samples_received_delta = delta!(curr_stats, prev_stats, total_samples_received);
let concealed_samples_delta = delta!(curr_stats, prev_stats, concealed_samples);
let silent_concealed_samples_delta =
delta!(curr_stats, prev_stats, silent_concealed_samples);
let fec_packets_received_delta = delta!(curr_stats, prev_stats, fec_packets_received);
let jitter_buffer_flushes_delta = delta!(curr_stats, prev_stats, jitter_buffer_flushes);
let packets_discarded_delta = delta!(curr_stats, prev_stats, packets_discarded);
let relative_packet_arrival_delay_delta =
delta!(curr_stats, prev_stats, relative_packet_arrival_delay);
let packets_per_second =
compute_packets_per_second(packets_received_delta, seconds_elapsed);
let packets_lost = packets_lost_delta.max(0) as u32;
let packets_lost_pct =
compute_packets_lost_pct(packets_lost, packets_received_delta + packets_lost);
let packets_lost_pct = {
let numerator = signed_packets_lost_delta + packets_discarded_delta as i32;
let denominator = packets_received_delta as i32 + signed_packets_lost_delta;
if denominator > 0 {
((numerator as f32 * 100.0) / denominator as f32).max(0.0)
} else {
0.0
}
};
let bitrate = compute_bitrate(bytes_received_delta, seconds_elapsed);
let jitter = 1000.0 * curr_stats.jitter;
@ -383,10 +413,18 @@ impl AudioReceiverStatsSnapshot {
jitter,
jitter_buffer_delay,
jitter_buffer_target_delay,
jitter_buffer_flushes: jitter_buffer_flushes_delta,
audio_energy: audio_energy_delta,
total_samples_received: total_samples_received_delta,
concealed_samples: concealed_samples_delta,
concealed_samples_pct: (concealed_samples_delta
.saturating_sub(silent_concealed_samples_delta)
as f32
* 100.0)
.naive_checked_div(total_samples_received_delta as f32)
.unwrap_or(0.0),
fec_packets_received: fec_packets_received_delta,
relative_arrival_delay_per_packet: (1000.0 * relative_packet_arrival_delay_delta)
.naive_checked_div(packets_received_delta as f64)
.unwrap_or(0.0),
}
}
}
@ -1210,7 +1248,10 @@ pub struct AudioReceiverStatistics {
pub estimated_playout_timestamp: f64,
pub total_samples_received: u64,
pub concealed_samples: u64,
pub silent_concealed_samples: u64,
pub fec_packets_received: u64,
pub packets_discarded: u64,
pub relative_packet_arrival_delay: f64,
}
#[repr(C)]