Skip to content

Instantly share code, notes, and snippets.

@eddy-22
Last active August 3, 2023 23:58
Show Gist options
  • Save eddy-22/55d368c32e8037c58a722f4f72f73305 to your computer and use it in GitHub Desktop.
Save eddy-22/55d368c32e8037c58a722f4f72f73305 to your computer and use it in GitHub Desktop.
ffmpeg cheat sheet and useful scripts
#!/bin/sh
#===============================================================================
# audio-silence
# add silent audio to a video clip
#===============================================================================
# dependencies:
# ffmpeg ffprobe
#===============================================================================
# script usage
#===============================================================================
usage ()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# audio-silence add silent audio to a video clip
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -c (mono|stereo) -r (44100|48000) -o output.mp4
-i input.(mp4|mkv|mov|m4v)
-c (mono|stereo) : optional argument # if option not provided defaults to mono
-r (44100|48000) : optional argument # if option not provided defaults to 44100
-o output.mp4 : optional argument # if option not provided defaults to input-name-silence-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:r:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
c) channel="${OPTARG}"
{ [ "${channel}" = 'mono' ] || [ "${channel}" = 'stereo' ]; } || usage;;
r) rate="${OPTARG}"
{ [ "${rate}" = '44100' ] || [ "${rate}" = '48000' ]; } || usage;;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables if not defined
channel_default='mono'
rate_default='44100'
outfile_default="${infile_name}-silence-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
# video function
video_silence () {
ffmpeg \
-hide_banner \
-stats -v panic \
-f lavfi \
-i anullsrc=channel_layout="${channel:=${channel_default}}":sample_rate="${rate:=${rate_default}}" \
-i "${infile}" \
-shortest -c:v copy -c:a "${aac}" \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# video and audio function
video_audio_silence () {
ffmpeg \
-hide_banner \
-stats -v panic \
-f lavfi \
-i anullsrc=channel_layout="${channel:=${channel_default}}":sample_rate="${rate:=${rate_default}}" \
-i "${infile}" \
-shortest -c:v copy -c:a "${aac}" \
-map 0:0 -map 1:0 \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# check if the video has an audio track
audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)"
# check if audio_check is null which means the video doesnt have an audio track
if [ -z "${audio_check}" ]; then
video_silence "${infile}" # null value
else
video_audio_silence "${infile}" # non null value
fi
#!/bin/sh
#===============================================================================
# chapter-add
# add chapters to a video or audio file with ffmpeg
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
add chapters to a video or audio file with ffmpeg
$(basename "$0") -i input -c metadata.txt -o output"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
c) metadata="${OPTARG}";;
h) usage;;
o) output="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# input file extension
input_ext="${input##*.}"
# output file name
output_default="${input_name}-metadata.${input_ext}"
#===============================================================================
# sexagesimal function
#===============================================================================
meta () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${input}" \
-i "${metadata}" -map_metadata 1 -c copy \
"${output:=${output_default}}"
}
#===============================================================================
# run function
#===============================================================================
meta
#!/bin/sh
#===============================================================================
# chapter-csv
# convert a csv file into a chapter metadata file for ffmpeg
#===============================================================================
# dependencies:
# awk
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
convert a csv file into a chapter metadata file for ffmpeg
$(basename "$0") -i input -o output"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
h) usage;;
o) output="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# output file name
output_default="${input_name}-metadata.txt"
#===============================================================================
# sexagesimal function
#===============================================================================
sexagesimal () {
total="$(awk 'END { print NR }' < "${input}")"
awk -F "," '
{
time = $1
chapter = $2
if (time ~ /:/) {
split(time, t, ":")
combined = (t[1] * 3600) + (t[2] * 60) + t[3]
milliseconds = (combined * 1000)
}
printf("%d,%s\n"), milliseconds, chapter
}' < "${input}" \
| awk -v records="$total" -F "," ' \
{ if (FNR < records) end = $1-1; else end = $1 }
NR == 1 {print ";FFMETADATA1"} NR > 1 {printf("%s\n%s\n%s%s\n%s%s\n%s%s\n"), "[CHAPTER]", "TIMEBASE=1/1000", "START=", prev, "END=", end, "title=", chapter}; {prev = $1; chapter = $2}' > "${output:=${output_default}}"
}
#===============================================================================
# run function
#===============================================================================
sexagesimal
#!/bin/sh
#===============================================================================
# chapter-extract
# extract chapters from a video or audo file and save as a csv file
#===============================================================================
# dependencies:
# ffprobe awk
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
extract chapters from a video or audo file and save as a csv file
$(basename "$0") -i input -o output"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
h) usage;;
o) output="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# output file name
output_default="${input_name}-chapters.txt"
#===============================================================================
# sexagesimal function
#===============================================================================
extract () {
ffprobe -v panic -hide_banner \
-show_chapters -sexagesimal -of csv \
-i "${input}" \
| awk -F "," '{
time = $5
chapter = $NF
if (time ~ /:/) {
split(time, t, ".")
sexagesimal = t[1]
}
printf("%s,%s\n"), sexagesimal, chapter
}' > "${output:=${output_default}}"
}
#===============================================================================
# run function
#===============================================================================
extract
#!/bin/sh
#===============================================================================
# clip-time
# create ffmpeg cutlist
#===============================================================================
# dependencies:
# ffmpeg ffprobe
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
$(basename "$0") -i input -o output
-i input
-o output"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
h) usage;;
o) output="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# output file name
output_default="${input_name}-cutlist-$(date +"%Y-%m-%d-%H-%M-%S").txt"
#===============================================================================
# awk subtract 2nd line from first line - print start and duration
#===============================================================================
seconds () {
awk -F. 'NR > 1 && NR%2 == 0 {printf("%s%s%s\n"), prev, ",", $0-prev}; {prev = $0}' < "${input}" > "${output:=${output_default}}"
}
#===============================================================================
# convert sexagesimal to seconds - and subtract 2nd line from first line
# convert from seconds back to sexagesimal
#===============================================================================
minutes () {
awk -F: 'NF==3 { printf("%s\n"), ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { printf("$s\n"), 0 + $1 }' < "${input}" \
| awk 'NR > 1 && NR%2 == 0 {printf("%s%s%s\n"), prev, "\n", $0-prev}; {prev = $0}' \
| awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\
NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \
| awk '{ORS = NR%2 ? "," : "\n"} 1' > "${output:=${output_default}}"
}
#===============================================================================
# timecode regex
#===============================================================================
# grab the first line of the file
check=$(head -n1 "${input}")
# minutes - match 00:00:00.000
minutes_regex='^[0-9]{1,2}:[0-9]{2}:[0-9]{2}([.]{1}[0-9]{1,6})?$'
# seconds - match 00:00:00.000
seconds_regex='^[0-9]{1,8}([.]{1}[0-9]{1,6})?$'
# grep for the minutes
minutes=$(echo "${check}" | grep -E "${minutes_regex}")
# grep for the seconds
seconds=$(echo "${check}" | grep -E "${seconds_regex}")
#===============================================================================
# check timecode and run function
#===============================================================================
if [ -n "${minutes}" ]; then
minutes
elif [ -n "${seconds}" ]; then
seconds
else
usage
fi
#!/bin/sh
#===============================================================================
# combine-clips
# combine an image or video file with an audio clip
#===============================================================================
# dependencies:
# ffmpeg file grep
#===============================================================================
# script usage
#===============================================================================
usage ()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# combine an image or video file with an audio clip
$(basename "$0") -i input.(mp4|mkv|mov|m4v|png|jpg) -a audio.(m4a|aac|wav|mp3) -o output.mp4
-i input.(mp4|mkv|mov|m4v|png|jpg)
-a audio.(m4a|aac|wav|mp3)
-o output.mp4 : optional argument # if option not provided defaults to input-name-combined-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:a:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
a) audio="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# audio file extension
audio_ext="${audio##*.}"
# file command check input file mime type
infile_filetype="$(file --mime-type -b "${infile}")"
audio_filetype="$(file --mime-type -b "${audio}")"
# audio and video mimetypes
mov_mime='video/quicktime'
mkv_mime='video/x-matroska'
mp4_mime='video/mp4'
m4v_mime='video/x-m4v'
aac_mime='audio/x-hx-aac-adts'
m4a_mime='audio/mp4'
# the file command wrongly identifies .m4a audio as a video file
# so we check if the file extension is .m4a and set the mime type to audio/mp4
if [ "${audio_ext}" = 'm4a' ]; then
audio_filetype="${m4a_mime}"
fi
# image mimetypes
png_mime='image/png'
jpg_mime='image/jpeg'
# defaults for variables
outfile_default="${infile_name}-combined-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
# video - audio is aac, copy audio stream
record_copy () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-i "${audio}" \
-shortest -fflags shortest -max_interleave_delta 100M \
-c:a copy \
-c:v copy \
-map 0:0 -map 1:0 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# video - audio isnt aac, encode audio as aac
record_aac () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-i "${audio}" \
-shortest -fflags shortest -max_interleave_delta 100M \
-c:a "${aac}" \
-c:v copy \
-map 0:0 -map 1:0 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# image - audio is aac, copy audio stream
record_img_copy () {
dur="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audio}")"
ffmpeg \
-hide_banner \
-stats -v panic \
-framerate 1/"${dur}" \
-i "${infile}" \
-i "${audio}" \
-c:a copy \
-c:v libx264 -crf 18 -profile:v high \
-r 30 -pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# image - audio isnt aac, encode audio as aac
record_img_aac () {
dur="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audio}")"
ffmpeg \
-hide_banner \
-stats -v panic \
-framerate 1/"${dur}" \
-i "${infile}" \
-i "${audio}" \
-c:a "${aac}" \
-c:v libx264 -crf 18 -profile:v high \
-r 30 -pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
#===============================================================================
# case statement
#===============================================================================
# run the ffmpeg function based on the audio mime type
case "${infile_filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}")
if [ "${audio_filetype}" = 'audio/x-hx-aac-adts' ]; then
# video - audio is aac, copy audio stream
record_copy
else
# video - audio isnt aac, encode audio as aac
record_aac
fi;;
"${png_mime}"|"${jpg_mime}")
if [ "${audio_filetype}" = "${aac_mime}" ]; then
# image - audio is aac, copy audio stream
record_img_copy
else
# image - audio isnt aac, encode audio as aac
record_img_aac
fi;;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
#!/bin/sh
#===============================================================================
# correct-clip
# correct a video clip by using a gimp curve
#===============================================================================
# code based on:
# https://video.stackexchange.com/questions/16352/converting-gimp-curves-files-to-photoshop-acv-for-ffmpeg/20005#20005
# converted into a ffmpeg curves filter command
# to adjust the levels and white balance
# requires a curve file created with the following script
# https://github.com/NapoleonWils0n/curve2ffmpeg
# dependencies:
# ffmpeg file grep
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
echo "\
# correct a video clip by using a gimp curve
# requires a curve file created with the following script
# https://github.com/NapoleonWils0n/curve2ffmpeg
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -c curve.txt -o output.mp4
-i input.(mp4|mkv|mov|m4v)
-c curve.txt
-o output.mp4 : optional argument # if option not provided defaults to input-name-corrected-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
NOT_TEXT_FILE_ERR='is not a text file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
c) text="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) echo "${INVALID_OPT_ERR} ${OPTARG}" 1>&2 && usage;;
:) echo "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2 && usage;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables
outfile_default="${infile_name}-corrected-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
correct_v () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
"[0:v]${text_contents}[video]" \
-map "[video]" \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# correct video and audio tracks
correct_va () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
"[0:v]${text_contents}[video]" \
-map "[video]" -map 0:a \
-c:a "${aac}" \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# file command check input file mime type
filetype="$(file --mime-type -b "${infile}")"
textfile="$(file --mime-type -b "${text}")"
# video mimetypes
mov_mime='video/quicktime'
mkv_mime='video/x-matroska'
mp4_mime='video/mp4'
m4v_mime='video/x-m4v'
# text mimetype
txt_mime='text/plain'
# check the files mime type is a video
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}");;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
# check the text file mime type is a text
case "${textfile}" in
"${txt_mime}");;
*) usage "${textfile} ${NOT_TEXT_FILE_ERR}";;
esac
# read the contents of the curve text file and store in a variable
text_contents="$(while IFS= read -r line; do echo "${line}"; done < "${text}")"
# check if video has an audio track
audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)"
# check if audio_check is null which means the video doesnt have an audio track
if [ -z "${audio_check}" ]; then
correct_v "${infile}" # null value
else
correct_va "${infile}" # non null value
fi
#!/bin/sh
#===============================================================================
# crossfade-clips
# cross fade video clips
#===============================================================================
# dependencies:
# ffmpeg file grep
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# ffmpeg cross fade clips
$(basename "$0") -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d (1|2) -o output.mp4
-a clip1.(mp4|mkv|mov|m4v) : first clip
-b clip2.(mp4|mkv|mov|m4v) : second clip
-d (1|2) : cross fade duration :optional argument # if option not provided defaults to 1 second
-o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':a:b:d:o:h' opt
do
case ${opt} in
a) clip1="${OPTARG}";;
b) clip2="${OPTARG}";;
d) dur="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
clip1_nopath="${clip1##*/}"
clip1_name="${clip1_nopath%.*}"
# clip durations for fades
clip1_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$clip1" | cut -d\. -f1)
clip2_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$clip2" | cut -d\. -f1)
# clip1 use the bc command to remove 1 second from length of clip for cross fade
clip1_offset=$(echo "${clip1_dur}-1" | bc -l)
clip2_offset=$(echo "${clip2_dur}-1" | bc -l)
# variables
outfile_default="${clip1_name}-xfade-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
duration_default='1'
#===============================================================================
# functions
#===============================================================================
# cross fade video and audio
xfade_va () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${clip1}" -i "${clip2}" \
-an -filter_complex \
" [0:v]trim=start=0:end='${clip1_offset}',setpts=PTS-STARTPTS[firstclip];
[1:v]trim=start=${dur:=${duration_default}},setpts=PTS-STARTPTS[secondclip];
[0:v]trim=start='${clip1_offset}':end='${clip1_dur}',setpts=PTS-STARTPTS[fadeoutsrc];
[1:v]trim=start=0:end=${dur:=${duration_default}},setpts=PTS-STARTPTS[fadeinsrc];
[fadeinsrc]format=pix_fmts=yuva420p,
fade=t=in:st=0:d=${dur:=${duration_default}}:alpha=1[fadein];
[fadeoutsrc]format=pix_fmts=yuva420p,
fade=t=out:st=0:d=${dur:=${duration_default}}:alpha=1[fadeout];
[fadein]fifo[fadeinfifo];
[fadeout]fifo[fadeoutfifo];
[fadeoutfifo][fadeinfifo]overlay[crossfade];
[firstclip][crossfade][secondclip]concat=n=3[output];
[0:a] afade=t=in:st=0:d=${dur:=${duration_default}} [audiofadein];
[1:a] afade=t=out:st='${clip2_offset}':d=${dur:=${duration_default}} [audiofadeout];
[audiofadein][audiofadeout] acrossfade=d=${dur:=${duration_default}} [audio]
" \
-map "[output]" -map "[audio]" "${outfile:=${outfile_default}}"
}
# cross fade video
xfade_v () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${clip1}" -i "${clip2}" \
-an -filter_complex \
" [0:v]trim=start=0:end='${clip1_offset}',setpts=PTS-STARTPTS[firstclip];
[1:v]trim=start=${dur:=${duration_default}},setpts=PTS-STARTPTS[secondclip];
[0:v]trim=start='${clip1_offset}':end='${clip1_dur}',setpts=PTS-STARTPTS[fadeoutsrc];
[1:v]trim=start=0:end=${dur:=${duration_default}},setpts=PTS-STARTPTS[fadeinsrc];
[fadeinsrc]format=pix_fmts=yuva420p,
fade=t=in:st=0:d=${dur:=${duration_default}}:alpha=1[fadein];
[fadeoutsrc]format=pix_fmts=yuva420p,
fade=t=out:st=0:d=${dur:=${duration_default}}:alpha=1[fadeout];
[fadein]fifo[fadeinfifo];
[fadeout]fifo[fadeoutfifo];
[fadeoutfifo][fadeinfifo]overlay[crossfade];
[firstclip][crossfade][secondclip]concat=n=3[output]
" \
-map "[output]" "${outfile:=${outfile_default}}"
}
# check if video has an audio track
clip1_check="$(ffprobe -i "${clip1}" -show_streams -select_streams a -loglevel error)"
clip2_check="$(ffprobe -i "${clip2}" -show_streams -select_streams a -loglevel error)"
# check if audio_check is null which means the video doesnt have an audio track
if [ -z "${clip1_check}" ] || [ -z "${clip2_check}" ]; then
xfade_v "${clip1}" "${clip2}" # fade video track
else
xfade_va "${clip1}" "${clip2}" # fade video and audio track
fi
#!/bin/sh
#===============================================================================
# ebu-meter
# ffplay ebu meter
#===============================================================================
# dependencies:
# ffplay
#===============================================================================
# script usage
#===============================================================================
usage()
{
[ -z "${1}" ] || echo "! ${1}"
echo "\
# ffplay ebu meter
$(basename "$0") -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -t (00)"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
# getopts check and validate options
while getopts ':i:t:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
t) target="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# default target level
target_default='16'
#===============================================================================
# functions
#===============================================================================
# ebu function
ebu () {
ffplay -hide_banner \
-f lavfi -i \
"amovie=${infile},
ebur128=video=1:
meter=18:
dualmono=true:
target=-${target:=${target_default}}:
size=1280x720 [out0][out1]"
}
# run the ebu function
ebu "${infile}"
#!/bin/sh
#===============================================================================
# extract-frame
# extract a frame from a video as a png file
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# extract a frame from a video as a png file
https://trac.ffmpeg.org/wiki/Seeking
$(basename "$0") -i input.(mp4|mov|mkv|m4v|webm) -s 00:00:00.000 -o output.png
-i input.(mp4|mov|mkv|m4v)
-s 00:00:00.000 : optional argument # if option not provided defaults to 00:00:00
-o outfile.png : optional argument # if option not provided defaults to input-name-frame-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:s:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
s) seconds="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# output file recording destination
outfile_default="${infile_name}-frame-$(date +"%Y-%m-%d-%H-%M-%S").png"
seconds_default='00:00:00'
#===============================================================================
# functions
#===============================================================================
# image to video function
extract () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${seconds:=${seconds_default}}" \
-i "${infile}" \
-q:v 2 -f image2 \
-vframes 1 \
"${outfile:=${outfile_default}}"
}
# run the extract function
extract "${infile}"
#!/bin/sh
#===============================================================================
# fade-clip
# fade video and audio in and out
#===============================================================================
# dependencies:
# ffmpeg bc
#===============================================================================
# script usage
#===============================================================================
usage()
{
echo "\
# fade video and audio in and out
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4
-i infile.(mp4|mkv|mov|m4v)
-d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5
-o output.mp4 : optional argument # if option not provided defaults to input-name-fade-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables if not defined
outfile_default="${infile_name}-fade-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
duration_default="0.5"
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
# ffmpeg fade video track
fade_v () {
echo '+ Getting video duration' && \
video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1)
vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l)
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
" [0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fv] " \
-map "[fv]" \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# fade video and audio tracks
fade_va () {
echo '+ Getting video duration' && \
video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1)
vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l)
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
" [0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fa];
[0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fv]
" \
-map "[fv]" -map "[fa]" \
-c:a "${aac}" \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# check if video has an audio track
audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)"
# check if audio_check is null which means the video doesnt have an audio track
if [ -z "${audio_check}" ]; then
fade_v "${infile}" # null value
else
fade_va "${infile}" # non null value
fi
#!/bin/sh
#===============================================================================
# fade-normalize
# fade video and normalize audio levels
#===============================================================================
# dependencies:
# ffmpeg awk grep
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# fade video and normalize audio levels
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4
-d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5
-o output.mp4 : optional argument # if option not provided defaults to input-name-normalized-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and file extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables if not defined
outfile_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
duration_default="0.5"
# print analyzing file
echo '+ Analyzing file with ffmpeg'
# ffmpeg loudnorm get stats from file
normalize=$(ffmpeg \
-hide_banner \
-i "${infile}" \
-af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \
-f null - 2>&1 | tail -n 12)
# read the output of normalize line by line and store in variables
for line in "${normalize}"; do
measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}')
measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}')
measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}')
measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}')
offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}')
done
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
# video function
video () {
video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1)
vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l)
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
"[0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}},
compand=attacks=0:points=-70/-90|-24/-12|0/-6|20/-6,
highpass=f=60,
lowpass=f=13700,
afftdn=nt=w,
adeclick,
deesser,
loudnorm=I=-16:
dual_mono=true:
TP=-1.5:
LRA=11:
measured_I=${measured_I}:
measured_LRA=${measured_LRA}:
measured_TP=${measured_TP}:
measured_thresh=${measured_thresh}:
offset=${offset}:
linear=true:
print_format=summary [audio];
[0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[video]" \
-map "[video]" -map "[audio]" \
-c:a "${aac}" -ar 44100 \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# run the video function
video "${infile}"
#!/bin/sh
#===============================================================================
# fade-title
# fade video, audio add title from video filename
#===============================================================================
# dependencies:
# ffmpeg file awk grep bc cut tail
#===============================================================================
# script usage
#===============================================================================
usage ()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# fade video, audio add title from video filename
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -s 000 -e 000 -o output.mp4
-i input.(mp4|mkv|mov|m4v)
-d (0.[0-9]|1) : from 0.1 to 0.9 or 1 :optional argument # if option not provided defaults to 0.5
-s 000 : from 000 to 999
-e 000 : from 000 to 999
-o output.mp4 : optional argument # if option not provided defaults to input-name-title-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
TITLE_FADE_ERR='title end must be after title start'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:s:e:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
s) title_start="${OPTARG}";;
e) title_end="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and file extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# file command check input file mime type
filetype="$(file --mime-type -b "${infile}")"
# video mimetypes
mov_mime='video/quicktime'
mkv_mime='video/x-matroska'
mp4_mime='video/mp4'
m4v_mime='video/x-m4v'
# check the files mime type
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}");;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
# defaults for variables if not defined
outfile_default="${infile_name}-title-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
duration_default="0.5"
# print analyzing file
echo '+ Analyzing file with ffmpeg'
# ffmpeg loudnorm get stats from file
normalize=$(ffmpeg \
-hide_banner \
-i "${infile}" \
-af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \
-f null - 2>&1 | tail -n 12)
# read the output of normalize line by line and store in variables
for line in "${normalize}"; do
measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}')
measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}')
measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}')
measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}')
offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}')
done
# video duration and video offset minus 1 second for fade out
video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1)
vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l)
# video height
video_size=$(ffprobe -v error -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "${infile}")
# video title from filename
title="${infile_name}"
# video title variables
font="OpenSans-Regular.ttf"
font_color="white"
boxcolor="black@0.4"
# video title fade
DS="${title_start}" # display start
DE="${title_end}" # display end, number of seconds after start
FID="${dur:=${duration_default}}" # fade in duration
FOD="${dur:=${duration_default}}" # fade out duration
# check title end is a number larger than title start
if [ "${DE}" -le "${DS}" ]; then
echo "${TITLE_FADE_ERR}" && usage
fi
# calculate drawbox and drawtext size based on video height
case "${video_size}" in
1080) # 1080 height
drawbox_height=$(echo "${video_size}/13.4" | bc)
drawtext_size=$(echo "${drawbox_height}/2" | bc)
;;
720) # 720 height
drawbox_height=$(echo "${video_size}/9" | bc)
drawtext_size=$(echo "${drawbox_height}/2" | bc)
;;
*) # all other heights
drawbox_height=$(echo "${video_size}/9" | bc)
drawtext_size=$(echo "${drawbox_height}/2" | bc)
;;
esac
# drawbox, drawtext size
boxheight="${drawbox_height}"
font_size="${drawtext_size}"
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# functions
#===============================================================================
# video function
video () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
"[0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}},
compand=attacks=0:points=-70/-90|-24/-12|0/-6|20/-6,
highpass=f=60,
lowpass=f=13700,
afftdn=nt=w,
adeclick,
deesser,
loudnorm=I=-16:
dual_mono=true:
TP=-1.5:
LRA=11:
measured_I=${measured_I}:
measured_LRA=${measured_LRA}:
measured_TP=${measured_TP}:
measured_thresh=${measured_thresh}:
offset=${offset}:
linear=true:
print_format=summary [audio];
[0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}, \
format=yuv444p,
drawbox=enable='between(t,${DS},${DE})':
y=(ih-h/PHI)-(${boxheight}):
color=${boxcolor}:
width=iw:height=${boxheight}:t=fill,
drawtext=fontfile=${font}:
text=${title}:
fontcolor=${font_color}:fontsize=${font_size}:
x=20:
y=h-(${boxheight})-(${boxheight}/2)+th/4:
:fontcolor_expr=fdfdfd%{eif\\\\: clip(255*(1*between(t\\, $DS + $FID\\, $DE - $FOD) + ((t - $DS)/$FID)*between(t\\, $DS\\, $DS + $FID) + (-(t - $DE)/$FOD)*between(t\\, $DE - $FOD\\, $DE) )\\, 0\\, 255) \\\\: x\\\\: 2 }, \
format=yuv420p[video]" \
-map "[video]" -map "[audio]" \
-c:a "${aac}" -ar 44100 \
-c:v libx264 -preset fast \
-profile:v high \
-crf 18 -coder 1 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# check the files mime type
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") video "${infile}";;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac

