Skip to content

Instantly share code, notes, and snippets.

@miku1958
Created March 5, 2026 13:19
Show Gist options
  • Select an option

  • Save miku1958/69dfa61ac25c4c5672882783adf77689 to your computer and use it in GitHub Desktop.

Select an option

Save miku1958/69dfa61ac25c4c5672882783adf77689 to your computer and use it in GitHub Desktop.
ffmpeg.sh
#!/bin/zsh
main() {
local file="$1"
local filename="${file:t}"
local filestem="${file:t:r}"
local filedir="${file:h}"
local safe_name="${filename//\"/\\\"}"
# ───── probe video info ─────────────────────
local SRC_HEIGHT
SRC_HEIGHT=$(ffprobe -v error -select_streams v:0 -show_entries stream=height \
-of default=nokey=1:noprint_wrappers=1 "$file")
local SRC_FPS_RAW
SRC_FPS_RAW=$(ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate \
-of default=nokey=1:noprint_wrappers=1 "$file")
local FPS_NUM=${SRC_FPS_RAW%%/*}
local FPS_DEN=${SRC_FPS_RAW##*/}
[[ "$FPS_DEN" == "$SRC_FPS_RAW" ]] && FPS_DEN=1
local SRC_FPS=$(( (FPS_NUM + FPS_DEN / 2) / FPS_DEN ))
local SRC_DUR_RAW
SRC_DUR_RAW=$(ffprobe -v error -show_entries format=duration \
-of default=nokey=1:noprint_wrappers=1 "$file")
local SRC_DUR_SEC=${SRC_DUR_RAW%%.*}
local SRC_AUDIO_KBPS=128
local audio_br
audio_br=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate \
-of default=nokey=1:noprint_wrappers=1 "$file" 2>/dev/null)
[[ -n "$audio_br" && "$audio_br" != "N/A" ]] && SRC_AUDIO_KBPS=$(( audio_br / 1000 ))
echo " Probed: ${SRC_HEIGHT}p / ${SRC_FPS}fps / ${SRC_DUR_SEC}s"
# ───── build resolution choices ─────────────
local -a res_items=()
local res_default="keep"
for r in 2160 1440 1080 720; do
(( r <= SRC_HEIGHT )) && res_items+=("${r}p")
(( r == SRC_HEIGHT )) && res_default="${r}p"
done
[[ "$res_default" == "keep" ]] && res_items+=("keep")
local res_as_list=""
for item in "${res_items[@]}"; do
[[ -n "$res_as_list" ]] && res_as_list+=", "
res_as_list+="\"$item\""
done
local CHOSEN_RES
CHOSEN_RES=$(osascript -e "choose from list {${res_as_list}} with prompt \"Resolution (source: ${SRC_HEIGHT}p)\" default items {\"${res_default}\"} with title \"FFmpeg - ${safe_name}\"")
[[ "$CHOSEN_RES" == "false" ]] && echo " Cancelled." && return
# ───── build FPS choices ────────────────────
local -a fps_items=()
local fps_default="keep"
for fp in 240 120 60 30 24; do
(( fp <= SRC_FPS )) && fps_items+=("${fp}fps")
(( fp == SRC_FPS )) && fps_default="${fp}fps"
done
[[ "$fps_default" == "keep" ]] && fps_items+=("keep")
local fps_as_list=""
for item in "${fps_items[@]}"; do
[[ -n "$fps_as_list" ]] && fps_as_list+=", "
fps_as_list+="\"$item\""
done
local CHOSEN_FPS
CHOSEN_FPS=$(osascript -e "choose from list {${fps_as_list}} with prompt \"FPS (source: ${SRC_FPS}fps)\" default items {\"${fps_default}\"} with title \"FFmpeg - ${safe_name}\"")
[[ "$CHOSEN_FPS" == "false" ]] && echo " Cancelled." && return
# ───── Codec ────────────────────────────────
local CHOSEN_CODEC_LABEL
CHOSEN_CODEC_LABEL=$(osascript -e "choose from list {\"H.264 (libx264)\", \"H.265 (libx265)\"} with prompt \"Codec\" default items {\"H.264 (libx264)\"} with title \"FFmpeg - ${safe_name}\"")
[[ "$CHOSEN_CODEC_LABEL" == "false" ]] && echo " Cancelled." && return
# ───── CRF ──────────────────────────────────
local crf_result
crf_result=$(osascript -e "display dialog \"CRF (default 23)\" default answer \"23\" with title \"FFmpeg - ${safe_name}\" buttons {\"Cancel\", \"OK\"} default button \"OK\"" 2>/dev/null) || { echo " Cancelled."; return; }
local CHOSEN_CRF="${crf_result##*text returned:}"
[[ -z "$CHOSEN_CRF" ]] && CHOSEN_CRF="23"
# ───── Dedup ────────────────────────────────
local CHOSEN_DEDUP
CHOSEN_DEDUP=$(osascript -e "choose from list {\"None\", \"Dedup (CFR, shorter)\", \"Dedup (VFR)\"} with prompt \"Dedup frames\" default items {\"None\"} with title \"FFmpeg - ${safe_name}\"")
[[ "$CHOSEN_DEDUP" == "false" ]] && echo " Cancelled." && return
# ───── Audio ────────────────────────────────
local CHOSEN_AUDIO_LABEL
CHOSEN_AUDIO_LABEL=$(osascript -e "choose from list {\"Keep audio\", \"Remove audio\"} with prompt \"Audio\" default items {\"Keep audio\"} with title \"FFmpeg - ${safe_name}\"")
[[ "$CHOSEN_AUDIO_LABEL" == "false" ]] && echo " Cancelled." && return
# ───── Target MB ────────────────────────────
local target_result
target_result=$(osascript -e "display dialog \"Target file size in MB (leave empty to skip)\" default answer \"\" with title \"FFmpeg - ${safe_name}\" buttons {\"Cancel\", \"OK\"} default button \"OK\"" 2>/dev/null) || { echo " Cancelled."; return; }
local TARGET_MB="${target_result##*text returned:}"
[[ -z "$TARGET_MB" ]] && TARGET_MB="0"
# ───── parse selections ─────────────────────
[[ "$CHOSEN_RES" != "keep" ]] && CHOSEN_RES="${CHOSEN_RES%p}"
[[ "$CHOSEN_FPS" != "keep" ]] && CHOSEN_FPS="${CHOSEN_FPS%fps}"
local CHOSEN_CODEC="libx264"
[[ "$CHOSEN_CODEC_LABEL" == *"libx265"* ]] && CHOSEN_CODEC="libx265"
local DEDUP_MODE="none"
[[ "$CHOSEN_DEDUP" == *"CFR"* ]] && DEDUP_MODE="cfr"
[[ "$CHOSEN_DEDUP" == *"VFR"* ]] && DEDUP_MODE="vfr"
local AUDIO_IDX=0
[[ "$CHOSEN_AUDIO_LABEL" == *"Remove"* ]] && AUDIO_IDX=1
# ───── build ffmpeg arguments ───────────────
local -a ffargs=(-i "$file" -c:v "$CHOSEN_CODEC")
[[ "$CHOSEN_CODEC" == "libx265" ]] && ffargs+=(-tag:v hvc1)
ffargs+=(-crf "$CHOSEN_CRF")
# maxrate cap
local MAXRATE=0 BUFSIZE=0
if [[ "$TARGET_MB" != "0" ]]; then
local AUDIO_COST=$SRC_AUDIO_KBPS
(( AUDIO_IDX == 1 )) && AUDIO_COST=0
local TOTAL_KBPS=$(( TARGET_MB * 8192 / SRC_DUR_SEC ))
local VIDEO_KBPS=$(( TOTAL_KBPS - AUDIO_COST ))
(( VIDEO_KBPS <= 0 )) && VIDEO_KBPS=100
MAXRATE=$(( VIDEO_KBPS * 3 / 2 ))
BUFSIZE=$(( MAXRATE * 2 ))
ffargs+=(-maxrate "${MAXRATE}k" -bufsize "${BUFSIZE}k")
fi
# video filters
local VF_CHAIN=""
[[ "$DEDUP_MODE" == "cfr" ]] && VF_CHAIN="mpdecimate=hi=300:lo=300:frac=1:max=0,setpts=N/FRAME_RATE/TB"
[[ "$DEDUP_MODE" == "vfr" ]] && VF_CHAIN="mpdecimate=hi=300:lo=300:frac=1:max=0"
if [[ "$CHOSEN_RES" != "keep" ]]; then
[[ -z "$VF_CHAIN" ]] && VF_CHAIN="scale=-2:${CHOSEN_RES}" || VF_CHAIN="${VF_CHAIN},scale=-2:${CHOSEN_RES}"
fi
[[ -n "$VF_CHAIN" ]] && ffargs+=(-vf "$VF_CHAIN")
[[ "$CHOSEN_FPS" != "keep" ]] && ffargs+=(-r "$CHOSEN_FPS")
[[ "$DEDUP_MODE" == "vfr" ]] && ffargs+=(-vsync vfr)
if (( AUDIO_IDX == 1 )); then
ffargs+=(-an)
else
ffargs+=(-c:a copy)
fi
# ───── build output filename suffix ─────────
local SUFFIX=""
[[ "$CHOSEN_RES" != "keep" ]] && SUFFIX="${SUFFIX}_${CHOSEN_RES}p"
[[ "$CHOSEN_FPS" != "keep" ]] && SUFFIX="${SUFFIX}_${CHOSEN_FPS}fps"
[[ "$CHOSEN_CODEC" == "libx265" ]] && SUFFIX="${SUFFIX}_h265"
[[ "$CHOSEN_CRF" != "23" ]] && SUFFIX="${SUFFIX}_crf${CHOSEN_CRF}"
[[ "$DEDUP_MODE" != "none" ]] && SUFFIX="${SUFFIX}_dedup"
[[ "$DEDUP_MODE" == "vfr" ]] && SUFFIX="${SUFFIX}-vfr"
(( AUDIO_IDX == 1 )) && SUFFIX="${SUFFIX}_noaudio"
[[ "$TARGET_MB" != "0" ]] && SUFFIX="${SUFFIX}_${TARGET_MB}mb"
[[ -z "$SUFFIX" ]] && SUFFIX="_transcoded"
local OUTPUT="${filedir}/${filestem}${SUFFIX}.mp4"
ffargs+=("$OUTPUT")
local AUDIO_DISPLAY="-c:a copy"
(( AUDIO_IDX == 1 )) && AUDIO_DISPLAY="-an"
local RATE_DISPLAY="(none)"
[[ "$TARGET_MB" != "0" ]] && RATE_DISPLAY="-maxrate ${MAXRATE}k -bufsize ${BUFSIZE}k"
echo ""
echo " ================================================"
echo " Res: ${CHOSEN_RES}"
echo " FPS: ${CHOSEN_FPS}"
echo " Codec: ${CHOSEN_CODEC}"
echo " CRF: ${CHOSEN_CRF}"
echo " Dedup: ${DEDUP_MODE}"
echo " Audio: ${AUDIO_DISPLAY}"
echo " Rate: ${RATE_DISPLAY}"
echo " Output: ${filestem}${SUFFIX}.mp4"
echo " ================================================"
echo ""
ffmpeg "${ffargs[@]}"
echo ""
echo " [done] ${filename}"
echo ""
}
# ───── entry point ──────────────────────────────
if [[ $# -eq 0 ]]; then
# No arguments: open a file picker
local chosen
chosen=$(osascript <<'EOF'
set fileList to choose file with prompt "Select video file(s) to transcode" with multiple selections allowed
set output to ""
repeat with f in fileList
if output is not "" then set output to output & linefeed
set output to output & POSIX path of f
end repeat
return output
EOF
) || { echo " No file selected."; exit 0; }
while IFS= read -r line; do
[[ -n "$line" ]] && main "$line"
done <<< "$chosen"
else
for f in "$@"; do
main "$f"
done
fi
echo " All done. Press Enter to close."
read -r
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment