fix: stamp upstream timing before async send
This commit is contained in:
parent
d2f312b14d
commit
0188c8661b
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -89,7 +89,7 @@ impl LesavkaClientApp {
|
|||||||
queue_depth_u32(next.queue_depth),
|
queue_depth_u32(next.queue_depth),
|
||||||
duration_ms(next.delivery_age),
|
duration_ms(next.delivery_age),
|
||||||
);
|
);
|
||||||
attach_audio_timing_metadata(
|
attach_audio_queue_metadata(
|
||||||
&mut packet,
|
&mut packet,
|
||||||
next.queue_depth,
|
next.queue_depth,
|
||||||
next.delivery_age,
|
next.delivery_age,
|
||||||
@ -140,9 +140,9 @@ impl LesavkaClientApp {
|
|||||||
tracing::info!("🎤 microphone uplink resumed");
|
tracing::info!("🎤 microphone uplink resumed");
|
||||||
paused = false;
|
paused = false;
|
||||||
}
|
}
|
||||||
if let Some(pkt) = mic_clone.pull() {
|
if let Some(mut pkt) = mic_clone.pull() {
|
||||||
trace!("🎤📤 cli {} bytes → gRPC", pkt.data.len());
|
trace!("🎤📤 cli {} bytes → gRPC", pkt.data.len());
|
||||||
let enqueue_age = crate::live_capture_clock::packet_age(pkt.pts);
|
let enqueue_age = stamp_audio_timing_metadata_at_enqueue(&mut pkt);
|
||||||
let stats = queue_thread.push(pkt, enqueue_age);
|
let stats = queue_thread.push(pkt, enqueue_age);
|
||||||
if stats.dropped_queue_full > 0 {
|
if stats.dropped_queue_full > 0 {
|
||||||
telemetry_thread.record_queue_full_drop(stats.dropped_queue_full);
|
telemetry_thread.record_queue_full_drop(stats.dropped_queue_full);
|
||||||
@ -275,7 +275,7 @@ impl LesavkaClientApp {
|
|||||||
queue_depth_u32(next.queue_depth),
|
queue_depth_u32(next.queue_depth),
|
||||||
duration_ms(next.delivery_age),
|
duration_ms(next.delivery_age),
|
||||||
);
|
);
|
||||||
attach_video_timing_metadata(
|
attach_video_queue_metadata(
|
||||||
&mut packet,
|
&mut packet,
|
||||||
next.queue_depth,
|
next.queue_depth,
|
||||||
next.delivery_age,
|
next.delivery_age,
|
||||||
@ -361,7 +361,7 @@ impl LesavkaClientApp {
|
|||||||
telemetry.record_enabled(true);
|
telemetry.record_enabled(true);
|
||||||
tracing::info!("📸 webcam uplink resumed");
|
tracing::info!("📸 webcam uplink resumed");
|
||||||
}
|
}
|
||||||
let Some(pkt) = cam.pull() else {
|
let Some(mut pkt) = cam.pull() else {
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@ -372,7 +372,7 @@ impl LesavkaClientApp {
|
|||||||
tracing::trace!("📸 cli frame#{n} {} B", pkt.data.len());
|
tracing::trace!("📸 cli frame#{n} {} B", pkt.data.len());
|
||||||
}
|
}
|
||||||
tracing::trace!("📸⬆️ sent webcam AU pts={} {} B", pkt.pts, pkt.data.len());
|
tracing::trace!("📸⬆️ sent webcam AU pts={} {} B", pkt.pts, pkt.data.len());
|
||||||
let enqueue_age = crate::live_capture_clock::packet_age(pkt.pts);
|
let enqueue_age = stamp_video_timing_metadata_at_enqueue(&mut pkt);
|
||||||
let stats = queue.push(pkt, enqueue_age);
|
let stats = queue.push(pkt, enqueue_age);
|
||||||
if stats.dropped_queue_full > 0 {
|
if stats.dropped_queue_full > 0 {
|
||||||
telemetry.record_queue_full_drop(stats.dropped_queue_full);
|
telemetry.record_queue_full_drop(stats.dropped_queue_full);
|
||||||
@ -501,38 +501,54 @@ fn duration_ms_u32(duration: Duration) -> u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn shared_capture_window_from_delivery_age(delivery_age: Duration) -> (u64, u64) {
|
fn age_between_capture_and_enqueue(capture_pts_us: u64, enqueue_pts_us: u64) -> Duration {
|
||||||
let send_pts_us = crate::live_capture_clock::capture_pts_us();
|
Duration::from_micros(enqueue_pts_us.saturating_sub(capture_pts_us))
|
||||||
let age_us = delivery_age.as_micros().min(u128::from(u64::MAX)) as u64;
|
|
||||||
(send_pts_us.saturating_sub(age_us), send_pts_us)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn attach_audio_timing_metadata(
|
fn stamp_audio_timing_metadata_at_enqueue(packet: &mut AudioPacket) -> Duration {
|
||||||
|
static AUDIO_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
let enqueue_pts_us = crate::live_capture_clock::capture_pts_us();
|
||||||
|
let capture_pts_us = packet.pts.min(enqueue_pts_us);
|
||||||
|
packet.seq = AUDIO_SEQUENCE.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
||||||
|
packet.client_capture_pts_us = capture_pts_us;
|
||||||
|
packet.client_send_pts_us = enqueue_pts_us;
|
||||||
|
age_between_capture_and_enqueue(capture_pts_us, enqueue_pts_us)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn stamp_video_timing_metadata_at_enqueue(packet: &mut VideoPacket) -> Duration {
|
||||||
|
static VIDEO_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
let enqueue_pts_us = crate::live_capture_clock::capture_pts_us();
|
||||||
|
let capture_pts_us = packet.pts.min(enqueue_pts_us);
|
||||||
|
packet.seq = VIDEO_SEQUENCE.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
||||||
|
packet.client_capture_pts_us = capture_pts_us;
|
||||||
|
packet.client_send_pts_us = enqueue_pts_us;
|
||||||
|
age_between_capture_and_enqueue(capture_pts_us, enqueue_pts_us)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn attach_audio_queue_metadata(
|
||||||
packet: &mut AudioPacket,
|
packet: &mut AudioPacket,
|
||||||
queue_depth: usize,
|
queue_depth: usize,
|
||||||
delivery_age: Duration,
|
delivery_age: Duration,
|
||||||
) {
|
) {
|
||||||
static AUDIO_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
if packet.seq == 0 {
|
||||||
packet.seq = AUDIO_SEQUENCE.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
let _ = stamp_audio_timing_metadata_at_enqueue(packet);
|
||||||
let (capture_pts_us, send_pts_us) = shared_capture_window_from_delivery_age(delivery_age);
|
}
|
||||||
packet.client_capture_pts_us = capture_pts_us;
|
|
||||||
packet.client_send_pts_us = send_pts_us;
|
|
||||||
packet.client_queue_depth = queue_depth_u32(queue_depth);
|
packet.client_queue_depth = queue_depth_u32(queue_depth);
|
||||||
packet.client_queue_age_ms = duration_ms_u32(delivery_age);
|
packet.client_queue_age_ms = duration_ms_u32(delivery_age);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn attach_video_timing_metadata(
|
fn attach_video_queue_metadata(
|
||||||
packet: &mut VideoPacket,
|
packet: &mut VideoPacket,
|
||||||
queue_depth: usize,
|
queue_depth: usize,
|
||||||
delivery_age: Duration,
|
delivery_age: Duration,
|
||||||
) {
|
) {
|
||||||
static VIDEO_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
if packet.seq == 0 {
|
||||||
packet.seq = VIDEO_SEQUENCE.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
let _ = stamp_video_timing_metadata_at_enqueue(packet);
|
||||||
let (capture_pts_us, send_pts_us) = shared_capture_window_from_delivery_age(delivery_age);
|
}
|
||||||
packet.client_capture_pts_us = capture_pts_us;
|
|
||||||
packet.client_send_pts_us = send_pts_us;
|
|
||||||
packet.client_queue_depth = queue_depth_u32(queue_depth);
|
packet.client_queue_depth = queue_depth_u32(queue_depth);
|
||||||
packet.client_queue_age_ms = duration_ms_u32(delivery_age);
|
packet.client_queue_age_ms = duration_ms_u32(delivery_age);
|
||||||
}
|
}
|
||||||
@ -631,48 +647,70 @@ mod uplink_timing_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn audio_timing_metadata_uses_shared_clock_window_instead_of_packet_pts_domain() {
|
fn audio_timing_metadata_is_stamped_before_async_queue_pop() {
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
let packet_pts_us = crate::live_capture_clock::capture_pts_us().saturating_sub(2_000);
|
||||||
let mut packet = AudioPacket {
|
let mut packet = AudioPacket {
|
||||||
pts: 9_999_999,
|
pts: packet_pts_us,
|
||||||
..AudioPacket::default()
|
..AudioPacket::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
attach_audio_timing_metadata(&mut packet, 3, Duration::from_millis(2));
|
let enqueue_age = stamp_audio_timing_metadata_at_enqueue(&mut packet);
|
||||||
|
let capture_pts_us = packet.client_capture_pts_us;
|
||||||
|
let send_pts_us = packet.client_send_pts_us;
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
attach_audio_queue_metadata(
|
||||||
|
&mut packet,
|
||||||
|
3,
|
||||||
|
enqueue_age.saturating_add(Duration::from_millis(5)),
|
||||||
|
);
|
||||||
|
|
||||||
assert!(packet.seq > 0);
|
assert!(packet.seq > 0);
|
||||||
assert_eq!(packet.client_queue_depth, 3);
|
assert_eq!(packet.client_queue_depth, 3);
|
||||||
assert_eq!(packet.client_queue_age_ms, 2);
|
assert!(packet.client_queue_age_ms >= 5);
|
||||||
|
assert_eq!(packet.client_capture_pts_us, capture_pts_us);
|
||||||
|
assert_eq!(packet.client_send_pts_us, send_pts_us);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
||||||
"send must be on or after the shared-clock capture estimate"
|
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us - packet.client_capture_pts_us <= 2_000,
|
packet.client_send_pts_us - packet.client_capture_pts_us <= 3_000,
|
||||||
"delivery age, not packet PTS domain, should define the timing window"
|
"capture-to-enqueue age, not async pop delay, should define the timing window"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_timing_metadata_uses_shared_clock_window_instead_of_packet_pts_domain() {
|
fn video_timing_metadata_is_stamped_before_async_queue_pop() {
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
let packet_pts_us = crate::live_capture_clock::capture_pts_us().saturating_sub(3_000);
|
||||||
let mut packet = VideoPacket {
|
let mut packet = VideoPacket {
|
||||||
pts: 9_999_999,
|
pts: packet_pts_us,
|
||||||
..VideoPacket::default()
|
..VideoPacket::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
attach_video_timing_metadata(&mut packet, 4, Duration::from_millis(3));
|
let enqueue_age = stamp_video_timing_metadata_at_enqueue(&mut packet);
|
||||||
|
let capture_pts_us = packet.client_capture_pts_us;
|
||||||
|
let send_pts_us = packet.client_send_pts_us;
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
attach_video_queue_metadata(
|
||||||
|
&mut packet,
|
||||||
|
4,
|
||||||
|
enqueue_age.saturating_add(Duration::from_millis(5)),
|
||||||
|
);
|
||||||
|
|
||||||
assert!(packet.seq > 0);
|
assert!(packet.seq > 0);
|
||||||
assert_eq!(packet.client_queue_depth, 4);
|
assert_eq!(packet.client_queue_depth, 4);
|
||||||
assert_eq!(packet.client_queue_age_ms, 3);
|
assert!(packet.client_queue_age_ms >= 5);
|
||||||
|
assert_eq!(packet.client_capture_pts_us, capture_pts_us);
|
||||||
|
assert_eq!(packet.client_send_pts_us, send_pts_us);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
||||||
"send must be on or after the shared-clock capture estimate"
|
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us - packet.client_capture_pts_us <= 3_000,
|
packet.client_send_pts_us - packet.client_capture_pts_us <= 4_000,
|
||||||
"delivery age, not packet PTS domain, should define the timing window"
|
"capture-to-enqueue age, not async pop delay, should define the timing window"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ pub fn capture_pts_us() -> u64 {
|
|||||||
/// Why: upstream freshness telemetry should use the same shared live clock as
|
/// Why: upstream freshness telemetry should use the same shared live clock as
|
||||||
/// packet timestamps so queue-age calculations stay honest.
|
/// packet timestamps so queue-age calculations stay honest.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn packet_age(pts_us: u64) -> Duration {
|
pub fn packet_age(pts_us: u64) -> Duration {
|
||||||
Duration::from_micros(capture_pts_us().saturating_sub(pts_us))
|
Duration::from_micros(capture_pts_us().saturating_sub(pts_us))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,9 @@ message VideoPacket {
|
|||||||
uint32 server_queue_peak = 10;
|
uint32 server_queue_peak = 10;
|
||||||
string server_encoder_label = 11;
|
string server_encoder_label = 11;
|
||||||
uint32 server_process_cpu_tenths = 12;
|
uint32 server_process_cpu_tenths = 12;
|
||||||
|
// Shared client media-clock timestamp for the packet's capture estimate.
|
||||||
uint64 client_capture_pts_us = 13;
|
uint64 client_capture_pts_us = 13;
|
||||||
|
// Shared client media-clock timestamp when the packet entered the uplink queue.
|
||||||
uint64 client_send_pts_us = 14;
|
uint64 client_send_pts_us = 14;
|
||||||
uint32 client_queue_depth = 15;
|
uint32 client_queue_depth = 15;
|
||||||
uint32 client_queue_age_ms = 16;
|
uint32 client_queue_age_ms = 16;
|
||||||
@ -36,7 +38,9 @@ message AudioPacket {
|
|||||||
uint64 pts = 2;
|
uint64 pts = 2;
|
||||||
bytes data = 3;
|
bytes data = 3;
|
||||||
uint64 seq = 4;
|
uint64 seq = 4;
|
||||||
|
// Shared client media-clock timestamp for the packet's capture estimate.
|
||||||
uint64 client_capture_pts_us = 5;
|
uint64 client_capture_pts_us = 5;
|
||||||
|
// Shared client media-clock timestamp when the packet entered the uplink queue.
|
||||||
uint64 client_send_pts_us = 6;
|
uint64 client_send_pts_us = 6;
|
||||||
uint32 client_queue_depth = 7;
|
uint32 client_queue_depth = 7;
|
||||||
uint32 client_queue_age_ms = 8;
|
uint32 client_queue_age_ms = 8;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.17.28"
|
version = "0.17.29"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -137,10 +137,22 @@ impl UpstreamMediaRuntime {
|
|||||||
received_at: Instant::now(),
|
received_at: Instant::now(),
|
||||||
};
|
};
|
||||||
match kind {
|
match kind {
|
||||||
UpstreamMediaKind::Camera => state.latest_camera_timing = Some(sample),
|
UpstreamMediaKind::Camera => {
|
||||||
UpstreamMediaKind::Microphone => state.latest_microphone_timing = Some(sample),
|
state.latest_camera_timing = Some(sample);
|
||||||
|
push_timing_sample(&mut state.recent_camera_timing, sample);
|
||||||
|
state
|
||||||
|
.camera_client_queue_age_window_ms
|
||||||
|
.push(f64::from(timing.queue_age_ms));
|
||||||
}
|
}
|
||||||
record_client_timing_windows(&mut state);
|
UpstreamMediaKind::Microphone => {
|
||||||
|
state.latest_microphone_timing = Some(sample);
|
||||||
|
push_timing_sample(&mut state.recent_microphone_timing, sample);
|
||||||
|
state
|
||||||
|
.microphone_client_queue_age_window_ms
|
||||||
|
.push(f64::from(timing.queue_age_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record_client_timing_windows(&mut state, kind, sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark one video frame as actually handed to the UVC/HDMI sink.
|
/// Mark one video frame as actually handed to the UVC/HDMI sink.
|
||||||
@ -186,23 +198,9 @@ impl UpstreamMediaRuntime {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let client_capture_skew_ms = skew_ms_from_samples(
|
let client_capture_skew_ms = state.latest_paired_client_capture_skew_ms;
|
||||||
state.latest_camera_timing,
|
let client_send_skew_ms = state.latest_paired_client_send_skew_ms;
|
||||||
state.latest_microphone_timing,
|
let server_receive_skew_ms = state.latest_paired_server_receive_skew_ms;
|
||||||
|sample| sample.capture_pts_us,
|
|
||||||
);
|
|
||||||
let client_send_skew_ms = skew_ms_from_samples(
|
|
||||||
state.latest_camera_timing,
|
|
||||||
state.latest_microphone_timing,
|
|
||||||
|sample| sample.send_pts_us,
|
|
||||||
);
|
|
||||||
let server_receive_skew_ms =
|
|
||||||
match (state.latest_camera_timing, state.latest_microphone_timing) {
|
|
||||||
(Some(camera), Some(microphone)) => Some(
|
|
||||||
instant_delta_us(camera.received_at, microphone.received_at) as f64 / 1000.0,
|
|
||||||
),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
UpstreamPlannerSnapshot {
|
UpstreamPlannerSnapshot {
|
||||||
session_id: state.session_id,
|
session_id: state.session_id,
|
||||||
phase: state.phase.as_str(),
|
phase: state.phase.as_str(),
|
||||||
@ -710,19 +708,6 @@ fn us_to_ms(value: u64) -> f64 {
|
|||||||
value as f64 / 1000.0
|
value as f64 / 1000.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skew_ms_from_samples(
|
|
||||||
camera: Option<state::UpstreamTimingSample>,
|
|
||||||
microphone: Option<state::UpstreamTimingSample>,
|
|
||||||
value: impl Fn(state::UpstreamTimingSample) -> u64,
|
|
||||||
) -> Option<f64> {
|
|
||||||
match (camera, microphone) {
|
|
||||||
(Some(camera), Some(microphone)) => {
|
|
||||||
Some((value(camera) as i128 - value(microphone) as i128) as f64 / 1000.0)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instant_delta_us(left: Instant, right: Instant) -> i128 {
|
fn instant_delta_us(left: Instant, right: Instant) -> i128 {
|
||||||
if left >= right {
|
if left >= right {
|
||||||
left.saturating_duration_since(right).as_micros() as i128
|
left.saturating_duration_since(right).as_micros() as i128
|
||||||
@ -731,27 +716,72 @@ fn instant_delta_us(left: Instant, right: Instant) -> i128 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_client_timing_windows(state: &mut UpstreamClockState) {
|
const CLIENT_TIMING_PAIR_MAX_SEND_DELTA_US: u64 = 250_000;
|
||||||
let (Some(camera), Some(microphone)) =
|
|
||||||
(state.latest_camera_timing, state.latest_microphone_timing)
|
fn push_timing_sample(
|
||||||
else {
|
samples: &mut std::collections::VecDeque<state::UpstreamTimingSample>,
|
||||||
|
sample: state::UpstreamTimingSample,
|
||||||
|
) {
|
||||||
|
if samples.len() >= state::TIMING_WINDOW_CAPACITY {
|
||||||
|
samples.pop_front();
|
||||||
|
}
|
||||||
|
samples.push_back(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn abs_delta_us(left: u64, right: u64) -> u64 {
|
||||||
|
left.max(right) - left.min(right)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearest_timing_sample_by_send(
|
||||||
|
samples: &std::collections::VecDeque<state::UpstreamTimingSample>,
|
||||||
|
send_pts_us: u64,
|
||||||
|
) -> Option<state::UpstreamTimingSample> {
|
||||||
|
samples
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.min_by_key(|sample| abs_delta_us(sample.send_pts_us, send_pts_us))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_client_timing_windows(
|
||||||
|
state: &mut UpstreamClockState,
|
||||||
|
kind: UpstreamMediaKind,
|
||||||
|
sample: state::UpstreamTimingSample,
|
||||||
|
) {
|
||||||
|
let paired = match kind {
|
||||||
|
UpstreamMediaKind::Camera => {
|
||||||
|
nearest_timing_sample_by_send(&state.recent_microphone_timing, sample.send_pts_us)
|
||||||
|
.map(|microphone| (sample, microphone))
|
||||||
|
}
|
||||||
|
UpstreamMediaKind::Microphone => {
|
||||||
|
nearest_timing_sample_by_send(&state.recent_camera_timing, sample.send_pts_us)
|
||||||
|
.map(|camera| (camera, sample))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some((camera, microphone)) = paired else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if abs_delta_us(camera.send_pts_us, microphone.send_pts_us)
|
||||||
|
> CLIENT_TIMING_PAIR_MAX_SEND_DELTA_US
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let client_capture_skew_ms =
|
||||||
|
(camera.capture_pts_us as i128 - microphone.capture_pts_us as i128) as f64 / 1000.0;
|
||||||
|
let client_send_skew_ms =
|
||||||
|
(camera.send_pts_us as i128 - microphone.send_pts_us as i128) as f64 / 1000.0;
|
||||||
|
let server_receive_skew_ms =
|
||||||
|
instant_delta_us(camera.received_at, microphone.received_at) as f64 / 1000.0;
|
||||||
|
|
||||||
|
state.latest_paired_client_capture_skew_ms = Some(client_capture_skew_ms);
|
||||||
|
state.latest_paired_client_send_skew_ms = Some(client_send_skew_ms);
|
||||||
|
state.latest_paired_server_receive_skew_ms = Some(server_receive_skew_ms);
|
||||||
state
|
state
|
||||||
.client_capture_skew_window_ms
|
.client_capture_skew_window_ms
|
||||||
.push((camera.capture_pts_us as i128 - microphone.capture_pts_us as i128) as f64 / 1000.0);
|
.push(client_capture_skew_ms);
|
||||||
state
|
state.client_send_skew_window_ms.push(client_send_skew_ms);
|
||||||
.client_send_skew_window_ms
|
|
||||||
.push((camera.send_pts_us as i128 - microphone.send_pts_us as i128) as f64 / 1000.0);
|
|
||||||
state
|
state
|
||||||
.server_receive_skew_window_ms
|
.server_receive_skew_window_ms
|
||||||
.push(instant_delta_us(camera.received_at, microphone.received_at) as f64 / 1000.0);
|
.push(server_receive_skew_ms);
|
||||||
state
|
|
||||||
.camera_client_queue_age_window_ms
|
|
||||||
.push(f64::from(camera.queue_age_ms));
|
|
||||||
state
|
|
||||||
.microphone_client_queue_age_window_ms
|
|
||||||
.push(f64::from(microphone.queue_age_ms));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_presentation_sample(
|
fn record_presentation_sample(
|
||||||
|
|||||||
@ -168,6 +168,11 @@ fn reset_timing_anchors(state: &mut UpstreamClockState) {
|
|||||||
state.video_freezes = 0;
|
state.video_freezes = 0;
|
||||||
state.latest_camera_timing = None;
|
state.latest_camera_timing = None;
|
||||||
state.latest_microphone_timing = None;
|
state.latest_microphone_timing = None;
|
||||||
|
state.recent_camera_timing = Default::default();
|
||||||
|
state.recent_microphone_timing = Default::default();
|
||||||
|
state.latest_paired_client_capture_skew_ms = None;
|
||||||
|
state.latest_paired_client_send_skew_ms = None;
|
||||||
|
state.latest_paired_server_receive_skew_ms = None;
|
||||||
state.latest_camera_presentation = None;
|
state.latest_camera_presentation = None;
|
||||||
state.latest_microphone_presentation = None;
|
state.latest_microphone_presentation = None;
|
||||||
state.client_capture_skew_window_ms = Default::default();
|
state.client_capture_skew_window_ms = Default::default();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
const TIMING_WINDOW_CAPACITY: usize = 240;
|
pub(super) const TIMING_WINDOW_CAPACITY: usize = 240;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(super) struct UpstreamTimingSample {
|
pub(super) struct UpstreamTimingSample {
|
||||||
@ -105,6 +105,11 @@ pub(super) struct UpstreamClockState {
|
|||||||
pub last_reason: String,
|
pub last_reason: String,
|
||||||
pub latest_camera_timing: Option<UpstreamTimingSample>,
|
pub latest_camera_timing: Option<UpstreamTimingSample>,
|
||||||
pub latest_microphone_timing: Option<UpstreamTimingSample>,
|
pub latest_microphone_timing: Option<UpstreamTimingSample>,
|
||||||
|
pub recent_camera_timing: VecDeque<UpstreamTimingSample>,
|
||||||
|
pub recent_microphone_timing: VecDeque<UpstreamTimingSample>,
|
||||||
|
pub latest_paired_client_capture_skew_ms: Option<f64>,
|
||||||
|
pub latest_paired_client_send_skew_ms: Option<f64>,
|
||||||
|
pub latest_paired_server_receive_skew_ms: Option<f64>,
|
||||||
pub latest_camera_presentation: Option<UpstreamPresentationSample>,
|
pub latest_camera_presentation: Option<UpstreamPresentationSample>,
|
||||||
pub latest_microphone_presentation: Option<UpstreamPresentationSample>,
|
pub latest_microphone_presentation: Option<UpstreamPresentationSample>,
|
||||||
pub client_capture_skew_window_ms: UpstreamScalarWindow,
|
pub client_capture_skew_window_ms: UpstreamScalarWindow,
|
||||||
|
|||||||
@ -592,6 +592,47 @@ fn planner_snapshot_tracks_client_timing_sidecar_metrics() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial(upstream_media_runtime)]
|
||||||
|
fn planner_pairs_client_timing_by_nearby_send_time_not_latest_packet() {
|
||||||
|
let runtime = runtime_without_offsets();
|
||||||
|
|
||||||
|
runtime.record_client_timing(
|
||||||
|
super::UpstreamMediaKind::Camera,
|
||||||
|
super::UpstreamClientTiming {
|
||||||
|
capture_pts_us: 1_000_000,
|
||||||
|
send_pts_us: 1_000_000,
|
||||||
|
queue_depth: 1,
|
||||||
|
queue_age_ms: 5,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
runtime.record_client_timing(
|
||||||
|
super::UpstreamMediaKind::Microphone,
|
||||||
|
super::UpstreamClientTiming {
|
||||||
|
capture_pts_us: 1_010_000,
|
||||||
|
send_pts_us: 1_010_000,
|
||||||
|
queue_depth: 1,
|
||||||
|
queue_age_ms: 5,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
runtime.record_client_timing(
|
||||||
|
super::UpstreamMediaKind::Microphone,
|
||||||
|
super::UpstreamClientTiming {
|
||||||
|
capture_pts_us: 2_500_000,
|
||||||
|
send_pts_us: 2_500_000,
|
||||||
|
queue_depth: 1,
|
||||||
|
queue_age_ms: 5,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = runtime.snapshot();
|
||||||
|
|
||||||
|
assert_eq!(snapshot.client_capture_skew_ms, Some(-10.0));
|
||||||
|
assert_eq!(snapshot.client_send_skew_ms, Some(-10.0));
|
||||||
|
assert_eq!(snapshot.client_capture_abs_skew_p95_ms, Some(10.0));
|
||||||
|
assert_eq!(snapshot.client_send_abs_skew_p95_ms, Some(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial(upstream_media_runtime)]
|
#[serial(upstream_media_runtime)]
|
||||||
fn default_runtime_covers_video_map_play_path() {
|
fn default_runtime_covers_video_map_play_path() {
|
||||||
|
|||||||
@ -15,11 +15,13 @@ pub enum UpstreamMediaKind {
|
|||||||
pub struct UpstreamClientTiming {
|
pub struct UpstreamClientTiming {
|
||||||
/// Packet capture timestamp on the shared client media clock.
|
/// Packet capture timestamp on the shared client media clock.
|
||||||
pub capture_pts_us: u64,
|
pub capture_pts_us: u64,
|
||||||
/// Client media-clock timestamp when the packet left the uplink queue.
|
/// Client media-clock timestamp when the packet entered the async uplink
|
||||||
|
/// queue. Stamping at enqueue prevents stream scheduling from posing as
|
||||||
|
/// capture skew.
|
||||||
pub send_pts_us: u64,
|
pub send_pts_us: u64,
|
||||||
/// Uplink queue depth observed as the packet was sent.
|
/// Uplink queue depth observed as the packet left the async queue.
|
||||||
pub queue_depth: u32,
|
pub queue_depth: u32,
|
||||||
/// Packet age observed as the packet was sent.
|
/// Packet age observed as the packet left the async queue.
|
||||||
pub queue_age_ms: u32,
|
pub queue_age_ms: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user