function vrot -d 'Rotate a video with ffmpeg: metadata-only by default, re-encode with -r' argparse -n vrot 'h/help' 'r/reencode' 'a/angle=' 'p/preset=' -- $argv or return 2 if set -q _flag_help __vrot_usage return 0 end if test (count $argv) -lt 1 __vrot_usage return 2 end set -q _flag_angle; or set _flag_angle 90 set -q _flag_preset; or set _flag_preset medium switch $_flag_angle case 90 180 270 case '*' __vrot_error "angle must be 90, 180 or 270 (got '$_flag_angle')" return 2 end __vrot_colors set -l want_reencode 0 set -q _flag_reencode; and set want_reencode 1 set -l rc 0 for file in $argv if not test -f "$file" __vrot_error "no such file: '$file'" set rc 1 continue end __vrot_one "$file" "$_flag_angle" "$_flag_preset" "$want_reencode" or set rc 1 end return $rc end function __vrot_one set -l file $argv[1] set -l angle $argv[2] set -l preset $argv[3] set -l want_reencode $argv[4] set -l name (path basename "$file") set -l ext (path extension "$file") set -l out (path change-extension '' "$file")_rot$ext set -l outname (path basename "$out") __vrot_banner "$name → $outname" set -l lines (ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name,width,height,r_frame_rate,nb_frames:format=duration,bit_rate \ -of default=noprint_wrappers=1 "$file") set -l codec (string match 'codec_name=*' -- $lines | string replace 'codec_name=' '')[1] set -l width (string match 'width=*' -- $lines | string replace 'width=' '')[1] set -l height (string match 'height=*' -- $lines | string replace 'height=' '')[1] set -l fpsraw (string match 'r_frame_rate=*' -- $lines | string replace 'r_frame_rate=' '')[1] set -l frames (string match 'nb_frames=*' -- $lines | string replace 'nb_frames=' '')[1] set -l duration (string match 'duration=*' -- $lines | string replace 'duration=' '')[1] set -l bitrate (string match 'bit_rate=*' -- $lines | string replace 'bit_rate=' '')[1] set -l info "$width"x"$height" if test -n "$codec" set info "$info "(string upper "$codec") end if test -n "$fpsraw"; and string match -rq '^[0-9]+/[0-9]+$' "$fpsraw"; and not string match -q '*/0' "$fpsraw" set info "$info "(math -s 2 "$fpsraw")" fps" end if test -n "$duration"; and string match -rq '^[0-9.]+$' "$duration" set info "$info "(__vrot_fmtsecs "$duration") end if test -n "$bitrate"; and string match -rq '^[0-9.]+$' "$bitrate" set info "$info "(math -s 1 "$bitrate/1000000")" Mbit/s" end echo "$vrot_c_dim $info$vrot_c_reset" if test "$file" = "$out" __vrot_error "output would overwrite input: '$out'" return 1 end if test -e "$out" read -l -P "vrot: '$out' exists. Overwrite? [y/N] " ans if not string match -qi 'y' "$ans" echo "$vrot_c_dim skipped: $out$vrot_c_reset" return 1 end rm -f "$out" end if test "$want_reencode" -eq 1 __vrot_reencode "$file" "$out" "$angle" "$preset" "$height" "$codec" "$duration" "$frames" return $status end echo "$vrot_c_dim mode: metadata-only rotation (lossless, instant)$vrot_c_reset" ffmpeg -hide_banner -loglevel error -nostdin -i "$file" -map 0 -c copy -ignore_unknown \ -movflags +faststart -metadata:s:v:0 rotate=$angle "$out" if test $status -eq 0 __vrot_summary "$out" return 0 end echo "$vrot_c_warn ↷ dropping unsupported data/attachment tracks, retrying…$vrot_c_reset" rm -f "$out" ffmpeg -hide_banner -loglevel error -nostdin -i "$file" -map 0 -map '-0:d' -map '-0:t' \ -c copy -movflags +faststart -metadata:s:v:0 rotate=$angle "$out" if test $status -eq 0 __vrot_summary "$out" return 0 end echo "$vrot_c_warn ↷ metadata rotation unsupported here, re-encoding instead…$vrot_c_reset" rm -f "$out" __vrot_reencode "$file" "$out" "$angle" "$preset" "$height" "$codec" "$duration" "$frames" end function __vrot_reencode set -l file $argv[1] set -l out $argv[2] set -l angle $argv[3] set -l preset $argv[4] set -l height $argv[5] set -l codec $argv[6] set -l duration $argv[7] set -l frames $argv[8] test -n "$height"; or set height 1080 set -l enc libx264 switch "$codec" case 'hevc*' 'h265*' 'h266*' set enc libx265 end set -l crf 20 set -l maxrate 20M set -l bufsize 40M if test $height -ge 2160 set crf 22; set maxrate 40M; set bufsize 80M else if test $height -ge 1440 set crf 21; set maxrate 30M; set bufsize 60M else if test $height -ge 1080 set crf 20; set maxrate 20M; set bufsize 40M else if test $height -ge 720 set crf 19; set maxrate 10M; set bufsize 20M else set crf 18; set maxrate 6M; set bufsize 12M end set -l vf 'transpose=1' switch $angle case 180 set vf 'transpose=1,transpose=1' case 270 set vf 'transpose=2' end set -l faststart switch (string lower (path extension "$out")) case '.mp4' '.mov' '.m4v' set faststart '-movflags' '+faststart' end echo "$vrot_c_dim mode: re-encode ($enc · crf $crf · cap $maxrate)$vrot_c_reset" set -l total_frames '' string match -rq '^\d+$' "$frames"; and test "$frames" -gt 0; and set total_frames $frames set -l out_t '' set -l frame 0 set -l fps_txt '' set -l bitrate_txt '' set -l speed_txt '' ffmpeg -hide_banner -loglevel error -nostdin -progress pipe:1 -i "$file" \ -map 0:v -map '0:a?' -map '0:s?' -ignore_unknown -vf "$vf" -c:v $enc -crf $crf \ -maxrate $maxrate -bufsize $bufsize -preset $preset -c:a copy -c:s copy \ $faststart "$out" | while read -l line switch "$line" case 'frame=*' set frame (string replace 'frame=' '' -- "$line") case 'fps=*' set fps_txt (string replace 'fps=' '' -- "$line")"fps" case 'bitrate=*' set bitrate_txt (string replace 'bitrate=' '' -- "$line") if string match -q '*kbits/s' "$bitrate_txt" set bitrate_txt (math -s 1 (string replace 'kbits/s' '' -- "$bitrate_txt") / 1000)"Mbit/s" else if string match -q '*Mbits/s' "$bitrate_txt" set bitrate_txt (string replace 'Mbits/s' 'Mbit/s' -- "$bitrate_txt") end case 'out_time=*' set -l t (string replace 'out_time=' '' -- "$line") set -l parts (string split ':' -- "$t") if test (count $parts) -eq 3; and string match -rq '^\d+$' "$parts[1]"; and string match -rq '^\d+$' "$parts[2]"; and string match -rq '^[\d.]+$' "$parts[3]" set out_t (math "$parts[1]*3600+$parts[2]*60+$parts[3]") end case 'speed=*' set speed_txt (string replace 'speed=' '' -- "$line") end if isatty stdout printf '\r\033[K' set -l pct '--.-%' set -l eta_txt '' set -l bar (string repeat -n 24 '░') set -l frac '' if test -n "$total_frames"; and string match -rq '^\d+$' "$frame"; and test "$total_frames" -gt 0 set frac (math "min(1, $frame/$total_frames)") else if test -n "$out_t"; and string match -rq '^[0-9.]+$' "$duration"; and not string match -rq '^0+(\.0*)?$' "$duration" set frac (math "min(1, $out_t/$duration)") end if test -n "$frac" set -l filled (math "floor($frac*24)") set -l bar (string repeat -n $filled '█') set bar "$bar"(string repeat -n (math "24-$filled") '░') set pct (math -s 1 "$frac*100")% set -l sp (string replace -r '[xX]$' '' -- "$speed_txt") if test -n "$sp"; and string match -rq '^[0-9.]+$' "$sp"; and not string match -rq '^0+(\.0*)?$' "$sp"; and test -n "$out_t"; and string match -rq '^[0-9.]+$' "$duration" set eta_txt " ETA "(__vrot_fmtsecs (math "($duration - $out_t)/$sp")) end end set -l frame_txt $frame if test -n "$total_frames" set frame_txt "$frame/$total_frames" end echo -n " $vrot_c_ok$bar$vrot_c_reset $pct frame=$frame_txt $fps_txt $bitrate_txt $speed_txt$eta_txt" end end set -l ff_status $pipestatus[1] printf '\n' if test $ff_status -ne 0 __vrot_error "encoding failed (ffmpeg exited $ff_status)" return 1 end __vrot_summary "$out" end function __vrot_summary set -l out $argv[1] set -l size '' if command -q numfmt set size (numfmt --to=iec --suffix=B -- (stat -c %s -- "$out") 2>/dev/null) else set size (du -h -- "$out" | string replace -r '\t.*' '') end set -l dims (ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height -of default=noprint_wrappers=1:nokey=1 "$out" \ | string match -r '^\d+$' | string join '×') set -l extra '' if test -n "$dims" set extra " · $dims" end echo "$vrot_c_ok ✔ $out$vrot_c_dim ($size$extra)$vrot_c_reset" end function __vrot_banner set -l title $argv[1] echo "$vrot_c_dim"(string repeat -n 58 '─')"$vrot_c_reset" echo "$vrot_c_hi $title$vrot_c_reset" end function __vrot_colors set -q vrot_colors_set; and return 0 set -g vrot_colors_set 1 if isatty stdout set -g vrot_c_hi (set_color brwhite --bold) set -g vrot_c_dim (set_color brblack) set -g vrot_c_ok (set_color brgreen) set -g vrot_c_warn (set_color bryellow) set -g vrot_c_err (set_color brred) set -g vrot_c_reset (set_color normal) else for c in hi dim ok warn err reset set -g vrot_c_$c '' end end end function __vrot_fmtsecs set -l s (math "max(0, round($argv[1]))") set -l h (math "floor($s/3600)") set -l m (math "floor($s%3600/60)") set -l sec (math "$s%60") if test $h -gt 0 printf '%02d:%02d:%02d' $h $m $sec else printf '%02d:%02d' $m $sec end end function __vrot_error echo "$vrot_c_err""vrot: "$argv"$vrot_c_reset" >&2 end function __vrot_usage echo 'usage: vrot [options]