From d98700a26a2f247817e121b46d10d775a8f5dee3 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Thu, 4 Jun 2026 10:01:35 -0700 Subject: [PATCH] Add more stats to allow for refined loss analysis Co-authored-by: adel-signal --- call_sim/src/common.rs | 15 +- call_sim/src/report.rs | 235 ++++++++++-------- protobuf/protobuf/call_summary.proto | 20 +- .../call_sim-cli/endpoint/direct_call_sim.rs | 2 + src/rust/src/core/call_summary.rs | 98 ++++++-- src/rust/src/webrtc/stats_observer.rs | 75 ++++-- 6 files changed, 291 insertions(+), 154 deletions(-) diff --git a/call_sim/src/common.rs b/call_sim/src/common.rs index dbe5043a..11aa8d17 100644 --- a/call_sim/src/common.rs +++ b/call_sim/src/common.rs @@ -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", diff --git a/call_sim/src/report.rs b/call_sim/src/report.rs index 53992c5d..152669c6 100644 --- a/call_sim/src/report.rs +++ b/call_sim/src/report.rs @@ -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\d+),(?P[-+]?[0-9]*\.?[0-9]+),(?P[-+]?[0-9]*\.?[0-9]+),(?P[-+]?[0-9]*\.?[0-9]+)bps,(?P[0-9]*\.?[0-9]+)fps,(?P\d+),(?P[0-9]*\.?[0-9]+)ms,(?P\d+x\d+),(?P\d+),(?P[0-9]*\.?[0-9]+)bps,(?P[0-9]*\.?[0-9]+)ms,(?P\d+),(?P\d+),(?P\w+),(?P\d+),(?P[-+]?[0-9]*\.?[0-9]+)%,(?P[0-9]*\.?[0-9]+)ms,(?P[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\d+),(?P[-+]?[0-9]*\.?[0-9]+),(?P[-+]?[0-9]*\.?[0-9]+)%,(?P[-+]?[0-9]*\.?[0-9]+)bps,(?P\d+)ms,(?P[-+]?[0-9]*\.?[0-9]+),(?P\d+)ms,(?P\d+)ms,(?P\d+),(?P\d+),(?P\d+)", + r".*ringrtc_stats!,audio,recv,(?P\d+),(?P[-+]?[0-9]*\.?[0-9]+),(?P[-+]?[0-9]*\.?[0-9]+)%,(?P[-+]?[0-9]*\.?[0-9]+)bps,(?P\d+)ms,(?P[-+]?[0-9]*\.?[0-9]+),(?P\d+)ms,(?P\d+)ms,(?P\d+),(?P[-+]?[0-9]*\.?[0-9]+)%,(?P\d+),(?P\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, - "{}{:.2}", - indent, summary_row.video_send_resolution - ); + if group_config.summary_report_columns.show_video { + let _ = writeln!( + buf, + "{}{:.2}", + indent, summary_row.video_send_resolution + ); - let _ = writeln!( - buf, - "{}{:.2}", - indent, summary_row.video_send_framerate - ); + let _ = writeln!( + buf, + "{}{:.2}", + 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, - "{}{:.2}", - indent, summary_row.video_recv_resolution - ); + if group_config.summary_report_columns.show_video { + let _ = writeln!( + buf, + "{}{:.2}", + indent, summary_row.video_recv_resolution + ); - let _ = writeln!( - buf, - "{}{:.2}", - indent, summary_row.video_recv_framerate - ); + let _ = writeln!( + buf, + "{}{:.2}", + indent, summary_row.video_recv_framerate + ); + } } if group_config.summary_report_columns.show_receive_stats @@ -3381,12 +3398,7 @@ impl Html { { let _ = writeln!( buf, - "{}{:.0}", - indent, summary_row.concealed_samples_total - ); - let _ = writeln!( - buf, - "{}{:.3}%", + "{}{:.2}", indent, summary_row.concealed_samples_pct ); let _ = writeln!( @@ -3489,13 +3501,25 @@ impl Html { buf.push_str("Test Case\n"); } if group_config.summary_report_columns.show_send_stats { - buf.push_str("Client Send Stats (average)\n"); + let send_colspan = if group_config.summary_report_columns.show_video { + 5 + } else { + 3 + }; + let _ = writeln!( + buf, + "Client Send Stats (average)" + ); } 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("Packet Size\n"); buf.push_str("Packet Rate\n"); buf.push_str("Bitrate\n"); - buf.push_str("Resolution\n"); - buf.push_str("Framerate\n"); + if group_config.summary_report_columns.show_video { + buf.push_str("Resolution\n"); + buf.push_str("Framerate\n"); + } } if group_config.summary_report_columns.show_receive_stats { buf.push_str("Packet Rate\n"); buf.push_str("Bitrate\n"); buf.push_str("Loss\n"); - buf.push_str("Resolution\n"); - buf.push_str("Framerate\n"); + if group_config.summary_report_columns.show_video { + buf.push_str("Resolution\n"); + buf.push_str("Framerate\n"); + } if group_config.summary_report_columns.show_dred_stats { buf.push_str("Concealed\n"); - buf.push_str("Concealed %\n"); buf.push_str("FEC\n"); } } diff --git a/protobuf/protobuf/call_summary.proto b/protobuf/protobuf/call_summary.proto index d5a1d177..0db78bc9 100644 --- a/protobuf/protobuf/call_summary.proto +++ b/protobuf/protobuf/call_summary.proto @@ -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 { diff --git a/src/rust/src/bin/call_sim-cli/endpoint/direct_call_sim.rs b/src/rust/src/bin/call_sim-cli/endpoint/direct_call_sim.rs index 9d8ab95d..10f3b6ff 100644 --- a/src/rust/src/bin/call_sim-cli/endpoint/direct_call_sim.rs +++ b/src/rust/src/bin/call_sim-cli/endpoint/direct_call_sim.rs @@ -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(()) diff --git a/src/rust/src/core/call_summary.rs b/src/rust/src/core/call_summary.rs index 6892fc08..b29e64cb 100644 --- a/src/rust/src/core/call_summary.rs +++ b/src/rust/src/core/call_summary.rs @@ -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::() + }); + + let raw_stats_text_json = self.raw_stats_text.as_ref().and_then(|text| { + match serde_json::from_str::(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, // The most recent audio receive stream redundancy statistics. - audio_samples_received: Option, - audio_samples_concealed: Option, + audio_concealed_samples: DistributionSummary, audio_redundant_packets: Option, + audio_jitter_buffer_flushes: Option, + audio_relative_arrival_delay: DistributionSummary, } /// `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::>() } diff --git a/src/rust/src/webrtc/stats_observer.rs b/src/rust/src/webrtc/stats_observer.rs index 9b90679a..63f02070 100644 --- a/src/rust/src/webrtc/stats_observer.rs +++ b/src/rust/src/webrtc/stats_observer.rs @@ -99,6 +99,20 @@ macro_rules! delta { }; } +fn signed_delta_fn(lhs: &T, rhs: &T, extract: F) -> V +where + F: Fn(&T) -> V, + V: Sub, +{ + 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)]