Di bawah ini adalah contoh kode .sh untuk stream ke YouTube dari Linux, misal gdm3, dengan pengaturan yang bisa kita sesuaikan.
#!/bin/bash
# ============================================================================
# KONFIGURASI STREAMING
# ============================================================================
# Pengaturan RTMP
readonly RTMP_URL="rtmp://a.rtmp.youtube.com/live2/STREAMKEY"
# Pengaturan Capture Area (area layar yang diambil, sebelum di-scale)
readonly CAPTURE_WIDTH=750
readonly CAPTURE_HEIGHT=500
readonly OFFSET_X=150
readonly OFFSET_Y=100
# Pengaturan Output (HANYA satu resolusi, bebas kamu set sendiri)
# Contoh: mau square? set 512x512. Mau 16:9? set 1280x720. Dst.
readonly OUTPUT_WIDTH=512
readonly OUTPUT_HEIGHT=512
# Pengaturan Video
readonly FRAMERATE=12
readonly BITRATE="1000k"
readonly MAXRATE="1200k"
readonly BUFSIZE="2000k"
# Pengaturan Audio
readonly AUDIO_FILE="/home/habibie/Documents/stream/sbacodes/allsba.MP3"
readonly AUDIO_BITRATE="96k"
readonly AUDIO_SAMPLERATE=44100
# ============================================================================
# VARIABEL GLOBAL
# ============================================================================
DISPLAY_NUM=":0"
FFMPEG_PID=""
# ============================================================================
# FUNGSI UTILITY
# ============================================================================
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2
}
check_dependencies() {
local missing_deps=()
if ! command -v ffmpeg &> /dev/null; then
missing_deps+=("ffmpeg")
fi
if ! command -v xdpyinfo &> /dev/null; then
missing_deps+=("xdpyinfo")
fi
if ! command -v xhost &> /dev/null; then
missing_deps+=("x11-utils")
fi
if [ ${#missing_deps[@]} -ne 0 ]; then
log_error "Missing dependencies: ${missing_deps[*]}"
log_message "Install with: sudo apt install ${missing_deps[*]}"
exit 1
fi
log_message "All dependencies found"
}
# ============================================================================
# FUNGSI DETEKSI SISTEM
# ============================================================================
detect_display() {
if [ -n "$DISPLAY" ]; then
DISPLAY_NUM="$DISPLAY"
log_message "Using DISPLAY from environment: $DISPLAY_NUM"
else
local detected_display=$(who | grep -E "\(:[0-9]+\.[0-9]+\)" | head -1 | sed 's/.*(\(:[0-9]*\.[0-9]*\))/\1/' | cut -d. -f1)
if [ -n "$detected_display" ]; then
DISPLAY_NUM="$detected_display"
log_message "Detected active display: $DISPLAY_NUM"
else
DISPLAY_NUM=":0"
log_message "Using default display: $DISPLAY_NUM"
fi
fi
# PENTING (fix offset): x11grab sering gagal parsing offset "+x,y" kalau
# display tidak menyertakan nomor screen (mis. ":0" tanpa ".0").
# Pastikan selalu ada ".0" di belakang, kecuali sudah ada nomor screen.
if [[ "$DISPLAY_NUM" != *.* ]]; then
DISPLAY_NUM="${DISPLAY_NUM}.0"
log_message "Normalized display to include screen number: $DISPLAY_NUM"
fi
export DISPLAY="$DISPLAY_NUM"
}
detect_session_type() {
if [ -n "$XDG_SESSION_TYPE" ]; then
log_message "Session type: $XDG_SESSION_TYPE"
if [ "$XDG_SESSION_TYPE" = "wayland" ]; then
log_error "Wayland detected! x11grab won't work."
log_message "Please switch to X11 session at login screen."
log_message "Or use GNOME Screen Cast / PipeWire alternative."
return 1
fi
elif [ -n "$WAYLAND_DISPLAY" ]; then
log_error "Wayland detected (WAYLAND_DISPLAY set)"
return 1
else
log_message "Session type: X11 (assumed)"
fi
return 0
}
check_display_access() {
log_message "Checking display access..."
if ! xdpyinfo -display "$DISPLAY_NUM" &>/dev/null; then
log_error "Cannot access display: $DISPLAY_NUM"
log_message "Trying to grant access with xhost..."
if xhost +SI:localuser:$(whoami) 2>/dev/null; then
log_message "Display access granted"
else
log_error "Failed to grant display access"
log_message "Try running: xhost +local:"
return 1
fi
else
log_message "Display access OK"
fi
return 0
}
check_audio_file() {
if [ ! -f "$AUDIO_FILE" ]; then
log_message "WARNING: Audio file not found: $AUDIO_FILE"
log_message "Streaming will continue without audio"
return 1
fi
log_message "Audio file found: $AUDIO_FILE"
return 0
}
# ============================================================================
# FUNGSI INTI STREAMING
# ============================================================================
# Filter video: HANYA scale ke resolusi output yang kamu tentukan.
# TIDAK ada setdar dipaksa lagi -> aspect ratio otomatis ngikut OUTPUT_WIDTH x
# OUTPUT_HEIGHT yang kamu set (ini fix untuk masalah #3).
# setsar=1 memastikan pixel aspect ratio kotak/normal, biar tidak ada distorsi
# tambahan dari metadata SAR yang aneh.
build_video_filter() {
echo "scale=${OUTPUT_WIDTH}:${OUTPUT_HEIGHT}:flags=fast_bilinear,setsar=1"
}
# Build argumen ffmpeg sebagai ARRAY (bukan string + eval).
# Ini fix untuk masalah #1: eval pada string rawan salah parsing quote/offset,
# array jauh lebih aman dan predictable karena tiap argumen dikirim persis
# apa adanya ke ffmpeg, tanpa proses parsing shell tambahan.
build_ffmpeg_args() {
local has_audio="$1"
local video_filter
video_filter=$(build_video_filter)
# Format input capture: display.screen+offset_x,offset_y
# Contoh hasil: ":0.0+150,100"
local capture_input="${DISPLAY_NUM}+${OFFSET_X},${OFFSET_Y}"
FFMPEG_ARGS=(
-f x11grab
-framerate "$FRAMERATE"
-video_size "${CAPTURE_WIDTH}x${CAPTURE_HEIGHT}"
-i "$capture_input"
)
if [ "$has_audio" = "true" ]; then
FFMPEG_ARGS+=(-stream_loop -1 -i "$AUDIO_FILE")
FFMPEG_ARGS+=(-map 0:v -map 1:a)
else
FFMPEG_ARGS+=(-map 0:v -an)
fi
FFMPEG_ARGS+=(
-c:v libx264
-preset ultrafast
-tune zerolatency
-b:v "$BITRATE"
-maxrate "$MAXRATE"
-bufsize "$BUFSIZE"
-vf "$video_filter"
-pix_fmt yuv420p
-threads 1
)
if [ "$has_audio" = "true" ]; then
FFMPEG_ARGS+=(
-c:a aac
-b:a "$AUDIO_BITRATE"
-ar "$AUDIO_SAMPLERATE"
)
fi
FFMPEG_ARGS+=(-f flv "$RTMP_URL")
}
run_ffmpeg() {
local has_audio="false"
if [ -f "$AUDIO_FILE" ]; then
has_audio="true"
fi
log_message "========================================"
log_message "Starting stream with:"
log_message " - Display: $DISPLAY_NUM"
log_message " - Capture: ${CAPTURE_WIDTH}x${CAPTURE_HEIGHT} at offset (${OFFSET_X},${OFFSET_Y})"
log_message " - Output resolution: ${OUTPUT_WIDTH}x${OUTPUT_HEIGHT}"
log_message " - Audio: $( [ "$has_audio" = "true" ] && echo "Enabled" || echo "Disabled" )"
log_message "========================================"
build_ffmpeg_args "$has_audio"
log_message "Command: ffmpeg ${FFMPEG_ARGS[*]}"
ffmpeg "${FFMPEG_ARGS[@]}"
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "FFmpeg stopped with error code: $exit_code"
return $exit_code
else
log_message "FFmpeg stopped normally"
return 0
fi
}
# ============================================================================
# FUNGSI MANAJEMEN STREAM
# ============================================================================
kill_existing_streams() {
local pids=$(pgrep -f "ffmpeg.*$RTMP_URL")
if [ -n "$pids" ]; then
log_message "Killing existing ffmpeg processes: $pids"
kill -9 $pids 2>/dev/null
sleep 2
fi
}
cleanup() {
log_message "Received exit signal. Cleaning up..."
kill_existing_streams
xhost -SI:localuser:$(whoami) 2>/dev/null
log_message "Cleanup complete. Exiting."
exit 0
}
trap cleanup SIGINT SIGTERM
# ============================================================================
# FUNGSI MAIN
# ============================================================================
main() {
log_message "========================================="
log_message "Starting YouTube Live Stream Script v4.0"
log_message "========================================="
check_dependencies
detect_display
if ! detect_session_type; then
log_message "WARNING: Running on Wayland. Trying X11 capture anyway..."
log_message "If this fails, please switch to X11 session"
fi
if ! check_display_access; then
log_error "Cannot access display. Exiting."
exit 1
fi
check_audio_file
kill_existing_streams
log_message "========================================="
log_message "Stream URL: $RTMP_URL"
log_message "Capture Area: ${CAPTURE_WIDTH}x${CAPTURE_HEIGHT} at offset (${OFFSET_X},${OFFSET_Y})"
log_message "Output Resolution: ${OUTPUT_WIDTH}x${OUTPUT_HEIGHT}"
log_message "========================================="
log_message "To stop streaming: Press Ctrl+C"
log_message "========================================="
local restart_count=0
while true; do
((restart_count++))
log_message "Stream attempt #$restart_count"
run_ffmpeg
log_message "Stream stopped. Restarting in 10 seconds..."
sleep 10
done
}
# ============================================================================
# EKSEKUSI
# ============================================================================
main "$@"