Untitled
unknown
sh
a month ago
10 kB
7
Indexable
#!/usr/bin/env bash
# hypr-clip.sh — Hyprland quadrant recorder (left monitor), Discord-ready MP4/WebM/GIF
# Records a cropped region based on monitor quadrants, with optional padding and record-time downscale.
# - Uses global geometry (-g) so padding is exact; no prompts; outputs to CWD
# - GPU encode for capture if available (NVENC/AV1); ffmpeg shows progress (-stats)
# - Temp files live under ./.hyprclip.* and are removed after encode
set -euo pipefail
# --- logging ---
VERBOSE=${VERBOSE:-1}
log(){ [ "$VERBOSE" -eq 1 ] && echo "[hypr-clip] $*"; }
# --- defaults ---
QUAD=1 # 1..4 or "all"
SECS=6 # seconds
FPS=24 # capture fps
TARGET_MB=8 # target size (approx for mp4/webm)
OUTBASE=./hyprcap # output basename in CWD
MON_NAME=DP-2 # left monitor
FORMAT=mp4 # mp4|webm|gif (mp4 fastest + best autoplay)
ACCEL=auto # auto|nvenc|av1|none (GPU preference)
MUTE_RECORD=1 # 1 = silence wf-recorder stderr; set with -d to see logs
# record-time scaling (applied inside wf-recorder)
REC_SCALE_PCT=20 # % of output size at record time
PIXFMT=yuv420p # wf-recorder pixel format
NODAMAGE=0 # 1 = --no-damage
PAD_PX=0 # padding (pixels) inset on all sides inside the selected quadrant
# GIF knobs (if FORMAT=gif)
SIMPLE_GIF=1 # 1 = ffmpeg simple path (fast)
MAX_GIF_W=800 # cap gif width
SCALE_PCT=50 # encode-time width % (gif only)
FAST=1 # fast palette mode (if SIMPLE_GIF=0)
COLOR_MAX=112
usage(){
cat <<EOF
Usage: $0 [-q 1|2|3|4|all] [-s secs] [-f fps] [-t MB] [-m MON] [-o outbase] \
[-K mp4|webm|gif] [-R rec_scale%] [-A auto|nvenc|av1|none] [-p pad_px] [-n] [-d]
Examples:
$0 -q 1 -K mp4 -A nvenc -s 5 -f 30 -R 50 # fastest, H.264 NVENC MP4
$0 -q 1 -K webm -s 5 -f 24 -R 50 # VP9 WebM (CPU unless AV1 NVENC)
$0 -q all -K mp4 -A nvenc -s 4 -f 24 -R 50 # all quadrants, fast
$0 -q 3 -K gif -s 8 -f 18 -R 60 -p 15 # padded GIF within rounded corners
EOF
exit 1
}
while getopts ":q:s:f:t:m:o:K:R:A:p:ndh" opt; do
case "$opt" in
q) QUAD="$OPTARG" ;;
s) SECS="$OPTARG" ;;
f) FPS="$OPTARG" ;;
t) TARGET_MB="$OPTARG" ;;
m) MON_NAME="$OPTARG" ;;
o) OUTBASE="$OPTARG" ;;
K) FORMAT="$OPTARG" ;;
R) REC_SCALE_PCT="$OPTARG" ;;
A) ACCEL="$OPTARG" ;;
p) PAD_PX="$OPTARG" ;;
n) NODAMAGE=1 ;;
d) VERBOSE=1; MUTE_RECORD=0 ;;
h|*) usage ;;
esac
done
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing dep: $1" >&2; exit 2; }; }
need hyprctl; need jq; need wf-recorder; need ffmpeg
have_encoder(){ ffmpeg -hide_banner -encoders 2>/dev/null | grep -q "$1"; }
# monitor selection (prefer MON_NAME, else leftmost)
mons_json="$(hyprctl -j monitors)"
sel_line="$(jq --arg m "$MON_NAME" -r '((.[] | select(.name==$m)) // (. | min_by(.x))) | "\(.x) \(.y) \(.width) \(.height) \(.name)"' <<<"$mons_json")"
[ -z "$sel_line" ] || [ "$sel_line" = "null" ] && { echo "hyprctl monitors failed" >&2; exit 3; }
read -r MX MY MW MH MNAME <<<"$sel_line"
# pick recording codec (affects speed/size of MKV)
REC_CODEC="libx264"
if [ "$ACCEL" != "none" ]; then
if { [ "$ACCEL" = "nvenc" ] || [ "$ACCEL" = "auto" ]; } && have_encoder "h264_nvenc"; then
REC_CODEC="h264_nvenc"
elif { [ "$ACCEL" = "av1" ] || [ "$ACCEL" = "auto" ]; } && have_encoder "av1_nvenc"; then
REC_CODEC="av1_nvenc" # for WebM later if you transcode
fi
fi
log "record codec: $REC_CODEC (ACCEL=$ACCEL)"
# compute geometry (global coords) for a quadrant with padding
q_geom(){
local q="$1" pad="$PAD_PX"
local halfw=$(( MW / 2 ))
local halfh=$(( MH / 2 ))
local gx gy gw gh
case "$q" in
1) gx=$((MX + pad)); gy=$((MY + pad)); gw=$((halfw - 2*pad)); gh=$((halfh - 2*pad)) ;;
2) gx=$((MX + halfw + pad)); gy=$((MY + pad)); gw=$((halfw - 2*pad)); gh=$((halfh - 2*pad)) ;;
3) gx=$((MX + pad)); gy=$((MY + halfh + pad)); gw=$((halfw - 2*pad)); gh=$((halfh - 2*pad)) ;;
4) gx=$((MX + halfw + pad)); gy=$((MY + halfh + pad)); gw=$((halfw - 2*pad)); gh=$((halfh - 2*pad)) ;;
*) echo "bad quadrant: $q" >&2; exit 4 ;;
esac
[ $gw -lt 2 ] && gw=2; [ $gh -lt 2 ] && gh=2
gw=$(( gw - (gw % 2) )); gh=$(( gh - (gh % 2) ))
echo "${gx},${gy} ${gw}x${gh}"
}
# run wf-recorder for $SECS with robust timed teardown
_run_wf(){
local tmp="$1"; shift
if [ "$MUTE_RECORD" -eq 1 ]; then
wf-recorder -f "$tmp" -r "$FPS" -y "$@" >/dev/null 2>&1 &
else
wf-recorder -f "$tmp" -r "$FPS" -y "$@" &
fi
local pid=$!
# timed stop + robust teardown
for _ in $(seq 1 "$SECS"); do sleep 1; kill -0 "$pid" 2>/dev/null || break; done
kill -INT "$pid" 2>/dev/null || true
for _ in {1..20}; do sleep 0.05; kill -0 "$pid" 2>/dev/null || break; done
kill -0 "$pid" 2>/dev/null && kill -TERM "$pid" 2>/dev/null || true
for _ in {1..10}; do sleep 0.05; kill -0 "$pid" 2>/dev/null || break; done
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
}
record_region(){
local quadrant="$1" tmp="$2"
local geom; geom="$(q_geom "$quadrant")"
# Parse geom -> gx,gy gwxgh
local _a _b _c gx gy gw gh
_a=${geom%%,*}; _b=${geom#*,}; gx=${_a}
gy=${_b%% *}; _c=${_b#* }; gw=${_c%x*}; gh=${_c#*x}
local xrel=$(( gx - MX ))
local yrel=$(( gy - MY ))
# optional record-time scale filter piece
local scalef=""
if [ "$REC_SCALE_PCT" -lt 100 ]; then
local rsf; rsf=$(awk -v p="$REC_SCALE_PCT" 'BEGIN{printf "%.6f", p/100.0}')
scalef="scale=iw*${rsf}:ih*${rsf}:flags=bilinear"
fi
# --- Attempt A: geometry-only (-g), no -o ---
local extraA=( -x "$PIXFMT" -g "$geom" )
[ "$NODAMAGE" -eq 1 ] && extraA+=( -D )
[ -n "$scalef" ] && extraA+=( -F "$scalef" )
extraA+=( -c "$REC_CODEC" )
case "$REC_CODEC" in
h264_nvenc|av1_nvenc) extraA+=( -p preset=p1 ) ;;
libx264) extraA+=( -p preset=ultrafast ) ;;
esac
log "record(A): secs=$SECS fps=$FPS geom=[$geom] scale=${REC_SCALE_PCT}% pad=${PAD_PX}px"
: > "$tmp" || true
_run_wf "$tmp" "${extraA[@]}"
if [ -s "$tmp" ]; then
if command -v ffprobe >/dev/null 2>&1; then
local rsize; rsize=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "$tmp" || true)
[ -n "$rsize" ] && log "record dims(A): $rsize"
fi
return 0
fi
# --- Attempt B: output+crop filter fallback (handles buggy -g) ---
local filterB="crop=${gw}:${gh}:${xrel}:${yrel}"
[ -n "$scalef" ] && filterB+=" ,${scalef}" && filterB=${filterB// / }
local extraB=( -o "$MNAME" -x "$PIXFMT" -F "$filterB" )
[ "$NODAMAGE" -eq 1 ] && extraB+=( -D )
extraB+=( -c "$REC_CODEC" )
case "$REC_CODEC" in
h264_nvenc|av1_nvenc) extraB+=( -p preset=p1 ) ;;
libx264) extraB+=( -p preset=ultrafast ) ;;
esac
log "record(B): out=$MNAME crop=${gw}x${gh}+${xrel}+${yrel} scale=${REC_SCALE_PCT}%"
: > "$tmp" || true
_run_wf "$tmp" "${extraB[@]}"
[ -s "$tmp" ] || { echo "wf-recorder failed or empty: $tmp" >&2; exit 5; }
if command -v ffprobe >/dev/null 2>&1; then
local rsize; rsize=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "$tmp" || true)
[ -n "$rsize" ] && log "record dims(B): $rsize"
fi
}
# approximate target bitrate from MB + seconds
calc_kbps(){
local mb="$1" secs="$2"; [ "$secs" -lt 1 ] && secs=1
echo $(( mb * 8192 / secs )) # MB -> kbits / s
}
encode_mp4(){
local invid="$1" outfile="$2" kbps; kbps=$(calc_kbps "$TARGET_MB" "$SECS")
log "mp4: nvenc=$(have_encoder h264_nvenc && echo yes || echo no) target=${TARGET_MB}MB (~${kbps}kbps)"
if have_encoder h264_nvenc; then
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-c:v h264_nvenc -b:v ${kbps}k -maxrate ${kbps}k -bufsize $((kbps*2))k -preset p1 -tune hq \
-movflags +faststart -pix_fmt yuv420p -an "$outfile"
else
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-c:v libx264 -preset veryfast -crf 24 -movflags +faststart -pix_fmt yuv420p -an "$outfile"
fi
}
encode_webm(){
local invid="$1" outfile="$2" kbps; kbps=$(calc_kbps "$TARGET_MB" "$SECS")
if have_encoder av1_nvenc && [ "$ACCEL" != "none" ]; then
log "webm: av1_nvenc target=${TARGET_MB}MB (~${kbps}kbps)"
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-c:v av1_nvenc -b:v ${kbps}k -maxrate ${kbps}k -preset p1 -pix_fmt yuv420p -an "$outfile"
else
log "webm: vp9 (CPU) target=${TARGET_MB}MB (~${kbps}kbps)"
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-c:v libvpx-vp9 -b:v ${kbps}k -row-mt 1 -tile-columns 2 -speed 6 -pix_fmt yuv420p -an "$outfile"
fi
}
encode_gif(){
local invid="$1" outfile="$2"
if [ "$SIMPLE_GIF" -eq 1 ]; then
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-vf "fps=${FPS},scale=iw*${SCALE_PCT}/100:-1:flags=bicubic,scale=min\(${MAX_GIF_W}\,iw\):-1:flags=bicubic" \
-f gif "$outfile"
else
ffmpeg -hide_banner -stats -v warning -y -i "$invid" \
-vf "fps=${FPS},scale=iw*${SCALE_PCT}/100:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=${COLOR_MAX}[p];[s1][p]paletteuse=dither=bayer" \
-loop 0 "$outfile"
fi
}
one(){
local q="$1" tmp mkv out
tmp="$(mktemp -d ./.hyprclip.XXXXXX)"
mkv="${tmp}/cap.mkv"
echo "Monitor: $MNAME | Q$q"
record_region "$q" "$mkv"
case "$FORMAT" in
mp4) out="${OUTBASE}-Q${q}.mp4"; encode_mp4 "$mkv" "$out" ;;
webm) out="${OUTBASE}-Q${q}.webm"; encode_webm "$mkv" "$out" ;;
gif) out="${OUTBASE}-Q${q}.gif"; encode_gif "$mkv" "$out" ;;
*) echo "bad -K $FORMAT" >&2; rm -rf "$tmp"; exit 6 ;;
esac
rm -rf "$tmp"
echo "→ $out ($(du -h "$out" | awk '{print $1}'))"
dragon-drop $out --and-exit
}
main(){
if [ "$QUAD" = "all" ]; then for q in 1 2 3 4; do one "$q"; done; else one "$QUAD"; fi
}
main "$@"
Editor is loading...
Leave a Comment