FFmpeg cheat sheet

A list of useful commands for the ffmpeg command line tool.

Download FFmpeg: https://www.ffmpeg.org/download.html

Full documentation: https://www.ffmpeg.org/ffmpeg.html

Excellent cheat sheet and resource at https://amiaopensource.github.io/ffmprovisr/

Basic conversion

ffmpeg -i in.mp4 out.avi

Remux an MKV file into MP4

ffmpeg -i in.mkv -c:v copy -c:a copy out.mp4

High-quality encoding

Use the crf (Constant Rate Factor) parameter to control the output quality. The lower crf, the higher the quality (range: 0-51). The default value is 23, and visually lossless compression corresponds to -crf 18. Use the preset parameter to control the speed of the compression process. Additional info: https://trac.ffmpeg.org/wiki/Encode/H.264

ffmpeg -i in.mp4 -preset slower -crf 18 out.mp4

Trimming

Without re-encoding:

ffmpeg -ss [start] -i in.mp4 -t [duration] -c copy out.mp4
  • -ss specifies the start time, e.g. 00:01:23.000 or 83 (in seconds)
  • -t specifies the duration of the clip (same format).
  • Recent ffmpeg also has a flag to supply the end time with -to.
  • -c copy copies the first video, audio, and subtitle bitstream from the input to the output file without re-encoding them. This won't harm the quality and make the command run within seconds.

