Long scale Nest time lapse videos (part 2)

In Part One I showed you how to get the snapshot URL for your Nest camera, so you could get a full resolution still image from the camera. In this part, I’ll describe what I’m doing with those to make time lapse videos.

Step 1: Prerequisites

I’m doing this on an iMac running OS X, on which I’ve installed the Homebrew package manager. Using brew, I installed ffmpeg, a very handy video processing utility. I also installed the “coreutils” package, which gets me GNU versions of a number of command line utilities which may work differently under OS X than they do in linux. Notably, I’m using the “gdate” command to get GNU date’s behavior rather than the BSD version’s. Lastly, I installed ImageMagick (also using brew) to create daily date labels for the videos.

Under Windows, you may be able to get these (and curl, from part one) through Cygwin. ffmpeg might need to be downloaded from https://ffmpeg.zeranoe.com/builds/. I haven’t tried any of this under Windows myself. I’m using the “cron” utility to schedule grabbing images once per minute and assembling daily videos, and I have gotten cron working under Cygwin before, but I remember it being a bit of a pain. You’ll want to search for instructions for getting cron working with cygwin if you plan on doing this under Windows.

I’m also using the Astro::Sunrise perl module. Installation of that is left as an exercise for the reader. I probably used CPAN.

Step 2: Get a still image once per minute

I’m grabbing images once per minute between 6am and 8pm, and every half hour outside those hours. Here’s what my crontab (the control file for cron) looks like:

* 6-20 * * * /Users/jdlarios/BurkeTimeLapse/API/grab.sh >>/Users/jdlarios/BurkeTimeLapse/API/grab.log 2>&1
0,30 0-5,21-23 * * * /Users/jdlarios/BurkeTimeLapse/API/grab.sh >>/Users/jdlarios/BurkeTimeLapse/API/grab.log 2>&1

That runs the “grab.sh” script once per minute between 06:00 and 20:59, and on the hour and half-hour during other hours.

#!/bin/sh
cd /Users/jdlarios/BurkeTimeLapse

Gates='https://www.dropcam.com/api/wwn.get_snapshot/A_SNAPSHOT_URL'
UWIT='https://www.dropcam.com/api/wwn.get_snapshot/ANOTHER_SNAPSHOT_URL'
Tower='https://www.dropcam.com/api/wwn.get_snapshot/A_THIRD_SNAPSHOT_URL'
Burke='https://www.dropcam.com/api/wwn.get_snapshot/THE_LAST_SNAPSHOT_URL'

ts=`date +'%F-%H-%M-%S'`
dir=`date +'%F'`

for i in Gates UWIT Tower Burke; do
  mkdir -p ${i}/${dir}
  eval url=\$$i
  curl -s -o ${i}/${dir}/${ts}.jpg "${url}"
done

That gets me timestamped images in directories named after the camera they’re from. Each directory has a “mkmovie.sh” script which makes a banner containing the date to put over the Nest logo, and which makes a movie out of all the jpg images in its directory. (There’s a script in each directory because one of our cameras is old and works best at a lower resolution, and the numbers used for resolution and banner placement are different for it.)

#!/bin/sh
if [ "X" != "X$1" ] && [ -d $1 ]; then
    dir=`basename $1`
    label=`gdate -d "${dir}" +"%B %e, %G" | sed -e 's/  / /'`
    echo "Creating \"${label}\" banner."
    convert -size 520x152 xc:transparent -fill '#4b2e83' -stroke '#4b2e83' -draw 'polygon 50,0 520,0 520,152 0,152' -font 'Linotype - AvenirLTPro-Light' -pointsize 48 -draw "gravity west stroke white fill white text 55,5 '${label}'" ${dir}-label.png
    echo "Creating time lapse video."
    ffmpeg -framerate 30 -r 30 -f image2 -s 1920x1080 -pattern_type glob -i "$1"'/*.jpg' -i ${dir}-label.png -filter_complex "[0:v][1:v] overlay=1400:900" -vcodec libx264 -crf 25  -pix_fmt yuv420p ${dir}.mp4
else
    echo "$1 is not a directory."
    exit 1
fi

NB: This script contains a reference to the font “Linotype – AvenirLTPro-Light”, which you may not have. You might want to change that to “Arial” or something, although the positioning might then be off. I arrived at the positioning through trial and error; if you change the font you might also need to change the “55,5” bit to get it to align properly.

Step 3: Do something with those still images

There’s a script which runs every morning at around 3am which creates a 20-second video from the previous day for each camera.

#!/bin/sh
export PATH=$PATH:/usr/local/bin:/sbin:/usr/sbin:/usr/bin
cd /Users/jdlarios/BurkeTimeLapse

if [ "X$1" == "X" ]; then
    runtime='yesterday'
else 
    runtime=$1
fi

yesterday=`gdate -d "$runtime" +'%F'` || exit 1;

