#!/bin/bash # # $Rev$ $Date$ # # vcs # Video Contact Sheet *NIX: Generates contact sheets (previews) of videos # # Copyright (C) 2007 Toni Corvera # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # Author: Toni Corvera # declare -r VERSION="1.0.6b" # # History (The full changelog was moved to a separate file and can be found # at ). # # TODO: Support for ms timestamps (ffmpeg supports it e.g. 54.9 is ok and != 54) # # 1.0.6b: (2007-04-21) (Bugfix release) # * BUGFIX: Use mktemp instead of tempfile (Thanks to 'o kapi') # * Make sure mktemp is installed, just in case ;) # set -e # Configuration file, please, use this file to modify the behaviour of the # script. Using this allows overriding some variables (see below) # to your liking. Only lines with a variable assignment are evaluated, # it should follow bash syntax, note though that ';' can't be used # currently in the variable values; e.g.: # # # Sample configuration for vcs # user=myname # Sign all compositions as myname # bg_heading=gray # Make the heading gray # # There is a total of three configuration files than are loaded if the exist: # * /etc/vcs.conf: System wide conf, least precedence # * $CFGFILE (by default ~/.vcs.conf): Per-user conf, second least precedence # * ./vcs.conf: Per-dir confif, most precedence # # The variables that can be overriden are below the block of constants ahead. declare -r CFGFILE=~/.vcs.conf # Constants {{{ # see $decoder declare -ri DEC_MPLAYER=1 DEC_FFMPEG=3 # See $timecode_from declare -ri TC_INTERVAL=4 TC_NUMCAPS=8 # These can't be overriden, modify this line if you feel the need declare -r PROGRAM_SIGNATURE="with Video Contact Sheet *NIX ${VERSION} " # see $safe_rename_pattern declare -r DEFAULT_SAFE_REN_PATT="%b-%N.%e" # see $extended_factor declare -ri DEFAULT_EXT_FACTOR=4 # see $verbosity declare -ri V_ALL=5 V_NONE=-1 V_ERROR=1 V_WARN=2 V_INFO=3 # }}} # End of constants # Override-able variables {{{ declare -i DEFAULT_INTERVAL=300 declare -i DEFAULT_NUMCAPS=16 declare -i DEFAULT_COLS=2 # Text before the user name in the signature declare user_signature="Preview created by" # By default sign as the system's username (see -u, -U) declare user=$(id -un) # Which of the two methods should be used to guess the number of thumbnails declare -i timecode_from=$TC_INTERVAL # Which of the two vidcappers should be used (see -F, -M) # mplayer seems to fail for mpeg or WMV9 files, at least on my system # also, ffmpeg allows better seeking: ffmpeg allows exact second.fraction # seeking while mplayer apparently only seeks to nearest keyframe declare -i decoder=$DEC_FFMPEG # Options used in imagemagick, these options set the final aspect # of the contact sheet declare output_format=png # ImageMagick decides the type from the extension declare -i output_quality=92 # Output image quality (only affects the final # image and obviously only in lossy formats) # Colours, see convert -list color to get the list declare bg_heading=YellowGreen # Background for meta info (size, codec...) declare bg_sign=SlateGray # Background for signature declare fg_heading=black # Font colour for meta info box declare fg_sign=black # Font colour for signature declare fg_tstamps=white # Font colour for timestamps # Fonts, see convert -list type to get the list declare font_tstamps=courier # Used for timestamps behind the thumbnails declare font_heading=helvetica # Used for meta info box declare font_sign=$font_heading # Used for the signature box # Font sizes, in points declare pts_tstamps=18 # Used for the timestamps declare pts_meta=16 # Used for the meta info box declare pts_sign=11 # Used for the signature # See --shoehorn declare shoehorned= # This can only be changed in the configuration file # Change it to change the safe renanimg: # When writing the output file, the input name + output extension is # used (e.g.: "some video.avi.png"), if it already exists, though, # a number if appended to the name. This variable dictates where the number is # placed. # By default "%b-%N.%e" where: # %b is the basename (file name without extension) # %N is the appended number # %e is the extension # The default creates outputs like "output.avi-1.png" # # If overridden with an incorrect value it will be silently set to the default declare safe_rename_pattern="$DEFAULT_SAFE_REN_PATT" # Controls how many extra captures will be created in the extended mode # (see -e), 0 is the same as disabling the extended mode # This number is multiplied by the total number of captures to get # the number of extra captures. So, e.g. -n2 -e2 leads to 4 extra captures. declare extended_factor=0 # Options added always to the ones in the command line # (command line options override them). # Note using this is a bit tricky :P mostly because I've no clue of how this # should be done. # As an example: you want to set always the title to "My Title" and output # to jpeg: default_options="-T'My Title' -j" declare default_options= # Verbosity level so far from the command line can only be muted (see -q) # it can be overridden, though declare -i verbosity=$V_ALL # When set to 0 the status messages printed by vcs while running # are coloured if the terminal supports it. Set to 1 if this annoys you. declare -i plain_messages=0 # }}} # End of override-able variables # Options and other internal usage variables, no need to mess with this! declare -i interval=$DEFAULT_INTERVAL # Interval of captures (=numsecs/numcaps) declare -i numcaps=$DEFAULT_NUMCAPS # Number of captures (=numsecs/interval) declare title="" declare -i fromtime=0 # Starting second (see -f) declare -i totime=-1 # Ending second (see -t) declare -a initial_stamps=( ) # Manually added stamps (see -S) declare -i th_height= # Height of the thumbnails, by default use same as input declare -i cols=$DEFAULT_COLS # Number of output columns declare -i manual_mode=0 # if 1, only command line timestamps will be used declare aspect_ratio=0 # If 0 no transformations done (see -a) # If -1 try to guess (see -A) declare -a TEMPSTUFF=( ) # Temporal files declare -a TIMECODES=( ) # Timestamps of the video captures declare -a HLTIMECODES=( ) # Timestamps of the highlights (see -l) declare VCSTEMPDIR= # Temporal directory, all temporal files # go there # This holds the output of mplayer -identify on the current video declare MPLAYER_CACHE= # This holds the parsed values of MPLAYER_CACHE... declare -a VID= # ...and these are the indexes in $VID declare -ri W=0 H=1 FPS=2 LEN=3 VCODEC=4 ACODEC=5 VDEC=6 CHANS=7 # Exit codes, same codes as /usr/include/sysexits.h declare -r EX_OK=0 EX_USAGE=64 EX_UNAVAILABLE=69 \ EX_NOINPUT=66 EX_SOFTWARE=70 EX_CANTCREAT=73 \ EX_INTERRUPTED=79 # This one is not on sysexits.h # These are the variables allowed to be overriden in the config file, # please. # They're REGEXes, they'll be concatenated to form a regex like # (override1|override2|...). # Don't mess with this unless you're pretty sure of what you're doing. # All this extra complexity is done to avoid including the config # file directly for security reasons. declare -ra ALLOWED_OVERRIDES=( 'user' 'user_signature' 'bg_.*' 'font_.*' 'pts_.*' 'fg_.*' 'output_quality' 'DEFAULT_INTERVAL' 'DEFAULT_NUMCAPS' 'DEFAULT_COLS' 'decoder' 'output_format' 'shoehorned' 'timecode_from' 'safe_rename_pattern' 'default_options' 'extended_factor' 'verbosity' 'plain_messages' ) # Loads the configuration files if present # load_config() load_config() { local CONFIGS=( /etc/vcs.conf $CFGFILE ./vcs.conf) for cfgfile in ${CONFIGS[*]} ;do if [ ! -f "$cfgfile" ]; then continue; fi while read line ; do # auto variable $line override "$line" "file $cfgfile" # Feeding it comments should be harmless done <$cfgfile done } # Do an override # It takes basically an assignment (in the same format as bash) # to one of the override-able variables (see $ALLOWED_OVERRIDES). # There are some restrictions though. Currently ';' is not allowed to # be in the assignment. # override($1 = bash variable assignment, $2 = source) override() { local o="$1" local src="$2" local compregex=$( sed 's/ /|/g' <<<${ALLOWED_OVERRIDES[*]} ) # Don't allow ';', FIXME: dunno how secure that really is... # FIXME: ...it doesn't really works anyway if ! egrep -q '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*=[^;]*' <<<"$o" ; then return fi if ! egrep -q "^($compregex)=" <<<"$o" ; then return fi local varname=$(sed -r 's/^[[:space:]]*([a-zA-Z0-9_]*)=.*/\1/'<<<"$o") local varval=$(sed -r 's/[^=]*=(.*)/\1/'<<<"$o") # FIXME: Security! local curvarval= eval curvarval='$'"$varname" if [ "$curvarval" == "$varval" ]; then warn "Ignored override '$varname' (already had same value)" else eval "$varname=\"$varval\"" # FIXME: Only for really overridden ones warn "Overridden variable '$varname' from $src" fi } # {{{ # Convenience functions # Returns true if input is composed only of numbers # is_number($1 = input) is_number() { egrep -q '^[0-9]+$' <<<"$1" } # Returns true if input can be parsed as a floating point number # Accepted: XX.YY XX. .YY (.24=0.24 # is_float($1 = input) is_float() { egrep -q '^([0-9]+\.?([0-9])?+|(\.[0-9]+))$'<<<"$1" } # Returns true if input is a fraction # Only accepts XX/YY # is_fraction($1 = input) is_fraction() { egrep -q '^[0-9]+/[0-9]+$'<<<"$1" } # Rounded product # multiplies parameters and prints the result, rounded to the closest int # parameters can be separated by commas or spaces # e.g.: rmultiply 4/3,576 OR 4/3 576 = 4/3 * 576 = 768 # rmultiply($1 = operator1, [$2 = operator2, ...]) # rmultiply($1 = "operator1,operator2,...") rmultiply() { local exp=$(sed 's/[ ,]/*/g'<<<"$@") # bc expression local f=$(bc -lq<<<"$exp") # exact float value # division is integer by default (without -l) so it's the smae # as rounding to the lower int bc -q <<<"( $f + 0.5 ) / 1" } # Prints the width correspoding to the input height and the variable # aspect ratio # compute_width($1 = height) (=AR*height) (rounded) compute_width() { rmultiply $aspect_ratio,$1 } # Parse an interval and print the corresponding value in seconds # returns something not 0 if the interval is not recognized. # # The current code is a tad permissive, it allows e.g. things like # 10m1h (equivalent to 1h10m) # 1m1m (equivalent to 2m) # I don't see reason to make it more anal, though. # get_interval($1 = interval) get_interval() { if is_number "$1" ; then echo $1 ; return 0 ; fi local s=$(tr '[A-Z]' '[a-z]' <<<"$1") # Only allowed characters if ! grep -q '[0-9smh]' <<<"$s"; then return $EX_USAGE; fi # FIXME: Find some cleaner way local i= c= num= sum=0 for i in $(seq 0 $(( ${#s} - 1)) ); do c=${s:$i:1} if is_number $c ; then num+=$c else case $c in h) num=$(($num * 3600)) ;; m) num=$(($num * 60)) ;; s) ;; *) return $EX_SOFTWARE ;; esac sum=$(($sum + $num)) num= fi done # If last element was a number, it's seconds and they weren't added if is_number $c ; then sum=$(( $sum + $num )) fi echo $sum return 0 } # Pads a string with zeroes on the left until it is at least # the indicated length # pad($1 = minimum length, $2 = string) pad() { local len=$1 local str=$2 while [ ${#str} -lt $len ]; do str=0$str done echo $str } # Prints a number of seconds in a more human readable form # e.g.: 3600 becomes 1:00:00 # pretty_stamp($1 = seconds) pretty_stamp() { if ! is_number "$1" ; then return $EX_USAGE ; fi local t=$1 local h=$(( $t / 3600 )) t=$(( $t % 3600 )) local m=$(( $t / 60 )) t=$(( $t % 60 )) local s=$t local R="" if [ $h -gt 0 ]; then R+="$h:" fi R+=$(pad 2 "$m"):$(pad 2 $s) echo $R } # Prints the size of a file in a human friendly form # The units are in the IEC/IEEE/binary format (e.g. MiB -for mebibytes- # instead of MB -for megabytes-) # get_pretty_size($1 = file) get_pretty_size() { local f="$1" local bytes=$(du -DL --bytes "$f" | cut -f1) local size="" if [ "$bytes" -gt $(( 1024**3 )) ]; then local gibs=$(( $bytes / 1024**3 )) local mibs=$(( ( $bytes % 1024**3 ) / 1024**2 )) size="${gibs}.${mibs:0:2} GiB" elif [ "$bytes" -gt $(( 1024**2)) ]; then local mibs=$(( $bytes / 1024**2 )) local kibs=$(( ( $bytes % 1024**2 ) / 1024 )) size="${mibs}.${kibs:0:2} MiB" elif [ "$bytes" -gt 1024 ]; then local kibs=$(( $bytes / 1024 )) bytes=$(( $bytes % 1024 )) size="${kibs}.${bytes:0:2} KiB" else size="${bytes} B" fi echo $size } # Rename a file, if the target exists, try with appending numbers to the name # And print the output name to stdout # See $safe_rename_pattern # safe_rename($1 = original file, $2 = target file) safe_rename() { local from="$1" local to="$2" # Extension local ext=$(sed -r 's/.*\.(.*)/\1/g' <<<"$to") # Basename without extension local b=$(basename "$2" ".$ext") # safe_rename_pattern is override-able, ensure it has a valid value: if ! grep -q '%e' <<<"$safe_rename_pattern" || ! grep -q '%N' <<<"$safe_rename_pattern" || ! grep -q '%b' <<<"$safe_rename_pattern" ; then safe_rename_pattern=$DEFAULT_SAFE_REN_PATT fi local n=1 while [ -f "$to" ]; do # Only executes if $2 exists to=$(sed "s/%b/$b/g" <<<"$safe_rename_pattern") to=$(sed "s/%N/$n/g" <<<"$to") to=$(sed "s/%e/$ext/g" <<<"$to") let 'n++'; done mv "$from" "$to" echo "$to" } # Tests the presence of all required programs # test_programs() test_programs() { local retval=0 last=0 for prog in mplayer convert montage bc ffmpeg mktemp ; do type -pf "$prog" >/dev/null if [ $? -ne 0 ] ; then error "Required program $prog not found!" let 'retval++' fi done return $retval } # Remove any temporal files # Does nothing if none has been created so far # cleanup() cleanup() { if [ -z $TEMPSTUFF ]; then return 0 ; fi info "Cleaning up..." rm -rf ${TEMPSTUFF[*]} TEMPSTUFF=( ) } # Exit callback. This function is executed on exit (correct, failed or # interrupted) # exithdlr() exithdlr() { cleanup } # Feedback handling, these functions are use to print messages respecting # the verbosity level # Optional color usage added from explanation found in # # # error($1 = text) error() { if [ $verbosity -ge $V_ERROR ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 1; fi # sgr0 is always used, this way if # a) something prints inbetween messages it isn't affected # b) if plain_messages is overridden colour stops after the override echo "$1" >&2 ; tput sgr0 fi } # # Print a non-fatal error or warning # warning($1 = text) warn() { if [ $verbosity -ge $V_WARN ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 3; fi echo "$1" >&2 ; tput sgr0 fi } # # Print an informational message # info($1 = text) info() { if [ $verbosity -ge $V_INFO ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 2; fi echo "$1" >&2 ; tput sgr0 fi } # # Same as info but with no colour ever. # infoplain($1 = text) infoplain() { if [ $verbosity -ge $V_INFO ]; then echo "$1" >&2 fi } # }}} # Convenience functions # {{{ # Core functionality # Creates a new temporary directory # create_temp_dir() create_temp_dir() { VCSTEMPDIR=$(mktemp -d -t vcs.XXXXXX) if [ ! -d "$VCSTEMPDIR" ]; then error "Error creating temporary directory" return $EX_CANTCREAT fi TEMPSTUFF+=( "$VCSTEMPDIR" ) } # Create a new temporal file and print its filename # new_temp_file($1 = suffix) new_temp_file() { local r=$(mktemp -p "$VCSTEMPDIR" "vcs-XXXXXX") if [ ! -f "$r" ]; then error "Failed to create temporary file" return $EX_CANTCREAT fi r=$(safe_rename "$r" "$r$1") || { error "Failed to create temporary file" return $EX_CANTCREAT } TEMPSTUFF+=( "$r" ) echo "$r" } # Add to $TIMECODES the timecodes at which a capture should be taken # from the current video # compute_timecodes($1 = timecode_from, $2 = interval, $3 = numcaps) compute_timecodes() { local st=0 end=${VID[$LEN]} tcfrom=$1 tcint=$2 tcnumcaps=$3 # globals: fromtime, totime, timecode_from, TIMECODES if [ $st -lt $fromtime ]; then st=$fromtime fi if [ $totime -gt 0 ] && [ $end -gt $totime ]; then end=$totime fi local inc= if [ "$tcfrom" -eq $TC_INTERVAL ]; then inc=$tcint elif [ "$tcfrom" -eq $TC_NUMCAPS ]; then # Numcaps mandates: timecodes are obtained dividing the length # by the number of captures if [ $tcnumcaps -eq 1 ]; then # Special case, just one capture, center it inc=$(( ($end-$st) / 2 + 1)) else inc=$(( ($end-$st) / $tcnumcaps )) fi else error "Internal error" return $EX_SOFTWARE fi if [ $inc -gt ${VID[$LEN]} ]; then error "Interval is longer than video length, skipping $f" return $EX_USAGE fi local LTC=( ) stamp= for stamp in $(seq $st $inc $end); do LTC+=( $stamp ) done unset LTC[0] # Discard initial cap (=$st) TIMECODES=( ${TIMECODES[*]} ${LTC[*]} ) } # Tries to guess an aspect ratio comparing width and height to some # known values (e.g. VCD resolution turns into 4/3) # guess_aspect() guess_aspect() { # mplayer's ID_ASPECT seems to be always 0 ¿? local w=${VID[$W]} h=${VID[$H]} if [ $w -eq 352 ]; then # VCD / DVD @ VCD Res. / Half-D1 / CVD if [ $h -eq 288 ] || [ $h -eq 240 ]; then aspect_ratio=4/3 elif [ $h -eq 576 ] || [ $h -eq 480 ]; then # Half-D1 / CVD aspect_ratio=4/3 fi elif [ $w -eq 704 ] || [ $w -eq 720 ]; then # DVD / DVB # Actually for 720x576/720x480 16/9 is as good a guess if [ $h -eq 576 ] || [ $h -eq 480 ]; then aspect_ratio=4/3 fi elif [ $w -eq 480 ]; then # SVCD if [ $h -eq 576 ] || [ $h -eq 480 ]; then aspect_ratio=4/3 fi else warn "Couldn't guess aspect ratio." aspect_ratio=$(bc -lq <<<"$w / $h") fi local AR=$(sed -r 's/(\.[0-9]{2}).*/\1/g'<<<$aspect_ratio) info "Aspect ratio set to $AR" } # Capture a frame # capture($1 = filename, $2 = second) capture() { local f=$1 stamp=$2 local VIDCAPFILE=00000001.png # globals: $shoehorned $decoder if [ $decoder -eq $DEC_MPLAYER ]; then mplayer -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ -frames 1 -ss $stamp $shoehorned "$f" >/dev/null 2>&1 elif [ $decoder -eq $DEC_FFMPEG ]; then # XXX: For some reason -ss before -i failed on my mkv sample # while after -i it failed on my wmv9 sample ¿? ffmpeg -y -ss $stamp -i "$f" -an -dframes 1 -vframes 1 -vcodec png \ -f rawvideo $shoehorned $VIDCAPFILE >/dev/null 2>&1 else error "Internal error!" return $EX_SOFTWARE fi || { local retval=$? error "The capturing program failed!" return $retval } if [ ! -f "$VIDCAPFILE" ] || [ "0" == "$(du "$VIDCAPFILE" | cut -f1)" ]; then error "Failed to capture frame (at second $stamp)" return $EX_SOFTWARE fi return 0 } # Draw a timestamp in the file # apply_stamp($1 = filename, $2 = timestamp, $3 = width, $4 = height) apply_stamp() { local filename=$1 timestamp=$2 width=$3 height=$4 local temp=$(new_temp_file ".png") mv "$filename" "$temp" # Add the timestamp to each vidcap, doing it here is much powerful/simple # than with the next montage command # Note the '!', it is necessary to apply aspect ratio change convert -box '#000000aa' -geometry ${width}x${height}! \ -fill $fg_tstamps -pointsize $pts_tstamps -gravity SouthEast \ -stroke none -strokewidth 3 -annotate +5+5 " $(pretty_stamp $stamp) " \ "$temp" "$filename" if [ ! -f "$filename" ]; then error "Failed to add timestamp to capture" mv "$temp" "$filename" # Leave things as before return $EX_CANTCREAT fi } # Sorts timestamps and removes duplicates # clean_timestamps($1 = space separated timestamps) clean_timestamps() { # Note AFAIK sort only sorts lines, that's why y replace spaces by newlines local s=$1 sed 's/ /\n/g'<<<"$s" | sort -n | uniq } # Fills the $MPLAYER_CACHE and $VID variables with the video data # identify_video($1 = file) identify_video() { local f=$1 # Meta data extraction # Note to self: Don't change the -vc as it would affect $vdec MPLAYER_CACHE=$(mplayer -benchmark -ao null -vo null -identify -frames 0 -quiet "$f" 2>/dev/null | grep ^ID) VID[$VCODEC]=$(grep ID_VIDEO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # FourCC VID[$ACODEC]=$(grep ID_AUDIO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$VDEC]=$(grep ID_VIDEO_CODEC <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # Decoder (!= Codec) VID[$W]=$(grep ID_VIDEO_WIDTH <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$H]=$(grep ID_VIDEO_HEIGHT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$FPS]=$(grep ID_VIDEO_FPS <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$LEN]=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2 | cut -d. -f1) # For some reason my (one track) samples have two ..._NCH, first one 0 VID[$CHANS]=$(grep ID_AUDIO_NCH <<<"$MPLAYER_CACHE"|cut -d'=' -f2|head -2|tail -1) # Upon consideration: #if grep -q '\.[0-9]*0$' <<<${VID[$FPS]} ; then # # Remove trailing zeroes... # VID[$FPS]=$(sed -r 's/(\.[1-9]*)0*$/\1/' <<<${VID[$FPS]}) # # ...And trailing decimal point # VID[$FPS]=$(sed 's/\.$//'<<<${VID[$FPS]}) #fi # Voodoo :P Remove (one) trailing zero if [ "${VID[$FPS]:$(( ${#VID[$FPS]} - 1 ))}" == "0" ]; then VID[$FPS]="${VID[$FPS]:0:$(( ${#VID[$FPS]} - 1 ))}" fi # Check sanity of the most important values is_number "${VID[$W]}" && is_number "${VID[$H]}" && is_number "${VID[$LEN]}" } # Main function. # Creates the contact sheet. # process($1 = file) process() { local f=$1 local numcols=$cols if [ ! -f "$f" ]; then error "File \"$f\" doesn't exist" return $EX_NOINPUT fi info "Processing $f..." identify_video "$f" || { error "Found unsupported value while identifying video. Can't continue." return $EX_SOFTWARE } # Vidcap/Thumbnail height local vidcap_height=$th_height if ! is_number "$vidcap_height" || [ "$vidcap_height" -eq 0 ]; then vidcap_height=${VID[$H]} fi if [ "0" == "$aspect_ratio" ]; then aspect_ratio=$(bc -lq <<< "${VID[$W]} / ${VID[$H]}") elif [ "-1" == "$aspect_ratio" ]; then guess_aspect fi local vidcap_width=$(compute_width $vidcap_height) local numsecs=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2 | cut -d. -f1) local nc=$numcaps # Contact sheet minimum cols: if [ $nc -lt $numcols ]; then numcols=$nc fi create_temp_dir # Compute the stamps (if in auto mode)... TIMECODES=${initial_stamps[*]} if [ $manual_mode -ne 1 ]; then compute_timecodes $timecode_from $interval $numcaps || { return $? } fi local base_montage_command="montage -font $font_tstamps -pointsize $pts_tstamps \ -gravity SouthEast -fill white " local output=$(new_temp_file '-preview.png') local VIDCAPFILE=00000001.png # If the temporal vidcap already exists, abort if [ -f $VIDCAPFILE ]; then error "Temporal vidcap file ($VIDCAPFILE) exists, remove it before running!." return $EX_CANTCREAT fi TEMPSTUFF+=( $VIDCAPFILE ) # Highlighs local hlfile="$VCSTEMPDIR/highlights.png" n=1 # Must be outside the if! if [ "$HLTIMECODES" ]; then local hlmontage_command="montage -gravity SouthEast -texture xc:LightGoldenRod " local hlcapfile= local pretty= for stamp in $(clean_timestamps "${HLTIMECODES[*]}"); do if [ $stamp -gt $numsecs ]; then let 'n++' && continue ; fi pretty=$(pretty_stamp $stamp) info "Generating highlight #${n}/${#HLTIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? apply_stamp "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || return $? hlcapfile=$(new_temp_file "-hl-$(pad 6 $n).png") mv "$VIDCAPFILE" "$hlcapfile" hlmontage_command+=" \"$hlcapfile\"" let 'n++' done #if [ "$title" ]; then # hlmontage_command+=" -font $font_heading -fill $fg_heading -title '$title'" #fi info "Composing highlights contact sheet..." eval "$hlmontage_command -geometry ${vidcap_width}x${vidcap_height}!+10+5 \ -tile ${numcols}x -shadow \"$hlfile\"" unset hlcapfile hlmontage_command pretty fi unset n # Normal captures # TODO: Don't reference $VIDCAPFILE local capfile= pretty= n=1 montage_command=$base_montage_command for stamp in $(clean_timestamps "${TIMECODES[*]}"); do pretty=$(pretty_stamp $stamp) # Note that it must be checked against numsecs and not endsec, to allow # the user manually setting stamps beyond the boundaries # This shouldn't occur automatically anymore with the new code. if [ $stamp -gt $numsecs ]; then let 'n++' && continue; fi info "Generating capture #${n}/${#TIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? apply_stamp "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || return $? capfile=$(new_temp_file "-cap-$(pad 6 $n).png") # move to tempdir/.png, cap num is padded to 6 characters mv "$VIDCAPFILE" "$capfile" montage_command+=" \"$capfile\"" let 'n++' # $n++ done unset capfile pretty n # geometry affects the source images, not the target one! # Note the file name could also be added by using the "-title" option, but I reserved # it for used set titles # FIXME: Title should go before the highlights montage_command+=" -geometry ${vidcap_width}x${vidcap_height}+10+5 -tile ${numcols}x -shadow" if [ "$title" ]; then montage_command+=" -font $font_heading -fill $fg_heading -title '$title'" fi montage_command+=" \"$output\"" info "Composing standard contact sheet..." eval $montage_command # eval is required to evaluate correctly the text in quotes! unset montage_command # Extended mode local extoutput= if [ "$extended_factor" != 0 ]; then # Number of captures. Always rounded to a multiplier of 2 # TODO: Round it to a multiplier of the number of columns local hlnc=$(bc -q <<<"( (${#TIMECODES[*]} * $extended_factor) / 2 * 2)") unset TIMECODES # required step to get the right count TIMECODES=${initial_stamps[*]} compute_timecodes $TC_NUMCAPS "" $hlnc unset hlnc local n=1 w= h= capfile= pretty= montage_command=$base_montage_command extoutput=$(new_temp_file "-extended.png") # The image size of the extra captures is 1/4 let 'w=vidcap_width/2, h=vidcap_height/2' for stamp in $(clean_timestamps "${TIMECODES[*]}"); do pretty=$(pretty_stamp $stamp) info "Generating capture from extended set: ${n}/${#TIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? apply_stamp "$VIDCAPFILE" $pretty $w $h || return $? capfile=$(new_temp_file "-excap-$(pad 6 $n).png") mv "$VIDCAPFILE" "$capfile" montage_command+=" \"$capfile\"" let 'n++' done montage_command+=" -geometry ${w}x${h}+5+2 -tile $(($numcols * 2))x -shadow" info "Composing extended contact sheet..." eval $montage_command "$extoutput" unset montage_command w h capfile pretty n fi # Codec "prettyfication" # Official FourCCs: # Unofficial list: # Another software with a list of fourccs -> name mappings: # local vcodec= acodec= case $( tr '[A-Z]' '[a-z]' <<<${VID[$VCODEC]}) in xvid) vcodec=Xvid ;; dx50) vcodec="DivX 5" ;; 0x10000001) vcodec="MPEG-1" ;; 0x10000002) vcodec="MPEG-2" ;; 0x00000000) vcodec="Raw RGB" ;; # How correct is this? avc1) vcodec="MPEG-4 AVC" ;; wmv1) vcodec="WMV7" ;; wmv2) vcodec="WMV8" ;; wmv3) vcodec="WMV9" ;; fmp4) vcodec="FFmpeg" ;; # XXX: Would LAVC be a better name? mp42) vcodec="MS MPEG-4 v2" ;; mpg4) vcodec="MS MPEG-4 v1" ;; mp43) vcodec="MS MPEG-4 v3" ;; div3) vcodec="DivX ;) Low Motion" ;; # Technically same as mp43 i420) vcodec="Raw I420" ;; # XXX: 420 presumably stands by 4:2:0 ? rv10) vcodec="RealVideo 1.0/5.0" ;; rv20) vcodec="RealVideo G2" ;; svq1) vcodec="Sorenson Video 1" ;; svq3) vcodec="Sorenson Video 3" ;; # These are known FourCCs that I haven't tested against so far wmva) vcodec="WMV9 Advanced Profile" ;; # Not VC1 compliant. Unsupported. rv30) vcodec="RealVideo 8" ;; rv40) vcodec="RealVideo 9/10" ;; div4) vcodec="DivX ;) Fast Motion" ;; divx) vcodec="DivX" ;; # OpenDivX / DivX 5(?) iv50) vcodec="Indeo 5.0" ;; mjpg) vcodec="M-JPEG" ;; # XXX: Actually mJPG != MJPG # Legacy(-er) codecs (haven't seen files in these formats in awhile) iv32) vcodec="Indeo 3.2" ;; msvc) vcodec="Microsoft Video 1" ;; mrle) vcodec="Microsoft RLE" ;; *) # If not recognized show FOURCC vcodec=${VID[$VCODEC]} ;; esac if [ "${VID[$VDEC]}" == "ffodivx" ]; then vcodec+=" (MPEG-4)" elif [ "${VID[$VDEC]}" == "ffh264" ]; then vcodec+=" (h.264)" fi case $( tr '[A-Z]' '[a-z]' <<<${VID[$ACODEC]} ) in 85) acodec='MPEG Layer III (MP3)' ;; 80) acodec='MPEG Layer I/II (MP1/MP2)' ;; # Apparently they use the same tag mp4a) acodec='MPEG-4 AAC' ;; # LC and HE, apparently 352) acodec='WMA7' ;; # =WMA1 353) acodec='WMA8' ;; # =WMA2 No idea if lossless can be detected 354) acodec='WMA9' ;; # =WMA3 8192) acodec='AC3' ;; 1|65534) # 1 is standard PCM (apparently all sample sizes) # 65534 seems to be multichannel PCM acodec='Linear PCM' ;; vrbs|22127) # 22127 = Vorbis in AVI (with ffmpeg) DON'T! # vrbs = Vorbis in Matroska, probably other sane containers acodec='Vorbis' ;; qdm2) acodec="QDesign" ;; "") acodec="no audio" ;; # Following not seen by me so far, don't even know if mplayer would # identify them # 355) acodec="WMA9 Lossless" ;; 10) acodec="WMA9 Voice" ;; *) # If not recognized show audio id tag acodec=${VID[$ACODEC]} ;; esac if [ "${VID[$CHANS]}" ] && is_number "${VID[$CHANS]}" &&[ ${VID[$CHANS]} -ne 2 ]; then if [ ${VID[$CHANS]} -eq 0 ]; then # This happens e.g. in non-i386 when playing WMA9 at the time of # this writing warn "Detected 0 audio channels." warn " Does this version of mplayer support the audio codec ($acodec)?" elif [ ${VID[$CHANS]} -eq 1 ]; then acodec+=" (mono)" else acodec+=" (${VID[$CHANS]}ch)" fi fi local meta="Filename: $(basename "$f") File size: $(get_pretty_size "$f") Length: $(pretty_stamp "${VID[$LEN]}")" local meta2="Dimensions: ${VID[$W]}x${VID[$H]} Format: $vcodec / $acodec FPS: ${VID[$FPS]}" local signature="$user_signature $user $PROGRAM_SIGNATURE" unset acodec vcodec if [ "$HLTIMECODES" ] || [ "$extended_factor" != "0" ]; then info "Merging contact sheets..." fi # If there were highlights then mix them in if [ "$HLTIMECODES" ]; then #\( -geometry x2 xc:black -background black \) # This breaks it! convert \( "$hlfile" -background LightGoldenRod \) \ \( "$output" \) -append "$output" fi # Extended captures if [ "$extended_factor" != 0 ]; then convert "$output" "$extoutput" -append "$output" fi info "Creating header and footer..." # Now let's add meta info # This one enlarges the image to add the text, and puts # meta info in two columns convert -font $font_heading -pointsize $pts_meta \ -background $bg_heading -fill $fg_heading -splice 0x$(( $pts_meta * 4 )) \ -gravity NorthWest -draw "text 10,10 '$meta'" \ -gravity NorthEast -draw "text 10,10 '$meta2'" \ "$output" "$output" # Finishing touch, signature convert -gravity South -font $font_sign -pointsize $pts_sign \ -background $bg_sign -splice 0x34+0-0 \ -fill $fg_sign -draw "text 10,3 '$signature'" "$output" "$output" if [ $output_format != "png" ]; then local newout="$(dirname "$output")/$(basename "$output" .png).$output_format" convert -quality $output_quality "$output" "$newout" output="$newout" fi output_name=$( safe_rename "$output" "$(basename "$f").$output_format" ) || { error "Failed to write the output file!" return $EX_CANTCREAT } info "Done. Output wrote to $output_name" cleanup } # Prints the program identification to stderr show_vcs_info() { # Won't be printed in quiet modes info "Video Contact Sheet *NIX v${VERSION}, (c) 2007 Toni Corvera" "sgr0" } # Prints the list of options to stdout show_help() { local P=$(basename $0) cat < Options: -i|--interval Set the interval to arg. Units can be used (case-insensitive), i.e.: Seconds: 90 or 90s Minutes: 3m Hours: 1h Combined: 1h3m90 Use either -i or -n. -n|--numcaps Set the number of captured images to arg. Use either -i or -n. -f|--from Set starting time. No caps before this. Same format as -i. -t|--to Set ending time. No caps beyond this. Same format as -i. -T|--title Add a title above the vidcaps. -u|--user Set the username found in the signature to this. -U|--fullname Use user's full/real name (e.g. John Smith) as found in /etc/passwd. -S|--stamp Add the image found at the timestamp "arg". Same format as -i. -l|--highlight Add the image found at the timestamp "arg" as a highlight. Same format as -i. -e[arg] | --extended=[arg] Enables extended mode and optionally sets the extended factor. By default it's $DEFAULT_EXT_FACTOR. -m|--manual Manual mode: Only timestamps indicated by the user are used (use in conjunction with -S), when using this -i and -n are ignored. -H|--height Set the output (individual thumbnail) height. Width is derived accordingly. Note width cannot be manually set. -a|--aspect Aspect ration. Accepts floating point number or fractions. -A|--autoaspect Try to guess aspect ratio from resolution. -j|--jpeg Output in jpeg (by default output is in png). -q|--quiet Don't print progess messages just errors. Repeat to mute completely even on error. -h|--help Show this text. Options used for debugging purposes or to tweak the internal workings: -M|--mplayer Force the usage of mplayer. -F|--ffmpeg Force the usage of ffmpeg. --shoehorn Pass "arg" to mplayer/ffmpeg. You shouldn't need it. -D Debug mode: Currently just prints the parsed commandline as the title and to stderr. Examples: Create a contact sheet with default values (vidcaps at intervals of $DEFAULT_INTERVAL seconds), the resulting file will be called input.avi.png: \$ $P input.avi Create a sheet with vidcaps at intervals of 3 and a half minutes: \$ $P -i 3m30 input.avi Create a sheet with vidcaps starting at 3 mins and ending at 18 mins, add an extra vidcap at 2m and another one at 19m: \$ $P -f 3m -t 18m -S2m -S 19m input.avi See more examples at vcs' homepage . EOF } # }}} # Core functionality #### Execution starts here #### # Execute exithdlr on exit trap exithdlr EXIT show_vcs_info load_config # {{{ # Command line parsing # Based on getopt-parse.bash example. # On debian systems see # TODO: use no name at all with -u noarg #eval set -- "${default_options} ${@}" TEMP=$(getopt -s bash -o i:n:u:T:f:t:S:jhFMH:c:ma:l:De::UqAO: \ --long "interval:,numcaps:,username:,title:,from:,to:,stamp:,jpeg,help,"\ "shoehorn:,mplayer,ffmpeg,height:,columns:,manual,aspect:,highlight:,"\ "extended::,fullname,quiet,autoaspect,override:" \ -n $0 -- "$@") eval set -- "$TEMP" while true ; do case "$1" in -i|--interval) if ! interval=$(get_interval "$2") ; then error "Interval must be a (positive) number. Got '$2'." exit $EX_USAGE fi if [ "$interval" -le 0 ]; then error "Interval must be higher than 0, set to the default $DEFAULT_INTERVAL" interval=$DEFAULT_INTERVAL fi timecode_from=$TC_INTERVAL shift # Option arg ;; -n|--numcaps) if ! is_number "$2" ; then error "Number of captures must be (positive) a number! Got '$2'." exit $EX_USAGE fi if [ $2 -eq 0 ]; then error "Number of captures must be greater than 0! Got '$2'." exit $EX_USAGE fi numcaps="$2" timecode_from=$TC_NUMCAPS shift # Option arg ;; -u|--username) user="$2" ; shift ;; -U|--fullname) user=$(grep ^$(id -un): /etc/passwd | cut -d':' -f5 |sed 's/,.*//g') if [ -z "$user" ]; then user=$(id -un) error "No fullname found, falling back to default ($user)" fi ;; -T|--title) title="$2" ; shift ;; -f|--from) if ! fromtime=$(get_interval "$2") ; then error "Starting timestamp must be a valid timecode. Got '$2'." exit $EX_USAGE fi shift ;; -t|--to) if ! totime=$(get_interval "$2") ; then error "Ending timestamp must be a valid timecode. Got '$2'." exit $EX_USAGE fi if [ "$totime" -eq 0 ]; then error "Ending timestamp was set to 0, set to movie length." totime=-1 fi shift ;; -S|--stamp) if ! temp=$(get_interval "$2") ; then error "Timestamps must be a valid timecode. Got '$2'." exit $EX_USAGE fi initial_stamps=( ${initial_stamps[*]} $temp ) shift ;; -l|--highlight) if ! temp=$(get_interval "$2"); then error "Timestamps must be a valid timecode. Got '$2'." exit $EX_USAGE fi HLTIMECODES+=( $temp ) shift ;; -j|--jpeg) output_format=jpg ;; -h|--help) show_help ; exit $EX_OK ;; --shoehorn) shoehorned="$2" shift ;; -F) decoder=$DEC_FFMPEG ;; -M) decoder=$DEC_MPLAYER ;; -H|--height) if ! is_number "$2" ; then error "Height must be a (positive) number. Got '$2'." exit $EX_USAGE fi th_height="$2" shift ;; -a|--aspect) if ! is_float "$2" && ! is_fraction "$2" ; then error "Aspect ratio must be expressed as a (positive) floating " error " point number or a fraction (ie: 1, 1.33, 4/3, 2.5). Got '$2'." exit $EX_USAGE fi aspect_ratio="$2" shift ;; -A|--autoaspect) aspect_ratio=-1 ;; -c|--columns) if ! is_number "$2" ; then error "Columns must be a (positive) number. Got '$2'." exit $EX_USAGE fi cols="$2" shift ;; -m|--manual) manual_mode=1 ;; -D) echo "Command line: $0 $*" && title="$0 $*" ; ;; -e|--extended) # Optional argument quirks: $2 is always present, set to '' if unused # from the commandline it MUST be directly after the -e (-e2 not -e 2) # the long format is --extended=VAL # XXX: For some reason parsing of floats gives an error, so for now # ints and only fractions are allowed if [ "$2" ] && ! is_float "$2" && ! is_fraction "$2" ; then error "Extended multiplier must be a (positive) number (integer, float "\ "or fraction)." error " Got '$2'." exit $EX_USAGE fi if [ "$2" ]; then extended_factor="$2" else extended_factor=$DEFAULT_EXT_FACTOR fi shift ;; -O|--override) # Rough test if ! egrep -q '[a-zA-Z_]+=[^;]*' <<<"$2"; then error "Wrong override format, it should be variable=value. Got '$2'." exit $EX_USAGE fi override "$2" "command line" shift ;; -q|--quiet) # -q to only show errors # -qq to be completely quiet if [ $verbosity -gt $V_ERROR ]; then verbosity=$V_ERROR else verbosity=$V_NONE fi ;; --) shift ; break ;; *) error "Internal error! (remaining opts: $@)" ; exit $EX_SOFTWARE ; esac shift done # Remaining arguments if [ ! "$1" ]; then show_help exit $EX_USAGE fi # Test requirements test_programs || exit $EX_UNAVAILABLE # If -m is used then -S must be used if [ $manual_mode -eq 1 ] && [ -z $initial_stamps ]; then error "You must provide timestamps (-S) when using manual mode (-m)" exit $EX_USAGE fi set +e # Don't fail automatically for arg do process "$arg" ; done # }}} # Command line parsing # vim:set ts=4 ai: #