1
0

convert-formats.sh 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. #!/bin/bash
  2. # Convert MP4 animations to 60fps MP4 and optimized GIF.
  3. #
  4. # Usage:
  5. # ./convert-formats.sh input.mp4 [gif_width] [--minterpolate]
  6. #
  7. # Produces next to the input:
  8. # <name>-60fps.mp4 (1920x1080, 60fps, frame-duplicated by default)
  9. # <name>.gif (scaled width, 15fps, palette-optimized)
  10. #
  11. # Flags:
  12. # --minterpolate Enable motion-compensated interpolation (high quality
  13. # but elementary stream has known QuickTime/Safari
  14. # compat issues — only use if your player handles it).
  15. #
  16. # Default 60fps mode: simple `fps=60` filter (frame duplication). Wide
  17. # compatibility, plays in QuickTime / Safari / Chrome / VLC. The 60fps
  18. # label is for upload-platform optics; perceived smoothness is identical
  19. # to the source 25fps for most CSS-driven motion.
  20. #
  21. # When to enable --minterpolate: heavy translate/scale motion where you
  22. # want true 60fps interpolation. WARN: macOS QuickTime sometimes refuses
  23. # to open minterpolate output. Test before delivering.
  24. #
  25. # GIF uses two-pass palette:
  26. # pass 1: palettegen with stats_mode=diff (per-video optimal palette)
  27. # pass 2: paletteuse with bayer dither + rectangle diff
  28. # This keeps 30s/1080p animations GIF under ~4MB with good color fidelity.
  29. set -e
  30. INPUT=""
  31. GIF_WIDTH="960"
  32. USE_MINTERPOLATE=0
  33. for arg in "$@"; do
  34. case "$arg" in
  35. --minterpolate) USE_MINTERPOLATE=1 ;;
  36. --*) echo "Unknown flag: $arg" >&2; exit 1 ;;
  37. *)
  38. if [ -z "$INPUT" ]; then INPUT="$arg"
  39. else GIF_WIDTH="$arg"
  40. fi
  41. ;;
  42. esac
  43. done
  44. [ -z "$INPUT" ] && { echo "Usage: $0 input.mp4 [gif_width] [--minterpolate]" >&2; exit 1; }
  45. DIR=$(dirname "$INPUT")
  46. BASE=$(basename "$INPUT" .mp4)
  47. OUT60="$DIR/$BASE-60fps.mp4"
  48. OUTGIF="$DIR/$BASE.gif"
  49. PAL="$DIR/.palette-$BASE.png"
  50. if [ "$USE_MINTERPOLATE" = "1" ]; then
  51. echo "▸ 60fps interpolate (minterpolate, high quality): $OUT60"
  52. VFILTER="minterpolate=fps=60:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1"
  53. else
  54. echo "▸ 60fps frame-duplicate (compat mode): $OUT60"
  55. VFILTER="fps=60"
  56. fi
  57. # -profile:v high -level 4.0 → broad H.264 compatibility (QuickTime, Safari, mobile)
  58. # -movflags +faststart → moov atom upfront, streamable / instant-play
  59. ffmpeg -y -loglevel error -i "$INPUT" \
  60. -vf "$VFILTER" \
  61. -c:v libx264 -pix_fmt yuv420p -profile:v high -level 4.0 \
  62. -crf 18 -preset medium -movflags +faststart \
  63. "$OUT60"
  64. MP4_SIZE=$(du -h "$OUT60" | cut -f1)
  65. echo " ✓ $MP4_SIZE"
  66. echo "▸ GIF (${GIF_WIDTH}w, 15fps, palette-optimized): $OUTGIF"
  67. # Pass 1: generate palette tailored to this video
  68. ffmpeg -y -loglevel error -i "$INPUT" \
  69. -vf "fps=15,scale=${GIF_WIDTH}:-1:flags=lanczos,palettegen=stats_mode=diff" \
  70. "$PAL"
  71. # Pass 2: apply palette with dithering
  72. ffmpeg -y -loglevel error -i "$INPUT" -i "$PAL" \
  73. -lavfi "fps=15,scale=${GIF_WIDTH}:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
  74. "$OUTGIF"
  75. rm -f "$PAL"
  76. GIF_SIZE=$(du -h "$OUTGIF" | cut -f1)
  77. echo " ✓ $GIF_SIZE"