With re-encoding:

If you leave out the -c copy option, ffmpeg will automatically re-encode the output video and audio according to the format you chose. For high quality video and audio, read the x264 Encoding Guide and the AAC Encoding Guide, respectively.

For example:

ffmpeg -ss [start] -i in.mp4 -t [duration] -c:v libx264 -c:a aac -strict experimental -b:a 128k out.mp4

Mux video and audio from another video

To copy the video from in0.mp4 and audio from in1.mp4:

ffmpeg -i in0.mp4 -i in1.mp4 -c copy -map 0:0 -map 1:1 -shortest out.mp4

Concat demuxer

First, make a text file.

file 'in1.mp4'
file 'in2.mp4'
file 'in3.mp4'
file 'in4.mp4'

Then, run ffmpeg:

ffmpeg -f concat -i list.txt -c copy out.mp4

Delay audio/video

Delay video by 3.84 seconds:

ffmpeg -i in.mp4 -itsoffset 3.84 -i in.mp4 -map 1:v -map 0:a -vcodec copy -acodec copy out.mp4

Delay audio by 3.84 seconds:

ffmpeg -i in.mp4 -itsoffset 3.84 -i in.mp4 -map 0:v -map 1:a -vcodec copy -acodec copy out.mp4

Burn subtitles

Use the libass library (make sure your ffmpeg install has the library in the configuration --enable-libass).

