Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1195 lines (1071 sloc) 53.8 KB
#!/usr/bin/env bash
# vrecord
# Open-source software for capturing a video signal and turning it into a digital file.
SCRIPTNAME=$(basename "${0}")
SCRIPTDIR=$(dirname "${0}")
CONFIG_FILE="${HOME}/.${SCRIPTNAME}.conf"
unset VERSION
if [[ $(dirname "$(command -v "${0}")") = "/usr/local/bin" ]] ; then
VERSION=$(TMP=$(brew info vrecord | grep ".*\*$" | grep -Eo "/vrecord/.* \(") ; echo "${TMP:9:(${#TMP}-11)}")
LOGO_PATH="/usr/local/Cellar/vrecord/${VERSION}/vrecord_logo.png"
else
LOGO_PATH="${SCRIPTDIR}/vrecord_logo.png"
fi
unset INPUTOPTIONS
unset MIDDLEOPTIONS
unset SUFFIX
unset DURATION
unset TECHNICIAN
RUNTYPE="record"
CAPTURELOGSUFFIX="_ffmpeg_decklink_input.log"
unset PASHUAINSTALL
unset EXTRAOUTPUTS
UNDECLAREDOPTION="Undeclared"
DEFAULTFONT="/Library/Fonts/Andale Mono.ttf"
SAT_OUTLIER_THRSHLD=14
AUD_OUTLIER_THRSHLD=10
BRNG_OUTLIER_THRSHLD=14
BREW_PREFIX=$(brew --prefix ffmpegdecklink)
FFMPEG_DECKLINK=("${BREW_PREFIX}/bin/ffmpeg-dl")
FFPLAY_DECKLINK=("${BREW_PREFIX}/bin/ffplay-dl")
MPVOPTS=(--no-osc)
MPVOPTS+=(--load-scripts=no)
MPVOPTS+=(--script "${SCRIPTDIR}/qcview.lua")
MPVOPTS+=(--really-quiet)
if [[ "$("${FFMPEG_DECKLINK[@]}" -version 2>&1 | grep "Library not loaded" >/dev/null)" || ! -f "${FFMPEG_DECKLINK}" || ! -f "${FFPLAY_DECKLINK}" ]] ; then
echo "Please reinstall 'ffmpeg-dl':"
echo " brew reinstall amiaopensource/amiaos/ffmpegdecklink --with-sdl2 --with-freetype --with-openjpeg"
echo "Exiting."
exit 1
fi
_usage(){
cat <<EOF
${SCRIPTNAME} ${VERSION}
${SCRIPTNAME} will record a file via the Blackmagic SDK and ffmpeg. It is an
interactive script and will create 10 or 8-bit video files.
Dependencies: cowsay, amiaopensource/amiaos/ffmpegdecklink --with-sdl2
--with-freetype --with-openjpeg, mkvtoolnix, mpv, qcli and xmlstarlet
Usage: ${SCRIPTNAME} [ -g | -e | -r | -p | -a | -x | -v | -h ]
-g use the GUI
-e edit the configuration file before recording
-r enable record mode [default]
-p enable passthrough mode where the video signal coming into the
computer can be monitored, but not written to a file. Useful for
testing equipment and setting up a tape to bars.
-a enable audio passthrough mode. Identical to passthrough except for
the addition of audio bars. Note: Will eventually lag and crash if
left on too long.
-x reset the configuration: this will replace the default configuration
file at '${CONFIG_FILE}' with an empty one.
-v Run ffmpeg with '-loglevel debug'. Using this option creates a very
large log file, so avoid using this option with 'Visual + Numerical'
or any playback option that display the log as part of the view.
-h display this help menu
Advanced options
-I Provide a string of input options for the recording ffmpeg to use.
For example \"vrecord -i '-loglevel trace'\" would force an ffmpeg
logging level that is usually unaccessible via vrecord preferences.
-O Provide a string of output options for the recording ffmpeg to use.
-i Provide a file as an input to vrecord, rather than using the
decklink device. For testing without a decklink device.
See also the man page: man ${SCRIPTNAME}
EOF
}
# local functions
_get_iso8601(){
date +%FT%T
}
_report(){
local RED="$(tput setaf 1)" # Red - For Warnings
local GREEN="$(tput setaf 2)" # Green - For Declarations
local BLUE="$(tput setaf 4)" # Blue - For Questions
local NC="$(tput sgr0)" # No Color
local COLOR=""
local STARTMESSAGE=""
local ECHOOPT=""
OPTIND=1
while getopts "qdwstn" opt ; do
case "${opt}" in
q) COLOR="${BLUE}" ;; # question mode, use color blue
d) COLOR="${GREEN}" ;; # declaration mode, use color green
w) COLOR="${RED}" ;; # warning mode, use color red
s) STARTMESSAGE+=([${SCRIPTNAME}] ) ;; # prepend scriptname to the message
t) STARTMESSAGE+=($(_get_iso8601) '- ' ) ;; # prepend timestamp to the message
n) ECHOOPT="-n" ;; # to avoid line breaks after echo
esac
done
shift "$((OPTIND-1))"
MESSAGE="${1}"
echo ${ECHOOPT} "${COLOR}${STARTMESSAGE[@]}${MESSAGE}${NC}"
}
_cleanup(){
_report -wt "Vrecord is being stopped at $(_get_iso8601), noting this in the capture log."
_writeingestlog "exit status" "vrecord was forced to quit early at $(_get_iso8601). Some processing may be incomplete."
}
_check_mpv(){
if ! mpv > /dev/null ; then
mpv
_report -wt "mpv doesn't appear to be running correctly. Try 'brew reinstall mpv'."
exit 1
fi
}
_construct_pashua_id(){
# this function requires the first argument to be the pashua identifier
# all subsequent arguments should be pairs of pashua attributes and pashua values
# if a set of options is needed, place it as the last argument with no attribute
# for example:
# DOC_TYPE_OPTS=(Documented Undocumented)
# _construct_pashua_id DOC_TYPE x=10 y=10 type=radiobutton "${DOC_TYPE_OPTS[@]}"
PASHUA_IDENTIFIER="${1}"
shift
while [ "${*}" != "" ] ; do
if [[ "${1}" != *"="* ]] ; then
OPTION_ARRAY=("${@}")
echo "${PASHUA_IDENTIFIER}.option = ${UNDECLAREDOPTION}"
for PASHUA_OPTION in "${OPTION_ARRAY[@]}" ; do
echo "${PASHUA_IDENTIFIER}.option = ${PASHUA_OPTION}"
done
shift "${#OPTION_ARRAY[@]}"
else
PASHUA_ATTRIBUTE="${1%%=*}"
PASHUA_VALUE="${1#*=}"
echo "${PASHUA_IDENTIFIER}.${PASHUA_ATTRIBUTE} = ${PASHUA_VALUE}"
fi
shift
done
}
_construct_pashua_conf(){
_construct_pashua_id \* transparency=0.95 title="vrecord configuration"
_construct_pashua_id INTRO x=20 y=480 type=text width=500 \
text="Set file recording options. Leave the option as \"${UNDECLAREDOPTION}\" to be prompted later."
_construct_pashua_id DIR x=20 y=440 type=openbrowser filetype=directory width=400 \
default="${DIR}" \
label="Select a recording directory."
_construct_pashua_id LOGDIR x=20 y=395 type=openbrowser filetype=directory width=400 \
default="${LOGDIR}" \
label="Select a directory for auxiliary files (leave blank to match the recording directory)." \
tooltip="select the directory for automatically generated logs and checksums"
_construct_pashua_id VIDEO_INPUT_CHOICE x=20 y=280 type=radiobutton \
default="${VIDEO_INPUT_CHOICE}" \
label="Select Video Input" \
"${VIDEO_INPUT_OPTIONS[@]}"
_construct_pashua_id AUDIO_INPUT_CHOICE x=20 y=150 type=radiobutton \
label="Select Audio Input" \
default="${AUDIO_INPUT_CHOICE}" \
"${AUDIO_INPUT_OPTIONS[@]}"
_construct_pashua_id CONTAINER_CHOICE x=170 y=280 type=radiobutton \
label="Select File Format" \
default="${CONTAINER_CHOICE}" \
"${CONTAINER_OPTIONS[@]}"
_construct_pashua_id VIDEO_CODEC_CHOICE x=300 y=280 type=radiobutton \
label="Select Codec for Video" \
default="${VIDEO_CODEC_CHOICE}" \
"${VIDEO_CODEC_OPTIONS[@]}"
_construct_pashua_id VIDEO_BIT_DEPTH_CHOICE x=470 y=317 type=radiobutton \
label="Select Video Bit Depth" \
default="${VIDEO_BIT_DEPTH_CHOICE}" \
"${VIDEO_BITDEPTH_OPTIONS[@]}"
_construct_pashua_id AUDIO_MAPPING_CHOICE x=200 y=200 type=popup \
label="Select Audio Channel Mapping" \
default="${AUDIO_MAPPING_CHOICE}" \
"${CHANNEL_MAPPING_OPTIONS[@]}"
_construct_pashua_id STANDARD_CHOICE x=470 y=230 type=radiobutton \
label="Select Television Standard" \
default="${STANDARD_CHOICE}" \
"${STANDARD_OPTIONS[@]}"
_construct_pashua_id QCTOOLSXML_CHOICE x=660 y=317 type=radiobutton \
label="Create QCTools XML?" \
default="${QCTOOLSXML_CHOICE}" \
"${QCTOOLSXML_OPTIONS[@]}"
_construct_pashua_id FRAMEMD5_CHOICE x=660 y=230 type=radiobutton \
label="Create frame-level MD5 checksums? (recommended)" \
default="${FRAMEMD5_CHOICE}" \
"${FRAMEMD5_OPTIONS[@]}"
_construct_pashua_id EMBED_LOGS_CHOICE x=660 y=143 type=radiobutton \
label="Embed digitization logs in video file? (Matroska only)" \
default="${EMBED_LOGS_CHOICE}" \
"${EMBED_LOGS_OPTIONS[@]}"
_construct_pashua_id PLAYBACKVIEW_CHOICE x=20 y=3 type=popup \
label="Select View" \
default="${PLAYBACKVIEW_CHOICE}" \
"${PLAYBACKVIEW_OPTIONS[@]}"
_construct_pashua_id DURATION x=20 y=60 type=combobox \
label="Set recording time (integer or decimal) in minutes." \
tooltip="Leave blank for indefinite recording time" \
default="${DURATION}" \
"${DURATION_OPTIONS[@]}"
_construct_pashua_id TECHNICIAN x=350 y=55 type=textbox height=30 \
label="Enter the name of the person digitizing this tape."
_construct_pashua_id cb type=cancelbutton
_construct_pashua_id INVERT_PHASE x=410 y=0 type=checkbox \
label="Invert Second Channel of Audio" \
default="${INVERT_PHASE}"
_construct_pashua_id WARNING x=350 y=0 type=text \
default="WARNING: Do not use this option unless required"
}
# command-line options to set media id and original variables
OPTIND=1
while getopts ":herpaxgvI:O:i:" opt ; do
case "${opt}" in
h) _usage ; exit 0 ;;
e) RUNTYPE="edit" ;;
r) RUNTYPE="record" ;;
p) RUNTYPE="passthrough" ;;
a) RUNTYPE="audiopassthrough" ;;
x) RUNTYPE="reset" ;;
g) RUNTYPE="GUI" ; GUI=1 ;;
v) VERBOSE="true" ;;
I) EXTRAINPUTOPTIONS=(${OPTARG}) ;;
O) EXTRAOUTPUTOPTIONS=(${OPTARG}) ;;
i) ALT_INPUT="${OPTARG}" ;;
:) _report -w "Option -${OPTARG} requires an argument" ; _usage ; exit 1 ;;
*) _report -w "Error: bad option -${OPTARG}" ; _usage ; exit 1 ;;
esac
done
shift "$((OPTIND-1))"
if [[ -f "${CONFIG_FILE}" ]] ; then
. "${CONFIG_FILE}"
elif [[ "${RUNTYPE}" = "record" || "${RUNTYPE}" = "edit" ]] ; then
_report -d "No configuration file, restarting in edit mode."
touch "${CONFIG_FILE}"
exec "${SCRIPTNAME}" -e
fi
# local functions
_get_decklink_inputs(){
DECKLINK_INPUTS=$("${FFMPEG_DECKLINK[@]}" -f decklink -list_devices 1 -i dummy 2>&1 | grep -o "^\[decklink[^\]*][^']*'.*" | cut -d "'" -f2- | sed "s/'$//g")
if [[ -z "${DECKLINK_INPUTS}" ]] ; then
_report -w "No decklink inputs were found. Running \`${FFMPEG_DECKLINK} -hide_banner -f decklink -list_devices 1 -i dummy\` results in:"
DECKLINK_RESULT=$("${FFMPEG_DECKLINK[@]}" -hide_banner -f decklink -list_devices 1 -i dummy)
echo "${DECKLINK_RESULT}"
_report -w "Please check connections and drivers and try again."
exit 1
else
FIRST_DECKLINK_INPUT="$(echo "${DECKLINK_INPUTS}" | head -n 1 )"
fi
}
_pashua_run() {
# Wrapper function for interfacing to Pashua. Written by Carsten
# Bluem <carsten@bluem.net> in 10/2003, modified in 12/2003 (including
# a code snippet contributed by Tor Sigurdsson), 08/2004 and 12/2004.
# Write config file
# Find Pashua binary. We do search both . and dirname "$0"
# , as in a doubleclickable application, cwd is /
# BTW, all these quotes below are necessary to handle paths
# containing spaces.
BUNDLEPATH="Pashua.app/Contents/MacOS/Pashua"
MYPATH=$(dirname "$0")
for SEARCHPATH in "${MYPATH}/Pashua" "${MYPATH}/${BUNDLEPATH}" "./${BUNDLEPATH}" \
"/Applications/${BUNDLEPATH}" "${HOME}/Applications/${BUNDLEPATH}"
do
if [ -f "${SEARCHPATH}" -a -x "${SEARCHPATH}" ] ; then
PASHUAPATH="${SEARCHPATH}"
break
fi
done
if [[ ! "${PASHUAPATH}" ]] ; then
_report -w "Error: Pashua is used to edit vrecord options but is not found."
if [[ -z "${PASHUAINSTALL}" ]] ; then
_report -d "Attempting to run: brew cask install pashua"
if [[ "${PASHUAINSTALL}" != "Y" ]] ; then
brew cask install pashua
PASHUAINSTALL="Y"
_pashua_run
else
break 2
fi
fi
else
# Get result
RESULT=$("${PASHUAPATH}" "${PASHUA_CONFIGFILE}" | sed 's/ /;;;/g')
# Parse result
for LINE in ${RESULT} ; do
KEY=$(echo "${LINE}" | sed 's/^\([^=]*\)=.*$/\1/')
VALUE=$(echo "${LINE}" | sed 's/^[^=]*=\(.*\)$/\1/' | sed 's/;;;/ /g')
VARNAME="${KEY}"
VARVALUE="${VALUE}"
eval "${VARNAME}"='${VARVALUE}'
done
fi
}
# GUI dialog to set vrecord runtypes
_master_gui(){
PASHUA_CONFIGFILE=$(/usr/bin/mktemp /tmp/pashua_XXXXXXXXX)
{
_construct_pashua_id \* transparency=0.95 title="Welcome to Vrecord!"
_construct_pashua_id VRECORD_LOGO x=0 y=75 type=image path="${LOGO_PATH}"
_construct_pashua_id REC_BUTTON x=15 y=35 type=button label="Record"
_construct_pashua_id PASS_BUTTON x=170 y=35 type=button label="Passthrough"
_construct_pashua_id AUDIO_BUTTON x=335 y=35 type=button label="Audio Check"
_construct_pashua_id EDIT_BUTTON x=495 y=35 type=button label="Edit Settings"
_construct_pashua_id DOCUMENTATION_BUTTON x=800 y=35 type=button label="Documentation"
_construct_pashua_id HELP_BUTTON x=675 y=35 type=button label="Help"
_construct_pashua_id db type=defaultbutton label="Exit"
} > "${PASHUA_CONFIGFILE}"
_pashua_run
if [[ "${REC_BUTTON}" = 1 ]] ; then
RUNTYPE="record"
elif [[ "${PASS_BUTTON}" = 1 ]] ; then
RUNTYPE="passthrough"
elif [[ "${AUDIO_BUTTON}" = 1 ]] ; then
RUNTYPE="audiopassthrough"
elif [[ "${EDIT_BUTTON}" = 1 ]] ; then
RUNTYPE="edit"
elif [[ "${HELP_BUTTON}" = 1 ]] ; then
echo -n -e "\033]0;PRESS Q TO EXIT\007" && man vrecord && echo -n -e "\033]0;\007" && _master_gui
elif [[ "${DOCUMENTATION_BUTTON}" = 1 ]] ; then
open https://github.com/amiaopensource/vrecord#vrecord-documentation && _master_gui
# db = default button, set to exit
elif [[ "${db}" = 1 ]] ; then
echo "Exiting Vrecord. Goodbye!" && exit 0
else
# catch instance when the window is closed with no button pressed
echo "Exiting Vrecord. Goodbye!" && exit 0
fi
if [[ -f "${PASHUA_CONFIGFILE}" ]] ; then
rm "${PASHUA_CONFIGFILE}"
fi
}
# relaunch GUI in GUI mode/exit script in terminal mode
_gui_return(){
if [[ "${GUI}" = 1 ]] ; then
_master_gui
else
exit 0
fi
}
# edit mode
_edit_mode(){
PASHUA_CONFIGFILE=$(/usr/bin/mktemp /tmp/pashua_XXXXXXXXX)
_construct_pashua_conf > "${PASHUA_CONFIGFILE}"
_pashua_run
rm "${PASHUA_CONFIGFILE}"
# cb = cancel button; default button is "OK"
if [[ "${cb}" = 0 ]] ; then
_duration_check
# report back options
if [[ -z "${LOGDIR}" ]] ; then
LOGDIR="${DIR}"
echo "As auxiliary files directory was left blank, logs will be written to recording directory at ${DIR}."
fi
echo "Variables set:"
echo " DIR = ${DIR}"
echo " LOGDIR = ${LOGDIR}"
echo " CONTAINER_CHOICE = ${CONTAINER_CHOICE}"
echo " VIDEO_INPUT_CHOICE = ${VIDEO_INPUT_CHOICE}"
echo " AUDIO_INPUT_CHOICE = ${AUDIO_INPUT_CHOICE}"
echo " VIDEO_CODEC_CHOICE = ${VIDEO_CODEC_CHOICE}"
echo " VIDEO_BIT_DEPTH_CHOICE = ${VIDEO_BIT_DEPTH_CHOICE}"
echo " AUDIO_MAPPING_CHOICE = ${AUDIO_MAPPING_CHOICE}"
echo " STANDARD_CHOICE = ${STANDARD_CHOICE}"
echo " QCTOOLSXML_CHOICE = ${QCTOOLSXML_CHOICE}"
echo " FRAMEMD5_CHOICE = ${FRAMEMD5_CHOICE}"
echo " EMBED_LOGS_CHOICE = ${EMBED_LOGS_CHOICE}"
echo " PLAYBACKVIEW_CHOICE = ${PLAYBACKVIEW_CHOICE}"
echo " DURATION = ${DURATION}"
echo " TECHNICIAN = ${TECHNICIAN}"
if [[ "${INVERT_PHASE}" = 1 ]] ; then
echo -e " \033[101mWARNING: Option to invert phase of second audio channel has been selected\033[0m"
fi
if [ "${VIDEO_CODEC_CHOICE}" = "FFV1 version 3" -a "${CONTAINER_CHOICE}" = "MXF" ] ; then
echo -e " \033[101mWARNING: Incompatible video codecs and CONTAINERs have been selected\033[0m"
elif [ "${VIDEO_CODEC_CHOICE}" = "ProRes" -a "${CONTAINER_CHOICE}" = "MXF" ] ; then
echo -e " \033[101mWARNING: Incompatible video codecs and CONTAINERs have been selected\033[0m"
fi
if [ "${EMBED_LOGS_CHOICE}" = "Yes" -a "${CONTAINER_CHOICE}" != "Matroska" ] ; then
_report -w "WARNING: Logs cannot be embedded in non-Matroska files. This vrecord session will generate logs in ${LOGDIR}, but will not embed them in your video file."
fi
echo ""
# write config file
{
echo "# Set these variables to a valid option or leave as empty quotes (like \"\") to request each run."
for COMMENT_LINE in VIDEO_INPUT_CHOICE AUDIO_INPUT_CHOICE CONTAINER_CHOICE VIDEO_CODEC_CHOICE \
VIDEO_BIT_DEPTH_CHOICE AUDIO_MAPPING_CHOICE STANDARD_CHOICE QCTOOLSXML_CHOICE FRAMEMD5_CHOICE \
EMBED_LOGS_CHOICE PLAYBACKVIEW_CHOICE DIR LOGDIR INVERT_PHASE DURATION TECHNICIAN ; do
echo "${COMMENT_LINE}=\"${!COMMENT_LINE}\""
done
} > "${CONFIG_FILE}"
. "${CONFIG_FILE}"
else
_report -d "Editing of preferences was canceled by the user."
fi
if [[ "${GUI}" = 1 ]] ; then
_master_gui
else
RUNTYPE="record"
open /Applications/Utilities/Terminal.app
_report -nd "Press [q] to quit, [p] to enter passthrough mode or any other key to proceed: "
read AFTEREDITRESPONSE
if [[ "${AFTEREDITRESPONSE}" = "q" ]] ; then
_report -d "Bye then"
exit 0
elif [[ "${AFTEREDITRESPONSE}" = "p" ]] ; then
RUNTYPE="passthrough"
fi
fi
}
# passthrough and audiopassthrough modes
_passthrough_mode(){
if [[ "${MEDIA_PLAYER_CHOICE}" = "mpv" ]] ; then
_check_mpv
"${FFMPEG_DECKLINK}" -nostats "${GRAB_DECKLINK[@]}" 2> /tmp/vrecord_input.log \
"${PIPE_DECKLINK[@]}" | \
mpv "${MPVOPTS[@]}" --title="${WINDOW_NAME}" -
else
"${FFPLAY_DECKLINK}" -v info -nostats "${GRAB_DECKLINK[@]}" 2> /tmp/vrecord_input.log \
-window_title "${WINDOW_NAME}" \
-vf "${PLAYBACKFILTER}"
fi
_gui_return
}
_audiopassthrough_mode(){
PLAYBACKFILTER="\
[aid1]asplit=2[z][ao],\
[z]channelsplit=channel_layout=quad[s1][s2][s3][s4];[s1][s2][s3][s4]amerge=inputs=4,aformat=channel_layouts=quad[zz],\
[zz]showvolume=t=0:h=17:w=200[xx],\
[vid1]split=5[a][b][c][d][e],\
[b]field=top[b1],\
[c]field=bottom[c1],\
[b1]${WAVEFORM_FILTER}[b2],\
[c1]${WAVEFORM_FILTER}[c2],\
[a][b2][c2]vstack=inputs=3,format=yuv422p[abc1],\
[d]${VECTORSCOPE_FILTER}[d1],\
[e]signalstats=out=brng,scale=512:ih[e1],\
[e1][d1]vstack[de1],\
[abc1][de1]hstack[abcde1],\
[abcde1][xx]overlay=10:10[vo]"
echo "ESC quit" > ~/.config/mpv/input.conf
_check_mpv
"${FFMPEG_DECKLINK[@]}" -nostdin -hide_banner -nostats "${INPUTOPTIONS[@]}" "${GRAB_DECKLINK[@]}" "${PIPE_DECKLINK[@]}" 2> /tmp/vrecord_input.log | \
mpv - --title="${WINDOW_NAME}" -lavfi-complex "${PLAYBACKFILTER}"
_gui_return
}
# check validity of duration value
_duration_check(){
# Sets up function to verify validity of duration settings
if [[ -n "${DURATION}" ]] ; then
if ! [[ "${DURATION}" =~ ^$|^[0-9]+$|^[0-9]+\.[0-9]*$|^\.[0-9]+$ ]] ; then
_report -w "Illegal value for recording time. Input must only be numbers."
exit 1
fi
if (( $(bc <<< "${DURATION} == 0") )) ; then
_report -w "A recording duration of zero is invalid."
exit 1
fi
fi
}
# create a capture log of decisions made in vrecord
_writeingestlog(){
if [[ "${INGESTLOG}" ]] ; then
KEY="${1}"
shift
VALUE="${@}"
# need to add yaml style escaping
echo "${KEY}: ${VALUE}" >> "${INGESTLOG}"
else
_report -wt "The _writeingestlog function was called, but the ingestlog file (${INGESTLOG}) is not declared."
fi
}
# decipher vrecord options as specified by user
_lookup_choice(){
case "${2}" in
"quit"|"Quit"|"QUIT"|"q"|"Q") _report -dt "Bye." ; exit ;;
esac
case "${1}" in
# video inputs
"Composite") VIDEO_INPUT="composite" ;;
"SDI") VIDEO_INPUT="sdi" ;;
"Component") VIDEO_INPUT="component" ;;
"S-Video") VIDEO_INPUT="s_video" ;;
# audio inputs
"Analog") AUDIO_INPUT="analog" ;;
"SDI Embedded Audio") AUDIO_INPUT="embedded" ;;
"Digital Audio (AES/EBU)") AUDIO_INPUT="aes_ebu" ;;
# container
"QuickTime")
EXTENSION="mov"
MIDDLEOPTIONS+=(-movflags write_colr)
FORMAT="mov" ;;
"Matroska")
EXTENSION="mkv"
FORMAT="matroska" ;;
"AVI")
EXTENSION="avi"
FORMAT="avi" ;;
"MXF")
EXTENSION="mxf"
FORMAT="mxf" ;;
# video codec
"Uncompressed Video")
if [[ "${PIXEL_FORMAT}" = "yuv422p10" ]] ; then
CODECNAME="Uncompressed 10-bit 4:2:2"
MIDDLEOPTIONS+=(-c:v v210)
elif [[ "${PIXEL_FORMAT}" = "uyvy422" ]] ; then
CODECNAME="Uncompressed 8-bit 4:2:2"
MIDDLEOPTIONS+=(-c:v rawvideo -pix_fmt uyvy422 -tag:v 2vuy)
fi ;;
"FFV1 version 3")
CODECNAME="FFV1 version 3"
MIDDLEOPTIONS+=(-c:v ffv1 -level 3 -g 1 -slices 16 -slicecrc 1)
SUFFIX="_ffv1" ;;
"JPEG2000")
CODECNAME="JPEG2000"
MIDDLEOPTIONS+=(-c:v libopenjpeg)
SUFFIX="_j2k" ;;
"ProRes")
CODECNAME="Apple ProRes 422"
MIDDLEOPTIONS+=(-c:v prores_ks -flags +ilme+ildct)
SUFFIX="_prores" ;;
# video pixel format and bit depth
"10 bit") PIXEL_FORMAT="yuv422p10" ;;
"8 bit") PIXEL_FORMAT="uyvy422" ;;
# audio mappings
"2 Stereo Tracks (Channels 1 & 2 -> 1st Track Stereo, Channels 3 & 4 -> 2nd Track Stereo)")
AUDIOMAP="[0:a:0]pan=stereo| c0=c0 | c1=${PHASE_VALUE}c1[stereo1];[0:a:0]pan=stereo| c0=c2 | c1=c3[stereo2]"
MAP1V="[stereo1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}")
MAP2V="[stereo2]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP2V}") ;;
"1 Stereo Track (From Channels 1 & 2)")
AUDIOMAP="[0:a:0]pan=stereo| c0=c0 | c1=${PHASE_VALUE}c1[stereo1]"
MAP1V="[stereo1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}")
MAP2V="" ;;
"1 Stereo Track (From Channels 3 & 4)")
AUDIOMAP="[0:a:0]pan=stereo| c0=c2 | c1=${PHASE_VALUE}c3[stereo1]"
MAP1V="[stereo1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}")
MAP2V="" ;;
"Channel 1 -> 1st Track Mono, Channel 2 -> 2nd Track Mono")
AUDIOMAP="[0:a:0]pan=mono| c0=c0[mono1];[0:a:0]pan=mono| c0=${PHASE_VALUE}c1[mono2]"
MAP1V="[mono1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}")
MAP2V="[mono2]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP2V}") ;;
"Channel 2 -> 1st Track Mono, Channel 1 -> 2nd Track Mono")
AUDIOMAP="[0:a:0]pan=mono| c0=${PHASE_VALUE}c1[mono1];[0:a:0]pan=mono| c0=c0[mono2]"
MAP1V="[mono1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}")
MAP2V="[mono2]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP2V}") ;;
"Channel 1 -> Single Track Mono")
AUDIOMAP="[0:a:0]pan=mono| c0=c0[mono1]"
MAP1V="[mono1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}") ;;
"Channel 2 -> Single Track Mono")
AUDIOMAP="[0:a:0]pan=mono| c0=c1[mono1]"
MAP1V="[mono1]" ; AUDIO_CHANNEL_MAP+=(-map "${MAP1V}") ;;
# video standard
"NTSC")
STANDARD="ntsc"
DECKLINK_FPS="30000/1001"
RECORDINGFILTER="setfield=bff,setsar=40/27,setdar=4/3"
MIDDLEOPTIONS+=(-color_primaries smpte170m)
MIDDLEOPTIONS+=(-color_trc bt709)
MIDDLEOPTIONS+=(-colorspace smpte170m) ;;
"PAL")
STANDARD="pal "
DECKLINK_FPS="25000/1000"
RECORDINGFILTER="setfield=tff,setsar=16/15,setdar=4/3"
MIDDLEOPTIONS+=(-color_primaries bt470bg)
MIDDLEOPTIONS+=(-color_trc bt709)
MIDDLEOPTIONS+=(-colorspace bt470bg) ;;
# playback views
"Quality Control View (mpv)") MEDIA_PLAYER_CHOICE="mpv" ;;
"Broadcast Range Visual")
PLAYBACKFILTER="\
split=5[a][b][c][d][e];\
[b]field=top[b1];\
[c]field=bottom[c1];\
[b1]${WAVEFORM_FILTER}[b2];\
[c1]${WAVEFORM_FILTER}[c2];\
[a][b2][c2]vstack=inputs=3,format=yuv422p[abc1];\
[d]${VECTORSCOPE_FILTER}[d1];\
[e]signalstats=out=brng,scale=512:ih[e1];\
[e1][d1]vstack[de1];\
[abc1][de1]hstack" ;;
"Full Range Visual")
PLAYBACKFILTER="\
split=5[a][b][c][d][e];\
[b]field=top[b1];\
[c]field=bottom[c1];\
[b1]${WAVEFORM_FILTER}[b2];\
[c1]${WAVEFORM_FILTER}[c2];\
[a][b2][c2]vstack=inputs=3,format=yuv422p[abc1];\
[d]${VECTORSCOPE_FILTER}[d1];\
[e]format=yuv444p,pseudocolor=if(between(1\,val\,amax)+between(val\,254\,amax)\,65\,-1):if(between(1\,val\,amax)+between(val\,254\,amax)\,100\,-1):if(between(1\,val\,amax)+between(val\,254\,amax)\,212\,-1),scale=512:ih[e1];\
[e1][d1]vstack[de1];\
[abc1][de1]hstack" ;;
"Visual + Numerical")
PLAYBACKFILTER="\
split=7[a][b][c][d][e][f][g];\
[b]field=top[b1];\
[c]field=bottom[c1];\
[b1]${WAVEFORM_FILTER}[b2];\
[c1]${WAVEFORM_FILTER}[c2];\
[a][b2][c2]vstack=inputs=3,format=yuv422p[abc1];\
[d]${VECTORSCOPE_FILTER}[d1];\
[e]signalstats=out=brng,scale=512:ih[e1];\
[e1][d1]vstack[de1];\
[f]signalstats=stat=brng+vrep+tout,format=yuv422p,geq=lum=60:cb=128:cr=128,\
scale=180:ih+512,setsar=1/1,\
drawtext=fontcolor=white:fontsize=22:\
fontfile=${DEFAULTFONT}:textfile=/tmp/drawtext.txt,\
drawtext=fontcolor=white:fontsize=17:\
fontfile=${DEFAULTFONT}:textfile=/tmp/drawtext2.txt,\
drawtext=fontcolor=white:fontsize=52:\
fontfile=${DEFAULTFONT}:textfile=/tmp/drawtext3.txt[f1];\
[f1][abc1][de1]hstack=inputs=3[abcdef1];\
[g]scale=iw+512+180:82,format=yuv422p,geq=lum=60:cb=128:cr=128,drawtext=fontcolor=white:fontsize=22:\
fontfile=${DEFAULTFONT}:textfile=/tmp/vrecord_input.log:\
reload=1:y=82-th[g1];\
[abcdef1][g1]vstack" ;;
"Color Matrix")
HUE=20
SAT=0.3
PLAYBACKFILTER="\
scale=iw/4:ih/4,\
split=9[x][hm][hp][sm][sp][hmsm][hmsp][hpsm][hpsp];\
[hm]hue=h=-${HUE}[hm1];\
[hp]hue=h=${HUE}[hp1];\
[sm]hue=s=1-${SAT}[sm1];\
[sp]hue=s=1+${SAT}[sp1];\
[hmsm]hue=h=-${HUE}:s=1-${SAT}[hmsm1];\
[hmsp]hue=h=-${HUE}:s=1+${SAT}[hmsp1];\
[hpsm]hue=h=${HUE}:s=1-${SAT}[hpsm1];\
[hpsp]hue=h=${HUE}:s=1+${SAT}[hpsp1];\
[hpsm1][hp1][hpsp1]hstack=3[top];\
[sm1][x][sp1]hstack=3[mid];\
[hmsm1][hm1][hmsp1]hstack=3[bottom];\
[top][mid][bottom]vstack=3" ;;
"Bit Planes")
if [[ "${PIXEL_FORMAT}" == "uyvy422" ]] ; then
BITS=8
SPLIT="8[b0][b1][b2][b3][b4][b5][b6][b7]"
STACK="[b0c][b1c][b2c][b3c][b4c][b5c][b6c][b7c]hstack=8,format=yuv444p,drawgrid=w=iw/8:h=ih:t=2:c=green@0.5"
elif [[ "${PIXEL_FORMAT}" == "yuv422p10" ]] ; then
BITS=10
SPLIT="10[b0][b1][b2][b3][b4][b5][b6][b7][b8][b9]"
STACK="\
[b8]bitplanenoise=bitplane=2,crop=iw/10:ih:(iw/10)*8:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-9))*pow(2\\,9),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.2}:y=0:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.2}:y=20:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.2}:y=40:fontcolor=white:fontsize=20[b8c];\
[b9]bitplanenoise=bitplane=1,crop=iw/10:ih:(iw/10)*9:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-10))*pow(2\\,10),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.1}:y=0:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.1}:y=20:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.1}:y=40:fontcolor=silver:fontsize=20[b9c];\
[b0c][b1c][b2c][b3c][b4c][b5c][b6c][b7c][b8c][b9c]hstack=10,format=yuv444p,drawgrid=w=iw/10:h=ih:t=2:c=green@0.5"
fi
PLAYBACKFILTER="\
format=yuv420p10le|yuv422p10le|yuv444p10le|yuv440p10le,split=${SPLIT};\
[b0]bitplanenoise=bitplane=10,crop=iw/${BITS}:ih:(iw/${BITS})*0:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-1))*pow(2\\,1),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.10}:y=0:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.10}:y=20:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.10}:y=40:fontcolor=white:fontsize=20[b0c];\
[b1]bitplanenoise=bitplane=9,crop=iw/${BITS}:ih:(iw/${BITS})*1:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-2))*pow(2\\,2),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.9}:y=0:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.9}:y=20:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.9}:y=40:fontcolor=silver:fontsize=20[b1c];\
[b2]bitplanenoise=bitplane=8,crop=iw/${BITS}:ih:(iw/${BITS})*2:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-3))*pow(2\\,3),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.8}:y=0:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.8}:y=20:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.8}:y=40:fontcolor=white:fontsize=20[b2c];\
[b3]bitplanenoise=bitplane=7,crop=iw/${BITS}:ih:(iw/${BITS})*3:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-4))*pow(2\\,4),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.7}:y=0:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.7}:y=20:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.7}:y=40:fontcolor=silver:fontsize=20[b3c];\
[b4]bitplanenoise=bitplane=6,crop=iw/${BITS}:ih:(iw/${BITS})*4:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-5))*pow(2\\,5),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.6}:y=0:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.6}:y=20:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.6}:y=40:fontcolor=white:fontsize=20[b4c];\
[b5]bitplanenoise=bitplane=5,crop=iw/${BITS}:ih:(iw/${BITS})*5:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-6))*pow(2\\,6),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.5}:y=0:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.5}:y=20:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.5}:y=40:fontcolor=silver:fontsize=20[b5c];\
[b6]bitplanenoise=bitplane=4,crop=iw/${BITS}:ih:(iw/${BITS})*6:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-7))*pow(2\\,7),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.4}:y=0:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.4}:y=20:fontcolor=white:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.4}:y=40:fontcolor=white:fontsize=20[b6c];\
[b7]bitplanenoise=bitplane=3,crop=iw/${BITS}:ih:(iw/${BITS})*7:0,lutyuv=u=(maxval/2):v=(maxval/2):y=bitand(val\\,pow(2\\,10-8))*pow(2\\,8),pad=iw:ih+64:0:64,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.0.3}:y=0:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.1.3}:y=20:fontcolor=silver:fontsize=20,drawtext=fontfile=${DEFAULTFONT}:text=%{metadata\\\:lavfi.bitplanenoise.2.3}:y=40:fontcolor=silver:fontsize=20[b7c];\
${STACK}" ;;
# others
"Yes"|"Yes, after recording"|"Yes, concurrent with recording"|"No") ;;
*) _report -w "Error: Not a valid option, select a valid number." ; return 1 ;;
esac
}
_frames_to_hhmmss(){
H=$(echo "${i} / (60 * 60 * ${DECKLINK_FPS})" | bc)
M=$(echo "(${i} - (${H}*60 * 60 * ${DECKLINK_FPS})) / (60 * ${DECKLINK_FPS})" | bc)
S="$(echo "scale=3;(${i} - (${H}*60 * 60 * ${DECKLINK_FPS})-($M*60*${DECKLINK_FPS})) / (${DECKLINK_FPS})" | bc)"
Ss=$(echo "${S}" | cut -d. -f1)
Sm=$(echo "${S}" | cut -d. -f2)
printf "%02d:%02d:%02d.%03d\n" "${H}" "${M}" "${Ss}" "${Sm}"
}
# set up drawtext.txt files for Visual + Numerical playback view
_set_up_drawtext(){
echo -e "%{pts:hms}
Y
Low %{metadata:lavfi.signalstats.YLOW}
Avg %{metadata:lavfi.signalstats.YAVG}
High %{metadata:lavfi.signalstats.YHIGH}
Diff %{metadata:lavfi.signalstats.YDIF}
U
Low %{metadata:lavfi.signalstats.ULOW}
Avg %{metadata:lavfi.signalstats.UAVG}
High %{metadata:lavfi.signalstats.UHIGH}
Diff %{metadata:lavfi.signalstats.UDIF}
V
Low %{metadata:lavfi.signalstats.VLOW}
Avg %{metadata:lavfi.signalstats.VAVG}
High %{metadata:lavfi.signalstats.VHIGH}
Diff %{metadata:lavfi.signalstats.VDIF}
SAT
Low %{metadata:lavfi.signalstats.SATLOW}
Avg %{metadata:lavfi.signalstats.SATAVG}
High %{metadata:lavfi.signalstats.SATHIGH}
" > /tmp/drawtext.txt
echo -e "
HUE(med) %{metadata:lavfi.signalstats.HUEMED}
HUE(avg) %{metadata:lavfi.signalstats.HUEAVG}
TOUT %{metadata:lavfi.signalstats.TOUT}
VREP %{metadata:lavfi.signalstats.VREP}
" > /tmp/drawtext2.txt
echo -e "
BRNG
%{metadata:lavfi.signalstats.BRNG}
" > /tmp/drawtext3.txt
}
# select playback view
WAVEFORM_FILTER="\
format=yuv422p,\
waveform=intensity=0.1:mode=column:mirror=1:c=1:f=lowpass:e=instant:graticule=green:flags=numbers+dots"
VECTORSCOPE_FILTER="\
format=yuv422p,\
vectorscope=i=0.04:mode=color2:c=1:envelope=instant:graticule=green:flags=name,\
scale=512:512,\
drawbox=w=9:h=9:t=1:x=128-3:y=512-452-5:c=sienna@0.8,\
drawbox=w=9:h=9:t=1:x=160-3:y=512-404-5:c=sienna@0.8,\
drawbox=w=9:h=9:t=1:x=192-3:y=512-354-5:c=sienna@0.8,\
drawbox=w=9:h=9:t=1:x=224-3:y=512-304-5:c=sienna@0.8,\
drawgrid=w=32:h=32:t=1:c=white@0.1,\
drawgrid=w=256:h=256:t=1:c=white@0.2"
# list of selections for each vrecord option
VIDEO_INPUT_OPTIONS=("Composite" "SDI" "Component" "S-Video")
AUDIO_INPUT_OPTIONS=("Analog" "SDI Embedded Audio" "Digital Audio (AES/EBU)")
CONTAINER_OPTIONS=("QuickTime" "Matroska" "AVI" "MXF")
VIDEO_CODEC_OPTIONS=("Uncompressed Video" "FFV1 version 3" "JPEG2000" "ProRes" )
VIDEO_BITDEPTH_OPTIONS=("10 bit" "8 bit")
CHANNEL_MAPPING_OPTIONS=("2 Stereo Tracks (Channels 1 & 2 -> 1st Track Stereo, Channels 3 & 4 -> 2nd Track Stereo)" "1 Stereo Track (From Channels 1 & 2)" "1 Stereo Track (From Channels 3 & 4)" "Channel 1 -> 1st Track Mono, Channel 2 -> 2nd Track Mono" "Channel 2 -> 1st Track Mono, Channel 1 -> 2nd Track Mono" "Channel 1 -> Single Track Mono" "Channel 2 -> Single Track Mono")
STANDARD_OPTIONS=("NTSC" "PAL")
QCTOOLSXML_OPTIONS=("Yes, after recording" "Yes, concurrent with recording" "No")
FRAMEMD5_OPTIONS=("Yes" "No")
EMBED_LOGS_OPTIONS=("Yes" "No")
DURATION_OPTIONS=(23 33 63 93)
PLAYBACKVIEW_OPTIONS=("Quality Control View (mpv)" "Broadcast Range Visual" "Full Range Visual" "Visual + Numerical" "Color Matrix" "Bit Planes")
# GUI mode + CLI reset, edit modes
if [[ "${RUNTYPE}" = "GUI" ]] ; then
_master_gui
fi
if [[ "${RUNTYPE}" = "reset" ]] ; then
_report -q -n "Resetting the configuration will clear ${CONFIG_FILE}. Please enter [Y] to confirm: "
read RESET_RESPONSE
if [[ "${RESET_RESPONSE}" = [Yy] ]] ; then
_report -d "Clearing ${CONFIG_FILE}."
echo -n "" > "${CONFIG_FILE}"
RUNTYPE="edit"
else
_report -d "Reset aborted. Exiting."
exit 0
fi
fi
while [[ "${RUNTYPE}" = "edit" ]] ; do
_edit_mode
done
open /Applications/Utilities/Terminal.app
if [[ "${INVERT_PHASE}" = 1 ]] ; then
PHASE_VALUE="-1*"
fi
_review_option(){
VALUE_NAME="${1}"
SELECTED="${!1}"
QUERY="${2}"
shift 2
CHOICES=("${@}")
if [[ "${SELECTED}" && "${SELECTED}" != "${UNDECLAREDOPTION}" ]] ; then
_lookup_choice "${SELECTED}"
LOG_OF_OPTIONS+="${VALUE_NAME}: ${SELECTED}\n"
OPTION="${SELECTED}"
else
_report -q "${QUERY}"
PS3="Select an option or 'q' to quit: "
select OPTION in "${CHOICES[@]}" ; do
_lookup_choice "${OPTION}" "${REPLY}"
[[ "${?}" -eq 0 ]] && break
done
export "${VALUE_NAME}"="${OPTION}"
LOG_OF_OPTIONS+="${VALUE_NAME}: ${OPTION}\n"
fi
}
_review_option "VIDEO_INPUT_CHOICE" "Which video input are you using?" "${VIDEO_INPUT_OPTIONS[@]}"
_review_option "AUDIO_INPUT_CHOICE" "Which audio input are you using?" "${AUDIO_INPUT_OPTIONS[@]}"
_review_option "VIDEO_BIT_DEPTH_CHOICE" "Which video bit depth?" "${VIDEO_BITDEPTH_OPTIONS[@]}"
_review_option "AUDIO_MAPPING_CHOICE" "Which audio mapping?" "${CHANNEL_MAPPING_OPTIONS[@]}"
_review_option "STANDARD_CHOICE" "Which television STANDARD?" "${STANDARD_OPTIONS[@]}"
_review_option "PLAYBACKVIEW_CHOICE" "Which playback view?" "${PLAYBACKVIEW_OPTIONS[@]}"
if [[ "${OPTION}" = "Full Range Visual" ]] ; then
MIDDLEOPTIONS+=(-color_range jpeg)
else
MIDDLEOPTIONS+=(-color_range mpeg)
fi
MIDDLEOPTIONS+=(-metadata creation_time=now)
MIDDLEOPTIONS+=(${EXTRAOUTPUTOPTIONS[@]})
# set up input and playback
_set_up_drawtext
if [[ "$VERBOSE" == "true" ]] ; then
GRAB_DECKLINK=(-loglevel debug)
_report -wt "When running vrecord in verbose mode, avoid using Visual + Numerical option."
else
GRAB_DECKLINK=(-loglevel info)
fi
if [[ -n "${ALT_INPUT}" ]] ; then
GRAB_DECKLINK+=(-i "${ALT_INPUT}")
else
_get_decklink_inputs
GRAB_DECKLINK+=(-f decklink)
GRAB_DECKLINK+=(-draw_bars 0)
GRAB_DECKLINK+=(-audio_input "${AUDIO_INPUT}")
GRAB_DECKLINK+=(-video_input "${VIDEO_INPUT}")
GRAB_DECKLINK+=(-format_code "${STANDARD}")
GRAB_DECKLINK+=(-channels 8)
GRAB_DECKLINK+=(-audio_depth 32)
GRAB_DECKLINK+=(-raw_format "${PIXEL_FORMAT}")
GRAB_DECKLINK+=("${EXTRAINPUTOPTIONS[@]}")
GRAB_DECKLINK+=(-i "${FIRST_DECKLINK_INPUT}")
fi
PIPE_DECKLINK=(-c copy)
PIPE_DECKLINK+=(-map 0)
PIPE_DECKLINK+=(-f nut)
PIPE_DECKLINK+=(-syncpoints none -f_strict experimental)
PIPE_DECKLINK+=(-write_index 0)
PIPE_DECKLINK+=(-)
WINDOW_NAME="mode:${RUNTYPE} - video:'${VIDEO_INPUT}' audio:'${AUDIO_INPUT}' - to end recording press q, esc, or close video window"
# CLI passthrough and audiopassthrough modes
if [[ "${RUNTYPE}" = "passthrough" ]] ; then
_passthrough_mode
elif [[ "${RUNTYPE}" = "audiopassthrough" ]] ; then
_audiopassthrough_mode
fi
# GUI mode loops
while [[ "${GUI}" = 1 ]] && [[ "${RUNTYPE}" != "record" ]] ; do
if [[ "${RUNTYPE}" = "passthrough" ]] ; then
_passthrough_mode
elif [[ "${RUNTYPE}" = "audiopassthrough" ]] ; then
_audiopassthrough_mode
elif [[ "${RUNTYPE}" = "edit" ]] ; then
_edit_mode
fi
done
# record mode
while [[ -z "${ID}" ]] ; do
_report -q -n "Enter ${AHEM} Identifier: "
read ID
if [[ "${ID}" = "q" ]] ; then
_report -w "Interpreting 'q' to quit."
exit 1
fi
AHEM="a (non-blank)"
done
if [[ ! -d "${DIR}" ]] ; then
_report -q -n "Enter Directory: "
read DIR
if [[ ! -d "${DIR}" ]] ; then
_report -w "Error: Not a valid directory"
exit 1
fi
fi
if [[ ! -d "${LOGDIR}" ]] ; then
if [[ ! -d "${DIR}" ]] ; then
_report -q -n "Enter Directory for Auxiliary Files (If blank will default to recording directory): "
read LOGDIR
else
LOGDIR="${DIR}"
fi
if [[ ! -d "${LOGDIR}" ]] ; then
_report -w "Error: Not a valid directory"
exit 1
fi
fi
_review_option "CONTAINER_CHOICE" "Which audiovisual container format?" "${CONTAINER_OPTIONS[@]}"
_review_option "VIDEO_CODEC_CHOICE" "Which video codec?" "${VIDEO_CODEC_OPTIONS[@]}"
VRECORD_OUTPUT="${DIR}/${ID}${SUFFIX}.${EXTENSION}"
if [[ -f "${VRECORD_OUTPUT}" ]] ; then
_report -w "A file called ${VRECORD_OUTPUT} already exists."
_report -w "Exiting to avoid overwriting that file."
exit
fi
_review_option "QCTOOLSXML_CHOICE" "Create QCTools XML?" "${QCTOOLSXML_OPTIONS[@]}"
if [[ "${OPTION}" != "No" && ! "$(command -v qcli)" ]] ; then
_report -w "Please install qcli to use the qctools reporting option."
_report -w "Such as \`brew install qcli\`."
exit 1
fi
_review_option "FRAMEMD5_CHOICE" "Create frame-level MD5 checksums?" "${FRAMEMD5_OPTIONS[@]}"
if [[ "${OPTION}" = "Yes" ]] ; then
FRAMEMD5NAME="${LOGDIR}/${ID}${SUFFIX}.framemd5"
EXTRAOUTPUTS=(-an -f framemd5 "${FRAMEMD5NAME}")
fi
if [[ "${FORMAT}" = "matroska" ]] ; then
_review_option "EMBED_LOGS_CHOICE" "Embed logs in Matroska file?" "${EMBED_LOGS_OPTIONS[@]}"
fi
_duration_check
if [[ -n "${DURATION}" ]] ; then
DUR_SECONDS=$(bc <<< "${DURATION} * 60" | sed "s/^\./0./")
INPUTOPTIONS=(-t "${DUR_SECONDS}")
fi
if [[ -z "${TECHNICIAN}" ]] ; then
_report -q -n "Enter the name of the person digitizing the tape or leave blank: "
read TECHNICIAN
fi
_report -d "Summary: ${CODECNAME}/${FORMAT} ${PIXEL_FORMAT} file from ${STANDARD} ${VIDEO_INPUT} ${AUDIO_INPUT}. Frame MD5s=${FRAMEMD5_CHOICE}, QCTools XML=${QCTOOLSXML_CHOICE}, and Technician=${TECHNICIAN}. Inputs recorded to ${VRECORD_OUTPUT} and Auxiliary Files created in ${LOGDIR}"
if [[ "${INVERT_PHASE}" = 1 ]] ; then
echo -e " \033[101mWARNING: Option to invert phase of second audio channel has been selected\033[0m"
fi
_report -q "Hit enter to start recording"
read
# create log of vrecord decisions
INGESTLOG="${LOGDIR}/${ID}_capture_options.log"
QCXML="${LOGDIR}/${ID}${SUFFIX}.${EXTENSION}.qctools.xml.gz"
touch "${INGESTLOG}"
_writeingestlog "computer_name" "$(uname -n)"
_writeingestlog "user_name" "$(whoami)"
_writeingestlog "operating_system_VERSION" "$(uname -v)"
_writeingestlog "vrecord version" "${VERSION}"
_writeingestlog "datetime_start" "$(_get_iso8601)"
_writeingestlog "FILE_PATH" "${VRECORD_OUTPUT}"
echo -e "${LOG_OF_OPTIONS}" >> "${INGESTLOG}"
if [[ -z "${TECHNICIAN}" ]] ; then
_writeingestlog "TECHNICIAN" "N/A"
else
_writeingestlog "TECHNICIAN" "${TECHNICIAN}"
fi
if [[ "${INVERT_PHASE}" = 1 ]] ; then
_writeingestlog "INVERT_PHASE" "Yes"
fi
_report -d "Close the playback window to stop recording."
# vrecord process!
RECORD_COMMAND=("${FFMPEG_DECKLINK[@]}")
RECORD_COMMAND+=(-nostdin -nostats "${INPUTOPTIONS[@]}" "${GRAB_DECKLINK[@]}")
RECORD_COMMAND+=(-metadata:s:v:0 encoder="${CODECNAME}" "${MIDDLEOPTIONS[@]}" -c:a pcm_s24le)
RECORD_COMMAND+=(-filter_complex "[0:v:0]${RECORDINGFILTER}; ${AUDIOMAP}" "${AUDIO_CHANNEL_MAP[@]}")
RECORD_COMMAND+=(-f "${FORMAT}" "${VRECORD_OUTPUT}")
RECORD_COMMAND+=("${EXTRAOUTPUTS[@]}" "${PIPE_DECKLINK[@]}")
_writeingestlog "FFmpeg command" "${RECORD_COMMAND[@]}"
if [[ "${MEDIA_PLAYER_CHOICE}" = "mpv" ]] ; then
_check_mpv
"${RECORD_COMMAND[@]}" \
2> >(tee "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}" /tmp/vrecord_input.log >/dev/null) \
| \
if [[ "${QCTOOLSXML_CHOICE}" = "Yes, concurrent with recording" ]] ; then
tee >(mpv "${MPVOPTS[@]}" --title="${WINDOW_NAME}" -) | \
qcli -i - -o "${QCXML}"
else
mpv "${MPVOPTS[@]}" --title="${WINDOW_NAME}" -
fi
else
"${RECORD_COMMAND[@]}" \
2> >(tee "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}" /tmp/vrecord_input.log >/dev/null) \
| \
if [[ "${QCTOOLSXML_CHOICE}" = "Yes, concurrent with recording" ]] ; then
tee >("${FFPLAY_DECKLINK[@]}" \
-v info -hide_banner -stats -autoexit -i - \
-window_title "${WINDOW_NAME}" \
-vf "${PLAYBACKFILTER}") | \
qcli -i - -o "${QCXML}"
else
"${FFPLAY_DECKLINK[@]}" \
-v info -hide_banner -stats -autoexit -i - \
-window_title "${WINDOW_NAME}" \
-vf "${PLAYBACKFILTER}"
fi
fi
# capture errors from components of recording pipe
P1_ERR="${PIPESTATUS[0]}"
P2_ERR="${PIPESTATUS[1]}"
P3_ERR="${PIPESTATUS[2]}"
if [[ ! -f "${VRECORD_OUTPUT}" ]] ; then
if [[ "$P1_ERR" != "0" ]] ; then
_report -wts "Error: Running: ${RECORD_COMMAND[@]} gave an Error Code - ${P1_ERR}"
_report -w "Consider reporting this or asking for help at https://github.com/amiaopensource/vrecord/issues"
_writeingestlog "FFmpeg command Error" "${P1_ERR}"
exit 1
fi
fi
_writeingestlog "datetime_end" "$(_get_iso8601)"
trap _cleanup SIGHUP SIGINT SIGTERM
# qc tools process
if [[ "${QCTOOLSXML_CHOICE}" = "Yes, after recording" ]] ; then
qcli -i "${VRECORD_OUTPUT}" -o "${QCXML}"
fi
if [[ "${QCTOOLSXML_CHOICE}" != "No" ]] ; then
_report -d "Vrecord is analyzing your video file. Please be patient."
if [[ -s "${QCXML}" ]] ; then
if [[ "${PIXEL_FORMAT}" = "yuv422p10" ]] ; then
SAT_OUTLIERS=$(gzcat "${QCXML}" | perl -nle 'print if not m{lavfi.(?!signalstats.SATMAX)}' | xml sel -t -v "count(//tag[@key='lavfi.signalstats.SATMAX'][@value>496])" -n)
elif [[ "${PIXEL_FORMAT}" = "uyvy422" ]] ; then
SAT_OUTLIERS=$(gzcat "${QCXML}" | perl -nle 'print if not m{lavfi.(?!signalstats.SATMAX)}' | xml sel -t -v "count(//tag[@key='lavfi.signalstats.SATMAX'][@value>124])" -n)
fi
AUD_OUTLIERS=$(gzcat "${QCXML}" | perl -nle 'print if not m{lavfi.(?!astats.Overall.Peak_level)}' | grep -v "tag key=\"lavfi.[^a]" | xml sel -t -v "count(//tag[@key='lavfi.astats.Overall.Peak_level'][@value>=-0.01])" -n)
BRNG_OUTLIERS=$(gzcat "${QCXML}" | perl -nle 'print if not m{lavfi.(?!signalstats.BRNG)}' | xml sel -t -v "count(//tag[@key='lavfi.signalstats.BRNG'][@value>=0.03])" -n)
AUDIO_PEAK=$(gzcat "${QCXML}" | grep lavfi.astats.Overall.Peak_level | cut -d '"' -f 4 | sort -n | tail -n 1)
_writeingestlog "Peak Volume is (dB)" "${AUDIO_PEAK}"
else
_report -w "qctools XML ${QCXML} is empty or does not exist!"
fi
if [[ "${SAT_OUTLIERS}" -gt "${SAT_OUTLIER_THRSHLD}" ]] ; then
cowsay "$(_report -w "WARNING: Your video file contains ${SAT_OUTLIERS} frames with illegal saturation values. Your deck may require cleaning.")"
fi
if [[ "${AUD_OUTLIERS}" -gt "${AUD_OUTLIER_THRSHLD}" ]] ; then
cowsay "$(_report -w "WARNING: Your video file contains ${AUD_OUTLIERS} frames with clipped audio levels.")"
fi
if [[ "${BRNG_OUTLIERS}" -gt "${BRNG_OUTLIER_THRSHLD}" ]] ; then
cowsay "$(_report -w "WARNING: Your video file contains ${BRNG_OUTLIERS} frames with pixels out of broadcast range.")"
fi
_report -d "QCTools analysis is complete."
fi
# check for discontinuities in the Frame MD5s; if user chose not to use Frame MD5s, check for frame discontinuties in the FFmpeg file
if [[ "${FRAMEMD5_CHOICE}" = "Yes" ]] ; then
PTS_DISCONTINUITY=$(cat "${FRAMEMD5NAME}" | grep -v "^#" | cut -d, -f3 | sed 's/ //g' | grep -v "^0$" | awk '$1!=p+1{printf p+1"-"$1-1" "}{p=$1}')
if [[ -z "${PTS_DISCONTINUITY}" ]] ; then
_writeingestlog "PTS_DISCONTINUITY" "none"
else
_writeingestlog "PTS_DISCONTINUITY" "${PTS_DISCONTINUITY}"
cowsay "$(_report -w "WARNING: There were presentation timestamp discontinuities in the file's frame MD5s for these frame ranges: ${PTS_DISCONTINUITY}. This error may indicate frames dropped by FFmpeg or vrecord. The file may have sync issues.")"
fi
elif [[ "${FRAMEMD5_CHOICE}" = "No" ]] ; then
FRAMES_ENCODED=$(cat "${LOGDIR}/${ID}"_ffmpeg_*.log | grep -w "frames encoded" | awk '{print $10}' | grep -m 1 [0-9])
FRAMES_DECODED=$(cat "${LOGDIR}/${ID}"_ffmpeg_*.log | grep -w "frames decoded" | awk '{print $10}' | grep -m 1 [0-9])
if [[ "${FRAMES_ENCODED}" -lt $((FRAMES_DECODED-1)) ]] ; then
FRAMES_DISCREPANCY=$((FRAMES_DECODED-FRAMES_ENCODED))
cowsay "$(_report -w "WARNING: There were presentation timestamp discontinuities found in the framemd5s. This error may indicate frames dropped by FFmpeg or vrecord. The file may have sync issues.")"
_writeingestlog "ffmpeg_missing_frames" "${FRAMES_DISCREPANCY}"
else
_writeingestlog "ffmpeg_missing_frames" "None"
fi
fi
# check for frames dropped in decklink_input.log
DROPPED_FRAMES_INSTANCES=$(grep -c "Frames dropped" "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}")
DROPPED_FRAMES_FRAMENUMBERS=$(grep "Frames dropped" "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}" | awk '{print $6}' | sed 's/[(#)]//g')
if [[ "${DROPPED_FRAMES_INSTANCES}" -gt 0 ]] ; then
cowsay "$(_report -w "WARNING: FFmpeg Decklink input reported dropped frames in the following ${DROPPED_FRAMES_INSTANCES} locations. This error may indicate an interrupted signal between hardware components. The file may be missing content.")"
for i in ${DROPPED_FRAMES_FRAMENUMBERS} ; do # do not quote this variable
DROPPED_FRAMES_TIMESTAMPS+="$(_frames_to_hhmmss "${i}") "
done
_report -w "Dropped frames timestamps: ${DROPPED_FRAMES_TIMESTAMPS}"
_writeingestlog "DROPPED_FRAMES_TIMESTAMPS" "${DROPPED_FRAMES_TIMESTAMPS}"
fi
# check for input buffer overrun error
BUFFER_OVERRUN=$(grep -c "Decklink input buffer overrun" "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}")
if [[ "${BUFFER_OVERRUN}" -gt 0 ]] ; then
cowsay "$(_report -w "WARNING: FFmpeg Decklink input reported a buffer overrun. The file is likely missing frames or contains artifacts from the buffer overrun.")"
_writeingestlog "Decklink input buffer overrun" "Yes"
fi
# policy checks with mediaconch
if [[ "${VIDEO_CODEC_CHOICE}" = "Uncompressed Video" ]] ; then
_report -d "Checking file conformance against uncompressed video policy..."
STATUS=$(mediaconch -fx -p "${SCRIPTDIR}/vrecord_policy_uncompressed.xml" "${VRECORD_OUTPUT}" | xmlstarlet sel -N mc="https://mediaarea.net/mediaconch" -t -v mc:MediaConch/mc:media/mc:policy/@outcome -n)
if [[ "$STATUS" = "pass" ]] ; then
_report -dt "File passed policy check for uncompressed video."
elif [[ "$STATUS" = "fail" ]] ; then
_report -wt "File did not pass vrecord policy check for uncompressed video and may not conform to digital preservation standards. Try another file?"
mediaconch -fx -p "${SCRIPTDIR}/vrecord_policy_uncompressed.xml" "${VRECORD_OUTPUT}" | xmlstarlet fo > "${DIR}/${ID}${SUFFIX}_mediaconchreport.xml"
_report -wt "See ${DIR}/${ID}${SUFFIX}_mediaconchreport.xml for a full MediaConch policy report."
else
mediaconch -p "${SCRIPTDIR}/vrecord_policy_uncompressed.xml" "${VRECORD_OUTPUT}"
fi
elif [[ "${VIDEO_CODEC_CHOICE}" = "FFV1 version 3" ]] ; then
_report -d "Checking file conformance against FFV1 video policy..."
STATUS=$(mediaconch -fx -p "${SCRIPTDIR}/vrecord_policy_ffv1.xml" "${VRECORD_OUTPUT}" | xmlstarlet sel -N mc="https://mediaarea.net/mediaconch" -t -v mc:MediaConch/mc:media/mc:policy/@outcome -n)
if [[ "${STATUS}" = "pass" ]] ; then
_report -dt "File passed policy check for FFV1 video."
elif [[ "$STATUS" = "fail" ]] ; then
_report -wt "File did not pass vrecord policy check for FFV1 video and may not conform to digital preservation standards. Try another file?"
mediaconch -fx -p "${SCRIPTDIR}/vrecord_policy_ffv1.xml" "${VRECORD_OUTPUT}" | xmlstarlet fo > "${DIR}/${ID}${SUFFIX}_mediaconchreport.xml"
_report -wt "See ${DIR}/${ID}${SUFFIX}_mediaconchreport.xml for a full MediaConch policy report."
else
mediaconch -p "${SCRIPTDIR}/vrecord_policy_ffv1.xml" "${VRECORD_OUTPUT}"
fi
fi
# embed logs in Matroska files
if [[ "${CONTAINER_CHOICE}" = "Matroska" ]] && [[ "${EMBED_LOGS_CHOICE}" = "Yes" ]] ; then
_report -d "Vrecord is attaching logs to your Matroska file:"
mkvpropedit "${VRECORD_OUTPUT}" --attachment-description "Capture options selected by user during vrecord process" --add-attachment "${INGESTLOG}"
mkvpropedit "${VRECORD_OUTPUT}" --attachment-description "Full FFmpeg output from vrecord capture process" --add-attachment "${LOGDIR}/${ID}${CAPTURELOGSUFFIX}"
if [[ "${QCTOOLSXML_CHOICE}" = "Yes" ]] ; then
mkvpropedit "${VRECORD_OUTPUT}" --attachment-description "QCTools report from vrecord capture process (zipped XML)" --add-attachment "${QCXML}"
fi
_report -d "Vrecord is done attaching logs to your Matroska file!"
fi