123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- #!/usr/bin/env bash
- ## Compresses a video to reduce it's file size.
- ## by Adnan Shameem; MIT (Expat) license
- ##
- ## Usage:
- ## Run /path/to/compress-video.sh --help
- [ -z "$(command -v ffmpeg)" ] && echo 'ffmpeg not found, please install and continue' && exit 11
- ## Default config values
- # Video dimensions. Check ref below for other 16:9 and 4:3 dimensions
- # ref: https://studio.support.brightcove.com/general/optimal-video-dimensions.html
- max_width=960
- max_height=540
- # MKV supports good range of audio, especially vorbis.
- # ref: https://trac.ffmpeg.org/wiki/Encode/HighQualityAudio#Containerformats
- # If extension changed, you may have trouble with subtitles. Only mkv, mp4 and
- # mov support embedding subtitles. Although setting "burn_subtitles" to 1 may
- # be an option.
- # Single quotes are intentional so that later it can be replaced with actual
- # value.
- dest_filename='compressed_$source_filename_wo_ext.mkv'
- # Single quotes are intentional so that later it can be replaced with actual
- # value.
- dest_path='$source_path'
- # Burn subtitles into the video. 0 disables, 1 enables. Default: 0.
- # Shaves some kilobytes off if enabled, but removes the ability to turn
- # subtitles off, change language or ever to remove it again from video frames.
- # Disabled by default as a safety measure.
- burn_subtitles=0
- # Keep original audio and do not compress
- keep_audio=0
- ## Compresses given video file
- function _process_file() (
- ## Internal variables - no need to change these
- source="$1"
- source_path="$(dirname "$1")"
- source_filename="$(basename "$1")"
- source_filename_wo_ext="${source_filename%.*}"
- dest_filename_parsed="${dest_filename//\$source_filename_wo_ext/$source_filename_wo_ext}"
- dest_path_parsed="${dest_path//\$source_path/$source_path}"
- ## Check existing file/directory
- [ ! -f "${source_path}/${source_filename}" ] && echo "Input file '${source_path}/${source_filename}' doesn't exist" && exit 14
- [ -d "${dest_path_parsed}/${dest_filename_parsed}" ] && echo "A directory '$(realpath "${dest_path_parsed}/${dest_filename_parsed}")' already exists. Please delete or rename it and try again." && exit 45
- [ ! -d "${dest_path_parsed}" ] && echo "The output directory '$(realpath "${dest_path_parsed}")' does not exist. Creating it..." && mkdir -p "${dest_path_parsed}"
- [ -f "${dest_path_parsed}/${dest_filename_parsed}" ] && echo "'$(realpath "${dest_path_parsed}/${dest_filename_parsed}")' already exists. Press enter to override, or Ctrl+C to cancel..." && read && echo 'Chosen to override...'
- ## Notification
- echo "Processing ${source_path}/${source_filename}..."
- ## Video filters
- # All array members will be concatenated with "," in between
- video_filters=(
- # "force_divisible_by=2" saves us when the calculated width/height based on
- # aspect ratio is not divisable by 2
- "scale='min(${max_width},iw)':'min(${max_height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2"
- )
- ## Audio arguments
- if [ "$keep_audio" -eq '0' ]; then # "keep-audio" disabled, so compress...
- # ref:
- # - https://trac.ffmpeg.org/wiki/Encode/HighQualityAudio
- # - https://trac.ffmpeg.org/wiki/TheoraVorbisEncodingGuide
- # - https://trac.ffmpeg.org/wiki/Encode/AAC
- audio_args=(-codec:a libvorbis)
- else
- audio_args=(-codec:a copy)
- fi
- # Handle subtitles
- if [ "$burn_subtitles" -ne '0' ]; then # Burn Subtitles
- # Check for .srt subtitle files
- if [ -f "${source_path}/${source_filename_wo_ext}.srt" ]; then
- video_filters+=("subtitles=${source_path}/${source_filename_wo_ext}.srt")
- # Check for .ass subtitle files
- elif [ -f "${source_path}/${source_filename_wo_ext}.ass" ]; then
- video_filters+=("ass=${source_path}/${source_filename_wo_ext}.ass")
- # Take any subtitle data from source file itself
- else
- video_filters+=("subtitles='${source}'")
- fi
- else # Embed subtitles
- # Check for .srt subtitle files
- if [ -f "${source_path}/${source_filename_wo_ext}.srt" ]; then
- subtitle_options=(-f srt -i "${source_path}/${source_filename_wo_ext}.srt")
- # Check for .ass subtitle files
- elif [ -f "${source_path}/${source_filename_wo_ext}.ass" ]; then
- subtitle_options=(-f ass -i "${source_path}/${source_filename_wo_ext}.ass")
- # Take any subtitle data from source file itself
- else
- # Set subtitle codec based on container
- # ref: https://en.m.wikibooks.org/wiki/FFMPEG_An_Intermediate_Guide/subtitle_options#Set_Subtitle_Codec
- ext="${dest_filename_parsed: -4}"
- if [ "$ext" = '.mp4' ] || [ "$ext" = '.mov' ]; then
- subtitle_options+=(-codec:s mov_text)
- elif [ "$ext" = '.mkv' ]; then
- subtitle_options+=(-codec:s srt)
- else
- echo "WARNING: ${ext} format does not support embedding subtitles. Please try setting 'burn_subtitles' to 1 as a remedy."
- fi
- fi
- fi
- ## Arguments being passed to ffmpeg.
- # Feel free to comment and change any lines you want!
- ffmpeg_args=(
- # Add arguments to ffmpeg to aid in getting progress
- -progress /dev/stdout
- # Overwrite dest file without asking, because we asked already at the
- # beginning
- -y
- -nostdin
- # Show less output
- -hide_banner -loglevel error -nostats
- # Pass the source file
- -i "$source"
- # Subtitles options determined earlier
- "${subtitle_options[@]}"
- # Set codec as HEVC/H.265 for reduced file size
- # ref: https://spadebee.com/2020/06/06/how-to-highly-compress-videos-using-ffmpeg/
- -codec:v libx265 -crf 28
- # Resize video to fit inside $max_width x $max_height.
- # "force_original_aspect_ratio" is to maintain aspect ratio.
- # Ref: https://trac.ffmpeg.org/wiki/Scaling
- -filter:v "$(IFS=, ; echo "${video_filters[*]}")"
- # Audio arguments
- "${audio_args[@]}"
- # Enable progressive download - for streaming situations
- # Especially for mp4/m4a outputs
- # ref: https://trac.ffmpeg.org/wiki/Encode/AAC#ProgressiveDownload
- -movflags +faststart
- )
- # Get total frames in the source video
- # Ref: https://stackoverflow.com/a/28376817
- total_frames=$(ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 "$source")
- # Add the output at the end
- ffmpeg_args+=("${dest_path_parsed}/${dest_filename_parsed}")
- ## Reset bash time counter to zero.
- # $SECONDS is incremented every second by bash. If this is set to zero, it
- # starts counting from 0 onwards. e.g. 1, 2, 3... This can be used as a time
- # counting measure.
- SECONDS=0
- ## Progress bar related vars
- current_frame=0
- encode_fps=0
- prev_progress_print_buffer=''
- ## Draw progressbar
- function update_progress() {
- # This is to prevent blinking cursor all over the line when progress is
- # being updated. Instead of printing on screen directly, text is added to
- # this and printed with one single echo -n call.
- print_buffer=''
- # Calculations
- # A clever little trick to calculate float numbers with bash
- # ref: https://stackoverflow.com/a/22406193
- ( [ "$current_frame" != '0' ] && [ "$total_frames" != '0' ] ) && progress_percentage=$(awk "BEGIN {printf \"%.2f\",${current_frame}/${total_frames}*100}") || return
- # Progress width in integer. Divide by 3 to make it smaller.
- [ "$progress_percentage" != '0' ] && progress_done_chars=$(( ${progress_percentage%.*} / 3 )) || return
- frames_left=$(( $total_frames - $current_frame ))
- ( [ "$frames_left" != '0' ] && [ "${encode_fps%.*}" != '0' ] ) && seconds_left=$(( ( $frames_left / ${encode_fps%.*} ) )) || return
- [ "$seconds_left" != '0' ] && time_left=$(printf '%02d:%02d:%02d' $((seconds_left/3600)) $((seconds_left%3600/60)) $((seconds_left%60))) || return
- # Progress bar
- print_buffer+='['
- for ((i = 0 ; i <= $progress_done_chars; i++)); do print_buffer+='#'; done
- for ((j = i ; j <= 33 ; j++)); do print_buffer+='-'; done # 100 / 3 = 33
- print_buffer+='] '
- # Progress text
- [ -z "$COLUMNS" ] && TERM_COLUMNS="$(tput cols)" || TERM_COLUMNS="$COLUMNS"
- if [ "$TERM_COLUMNS" -gt '110' ]; then
- print_buffer+="frames: ${current_frame}/${total_frames} (${progress_percentage}%) left: ${frames_left}, ${time_left} - ${encode_fps}fps"
- else
- print_buffer+="${current_frame}/${total_frames}fr (${progress_percentage}%) - ${time_left}"
- fi
- # If same data, do not bother printing it.
- if [ "$prev_progress_print_buffer" != "$print_buffer" ]; then
- echo -n "${print_buffer}" $'\r'
- prev_progress_print_buffer="${print_buffer}"
- fi
- }
- echo -e 'Beginning the compression process, press Ctrl+C anytime to cancel...\n'
- ## Run ffmpeg
- ffmpeg "${ffmpeg_args[@]}" | while IFS='=' read -r key value; do
- # There is no guarantee how many lines will come through and which order.
- # But if it is the one we want, we update vars and draw progressbar.
- [ "$key" = 'frame' ] && current_frame="$value" && update_progress
- [ "$key" = 'fps' ] && encode_fps="$value" && update_progress
- done
- ## If inturrupted (e.g. pressed Ctrl+C)
- if [ 0 -ne "${PIPESTATUS[0]}" ]; then
- echo 'Process inturrupted. Exiting...'
- exit 1012
- fi
- ## Done message
- echo 'Done.'
- echo "Time taken: $(($SECONDS / 3600))hrs $((($SECONDS / 60) % 60))min $(($SECONDS % 60))sec"
- )
- ## Prints the help text (e.g. when --help is passed)
- function _show_help_text() {
- echo "Usage: compress-video.sh [OPTION]... [FILE]...
- Compress video FILE(s) to reduce filesize.
- -b, --burn-subtitles burn subtitles into video
- WARNING: Cannot be undone!
- -d, --dest-dir DIR set output directory to DIR
- -h, --max-height HEIGHT set maximum height of output to HEIGHT
- --help show this help and exit
- -k, --keep-audio keep audio as is, without compressing
- -w, --max-width WIDTH set maximum width of output to WIDTH
- Examples:
- compress-video.sh somevideo.mp4 Compress video and output as
- compressed_somevideo.mkv
- compress-video.sh -w 200 -h 200 somevideo.mp4 Compress video as usual and
- keep the resolution to 200x200 px
- compress-video.sh *.mp4 Compress all matching .mp4 files
- Script location:
- <https://notabug.org/adnan360/code-backups/src/master/bash/compress-video.sh>"
- }
- ## Process command line arguments
- while [[ "$#" -gt 0 ]]; do
- case $1 in
- -w|--max-width) max_width="$2"; shift ;;
- -h|--max-height) max_height="$2"; shift ;;
- -b|--burn-subtitles) burn_subtitles=1 ;;
- -k|--keep-audio) keep_audio=1 ;;
- -d|--dest-dir) dest_path="$2"; shift ;;
- --help) _show_help_text ;;
- # The for loop is to interpret patterns like *.mp4.
- # [ "$?" -eq 244 ] is to stop when Ctrl+C is pressed and previous file
- # was skipped.
- *) for i in "$1"; do _process_file "$i"; [ "$?" -eq 244 ] && exit 2446; done ;;
- esac
- shift
- done
|