#!/bin/sh # Copyright © 2010-2014 Jonas Smedegaard # Description: Recode a video into web-optimized format(s) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Depends: libav-tools melt mediainfo # Recommends: moreutils vpx-tools vorbis-tools opus-tools # # TODO: # * Offer to skip rendering again if an output file exist already. # * Support --width and --height, resolving the other part from input # or forced aspect ratio. # * Drop $melt_recent flag when melt 0.9.2 is stable. # * Check and fail if all needed tools are not available. # * Test if beneficial to apply real_time=-2. # * Normalize each infile separately when xml fed as infile keeps sync. # Maybe as workaround re-feed audio separately from xml, as done at # . # * Resolve flash player to use. # * Make choice of encoders configurable. # * Figure out how to apply application option when using opusenc. set -e PRG=$(basename "$0") showhelp() { cat < EOF } exit1() { response="${1:+Error: }${1:-Internal error!}" echo >&2 "$response" exit 1 } # defaults formats=webm,vp9,mp4 samplestart=0 samplelength=150 compression=normal # VP8 is rumored to compress ~20% worse than H.264 factor_vp8=120/100 # VP9 compresses natural video ~27% worse than H.264 (i.e. excluding animation) # factor_vp9=127/100 # Mimic ReplayGain level when loudness is enabled (EBU R128: -23LUFS) # lufs=-18 # parse cmdline options TEMP="`getopt -s sh -o hp:s:a:r:b:t: -l help,profile:,size:,aspect:,rate:,video:,refbpp:,formats:,audio:,audioprefilter:,loudness,loudness-data:,filter:,stem:,title:,sample,sample-start:,sample-length:,compression: -n "$PRG" -- "$@"`" || exit1 "Internal getopt error." eval set -- "$TEMP" while true ; do case "$1" in -h|--help) showhelp; exit;; -p|--profile) profile="$2"; shift 2;; -s|--size) size="$2"; shift 2;; -a|--aspect) aspect="$2"; shift 2;; -r|--rate) framerate="$2"; shift 2;; --video) video="$2"; shift 2;; --refbpp) refbpp="$2"; shift 2;; --formats) formats="$2"; shift 2;; --audio) audio="$2"; shift 2;; --audioprefilter) audioprefilters="${audioprefilters:+$audioprefilters }-filter $2"; shift 2;; --loudness) loudness=yes; shift;; --loudness-data) loudness=yes; loudness_data="$2"; shift 2;; --filter) filters="${filters:+$filters }-filter $2"; shift 2;; --stem) stem="$2"; shift 2;; -t|--title) title="$2"; shift 2;; --sample) sample=yes; shift;; --sample-start) sample=yes; samplestart="$2"; shift 2;; --sample-length) sample=yes; samplelength="$2"; shift 2;; --compression) compression="$2"; shift 2;; --) shift; break;; *) exit1 "Internal error resolving options.";; esac done # Resolve if system has many CPU cores processors=$(nproc) [ $processors -gt 2 ] || processors= # Resolve if melt is version 0.9.2 or newer melt_recent=$(melt -query filter=loudness | grep -qi R128 && echo yes) # sanitize infiles infiles=$* infile_first=$(perl -e 'print shift @ARGV' $infiles) [ -e "$infile_first" ] || exit1 "Cannot read first input file \"$infile_first\"!" # resolve stem and title (if not explicitly set) stem=${stem:-$(basename "$infile_first" | perl -pe 's/\.[^.]*//')} title=${title:-$stem} # Use vpxenc for VP8 and VP9: CRF is apparently broken with libav use_vpxenc=yes #use_oggenc=yes #use_opusenc=yes # resolve quality/speed hints multipass=yes qscale_theora=5 crf_vp8=30 cpu_vp8=2 crf_vp9=30 cpu_vp9=5 crf_h264=23 speedpreset_h264=medium case "$compression" in normal) :;; dirty) qscale_theora=1 crf_vp8=63 cpu_vp8=5 crf_vp9=63 crf_h264=51 ;; hq) qscale_theora=6 cpu_vp8=0 cpu_vp9=0 speedpreset_h264=veryslow ;; *) exit1 "Unknown compression mode \"$compression\".";; esac [ -n "$multipass" ] || singlepass=yes # parse/resolve size and framerate case "$profile" in '') :;; *@*) while read s r foo; do profilesize="${size:-$s}" framerate="${framerate:-$r}" done << EOF $(echo "$profile" | perl -F@ -anE 'say join " ", @F') EOF ;; *p*) while read s r foo; do profilesize="${size:-${s}p}" framerate="${framerate:-$r}" done << EOF $(echo "$profile" | perl -Fp -anE 'say join " ", @F') EOF ;; *) profilesize="$profile" ;; esac size=${size:-$profilesize} case "$size" in qvga) size=320x240;; hvga) size=480x360;; vga) size=640x480;; svga) size=800x600;; xga) size=1024x768;; 240p|wqvga) size=424x240;; 360p|nhd) size=640x360;; 480p|wvga) size=848x480;; 576p|wsvga) size=1024x576;; 720p|wxga|hd) size=1280x720;; esac if [ -n "$size" ]; then while read w h foo; do width="${width:-$w}" height="${height:-$h}" done << EOF $(echo "$size" | perl -Fx -anE 'say join " ", @F') EOF if [ -z "$width" ] || [ -z "$height" ]; then exit1 "Failed to parse size \"$size\"." fi fi case "$framerate" in */*) while read n d foo; do framerate_num="${framerate_num:-$n}" framerate_den="${framerate_den:-$d}" done << EOF $(echo "$framerate" | perl -F/ -anE 'say join " ", @F') EOF ;; ?*) framerate_num="$framerate" framerate_den=1 ;; esac # resolve input size and framerate (needed for computing bitrate) while read w h r s foo; do width_in="${width_in:-$w}" height_in="${height_in:-$h}" framerate_in="${framerate_in:-$r}" scantype_in="${scantype_in:-$s}" done << EOF $(mediainfo --Inform="Video;%Width% %Height% %FrameRate% %ScanType%" "$infile_first") EOF [ Progressive = "$scantype_in" ] || do_deinterlace=yes case "$video" in talkinghead) refbpp="${refbpp:-0.1}" x264tune=film ;; action) refbpp="${refbpp:-0.15}" x264tune=film ;; '') refbpp="${refbpp:-0.12}" ;; *) exit1 "Unknown video style \"$video\".";; esac for format in $(echo "$formats" | sed -e 's/,/ /g'); do case $format in theora|ogg) ogg=yes;; h264|mp4) mp4=yes;; vp8|webm) webm=yes;; vp9) vp9=yes;; *) exit1 "Unknown format \"$format\".";; esac done _width="${width:-$width_in}" _height="${height:-$height_in}" if [ -n "$_width" ] && [ -n "$_height" ]; then _pixels="$(($_width*$_height))" fi _frames="${framerate:-$framerate_in}" sizepreset_vpx=libvpx-360p webm_qmin=0 webm_qmax=63 webm_lag=16 webm_tokenparts_log2=0 if [ -n "$_pixels" ] && [ $_pixels -ge $((1024*768)) ]; then sizepreset_vpx=libvpx-720p webm_qmin=11 web_qmax=51 webm_tokenparts_log2=2 if [ -n "$_frames" ] && [ $_frames -gt 40 ]; then sizepreset_vpx=libvpx-720p50_60 webm_lag=25 fi fi # compute average bitrate from reference data and "power of .75" rule bitrate_vp8="$bitrate" bitrate_vp9="$bitrate" if [ -n "$_pixels" ] && [ -n "$_frames" ]; then bitrate=$(perl -E '$refsize=640*360;' \ -E "say int( +(($_pixels/\$refsize)**0.75*\$refsize*$_frames*$refbpp) )") bitrate_vp8=$(perl -E "say int( +($bitrate*$factor_vp8) )") #" bitrate_vp9=$(perl -E "say int( +($bitrate*$factor_vp9) )") #" fi # default per-codec-channel bitrates quality_vorbis=3 bitrate_opus=48 bitrate_aac=64 case "$audio" in music) opusapp=audio ;; hqspeech) stereo=yes quality_vorbis=1 bitrate_opus=32 opusapp=voip ;; speech) mono=yes quality_vorbis=1 bitrate_opus=32 compress=yes [ -z "$melt_recent" ] || _melt_loudness="$loudness_data" limit=yes opusapp=voip ;; silence) silence=yes ;; '') : ;; *) exit1 "Unknown audio style \"$audio\".";; esac # inspect channels only if potentially needing downmix/silence if [ -n "$stereo$mono$silence" ]; then channels=$(avprobe -v warning -show_streams "$infile_first" \ | perl -ne 's/channels=// and print $_' || echo -1) fi # adapt channel count and flags to reflect actual downmix/silence need if [ -n "$stereo" ]; then if [ $channels -gt 2 ]; then channels=2 else stereo= fi elif [ -n "$mono" ]; then if [ $channels -gt 1 ]; then channels=1 else mono= fi elif [ -n "$silence" ]; then if [ $channels -gt 0 ]; then channels=0 else silence= fi fi [ -z "$channels" ] || [ $channels -gt 0 ] || channels= if [ -n "$stereo$mono$silence" ]; then # melt cannot downmix with (stereo?) filters applied (bug#763911) if [ -n "$audioprefilters$compress$loudness$limit$filters" ]; then if [ -z "${webm:+$use_oggenc}${webm_vp9:+$use_opusenc}" ]; then downmix_avconv=yes else downmix_oggenc=yes downmix_oggenc=yes fi else downmix_melt=yes fi fi # generic options melt="melt -progress" avconv="avconv -threads auto -y -v info -stats" avconv_chained="avconv -threads auto -y -v warning" vpxenc_chained="vpxenc --quiet ${processors:+-t $((processors-1))}" oggenc="oggenc" opusenc="opusenc" _melt_sample="$infile_first ${sample:+in=${samplestart:-0} out=$((${samplestart:-0} + samplelength))}" # filter options _melt_video="progressive=1${framerate:+ frame_rate_num="$framerate_num" frame_rate_den="$framerate_den"}${size:+ s=${width:+$width}x${height:+$height}}${aspect:+ aspect=$aspect}" _avconv_video="${do_deinterlace:+-filter:v yadif}${framerate:+ -r $framerate_num/$framerate_den}${size:+ -s ${width:+$width}x${height:+$height}}${aspect:+ -aspect $aspect}" _melt_downmix="${downmix_melt:+ac=$channels}" _avconv_downmix="${downmix_avconv:+-ac $channels}" # FIXME: how to downmix to stereo? _oggenc_downmix="${downmix_oggenc:+${mono:+--downmix}}" _opusenc_downmix="${downmix_opusenc:+${stereo:+--downmix-stereo}${mono:+--downmix-mono}}" # limit (i.e. avoid peaks "clipping") _melt_postfilters_audio="${limit:+-filter ladspa.1077}" # codec options _melt_theora="vcodec=libtheora${bitrate:+ vb=$bitrate} qscale=$qscale_theora" _melt_vp8="vcodec=libvpx vpreset=$sizepreset_vpx${bitrate_vp8:+ vb=$bitrate_vp8 minrate=$((bitrate_vp8/20)) maxrate=$((bitrate_vp8*12))} crf=$crf_vp8 cpu-used=$cpu_vp8" _avconv_vp8="-c:v libvpx -pre:v $sizepreset_vpx${bitrate_vp8:+ -b:v $bitrate_vp8 -minrate $((bitrate_vp8/20)) -maxrate $((bitrate_vp8*12))} -crf $crf_vp8 -cpu-used $cpu_vp8" _vpxenc_vpx="--min-q=$webm_qmin --max-q=$webm_qmax${multipass:+ --minsection-pct=5 --maxsection-pct=1200} --kf-max-dist=120 --auto-alt-ref=1 --lag-in-frames=$webm_lag --arnr-maxframes=7 --arnr-strength=5 --arnr-type=3" _vpxenc_vp8="--codec=vp8${bitrate_vp8:+ --target-bitrate=$bitrate_vp8} --good --end-usage=cq --cq-level=$crf_vp8 --cpu-used=$cpu_vp8 $_vpxenc_vpx --token-parts=$webm_tokenparts_log2" _melt_vp9="vcodec=libvpx-vp9 vpreset=$sizepreset_vpx${bitrate_vp9:+ vb=$bitrate_vp9 minrate=$((bitrate_vp9/20)) maxrate=$((bitrate_vp9*12))} crf=$crf_vp9 cpu-used=$cpu_vp9" _avconv_vp9="-c:v libvpx-vp9 -pre:v $sizepreset_vpx${bitrate_vp9:+ -b:v $bitrate_vp9 -minrate $((bitrate_vp9/20)) -maxrate $((bitrate_vp9*12))} -crf $crf_vp9 -cpu-used $cpu_vp9" _vpxenc_vp9="--codec=vp9${bitrate_vp9:+ --target-bitrate=$bitrate_vp9} --good --end-usage=cq --cq-level=$crf_vp9 --cpu-used=$cpu_vp9 $_vpxenc_vpx --tile-columns=$webm_tokenparts_log2 --tile-rows=$webm_tokenparts_log2" _melt_h264="vcodec=libx264 vpreset=$speedpreset_h264 vprofile=baseline${x264tune:+ tune=$x264tune} threads=0 movflags=+faststart crf=$crf_h264" _melt_vorbis="$_melt_downmix acodec=libvorbis aq=$quality_vorbis" _avconv_vorbis="$_avconv_downmix -c:a libvorbis -aq $quality_vorbis" _oggenc_vorbis="$_oggenc_downmix -q $quality_vorbis" _melt_opus="$_melt_downmix acodec=libopus${channels:+ ab=$(($channels*$bitrate_opus))k}${opusapp:+ application=$opusapp}" _avconv_opus="$_avconv_downmix -c:a libopus${channels:+ -b:a $(($channels*$bitrate_opus))k}${opusapp:+ -application $opusapp}" _opusenc_opus="$_opusenc_downmix ${channels:+--bitrate $((channels*bitrate_opus))}" _melt_aac="$_melt_downmix acodec=aac${channels:+ ab=$(($channels*$bitrate_aac))k}" # container options _melt_yuv4mpeg2="f=yuv4mpegpipe $_melt_video pix_fmt=yuv420p an=1 audio_off=1" _avconv_yuv4mpeg2_in="-f yuv4mpegpipe" _avconv_rawvideo="-f rawvideo" _melt_wav="f=wav $_melt_downmix vn=1 video_off=1" _avconv_wav_in="-f wav" _oggenc_wav_in= _opusenc_wav_in= _melt_ogg="f=ogg $_melt_video $_melt_theora $_melt_vorbis" _avconv_ogg_in="-f ogg" _avconv_ogg_vorbis="-f ogg -vn $_avconv_vorbis" _avconv_ogg_opus="-f ogg -vn $_avconv_opus" _melt_webm="f=webm $_melt_video $_melt_vp8 $_melt_vorbis" _avconv_webm="-f webm $_avconv_video $_avconv_vp8 $_avconv_vorbis" _avconv_webm_in="-f webm" _avconv_webm_keepvideo="-f webm $_avconv_video -c:v copy $_avconv_vorbis" _avconv_webm_keepvideo_opus="-f webm $_avconv_video -c:v copy $_avconv_opus" _melt_webm_vp9="f=webm $_melt_video $_melt_vp9 $_melt_opus" _avconv_webm_vp9="-f webm $_avconv_video $_avconv_vp9 $_avconv_opus" _melt_mp4="f=mp4 $_melt_video $_melt_h264 $_melt_aac" _melt_img="f=image2 $_melt_video" [ -z "$loudness" ] || [ -n "$_melt_loudness" ] || do_loudness=yes if [ -n "$multipass" ] && [ -n "$webm$vp9" ]; then echo "Analyzing video complexity${do_loudness:+ and audio dynamics}..." if [ -n "$use_vpxenc" ]; then $melt ${_melt_sample:-$infiles} \ ${do_loudness:+$audioprefilters -filter loudness} \ $filters \ ${do_loudness:+-consumer xml:$stem.xml video_off=1 all=1} \ -consumer avformat:pipe:1 $_melt_yuv4mpeg2 \ | pee \ ${webm:+"$vpxenc_chained - $_vpxenc_vp8 -p 2 --pass=1 --fpf=${stem}_vp8.log -o /dev/null"} \ ${vp9:+"$vpxenc_chained - $_vpxenc_vp9 -p 2 --pass=1 --fpf=${stem}_vp9.log -o /dev/null"} else $melt ${_melt_sample:-$infiles} \ ${do_loudness:+$audioprefilters -filter loudness} \ $filters \ ${do_loudness:+-consumer xml:$stem.xml video_off=1 all=1} \ -consumer avformat:pipe:1 $_melt_yuv4mpeg2 \ | $avconv_chained $_avconv_yuv4mpeg2_in -i pipe:0 \ ${webm:+$_avconv_rawvideo $_avconv_vp8 -an -pass 1 -passlogfile ${stem}_vp8 /dev/null} \ ${vp9:+$_avconv_rawvideo $_avconv_vp9 -an -pass 1 -passlogfile ${stem}_vp9 /dev/null} [ -z "$webm" ] || mv -f ${stem}_vp8-*.log ${stem}_vp8_2pass.log [ -z "$vp9" ] || mv -f ${stem}_vp9-*.log ${stem}_vp9_2pass.log fi if [ -n "$do_loudness" ]; then _melt_loudness="$(perl -ne 'm!([^<]+)! and print $1' $stem.xml)" echo "Loudness data: $_melt_loudness" fi fi if [ -n "$use_oggenc$use_opusenc$use_vpxenc" ] && [ -n "$webm$vp9" ]; then echo "Encoding raw video and extracting audio..." $melt ${_melt_sample:-$infiles} \ ${channels:+$audioprefilters${_melt_loudness:+ -filter loudness program=$lufs results="$_melt_loudness"}} \ $filters${channels:+ $_melt_postfilters_audio} \ ${ogg:+-consumer avformat:$stem.ogv $_melt_ogg} \ ${mp4:+-consumer avformat:$stem.mp4 $_melt_mp4} \ ${channels:+-consumer avformat:$stem.wav $_melt_wav} \ -consumer avformat:pipe:1 $_melt_yuv4mpeg2 \ | pee \ ${webm:+"$vpxenc_chained - $_vpxenc_vp8${singlepass:+ -p 1}${multipass:+ -p 2 --pass=2 --fpf=${stem}_vp8.log} -o ${stem}_silent.webm"} \ ${vp9:+"$vpxenc_chained - $_vpxenc_vp9${singlepass:+ -p 1}${multipass:+ -p 2 --pass=2 --fpf=${stem}_vp9.log} -o ${stem}_vp9_silent.webm"} if [ -n "$webm" ]; then echo "Encoding Vorbis audio and muxing with VP8 video..." if [ -n "$use_oggenc" ]; then $oggenc $_oggenc_wav_in $_oggenc_vorbis -o - $stem.wav \ | $avconv_chained $_avconv_webm_in -i ${stem}_silent.webm $_avconv_ogg_in -i pipe:0 $_avconv_webm_keepvideo $stem.webm else $avconv $_avconv_webm_in -i ${stem}_silent.webm $_avconv_wav_in -i $stem.wav $_avconv_webm_keepvideo ${stem}.webm fi fi if [ -n "$vp9" ]; then echo "Encoding Opus audio and muxing with WebM/VP9 video..." if [ -n "$use_opusenc" ]; then $opusenc $_opusenc_wav_in $_opusenc_opus -o - $stem.wav \ | $avconv_chained $_avconv_webm_in -i ${stem}_vp9_silent.webm $_avconv_ogg_in -i pipe:0 $_avconv_webm_keepvideo_opus ${stem}_vp9.webm else $avconv $_avconv_webm_in -i ${stem}_vp9_silent.webm $_avconv_wav_in -i $stem.wav $_avconv_webm_keepvideo_opus ${stem}_vp9.webm fi fi else echo "Encoding video..." $melt ${_melt_sample:-$infiles} \ ${channels:+$audioprefilters${_melt_loudness:+ -filter loudness program=$lufs results="$_melt_loudness"}} \ $filters${channels:+ $_melt_postfilters_audio} \ ${ogg:+-consumer avformat:$stem.ogv $_melt_ogg} \ ${webm:+-consumer avformat:$stem.webm $_melt_webm${multipass:+ pass=2 passlogfile=${stem}_vp8}} \ ${vp9:+-consumer avformat:${stem}_vp9.webm $_melt_webm_vp9${multipass:+ pass=2 passlogfile=${stem}_vp9}} \ ${mp4:+-consumer avformat:$stem.mp4 $_melt_mp4} fi if [ -n "$mp4" ] && [ -z "$melt_recent" ]; then mv "$stem.mp4" "$stem.mp4"~ qt-faststart "$stem.mp4"~ "$stem.mp4" [ -f "$stem.mp4" ] && rm "$stem.mp4"~ || mv -f "$stem.mp4"~ "$stem.mp4" fi # cleanup encoding cruft rm -f $stem.xml $stem.wav ${stem}_vp8.log ${stem}_vp9.log ${stem}_silent.webm ${stem}_vp9_silent.webm # JPEG preview $melt $infile_first in=0 out=0 \ -group $filters \ -consumer avformat:$stem.jpg $_melt_img __width="${_width:+ width=\"$_width\"}" __height="${_height:+ height=\"$_height\"}" # Flash object needs extra space for controllers __heightplus=${_height:+ height=\"$(($_height+4))\"} _source_ogg="" _source_webm="" _source_vp9="" _source_mp4="" [ -z "$flashplayer" ] || flash=yes [ -n "$mp4" ] || [ -z "$flash" ] || error1 "Cannot enable flash when mp4 format is disabled." _object_flash="" _param_name="" _param_flashvars="" __oggfile=${ogg:+open format Ogg} __webmfile=${webm:+open format WebM (VP8)} __vp9file=${vp9:+open format WebM (VP9/Opus)} __mp4file=${mp4:+closed format MPEG-4} cat >"$stem.html" < ${mp4:+$_source_mp4 }${vp9:+$_source_vp9 }${webm:+$_source_webm }${ogg:+$_source_ogg }${flash:+$_object_flash $_param_name $_param_flashvars }$title ${flash:+ }

Download Video:

    ${vp9:+
  • $__vp9file }${webm:+
  • $__webmfile }${ogg:+
  • $__oggfile }${mp4:+
  • $__mp4file }

EOF