Skip to content

Instantly share code, notes, and snippets.

@pleasemarkdarkly
Created January 7, 2020 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pleasemarkdarkly/403d96a6f1109d8e6c1df6dd5434e0e9 to your computer and use it in GitHub Desktop.
Save pleasemarkdarkly/403d96a6f1109d8e6c1df6dd5434e0e9 to your computer and use it in GitHub Desktop.
Create variable length random or sequential video frames with specific or randomly selected royality free music. Created for an illustrator friend who needed automatic generation of advertisements amd social media.
#!/usr/bin/env bash
#--------------------------------------------------------------------------------------------------
# Script to create videos from PNGs with watermark and music, royality free except the marketing
# images, predominately for www.natasha-pannkina.com's illustrations.
#
# 2020 license is simply to reference www.natasha-pankina.com's illustrations on your blog AND
# with any file you create with this tool, notify her and she'll reference you too
#
# logging support: http://pretty.pleasemarkdarkly.com:8080/bsR9l/log4bash.sh
#
# version history
# transfersh version: http://pretty.pleasemarkdarkly.com:8080/9LeVc/create_video.sh
# transfersh backup: http://pretty.pleasemarkdarkly.com:8080/okd0V/create_video.sh
# this file
#
#
# script requires ffmpeg probably ffprobe, exiftool, and some other system level stuff
# you should probably know what you are doing with BASH, Linux, or scripting to mess around
# with this script - its probably worth getting help, this script saved a ton of time
#
# This script is not supported by author or natasha-pankina.com its just provided to maybe help
# someone with marketing their illustrations.
#
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Global Variables Start
#--------------------------------------------------------------------------------------------------
# Key Auto Generation Content Variables
# video, number to create
# range, of video frames to draw from. Used in the creation of the random PNGs files
# frames, to work with, overshooting is fine. Limiting reduces the pool from random selection.
# maxframes, sed to limit the duration of the video
videos="20"
range="15-400"
frames='400'
maxframes='30'
# Key Auto Generation Content Variables
#--------------------------------------------------------------------------------------------------
num_processed=0
# default value for random music file
# duration is used to determine seconds over to change audio
# long default used for sequential and full catalog audio
# intro video for merchandising
musicdir='../music'
rndmusic='/Users/mark.phillips/Developer/natasha_stocks/video_music/0322.mp3'
rndmusic_duration='200'
longmusic='/Users/mark.phillips/Developer/natasha_stocks/video_music/0322.mp3'
intro_video='./intro/20200101170115-5s-intro.mp4' # "./intro/20191228215149-15s-intro.mp4" 15 secs
workingdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if [ -d ./final_videos ]; then
starting_video_count="$(ls ./final_videos | wc -l)"
output_dir='./final_videos'
else
mkdir -p ./final_videos
fi
# argument of script is the output directory
if [ -d "$1" ]; then
output_dir=$1
echo "output to $output_dir"
else
echo "Usage: filename.sh and path to output final videos, no arguments or bad path"
fi
INTROIMG='/Users/mark.phillips/Developer/natasha_stocks/watermark/Video_page_Mrs.Whiskers_color_red.jpg'
WATERMARK='/Users/mark.phillips/Developer/natasha_stocks/watermark/Watermark_square_29.12.2019 6500px.png'
# Bail out if the temp directory wasn't created successfully.
if [ ! -e $tmpdir ]; then
>&2 echo "Failed to create temp directory"
exit 1
fi
# Make sure it gets removed even if the script exits abnormally.
trap "exit 1" HUP INT PIPE QUIT TERM
trap 'rm -rf "$tmpdir" "./staging_videos/*.mp4"' EXIT
# Global Variables Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Bash Utils Section Section
function verify_log4bash () {
if [ ! -e ./log4bash.sh ]; then
echo "./log4bash.sh not found"
wget http://pretty.pleasemarkdarkly.com:8080/jP8Nd/log4bash.sh
else
source "./log4bash.sh"
log_info "function: verify_log4bash"
log_info "log4bash found, outputing example logs notifiers"
fi
# verify_log_output
}
function verify_log_output () {
log "example log outputs"
log "log output";
log_info "log_info output";
log_success "log_success output";
log_warning "log_warning output";
log "end example log outputs"
}
# Bash Utils Section Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Video Creation Helper Functions Section
function random_audio_track () {
log_info "function: random_audio_track"
rndmusic=`find ../video_music -name '*.mp3' | sort -R | head -n 1`
rndmusic_duration="$((mp3_duration=$(mediainfo --Output='General;%Duration%' ${rndmusic})/1000))"
log_warning "music include attribution for bensound.com for music"
log_info "random_audio_track: $rndmusic, audio_track duration: $rndmusic_duration"
}
#--------------------------------------------------------------------------------------------------
# Video Creation Functions Section
function generate_frameslist () {
log_info "function: generate_frameslist"
find . -name '*.png' -type f -not -path './intro/*' | sort -R > ${tmpdir}/"${session}.txt"
log_info "generate_framelist: ${tmpdir}, enumerate_content: ${session}.txt"
# pushover "generate_framelist: ${tmpdir}, enumerate_content: ${session}.txt"
}
function prepare_png_dir_from_list () {
log_info "function: prepare_png_dir"
png_index=0
png_newname=''
while IFS= read -r png
do
png_newname="$(printf "%04d" ${png_index}).png"
log_info "transpose filename: $png, newpng name: $png_newname"
cp -f $png "$tmpdir/$png_newname"
if [ "$maxframes" -eq 0 ]; then
[ "$png_index" -gt "$frames" ] && break
elif [ "$png_index" -gt "$maxframes" ]; then
break
fi
((png_index=png_index+1))
done <"$tmpdir/${session}.txt"
shopt -s dotglob nullglob
num_pngs=($tmpdir/*.png)
# pushover "prepare_png_dir_from_list: (${#num_pngs[@]}) pngs ready, generated from $session.txt created"
}
# Video Creation Functions Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Video Cleanup Section
function reset () {
log_info "function: reset"
session=$(date +"%Y%m%d%H%M%S")
rm -rf "$tmpdir"
tmpdir=$(mktemp -d -t $session)
log_warning "reset: $session, tmpdir: $tmpdir"
# pushover "reset: $session, tmpdir: $tmpdir"
}
function clean_up () {
echo
log_warning "--------------------------------------------------------------------------------------------------"
log_warning "function: clean_up"
log_warning "called upon exit, exit trap still in effect"
rm -vrf "$tmpdir"
rm -vrf "./staging_videos/*.mp4"
log_warning "--------------------------------------------------------------------------------------------------"
echo
}
# Video Cleanup Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Video Generation Section
#
# Main loop
#
# Could probably be executed in one ffmpeg command but broken out in stages to check video quality
function generate_video () {
log_info "function: generate_video"
session=$(date +"%Y%m%d%H%M%S")
tmpdir=$(mktemp -d -t $session)
generate_frameslist
prepare_png_dir_from_list
log_info "generate_vieo: using session: $tmpdir/$session, frames: $frames"
log_info "generate_video: previously randomized, enumerate, prepares session dir with images to build from";
# ffmpeg -r 1 -f image2 -s 1920x1080 -i ${tmpdir}/%04d.png -vcodec libx264 -crf 15 "${tmpdir}/${session}-images.mp4"
ffmpeg -r 1 -f image2 -i ${tmpdir}/%04d.png -vcodec libx264 -crf 15 "${tmpdir}/${session}-images.mp4"
[[ -e "$WATERMARK" ]] && log_info "watermark: $WATERMARK" | log_warning "watermark: missing"
ffmpeg -i "${tmpdir}/${session}-images.mp4" -i "${WATERMARK}" -filter_complex "overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" "${tmpdir}/${session}-watermark.mp4"
# log_info "saving intermediate1.ts in working folder"
log_warning "skipping regenerating intro intermediate"
[[ -e "$INTROVIDEO" ]] && log_info "$INTROVIDEO located" | log_warning "$INTROVIDEO missing"
# various length of intro lenghts
# ffmpeg -i "$INTROVIDEO" -c copy -bsf:v h264_mp4toannexb -f mpegts "./intermediate1.ts.5s"
# ffmpeg -i "$INTROVIDEO" -c copy -bsf:v h264_mp4toannexb -f mpegts "./intermediate1.ts.15s"
log_info "generate_video: generating intermediate2.ts. actual random images"
ffmpeg -i "${tmpdir}/${session}-watermark.mp4" -c copy -bsf:v h264_mp4toannexb -f mpegts "${tmpdir}/intermediate2.ts"
## log_info "generate_video: using pregenerated intermediate1.ts.5s or intermediate1.ts.15s"
ffmpeg -i "concat:intermediate1.ts.5s|${tmpdir}/intermediate2.ts" -c copy -bsf:a aac_adtstoasc "${tmpdir}/${session}-combined.mp4"
## log_warning "generate_video: merge some metadata to mp4 file"
## log_warning "generate_video: if a .meta file exists merge into final file"
## [[ -f *.meta ]] && log_warning "metafile exists prepare to merge to mp4" | log_warning "no metadata file exists"
## log_info "generate_video ${session}.mp4: using $longmusic and ${tmpdir}/${session}-combined.mp4"
ffmpeg -i "${tmpdir}/${session}-combined.mp4" -i "${longmusic}" -c copy -map 0:v:0 -map 1:a:0 -shortest "./staging_videos/${session}.mp4"
(( num_processed = num_processed + 1 ))
duration=$(ffprobe -i "./staging_videos/${session}.mp4" -show_format -v quiet | sed -n 's/duration=//p'| xargs printf %.0f)
log_success "generate_video:./staging_videos/${session}.mp4 ($num_processed/$videos)"
reset
}
# Video Creation Helper Functions Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Video Test Section
function test_mode () {
log_info "function: test_mode"
}
# Video Test Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Multiple Video Creation Process
#
# Plural of the singluar function, bad function naming
function generate_videos () {
log_info "function: generate_videos ($videos}"
log_info "create n videos between $range frames each"
num_processed=0
for (( i=0; i<"$videos"; i++ ))
do
frames=$(shuf -i ${range} -n 1)
log_info "generating video ($i/$videos) with frames: ($frames/$range)"
generate_video
log_info "generated ./staging_videos/${session}.mp4, containing ($frames) frames and length ($duration)"
# pushover "generated ./staging_videos/${session}.mp4, containing ($frames) frames and length ($duration)"
(( num_processed = num_processed + 1 ))
done
shopt -s dotglob nullglob
num_staging_videos=("./staging_videos/*.mp4")
log_warning "num staging videos: ${#num_staging_videos[@]}"
log_info "generated ($num_processed/$videos), staging_videos: ${#num_staging_videos[@]}"
pushover "generated ($num_processed/$videos), staging_videos: ${#num_staging_videos[@]}"
replace_shortvideo_audio
}
# End
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Replace video audio with other content
#
# Main loop which replaces audio with random royality free music
# default long audio for generated video and change afterwards
function replace_shortvideo_audio () {
log_warning "function: replace_shortvideo_audio"
num_processed=0
random_audio_track
shopt -s dotglob nullglob
num_staging_videos=(./staging_vidoes/*.mp4)
log_warning "num staging videos to process: ${#num_staging_videos[@]}"
for currentvideo in ./staging_videos/*.mp4
do
newfile=$(date +"%Y%m%d%H%M%S")
currentduration=$(ffprobe -i $currentvideo -show_format -v quiet | sed -n 's/duration=//p'| xargs printf %.0f)
log_warning "(replace_shorten_audio) current video: $currentvideo, duration: $currentduration, random song duration: $rndmusic_duration, ($num_processed/$videos)"
# pushover "(replace_shorten_audio) current video: $currentvideo, duration: $currentduration, random song duration: $rndmusic_duration, ($num_processed/$videos)"
if [ "$currentduration" -gt "175" ]; then
log_warning "$currentduration > 300, moving video from staging to final $currentvideo -> ./final_video/$newfile.tmp$"
pushover "$currentduration > 300, moving video from staging to final $currentvideo -> ./final_video/$newfile.tmp$"
mv -v $currentvideo ./final_videos/"${newfile}.mp4"
(( num_processed = num_processed + 1 ))
else
while [ "${rndmusic_duration}" -lt "${currentduration}" ]
do
log_warning "video duration $rndmusic_duration is less than $currentduration, fetch new audio track"
pushover "video duration $rndmusic_duration is less than $currentduration, fetch new audio track"
random_audio_track
done
newfile=$(date +"%Y%m%d%H%M%S")
log_info "replacing ${currentvideo} audio with ${rndmusic}, newfile ${newfile}.mp4, ($num_processed/$videos)"
# pushover "replacing ${currentvideo} audio with ${rndmusic}, newfile ${newfile}.mp4, ($num_processed/$videos)"
ffmpeg -i "${currentvideo}" -i "${rndmusic}" -c copy -map 0:v:0 -map 1:a:0 -shortest "${output_dir}"/"${newfile}.mp4"
(( num_processed = num_processed + 1 ))
log_success "${newfile}.mp4 saved and ${currentvideo} removed, ($num_processed/$videos)"
# pushover "${newfile}.mp4 saved and ${currentvideo} removed, ($num_processed/$videos)"
rm -rf "${currentvideo}"
fi
log_success "num processed/requested ($num_processed/$videos) - total staging videos:${#num_staging_videos[@]}"
pushover "num processed/requested ($num_processed/$videos) - total staging videos:${#num_staging_videos[@]}"
done
# log_warning "removing staging videos"
}
# Video Audio Replacement Section
#--------------------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------------------
# Main Functions Start
function main () {
random_audio_track
generate_videos
clean_up
}
verify_log4bash
main "[@]"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment