Landing: telemetry collector, doomguard watermark, SIGNAL centering

- telemetry.sh: bash collector for static device + OBS profile specs.
  Default mode wraps telemetry.json → telemetry.js (script-tag-loaded
  global, dodging file:// fetch CORS in CEF). --collect re-reads
  hardware + OBS basic.ini / streamEncoder.json.
- index.html: footer subtitle now rotates real CPU/MEM/GPU/OBS data
  with soft 400ms crossfade; SIGNAL group re-centered via 3-col grid;
  doomguard watermark behind everything with periodic "tune-in"
  spikes through the noise canvas.
- telemetry.json: hand-edited cosmetic version of collected specs.
- .gitignore: track telemetry.json (source), ignore telemetry.js
  (build artifact) and *.manual backups.
This commit is contained in:
Jakub Zych
2026-04-26 00:47:59 +02:00
parent 6decec66cc
commit 704e126adc
4 changed files with 450 additions and 4 deletions

237
landing/telemetry.sh Executable file
View File

@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# Static device + OBS config snapshot for the OBS Landing overlay.
#
# Two modes:
# ./telemetry.sh — wrap telemetry.json → telemetry.js
# (idempotent; preserves any hand edits to the JSON)
# ./telemetry.sh --collect — re-read hardware + OBS config, overwrite
# telemetry.json, then wrap → telemetry.js
#
# Use --collect after a hardware change, kernel update, or OBS settings change.
# For cosmetic tweaks just edit telemetry.json by hand and re-run with no args.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OBS_DIR="$(cd "$DIR/.." && pwd)"
JSON="$DIR/telemetry.json"
JS="$DIR/telemetry.js"
sanitize() {
printf '%s' "${1:-}" \
| tr -d '"\\' \
| tr '\n\r\t' ' ' \
| sed 's/ */ /g; s/^ //; s/ $//'
}
emit_str() { [[ -z "${1:-}" ]] && printf 'null' || printf '"%s"' "$1"; }
emit_num() { [[ -z "${1:-}" ]] && printf 'null' || printf '%s' "$1"; }
# ── INI reader (section-scoped) ─────────────────────
ini_get() {
# Usage: ini_get FILE SECTION KEY → prints value (empty if missing)
local file="$1" section="$2" key="$3"
[[ -f "$file" ]] || return 0
awk -v section="[$section]" -v key="$key" '
$0 == section { in_section = 1; next }
/^\[/ { in_section = 0; next }
in_section && index($0, key"=") == 1 {
sub("^[^=]*=", "")
print
exit
}
' "$file"
}
# Friendlier names for the encoder ids OBS stores in basic.ini
prettify_encoder() {
case "${1:-}" in
obs_x264|x264) echo "x264";;
jim_nvenc|ffmpeg_nvenc|obs_nvenc_h264_tex|obs_nvenc_h264_soft) echo "NVENC H.264";;
obs_nvenc_hevc_tex|obs_nvenc_hevc_soft) echo "NVENC HEVC";;
obs_nvenc_av1_tex|obs_nvenc_av1_soft) echo "NVENC AV1";;
obs_qsv11) echo "QSV";;
*) echo "${1:-}";;
esac
}
# Pull a top-level field out of a flat JSON object via python3.
json_field() {
local file="$1" field="$2"
[[ -f "$file" ]] || return 0
python3 - "$file" "$field" <<'PY' 2>/dev/null || true
import json, sys
try:
with open(sys.argv[1]) as f:
d = json.load(f)
v = d.get(sys.argv[2])
print('' if v is None else v)
except Exception:
pass
PY
}
# ── OBS config block ────────────────────────────────
collect_obs() {
local user_ini="$OBS_DIR/user.ini"
[[ -f "$user_ini" ]] || { echo "null"; return; }
local profile renderer
profile=$(ini_get "$user_ini" "Basic" "Profile")
renderer=$(ini_get "$user_ini" "Video" "Renderer")
[[ -z "$profile" ]] && profile="Untitled"
local profile_dir="$OBS_DIR/basic/profiles/$profile"
local ini="$profile_dir/basic.ini"
[[ -f "$ini" ]] || { echo "null"; return; }
local mode base_w base_h out_w out_h fps
mode=$(ini_get "$ini" "Output" "Mode")
base_w=$(ini_get "$ini" "Video" "BaseCX")
base_h=$(ini_get "$ini" "Video" "BaseCY")
out_w=$(ini_get "$ini" "Video" "OutputCX")
out_h=$(ini_get "$ini" "Video" "OutputCY")
fps=$(ini_get "$ini" "Video" "FPSInt")
local color_fmt color_space color_range
color_fmt=$(ini_get "$ini" "Video" "ColorFormat")
color_space=$(ini_get "$ini" "Video" "ColorSpace")
color_range=$(ini_get "$ini" "Video" "ColorRange")
local sample_rate ch_setup
sample_rate=$(ini_get "$ini" "Audio" "SampleRate")
ch_setup=$(ini_get "$ini" "Audio" "ChannelSetup")
# Stream encoder + bitrate. Path differs Simple vs Advanced.
local enc_raw="" bitrate="" rc="" keyint="" h264_profile=""
if [[ "$mode" == "Advanced" ]]; then
enc_raw=$(ini_get "$ini" "AdvOut" "Encoder")
local sej="$profile_dir/streamEncoder.json"
rc=$(json_field "$sej" "rate_control")
bitrate=$(json_field "$sej" "bitrate")
keyint=$(json_field "$sej" "keyint_sec")
h264_profile=$(json_field "$sej" "profile")
else
enc_raw=$(ini_get "$ini" "SimpleOutput" "StreamEncoder")
bitrate=$(ini_get "$ini" "SimpleOutput" "VBitrate")
fi
local enc_pretty
enc_pretty=$(prettify_encoder "$enc_raw")
profile=$(sanitize "$profile")
renderer=$(sanitize "$renderer")
mode=$(sanitize "$mode")
color_fmt=$(sanitize "$color_fmt")
color_space=$(sanitize "$color_space")
color_range=$(sanitize "$color_range")
ch_setup=$(sanitize "$ch_setup")
enc_pretty=$(sanitize "$enc_pretty")
rc=$(sanitize "$rc")
h264_profile=$(sanitize "$h264_profile")
cat <<JSON
{
"profile": $(emit_str "$profile"),
"renderer": $(emit_str "$renderer"),
"outputMode": $(emit_str "$mode"),
"canvas": { "w": $(emit_num "$base_w"), "h": $(emit_num "$base_h") },
"output": { "w": $(emit_num "$out_w"), "h": $(emit_num "$out_h") },
"fps": $(emit_num "$fps"),
"color": {
"format": $(emit_str "$color_fmt"),
"space": $(emit_str "$color_space"),
"range": $(emit_str "$color_range")
},
"stream": {
"encoder": $(emit_str "$enc_pretty"),
"rateControl": $(emit_str "$rc"),
"bitrateKbps": $(emit_num "$bitrate"),
"keyintSec": $(emit_num "$keyint"),
"profile": $(emit_str "$h264_profile")
},
"audio": {
"sampleRateHz": $(emit_num "$sample_rate"),
"channels": $(emit_str "$ch_setup")
}
}
JSON
}
# ── Hardware specs ──────────────────────────────────
collect() {
local cpu_model cpu_threads
cpu_model=$(awk -F': ' '/^model name/ {print $2; exit}' /proc/cpuinfo)
cpu_threads=$(nproc)
local mem_total_kb mem_total_g
mem_total_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
mem_total_g=$(awk -v k="$mem_total_kb" 'BEGIN {printf "%.1f", k/1024/1024}')
local kernel; kernel=$(uname -r)
local gpu_name="" gpu_vram_total_mb=""
if command -v nvidia-smi >/dev/null 2>&1; then
local q
q=$(nvidia-smi --query-gpu=name,memory.total \
--format=csv,noheader,nounits 2>/dev/null | head -n1 || true)
if [[ -n "$q" ]]; then
IFS=',' read -r gpu_name gpu_vram_total_mb <<<"$q"
gpu_name=$(sanitize "$gpu_name")
gpu_vram_total_mb=$(printf '%s' "$gpu_vram_total_mb" | tr -d ' ')
fi
fi
cpu_model=$(sanitize "$cpu_model")
kernel=$(sanitize "$kernel")
local now_iso; now_iso=$(date -u +%FT%TZ)
local obs_block; obs_block=$(collect_obs)
local tmp="$JSON.tmp.$$"
cat > "$tmp" <<JSON
{
"collectedAt": "$now_iso",
"cpu": {
"model": $(emit_str "$cpu_model"),
"threads": $(emit_num "$cpu_threads")
},
"mem": {
"totalGB": $(emit_num "$mem_total_g")
},
"host": {
"kernel": $(emit_str "$kernel")
},
"gpu": {
"name": $(emit_str "$gpu_name"),
"vramTotalMB": $(emit_num "$gpu_vram_total_mb")
},
"obs": $obs_block
}
JSON
# Validate before promoting — catches any malformed substitution.
if ! python3 -m json.tool "$tmp" >/dev/null 2>&1; then
echo "collect: produced invalid JSON, leaving $tmp for inspection" >&2
exit 1
fi
mv -f "$tmp" "$JSON"
echo "wrote $JSON"
}
# Wraps telemetry.json as `window.__TEL = {...};` so index.html can load it
# via a <script> tag. fetch() on file:// is blocked in CEF; script-tag isn't.
wrap_js() {
if [[ ! -f "$JSON" ]]; then
echo "telemetry.json not found — run with --collect first" >&2
exit 1
fi
if ! python3 -m json.tool "$JSON" >/dev/null 2>&1; then
echo "telemetry.json is not valid JSON — fix it before re-wrapping" >&2
exit 1
fi
local tmp="$JS.tmp.$$"
{ printf 'window.__TEL = '; cat "$JSON"; printf ';\n'; } > "$tmp"
mv -f "$tmp" "$JS"
echo "wrote $JS"
}
[[ "${1:-}" == "--collect" ]] && collect
wrap_js