Here's a script I wrote to "fix" videos I ripped from DVD/BD, where the rip is too big to fit on a FAT32 formatted flash drive (my Smart TV doesn't recognise NTFS or ext2/3/4 filesystems), has the wrong resolution/aspect, and/or possibly contains multiple language audio tracks I don't need (my Smart TV also doesn't play DTS audio).
Since calculating the bitrates is simply a question of picking an output file size and determining the run time, the process can be automated by incorporating a bitrate calculator into a script, so that's what I've done. It also discards all but one of the language tracks (the "best" one in my chosen language - configurable). I'm left with a high quality and in-spec H.264 video, and the "best" audio track, in a MKV container that's <= 4GB. Stuff like chapters and metadata should be preserved, if present.
Admittedly some of the logic below is a bit "fuzzy" (or plain wrong). It's a work in progress, but has worked very well so far. I'm sure some videos will break it, though. Just pass a video file (any type supported by ffmpeg) as input to the script, then let it run. The whole process is automatic. The source file remains untouched, and an output file with the postfix "_fixed.mkv" is created.
Please feel free to post comments, suggestions, corrections, flames or whatever below.
#!/bin/bash # name: videofixer # version: 0.1.2 # license: GPLv3 # requires: bash, mediainfo, ffmpeg ### input ### infile=$1 if [ -z "$infile" ] # no args then echo "usage: $(basename $0) [infile]" exit 1 fi if [ ! -e "$infile" ] # no file then echo "error: input file \"$infile\" not found" exit 1 fi ###### ### conf ### conf="$HOME/.config/videofixer/videofixer.conf" if [ ! -e "$conf" ] then mkdir -p $HOME/.config/videofixer touch "$conf" fi source "$conf" if [ -z "$audio_language" ] then echo "audio_language=\"English\"" >> "$conf" audio_language="English" fi if [ -z "$target_file_size" ] then echo "target_file_size=33554432" >> "$conf" target_file_size=33554432 # kilobits (33554432Kb = 4GB) fi ###### ### get video info ### video_track_index=$(mediainfo --Inform="Video;%ID%\n" "$infile" | head -n1) # fixme: currently I just simplistically select the first video track. # I need to somehow automagically determine the "best" or most appropriate # track. base_adjust=$video_track_index # fixme: this is plain wrong, and will break when I start selecting video # tracks > the first, but I've found that some "first video tracks" are #0, # while some "first video tracks" are #1, depending on the container format, # so this is either a "bug" or I have some more reading to do. if [ $? -ne 0 ] # sanity check then echo "error: file has no video track" exit 1 fi video_track_index=$((video_track_index - base_adjust)) # base 0 # fixme: see above. Mediainfo doesn't always seem to return base 0. video_width=$(mediainfo --Output="Video;%Width%" "$infile") # pixels if [ $? -ne 0 ] # sanity check then echo "error: video width could not be determined" exit 1 fi duration=$(mediainfo --Output="General;%Duration%" "$infile") # milliseconds ###### ### fix width ### # some people encode some very weird resolutions eval " case $video_width in $(seq -s'|' 1824 2016) ) refs=4 target_width=1920 ;; $(seq -s'|' 1216 1344) ) refs=9 target_width=1280 ;; *) refs=3 target_width=$(( ($video_width / 16) * 16 )) # must be exactly divisible by 16 ;; esac " ###### ### prioritise audio ### audio_track_index=$(mediainfo \ --Inform="Audio;%ID%:%Language/String%:%Format%:%Channels%\n" "$infile" |\ grep "$audio_language:AC-3:6" | head -n1 | cut -d':' -f1) if [ -z $audio_track_index ] then audio_track_index=$(mediainfo \ --Inform="Audio;%ID%:%Language/String%:%Format%:%Channels%\n" \ "$infile" | grep "$audio_language:DTS:6" | head -n1 | cut -d':' -f1) if [ -z $audio_track_index ] then audio_track_index=$(mediainfo \ --Inform="Audio;%ID%:%Language/String%:%Format%:%Channels%\n" \ "$infile" | grep "$audio_language:AC-3" | head -n1 | cut -d':' -f1) if [ -z $audio_track_index ] then audio_track_index=$(mediainfo \ --Inform="Audio;%ID%:%Language/String%:%Format%:%Channels%\n" \ "$infile" | grep "$audio_language" | head -n1 | cut -d':' -f1) if [ -z $audio_track_index ] then echo "error: no $audio_language language tracks found" exit 1 # no other way to determine the right audio track? fi fi fi fi audio_track=$(mediainfo \ --Inform="Audio;%ID%:%Language/String%:%Format%:%Channels%\n" \ "$infile" | grep "^${audio_track_index}") ###### ### set audio target codec ### audio_source_codec="$(echo $audio_track | cut -d':' -f3)" if [ "$audio_source_codec" = "DTS" ] then audio_flags="-c:a ac3 -ac 6 -ab 640k" # my Smart TV can't handle DTS audio_size=$(echo "scale=20; (640 * 128) * ($duration / 1000)" | bc) # bytes else audio_flags="-c:a copy" audio_size=$(mediainfo --Output="Audio;%ID%:%StreamSize%\n" "$infile" |\ grep "^${audio_track_index}" | cut -d':' -f2) # bytes fi audio_track_index=$((audio_track_index - base_adjust)) # base 0 ###### ### set video bitrate ### video_target_bitrate=$(echo "scale=20; ($target_file_size - \ ($audio_size / 128)) / ($duration / 1000)" | bc) # kilobits/second video_target_bitrate=$(echo ${video_target_bitrate%.*}) # truncate to integer ###### ### transcode ### # fixme: pipe ffmpeg's output through a nice progress bar ffmpeg -i "$infile" -y -c:v libx264 -preset:v veryslow -level 4.1 \ -x264opts frameref=$refs:fast_pskip=0 -b:v ${video_target_bitrate}k \ -map 0:${video_track_index} -vf scale=${target_width}:-1 -sws_flags \ lanczos -pass 1 -an -f rawvideo /dev/null ffmpeg -i "$infile" -y -c:v libx264 -preset:v veryslow -level 4.1 \ -x264opts frameref=$refs:fast_pskip=0 -b:v ${video_target_bitrate}k \ -map 0:${video_track_index} -map 0:${audio_track_index} \ -vf scale=${target_width}:-1 -sws_flags lanczos $audio_flags -pass 2 \ "${infile%.*}_fixed.mkv" ###### echo "transcoding completed" actual_file_size_bytes=$(find "${infile%.*}_fixed.mkv" -printf "%s") target_file_size_bytes=$(( target_file_size * 128 )) target_file_size_diff=$((target_file_size_bytes - actual_file_size_bytes)) if [ $target_file_size_diff -lt 0 ] then target_file_size_diff=$((-1 * target_file_size_diff)) # abs echo "error: output is > $target_file_size_bytes bytes ($target_file_size_diff over)" exit 1 else echo "pass: output is <= $target_file_size_bytes bytes ($target_file_size_diff under)" fi # fixme: add routines to rip DVD/BD and autocrop exit 0