Video Fixer

Homer's picture

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