Skip to content

Instantly share code, notes, and snippets.

@inactivist
Last active May 23, 2019 02:52
Show Gist options
  • Save inactivist/8938b9a3f31194e45a36dc726c34903c to your computer and use it in GitHub Desktop.
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)
#!/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
#!/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