calibration: bake per-mode RC delays

This commit is contained in:
Brad Stein 2026-05-06 03:59:20 -03:00
parent 40287aca33
commit 1b3b8c2cbb
11 changed files with 408 additions and 65 deletions

6
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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_TOUCHPAD_SCALE` | input routing/clipboard override |
| `LESAVKA_UAC_DEV` | server hardware/device 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_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_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_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` | | `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_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_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_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_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_CAMERA_PREVIEW` | client media capture/playback override |
| `LESAVKA_UPLINK_MIC_LEVEL` | client media capture/playback override | | `LESAVKA_UPLINK_MIC_LEVEL` | client media capture/playback override |

View File

@ -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_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz} LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz}
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0 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 LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000
PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000 PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000 PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000
PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=0 PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=0
PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=350000 PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=350000
PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=130000 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 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() { 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 if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then
printf '%s\n' "$LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" printf '%s\n' "$LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"
return 0 return 0
fi 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 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 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 return 0
fi fi
@ -42,19 +85,22 @@ resolve_upstream_audio_playout_offset_us() {
return 0 return 0
fi fi
printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" printf '%s\n' "$default_offset_us"
} }
resolve_upstream_video_playout_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 if [[ -n ${LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-} ]]; then
printf '%s\n' "$LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" printf '%s\n' "$LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US"
return 0 return 0
fi 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 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 direct UVC/UAC MJPEG sync center." >&2 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 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 return 0
fi fi
@ -63,7 +109,7 @@ resolve_upstream_video_playout_offset_us() {
return 0 return 0
fi fi
printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US" printf '%s\n' "$default_offset_us"
} }
manifest_package_version() { 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_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_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_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_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_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}" printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"

View File