for i in UWIT Gates Tower Burke; do
    echo ${i}/${yesterday}
    find ${i}/${yesterday} -type f -iname "*.jpg" -size -100c | xargs rm 
    rm -fr build/*
    ./timelapse.sh ${yesterday} ${yesterday} 20 ${i} 0
    mv build/output.mp4 ${i}/${yesterday}.mp4
    rm -fr build/*
    ls -l ${i}/${yesterday}.mp4
done

That script relies on a script named “timelapse.sh”, which takes a start and end date, a desired length, the name of the camera directory, and whether to skip weekends.

#!/bin/bash
# Usage: timelapse.sh 2016-09-01 2016-09-31 60      4545    1
#                     start      end        length  camera  skip weekends
cd /Users/jdlarios/BurkeTimeLapse

./framelist.pl $1 $2 $3 $5  | while read i; do dir=`echo $i | cut -f 1-3 -d '-'`; echo $4/$dir/$i-*; done | grep -v '*' > frames.txt
cut -f 1-2 -d '/' < frames.txt | sort | uniq | while read i; do mkdir -p build/$i; done
n=0
while read i; do dir=`dirname $i`; n=$((n+1)); ln -h -v $i build/$dir/`printf '%07i.jpg' $n`; done < frames.txt
cd build
for i in $4/*; do ../$4/mkmovie.sh $i; done
rm concat.txt
for i in *.mp4; do echo "file $i" >> concat.txt ; done
ffmpeg -f concat -i concat.txt -c copy output.mp4
ls -l output.mp4

It, in turn, relies on a script named “framelist.pl”, which takes the arguments supplied by timelapse.sh and copies (hard links, actually, so no extra disk space is consumed by temporary copies) all the jpg images required to make the requested video into a temporary directory.

#!/usr/bin/perl
use Astro::Sunrise;
$lat = 47.6062;
$lon = -122.3321;

$gdate="/usr/local/bin/gdate";

# Number of seconds between start and end times
# $timediff = `$gdate -d "$endtime" +"%s"` - `$gdate -d "$starttime" +"%s"`;

$fps = 30;

$from = $ARGV[0];	# Start date, YYYY-MM-DD
$to = $ARGV[1];		# End date, YYYY-MM-DD
$length = $ARGV[2];	# Desired length in seconds, NNN
$skip = $ARGV[3];	# Skip weekends?

# First, iterate over the days between start and end, skipping weekends
$startts = `$gdate -d "$from 08:00:00" +"%s"`; chomp $startts;
$endts = `$gdate -d "$to 18:00:00" +"%s"`; chomp $endts;

for ($i=$startts; $i<$endts; $i+=86400) {
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($i);
    $year += 1900;
    $mon = sprintf("%02i", $mon+1);
    $mday = sprintf("%02i", $mday);
    $hour = sprintf("%02i", $hour);
    $min = sprintf("%02i", $min);

    # Skip Sat and Sun
    if ( $skip && (($wday == 6) || ($wday == 0))) {
 	  next;
    } else {
	  my ($sunrise, $sunset) = sunrise( { year => $year, month => $mon, day => $mday, lon => $lon, lat => $lat, tz => (-8 + $isdst), dst => $isdst } ); 
	  print STDERR "Sunrise/sunset for $year-$mon-$mday: $sunrise / $sunset ($isdst)\n";
	  # $sunrise and $sunset are now set in the HH:MM format for this day.
	  my $startts = `$gdate -d "$year-$mon-$mday $sunrise:00" +"%s"`; chomp $startts;
	  my $endts = `$gdate -d "$year-$mon-$mday $sunset:00" +"%s"`; chomp $endts;
#	  $startts += 1800; # Sunrise plus half an hour
#	  $endts -= 1800; # Half an hour before sunset.
	  $endts -= 900; # 15 minutes before sunset.
	  $timediff = $endts - $startts;
#	  print STDERR "$startts -> $timediff\n";

	  # So now add each minute between start and end for this day to an array
	  for ($m=$startts; $m<=$startts+$timediff; $m+=60) {
	    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($m);
	    $year += 1900;
	    $mon = sprintf("%02i", $mon+1);
	    $mday = sprintf("%02i", $mday);
	    $hour = sprintf("%02i", $hour);
	    $min = sprintf("%02i", $min);
	    $minute = "${year}-${mon}-${mday}-${hour}-${min}";
	    push (@minutes, $minute);
	  }
    }
}

$available_frames = $#minutes; # This is in still frames (or minutes)
$desired_frames = $length * $fps; # This is in seconds

# print "Available: $available_frames\n";
# print "Desired: $desired_frames\n";

for ($i = 0; $i < $available_frames; $i+=($available_frames/$desired_frames)) {
    print $minutes[int($i+.5)] . "\n";
}

To be honest, I’ve forgotten how some of this works already. But the end result is that it returns a list of frames between sunrise and sunset on the requested dates, spaced so that the number of frames would create a video of the desired length. Sometimes a few of the files for those frames are missing or corrupt, in which case the video’s a hair shorter than it ought to be. I’m ok with that.

Whew! I think that’s it. Now when I want to create a new time lapse video, I can do so like this:

./timelapse.sh 2017-01-01 2017-2-28 60 Burke 0

That will eventually produce a file named “output.mp4” in the “build” directory containing a 60 second video covering January 1st through February 28th, including weekends.

The end result is something like this:

Leave a Reply

Your email address will not be published. Required fields are marked *