#!/usr/bin/env bash # scripts/manual/run_google_meet_observer_probe.sh # Manual: Google Meet observer-path probe; not part of CI. # # Why this exists: Google Meet blocks reliable unattended automation, but we # still need repeatable evidence about whether distortion appears after raw # Lesavka UVC/UAC output. This harness leaves Meet joining and recording to a # human operator, then injects the same synthetic bundled media used by the # client-to-RCT probe and analyzes the observer-side recording when provided. set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd)" LESAVKA_SERVER_ADDR=${LESAVKA_SERVER_ADDR:-https://38.28.125.112:50051} LESAVKA_TLS_DOMAIN=${LESAVKA_TLS_DOMAIN:-lesavka-server} LESAVKA_GOOGLE_MEET_URL=${LESAVKA_GOOGLE_MEET_URL:-} LESAVKA_MEET_OPEN_URL=${LESAVKA_MEET_OPEN_URL:-0} LESAVKA_MEET_SKIP_PROMPTS=${LESAVKA_MEET_SKIP_PROMPTS:-0} LESAVKA_MEET_START_DELAY_SECONDS=${LESAVKA_MEET_START_DELAY_SECONDS:-0} LESAVKA_MEET_OBSERVER_CAPTURE=${LESAVKA_MEET_OBSERVER_CAPTURE:-} LESAVKA_MEET_ANALYSIS_REQUIRED=${LESAVKA_MEET_ANALYSIS_REQUIRED:-0} LESAVKA_MEET_LOCAL_REVIEW=${LESAVKA_MEET_LOCAL_REVIEW:-1} PROBE_DURATION_SECONDS=${PROBE_DURATION_SECONDS:-20} PROBE_WARMUP_SECONDS=${PROBE_WARMUP_SECONDS:-4} PROBE_PULSE_PERIOD_MS=${PROBE_PULSE_PERIOD_MS:-1000} PROBE_PULSE_WIDTH_MS=${PROBE_PULSE_WIDTH_MS:-120} PROBE_MARKER_TICK_PERIOD=${PROBE_MARKER_TICK_PERIOD:-5} PROBE_EVENT_WIDTH_CODES=${PROBE_EVENT_WIDTH_CODES:-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} LOCAL_OUTPUT_DIR=${LOCAL_OUTPUT_DIR:-/tmp} STAMP="$(date +%Y%m%d-%H%M%S)" ARTIFACT_DIR="${LOCAL_OUTPUT_DIR%/}/lesavka-google-meet-observer-probe-${STAMP}" RUN_LOG="${ARTIFACT_DIR}/google-meet-observer-run.log" VERSION_TXT="${ARTIFACT_DIR}/lesavka-versions.txt" OPERATOR_CHECKLIST="${ARTIFACT_DIR}/operator-checklist.txt" MANUAL_TIMING_JSON="${ARTIFACT_DIR}/manual-timing.json" CLIENT_TIMELINE_JSON="${ARTIFACT_DIR}/client-transport-timeline.json" OBSERVER_CAPTURE_LOCAL="" ANALYSIS_DIR="${ARTIFACT_DIR}/observer-analysis" mkdir -p "${ARTIFACT_DIR}" exec > >(tee -a "${RUN_LOG}") 2>&1 json_now_ns() { python3 - <<'PY' import time print(time.time_ns()) PY } prompt_or_sleep() { local message=$1 local default_sleep=$2 printf '\a' if [[ "${LESAVKA_MEET_SKIP_PROMPTS}" == "1" ]]; then echo "==> ${message}; sleeping ${default_sleep}s because prompts are disabled" sleep "${default_sleep}" return 0 fi if [[ -t 0 ]]; then echo echo "==> ${message}" read -r -p "Press Enter when ready to continue..." _ else echo "==> ${message}; stdin is not interactive, sleeping ${default_sleep}s" sleep "${default_sleep}" fi } write_operator_checklist() { cat >"${OPERATOR_CHECKLIST}" < prebuilding Meet observer probe tools" ( cd "${REPO_ROOT}" cargo build -p lesavka_client \ --bin lesavka-sync-probe \ --bin lesavka-sync-analyze \ --bin lesavka-relayctl ) } print_versions() { echo "==> Lesavka versions under test" local status=0 ( cd "${REPO_ROOT}" LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \ "${REPO_ROOT}/target/debug/lesavka-relayctl" \ --server "${LESAVKA_SERVER_ADDR}" \ version ) >"${VERSION_TXT}" 2>&1 || status=$? sed 's/^/ ↪ /' "${VERSION_TXT}" || true return "${status}" } maybe_open_meet_url() { if [[ -z "${LESAVKA_GOOGLE_MEET_URL}" ]]; then return 0 fi echo "==> Meet URL: ${LESAVKA_GOOGLE_MEET_URL}" if [[ "${LESAVKA_MEET_OPEN_URL}" == "1" ]]; then xdg-open "${LESAVKA_GOOGLE_MEET_URL}" >/dev/null 2>&1 || true fi } validate_start_delay() { if ! [[ "${LESAVKA_MEET_START_DELAY_SECONDS}" =~ ^[0-9]+$ ]]; then echo "LESAVKA_MEET_START_DELAY_SECONDS must be a non-negative integer" >&2 exit 2 fi } run_synthetic_probe() { echo "==> running synthetic bundled media through Google Meet path" local probe_started_ns probe_finished_ns probe_started_ns="$(json_now_ns)" ( cd "${REPO_ROOT}" LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \ "${REPO_ROOT}/target/debug/lesavka-sync-probe" \ --server "${LESAVKA_SERVER_ADDR}" \ --duration-seconds "${PROBE_DURATION_SECONDS}" \ --warmup-seconds "${PROBE_WARMUP_SECONDS}" \ --pulse-period-ms "${PROBE_PULSE_PERIOD_MS}" \ --pulse-width-ms "${PROBE_PULSE_WIDTH_MS}" \ --marker-tick-period "${PROBE_MARKER_TICK_PERIOD}" \ --event-width-codes "${PROBE_EVENT_WIDTH_CODES}" \ --timeline-json "${CLIENT_TIMELINE_JSON}" ) probe_finished_ns="$(json_now_ns)" python3 - "${MANUAL_TIMING_JSON}" "${probe_started_ns}" "${probe_finished_ns}" <<'PY' import json import sys import time path = sys.argv[1] payload = {} try: with open(path, "r", encoding="utf-8") as handle: payload = json.load(handle) except FileNotFoundError: pass payload.update({ "probe_started_unix_ns": int(sys.argv[2]), "probe_finished_unix_ns": int(sys.argv[3]), "timing_note": ( "Observer capture times are operator-confirmed, so freshness from a " "manual recording is approximate unless the recorder supplies its own " "wall-clock start timestamp." ), "updated_at_unix_ns": time.time_ns(), }) with open(path, "w", encoding="utf-8") as handle: json.dump(payload, handle, indent=2, sort_keys=True) handle.write("\n") PY } record_operator_ready_time() { local ready_ns ready_ns="$(json_now_ns)" python3 - "${MANUAL_TIMING_JSON}" "${ready_ns}" <<'PY' import json import sys import time path = sys.argv[1] ready_ns = int(sys.argv[2]) payload = { "observer_recording_confirmed_unix_ns": ready_ns, "created_at_unix_ns": time.time_ns(), } with open(path, "w", encoding="utf-8") as handle: json.dump(payload, handle, indent=2, sort_keys=True) handle.write("\n") PY } copy_observer_capture_if_present() { if [[ -z "${LESAVKA_MEET_OBSERVER_CAPTURE}" ]]; then return 0 fi if [[ ! -f "${LESAVKA_MEET_OBSERVER_CAPTURE}" ]]; then echo "observer recording not found: ${LESAVKA_MEET_OBSERVER_CAPTURE}" >&2 exit 1 fi local ext ext="${LESAVKA_MEET_OBSERVER_CAPTURE##*.}" if [[ "${ext}" == "${LESAVKA_MEET_OBSERVER_CAPTURE}" ]]; then ext="mkv" fi OBSERVER_CAPTURE_LOCAL="${ARTIFACT_DIR}/observer-google-meet-capture.${ext}" cp "${LESAVKA_MEET_OBSERVER_CAPTURE}" "${OBSERVER_CAPTURE_LOCAL}" echo " ↪ observer_capture=${OBSERVER_CAPTURE_LOCAL}" } prompt_for_observer_capture_path() { if [[ -n "${LESAVKA_MEET_OBSERVER_CAPTURE}" ]]; then return 0 fi if [[ "${LESAVKA_MEET_SKIP_PROMPTS}" == "1" || ! -t 0 ]]; then return 0 fi echo read -r -p "Path to observer recording for analysis (blank to skip): " LESAVKA_MEET_OBSERVER_CAPTURE } analyze_observer_capture() { if [[ -z "${OBSERVER_CAPTURE_LOCAL}" ]]; then echo "==> observer capture analysis skipped" echo " ↪ set LESAVKA_MEET_OBSERVER_CAPTURE=/path/to/recording.webm and rerun analysis" return 0 fi echo "==> analyzing observer-side Meet recording" mkdir -p "${ANALYSIS_DIR}" local status=0 ( cd "${REPO_ROOT}" "${REPO_ROOT}/target/debug/lesavka-sync-analyze" \ --event-width-codes "${PROBE_EVENT_WIDTH_CODES}" \ --report-dir "${ANALYSIS_DIR}" \ "${OBSERVER_CAPTURE_LOCAL}" ) || status=$? if (( status != 0 )); then echo " ↪ analyzer failed for observer recording; status=${status}" if [[ "${LESAVKA_MEET_ANALYSIS_REQUIRED}" == "1" ]]; then exit "${status}" fi return 0 fi echo " ↪ observer_report=${ANALYSIS_DIR}/report.txt" } maybe_open_local_review() { if [[ "${LESAVKA_MEET_LOCAL_REVIEW}" != "1" ]]; then return 0 fi if [[ -n "${OBSERVER_CAPTURE_LOCAL}" ]]; then xdg-open "${OBSERVER_CAPTURE_LOCAL}" >/dev/null 2>&1 || true fi if [[ -f "${ANALYSIS_DIR}/report.txt" ]]; then xdg-open "${ANALYSIS_DIR}/report.txt" >/dev/null 2>&1 || true fi } main() { validate_start_delay write_operator_checklist echo "==> Google Meet observer probe" echo " ↪ server_addr=${LESAVKA_SERVER_ADDR}" echo " ↪ meet_url=${LESAVKA_GOOGLE_MEET_URL:-manual}" echo " ↪ artifact_dir=${ARTIFACT_DIR}" echo " ↪ run_log=${RUN_LOG}" echo " ↪ no Google credentials, sudo, or browser automation are used" build_probe_tools print_versions || true maybe_open_meet_url if (( LESAVKA_MEET_START_DELAY_SECONDS > 0 )); then echo "==> start delay: ${LESAVKA_MEET_START_DELAY_SECONDS}s" sleep "${LESAVKA_MEET_START_DELAY_SECONDS}" fi echo "==> operator checklist written" sed 's/^/ | /' "${OPERATOR_CHECKLIST}" prompt_or_sleep "Join Meet on RCT, join as observer here, pin the RCT tile, and start observer recording" 30 record_operator_ready_time run_synthetic_probe prompt_or_sleep "Stop/save the observer recording now" 5 prompt_for_observer_capture_path copy_observer_capture_if_present analyze_observer_capture maybe_open_local_review echo "==> done" echo "artifact_dir: ${ARTIFACT_DIR}" echo "operator_checklist: ${OPERATOR_CHECKLIST}" echo "manual_timing_json: ${MANUAL_TIMING_JSON}" echo "client_timeline_json: ${CLIENT_TIMELINE_JSON}" [[ -n "${OBSERVER_CAPTURE_LOCAL}" ]] && echo "observer_capture: ${OBSERVER_CAPTURE_LOCAL}" [[ -f "${ANALYSIS_DIR}/report.txt" ]] && echo "observer_report: ${ANALYSIS_DIR}/report.txt" } main "$@"