320 lines
10 KiB
Bash
Executable File
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 "$@"
|