24x7 RTMP desktop stream, client side + UAT PASS (#798)
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
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
.crush/
|
||||
uat/.env
|
||||
logs/*.png
|
||||
logs/*stream.log
|
||||
|
||||
Executable
+66
@@ -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://<cloudron-host>:1935/live/<stream-key>
|
||||
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"
|
||||
Executable
+107
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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://<cloudron>`
|
||||
|
||||
## 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)
|
||||
@@ -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).
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
+16
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+138
@@ -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' ' ' </proc/"$pid"/cmdline 2>/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
|
||||
Reference in New Issue
Block a user