@ -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_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_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_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_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_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_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} 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 width=$2
local height=$3 local height=$3
local fps=$4 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 [[ "${LESAVKA_SERVER_RC_RECONFIGURE}" != "0" ]] || return 0
echo "==> reconfiguring ${LESAVKA_SERVER_HOST} UVC gadget for ${mode}" echo "==> reconfiguring ${LESAVKA_SERVER_HOST} UVC gadget for ${mode}"
@ -987,6 +989,8 @@ reconfigure_server_mode() {
LESAVKA_UVC_WIDTH="${width}" \ LESAVKA_UVC_WIDTH="${width}" \
LESAVKA_UVC_HEIGHT="${height}" \ LESAVKA_UVC_HEIGHT="${height}" \
LESAVKA_UVC_FPS="${fps}" \ 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}" bash -c "${LESAVKA_SERVER_RC_RECONFIGURE_COMMAND}"
return 0 return 0
fi fi
@ -1009,7 +1013,11 @@ reconfigure_server_mode() {
"${LESAVKA_SERVER_RC_ALLOW_GADGET_RESET}" \ "${LESAVKA_SERVER_RC_ALLOW_GADGET_RESET}" \
"${LESAVKA_SERVER_RC_FORCE_GADGET_REBUILD}" \ "${LESAVKA_SERVER_RC_FORCE_GADGET_REBUILD}" \
"${LESAVKA_SERVER_RC_RECONFIGURE_SETTLE_SECONDS}" \ "${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 set -euo pipefail
mode=$1 mode=$1
width=$2 width=$2
@ -1021,6 +1029,10 @@ allow_gadget_reset=$7
force_gadget_rebuild=$8 force_gadget_rebuild=$8
settle_seconds=$9 settle_seconds=$9
verbose=${10} verbose=${10}
audio_delay_us=${11}
video_delay_us=${12}
audio_delay_map=${13}
video_delay_map=${14}
set_env_value() { set_env_value() {
local file=$1 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_HEIGHT "${height}"
set_env_value /etc/lesavka/server.env LESAVKA_UVC_FPS "${fps}" 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_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_CODEC "${codec}"
set_env_value /etc/lesavka/uvc.env LESAVKA_UVC_WIDTH "${width}" set_env_value /etc/lesavka/uvc.env LESAVKA_UVC_WIDTH "${width}"
@ -2282,7 +2298,7 @@ for mode in "${modes[@]}"; do
mkdir -p "${mode_dir}" 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}" 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}" wait_tethys_media_ready "${mode}" "${width}" "${height}" "${fps}"
if [[ "${LESAVKA_SERVER_RC_SIGNAL_READY}" != "0" && "${LESAVKA_SERVER_RC_SIGNAL_READY_MODE}" != "separate" ]]; then if [[ "${LESAVKA_SERVER_RC_SIGNAL_READY}" != "0" && "${LESAVKA_SERVER_RC_SIGNAL_READY_MODE}" != "separate" ]]; then

View File

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

View File

@ -10,16 +10,25 @@ use lesavka_common::lesavka::{
use crate::upstream_media_runtime::UpstreamMediaRuntime; use crate::upstream_media_runtime::UpstreamMediaRuntime;
pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0; pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0;
// Direct UVC/UAC output-delay probes against the lab RC target put the pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US: i64 = 162_659;
// server-to-target sync center near 170ms for MJPEG/UVC video. This is an pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US: i64 = 135_090;
// output-path compensation, not a freshness buffer. pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US: i64 = 160_045;
pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 170_000; 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 LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000;
const PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000; const PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000;
const PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US: i64 = 1_260_000; const PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US: i64 = 1_260_000;
const PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0; const PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0;
const PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 350_000; const PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 350_000;
const PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 130_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 PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000;
const PROFILE: &str = "mjpeg"; const PROFILE: &str = "mjpeg";
const FACTORY_CONFIDENCE: &str = "factory"; const FACTORY_CONFIDENCE: &str = "factory";
@ -208,10 +217,29 @@ pub fn calibration_path() -> PathBuf {
} }
fn snapshot_from_env() -> CalibrationSnapshot { fn snapshot_from_env() -> CalibrationSnapshot {
let env_audio = env_i64("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"); let mode = current_uvc_mode();
let env_video = env_i64("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US"); let factory_audio_offset_us = mode
let default_audio_offset_us = env_audio.unwrap_or(FACTORY_MJPEG_AUDIO_OFFSET_US); .as_deref()
let default_video_offset_us = env_video.unwrap_or(FACTORY_MJPEG_VIDEO_OFFSET_US); .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() { let source = if env_audio.is_some() || env_video.is_some() {
"env".to_string() "env".to_string()
} else { } else {
@ -224,8 +252,8 @@ fn snapshot_from_env() -> CalibrationSnapshot {
}; };
CalibrationSnapshot { CalibrationSnapshot {
profile: PROFILE.to_string(), profile: PROFILE.to_string(),
factory_audio_offset_us: FACTORY_MJPEG_AUDIO_OFFSET_US, factory_audio_offset_us,
factory_video_offset_us: FACTORY_MJPEG_VIDEO_OFFSET_US, factory_video_offset_us,
default_audio_offset_us, default_audio_offset_us,
default_video_offset_us, default_video_offset_us,
active_audio_offset_us: default_audio_offset_us, active_audio_offset_us: default_audio_offset_us,
@ -254,8 +282,8 @@ fn parse_snapshot(raw: &str) -> CalibrationSnapshot {
}; };
CalibrationSnapshot { CalibrationSnapshot {
profile: value("profile").unwrap_or(fallback.profile), profile: value("profile").unwrap_or(fallback.profile),
factory_audio_offset_us: FACTORY_MJPEG_AUDIO_OFFSET_US, factory_audio_offset_us: fallback.factory_audio_offset_us,
factory_video_offset_us: FACTORY_MJPEG_VIDEO_OFFSET_US, factory_video_offset_us: fallback.factory_video_offset_us,
default_audio_offset_us: number( default_audio_offset_us: number(
"default_audio_offset_us", "default_audio_offset_us",
fallback.default_audio_offset_us, fallback.default_audio_offset_us,
@ -282,21 +310,12 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
) && state ) && state
.detail .detail
.contains("loaded upstream A/V calibration defaults"); .contains("loaded upstream A/V calibration defaults");
let untouched_legacy_audio = (matches!( let untouched_legacy_audio = (is_stale_audio_offset_us(state.default_audio_offset_us)
state.default_audio_offset_us, || state.default_audio_offset_us == state.factory_audio_offset_us
FACTORY_MJPEG_AUDIO_OFFSET_US || clamped_previous_baseline)
| LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US
| PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US
| PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US
) || clamped_previous_baseline)
&& state.active_audio_offset_us == state.default_audio_offset_us; && state.active_audio_offset_us == state.default_audio_offset_us;
let untouched_legacy_video = matches!( let untouched_legacy_video = is_stale_video_offset_us(state.default_video_offset_us)
state.default_video_offset_us, && state.active_video_offset_us == 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;
if state.profile == PROFILE if state.profile == PROFILE
&& source_allows_migration && source_allows_migration
&& confidence_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_audio_offset_us = state.default_audio_offset_us;
let old_video_offset_us = state.default_video_offset_us; let old_video_offset_us = state.default_video_offset_us;
state.default_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; state.default_audio_offset_us = state.factory_audio_offset_us;
state.active_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; state.active_audio_offset_us = state.factory_audio_offset_us;
state.default_video_offset_us = FACTORY_MJPEG_VIDEO_OFFSET_US; state.default_video_offset_us = state.factory_video_offset_us;
state.active_video_offset_us = FACTORY_MJPEG_VIDEO_OFFSET_US; state.active_video_offset_us = state.factory_video_offset_us;
state.source = "factory".to_string(); state.source = "factory".to_string();
state.confidence = FACTORY_CONFIDENCE.to_string(); state.confidence = FACTORY_CONFIDENCE.to_string();
state.detail = format!( state.detail = format!(
"migrated legacy MJPEG upstream A/V baseline from audio {:+.1}ms/video {:+.1}ms to audio {:+.1}ms/video {:+.1}ms", "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_audio_offset_us as f64 / 1000.0,
old_video_offset_us as f64 / 1000.0, old_video_offset_us as f64 / 1000.0,
FACTORY_MJPEG_AUDIO_OFFSET_US as f64 / 1000.0, state.factory_audio_offset_us as f64 / 1000.0,
FACTORY_MJPEG_VIDEO_OFFSET_US as f64 / 1000.0 state.factory_video_offset_us as f64 / 1000.0
); );
touch(&mut state); touch(&mut state);
} }
@ -351,6 +370,67 @@ fn touch(state: &mut CalibrationSnapshot) {
state.updated_at = Utc::now().to_rfc3339(); 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<i64> {
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<String> {
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<String> {
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::<u32>().ok()?;
height.parse::<u32>().ok()?;
fps.parse::<u32>().ok()?;
Some(())
});
valid.map(|()| trimmed.to_string())
})
}
fn env_mode_offset_us(name: &str, mode: &str) -> Option<i64> {
std::env::var(name)
.ok()
.and_then(|map| lookup_mode_offset_us(&map, mode))
}
fn lookup_mode_offset_us(map: &str, mode: &str) -> Option<i64> {
map.split(',').find_map(|entry| {
let (key, value) = entry.trim().split_once('=')?;
(key.trim() == mode)
.then(|| value.trim().parse::<i64>().ok().map(clamp_offset))
.flatten()
})
}
fn env_i64(name: &str) -> Option<i64> { fn env_i64(name: &str) -> Option<i64> {
std::env::var(name) std::env::var(name)
.ok() .ok()
@ -358,6 +438,32 @@ fn env_i64(name: &str) -> Option<i64> {
.map(clamp_offset) .map(clamp_offset)
} }
fn env_u32(name: &str) -> Option<u32> {
std::env::var(name)
.ok()
.and_then(|value| value.trim().parse::<u32>().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 { fn clamp_offset(value: i64) -> i64 {
value.clamp(-OFFSET_LIMIT_US, OFFSET_LIMIT_US) value.clamp(-OFFSET_LIMIT_US, OFFSET_LIMIT_US)
} }
@ -377,6 +483,18 @@ mod tests {
[ [
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>), ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
("LESAVKA_UPSTREAM_VIDEO_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(); 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] #[test]
fn store_persists_manual_adjustments_and_updates_runtime() { fn store_persists_manual_adjustments_and_updates_runtime() {
let file = NamedTempFile::new().expect("temp calibration file"); let file = NamedTempFile::new().expect("temp calibration file");
@ -535,7 +745,7 @@ mod tests {
runtime.playout_offsets(), runtime.playout_offsets(),
(FACTORY_MJPEG_VIDEO_OFFSET_US, 0) (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"));
}); });
} }

View File

@ -7,9 +7,12 @@ use std::time::Duration;
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::time::Instant; 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 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)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UpstreamMediaKind { pub enum UpstreamMediaKind {
@ -727,17 +730,68 @@ fn upstream_playout_delay() -> Duration {
} }
fn playout_offset_us(kind: UpstreamMediaKind) -> i64 { fn playout_offset_us(kind: UpstreamMediaKind) -> i64 {
let name = match kind { let (scalar_name, mode_map_name, factory_map, factory_offset_us) = match kind {
UpstreamMediaKind::Camera => "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", UpstreamMediaKind::Camera => (
UpstreamMediaKind::Microphone => "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", "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<String> {
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<i64> {
std::env::var(name)
.ok()
.and_then(|map| lookup_mode_offset_us(&map, mode))
}
fn lookup_mode_offset_us(map: &str, mode: &str) -> Option<i64> {
map.split(',').find_map(|entry| {
let (key, value) = entry.trim().split_once('=')?;
(key.trim() == mode)
.then(|| value.trim().parse::<i64>().ok())
.flatten()
})
}
fn env_i64(name: &str) -> Option<i64> {
std::env::var(name) std::env::var(name)
.ok() .ok()
.and_then(|value| value.trim().parse::<i64>().ok()) .and_then(|value| value.trim().parse::<i64>().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<u32> {
}) std::env::var(name)
.ok()
.and_then(|value| value.trim().parse::<u32>().ok())
} }
fn apply_offset(instant: Instant, offset_us: i64) -> Instant { fn apply_offset(instant: Instant, offset_us: i64) -> Instant {

View File

@ -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_RC_MODES=${LESAVKA_SERVER_RC_MODES:-${LESAVKA_SERVER_RC_CORE_WEBCAM_MODES}}",
"LESAVKA_SERVER_REPO=${LESAVKA_SERVER_REPO:-auto}", "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_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_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_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_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}", "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}",

View File

@ -21,6 +21,8 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s", "LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s",
"LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS=%s", "LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS=%s",
"LESAVKA_UPSTREAM_STARTUP_TIMEOUT_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_AUDIO_PLAYOUT_OFFSET_US=%s",
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s", "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s",
"LESAVKA_UPSTREAM_PAIR_SLACK_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_MAX_LIVE_LAG_MS:-1000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS:-60000}")); 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_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!( assert!(
SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"), SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"),
"video offset should be resolved through stale-baseline migration logic" "video offset should be resolved through stale-baseline migration logic"
@ -72,6 +80,9 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
assert!( assert!(
SERVER_INSTALL.contains("PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=130000") 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!( assert!(
SERVER_INSTALL.contains("PREVIOUS_OVERSHOT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000") 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" "install-specific video offset override should bypass stale ambient runtime env"
); );
assert!( 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" "installer should not preserve old MJPEG/UVC sync baselines accidentally"
); );
assert!( assert!(
SERVER_INSTALL.contains( 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" "installer should not preserve old video delay baselines accidentally"
); );