fix(sync): default uac clock alignment off

This commit is contained in:
Brad Stein 2026-04-25 04:26:51 -03:00
parent 74706aaf32
commit c482f44fbc
7 changed files with 156 additions and 30 deletions

6
Cargo.lock generated
View File

@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.13.13" version = "0.13.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1676,7 +1676,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.13.13" version = "0.13.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.13.13" version = "0.13.14"
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.13.13" version = "0.13.14"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

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

View File

@ -187,7 +187,7 @@ Hardware-facing assumptions belong near the code that uses them; this file is th
| `LESAVKA_UAC_DEV` | server hardware/device override | | `LESAVKA_UAC_DEV` | server hardware/device override |
| `LESAVKA_UAC_HDMI_COMPENSATION_US` | server HDMI audio sink latency override | | `LESAVKA_UAC_HDMI_COMPENSATION_US` | server HDMI audio sink latency override |
| `LESAVKA_UAC_LATENCY_TIME_US` | server audio sink latency override | | `LESAVKA_UAC_LATENCY_TIME_US` | server audio sink latency override |
| `LESAVKA_UAC_SESSION_CLOCK_ALIGN` | server audio sink clock-alignment override | | `LESAVKA_UAC_SESSION_CLOCK_ALIGN` | server audio sink clock-alignment override; defaults to `0` |
| `LESAVKA_TEST_CAM_U32` | test/build contract variable; not runtime operator config | | `LESAVKA_TEST_CAM_U32` | test/build contract variable; not runtime operator config |
| `LESAVKA_TEST_CAP_CAMERA` | test/build contract variable; not runtime operator config | | `LESAVKA_TEST_CAP_CAMERA` | test/build contract variable; not runtime operator config |
| `LESAVKA_TEST_CAP_MIC` | test/build contract variable; not runtime operator config | | `LESAVKA_TEST_CAP_MIC` | test/build contract variable; not runtime operator config |
@ -212,7 +212,7 @@ Hardware-facing assumptions belong near the code that uses them; this file is th
| `LESAVKA_TEST_VIDEO_SOURCE` | test/build contract variable; not runtime operator config | | `LESAVKA_TEST_VIDEO_SOURCE` | test/build contract variable; not runtime operator config |
| `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` | disable only for A/B diagnosing UAC sink timing vs silence | | `LESAVKA_UAC_SESSION_CLOCK_ALIGN` | server audio sink clock-alignment override; `0` is the host-validated default |
| `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 |
| `LESAVKA_USB_RECOVERY_` | USB recovery timing override | | `LESAVKA_USB_RECOVERY_` | USB recovery timing override |

View File

@ -443,7 +443,7 @@ fi
printf 'LESAVKA_UAC_DEV=%s\n' "${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}" printf 'LESAVKA_UAC_DEV=%s\n' "${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}"
printf 'LESAVKA_ALSA_DEV=%s\n' "${LESAVKA_ALSA_DEV:-hw:UAC2Gadget,0}" printf 'LESAVKA_ALSA_DEV=%s\n' "${LESAVKA_ALSA_DEV:-hw:UAC2Gadget,0}"
printf 'LESAVKA_UAC_HDMI_COMPENSATION_US=%s\n' "${LESAVKA_UAC_HDMI_COMPENSATION_US:-0}" printf 'LESAVKA_UAC_HDMI_COMPENSATION_US=%s\n' "${LESAVKA_UAC_HDMI_COMPENSATION_US:-0}"
printf 'LESAVKA_UAC_SESSION_CLOCK_ALIGN=%s\n' "${LESAVKA_UAC_SESSION_CLOCK_ALIGN:-1}" printf 'LESAVKA_UAC_SESSION_CLOCK_ALIGN=%s\n' "${LESAVKA_UAC_SESSION_CLOCK_ALIGN:-0}"
} | sudo tee /etc/lesavka/server.env >/dev/null } | sudo tee /etc/lesavka/server.env >/dev/null
echo "==> 6a. Systemd units - lesavka-core" echo "==> 6a. Systemd units - lesavka-core"

View File

@ -21,8 +21,10 @@ LOCAL_OUTPUT_DIR=${LOCAL_OUTPUT_DIR:-"${REPO_ROOT}/tmp"}
VIDEO_SIZE=${VIDEO_SIZE:-1280x720} VIDEO_SIZE=${VIDEO_SIZE:-1280x720}
VIDEO_FPS=${VIDEO_FPS:-30} VIDEO_FPS=${VIDEO_FPS:-30}
VIDEO_FORMAT=${VIDEO_FORMAT:-mjpeg} VIDEO_FORMAT=${VIDEO_FORMAT:-mjpeg}
REMOTE_CAPTURE_STACK=${REMOTE_CAPTURE_STACK:-auto}
REMOTE_AUDIO_SOURCE=${REMOTE_AUDIO_SOURCE:-auto} REMOTE_AUDIO_SOURCE=${REMOTE_AUDIO_SOURCE:-auto}
REMOTE_AUDIO_QUIESCE_USER_AUDIO=${REMOTE_AUDIO_QUIESCE_USER_AUDIO:-auto} REMOTE_AUDIO_QUIESCE_USER_AUDIO=${REMOTE_AUDIO_QUIESCE_USER_AUDIO:-auto}
ANALYSIS_NORMALIZE=${ANALYSIS_NORMALIZE:-1}
SSH_OPTS=${SSH_OPTS:-"-o BatchMode=yes -o ConnectTimeout=5"} SSH_OPTS=${SSH_OPTS:-"-o BatchMode=yes -o ConnectTimeout=5"}
LOCAL_AUDIO_SANITY=${LOCAL_AUDIO_SANITY:-1} LOCAL_AUDIO_SANITY=${LOCAL_AUDIO_SANITY:-1}
PROBE_PREBUILD=${PROBE_PREBUILD:-1} PROBE_PREBUILD=${PROBE_PREBUILD:-1}
@ -62,6 +64,7 @@ ssh ${SSH_OPTS} "${TETHYS_HOST}" bash -s -- \
"${VIDEO_SIZE}" \ "${VIDEO_SIZE}" \
"${VIDEO_FPS}" \ "${VIDEO_FPS}" \
"${VIDEO_FORMAT}" \ "${VIDEO_FORMAT}" \
"${REMOTE_CAPTURE_STACK}" \
"${REMOTE_AUDIO_SOURCE}" \ "${REMOTE_AUDIO_SOURCE}" \
"${REMOTE_AUDIO_QUIESCE_USER_AUDIO}" <<'REMOTE_CAPTURE_SCRIPT' & "${REMOTE_AUDIO_QUIESCE_USER_AUDIO}" <<'REMOTE_CAPTURE_SCRIPT' &
set -euo pipefail set -euo pipefail
@ -70,8 +73,9 @@ capture_seconds=$2
video_size=$3 video_size=$3
video_fps=$4 video_fps=$4
video_format=$5 video_format=$5
remote_audio_source=$6 remote_capture_stack=$6
remote_audio_quiesce_user_audio=$7 remote_audio_source=$7
remote_audio_quiesce_user_audio=$8
rm -f "${remote_capture}" rm -f "${remote_capture}"
video_args=(-f video4linux2 -framerate "${video_fps}" -video_size "${video_size}") video_args=(-f video4linux2 -framerate "${video_fps}" -video_size "${video_size}")
@ -107,24 +111,102 @@ resolve_pulse_source() {
' '
} }
audio_mode="alsa" resolve_pw_audio_target() {
if ! command -v pw-dump >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then
return 1
fi
pw-dump | python3 - <<'PY'
import json
import sys
try:
objs = json.load(sys.stdin)
except Exception:
raise SystemExit(1)
for obj in objs:
if obj.get("type") != "PipeWire:Interface:Node":
continue
props = (obj.get("info") or {}).get("props") or {}
if props.get("media.class") != "Audio/Source":
continue
serial = props.get("object.serial")
name = props.get("node.name", "")
desc = props.get("node.description", "")
if serial is None:
continue
if "Lesavka_Composite" in name or "Lesavka Composite" in desc:
print(serial)
raise SystemExit(0)
raise SystemExit(1)
PY
}
capture_mode="alsa"
alsa_audio_dev="hw:3,0" alsa_audio_dev="hw:3,0"
pulse_source="" pulse_source=""
if [[ "${remote_audio_source}" == "auto" ]]; then pw_audio_target=""
if pulse_source="$(resolve_pulse_source)"; then
audio_mode="pulse" case "${remote_capture_stack}" in
else auto)
printf 'PipeWire Lesavka source not found; falling back to hw:3,0\n' >&2 if command -v pw-record >/dev/null 2>&1 \
fi && command -v pw-v4l2 >/dev/null 2>&1 \
elif [[ "${remote_audio_source}" == pulse:* ]]; then && pw_audio_target="$(resolve_pw_audio_target)"; then
audio_mode="pulse" capture_mode="pwpipe"
pulse_source="${remote_audio_source#pulse:}" elif [[ "${remote_audio_source}" == "auto" ]]; then
elif [[ "${remote_audio_source}" == alsa:* ]]; then if pulse_source="$(resolve_pulse_source)"; then
alsa_audio_dev="${remote_audio_source#alsa:}" capture_mode="pulse"
else else
printf 'unsupported REMOTE_AUDIO_SOURCE=%s\n' "${remote_audio_source}" >&2 printf 'PipeWire Lesavka source not found; falling back to hw:3,0\n' >&2
exit 64 fi
fi elif [[ "${remote_audio_source}" == pulse:* ]]; then
capture_mode="pulse"
pulse_source="${remote_audio_source#pulse:}"
elif [[ "${remote_audio_source}" == alsa:* ]]; then
alsa_audio_dev="${remote_audio_source#alsa:}"
else
printf 'unsupported REMOTE_AUDIO_SOURCE=%s\n' "${remote_audio_source}" >&2
exit 64
fi
;;
pwpipe)
if ! command -v pw-record >/dev/null 2>&1 || ! command -v pw-v4l2 >/dev/null 2>&1; then
printf 'REMOTE_CAPTURE_STACK=pwpipe requires pw-record and pw-v4l2\n' >&2
exit 64
fi
pw_audio_target="$(resolve_pw_audio_target)" || {
printf 'PipeWire Lesavka capture target not found for REMOTE_CAPTURE_STACK=pwpipe\n' >&2
exit 64
}
capture_mode="pwpipe"
;;
pulse)
if [[ "${remote_audio_source}" == pulse:* ]]; then
pulse_source="${remote_audio_source#pulse:}"
elif [[ "${remote_audio_source}" == "auto" ]]; then
pulse_source="$(resolve_pulse_source)" || {
printf 'PipeWire Lesavka source not found for REMOTE_CAPTURE_STACK=pulse\n' >&2
exit 64
}
else
pulse_source="${remote_audio_source}"
fi
capture_mode="pulse"
;;
alsa)
if [[ "${remote_audio_source}" == alsa:* ]]; then
alsa_audio_dev="${remote_audio_source#alsa:}"
elif [[ "${remote_audio_source}" != "auto" ]]; then
alsa_audio_dev="${remote_audio_source}"
fi
capture_mode="alsa"
;;
*)
printf 'unsupported REMOTE_CAPTURE_STACK=%s\n' "${remote_capture_stack}" >&2
exit 64
;;
esac
quiesce_for_alsa=0 quiesce_for_alsa=0
case "${remote_audio_quiesce_user_audio}" in case "${remote_audio_quiesce_user_audio}" in
@ -145,13 +227,32 @@ case "${remote_audio_quiesce_user_audio}" in
;; ;;
esac esac
if [[ "${quiesce_for_alsa}" == "1" ]]; then if [[ "${capture_mode}" == "alsa" && "${quiesce_for_alsa}" == "1" ]]; then
printf 'quiescing Tethys user audio before raw ALSA capture\n' >&2 printf 'quiescing Tethys user audio before raw ALSA capture\n' >&2
quiesce_user_audio quiesce_user_audio
trap restore_user_audio EXIT trap restore_user_audio EXIT
fi fi
if [[ "${audio_mode}" == "pulse" ]]; then if [[ "${capture_mode}" == "pwpipe" ]]; then
printf 'using PipeWire-native mux capture target serial: %s\n' "${pw_audio_target}" >&2
timeout "${capture_seconds}" pw-record \
--target "${pw_audio_target}" \
--rate 48000 \
--channels 2 \
--format s16 \
--raw - \
| pw-v4l2 ffmpeg -hide_banner -loglevel error -y \
-thread_queue_size 1024 \
"${video_args[@]}" \
-i /dev/video0 \
-thread_queue_size 1024 \
-f s16le -ar 48000 -ac 2 \
-i pipe:0 \
-t "${capture_seconds}" \
-c:v copy \
-c:a pcm_s16le \
"${remote_capture}"
elif [[ "${capture_mode}" == "pulse" ]]; then
printf 'using Pulse source: %s\n' "${pulse_source}" >&2 printf 'using Pulse source: %s\n' "${pulse_source}" >&2
ffmpeg -hide_banner -loglevel error -y \ ffmpeg -hide_banner -loglevel error -y \
-thread_queue_size 1024 \ -thread_queue_size 1024 \
@ -196,8 +297,33 @@ capture_status=0
wait "${capture_pid}" || capture_status=$? wait "${capture_pid}" || capture_status=$?
if ssh ${SSH_OPTS} "${TETHYS_HOST}" "test -f '${REMOTE_CAPTURE}'"; then if ssh ${SSH_OPTS} "${TETHYS_HOST}" "test -f '${REMOTE_CAPTURE}'"; then
remote_fetch_capture="${REMOTE_CAPTURE}"
if [[ "${ANALYSIS_NORMALIZE}" != "0" ]]; then
remote_fetch_capture="${REMOTE_CAPTURE%.mkv}-analysis.mkv"
echo "==> normalizing remote capture to CFR for analysis"
normalize_status=0
ssh ${SSH_OPTS} "${TETHYS_HOST}" bash -s -- \
"${REMOTE_CAPTURE}" \
"${remote_fetch_capture}" \
"${VIDEO_FPS}" <<'REMOTE_NORMALIZE_SCRIPT' || normalize_status=$?
set -euo pipefail
src=$1
dst=$2
fps=$3
ffmpeg -hide_banner -loglevel error -y \
-i "${src}" \
-vf "fps=${fps}" \
-c:v libx264 -preset ultrafast -crf 12 -g 1 -pix_fmt yuv420p \
-c:a pcm_s16le \
"${dst}"
REMOTE_NORMALIZE_SCRIPT
if [[ "${normalize_status}" -ne 0 ]]; then
echo "remote CFR normalization failed; falling back to raw capture" >&2
remote_fetch_capture="${REMOTE_CAPTURE}"
fi
fi
echo "==> fetching capture back to ${LOCAL_CAPTURE}" echo "==> fetching capture back to ${LOCAL_CAPTURE}"
scp ${SSH_OPTS} "${TETHYS_HOST}:${REMOTE_CAPTURE}" "${LOCAL_CAPTURE}" scp ${SSH_OPTS} "${TETHYS_HOST}:${remote_fetch_capture}" "${LOCAL_CAPTURE}"
fi fi
if [[ "${probe_status}" -ne 0 ]]; then if [[ "${probe_status}" -ne 0 ]]; then

View File

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