Untitled

 avatar
unknown
sh
18 days ago
10 kB
6
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