Skip to content

Instantly share code, notes, and snippets.

@Cdaprod
Last active April 3, 2025 21:57
Show Gist options
  • Save Cdaprod/57cf73d8773fadaa2f6b8aaf7a0c893d to your computer and use it in GitHub Desktop.
Save Cdaprod/57cf73d8773fadaa2f6b8aaf7a0c893d to your computer and use it in GitHub Desktop.
My Linux live-streaming command—“Secret-Weapon Grade” Alias
#!/usr/bin/env bash
# sudo tee /dev/null <<'EOF'
# ==============================================================================
# Streamctl - Wayland/Wlroots Display Streaming Tool (Hardened, Parallel-Enabled)
#
# A versatile, fault-tolerant streaming control utility with support for:
# • HLS, MJPEG, RTSP streaming modes with graceful fallback
# • Custom streaming profiles (e.g. YouTube with your stream key)
# • OBS WebSocket configuration (port 4455) for eventual integration
# • Desktop recording mode
# • Live system stats, self-healing, and redundant failover
# • Parallel execution where beneficial (using GNU parallel if available)
#
# Created by: David Cannan
# GitHub: https://github.com/Cdaprod
# Email: [email protected]
#
# Save to /usr/local/bin/streamctl and run: chmod +x /usr/local/bin/streamctl
# ==============================================================================
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Environment & Configuration
export XDG_RUNTIME_DIR=/run/user/1000
export WAYLAND_DISPLAY=wayland-0
export DISPLAY=:0
SESSION="wfstream"
OUTPUT_MODE="${2:-hls}" # default mode if not specified
HLS_DIR="/dev/shm/hls"
HLS_PORT="8080"
STREAM_PORT="8090"
RTSP_PORT="8554"
LOG_FILE="/tmp/streamctl.log"
FIFO="/tmp/wf-stream.fifo"
PID_FILE="/tmp/streamctl.pid"
# OBS WebSocket configuration (future integration)
OBS_WEBSOCKET_PORT="4455"
# YouTube Streaming (update your key here or via a profile)
YOUTUBE_KEY="abcd-1234-xyz9-wxyz"
YOUTUBE_URL="rtmp://a.rtmp.youtube.com/live2/${YOUTUBE_KEY}"
# -----------------------------------------------------------------------------
# Logging functions
log() { echo -e "${CYAN}[$(date +"%Y-%m-%d %H:%M:%S")]${NC} $1" | tee -a "$LOG_FILE"; }
error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"; exit 1; }
warning(){ echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE"; }
success(){ echo -e "${GREEN}[SUCCESS]${NC} $1"; }
# -----------------------------------------------------------------------------
# Fallback display detection: try wlr-randr, then sway, then waybar
get_display_id() {
local try_order=("wlr-randr" "sway" "waybar")
local DISPLAY_ID=""
for method in "${try_order[@]}"; do
case "$method" in
wlr-randr)
if command -v wlr-randr &>/dev/null; then
DISPLAY_ID=$(wlr-randr | awk '/^[^ ]/ {id=$1} /Enabled: yes/ {print id}' | head -n1)
[[ -n "$DISPLAY_ID" ]] && break
fi
;;
sway)
if command -v sway &>/dev/null; then
DISPLAY_ID=$(sway -t get_outputs | grep name | head -n1 | awk '{print $2}' | tr -d '"')
[[ -n "$DISPLAY_ID" ]] && break
fi
;;
waybar)
if command -v waybar &>/dev/null; then
DISPLAY_ID=$(waybar -l | grep "Output:" | head -n1 | awk '{print $2}')
[[ -n "$DISPLAY_ID" ]] && break
fi
;;
esac
done
if [[ -z "$DISPLAY_ID" ]]; then
error "No valid display output found after trying: ${try_order[*]}"
fi
echo "$DISPLAY_ID"
}
# -----------------------------------------------------------------------------
# Create FIFO with safe retries
ensure_fifo() {
local attempt=1
local max_attempts=3
while [[ $attempt -le $max_attempts ]]; do
if [[ -p "$FIFO" ]]; then
rm -f "$FIFO"
sleep 0.5
fi
mkfifo "$FIFO" && { log "FIFO created at $FIFO"; return 0; }
warning "Attempt $attempt to create FIFO failed."
attempt=$((attempt + 1))
sleep 1
done
error "Failed to create FIFO after $max_attempts attempts."
}
# -----------------------------------------------------------------------------
# Check for required commands; return first found from a list
safe_command() {
for cmd in "$@"; do
if command -v "$cmd" &>/dev/null; then
echo "$cmd" && return 0
fi
done
return 1
}
# -----------------------------------------------------------------------------
# Kill process if alive based on a PID file
kill_pid_if_alive() {
local pidfile="$1"
if [[ -f "$pidfile" ]]; then
local pid
pid=$(<"$pidfile")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null
log "Killed process $pid from $pidfile"
fi
rm -f "$pidfile"
fi
}
# -----------------------------------------------------------------------------
# Load streaming profile if provided (profiles stored in ~/.config/streamctl/profiles/)
load_profile() {
local profile="$1"
local profile_file="$HOME/.config/streamctl/profiles/${profile}.profile"
if [ -f "$profile_file" ]; then
source "$profile_file"
log "Loaded profile: $profile"
else
warning "Profile not found: $profile. Using defaults."
fi
}
# -----------------------------------------------------------------------------
# Start streaming modes
start_hls() {
mkdir -p "$HLS_DIR" || error "Failed to create HLS directory: $HLS_DIR"
ensure_fifo
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
log "Starting HLS stream: http://${ip_address}:${HLS_PORT}/stream.m3u8"
tmux new-session -d -s "$SESSION" bash -c "
$(safe_command wf-recorder) -o \"\$DISPLAY_ID\" -m matroska -f \"$FIFO\" 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.recorder\"
ffmpeg -nostdin -y -f matroska -i \"$FIFO\" \
-c:v libx264 -preset ultrafast -b:v ${BITRATE:-4500k} -maxrate ${BITRATE:-4500k} -bufsize 9000k -g 60 \
-f hls -hls_time 2 -hls_list_size 5 -hls_flags delete_segments \
-hls_segment_filename \"${HLS_DIR}/stream_%03d.ts\" \"${HLS_DIR}/stream.m3u8\" 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.ffmpeg\"
wait
" && return 0
return 1
}
start_mjpeg() {
ensure_fifo
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
log "Starting MJPEG stream: http://${ip_address}:${STREAM_PORT}/"
tmux new-session -d -s "$SESSION" bash -c "
$(safe_command wf-recorder) -o \"\$DISPLAY_ID\" -m matroska -f \"$FIFO\" 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.recorder\"
ffmpeg -nostdin -y -f matroska -i \"$FIFO\" \
-c:v mjpeg -f mjpeg -q:v 5 http://0.0.0.0:${STREAM_PORT}/ 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.ffmpeg\"
wait
" && return 0
return 1
}
start_rtsp() {
ensure_fifo
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
log "Starting RTSP stream: rtsp://${ip_address}:${RTSP_PORT}/live.stream"
tmux new-session -d -s "$SESSION" bash -c "
$(safe_command wf-recorder) -o \"\$DISPLAY_ID\" -m matroska -f \"$FIFO\" 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.recorder\"
ffmpeg -nostdin -y -f matroska -i \"$FIFO\" \
-c:v libx264 -preset ultrafast -tune zerolatency \
-f rtsp rtsp://0.0.0.0:${RTSP_PORT}/live.stream 2>&1 | tee -a \"${LOG_FILE}\" &
echo \$! > \"${PID_FILE}.ffmpeg\"
wait
" && return 0
return 1
}
# -----------------------------------------------------------------------------
# Fallback streaming: try HLS, then MJPEG, then RTSP
start_stream_with_fallback() {
local modes=("hls" "mjpeg" "rtsp")
local success=0
for mode in "${modes[@]}"; do
log "Attempting stream mode: $mode"
case "$mode" in
hls) start_hls && { success=1; break; } ;;
mjpeg) start_mjpeg && { success=1; break; } ;;
rtsp) start_rtsp && { success=1; break; } ;;
esac
warning "Stream mode $mode failed. Trying next..."
done
[[ $success -eq 1 ]] || error "All stream modes failed."
}
# -----------------------------------------------------------------------------
# Desktop recording mode
record_desktop() {
local output_file="${HOME}/Videos/streamctl_recording_$(date +%Y%m%d_%H%M%S).mkv"
log "Recording desktop to: $output_file"
$(safe_command wf-recorder) -o "$DISPLAY_ID" -f "$output_file"
}
# -----------------------------------------------------------------------------
# Stop streaming and clean up resources, using parallel where possible
stop_stream() {
# Kill tmux sessions concurrently (background them)
if tmux has-session -t "$SESSION" 2>/dev/null; then
tmux kill-session -t "$SESSION" 2>/dev/null &
log "Terminated streaming session: $SESSION"
fi
if tmux has-session -t "${SESSION}_http" 2>/dev/null; then
tmux kill-session -t "${SESSION}_http" 2>/dev/null &
log "Terminated HTTP session."
fi
wait
# Use GNU parallel to kill PID files concurrently if available
if command -v parallel &>/dev/null; then
echo "${PID_FILE}.recorder ${PID_FILE}.ffmpeg ${PID_FILE}.http" | tr ' ' '\n' | parallel kill_pid_if_alive {}
else
for pid_file in "${PID_FILE}.recorder" "${PID_FILE}.ffmpeg" "${PID_FILE}.http"; do
kill_pid_if_alive "$pid_file" &
done
wait
fi
# Remove FIFO if exists
[[ -p "$FIFO" ]] && { rm -f "$FIFO"; log "FIFO removed."; }
# Clean HLS files concurrently
if [ -d "$HLS_DIR" ]; then
rm -rf "${HLS_DIR:?}"/*.ts "${HLS_DIR:?}"/*.m3u8 2>/dev/null &
wait
log "Cleaned up HLS files."
fi
success "Stream stopped and resources cleaned up."
}
# -----------------------------------------------------------------------------
# Check stream status and system stats
check_status() {
echo -e "${BOLD}${CYAN}=== Streamctl Status ===${NC}"
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo -e "✅ ${GREEN}Streaming session ($SESSION) is running.${NC}"
else
echo -e "❌ ${RED}No active streaming session.${NC}"
fi
if tmux has-session -t "${SESSION}_http" 2>/dev/null; then
echo -e "✅ ${GREEN}HTTP server is running on port ${HLS_PORT}.${NC}"
else
echo -e "❌ ${RED}HTTP server is not running.${NC}"
fi
# Show PID status
for pid_file in "${PID_FILE}.recorder" "${PID_FILE}.ffmpeg" "${PID_FILE}.http"; do
if [ -f "$pid_file" ]; then
pid=$(<"$pid_file")
if kill -0 "$pid" 2>/dev/null; then
echo -e "✅ ${GREEN}Process running (PID: ${pid}).${NC}"
fi
fi
done
# System resource stats
echo -e "${BOLD}${CYAN}=== System Stats ===${NC}"
echo -e "${BOLD}CPU:${NC} $(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}')"
echo -e "${BOLD}Memory:${NC} $(free -h | awk '/Mem:/ {print $3 "/" $2}')"
echo -e "${BOLD}Disk:${NC} $(df -h / | awk 'NR==2 {print $3 "/" $2}')"
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
case "$OUTPUT_MODE" in
hls) echo -e "${YELLOW}Stream URL:${NC} http://${ip_address}:${HLS_PORT}/stream.m3u8" ;;
mjpeg) echo -e "${YELLOW}Stream URL:${NC} http://${ip_address}:${STREAM_PORT}/" ;;
rtsp) echo -e "${YELLOW}Stream URL:${NC} rtsp://${ip_address}:${RTSP_PORT}/live.stream" ;;
esac
echo -e "${BOLD}${CYAN}OBS WebSocket:${NC} Configured on port ${OBS_WEBSOCKET_PORT}"
echo -e "${BOLD}YouTube URL:${NC} ${YOUTUBE_URL}"
echo -e "${BOLD}Cooked by:${NC} ${BOLD}David Cannan (Cdaprod)${NC}"
}
# -----------------------------------------------------------------------------
# Self-healing: if no streaming session is found, try to restart
self_heal() {
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
log "No streaming session detected. Initiating self-heal..."
start_stream_with_fallback
else
log "Streaming session is active."
fi
}
# -----------------------------------------------------------------------------
# Retry logic for starting stream (if --retry flag is provided)
retry_start() {
local retries=5
for ((i = 1; i <= retries; i++)); do
log "Attempt $i to start stream..."
if start_hls; then
log "Stream started successfully on attempt $i."
return 0
fi
sleep 2
done
error "All $retries attempts to start the stream failed."
}
# -----------------------------------------------------------------------------
# Cleanup handler: ensure resources are freed on exit
cleanup() {
log "Cleaning up before exit..."
stop_stream
}
trap cleanup EXIT INT TERM
# -----------------------------------------------------------------------------
# Main command-line argument parsing
case "$1" in
start)
# Optionally load a profile if provided as third parameter (e.g., 'youtube')
[ -n "$3" ] && load_profile "$3"
# Use --retry flag if provided
if [[ "$2" == "--retry" ]]; then
retry_start
else
start_stream_with_fallback
fi
;;
stop)
stop_stream
;;
status)
check_status
;;
record)
record_desktop
;;
attach)
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
error "No streaming session to attach to. Start one with 'streamctl start'."
fi
tmux attach -t "$SESSION"
;;
log)
if [ ! -f "$LOG_FILE" ]; then
error "Log file not found: $LOG_FILE"
fi
tail -f "$LOG_FILE"
;;
selfheal)
self_heal
;;
help|*)
echo -e "${BOLD}${CYAN}=== Streamctl Help ===${NC}"
echo -e "${BOLD}Usage:${NC}"
echo -e " streamctl ${BOLD}start${NC} [--retry] [profile] Start streaming (optionally retry, and load a profile)"
echo -e " streamctl ${BOLD}stop${NC} Stop streaming and clean up"
echo -e " streamctl ${BOLD}status${NC} Show streaming status and system stats"
echo -e " streamctl ${BOLD}record${NC} Record desktop to a file"
echo -e " streamctl ${BOLD}attach${NC} Attach to the streaming tmux session"
echo -e " streamctl ${BOLD}log${NC} Tail the stream log"
echo -e " streamctl ${BOLD}selfheal${NC} Self-heal: restart stream if not running"
echo -e " streamctl ${BOLD}help${NC} Show this help message"
echo ""
echo -e "${BOLD}Additional Config:${NC}"
echo -e " OBS WebSocket configured on port ${OBS_WEBSOCKET_PORT}"
echo -e " YouTube URL: ${YOUTUBE_URL}"
exit 1
;;
esac
# EOF (sudo tee /dev/null <<'EOF' block end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment