From fresh Debian/Ubuntu server to a fully automated ET: Legacy spectator client streaming to Twitch — with audio, Xvfb and ultra-low graphics for smooth FPS.
tvbot (never run this as root).xvfb, ffmpeg, pulseaudio, Mesa OpenGL libs, xdotool./home/tvbot/etlegacy.Xvfb :1) as a spectator.follownext for a TV-style camera.ffmpeg, with audio from PulseAudio Null sink.
The setup runs an ET: Legacy client as a spectator TV-bot on a headless server, inside a virtual X display,
and streams that display (with in-game sound) to Twitch using ffmpeg.
:1
Audio PulseAudio Null sink (ETTV)
Stream ffmpeg → Twitch RTMP
All processes (ET client, PulseAudio, ffmpeg) should run under a normal user, not root. Here we use
tvbot.
adduser tvbot
# set a password, press Enter through the remaining questions
Install the required tools and libraries on your Debian/Ubuntu server:
apt update
apt install -y \
xvfb \
ffmpeg \
pulseaudio \
libgl1 libglx-mesa0 libgl1-mesa-dri mesa-utils \
xdotool
Note: xdotool is optional (for debugging or manual key injection), but does not
hurt to have installed.
.tar.gz)
from the official ET:Legacy download page.
/home/tvbot:
scp ETLegacy-*-x86_64.tar.gz tvbot@your-server-ip:/home/tvbot/
tvbot user and unpack the archive:
su - tvbot
mkdir -p ~/etlegacy
tar -xf ETLegacy-*-x86_64.tar.gz -C ~/etlegacy --strip-components=1
ls ~/etlegacy
# etl.x86_64, etlded.x86_64, etmain, legacy, ...
Create ET: Legacy configs under ~/.etlegacy/etmain (for user tvbot).
// tv_fps.cfg – Ultra-low graphics for TV-bot
seta com_maxfps "60"
seta r_mode "-1"
seta r_customwidth "800"
seta r_customheight "600"
seta r_fullscreen "0"
seta r_colorbits "16"
seta r_depthbits "16"
seta r_texturebits "16"
seta r_ext_multisample "0"
seta r_ext_texture_filter_anisotropic "0"
seta r_textureMode "GL_LINEAR_MIPMAP_NEAREST"
seta r_picmip "3"
seta r_ext_compressed_textures "1"
seta r_dynamiclight "0"
seta r_dynamicTextures "0"
seta r_flares "0"
seta r_wolffog "0"
seta r_lodscale "30"
seta cg_shadows "0"
seta cg_coronas "0"
seta cg_wolfparticles "0"
seta cg_impactparticles "0"
seta cg_trailparticles "0"
seta cg_smokeparticles "0"
seta cg_atmosphericEffects "0"
seta cg_gibs "0"
seta cg_showblood "0"
seta cg_bloodTime "0"
seta cg_bloodFlash "0"
seta cg_bloodDamageBlend "0"
seta cg_markTime "2000"
seta cg_markDistance "128"
seta cg_railTrailTime "0"
seta cg_brassTime "0"
seta cg_tracers "0"
seta cg_muzzleFlash "0"
seta cg_bobyaw "0"
seta cg_bobroll "0"
seta cg_bobpitch "0"
seta cg_bobup "0"
seta cg_runroll "0"
seta cg_runpitch "0"
seta cg_drawGun "0"
seta cg_drawBanners "0"
seta cg_drawHUDStats "0"
seta cg_drawFireteamOverlay "0"
seta cg_drawBuddies "0"
seta cg_drawSnapshot "0"
seta cg_drawPing "0"
seta cg_lagometer "0"
seta cg_drawReinforcementTime "0"
seta cg_drawSpectatorNames "0"
seta cg_countryflags "0"
seta cg_pingColors "0"
seta cg_hitSounds "0"
seta cg_goatSound "0"
seta cg_announcer "0"
seta cg_drawTime "0"
seta cg_drawTimeSeconds "0"
seta cg_drawStatus "0"
seta cg_drawRoundTimer "1"
seta cg_draw2D "1"
seta cg_fov "90"
follownext loop// tv_autofollow.cfg – follownext every 60 seconds
// Assumes com_maxfps 60 → 60fps → 60s * 60 = 3600 frames
seta com_maxfps "60"
seta tv_cycle_start "vstr tv_cycle"
seta tv_cycle "echo ^2[TV]^7 follownext; follownext; wait 3600; vstr tv_cycle"
// Optional manual key:
bind h "follownext"
exec tv_fps.cfg
exec tv_autofollow.cfg
vstr tv_cycle_start
Create a script to start Xvfb and the ET: Legacy client as TV-bot. Both Xvfb and ETL are detached using nohup + setsid, so they keep running after you close PuTTY.
#!/bin/bash
# etl.sh – ET: Legacy TV-Bot im Hintergrund (User: tvbot)
DISPLAY_NUM=":1"
SCREEN_RES="800x600x24"
# Xvfb für diesen User starten, falls noch nicht läuft (voll detached mit nohup + setsid)
if ! pgrep -u "$USER" -x Xvfb >/dev/null 2>&1; then
nohup setsid Xvfb "$DISPLAY_NUM" -screen 0 "$SCREEN_RES" \
> "$HOME/xvfb.log" 2>&1 &
sleep 2
fi
export DISPLAY="$DISPLAY_NUM"
export PULSE_SINK="ETTV" # Sound in den Null-Sink schicken
# in das Verzeichnis des Scripts wechseln
cd "$(dirname "$(readlink -f "$0")")"
LOGFILE="$PWD/etltv.log"
PIDFILE="$PWD/etltv.pid"
# Läuft schon ein TV-Bot?
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "ETc|TV läuft bereits mit PID $(cat "$PIDFILE")."
exit 0
fi
# ET:Legacy-Client als TV-Bot starten (nohup + setsid, damit auch SSH-Logout überlebt)
nohup setsid ./etl.x86_64 \
+connect 84.200.135.3:27960 \
+set name "ETc|TV" \
+set r_fullscreen 0 \
+set r_mode -1 \
+set r_customwidth 800 \
+set r_customheight 600 \
+set com_maxfps 60 \
+set cg_draw2D 1 \
+set s_mute 0 \
+set s_volume 1 \
+exec bot.cfg \
>> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "ETc|TV gestartet, PID: $(cat "$PIDFILE")"
echo "Logfile: $LOGFILE"
chmod 700 /home/tvbot/etlegacy/etl.sh
Run PulseAudio and create a Null sink named ETTV under the tvbot user.
pulseaudio --start --exit-idle-time=-1
pactl load-module module-null-sink \
sink_name=ETTV sink_properties=device.description="ETTV"
pactl set-default-sink ETTV
pactl list short sinks
pactl list short sources | grep ETTV
# you should see ETTV and ETTV.monitor
If you control this from a root shell, always use:
sudo -u tvbot -H <command>
so PulseAudio and pactl talk to the same user session.
Create a script that grabs Xvfb :1, mixes in audio from ETTV.monitor and sends it to Twitch. ffmpeg is also detached with nohup + setsid.
#!/bin/bash
# tw.sh – Streamt Xvfb :1 nach Twitch MIT Audio (User: tvbot)
DISPLAY_NUM=":1"
RESOLUTION="800x600"
FRAMERATE=30
RTMP_URL="rtmp://live.twitch.tv/app/your_key"
LOGFILE="/home/tvbot/twitch_stream.log"
PIDFILE="/home/tvbot/twitch_stream.pid"
export DISPLAY="$DISPLAY_NUM"
# ffmpeg vorhanden?
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "Fehler: ffmpeg wurde nicht gefunden. Bitte mit 'sudo apt install ffmpeg' installieren." >&2
exit 1
fi
# PulseAudio-Quelle für ETTV-Null-Sink ermitteln
AUDIO_SOURCE=$(pactl list short sources | awk '/ETTV\.monitor/ {print $2; exit}')
if [ -z "$AUDIO_SOURCE" ]; then
echo "Fehler: Konnte PulseAudio-Quelle 'ETTV.monitor' nicht finden. Läuft PulseAudio + Null-Sink für tvbot?" >&2
exit 1
fi
# Läuft schon ein Stream?
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "Twitch-Stream läuft bereits mit PID $(cat "$PIDFILE")."
exit 0
fi
# ffmpeg im Hintergrund starten – Video + Audio (nohup + setsid, logout-sicher)
nohup setsid ffmpeg \
-f x11grab -video_size "$RESOLUTION" -framerate "$FRAMERATE" -i "${DISPLAY_NUM}.0" \
-f pulse -i "$AUDIO_SOURCE" \
-c:v libx264 -preset veryfast -tune zerolatency \
-crf 21 -maxrate 2500k -bufsize 5000k \
-pix_fmt yuv420p \
-g 60 \
-c:a aac -b:a 128k -ac 2 \
-f flv "$RTMP_URL" \
>> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "Twitch-Stream gestartet, PID: $(cat "$PIDFILE")"
echo "Logfile: $LOGFILE"
chmod 700 /home/tvbot/etlegacy/tw.sh
Replace RTMP_URL with your own Twitch stream key from the Creator Dashboard if it changes.
All of the following should be run as tvbot (or via sudo -u tvbot -H from root).
cd ~/etlegacy
./etl.sh
cd ~/etlegacy
./tw.sh
kill "$(cat ~/etlegacy/etltv.pid)"
kill "$(cat ~/twitch_stream.pid)"
Example: restart the TV-bot at 05:51 and then every 2 hours (07:51, 09:51, …, 23:51).
#!/bin/bash
# restart_etl.sh – TV-Bot (etl) neu starten
# cron-Beispiel:
# 51 5-23/2 * * * /home/tvbot/etlegacy/restart_etl.sh >> /home/tvbot/etlegacy/cron_restart.log 2>&1
cd /home/tvbot/etlegacy
PIDFILE="etltv.pid"
# alten TV-Bot beenden, falls er läuft
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "$(date '+%F %T') – stoppe ETc|TV (PID $PID)" >> etltv_restart.log
kill "$PID"
sleep 5
fi
fi
# ggf. alte PID-Datei entfernen
rm -f "$PIDFILE"
# neu starten
echo "$(date '+%F %T') – starte ETc|TV neu" >> etltv_restart.log
./etl.sh
chmod 700 /home/tvbot/etlegacy/restart_etl.sh
tvbot)crontab -e
51 5-23/2 * * * /home/tvbot/etlegacy/restart_etl.sh >> /home/tvbot/etlegacy/cron_restart.log 2>&1