Scripts, scenes, bridges, bots and more
This commit is contained in:
336
scripts/telemetry.sh
Executable file
336
scripts/telemetry.sh
Executable file
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env bash
|
||||
# Static device + OBS config snapshot for the OBS Landing overlay.
|
||||
# Output JSON also carries the `rig` field consumed by the Project Loading
|
||||
# overlay's "detected rig :: …" line.
|
||||
#
|
||||
# Modes:
|
||||
# ./telemetry.sh — wrap telemetry.json → telemetry.js
|
||||
# (idempotent; preserves hand edits to JSON)
|
||||
# ./telemetry.sh --collect — re-read hardware + OBS + hostname,
|
||||
# interactively review every field, then wrap
|
||||
# ./telemetry.sh --collect --no-review
|
||||
# — collect, skip prompts (unattended)
|
||||
#
|
||||
# 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="$OBS_DIR/landing/telemetry.json"
|
||||
JS="$OBS_DIR/landing/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)
|
||||
|
||||
# Rig identity — title-cased hostname (ignia → Ignia, midgolem → Midgolem).
|
||||
# Consumed by the Project Loading overlay's "detected rig :: …" line.
|
||||
local rig; rig=$(hostname)
|
||||
rig="${rig^}"
|
||||
|
||||
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")
|
||||
rig=$(sanitize "$rig")
|
||||
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",
|
||||
"rig": $(emit_str "$rig"),
|
||||
"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"
|
||||
}
|
||||
|
||||
# Walk every leaf in telemetry.json and prompt for a per-field override.
|
||||
# Preserves JSON types (numbers stay numbers, strings stay strings, the
|
||||
# literal word `null` becomes JSON null). Empty input keeps the current value.
|
||||
# Skips `collectedAt` — it's auto-generated.
|
||||
review() {
|
||||
[[ -f "$JSON" ]] || { echo "review: $JSON not found" >&2; exit 1; }
|
||||
echo
|
||||
echo "── Review telemetry fields ─────────────────────────────"
|
||||
echo " Press Enter to keep the value shown."
|
||||
echo " Type a new value to override (use 'null' to clear)."
|
||||
echo " Ctrl-C to abort without writing."
|
||||
echo
|
||||
local tmp="$JSON.review.$$"
|
||||
if ! python3 - "$JSON" "$tmp" <<'PY'
|
||||
import json, sys
|
||||
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
with open(src) as f:
|
||||
doc = json.load(f)
|
||||
|
||||
SKIP = {'collectedAt'}
|
||||
|
||||
def coerce(raw, original):
|
||||
if raw == '':
|
||||
return original
|
||||
if raw.strip().lower() == 'null':
|
||||
return None
|
||||
if isinstance(original, bool):
|
||||
return raw.strip().lower() in ('1', 'true', 'yes', 'y', 'on')
|
||||
if isinstance(original, int) and not isinstance(original, bool):
|
||||
try: return int(raw)
|
||||
except: return raw
|
||||
if isinstance(original, float):
|
||||
try: return float(raw)
|
||||
except: return raw
|
||||
return raw
|
||||
|
||||
def walk(node, prefix=''):
|
||||
if isinstance(node, dict):
|
||||
for k in list(node.keys()):
|
||||
path = f'{prefix}.{k}' if prefix else k
|
||||
if k in SKIP:
|
||||
continue
|
||||
v = node[k]
|
||||
if isinstance(v, dict):
|
||||
walk(v, path)
|
||||
else:
|
||||
shown = 'null' if v is None else json.dumps(v, ensure_ascii=False)
|
||||
try:
|
||||
raw = input(f' {path:28s} = {shown:30s} override: ')
|
||||
except EOFError:
|
||||
raw = ''
|
||||
node[k] = coerce(raw, v)
|
||||
|
||||
walk(doc)
|
||||
with open(dst, 'w') as f:
|
||||
json.dump(doc, f, indent=2, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
PY
|
||||
then
|
||||
echo "review: aborted (or python error) — leaving $JSON unchanged" >&2
|
||||
rm -f "$tmp"
|
||||
exit 1
|
||||
fi
|
||||
if ! python3 -m json.tool "$tmp" >/dev/null 2>&1; then
|
||||
echo "review: produced invalid JSON, leaving $tmp for inspection" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv -f "$tmp" "$JSON"
|
||||
echo
|
||||
echo " ✓ updated $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"
|
||||
}
|
||||
|
||||
# ── Argument parsing ────────────────────────────────
|
||||
DO_COLLECT=0
|
||||
DO_REVIEW=1
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--collect) DO_COLLECT=1 ;;
|
||||
--no-review) DO_REVIEW=0 ;;
|
||||
-h|--help) sed -n 's/^# \?//p' "$0" | head -16; exit 0 ;;
|
||||
*) echo "unknown flag: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( DO_COLLECT )); then
|
||||
collect
|
||||
(( DO_REVIEW )) && review
|
||||
fi
|
||||
wrap_js
|
||||
Reference in New Issue
Block a user