sync: trust coded probe pairs for verdicts

This commit is contained in:
Brad Stein 2026-05-01 14:59:30 -03:00
parent e73e7f0a0f
commit aeb85ca998
9 changed files with 303 additions and 32 deletions

View File

@ -122,6 +122,8 @@ Context: the mirrored browser probe finally reproduced the real failure class on
- [x] Preserve raw activity-start timing before cadence cleanup in coded reports.
- [x] Merge short audio envelope dropouts inside one coded pulse so a single tone burst cannot become two fake events.
- [x] Add diagnostic coded-pair correlation so stable large skew reports as measured failure instead of `not enough pairs`.
- [x] Make coded mirrored verdicts/calibration use matched coded pulses as authority; raw activity-start deltas are reported separately unless they agree with the coded pairs.
- [x] Print unpaired video/audio onsets in the human report so missed coded pulses are visible during probe triage.
- [ ] Keep the mirrored browser probe as the release/blocking upstream A/V gate.
- [ ] Keep the old raw-device probe as a lower-level diagnostic only.
@ -168,5 +170,6 @@ Context: the mirrored browser probe finally reproduced the real failure class on
- 0.16.19 mirrored browser probe did not move the measured skew: p95 `885.7 ms`, median `-788.4 ms`, activity start `-659.1 ms`, drift `-81.2 ms`. SSH inspection showed Theia was on commit `c348597`, but `/etc/lesavka/server.env` still contained `LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000`; the new `+720ms` baseline was not actually installed. Patch the installer to migrate leaked legacy ambient `-45000` to `+720000` unless `LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US` explicitly asks for the legacy value.
- 0.16.20 installed the `+720ms` offset (`/etc/lesavka/server.env` had `LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000`), but the mirrored browser capture contained no recognizable color pulses. Theia server logs showed repeated `upstream video frame dropped because the audio master never caught up inside the pairing window`; UVC was effectively starved by the positive audio delay instead of flowing delayed-but-fresh frames.
- 0.16.21 makes that wait offset-aware and adds a regression test proving a configured positive audio delay does not freeze UVC video while UAC sleeps before playout.
- Replaying the 0.16.21 artifact after 0.16.22 analyzer hardening changes the verdict from false `catastrophic_failure` to `gross_failure`: p95 `273.8 ms`, median `-188.4 ms`, 7 paired coded pulses. The raw activity-start delta (`-3620.7 ms`) is still printed, but it is ignored for verdict/calibration because it disagrees with coded pairs by `3432.3 ms`; unpaired video/audio onsets are printed for triage.
- [ ] Re-run the mirrored browser probe after the pre-start false-positive fix.
- [ ] Run Google Meet manual validation.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.16.21"
version = "0.16.22"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.16.21"
version = "0.16.22"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.16.21"
version = "0.16.22"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.16.21"
version = "0.16.22"
edition = "2024"
[dependencies]

View File

