Grabbing Stills and Making FLV Movies from Axis IP Cameras

About a week and a half ago, I was reminded of a long-dormant project to archive still images from an Axis IP camera. I started this up a few years ago as a favor to a coworker, but it never really got finished. Previously, it was a pretty simple cron job that would just authenticate to the camera and download the current still. At some point, it would also use ImageMagick to convert the captured JPEGs to an MPEG or similar, but it was decidedly non-optimal.

So now that I was reminded by people who were very interested in seeing the results (i.e., monitoring their remotely-located labs), I took another stab at it. Much better results this time. Now my customers get:

  • Still images captured every 30 seconds
  • FLV movies of the day’s stills made every 5 minutes
  • A playlist that lets them browse through previous days’ activity for as long as we keep the movies around

The programs and pages that make this mini-site follow below:

axisgrab.py — extracts current image from the designated Axis camera, encodes today’s images into an FLV

#!/usr/bin/python

basename='denso'
axisip='192.168.0.1'
axisuser='someuser'
axispass='somepassword'
flvcreator='TTU ME Department'

import urllib2, time, os, re, getopt, sys

def usage():
    print """axisgrab.py -- grab or encode images from an Axis IP camera
Usage: axisgrab.py --grab
       axisgrab.py --encode
"""

def main():

    try:
        optlist, args = getopt.getopt(sys.argv[1:], 'g:e:',
                                      [ 'grab', 'encode' ])
    except getopt.GetoptError:
        usage()
        sys.exit(2)

    for opt, junk in optlist:
        if (opt=='--grab'):    
            auth_handler = urllib2.HTTPBasicAuthHandler()
            auth_handler.add_password('/',axisip,axisuser,axispass)
            opener = urllib2.build_opener(auth_handler)
            urllib2.install_opener(opener)
            axis_jpg=urllib2.urlopen('http://%s/axis-cgi/jpg/image.cgi' % ( axisip ) )
    
            filename='%s_%s_%s.jpg' % ( basename, time.strftime('%Y%m%d'), time.strftime('%H%M%S') )
            local_jpg=open(filename,'w')
            local_jpg.write(axis_jpg.read())
            local_jpg.close()
            os.chmod(filename,0644)

        if (opt=='--encode'):
            os.system('mencoder -really-quiet -ovc lavc -lavcopts vcodec=flv -mf fps=25:type=jpg \\'mf://%s_%s*.jpg\\' -of lavf -lavfopts i_certify_that_my_video_stream_does_not_use_b_frames -o %s_%s.flv >&/dev/null' % ( basename, time.strftime('%Y%m%d'), basename, time.strftime('%Y%m%d') ))
    
            playlistname="playlist-%s.xml" % ( basename )
            playlist=open(playlistname,'w');
            playlist.write("""
            <?xml version="1.0" encoding="utf-8"?>
            <playlist version="1" xmlns="http://xspf.org/ns/0/">
            <trackList>
            """)

    
            files=os.listdir(".")
            files.sort()
            repattern="%s.+flv" % ( basename )
            recompiled=re.compile(repattern)
            for file in files:
                if recompiled.search(file):
                    playlist.write("    <track>\n")
                    playlist.write("      <title>%s</title>\n" % (file))
                    playlist.write("      <creator>%s</creator>\n" % (flvcreator))
                    playlist.write("      <location>%s</location>\n" % (file))
                    playlist.write("      <info>force_download.php?file=%s</info>\n" % (file))
                    playlist.write("    </track>\n")

            playlist.write("""
            </trackList>
            </playlist>
            """)

if __name__ == "__main__":
    main()

index.php — main web interface to the images and movies

<?php
  // If I did all this correctly, you shouldn't have to change
  // anything but the basename variable.
$basename = "denso";

$today = date("Ymd"); // e.g., 20070901
$mediabase = $basename."_".$today; // axisgrab.py stores images with this naming scheme
$imagewidth = 480; // Controlled on the camera video/image settings
$imageheight = 360; // Controlled on the camera video/image settings
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Denso Lab Camera Feed<title>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
var browser = navigator.appName;
var version = parseInt(navigator.appVersion);

function showImage(object) {
  var imageToShow = object.options[object.selectedIndex].value;

  if (browser == 'Netscape') {
    if (version >= 3) document.dropImage.src = imageToShow;
    else              alert('Your browser does not support the image object - sorry');
  }
  else if (browser == 'Microsoft Internet Explorer') {
    if (version >= 4) document.dropImage.src = imageToShow;
    else              frames[0].location.href = imageToShow;
  }
}
//--></script>
<meta http-equiv="refresh" content="300">

</head>
<body>
<table>
<tr valign="top">
<td>
<h1>Denso Lab Camera Footage</h1>

<p>Select a day from the list below to play, or click the icon at the right of the filename to download.</p>
<p id="player1"><a href="http://www.macromedia.com/go/getflashplayer">Get the Flash Player</a> to see this player.</p>
<script type="text/javascript">
   var s1 = new SWFObject("mediaplayer.swf","single","480","480","7");
s1.addParam("allowfullscreen","true");
s1.addParam('allowscriptaccess','always');
s1.addVariable("file","<?php echo "playlist-".$basename.".xml"; ?>");
s1.addVariable('displayheight','360');
s1.write("player1");
</script>
</td>
<td>
<h1>Still Images</h1>
<form name="imageForm">
<select name="imageSelect">
<?php
if ($dh = opendir('./')) {
  $files = array();
  while (($file = readdir($dh)) !== false) {
    if (substr($file, strlen($file) - 4) == '.jpg') {
      array_push($files, $file);
    }
  }
  closedir($dh);
}

// Sort the files and display
sort($files);

foreach ($files as $file) {
  echo "<option value=$file>$file</option>\n";
}
?>
</select>
<input name="submitName" type="button" value="Show" onClick="showImage(document.imageForm.imageSelect)">
</form>

<SCRIPT LANGUAGE="JavaScript"><!--
if (browser == 'Netscape')
  document.write('<IMG SRC="<?php echo $mediabase."_0000.jpg"; ?>" NAME="dropImage" WIDTH=<?php echo $imagewidth;?> HEIGHT=<?php echo $imageheight;?>>');
 else if (browser == 'Microsoft Internet Explorer') {
   if (version >= 4)
     document.write('<IMG SRC="<?php echo $mediabase."_0000.jpg"; ?>" NAME="dropImage" WIDTH=<?php echo $imagewidth;?> HEIGHT=<?php echo $imageheight;?>>');
    else
      document.write('<IFRAME FRAMEBORDER=0 WIDTH=<?php echo $imagewidth;?> HEIGHT=<?php echo $imageheight
;?> MARGINHEIGHT=0 MARGINWIDTH=0 SCROLLING=no SRC="<?php echo $mediabase."_0000.jpg"; ?>"></IFRAME>');
 }
//--></SCRIPT>
</td>
</tr>
</table>

swfobject.js and mediaplayer.swf are part of Jeroen Wijering’s JW Media Player.

Cron jobs on the webserver (to grab 2 images per minute):

* * * * * (cd /path/to/axis/folder && ./axisgrab.py --grab && sleep 30 && ./axisgrab.py --grab)
0,5,10,15,20,25,30,35,40,45,50,55 * * * * (cd /path/to/axis/folder && ./axisgrab.py --encode)

The results:
axisscreenshot.png

Join the Conversation

7 Comments

  1. Well that sounds very nice,
    i think the python part could be done in PHP aswell even if i never wrote python its quite easy to understand. But i think the bigger issue would be to get mencoder running on my server.

  2. Sure could. I just have a mental disconnect regarding using PHP for anything other than webpages on demand. Python tends to be my default tool for anything related to systems administration that isn’t a very quick and easy shell script.

    And this definitely isn’t for people who have a shared host at some random web hosting service. For me, getting mencoder was a matter of adding a Debian-multimedia package source and installing it.

  3. Hi Mike,
    I found your blog while searching for a way to archive images from an IP camera. I am trying to create a website to monitor a construction project I am going to be superintendent on. Time lapse video of the construction progress was also on my wish list. Unfortunately I am no very little about programming. I have managed to get a basic site going, and found templates that create archived images, but I have seen websites where you can click on the day in a calendar and the archived images from that day will be available. The archive system I have creates pages of thumbnails and there is no way to jump to a specific day.
    I guess I’m getting a little off subject. What I was hoping is you could explain in simple terms what I need to do to get your code to accomplish archiving the images and creating a time lapse video on my website. I would be willing to pay for your services if within my meager budget.
    Thanks,
    Mike

  4. This setup may or may not be usable with your server setup. So, some questions to help narrow that down:

    1. Is your web server a Windows, Mac, or Linux/Unix system? If Windows, then the axisgrab.py script would be able to grab the images from the camera, but might have trouble running mencoder to convert the images into a Flash video. If it’s a Mac or Linux/Unix system, then it’s a matter of getting PHP, Python, and mencoder installed on it.

    2. Is this a cheap shared-hosting plan, or do you have a dedicated server? If the former, you may not have the ability to run the axisgrab.py script on a recurring basis. Ask your hosting provider if they support you running ‘cron’ jobs, assuming they’re not a Windows server shop. If they say no, then it’ll be difficult to get the images off the camera automatically.

    As far as how to get it running, the best thing I can say is “one step at a time”. First, get axisgrab.py to grab an image off your camera and save it. Next, get axisgrab.py to encode the day’s images into an FLV. If both of those work ok, then it’s fairly simple to get JW Media Player set up to play a hand-typed playlist of FLV files (that you encoded with axisgrab.py). Finally, get index.php to create a playlist for JW Media Player from all the FLV files in your folder.

    Each step has to be working properly before you go to the next step. I’m not really in a position to do side work, but you (or whoever you’re working with at your web host) can email me if needed (addresses on my “About Me” page).

  5. Hi Mike,
    I Just want to say a BIG thanks for the post, I’ve been trying to get something very similar to work (without success) for a few days now using various different server side encoding tools and found your post to be a very helpful.
    Thanks
    Adam

  6. In case it helps anyone, I have sample code of how to do time lapse video off an IP camera on blog http://pcgrind.blogspot.com/ near the bottom.

    This is PC Based, but if you can do a little scripting it can run on any platform. I am basically doing a loop and using wget to obtain images. My video server is currently getting a few FPS per IP camera.

    So step one is to have a scheduled task or cron job in startup doing wget to get your images.

    Step two is to convert these into a video. I use mencoder to do this. This is available form any platform too. So there is a daily job which moves the JPGs into working directories for each camera, then use mencoder to combine the images into a video as described here; http://www.mplayerhq.hu/DOCS/HTML/en/menc-feat-enc-images.html

    I have been doing this for several years with many cameras. Depending on how you configure the settings on the camera/wget/mencoder you can end up with: time lapse video a frame a minute real high quality, low size that looks nice; live video captured at several FPS depending on your bandwidth and CPU at whatever quality you want taking about a GB per day; and anything in between.

    Some cameras will output gray scale images by night and color by day. mencoder will choke on those and stop encoding on the first image that is different. mencoder requires all images to be the same. If this is an issue you need to either normalize the images with something like my Irfanview sample, or setup the camera to always output the same image type. If your camera doesn’t do this, skip that step as it is very time consuming.

  7. I have an Axis IP and managed to follow your directions until around the half-way point but then I got hopelessly lost. Guess I’m just not enough of a propellerhead!

Leave a comment

Leave a Reply to Edwin D Cancel reply

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