Rofi screen recording with zoom
2026-01-17

Screen recording with rofi menu, VAAPI hardware acceleration, and smooth zoom animations.
- Rofi menu interface for selecting recording mode (screencast, zoom, selected area)
- Hardware-accelerated encoding via VAAPI with automatic software fallback
- Automatic multi-monitor detection based on current mouse position
- Zoom mode with smooth easing animations triggered by AltGr key (1s hold)
- Background post-processing with notifications and detailed debug logging
#!/bin/bash
# ==============================================================================
# ZOOM CONFIGURATION
# ==============================================================================
# Temporary workspace (will be created and cleaned up)
ZOOM_WORK_DIR="/tmp/video_zoom_session"
ZOOM_LOCK_FILE="/tmp/video_zoom.pid" # Internal lock for zoom components
# Internal Files for Zoom
ZOOM_LOG_MOUSE="$ZOOM_WORK_DIR/mouse.log"
ZOOM_LOG_KEYS="$ZOOM_WORK_DIR/keys.log"
ZOOM_LOG_COORD="$ZOOM_WORK_DIR/coords.txt"
ZOOM_VIDEO_RAW="$ZOOM_WORK_DIR/raw.mp4"
ZOOM_CMD_FILE="$ZOOM_WORK_DIR/overlay.cmd"
ZOOM_DEBUG_LOG="/tmp/video_zoom_debug.log"
# ==============================================================================
# SHARED HELPERS
# ==============================================================================
zoom_log_debug() {
echo "[$(date '+%H:%M:%S')] $1" >> "$ZOOM_DEBUG_LOG"
}
detect_monitor() {
# Get mouse position
eval $(xdotool getmouselocation --shell)
# Default to main screen if detection fails
local w h x y
local DETECTED_W=1920 DETECTED_H=1080 DETECTED_X=0 DETECTED_Y=0
# Check each connected monitor
# Format: 1920x1080+0+0
while read -r geom; do
w=$(echo "$geom" | cut -dx -f1)
h=$(echo "$geom" | cut -dx -f2 | cut -d+ -f1)
x=$(echo "$geom" | cut -d+ -f2)
y=$(echo "$geom" | cut -d+ -f3)
# Check if mouse is within this monitor's bounds
if (( X >= x && X < x + w && Y >= y && Y < y + h )); then
DETECTED_W=$w
DETECTED_H=$h
DETECTED_X=$x
DETECTED_Y=$y
break
fi
done < <(xrandr | grep " connected" | grep -oE '[0-9]+x[0-9]+\+[0-9]+\+[0-9]+')
# Return values (only this goes to stdout)
echo "DETECTED_W=$DETECTED_W DETECTED_H=$DETECTED_H DETECTED_X=$DETECTED_X DETECTED_Y=$DETECTED_Y"
}
screencast() {
# 1. Detect Monitor
eval $(detect_monitor)
local outfile="$HOME/screencast-$(date '+%y%m%d-%H%M-%S').mp4"
# 2. Try VAAPI first, fallback to software
if [ -e /dev/dri/renderD128 ]; then
ffmpeg -y \
-f x11grab \
-framerate 30 \
-video_size "${DETECTED_W}x${DETECTED_H}" \
-i ":0.0+${DETECTED_X},${DETECTED_Y}" \
-f alsa -i default \
-vaapi_device /dev/dri/renderD128 \
-vf 'format=nv12,hwupload' \
-c:v h264_vaapi -qp 24 \
-c:a aac \
"$outfile" >> "$ZOOM_DEBUG_LOG" 2>&1 &
else
ffmpeg -y \
-f x11grab \
-framerate 30 \
-video_size "${DETECTED_W}x${DETECTED_H}" \
-i ":0.0+${DETECTED_X},${DETECTED_Y}" \
-f alsa -i default \
-c:v libx264 -preset ultrafast -crf 23 \
-c:a aac \
"$outfile" >> "$ZOOM_DEBUG_LOG" 2>&1 &
fi
echo $! > /tmp/recordingpid
}
killrecording() {
if [ -f /tmp/recordingpid ]; then
recpid="$(cat /tmp/recordingpid)"
kill -15 "$recpid" 2>/dev/null
rm -f /tmp/recordingpid
sleep 2
kill -9 "$recpid" 2>/dev/null
notify-send "Recording" "Recording ended."
fi
exit
}
# ==============================================================================
# ZOOM LOGIC
# ==============================================================================
zoom_process() {
notify-send "Video Zoom" "Processing video... This may take a moment."
zoom_log_debug "Processing started."
if [ ! -f "$ZOOM_VIDEO_RAW" ] || [ ! -f "$ZOOM_LOG_COORD" ]; then
notify-send "Video Zoom" "Error: Missing raw files."
zoom_log_debug "Error: Missing raw files."
exit 1
fi
# Check if any keys were pressed
KEY_COUNT=$(grep -c "KEY" "$ZOOM_LOG_COORD")
zoom_log_debug "Found $KEY_COUNT key toggle events."
if [ "$KEY_COUNT" -eq "0" ]; then
notify-send "Video Zoom" "Warning: No zoom toggles detected."
fi
# Detect Dimensions - parse simply
VID_W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$ZOOM_VIDEO_RAW")
VID_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$ZOOM_VIDEO_RAW")
# Fallback
if [ -z "$VID_W" ]; then
zoom_log_debug "ffprobe failed to detect width. Defaulting to 1920."
VID_W=1920
VID_H=1080
fi
zoom_log_debug "Raw video dimensions: ${VID_W}x${VID_H}"
# Calculate zoom target size (1/3 of video dimensions, maintaining aspect ratio)
ZOOM_W=$((VID_W / 3))
ZOOM_H=$((VID_H / 3))
# Generate Animation Commands
awk -v vid_w="$VID_W" -v vid_h="$VID_H" -v target_w="$ZOOM_W" -v target_h="$ZOOM_H" '
BEGIN {
anim_duration = 0.3;
zoom_active = 0; zoom_level = 0.0; last_ts = 0;
smooth_len = 5; idx = 0;
for(i=0; i<smooth_len; i++) { x_buf[i]=vid_w/2; y_buf[i]=vid_h/2; }
last_key_time = -1.0;
zoom_start_time = 0;
}
function ease(t) {
if (t <= 0) return 0;
if (t >= 1) return 1;
return t * t * (3 - 2 * t);
}
$2 == "KEY" {
# Simple debounce
if ($1 - last_key_time > 0.3) {
zoom_active = 1;
zoom_start_time = $1;
last_key_time = $1;
}
}
$2 == "MOUSE" {
ts = $1; raw_x = $3; raw_y = $4;
dt = ts - last_ts; last_ts = ts;
if (last_ts == 0) dt = 0;
# Auto zoom-out after 1 second
if (zoom_active && (ts - zoom_start_time) > 1.0) {
zoom_active = 0;
}
step = dt / anim_duration;
if (zoom_active) { zoom_level += step; if (zoom_level > 1.0) zoom_level = 1.0; }
else { zoom_level -= step; if (zoom_level < 0.0) zoom_level = 0.0; }
z_factor = ease(zoom_level);
# Smoothing
x_buf[idx] = raw_x; y_buf[idx] = raw_y; idx = (idx + 1) % smooth_len;
sum_x=0; sum_y=0; count=0;
for (i in x_buf) { sum_x += x_buf[i]; sum_y += y_buf[i]; count++; }
avg_x = sum_x/count; avg_y = sum_y/count;
# Geometry Math
cur_w = vid_w + (target_w - vid_w) * z_factor;
cur_h = vid_h + (target_h - vid_h) * z_factor;
c1_x = vid_w / 2; c1_y = vid_h / 2;
cur_cx = c1_x + (avg_x - c1_x) * z_factor;
cur_cy = c1_y + (avg_y - c1_y) * z_factor;
out_x = cur_cx - (cur_w / 2);
out_y = cur_cy - (cur_h / 2);
if (out_x < 0) out_x = 0; if (out_x > vid_w - cur_w) out_x = vid_w - cur_w;
if (out_y < 0) out_y = 0; if (out_y > vid_h - cur_h) out_y = vid_h - cur_h;
printf "%f crop@anim w %d;\n%f crop@anim h %d;\n%f crop@anim x %d;\n%f crop@anim y %d;\n", ts, int(cur_w), ts, int(cur_h), ts, int(out_x), ts, int(out_y);
}
' "$ZOOM_LOG_COORD" > "$ZOOM_CMD_FILE"
# Generate timestamp for output
FINAL_OUTPUT="$HOME/zoom_capture_$(date +%Y%m%d_%H%M%S).mp4"
zoom_log_debug "Output target: $FINAL_OUTPUT"
zoom_log_debug "Running FFmpeg..."
# FFmpeg Render - scale to original resolution
ffmpeg -y \
-i "$ZOOM_VIDEO_RAW" \
-filter_complex "[0:v] sendcmd=f='$ZOOM_CMD_FILE', crop@anim=w=$VID_W:h=$VID_H:x=0:y=0, scale=${VID_W}:${VID_H} [out]" \
-map "[out]" -map 0:a? -c:v libx264 -crf 23 -preset fast -c:a copy "$FINAL_OUTPUT" 2>> "$ZOOM_DEBUG_LOG"
RET=$?
zoom_log_debug "FFmpeg exit code: $RET"
if [ $RET -eq 0 ] && [ -f "$FINAL_OUTPUT" ]; then
notify-send "Video Zoom" "Success! Saved to: $FINAL_OUTPUT"
zoom_log_debug "Success. Output: $FINAL_OUTPUT"
rm -rf "$ZOOM_WORK_DIR"
else
notify-send "Video Zoom" "Processing Failed. Check $ZOOM_DEBUG_LOG"
zoom_log_debug "FFmpeg failed or output missing."
fi
}
zoom_start() {
# Ensure Work Dir Exists
mkdir -p "$ZOOM_WORK_DIR"
zoom_log_debug "Starting new recording session..."
# 1. Detect Monitor Geometry
eval $(detect_monitor)
# Verify detection
if [ -z "$DETECTED_W" ] || [ "$DETECTED_W" -eq 0 ]; then
DETECTED_W=1920; DETECTED_H=1080; DETECTED_X=0; DETECTED_Y=0
fi
zoom_log_debug "Monitor: ${DETECTED_W}x${DETECTED_H}+${DETECTED_X}+${DETECTED_Y}"
# Save for post-processing and mouse logger
echo "$DETECTED_X $DETECTED_Y" > "$ZOOM_WORK_DIR/monitor_offset"
# 2. Start Loggers
# Mouse Logger
(
read OFF_X OFF_Y < "$ZOOM_WORK_DIR/monitor_offset"
while true; do
eval $(xdotool getmouselocation --shell)
# Adjust coordinates relative to the recorded monitor
echo "$(date +%s.%N) $((X - OFF_X)) $((Y - OFF_Y))"
sleep 0.04
done
) > "$ZOOM_LOG_MOUSE" &
PID_MOUSE=$!
# Keyboard Logger
# AltGr is usually keycode 108.
KEYCODE=108
(
# Find all slave keyboards
KBD_IDS=$(xinput list | grep "slave keyboard" | grep -v -i "Virtual\|XTEST\|Power\|Video\|Sleep\|Button" | sed 's/.*id=\([0-9]*\).*/\1/')
# Start a listener for each keyboard in the background
for kid in $KBD_IDS; do
# Use stdbuf to ensure output is not buffered
stdbuf -oL xinput test "$kid" 2>/dev/null | grep --line-buffered "key press.*$KEYCODE" | while read line; do
echo "$(date +%s.%N) TOGGLE"
done &
done
wait
) > "$ZOOM_LOG_KEYS" &
PID_KEYS=$!
zoom_log_debug "Listening for AltGr (108) on keyboards: $(echo $KBD_IDS)"
# Sync Time
SYNC_START=$(date +%s.%N)
echo "$SYNC_START" > "$ZOOM_WORK_DIR/start_time"
# 3. Start FFmpeg
# Explicitly construct geometry strings
GEOM="${DETECTED_W}x${DETECTED_H}"
OFFSET=":0.0+${DETECTED_X},${DETECTED_Y}"
zoom_log_debug "FFmpeg starting with: $GEOM at $OFFSET"
if [ -e /dev/dri/renderD128 ]; then
ffmpeg -y -f x11grab -draw_mouse 1 -framerate 30 -video_size "$GEOM" -i "$OFFSET" \
-f alsa -i default \
-vaapi_device /dev/dri/renderD128 \
-vf 'format=nv12,hwupload' \
-c:v h264_vaapi -qp 24 \
-c:a aac \
"$ZOOM_VIDEO_RAW" > /dev/null 2>&1 &
else
ffmpeg -y -f x11grab -draw_mouse 1 -framerate 30 -video_size "$GEOM" -i "$OFFSET" \
-f alsa -i default \
-c:v libx264 -preset ultrafast -crf 23 \
-c:a aac \
"$ZOOM_VIDEO_RAW" > /dev/null 2>&1 &
fi
PID_FFMPEG=$!
# Save PIDs
echo "$$ $PID_FFMPEG $PID_MOUSE $PID_KEYS" > "$ZOOM_LOCK_FILE"
echo $$ > /tmp/recordingpid
zoom_log_debug "Recording active. PIDs: $$ $PID_FFMPEG $PID_MOUSE $PID_KEYS"
notify-send "Video Zoom" "Started. Press AltGr to Zoom."
# Keep alive until signaled
trap "break" SIGINT SIGTERM
while true; do sleep 1; done
# 4. Stop & Cleanup
zoom_log_debug "Stopping recording..."
kill $PID_FFMPEG $PID_MOUSE $PID_KEYS 2>/dev/null
# Wait for logs to flush
sleep 0.5
# Merge Logs
zoom_log_debug "Merging logs..."
if [ -f "$ZOOM_WORK_DIR/start_time" ]; then
SYNC_START=$(cat "$ZOOM_WORK_DIR/start_time")
else
SYNC_START=$(head -n1 "$ZOOM_LOG_MOUSE" | awk '{print $1}')
fi
# Process Mouse
awk -v start="$SYNC_START" '{
t = $1 - start;
if (t >= 0) print t " MOUSE " $2 " " $3
}' "$ZOOM_LOG_MOUSE" > "$ZOOM_WORK_DIR/merged.temp"
# Process Keys
awk -v start="$SYNC_START" '{
t = $1 - start;
if (t >= 0) print t " KEY " $2
}' "$ZOOM_LOG_KEYS" >> "$ZOOM_WORK_DIR/merged.temp"
# Sort
sort -n "$ZOOM_WORK_DIR/merged.temp" > "$ZOOM_LOG_COORD"
rm "$ZOOM_WORK_DIR/merged.temp" "$ZOOM_LOG_MOUSE" "$ZOOM_LOG_KEYS" "$ZOOM_WORK_DIR/start_time" "$ZOOM_LOCK_FILE"
zoom_log_debug "Logs merged. Starting background processing..."
# IMPORTANT: Run processing in background and disown so it survives parent death
zoom_process >/dev/null 2>&1 & disown
}
# ==============================================================================
# OTHER RECORDING FUNCTIONS
# ==============================================================================
selected() {
slop -f "%x %y %w %h" > /tmp/slop
read -r X Y W H < /tmp/slop
rm /tmp/slop
local outfile="$HOME/selected-$(date '+%y%m%d-%H%M-%S').mp4"
# VAAPI requires minimum 128x128 and device present
if [ -e /dev/dri/renderD128 ] && (( W >= 128 && H >= 128 )); then
ffmpeg -y \
-f x11grab \
-framerate 30 \
-video_size "${W}x${H}" \
-i ":0.0+${X},${Y}" \
-f alsa -i default \
-vaapi_device /dev/dri/renderD128 \
-vf 'format=nv12,hwupload' \
-c:v h264_vaapi -qp 24 \
-c:a aac \
"$outfile" >> "$ZOOM_DEBUG_LOG" 2>&1 &
else
ffmpeg -y \
-f x11grab \
-framerate 30 \
-video_size "${W}x${H}" \
-i ":0.0+${X},${Y}" \
-f alsa -i default \
-c:v libx264 -preset ultrafast -crf 23 \
-c:a aac \
"$outfile" >> "$ZOOM_DEBUG_LOG" 2>&1 &
fi
echo $! > /tmp/recordingpid
}
# ==============================================================================
# MENU & CONTROL
# ==============================================================================
askrecording() {
choice=$(printf "screencast\\nzoom\\nselected" | rofi -dmenu -i -p "Record")
case "$choice" in
screencast) screencast;;
zoom) zoom_start;;
selected) selected;;
esac
}
asktoend() {
response=$(printf "Yes\\nNo" | rofi -dmenu -i -p "End recording?")
[ "$response" = "Yes" ] && killrecording
}
case "$1" in
screencast) screencast;;
zoom) zoom_start;;
selected) selected;;
kill) killrecording;;
*) if [ -f /tmp/recordingpid ]; then asktoend; else askrecording; fi;;
esac