First convert the subtitles to .ass format:

ffmpeg -i sub.srt sub.ass

Then add them using a video filter:

ffmpeg -i in.mp4 -vf ass=sub.ass out.mp4

Extract the frames from a video

To extract all frames from between 1 and 5 seconds, and also between 11 and 15 seconds:

ffmpeg -i in.mp4 -vf select='between(t,1,5)+between(t,11,15)' -vsync 0 out%d.png

To extract one frame per second only:

ffmpeg -i in.mp4 -fps=1 -vsync 0 out%d.png

Rotate a video

Rotate 90 clockwise:

ffmpeg -i in.mov -vf "transpose=1" out.mov

For the transpose parameter you can pass:

0 = 90CounterCLockwise and Vertical Flip (default)
1 = 90Clockwise
2 = 90CounterClockwise
3 = 90Clockwise and Vertical Flip

Use -vf "transpose=2,transpose=2" for 180 degrees.

Download "Transport Stream" video streams

  1. Locate the playlist file, e.g. using Chrome > F12 > Network > Filter: m3u8
  2. Download and concatenate the video fragments:
ffmpeg -i "path_to_playlist.m3u8" -c copy -bsf:a aac_adtstoasc out.mp4

If you get a "Protocol 'https not on whitelist 'file,crypto'!" error, add the protocol_whitelist option:

ffmpeg -protocol_whitelist "file,http,https,tcp,tls" -i "path_to_playlist.m3u8" -c copy -bsf:a aac_adtstoasc out.mp4

Mute some of the audio

To replace the first 90 seconds of audio with silence:

ffmpeg -i in.mp4 -vcodec copy -af "volume=enable='lte(t,90)':volume=0" out.mp4

To replace all audio between 1'20" and 1'30" with silence:

ffmpeg -i in.mp4 -vcodec copy -af "volume=enable='between(t,80,90)':volume=0" out.mp4

Deinterlace

Deinterlacing using "yet another deinterlacing filter".

ffmpeg -i in.mp4 -vf yadif out.mp4

Create a video slideshow from images

Parameters: -r marks the image framerate (inverse time of each image); -vf fps=25 marks the true framerate of the output.

ffmpeg -r 1/5 -i img%03d.png -c:v libx264 -vf fps=25 -pix_fmt yuv420p out.mp4

Extract images from a video

  • Extract all frames: ffmpeg -i input.mp4 thumb%04d.jpg -hide_banner
  • Extract a frame each second: ffmpeg -i input.mp4 -vf fps=1 thumb%04d.jpg -hide_banner
  • Extract only one frame: ffmpeg -i input.mp4 -ss 00:00:10.000 -vframes 1 thumb.jpg

Display the frame number on each frame

ffmpeg -i in.mov -vf "drawtext=fontfile=arial.ttf: text=%{n}: x=(w-tw)/2: y=h-(2*lh): fontcolor=white: box=1: boxcolor=0x00000099: fontsize=72" -y out.mov

Metadata: Change the title

