Created
March 5, 2026 13:19
-
-
Save miku1958/69dfa61ac25c4c5672882783adf77689 to your computer and use it in GitHub Desktop.
ffmpeg.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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