diff --git a/AGENTS.md b/AGENTS.md index 7921150..4e65056 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 86a508d..4da5257 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/client/Cargo.toml b/client/Cargo.toml index 3f538e7..c518efb 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.19.8" +version = "0.19.9" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 304fec1..5b2a1ee 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.19.8" +version = "0.19.9" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 0c2df7f..02a2295 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -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 diff --git a/scripts/manual/run_upstream_av_sync.sh b/scripts/manual/run_upstream_av_sync.sh index 1b1e4e2..e9bbd0d 100755 --- a/scripts/manual/run_upstream_av_sync.sh +++ b/scripts/manual/run_upstream_av_sync.sh @@ -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) diff --git a/server/Cargo.toml b/server/Cargo.toml index d6cd4a7..f0d7221 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.19.8" +version = "0.19.9" edition = "2024" autobins = false diff --git a/server/src/calibration.rs b/server/src/calibration.rs index d59294c..86de84a 100644 --- a/server/src/calibration.rs +++ b/server/src/calibration.rs @@ -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")); }); } diff --git a/testing/tests/client_manual_sync_script_contract.rs b/testing/tests/client_manual_sync_script_contract.rs index 39fa5e3..8858aa5 100644 --- a/testing/tests/client_manual_sync_script_contract.rs +++ b/testing/tests/client_manual_sync_script_contract.rs @@ -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\"", diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index 731053a..0eb3d30 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -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}"));