#!/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