#!/usr/bin/env python2.7 # # FFMpeg stream control, able to recover from 'broken' streams # # Initial mockup - Ricard van Mansom # Modified by - Rick van der Zwet # # TODO(rvdz): This is ment to be started like an multi-threaded daemon, so # convert it as such. # TODO(rvdz): Are we not mimicking something like supervisord or some other # regular shell? # TODO(rvdz): startup utilities? # import argparse import logging import logging.handlers import os import signal import subprocess import sys import threading import time import yaml # Note: daemon.runner is a DIFFERENT (base) package from daemonrunner import DaemonRunner from ffmpeg_config import make_ffserver_config from persistent_cmd import RunCmd try: from yaml import CLoader as Loader from yaml import CDumper as Dumper except ImportError: from yaml import Loader, Dumper directory_root = os.path.dirname(os.path.abspath(sys.argv[0])) # Using two files to avoid the authentication credentials of getting committed class DEFAULT: setting_file = 'ffmpeg-cams.cfg' auth_file = 'ffmpeg-auth.cfg' ffserver_tmpl = os.path.join(directory_root, 'ffserver.conf.tmpl') var = os.path.join(directory_root,'var') name = os.path.splitext(os.path.basename(sys.argv[0]))[0] threshold = 120 def getSettings(filename): """ Get settings from yaml config file """ logger.info("Loading settings from %s file", filename) with open(filename, 'r') as f: cfg = yaml.load(f,Loader=Loader) return cfg class ControlApp(): def __init__(self, args): self.args = args def run(self): logger = logging.getLogger() args = self.args # Auth en normal settings are seperated to allow publishing settings of # camera, but not auth credentials. cfg = getSettings(args.config) auth_cfg = getSettings(args.auth) if auth_cfg: for key,items in auth_cfg.iteritems(): if not cfg['cams'].has_key(key): logger.error("Auth key '%s' of '%s' not found in setting file '%s'", key, args.auth, args.config) sys.exit(1) cfg['cams'][key].update(auth_cfg[key]) # Validate configurations and process open variables cam_config = {} for key,tmp_cfg in cfg['cams'].iteritems(): try: # Process all open variables for t in tmp_cfg.keys(): tmp_cfg[t] = tmp_cfg[t] % tmp_cfg # Create CLI line tmp_cfg['binary'] = os.path.join(os.path.expandvars(cfg['binpath']), 'ffmpeg') if tmp_cfg.has_key('full'): tmp_cfg['cmd'] = "%(binary)s %(full)s -i %(source)s %(target)s" % tmp_cfg cam_config[key] = tmp_cfg except KeyError as e: logger.error("Config %s cannot be validated: %s", key, e) sys.exit(1) # Specify active camera's in use if args.cams == 'all': active_cams = sorted(cam_config.keys()) else: active_cams = sorted(args.cams.split(',')) logging.info("Streams to-be activated %s", active_cams) # # Main loop - starting all threads # threads = {} # Starting server logger.info("Creating a server thread") ffserver = os.path.join(os.path.expandvars(cfg['binpath']), 'ffserver') ffserver_configfile = os.path.join(args.var, 'ffserver.conf') make_ffserver_config(DEFAULT.ffserver_tmpl, ffserver_configfile, directory_root) threads['server'] = RunCmd( 'server', '%s -f %s -d' % (ffserver, ffserver_configfile), restart_timeout=args.threshold, cmd_logfile='var/thread-server.log', ) threads['server'].start() startup_wait = 2 logger.info("Waiting %i seconds, for the server to start", startup_wait) time.sleep(startup_wait) # Starting required camera's for cam in active_cams: logger.info("Creating a cam thread %s", cam) threads[cam] = RunCmd(cam, cam_config[cam]['cmd'], restart_timeout=args.threshold, cmd_logfile='var/thread-%s.log' % cam, ) # Start the thread threads[cam].start() try: # Keep in foreground, mind not to use join and such as they do not work with # interrupt handling: http://bugs.python.org/issue1167930#msg56947 while True: time.sleep(60) except (KeyboardInterrupt, SystemExit) as e: logger.info('Received signal to exit (%s), shutting down threads' % e) # Could combine this loops, but I rather have all my threads to close as # soon as possible. for _,thread in threads.items(): logger.info("Signalling thread %s to shutdown", thread.name) thread.stop() for _,thread in threads.items(): thread.join() logger.info("Thread %s has stopped", thread.name) logger.info('All done, goodbye!') if __name__ == '__main__': parser = argparse.ArgumentParser( description='Starting all camera threads', formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument('--config', default=DEFAULT.setting_file, help='Configuration file') parser.add_argument('--auth', default=DEFAULT.auth_file, help='Username/Password Configuration file') parser.add_argument('--threshold', default=DEFAULT.threshold, type=int, help='Threshold to wait before re-connect on failure') parser.add_argument('--var', default=DEFAULT.var, help='Storage directory for log and pid-files') parser.add_argument('--cams', default='all', help="Camera's to start, comma seperated") parser.add_argument('action', choices=['start','stop','restart','status'], help="Control the master process") args = parser.parse_args() args.var = os.path.expandvars(args.var) logfile = "%s/%s.log" % (args.var, DEFAULT.name) # Add logfile logger = logging.getLogger() logger.setLevel(logging.DEBUG) #Mar 21 01:00:00 arenal sshguard[20105]: Started successfully [(a,p,s)=(40, 420, 1200)], now ready to scan. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') fh = logging.handlers.WatchedFileHandler(logfile) fh.setFormatter(formatter) logger.addHandler(fh) def callback(): try: app = ControlApp(args) app.run() except Exception as exc: logger.exception("Unable to start, programming error!") raise runner = DaemonRunner(callback, '%s/%s.pid' % (args.var, DEFAULT.name), directory_root, logfile) runner.register_logger(logging.getLogger()) logger.debug("Called from CLI: %s" % str(args)) runner.run(args.action)