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:
+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