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:
2026-09-05 05:01:23 -05:00
parent f4e36ec647
commit 777767db54
15 changed files with 503 additions and 5 deletions
+16 -3
View File
@@ -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
+13
View File
@@ -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
+24
View File
@@ -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
+138
View File
@@ -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