probe: pause live upstream during synthetic rct runs

This commit is contained in:
Brad Stein 2026-05-17 17:54:56 -03:00
parent 5667608707
commit 32b058973e
6 changed files with 89 additions and 22 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.55" version = "0.22.56"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.55" version = "0.22.56"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.55" version = "0.22.56"
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.22.55" version = "0.22.56"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

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

View File

@ -21,6 +21,7 @@ DEFAULT_JPEG_QUALITY = 82
HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC = 8000 HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC = 8000
DEFAULT_ISOCHRONOUS_LIMIT_PCT = 85 DEFAULT_ISOCHRONOUS_LIMIT_PCT = 85
DEFAULT_UVC_MAX_PACKET = 1024 DEFAULT_UVC_MAX_PACKET = 1024
DEFAULT_MEDIA_CONTROL_PATH = "/tmp/lesavka-media.control"
MARKER_BITS = 32 MARKER_BITS = 32
MARKER_COLUMNS = 16 MARKER_COLUMNS = 16
CADENCE_REASONS = {"frame_repeat", "frame_gap", "frame_backwards"} CADENCE_REASONS = {"frame_repeat", "frame_gap", "frame_backwards"}
@ -53,6 +54,16 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--artifact-dir", default="") parser.add_argument("--artifact-dir", default="")
parser.add_argument("--remote-rct-dir", default="") parser.add_argument("--remote-rct-dir", default="")
parser.add_argument("--remote-inject-dir", default="") parser.add_argument("--remote-inject-dir", default="")
parser.add_argument(
"--pause-local-live-upstream",
action="store_true",
help="temporarily write camera=0 to the local Lesavka media control file so a live client does not preempt the synthetic injector",
)
parser.add_argument(
"--media-control-path",
default=os.environ.get("LESAVKA_MEDIA_CONTROL", DEFAULT_MEDIA_CONTROL_PATH),
help=f"local live-media control file used with --pause-local-live-upstream (default: {DEFAULT_MEDIA_CONTROL_PATH})",
)
parser.add_argument( parser.add_argument(
"--capture-before-inject", "--capture-before-inject",
action="store_true", action="store_true",
@ -155,6 +166,48 @@ def default_artifact_dir(mode: str) -> pathlib.Path:
return pathlib.Path("artifacts/synthetic-rct") / f"{safe_mode}-{timestamp()}" return pathlib.Path("artifacts/synthetic-rct") / f"{safe_mode}-{timestamp()}"
def media_control_with_camera(raw: str | None, enabled: bool) -> str:
tokens = raw.split() if raw else []
rendered: list[str] = []
saw_camera = False
saw_microphone = False
saw_audio = False
for token in tokens:
key, sep, _value = token.partition("=")
if sep and key == "camera":
rendered.append(f"camera={1 if enabled else 0}")
saw_camera = True
else:
rendered.append(token)
saw_microphone = saw_microphone or (sep and key in {"microphone", "mic"})
saw_audio = saw_audio or (sep and key in {"audio", "speaker"})
if not saw_camera:
rendered.insert(0, f"camera={1 if enabled else 0}")
if not saw_microphone:
rendered.append("microphone=1")
if not saw_audio:
rendered.append("audio=1")
return " ".join(rendered) + "\n"
def pause_local_live_upstream(args: argparse.Namespace) -> tuple[pathlib.Path, bytes | None]:
path = pathlib.Path(args.media_control_path)
original = path.read_bytes() if path.exists() else None
raw = original.decode(errors="replace") if original is not None else None
path.write_text(media_control_with_camera(raw, False))
print(f"paused local live camera upstream via {path}", file=sys.stderr)
time.sleep(0.5)
return path, original
def restore_local_live_upstream(path: pathlib.Path, original: bytes | None) -> None:
if original is None:
path.unlink(missing_ok=True)
else:
path.write_bytes(original)
print(f"restored local live media control at {path}", file=sys.stderr)
def run_remote_orchestrated(args: argparse.Namespace) -> int: def run_remote_orchestrated(args: argparse.Namespace) -> int:
if (not args.inject_host and not args.local_inject) or not args.rct_host: if (not args.inject_host and not args.local_inject) or not args.rct_host:
raise SystemExit( raise SystemExit(
@ -273,6 +326,8 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
"inject_host": args.inject_host, "inject_host": args.inject_host,
"local_inject": args.local_inject, "local_inject": args.local_inject,
"rct_host": args.rct_host, "rct_host": args.rct_host,
"pause_local_live_upstream": args.pause_local_live_upstream,
"media_control_path": args.media_control_path,
}, },
indent=2, indent=2,
sort_keys=True, sort_keys=True,
@ -332,24 +387,31 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
capture: subprocess.Popen[Any] | None = None capture: subprocess.Popen[Any] | None = None
diagnosis: list[str] = [] diagnosis: list[str] = []
if args.capture_before_inject: paused_control: tuple[pathlib.Path, bytes | None] | None = None
capture = start_capture() try:
time.sleep(1.0) if args.pause_local_live_upstream:
inject = start_inject() paused_control = pause_local_live_upstream(args)
capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject) if args.capture_before_inject:
else:
inject = start_inject()
time.sleep(max(0.0, args.inject_warmup_s))
inject_rc = inject.poll()
if inject_rc is not None:
capture_rc = None
diagnosis.append(
"synthetic uplink exited before capture warmup completed; disconnect the live client or pause upstream webcam before running the isolated probe"
)
print(f"synthetic uplink exited before capture started rc={inject_rc}", file=sys.stderr)
else:
capture = start_capture() capture = start_capture()
time.sleep(1.0)
inject = start_inject()
capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject) capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject)
else:
inject = start_inject()
time.sleep(max(0.0, args.inject_warmup_s))
inject_rc = inject.poll()
if inject_rc is not None:
capture_rc = None
diagnosis.append(
"synthetic uplink exited before capture warmup completed; disconnect the live client or pause upstream webcam before running the isolated probe"
)
print(f"synthetic uplink exited before capture started rc={inject_rc}", file=sys.stderr)
else:
capture = start_capture()
capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject)
finally:
if paused_control is not None:
restore_local_live_upstream(*paused_control)
local_capture = artifact_dir / "capture" local_capture = artifact_dir / "capture"
local_inject = artifact_dir / "inject" local_inject = artifact_dir / "inject"
if capture is not None: if capture is not None:

View File

@ -16,7 +16,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.55" version = "0.22.56"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -41,6 +41,8 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"--capture-before-inject", "--capture-before-inject",
"--inject-warmup-s", "--inject-warmup-s",
"--capture-finish-grace-s", "--capture-finish-grace-s",
"--pause-local-live-upstream",
"--media-control-path",
"--jpeg-quality", "--jpeg-quality",
"--inject-max-frame-bytes", "--inject-max-frame-bytes",
"--stream-analyze", "--stream-analyze",
@ -93,6 +95,9 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"synthetic uplink completed but RCT capture did not finish", "synthetic uplink completed but RCT capture did not finish",
"synthetic injector was preempted after sending", "synthetic injector was preempted after sending",
"synthetic uplink exited before capture warmup completed", "synthetic uplink exited before capture warmup completed",
"paused local live camera upstream",
"restored local live media control",
"media_control_with_camera",
"max_lower_mae", "max_lower_mae",
"ffmpeg", "ffmpeg",
"v4l2", "v4l2",