From 1b3b8c2cbb304fa6c3bf5c2f15e5331b7b76e511 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 6 May 2026 03:59:20 -0300 Subject: [PATCH] calibration: bake per-mode RC delays --- Cargo.lock | 6 +- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- docs/operational-env.md | 2 + scripts/install/server.sh | 64 +++- .../manual/run_server_to_rc_mode_matrix.sh | 24 +- server/Cargo.toml | 2 +- server/src/calibration.rs | 276 +++++++++++++++--- server/src/upstream_media_runtime.rs | 72 ++++- .../client_manual_sync_script_contract.rs | 4 +- .../tests/server_install_script_contract.rs | 19 +- 11 files changed, 408 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e42784b..10cc47d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.19.28" +version = "0.19.29" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.19.28" +version = "0.19.29" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.19.28" +version = "0.19.29" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index cae3796..ea957ae 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.19.28" +version = "0.19.29" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 9c46872..489b469 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.19.28" +version = "0.19.29" edition = "2024" build = "build.rs" diff --git a/docs/operational-env.md b/docs/operational-env.md index e5c410b..4ba2add 100644 --- a/docs/operational-env.md +++ b/docs/operational-env.md @@ -263,6 +263,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_TOUCHPAD_SCALE` | input routing/clipboard override | | `LESAVKA_UAC_DEV` | server hardware/device override | | `LESAVKA_UAC_SESSION_CLOCK_ALIGN` | server audio sink clock-alignment override; `0` is the host-validated default | +| `LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US` | server upstream per-UVC-mode UAC output-path map, e.g. `1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0` | | `LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US` | server upstream output-path override; v2 uses it as the explicit UAC handoff delay relative to the shared client capture clock | | `LESAVKA_UPSTREAM_AUDIO_MASTER_WAIT_GRACE_MS` | server upstream sync override; how long video may wait past its nominal due time for UAC audio to reach the matching timestamp, defaults to `350` | | `LESAVKA_UPSTREAM_BUNDLED_PLAYOUT_DELAY_MS` | compatibility alias for `LESAVKA_UPSTREAM_V2_PLAYOUT_DELAY_MS` | @@ -274,6 +275,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_UPSTREAM_TIMING_TRACE` | upstream capture/rebase trace override for sync debugging | | `LESAVKA_UPSTREAM_V2_MAX_LIVE_AGE_MS` | v2 bundled webcam freshness ceiling; bundles already older than this are dropped as one unit, defaults to `1000` | | `LESAVKA_UPSTREAM_V2_PLAYOUT_DELAY_MS` | v2 optional common playout slack after sync offsets; defaults to `20` and is reduced when needed to protect the live-age budget | +| `LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US` | server upstream per-UVC-mode output-path map; shipped MJPEG defaults are `1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952` | | `LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US` | server upstream output-path override; v2 uses it as the explicit UVC handoff delay relative to the shared client capture clock, defaults to the calibrated MJPEG/UVC offset | | `LESAVKA_UPLINK_CAMERA_PREVIEW` | client media capture/playback override | | `LESAVKA_UPLINK_MIC_LEVEL` | client media capture/playback override | diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 9b77560..1b62289 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -15,25 +15,68 @@ 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=170000 +DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=135090 +DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0 +DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952 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_SCALAR_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=170000 PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000 +lookup_mode_offset_us() { + local mode=$1 + local offset_map=$2 + local entry key value + IFS=',' read -r -a offset_entries <<<"${offset_map}" + for entry in "${offset_entries[@]}"; do + key=${entry%%=*} + value=${entry#*=} + if [[ "${key}" == "${mode}" && "${value}" =~ ^-?[0-9]+$ ]]; then + printf '%s\n' "${value}" + return 0 + fi + done + return 1 +} + +default_uvc_mode() { + local width=${LESAVKA_UVC_WIDTH:-1280} + local height=${LESAVKA_UVC_HEIGHT:-720} + local fps=${LESAVKA_UVC_FPS:-30} + printf '%sx%s@%s\n' "${width}" "${height}" "${fps}" +} + +default_mjpeg_upstream_audio_playout_offset_us() { + local mode + mode=$(default_uvc_mode) + lookup_mode_offset_us "${mode}" "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US" \ + || printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" +} + +default_mjpeg_upstream_video_playout_offset_us() { + local mode + mode=$(default_uvc_mode) + lookup_mode_offset_us "${mode}" "$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US" \ + || printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" +} + resolve_upstream_audio_playout_offset_us() { + local default_offset_us + default_offset_us=$(default_mjpeg_upstream_audio_playout_offset_us) + if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then printf '%s\n' "$LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" return 0 fi if [[ ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" ]]; then - echo "⚠️ migrating stale upstream audio playout offset to the 0.17 freshness-first planner default." >&2 + echo "⚠️ migrating stale upstream audio playout offset to the per-mode MJPEG/UAC default." >&2 echo " Use LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US to intentionally keep an older value." >&2 - printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" + printf '%s\n' "$default_offset_us" return 0 fi @@ -42,19 +85,22 @@ resolve_upstream_audio_playout_offset_us() { return 0 fi - printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" + printf '%s\n' "$default_offset_us" } resolve_upstream_video_playout_offset_us() { + local default_offset_us + default_offset_us=$(default_mjpeg_upstream_video_playout_offset_us) + if [[ -n ${LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} ]]; then printf '%s\n' "$LESAVKA_INSTALL_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" || ${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 + 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_SCALAR_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 per-mode 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" + printf '%s\n' "$default_offset_us" return 0 fi @@ -63,7 +109,7 @@ resolve_upstream_video_playout_offset_us() { return 0 fi - printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" + printf '%s\n' "$default_offset_us" } manifest_package_version() { @@ -1004,6 +1050,8 @@ fi printf 'LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s\n' "${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-350}" printf 'LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS=%s\n' "${LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS:-1000}" printf 'LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS=%s\n' "${LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS:-60000}" + printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US}" + printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US:-$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US}" printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "$(resolve_upstream_audio_playout_offset_us)" printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "$(resolve_upstream_video_playout_offset_us)" printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}" diff --git a/scripts/manual/run_server_to_rc_mode_matrix.sh b/scripts/manual/run_server_to_rc_mode_matrix.sh index de256cc..6b74c50 100755 --- a/scripts/manual/run_server_to_rc_mode_matrix.sh +++ b/scripts/manual/run_server_to_rc_mode_matrix.sh @@ -32,9 +32,9 @@ SSH_OPTS=${SSH_OPTS:-"-o BatchMode=yes -o ConnectTimeout=30"} LESAVKA_SERVER_RC_CORE_WEBCAM_MODES=${LESAVKA_SERVER_RC_CORE_WEBCAM_MODES:-1280x720@20,1280x720@30,1920x1080@20,1920x1080@30} LESAVKA_SERVER_RC_MODES=${LESAVKA_SERVER_RC_MODES:-${LESAVKA_SERVER_RC_CORE_WEBCAM_MODES}} LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US:-${LESAVKA_OUTPUT_DELAY_PROBE_AUDIO_DELAY_US:-0}} -LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US:-170000} +LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US:-135090} LESAVKA_SERVER_RC_MODE_AUDIO_DELAYS_US=${LESAVKA_SERVER_RC_MODE_AUDIO_DELAYS_US:-1280x720@20=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1280x720@30=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1920x1080@20=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1920x1080@30=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US}} -LESAVKA_SERVER_RC_MODE_DELAYS_US=${LESAVKA_SERVER_RC_MODE_DELAYS_US:-1280x720@20=170000,1280x720@30=170000,1920x1080@20=170000,1920x1080@30=170000} +LESAVKA_SERVER_RC_MODE_DELAYS_US=${LESAVKA_SERVER_RC_MODE_DELAYS_US:-1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952} LESAVKA_SERVER_RC_MODE_DISCOVERY_SIZES=${LESAVKA_SERVER_RC_MODE_DISCOVERY_SIZES:-1280x720,1920x1080} LESAVKA_SERVER_RC_MODE_DISCOVERY_FPS=${LESAVKA_SERVER_RC_MODE_DISCOVERY_FPS:-20,30} LESAVKA_SERVER_RC_MODE_DISCOVERY_INCLUDE_REGEX=${LESAVKA_SERVER_RC_MODE_DISCOVERY_INCLUDE_REGEX:-Logitech|BRIO|C9[0-9]+|HD UVC WebCam|USB2[.]0 HD|Integrated Camera|Webcam|Camera} @@ -979,6 +979,8 @@ reconfigure_server_mode() { local width=$2 local height=$3 local fps=$4 + local audio_delay_us=${5:-$(lookup_audio_delay_us "${mode}")} + local video_delay_us=${6:-$(lookup_video_delay_us "${mode}")} [[ "${LESAVKA_SERVER_RC_RECONFIGURE}" != "0" ]] || return 0 echo "==> reconfiguring ${LESAVKA_SERVER_HOST} UVC gadget for ${mode}" @@ -987,6 +989,8 @@ reconfigure_server_mode() { LESAVKA_UVC_WIDTH="${width}" \ LESAVKA_UVC_HEIGHT="${height}" \ LESAVKA_UVC_FPS="${fps}" \ + LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US="${audio_delay_us}" \ + LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US="${video_delay_us}" \ bash -c "${LESAVKA_SERVER_RC_RECONFIGURE_COMMAND}" return 0 fi @@ -1009,7 +1013,11 @@ reconfigure_server_mode() { "${LESAVKA_SERVER_RC_ALLOW_GADGET_RESET}" \ "${LESAVKA_SERVER_RC_FORCE_GADGET_REBUILD}" \ "${LESAVKA_SERVER_RC_RECONFIGURE_SETTLE_SECONDS}" \ - "${LESAVKA_SERVER_RC_RECONFIGURE_VERBOSE}" <<'REMOTE_RECONFIGURE' + "${LESAVKA_SERVER_RC_RECONFIGURE_VERBOSE}" \ + "${audio_delay_us}" \ + "${video_delay_us}" \ + "${LESAVKA_SERVER_RC_MODE_AUDIO_DELAYS_US}" \ + "${LESAVKA_SERVER_RC_MODE_DELAYS_US}" <<'REMOTE_RECONFIGURE' set -euo pipefail mode=$1 width=$2 @@ -1021,6 +1029,10 @@ allow_gadget_reset=$7 force_gadget_rebuild=$8 settle_seconds=$9 verbose=${10} +audio_delay_us=${11} +video_delay_us=${12} +audio_delay_map=${13} +video_delay_map=${14} set_env_value() { local file=$1 @@ -1068,6 +1080,10 @@ set_env_value /etc/lesavka/server.env LESAVKA_UVC_WIDTH "${width}" set_env_value /etc/lesavka/server.env LESAVKA_UVC_HEIGHT "${height}" set_env_value /etc/lesavka/server.env LESAVKA_UVC_FPS "${fps}" set_env_value /etc/lesavka/server.env LESAVKA_UVC_INTERVAL "${interval}" +set_env_value /etc/lesavka/server.env LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US "${audio_delay_map}" +set_env_value /etc/lesavka/server.env LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US "${video_delay_map}" +set_env_value /etc/lesavka/server.env LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US "${audio_delay_us}" +set_env_value /etc/lesavka/server.env LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US "${video_delay_us}" set_env_value /etc/lesavka/uvc.env LESAVKA_UVC_CODEC "${codec}" set_env_value /etc/lesavka/uvc.env LESAVKA_UVC_WIDTH "${width}" @@ -2282,7 +2298,7 @@ for mode in "${modes[@]}"; do mkdir -p "${mode_dir}" echo "==> mode ${mode} run ${mode_run_index} repeat ${repeat_index}/${LESAVKA_SERVER_RC_REPEAT_COUNT}: video_delay_us=${video_delay_us} audio_delay_us=${audio_delay_us}" - reconfigure_server_mode "${mode}" "${width}" "${height}" "${fps}" + reconfigure_server_mode "${mode}" "${width}" "${height}" "${fps}" "${audio_delay_us}" "${video_delay_us}" wait_tethys_media_ready "${mode}" "${width}" "${height}" "${fps}" if [[ "${LESAVKA_SERVER_RC_SIGNAL_READY}" != "0" && "${LESAVKA_SERVER_RC_SIGNAL_READY_MODE}" != "separate" ]]; then diff --git a/server/Cargo.toml b/server/Cargo.toml index 0a35a4f..2211d25 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.19.28" +version = "0.19.29" edition = "2024" autobins = false diff --git a/server/src/calibration.rs b/server/src/calibration.rs index 86de84a..9672d24 100644 --- a/server/src/calibration.rs +++ b/server/src/calibration.rs @@ -10,16 +10,25 @@ use lesavka_common::lesavka::{ use crate::upstream_media_runtime::UpstreamMediaRuntime; pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0; -// 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; +pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US: i64 = 162_659; +pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US: i64 = 135_090; +pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US: i64 = 160_045; +pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US: i64 = 127_952; +pub const FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US: &str = + "1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0"; +pub const FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US: &str = + "1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952"; +// Direct UVC/UAC output-delay probes against the lab RC target showed a +// per-mode sync center for MJPEG/UVC video. This is output-path compensation, +// not a freshness buffer. The scalar fallback follows the default UVC mode. +pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US; 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_SCALAR_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 170_000; const PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000; const PROFILE: &str = "mjpeg"; const FACTORY_CONFIDENCE: &str = "factory"; @@ -208,10 +217,29 @@ pub fn calibration_path() -> PathBuf { } fn snapshot_from_env() -> CalibrationSnapshot { - let env_audio = env_i64("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"); - let env_video = env_i64("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US"); - let default_audio_offset_us = env_audio.unwrap_or(FACTORY_MJPEG_AUDIO_OFFSET_US); - let default_video_offset_us = env_video.unwrap_or(FACTORY_MJPEG_VIDEO_OFFSET_US); + let mode = current_uvc_mode(); + let factory_audio_offset_us = mode + .as_deref() + .and_then(|mode| lookup_mode_offset_us(FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, mode)) + .unwrap_or(FACTORY_MJPEG_AUDIO_OFFSET_US); + let factory_video_offset_us = mode + .as_deref() + .and_then(|mode| lookup_mode_offset_us(FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, mode)) + .unwrap_or(FACTORY_MJPEG_VIDEO_OFFSET_US); + let env_audio = configured_offset_us( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", + mode.as_deref(), + is_stale_audio_offset_us, + ); + let env_video = configured_offset_us( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", + mode.as_deref(), + is_stale_video_offset_us, + ); + let default_audio_offset_us = env_audio.unwrap_or(factory_audio_offset_us); + let default_video_offset_us = env_video.unwrap_or(factory_video_offset_us); let source = if env_audio.is_some() || env_video.is_some() { "env".to_string() } else { @@ -224,8 +252,8 @@ fn snapshot_from_env() -> CalibrationSnapshot { }; CalibrationSnapshot { profile: PROFILE.to_string(), - factory_audio_offset_us: FACTORY_MJPEG_AUDIO_OFFSET_US, - factory_video_offset_us: FACTORY_MJPEG_VIDEO_OFFSET_US, + factory_audio_offset_us, + factory_video_offset_us, default_audio_offset_us, default_video_offset_us, active_audio_offset_us: default_audio_offset_us, @@ -254,8 +282,8 @@ fn parse_snapshot(raw: &str) -> CalibrationSnapshot { }; CalibrationSnapshot { profile: value("profile").unwrap_or(fallback.profile), - factory_audio_offset_us: FACTORY_MJPEG_AUDIO_OFFSET_US, - factory_video_offset_us: FACTORY_MJPEG_VIDEO_OFFSET_US, + factory_audio_offset_us: fallback.factory_audio_offset_us, + factory_video_offset_us: fallback.factory_video_offset_us, default_audio_offset_us: number( "default_audio_offset_us", fallback.default_audio_offset_us, @@ -282,21 +310,12 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho ) && state .detail .contains("loaded upstream A/V calibration defaults"); - let untouched_legacy_audio = (matches!( - state.default_audio_offset_us, - FACTORY_MJPEG_AUDIO_OFFSET_US - | LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US - | PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US - | PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US - ) || clamped_previous_baseline) + let untouched_legacy_audio = (is_stale_audio_offset_us(state.default_audio_offset_us) + || state.default_audio_offset_us == state.factory_audio_offset_us + || clamped_previous_baseline) && state.active_audio_offset_us == state.default_audio_offset_us; - let untouched_legacy_video = matches!( - state.default_video_offset_us, - 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; + let untouched_legacy_video = is_stale_video_offset_us(state.default_video_offset_us) + && state.active_video_offset_us == state.default_video_offset_us; if state.profile == PROFILE && source_allows_migration && confidence_allows_migration @@ -305,18 +324,18 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho { let old_audio_offset_us = state.default_audio_offset_us; let old_video_offset_us = state.default_video_offset_us; - state.default_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; - state.active_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; - state.default_video_offset_us = FACTORY_MJPEG_VIDEO_OFFSET_US; - state.active_video_offset_us = FACTORY_MJPEG_VIDEO_OFFSET_US; + state.default_audio_offset_us = state.factory_audio_offset_us; + state.active_audio_offset_us = state.factory_audio_offset_us; + state.default_video_offset_us = state.factory_video_offset_us; + state.active_video_offset_us = state.factory_video_offset_us; state.source = "factory".to_string(); state.confidence = FACTORY_CONFIDENCE.to_string(); state.detail = format!( "migrated legacy MJPEG upstream A/V baseline from audio {:+.1}ms/video {:+.1}ms to audio {:+.1}ms/video {:+.1}ms", old_audio_offset_us as f64 / 1000.0, old_video_offset_us as f64 / 1000.0, - FACTORY_MJPEG_AUDIO_OFFSET_US as f64 / 1000.0, - FACTORY_MJPEG_VIDEO_OFFSET_US as f64 / 1000.0 + state.factory_audio_offset_us as f64 / 1000.0, + state.factory_video_offset_us as f64 / 1000.0 ); touch(&mut state); } @@ -351,6 +370,67 @@ fn touch(state: &mut CalibrationSnapshot) { state.updated_at = Utc::now().to_rfc3339(); } +fn configured_offset_us( + mode_map_name: &str, + scalar_name: &str, + mode: Option<&str>, + is_stale_scalar: impl Fn(i64) -> bool, +) -> Option { + mode.and_then(|mode| env_mode_offset_us(mode_map_name, mode)) + .or_else(|| env_i64(scalar_name).filter(|offset| !is_stale_scalar(*offset))) +} + +fn current_uvc_mode() -> Option { + env_mode("UVC_MODE") + .or_else(|| env_mode("LESAVKA_UVC_MODE")) + .or_else(|| { + let width = env_u32("LESAVKA_UVC_WIDTH")?; + let height = env_u32("LESAVKA_UVC_HEIGHT")?; + let fps = env_u32("LESAVKA_UVC_FPS") + .or_else(|| { + env_u32("LESAVKA_UVC_INTERVAL") + .and_then(|interval| (interval > 0).then_some(10_000_000 / interval)) + })? + .max(1); + Some(format!("{width}x{height}@{fps}")) + }) + .or_else(|| { + let width = env_u32("LESAVKA_CAM_WIDTH")?; + let height = env_u32("LESAVKA_CAM_HEIGHT")?; + let fps = env_u32("LESAVKA_CAM_FPS")?.max(1); + Some(format!("{width}x{height}@{fps}")) + }) +} + +fn env_mode(name: &str) -> Option { + std::env::var(name).ok().and_then(|value| { + let trimmed = value.trim(); + let valid = trimmed.split_once('@').and_then(|(size, fps)| { + let (width, height) = size.split_once('x')?; + width.parse::().ok()?; + height.parse::().ok()?; + fps.parse::().ok()?; + Some(()) + }); + valid.map(|()| trimmed.to_string()) + }) +} + +fn env_mode_offset_us(name: &str, mode: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|map| lookup_mode_offset_us(&map, mode)) +} + +fn lookup_mode_offset_us(map: &str, mode: &str) -> Option { + map.split(',').find_map(|entry| { + let (key, value) = entry.trim().split_once('=')?; + (key.trim() == mode) + .then(|| value.trim().parse::().ok().map(clamp_offset)) + .flatten() + }) +} + fn env_i64(name: &str) -> Option { std::env::var(name) .ok() @@ -358,6 +438,32 @@ fn env_i64(name: &str) -> Option { .map(clamp_offset) } +fn env_u32(name: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|value| value.trim().parse::().ok()) +} + +fn is_stale_audio_offset_us(offset: i64) -> bool { + matches!( + offset, + LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US + | PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US + | PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US + ) +} + +fn is_stale_video_offset_us(offset: i64) -> bool { + matches!( + offset, + PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US + | PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US + | PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US + | PREVIOUS_SCALAR_FACTORY_MJPEG_VIDEO_OFFSET_US + | PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US + ) +} + fn clamp_offset(value: i64) -> i64 { value.clamp(-OFFSET_LIMIT_US, OFFSET_LIMIT_US) } @@ -377,6 +483,18 @@ mod tests { [ ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>), ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", None::<&str>), + ( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ("LESAVKA_UVC_WIDTH", None::<&str>), + ("LESAVKA_UVC_HEIGHT", None::<&str>), + ("LESAVKA_UVC_FPS", None::<&str>), + ("LESAVKA_UVC_INTERVAL", None::<&str>), ], || { let state = snapshot_from_env(); @@ -387,6 +505,98 @@ mod tests { ); } + #[test] + fn default_snapshot_uses_uvc_mode_factory_calibration() { + temp_env::with_vars( + [ + ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>), + ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", None::<&str>), + ( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ("LESAVKA_UVC_WIDTH", Some("1920")), + ("LESAVKA_UVC_HEIGHT", Some("1080")), + ("LESAVKA_UVC_FPS", Some("30")), + ("LESAVKA_UVC_INTERVAL", None::<&str>), + ], + || { + let state = snapshot_from_env(); + assert_eq!( + state.default_video_offset_us, + FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US + ); + assert_eq!( + state.factory_video_offset_us, + FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US + ); + assert_eq!(state.source, "factory"); + }, + ); + } + + #[test] + fn mode_offset_map_overrides_stale_scalar_offset() { + temp_env::with_vars( + [ + ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>), + ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", Some("170000")), + ( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + Some("1280x720@20=123456"), + ), + ("LESAVKA_UVC_WIDTH", Some("1280")), + ("LESAVKA_UVC_HEIGHT", Some("720")), + ("LESAVKA_UVC_FPS", Some("20")), + ("LESAVKA_UVC_INTERVAL", None::<&str>), + ], + || { + let state = snapshot_from_env(); + assert_eq!(state.default_video_offset_us, 123_456); + assert_eq!(state.source, "env"); + assert_eq!(state.confidence, "configured"); + }, + ); + } + + #[test] + fn stale_scalar_video_offset_falls_back_to_mode_factory() { + temp_env::with_vars( + [ + ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>), + ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", Some("170000")), + ( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + None::<&str>, + ), + ("LESAVKA_UVC_WIDTH", Some("1920")), + ("LESAVKA_UVC_HEIGHT", Some("1080")), + ("LESAVKA_UVC_FPS", Some("20")), + ("LESAVKA_UVC_INTERVAL", None::<&str>), + ], + || { + let state = snapshot_from_env(); + assert_eq!( + state.default_video_offset_us, + FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US + ); + assert_eq!(state.source, "factory"); + }, + ); + } + #[test] fn store_persists_manual_adjustments_and_updates_runtime() { let file = NamedTempFile::new().expect("temp calibration file"); @@ -535,7 +745,7 @@ mod tests { runtime.playout_offsets(), (FACTORY_MJPEG_VIDEO_OFFSET_US, 0) ); - assert!(state.detail.contains("to audio +0.0ms/video +170.0ms")); + assert!(state.detail.contains("to audio +0.0ms/video +135.1ms")); }); } diff --git a/server/src/upstream_media_runtime.rs b/server/src/upstream_media_runtime.rs index 89dbeac..f849cd2 100644 --- a/server/src/upstream_media_runtime.rs +++ b/server/src/upstream_media_runtime.rs @@ -7,9 +7,12 @@ use std::time::Duration; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::time::Instant; +use crate::calibration::{ + FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, FACTORY_MJPEG_AUDIO_OFFSET_US, + FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, FACTORY_MJPEG_VIDEO_OFFSET_US, +}; + const TIMING_WINDOW_CAPACITY: usize = 240; -const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0; -const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum UpstreamMediaKind { @@ -727,17 +730,68 @@ fn upstream_playout_delay() -> Duration { } fn playout_offset_us(kind: UpstreamMediaKind) -> i64 { - let name = match kind { - UpstreamMediaKind::Camera => "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", - UpstreamMediaKind::Microphone => "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", + let (scalar_name, mode_map_name, factory_map, factory_offset_us) = match kind { + UpstreamMediaKind::Camera => ( + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", + FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, + FACTORY_MJPEG_VIDEO_OFFSET_US, + ), + UpstreamMediaKind::Microphone => ( + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", + FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, + FACTORY_MJPEG_AUDIO_OFFSET_US, + ), }; + let mode = current_uvc_mode(); + mode.as_deref() + .and_then(|mode| env_mode_offset_us(mode_map_name, mode)) + .or_else(|| env_i64(scalar_name)) + .or_else(|| { + mode.as_deref() + .and_then(|mode| lookup_mode_offset_us(factory_map, mode)) + }) + .unwrap_or(factory_offset_us) +} + +fn current_uvc_mode() -> Option { + let width = env_u32("LESAVKA_UVC_WIDTH")?; + let height = env_u32("LESAVKA_UVC_HEIGHT")?; + let fps = env_u32("LESAVKA_UVC_FPS") + .or_else(|| { + env_u32("LESAVKA_UVC_INTERVAL") + .and_then(|interval| (interval > 0).then_some(10_000_000 / interval)) + })? + .max(1); + Some(format!("{width}x{height}@{fps}")) +} + +fn env_mode_offset_us(name: &str, mode: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|map| lookup_mode_offset_us(&map, mode)) +} + +fn lookup_mode_offset_us(map: &str, mode: &str) -> Option { + map.split(',').find_map(|entry| { + let (key, value) = entry.trim().split_once('=')?; + (key.trim() == mode) + .then(|| value.trim().parse::().ok()) + .flatten() + }) +} + +fn env_i64(name: &str) -> Option { std::env::var(name) .ok() .and_then(|value| value.trim().parse::().ok()) - .unwrap_or(match kind { - UpstreamMediaKind::Camera => FACTORY_MJPEG_VIDEO_OFFSET_US, - UpstreamMediaKind::Microphone => FACTORY_MJPEG_AUDIO_OFFSET_US, - }) +} + +fn env_u32(name: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|value| value.trim().parse::().ok()) } fn apply_offset(instant: Instant, offset_us: i64) -> Instant { diff --git a/testing/tests/client_manual_sync_script_contract.rs b/testing/tests/client_manual_sync_script_contract.rs index d61d85e..9d25216 100644 --- a/testing/tests/client_manual_sync_script_contract.rs +++ b/testing/tests/client_manual_sync_script_contract.rs @@ -378,9 +378,9 @@ fn server_rc_mode_matrix_validates_advertised_uvc_profiles() { "LESAVKA_SERVER_RC_MODES=${LESAVKA_SERVER_RC_MODES:-${LESAVKA_SERVER_RC_CORE_WEBCAM_MODES}}", "LESAVKA_SERVER_REPO=${LESAVKA_SERVER_REPO:-auto}", "LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US:-${LESAVKA_OUTPUT_DELAY_PROBE_AUDIO_DELAY_US:-0}}", - "LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US:-170000}", + "LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US=${LESAVKA_SERVER_RC_DEFAULT_VIDEO_DELAY_US:-135090}", "LESAVKA_SERVER_RC_MODE_AUDIO_DELAYS_US=${LESAVKA_SERVER_RC_MODE_AUDIO_DELAYS_US:-1280x720@20=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1280x720@30=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1920x1080@20=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US},1920x1080@30=${LESAVKA_SERVER_RC_DEFAULT_AUDIO_DELAY_US}}", - "LESAVKA_SERVER_RC_MODE_DELAYS_US=${LESAVKA_SERVER_RC_MODE_DELAYS_US:-1280x720@20=170000,1280x720@30=170000,1920x1080@20=170000,1920x1080@30=170000}", + "LESAVKA_SERVER_RC_MODE_DELAYS_US=${LESAVKA_SERVER_RC_MODE_DELAYS_US:-1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952}", "LESAVKA_SERVER_RC_MODE_DISCOVERY_SIZES=${LESAVKA_SERVER_RC_MODE_DISCOVERY_SIZES:-1280x720,1920x1080}", "LESAVKA_SERVER_RC_MODE_DISCOVERY_FPS=${LESAVKA_SERVER_RC_MODE_DISCOVERY_FPS:-20,30}", "LESAVKA_SERVER_RC_MODE_DISCOVERY_INCLUDE_REGEX=${LESAVKA_SERVER_RC_MODE_DISCOVERY_INCLUDE_REGEX:-Logitech|BRIO|C9[0-9]+|HD UVC WebCam|USB2[.]0 HD|Integrated Camera|Webcam|Camera}", diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index 0eb3d30..4186a69 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -21,6 +21,8 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { "LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s", "LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS=%s", "LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS=%s", + "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=%s", + "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=%s", "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s", "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s", "LESAVKA_UPSTREAM_PAIR_SLACK_US=%s", @@ -56,7 +58,13 @@ 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=170000")); + assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=135090")); + assert!(SERVER_INSTALL.contains( + "DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0" + )); + assert!(SERVER_INSTALL.contains( + "DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952" + )); assert!( SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"), "video offset should be resolved through stale-baseline migration logic" @@ -72,6 +80,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_SCALAR_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=170000") + ); assert!( SERVER_INSTALL.contains("PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000") ); @@ -84,12 +95,14 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { "install-specific video offset override should bypass stale ambient runtime env" ); assert!( - SERVER_INSTALL.contains("migrating stale upstream audio playout offset to the 0.17 freshness-first planner default"), + SERVER_INSTALL.contains( + "migrating stale upstream audio playout offset to the per-mode MJPEG/UAC default" + ), "installer should not preserve old MJPEG/UVC sync baselines accidentally" ); assert!( SERVER_INSTALL.contains( - "migrating stale upstream video playout offset to the direct UVC/UAC MJPEG sync center" + "migrating stale upstream video playout offset to the per-mode direct UVC/UAC MJPEG sync center" ), "installer should not preserve old video delay baselines accidentally" );