fix: bake direct UVC sync center
This commit is contained in:
parent
ffa4c44af1
commit
1ec6baa06c
@ -114,6 +114,12 @@ path.
|
||||
- [x] Measure freshness clock alignment from the server host to Tethys directly
|
||||
instead of routing both clock samples through the client laptop.
|
||||
- [x] Include clock uncertainty as a margin in freshness pass/fail decisions.
|
||||
- [x] Recenter the normal MJPEG/UVC output-path video baseline to `+170ms`
|
||||
from repeated direct UVC/UAC target estimates, and migrate untouched
|
||||
`+1090ms` factory/env baselines down to that center.
|
||||
- [x] Split freshness reporting into fixed sync delay, last-hop device
|
||||
overhead, and total RC target event age so freshness work cannot hide
|
||||
the sync cost.
|
||||
- [ ] Keep UI/profile controls authoritative for UVC output profiles beyond
|
||||
`640x480@20`; validate `1280x720@30` and `1920x1080@20/30` after sync is
|
||||
locked.
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -15,13 +15,14 @@ INSTALL_SERVER_BIND_ADDR=${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}
|
||||
LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
|
||||
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz}
|
||||
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0
|
||||
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000
|
||||
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=170000
|
||||
LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000
|
||||
PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
|
||||
PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000
|
||||
PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=0
|
||||
PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=350000
|
||||
PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=130000
|
||||
PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000
|
||||
|
||||
resolve_upstream_audio_playout_offset_us() {
|
||||
if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then
|
||||
@ -50,8 +51,8 @@ resolve_upstream_video_playout_offset_us() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" ]]; then
|
||||
echo "⚠️ migrating stale upstream video playout offset to the 0.17 browser-visible MJPEG/UVC sync baseline." >&2
|
||||
if [[ ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" ]]; then
|
||||
echo "⚠️ migrating stale upstream video playout offset to the direct UVC/UAC MJPEG sync center." >&2
|
||||
echo " Use LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US to intentionally keep an older value." >&2
|
||||
printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US"
|
||||
return 0
|
||||
|
||||
@ -931,6 +931,17 @@ def stats(rows, key):
|
||||
}
|
||||
|
||||
|
||||
def shift_stats(base, delta_ms):
|
||||
shifted = dict(base)
|
||||
if not shifted.get("available"):
|
||||
return shifted
|
||||
for key in ["first_ms", "last_ms", "mean_ms", "median_ms", "p95_ms", "max_ms"]:
|
||||
value = shifted.get(key)
|
||||
shifted[key] = value + delta_ms if value is not None else None
|
||||
shifted["intentional_delay_ms"] = delta_ms
|
||||
return shifted
|
||||
|
||||
|
||||
def fit_linear(rows, key):
|
||||
points = [(row["event_time_s"], row[key]) for row in rows if row.get(key) is not None]
|
||||
if len(points) < 2:
|
||||
@ -992,6 +1003,8 @@ theia_to_tethys_offset_ns = (
|
||||
else None
|
||||
)
|
||||
clock_uncertainty_ms = as_float(clock_alignment.get("uncertainty_ms"), 0.0)
|
||||
audio_delay_ms = as_float(timeline.get("audio_delay_us"), 0.0) / 1000.0
|
||||
video_delay_ms = as_float(timeline.get("video_delay_us"), 0.0) / 1000.0
|
||||
|
||||
joined = []
|
||||
for observed in report.get("paired_events", []):
|
||||
@ -1041,6 +1054,12 @@ for observed in report.get("paired_events", []):
|
||||
if tethys_audio_unix_ns is not None and corrected_server_audio_push_unix_ns is not None
|
||||
else None
|
||||
)
|
||||
video_event_age_ms = (
|
||||
video_freshness_ms + video_delay_ms if video_freshness_ms is not None else None
|
||||
)
|
||||
audio_event_age_ms = (
|
||||
audio_freshness_ms + audio_delay_ms if audio_freshness_ms is not None else None
|
||||
)
|
||||
residual_path_skew_ms = (
|
||||
observed_skew_ms - server_feed_delta_ms
|
||||
if observed_skew_ms is not None and server_feed_delta_ms is not None
|
||||
@ -1066,6 +1085,8 @@ for observed in report.get("paired_events", []):
|
||||
"server_audio_push_s": server_audio_push_s,
|
||||
"video_freshness_ms": video_freshness_ms,
|
||||
"audio_freshness_ms": audio_freshness_ms,
|
||||
"video_event_age_ms": video_event_age_ms,
|
||||
"audio_event_age_ms": audio_event_age_ms,
|
||||
"server_feed_delta_ms": server_feed_delta_ms,
|
||||
"residual_path_skew_ms": residual_path_skew_ms,
|
||||
"confidence": finite(observed.get("confidence")),
|
||||
@ -1081,8 +1102,12 @@ server_model = fit_linear(joined, "server_feed_delta_ms")
|
||||
residual_model = fit_linear(joined, "residual_path_skew_ms")
|
||||
video_freshness_model = fit_linear(joined, "video_freshness_ms")
|
||||
audio_freshness_model = fit_linear(joined, "audio_freshness_ms")
|
||||
video_event_age_model = fit_linear(joined, "video_event_age_ms")
|
||||
audio_event_age_model = fit_linear(joined, "audio_event_age_ms")
|
||||
video_freshness_stats = stats(joined, "video_freshness_ms")
|
||||
audio_freshness_stats = stats(joined, "audio_freshness_ms")
|
||||
video_event_age_stats = shift_stats(video_freshness_stats, video_delay_ms)
|
||||
audio_event_age_stats = shift_stats(audio_freshness_stats, audio_delay_ms)
|
||||
server_observed_correlation = correlation(joined, "server_feed_delta_ms", "observed_skew_ms")
|
||||
|
||||
observed_drift = observed_model.get("drift_ms", 0.0)
|
||||
@ -1112,17 +1137,31 @@ freshness_p95_values = [
|
||||
]
|
||||
if value is not None
|
||||
]
|
||||
event_age_p95_values = [
|
||||
value
|
||||
for value in [
|
||||
video_event_age_stats.get("p95_ms"),
|
||||
audio_event_age_stats.get("p95_ms"),
|
||||
]
|
||||
if value is not None
|
||||
]
|
||||
freshness_drift_values = [
|
||||
abs(value)
|
||||
for value in [
|
||||
video_freshness_model.get("drift_ms"),
|
||||
audio_freshness_model.get("drift_ms"),
|
||||
video_event_age_model.get("drift_ms"),
|
||||
audio_event_age_model.get("drift_ms"),
|
||||
]
|
||||
if value is not None and math.isfinite(value)
|
||||
]
|
||||
freshness_worst_p95_ms = max(freshness_p95_values) if freshness_p95_values else None
|
||||
freshness_worst_event_p95_ms = max(event_age_p95_values) if event_age_p95_values else None
|
||||
freshness_worst_drift_ms = max(freshness_drift_values) if freshness_drift_values else None
|
||||
if not freshness_p95_values:
|
||||
freshness_worst_event_with_uncertainty_ms = (
|
||||
freshness_worst_event_p95_ms + clock_uncertainty_ms
|
||||
if freshness_worst_event_p95_ms is not None
|
||||
else None
|
||||
)
|
||||
if not event_age_p95_values:
|
||||
freshness_status = "unknown"
|
||||
freshness_reason = "clock-aligned server feed and Tethys capture timestamps were not available"
|
||||
elif not clock_alignment_available or clock_uncertainty_ms > max_clock_uncertainty_ms:
|
||||
@ -1131,27 +1170,27 @@ elif not clock_alignment_available or clock_uncertainty_ms > max_clock_uncertain
|
||||
f"clock uncertainty {clock_uncertainty_ms:.1f} ms exceeds "
|
||||
f"{max_clock_uncertainty_ms:.1f} ms freshness measurement limit"
|
||||
)
|
||||
elif freshness_worst_p95_ms < -clock_uncertainty_ms:
|
||||
elif freshness_worst_event_p95_ms < -clock_uncertainty_ms:
|
||||
freshness_status = "invalid"
|
||||
freshness_reason = (
|
||||
f"freshness was negative beyond clock uncertainty: "
|
||||
f"worst p95 {freshness_worst_p95_ms:.1f} ms, uncertainty "
|
||||
f"worst RC event-age p95 {freshness_worst_event_p95_ms:.1f} ms, uncertainty "
|
||||
f"{clock_uncertainty_ms:.1f} ms"
|
||||
)
|
||||
elif (freshness_worst_p95_ms + clock_uncertainty_ms) <= max_freshness_age_ms and (
|
||||
elif freshness_worst_event_with_uncertainty_ms <= max_freshness_age_ms and (
|
||||
freshness_worst_drift_ms is None or freshness_worst_drift_ms <= max_freshness_drift_ms
|
||||
):
|
||||
freshness_status = "pass"
|
||||
freshness_reason = (
|
||||
f"worst p95 freshness {freshness_worst_p95_ms:.1f} ms + clock uncertainty "
|
||||
f"worst RC event-age p95 {freshness_worst_event_p95_ms:.1f} ms + clock uncertainty "
|
||||
f"{clock_uncertainty_ms:.1f} ms <= {max_freshness_age_ms:.1f} ms and worst freshness drift "
|
||||
f"{(freshness_worst_drift_ms or 0.0):.1f} ms <= {max_freshness_drift_ms:.1f} ms"
|
||||
)
|
||||
else:
|
||||
freshness_status = "fail"
|
||||
freshness_reason = (
|
||||
f"worst p95 freshness "
|
||||
f"{freshness_worst_p95_ms if freshness_worst_p95_ms is not None else 0.0:.1f} ms "
|
||||
f"worst RC event-age p95 "
|
||||
f"{freshness_worst_event_p95_ms if freshness_worst_event_p95_ms is not None else 0.0:.1f} ms "
|
||||
f"+ clock uncertainty {clock_uncertainty_ms:.1f} ms "
|
||||
f"(limit {max_freshness_age_ms:.1f} ms), worst freshness drift "
|
||||
f"{freshness_worst_drift_ms if freshness_worst_drift_ms is not None else 0.0:.1f} ms "
|
||||
@ -1178,12 +1217,21 @@ artifact = {
|
||||
"max_age_limit_ms": max_freshness_age_ms,
|
||||
"max_drift_limit_ms": max_freshness_drift_ms,
|
||||
"max_clock_uncertainty_ms": max_clock_uncertainty_ms,
|
||||
"worst_p95_freshness_ms": freshness_worst_p95_ms,
|
||||
"intentional_audio_delay_ms": audio_delay_ms,
|
||||
"intentional_video_delay_ms": video_delay_ms,
|
||||
"worst_p95_path_freshness_ms": freshness_worst_p95_ms,
|
||||
"worst_event_age_p95_ms": freshness_worst_event_p95_ms,
|
||||
"worst_event_age_with_uncertainty_ms": freshness_worst_event_with_uncertainty_ms,
|
||||
"worst_p95_freshness_ms": freshness_worst_event_p95_ms,
|
||||
"worst_freshness_drift_ms": freshness_worst_drift_ms,
|
||||
"video_freshness_stats": video_freshness_stats,
|
||||
"audio_freshness_stats": audio_freshness_stats,
|
||||
"video_event_age_stats": video_event_age_stats,
|
||||
"audio_event_age_stats": audio_event_age_stats,
|
||||
"video_freshness_model": video_freshness_model,
|
||||
"audio_freshness_model": audio_freshness_model,
|
||||
"video_event_age_model": video_event_age_model,
|
||||
"audio_event_age_model": audio_event_age_model,
|
||||
},
|
||||
"server_observed_correlation": server_observed_correlation,
|
||||
"server_drift_share_of_observed": server_share,
|
||||
@ -1222,6 +1270,8 @@ with pathlib.Path(output_csv_path).open("w", newline="", encoding="utf-8") as ha
|
||||
"tethys_audio_unix_ns",
|
||||
"video_freshness_ms",
|
||||
"audio_freshness_ms",
|
||||
"video_event_age_ms",
|
||||
"audio_event_age_ms",
|
||||
"server_feed_delta_ms",
|
||||
"residual_path_skew_ms",
|
||||
"server_video_feed_monotonic_us",
|
||||
@ -1248,10 +1298,12 @@ lines = [
|
||||
"- scope: clock-corrected server feed to Tethys capture event from the same paired signatures",
|
||||
f"- freshness status: {freshness_status} ({freshness_reason})",
|
||||
f"- clock uncertainty: +/-{clock_uncertainty_ms:.1f} ms",
|
||||
f"- video freshness: first {video_freshness_stats.get('first_ms') or 0.0:.1f} ms, median {video_freshness_stats.get('median_ms') or 0.0:.1f} ms, p95 {video_freshness_stats.get('p95_ms') or 0.0:.1f} ms, max {video_freshness_stats.get('max_ms') or 0.0:.1f} ms",
|
||||
f"- audio freshness: first {audio_freshness_stats.get('first_ms') or 0.0:.1f} ms, median {audio_freshness_stats.get('median_ms') or 0.0:.1f} ms, p95 {audio_freshness_stats.get('p95_ms') or 0.0:.1f} ms, max {audio_freshness_stats.get('max_ms') or 0.0:.1f} ms",
|
||||
f"- video freshness drift: {video_freshness_model.get('drift_ms', 0.0):+.1f} ms over paired events ({video_freshness_model.get('slope_ms_per_s', 0.0):+.3f} ms/s)",
|
||||
f"- audio freshness drift: {audio_freshness_model.get('drift_ms', 0.0):+.1f} ms over paired events ({audio_freshness_model.get('slope_ms_per_s', 0.0):+.3f} ms/s)",
|
||||
f"- intentional sync delays: audio {audio_delay_ms:+.1f} ms, video {video_delay_ms:+.1f} ms",
|
||||
f"- device path overhead: video median {video_freshness_stats.get('median_ms') or 0.0:.1f} ms / p95 {video_freshness_stats.get('p95_ms') or 0.0:.1f} ms / max {video_freshness_stats.get('max_ms') or 0.0:.1f} ms; audio median {audio_freshness_stats.get('median_ms') or 0.0:.1f} ms / p95 {audio_freshness_stats.get('p95_ms') or 0.0:.1f} ms / max {audio_freshness_stats.get('max_ms') or 0.0:.1f} ms",
|
||||
f"- RC target event age: video median {video_event_age_stats.get('median_ms') or 0.0:.1f} ms / p95 {video_event_age_stats.get('p95_ms') or 0.0:.1f} ms / max {video_event_age_stats.get('max_ms') or 0.0:.1f} ms; audio median {audio_event_age_stats.get('median_ms') or 0.0:.1f} ms / p95 {audio_event_age_stats.get('p95_ms') or 0.0:.1f} ms / max {audio_event_age_stats.get('max_ms') or 0.0:.1f} ms",
|
||||
f"- freshness budget: worst RC event-age p95 {(freshness_worst_event_p95_ms or 0.0):.1f} ms + clock uncertainty {clock_uncertainty_ms:.1f} ms = {(freshness_worst_event_with_uncertainty_ms or 0.0):.1f} ms vs limit {max_freshness_age_ms:.1f} ms",
|
||||
f"- video event-age drift: {video_event_age_model.get('drift_ms', 0.0):+.1f} ms over paired events ({video_event_age_model.get('slope_ms_per_s', 0.0):+.3f} ms/s)",
|
||||
f"- audio event-age drift: {audio_event_age_model.get('drift_ms', 0.0):+.1f} ms over paired events ({audio_event_age_model.get('slope_ms_per_s', 0.0):+.3f} ms/s)",
|
||||
]
|
||||
summary = "\n".join(lines) + "\n"
|
||||
pathlib.Path(output_txt_path).write_text(summary)
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.19.8"
|
||||
version = "0.19.9"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -10,18 +10,17 @@ use lesavka_common::lesavka::{
|
||||
use crate::upstream_media_runtime::UpstreamMediaRuntime;
|
||||
|
||||
pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0;
|
||||
// 0.17.10's mirrored browser probe showed the remaining sync error lives
|
||||
// after server enqueue: UAC audio becomes browser-visible on Tethys roughly
|
||||
// 950-970ms after the matching UVC frame. Delay MJPEG/UVC video by that
|
||||
// measured egress delta; the planner treats this as intentional output-path
|
||||
// sync compensation rather than stale-media drift.
|
||||
pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000;
|
||||
// Direct UVC/UAC output-delay probes against the lab RC target put the
|
||||
// server-to-target sync center near 170ms for MJPEG/UVC video. This is an
|
||||
// output-path compensation, not a freshness buffer.
|
||||
pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 170_000;
|
||||
const LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000;
|
||||
const PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000;
|
||||
const PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US: i64 = 1_260_000;
|
||||
const PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0;
|
||||
const PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 350_000;
|
||||
const PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 130_000;
|
||||
const PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000;
|
||||
const PROFILE: &str = "mjpeg";
|
||||
const FACTORY_CONFIDENCE: &str = "factory";
|
||||
const PREVIOUS_OFFSET_LIMIT_US: i64 = 500_000;
|
||||
@ -296,6 +295,7 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
|
||||
PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US
|
||||
| PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US
|
||||
| PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US
|
||||
| PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US
|
||||
) && state.active_video_offset_us == state.default_video_offset_us;
|
||||
if state.profile == PROFILE
|
||||
&& source_allows_migration
|
||||
@ -535,7 +535,42 @@ mod tests {
|
||||
runtime.playout_offsets(),
|
||||
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
|
||||
);
|
||||
assert!(state.detail.contains("to audio +0.0ms/video +1090.0ms"));
|
||||
assert!(state.detail.contains("to audio +0.0ms/video +170.0ms"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_migrates_overshot_video_factory_mjpeg_baseline() {
|
||||
let file = NamedTempFile::new().expect("temp calibration file");
|
||||
std::fs::write(
|
||||
file.path(),
|
||||
r#"
|
||||
profile="mjpeg"
|
||||
default_audio_offset_us=0
|
||||
default_video_offset_us=1090000
|
||||
active_audio_offset_us=0
|
||||
active_video_offset_us=1090000
|
||||
source="factory"
|
||||
confidence="factory"
|
||||
detail="loaded upstream A/V calibration defaults"
|
||||
"#,
|
||||
)
|
||||
.expect("overshot video calibration seed");
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
|
||||
let runtime = Arc::new(UpstreamMediaRuntime::new());
|
||||
let store = CalibrationStore::load(runtime.clone());
|
||||
let state = store.current();
|
||||
assert_eq!(state.active_audio_offset_us, 0);
|
||||
assert_eq!(state.default_audio_offset_us, 0);
|
||||
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
|
||||
assert_eq!(state.default_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
|
||||
assert_eq!(state.source, "factory");
|
||||
assert_eq!(
|
||||
runtime.playout_offsets(),
|
||||
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0)
|
||||
);
|
||||
assert!(state.detail.contains("from audio +0.0ms/video +1090.0ms"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -79,8 +79,17 @@ fn upstream_sync_script_tunnels_auto_server_addr_through_ssh() {
|
||||
"dominant_layer",
|
||||
"freshness status",
|
||||
"clock-corrected server feed to Tethys capture event",
|
||||
"intentional sync delays",
|
||||
"device path overhead",
|
||||
"RC target event age",
|
||||
"freshness budget",
|
||||
"video_freshness_ms",
|
||||
"audio_freshness_ms",
|
||||
"video_event_age_ms",
|
||||
"audio_event_age_ms",
|
||||
"intentional_video_delay_ms",
|
||||
"video_event_age_stats",
|
||||
"worst_event_age_with_uncertainty_ms",
|
||||
"video_delay_function_candidate",
|
||||
"source\": \"direct-uvc-uac-output-probe\"",
|
||||
"scope\": \"server-output-static-baseline\"",
|
||||
|
||||
@ -56,7 +56,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS:-1000}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS:-60000}"));
|
||||
assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0"));
|
||||
assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000"));
|
||||
assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=170000"));
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"),
|
||||
"video offset should be resolved through stale-baseline migration logic"
|
||||
@ -72,6 +72,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=130000")
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000")
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"),
|
||||
"install-specific offset override should bypass stale ambient runtime env"
|
||||
@ -85,7 +88,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
"installer should not preserve old MJPEG/UVC sync baselines accidentally"
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("migrating stale upstream video playout offset to the 0.17 browser-visible MJPEG/UVC sync baseline"),
|
||||
SERVER_INSTALL.contains(
|
||||
"migrating stale upstream video playout offset to the direct UVC/UAC MJPEG sync center"
|
||||
),
|
||||
"installer should not preserve old video delay baselines accidentally"
|
||||
);
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user