@ -157,11 +157,30 @@ fn format_human_report(
calibration: &SyncCalibrationRecommendation,
verdict: &SyncAnalysisVerdict,
) -> String {
let first_paired_video = report
.paired_events
.first()
.map(|event| event.video_time_s)
.unwrap_or(0.0);
let first_paired_audio = report
.paired_events
.first()
.map(|event| event.audio_time_s)
.unwrap_or(0.0);
let unpaired_video = format_onset_list(&unpaired_video_onsets(report));
let unpaired_audio = format_onset_list(&unpaired_audio_onsets(report));
let raw_activity_handling =
if report.coded_events && !report.raw_activity_start_confirms_pairs() {
"reported only; ignored for verdict/calibration because it disagrees with coded pairs"
} else {
"used as verdict evidence"
};
format!(
"\
A/V sync report for {capture}
- verdict: {status} ({passed})
- verdict reason: {reason}
- evidence mode: {evidence_mode}
- p95 abs skew: {p95:.1} ms
- video onsets: {video_events}
- audio onsets: {audio_events}
@ -169,7 +188,11 @@ A/V sync report for {capture}
- activity start delta: {activity_start_delta:+.1} ms (audio after video is positive)
- raw first video activity: {raw_video:.3} s
- raw first audio activity: {raw_audio:.3} s
- raw-vs-paired disagreement: {raw_pair_disagreement:.1} ms
- raw activity handling: {raw_activity_handling}
- paired window first video/audio: {paired_video:.3} s / {paired_audio:.3} s
- unpaired video onsets: {unpaired_video}
- unpaired audio onsets: {unpaired_audio}
- first skew: {first_skew:+.1} ms (audio after video is positive)
- last skew: {last_skew:+.1} ms
- mean skew: {mean_skew:+.1} ms
@ -185,6 +208,11 @@ A/V sync report for {capture}
status = verdict.status,
passed = if verdict.passed { "pass" } else { "fail" },
reason = verdict.reason,
evidence_mode = if report.coded_events {
"coded pulses"
} else {
"cadence/brightness pulses"
},
p95 = verdict.p95_abs_skew_ms,
video_events = report.video_event_count,
audio_events = report.audio_event_count,
@ -192,8 +220,12 @@ A/V sync report for {capture}
activity_start_delta = report.activity_start_delta_ms,
raw_video = report.raw_first_video_activity_s,
raw_audio = report.raw_first_audio_activity_s,
paired_video = report.video_onsets_s.first().copied().unwrap_or(0.0),
paired_audio = report.audio_onsets_s.first().copied().unwrap_or(0.0),
raw_pair_disagreement = report.activity_start_pair_disagreement_ms().abs(),
raw_activity_handling = raw_activity_handling,
paired_video = first_paired_video,
paired_audio = first_paired_audio,
unpaired_video = unpaired_video,
unpaired_audio = unpaired_audio,
first_skew = report.first_skew_ms,
last_skew = report.last_skew_ms,
mean_skew = report.mean_skew_ms,
@ -207,6 +239,61 @@ A/V sync report for {capture}
)
}
#[cfg(not(coverage))]
fn unpaired_video_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
unpaired_onsets(
&report.video_onsets_s,
&report
.paired_events
.iter()
.map(|event| event.video_time_s)
.collect::<Vec<_>>(),
)
}
#[cfg(not(coverage))]
fn unpaired_audio_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
unpaired_onsets(
&report.audio_onsets_s,
&report
.paired_events
.iter()
.map(|event| event.audio_time_s)
.collect::<Vec<_>>(),
)
}
#[cfg(not(coverage))]
fn unpaired_onsets(all_onsets: &[f64], paired_onsets: &[f64]) -> Vec<f64> {
const SAME_ONSET_EPSILON_S: f64 = 0.000_001;
all_onsets
.iter()
.copied()
.filter(|onset| {
!paired_onsets
.iter()
.any(|paired| (paired - onset).abs() <= SAME_ONSET_EPSILON_S)
})
.collect()
}
#[cfg(not(coverage))]
fn format_onset_list(onsets: &[f64]) -> String {
const MAX_PRINTED_ONSETS: usize = 12;
if onsets.is_empty() {
return "none".to_string();
}
let mut formatted = onsets
.iter()
.take(MAX_PRINTED_ONSETS)
.map(|onset| format!("{onset:.3}s"))
.collect::<Vec<_>>();
if onsets.len() > MAX_PRINTED_ONSETS {
formatted.push(format!("...+{}", onsets.len() - MAX_PRINTED_ONSETS));
}
formatted.join(", ")
}
#[cfg(not(coverage))]
fn write_report_dir(
report_dir: &std::path::Path,
@ -244,6 +331,9 @@ fn main() {}
#[cfg(test)]
mod tests {
use super::parse_args;
use lesavka_client::sync_probe::analyze::{
SyncAnalysisReport, SyncAnalysisVerdict, SyncCalibrationRecommendation, SyncEventPair,
};
#[test]
fn parse_args_accepts_capture_path_and_json_flag() {
@ -289,4 +379,63 @@ mod tests {
fn coverage_main_stub_is_non_panicking() {
let _ = super::main();
}
#[test]
fn human_report_explains_coded_raw_activity_and_unpaired_onsets() {
let report = SyncAnalysisReport {
video_event_count: 3,
audio_event_count: 3,
paired_event_count: 1,
coded_events: true,
activity_start_delta_ms: -3_620.7,
raw_first_video_activity_s: 9.361,
raw_first_audio_activity_s: 5.740,
first_skew_ms: -188.4,
last_skew_ms: -188.4,
mean_skew_ms: -188.4,
median_skew_ms: -188.4,
max_abs_skew_ms: 188.4,
drift_ms: 0.0,
skews_ms: vec![-188.4],
video_onsets_s: vec![9.461, 11.420, 13.367],
audio_onsets_s: vec![9.135, 11.146, 13.135],
paired_events: vec![SyncEventPair {
event_id: 0,
video_time_s: 11.420,
audio_time_s: 11.146,
skew_ms: -188.4,
confidence: 0.62,
}],
};
let calibration = SyncCalibrationRecommendation {
ready: false,
recommended_audio_offset_adjust_us: 0,
recommended_video_offset_adjust_us: 0,
note: "need more pairs".to_string(),
};
let verdict = SyncAnalysisVerdict {
status: "gross_failure".to_string(),
passed: false,
p95_abs_skew_ms: 188.4,
max_abs_skew_ms: 188.4,
preferred_p95_abs_skew_ms: 35.0,
acceptable_p95_abs_skew_ms: 80.0,
gross_failure_p95_abs_skew_ms: 250.0,
catastrophic_max_abs_skew_ms: 1_000.0,
reason: "coded pulse skew is too high".to_string(),
};
let text = super::format_human_report(
std::path::Path::new("/tmp/capture.webm"),
&report,
&calibration,
&verdict,
);
assert!(text.contains("- evidence mode: coded pulses"));
assert!(text.contains("raw activity handling: reported only"));
assert!(text.contains("- paired window first video/audio: 11.420 s / 11.146 s"));
assert!(text.contains("- unpaired video onsets: 9.461s, 13.367s"));
assert!(text.contains("- unpaired audio onsets: 9.135s, 13.135s"));
}
}

View File

@ -75,6 +75,7 @@ pub(super) fn correlate_onsets(
Ok(sync_report_from_pairs(
common_window.filter_onsets(video_onsets_s),
common_window.filter_onsets(audio_onsets_s),
false,
activity_start_delta_ms,
raw_first_video_activity_s,
raw_first_audio_activity_s,
@ -186,6 +187,7 @@ pub(crate) fn correlate_segments(
Ok(sync_report_from_pairs(
video_onsets_s,
audio_onsets_s,
false,
activity_start_delta_ms,
raw_first_video_activity_s,
raw_first_audio_activity_s,
@ -312,6 +314,7 @@ pub(crate) fn correlate_coded_segments(
return Ok(sync_report_from_pairs(
&full_video_onsets_s,
&full_audio_onsets_s,
true,
activity_start_delta_ms,
raw_first_video_activity_s,
raw_first_audio_activity_s,
@ -346,6 +349,7 @@ pub(crate) fn correlate_coded_segments(
Ok(sync_report_from_pairs(
&video_onsets_s,
&audio_onsets_s,
true,
activity_start_delta_ms,
raw_first_video_activity_s,
raw_first_audio_activity_s,
@ -830,6 +834,7 @@ pub(super) fn shortest_wrapped_difference(delta_s: f64, pulse_period_s: f64) ->
fn sync_report_from_pairs(
video_onsets_s: &[f64],
audio_onsets_s: &[f64],
coded_events: bool,
activity_start_delta_ms: f64,
raw_first_video_activity_s: f64,
raw_first_audio_activity_s: f64,
@ -866,6 +871,7 @@ fn sync_report_from_pairs(
video_event_count: video_onsets_s.len(),
audio_event_count: audio_onsets_s.len(),
paired_event_count: skews_ms.len(),
coded_events,
activity_start_delta_ms,
raw_first_video_activity_s,
raw_first_audio_activity_s,

View File

@ -9,6 +9,7 @@ const CALIBRATION_MIN_PAIRED_EVENTS: usize = 8;
const CALIBRATION_MAX_DRIFT_MS: f64 = 40.0;
const CALIBRATION_MAX_START_MEDIAN_DISAGREEMENT_MS: f64 = 250.0;
const CALIBRATION_SETTLED_SKEW_MS: f64 = 5.0;
const RAW_ACTIVITY_CONFIRMS_PAIR_MAX_DISAGREEMENT_MS: f64 = 250.0;
const VERDICT_MIN_PAIRED_EVENTS: usize = 3;
const VERDICT_PREFERRED_P95_ABS_SKEW_MS: f64 = 35.0;
const VERDICT_ACCEPTABLE_P95_ABS_SKEW_MS: f64 = 80.0;
@ -20,6 +21,7 @@ pub struct SyncAnalysisReport {
pub video_event_count: usize,
pub audio_event_count: usize,
pub paired_event_count: usize,
pub coded_events: bool,
pub activity_start_delta_ms: f64,
pub raw_first_video_activity_s: f64,
pub raw_first_audio_activity_s: f64,
@ -81,17 +83,6 @@ impl SyncAnalysisReport {
reason: String::new(),
};
if self.activity_start_delta_ms.abs() >= VERDICT_CATASTROPHIC_MAX_ABS_SKEW_MS {
return SyncAnalysisVerdict {
status: "catastrophic_failure".to_string(),
reason: format!(
"audio/video activity starts are separated by {:+.1} ms, at or above the {:.1} ms catastrophic boundary",
self.activity_start_delta_ms, VERDICT_CATASTROPHIC_MAX_ABS_SKEW_MS
),
..base
};
}
if self.paired_event_count < VERDICT_MIN_PAIRED_EVENTS {
return SyncAnalysisVerdict {
status: "insufficient_data".to_string(),
@ -114,6 +105,19 @@ impl SyncAnalysisReport {
};
}
if self.activity_start_delta_ms.abs() >= VERDICT_CATASTROPHIC_MAX_ABS_SKEW_MS
&& self.raw_activity_start_is_verdict_relevant()
{
return SyncAnalysisVerdict {
status: "catastrophic_failure".to_string(),
reason: format!(
"audio/video activity starts are separated by {:+.1} ms, at or above the {:.1} ms catastrophic boundary",
self.activity_start_delta_ms, VERDICT_CATASTROPHIC_MAX_ABS_SKEW_MS
),
..base
};
}
if p95_abs_skew_ms > VERDICT_GROSS_FAILURE_P95_ABS_SKEW_MS {
return SyncAnalysisVerdict {
status: "gross_failure".to_string(),
@ -125,13 +129,14 @@ impl SyncAnalysisReport {
};
}
let raw_activity_note = self.raw_activity_note();
if p95_abs_skew_ms <= VERDICT_PREFERRED_P95_ABS_SKEW_MS {
return SyncAnalysisVerdict {
status: "preferred".to_string(),
passed: true,
reason: format!(
"p95 skew {:.1} ms is inside the preferred {:.1} ms band",
p95_abs_skew_ms, VERDICT_PREFERRED_P95_ABS_SKEW_MS
"p95 skew {:.1} ms is inside the preferred {:.1} ms band{}",
p95_abs_skew_ms, VERDICT_PREFERRED_P95_ABS_SKEW_MS, raw_activity_note
),
..base
};
@ -142,8 +147,8 @@ impl SyncAnalysisReport {
status: "acceptable".to_string(),
passed: true,
reason: format!(
"p95 skew {:.1} ms is inside the acceptable {:.1} ms band",
p95_abs_skew_ms, VERDICT_ACCEPTABLE_P95_ABS_SKEW_MS
"p95 skew {:.1} ms is inside the acceptable {:.1} ms band{}",
p95_abs_skew_ms, VERDICT_ACCEPTABLE_P95_ABS_SKEW_MS, raw_activity_note
),
..base
};
@ -185,8 +190,10 @@ impl SyncAnalysisReport {
};
}
let start_median_disagreement_ms = self.activity_start_delta_ms - self.median_skew_ms;
if start_median_disagreement_ms.abs() > CALIBRATION_MAX_START_MEDIAN_DISAGREEMENT_MS {
let start_median_disagreement_ms = self.activity_start_pair_disagreement_ms();
if !self.coded_events
&& start_median_disagreement_ms.abs() > CALIBRATION_MAX_START_MEDIAN_DISAGREEMENT_MS
{
return SyncCalibrationRecommendation {
ready: false,
recommended_audio_offset_adjust_us: 0,
@ -203,15 +210,16 @@ impl SyncAnalysisReport {
let recommended_audio_offset_adjust_us = (-(self.median_skew_ms * 1_000.0)).round() as i64;
let recommended_video_offset_adjust_us = -recommended_audio_offset_adjust_us;
let raw_activity_note = self.raw_activity_calibration_note();
let note = if self.median_skew_ms.abs() <= CALIBRATION_SETTLED_SKEW_MS {
format!(
"median skew {:.1} ms is already within the settled {:.1} ms band",
self.median_skew_ms, CALIBRATION_SETTLED_SKEW_MS
"median skew {:.1} ms is already within the settled {:.1} ms band{}",
self.median_skew_ms, CALIBRATION_SETTLED_SKEW_MS, raw_activity_note
)
} else {
format!(
"apply the audio offset adjustment to move median skew from {:+.1} ms toward 0.0 ms",
self.median_skew_ms
"apply the audio offset adjustment to move median skew from {:+.1} ms toward 0.0 ms{}",
self.median_skew_ms, raw_activity_note
)
};
@ -222,6 +230,48 @@ impl SyncAnalysisReport {
note,
}
}
#[must_use]
pub fn activity_start_pair_disagreement_ms(&self) -> f64 {
self.activity_start_delta_ms - self.median_skew_ms
}
#[must_use]
pub fn raw_activity_start_confirms_pairs(&self) -> bool {
self.activity_start_pair_disagreement_ms().abs()
<= RAW_ACTIVITY_CONFIRMS_PAIR_MAX_DISAGREEMENT_MS
}
fn raw_activity_start_is_verdict_relevant(&self) -> bool {
!self.coded_events || self.raw_activity_start_confirms_pairs()
}
fn raw_activity_note(&self) -> String {
if !self.coded_events
|| self.activity_start_delta_ms.abs() < VERDICT_CATASTROPHIC_MAX_ABS_SKEW_MS
|| self.raw_activity_start_confirms_pairs()
{
String::new()
} else {
format!(
"; raw activity start delta {:+.1} ms is reported separately because it disagrees with coded pairs by {:.1} ms",
self.activity_start_delta_ms,
self.activity_start_pair_disagreement_ms().abs()
)
}
}
fn raw_activity_calibration_note(&self) -> String {
if !self.coded_events || self.raw_activity_start_confirms_pairs() {
String::new()
} else {
format!(
"; raw activity start delta {:+.1} ms is not used for calibration because coded pairs disagree by {:.1} ms",
self.activity_start_delta_ms,
self.activity_start_pair_disagreement_ms().abs()
)
}
}
}
fn percentile_abs(values: &[f64], percentile: f64) -> f64 {
@ -281,6 +331,7 @@ mod tests {
video_event_count: 4,
audio_event_count: 4,
paired_event_count: 4,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -312,6 +363,7 @@ mod tests {
video_event_count: 12,
audio_event_count: 12,
paired_event_count: 12,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -339,6 +391,7 @@ mod tests {
video_event_count: 14,
audio_event_count: 14,
paired_event_count: 12,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -367,6 +420,7 @@ mod tests {
video_event_count: 16,
audio_event_count: 16,
paired_event_count: 16,
coded_events: false,
activity_start_delta_ms: -766.4,
raw_first_video_activity_s: 7.491,
raw_first_audio_activity_s: 6.725,
@ -394,6 +448,7 @@ mod tests {
video_event_count: 16,
audio_event_count: 16,
paired_event_count: 16,
coded_events: false,
activity_start_delta_ms: 6_735.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 6.735,
@ -414,12 +469,41 @@ mod tests {
assert!(recommendation.note.contains("disagrees with median skew"));
}
#[test]
fn calibration_recommendation_uses_coded_pairs_when_raw_activity_disagrees() {
let report = SyncAnalysisReport {
video_event_count: 14,
audio_event_count: 14,
paired_event_count: 10,
coded_events: true,
activity_start_delta_ms: -3_620.7,
raw_first_video_activity_s: 9.361,
raw_first_audio_activity_s: 5.740,
first_skew_ms: -270.0,
last_skew_ms: -230.0,
mean_skew_ms: -205.0,
median_skew_ms: -188.4,
max_abs_skew_ms: 273.8,
drift_ms: 40.0,
skews_ms: vec![-270.0, -240.0, -188.4, -175.0, -130.0],
video_onsets_s: vec![],
audio_onsets_s: vec![],
paired_events: vec![],
};
let recommendation = report.calibration_recommendation();
assert!(recommendation.ready);
assert_eq!(recommendation.recommended_audio_offset_adjust_us, 188_400);
assert!(recommendation.note.contains("coded pairs disagree"));
}
#[test]
fn calibration_recommendation_reports_when_skew_is_already_settled() {
let report = SyncAnalysisReport {
video_event_count: 14,
audio_event_count: 14,
paired_event_count: 12,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -447,6 +531,7 @@ mod tests {
video_event_count: 5,
audio_event_count: 5,
paired_event_count: 5,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -473,6 +558,7 @@ mod tests {
video_event_count: 5,
audio_event_count: 5,
paired_event_count: 5,
coded_events: false,
activity_start_delta_ms: 0.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -499,6 +585,7 @@ mod tests {
video_event_count: 20,
audio_event_count: 20,
paired_event_count: 20,
coded_events: false,
activity_start_delta_ms: 20_000.0,
raw_first_video_activity_s: 0.0,
raw_first_audio_activity_s: 0.0,
@ -519,4 +606,32 @@ mod tests {
assert_eq!(verdict.status, "catastrophic_failure");
assert!(verdict.reason.contains("activity starts"));
}
#[test]
fn verdict_ignores_uncorroborated_raw_activity_for_coded_runs() {
let report = SyncAnalysisReport {
video_event_count: 20,
audio_event_count: 20,
paired_event_count: 20,
coded_events: true,
activity_start_delta_ms: -3_620.7,
raw_first_video_activity_s: 9.361,
raw_first_audio_activity_s: 5.740,
first_skew_ms: -20.0,
last_skew_ms: -18.0,
mean_skew_ms: -19.0,
median_skew_ms: -19.0,
max_abs_skew_ms: 20.0,
drift_ms: 2.0,
skews_ms: vec![-20.0; 20],
video_onsets_s: vec![],
audio_onsets_s: vec![],
paired_events: vec![],
};
let verdict = report.verdict();
assert!(verdict.passed);
assert_eq!(verdict.status, "preferred");
assert!(verdict.reason.contains("raw activity start delta"));
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.16.21"
version = "0.16.22"
edition = "2024"
build = "build.rs"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.16.21"
version = "0.16.22"
edition = "2024"
autobins = false

View File

@ -91,9 +91,7 @@ impl UpstreamMediaRuntime {
fn positive_audio_delay_allowance_us(&self) -> u64 {
let camera_offset_us = self.camera_playout_offset_us.load(Ordering::Relaxed);
let microphone_offset_us = self.microphone_playout_offset_us.load(Ordering::Relaxed);
microphone_offset_us
.saturating_sub(camera_offset_us)
.max(0) as u64
microphone_offset_us.saturating_sub(camera_offset_us).max(0) as u64
}
}