ffmpeg -i in.mp4 -map_metadata -1 -metadata title="My Title" -c:v copy -c:a copy out.mp4
#!/bin/sh
#===============================================================================
# img2video
# image to video
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage()
{
echo "\
# image to video
$(basename "$0") -i input.(png|jpg|jpeg) -d (000) -o output.mp4
-i input.(mp4|mkv|mov|m4v)
-d (000) : duration
-o output.mp4 : optional argument # if option not provided defaults to input-name-video-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# output file recording destination
outfile_default="${infile_name}-video-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# image to video function
imgtovid () {
ffmpeg \
-hide_banner \
-stats -v panic \
-framerate 1/"${dur}" \
-i "${infile}" \
-c:v libx264 -crf 18 -profile:v high \
-r 30 -pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# run the imgtovid function
imgtovid "${infile}"
#!/bin/sh
#===============================================================================
# loudnorm
# ffmpeg loudnorm
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# ffmpeg loudnorm
$(basename "$0") -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3)"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# functions
#===============================================================================
# ffmpeg loudnorm get stats from file
normalize () {
ffmpeg \
-hide_banner \
-i "${infile}" \
-af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \
-f null -
}
# run the normalize function
normalize "${infile}"
#!/bin/sh
#===============================================================================
# nomalize
# normalize audio levels
#===============================================================================
# dependencies:
# ffmpeg file awk tail
#===============================================================================
# script usage
#===============================================================================
usage()
{
echo "\
# normalize audio levels
$(basename "$0") -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3)
-i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3)
-o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) : optional argument
# if option not provided defaults to input-name-normalized-date-time-extension"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
infile_ext="${infile##*.}"
# file command check input file mime type
filetype="$(file --mime-type -b "${infile}")"
# audio and video mimetypes
mov_mime='video/quicktime'
mkv_mime='video/x-matroska'
mp4_mime='video/mp4'
m4v_mime='video/x-m4v'
wav_mime='audio/x-wav'
audio_mime='audio/mpeg'
aac_mime='audio/x-hx-aac-adts'
m4a_mime='audio/mp4'
# the file command wrongly identifies .m4a audio as a video file
# so we check if the file extension is .m4a and set the mime type to audio/mp4
if [ "${infile_ext}" = 'm4a' ]; then
filetype="${m4a_mime}"
fi
# check the files mime type
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}"| \
"${wav_mime}"|"${audio_mime}"|"${aac_mime}"|"${m4a_mime}");;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
# print analyzing file
echo '+ Analyzing file with ffmpeg'
# ffmpeg loudnorm get stats from file
normalize=$(ffmpeg \
-hide_banner \
-i "${infile}" \
-af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \
-f null - 2>&1 | tail -n 12)
# read the output of normalize line by line and store in variables
for line in "${normalize}"; do
measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}')
measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}')
measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}')
measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}')
offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}')
done
# defaults for variables if not defined
audio_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").${infile_ext}"
video_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# audio function
audio () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex \
"loudnorm=I=-16:
dual_mono=true:
TP=-1.5:
LRA=11:
measured_I=${measured_I}:
measured_LRA=${measured_LRA}:
measured_TP=${measured_TP}:
measured_thresh=${measured_thresh}:
offset=${offset}:
linear=true:
print_format=summary [audio]" \
-map "[audio]" \
-ar 44100 \
"${outfile:=${audio_default}}"
}
# video function
video () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-c:v copy \
-filter_complex \
"loudnorm=I=-16:
dual_mono=true:
TP=-1.5:
LRA=11:
measured_I=${measured_I}:
measured_LRA=${measured_LRA}:
measured_TP=${measured_TP}:
measured_thresh=${measured_thresh}:
offset=${offset}:
linear=true:
print_format=summary [audio]" \
-map 0:v -map "[audio]" \
-ar 44100 \
-pix_fmt yuv420p \
-movflags +faststart \
-f mp4 \
"${outfile:=${video_default}}"
}
# check if the mime type is audio or video then run the correct function
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") video "${infile}";;
"${wav_mime}"|"${audio_mime}"|"${aac_mime}"|"${m4a_mime}") audio "${infile}";;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
#!/bin/sh
#===============================================================================
# overlay-clip
# overlay one video clip on top of another video clip
#===============================================================================
# dependencies:
# ffmpeg ffprobe cut
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# overlay one video clip on top of another video clip
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] -o output.mp4
-i input.(mp4|mkv|mov|m4v) : bottom video
-v input.(mp4|mkv|mov|m4v) : overlay video
-p [0-999] : time to overlay the video
-o output.mp4 : optional argument # if option not provided defaults to input-name-overlay-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:v:p:o:h' opt
do
case ${opt} in
i) video="${OPTARG}";;
v) overlay="${OPTARG}";;
p) position="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# video name extension and overlay video extensions
video_nopath="${video##*/}"
video_name="${video_nopath%.*}"
# defaults for variables if not defined
outfile_default="${video_name}-overlay-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# overlay video function
overlay_video () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${video}" \
-i "${overlay}" \
-filter_complex \
"[0:0]setpts=PTS-STARTPTS[firstclip];
[1:0]setpts=PTS+${position}/TB[secondclip];
[firstclip][secondclip]overlay=enable='between(t\,${position},${dp})'[ov]" \
-map "[ov]" -map 0:1 \
-pix_fmt yuv420p \
-c:a copy -c:v libx264 -crf 18 \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# get overlay videos duration with ffprobe
duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${overlay}" | cut -d\. -f1)"
# position + duration
dp="$((position+duration))"
# run the overlay_video function
overlay_video "${video}" "${overlay}"
#!/bin/sh
#===============================================================================
# overlay-pip
# create a picture in picture video
#===============================================================================
# dependencies:
# ffmpeg ffprobe cut bc
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# create a picture in picture
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999]
-m [00] -x (tl|tr|bl|br) -w [000] -f (0.1-9|1) -b [00] -c colour -o output.mp4
-i input.(mp4|mkv|mov|m4v) : bottom video
-v input.(mp4|mkv|mov|m4v) : overlay video
-p [0-999] : time to overlay the video
-m [00] : margin defaults to 0
-x (tl|tr|bl|br) : pip position - defaults to tr
-w [000] : width - defaults to 1/4 of video size
-f (0.1-9|1) : fade from 0.1 to 1 - defaults to 0.2
-b [00] : border
-c colour : colour
-o output.mp4 : optional argument # if option not provided defaults to input-name-pip-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:v:p:m:x:w:f:c:b:o:h' opt
do
case ${opt} in
i) video="${OPTARG}";;
v) overlay="${OPTARG}";;
p) position="${OPTARG}";;
m) margin="${OPTARG}";;
x) pip="${OPTARG}"
case "${pip}" in
tl|tr|bl|br);;
*) usage "${pip} ${INVALID_OPT_ERR}";;
esac;;
w) width="${OPTARG}";;
f) fade="${OPTARG}";;
c) colour="${OPTARG}";;
b) border="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# video name extension and overlay video extensions
video_nopath="${video##*/}"
video_name="${video_nopath%.*}"
# defaults for variables if not defined
outfile_default="${video_name}-pip-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# overlay video function
overlay_video () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${video}" \
-i "${overlay}" \
-filter_complex \
"[0:0]setpts=PTS-STARTPTS[firstclip];
[1:0]setpts=PTS+${position}/TB[secondclip];
[firstclip][secondclip]overlay=enable='between(t\,${position},${dp})'[ov];
[0:0]scale=${width_scale:=${width_default}}${draw_border}[pip];
[pip]format=pix_fmts=yuva420p,fade=t=in:st=${position}:d=${fade:=${fade_default}}:alpha=1,fade=t=out:st=${fade_end}:d=${fade:=${fade_default}}:alpha=1[pipfade];
[ov][pipfade]overlay=${pip:=${pip_default}}:enable='between(t\,${position},${dp})'[pv]" \
-map "[pv]" -map 0:1 \
-pix_fmt yuv420p \
-c:a copy -c:v libx264 -crf 18 \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# get overlay videos duration with ffprobe
duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${overlay}" | cut -d\. -f1)"
# position + duration
dp="$((position+duration))"
# fade default
fade_default='0.2'
fade_end="$(echo "${dp}" - "${fade:=${fade_default}}" | bc)"
# pip padding
margin_default='20'
# pip default position
pip_default="${tr}"
# pip position
tl="${margin:=${margin_default}}:${margin:=${margin_default}}"
tr="main_w-overlay_w-${margin:=${margin_default}}:${margin:=${margin_default}}"
bl="${margin:=${margin_default}}:main_h-overlay_h-${margin:=${margin_default}}"
br="main_w-overlay_w-${margin:=${margin_default}}:main_h-overlay_h-${margin:=${margin_default}}"
# pip window size
width_default='iw/4:ih/4'
# check if width is null and unset
if [ -z "${width}" ]; then
: # width not set pass
else
width_scale="${width}:-1"
fi
# border
# divide border by 2 for the offset
border_default='4'
offset_default='2'
colour_default='#2f2f2f'
offset="$((${border:=${border_default}}/2))"
draw_border=",pad=w=${border:=${border_default}}+iw:h=${border:=${border_default}}+ih:x=${offset:=${offset_default}}:y=${offset:=${offset_default}}:color=${colour:=${colour_default}}"
# dont show border if border set to 0
if [ "${border}" = 0 ]; then
draw_border=''
fi
# pip position case
case "${pip}" in
tl)pip="${tl}";;
tr)pip="${tr}";;
bl)pip="${bl}";;
br)pip="${br}";;
*) pip="${tr}";;
esac
# run the overlay_video function
overlay_video "${video}" "${overlay}"
#!/bin/sh
#===============================================================================
# pan-scan
# pan scan over an image
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage ()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# pan scan image
$(basename "$0") -i input.(png|jpg|jpeg) -d (000) -p (l|r|u|d) -o output.mp4
-i = input.(png|jpg|jpeg)
-d = duration : from 1-999
-p = position : left, right, up, down
-o = output.mp4 : optional argument # default is input-name-pan-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:p:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
p) position="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# check if tile is null
if [ -z "${infile}" ]; then
: # tile variable not set : = pass
else
# ffprobe get image height
imgsize="$(ffprobe -v error -select_streams v:0 -show_entries stream=height,width -of csv=s=x:p=0 "${infile}")"
img_w=$(echo "${imgsize}" | awk -F'x' '{print $1}')
img_h=$(echo "${imgsize}" | awk -F'x' '{print $2}')
fi
# pan
left="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.05:h=3*${img_h}/1.05:x=t*(in_w-out_w)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \
right="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.05:h=3*${img_h}/1.05:x=(in_w-out_w)-t*(in_w-out_w)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \
up="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.2:h=3*${img_h}/1.2:y=t*(in_h-out_h)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \
down="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.2:h=3*${img_h}/1.2:y=(in_h-out_h)-t*(in_h-out_h)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \
# output file recording destination
outfile_default="${infile_name}-pan-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# zoom function
pan_func () {
ffmpeg \
-hide_banner \
-stats -v panic \
-r 30 \
-y -loop 1 \
-i "${infile}" -ss 0 -t "${dur}" \
-filter_complex \
"${1}" \
-y -shortest \
-c:v libx264 -crf 18 -profile:v high \
-r 30 -pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# check zoom and position
case "${position}" in
l) pan_func "${left}";;
r) pan_func "${right}" "${infile}";;
u) pan_func "${up}" "${infile}";;
d) pan_func "${down}" "${infile}";;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
#!/bin/sh
#===============================================================================
# scene-cut
# ffmpeg scene cut
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
$(basename "$0") -i input -c cutfile
-i input.(mp4|mov|mkv|m4v)
-c cutfile"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
c) cutfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
#===============================================================================
# ffmpeg create clips - nostdin needed to avoid clash with read command
#===============================================================================
trim_video () {
output="${input_name}-${count}.mp4"
ffmpeg \
-nostdin \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${input}" \
-t "${duration}" \
-c:a aac \
-c:v libx264 -profile:v high \
-pix_fmt yuv420p -movflags +faststart \
-f mp4 \
"${output}"
}
#===============================================================================
# read file and set IFS=, read = input before , duration = input after ,
#===============================================================================
count=1
while IFS=, read -r start duration; do
trim_video
count="$((count+1))"
done < "${cutfile}"
#!/bin/sh
#===============================================================================
# scene-detect
# ffmpeg scene detection
#===============================================================================
# dependencies:
# ffmpeg ffprobe awk
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
$(basename "$0") -s 00:00:00 -i input -e 00:00:00 -t (0.1 - 0.9) -f sec -o output
-s 00:00:00 : start time
-i input.(mp4|mov|mkv|m4v)
-e 00:00:00 : end time
-t (0.1 - 0.9) # threshold
-f sec # output in seconds
-o output.txt"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':s:i:e:t:o:f:h' opt
do
case ${opt} in
s) start="${OPTARG}";;
i) input="${OPTARG}";;
e) end="${OPTARG}";;
t) threshold="${OPTARG}";;
o) output="${OPTARG}";;
f) format="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# output file name
output_default="${input_name}-detection-$(date +"%Y-%m-%d-%H-%M-%S").txt"
# threshold default
threshold_default='0.3'
# start default
start_default='0.0'
#===============================================================================
# ffprobe video duration
#===============================================================================
ffduration () {
# video duration to append to cutfile
duration_default=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${input}")
# if video duration is empty exit
[ -n "${duration_default}" ] || usage "${input} ${NOT_MEDIA_FILE_ERR}"
}
#===============================================================================
# convert time input to seconds
#===============================================================================
checktime () {
start=$(echo "${start}" | awk -F: 'NF==3 { print ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { print 0 + $1 }')
end=$(echo "${end}" | awk -F: 'NF==3 { print ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { print 0 + $1 }')
}
#===============================================================================
# ffmpeg scene detection
#===============================================================================
# scene detection
ffdetection () {
detection="$(ffmpeg -hide_banner -i "${input}" -filter_complex "select='gt(scene,"${threshold:=${threshold_default}}")',metadata=print:file=-" -f null -)"
}
# scene detection range
ffdetection_range () {
detection="$(ffmpeg -hide_banner -i "${input}" \
-filter_complex "[0:0]select='between(t\,"${start}"\,"${end}")'[time];\
[time]select='gt(scene,"${threshold:=${threshold_default}}")',metadata=print:file=-[out]" -map "[out]" -f null -)"
}
#===============================================================================
# create cutfile - prepend start and append end or duration
#===============================================================================
cutfile_seconds () {
echo "${detection}" \
| awk -F':' 'BEGIN { printf("%s\n", '"${start:=${start_default}}"') }/pts_time/ { printf("%s\n", $4) } END { printf("%s\n", '"${end:=${duration_default}}"') }' > "${output:=${output_default}}"
}
cutfile_minutes () {
echo "${detection}" \
| awk -F':' 'BEGIN { printf("%s\n", '"${start:=${start_default}}"') }/pts_time/ { printf("%s\n", $4) } END { printf("%s\n", '"${end:=${duration_default}}"') }' | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\
NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \
> "${output:=${output_default}}"
}
#===============================================================================
# run function
#===============================================================================
if [ -n "${start}" ] && [ -n "${end}" ]; then
# scene detect range in video
checktime
ffdetection_range
if [ -n "${format}" ]; then
cutfile_seconds
else
cutfile_minutes
fi
elif [ -n "${start}" ]; then
usage "${start} ${WRONG_ARGS_ERR}"
elif [ -n "${end}" ]; then
usage "${end} ${WRONG_ARGS_ERR}"
else
# scene detect entire video
ffduration
ffdetection
if [ -n "${format}" ]; then
cutfile_seconds
else
cutfile_minutes
fi
fi
#!/bin/sh
#===============================================================================
# scene-images
# ffmpeg scene thumbnails
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
$(basename "$0") -i input -c cutfile
-i input.(mp4|mov|mkv|m4v)
-c cutfile"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
c) cutfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
#===============================================================================
# ffmpeg extract image
#===============================================================================
extract () {
output="${input_name}-${count}.png"
ffmpeg \
-nostdin \
-hide_banner \
-stats -v panic \
-ss "${seconds}" \
-i "${input}" \
-q:v 2 -f image2 \
-vframes 1 \
"${output}"
}
#===============================================================================
# read file and run ffmpeg
#===============================================================================
count=1
while IFS= read -r seconds; do
extract
count="$((count+1))"
done < "${cutfile}"
#!/bin/sh
#===============================================================================
# scene-time
# create ffmpeg cutlist
#===============================================================================
# dependencies:
# ffmpeg awk head grep
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
$(basename "$0") -i input -o output
-i input
-o output"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
h) usage;;
o) output="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# get the input file name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# output file name
output_default="${input_name}-cutlist-$(date +"%Y-%m-%d-%H-%M-%S").txt"
#===============================================================================
# awk subtract 2nd line from first line - print start and duration
#===============================================================================
seconds () {
awk -F. 'NR > 1 {printf("%s%s%s\n"), prev, ",", $0-prev}; {prev = $0}' < "${input}" > "${output:=${output_default}}"
}
#===============================================================================
# convert sexagesimal to seconds - and subtract 2nd line from first line
# convert from seconds back to sexagesimal
#===============================================================================
minutes () {
awk -F: 'NF==3 { printf("%s\n"), ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { printf("$s\n"), 0 + $1 }' < "${input}" \
| awk 'NR > 1 {printf("%s%s%s\n"), prev, "\n", $0-prev}; {prev = $0}' \
| awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\
NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \
| awk '{ORS = NR%2 ? "," : "\n"} 1' > "${output:=${output_default}}"
}
#===============================================================================
# timecode regex
#===============================================================================
# grab the first line of the file
check=$(head -n1 "${input}")
# minutes - match 00:00:00.000
minutes_regex='^[0-9]{1,2}:[0-9]{2}:[0-9]{2}([.]{1}[0-9]{1,3})?$'
# seconds - match 00:00:00.000
seconds_regex='^[0-9]{1,8}([.]{1}[0-9]{1,3})?$'
# grep for the minutes
minutes=$(echo "${check}" | grep -E "${minutes_regex}")
# grep for the seconds
seconds=$(echo "${check}" | grep -E "${seconds_regex}")
#===============================================================================
# check timecode and run function
#===============================================================================
if [ -n "${minutes}" ]; then
minutes
elif [ -n "${seconds}" ]; then
seconds
else
usage
fi
#!/bin/sh
#===============================================================================
# scopes
# ffplay video scopes
#===============================================================================
# dependencies:
# ffplay
#===============================================================================
# script usage
#===============================================================================
usage()
{
[ -z "${1}" ] || echo "! ${1}"
echo "\
# ffplay video scopes
$(basename "$0") -i input = histogram
$(basename "$0") -o input = rgb overlay
$(basename "$0") -p input = rgb parade
$(basename "$0") -s input = rgb overlay and parade
$(basename "$0") -w input = waveform
$(basename "$0") -v input = vector scope
$(basename "$0") -h = help
"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# functions
#===============================================================================
# histogram overlay
histogram_overlay () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"split=2[a][b],
[b]histogram,
format=yuva444p[hh],
[a][hh]overlay=x=W-w:y=H-h"
}
# rgb overlay
rgb_overlay () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"format=gbrp,
split=2[a][b],
[b]waveform=g=green:
s=ire:
fl=numbers:
filter=lowpass:
components=7:
display=overlay[bb],
[a][bb]vstack"
}
# rgb parade
rgb_parade () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"format=gbrp,
split=2[a][b],
[b]waveform=g=green:
s=ire:
fl=numbers:
filter=lowpass:
components=7[bb],
[a][bb]vstack"
}
# rgb overlay and parade stacked
rgb_stacked () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"format=gbrp,
split=3[a][b][c],
[b]waveform=g=green:
s=ire:
fl=numbers:
filter=lowpass:
components=7:
display=overlay[bb],
[c]waveform=g=green:
s=ire:
fl=numbers:
filter=lowpass:
components=7[cc],
[bb][cc]vstack[d],
[a][d]vstack"
}
# waveform lowpass
waveform_lowpass () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"split=2[a][b],
[b]waveform=f=lowpass:
s=ire:
g=green:
e=instant[bb],
[a][bb]vstack"
}
# vectorscope
vectorscope () {
ffplay "${infile}" \
-hide_banner \
-stats -v panic \
-vf \
"format=yuva444p9,
split=2[m][v],
[v]vectorscope=b=0.7:
m=color4:
e=peak+instant:
f=name:
g=color[v],
[m][v]overlay=x=W-w:y=H-h"
}
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:p:s:w:v:h' opt
do
case ${opt} in
i) infile="${OPTARG}"
histogram_overlay;;
o) infile="${OPTARG}"
rgb_overlay;;
p) infile="${OPTARG}"
rgb_parade;;
s) infile="${OPTARG}"
rgb_stacked;;
w) infile="${OPTARG}"
waveform_lowpass;;
v) infile="${OPTARG}"
vectorscope;;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#!/bin/sh
#===============================================================================
# sexagesimal-time
# calculate sexagesimal duration by subtacting end time from start time
#===============================================================================
# dependencies:
# awk
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
calculate sexagesimal duration by subtacting end time from start time
$(basename "$0") -s 00:00:00 -e 00:00:00"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':s:e:h' opt
do
case ${opt} in
s) start="${OPTARG}";;
h) usage;;
e) end="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# function
#===============================================================================
sexagesimal () {
printf "%s %s\n" "${start}" "${end}" \
| awk '
{
start = $1
end = $2
if (start ~ /:/) {
split(start, t, ":")
sseconds = (t[1] * 3600) + (t[2] * 60) + t[3]
}
if (end ~ /:/) {
split(end, t, ":")
eseconds = (t[1] * 3600) + (t[2] * 60) + t[3]
}
duration = eseconds - sseconds
printf("%s\n"), duration
}' \
| awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\
NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }'
}
#===============================================================================
# run function
#===============================================================================
sexagesimal
#!/bin/sh
#===============================================================================
# subtitle-add
# add subtitles to a video
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# add subtitles to a video
$(basename "$0") -i input.(mp4|mkv|mov|m4v) -s subtitle.srt -o output.mp4
-i input.(mp4|mkv|mov|m4v)
-s subtitle.srt
-o output.mp4 : optional argument # if option not provided defaults to input-name-subs-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:s:o:h' opt
do
case ${opt} in
i) video="${OPTARG}";;
s) subs="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
video_nopath="${video##*/}"
video_name="${video_nopath%.*}"
# defaults for variables if not defined
outfile_default="${video_name}-subs-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# functions
#===============================================================================
# audio is aac, copy audio stream
addsubs () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${video}" \
-f srt \
-i "${subs}" \
-c:a copy \
-c:v copy \
-c:s mov_text -metadata:s:s:0 \
language=eng \
-movflags +faststart \
-f mp4 \
"${outfile:=${outfile_default}}"
}
# run the addsubs function
addsubs "$video" "$subs"
#!/bin/sh
#===============================================================================
# tile-thumbnails
# create an image with thumbnails from a video
#===============================================================================
# dependencies:
# ffmpeg ffprobe awk bc
#===============================================================================
# script usage
#===============================================================================
usage()
{
echo "\
# create an image with thumbnails from a video
$(basename "$0") -i input -s 00:00:00.000 -w 000 -t 0x0 -p 00 -m 00 -c color -f fontcolor -b boxcolor -x on -o output.png
-i input.(mp4|mkv|mov|m4v|webm)
-s seek into the video file : default 00:00:05
-w thumbnail width : 160
-t tile layout format width x height : 4x3 : default 4x3
-p padding between images : default 7
-m margin : default 2
-c color = https://ffmpeg.org/ffmpeg-utils.html#color-syntax : default black
-f fontcolor : default white
-b boxcolor : default black
-x on : default off, display timestamps
-o output.png : optional argument
# if option not provided defaults to input-name-tile-date-time.png"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:s:w:t:p:m:c:b:f:x:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
s) seek="${OPTARG}";;
w) scale="${OPTARG}";;
t) tile="${OPTARG}";;
p) padding="${OPTARG}";;
m) margin="${OPTARG}";;
c) color="${OPTARG}";;
f) fontcolor="${OPTARG}";;
b) boxcolor="${OPTARG}";;
x) timestamp="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# ffprobe get fps and duration
videostats=$(ffprobe \
-v error \
-select_streams v:0 \
-show_entries stream=r_frame_rate:format=duration \
-of default=noprint_wrappers=1 \
"${infile}")
# fps
fps=$(echo "${videostats}" | awk -F'[=//]' '/r_frame_rate/{print $2}')
# duration
duration=$(echo "${videostats}" | awk -F'[=/.]' '/duration/{print $2}')
# check if tile is null
if [ -z "${tile}" ]; then
: # tile variable not set : = pass
else
# tile variable set
# tile layout
tile_w=$(echo "${tile}" | awk -F'x' '{print $1}')
tile_h=$(echo "${tile}" | awk -F'x' '{print $2}')
# title sum
tile_sum=$(echo "${tile_w} * ${tile_h}" | bc)
fi
# defaults
seek_default='00:00:05'
scale_default='160'
tile_layout_default='4x3'
tile_default='12'
padding_default='7'
margin_default='2'
color_default='black'
fontcolor_default='white'
boxcolor_default='black'
timestamp_default='off'
pts_default='5'
pts=$(printf "%s %s\n" "${seek}" | awk '{
start = $1
if (start ~ /:/) {
split(start, t, ":")
seconds = (t[1] * 3600) + (t[2] * 60) + t[3]
}
printf("%s\n"), seconds
}')
outfile_default="${infile_name}-tile-$(date +"%Y-%m-%d-%H-%M-%S").png"
# duration * fps / number of tiles
frames=$(echo "${duration} * ${fps} / ${tile_sum:=${tile_default}}" | bc)
#===============================================================================
# functions
#===============================================================================
# contact sheet - no timestamps
tilevideo () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${seek:=${seek_default}}" \
-i "${infile}" \
-frames 1 -vf "select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \
"${outfile:=${outfile_default}}"
}
# contact sheet - timestamps
timestamp () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${seek:=${seek_default}}" \
-i "${infile}" \
-frames 1 -vf "drawtext=text='%{pts\:hms\:${pts:=${pts_default}}}':x='(main_w-text_w)/2':y='(main_h-text_h)':fontcolor=${fontcolor:=${fontcolor_default}}:fontsize='(main_h/8)':boxcolor=${boxcolor:=${boxcolor_default}}:box=1,select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \
"${outfile:=${outfile_default}}"
}
#===============================================================================
# check option passed to script
#===============================================================================
if [ "${timestamp}" == on ]; then
timestamp "${infile}" # -x on
elif [ ! -z "${fontcolor}" ]; then
timestamp "${infile}" # -f
elif [ ! -z "${boxcolor}" ]; then
timestamp "${infile}" # -b
elif [ -z "${timestamp}" ]; then
tilevideo "${infile}" # no timestamp
else
tilevideo "${infile}" # no timestamp
fi
#!/bin/sh
#===============================================================================
# trim-clip
# trim video or audio clips with millisecond accuracy
#===============================================================================
# dependencies:
# ffmpeg file
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# trim video or audio clips with millisecond accuracy
https://trac.ffmpeg.org/wiki/Seeking
$(basename "$0") -s 00:00:00.000 -i input -t 00:00:00.000 -o output)
-s 00:00:00.000 : start time
-i input.(mp4|mov|mkv|m4v|webm|aac|m4a|wav|mp3|ogg)
-t 00:00:00.000 : number of seconds after start time
-o output.(mp4|webm|aac|mp3|wav|ogg) : optional argument
# if option not provided defaults input-name-trimmed-date-time.(mp4|wav)"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':s:i:t:o:h' opt
do
case ${opt} in
s) start="${OPTARG}";;
i) infile="${OPTARG}";;
t) end="${OPTARG}";;
h) usage;;
o) outfile="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# input file extension
infile_ext="${infile##*.}"
# file command check input file mime type
filetype="$(file --mime-type -b "${infile}")"
# video mimetypes
mov_mime='video/quicktime'
mkv_mime='video/x-matroska'
mp4_mime='video/mp4'
webm_mime='video/webm'
m4v_mime='video/x-m4v'
wav_mime='audio/x-wav'
audio_mime='audio/mpeg'
aac_mime='audio/x-hx-aac-adts'
m4a_mime='audio/mp4'
ogg_mime='audio/ogg'
# the file command wrongly identifies .m4a audio as a video file
# so we check if the file extension is .m4a and set the mime type to audio/mp4
if [ "${infile_ext}" = 'm4a' ]; then
filetype="${m4a_mime}"
fi
# defaults for variables if not defined
videofile_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
webm_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").webm"
aac_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").aac"
mp3_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").mp3"
wav_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").wav"
m4a_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").m4a"
ogg_default="${infile_name}-trimmed-$(date +"%Y-%m-%d-%H-%M-%S").ogg"
#===============================================================================
# check if the libfdk_aac codec is installed, if not fall back to the aac codec
#===============================================================================
aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)"
aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg."
aac_check="$(echo "${aac_codec}" | grep "${aac_error}")"
# check ffmpeg aac codecs
if [ -z "${aac_check}" ]; then
aac='libfdk_aac' # libfdk_aac codec is installed
else
aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec
fi
#===============================================================================
# audio and video functions
#===============================================================================
# trim video clip
trim_video () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a "${aac}" \
-c:v libx264 -profile:v high \
-pix_fmt yuv420p -movflags +faststart \
-f mp4 \
"${outfile:=${videofile_default}}"
}
# trim webm video clip
trim_webm () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a libopus \
-c:v vp9 \
-f webm \
"${outfile:=${webm_default}}"
}
# trim aac audio clip
trim_aac () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a "${aac}" \
-f adts \
"${outfile:=${aac_default}}"
}
# trim m4a audio clip
trim_m4a () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a "${aac}" \
-f mp4 \
"${outfile:=${m4a_default}}"
}
# trim mp3 audio clip
trim_mp3 () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a libmp3lame \
-f mp3 \
"${outfile:=${mp3_default}}"
}
# trim wav audio clip
trim_wav () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a pcm_s16le \
-f wav \
"${outfile:=${wav_default}}"
}
# trim ogg audio clip
trim_ogg () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start}" \
-i "${infile}" \
-t "${end}" \
-c:a libopus \
-f ogg \
"${outfile:=${ogg_default}}"
}
#===============================================================================
# check the files mime type
#===============================================================================
case "${filetype}" in
"${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") trim_video "${infile}";;
"${webm_mime}") trim_webm "${infile}";;
"${aac_mime}") trim_aac "${infile}";;
"${m4a_mime}") trim_m4a "${infile}";;
"${audio_mime}") trim_mp3 "${infile}";;
"${wav_mime}") trim_wav "${infile}";;
"${ogg_mime}") trim_ogg "${infile}";;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
#!/usr/bin/env bash
# ffmpeg -i input.mp4 -ss 00:05:20 -t 00:10:00 -c:v copy -c:a copy output1.mp4
# ffmpeg -i input.mp4 -ss 00:05:10 -to 00:15:30 -c:v copy -c:a copy output2.mp4
#!/bin/sh
#===============================================================================
# vid2gif
# convert a video into a gif animation
#===============================================================================
# dependencies:
# ffmpeg ffprobe cut
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# convert a video into a gif animation
$(basename "$0") -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v) -t 00:00:00.000 -f [00] -w [0000] -o output.gif
-s 00:00:00.000 : start time
-i input.(mp4|mov|mkv|m4v)
-t 00:00:00.000 : number of seconds after start time
-f [00] : framerate
-w [0000] : width
-o output.gif : optional argument
# if option not provided defaults input-name-gif-date-time.gif"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':s:i:t:f:w:o:h' opt
do
case ${opt} in
s) start="${OPTARG}";;
i) infile="${OPTARG}";;
t) end="${OPTARG}";;
f) framerate="${OPTARG}";;
w) width="${OPTARG}";;
h) usage;;
o) outfile="${OPTARG}";;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables if not defined
start_default='00:00:00'
end_default=$(ffprobe -v error -sexagesimal -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$infile" | cut -d\. -f1)
framerate_default='15'
width_default='320'
outfile_default="${infile_name}-gif-$(date +"%Y-%m-%d-%H-%M-%S").gif"
#===============================================================================
# functions
#===============================================================================
# create gif function
create_gif () {
ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${start:=${start_default}}" \
-i "${infile}" \
-t "${end:=${end_default}}" \
-filter_complex "[0:v] fps=${framerate:=${framerate_default}},scale=${width:=${width_default}}:-1:flags=lanczos,split [a][b];[a] palettegen [p];[b][p] paletteuse" \
"${outfile:=${outfile_default}}"
}
# run the create_gif function
create_gif "${infile}"
#!/bin/sh
#===============================================================================
# waveform
# create a waveform from an audio or video file and save as a png
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# create a waveform from an audio or video file and save as a png
$(basename "$0") -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -o output.png
-i output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3)
-o output.png : optional argument # if option not provided defaults to input-name-waveform-date-time"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# variables
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# defaults for variables if not defined
outfile_default="${infile_name}-waveform-$(date +"%Y-%m-%d-%H-%M-%S").png"
#===============================================================================
# functions
#===============================================================================
# waveform function
wform () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${infile}" \
-filter_complex "aformat=channel_layouts=mono,showwavespic=s=1000x200" \
-frames:v 1 \
-f apng \
"${outfile:=${outfile_default}}"
}
# run the wform function
wform "${infile}"
#!/bin/sh
#===============================================================================
# webp
# ffmpeg libwebp_anim
#===============================================================================
# dependencies:
# ffmpeg
#===============================================================================
# script usage
#===============================================================================
usage () {
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# webp animated image
$(basename "$0") -i input -c 0-6 -q 0-100 -f 15 -w 600 -p none -o output.webp
-i input
-c compression level: 0 - 6 : default 4
-q quality: 0 - 100 : default 80
-f framerate: default 15
-w width: default 600px
-p preset: none|default|picture|photo|drawing|icon|text : default none
-o output.webp : optional agument
# if option not provided defaults input-name.webp"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:c:q:f:w:p:o:h' opt
do
case ${opt} in
i) input="${OPTARG}";;
c) compression="${OPTARG}";;
q) quality="${OPTARG}";;
f) framerate="${OPTARG}";;
w) width="${OPTARG}";;
p) preset="${OPTARG}";;
o) output="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input name
input_nopath="${input##*/}"
input_name="${input_nopath%.*}"
# defaults for variables if not defined
compression_default='4'
quality_default='80'
preset_default='none'
framerate_default='15'
width_default='600'
output_default="${input_name}.webp"
#===============================================================================
# ffmpeg webp animation function
#===============================================================================
animation () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${input}" \
-c:v libwebp_anim \
-lossless 0 \
-compression_level "${compression:=${compression_default}}" \
-quality "${quality:=${quality_default}}" \
-cr_threshold 0 \
-cr_size 16 \
-preset "${preset:=${preset_default}}" \
-loop 1 \
-an -vsync 0 \
-vf "fps=fps=${framerate:=${framerate_default}},scale=${width:=${width_default}}:-1:flags=lanczos" \
"${output:=${output_default}}"
}
#===============================================================================
# run ffmpeg webp animation function
#===============================================================================
animation
#!/bin/sh
#===============================================================================
# xfade
# ffmpeg xfade transitions
#===============================================================================
# dependencies:
# ffmpeg ffprobe bc
#===============================================================================
# script usage
#===============================================================================
usage()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# ffmpeg xfade transitions
$(basename "$0") -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d duration -t transition -f offset -o output.mp4
-a clip1.(mp4|mkv|mov|m4v) : first clip
-b clip2.(mp4|mkv|mov|m4v) : second clip
-d duration : transition duration
-t transition : transition
-f offset : offset
-o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time
+ transitions
circleclose, circlecrop, circleopen, diagbl, diagbr, diagtl, diagtr, dissolve, distance
fade, fadeblack, fadegrays, fadewhite, hblur, hlslice, horzclose, horzopen, hrslice
pixelize, radial, rectcrop, slidedown, slideleft, slideright, slideup, smoothdown
smoothleft, smoothright, smoothup, squeezeh, squeezev, vdslice, vertclose, vertopen, vuslice
wipebl, wipebr, wipedown, wipeleft, wiperight, wipetl, wipetr, wipeup"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':a:b:d:t:f:o:h' opt
do
case ${opt} in
a) clip1="${OPTARG}";;
b) clip2="${OPTARG}";;
d) dur="${OPTARG}";;
t) transition="${OPTARG}";;
f) offset="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
clip1_nopath="${clip1##*/}"
clip1_name="${clip1_nopath%.*}"
# defaults
duration_default='0.5'
transition_default='fade'
outfile_default="${clip1_name}-xfade-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
# offset and duration options not set
if [ -z "${offset}" ];then
# clip 1 duration to calculate default transition offset
clip1_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${clip1}")
# round down decimal point
clip1_time=$(printf "%.1f\n" "${clip1_dur}")
# clip1 duration minus duration default and 0.1 seconds which is needed for the transition
offset_default=$(echo "${clip1_time}" - "${dur:=${duration_default}}" - 0.1 | bc -l)
# audio trim default match offset default
audio_trim_default=$(echo "${clip1_time}" - 0.1 | bc -l)
else
# offset and duration options set
# trim audio to match length of the duration and offset
audio_trim=$(echo "${dur} + ${offset}" | bc -l)
fi
#===============================================================================
# function
#===============================================================================
# cross fade video and audio
xfade_va () {
ffmpeg \
-hide_banner \
-stats -v panic \
-i "${clip1}" -i "${clip2}" \
-filter_complex \
"[0:v][1:v]xfade=transition='${transition:=${transition_default}}':duration='${dur:=${duration_default}}':offset='${offset:=${offset_default}}'[xfade];
[0:a]atrim=0:'${audio_trim:=${audio_trim_default}}'[atrim];
[atrim][1:a]acrossfade=d='${dur:=${duration_default}}'[afade]
" \
-map "[xfade]" -map "[afade]" \
-pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# run the xfade_va function
xfade_va "${clip1}" "${clip2}"
#!/bin/sh
#===============================================================================
# zoompan
# ken burns effect
#===============================================================================
# dependencies:
# ffmpeg ffprobe
#===============================================================================
# script usage
#===============================================================================
# script usage
usage ()
{
# if argument passed to function echo it
[ -z "${1}" ] || echo "! ${1}"
# display help
echo "\
# zoompan, ken burns effect
$(basename "$0") -i input.(png|jpg|jpeg) -d (000) -z (in|out) -p (tl|c|tc|tr|bl|br) -o output.mp4
-i = input.(png|jpg|jpeg)
-d = duration : from 1-999
-z = zoom : in or out
-p = position : zoom to location listed below
-o = outfile.mp4 : optional argument # default is input-name-zoompan-date-time
+------------------------------+
+tl tc tr+
+ +
+ c +
+ +
+bl br+
+------------------------------+"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
NOT_MEDIA_FILE_ERR='is not a media file'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:d:z:p:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
d) dur="${OPTARG}";;
z) zoom="${OPTARG}"
[ "${zoom}" = 'in' ] || [ "${zoom}" = 'out' ];;
p) position="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name and extension
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# ffprobe get image height
imgheight="$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "${infile}")"
# frames per seconds
infps='30'
# zoom in positions
# zoom in top left
zi_top_left="scale=-2:10*ih,\
zoompan=z='min(zoom+0.0015,1.5)':\
fps=${infps}:d=${infps}*${dur},\
scale=-2:${imgheight}" \
# zoom in center
zi_center="scale=-2:10*ih,\
zoompan=z='min(zoom+0.0015,1.5)':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/2-(iw/zoom/2)':\
y='ih/2-(ih/zoom/2)',\
scale=-2:${imgheight}" \
# zoom in top center
zi_top_center="scale=-2:10*ih,\
zoompan=z='min(zoom+0.0015,1.5)':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/2-(iw/zoom/2)',\
scale=-2:${imgheight}" \
# zoom in top right
zi_top_right="scale=-2:10*ih,\
zoompan=z='min(zoom+0.0015,1.5)':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/zoom-(iw/zoom/2)',\
scale=-2:${imgheight}" \
# zoom out positions
# zoom out top left
zo_top_left="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur},\
scale=-2:${imgheight}" \
zo_center="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)',\
scale=-2:${imgheight}" \
# zoom out top center
zo_top_center="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/2-(iw/zoom/2)',\
scale=-2:${imgheight}" \
# zoom out top right
zo_top_right="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur}:\
x='iw/zoom-(iw/zoom/2)',\
scale=-2:${imgheight}" \
# zoom out bottom left
zo_bottom_left="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur}:\
y='ih-ih/zoom',\
scale=-2:${imgheight}" \
# zoom out bottom right
zo_bottom_right="scale=-2:2*ih,\
zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\
fps=${infps}:d=${infps}*${dur}:\
x='iw-iw/zoom':\
y='ih-ih/zoom',\
scale=-2:${imgheight}" \
# outfile file recording destination
outfile_default="${infile_name}-zoompan-$(date +"%Y-%m-%d-%H-%M-%S").mp4"
#===============================================================================
# function
#===============================================================================
# zoom function
zoom_func () {
ffmpeg \
-hide_banner \
-stats -v panic \
-r 30 \
-i "${infile}" \
-filter_complex \
"${1}" \
-y -shortest \
-c:v libx264 -crf 18 -profile:v high \
-r 30 -pix_fmt yuv420p \
-movflags +faststart -f mp4 \
"${outfile:=${outfile_default}}"
}
# check zoom and position
case "${zoom}" in
in)
case "${position}" in
tl) zoom_func "${zi_top_left}" "${infile}";;
tc) zoom_func "${zi_top_center}" "${infile}";;
c) zoom_func "${zi_center}" "${infile}";;
tr) zoom_func "${zi_top_right}" "${infile}";;
*) help;;
esac
;;
out)
case "${position}" in
tl) zoom_func "${zo_top_left}" "${infile}";;
tc) zoom_func "${zo_top_center}" "${infile}";;
c) zoom_func "${zo_center}" "${infile}";;
tr) zoom_func "${zo_top_right}" "${infile}";;
bl) zoom_func "${zo_bottom_left}" "${infile}";;
br) zoom_func "${zo_bottom_right}" "${infile}";;
*) help;;
esac
;;
*) usage "${infile} ${NOT_MEDIA_FILE_ERR}";;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment