0,0 → 1,4030 |
#!/usr/bin/env bash |
# |
# $Rev$ $Date$ |
# |
# vcs |
# Video Contact Sheet *NIX: Generates contact sheets (previews) of videos |
# |
# Copyright (C) 2007, 2008, 2009, 2010 Toni Corvera |
# with patches from Phil Grundig and suggestions/corrections from |
# many others (see homepage) |
# |
# 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 <outlyer@gmail.com> |
# |
# (Note: The references that used to be here have been moved to |
#+ <http://p.outlyer.net/dox/vcs:devel:references>) |
|
declare -r VERSION="1.11" |
declare -r RELEASE=1 |
|
# {{{ # CHANGELOG |
# Last release changes: |
# (The full changelog can be found at <http://p.outlyer.net/vcs/files/CHANGELOG>). |
# |
# 1.11: |
# * FEAT: Allow setting output filename. With extension will set output |
# format, without will inherit it. |
# * FEAT: Require mplayer OR ffmpeg instead of both. Having both is still |
# recommended for better results. |
# * FEAT: Safe mode, for files whose length doesn't get reported correctly. |
# Completely automated. |
# Number of tries can be increased with -Ws. Repeat to increase |
# further. Use -WS to do try as many times as possible. |
# Accuracy (stepping) can be increased with -Wp. Repeat to increase |
# accuracy. Decrease with -WP. |
# Can be deliberately disabled with -Wb to force processing of |
# broken files. *VCS WITH -Wb WILL FAIL ON BROKEN FILES* |
# * FEAT: Allow percentages in height. |
# * BUGFIX: Don't pass ms to mplayer. It ignores them anyway and in some |
# rare cases breaks the last capture (possibly due to the 5-frames |
# hack) |
# * BUGFIX: Honor detected aspect ratio if found |
# * BUGFIX: Try to detect files that might fail on the last capture and |
# trigger safe mode |
# * BUGFIX: Timestamps font was being ignored. As a side effect this produced |
# italiced timestamps in some systems. |
# * BUGFIX: Fixed obscure bug with safe_rename_pattern overrides |
# * COMPAT: Support for bash 2.05b. This will (probably) be the last version |
# capable of running under bash 2. |
# * DVD mode revamp |
# - Print title file size instead of disc size when possible |
# - Aspect ratio detection, if available |
# - Use of FFmpeg if available to get better information |
# - Mostly x-platform, only ISOs identification is a bit better in Linux |
# * Added FourCCs: 3IV1, 3IV2 (3ivx); s263 (H.263); mp4v, MP4V, H264 |
# (MPEG-4 and AVC in mov/mp4), VP6F (VP6 Flash Version), AMR |
# Video codec renamings: |
# - TechSmith codec name shortened to TechSmith SCC |
# - Raw RGB renamed to Raw video |
# * Help cleanup. The default help output is much shorter, the full text |
# can be displayed with --fullhelp. Also print the decoder choice near |
# the appropriate option (-M/-F) |
# * Added --anonymous to help (never was in it) |
# * Drop requirement on seq/jot and bc, replaced by inline awk |
# ... New requirement: Perl. It's required by POSIX anyway |
# * Adopt new/fixed numbering scheme |
# <http://p.outlyer.net/dox/vcs:devel:renumbering> |
# * Check ImageMagick version (must decide which is the real minimum |
# required) |
# * Non-latin fonts revamp: |
# - -I no longer works alone (use -Ij or -Ik instead) |
# - -Ik, -Ij and --nonlatin try to pick an appropriate font automatically |
# - -I accepts a font name or font filename like |
# -Ij=Kochi-Mincho-Regular or |
# -Ij=/usr/share/fonts/truetype/kochi/kochi-mincho.ttf |
# * Deprecated options: |
# --shoehorn: Will be removed unless someone really needs it. |
# --mincho: Replaced by --nonlatin |
# * COSMETIC: |
# - Default font switched to DejaVu Sans. |
# Font sizes reduced to accomodate the new default. |
# Should fall back to a sane default if it's not available |
# - Much tighter padding |
# - Smaller timestamps font by default |
# - Print friendlier timestamp when a capture fails |
# - Print program signature to console without colour |
# - Use main font by default in timestamps |
# - Heading background colour toned down |
# - Added colourised output when tput is not capable (i.e. FreeBSD) |
# - Added prefixes when colour is not available for console output |
# - Don't print lsdvd error channel is DVD mode |
# - Suppress mv errors (e.g. over VFS being unable to preserve) |
# * UNDOCUMENTED/DEBUG: |
# - Allow stopping the main loop before cleaning up (--undocumented hang) |
# - Identification-only mode. Might be promoted to an actual feature |
# (--undocumented idonly) |
# - Allow setting ffmpeg and mplayer path (--undocumented set_ffmpeg and |
# set_mplayer) |
# - Allow disabling either mplayer of ffmpeg (as if they weren't |
# installed (--undocumented disable_ffmpeg and disable_mplayer) |
# - Added -Wc to disable console colour, repeat to disable prefixes |
# * INTERNAL: assert() |
# * INTERNAL: Cleanup: correctness checks converted to asserts, removal |
# of old dead code, fixed typos in comments |
# * Fixed various typos and cut out lines in CHANGELOG |
# * Minimum ImageMagick version set to 6.3.5-7 |
# * Better detection of requirements (e.g. disallow decoders without png |
# support) |
# * Allow overriding height, number of captures, interval, columns, and |
# padding |
# * Added -dp (--disable padding) equivalent to overriding HPAD to 0 |
# }}} # CHANGELOG |
|
set -e |
|
# Fail soon if this version of bash is too old for the syntax, don't expose bash to the newer |
# syntax |
# See the "Bash syntax notes" section for details |
[ "$BASH_VERSINFO" ] && { |
# Absolute minimum right now is 2.05b |
if [ "${BASH_VERSINFO[0]}" -le 2 ]; then # Only bash <=2 needs extra testing |
# I guess we can't expect any new bash2 release |
if [ "${BASH_VERSINFO[0]}" -lt 2 ] || |
( [ "${BASH_VERSINFO[0]}" -eq 2 ] && [ "${BASH_VERSINFO[1]}" != '05b' ] ); then |
echo "Bash 2.05b or higher is required" >&2 |
exit 1 |
fi |
fi |
} |
|
# {{{ # TO-DO |
# TODO / FIXME: |
# * (1.12) Start replacing 'grep' with bash's '[[ =~ ]]'. Will break bash 2 compatibility for good |
# * [[x1]] Find out why the order of ffmpeg arguments breaks some files. |
# * [[x2]] Find out if egrep is safe to use or grep -E is more commonplace. => |
#+ SUS v2: egrep is deprecated, grep -E replaces it |
# * Change default DVD_TITLE to 0 |
# * Deprecations: |
# OPTION/VAR -> ALTERNATIVE DEPRECATED FROM VERSION REMOVAL ETA |
# --shoehorn -> --undocumented shoehorn 1.11 1.12 |
# --undocumented shoehorn -> NONE 1.12 1.13 or 1.14 |
# --funky -> --profile ? ?+1 |
# MIN_LENGTH_FOR_END_OFFSET -> none 1.11 1.12 or 1.13 |
# * No warning shown in 1.11. 1.12 will switch back default end offset either to 0 or a |
# percentage *and* use it always (disable with -E0) |
# end offset -> none ? ?+1 |
# --mincho -> --nonlatin 1.11 1.12 |
# * Better check for coherence of overrides |
# * Variables cleanup: |
# Variables will use a more uniform scheme, with prefixes where appropriate: |
# - INTERNAL_*: Used internally to adapt messages and the like to the input |
# - UNDFLAG_*: Undocumented flags. Used internally to keep track of undocumented modes (-Z) |
# - USR_*: Holds values of variables as set by the user, either from overrides or from the |
# command-line |
# }}} # TO-DO |
|
# {{{ # Constants |
|
# 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 config, most precedence |
# |
# The variables that can be overriden are below the block of constants ahead. |
declare -r CFGFILE=~/.vcs.conf |
|
# 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="Video Contact Sheet *NIX ${VERSION} <http://p.outlyer.net/vcs/>" |
# 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 |
# see $font_filename and $FONT_MINCHO |
declare -ri FF_DEFAULT=5 FF_MINCHO=7 |
# Indexes in $VID |
declare -ri W=0 H=1 FPS=2 LEN=3 VCODEC=4 ACODEC=5 VDEC=6 CHANS=7 ASPECT=8 VCNAME=9 ACNAME=10 |
# Exit codes, same numbers 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 |
# The context allows the creator to identify which contact sheet it is creating |
# (CTX_*) HL: Highlight (-l), STD: Normal, EXT: Extended (-e) |
declare -ri CTX_HL=1 CTX_STD=2 CTX_EXT=3 |
|
# }}} # End of constants |
|
# {{{ # Override-able variables |
# GETOPT must be correctly set or the script will fail. |
# It can be set in the configuration files if it isn't in the path or |
# the first getopt in the path isn't the right version. |
# A check will be made and a warning with details shown if required. |
declare GETOPT=getopt |
# Set to 1 to print function calls |
declare -i DEBUG=0 |
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='#afcd7a' # Background for meta info (size, codec...) |
declare bg_sign=SlateGray #'#a2a9af' # Background for signature |
declare bg_title=White # Background for the title (see -T) |
declare bg_contact=White # Background for the captures |
declare bg_tstamps='#000000aa' # Background for the timestamps box |
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 |
declare fg_title=Black # Font colour for the title |
# Fonts, use identify -list font to get the list, up to IM 6.3.5-7 was '-list type' [[IM1]] |
# If a font is not available IM will pick a sane default. In theory it will be silent |
# although in practica it prints an error |
declare font_tstamps=DejaVu-Sans-Book # Used for timestamps over the thumbnails |
declare font_heading=DejaVu-Sans-Book # Used for the meta info heading |
declare font_sign=$font_heading # Used for the signature box |
# Unlike other font_ variables this doesn't take a font name directly |
# but is restricted to the $FF_ values. This is to allow overrides |
# from the command line to be placed anywhere, i.e. in |
# $ vcs -I file.avi -O 'FONT_MINCHO=whatever' |
# as the font is overridden is after requesting its use, it wouldn't be |
# affected |
# The other font_ variables are only affected by overrides and not command |
# line options that's why this one is special. |
declare font_filename=$FF_DEFAULT # Used to print only the filename in the heading |
declare font_title=$font_heading # Used for the title (see -T) |
# Font sizes, in points |
declare -i pts_tstamps=14 # Used for the timestamps |
declare -i pts_meta=14 # Used for the meta info heading |
declare -i pts_sign=10 # Used for the signature |
declare -i pts_title=33 # Used for the title (see -T) |
# See --shoehorn |
declare shoehorned= |
# See -E / $end_offset |
declare -i DEFAULT_END_OFFSET=60 |
# If the video is less than this length, end offset won't be used at all |
declare MIN_LENGTH_FOR_END_OFFSET=19m30s |
# 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 |
# Verbosity level so far from the command line can only be muted (see -q) |
# it can be overridden, though |
declare -i verbosity=$V_ALL |
# Set to 1 to disable colours in console output |
declare -i plain_messages=0 |
# See coherence_check for more details |
declare -i DISABLE_SHADOWS=0 |
declare -i DISABLE_TIMESTAMPS=0 |
|
# Introduced in 1.0.7b, revamped in 1.11: |
# This font is used to display international names (i.e. CJK names) correctly |
# Help from users actually needing this would be appreciated :) |
# This variable is filled either automatically through the set_extended_font() |
#+function (and option -Ij) or manually (with option -Ij=MyFontName) |
# The automatic picks a semi-random one from the fonts believed to support CJK/Cyrillic |
#+characters. |
declare FONT_MINCHO= # Filename or font name as known to ImageMagick (identify -list font) |
# Output of capturing programs is redirected here |
declare stdout=/dev/null stderr=/dev/null |
declare -i DVD_MODE=0 DVD_TITLE=1 |
declare DVD_FILE= |
|
# Override-able since 1.11: |
# Height of the thumbnails, by default use same as input |
declare th_height= # *WILL CHANGE NAME* |
declare interval=$DEFAULT_INTERVAL # Interval of captures (=numsecs/numcaps) |
declare -i numcaps=$DEFAULT_NUMCAPS # Number of captures (=numsecs/interval) |
# This is the horizontal padding added to each capture. |
# Beware when changing this since extended set's alignment might break. |
# When shadows are enabled this is ignored since they already add padding. |
declare -i HPAD=2 # *WILL CHANGE NAME* |
declare -i cols=$DEFAULT_COLS # Number of output columns |
|
# }}} # End of override-able variables |
|
# {{{ # Variables |
|
# Options and other internal usage variables, no need to mess with this! |
declare title="" |
declare fromtime=0 # Starting second (see -f) |
declare totime=-1 # Ending second (see -t) |
declare -a initial_stamps # Manually added stamps (see -S) |
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 |
# Identification output from ffmpeg and mplayer for the current video |
declare FFMPEG_CACHE= |
declare MPLAYER_CACHE= |
# This holds the parsed identification values, see also the Indexes in VID |
# (defined in the constants block) |
declare -a VID=( ) |
|
# These variables will hold the output of tput, used |
# to colourise feedback |
declare prefix_err= prefix_inf= prefix_warn= suffix_fback= |
|
# Workarounds: |
# Argument order in FFmpeg is important -ss before or after -i will make |
# the capture work or not depending on the file. See -Wo. |
# TODO: [x1]. |
# Admittedly the workaraound is abit obscure: those variables will be added to |
# the ffmpeg arguments, before and after -i, replacing spaces by the timestamp. |
# e.g.: for second 50 '-ss ' will become '-ss 50' while '' will stay empty |
# By default -ss goes before -i. |
declare wa_ss_af="" wa_ss_be="-ss " |
# This number of seconds is *not* captured from the end of the video |
declare -i end_offset=$DEFAULT_END_OFFSET |
|
# Transformations/filters |
# Operations are decomposed into independent optional steps, this allows |
# to add some intermediate steps (e.g. polaroid/photo mode's frames) |
# Filters in this context are functions. |
# There're two kinds of filters and a delegate: |
# * individual filters are run over each vidcap |
# * global filters are run over all vidcaps at once (currently deprecated) |
# * The contact sheet creator delegates on some function to create the actual |
# contact sheet |
# |
# Individual filters take the form: |
# filt_name( vidcapfile, timestamp in seconds.milliseconds, width, height, [context, [index]] ) |
# They're executed in order by filter_vidcap() |
declare -a FILTERS_IND=( 'filt_resize' 'filt_apply_stamp' 'filt_softshadow' ) |
# Deprecated: Global filters take the form |
# filtall_name( vidcapfile1, vidcapfile2, ... ) |
# They're executed in order by filter_all_vidcaps |
declare -a FILTERS_CS |
# The contact sheet creators take the form |
# csheet_name( number of columns, context, width, height, vidcapfile1, |
# vidcapfile2, ... ) : outputfile |
# Context is one of the CTX_* constants (see below) |
# The width and height are those of an individual capture |
# It is executed by create_contact_sheet() |
declare CSHEET_DELEGATE=csheet_montage |
|
# Gravity of the timestamp (will be override-able in the future) |
declare grav_timestamp=SouthEast |
|
# When set to 1 the signature won't contain the "Preview created by..." line |
declare -i anonymous_mode=0 |
|
# Sets which function is used to obtain random numbers valid values are |
# bashrand and filerand. |
# Setting it manually will break it, calling with -R changes this to filerand. |
# See rand() for an explanation |
declare RANDFUNCTION=bashrand |
|
# Which file are we working on (i.e. how many times has process() been called) |
declare -i FILEIDX=0 |
|
# Names for output files, each index is a file name, an empty index will use |
# the input file and append an extension to it |
declare -a OUTPUT_FILES=( ) |
|
# Mplayer and FFmpeg binaries. Will be detected. |
# Don't set manually, if you need to override set the path temporarily, e.g.: |
# $ env PATH=/whatever:$PATH vcs ... |
# or use the undocumented (and unchecked!) appropriate option: |
# $ vcs --undocumented set_ffmpeg=/mypath/ffmpeg |
declare MPLAYER= |
declare FFMPEG= |
|
# When set to 1 the reported length by mplayer and ffmpeg won't be trusted |
# and will trigger some custom tests. |
# Enabled automatically on problematic files |
declare -i QUIRKS=0 |
# If the reported lengths differ by at least this much QUIRKS will be enabled |
declare QUIRKS_LEN_THRESHOLD=0.2 |
# When trying to determine the correct length, file will be probed each...: |
declare QUIRKS_LEN_STEP=0.5 # ~ 10 frames @ 20fps |
# Maximum number of seconds to "rewind" from reported length (after this |
# vcs surrenders but processing continues with a rewinded length) |
declare QUIRKS_MAX_REWIND=20 |
|
# Set when the console output will be in color. It doesn't control color! |
declare HAS_COLORS= |
|
declare -i multiple_input_files=0 |
|
# Internal counts, used only to adjust messages |
declare -i INTERNAL_WS_C=0 # -Ws count |
declare -i INTERNAL_WP_C=0 # -Wp count |
declare -i INTERNAL_MAXREWIND_REACHED=0 # More -Ws in the command-line won't help |
|
# }}} # Variables |
|
# {{{ # Configuration handling |
|
# 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' |
'FONT_MINCHO' |
'stdout' |
'stderr' |
'DEFAULT_END_OFFSET' |
'MIN_LENGTH_FOR_END_OFFSET' |
'DEBUG' |
'DISABLE_.*' |
'th_height' |
'interval' |
'numcaps' |
'HPAD' |
'cols' |
# Note GETOPT doesn't make sense to be overridden from the command-line |
'GETOPT' |
) |
|
# This is only used to exit when -DD is used |
declare -i DEBUGGED=0 # It will be 1 after using -D |
|
# Loads the configuration files if present |
# load_config() |
load_config() { |
local -a 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 |
|
end_offset=$DEFAULT_END_OFFSET |
} |
|
# 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=${ALLOWED_OVERRIDES[*]} |
compregex=${compregex// /|} # = s ' ' => '|' |
|
# Don't allow ';', FIXME: dunno how secure that really is... |
# FIXME: ...it doesn't really work 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=$(egrep -o '^[[:space:]]*[a-zA-Z0-9_]*=.' <<<"$o" | cut -d'=' -f1 | egrep -o '[^ ]*') |
local varval=$(egrep -o '^[^=]*=.*' <<<"$o" | cut -d'=' -f2-) |
# FIXME: Security! |
local curvarval= |
eval curvarval='$'"$varname" |
if [ "$curvarval" == "$varval" ]; then |
warn "Ignored override '$varname' (already had same value)" |
else |
eval "$varname=\"$varval\"" |
eval "USR_$varname=\"$varval\"" |
# FIXME: Only for really overridden ones |
warn "Overridden variable '$varname' from $src" |
fi |
} |
|
# }}} # Configuration handling |
|
# {{{ # 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 is a valid percentage (xx% or xx.yy%) |
# is_percentage($1 = input) |
is_percentage() { |
egrep -q '^([0-9]+\.?([0-9])?+|(\.[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 (*strictly*, i.e. "1" is not a fraction) |
# Only accepts XX/YY |
# is_fraction($1 = input) |
is_fraction() { |
egrep -q '^[0-9]+/[0-9]+$'<<<"$1" |
} |
|
# Makes a string lowercase |
# tolower($1 = string) |
tolower() { |
tr '[A-Z]' '[a-z]' <<<"$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() { |
awkex "int(${*//[ ,]/ * }+0.5)" # ' ' = ',' => '*' |
} |
|
# Like rmultiply() but always rounded upwards |
ceilmultiply() { |
# TODO: breaks with $@. Why? |
awkex "int(${*//[ ,]/ * }+0.99999)" # ' ' = ',' => '*' |
} |
|
# Basic mathematic stuff |
# min($1 = operand1, $2 = operand2) |
min() { awk "BEGIN { if (($1) < ($2)) print ($1) ; else print ($2) }" ; } |
max() { awk "BEGIN { if (($1) > ($2)) print ($1) ; else print ($2) }" ; } |
abs() { awk "BEGIN { if (($1) < (0)) print (($1) * -1) ; else print ($1) }" ; } |
|
# Round to a multiple |
# Rounds a number ($1) to a multiple of ($2) |
# rtomult($1 = number, $2 = divisor) |
rtomult() { |
local n=$1 d=$2 |
local r=$(( $n % $d )) |
if [ $r -ne 0 ]; then |
let 'n += ( d - r )' |
fi |
echo $n |
} |
|
# numeric test eqivalent for floating point |
# fptest($1 = op1, $2 = operator, $3 = op2) |
fptest() { |
local op= |
case $2 in |
-gt) op='>' ;; |
-lt) op='<' ;; |
-ge) op='>=' ;; |
-le) op='<=' ;; |
-eq) op='==' ;; |
-ne) op='!=' ;; |
*) assert $LINENO "[ \"'$1' '$2' '$3'\" ] && false" && return $EX_SOFTWARE |
esac |
# Empty operands |
if [ -z "$1" ] || [ -z "$3" ]; then |
assert $LINENO "[ \"'$1' '$2' '$3'\" ] && false" |
else |
awk "BEGIN { if ($1 $op $3) exit 0 ; else exit 1 }" |
fi |
} |
|
# Keep a number of decimals *rounded* |
keepdecimals() { |
local N="$1" D="$2" |
awk "BEGIN { printf \"%.${D}f\", (($N)+0) }" |
} |
|
# Keep a number of decimals, last decimal rounded to lower |
keepdecimals_lower() { |
grep -q '\.' <<<"$1" || { echo "$1" ; return ; } |
local D=${1/#*.} # Decimals only |
echo ${1/%.*}.${D:0:$2} # Integer part + . + Number of decimals |
} |
|
# Evaluate in AWK. Intended for arithmetic operations. |
#+Keep decimals. I.e. 5 = 5.000000... |
# awkex($1 = expression) |
awkexf() { |
# By default awk prints in compact form (scientific notation and/or up to 6 digits/decimals), |
# printf is used to avoid this, TODO: Is there any direct way? |
# .%20f is clearly overkill but matches the all code (default bc -l) |
awk "BEGIN { printf \"%.20f\", ($1)+0 }" |
} |
|
# Evaluate in AWK. Intended for arithmetic operations. |
#+Use default output. I.e. 5 = 5 |
# awkex($1 = expression) |
awkex() { |
awk "BEGIN { print ($1)+0 }" |
} |
|
# converts spaces to newlines in a x-platform way [[FNL]] |
# stonl($1 = string) |
stonl() { |
awk '{gsub(" ", "\n");print}' <<<"$1" | egrep -v '^$' |
} |
|
# Converts newlines to spaces portably |
# nltos($1 = string) |
nltos() { |
awk '{printf "%s ",$0}' <<<"$1" | sed 's/ *//' |
} |
|
# bash version of ord() [[ORD]] |
# prints the ASCII value of a character |
ord() { |
printf '%d' "'$1" |
} |
|
# Get file extension |
filext() { |
grep -o '\.[^.]*$' <<<"$1" | cut -d. -f2 |
} |
|
# Wrapper around $RANDOM, not called directly, wrapped again in rand(). |
# See rand() for an explanation. |
bashrand() { |
echo $RANDOM |
} |
|
# Prepares for "filerand()" calls |
# File descriptor 7 is used to keep a file open, from which data is read |
# and then transformed into a number. |
# init_filerand($1 = filename) |
init_filerand() { # [[FD1]], [[FD2]] |
test -r "$1" |
exec 7<"$1" |
# closed in exithdlr |
} |
|
# Produce a (not-really-)random number from a file, not called directly wrapped |
# in rand() |
# Note that once the file end is reached, the random values will always |
# be the same (hash_string result for an empty string) |
filerand() { |
local b= |
# "read 5 bytes from file descriptor 7 and put them in $b" |
read -n5 -u7 b |
hash_string "$b" |
} |
|
# Produce a random number |
# $RANDFUNCTION defines wich one to use (bashrand or filerand). |
# Since functions using random values are most often run in subshells |
# setting $RANDOM to a given seed has not the desired effect. |
# filerand() is used to that effect; it keeps a file open from which bytes |
# are read and not-so-random values generated; since file descriptors are |
# inherited, subshells will "advance" the random sequence. |
# Argument -R enables the filerand() function |
rand() { |
$RANDFUNCTION |
} |
|
# produces a numeric value from a string |
hash_string() { |
local HASH_LIMIT=65536 |
local v="$1" |
local -i hv=15031 |
local c= |
if [ "$v" ]; then |
for i in $(seqr 0 ${#v} ); do |
c=$( ord ${v:$i:1} ) |
hv=$(( ( ( $hv << 1 ) + $c ) % $HASH_LIMIT )) |
done |
fi |
echo $hv |
} |
|
# Applies the Pythagorean Theorem |
# pyth_th($1 = cathetus1, $2 = cathetus2) |
pyth_th() { |
awkex "sqrt($1 ** 2 + $2 ** 2)" |
} |
|
# 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() { |
trace $FUNCNAME $@ |
# eval it even if it's numeric to strip leading zeroes. Note the quoting |
if is_number "$1" ; then awkex "\"$1\"" ; return 0 ; fi |
|
local s=$(tolower "$1") t r n |
|
# Only allowed characters |
if ! egrep -qi '^[0-9smh.]+$' <<<"$s"; then |
return $EX_USAGE; |
fi |
|
# Two consecutive dots are no longer accepted |
if egrep -q '\.\.' <<<"$s" ; then |
return $EX_USAGE |
fi |
|
# Newer parsing code: replaces units by a product |
# and feeds the resulting string to awk for evaluation |
# Note leading zeroes will lead awk to believe they are octal numbers |
# as a quick and dirty fix I'm just wrapping them in quotes, forcing awk |
# to re-evaluate them, which appears to be enough to make them decimal. |
# This is the only place where leading zeroes have no meaning. |
|
# Split into lines of time + unit: |
t= |
for item in $(echo "$s" | grep -o '[0-9]*[hms]') ;do |
n="\"$(echo $item | grep -o '[0-9]*')\"" # Number, quoted |
t=$t$n$(echo $item | grep -o '[hms]') # + Number + Unit |
done |
# Split all ms or s.ms |
for item in $(echo "$s" | grep -o '[0-9]*\.[0-9]*') ;do |
t="${t}\"$item\" + " |
done |
# Seconds without unit. They must be preceded by h, m or s at this point |
local secs=$(echo $s | egrep -o '.?[0-9]*$') |
# When preceded by '.', they're ms |
[ "$secs" ] && grep -q '\.'<<<"$secs" && secs= |
# Quote and addition. Note BSD grep/egrep wants the anchor ($) or won't match |
[ "$secs" ] && secs=" \"$(egrep -o '[0-9]*$'<<<"$secs")\" + " |
t=${t//h/ * 3600 + } |
t=${t//m/ * 60 + } |
t=${t//s/ + } |
t="$t$secs" |
t=${t/% + /} # Strip empty addition |
r=$(awkex "$t" 2>/dev/null) |
|
# Negative and empty intervals |
assert $LINENO "[ '$r' ] && [ '$t' ]" |
assert $LINENO "fptest $r -gt 0" |
|
echo $r |
} |
|
# Pads a string with zeroes on the left until it is at least |
# the indicated length |
# pad($1 = minimum length, $2 = string) |
pad() { |
# printf "%0${1}d\n" "$2" # [[R1#18]] # Can't be used with non-numbers |
local str=$2 |
while [ "${#str}" -lt $1 ]; do |
str="0$str" |
done |
echo $str |
} |
|
# Get Image Width |
# imw($1 = file) |
imw() { |
identify -format '%w' "$1" |
} |
|
# Get Image Height |
# imh($1 = file) |
imh() { |
identify -format '%h' "$1" |
} |
|
# Prints a number of seconds in a more human readable form |
# e.g.: 3600 becomes 1:00:00 |
# pretty_stamp($1 = seconds) |
pretty_stamp() { |
assert $LINENO "is_float '$1'" |
# Fully implemented in AWK to discard bc. |
# As a bonus now it's much faster and compact |
awk "BEGIN { |
t=$1 ; NOTMS=($DEC_MPLAYER==$decoder); |
MS=(t - int(t)); |
h=int(t / 3600); |
t=(t % 3600); |
m=int(t / 60); |
t=(t % 60); |
s=t |
if (h != 0) h=h\":\" ; else h=\"\" |
if (NOTMS!=1) ms=sprintf(\".%02d\", int(MS*100+0.5)); |
printf \"%s%02d:%02d%s\", h, m, s, ms |
}" |
# Note the rounding applied to $MS, it is required to match the precission passed on |
# to ffmpeg |
} |
|
# Prints a given size in human friendly form |
get_pretty_size() { |
local bytes=$1 |
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 |
} |
|
# 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_file_size($1 = file) |
get_pretty_file_size() { |
local f="$1" |
local bytes=$(get_file_size "$f") |
|
get_pretty_size "$bytes" |
} |
|
# Clean $safe_rename_pattern, which is override-able |
# Since safe_rename() is called from $() it won't be able to affect global variables directly |
sanitise_rename_pattern() { |
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 |
# Hashmarks will break the regex used in safe_rename() |
if grep -q '#' <<<"$safe_rename_pattern" ; then |
warn "Illegal character \"#\" found in safe renaming pattern, resetting it" |
safe_rename_pattern=$DEFAULT_SAFE_REN_PATT |
fi |
} |
|
# mv quiet |
# Move a file, be quiet about errors. |
# Ownership preservation is a common error on vfs, for example |
mvq() { |
mv -- "$@" 2>/dev/null |
} |
|
# 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) |
# XXX: Note it fails if target has no extension |
safe_rename() { |
trace $FUNCNAME $@ |
local from="$1" |
local to="$2" |
|
# Output extension |
local ext=$(filext "$to") |
# Output filename without extension |
local b=${to%.$ext} |
|
local n=1 |
while [ -f "$to" ]; do # Only executes if $2 exists |
# Bash 2 and Bash 3 behave differently with substring replacement (${//}) and '%' |
# Sed is a safer bet |
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 |
|
mvq "$from" "$to" |
echo "$to" |
} |
|
# Gets the file size in bytes |
# get_file_size($1 = filename) |
# du can provide bytes or kilobytes depending on the version used. The difference |
# can be notorius... |
# Neither busybox's nor BSD's du allow --bytes. |
# Note that using "ls -H" is not an option for portability reasons either. |
get_file_size() { |
# First, try the extended du arguments: |
local bytes |
bytes=$(du -L --bytes "$1" 2>/dev/null) || { |
echo $(( 1024 * $(du -Lk "$1" | cut -f1) )) |
return |
} |
# Getting to here means the first du worked correctly |
cut -f1 <<<"$bytes" |
} |
|
# Du replacement. This differs from get_file_size in that it takes multiple arguments |
dur() { |
for file in $@ ; do |
get_file_size "$file" |
done |
} |
|
# Gets the size of the dvd device, in DVD mode |
get_dvd_size() { |
# FIXME: Case sensivity might break with iso9660 |
if [ -f "$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_1.VOB" ]; then |
# Some VOBs available |
local vfiles="$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_*.VOB" |
# Print all sizes, each on a line, add '+' to the end of each line, add 0 to the end. |
local feed="$(dur "$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_"*".VOB" | cut -f1 | sed 's/$/ + /') 0" |
get_pretty_size $(awkex "$(nltos "$feed")") |
else |
echo "?" |
fi |
} |
|
is_linux() { |
uname -s | grep -iq '^Linux$' |
} |
|
# Get the mountpoint of a mounted image. |
# This only works on Linux. *BSD normal users aren't able to use mdconfig -l |
# Is there any better way? |
get_dvd_image_mountpoint() { |
if is_linux ; then |
local lodev=$(/sbin/losetup -j "$DVD_FILE" | cut -d':' -f1 | head -1) |
mount | grep "^$lodev " | cut -d' ' -f3 |
fi |
} |
|
# Tests the presence of all required programs |
# test_programs() |
test_programs() { |
local retval=0 last=0 |
local nopng=0 |
|
MPLAYER=$(type -pf mplayer) || true |
FFMPEG=$(type -pf ffmpeg) || true |
|
# Test we can actually use FFmpeg |
[ "$FFMPEG" ] && { |
# Newer FF has -codecs, -formats, -protocols, older has only -formats |
#+png is a codec so it's on different lists on newer and older |
if ! "$FFMPEG" -formats 2>/dev/null | grep -q 'EV.* png' && \ |
! "$FFMPEG" -codecs 2>/dev/null | grep -q 'EV.* png' ; then |
warn "FFmpeg can't output to png, won't be able to use it." |
FFMPEG='' |
nopng=1 |
fi |
} |
# Same for Mplayer |
[ "$MPLAYER" ] && { |
if ! "$MPLAYER" -vo help 2>&1 | grep -q 'png' ; then |
warn "MPlayer can't output to png, won't be able to use it." |
MPLAYER='' |
nopng=1 |
fi |
} |
|
[ "$MPLAYER" ] || [ "$FFMPEG" ] || { |
local pngwarn= |
[ $nopng -eq 1 ] && pngwarn=', with PNG output support,' |
error "mplayer and/or ffmpeg$pngwarn are required!" |
let 'retval++,1' |
} |
|
|
if [ $decoder -eq $DEC_FFMPEG ] && [ -z "$FFMPEG" ]; then |
decoder=$DEC_MPLAYER |
elif [ $decoder -eq $DEC_MPLAYER ] && [ -z "$MPLAYER" ]; then |
decoder=$DEC_FFMPEG |
fi |
|
# awk is required by SUS/POSIX but just to be sure... |
for prog in convert montage identify mktemp grep egrep cut sed awk ; do |
if ! type -pf "$prog" ; then |
error "Required program $prog not found!" |
let 'retval++,1' |
fi >/dev/null |
done |
# TODO: [[x2]] |
|
# Early exit |
[ $retval -eq 0 ] || return $EX_UNAVAILABLE |
|
# ImageMagick version. 6 is a must, I'm probably using some |
# features that require a higher minor version |
# Versions tested: |
# * Fedora 9: IM 6.4.0 |
local ver |
ver=$(identify -version | head -n1 | grep -o 'ImageMagick\s*[^ ]*' |\ |
cut -f 2 -d' ') |
if [ "$ver" ]; then |
local verx=${ver//-/.}.0 # Extra .0 in case rev doesn't exist |
local major=$(cut -d'.' -f1 <<<"$verx") |
local minor=$(cut -d'.' -f2 <<<"$verx") |
local micro=$(cut -d'.' -f3 <<<"$verx") |
local rev=$(cut -d'.' -f4 <<<"$verx") |
local serial=$(( $major * 100000 + $minor * 10000 + $micro * 100 + $rev)) |
if [ "$serial" -lt 630507 ]; then |
error "ImageMagick 6.3.5-7 or higher is required. Found $ver." ; |
let 'retval++,1' |
fi |
else |
error "Failed to check ImageMagick version." |
let 'retval++,1' |
fi |
|
[ $retval -eq 0 ] || return $EX_UNAVAILABLE |
} |
|
# Test wether $GETOP is a compatible version; try to choose an alternate if |
# possible |
choose_getopt() { |
if ! type -pf $GETOPT ; then |
# getopt not in path |
error "Required program getopt not found!" |
return $EX_UNAVAILABLE |
fi >/dev/null |
local goe= gor=0 |
# Try getopt. If there's more than one in the path, try all of them |
for goe in $(type -paf $GETOPT) ; do |
"$goe" -T || gor=$? |
if [ $gor -eq 4 ]; then |
# Correct getopt found |
GETOPT="$goe" |
break; |
fi |
done >/dev/null |
if [ $gor -ne 4 ]; then |
error "No compatible version of getopt in path, can't continue." |
error " For details on how to correct this problems, see <http://p.outlyer.net/vcs#getopt>" |
return $EX_UNAVAILABLE |
fi |
return 0 |
} |
|
# Remove any temporal files |
# Does nothing if none has been created so far |
# cleanup() |
cleanup() { |
if [ -z $TEMPSTUFF ]; then return 0 ; fi |
inf "Cleaning up..." |
rm -rf "${TEMPSTUFF[@]}" |
unset VCSTEMPDIR |
unset TEMPSTUFF ; declare -a TEMPSTUFF |
} |
|
# Exit callback. This function is executed on exit (correct, failed or |
# interrupted) |
# exithdlr() |
exithdlr() { |
# I don't think that's really required anyway |
if [ "$RANDFUNCTION" == "filerand" ]; then |
7<&- # Close FD 7 |
fi |
cleanup |
} |
|
# Feedback handling, these functions are use to print messages respecting |
# the verbosity level |
# Optional color usage added from explanation found in |
# <http://wooledge.org/mywiki/BashFaq> |
# |
# error($1 = text) |
error() { |
if [ $verbosity -ge $V_ERROR ]; then |
[ $plain_messages -eq 0 ] && echo -n "$prefix_err" |
# 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$suffix_fback" |
fi >&2 |
# It is important to redirect both tput and echo to stderr. Otherwise |
# n=$(something) wouldn't be coloured |
} |
# |
# Print a non-fatal error or warning |
# warning($1 = text) |
warn() { |
if [ $verbosity -ge $V_WARN ]; then |
[ $plain_messages -eq 0 ] && echo -n "$prefix_warn" |
echo "$1$suffix_fback" |
fi >&2 |
} |
# |
# Print an informational message |
# inf($1 = text) |
inf() { |
if [ $verbosity -ge $V_INFO ]; then |
[ $plain_messages -eq 0 ] && echo -n "$prefix_inf" |
echo "$1$suffix_fback" |
fi >&2 |
} |
# |
# Same as inf but with no colour ever. |
# infplain($1 = text) |
infplain() { |
if [ $verbosity -ge $V_INFO ]; then |
echo "$1" >&2 |
fi |
} |
|
# |
# trace($1 = function name = $FUNCNAME, function arguments...) |
trace() { |
if [ "$DEBUG" -ne "1" ]; then return; fi |
echo "[TRACE]: $@" >&2 |
} |
|
# |
# Tests if the filter chain contains the provided filter |
# has_filter($1 = filtername) |
has_filter() { |
local filter= ref=$1 |
for filter in ${FILTERS_IND[@]} ; do |
[ "$filter" == "$ref" ] || continue |
return 0 |
done |
return 1 |
} |
|
# |
# Enables prefixes in console output (instead of colour) |
set_feedback_prefixes() { |
prefix_err='[E] ' |
prefix_inf='[i] ' |
prefix_warn='[w] ' |
suffix_fback= |
} |
|
# |
# Initialises the variables affecting colourised feedback |
init_feedback() { |
HAS_COLORS= |
|
# tput might be preferable (Linux: man console_codes), but it doesn't |
# work on FreeBSD to set colors |
|
# Is tput available? |
if type -pf tput >/dev/null ; then |
# Is it able to set colours? |
if tput bold && tput setaf 0 >/dev/null && tput sgr0 ; then |
prefix_err=$(tput bold; tput setaf 1) |
prefix_warn=$(tput bold; tput setaf 3) |
prefix_inf=$(tput bold; tput setaf 2) |
suffix_fback=$(tput sgr0) |
HAS_COLORS="yes" |
fi |
fi |
|
if [ -z "$HAS_COLORS" ]; then |
# tput was not an option, let's try ANSI escape codes instead [[AEC]] |
# TODO: Detect support |
# Alternatively: $ perl -e 'print "\e[31m\e[1m"' |
# echo -e is not portable but echo $'' is bash-specific so it should be fine... |
# except when ANSI escape codes aren't supported of course |
prefix_err=$(echo $'\033[1m\033[31m') |
prefix_warn=$(echo $'\033[1m\033[33m') |
prefix_inf=$(echo $'\033[1m\033[32m') |
suffix_fback=$(echo $'\033[0m') |
HAS_COLORS="yes" |
fi |
|
# Finally, if there's no colour support, use prefixes instead |
if [ -z "$HAS_COLORS" ]; then |
set_feedback_prefixes |
fi |
} |
|
# |
# seq replacement |
# seq is not always present, jot is an alternative on FreeBSD. Instead, this is |
# a direct replacement |
# Note pure bash is *slower* than the awk (or perl) version |
# seqr($1 = from, $2 = to, $3 = increment) |
seqr() { |
local from=$1 to=$2 inc=$3 |
[ "$inc" ] || inc=1 |
awk "BEGIN { for (i=$from;i<=$to;i+=$inc) print i }" |
} |
|
# |
# assertion operator |
# assert($1 = line, $* = code) |
# TODO: Limit usage to values that will expand correctly always (i.e. not with quotes) |
assert() { |
[ $RELEASE -eq 1 ] && return |
LINE=$1 |
shift |
eval "$@" || { |
error "Internal error at line $LINE: $@" |
exit $EX_SOFTWARE |
} |
} |
|
# }}} # Convenience functions |
|
# {{{ # Core functionality |
|
# Creates a new temporary directory |
# create_temp_dir() |
create_temp_dir() { |
trace $FUNCNAME $@ |
|
[ "$VCSTEMPDIR" ] && return 0 |
|
# Try to use /dev/shm if available, this provided a very small |
# benefit on my system but me of help for huge files. Or maybe won't. |
# Passing a full path template is more x-platform than using |
# -t / -p |
if [ -d /dev/shm ] && [ -w /dev/shm ]; then |
VCSTEMPDIR=$(mktemp -d /dev/shm/vcs.XXXXXX) |
else |
[ "$TMPDIR" ] || TMPDIR="/tmp" |
VCSTEMPDIR=$(env TMPDIR="$TMPDIR" mktemp -d "$TMPDIR/vcs.XXXXXX") |
fi |
if [ ! -d "$VCSTEMPDIR" ]; then |
error "Error creating temporary directory" |
return $EX_CANTCREAT |
fi |
TEMPSTUFF=( "${TEMPSTUFF[@]}" "$VCSTEMPDIR" ) |
} |
|
# Resolve path. Realpath is not always available and readlink [[LC]] behaves differently in |
# GNU and BSD. FIXME: Has AWK or bash something similar? This is the only place requiring perl! |
realpathr() { |
perl -e "use Cwd qw(realpath);print realpath('$1')" |
} |
|
# Create a new temporal file and print its filename |
# new_temp_file($1 = suffix) |
new_temp_file() { |
trace $FUNCNAME $@ |
local r=$(env TMPDIR="$VCSTEMPDIR" mktemp "$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=( "${TEMPSTUFF[@]}" "$r" ) |
echo "$r" |
} |
|
# Randomizes the colours and fonts. The result won't be of much use |
# in most cases but it might be a good way to discover some colour/font |
# or colour combination you like. |
# randomize_look() |
randomize_look() { |
trace $FUNCNAME $@ |
local mode=f lineno |
|
if [ "f" == $mode ]; then # Random mode |
# There're 5 rows of extra info printed |
local ncolours=$(( $(convert -list color | wc -l) - 5 )) |
randcolour() { |
lineno=$(( 5 + ( $(rand) % $ncolours ) )) |
convert -list color | sed -n "${lineno}{p;q;}" | cut -d' ' -f1 # [[R1#11]] |
} |
else # Pseudo-random mode, WIP! |
randccomp() { |
# colours are in the 0..65535 range, while RANDOM in 0..32767 |
echo $(( $(rand) + $(rand) + ($(rand) % 1) )) |
} |
randcolour() { |
echo "rgb($(randccomp),$(randccomp),$(randccomp))" |
} |
fi |
|
# Older IM output was pretty different. Since this is a mode used for testing |
# I don't believe it's worth the effort to get it always right |
|
# This used to be -list type. Was this an older IM version or a bug in vcs? |
local nfonts=$(convert -list font | grep '^\s*Font:' | wc -l) |
randfont() { |
lineno=$(( $(rand) % $nfonts )) |
convert -list font | grep -o 'Font:.*' | sed -n "${lineno}{p;q;}" | cut -d' ' -f2 |
} |
|
bg_heading=$(randcolour) |
bg_sign=$(randcolour) |
bg_title=$(randcolour) |
bg_contact=$(randcolour) |
fg_heading=$(randcolour) |
fg_sign=$(randcolour) |
fg_tstamps=$(randcolour) |
fg_title=$(randcolour) |
font_tstamps=$(randfont) |
font_heading=$(randfont) |
font_sign=$(randfont) |
font_title=$(randfont) |
inf "Randomization result: |
Chosen backgrounds: |
'$bg_heading' for the heading |
'$bg_sign' for the signature |
'$bg_title' for the title |
'$bg_contact' for the contact sheet |
Chosen font colours: |
'$fg_heading' for the heading |
'$fg_sign' for the signature |
'$fg_title' for the title |
'$fg_tstamps' for the timestamps, |
Chosen fonts: |
'$font_heading' for the heading |
'$font_sign' for the signature |
'$font_title' for the title |
'$font_tstamps' for the timestamps" |
|
unset -f randcolour randfound randccomp |
} |
|
# 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() { |
trace $FUNCNAME $@ |
|
local st=0 end=${VID[$LEN]} tcfrom=$1 tcint=$2 tcnumcaps=$3 eo=0 |
# globals: fromtime, totime, timecode_from, TIMECODES, end_offset |
if fptest $st -lt $fromtime ; then |
st=$fromtime |
fi |
if fptest $totime -gt 0 && fptest $end -gt $totime ; then |
end=$totime |
fi |
if fptest $totime -le 0 ; then # If no totime is set, use end_offset |
eo=$end_offset |
|
local runlen=$(awkex "$end - $st") |
|
if fptest "$runlen" -lt $(get_interval "$MIN_LENGTH_FOR_END_OFFSET") ; then |
# Min length to use end offset not met, it won't be used |
inf "End offset won't be used, video too short." |
eo=0 |
elif fptest "($end-$eo-$st)" -le 0 ; then |
if fptest "$eo" -gt 0 && fptest "$eo" -eq "$DEFAULT_END_OFFSET" ; then |
warn "Default end offset was too high for the video, ignoring it." |
eo=0 |
else |
error "End offset too high, use e.g. '-E0'." |
return $EX_UNAVAILABLE |
fi |
fi |
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=$(awkexf "(($end-$st)/2 + 1)") |
else |
inc=$(awkexf "(($end-$eo-$st)/$tcnumcaps)") |
fi |
else |
error "Internal error" |
return $EX_SOFTWARE |
fi |
# Keep three decimals, round to lower to avoid exceeding the video length |
inc=$(keepdecimals_lower $inc 3) |
|
if fptest $inc -gt ${VID[$LEN]}; then |
error "Interval is longer than video length, skipping $f" |
return $EX_USAGE |
fi |
|
local stamp=$st |
local -a LTC |
local bound=$(awkexf "$end - $eo") |
while fptest $stamp -le "$bound"; do |
assert $LINENO fptest $stamp -ge 0 |
LTC=( "${LTC[@]}" "$stamp" ) |
stamp=$(keepdecimals_lower $(awkexf "$stamp + $inc") 3) |
done |
unset LTC[0] # Discard initial cap (=$st) |
TIMECODES=( ${TIMECODES[@]} ${LTC[@]} ) # Don't quote or extra empty stamp! |
} |
|
# Tries to guess an aspect ratio comparing width and height to some |
# known values (e.g. VCD resolution turns into 4/3) |
# guess_aspect($1 = width, $2 = height) |
guess_aspect() { |
trace $FUNCNAME $@ |
local w=$1 h=$2 ar |
|
case "$w" in |
352) |
if [ $h -eq 288 ] || [ $h -eq 240 ]; then |
# Ambiguous, could perfectly be 16/9 |
# VCD / DVD @ VCD Res. / Half-D1 / CVD |
ar=4/3 |
elif [ $h -eq 576 ] || [ $h -eq 480 ]; then |
# Ambiguous, could perfectly be 16/9 |
# Half-D1 / CVD |
ar=4/3 |
fi |
;; |
704|720) |
if [ $h -eq 576 ] || [ $h -eq 480 ]; then # DVD / DVB |
# Ambiguous, could perfectly be 16/9 |
ar=4/3 |
fi |
;; |
480) |
if [ $h -eq 576 ] || [ $h -eq 480 ]; then # SVCD |
ar=4/3 |
fi |
;; |
esac |
|
if [ -z "$ar" ]; then |
if [ $h -eq 720 ] || [ $h -eq 1080 ]; then # HD |
ar=16/9 |
fi |
fi |
|
if [ -z "$ar" ]; then |
warn "Couldn't guess aspect ratio." |
ar="$w/$h" # Don't calculate it yet |
fi |
|
echo $ar |
} |
|
# Capture a frame with ffmpeg |
# capture_ffmpeg($1 = inputfile, $2 = outputfile, $3 = timestamp[, $4 = extra opts]) |
capture_ffmpeg() { |
local f=$1 |
local o=$2 |
local ts=$3 |
|
# XXX: It would be nice to show a message if it takes too long |
# See wa_ss_* declarations at the start of the file for details |
"$FFMPEG" -y ${wa_ss_be/ / $ts} -i "$f" ${wa_ss_af/ / $ts} -an \ |
-dframes 1 -vframes 1 -vcodec png \ |
-f rawvideo $4 $shoehorned "$o" >"$stdout" 2>"$stderr" |
[ -f "$o" ] && [ "0" != "$(du "$o" | cut -f1)" ] |
} |
|
# Capture a frame with mplayer |
# capture_mplayer($1 = inputfile, $2 = UNUSED, $3 = timestamp[, $4 = extra opts]) |
capture_mplayer() { |
# Note mplayer CAN'T set the output filename |
local f="$1" |
local o=00000005.png |
local ts=$3 |
|
# No point in passing ms to mplayer |
ts=$(cut -d'.' -f1 <<<"$ts") |
# Capture 5 frames and drop the first 4, fixes a weird bug/feature of mplayer ([M1]) |
|
{ |
if [ $DVD_MODE -eq 1 ]; then |
"$MPLAYER" -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ |
-frames 5 -ss "$ts" $shoehorned -dvd-device "$DVD_FILE" \ |
$4 "dvd://$DVD_TITLE" |
else |
"$MPLAYER" -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ |
-frames 5 -ss "$ts" $4 $shoehorned "$f" |
fi |
|
} >"$stdout" 2>"$stderr" |
rm -f 0000000{1,2,3,4}.png # Remove the first four |
[ -f "$o" ] && [ "0" != "$(du "$o" | cut -f1)" ] |
} |
|
# Capture a frame |
# capture($1 = filename, $2 = second) |
capture() { |
trace $FUNCNAME $@ |
local f=$1 stamp=$2 |
local VIDCAPFILE=00000005.png |
# globals: $shoehorned $decoder |
|
if [ $decoder -eq $DEC_MPLAYER ]; then |
capture_mplayer "$f" 'IGNOREME' "$stamp" |
elif [ $decoder -eq $DEC_FFMPEG ]; then |
# FIXME: ffmpeg can put the temporary file anywhere |
capture_ffmpeg "$f" "$VIDCAPFILE" "$stamp" |
else |
error "Internal error!" |
return $EX_SOFTWARE |
fi || true |
if [ ! -f "$VIDCAPFILE" ] || [ "0" == "$(du "$VIDCAPFILE" | cut -f1)" ]; then |
[ $decoder -eq $DEC_MPLAYER ] && stamp=${stamp/%.*} |
error "Failed to capture frame at $(pretty_stamp $stamp) (${stamp}s)" |
return $EX_SOFTWARE |
fi |
|
return 0 |
} |
|
# Applies all individual vidcap filters |
# filter_vidcap($1 = filename, $2 = timestamp, $3 = width, $4 = height, $5 = context, $6 = index[1..]) |
filter_vidcap() { |
trace $FUNCNAME $@ |
# For performance purposes each filter simply prints a set of options |
# to 'convert'. That's less flexible but enough right now for the current |
# filters. |
local cmdopts= |
for filter in ${FILTERS_IND[@]}; do |
cmdopts="$cmdopts $( $filter "$1" "$2" "$3" "$4" "$5" "$6" ) -flatten " |
done |
local t=$(new_temp_file .png) |
eval "convert '$1' $cmdopts '$t'" |
# If $t doesn't exist returns non-zero |
[ -f "$t" ] && mvq "$t" "$1" |
} |
|
# Applies all global vidcap filters |
#filter_all_vidcaps() { |
# # TODO: Do something with "$@" |
# true |
#} |
|
filt_resize() { |
trace $FUNCNAME $@ |
local f="$1" t=$2 w=$3 h=$4 |
|
# Note the '!', required to change the aspect ratio |
echo " \( -geometry ${w}x${h}! \) " |
} |
|
# Draw a timestamp in the file |
# filt_apply_stamp($1 = filename, $2 = timestamp, $3 = width, $4 = height, $5 = context, $6 = index) |
filt_apply_stamp() { |
trace $FUNCNAME $@ |
local filename=$1 timestamp=$2 width=$3 height=$4 context=$5 index=$6 |
|
local pts=$pts_tstamps |
if [ $height -lt 200 ]; then |
pts=$(( $pts_tstamps / 3 )) |
elif [ $height -lt 400 ]; then |
pts=$(( $pts_tstamps * 2 / 3 )) |
fi |
# If the size is too small they won't be readable at all |
# With the original font 8 was the minimum, with DejaVu 7 is readable |
if [ $pts -le 7 ]; then |
pts=7 |
if [ $index -eq 1 ] && [ $context -ne $CTX_EXT ]; then |
warn "Very small timestamps in use. Disabling them with -dt might be preferable" |
fi |
fi |
# The last -gravity None is used to "forget" the previous gravity (otherwise it would |
# affect stuff like the polaroid frames) |
echo -n " \( -box '$bg_tstamps' -fill '$fg_tstamps' -stroke none -pointsize '$pts' " |
echo -n " -gravity '$grav_timestamp' -font '$font_tstamps' -strokewidth 3 -annotate +5+5 " |
echo " ' $(pretty_stamp $stamp) ' \) -flatten -gravity None " |
} |
|
# Apply a framed photo-like effect |
# Taken from <http://www.imagemagick.org/Usage/thumbnails/#polaroid> |
# filt_photoframe($1 = filename, $2 = timestamp, $3 = width, $4 = height) |
filt_photoframe() { |
trace $FUNCNAME $@ |
# local file="$1" ts=$2 w=$3 h=$4 |
# Tweaking the size gives a nice effect too |
# w=$(( $w - ( $RANDOM % ( $w / 3 ) ) )) |
# The border is relative to the input size (since 1.0.99), with a maximum of 6 |
# Should probably be bigger for really big frames |
# Note that only images below 21600px (e.g. 160x120) go below a 6px border |
local border=$(( ($3*$4) / 3600 )) |
[ $border -lt 7 ] || border=6 |
echo -n "-bordercolor white -border $border -bordercolor grey60 -border 1 " |
} |
|
filt_softshadow() { |
# Before this was a filter, there was the global (montage) softshadow (50x2+10+10) and the |
# photoframe inline softshadow 60x4+4+4 |
echo -n "\( -background black +clone -shadow 50x2+4+4 -background none \) +swap -flatten -trim +repage " |
} |
|
|
# Apply a polaroid-like border effect |
# Based on filt_photoframe(), with a bigger lower border |
# filt_polaroid($1 = filename, $2 = timestamp, $3 = width, $4 = height) |
filt_polaroid() { |
trace $FUNCNAME $@ |
# local file="$1" ts=$2 w=$3 h=$4 |
local border=$(( ($3*$4) / 3600 )) # Read filt_photoframe for details |
[ $border -lt 7 ] || border=6 |
echo -n "-bordercolor white -mattecolor white -frame ${border}x${border} " |
# FIXME: This is rather ugly (double-flipping) there's sure a better way |
echo -n "\( -flip -splice 0x$(( $border*5 )) \) " |
echo "-flip -bordercolor grey60 -border 1 +repage" |
} |
|
# Applies a random rotation |
# Taken from <http://www.imagemagick.org/Usage/thumbnails/#polaroid> |
# filt_randrot($1 = filename, $2 = timestamp, $3 = width, $4 = height) |
filt_randrot() { |
trace $FUNCNAME $@ |
# Rotation angle [-18..18] |
local angle=$(( ($(rand) % 37) - 18 )) |
echo "-background none -rotate $angle " |
} |
|
# This one requires much more work, the results are pretty rough, but ok as |
# a starting point / proof of concept |
filt_film() { |
trace $FUNCNAME $@ |
local file="$1" ts=$2 w=$3 h=$4 |
# Base reel dimensions |
local rw=$(rmultiply $w,0.08) # 8% width |
local rh=$(( $rw / 2 )) |
# Ellipse center |
local ecx=$(( $rw / 2 )) ecy=0 |
# Ellipse x, y radius |
local erx=$(( (rw/2)*60/100 )) # 60% halt rect width |
local ery=$(( $erx / 2)) |
|
local base_reel=$(new_temp_file .png) reel_strip=$(new_temp_file .png) |
|
# Create the reel pattern... |
convert -size ${rw}x${rh} 'xc:black' \ |
-fill white -draw "ellipse $ecx,$ecy $erx,$ery 0,360" -flatten \ |
\( +clone -flip \) -append \ |
-fuzz '40%' -transparent white \ |
"$base_reel" |
# FIXME: Error handling |
|
# Repeat it until the height is reached and crop to the exact height |
local sh=$(imh "$base_reel") in= |
local repeat=$( ceilmultiply $h/$sh) |
while [ $repeat -gt 1 ]; do |
in="$in '$base_reel' " |
let 'repeat--' |
done |
eval convert "$base_reel" $in -append -crop $(imw "$base_reel")x${h}+0+0 \ |
"$reel_strip" |
|
# As this options will be appended to the commandline we cannot |
# order the arguments optimally (eg: reel.png image.png reel.png +append) |
# A bit of trickery must be done flipping the image. Note also that the |
# second strip will be appended flipped, which is intended. |
echo -n "'$reel_strip' +append -flop '$reel_strip' +append -flop " |
} |
|
# Creates a contact sheet by calling the delegate |
# create_contact_sheet($1 = columns, $2 = context, $3 = width, $4 = height, |
# $5...$# = vidcaps) : output |
create_contact_sheet() { |
trace $FUNCNAME $@ |
$CSHEET_DELEGATE "$@" |
} |
|
# This is the standard contact sheet creator |
# csheet_montage($1 = columns, $2 = context, $3 = width, $4 = height, |
# $5... = vidcaps) : output |
csheet_montage() { |
trace $FUNCNAME $@ |
local cols=$1 ctx=$2 width=$3 height=$4 output=$(new_temp_file .png) |
shift 4 |
# Padding is no longer dependant upong context since alignment of the |
# captures was far trickier then |
local hpad= vpad= splice= |
|
# The shadows already add a good amount of padding |
if has_filter filt_softshadow ; then |
hpad=0 |
vpad=0 |
splice=5x10 |
else |
hpad=$HPAD |
vpad=$HPAD |
splice=0x8 |
fi |
|
montage -background Transparent "$@" -geometry +$hpad+$vpad -tile "$cols"x "$output" |
convert "$output" -background Transparent -splice $splice "$output" |
|
# FIXME: Error handling |
echo $output |
} |
|
# Polaroid contact sheet creator: it overlaps vidcaps with some randomness |
# csheet_overlap($1 = columns, $2 = context, $3 = width, $4 = height, |
# $5... = $vidcaps) : output |
csheet_overlap() { |
trace $FUNCNAME $@ |
local cols=$1 ctx=$2 width=$3 height=$4 |
# globals: $VID |
shift 4 |
|
# TBD: Handle context |
|
# Explanation of how this works: |
# On the first loop we do what the "montage" command would do (arrange the |
# images in a grid) but overlapping each image to the one on their left, |
# creating the output row by row, each row in a file. |
# On the second loop we append the rows, again overlapping each one to the |
# one before (above) it. |
# XXX: Compositing over huge images is quite slow, there's probably a |
# better way to do it |
|
# Offset bounds, this controls how much of each snap will be over the |
# previous one. Note it is important to work over $width and not $VID[$W] |
# to cover all possibilities (extended mode and -H change the vidcap size) |
local maxoffset=$(( $width / 3 )) |
local minoffset=$(( $width / 6 )) |
|
# Holds the files that will form the full contact sheet |
# each file is a row on the final composition |
local -a rowfiles |
|
# Dimensions of the canvas for each row, it should be big enough |
# to hold all snaps. |
# My trigonometry is pretty rusty but considering we restrict the angle a lot |
# I believe no image should ever be wider/taller than the diagonal (note the |
# ceilmultiply is there to simply round the result) |
local diagonal=$(ceilmultiply $(pyth_th $width $height) 1) |
# XXX: The width, though isn't guaranteed (e.g. using filt_film it ends wider) |
# adding 3% to the diagonal *should* be enough to compensate |
local canvasw=$(( ( $diagonal + $(rmultiply $diagonal,0.3) ) * $cols )) |
local canvash=$(( $diagonal )) |
|
# The number of rows required to hold all the snaps |
local numrows=$(ceilmultiply ${#@},1/$cols) # rounded division |
|
# Variables inside the loop |
local col # Current column |
local rowfile # Holds the row we're working on |
local offset # Random offset of the current snap [$minoffset..$maxoffset] |
local accoffset # The absolute (horizontal) offset used on the next iteration |
local cmdopts # Holds the arguments passed to convert to compose the sheet |
local w # Width of the current snap |
for row in $(seqr 1 $numrows) ; do |
col=0 |
rowfile=$(new_temp_file .png) |
rowfiles=( "${rowfiles[@]}" "$rowfile" ) |
accoffset=0 |
cmdopts= # This command is pretty time-consuming, let's make it in a row |
|
# Base canvas # Integrated in the row creation since 1.0.99 |
|
# Step through vidcaps (col=[0..cols-1]) |
for col in $(seqr 0 $(( $cols - 1 ))); do |
# More cols than files in the last iteration (e.g. -n10 -c4) |
if [ -z "$1" ]; then break; fi |
w=$(imw "$1") |
|
# Stick the vicap in the canvas |
cmdopts="$cmdopts '$1' -geometry +${accoffset}+0 -composite " |
|
offset=$(( $minoffset + ( $(rand) % $maxoffset ) )) |
let 'accoffset=accoffset + w - offset' |
shift |
done |
inf "Composing overlapped row $row/$numrows..." |
eval convert -size ${canvasw}x${canvash} xc:transparent -geometry +0+0 "$cmdopts" -trim +repage "'$rowfile'" >&2 |
done |
|
inf "Merging overlapped rows..." |
output=$(new_temp_file .png) |
|
cmdopts= |
accoffset=0 |
local h |
for row in "${rowfiles[@]}" ; do |
w=$(imw "$row") |
h=$(imh "$row") |
minoffset=$(( $h / 8 )) |
maxoffset=$(( $h / 4 )) |
offset=$(( $minoffset + ( $(rand) % $maxoffset ) )) |
# The row is also offset horizontally |
cmdopts="$cmdopts '$row' -geometry +$(( $(rand) % $maxoffset ))+$accoffset -composite " |
let 'accoffset=accoffset + h - offset' |
done |
# After the trim the image will be touching the outer borders and the heading and footer, |
# older versions (prior to 1.0.99) used -splice 0x10 to correct the heading spacing, 1.0.99 |
# onwards uses -frame to add spacing in all borders + splice to add a bit more space on the |
# upper border. Note splice uses the background colour while frame uses the matte colour |
eval convert -size ${canvasw}x$(( $canvash * $cols )) xc:transparent -geometry +0+0 \ |
$cmdopts -trim +repage -bordercolor Transparent -background Transparent -mattecolor Transparent \ |
-frame 5x5 -splice 0x5 "$output" >&2 |
|
# FIXME: Error handling |
echo $output |
} |
|
# Sorts timestamps and removes duplicates |
# clean_timestamps($1 = space separated timestamps) |
clean_timestamps() { |
trace $FUNCNAME $@ |
# Note AFAIK sort only sorts lines, that's why I replace spaces by newlines |
local s=$1 |
stonl "$s" | sort -n | uniq |
} |
|
# Test the video at a given timestamp (to see if it can be reached) |
# See safe_length_measure() |
# probe_video($1 = input file, $2 = stamp) |
probe_video() { |
local f="$1" |
local ts="$2" |
local tempfile= |
local ret=0 |
|
# create_temp_dir should have been called |
|
# This time a resize filter is applied to the player to produce smaller |
# output |
if [ $decoder -eq $DEC_MPLAYER ]; then |
tempfile=00000005.png |
TEMPSTUFF=( "${TEMPSTUFF[@]}" "$tempfile" ) |
if ! capture_mplayer "$f" "IGNOREME" "$ts" "-vf scale=96:96"; then |
ret=1 |
fi |
elif [ $decoder -eq $DEC_FFMPEG ]; then |
tempfile=$(new_temp_file '-safelen.png') |
if ! capture_ffmpeg "$f" "$tempfile" "$ts" "-s 96x96"; then |
ret=1 |
fi |
else |
assert $LINENO false |
ret=1 |
fi |
rm -f "$tempfile" |
return $ret |
} |
|
# Try to guess a correct length for the video, taking the reported lengths a |
# starting point |
# safe_length_measure($1 = filename) |
safe_length_measure() { |
trace $FUNCNAME $@ |
local f="$1" |
local len=${VID[$LEN]} |
local tempfile= |
local newlen=$len |
local capturefn= |
|
create_temp_dir |
|
if probe_video "$1" $len ; then |
inf " File looks fine, suspicion withdrawn" |
echo "$len" |
return 0 |
else |
# Can't seek to the very end, adjust |
warn "Starting safe length measuring (this might take a while)..." |
local maxrew=$(min $QUIRKS_MAX_REWIND $(awkex "int($len)")) # At most we'll rewind 20 seconds |
# -1 (-WS) => Rewind up to the start |
# Might be -2, -4, ... e.g. (-WS -Ws) |
if fptest $maxrew -ge $len || fptest "$maxrew" -lt 0 ; then |
maxrew=$len |
INTERNAL_MAXREWIND_REACHED=1 |
fi |
for rew in $(seqr $QUIRKS_LEN_STEP $maxrew $QUIRKS_LEN_STEP); do |
newlen=$(keepdecimals_lower $(awkex "$len - $rew") 3) |
warn " ... trying $newlen" |
if probe_video "$f" "$newlen" ; then |
echo $newlen |
return 0 |
fi |
done |
fi |
# Hitting this line means we're doomed! |
return 1 |
} |
|
##### {{{{ Codec names |
|
# Codecs TODO: Clean this |
# Translates an mplayer codec id/fourcc to its name |
get_vcodec_name() { |
local vcid="$1" |
local vcodec= |
# Video codec "prettyfication", see [[R2]], [[R3]], [[R4]] |
case "$vcid" in |
0x10000001) vcodec="MPEG-1" ;; |
0x10000002) vcodec="MPEG-2" ;; |
0x00000000) vcodec="Raw video" ;; # How correct is this? |
avc1|H264) vcodec="MPEG-4 AVC" ;; # H264 is used in mov/mp4 |
DIV3) vcodec="DivX ;-) Low-Motion" ;; # Technically same as mp43 |
DX50) vcodec="DivX 5" ;; |
FMP4) vcodec="FFmpeg" ;; # XXX: Would LAVC be a better name? |
I420) vcodec="Raw I420 Video" ;; # XXX: Officially I420 is Indeo 4 but it is mapped to raw ¿? |
MJPG) vcodec="M-JPEG" ;; # mJPG != MJPG |
MPG4) vcodec="MS MPEG-4 V1" ;; |
MP42) vcodec="MS MPEG-4 V2" ;; |
MP43) vcodec="MS MPEG-4 V3" ;; |
RV10) vcodec="RealVideo 1.0/5.0" ;; |
RV20) vcodec="RealVideo G2" ;; |
RV30) vcodec="RealVideo 8" ;; |
RV40) vcodec="RealVideo 9/10" ;; |
SVQ1) vcodec="Sorenson Video 1" ;; |
SVQ3) vcodec="Sorenson Video 3" ;; |
theo) vcodec="Ogg Theora" ;; |
tscc) vcodec="TechSmith SCC" ;; |
VP6[012F]) vcodec="On2 Truemotion VP6" ;; |
WMV1) vcodec="WMV7" ;; |
WMV2) vcodec="WMV8" ;; |
WMV3) vcodec="WMV9" ;; |
WMVA) vcodec="WMV9 Advanced Profile" ;; # Not VC1 compliant. Deprecated by Microsoft. |
XVID) vcodec="Xvid" ;; |
3IV2) vcodec="3ivx Delta 4.0" ;; # Rare but seen |
|
# These are known FourCCs that I haven't tested against so far |
WVC1) vcodec="VC-1" ;; |
DIV4) vcodec="DivX ;-) Fast-Motion" ;; |
DIVX|divx) vcodec="DivX" ;; # OpenDivX / DivX 5(?) / Project Mayo |
IV4[0-9]) vcodec="Indeo Video 4" ;; |
IV50) vcodec="Indeo 5.0" ;; |
VP3[01]) vcodec="On2 VP3" ;; |
VP40) vcodec="On2 VP4" ;; |
VP50) vcodec="On2 VP5" ;; |
s263) vcodec="H.263" ;; # 3GPP |
# Legacy(-er) codecs (haven't seen files in these formats in awhile) |
IV3[0-9]) vcodec="Indeo Video 3" ;; # FF only recognises IV31 |
MSVC) vcodec="Microsoft Video 1" ;; |
MRLE) vcodec="Microsoft RLE" ;; |
3IV1) vcodec="3ivx Delta" ;; |
# "mp4v" is the MPEG-4 fourcc *in mov/mp4/3gp*; but I also found MP4V (Apple's iTunes sample) |
#mp4v|MP4V) vcodec="MPEG-4" ;; |
mp4v|MP4V) vcodec="MPEG-4" ;; |
# Synthetic, used for ffmpeg translations |
vcs_divx) vcodec="DivX ;-)" ;; |
*) # If not recognized fall back to FourCC |
vcodec="$vcid" |
;; |
esac |
echo "$vcodec" |
} |
|
# Translates an FFmpeg codec id to an MPlayer codec id/fourcc |
# TODO: Clean this |
translate_ffmpeg_vcodec_id() { |
# The list of ffmpeg codecs might be retrieved by looking at the code but I |
#+simply used the ffmpeg -formats / ffmpeg -codecs command |
# Supported video decoders: $ ffmepg -codecs | grep '^ D.V' |
local vcid="$1" |
local mpid= |
case "$vcid" in |
mpeg1video) mpid="0x10000001" ;; # mpeg1video_vdpau? |
mpeg2video) mpid="0x10000002" ;; |
rawvideo) mpid="0x00000000" ;; # can't distinguish from I420 |
h264) mpid="avc1" ;; |
mjpeg) mpid="MJPG" ;; |
msmpeg4v1) mpid="MPG4" ;; |
msmpeg4v2) mpid="MP42" ;; |
theora) mpid="theo" ;; |
camtasia) mpid="tscc" ;; |
vp6|vp6a|vp6f) mpid="VP60" ;; |
# FIXME List of codec id's I translate but haven't test: |
# svq3, rv40, theora, camtasia, vp6* |
# MPlayer uses uppercase while FFmpeg uses lowercase |
rv10|rv20|rv30|rv40|svq1|svq3|wmv1|wmv2|wmv3) mpid=$(echo $vcid | tr '[a-z]' '[A-Z]') ;; |
# FFmpeg doesn't print FourCC's so there's some codecs that can't be told apart |
msmpeg4) mpid="vcs_divx" ;; # DIV3 = DIV4 = MP43 |
# XVID = DIVX = DX50 = FMP4 = ... = mpeg4 |
mpeg4) mpid="mp4v" ;; # Take advantage of an unamed MPEG-4 |
|
h263) mpid="s263" ;; |
|
vc1) mpid="WVC1" ;; # In FF: WMVA = vc1 |
# Not supported (ff just prints the FourCC) |
# IV4*, vp4 |
vp3) mpid="VP30" ;; |
vp5) mpid="VP50" ;; |
# Legacy(-er) codecs (haven't seen files in these formats in awhile) |
# MSVC? MRLE? |
indeo3) mpid="IV31" ;; |
*) # If not recognized fall back to FourCC |
mpid="$vcid" |
;; |
|
esac |
echo $mpid |
} |
|
get_acodec_name() { |
local acid="$1" |
local acodec= |
|
if grep -q '[ -]' <<<"$acid" ; then |
# Won't be recognised anyway |
echo "$acid" |
return |
fi |
|
case "$(tolower "$acid")" 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, Ogg, probably others |
acodec='Vorbis' |
;; |
qdm2) acodec="QDesign" ;; |
"") acodec="no audio" ;; |
samr) acodec="AMR" ;; # AMR-NB/AMR-WB? |
# Following not seen by me so far, don't even know if mplayer would |
# identify them |
#<http://lists.mplayerhq.hu/pipermail/ffmpeg-devel/2005-November/005054.html> |
355) acodec="WMA9 Lossless" ;; |
10) acodec="WMA9 Voice" ;; |
*) # If not recognized show audio id tag |
acodec="$acid" |
;; |
esac |
echo "$acodec" |
} |
|
translate_ffmpeg_acodec_id() { |
local acid="$1" |
local mpid= |
|
# ffmpeg -codecs | grep ^\ D.A |
case "$acid" in |
mp3) mpid='85' ;; |
# Note FF can tell apart mp1/mp2 directly |
mp1) mpid='MPEG Layer I (MP1)' ;; |
mp2) mpid='MPEG Layer II (MP2)' ;; |
aac) mpid='mp4a' ;; # Can aac be MPEG2? |
wmav1) mpid='352' ;; |
wmav2) mpid='353' ;; |
wmapro) mpid='354' ;; # Actually WMA9 Professional |
ac3) mpid='8192' ;; |
# FF has a ton of pcm variants (sign, endianness, ...) |
pcm_*) mpid="1" ;; |
vorbis) mpid="vrbs" ;; |
|
qdm2) mpid="QDM2" ;; |
libopencore_amrnb) mpid="AMR-NB" ;; |
libopencore_amrwb) mpid="AMR-WB" ;; |
*) # If not recognized show audio id tag |
mpid="$acid" |
;; |
esac |
echo "$mpid" |
} |
|
##### }}}} # Codec names |
|
# Try to identify video properties using mplayer |
# Fills $MPLAYER_CACHE with the relevant output and $VID_MPLAYER with |
# the actual values. See identify_video() |
# mplayer_identify($1 = file) |
mplayer_identify() { |
trace $FUNCNAME $@ |
[ "$MPLAYER" ] || return |
local f="$1" |
local mi=( ) |
# Note to self: Don't change the -vc as it would affect $vdec |
if [ $DVD_MODE -eq 0 ]; then |
MPLAYER_CACHE=$("$MPLAYER" -benchmark -ao null -vo null -identify -frames 0 \ |
-quiet "$f" 2>/dev/null | grep ^ID) |
else |
MPLAYER_CACHE=$("$MPLAYER" -benchmark -ao null -vo null -identify -frames 0 \ |
-quiet -dvd-device $DVD_FILE dvd://$DVD_TITLE \ |
2>/dev/null | grep ^ID) |
fi |
# Note the head -1! |
mi[$VCODEC]=$(grep ID_VIDEO_FORMAT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) # FourCC |
mi[$ACODEC]=$(grep ID_AUDIO_FORMAT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) |
mi[$VDEC]=$(grep ID_VIDEO_CODEC <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) # Decoder (!= Codec) |
mi[$W]=$(grep ID_VIDEO_WIDTH <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) |
mi[$H]=$(grep ID_VIDEO_HEIGHT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) |
mi[$FPS]=$(grep ID_VIDEO_FPS <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) |
# For some reason my (one track) samples have two ..._NCH, first one 0 |
#+Also multichannel is detected as 2 ch |
mi[$CHANS]=$(grep ID_AUDIO_NCH <<<"$MPLAYER_CACHE"| grep -v '=0' | cut -d'=' -f2|head -1) |
if [ $DVD_MODE -eq 0 ]; then |
# For DVDs it prints ID_DVD_TITLE_x_LENGTH and ID_LENGTH. |
#+Both appear valid. |
mi[$LEN]=$(grep ID_DVD_TITLE_${DVD_TITLE}_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2) |
[ "${mi[$LEN]}" ] || mi[$LEN]=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| head -1 | cut -d'=' -f2) |
else |
mi[$LEN]=$(grep ID_DVD_TITLE_${DVD_TITLE}_LENGTH <<<"$MPLAYER_CACHE"| head -1 | cut -d'=' -f2) |
fi |
# Voodoo :P Remove (one) trailing zero |
if [ "${mi[$FPS]:$(( ${#mi[$FPS]} - 1 ))}" == "0" ]; then |
mi[$FPS]="${mi[$FPS]:0:$(( ${#mi[$FPS]} - 1 ))}" |
fi |
mi[$ASPECT]=$(grep ID_VIDEO_ASPECT <<<"$MPLAYER_CACHE" | egrep -v '^0.0000$' | cut -d'=' -f2 | tail -1) |
# If none set, delete it |
[ "${mi[$ASPECT]}" ] && fptest "${mi[$ASPECT]}" -eq 0.0 && mi[$ASPECT]='' |
mi[$VCNAME]=$(get_vcodec_name "${mi[$VCODEC]}") |
if [ "${mi[$VDEC]}" == "ffodivx" ] && [ "${mi[$VCNAME]}" != "MPEG-4" ]; then |
mi[$VCNAME]="${mi[$VCNAME]} (MPEG-4)" |
elif [ "${mi[$VDEC]}" == "ffh264" ]; then # At least two different fourccs use h264, maybe more |
mi[$VCNAME]="${mi[$VCNAME]} (h.264)" |
fi |
mi[$ACNAME]=$(get_acodec_name "${mi[$ACODEC]}") |
if [ "${mi[$ACODEC]}" == "samr" ] ; then |
local adec=$(grep ID_AUDIO_CODEC <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) |
if [ "$adec" == "ffamrnb" ]; then |
mi[$ACNAME]="AMR-NB"; |
fi |
fi |
|
# Array assignment |
VID_MPLAYER=("${mi[@]}") |
} |
|
# Try to identify video properties using ffmpeg |
# Fills $FFMPEG_CACHE with the relevant output and $VID_FFMPEG with |
# the actual values. See identify_video() |
# mplayer_identify($1 = file) |
ffmpeg_identify() { |
trace $FUNCNAME $@ |
[ "$FFMPEG" ] || return |
# (AFAIK) Can't use ffmpeg in DVD Mode |
#[ $DVD_MODE -eq 0 ] || return |
local f="$1" |
# DVD Devices *MUST* be mounted for the identifying process to even start |
assert $LINENO "[ $DVD_MODE -eq 0 ] || [ '$DVD_MOUNTP' ]" |
[ $DVD_MODE -eq 1 ] && [ "$DVD_DEVICE" ] && { |
local vfile="$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_1.VOB" |
if [ ! -r "$vfile" ]; then |
error "Failed to locale mounted DVD. Detection will be less accurate." |
return 0 # We can continue anyway |
fi |
f="$vfile" |
} |
|
local fi=( ) vs= as= obs= vsid= |
# FFmpeg is relatively new, introduced in 1.0.99 so it needs more testing |
FFMPEG_CACHE=$("$FFMPEG" -i "$f" -dframes 0 -vframes 0 /dev/null 2>&1 | egrep '(Stream|Duration:|^Seems)') |
# Only the first streams of each type are honored. FIXME: Add multi-audio support. |
vs=$(grep Stream <<<"$FFMPEG_CACHE" | grep 'Video:' | head -1) |
as=$(grep Stream <<<"$FFMPEG_CACHE" | grep 'Audio:' | head -1) |
obs=$(grep Seems <<<"$FFMPEG_CACHE") |
# Stream #0.0: Video: mpeg4, yuv420p, 624x352 [PAR 1:1 DAR 39:22], 23.98 tbr, 23.98 tbn, 24k tbc |
# New and old versions of ffmpeg changed their output considerably, e.g.: |
# (same file, Robotica_720.wmv) |
# New output: |
# Seems stream 1 codec frame rate differs from container frame rate: 1000.00 (1000/1) -> 23.98 (24000/1001) |
# [...] |
# Duration: 00:00:20.77, start: 3.000000, bitrate: 6250 kb/s |
# Stream #0.0(eng): Audio: wmapro, 48000 Hz, 6 channels, s16, 384 kb/s |
# Stream #0.1(eng): Video: wmv3, yuv420p, 1280x720, 6500 kb/s, 23.98 tbr, 1k tbn, 1k tbc |
# Old output: |
# Duration: 00:00:20.7, start: 3.000000, bitrate: 6250 kb/s |
# Stream #0.0: Audio: 0x0162, 48000 Hz, 5:1, 384 kb/s |
# Stream #0.1: Video: wmv3, yuv420p, 1280x720, 24.00 fps(r) |
# TODO: tbr is rounded to two decimals but the actual ratio is printed: |
# 24000/1001 = 23.97602 |
# (older ffmpeg prints 24 fps, 24/1 so no luck here |
# **Also seen**: (note the 'tb(r)') |
# Stream #0.1: Video: wmv3, yuv420p, 1440x1080 [PAR 4:3 DAR 16:9], 8000 kb/s, 23.98 tb(r) |
# **Also seen**: (VOB, latest ffmpeg as of this writing): |
# Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 9800 kb/s, 23.53 fps, 25 tbr, 90k tbn, 50 tbc |
# **Also seen**: (DVB TS to DX50 in MKV), note the DAR mess, the second one is the correct one |
# Stream #0.0: Video: mpeg4, yuv420p, 640x326 [PAR 1:1 DAR 320:163], PAR 231:193 DAR 73920:31459, 25 fps, 25 tbr, 1k tbn, 25 tbc |
vsid=$(grep -o '#0.[0-9]' <<<"$vs" | cut -d'.' -f2) # Video Stream ID |
fi[$VCODEC]=$(egrep -o 'Video: [^,]*' <<<"$vs" | cut -d' ' -f2-) |
# ffmpeg's codec might contain spaces in some cases, i.e. iv4 in mov (see mplayer's bestiary) |
#+unless this turns out to be common I won't be handling it specially |
# Note unidentified audio codecs will be printed in hexadecimal |
fi[$ACODEC]=$(egrep -o 'Audio: [^,]*' <<<"$as" | cut -d' ' -f2) |
fi[$VDEC]='' |
# The comma is required for cases where the stream id is printed (in hex) |
fi[$W]=$(egrep -o ', [0-9]*x[0-9]*' <<<"$vs" | sed 's/^, //' | cut -dx -f1) |
fi[$H]=$(egrep -o ', [0-9]*x[0-9]*' <<<"$vs" | cut -dx -f2) |
# Newer CHANS and some older... |
fi[$CHANS]=$(egrep -o '[0-9]* channels' <<<"$as" | cut -d' ' -f1) |
# ...fallback for older |
if [ -z "${fi[$CHANS]}" ]; then |
local chans=$(egrep -o 'Hz, [^,]*' <<<"$as" | cut -d' ' -f2) |
case $chans in |
mono) fi[$CHANS]=1 ;; |
stereo) fi[$CHANS]=2 ;; |
5.1|5:1) fi[$CHANS]=6 ;; # * |
*) ;; # Other layouts use 'N channels' |
# 5.1 was in the previous version (can't remember if it was empirical). |
esac |
fi |
# Newer FPS... |
# tbr/tbn/tbc explanation: tb stands for time base |
# n: AVStream, c: AVCodecContext, r: VideoStream (Guessed) |
# tbr is the best bet. Note it's common for WMVs to contains "1k tbn, 1k tbc" |
# tbr is rounded to two decimals, the values used to derived it might be |
# printed in a "Seems ..." line like the one in the example above so it |
# can be re-calculated. |
fi[$FPS]=$(egrep -o '[0-9]*\.?[0-9]*k? tb(r|\(r\))' <<<"$vs" | cut -d' ' -f1) |
# Let's convert e.g. 23.98 into 23.976...: |
if [ "${fi[$FPS]}" ] && grep -q '\.' <<<"${fi[$FPS]}" ; then |
# Decimals, see if we got better values available |
local vsobs=$(grep "stream $vsid" <<<"$obs") |
# Observations regarding video stream found |
if [ "$vsobs" ] && grep -q " -> ${fi[$FPS]} (.*)" <<<"$vsobs" ; then |
# FPS candidate |
local newfps=$(egrep -o -- '-> [^ ]* \([0-9]*/[0-9]*' <<<"$vsobs" | cut -d'(' -f2) |
is_fraction $newfps && fi[$FPS]=$(keepdecimals "$newfps" 3) |
fi |
fi |
# ...fallback for older. The older version I tried seems to round further, i.e. |
# 23.976 became 24 so no fix for this one |
if [ -z "${fi[$FPS]}" ]; then |
# No k suffix here, 1000 is 1000 |
fi[$FPS]=$(egrep -o '[0-9]*\.?[0-9]* fps' <<<"$vs" | cut -d' ' -f1) |
fi |
# Be consistent with mplayer's output: at least two decimals |
[ "${fi[$FPS]}" ] && { |
fi[$FPS]=$(keepdecimals "${fi[$FPS]}" 3) |
fi[$FPS]=${fi[$FPS]/%0} # Strip 0$ |
} |
fi[$LEN]=$(egrep -o 'Duration: [^,]*' <<<"$FFMPEG_CACHE" | cut -d' ' -f2) |
if [ "${fi[$LEN]}" == "N/A" ]; then # It might be unable to detect |
fi[$LEN]="" |
fi |
fi[$LEN]=$( get_interval $(echo "${fi[$LEN]}" | sed 's/:/h/' | sed 's/:/m/') ) |
# Aspect ratio in FFmpeg is only provided in newer ffmpeg |
# It might be calculated for files without one (which is ok anyway) |
# TODO: Replace tail -1 with some better option (see the double DAR example above) |
fi[$ASPECT]=$(egrep -o 'DAR [0-9]*:[0-9]*'<<<"$FFMPEG_CACHE" | tail -1 | cut -d' ' -f2 | sed 's#:#/#') |
# Due to calling ffmpeg on a single VOB when in DVD Device mode, the length will be partial |
[ $DVD_MODE -eq 1 ] && [ "$DVD_DEVICE" ] && fi[$LEN]='' |
fi[$VCNAME]=$(get_vcodec_name $(translate_ffmpeg_vcodec_id "${fi[$VCODEC]}")) |
fi[$ACNAME]=$(get_acodec_name $(translate_ffmpeg_acodec_id "${fi[$ACODEC]}")) |
VID_FFMPEG=("${fi[@]}") |
} |
|
# Use the available tools to identify video meta-data |
# fills $VID with the values |
# Return codes: |
# 3: Failed to detect length |
# 4: Failed to detect width or height |
# identify_video($1 = file) |
identify_video() { |
trace $FUNCNAME $@ |
local RET_NOLEN=3 RET_NODIM=4 |
|
[ "$MPLAYER" ] && mplayer_identify "$1" |
# ffmpeg_identify in DVD mode only works when the DVD is mounted: |
[ $DVD_MODE -eq 0 ] && [ "$FFMPEG" ] && ffmpeg_identify "$1" |
[ $DVD_MODE -eq 1 ] && [ "$FFMPEG" ] && [ "$DVD_MOUNTP" ] && ffmpeg_identify "$1" |
|
# Fail early if none detected length |
[ -z "${VID_MPLAYER[$LEN]}" ] && [ -z "${VID_FFMPEG[$LEN]}" ] && return $RET_NOLEN |
|
# Classic mode, use both mplayer and ffmpeg when available |
if [ "$MPLAYER" ] && [ "$FFMPEG" ]; then |
# By default take mplayer's values |
VID=("${VID_MPLAYER[@]}") |
# FFmpeg seems better at getting the correct number of FPS, specially with |
# WMVs, where mplayer often accepts 1000fps while ffmpeg notices the |
# inconsistency in container vs codec and guesses better, *but* it only |
# uses two decimals so 23.976 becomes 23.98. So it is only used when |
# the number of decimals seems right. |
# When a "Seems..." line is printed the correct FPS can be obtained though. |
[ -z "${VID_MPLAYER[$FPS]}" ] && VID[$FPS]=${VID_FFMPEG[$FPS]} |
[ "${VID_MPLAYER[$FPS]}" ] && [ "${VID_FFMPEG[$FPS]}" ] && { |
# Trust ffmpeg if it has three decimals OR if mplayer is probably-wrong |
local ffps=${VID_FFMPEG[$FPS]} |
echo $ffps | grep -q '\.[0-9][0-9][0-9]' && VID[$FPS]=$ffps || { |
fptest "${VID_MPLAYER[$FPS]}" -gt 500 && VID[$FPS]=$ffps |
} |
} |
# It doesn't appear to need any workarounds for num. channels either |
[ "${VID_FFMPEG[$CHANS]}" ] && VID[$CHANS]=${VID_FFMPEG[$CHANS]} |
[ "${VID_FFMPEG[$ASPECT]}" ] && VID[$ASPECT]=${VID_FFMPEG[$ASPECT]} |
# There's a huge inconsistency with some files, both mplayer vs ffmpeg |
# same application on different OSes |
local fflen=${VID_FFMPEG[$LEN]} mplen=${VID_MPLAYER[$LEN]} # Shorthands |
[ -z "$fflen" ] && fflen=0 |
# If both report 0, there's no good value... |
fptest "$fflen" -eq 0 && fptest "$mplen" -eq 0 && return $RET_NOLEN |
if [ $DVD_MODE -eq 0 ] && [ $QUIRKS -eq 0 ]; then # In DVD mode ffmpeg has no length |
# Quirks disabled, should be enabled? |
local delta=$(abs $(awkex "($fflen - $mplen)")) |
# If they don't agree, take the shorter as a starting point, |
#+if both are different than zero take min, if one of them is 0, take max to start |
if fptest "$fflen" -ne 0 && fptest "$mplen" -ne 0 ; then |
VID[$LEN]=$(min $fflen $mplen) |
else |
VID[$LEN]=$(max $fflen $mplen) |
delta=$QUIRKS_LEN_THRESHOLD # Ensure it's considered inconsistent |
fi |
# If they differ too much, enter safe mode. If one reports 0, they'll differ... |
# FIXME: If $decoder reports 0, can it seek?? |
if fptest "$delta" -ge $QUIRKS_LEN_THRESHOLD ; then |
warn "Found inconsistency in reported length. Safe measuring enabled." |
QUIRKS=1 |
fi |
fi |
elif [ "$MPLAYER" ]; then |
# Must do with mplayer only... |
VID=("${VID_MPLAYER[@]}") |
# Warn if a known pitfall is found |
# See above for 1000 fps |
[ "${VID[$FPS]}" == "1000.00" ] && \ |
warn "Possible inaccuracy in FPS detection." && \ |
warn " Install both mplayer and ffmpeg for better detection." |
# Number of channels 0 happened for WMA in non-x86 |
[ "${VID[$CHANS]}" == "0" ] && \ |
warn "Failed to detect number of audio channels." && \ |
warn " Install both mplayer and ffmpeg for better detection." |
elif [ "$FFMPEG" ]; then |
# Must do with mplayer only... |
VID=("${VID_FFMPEG[@]}") |
# So far I know of no weird results. Yet. |
else |
assert $LINENO 'false' |
fi |
|
# Ensure sanity of the most important values |
is_float "${VID[$LEN]}" || return $RET_NOLEN |
is_number "${VID[$W]}" && is_number "${VID[$H]}" || return $RET_NODIM |
|
if [ "$FFMPEG" ]; then |
# FPS at least with two decimals |
if [ $(awkex "int(${VID[$FPS]})") == ${VID[$FPS]} ]; then |
VID[$FPS]="${VID[$FPS]}.00" |
fi |
fi |
|
local mfps="${VID_MPLAYER[$FPS]}" |
if [ $QUIRKS -eq 0 ] && [ "$MPLAYER" ] && fptest "$mfps" -eq 1000 ; then |
warn "Suspect file. Safe measuring enabled." |
QUIRKS=1 |
fi |
|
# Last safeguard: Try to reach the detected length, if it fails, trigger |
# quirks mode |
if [ $QUIRKS -eq 0 ]; then |
create_temp_dir |
if ! probe_video "$1" "${VID[$LEN]}" ; then |
warn "Detected video length can't be reached. Safe measuring enabled." |
QUIRKS=1 |
fi |
fi |
|
if [ $QUIRKS -eq 1 ]; then |
VID[$LEN]=$(safe_length_measure "$1") |
if [ -z "${VID[$LEN]}" ]; then |
error "Couldn't measure length in a reasonable amount of tries." |
if [ $INTERNAL_MAXREWIND_REACHED -eq 1 ]; then |
error " Will not be able to capture this file with the current settings." |
else |
local reqs=$(( $INTERNAL_WS_C + 1 )) reqp='' |
[ $reqs -eq 1 ] && reqp=" -WP" || reqp=" -WP$reqs" |
[ $reqs -ge 3 ] && reqs=" -WS" || { # Third try => Recommend -WS |
[ $reqs -eq 1 ] && reqs=" -Ws" || reqs=" -Ws$reqs" |
} |
assert 'fptest "$QUIRKS_MAX_REWIND" -gt 0' |
local offby=$(pretty_stamp $QUIRKS_MAX_REWIND) |
warn " Capturing won't work, video is at least $offby shorter than reported." |
local dname='ffmpeg' |
[ $decoder -eq $DEC_MPLAYER ] && dname='mplayer' |
warn " Does $dname support ${VID[$VCODEC]}?." |
warn " Try re-running with$reqs$reqp." |
fi |
return 1 |
fi |
elif [ $QUIRKS -eq -2 ]; then |
warn "Safe mode disabled." |
fi |
|
# Re-check sanity of the most important values |
is_float "${VID[$LEN]}" || return $RET_NOLEN |
is_number "${VID[$W]}" && is_number "${VID[$H]}" || return $RET_NODIM |
} |
|
dump_idinfo() { |
trace $FUNCNAME $@ |
[ "$MPLAYER" ] && echo "Mplayer: $MPLAYER" |
[ "$FFMPEG" ] && echo "FFmpeg: $FFMPEG" |
[ "$MPLAYER" ] && cat <<-EODUMP |
=========== Mplayer Identification =========== |
Length: $(pretty_stamp ${VID_MPLAYER[$LEN]}) |
Video |
Codec: ${VID_MPLAYER[$VCODEC]} (${VID_MPLAYER[$VCNAME]}) |
Dimensions: ${VID_MPLAYER[$W]}x${VID_MPLAYER[$H]} |
FPS: ${VID_MPLAYER[$FPS]} |
Aspect: ${VID_MPLAYER[$ASPECT]} |
Audio |
Codec: ${VID_MPLAYER[$ACODEC]} (${VID_MPLAYER[$ACNAME]}) |
Channels: ${VID_MPLAYER[$CHANS]} |
============================================== |
|
EODUMP |
local ffl="${VID_FFMPEG[$LEN]}" |
[ "$ffl" ] && ffl=$(pretty_stamp "$ffl") |
[ -z "$ffl" ] && [ $DVD_MODE -eq 1 ] && ffl="(unavailable in DVD mode)" |
[ "$FFMPEG" ] && cat <<-EODUMP |
=========== FFmpeg Identification =========== |
Length: $ffl |
Video |
Codec: ${VID_FFMPEG[$VCODEC]} (${VID_FFMPEG[$VCNAME]}) |
Dimensions: ${VID_FFMPEG[$W]}x${VID_FFMPEG[$H]} |
FPS: ${VID_FFMPEG[$FPS]} |
Aspect: ${VID_FFMPEG[$ASPECT]} |
Audio |
Codec: ${VID_FFMPEG[$ACODEC]} (${VID_FFMPEG[$ACNAME]}) |
Channels: ${VID_FFMPEG[$CHANS]} |
============================================= |
|
EODUMP |
local xar= |
if [ "${VID[$ASPECT]}" ]; then |
xar=$(keepdecimals "${VID[$ASPECT]}" 4) |
[ "$xar" ] && xar=" ($xar)" |
fi |
cat <<-EODUMP |
=========== Combined Identification =========== |
Length: $(pretty_stamp ${VID[$LEN]}) |
Video |
Codec: ${VID[$VCODEC]} (${VID[$VCNAME]}) |
Dimensions: ${VID[$W]}x${VID[$H]} |
FPS: ${VID[$FPS]} |
Aspect: ${VID[$ASPECT]}$xar |
Audio |
Codec: ${VID[$ACODEC]} (${VID[$ACNAME]}) |
Channels: ${VID[$CHANS]} |
============================================= |
EODUMP |
|
|
} |
|
# Try to pick some font capable of handling non-latin text |
set_extended_font() { |
trace $FUNCNAME $@ |
# This selection includes japanese fonts |
local candidates=$(identify -list font | grep 'Font: ' | \ |
egrep -io '[a-z-]*(kochi|mincho|sazanami|ipafont)[a-z-]*') |
if [ -z "$candidates" ]; then |
error "Unable to auto-select filename font, please provide one (see -fullhelp)" |
return 1 |
else |
if [ "$DEBUG" -eq 1 ]; then |
local list=$(echo "$candidates" | sed 's/^/ >/g') |
inf "Available non-latin fonts detected:" |
inf "$list" |
fi |
fi |
|
# Bias towards the Sazanami family |
if grep -qi 'sazanami' <<<"$candidates" ; then |
FONT_MINCHO=$(grep -i 'sazanami' <<<"$candidates" | head -1) |
else |
FONT_MINCHO=$(head -1 <<<"$candidates") |
fi |
} |
|
|
# Checks if the provided arguments make sense and are allowed to be used |
# together |
coherence_check() { |
trace $FUNCNAME $@ |
# 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)" |
return $EX_USAGE |
fi |
|
if [ $decoder -eq $DEC_MPLAYER ] && [ -z "$MPLAYER" ]; then |
inf "No mplayer available. Using ffmpeg only." |
decoder=$DEC_FFMPEG |
elif [ $decoder -eq $DEC_FFMPEG ] && [ -z "$FFMPEG" ]; then |
inf "No ffmpeg available. Using mplayer only." |
decoder=$DEC_MPLAYER |
fi |
|
if [ $DVD_MODE -eq 1 ] ; then |
# Currently it's not allowed to use dvd mode with more than one input |
# "file" (in this mode, input files are actually dvd titles of the file |
# provided in -V) |
if [ $multiple_input_files -eq 1 ]; then |
error "Only an input file is allowed in DVD mode" |
return $EX_UNAVAILABLE |
fi |
|
# DVD Mode only works with mplayer, the decoder is changed when |
# the DVD mode option is found, so if it's ffmpeg at this point, |
# it's by user request (i.e. -F after -V) |
if [ $decoder -ne $DEC_MPLAYER ]; then |
if [ "$MPLAYER" ]; then |
warn "DVD mode requires the use of mplayer, falling back to it" |
decoder=$DEC_MPLAYER |
else |
error "DVD mode requires the use of mplayer." |
return $EX_UNAVAILABLE |
fi |
fi |
fi |
|
local filter= |
if [ $DISABLE_TIMESTAMPS -eq 0 ] && |
local -a filts=( ) |
has_filter filt_polaroid && has_filter filt_apply_stamp ; then |
|
for filter in ${FILTERS_IND[@]} ; do |
if [ "$filter" == "filt_polaroid" ]; then |
filts=( "${filts[@]}" "$filter" filt_apply_stamp ) |
elif [ "$filter" == "filt_apply_stamp" ]; then |
continue; |
else |
filts=( "${filts[@]}" $filter ) |
fi |
done |
FILTERS_IND=( "${filts[@]}" ) |
unset filts |
fi |
# The shoftshadow and randrot filters must be in the correct place |
# or they will affect the image incorrectly. |
# Additionally the default filters can be disabled from the command |
# line (with --disable), they're removed from the filter chain here |
local -a filts=( ) end_filts=( ) |
for filter in ${FILTERS_IND[@]} ; do |
case "$filter" in |
filt_softshadow) |
# Note the newer soft shadows code (1.0.99 onwards) behaves slightly |
# differently. On previous versions disabling shadows only affected |
# the montage shadow (but e.g. the polaroid mode preserved them), |
# this is no longer true |
if [ $DISABLE_SHADOWS -ne 1 ]; then |
end_filts[100]="filt_softshadow" |
fi |
;; |
filt_apply_stamp) |
if [ $DISABLE_TIMESTAMPS -ne 1 ]; then |
filts=( "${filts[@]}" "$filter" ) |
fi |
;; |
filt_randrot) end_filts[200]="filt_randrot" ;; |
*) filts=( "${filts[@]}" "$filter" ) ;; |
esac |
done |
FILTERS_IND=( "${filts[@]}" "${end_filts[@]}" ) |
|
# Override-able options check, in case they were set from overrides instead |
#+of equivalent command-line options. Will check the actual selected values, |
#+i.e. fail silently if the overrides aren't effective |
[ "$USR_th_height" ] && { check_height "$th_height" || exit $? ; } |
[ "$USR_numcaps" ] && { check_numcaps "$numcaps" || exit $? ; } |
[ "$USR_interval" ] && { check_interval "$interval" || exit $? ; } |
# Interval=0 == default interval |
fptest "$interval" -eq 0 && interval=$DEFAULT_INTERVAL |
|
sanitise_rename_pattern |
} |
|
check_height() { # Acceptable height |
if ! is_number "$1" && ! is_percentage "$1" ; then |
error "Height must be a (positive) number or a percentage. Got '$1'." |
return $EX_USAGE |
fi |
} |
|
check_numcaps() { # Acceptable numcaps |
if ! is_number "$1" ; then |
error "Number of captures must be a (positive) a number! Got '$1'." |
return $EX_USAGE |
fi |
if [ $1 -eq 0 ]; then |
error "Number of captures must be greater than 0! Got '$1'." |
return $EX_USAGE |
fi |
} |
|
check_interval() { # Acceptable interval |
if ! get_interval "$1" >/dev/null ; then |
error "Incorrect interval format. Got '$1'." |
return $EX_USAGE |
fi |
} |
|
# Main function. |
# Creates the contact sheet. |
# process($1 = file) |
process() { |
trace $FUNCNAME $@ |
local f=$1 |
|
local numcols= |
# Save variables that will be overwritten and must be reset with multiple files |
# pre_* will contain the user-supplied or default values |
local pre_quirks=$QUIRKS |
local pre_aspect_ratio=$aspect_ratio |
local pre_output_format="$output_format" |
INTERNAL_MAXREWIND_REACHED=0 # Reset for each file |
|
# XXX: Some of this should be moved to coherence_check |
if [ $DVD_MODE -eq 1 ]; then # DVD Mode |
f="$DVD_FILE" |
DVD_DEVICE= |
local dvdn=$(realpathr "$f") # dvdn might be a device or an ISO |
if [ -f "$dvdn" ]; then |
# It's an ISO |
DVD_MOUNTP=$(get_dvd_image_mountpoint) |
if [ -z "$DVD_MOUNTP" ]; then |
# Only in Linux does this matter |
if ! is_linux ; then |
warn "Video properties detection for ISO files is not accurate" |
else |
warn "Mount DVD image to get better video properties detection" |
fi |
fi |
elif [ ! -r "$dvdn" ]; then |
# It's something else we cannot read |
error "Can't access DVD ($f)" |
return $EX_NOINPUT |
else |
# It's a device. Note BSD has no concept of block devices. |
# It MUST be mounted to continue. This is required to allow ffmpeg detection |
#+and to calculate file size |
if ! mount | egrep -q "^$dvdn\ " ; then |
error "DVD mode requires device ($f) to be mounted" |
return $EX_UNAVAILABLE |
fi |
DVD_DEVICE="$dvdn" |
DVD_MOUNTP=$(mount | grep -o "^$DVD_DEVICE *on [^ ]*" | cut -d' ' -f3) |
dvdn="DVD" |
fi |
inf "Processing $dvdn..." |
unset dvdn |
if ! is_number "$1" ; then |
error "DVD Title must be a number (e.g.: \$ vcs -V /dev/dvd 1)" |
exit $EX_USAGE |
fi |
DVD_TITLE=$1 |
if [ $DVD_TITLE -eq 0 ]; then |
local dt="$(lsdvd "$DVD_FILE" 2>/dev/null | grep 'Longest track:' | \ |
cut -d' ' -f3- | sed 's/^0*//')" |
if ! is_number "$dt" ; then |
error "Failed to autodetect longest DVD title" |
exit $EX_INTERNAL |
fi |
DVD_TITLE=$dt |
DVD_VTS=$(lsdvd -t$DVD_TITLE -v "$DVD_FILE" 2>/dev/null | grep -o 'VTS: [0-9]*' | cut -d' ' -f2) |
unset dt |
inf "Using DVD Title #$DVD_TITLE (VTS: $DVD_VTS)" |
fi |
else # Not DVD Mode: |
if [ ! -f "$f" ]; then |
error "File \"$f\" doesn't exist" |
return $EX_NOINPUT |
fi |
|
inf "Processing $f..." |
fi |
|
# {{SET_E}} Beware, set -e will break this |
identify_video "$f" |
local ecode=$? |
[ $ecode -eq 0 ] || { |
case $ecode in |
3) error "Unable to find length of file \"$f\". Can't continue." ;; |
4) error "Unable to detect dimensions of file \"$f\". Can't continue." ;; |
*) error "Failure while analysing file \"$f\". Can't continue." ;; |
esac |
return $EX_UNAVAILABLE |
} |
|
# Identification-only mode? |
[ "$UNDFLAG_IDONLY" ] && dump_idinfo && return 0 |
|
# Vidcap/Thumbnail height |
local vidcap_height=$th_height |
if is_percentage "$th_height" ; then |
local pc=${th_height/%%/} # BASH %% == RE %$ |
vidcap_height=$(awkex "int ((${VID[$H]} * ${pc}) / 100 + 0.5)") |
inf "Height: $th_height of ${VID[$H]} = $vidcap_height" |
fi |
if ! is_number "$vidcap_height" || [ "$vidcap_height" -eq 0 ]; then |
vidcap_height=${VID[$H]} |
fi |
# -2: DVD Mode autodetection => If ffmpeg/mplayer was unable autodetect, otherwise |
#+ honor detected value |
[ "-2" == "$aspect_ratio" ] && [ -z "${VID[$ASPECT]}" ] && aspect_ratio=-1 |
[ "-2" == "$aspect_ratio" ] && [ "${VID[$ASPECT]}" ] && aspect_ratio=0 |
if [ "0" == "$aspect_ratio" ]; then |
if [ "${VID[$ASPECT]}" ]; then |
# Aspect ratio in file headers, honor it |
aspect_ratio=$(awkex "${VID[$ASPECT]}") |
else |
aspect_ratio=$(awkex "${VID[$W]} / ${VID[$H]}") |
fi |
elif [ "-1" == "$aspect_ratio" ]; then |
aspect_ratio=$(guess_aspect ${VID[$W]} ${VID[$H]}) |
inf "Aspect ratio set to $aspect_ratio." |
fi |
local vidcap_width=$(compute_width $vidcap_height) |
|
local numsecs=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2 | cut -d. -f1) |
|
local nc=$numcaps |
|
create_temp_dir |
|
unset TIMECODES |
# Compute the stamps (if in auto mode)... |
if [ $manual_mode -eq 1 ]; then |
# Note TIMECODES must be set as an array to get the correct count in |
# manual mode; in automatic mode it will be set correctly inside |
# compute_timecodes() |
TIMECODES=( ${initial_stamps[@]} ) |
else |
TIMECODES=${initial_stamps[@]} |
compute_timecodes $timecode_from $interval $numcaps || { |
return $? |
} |
fi |
|
local output=$(new_temp_file '-preview.png') |
local VIDCAPFILE=00000005.png |
|
# If the temporal vidcap already exists, abort |
if [ -f $VIDCAPFILE ]; then |
error "File $VIDCAPFILE exists and would be overwritten, move it out before running." |
return $EX_CANTCREAT |
fi |
# mplayer will re-write also 00000001.png-00000004.png |
if [ $decoder -eq $DEC_MPLAYER ]; then |
for f_ in 1 2 3 4; do |
if [ -f "0000000${f_}.png" ]; then |
error "File 0000000${f_}.png exists and would be overwritten, move it out before running." |
return $EX_CANTCREAT |
fi |
done |
fi |
|
TEMPSTUFF=( "${TEMPSTUFF[@]}" "$VIDCAPFILE" ) |
|
# Highlights |
local hlfile n=1 # hlfile Must be outside the if! |
if [ "$HLTIMECODES" ]; then |
local hlcapfile= pretty= |
local -a capfiles |
for stamp in $(clean_timestamps "${HLTIMECODES[*]}"); do |
if fptest $stamp -gt $numsecs ; then let 'n++' && continue ; fi |
pretty=$(pretty_stamp $stamp) |
inf "Generating highlight #${n}/${#HLTIMECODES[@]} ($pretty)..." |
|
capture "$f" $stamp || return $? |
filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height $CTX_HL $n || { |
local r=$? |
error "Failed to apply transformations to the capture." |
return $r |
} |
|
hlcapfile=$(new_temp_file "-hl-$(pad 6 $n).png") |
mvq "$VIDCAPFILE" "$hlcapfile" |
capfiles=( "${capfiles[@]}" "$hlcapfile" ) |
let 'n++' |
done |
|
let 'n--' # There's an extra inc |
if [ "$n" -lt "$cols" ]; then |
numcols=$n |
else |
numcols=$cols |
fi |
|
inf "Composing highlights contact sheet..." |
hlfile=$( create_contact_sheet $numcols $CTX_HL $vidcap_width $vidcap_height "${capfiles[@]}" ) |
unset hlcapfile pretty n capfiles numcols |
fi |
unset n |
|
# Normal captures |
# TODO: Don't reference $VIDCAPFILE |
local capfile pretty n=1 |
unset capfiles ; local -a capfiles |
for stamp in $(clean_timestamps "${TIMECODES[*]}"); do |
pretty=$(pretty_stamp $stamp) |
inf "Generating capture #${n}/${#TIMECODES[*]} ($pretty)..." |
|
capture "$f" $stamp || return $? |
filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height $CTX_STD $n || return $? |
|
# identified by capture number, padded to 6 characters |
capfile=$(new_temp_file "-cap-$(pad 6 $n).png") |
mvq "$VIDCAPFILE" "$capfile" |
capfiles=( "${capfiles[@]}" "$capfile" ) |
let 'n++' # $n++ |
done |
#filter_all_vidcaps "${capfiles[@]}" |
|
let 'n--' # there's an extra inc |
if [ "$n" -lt "$cols" ]; then |
numcols=$n |
else |
numcols=$cols |
fi |
|
inf "Composing standard contact sheet..." |
output=$(create_contact_sheet $numcols $CTX_STD $vidcap_width $vidcap_height "${capfiles[@]}") |
unset capfile capfiles pretty n # must carry on to the extended caps: numcols |
|
# Extended mode |
local extoutput= |
if [ "$extended_factor" != 0 ]; then |
# Number of captures. Always rounded to a multiplier of *double* the |
# number of columns (the extended caps are half width, this way they |
# match approx with the standard caps width) |
local hlnc=$(rtomult "$(( ${#TIMECODES[@]} * $extended_factor ))" $((2*$numcols))) |
|
unset TIMECODES # required step to get the right count |
declare -a TIMECODES # Note the manual stamps are not included anymore |
compute_timecodes $TC_NUMCAPS "" $hlnc |
unset hlnc |
|
local n=1 w= h= capfile= pretty= |
unset capfiles ; local -a capfiles |
# The image size of the extra captures is 1/4, adjusted to compensante the padding |
let 'w=vidcap_width/2-HPAD, h=vidcap_height*w/vidcap_width' |
for stamp in $(clean_timestamps "${TIMECODES[*]}"); do |
pretty=$(pretty_stamp $stamp) |
inf "Generating capture from extended set: ${n}/${#TIMECODES[*]} ($pretty)..." |
capture "$f" $stamp || return $? |
filter_vidcap "$VIDCAPFILE" $pretty $w $h $CTX_EXT $n || return $? |
|
capfile=$(new_temp_file "-excap-$(pad 6 $n).png") |
mvq "$VIDCAPFILE" "$capfile" |
capfiles=( "${capfiles[@]}" "$capfile" ) |
let 'n++' |
done |
|
let 'n--' # There's an extra inc |
if [ $n -lt $(( $cols * 2 )) ]; then |
numcols=$n |
else |
numcols=$(( $cols * 2 )) |
fi |
|
inf "Composing extended contact sheet..." |
extoutput=$( create_contact_sheet $numcols $CTX_EXT $w $h "${capfiles[@]}" ) |
|
unset w h capfile pretty n numcols |
fi # Extended mode |
|
local vcodec=${VID[$VCNAME]} |
local acodec=${VID[$ACNAME]} |
|
if [ "${VID[$CHANS]}" ] && is_number "${VID[$CHANS]}" &&[ ${VID[$CHANS]} -ne 2 ]; then |
if [ ${VID[$CHANS]} -eq 1 ]; then |
acodec="$acodec (mono)" |
else |
acodec="$acodec (${VID[$CHANS]}ch)" |
fi |
fi |
|
local csw=$(imw "$output") exw= hlw= |
local width=$csw |
if [ "$HLTIMECODES" ] || [ "$extended_factor" != "0" ]; then |
inf "Merging contact sheets..." |
if [ "$HLTIMECODES" ]; then |
local hlw=$(imw "$hlfile") |
if [ $hlw -gt $width ]; then width=$hlw ; fi |
fi |
if [ "$extended_factor" != "0" ]; then |
local exw=$(imw $extoutput) |
if [ $exw -gt $width ]; then width=$exw ; fi |
fi |
fi |
if [ $csw -lt $width ]; then |
local csh=$(imh "$output") |
# Expand the standard set to the maximum width of the sets by padding both sides |
# For some reason the more obvious (to me) convert command-lines lose |
# the transparency |
convert \( -size $(( ($width - $csw) / 2 ))x$csh xc:transparent \) "$output" \ |
\( -size $(( ($width - $csw) / 2 ))x$csh xc:transparent \) +append "$output" |
unset csh |
fi |
|
# If there were highlights then mix them in |
if [ "$HLTIMECODES" ]; then |
# For some reason adding the background also adds padding with: |
# convert \( -background LightGoldenRod "$hlfile" -flatten \) \ |
# \( "$output" \) -append "$output" |
# replacing it with a "-composite" operation apparently works |
# Expand the highlights to the correct size by padding |
local hlh=$(imh "$hlfile") |
if [ $hlw -lt $width ]; then |
convert \( -size $(( ($width - $hlw) / 2 ))x$hlh xc:transparent \) "$hlfile" \ |
\( -size $(( ($width - $hlw) / 2 ))x$hlh xc:transparent \) +append "$hlfile" |
fi |
convert \( -size ${width}x${hlh} xc:LightGoldenRod "$hlfile" -composite \) \ |
\( -size ${width}x1 xc:black \) \ |
"$output" -append "$output" |
unset hlh |
fi |
# Extended captures |
if [ "$extended_factor" != 0 ]; then |
# Already set local exw=$(imw "$extoutput") |
local exh=$(imh "$extoutput") |
if [ $exw -lt $width ]; then |
# Expand the extended set to be the correct size |
convert \( -size $(( ($width - $exw) / 2 ))x$exh xc:transparent \) "$extoutput" \ |
\( -size $(( ($width - $exw) / 2 ))x$exh xc:transparent \) +append "$extoutput" |
fi |
convert "$output" -background Transparent "$extoutput" -append "$output" |
fi |
# Add the background; -trim added in 1.11. I'm unsure of why but whithout trimmin extra blank |
#+space is added at the top |
local dotrim= |
[ $DISABLE_SHADOWS -eq 1 ] && [ -z "$HLTIMECODES" ] && dotrim=-trim |
convert -background "$bg_contact" "$output" -flatten $dotrim "$output" |
|
|
# Let's add meta inf and signature |
inf "Adding header and footer..." |
local meta2="Dimensions: ${VID[$W]}x${VID[$H]} |
Format: $vcodec / $acodec |
FPS: ${VID[$FPS]}" |
local signature |
if [ $anonymous_mode -eq 0 ]; then |
signature="$user_signature $user |
with $PROGRAM_SIGNATURE" |
else |
signature="Created with $PROGRAM_SIGNATURE" |
fi |
local headwidth=$(imw "$output") |
# TODO: Use a better height calculation |
local headheight=$(($pts_meta * 4 )) |
local heading=$(new_temp_file .png) |
# Add the title if any |
if [ "$title" ]; then |
# TODO: Use a better height calculation |
convert \ |
\( \ |
-size ${headwidth}x$(( $pts_title + ($pts_title/2) )) "xc:$bg_title" \ |
-font "$font_title" -pointsize "$pts_title" \ |
-background "$bg_title" -fill "$fg_title" \ |
-gravity Center -annotate 0 "$title" \ |
\) \ |
-flatten \ |
"$output" -append "$output" |
fi |
local fn_font= # see $font_filename |
case $font_filename in |
$FF_DEFAULT) fn_font="$font_heading" ;; |
$FF_MINCHO) fn_font="$FONT_MINCHO" ;; |
*) |
warn "\$font_filename was overridden with an incorrect value, using default." |
fn_font="$font_heading" |
;; |
esac |
# Talk about voodoo... feel the power of IM... let's try to explain what's this: |
# It might technically be wrong but it seems to work as I think it should |
# (hence the voodoo I was talking) |
# Parentheses restrict options inside them to only affect what's inside too |
# * Create a base canvas of the desired width and height 1. The width is tweaked |
# because using "label:" later makes the text too close to the border, that |
# will be compensated in the last step. |
# * Create independent intermediate images with each row of information, the |
# filename row is split in two images to allow changing the font, and then |
# they're horizontally appended (and the font reset) |
# * All rows are vertically appended and cropped to regain the width in case |
# the filename is too long |
# * The appended rows are appended to the original canvas, the resulting image |
# contains the left row of information with the full heading width and |
# height, and this is the *new base canvas* |
# * Draw over the new canvas the right row with annotate in one |
# operation, the offset compensates for the extra pixel from the original |
# base canvas. XXX: Using -annotate allows setting alignment but it breaks |
# vertical alignment with the other rows' labels. |
# * Finally add the border that was missing from the initial width, we have |
# now the *complete header* |
# * Add the contact sheet and append it to what we had. |
# * Start a new image and annotate it with the signature, then append it too. |
local filename_label="Filename" |
local filesize_label="File size" |
local filename_value= |
local filesize_value= |
if [ $DVD_MODE -eq 1 ]; then |
# lsdvd is guaranteed to be installed if DVD mode is enabled |
local dvd_label=$(lsdvd "$DVD_FILE" 2>/dev/null | grep -o 'Disc Title: .*' | cut -d' ' -f3-) |
# Need a mountpoint to get the actual *title* size |
if [ "$DVD_MOUNTP" ]; then |
filename_label="Disc label" |
filename_value="$dvd_label" |
filesize_label="Title size" |
filesize_value="$(get_dvd_size)" |
else |
# Not mounted. We can get the disc size but this will include any other titles. |
# Since 1.11 mounting DVDs is mandatory to get the title size. Both for ISOs and |
#+ devices |
filename_value="$(basename "$DVD_FILE") $filename_value (DVD Label: $dvd_label)" |
is_linux && warn "DVD not mounted: Can't detect title file size." |
filesize_label='Disc image size' |
filesize_value="$(get_pretty_size $(dur "$f"))" |
fi |
else |
filename_value="$(basename "$f")" |
filesize_value="$(get_pretty_file_size "$f")" |
fi |
convert \ |
\( \ |
-size $(( ${headwidth} -18 ))x1 "xc:$bg_heading" +size \ |
-font "$font_heading" -pointsize "$pts_meta" \ |
-background "$bg_heading" -fill "$fg_heading" \ |
\( \ |
-gravity West \ |
\( label:"$filename_label: " \ |
-font "$fn_font" label:"$filename_value" +append \ |
\) \ |
-font "$font_heading" \ |
label:"$filesize_label: $filesize_value" \ |
label:"Length: $(cut -d'.' -f1 <<<$(pretty_stamp ${VID[$LEN]}))" \ |
-append -crop ${headwidth}x${headheight}+0+0 \ |
\) \ |
-append \ |
\( \ |
-size ${headwidth}x${headheight} \ |
-gravity East -fill "$fg_heading" -annotate +0-1 "$meta2" \ |
\) \ |
-bordercolor "$bg_heading" -border 9 \ |
\) \ |
"$output" -append \ |
\( \ |
-size ${headwidth}x34 -gravity Center "xc:$bg_sign" \ |
-font "$font_sign" -pointsize "$pts_sign" \ |
-fill "$fg_sign" -annotate 0 "$signature" \ |
\) \ |
-append \ |
"$output" |
unset signature meta2 headwidth headheight heading fn_font |
|
local wanted_name=${OUTPUT_FILES[$FILEIDX]} |
[ "$wanted_name" ] && \ |
if egrep -q '\.[^\.]+$' <<<"$wanted_name" ; then |
output_format=$(filext "$wanted_name") |
inf "Output format set from output filename" |
else # No file extension in wanted_name |
wanted_name="$wanted_name.$output_format" |
fi |
[ "$wanted_name" ] || wanted_name="$(basename "$f").$output_format" |
|
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" "$wanted_name" ) || { |
error "Failed to write the output file!" |
return $EX_CANTCREAT |
} |
inf "Done. Output wrote to $output_name" |
|
let 'FILEIDX++,1' #,1 so that it's always ok |
[ "$UNDFLAG_HANG" ] && read -p 'Main loop paused, hit Enter key to continue... ' |
cleanup |
|
# Re-set variables (for multi-file input) |
QUIRKS=$pre_quirks |
aspect_ratio=$pre_aspect_ratio |
output_format="$pre_output_format" |
} |
|
# }}} # Core functionality |
|
# {{{ # Debugging helpers |
|
# Tests integrity of some operations. |
# Used to test internal changes for consistency. |
# It helps me to identify incorrect optimizations. |
# internal_integrity_test(). Running with -D triggers this. |
internal_integrity_test() { |
local t op val ret comm retval=0 |
|
# Replacements |
local SEQ=$(type -pf seq) |
local JOT=$(type -pf jot) |
local ex rex |
if [ "$SEQ" ]; then |
ex=$($SEQ 1 10) |
elif [ "$JOT" ]; then |
ex=$($JOT 10 1) |
else |
warn "Can't check seqr() correctness, neither seq nor jot found" |
fi |
if [ "$ex" ]; then |
exr=$(seqr 1 10) |
if [ "$exr" != "$ex" ]; then |
error "Failed test: seqr() not consistent with external result" |
let 'retval++,1' |
else |
inf "Passed test (seq replacement): consistent result" |
fi |
fi |
|
# Textual tests, compare output to expected output |
# Tests are in the form "operation arguments correct_result #Description" |
TESTS=( # Note bash2 doesn't like this array as a local variable |
# TODO: UNIX vs GNU |
#"stonl ..." |
|
"rmultiply 1,1 1 #Identity" |
"rmultiply 16/9,1 2 #Rounding" # 1.77 rounded 2 |
"rmultiply 4/3 1 #Rounding" # 1.33 rounded 1 |
"rmultiply 1,16/9 2 #Commutative property" |
"rmultiply 1.7 2 #Alternate syntax" |
|
"ceilmultiply 1,1 1 #" |
"ceilmultiply 4/3 2 #" # 1.33 rounded 2 |
"ceilmultiply 7,1/2 4 #" # 3.5 rounded 4 |
"ceilmultiply 7/2 4 #Alternative syntax" |
"ceilmultiply 1/2,7 4 #Commutative property" |
|
"pad 10 0 0000000000 #Padding" |
"pad 1 20 20 #Unneeded padding" |
"pad 5 23.3 023.3 #Floating point padding" |
|
"guess_aspect 720 576 4/3 #DVD AR Guess" |
"guess_aspect 1024 576 1024/576 #Unsupported Wide AR Guess" |
|
"tolower ABC abc #lowercase conversion" |
|
"pyth_th 4 3 5 #Integer pythagorean theorem" |
#bc result: "pyth_th 16 9 18.35755975068581929849 #FP pythagorean theorem" |
#perl result: "pyth_th 16 9 18.3575597506858 #FP pythagorean theorem" |
"pyth_th 16 9 18.3576 #FP pythagorean theorem" |
|
"get_interval 2h 7200 #Hours parsing" |
"get_interval 2m 120 #Minutes parsing" |
"get_interval 30S 30 #Seconds parsing" |
"get_interval .30 .30 #Milliseconds parsing" |
# Since now the numbers are passed to perl, leading zeroes become octal |
# numbers. Must ensure they are handled correctly |
"get_interval 09h010m09s1 33010 #Parsing with leading zeroes" |
"get_interval 0400 400 #Parsing shorthand" |
# Extended syntax |
"get_interval 30m30m1h 7200 #Repeated minutes parsing" |
) |
|
for t in "${TESTS[@]}" ; do |
# Note the use of ! as separator, this is because # and / are used in |
# many of the inputs |
comm=$(sed 's!.* #!!g' <<<$t) |
# Expected value |
val=$(grep -o "[^ ]* #$comm\$"<<<$t | cut -d' ' -f1) |
op=$(sed "s! $val #$comm\$!!g" <<<$t) |
if [ -z "$comm" ]; then |
comm=unnamed |
fi |
ret=$($op) || true |
|
if [ "$ret" != "$val" ] && fptest "$ret" -ne "$val" ; then |
error "Failed test ($comm): '$op $val'. Got result '$ret'." |
let 'retval++,1' # The ,1 ensures let doesn't fail |
else |
inf "Passed test ($comm): '$op $val'." |
fi |
done |
|
# Returned value tests, compare return to expected return |
TESTS=( |
# Don't use anything with a RE meaning |
|
# Floating point numeric "test" |
"fptest 3 -eq 3 0 #FP test" |
"fptest 3.2 -gt 1 0 #FP test" |
"fptest 1/2 -le 2/3 0 #FP test" |
"fptest 6.34 -gt 6.34 1 #FP test" |
"fptest (1>0) -eq 1 0 #FP -logical- test" |
|
"is_number 3 0 #Numeric recognition" |
"is_number '3' 1 #Quoted numeric recognition" |
"is_number 3.3 1 #Non-numeric recognition" |
|
"is_float 3.33 0 #Float recognition" |
"is_float 3 0 #Float recognition" |
"is_float 1/3 1 #Non-float recognition" |
|
"is_fraction 1/1 0 #Fraction recognition" |
"is_fraction 1 1 #non-fraction recognition" |
"is_fraction 1.1 1 #Non-fraction recognition" |
) |
for t in "${TESTS[@]}"; do |
comm=$(sed 's!.* #!!g' <<<$t) |
# Expected value |
val=$(grep -o "[^ ]* #$comm\$"<<<$t | cut -d' ' -f1) |
op=$(sed "s! $val #$comm\$!!g" <<<$t) |
if [ -z "$comm" ]; then |
comm=unnamed |
fi |
ret=0 |
$op || { |
ret=$? |
} |
|
if [ $val -eq $ret ]; then |
inf "Passed test ($comm): '$op; returns $val'." |
else |
error "Failed test ($comm): '$op; returns $val'. Returned '$ret'" |
let 'retval++,1' |
fi |
done |
|
return $retval |
} |
|
|
# }}} # Debugging helpers |
|
# {{{ # Help / Info |
|
# Prints the program identification to stderr |
show_vcs_info() { # Won't be printed in quiet modes |
local inff=inf |
[ "$HAS_COLORS" ] && inff=infplain |
$inff "Video Contact Sheet *NIX v${VERSION}, (c) 2007-2010 Toni Corvera" "sgr0" |
} |
|
# Prints the list of options to stdout |
# show_help($1 = long = '') |
show_help() { |
local P=$(basename $0) |
local showlong=$1 |
local mpchosen= ffchosen= longhelp= funkyex= |
[ -z "$MPLAYER" ] && mpchosen=' [Not available]' |
[ "$MPLAYER" ] && [ $decoder == $DEC_MPLAYER ] && mpchosen=' [Selected]' |
[ -z "$FFMPEG" ] && ffchosen=', Not available' |
[ "$FFMPEG" ] && [ $decoder == $DEC_FFMPEG ] && ffchosen=', Selected' |
# This portion of help is only shown when in full help mode (--fullhelp) |
[ "$showlong" ] && longhelp=\ |
" --anonymous Disable the 'Preview created by' line in the footer. |
-Ij|-Ik|-Ij=fontname|-Ik=fontname |
--nonlatin Use an alternate font in the heading for the video file |
name. Required to display correctly file names in |
some languages (Chinese, Japanese, Hangul, |
Cyrillic, ...). |
Will try to use a reasonable font. Can also be set |
manually like: |
$ vcs -Ij=Sazanami-Mincho-Regular file.avi |
or |
$ vcs -Ij=/usr/share/fonts/ttf/ttf-japanese-mincho.ttf |
Use \"identify -list font\" to list the available fonts |
-O|--override <arg> Use it to override a variable (see the homepage for |
more details). Format accepted is 'variable=value' (can |
also be quoted -variable=\"some value\"- and can take an |
internal variable too -variable=\"\$SOME_VAR\"-). |
|
Tweaks and workarounds: |
-Ws Increase length of safe measuring (try harder). Repeat |
to increase further. |
-WS Scan all video, if required, to get a safe measuring. |
-Wp Increase safe measuring precission (i.e. halve the |
probe stepping). Repeat to increase further. |
-WP Inverse of -Wp. |
-Wo Change ffmpeg's arguments order, might work with some |
files that fail otherwise. |
-Wc Disable colour in console messages. |
Obscure options, debugging tools and workarounds: |
-R <file> |
--randomsource <file> Use the provided file as a source for random \"values\": |
they won't be random anymore, so two runs with the same |
source and same arguments will produce the same output |
in modes which use using randomisation (e.g. the |
\"photos\" and \"polaroid\" modes). |
-D Debug mode. Used to test features/integrity. It: |
* Prints the input command line |
* Sets the title to reflect the command line |
* Does a basic test of consistency. |
" |
# The --funky help is really long, so make it shorter by default, |
# only show the complete help when --fullhelp is used |
[ "$showlong" ] && funkyex=" |
These are toy output modes in which the contact sheet |
gets a more informal look. |
Order *IS IMPORTANT*. A bad order gets a bad result :P |
They're random in nature so using the same funky mode |
twice will usually lead to quite different results. |
Currently available \"funky modes\": |
\"overlap\": Use '-ko' or '--funky overlap' |
Randomly overlap captures. |
\"rotate\": Use '-kr' or '--funky rotate' |
Randomly rotate each image. |
\"photoframe\": Use '-kf' or '--funky photoframe' |
Adds a photo-like white frame to each image. |
\"polaroidframe\": Use '-kL' or '--funky polaroidframe' |
Adds a polaroid picture-like white frame to each |
image. |
\"photos\": Use '-kc' or '--funky photos' |
Combination of rotate, photoframe and overlap. |
Same as -kp -kr -ko. |
\"polaroid\": Use '-kp' or '--funky polaroid' |
Combination of rotate, polaroidframe and overlap. |
Same as -kL -kr -ko. |
\"film\": Use '-ki' or '--funky film' |
Imitates filmstrip look. |
\"random\": Use '-kx' or '--funky random' |
Randomizes colours and fonts." |
[ -z "$showlong" ] && funkyex=" |
Available: overlap, rotate, photoframe, polaroidframe, |
photos, polaroid, film, random |
Use --fullhelp for more details." |
cat <<EOF |
Usage: $P [options] <file> |
|
Options: |
-i|--interval <arg> 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 <arg> Set the number of captured images to arg. Use either |
-i or -n. |
-c|--columns <arg> Arrange the output in 'arg' columns. |
-H|--height <arg> Set the output (individual thumbnail) height. Width is |
derived accordingly. Note width cannot be manually set. |
-o|--output <file> File name of output. When ommited will be derived from |
the input filename. Can be repeated for multiple files. |
-a|--aspect <aspect> Aspect ratio. Accepts a floating point number or a |
fraction. |
-f|--from <arg> Set starting time. No caps before this. Same format |
as -i. |
-t|--to <arg> Set ending time. No caps beyond this. Same format |
as -i. |
-T|--title <arg> Add a title above the vidcaps. |
-j|--jpeg Output in jpeg (by default output is in png). |
-j2|--jpeg 2 Output in jpeg 2000 |
-V|--dvd <file.iso|dvd_device> |
DVD Mode, use file.iso as DVD. In this mode the |
<file> argument must point to the title number, e.g.: |
$ vcs -V somedvd.iso 1 |
Passing title 0 will use the default (longest) title. |
$ vcs -V /dev/dvd 0 |
Implies -A (auto aspect ratio) |
-M|--mplayer Use Mplayer to capture$mpchosen |
-F|--ffmpeg Use FFmpeg to capture [Default$ffchosen] |
-E|--end_offset <arg> This time is ignored, from the end of the video. Same |
format as -i. This value is not used when a explicit |
ending time is set. By default it is $DEFAULT_END_OFFSET. |
-q|--quiet Don't print progess messages just errors. Repeat to |
mute completely even on error. |
-h|--help Show basic help and exit. |
--fullhelp Show the complete help and exit. |
-d|--disable <arg> Disable some default functionality. |
Features that can be disabled are: |
* timestamps: use -dt or --disable timestamps |
* shadows: use -ds or --disable shadows |
* padding: use -dp or --disable padding |
(note shadows introduce some extra padding) |
-A|--autoaspect Try to guess aspect ratio from resolution. |
-e[num] | --extended=[num] |
Enables extended mode and optionally sets the extended |
factor. -e is the same as -e$DEFAULT_EXT_FACTOR. |
-l|--highlight <arg> Add the image found at the timestamp "arg" as a |
highlight. Same format as -i. |
-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. |
-S|--stamp <arg> Add the image found at the timestamp "arg". Same format |
as -i. |
|
-u|--user <arg> 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. |
-k <arg> |
--funky <arg> Funky modes:$funkyex |
$longhelp |
Examples: |
Create a contact sheet with default values (vidcaps at intervals of |
$DEFAULT_INTERVAL seconds), will be saved to 'video.avi.png': |
\$ $P video.avi |
|
Create a sheet with vidcaps at intervals of 3 and a half minutes, save to |
'output.jpg': |
\$ $P -i 3m30 input.wmv -o output.jpg |
|
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 <http://p.outlyer.net/vcs/>. |
|
EOF |
} |
|
# }}} # Help / Info |
|
#### Entry point #### |
|
# Important to do this before any message can be thrown |
init_feedback |
|
# Ensure $GETOPT is GNU/Linux-style getopt |
choose_getopt |
|
# Execute exithdlr on exit |
trap exithdlr EXIT |
|
show_vcs_info |
|
# Test requirements. Important, must check before looking at the |
# command line (since getopt is used for the task) |
test_programs |
|
# The command-line overrides any configuration. And the configuration |
# is able to change the program in charge of parsing options ($GETOPT) |
load_config |
|
# {{{ # Command line parsing |
|
# TODO: Find how to do this correctly (this way the quoting of $@ gets messed): |
#eval set -- "${default_options} ${@}" |
ARGS="$@" |
|
# [[R0]] |
# TODO: Why does FreeBSD's GNU getopt ignore -n?? |
TEMP=$("$GETOPT" -n "$0" -s bash \ |
-o i:n:u:T:f:t:S:j::hFMH:c:ma:l:De::U::qAO:I:k:W:E:d:V:R:Z:o:P: \ |
--long "interval:,numcaps:,username:,title:,from:,to:,stamp:,jpeg::,help,"\ |
"shoehorn:,mplayer,ffmpeg,height:,columns:,manual,aspect:,highlight:"\ |
"extended::,fullname,anonymous,quiet,autoaspect,override:,mincho,funky:,"\ |
"end_offset:,disable:,dvd:,randomsource:,undocumented:,output:,fullhelp,profile:,"\ |
"jpeg2,nonlatin" \ |
-- "$@") |
eval set -- "$TEMP" |
|
while true ; do |
case "$1" in |
-i|--interval) |
check_interval "$2" |
interval=$(get_interval "$2") |
timecode_from=$TC_INTERVAL |
USR_interval=$interval |
USR_timecode_from=$TC_INTERVAL |
shift # Option arg |
;; |
-n|--numcaps) |
check_numcaps "$2" |
numcaps="$2" |
timecode_from=$TC_NUMCAPS |
USR_numcaps="$2" |
USR_timecode_from=$TC_NUMCAPS |
shift # Option arg |
;; |
-o|--output) |
current=${#OUTPUT_FILES[@]} |
OUTPUT_FILES[$current]="$2" |
shift ;; |
-u|--username) user="$2" ; USR_user="$user" ; shift ;; |
-U|--fullname) |
# -U accepts an optional argument, 0, to make an anonymous signature |
# --fullname accepts no argument |
if [ "$2" ]; then # With argument, special handling |
if [ "$2" != "0" ]; then |
error "Use '-U0' to make an anonymous contact sheet or '-u \"My Name\"'" |
error " to sign as My Name. Got -U$2" |
exit $EX_USAGE |
fi |
anonymous_mode=1 |
shift |
else # No argument, default handling (try to guess real name) |
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 |
fi |
;; |
--anonymous) anonymous_mode=1 ;; # Same as -U0 |
-T|--title) title="$2" ; USR_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 |
USR_fromtime="$fromtime" |
shift |
;; |
-E|--end_offset) |
if ! end_offset=$(get_interval "$2") ; then |
error "End offset must be a valid timecode. Got '$2'." |
exit $EX_USAGE |
fi |
USR_end_offset="$end_offset" |
shift |
;; |
-t|--to) |
if ! totime=$(get_interval "$2") ; then |
error "Ending timestamp must be a valid timecode. Got '$2'." |
exit $EX_USAGE |
fi |
if fptest "$totime" -eq 0 ; then |
error "Ending timestamp was set to 0, set to movie length." |
totime=-1 |
fi |
USR_totime=$totime |
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=( "${HLTIMECODES[@]}" "$temp" ) |
shift |
;; |
--jpeg2) # Note --jpeg 2 is also accepted |
output_format=jp2 |
USR_output_format=jp2 |
;; |
-j|--jpeg) |
if [ "$2" ]; then # Arg is optional, 2 is for JPEG 2000 |
# 2000 is also accepted |
if [ "$2" != "2" ] && [ "$2" != "2000" ]; then |
error "Use -j for jpeg output or -j2 for JPEG 2000 output. Got '-j$2'." |
exit $EX_USAGE |
fi |
output_format=jp2 |
else |
output_format=jpg |
fi |
USR_output_format="$output_format" |
shift |
;; |
-h|--help) show_help ; exit $EX_OK ;; |
--fullhelp) show_help 'full' ; exit $EX_OK ;; |
--shoehorn) |
warn "$1 is deprecated, please use '--undocumented shoehorn=\"$2\"' instead" |
shoehorned="$2" |
shift |
;; |
-F) decoder=$DEC_FFMPEG ; USR_decoder=$decoder ;; |
-M) decoder=$DEC_MPLAYER ; USR_decoder=$decoder ;; |
-H|--height) |
check_height "$2" |
th_height="$2" |
USR_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" |
USR_aspect_ratio="$2" |
shift |
;; |
-A|--autoaspect) aspect_ratio=-1 ; USR_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" |
USR_cols="$2" |
shift |
;; |
-m|--manual) manual_mode=1 ;; |
-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 |
USR_extended_factor=$extended_factor |
shift |
;; |
--mincho) |
warn "--mincho is deprecated, please use -Ij or --nonlatin instead" |
if [ -z "$USR_FONT_MINCHO" ]; then |
set_extended_font |
inf "Filename font set to '$FONT_MINCHO'" |
fi |
;; |
# Unlike -I, --nonlatin does not accept a font name |
--nonlatin) |
if [ -z "$USR_FONT_MINCHO" ]; then |
set_extended_font |
inf "Filename font set to '$FONT_MINCHO'" |
fi |
;; |
-I) |
# Extended/non-latin font |
# New syntax introduced in 1.11: |
# -Ij: Try to pick automatically a CJK font. Might fail and abort |
# -Ij='Font name or file': Set font manually |
|
if [ "$2" ] ; then |
# If an argument is passed, test it is one of the known ones |
case "$2" in |
k|j|k=*|j=*) ;; |
*) error "-I must be followed by j or k!" && exit $EX_USAGE ;; |
esac |
fi |
# It isn't tested for existence because it could also be a font |
# which convert would understand without giving the full path |
font_filename=$FF_MINCHO; |
if [ ${#2} -gt 1 ]; then |
# j=, k= syntax |
FONT_MINCHO="${2:2}" |
USR_FONT_MINCHO="$FONT_MINCHO" |
inf "Filename font set to '$FONT_MINCHO'" |
fi |
# If the user didn't pick one, try to select automatically |
if [ -z "$USR_FONT_MINCHO" ]; then |
set_extended_font |
inf "Filename font set to '$FONT_MINCHO'" |
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 |
if grep -q 'GETOPT=' <<<"$2" ; then |
# If we're here, getopt has already been found and works, so it makes no |
# sense to override it; on the other hand, if it hasn't been correctly |
# set/detected we won't reach here |
warn "GETOPT can't be overridden from the command line." |
else |
override "$2" "command line" |
fi |
shift |
;; |
-W) |
case "$2" in |
# (classic) Workaround mode. See wa_ss_* declarations at the start for details |
o) wa_ss_af='-ss ' ; wa_ss_be='' ;; |
# Console colout |
# Once: Disable console colour, use prefixes instead |
# Twice: Disable prefixes too |
c) |
set_feedback_prefixes |
[ "$UNDFLAG_NOPREFIX" ] && plain_messages=1 |
UNDFLAG_NOPREFIX=1 |
;; |
# Double length of video probed in safe measuring |
# Semi-undocumented traits: |
# - Can be repeated, will double for each instance |
# - -Ws -Ws -Ws = -Ws3 |
s|s[0-9]|s[0-9][0-9]) |
[ ${#2} -gt 1 ] && n=${2:1} || n=1 |
QUIRKS_MAX_REWIND=$(awkex "$QUIRKS_MAX_REWIND * (2^$n)") |
let 'INTERNAL_WS_C+=n,1' |
;; |
# Brute force -Ws: Test all the length of the file if required |
S) QUIRKS_MAX_REWIND=-1 ;; |
# Increase precission of safe length measuring (halve the stepping) |
# Like -Ws can be repeated |
p|p[0-9]|p[0-9][0-9]) |
[ ${#2} -gt 1 ] && n=${2:1} || n=1 |
QUIRKS_LEN_STEP=$(awkex "$QUIRKS_LEN_STEP / (2^$n)") |
let 'INTERNAL_WP_C+=n,1' |
;; |
# Inverse of -Wp: Decrease precission of safe length measuring |
# i.e.: will try less times <-> will be quicker but less accurate |
# desirable when -Ws or -WS are used. |
# Can also be repeated |
P|P[0-9]|P[0-9][0-9]) |
[ ${#2} -gt 1 ] && n=${2:1} || n=1 |
QUIRKS_LEN_STEP=$(awkex "$QUIRKS_LEN_STEP * (2^$n)") |
let 'INTERNAL_WP_C-=n,1' |
;; |
# -Wb (Semi-undocumented): Disable safe mode. Use this to force accepting |
#+broken/partial files. Only makes sense when testing or in combination |
#+with stuff like '-Z idonly' |
b) QUIRKS=-2 ;; # Quirks < 0 : No safe mode |
*) |
error "Wrong argument. Use --fullhelp for a list available workarounds. Got -W$2." |
exit $EX_USAGE |
;; |
esac |
shift |
;; |
-k|--funky) # Funky modes |
case "$2" in # Note older versions (<1.0.99) were case-insensitive |
p|polaroid) # Same as overlap + rotate + polaroid |
inf "Changed to polaroid funky mode." |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_polaroid' 'filt_randrot' ) |
CSHEET_DELEGATE='csheet_overlap' |
# XXX: The newer version has a lot less flexibility with these many |
# hardcoded values... |
grav_timestamp=South |
fg_tstamps=Black |
bg_tstamps=Transparent |
pts_tstamps=$(( $pts_tstamps * 3 / 2 )) |
;; |
c|photos) # Same as overlap + rotate + photoframe, this is the older polaroid |
inf "Changed to photos funky mode." |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_photoframe' 'filt_randrot' ) |
CSHEET_DELEGATE='csheet_overlap' |
# The timestamp must change location to be visible most of the time |
grav_timestamp=NorthWest |
;; |
o|overlap) # Random overlap mode |
CSHEET_DELEGATE='csheet_overlap' |
grav_timestamp=NorthWest |
;; |
r|rotate) # Random rotation |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_randrot' ) |
;; |
f|photoframe) # White photo frame |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_photoframe' ) |
;; |
L|polaroidframe) # White polaroid frame |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_polaroid ') |
grav_timestamp=South |
fg_tstamps=Black |
bg_tstamps=Transparent |
pts_tstamps=$(( $pts_tstamps * 3 / 2 )) |
;; |
i|film) |
inf "Enabled film mode." |
FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_film' ) |
;; |
x|random) # Random colours/fonts |
inf "Enabled random colours and fonts." |
randomize_look |
;; |
*) |
error "Unknown funky mode. Got '$2'." |
exit $EX_USAGE |
;; |
esac |
shift |
;; |
-P|--profile) |
case "$2" in |
classic) # Classic colour scheme |
bg_heading=YellowGreen bg_sign=SlateGray bg_contact=White |
bg_title=White fg_heading=Black fg_sign=Black |
;; |
1.0) # 1.0a, 1.0.1a and 1.0.2b colourscheme |
bg_heading=YellowGreen bg_sign=SandyBrown bg_contact=White |
bg_title=White fg_heading=Black fg_sign=Black |
;; |
esac |
shift |
;; |
-R|--randomsource) |
if [ ! -r "$2" ]; then |
error "Random source file '$2' can't be read" |
exit $EX_USAGE |
fi |
init_filerand "$2" |
inf "Using '$2' as source of semi-random values" |
RANDFUNCTION=filerand |
shift |
;; |
-d|--disable) # Disable default features |
case $(tolower "$2") in |
# timestamp (with no final s) is undocumented but will stay |
t|timestamps|timestamp) |
if [ $DISABLE_TIMESTAMPS -eq 0 ]; then |
inf "Timestamps disabled." |
# They'll be removed from the filter chain in coherence_check |
DISABLE_TIMESTAMPS=1 |
fi |
;; |
s|shadows|shadow) |
if [ $DISABLE_SHADOWS -eq 0 ]; then |
inf "Shadows disabled." |
# They will be removed from the filter chain in coherence_check |
DISABLE_SHADOWS=1 |
fi |
;; |
p|padding) |
if [ $HPAD -ne 0 ] ; then |
inf "Padding disabled." # Kinda... |
HPAD=0 |
fi |
;; |
*) |
error "Requested disabling unknown feature. Got '$2'." |
exit $EX_USAGE |
;; |
esac |
shift |
;; |
-V|--dvd) |
# XXX; Are there systems with no perl??? |
if ! type -pf perl >/dev/null ; then |
error "DVD support requires perl" |
exit $EX_UNAVAILABLE |
fi |
# DVD Mode requires lsdvd |
if ! type -pf lsdvd >/dev/null ; then |
error "DVD support requires the lsdvd program" |
exit $EX_UNAVAILABLE |
fi |
DVD_MODE=1 |
DVD_FILE="$2" |
decoder=$DEC_MPLAYER |
aspect_ratio=-2 # Special value: Auto detect only if ffmpeg couldn't |
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 |
;; |
-Z|--undocumented) |
# This is a container for, of course, undocumented functions |
# These are used for testing/debugging purposes. Might (and will) |
# change between versions, break easily and do no safety checks. |
# In short, don't look at them unless told to do so :P |
case "$2" in |
# AWK was used for a little while in a WiP version |
#set_awk=*) AWK="$(cut -d'=' -f2<<<"$2")" ; warn "[U] AWK=$AWK" ;; |
# Hang the main process loop just before cleanup. |
hang) UNDFLAG_HANG="On" ; warn "[U] Hang flag" ;; |
# Print identification results, do nothing else |
idonly) UNDFLAG_IDONLY="On" ; warn "[U] Id only" ;; |
# ffmpeg path |
set_ffmpeg=*) |
FFMPEG=$(realpathr "$(cut -d'=' -f2<<<"$2")") |
assert $LINENO 'test -x "$FFMPEG"' |
warn "[U] FFMPEG=$FFMPEG" |
;; |
# mplayer path |
set_mplayer=*) |
MPLAYER=$(realpathr "$(cut -d'=' -f2<<<"$2")") |
assert $LINENO 'test -x "$MPLAYER"' |
warn "[U] MPLAYER=$MPLAYER" |
;; |
# Ignore one of the players |
disable_ffmpeg) |
FFMPEG='' |
warn "FFmpeg disabled" |
assert $LINENO "[ '$MPLAYER' ] || [ '$FFMPEG' ]" |
decoder=$DEC_MPLAYER |
;; |
disable_mplayer) |
MPLAYER='' |
warn "Mplayer disabled" |
assert $LINENO "[ '$MPLAYER' ] || [ '$FFMPEG' ]" |
decoder=$DEC_FFMPEG |
;; |
# This is an old option from the first versions when the script |
# failed a lot more, I haven't used it for years and I don't think |
# anyone would need it anymore but I'll keep it at least for |
# a few more versions |
shoehorn=*) |
shoehorned="$(cut -d'=' -f2-<<<"$2")" |
;; |
*) false ;; |
esac |
shift |
;; |
-D) # Repeat to just test consistency |
if [ $DEBUGGED -gt 0 ]; then |
[ $decoder -eq $DEC_MPLAYER ] && d='mplayer' |
[ $decoder -eq $DEC_FFMPEG ] && d='ffmpeg' |
infplain '[ svn $Rev$ ]' |
cat >&2 <<-EOD |
=== Setup === |
GETOPT: $GETOPT |
MPLAYER: $MPLAYER |
FFMPEG: $FFMPEG |
AWK: $(type -pf awk) |
Filterchain: [ ${FILTERS_IND[*]} ] |
Decoder: $d |
Safe step: $QUIRKS_LEN_STEP |
=== Versions === |
Bash: $BASH_VERSION |
Getopt: $($GETOPT --version) |
EOD |
# FIXME: Any portable way to print AWK version? |
exit |
fi |
DEBUG=1 |
inf "Testing internal consistency..." |
internal_integrity_test && warn "All tests passed" || error "Some tests failed!" |
DEBUGGED=1 |
warn "Command line: $0 $ARGS" |
title="$(basename "$0") $ARGS" |
;; |
--) shift ; break ;; |
*) error "Internal error! (remaining opts: $@)" ; exit $EX_SOFTWARE ; |
esac |
shift |
done |
|
# Remaining arguments |
if [ ! "$1" ]; then |
show_help |
exit $EX_USAGE |
elif [ "$2" ]; then |
multiple_input_files=1 |
fi |
# }}} # Command line parsing |
|
# The coherence check ensures the processed options are |
# not incoherent/incompatible with the input files or with |
# other given options |
coherence_check || { |
exit $? |
} |
|
set +e # Don't fail automatically. Blocks marked with {{SET_E}} will break if this changes |
for arg do process "$arg" ; done |
|
# Script ends here, everything below are comments |
# =========================================================================== |
# |
# Bash syntax notes # {{{ |
# These are some notes for my own reference (or for those trying to read the script) |
# regarding bash syntax nuissances. |
# |
# * herestring redirection, '<<<$string', (used extensively in vcs) was introduced in bash 2.05b |
# * sed s/[ ,]/ * /g <=> ${var//[ ,]/ * } [Much faster due to not forking] |
# sed s/[ ,]/ * / <=> ${var/[ ,]/ * } |
# * bash2: declaring local empty arrays like 'local a=( )' makes bash think they're strings |
# 'local -a' must be used instead |
# bash3 has no problem with this |
# * bash2: 'arr+=( elem )' for array push is not supported, use 'arr=( "${arr[@]}" elem )' instead |
# += is a bash3 syntax modification, bash3.1 extended it further, arithmetic += works |
# inside let |
# * bash2: [*] expands as a string while [@] expands as an array. Both have trouble with spaces |
# in elements though |
# * performance: bash loops are often slower than awk or perl |
# * performance: grep + cut proved faster than an equivalent sed -r s// replacement |
# }}} # Bash syntax notes |
# |
# vim:set ts=4 ai foldmethod=marker nu: # |
Property changes: |
Added: svn:executable |
Added: svn:keywords |
+Rev Id Date |
\ No newline at end of property |