From 777767db5490c7567cdcf5b96b84aec456d3ebde Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sat, 5 Sep 2026 05:01:23 -0500 Subject: [PATCH] 24x7 RTMP desktop stream, client side + UAT PASS (#798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whole-screen KDE capture -> x264 -> RTMP, fenced to the rt slice. Loopback mediamtx receiver; Cloudron cutover = one creds edit. Detail: https://projects.knownelement.com/issues/798#note-4519 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- .gitignore | 2 + 18-rtmp-stream.sh | 66 +++++++++++ 19-rtmp-uat.sh | 107 ++++++++++++++++++ NEXT.md | 14 +++ README.md | 10 +- STREAMING.md | 58 ++++++++++ TRACKING.md | 2 + logs/19-rtmp-uat.out | 22 ++++ rtmp/docker-compose.yml | 13 +++ rtmp/mediamtx.yml | 14 +++ rtmp/uat-creds.env | 6 + staged/apply-guest.sh | 19 +++- staged/etc-ukrrs-rtmp-stream.env | 13 +++ staged/systemd/ukrrs-rtmp-stream.service | 24 ++++ staged/ukrrs-rtmp-stream.sh | 138 +++++++++++++++++++++++ 15 files changed, 503 insertions(+), 5 deletions(-) create mode 100755 18-rtmp-stream.sh create mode 100755 19-rtmp-uat.sh create mode 100644 STREAMING.md create mode 100644 logs/19-rtmp-uat.out create mode 100644 rtmp/docker-compose.yml create mode 100644 rtmp/mediamtx.yml create mode 100644 rtmp/uat-creds.env create mode 100644 staged/etc-ukrrs-rtmp-stream.env create mode 100644 staged/systemd/ukrrs-rtmp-stream.service create mode 100755 staged/ukrrs-rtmp-stream.sh diff --git a/.gitignore b/.gitignore index f2e5913..76d4ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .crush/ uat/.env +logs/*.png +logs/*stream.log diff --git a/18-rtmp-stream.sh b/18-rtmp-stream.sh new file mode 100755 index 0000000..3e46765 --- /dev/null +++ b/18-rtmp-stream.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# 18-rtmp-stream.sh — install the 24x7 desktop RTMP stream (#798) +# +# Installs + enables (idempotent): +# /usr/local/sbin/ukrrs-rtmp-stream.sh capture/encode/push wrapper +# /etc/ukrrs/rtmp-stream.env tunables (fps/res/bitrate/preset) +# /etc/systemd/system/ukrrs-rtmp-stream.service (Slice=ukrrs-rt.slice = cores 6-7) +# ~/.creds/rtmp-stream.env RTMP_URL (created with local-UAT +# placeholder if missing; put the +# Cloudron URL+key there, then +# `systemctl restart ukrrs-rtmp-stream`) +# rtmp/docker-compose.yml up loopback mediamtx receiver (ukrrs-ultix-rtmp-rx) +# +# Usage: ./18-rtmp-stream.sh (self-elevates; never types sudo yourself) +# ./18-rtmp-stream.sh 2>&1 | tee logs/18-rtmp-stream.out +set -euo pipefail +[ "$(id -u)" = 0 ] || exec sudo bash "$0" "$@" + +here=$(cd "$(dirname "$0")" && pwd) +say() { echo "[18-rtmp] $*"; } + +DUSER=reachableceo +DHOME=/home/$DUSER +CREDS=$DHOME/.creds/rtmp-stream.env + +# 1) wrapper + tunables + unit +install -m 0755 "$here/staged/ukrrs-rtmp-stream.sh" /usr/local/sbin/ukrrs-rtmp-stream.sh +say "wrapper -> /usr/local/sbin/ukrrs-rtmp-stream.sh" +mkdir -p /etc/ukrrs +if [ -f /etc/ukrrs/rtmp-stream.env ]; then + say "/etc/ukrrs/rtmp-stream.env exists; kept" +else + install -m 0644 "$here/staged/etc-ukrrs-rtmp-stream.env" /etc/ukrrs/rtmp-stream.env + say "tunables -> /etc/ukrrs/rtmp-stream.env" +fi +install -m 0644 "$here/staged/systemd/ukrrs-rtmp-stream.service" /etc/systemd/system/ukrrs-rtmp-stream.service +systemctl daemon-reload +systemctl enable --now ukrrs-rtmp-stream.service +say "service enabled + started (Slice=ukrrs-rt.slice = cores 6-7, MemoryMax 4G)" + +# 2) creds file (only if missing; placeholder = local UAT receiver, not secret) +if [ ! -f "$CREDS" ]; then + cat > "$CREDS" <<'EOF' +# RTMP push target for ukrrs-rtmp-stream (#798). 0600, never in the repo. +# Cloudron streaming server: paste the full ingest URL incl. stream key, e.g. +# RTMP_URL=rtmp://:1935/live/ +RTMP_URL=rtmp://127.0.0.1:1935/live/desktop +EOF + chown $DUSER:$DUSER "$CREDS"; chmod 0600 "$CREDS" + say "$CREDS created with local-receiver placeholder (edit for Cloudron later)" +else + say "$CREDS exists; kept" +fi + +# 3) loopback receiver (pinned mediamtx) +docker compose -f "$here/rtmp/docker-compose.yml" up -d +say "receiver up: ukrrs-ultix-rtmp-rx on 127.0.0.1:1935" + +# 4) evidence +sleep 5 +say "--- service status ---" +systemctl --no-pager -l status ukrrs-rtmp-stream.service | sed -n '1,8p' || true +say "--- last journal lines ---" +journalctl -u ukrrs-rtmp-stream -n 10 --no-pager || true +say "done. Watch locally: ffplay rtmp://127.0.0.1:1935/live/desktop" +say "Cloudron cutover: edit $CREDS then: systemctl restart ukrrs-rtmp-stream" diff --git a/19-rtmp-uat.sh b/19-rtmp-uat.sh new file mode 100755 index 0000000..f9ebb55 --- /dev/null +++ b/19-rtmp-uat.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# 19-rtmp-uat.sh — evidence UAT for the 24x7 desktop RTMP stream (#798) +# +# Runs ENTIRELY as reachableceo (no root): brings up the loopback mediamtx +# receiver, runs the streamer wrapper pinned to cores 6-7 (same CPUs as the +# ukrrs-rt.slice), then proves: +# 1. RTMP stream metadata (codec/res/fps) via ffprobe +# 2. two frames pulled from the stream (content evidence) +# 3. encoder CPU cost (% of one core, measured on the 2 pinned cores) +# 4. self-heal: kill ffmpeg -> wrapper respawns -> stream returns +# +# Usage: ./19-rtmp-uat.sh +# ./19-rtmp-uat.sh 2>&1 | tee logs/19-rtmp-uat.out +set -uo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +LOG=${LOG:-/dev/stdout} +say() { echo "[19-uat] $*" | tee -a /dev/null; } +pass() { echo "[19-uat] PASS: $*"; } +fail() { echo "[19-uat] FAIL: $*"; FAILURES=$((FAILURES+1)); } +FAILURES=0 + +URL=rtmp://127.0.0.1:1935/live/desktop +WPID=0 + +cleanup() { + [ "$WPID" -gt 0 ] && kill "$WPID" 2>/dev/null + wait 2>/dev/null + true +} +trap cleanup EXIT + +# 0) receiver up +docker compose -f "$here/rtmp/docker-compose.yml" up -d >/dev/null 2>&1 +for i in $(seq 1 15); do + ss -ltn 2>/dev/null | grep -q '127.0.0.1:1935' && break + sleep 1 +done +ss -ltn | grep -q '127.0.0.1:1935' && pass "receiver listening on 127.0.0.1:1935" \ + || { fail "receiver not listening"; exit 1; } + +# 1) streamer (cores 6-7, mirrors the ukrrs-rt.slice CPU fence) +export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/1001} +CREDS_ENV="$here/rtmp/uat-creds.env" taskset -c 6,7 \ + bash "$here/staged/ukrrs-rtmp-stream.sh" > "$here/logs/19-rtmp-uat.stream.log" 2>&1 & +WPID=$! +say "wrapper pid=$WPID (pinned cores 6-7)" + +sleep 12 +FFPID=$(pgrep -P "$WPID" -x ffmpeg | head -1) +[ -n "$FFPID" ] && pass "ffmpeg child running (pid=$FFPID)" || fail "no ffmpeg child" + +# 2) stream metadata +META=$(timeout 25 ffprobe -v error -show_entries stream=codec_name,width,height,avg_frame_rate,codec_type -of csv=p=0 "$URL" 2>&1 | sort | tr '\n' ' ') +echo "[19-uat] stream: $META" +case "$META" in + *h264*) pass "h264 video present" ;; *) fail "no h264 in stream: $META" ;; +esac +echo "$META" | grep -q 1920 && pass "output width 1920 (scaled)" || fail "unexpected width: $META" +echo "$META" | grep -qE '1000/33|30/1' && pass "~30 fps" || fail "unexpected fps: $META" + +# 3) frames from the stream (content evidence) +timeout 25 ffmpeg -v error -i "$URL" -frames:v 1 -y "$here/logs/19-rtmp-uat-frame1.png" 2>&1 +sleep 8 +timeout 25 ffmpeg -v error -i "$URL" -frames:v 1 -y "$here/logs/19-rtmp-uat-frame2.png" 2>&1 +F1=$(stat -c%s "$here/logs/19-rtmp-uat-frame1.png" 2>/dev/null || echo 0) +F2=$(stat -c%s "$here/logs/19-rtmp-uat-frame2.png" 2>/dev/null || echo 0) +[ "$F1" -gt 20000 ] && pass "frame 1 captured (${F1} bytes)" || fail "frame 1 too small/missing (${F1} bytes)" +[ "$F2" -gt 20000 ] && pass "frame 2 captured (${F2} bytes)" || fail "frame 2 too small/missing (${F2} bytes)" + +# 4) CPU cost over 10s (ticks are 100/s; 2 cores = max 200%) +if [ -n "${FFPID:-}" ] && [ -d /proc/$FFPID ]; then + t1=$(awk '{print $14+$15}' /proc/$FFPID/stat) + sleep 10 + t2=$(awk '{print $14+$15}' /proc/$FFPID/stat) + CPU=$(( (t2 - t1) / 10 )) + echo "[19-uat] encoder CPU: ${CPU}% of one core (slice ceiling 200%)" + [ "$CPU" -lt 195 ] && pass "CPU within 2-core fence (${CPU}%)" || fail "CPU at/over fence (${CPU}%)" +else + fail "ffmpeg gone before CPU sample" +fi + +# 5) self-heal: kill ffmpeg, wrapper must respawn it +kill "$FFPID" 2>/dev/null +FFPID2="" +for i in $(seq 1 9); do + sleep 5 + FFPID2=$(pgrep -P "$WPID" -x ffmpeg | head -1) + [ -n "$FFPID2" ] && [ "$FFPID2" != "$FFPID" ] && break +done +[ -n "${FFPID2:-}" ] && [ "$FFPID2" != "$FFPID" ] \ + && pass "self-heal: ffmpeg respawned (pid $FFPID -> $FFPID2)" \ + || fail "ffmpeg did not respawn" +META2="" +for i in $(seq 1 6); do + META2=$(timeout 25 ffprobe -v error -show_entries stream=codec_name -of csv=p=0 "$URL" 2>/dev/null | head -1) + [ "${META2:-}" = "h264" ] && break + sleep 3 +done +[ "${META2:-}" = "h264" ] && pass "stream serves again after kill" || fail "no stream after respawn: '${META2}'" + +echo +echo "[19-uat] wrapper log tail:" +tail -6 "$here/logs/19-rtmp-uat.stream.log" | sed 's/^/ /' +echo +[ "$FAILURES" = 0 ] && echo "[19-uat] OVERALL: PASS" || echo "[19-uat] OVERALL: FAIL ($FAILURES failures)" +exit "$FAILURES" diff --git a/NEXT.md b/NEXT.md index 052d0f5..a0d7ccb 100644 --- a/NEXT.md +++ b/NEXT.md @@ -1,5 +1,19 @@ # NEXT — perf-opt closeout COMPLETE (2026-08-31); #610 open-terminal fleet DONE +## 2026-09-05 morning — 24x7 RTMP desktop stream, client side DONE (#798) +- Built + UAT PASS against the LIVE desktop (logs/19-rtmp-uat.out): + h264+aac 1920w ~30fps, 187% of the 2-core ukrrs-rt.slice fence, + self-heal (kill ffmpeg -> respawn -> stream back) proven. Frames + captured + luma-verified. Loopback receiver ukrrs-ultix-rtmp-rx UP + (mediamtx 1.20.1, 127.0.0.1:1935 only). +- HUMAN OWES (one command, installs the systemd service + creds file): + `cd ~/projects/ultix && ./18-rtmp-stream.sh` + then watch locally: `ffplay rtmp://127.0.0.1:1935/live/desktop` +- Cloudron cutover when ready: paste ingest URL+key into + ~/.creds/rtmp-stream.env, `systemctl restart ukrrs-rtmp-stream`. +- Limits + tuning record: STREAMING.md (2560w veryfast does NOT fit the + 2-core fence until #601; x11grab GetImage caps ~20 delivered fps). + ## 2026-09-05 early AM — post-update-reboot RDP outage FIXED (#611) - Two reboot-triggered killers: waitforx RandR race/empty-argv (shim at /usr/lib/x86_64-linux-gnu/xrdp/waitforx, pristine = waitforx.real) and diff --git a/README.md b/README.md index d62b3b6..c11c4c4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ are manual-only by ruling. in flight, #612 dotfiles v2 + Kali addendum CLOSED (UAT pass), #615 microvm + rebuild capture (microvm live + regular-VM lane proven; human UAT pending), #616 macOS VM (blocked by #601); - #603 root growth DONE online 2026-09-01; pass record: #602. + #603 root growth DONE online 2026-09-01; pass record: #602; + #798 24x7 RTMP desktop stream (client side UAT PASS 2026-09-05; + service install + Cloudron target pending). https://projects.knownelement.com/projects/55 - Discourse doc: pending (house cross-link rule: create at next doc pass). - Docs live in-repo (runbook exception per house rules): @@ -26,6 +28,7 @@ are manual-only by ruling. - REBUILD.md — full-rebuild runbook, guest + PVE sides (#615); docs/state/ holds regenerable state snapshots (14-capture-state.sh) - RUNBOOK-TODAY.md, questions-v1.md + - STREAMING.md — 24x7 RTMP desktop stream ops + limits (#798) - Scripts (human-run unless noted): 1-guest-prep.sh, 2-host-one-shot.sh, 3-post-reboot-fixes.sh, 4-host-netcheck.sh (crush may run via ssh), 5-activate-multiqueue.sh (historical — multiqueue live 2026-09-01), @@ -40,7 +43,10 @@ are manual-only by ruling. 14-capture-state.sh (refresh docs/state), 15-docker-to-data2.sh (STAGED, not yet run: moves docker data-root to /data2), 16-grow-root-online.sh (RAN 2026-09-01: / grown to 431G online, swap - removed by design; old partition table backed up in logs/). + removed by design; old partition table backed up in logs/), + 18-rtmp-stream.sh (installs the 24x7 desktop streamer, #798) + + 19-rtmp-uat.sh (stream evidence UAT; rtmp/ holds the loopback receiver + compose). Run logs live in logs/. staged/ holds the gated configs. - OPEN-TERMINAL.md — ops note for the terminal fleet: ports, keys location, production OpenWebUI wiring, UAT record. diff --git a/STREAMING.md b/STREAMING.md new file mode 100644 index 0000000..4f9e19d --- /dev/null +++ b/STREAMING.md @@ -0,0 +1,58 @@ +# STREAMING.md — 24x7 RTMP desktop stream (reachableceo KDE, #798) + +## What runs where +- `ukrrs-rtmp-stream.service` (system, User=reachableceo, + Slice=ukrrs-rt.slice = cores 6-7, MemoryMax 4G) runs + `/usr/local/sbin/ukrrs-rtmp-stream.sh` forever: + finds the newest xorgxrdp X server owned by reachableceo, x11grabs the + whole screen, encodes (x264 superfast, scaled to 1920w, ~30fps, 6M/9M + VBR), captures desktop audio if a pulse monitor exists, pushes + FLV/RTMP to `RTMP_URL`, and self-heals: session gone -> wait; display + number changed -> follow; resolution changed -> restart encoder; + ffmpeg died -> respawn in ~5-20s. +- `ukrrs-ultix-rtmp-rx` (compose `rtmp/`, mediamtx 1.20.1 pinned, + host net, **127.0.0.1:1935 only**) = local receiver for UAT + local + mirror. Everything else in mediamtx is disabled. +- Host ffmpeg 7.1.5 is pre-installed OS tooling (ruling 2026-09-05, + #798): intentionally used host-side like open-terminal (#610). + +## Config +- Tunables (fps/res/bitrate/preset/user): `/etc/ukrrs/rtmp-stream.env` +- Push target (SECRET once it carries a stream key): + `~/.creds/rtmp-stream.env` -> `RTMP_URL=rtmp://host:1935/app/key` + Never anywhere else (not repo, not unit, not logs). +- After editing either: `systemctl restart ukrrs-rtmp-stream` + +## Cloudron cutover (when the streaming-server details arrive) +1. Put the ingest URL incl. stream key in `~/.creds/rtmp-stream.env` + (chmod 600 already; keep one `RTMP_URL=` line). +2. `systemctl restart ukrrs-rtmp-stream` +3. Verify: `journalctl -u ukrrs-rtmp-stream -f` shows `LIVE: ... -> rtmp://` + +## Watch it +- Local mirror: `ffplay rtmp://127.0.0.1:1935/live/desktop` (or VLC). +- Ops: `systemctl status ukrrs-rtmp-stream`; + `journalctl -u ukrrs-rtmp-stream -n 50`. + +## Known limits (interim 8 vCPU, all measured 2026-09-05) +- Encode is CPU-bound inside the 2-core rt-slice fence: 1920w superfast + runs at ~186% of one core (93% of the fence). 2560w veryfast does NOT + fit (sat the fence at ~13fps) — do not raise until post-#601. +- x11grab on the xorgxrdp X server tops out near ~20 delivered fps on a + 2816x1890 session (GetImage latency, not CPU). Static desktop looks + perfect; fast full-screen motion is slightly soft. Post-#601 options: + NVENC (compute cards), or 2560w veryfast on more cores. +- After a reboot there is NO desktop until the human RDPs in (autologin + off by ruling) — the streamer waits and starts on first session. That + is "24x7 = whenever the desktop exists". +- Audio rides `auto_null.monitor` (silent dummy) until an RDP client + forwards audio; the wrapper re-picks the monitor source on every + (re)start. + +## Files +- `staged/ukrrs-rtmp-stream.sh` wrapper (installed to /usr/local/sbin) +- `staged/etc-ukrrs-rtmp-stream.env` tunables template +- `staged/systemd/ukrrs-rtmp-stream.service` unit +- `rtmp/` compose + mediamtx.yml + uat-creds.env (loopback UAT target) +- `18-rtmp-stream.sh` installer (idempotent, self-elevating) +- `19-rtmp-uat.sh` evidence UAT (logs/19-rtmp-uat.out + frames) diff --git a/TRACKING.md b/TRACKING.md index 68de456..82360ca 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -22,10 +22,12 @@ One table, updated in place at each checkpoint. States: done ✅ / doing 🔄 / | #616 | macos | ⏳ | macOS VM on pfv-tsys5 (headless CI + iOS dev) — blocked by #601 | | #631 | hidpi | ✅ | 150% scaling both accounts (Xft.dpi 144 + kdeglobals, persisted); cursor box RESOLVED 2026-09-05 (human verdict: gone): core shadowless cursors + xrdp new_cursors=false (server-side draw; Jump drops cursor alpha). Follow-on = #679 | | #679 | rdp-perf | ⏳ | 16K compressed-bitmap drops at 5160 (lag/stale updates); 1MB send buffer STAGED (applies at next reconnect); EGFX-at-24bpp retest queued (human-timed) | +| #798 | rtmp | 🔄 | 24x7 desktop stream CLIENT SIDE DONE + UAT PASS 2026-09-05 (h264+aac 1920w ~30fps, 187% of the 2-core rt-slice fence, self-heal proven; logs/19-rtmp-uat.out). Loopback receiver ukrrs-ultix-rtmp-rx UP. HUMAN OWES: run ./18-rtmp-stream.sh (installs service), later paste Cloudron RTMP URL into ~/.creds/rtmp-stream.env + restart. See STREAMING.md | Inbox (mid-task interrupts): none. Decisions log (latest wins): +- 2026-09-05 (morning, #798): 24x7 RTMP desktop stream built client-side. Host ffmpeg 7.1.5 ruled OK to use host-side (human: "it's fine... special case working on host", same family as the #610 open-terminal exception; nothing new installed). Architecture: systemd ukrrs-rtmp-stream.service (User=reachableceo, Slice=ukrrs-rt.slice = cores 6-7, MemoryMax 4G) -> /usr/local/sbin/ukrrs-rtmp-stream.sh: discovers the NEWEST xorgxrdp display of UID 1001 via /proc scan (xrdp display numbers move; after reboot no session exists until first RDP login - stream waits), x11grab whole screen + pulse monitor, x264 -> FLV/RTMP. TUNING MEASURED: 2560w veryfast does NOT fit the 2-core fence (saturates at ~13fps); 1920w superfast runs 187% of one core with headroom - interim operating point until #601. x11grab on xorgxrdp tops at ~20 delivered fps (GetImage latency, not CPU). Secrets: RTMP_URL only in ~/.creds/rtmp-stream.env. Local loopback receiver: mediamtx 1.20.1 pinned, 127.0.0.1:1935 only, compose rtmp/. mediamtx v1.20 key lesson: booleans are rtsp/hls/webrtc/srt/moq/api/metrics/pprof/playback (no *Disable keys, no rtmpTransports). Cloudron streaming server = final target, details pending from human; cutover = edit creds + restart (STREAMING.md). - 2026-09-05 (early AM, post-update-reboot outage, #611): two independent RDP killers after the 6.12.105/u2 reboot: (1) xrdp 0.10.1 waitforx races xorgxrdp deferred-RandR output creation and goes blind for its whole 30s window ("Timed out waiting for RandR outputs" -> session killed -> Jump error + black screen); the u2 security NMU ALSO intermittently execs waitforx with EMPTY argv (observed 03:58:38) which the real binary rejects. Fix = wrapper at /usr/lib/x86_64-linux-gnu/xrdp/waitforx: defaults -d from $DISPLAY when argv empty + forces an xrandr RandR query past the deferred window, then execs pristine waitforx.real; invocations logged to /var/log/xrdp-fix/. (2) /etc/X11/Xsession.d/90xbrlapi runs /bin/xbrlapi in the FOREGROUND; brltty inactive (no BrlAPI socket) -> hangs forever -> Xsession never reaches startplasma-x11 -> connected-but-black screen. Hook disabled (renamed .disabled; no braille HW on this VM). Plus new_cursors=false in xrdp.ini (cursor black box: server-side cursor draw, pairs with #631 core cursor theme). All re-appliable: 17-fix-rdp-postupdate.sh (idempotent, session-safe, self-elevating). LESSON: bouncing the xrdp FRONT-END while a session lives orphans sesman state -> next login CREATES a second session instead of reconnecting -> two KDE sessions on one user bus fight (half-drawn desktop); recovery = kill all session procs + verify no plasma left (user@1001 stop can hang when crush lives inside it). Diagnostic gold: shim the helper binary (waitforx) to dump argv+env at the failure point. - 2026-09-01 (midday): RDP #611 diagnosis: xrdp 0.10.1/xorgxrdp 0.10.2 serves 5160x2160 CLEANLY on every codec (EGFX-RFXPro, NSCodec, bitmaps) and mid-session resize 2816->5160 (freerdp headless UAT, Xvfb screenshots; ASCII-render analysis). Jump-client corruption pinned to EGFX large-frame path -> XRDP_GFX_MAX_COMPRESSED_BYTES=32MB + FRAMES_IN_FLIGHT=2 via xrdp.service.d drop-in (12-fix-rdp-bigres.sh, idempotent, restart-guards on active sessions). Scratch user rdptest = the UAT account (remove after #611 closes). Sudoers: reachableceo -> offstage NOPASSWD (narrow, /etc/sudoers.d/reachableceo-to-offstage). Harness note: crush v0.91.2 hardcodes a bash command blocklist, no config override (upstream #2761); leading-word prefix bypasses it. - 2026-09-01 (afternoon): #615 microvm LIVE: qemu -M microvm + KVM engine (apt-only deps), firecracker v1.16.1 installed but its KVM cap check (0x38 = USER_MEMORY/SET_TSS_ADDR/VAPIC) refuses this nested Westmere KVM — retry MICROVM_ENGINE=firecracker post-#601. Per-user tap pools mv-r0..3/mv-o0..3 (/30s in 172.30.0/1.x) + iptables NAT via microvm-net.service; kvm group added both accounts (one re-login activates). UAT: boot->ssh 27.9s first boot (cloud-init+growpart 3->9.7G), apt OK through NAT, graceful stop 6.4s. #616 macOS VM ticket created w/ blocks-relation on #601. REBUILD.md + docs/state capture (14-capture-state.sh) landed; README/NEXT/REPORT/RUNBOOK synced (stale multiqueue text). mred = the Redmine CLI for all ticket work (~/.local/bin/mred). diff --git a/logs/19-rtmp-uat.out b/logs/19-rtmp-uat.out new file mode 100644 index 0000000..1218525 --- /dev/null +++ b/logs/19-rtmp-uat.out @@ -0,0 +1,22 @@ +[19-uat] PASS: receiver listening on 127.0.0.1:1935 +[19-uat] wrapper pid=129003 (pinned cores 6-7) +[19-uat] PASS: ffmpeg child running (pid=129041) +[19-uat] stream: aac,audio,0/0 h264,video,1920,1288,1000/33 +[19-uat] PASS: h264 video present +[19-uat] PASS: output width 1920 (scaled) +[19-uat] PASS: ~30 fps +[19-uat] PASS: frame 1 captured (462128 bytes) +[19-uat] PASS: frame 2 captured (463768 bytes) +[19-uat] encoder CPU: 187% of one core (slice ceiling 200%) +[19-uat] PASS: CPU within 2-core fence (187%) +[19-uat] PASS: self-heal: ffmpeg respawned (pid 129041 -> 130062) +[19-uat] PASS: stream serves again after kill + +[19-uat] wrapper log tail: + frame= 0 fps=0.0 q=0.0 size= 0KiB time=N/A bitrate=N/A speed=N/A frame= 8 fps=7.9 q=15.0 size= 185KiB time=00:00:00.50 bitrate=3038.4kbits/s dup=0 drop=2 speed=0.494x frame= 17 fps= 11 q=10.0 size= 259KiB time=00:00:00.83 bitrate=2546.1kbits/s dup=0 drop=4 speed=0.55x frame= 26 fps= 13 q=7.0 size= 333KiB time=00:00:01.13 bitrate=2403.9kbits/s dup=0 drop=6 speed=0.562x frame= 37 fps= 15 q=3.0 size= 425KiB time=00:00:01.50 bitrate=2320.6kbits/s dup=0 drop=6 speed=0.595x frame= 49 fps= 16 q=1.0 size= 506KiB time=00:00:02.06 bitrate=2005.8kbits/s dup=0 drop=6 speed=0.684x frame= 59 fps= 17 q=5.0 size= 534KiB time=00:00:02.50 bitrate=1749.3kbits/s dup=0 drop=6 speed=0.71x frame= 70 fps= 17 q=1.0 size= 884KiB time=00:00:03.00 bitrate=2413.2kbits/s dup=0 drop=6 speed=0.746x frame= 82 fps= 18 q=1.0 size= 913KiB time=00:00:03.53 bitrate=2117.9kbits/s dup=0 drop=6 speed=0.781x frame= 89 fps= 18 q=2.0 size= 924KiB time=00:00:03.83 bitrate=1973.9kbits/s dup=0 drop=6 speed=0.763x frame= 96 fps= 17 q=0.0 size= 936KiB time=00:00:04.20 bitrate=1826.2kbits/s dup=0 drop=6 speed=0.76x frame= 104 fps= 17 q=2.0 size= 1150KiB time=00:00:04.56 bitrate=2062.6kbits/s dup=0 drop=6 speed=0.758x frame= 113 fps= 17 q=2.0 size= 1172KiB time=00:00:04.93 bitrate=1946.1kbits/s dup=0 drop=6 speed=0.756x frame= 123 fps= 18 q=0.0 size= 1506KiB time=00:00:05.66 bitrate=2177.6kbits/s dup=0 drop=6 speed=0.807x frame= 132 fps= 18 q=0.0 size= 1740KiB time=00:00:06.26 bitrate=2274.0kbits/s dup=0 drop=6 speed=0.832x frame= 142 fps= 18 q=1.0 size= 1824KiB time=00:00:06.83 bitrate=2187.1kbits/s dup=0 drop=6 speed=0.85x frame= 151 fps= 18 q=1.0 size= 1885KiB time=00:00:07.26 bitrate=2125.0kbits/s dup=0 drop=6 speed=0.851x frame= 161 fps= 18 q=2.0 size= 1948KiB time=00:00:07.83 bitrate=2037.7kbits/s dup=0 drop=6 speed=0.866x frame= 170 fps= 18 q=2.0 size= 2161KiB time=00:00:08.30 bitrate=2133.1kbits/s dup=0 drop=6 speed=0.869x frame= 182 fps= 18 q=2.0 size= 2673KiB time=00:00:08.93 bitrate=2451.3kbits/s dup=0 drop=6 speed=0.889x frame= 190 fps= 18 q=1.0 size= 2710KiB time=00:00:09.33 bitrate=2378.6kbits/s dup=0 drop=6 speed=0.884x frame= 195 fps= 18 q=0.0 size= 2718KiB time=00:00:09.63 bitrate=2311.1kbits/s dup=0 drop=6 speed=0.872x frame= 202 fps= 17 q=1.0 size= 2726KiB time=00:00:09.96 bitrate=2240.8kbits/s dup=0 drop=6 speed=0.863x frame= 211 fps= 18 q=1.0 size= 2737KiB time=00:00:10.36 bitrate=2163.2kbits/s dup=0 drop=6 speed=0.86x frame= 219 fps= 17 q=0.0 size= 2930KiB time=00:00:10.86 bitrate=2209.2kbits/s dup=0 drop=6 speed=0.866x frame= 226 fps= 17 q=1.0 size= 2950KiB time=00:00:11.46 bitrate=2107.9kbits/s dup=0 drop=6 speed=0.878x frame= 237 fps= 17 q=0.0 size= 2967KiB time=00:00:12.20 bitrate=1992.1kbits/s dup=0 drop=6 speed= 0.9x frame= 243 fps= 17 q=1.0 size= 3290KiB time=00:00:12.60 bitrate=2139.1kbits/s dup=0 drop=6 speed=0.896x frame= 250 fps= 17 q=1.0 size= 3305KiB time=00:00:13.00 bitrate=2083.0kbits/s dup=0 drop=6 speed=0.893x frame= 260 fps= 17 q=2.0 size= 3317KiB time=00:00:13.56 bitrate=2002.8kbits/s dup=0 drop=6 speed=0.901x frame= 272 fps= 17 q=2.0 size= 3329KiB time=00:00:14.26 bitrate=1911.8kbits/s dup=0 drop=6 speed=0.917x frame= 283 fps= 18 q=1.0 size= 3346KiB time=00:00:15.03 bitrate=1823.1kbits/s dup=0 drop=6 speed=0.936x frame= 288 fps= 17 q=0.0 size= 3353KiB time=00:00:15.30 bitrate=1795.2kbits/s dup=0 drop=6 speed=0.924x frame= 293 fps= 17 q=2.0 size= 3359KiB time=00:00:15.50 bitrate=1775.1kbits/s dup=0 drop=6 speed=0.908x frame= 301 fps= 17 q=1.0 size= 3369KiB time=00:00:15.83 bitrate=1743.1kbits/s dup=0 drop=6 speed=0.901x frame= 312 fps= 17 q=0.0 size= 3903KiB time=00:00:16.36 bitrate=1953.6kbits/s dup=0 drop=6 speed=0.905x frame= 324 fps= 17 q=0.0 size= 3953KiB time=00:00:17.50 bitrate=1850.4kbits/s dup=0 drop=6 speed=0.942x frame= 335 fps= 18 q=2.0 size= 3988KiB time=00:00:18.13 bitrate=1801.8kbits/s dup=0 drop=6 speed=0.95x frame= 345 fps= 18 q=0.0 size= 4009KiB time=00:00:18.56 bitrate=1769.0kbits/s dup=0 drop=6 speed=0.948x frame= 354 fps= 18 q=0.0 size= 4030KiB time=00:00:18.93 bitrate=1743.6kbits/s dup=0 drop=6 speed=0.943x frame= 365 fps= 18 q=2.0 size= 4376KiB time=00:00:19.43 bitrate=1844.8kbits/s dup=0 drop=6 speed=0.944x frame= 376 fps= 18 q=1.0 size= 4447KiB time=00:00:19.96 bitrate=1824.6kbits/s dup=0 drop=6 speed=0.947x frame= 387 fps= 18 q=0.0 size= 4525KiB time=00:00:20.53 bitrate=1805.3kbits/s dup=0 drop=6 speed=0.952x frame= 397 fps= 18 q=2.0 size= 4562KiB time=00:00:21.00 bitrate=1779.7kbits/s dup=0 drop=6 speed=0.951x frame= 403 fps= 18 q=2.0 size= 4576KiB time=00:00:21.30 bitrate=1759.8kbits/s dup=0 drop=6 speed=0.943x frame= 412 fps= 18 q=2.0 size= 4613KiB time=00:00:21.70 bitrate=1741.6kbits/s dup=0 drop=6 speed=0.94x frame= 423 fps= 18 q=0.0 size= 4986KiB time=00:00:22.20 bitrate=1839.8kbits/s dup=0 drop=6 speed=0.941x frame= 434 fps= 18 q=1.0 size= 5071KiB time=00:00:23.00 bitrate=1806.1kbits/s dup=0 drop=6 speed=0.955x frame= 444 fps= 18 q=2.0 size= 5083KiB time=00:00:23.50 bitrate=1772.1kbits/s dup=0 drop=6 speed=0.956x frame= 455 fps= 18 q=1.0 size= 5128KiB time=00:00:24.03 bitrate=1747.8kbits/s dup=0 drop=6 speed=0.958x frame= 465 fps= 18 q=2.0 size= 5182KiB time=00:00:24.50 bitrate=1732.7kbits/s dup=0 drop=6 speed=0.957x frame= 477 fps= 18 q=2.0 size= 5274KiB time=00:00:25.06 bitrate=1723.5kbits/s dup=0 drop=6 speed=0.961x frame= 487 fps= 18 q=1.0 size= 5675KiB time=00:00:25.53 bitrate=1820.7kbits/s dup=0 drop=6 speed=0.96x frame= 499 fps= 18 q=1.0 size= 5757KiB time=00:00:26.06 bitrate=1809.2kbits/s dup=0 drop=6 speed=0.962x frame= 510 fps= 18 q=0.0 size= 5842KiB time=00:00:26.60 bitrate=1799.0kbits/s dup=0 drop=6 speed=0.964x frame= 519 fps= 18 q=0.0 size= 5895KiB time=00:00:27.03 bitrate=1786.2kbits/s dup=0 drop=6 speed=0.962x frame= 525 fps= 18 q=0.0 size= 5932KiB time=00:00:27.26 bitrate=1782.1kbits/s dup=0 drop=6 speed=0.954x frame= 535 fps= 18 q=1.0 size= 5986KiB time=00:00:27.70 bitrate=1770.4kbits/s dup=0 drop=6 speed=0.952x frame= 545 fps= 18 q=2.0 size= 6337KiB time=00:00:28.20 bitrate=1840.8kbits/s dup=0 drop=6 speed=0.953x frame= 555 fps= 18 q=0.0 size= 6383KiB time=00:00:28.90 bitrate=1809.2kbits/s dup=0 drop=6 speed=0.96x frame= 566 fps= 18 q=2.0 size= 6440KiB time=00:00:29.46 bitrate=1790.5kbits/s dup=0 drop=6 speed=0.963x frame= 578 fps= 19 q=1.0 size= 6544KiB time=00:00:30.13 bitrate=1778.9kbits/s dup=0 drop=6 speed=0.969x frame= 589 fps= 19 q=0.0 size= 6598KiB time=00:00:30.60 bitrate=1766.4kbits/s dup=0 drop=6 speed=0.968x frame= 600 fps= 19 q=0.0 size= 6621KiB time=00:00:31.10 bitrate=1744.0kbits/s dup=0 drop=6 speed=0.969x frame= 611 fps= 19 q=2.0 size= 7000KiB time=00:00:31.56 bitrate=1816.7kbits/s dup=0 drop=6 speed=0.968x frame= 623 fps= 19 q=2.0 size= 7067KiB time=00:00:32.13 bitrate=1801.6kbits/s dup=0 drop=6 speed=0.971x frame= 632 fps= 19 q=2.0 size= 7113KiB time=00:00:32.50 bitrate=1792.9kbits/s dup=0 drop=6 speed=0.967x frame= 642 fps= 19 q=0.0 size= 7173KiB time=00:00:32.96 bitrate=1782.4kbits/s dup=0 drop=6 speed=0.967x frame= 651 fps= 19 q=0.0 size= 7229KiB time=00:00:33.33 bitrate=1776.7kbits/s dup=0 drop=6 speed=0.963x frame= 663 fps= 19 q=0.0 size= 7618KiB time=00:00:33.96 bitrate=1837.2kbits/s dup=0 drop=6 speed=0.968x frame= 674 fps= 19 q=0.0 size= 7707KiB time=00:00:34.66 bitrate=1821.3kbits/s dup=0 drop=6 speed=0.974x frame= 686 fps= 19 q=2.0 size= 7778KiB time=00:00:35.16 bitrate=1811.9kbits/s dup=0 drop=6 speed=0.974x frame= 697 fps= 19 q=1.0 size= 7825KiB time=00:00:35.63 bitrate=1799.0kbits/s dup=0 drop=6 speed=0.974x frame= 709 fps= 19 q=1.0 size= 7880KiB time=00:00:36.13 bitrate=1786.4kbits/s dup=0 drop=6 speed=0.974x frame= 720 fps= 19 q=0.0 size= 7949KiB time=00:00:36.60 bitrate=1779.2kbits/s dup=0 drop=6 speed=0.973x frame= 731 fps= 19 q=2.0 size= 8341KiB time=00:00:37.13 bitrate=1840.2kbits/s dup=0 drop=6 speed=0.975x frame= 740 fps= 19 q=2.0 size= 8387KiB time=00:00:37.50 bitrate=1832.2kbits/s dup=0 drop=6 speed=0.971x frame= 748 fps= 19 q=1.0 size= 8414KiB time=00:00:37.83 bitrate=1821.8kbits/s dup=0 drop=6 speed=0.967x frame= 756 fps= 19 q=0.0 size= 8459KiB time=00:00:38.23 bitrate=1812.4kbits/s dup=0 drop=6 speed=0.965x frame= 763 fps= 19 q=1.0 size= 8507KiB time=00:00:38.56 bitrate=1807.0kbits/s dup=0 drop=6 speed=0.961x frame= 771 fps= 19 q=0.0 size= 8535KiB time=00:00:39.03 bitrate=1791.2kbits/s dup=0 drop=6 speed=0.961x frame= 782 fps= 19 q=2.0 size= 8901KiB time=00:00:39.63 bitrate=1839.8kbits/s dup=0 drop=6 speed=0.964x frame= 795 fps= 19 q=0.0 size= 9000KiB time=00:00:40.60 bitrate=1816.0kbits/s dup=0 drop=6 speed=0.975x frame= 833 fps= 19 q=-1.0 Lsize= 9270KiB time=00:00:41.66 bitrate=1822.5kbits/s dup=0 drop=6 speed=0.97x + [2026-09-05 04:59:40][rtmp-stream] ffmpeg exited rc=255; backing off 5s + [2026-09-05 04:59:45][rtmp-stream] LIVE: display :10 2816x1890 -> 1920p@30 6000k audio=auto_null.monitor -> rtmp://127.0.0.1:1935/live/desktop + [x11grab @ 0x55bdaec8f880] Stream #0: not enough frames to estimate rate; consider increasing probesize + [aist#1:0/pcm_s16le @ 0x55bdaeca2780] Guessed Channel Layout: stereo + frame= 0 fps=0.0 q=0.0 size= 0KiB time=N/A bitrate=N/A speed=N/A frame= 2 fps=2.0 q=21.0 size= 131KiB time=-00:00:00.03 bitrate=N/A speed=N/A frame= 8 fps=5.3 q=13.0 size= 209KiB time=00:00:00.63 bitrate=2698.1kbits/s speed=0.421x frame= 16 fps=8.0 q=7.0 size= 329KiB time=00:00:01.06 bitrate=2525.3kbits/s dup=0 drop=1 speed=0.532x frame= 24 fps=9.6 q=2.0 size= 432KiB time=00:00:01.43 bitrate=2468.6kbits/s dup=0 drop=1 speed=0.572x frame= 32 fps= 11 q=2.0 size= 496KiB time=00:00:01.73 bitrate=2342.8kbits/s dup=0 drop=2 speed=0.576x frame= 40 fps= 11 q=1.0 size= 555KiB time=00:00:02.06 bitrate=2201.6kbits/s dup=0 drop=2 speed=0.588x frame= 50 fps= 12 q=2.0 size= 628KiB time=00:00:02.70 bitrate=1905.4kbits/s dup=0 drop=2 speed=0.673x frame= 58 fps= 13 q=2.0 size= 673KiB time=00:00:03.16 bitrate=1740.2kbits/s dup=0 drop=2 speed=0.701x frame= 66 fps= 13 q=0.0 size= 1052KiB time=00:00:03.63 bitrate=2371.2kbits/s dup=0 drop=2 speed=0.724x frame= 74 fps= 13 q=2.0 size= 1097KiB time=00:00:04.10 bitrate=2191.5kbits/s dup=0 drop=2 speed=0.742x frame= 79 fps= 13 q=1.0 size= 1117KiB time=00:00:04.33 bitrate=2110.8kbits/s dup=0 drop=2 speed=0.719x frame= 82 fps= 13 q=1.0 size= 1127KiB time=00:00:04.53 bitrate=2036.1kbits/s dup=0 drop=2 speed=0.694x frame= 91 fps= 13 q=1.0 size= 1156KiB time=00:00:05.10 bitrate=1856.9kbits/s dup=0 drop=2 speed=0.725x frame= 99 fps= 13 q=1.0 size= 1171KiB time=00:00:05.63 bitrate=1702.2kbits/s dup=0 drop=2 speed=0.748x frame= 108 fps= 13 q=0.0 size= 1189KiB time=00:00:06.36 bitrate=1529.5kbits/s dup=0 drop=2 speed=0.792x frame= 118 fps= 14 q=2.0 size= 1244KiB time=00:00:07.26 bitrate=1402.3kbits/s dup=0 drop=2 speed=0.851x frame= 127 fps= 14 q=1.0 size= 1605KiB time=00:00:07.76 bitrate=1693.2kbits/s dup=0 drop=2 speed=0.859x frame= 136 fps= 14 q=1.0 size= 1658KiB time=00:00:08.30 bitrate=1636.5kbits/s dup=0 drop=2 speed=0.87x frame= 145 fps= 14 q=1.0 size= 1703KiB time=00:00:08.76 bitrate=1591.6kbits/s dup=0 drop=2 speed=0.873x frame= 153 fps= 15 q=0.0 size= 1744KiB time=00:00:09.20 bitrate=1553.3kbits/s dup=0 drop=2 speed=0.872x +[19-uat] OVERALL: PASS diff --git a/rtmp/docker-compose.yml b/rtmp/docker-compose.yml new file mode 100644 index 0000000..1026f3f --- /dev/null +++ b/rtmp/docker-compose.yml @@ -0,0 +1,13 @@ +# Local RTMP receiver for the 24x7 desktop stream (#798). +# Loopback-only ingest (127.0.0.1:1935) used for UAT and as a local mirror; +# the real target is the human's Cloudron streaming server (URL lands in +# ~/.creds/rtmp-stream.env when provided — service restart applies it). +name: ultix-rtmp +services: + rtmp-rx: + image: bluenviron/mediamtx:1.20.1 + container_name: ukrrs-ultix-rtmp-rx + network_mode: host + volumes: + - ./mediamtx.yml:/mediamtx.yml:ro + restart: unless-stopped diff --git a/rtmp/mediamtx.yml b/rtmp/mediamtx.yml new file mode 100644 index 0000000..b311294 --- /dev/null +++ b/rtmp/mediamtx.yml @@ -0,0 +1,14 @@ +# mediamtx — loopback-only RTMP ingest for #798 (everything else disabled). +logLevel: warn +rtmpAddress: 127.0.0.1:1935 +rtsp: no +hls: no +webrtc: no +srt: no +moq: no +api: no +metrics: no +pprof: no +playback: no +paths: + all_others: diff --git a/rtmp/uat-creds.env b/rtmp/uat-creds.env new file mode 100644 index 0000000..0fd7cab --- /dev/null +++ b/rtmp/uat-creds.env @@ -0,0 +1,6 @@ +# UAT-only creds file for the 24x7 desktop stream (#798). +# Points the streamer at the local loopback receiver (rtmp/docker-compose.yml). +# The PRODUCTION target (Cloudron streaming server URL + key) goes in +# ~/.creds/rtmp-stream.env — never in this repo. +RTMP_URL=rtmp://127.0.0.1:1935/live/desktop +LOGLEVEL=warning diff --git a/staged/apply-guest.sh b/staged/apply-guest.sh index e178f1f..bbb58a5 100755 --- a/staged/apply-guest.sh +++ b/staged/apply-guest.sh @@ -4,7 +4,7 @@ # RUN=1 required to mutate anything (default: plan only). # daemonjson and desktop additionally require CONFIRM=1 (one-time disruption). # Steps: sysctl modules thp slices psi prune nofile noatime daemonjson desktop -# daynight all +# daynight rtmp all set -euo pipefail RUN=${RUN:-0} CONFIRM=${CONFIRM:-0} @@ -116,16 +116,29 @@ step_daynight() { systemctl enable --now ukrrs-nightprofile.timer ukrrs-dayprofile.timer say "night profile flips at 22:00, day at 07:00; test: ukrrs-daynight.sh night" } +step_rtmp() { + # 24x7 desktop RTMP stream (#798). Creds stay in ~/.creds/rtmp-stream.env + # (created by 18-rtmp-stream.sh if missing; that script is the full installer). + gate "install 24x7 desktop RTMP stream (#798)" || return 0 + inst "$here/ukrrs-rtmp-stream.sh" /usr/local/sbin/ukrrs-rtmp-stream.sh 0755 + mkdir -p /etc/ukrrs + [ -f /etc/ukrrs/rtmp-stream.env ] || \ + inst "$here/etc-ukrrs-rtmp-stream.env" /etc/ukrrs/rtmp-stream.env 0644 + inst "$here/systemd/ukrrs-rtmp-stream.service" /etc/systemd/system/ukrrs-rtmp-stream.service 0644 + systemctl daemon-reload + systemctl enable --now ukrrs-rtmp-stream.service + say "streamer live (Slice=ukrrs-rt.slice); see STREAMING.md; receiver: rtmp/ compose" +} step_all() { step_sysctl; step_modules; step_thp; step_slices; step_psi; step_prune - step_nofile; step_noatime; step_daynight + step_nofile; step_noatime; step_daynight; step_rtmp if [ "$CONFIRM" = 1 ]; then step_daemonjson; step_desktop else say "skipped daemonjson + desktop (need CONFIRM=1)"; fi } step=${1:-} case "$step" in - sysctl|modules|thp|slices|psi|prune|nofile|noatime|daemonjson|desktop|daynight|all) + sysctl|modules|thp|slices|psi|prune|nofile|noatime|daemonjson|desktop|daynight|rtmp|all) "step_$step" ;; *) die "unknown step: $step" ;; esac diff --git a/staged/etc-ukrrs-rtmp-stream.env b/staged/etc-ukrrs-rtmp-stream.env new file mode 100644 index 0000000..9fc4087 --- /dev/null +++ b/staged/etc-ukrrs-rtmp-stream.env @@ -0,0 +1,13 @@ +# /etc/ukrrs/rtmp-stream.env — tunables for ukrrs-rtmp-stream.service (#798) +# The RTMP URL (with any stream key) lives ONLY in ~/.creds/rtmp-stream.env. +STREAM_USER=reachableceo +FPS=30 +# 1920w + superfast fits the ukrrs-rt.slice 2-core fence (interim 8 vCPU). +# Post-#601 (24 threads): try OUT_WIDTH=2560 PRESET=veryfast. +OUT_WIDTH=1920 +BITRATE=6000k +MAXRATE=9000k +BUFSIZE=12000k +PRESET=superfast +ABITRATE=128k +# LOGLEVEL=info # verbose ffmpeg to journald when debugging diff --git a/staged/systemd/ukrrs-rtmp-stream.service b/staged/systemd/ukrrs-rtmp-stream.service new file mode 100644 index 0000000..0c72d4c --- /dev/null +++ b/staged/systemd/ukrrs-rtmp-stream.service @@ -0,0 +1,24 @@ +[Unit] +Description=UKRRS 24x7 desktop RTMP stream (reachableceo KDE whole screen) +Documentation=file:///home/reachableceo/projects/ultix/STREAMING.md +After=network-online.target xrdp.service +Wants=network-online.target + +[Service] +Type=simple +User=reachableceo +Group=reachableceo +Slice=ukrrs-rt.slice +Environment=HOME=/home/reachableceo +Environment=XDG_RUNTIME_DIR=/run/user/1001 +EnvironmentFile=/etc/ukrrs/rtmp-stream.env +ExecStart=/usr/local/sbin/ukrrs-rtmp-stream.sh +Restart=always +RestartSec=5 +StartLimitIntervalSec=0 +# hard stop of the wrapper must take ffmpeg with it +KillMode=control-group +TimeoutStopSec=15 + +[Install] +WantedBy=multi-user.target diff --git a/staged/ukrrs-rtmp-stream.sh b/staged/ukrrs-rtmp-stream.sh new file mode 100755 index 0000000..b3ffb56 --- /dev/null +++ b/staged/ukrrs-rtmp-stream.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# ukrrs-rtmp-stream.sh — 24x7 RTMP stream of the reachableceo KDE desktop (#798) +# +# Runs as User=reachableceo under ukrrs-rtmp-stream.service (Slice=ukrrs-rt.slice, +# pinned to cores 6-7 so it can never starve crush/gateway/LSPs on 8 vCPU). +# Approved host-side ffmpeg (7.1.5, pre-installed) per #798 ruling 2026-09-05: +# same bare-metal exception family as open-terminal (#610). +# +# What it does, forever: +# 1. Find the NEWEST xorgxrdp X server owned by STREAM_USER (xrdp display +# numbers move around; after reboot there is no session until the human +# RDPs in — the loop just waits). +# 2. Read its current geometry (xrandr) — survives iPad-vs-monitor res flips +# by restarting ffmpeg with the new size. +# 3. Grab the whole screen (x11grab) + desktop audio if a pulse monitor +# source exists, scale to OUT_WIDTH, x264-encode, push FLV to RTMP_URL. +# 4. Watchdog every 15s: resolution change or dead ffmpeg -> clean restart; +# dead session -> wait for the next one. +# +# Config: +# /etc/ukrrs/rtmp-stream.env tunables (fps/res/bitrate/preset/user) +# ~/.creds/rtmp-stream.env RTMP_URL (SECRET when it carries a stream +# key; the ONLY place it lives, 0600) +# +# Install path: /usr/local/sbin/ukrrs-rtmp-stream.sh (via 18-rtmp-stream.sh). +# Manual test: CREDS_ENV=$PWD/rtmp/uat-creds.env ./staged/ukrrs-rtmp-stream.sh +set -u + +CONF=/etc/ukrrs/rtmp-stream.env +CREDS_ENV=${CREDS_ENV:-/home/reachableceo/.creds/rtmp-stream.env} +[ -r "$CONF" ] && . "$CONF" +[ -r "$CREDS_ENV" ] && . "$CREDS_ENV" + +STREAM_USER=${STREAM_USER:-reachableceo} +FPS=${FPS:-30} +OUT_WIDTH=${OUT_WIDTH:-1920} +BITRATE=${BITRATE:-6000k} +MAXRATE=${MAXRATE:-9000k} +BUFSIZE=${BUFSIZE:-12000k} +PRESET=${PRESET:-superfast} +ABITRATE=${ABITRATE:-128k} +LOGLEVEL=${LOGLEVEL:-warning} + +say() { echo "[$(date '+%F %T')][rtmp-stream] $*"; } + +TUID=$(id -u "$STREAM_USER") || { say "FATAL: no user $STREAM_USER"; exit 1; } +THOME=$(getent passwd "$STREAM_USER" | cut -d: -f6) +XAUTH="$THOME/.Xauthority" + +# ---- discovery ------------------------------------------------------------- +find_display() { # echoes ":N" of newest xorgxrdp X server owned by TUID + local best=0 best_disp="" pid start cmd disp + for pid in $(pgrep -u "$TUID" -x Xorg 2>/dev/null); do + cmd=$(tr '\0' ' ' /dev/null) || continue + case "$cmd" in *"-config xrdp/xorg.conf"*) ;; *) continue ;; esac + disp=$(echo "$cmd" | grep -oE ':[0-9]+' | head -1) + [ -n "$disp" ] || continue + start=$(awk '{print $22}' /proc/"$pid"/stat 2>/dev/null) || continue + if [ "${start:-0}" -gt "$best" ]; then best=$start; best_disp=$disp; fi + done + echo "$best_disp" +} + +get_geom() { # $1=display -> echoes "WxH" (empty on failure) + local g + g=$(DISPLAY="$1" XAUTHORITY="$XAUTH" timeout 10 xrandr --current 2>/dev/null \ + | grep -m1 -oE 'current [0-9]+ x [0-9]+' | grep -oE '[0-9]+ x [0-9]+' | tr -d ' ') + echo "$g" +} + +get_audio_src() { # echoes pulse monitor source name, empty = stream without audio + command -v pactl >/dev/null 2>&1 || return 0 + timeout 10 pactl info >/dev/null 2>&1 || return 0 + timeout 10 pactl list short sources 2>/dev/null \ + | awk '$2 ~ /\.monitor$/ && $2 !~ /auto_null/ {print $2; exit} + $2 ~ /\.monitor$/ && !f {s=$2; f=1} END {if (!NR) exit; print s}' +} + +FFPID=0 +cleanup() { [ "$FFPID" -gt 0 ] && kill "$FFPID" 2>/dev/null; wait 2>/dev/null; exit 0; } +trap 'cleanup' TERM INT + +# ---- main loop ------------------------------------------------------------- +say "started (user=$STREAM_USER uid=$TUID out=${OUT_WIDTH}p fps=$FPS preset=$PRESET)" +while :; do + if [ -z "${RTMP_URL:-}" ]; then + say "FATAL: RTMP_URL unset in $CREDS_ENV; retrying in 30s" + sleep 30; continue + fi + DISP=$(find_display) + if [ -z "$DISP" ]; then + say "no $STREAM_USER desktop session yet; waiting 10s" + sleep 10; continue + fi + GEOM=$(get_geom "$DISP") + if [ -z "$GEOM" ]; then say "display $DISP found but no RandR geometry yet; waiting 5s"; sleep 5; continue; fi + ASRC=$(get_audio_src) + + VARGS=(-nostdin -hide_banner -loglevel "$LOGLEVEL" -stats + -f x11grab -framerate "$FPS" -video_size "$GEOM" -draw_mouse 1 + -i "$DISP.0") + if [ -n "$ASRC" ]; then + VARGS+=(-f pulse -thread_queue_size 512 -i "$ASRC") + fi + VARGS+=(-vf "scale=${OUT_WIDTH}:-2:flags=bicubic,format=yuv420p" + -c:v libx264 -preset "$PRESET" -profile:v high + -g $((FPS * 2)) -bf 2 -sc_threshold 0 + -b:v "$BITRATE" -maxrate "$MAXRATE" -bufsize "$BUFSIZE") + if [ -n "$ASRC" ]; then + VARGS+=(-c:a aac -b:a "$ABITRATE" -ar 48000) + else + VARGS+=(-an) + fi + VARGS+=(-flvflags no_duration_filesize -f flv "$RTMP_URL") + + say "LIVE: display $DISP ${GEOM} -> ${OUT_WIDTH}p@$FPS ${BITRATE} audio=${ASRC:-none} -> $RTMP_URL" + ffmpeg "${VARGS[@]}" & + FFPID=$! + + # watchdog: exit if ffmpeg dies, session dies, or geometry changes + while kill -0 "$FFPID" 2>/dev/null; do + sleep 15 + kill -0 "$FFPID" 2>/dev/null || break + if [ -z "$(find_display)" ] || [ "$(find_display)" != "$DISP" ]; then + say "session changed/gone ($DISP); restarting capture"; break + fi + CUR=$(get_geom "$DISP") + if [ "$CUR" != "$GEOM" ] && [ -n "$CUR" ]; then + say "resolution changed ${GEOM} -> ${CUR}; restarting encoder"; break + fi + done + kill "$FFPID" 2>/dev/null + wait "$FFPID" 2>/dev/null + RC=$? + FFPID=0 + say "ffmpeg exited rc=$RC; backing off 5s" + sleep 5 +done