317 lines
8.2 KiB
Bash
Executable File
317 lines
8.2 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
## TODO: handle srt encoding?
|
|
|
|
_usage () {
|
|
cat <<EOF
|
|
Usage: ${1##*/} [OPTIONS] FILES|FOLDERS
|
|
|
|
Transcode FILES or files found in FOLDERS to x264 and ogg vorbis in a MKV container.
|
|
Black stripes are *not* cropped by default since it may be sometimes inaccurate.
|
|
A time stamp is appended to the output filenames.
|
|
|
|
CRF encoding is performed by default. It targets a desired quality. CRF 20 is
|
|
usually a good factor, 18 is higher quality, 22 is lower. The 18-22 range is
|
|
reasonable. The quality loss is usually noticeable above 22 and the size
|
|
increase is significant below 18 for a barely noticeable quality change.
|
|
|
|
2-pass encoding can achieve best compression, if the given bitrate is
|
|
appropriate. A sample in CRF encoding can give a good bitrate estimate for the
|
|
desired quality. Bitrates for 1080p movies are found within the 1000k-10000k
|
|
range.
|
|
|
|
You can get a list of supported codecs with
|
|
|
|
$ ffmpeg -codecs
|
|
|
|
User options are read from the TC_VIDEO_OPT variable. This can be useful if you
|
|
often use the same options.
|
|
|
|
Options:
|
|
-a CODEC: Audio codec supported by ffmpeg. (Default: libvorbis).
|
|
-b: Default bitrate for audio stream with unidentifiable bitrate.
|
|
If 0, copy stream (default).
|
|
-c: Copy streams (no reencoding).
|
|
-C: Enable auto-crop (needs video reencoding).
|
|
-d BR: Perform a 2-pass video encoding at bitrate BR and set '-tune film'.
|
|
-f: Remove source when done.
|
|
-h: Display this help.
|
|
-o OPT: Additional options.
|
|
-p: Preview changes, do not encode.
|
|
-P: Generate two sequence of thumbnails, one cropped, the other not.
|
|
Enable auto-crop, do not encode.
|
|
-q QUAL: x264 CRF quality (default 20).
|
|
-s: Sample of 5 minutes.
|
|
-S MIN: Sample of MIN minutes.
|
|
-t: Remove all "title" metadata.
|
|
|
|
Examples:
|
|
|
|
* Get a preview of changes.
|
|
|
|
${1##*/} -p input.video
|
|
|
|
* Proceed with default options over folders and files.
|
|
|
|
${1##*/} input-folder input.video
|
|
|
|
* Preview black stripes cropping.
|
|
|
|
${1##*/} -P input.video
|
|
|
|
* Process with default options and remove black stripes.
|
|
|
|
${1##*/} -C input.video
|
|
|
|
* Exchange streams 1 and 2 by first removing them, then adding them in the
|
|
desired order.
|
|
|
|
${1##*/} -o '-map -0:1 -map -0:2 -map 0:2 -map 0:1' input.video
|
|
|
|
* Change audio stream 1 title and remove audio stream 2 title:
|
|
|
|
${1##*/} -o '-metadata:s:a:0 title="FR: OGG Stereo" -metadata:s:a:1 title=' input.video
|
|
|
|
See <https://trac.ffmpeg.org/wiki/Encode/H.264>, 'ffmpeg -h encoder=libx264' and 'x264 --fullhelp'.
|
|
EOF
|
|
}
|
|
|
|
EXT="mkv"
|
|
AUDIO_CODEC="libvorbis"
|
|
VIDEO_PARAM="-c:v libx264 -preset slower -crf 20"
|
|
AUDIO_PARAM=""
|
|
|
|
VIDEO_FILTER=""
|
|
AUDIO_DEFAULT_RATE=0
|
|
|
|
OVERWRITE="-n"
|
|
SAMPLE=""
|
|
OPT_OVERWRITE=false
|
|
OPT_REMOVE_TITLE=false
|
|
OPT_CROP=false
|
|
OPT_CROPPREVIEW=false
|
|
OPT_PREVIEW=false
|
|
OPT_COPY=false
|
|
OPT_2PASS=false
|
|
|
|
while getopts ":a:b:cCd:fho:pPq:sS:t" opt; do
|
|
case $opt in
|
|
a)
|
|
AUDIO_CODEC=$OPTARG ;;
|
|
b)
|
|
AUDIO_DEFAULT_RATE=$OPTARG ;;
|
|
c)
|
|
VIDEO_PARAM="-c:v copy"
|
|
AUDIO_PARAM="-c:a copy"
|
|
OPT_COPY=true;;
|
|
C)
|
|
OPT_CROP=true;;
|
|
d)
|
|
VIDEO_PARAM="-c:v libx264 -preset slower -b:v ${OPTARG}k"
|
|
OPT_2PASS=true;;
|
|
f)
|
|
OVERWRITE="-y"
|
|
OPT_OVERWRITE=true;;
|
|
h)
|
|
_usage "$0"
|
|
exit 1 ;;
|
|
o)
|
|
TC_VIDEO_OPT="$OPTARG" ;;
|
|
p)
|
|
OPT_PREVIEW=true ;;
|
|
P)
|
|
OPT_CROP=true
|
|
OPT_PREVIEW=true
|
|
OPT_CROPPREVIEW=true ;;
|
|
q)
|
|
VIDEO_PARAM="-c:v libx264 -preset slower -crf $OPTARG";;
|
|
s)
|
|
SAMPLE="-ss 60 -t 300" ;;
|
|
S)
|
|
SAMPLE="-ss 60 -t $((60*OPTARG))" ;;
|
|
t)
|
|
OPT_REMOVE_TITLE=true ;;
|
|
\?)
|
|
_usage "$0"
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
shift $((OPTIND - 1))
|
|
if [ $# -eq 0 ]; then
|
|
_usage "$0"
|
|
exit
|
|
fi
|
|
|
|
if ! command -v ffmpeg >/dev/null; then
|
|
echo "ffmpeg required."
|
|
exit
|
|
fi
|
|
|
|
_highfreq() {
|
|
awk 'BEGIN{max=0} /crop=/ {t[$NF]++; if (t[$NF]>max) {max=t[$NF]; val=$NF}} END{print val}'
|
|
}
|
|
|
|
## Usage: _cropvalue FILE STEP
|
|
## Return the crop values as ffmpeg output them.
|
|
_cropvalue() {
|
|
local step=$2
|
|
for i in $(seq "$step" "$step" $((5*step))); do
|
|
ffmpeg -nostdin -ss "$i" -i "$1" -t 10 -vf "cropdetect=24:2:0" -f null - 2>&1
|
|
done | _highfreq
|
|
}
|
|
|
|
## Return the audio encoding parameters. For instance
|
|
## -c:2 libvorbis -b:2 384k -c:5 copy
|
|
## If input codec is $AUDIO_CODEC, we copy. If some stream bitrates are missing
|
|
## or 0, we encode it to a default value. If default value is 0, then we copy
|
|
## stream.
|
|
_audiobitrate() {
|
|
local bitrate
|
|
|
|
for i in $(seq 0 $((format_nb_streams-1))); do
|
|
## Skip non audio tracks.
|
|
[ "$(eval echo \$streams_stream_"$i"_codec_type)" != "audio" ] && continue
|
|
|
|
bitrate=$(eval echo \$streams_stream_"$i"_bit_rate)
|
|
if [ -n "$bitrate" ] && [ "$bitrate" -gt 0 ] 2>/dev/null; then
|
|
## If non-empty and a positive number.
|
|
bitrate=$((bitrate / 1000))
|
|
else
|
|
bitrate="$AUDIO_DEFAULT_RATE"
|
|
fi
|
|
if [ "$bitrate" -le 0 ] || \
|
|
[ "$AUDIO_CODEC" = "$(eval echo \$streams_stream_"$i"_codec_name)" ] || \
|
|
[ "$AUDIO_CODEC" = "lib$(eval echo \$streams_stream_"$i"_codec_name)" ]; then
|
|
printf -- "-c:%s copy " "$i"
|
|
else
|
|
[ "$bitrate" -gt 500 ] && bitrate=500
|
|
printf -- "-c:%s %s -b:%s %sk " "$i" "$AUDIO_CODEC" "$i" "$bitrate"
|
|
fi
|
|
done
|
|
}
|
|
|
|
_transcode() {
|
|
echo "==> [$1]"
|
|
OUTPUT="${1%.*}.$EXT"
|
|
[ -e "$OUTPUT" ] && OUTPUT="${1%.*}-$(date '+%F-%H%M%S').$EXT"
|
|
|
|
## Metadata (i.e. tags + technical data).
|
|
_buffer="$(ffprobe -v quiet -print_format flat=s=_ -show_streams -show_format "$1")"
|
|
if [ $? -ne 0 ]; then
|
|
_error "File [$1] is unsupported by FFmpeg."
|
|
return
|
|
fi
|
|
## The following 'eval' defines variables such as format_nb_streams
|
|
eval "$_buffer"
|
|
unset _buffer
|
|
|
|
STREAM_TITLE=""
|
|
if $OPT_REMOVE_TITLE; then
|
|
for i in $(seq 0 $((format_nb_streams-1)) ); do
|
|
STREAM_TITLE="${STREAM_TITLE}-metadata:s:$i title= "
|
|
done
|
|
fi
|
|
|
|
if $OPT_CROP; then
|
|
echo "Computing crop values... "
|
|
## For 5 different timeslices of 1 second at every 1/6th of the video,
|
|
## we sample the crop values. We keep the values with highest
|
|
## frequency. This is much faster than encoding in one pass with low
|
|
## framerate.
|
|
STEP=${format_duration:-$streams_stream_0_duration}
|
|
STEP="${STEP%%.*}"
|
|
STEP=$((STEP/6))
|
|
VIDEO_FILTER="-vf $(_cropvalue "$1" "$STEP")"
|
|
if $OPT_CROPPREVIEW; then
|
|
echo "Generating preview... "
|
|
for i in $(seq $STEP $STEP $((5*STEP))); do
|
|
ffmpeg -nostdin -v warning -y -ss "$i" -i "$1" \
|
|
-f image2 -vframes 1 "$VIDEO_FILTER" "${1%.*}-preview-$i-cropped.png" \
|
|
-f image2 -vframes 1 "${1%.*}-preview-$i-uncropped.png"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
## WARNING: We mix down audio to 2 channels with '-ac 2'. This greatly reduce
|
|
## file size and avoid any confusion for playback, which is often the case
|
|
## when converting DTS to any other format. (DTS has embedded channel
|
|
## description which is not always available in other formats.)
|
|
! $OPT_COPY && AUDIO_PARAM="$(_audiobitrate "$1") -ac 2"
|
|
|
|
cat<<EOF
|
|
User options: ${TC_VIDEO_OPT:-none}
|
|
Sample: ${SAMPLE:-no}
|
|
Clear tags: $OPT_REMOVE_TITLE
|
|
In place: $OPT_OVERWRITE
|
|
Crop: $OPT_CROP
|
|
Video params: $VIDEO_PARAM
|
|
Video filter: $VIDEO_FILTER
|
|
Audio params: $AUDIO_PARAM
|
|
Output file: $OUTPUT
|
|
|
|
EOF
|
|
|
|
$OPT_PREVIEW && return
|
|
|
|
if $OPT_2PASS; then
|
|
echo ":: FIRST PASS"
|
|
ffmpeg -nostdin -y "$SAMPLE" -i "$1" \
|
|
"$VIDEO_PARAM" -pass 1 -tune film "$VIDEO_FILTER" \
|
|
"$AUDIO_PARAM" \
|
|
-c:s copy \
|
|
-dn \
|
|
-map 0 "$STREAM_TITLE" \
|
|
"$TC_VIDEO_OPT" -f matroska /dev/null
|
|
VIDEO_PARAM="$VIDEO_PARAM -pass 2 -tune film"
|
|
echo
|
|
echo ":: SECOND PASS"
|
|
fi
|
|
|
|
ffmpeg -nostdin "$OVERWRITE" -i "$1" \
|
|
"$VIDEO_PARAM" "$VIDEO_FILTER" \
|
|
"$AUDIO_PARAM" \
|
|
-c:s copy \
|
|
-dn \
|
|
-map 0 "$STREAM_TITLE" \
|
|
"$SAMPLE" "$TC_VIDEO_OPT" "$OUTPUT"
|
|
|
|
$OPT_2PASS && rm -v ffmpeg2pass-0.log ffmpeg2pass-0.log.mbtree
|
|
|
|
if $OPT_OVERWRITE; then
|
|
rm "$1"
|
|
mv -f "$OUTPUT" "${1%.*}.$EXT"
|
|
fi
|
|
echo
|
|
}
|
|
|
|
for i in "$@"; do
|
|
|
|
## Argument is a folder. We search for all video files in there.
|
|
if [ -d "$i" ]; then
|
|
|
|
## WARNING: ffmpeg continues to read stdin once it has started, so it should
|
|
## not be called from within a while<<EOF loop without disabling stdin.
|
|
while IFS= read -r j; do
|
|
_transcode "$j"
|
|
done<<EOF
|
|
$(find "$i" \( \
|
|
-iname '*.avi' -o \
|
|
-iname '*.flv' -o \
|
|
-iname '*.mkv' -o \
|
|
-iname '*.mov' -o \
|
|
-iname '*.mp4' -o \
|
|
-iname '*.mpeg' -o \
|
|
-iname '*.mpg' -o \
|
|
-iname '*.ogm' -o \
|
|
-iname '*.webm' -o \
|
|
-iname '*.wmv' \) )
|
|
EOF
|
|
|
|
else
|
|
## Argument is a regular file.
|
|
_transcode "$i"
|
|
fi
|
|
|
|
done
|