
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