- #!/bin/sh
- # Copyright © 2010-2014 Jonas Smedegaard <dr@jones.dk>
- # 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 <http://www.gnu.org/licenses/>.
- #
- # Depends: libav-tools melt ffmpegthumbnailer
- #
- # Origins:
- # http://diveintohtml5.org/video.html
- # http://camendesign.com/code/video_for_everybody
- # http://www.streaminglearningcenter.com/articles/so-you-want-to-get-to-know-h264.html
- #
- # Possible flashplayers:
- # http://www.internetmarketingnotes.com/2010/07/free-embeddable-flash-video-flv-players-for-commercial-use/
- #
- # TODO: offer to skip rendering again if an output file exist already
- # TODO: support --width and --height (resolving the other part from input/forced aspect ratio)
- # TODO: support --formats (comma-separated, to allow e.g. excluding webm even if supported)
- # TODO: add --speech option, using mono, lower audio rate, and (when solved how) speex
- set -e
- PRG=$(basename "$0")
- showhelp() {
- cat <<EOF
- Usage: $PRG [OPTION...] [--] [ARG=VALUE...] INPUTFILE... [ARG=VALUE...]
- Encode video file in multiple web-optimized formats, and provide sample
- html favoring open formats with optional non-JavaScript Flash fallback.
- -p, --profile Video format:
- [modulus 16]
- 320x240 qvga 240p 432x240 wqvga
- 640x480 vga 480p 848x480 wvga
- 576p 1024x576 wsvga
- 1024x768 xga hd 720p 1280x720 wxga
- [modulus 8]
- 480x360 hvga nhd 360p 640x360
- 800x600 svga
- -s, --size Output size (ffmpeg): WIDTHxHEIGHT vga qcif etc.
- (default: use input size)
- -a, --aspect Display Aspect Ratio in melt format e.g. @16/9
- (default: no aspect hinting)
- -r, --rate Video framerate
- (default: 25)
- -b, --bitrate Video bitrate in bytes, with optional ISO suffix
- (default: none)
- --h264profile MPEG-4 AVC target profile: baseline main high
- (default: baseline)
- --h264preset MPEG-4 AVC target preset: slow ultrafast etc.
- (default: medium)
- --webmpreset WebM target preset: 360p 720p 720p50_60 etc.
- (default: profile-related or none)
- --audio Audio style: music speech silence
- (default: none - use input channel count)
- --stem Stem of output filenames, optionally with path
- (default: basename of last input file)
- -t, --title Title used in html fallback graphics
- (default: stem)
- --filter Add melt filter (applied to all input files)
- --sample[=frame] Limit output to a 150 frames sample from
- beginning or optionally a later start frame
- -h, --help This help text
- Examples:
- $PRG -s qvga -t "Funny guy" intro.dv myvideo.dv
- $PRG -p 480p --stem funny -t "Funny guy" myvideo.dv
- Options before input files are passed to melt producer, and after to
- melt avformat consumer.
- When video bitrate is set, 2-pass encoding is used for MPEG-4 output.
- Hints:
- * Use max. 640x480 pixel size for widest compatibility
- (ie. square-pixel 640x360 or anamorphic svcd_ntsc_wide for 16:9)
- * Use a modulus 16 profile (qvga vga 480p 720p) for best compression.
- * Append these for single-mic speech: ac=1 ar=32000 ab=64k
- * Set bitrate for optimal MPEG-4/WebM bits-per-pixel ratio:
- + talking head: 0.1
- + high-motion or detailed content: 0.15
- (inspect result with mediainfo)
- More info: <http://camendesign.com/code/video_for_everybody>
- <http://www.streaminglearningcenter.com/>
- <http://en.wikipedia.org/wiki/HTML5_video>
- EOF
- }
- exit1() {
- response="${1:+Error: }${1:-Internal error!}"
- echo >&2 "$response"
- exit 1
- }
- # defaults
- rate=25
- h264profile=baseline
- # parse cmdline options
- TEMP="`getopt -s sh -o hp:s:a:r:b:t: -l help,profile:,size:,aspect:,rate:,bitrate:,h264profile:,h264preset:,webmpreset:,audio:,stem:,title:,filter:,sample:: -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) rate="$2"; shift 2;;
- -b|--bitrate) bitrate="$2"; shift 2;;
- --h264profile) h264profile="$2"; shift 2;;
- --h264preset) h264preset="$2"; shift 2;;
- --webmpreset) webmpreset="$2"; shift 2;;
- --audio) audio="$2"; shift 2;;
- --stem) stem="$2"; shift 2;;
- -t|--title) title="$2"; shift 2;;
- --filter) filters="${filters:+$filters }-filter $2"; shift 2
- while [ $# -gt 0 ] ; do
- case "$1" in
- *=*) filters="${filters:+$filters }$1"; shift;;
- *) break;;
- esac
- done
- ;;
- --sample) sample="in=${2:-0} out=$((${2:-0} + 150))"; shift 2;;
- --) shift; break;;
- *) exit1 "Internal error resolving options.";;
- esac
- done
- while [ $# -gt 0 ] ; do
- case "$1" in
- *=*) _melt_in="${_melt_in:+$_melt_in }$1"; shift;;
- *) break;;
- esac
- done
- while [ $# -gt 0 ] ; do
- case "$1" in
- *=*) _melt_out="${_melt_out:+$_melt_out }$1"; shift;;
- *) infiles="${infiles:+$infiles }$1"; shift;;
- esac
- done
- if [ -z "$infiles" ]; then
- showhelp
- exit1 "Too few parameters!"
- fi
- # input filename (mandatory)
- infile=$(perl -e 'print pop @ARGV' $infiles)
- [ -e "$infile" ] || exit1 "Input file missing!"
- # resolve stem and title (if not explicitly set)
- stem=${stem:-$(basename "$infile" | perl -pe 's/\.[^.]*//')}
- title=${title:-$stem}
- case "$profile" in
- 320x240|qvga)
- _melt_in="-profile=quarter_pal${_melt_in:+ $_melt_in}"
- _melt="${size:-width=320 height=240}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 480x360|hvga)
- _melt_in="-profile=quarter_pal${_melt_in:+ $_melt_in}"
- _melt="${size:-width=480 height=360}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 640x480|vga)
- _melt_in="-profile=quarter_pal${_melt_in:+ $_melt_in}"
- _melt="${size:-width=640 height=480}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 800x600|svga)
- _melt_in="-profile=quarter_pal${_melt_in:+ $_melt_in}"
- _melt="${size:-width=800 height=600}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 1024x768|xga)
- _melt_in="-profile=quarter_pal${_melt_in:+ $_melt_in}"
- _melt="${size:-width=1024 height=768}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-720p}"
- ;;
- 240p|432x240|wqvga)
- _melt_in="-profile=atsc_720p_25${_melt_in:+ $_melt_in}"
- _melt="${size:-width=432 height=240}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 360p|640x360|nhd)
- _melt_in="-profile=atsc_720p_25${_melt_in:+ $_melt_in}"
- _melt="${size:-width=640 height=360}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 480p|848x480|wvga)
- _melt_in="-profile=atsc_720p_25${_melt_in:+ $_melt_in}"
- _melt="${size:-width=848 height=480}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 576p|1024x576|wsvga)
- _melt_in="-profile=atsc_720p_25${_melt_in:+ $_melt_in}"
- _melt="${size:-width=1024 height=576}${_melt:+ $_melt}"
- webmpreset="${webmpreset:-360p}"
- ;;
- 720p|1280x720|wxga|hd)
- _melt_in="-profile=atsc_720p_25${_melt_in:+ $_melt_in}"
- webmpreset="${webmpreset:-720p}"
- ;;
- '')
- _melt="progressive=1 frame_rate_den=1${_melt:+ $_melt}"
- [ "25" != "$rate" ] || _melt="{_melt:+$_melt }frame_rate_num=$rate"
- ;;
- *) exit1 "Unknown profile \"$profile\" - please specify size and aspect directly.";;
- esac
- [ "25" = "$rate" ] || _melt="{_melt:+$_melt }frame_rate_num=$rate"
- case "$h264profile" in
- baseline|main)
- _melt_h264="properties=x264-medium-$h264profile ${h264preset:+-vpre=libx264-$h264preset}"
- ;;
- high)
- _melt_h264="properties=x264-medium ${h264preset:+-vpre=libx264-$h264preset}"
- ;;
- *) exit1 "Unknown MPEG-4 AVC profile \"$h264profile\".";;
- esac
- # default per-codec-channel bitrates
- bitrate_vorbis=64
- bitrate_aac=96
- case "$audio" in
- music)
- channels=2
- ;;
- speech)
- channels=1
- bitrate_vorbis=48
- bitrate_aac=64
- ;;
- silence)
- channels=0
- ;;
- '')
- channels=$(avprobe -v warning -show_streams "$infile" | perl -ne 's/channels=// and print $_')
- ;;
- *) exit1 "Unknown audio style \"$audio\".";;
- esac
- [ $channels -le 2 ] || channels=2
- [ $channels -gt 0 ] || channels=
- # TODO: Check and fail if all needed tools are not available
- # TODO: When verified beneficial, add option real_time=-2
- _melt_in="${_melt_in:+$_melt_in }-progress $sample"
- _melt="$_melt ${bitrate:+vb=${bitrate}} ${size:+s=$size} ${aspect:+aspect=$aspect}"
- _melt_ogg="f=ogg vcodec=libtheora ${bitrate:-qscale=5}"
- _melt_h264="$_melt_h264 ${bitrate:-qscale=5}"
- _melt_webm="${webmpreset:+-vpre=libvpx-$webmpreset}"
- _melt_audio="${channels:+ac=$channels}"
- _melt_vorbis="$_melt_audio acodec=libvorbis ab=$(($channels*$bitrate_vorbis))k"
- _melt_aac="$_melt_audio acodec=aac ab=$(($channels*$bitrate_aac))k"
- ## Theora/Vorbis/Ogg
- melt -group $_melt_in $infiles -group $filters -consumer avformat:"$stem.ogv" $_melt_ogg $_melt $_melt_vorbis $_melt_out
- ## H.264/AAC/MP4
- [ -z "$bitrate" ] || melt -group $_melt_in $infiles -group $filters -consumer avformat:/dev/null properties=x264-medium-pass1 $_melt_h264 $_melt $_melt_out
- melt -group $_melt_in $infiles -group $filters -consumer avformat:"$stem.mp4" ${bitrate:+pass=2} $_melt_h264 $_melt $_melt_aac $_melt_out
- # TODO: drop qt-faststart when melt 0.9.2 is stable
- mv "$stem.mp4" "$stem.mp4"~
- qt-faststart "$stem.mp4"~ "$stem.mp4"
- [ -f "$stem.mp4" ] && rm "$stem.mp4"~ || mv -f "$stem.mp4"~ "$stem.mp4"
- ## VP8/Vorbis/WebM
- # TODO: use two-pass when supported by melt
- melt -group $_melt_in $infiles -group $filters -consumer avformat:"$stem.webm" properties=webm $_melt_webm $_melt $_melt_vorbis $_melt_out
- ## JPEG preview
- ffmpegthumbnailer -s0 -i "$stem.mp4" -o "$stem.jpg"
- # resolve width and height from preview image
- size=$(jpeginfo "$stem.jpg" | perl -ane 'print "$F[1]x$F[3]"')
- width=$(echo "$size" | perl -Fx -ane 'print $F[0]')
- height=$(echo "$size" | perl -Fx -ane 'print $F[1]')
- heightplus=${height:+$(($height+4))}
- # TODO: resolve flash player to use
- [ -z "$flashplayer" ] || flash=yes
- cat >"$stem.html" <<EOF
- <!-- Video for Everybody, Kroc Camen of Camen Design -->
- <video width="$width" height="$height" preload controls>
- <source src="$stem.mp4" type="video/mp4" />
- <source src="$stem.webm" type="video/webm" />
- <source src="$stem.ogv" type="video/ogg" />
- ${flash:+<object width="$width" height="$heightplus" type="application/x-shockwave-flash" data="$flashplayer.swf">
- <param name="movie" value="$flashplayer.swf" />
- <param name="flashvars" value="image=$stem.jpg&file=$stem.mp4" />
- }<img src="$stem.jpg" width="$width" height="$height" alt="$title"
- title="No video playback capabilities, please download the video below" />
- ${flash:+</object>
- }</video>
- <p><strong>Download Video:</strong>
- open format <a href="$stem.webm">WebM</a>,
- open format <a href="$stem.ogv">Ogg</a>,
- closed Format <a href="$stem.mp4">MPEG-4</a>.
- </p>
- EOF
|