#!/usr/bin/perl # "linux" video encoder for the D-Link DCS-900 series ip cameras. # (c) Copyright 2004 Seth Alan Woolley, released under GPL v2 or above. # reverse-engineered with packet analysis from the windows drivers. # example fcrontab file: # &exesev(true) 0 * * * * perl /home/camera/captureimage.pl 01 3600 10.10.1.121 # &exesev(true) 0 * * * * perl /home/camera/captureimage.pl 02 3600 10.10.1.122 # &exesev(true) 0 0 * * * bash /home/camera/purgeold.sh '01 02' 14 # purges videos after 14 days. # example purgeold.sh: # #!/bin/bash # # LIST="$1" # DAYS="$2" # # cd /home/camera/video/ # for i in $LIST; do # for j in $( # ls video-$i-*.avi | # sed -e 's/-[0-9.]\+\.avi$//' -e 's/^video-//' | # sort | # head -n -$((24*$DAYS)) # ); do # rm video-$j-*.avi video-$j-*.sub ../logs/capture-$j.log # done # done use Socket; use Time::HiRes qw ' time sleep '; $cameraid = $ARGV[0] eq ''?"01" :$ARGV[0] ; $runseconds = $ARGV[1] eq ''?3600 :$ARGV[1] ; $cameraaddr = $ARGV[2] eq ''?"10.10.1.121" :$ARGV[2] ; $cameraport = $ARGV[3] eq ''?80 :$ARGV[3] ; $height = $ARGV[4] eq ''?640 :$ARGV[4] ; $width = $ARGV[5] eq ''?480 :$ARGV[5] ; $minkbits = $ARGV[6] eq ''?000 :$ARGV[6] ; $maxkbits = $ARGV[7] eq ''?300 :$ARGV[7] ; $minquant = $ARGV[8] eq ''?3 :$ARGV[8] ; $maxquant = $ARGV[9] eq ''?16 :$ARGV[9] ; $softdropfps = $ARGV[10] eq ''?4 :$ARGV[10]; # delay network reads to be at most this fps $harddropfps = $ARGV[11] eq ''?30 :$ARGV[11]; # drop already read frames to be at most this fps $softdropnow = $ARGV[12] eq ''?30 :$ARGV[12]; # point-based drops, the aboves are averages $harddropnow = $ARGV[13] eq ''?30 :$ARGV[13]; # ditto $imagestore = $ARGV[14] eq ''?"/home/camera/image/":$ARGV[14]; $moviestore = $ARGV[15] eq ''?"/home/camera/video/":$ARGV[15]; $logstore = $ARGV[16] eq ''?"/home/camera/logs/" :$ARGV[16]; $password = $ARGV[17] eq ''?"" :$ARGV[17]; # in basic auth format $timeout = 15; # seconds to wait before we assume catastrophe, flush connections, and retry $f = 1 ; # start frame number $sub = ''; # mpsub file contents; $time = time(); # when we begin this capture (now) $timestop = $time + $runseconds; # when we end this capture (later) ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($time); $_=$wday.$yday; #these not used $date = sprintf("%04d-%02d-%02d-%02d-%02d-%02d-%06d", 1900+$year, $mon+1, $mday, $hour, $min, $sec, $runseconds); $avgkbits = ($maxkbits + $minkbits) / 2; $errkbits = ($maxkbits - $minkbits) / 2; open(LOG, ">>${logstore}capture-$cameraid-$date.log"); begin: $SIG{INT} = sub { die "int\n" }; socket(SH, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die "socket: $!"; connect(SH,sockaddr_in($cameraport,inet_aton($cameraaddr))) or warn "connect: $!"; # may be a network error, keep retrying until we re-establish $| = 1; binmode(SH); # username 'read' and password 'data' in Authorization header below syswrite SH, "GET /VIDEO.CGI HTTP/1.0\r\nhost: $cameraaddr\r\nAuthorization: Basic $password\r\n\r\n" or die "syswrite: $!"; eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; # set the alarm for reading the header while() { if (/^Content-length: ([0-9]+)/) { alarm 0; $SIG{ALRM} = undef; local $dropframe = undef ; $oldst = $st ; $st = time(); $stactual = $st ; $ah = (1 / $softdropnow) - ($st - $oldst); if ($ah > 0 ) { sleep($ah); $st = time(); } $ah = ($f / $softdropfps) - ($st - $time ); if ($ah > 0 ) { sleep($ah); $st = time(); } $ah = (1 / $harddropnow) - ($st - $oldst); if ($ah > 0 ) { $dropframe = 1 ; } $ah = ($f / $harddropfps) - ($st - $time ); if ($ah > 0 ) { $dropframe = 1 ; } $SIG{ALRM} = sub { die "alarm 1\n" }; alarm $timeout; # reset the alarm for reading more of the header $l = $1; print LOG "$l bytes\t"; do true until eq "\r\n"; # queue to the end of the header $/ = \$l; $data = ; $/ = "\n"; # read $l bytes of data alarm 0; $SIG{ALRM} = sub { die "alarm 2\n" }; alarm $timeout; # reset the alarm for reading the payload $file = "${imagestore}image-${cameraid}-${date}-" . sprintf("%08d", $f) . ".jpg"; $timestamp = scalar(localtime($stactual)) . " " . sprintf("%06d", ($stactual - int($stactual))*1000000) . " us"; if (!$dropframe) { open(FILE, ">$file") or die "open: $!"; print(FILE "$data") && $f++; close(FILE) or die "close: $!"; print LOG "$file\t$timestamp\n"; # $sub .= "\n0 1\n" . scalar(localtime($stactual)) . "\n"; $sub .= "\n0 1\n$timestamp\n"; } else { print LOG "$file\t$timestamp DROPPED\n"; } } if (time() > $timestop) { die "done\n"; } alarm 0; local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; # reset the alarm for reading the header } }; if ($@) { # eval completed die unless $@ eq "alarm\n" or $@ eq "alarm 2\n" or $@ eq "alarm 1\n" or $@ eq "done\n" or $@ eq "int\n"; # propagate unexpected errors if ($@ eq "alarm\n") { print LOG "SIGALRM received: restarting due to $timeout second timeout in reading data header\n"; close(SH) or die "close: $!"; # close the socket goto begin; } elsif ($@ eq "alarm 1\n") { print LOG "SIGALRM received: restarting due to $timeout second timeout in reading more data header\n"; close(SH) or die "close: $!"; # close the socket goto begin; } elsif ($@ eq "alarm 2\n") { print LOG "SIGALRM received: restarting due to $timeout second timeout in reading data payload\n"; close(SH) or die "close: $!"; # close the socket goto begin; } elsif ($@ eq "int\n") { alarm 0; # we are interrupted, turn off any alarm close(SH) or die "close: $!"; # close the socket opendir(DIR, $imagestore); @imagefiles = grep { /^image-${cameraid}-${date}-.*\.jpg$/ && -f "${imagestore}$_" } readdir(DIR); close(DIR); print LOG "unlinking all jpeg frames\n"; for (@imagefiles) { unlink "${imagestore}$_" or die "unlink: $!"; } print LOG "no video generated\n"; } else { alarm 0; # we are done, turn off any alarm close(SH) or die "close: $!"; # close the socket opendir(DIR, $imagestore); @imagefiles = grep { /^image-${cameraid}-${date}-.*\.jpg$/ && -f "${imagestore}$_" } readdir(DIR); close(DIR); $frames = ($#imagefiles + 1); $fps = sprintf("%.3f", $frames / $runseconds ); print LOG "Images captured, now encoding $frames frames at $fps fps...\n"; $captime = time(); print LOG "starting generation at " . scalar(localtime($captime)) . " " . sprintf("%06d", ($captime - int($captime))*1000000) . " us\n"; $SIG{INT} = sub { print LOG "unlinking all jpeg frames\n"; for (@imagefiles) { unlink "${imagestore}$_" or warn "unlink: $!"; } die "SIGINT received during encoding, no video generaged.\n"; }; print LOG "creating mpsub file for feeding into mencoder\n"; open(SUB, ">${moviestore}video-${cameraid}-${date}-${fps}.sub"); print SUB "FORMAT=$fps\n$sub" or warn "print SUB: $!"; close(SUB); print LOG "executing mencoder now...\n"; system("mencoder -quiet -vc ijpg -mf type=jpg:h=${height}:w=${width}:fps=${fps} " . " mf://${imagestore}image-${cameraid}-${date}-*.jpg " . " -o ${moviestore}video-${cameraid}-${date}-${fps}.avi " . " -sub ${moviestore}video-${cameraid}-${date}-${fps}.sub " . "-ovc lavc -lavcopts " . "vcodec=msmpeg4v2:mv0:trell:cbp:vqmin=${minquant}:vqmax=${maxquant}:vbitrate=${avgkbits}:vratetol=${errkbits}000 " . "-vf hqdn3d=1:1:16" . ":decimate=3:512:32:1 " . " > /dev/null 2> /dev/null" . ""); print LOG "...unlinking all jpeg frames\n"; for (@imagefiles) { unlink "${imagestore}$_" or die "unlink: $!"; } $donetime = time(); print LOG "video-${cameraid}-${date}-${fps}.avi generated at " . scalar(localtime($donetime)) . " " . sprintf("%06d", ($donetime - int($donetime))*1000000) . " us, taking " . ($donetime - $captime) . " seconds\n"; } } close(SH); close(LOG); 1;