lesavka/scripts/manual/run_google_meet_observer_probe.sh

320 lines
10 KiB
Bash
Executable File

#!/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}" <<EOF
Google Meet observer probe checklist
Goal:
Determine whether audio/video corruption starts inside Google Meet/WebRTC,
after raw Lesavka UVC/UAC output has already been proven clean.
Manual setup:
1. On the RCT/Tethys browser, join the Meet using Lesavka Camera and Lesavka Microphone.
2. On this workstation, join the same Meet as an observer: muted mic, camera off.
3. Pin or spotlight the RCT/Lesavka participant so the synthetic flash video is large.
4. Disable captions and any visual overlays that could cover the tile.
5. Start an observer-side recording that captures both Meet video and Meet audio.
6. Return to this terminal and continue the probe.
Preferred observer recording:
A local screen/tab recording from this workstation is best. Google cloud
recording is usable, but it adds another processing layer.
Analysis:
If LESAVKA_MEET_OBSERVER_CAPTURE is set, this script copies and analyzes it.
Otherwise, rerun or invoke lesavka-sync-analyze manually against the recording.
Meet URL:
${LESAVKA_GOOGLE_MEET_URL:-not provided}
EOF
}
build_probe_tools() {
echo "==> 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 "$@"