Last active
May 23, 2019 02:52
-
-
Save inactivist/8938b9a3f31194e45a36dc726c34903c to your computer and use it in GitHub Desktop.
Script to generate "daily video" compilation from Zoneminder storage (from: https://forums.zoneminder.com/viewtopic.php?t=24686#p99685)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
ZM_MKVID=/path/to/zm_mkvid.py | |
ZM_MONITORS='Front_Porch_Substream Rear_Porch_Substream' | |
ZM_EVENTS_DIR=/var/lib/zoneminder/events | |
NOW_STRING=$( date +%Y%m%d-%H%M%S ) | |
DEST_DIR=/path/to/video_archive | |
cd ${DEST_DIR} | |
for monitor in ${ZM_MONITORS} ; do | |
log=/tmp/zm_mkvid.${monitor}.${NOW_STRING}.txt | |
${ZM_MKVID} \ | |
--scale=0 \ | |
--yesterday=${ZM_EVENTS_DIR}/${monitor} \ | |
--prefix=${monitor} > ${log} 2>&1 | |
done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# Original source and additional details: https://forums.zoneminder.com/viewtopic.php?t=24686#p99685 | |
import argparse, os, logging, sys, time, subprocess, tempfile, shutil, glob, operator | |
FFMPEG = '/usr/bin/ffmpeg' # ffmpeg package on CentOS | |
ZM_EVENT_DIR = '/var/lib/zoneminder/events' | |
DEFAULT_SCALE_PCT = 50 | |
DEFAULT_FRAME_RATE = 15 | |
DEFAULT_FN_PREFIX = 'capture' | |
DEFAULT_LOG_LEVEL = 'WARNING' | |
############################################################################## | |
class VideoMaker(object): | |
def __init__(self, framerate, scalepct, dry_run=False): | |
self.framerate = framerate | |
self.dry_run = dry_run | |
self.scalepct = scalepct | |
self.logger = logging.getLogger(type(self).__name__) | |
def mkvid(self, jpeg_list, outfilename): | |
scalestr = 'scale=iw:ih' | |
if self.scalepct != 0 and self.scalepct != 100: | |
scalestr = 'scale=iw*{0}:ih*{0}'.format(self.scalepct/100.0) | |
cmd = [ FFMPEG, | |
'-safe', '0', | |
'-f', 'concat', | |
'-i', jpeg_list, | |
'-vf', scalestr, | |
'-framerate', str(self.framerate), | |
'-pix_fmt', 'yuv420p', | |
outfilename ] | |
self.logger.debug('cmd="%s"', str(cmd)) | |
if self.dry_run: | |
self.logger.info('dry_run==True, not running cmd') | |
return True | |
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
stdout, stderr = pipe.communicate() | |
rc = pipe.wait() | |
if 0 != rc: | |
self.logger.error( | |
'%s returned %d\n' | |
' cmd="%s"\n' | |
' stdout="%s"\n' | |
' stderr="%s"\n', | |
FFMPEG, rc, str(cmd), str(stdout), str(stderr)) | |
return False | |
return True | |
############################################################################## | |
class FileFetcher(object): | |
def __init__(self, suffix='.jpg'): | |
self.logger = logging.getLogger(type(self).__name__) | |
self.suffix = suffix | |
self.jpegs = dict() | |
self.oldest = sys.maxint | |
self.newest = 0 | |
# build output video filename | |
def makeVidfilename(self, prefix): | |
vidfilename = prefix | |
vidfilename += '_' | |
vidfilename += time.strftime("%Y%m%d_%H%M%S", time.localtime(self.oldest)) | |
vidfilename += '-' | |
vidfilename += time.strftime("%Y%m%d_%H%M%S", time.localtime(self.newest)) | |
vidfilename += '.mkv' | |
return vidfilename | |
# create a file that lists all the JPG files - this will be an | |
# input for ffmpeg; this is to ensure proper ordering of the | |
# JPGs so the resulting video has the right sequence | |
def writeFilelist(self): | |
tf = tempfile.NamedTemporaryFile(suffix='.txt', prefix='mkvid_filelist.', delete=False) | |
sorted_jpegs = sorted(self.jpegs.items(), key=operator.itemgetter(1)) | |
for (jpeg, mtime) in sorted_jpegs: | |
tf.write("file '{0}'\n".format(jpeg)) | |
tf.close() | |
return tf.name | |
def fetch(self, event_dir): | |
for root, dirs, files in os.walk(event_dir): | |
for f in files: | |
if f.startswith('.'): | |
self.logger.debug('file "%s" starts with period, skipping', f) | |
continue | |
elif not f.endswith(self.suffix): | |
self.logger.debug('file "%s" not jpg, skipping', f) | |
continue | |
jpegfile = os.path.join(root, f) # full path to file | |
statdata = os.stat(jpegfile) | |
mtime = statdata.st_mtime | |
if mtime > self.newest: self.newest = mtime | |
if mtime < self.oldest: self.oldest = mtime | |
#if jpegfile in self.jpegs.keys(): | |
# self.logger.debug('file "%s" duplicate, skipping', f) | |
# continue | |
self.jpegs[jpegfile] = mtime | |
############################################################################## | |
def sec2hms(secs): | |
int_secs = int(secs) | |
hours = int_secs / 3600 | |
mins = (int_secs % 3600) / 60 | |
s = int_secs % 60 | |
frac = secs - int_secs | |
newsecs = s+frac | |
return '%d hours %d minutes %.1lf seconds' % (hours, mins, newsecs) | |
############################################################################## | |
def main(): | |
os.nice(20) # can't conceive of a scenario where you wouldn't want this to run at lowest priority | |
main_start_time = time.time() # keep some basic timing/performance metrics | |
# https://docs.python.org/2/library/logging.html#logrecord-attributes | |
date_format='%Y%m%d-%H:%M:%S' | |
log_format='%(asctime)s %(levelname)s %(threadName)s %(name)s.%(funcName)s(): %(message)s' | |
log_config = { | |
'CRITICAL' : { 'datefmt' : date_format, 'logfmt' : log_format }, | |
'ERROR' : { 'datefmt' : date_format, 'logfmt' : log_format }, | |
'WARNING' : { 'datefmt' : date_format, 'logfmt' : log_format }, | |
'INFO' : { 'datefmt' : date_format, 'logfmt' : log_format }, | |
'DEBUG' : { 'datefmt' : date_format, 'logfmt' : log_format }, | |
} | |
parser = argparse.ArgumentParser(description='Concatenate Zoneminder event image files into video') | |
parser.add_argument('--event-dir', '-d', | |
dest='event_dirs', | |
metavar='EVENTDIR', | |
action='append', | |
default=[ ], | |
help='Top level zoneminder event directory(ies) to walk for image files. Can be a glob pattern and/or specified multiple times.') | |
parser.add_argument('--yesterday', '-y', | |
dest='yesterday', | |
metavar='EVENTDIR', | |
action='store', | |
default=None, | |
help='Top level zoneminder event directory for a monitor; example: /var/lib/zoneminder/events/2 - remaining date portion will be computed via yesterday\'s date.') | |
parser.add_argument('--verbosity', '-v', | |
dest='verbosity', | |
metavar='LEVEL', | |
action='store', | |
default=DEFAULT_LOG_LEVEL, | |
choices=log_config.keys(), | |
help='Set verbosity/logging level. Valid options: %s, default=%s.' % (str(log_config.keys()), DEFAULT_LOG_LEVEL)) | |
parser.add_argument('--dry-run', '-n', | |
dest='dry_run', | |
action='store_true', | |
help='Don\'t actually do anything, just pretend.') | |
parser.add_argument('--scale', '-s', | |
dest='scale_pct', | |
metavar='SCALE_PCT', | |
action='store', | |
type=int, | |
default=DEFAULT_SCALE_PCT, | |
help='Specify scaling factor as percentage, default=%d.' % (DEFAULT_SCALE_PCT)) | |
parser.add_argument('--framerate', '-f', | |
dest='framerate', | |
metavar='FRAMERATE', | |
action='store', | |
type=int, | |
default=DEFAULT_FRAME_RATE, | |
help='Specify framerate to ffmpeg, default=%d.' % (DEFAULT_FRAME_RATE)) | |
parser.add_argument('--prefix', '-p', | |
dest='vidfile_prefix', | |
metavar='PREFIX', | |
action='store', | |
default=DEFAULT_FN_PREFIX, | |
help='Prefix for output video file name, default=%s.' % (DEFAULT_FN_PREFIX)) | |
parser.add_argument('--no-cleanup', | |
dest='cleanup', | |
default=True, | |
action='store_false', | |
help='Do not clean up temporary files when done, default=do cleanup.') | |
args = parser.parse_args() | |
log_level_numeric = getattr(logging, args.verbosity, None) | |
if not isinstance(log_level_numeric, int): | |
raise ValueError('Invalid log level: %s' % loglevel) | |
logging.basicConfig( | |
level=log_level_numeric, | |
datefmt=log_config[args.verbosity]['datefmt'], | |
format=log_config[args.verbosity]['logfmt']) | |
logging.debug('args => ' + str(args)) | |
if 0 == len(args.event_dirs) and None == args.yesterday: | |
print >>sys.stderr, 'ERROR: no event directory(ies) specified, abort' | |
sys.exit(-1) | |
# obtain a list of JPG files that will be concatenated into a | |
# video; sorted by date (oldest first, newest last) | |
fetcher = FileFetcher() | |
if args.yesterday: | |
if not os.path.exists(args.yesterday): | |
print >>sys.stderr, 'ERROR: {0}: does not exist, abort'.format(args.yesterday) | |
sys.exit(-1) | |
yesterday = time.localtime(time.time()-86400) | |
vid_dir = args.yesterday + os.sep | |
vid_dir += time.strftime("%y", yesterday) + os.sep | |
vid_dir += time.strftime("%m", yesterday) + os.sep | |
vid_dir += time.strftime("%d", yesterday) | |
print 'DEBUG: vid_dir={0}'.format(vid_dir) | |
if not os.path.exists(vid_dir): | |
print >>sys.stderr, 'ERROR: {0}: does not exist, abort'.format(vid_dir) | |
sys.exit(-1) | |
fetcher.fetch(vid_dir) | |
else: | |
scandirs = set() | |
for ed in args.event_dirs: | |
for d in glob.glob(ed): | |
logging.debug('adding directory %s from glob %s' % (d, ed)) | |
scandirs.add(d) | |
for sd in scandirs: | |
logging.debug('fetching files from %s' % (sd)) | |
fetcher.fetch(sd) | |
print 'Found {0} JPEG files, converting to video...'.format(len(fetcher.jpegs)) | |
filelist = fetcher.writeFilelist() | |
vidfilename = fetcher.makeVidfilename(args.vidfile_prefix) | |
logging.debug('filelist=' + str(filelist)) | |
logging.debug('vidfilename=' + str(vidfilename)) | |
# actually run ffmpeg to create the video | |
vidmaker = VideoMaker(scalepct=args.scale_pct, framerate=args.framerate, dry_run=args.dry_run) | |
good = vidmaker.mkvid(filelist, vidfilename) | |
if not good: | |
print >>sys.stderr, 'ERROR: ffmpeg process failure, abort' | |
sys.exit(-1) | |
# cleanup temporary files/directories | |
if args.cleanup: | |
os.unlink(filelist) | |
# print some possibly interesting timing stats | |
now = time.time() | |
total_runtime = now - main_start_time | |
print 'Total runtime: %lf seconds' % (total_runtime) | |
print ' %s' % (sec2hms(total_runtime)) | |
print '' | |
############################################################################## | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment