diff --git a/board/raspberrypi3/motioneye-modules/boardctl.py b/board/raspberrypi3/motioneye-modules/boardctl.py index c65ab5591c..d98fb22753 100644 --- a/board/raspberrypi3/motioneye-modules/boardctl.py +++ b/board/raspberrypi3/motioneye-modules/boardctl.py @@ -48,14 +48,6 @@ speed=$(($speed / 1024)) echo -n "$temp°|$load|${speed}kB/s" ''' -OVERCLOCK = { - 700: '700|250|400|0', - 800: '800|250|400|0', - 900: '900|250|450|0', - 950: '950|250|450|0', - 1000: '1000|500|600|6' -} - def _get_board_settings(): gpu_mem = 128 camera_led = True diff --git a/board/raspberrypi4/motioneye-modules/boardctl.py b/board/raspberrypi4/motioneye-modules/boardctl.py new file mode 100644 index 0000000000..d98fb22753 --- /dev/null +++ b/board/raspberrypi4/motioneye-modules/boardctl.py @@ -0,0 +1,225 @@ + +# Copyright (c) 2015 Calin Crisan +# This file is part of motionEyeOS. +# +# motionEyeOS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import os.path + +from config import additional_config + +import streameyectl + + +CONFIG_TXT = '/boot/config.txt' +MONITOR = '/data/etc/monitor_1' + +MONITOR_SCRIPT = '''#!/bin/bash + +net_tmp=/tmp/netspeed.tmp +temp=$(($(cat /sys/devices/virtual/thermal/thermal_zone0/temp) / 1000)) +load=$(cat /proc/loadavg | cut -d ' ' -f 2) +recv=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 3 | tail -n +3 | awk '{s+=$1} END {print s}') +send=$(cat /proc/net/dev | grep -v 'lo:' | tr -s ' ' | cut -d ' ' -f 11 | tail -n +3 | awk '{s+=$1} END {print s}') +total=$(($recv + $send)) + +if [ -e $net_tmp ]; then + prev_total=$(cat $net_tmp) + speed=$(($total - $prev_total)) +else + speed=0 +fi + +echo $total > $net_tmp +speed=$(($speed / 1024)) + +echo -n "$temp°|$load|${speed}kB/s" +''' + +def _get_board_settings(): + gpu_mem = 128 + camera_led = True + arm_freq = None + + if os.path.exists(CONFIG_TXT): + logging.debug('reading board settings from %s' % CONFIG_TXT) + + with open(CONFIG_TXT) as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + + parts = line.split('=', 1) + if len(parts) != 2: + continue + + name, value = parts + name = name.strip() + value = value.strip() + + if name == 'gpu_mem': + gpu_mem = int(value) + + elif name == 'arm_freq': + arm_freq = int(value) + + elif name == 'disable_camera_led': + camera_led = value == '0' + + + s = { + 'gpuMem': gpu_mem, + 'cameraLed': camera_led + } + + logging.debug('board settings: gpu_mem=%(gpuMem)s, camera_led=%(cameraLed)s' % s) + + return s + + +def _set_board_settings(s): + s.setdefault('gpuMem', 128) + s.setdefault('cameraLed', True) + + old_settings = _get_board_settings() + if s == old_settings: + return # nothing has changed + + seen = set() + + logging.debug('writing board settings to %s: ' % CONFIG_TXT + + 'gpu_mem=%(gpuMem)s, camera_led=%(cameraLed)s' % s) + + lines = [] + if os.path.exists(CONFIG_TXT): + with open(CONFIG_TXT) as f: + lines = f.readlines() + + for i, line in enumerate(lines): + line = line.strip() + if not line: + continue + + line = line.strip('#') + + try: + name, _ = line.split('=', 1) + name = name.strip() + + except: + continue + + seen.add(name) + + if name.startswith('gpu_mem'): + lines[i] = '%s=%s' % (name, s['gpuMem']) + + elif name == 'disable_camera_led': + lines[i] = 'disable_camera_led=%s' % ['1', '0'][s['cameraLed']] + + if 'gpu_mem' not in seen: + lines.append('gpu_mem=%s' % s['gpuMem']) + + if 'disable_camera_led' not in seen: + lines.append('disable_camera_led=%s' % ['1', '0'][s['cameraLed']]) + + logging.debug('remounting /boot read-write') + if os.system('mount -o remount,rw /boot'): + logging.error('failed to remount /boot read-write') + + with open(CONFIG_TXT, 'w') as f: + for line in lines: + if not line.strip(): + continue + if not line.endswith('\n'): + line += '\n' + f.write(line) + + +def _get_sys_mon(): + return os.access(MONITOR, os.X_OK) + + +def _set_sys_mon(sys_mon): + if sys_mon: + if os.access(MONITOR, os.X_OK): + pass + + else: + if os.path.exists(MONITOR): + os.system('chmod +x %s' % MONITOR) + + else: + with open(MONITOR, 'w') as f: + f.write(MONITOR_SCRIPT) + + os.system('chmod +x %s' % MONITOR) + + else: + if os.access(MONITOR, os.X_OK): + os.system('chmod -x %s' % MONITOR) + + +@additional_config +def boardSeparator(): + return { + 'type': 'separator', + 'section': 'expertSettings' + } + + +@additional_config +def gpuMem(): + return { + 'label': 'GPU Memory', + 'description': 'set the amount of memory reserved for the GPU (choose at least 96MB if you use the CSI camera board)', + 'type': 'number', + 'min': '16', + 'max': '944', + 'section': 'expertSettings', + 'reboot': True, + 'get': _get_board_settings, + 'set': _set_board_settings, + 'get_set_dict': True + } + + +@additional_config +def cameraLed(): + return { + 'label': 'Enable CSI Camera Led', + 'description': 'control the led on the CSI camera board', + 'type': 'bool', + 'section': 'expertSettings', + 'reboot': True, + 'get': _get_board_settings, + 'set': _set_board_settings, + 'get_set_dict': True + } + + +@additional_config +def sysMon(): + return { + 'label': 'Enable System Monitoring', + 'description': 'when this is enabled, system monitoring info will be overlaid on top of the first camera frame', + 'type': 'bool', + 'section': 'expertSettings', + 'reboot': False, + 'get': _get_sys_mon, + 'set': _set_sys_mon + } + diff --git a/board/raspberrypi4/motioneye-modules/streameyectl.py b/board/raspberrypi4/motioneye-modules/streameyectl.py new file mode 100644 index 0000000000..7fd7f34eb4 --- /dev/null +++ b/board/raspberrypi4/motioneye-modules/streameyectl.py @@ -0,0 +1,1220 @@ + +# Copyright (c) 2015 Calin Crisan +# This file is part of motionEyeOS. +# +# motionEyeOS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import config +import hashlib +import logging +import os.path +import re + +from tornado.ioloop import IOLoop + +import settings + +from config import additional_config + + +MOTIONEYE_CONF = '/data/etc/motioneye.conf' +RASPIMJPEG_CONF = '/data/etc/raspimjpeg.conf' +STREAMEYE_CONF = '/data/etc/streameye.conf' + +EXPOSURE_CHOICES = [ + ('off', 'Off'), + ('auto', 'Auto'), + ('night', 'Night'), + ('nightpreview', 'Night Preview'), + ('backlight', 'Backlight'), + ('spotlight', 'Spotlight'), + ('sports', 'Sports'), + ('snow', 'Snow'), + ('beach', 'Beach'), + ('verylong', 'Very Long'), + ('fixedfps', 'Fixed FPS'), + ('antishake', 'Antishake'), + ('fireworks', 'Fireworks') +] + +AWB_CHOICES = [ + ('off', 'Off'), + ('auto', 'Auto'), + ('sunlight', 'Sunlight'), + ('cloudy', 'Cloudy'), + ('shade', 'Shade'), + ('tungsten', 'Tungsten'), + ('fluorescent', 'Fluorescent'), + ('incandescent', 'Incandescent'), + ('flash', 'Flash'), + ('horizon', 'Horizon') +] + +METERING_CHOICES = [ + ('average', 'Average'), + ('spot', 'Spot'), + ('backlit', 'Backlit'), + ('matrix', 'Matrix') +] + +DRC_CHOICES = [ + ('off', 'Off'), + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High') +] + +IMXFX_CHOICES = [ + ('none', 'None'), + ('negative', 'Negative'), + ('solarize', 'Solarize'), + ('sketch', 'Sketch'), + ('denoise', 'Denoise'), + ('emboss', 'Emboss'), + ('oilpaint', 'Oilpaint'), + ('hatch', 'Hatch'), + ('gpen', 'G Pen'), + ('pastel', 'Pastel'), + ('watercolor', 'Water Color'), + ('film', 'Film'), + ('blur', 'Blur'), + ('saturation', 'Saturation'), + ('colorswap', 'Color Swap'), + ('washedout', 'Washed Out'), + ('posterise', 'Posterize'), + ('colorpoint', 'Color Point'), + ('colorbalance', 'Color Balance'), + ('cartoon', 'Cartoon'), + ('deinterlace1', 'Deinterlace 1'), + ('deinterlace2', 'Deinterlace 2') +] + +RESOLUTION_CHOICES = [ + ('320x200', '320x200'), + ('320x240', '320x240'), + ('640x480', '640x480'), + ('800x480', '800x480'), + ('800x600', '800x600'), + ('1024x576', '1024x576'), + ('1024x768', '1024x768'), + ('1280x720', '1280x720'), + ('1280x800', '1280x800'), + ('1280x960', '1280x960'), + ('1280x1024', '1280x1024'), + ('1296x972', '1296x972'), + ('1440x960', '1440x960'), + ('1440x1024', '1440x1024'), + ('1600x1200', '1600x1200'), + ('1640x922', '1640x922'), + ('1640x1232', '1640x1232'), + ('1920x1080', '1920x1080'), + ('2592x1944', '2592x1944'), + ('3280x2464', '3280x2464') +] + +ROTATION_CHOICES = [ + ('0', '0°'), + ('90', '90°'), + ('180', '180°'), + ('270', '270°') +] + +PROTO_CHOICES = [ + ('mjpeg', 'MJPEG'), + ('rtsp', 'RTSP'), +] + +AUTH_CHOICES = [ + ('disabled', 'Disabled'), + ('basic', 'Basic'), +] + +_streameye_enabled = None + + +def _get_streameye_enabled(): + global _streameye_enabled + + if _streameye_enabled is not None: + return _streameye_enabled + + camera_ids = config.get_camera_ids(filter_valid=False) # filter_valid prevents infinte recursion + if len(camera_ids) != 1: + _streameye_enabled = False + return False + + camera_config = config.get_camera(camera_ids[0], as_lines=True) # as_lines prevents infinte recursion + camera_config = config._conf_to_dict(camera_config) + if camera_config.get('@proto') != 'mjpeg': + _streameye_enabled = False + return False + if '127.0.0.1:' not in camera_config.get('@url', ''): + _streameye_enabled = False + return False + + _streameye_enabled = True + return True + + +def _set_streameye_enabled_deferred(enabled): + was_enabled = _get_streameye_enabled() + if enabled and not was_enabled: + io_loop = IOLoop.instance() + io_loop.add_callback(_set_streameye_enabled, True) + + elif not enabled and was_enabled: + io_loop = IOLoop.instance() + io_loop.add_callback(_set_streameye_enabled, False) + + if enabled: + # this will force updating streameye settings whenever the surveillance credentials are changed + streameye_settings = _get_streameye_settings(1) + _set_streameye_settings(1, streameye_settings) + + +def _set_streameye_enabled(enabled): + global _streameye_enabled + + if enabled: + logging.debug('removing all cameras from cache') + config._camera_config_cache = {} + config._camera_ids_cache = [] + + logging.debug('disabling all cameras in motion.conf') + cmd = 'sed -r -i "s/^camera (.*)/#camera \\1/" /data/etc/motion.conf &>/dev/null' + if os.system(cmd): + logging.error('failed to disable cameras in motion.conf') + + logging.debug('renaming camera files') + for name in os.listdir(settings.CONF_PATH): + if re.match('^camera-\d+.conf$', name): + os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name + '.bak')) + + logging.debug('adding simple mjpeg camera') + + streameye_settings = _get_streameye_settings(1) + main_config = config.get_main() + + device_details = { + 'proto': 'mjpeg', + 'host': '127.0.0.1', + 'port': streameye_settings['sePort'], + 'username': '', + 'password': '', + 'scheme': 'http', + 'path': '/' + } + + if streameye_settings['seAuthMode'] == 'basic': + device_details['username'] = main_config['@normal_username'] + device_details['password'] = main_config['@normal_password'] + + _streameye_enabled = True + config._additional_structure_cache = {} + camera_config = config.add_camera(device_details) + + # call set_camera again so that the streamEye-related defaults are saved + config.set_camera(camera_config['@id'], camera_config) + + _set_motioneye_add_remove_cameras(False) + + else: # disabled + logging.debug('removing simple mjpeg camera') + for camera_id in config.get_camera_ids(): + camera_config = config.get_camera(camera_id) + if camera_config.get('@proto') == 'mjpeg': + config.rem_camera(camera_id) + + logging.debug('renaming camera files') + for name in os.listdir(settings.CONF_PATH): + if re.match('^camera-\d+.conf.bak$', name): + os.rename(os.path.join(settings.CONF_PATH, name), os.path.join(settings.CONF_PATH, name[:-4])) + + _streameye_enabled = False + config.invalidate() + + logging.debug('enabling all cameras') + for camera_id in config.get_camera_ids(): + camera_config = config.get_camera(camera_id) + camera_config['@enabled'] = True + config.set_camera(camera_id, camera_config) + + _set_motioneye_add_remove_cameras(True) + + +def _set_motioneye_add_remove_cameras(enabled): + logging.debug('%s motionEye add/remove cameras' % ['disabling', 'enabling'][enabled]) + + lines = [] + found = False + if os.path.exists(MOTIONEYE_CONF): + with open(MOTIONEYE_CONF) as f: + lines = f.readlines() + + for i, line in enumerate(lines): + line = line.strip() + if not line: + continue + + try: + name, _ = line.split(' ', 2) + + except: + continue + + name = name.replace('_', '-') + + if name == 'add-remove-cameras': + lines[i] = 'add-remove-cameras %s' % str(enabled).lower() + found = True + + if not found: + lines.append('add-remove-cameras %s' % str(enabled).lower()) + + with open(MOTIONEYE_CONF, 'w') as f: + for line in lines: + if not line.strip(): + continue + if not line.endswith('\n'): + line += '\n' + f.write(line) + + +def _get_raspimjpeg_settings(camera_id): + s = { + 'preview': False, + 'brightness': 50, + 'contrast': 0, + 'saturation': 0, + 'sharpness': 0, + 'iso': 400, + 'ev': 0, + 'shutter': 0, + 'exposure': 'auto', + 'awb': 'auto', + 'metering': 'average', + 'drc': 'off', + 'vstab': False, + 'denoise': False, + 'imxfx': 'none', + 'width': 640, + 'height': 480, + 'rotation': 0, + 'vflip': False, + 'hflip': False, + 'framerate': 15, + 'quality': 25, + 'bitrate': 1000000, + 'zoomx': 0, + 'zoomy': 0, + 'zoomw': 100, + 'zoomh': 100 + } + + if os.path.exists(RASPIMJPEG_CONF): + logging.debug('reading raspimjpeg settings from %s' % RASPIMJPEG_CONF) + + with open(RASPIMJPEG_CONF) as f: + for line in f: + line = line.strip() + if not line: + continue + + try: + name, value = line.split(' ', 1) + + except: + continue + + name = name.replace('_', '-') + + try: + value = int(value) + + except: + pass + + if value == 'false': + value = False + + elif value == 'true': + value = True + + if name == 'zoom': + try: + parts = value.split(',') + s['zoomx'] = int(float(parts[0]) * 100) + s['zoomy'] = int(float(parts[1]) * 100) + s['zoomw'] = int(float(parts[2]) * 100) + s['zoomh'] = int(float(parts[3]) * 100) + + except: + logging.error('failed to parse zoom setting "%s"' % value) + + continue + + s[name] = value + + s['contrast'] = (s['contrast'] + 100) / 2 + s['saturation'] = (s['saturation'] + 100) / 2 + s['sharpness'] = (s['sharpness'] + 100) / 2 + + s['resolution'] = '%sx%s' % (s.pop('width'), s.pop('height')) + + s = dict(('se' + n[0].upper() + n[1:], v) for (n, v) in s.items()) + + return s + + +def _set_raspimjpeg_settings(camera_id, s): + s = dict((n[2].lower() + n[3:], v) for (n, v) in s.items()) + + s['width'] = int(s['resolution'].split('x')[0]) + s['height'] = int(s.pop('resolution').split('x')[1]) + + s['zoom'] = '%.2f,%.2f,%.2f,%.2f' % ( + s.pop('zoomx') / 100.0, s.pop('zoomy') / 100.0, + s.pop('zoomw') / 100.0, s.pop('zoomh') / 100.0) + + s['contrast'] = s['contrast'] * 2 - 100 + s['saturation'] = s['saturation'] * 2 - 100 + s['sharpness'] = s['sharpness'] * 2 - 100 + + logging.debug('writing raspimjpeg settings to %s' % RASPIMJPEG_CONF) + + lines = [] + for name, value in sorted(s.items(), key=lambda i: i[0]): + if isinstance(value, bool): + value = str(value).lower() + + line = '%s %s\n' % (name, value) + lines.append(line) + + with open(RASPIMJPEG_CONF, 'w') as f: + for line in lines: + f.write(line) + + +def _get_streameye_settings(camera_id): + s = { + 'seProto': 'mjpeg', + 'seAuthMode': 'disabled', + 'sePort': 8081, + 'seRTSPPort': 554 + } + + if os.path.exists(STREAMEYE_CONF): + logging.debug('reading streameye settings from %s' % STREAMEYE_CONF) + + with open(STREAMEYE_CONF) as f: + for line in f: + line = line.strip() + if not line: + continue + + m = re.findall('^PORT="?(\d+)"?', line) + if m: + s['sePort'] = int(m[0]) + continue + + m = re.findall('^RTSP_PORT="?(\d+)"?', line) + if m: + s['seRTSPPort'] = int(m[0]) + continue + + m = re.findall('^AUTH="?(\w+)"?', line) + if m: + s['seAuthMode'] = m[0] + + m = re.findall('^PROTO="?(\w+)"?', line) + if m: + s['seProto'] = m[0] + + return s + + +def _set_streameye_settings(camera_id, s): + s = dict(s) + s.setdefault('sePort', 8081) + s.setdefault('seRTSPPort', 554) + s.setdefault('seAuthMode', 'disabled') + + main_config = config.get_main() + username = main_config['@normal_username'] + password = main_config['@normal_password'] + realm = 'motionEyeOS' + + logging.debug('writing streameye settings to %s' % STREAMEYE_CONF) + + lines = [ + 'PROTO="%s"' % s['seProto'], + 'PORT="%s"' % s['sePort'], + 'RTSP_PORT="%s"' % s['seRTSPPort'], + 'AUTH="%s"' % s['seAuthMode'], + 'CREDENTIALS="%s:%s:%s"' % (username, password, realm) + ] + + with open(STREAMEYE_CONF, 'w') as f: + for line in lines: + f.write(line + '\n') + + if 1 in config.get_camera_ids(): + # a workaround to update the camera username and password + # since we cannot call set_camera() from here + if s['seAuthMode'] == 'basic': + url = 'http://%s:%s@127.0.0.1:%s/' % (username, password, s['sePort']) + + else: + url = 'http://127.0.0.1:%s/' % s['sePort'] + + if 1 in config._camera_config_cache: + logging.debug('updating streaming authentication in config cache') + config._camera_config_cache[1]['@url'] = url + + lines = config.get_camera(1, as_lines=True) + for i, line in enumerate(lines): + if line.startswith('# @url'): + lines[i] = '# @url %s' % url + + config_file = os.path.join(settings.CONF_PATH, config._CAMERA_CONFIG_FILE_NAME % {'id': 1}) + logging.debug('updating streaming authentication in camera config file %s' % config_file) + with open(config_file, 'w') as f: + for line in lines: + f.write(line + '\n') + + logging.debug('restarting streameye') + if os.system('streameye.sh restart'): + logging.error('streameye restart failed') + + +# make streameye-related log files downloadable + +if _get_streameye_enabled(): + def _add_log_handlers(): + import handlers + handlers.LogHandler.LOGS['streameye'] = (os.path.join(settings.LOG_PATH, 'streameye.log'), 'streameye.log') + handlers.LogHandler.LOGS['raspimjpeg'] = (os.path.join(settings.LOG_PATH, 'raspimjpeg.log'), 'raspimjpeg.log') + + # handlers.LogHandler is not yet available + # at the time streameyectl is imported + io_loop = IOLoop.instance() + io_loop.add_callback(_add_log_handlers) + + @additional_config + def streamEyeLog(): + return { + 'type': 'html', + 'section': 'expertSettings', + 'get': lambda: 'streameye.log', + } + + @additional_config + def raspiMjpegLog(): + return { + 'type': 'html', + 'section': 'expertSettings', + 'get': lambda: 'raspimjpeg.log', + } + + +@additional_config +def streamEyeMainSeparator(): + return { + 'type': 'separator', + 'section': 'expertSettings' + } + + +@additional_config +def streamEye(): + return { + 'label': 'Fast Network Camera', + 'description': 'Enabling this option will turn your Raspberry PI into a simple and fast MJPEG network camera, ' + + 'disabling motion detection, media files and all other advanced features (works only with the CSI camera)', + 'type': 'bool', + 'section': 'expertSettings', + 'reboot': True, + 'get': _get_streameye_enabled, + 'set': _set_streameye_enabled_deferred, + } + + +@additional_config +def streamEyeCameraSeparator1(): + return { + 'type': 'separator', + 'section': 'device', + 'camera': True + } + + +@additional_config +def seBrightness(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Brightness', + 'description': 'sets a desired brightness level for this camera', + 'type': 'range', + 'min': 0, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seContrast(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Contrast', + 'description': 'sets a desired contrast level for this camera', + 'type': 'range', + 'min': 0, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seSaturation(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Saturation', + 'description': 'sets a desired saturation level for this camera', + 'type': 'range', + 'min': 0, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seSharpness(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Sharpness', + 'description': 'sets a desired sharpness level for this camera', + 'type': 'range', + 'min': 0, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + + +@additional_config +def streamEyeCameraSeparator2(): + return { + 'type': 'separator', + 'section': 'device', + 'camera': True + } + + +@additional_config +def seResolution(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Video Resolution', + 'description': 'the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)', + 'type': 'choices', + 'choices': RESOLUTION_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seRotation(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Video Rotation', + 'description': 'use this to rotate the captured image, if your camera is not positioned correctly', + 'type': 'choices', + 'choices': ROTATION_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seVflip(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Flip Vertically', + 'description': 'enable this to flip the captured image vertically', + 'type': 'bool', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seHflip(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Flip Horizontally', + 'description': 'enable this to flip the captured image horizontally', + 'type': 'bool', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seFramerate(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Frame Rate', + 'description': 'sets the number of frames captured by the camera every second (higher values produce smoother videos but require more CPU power, larger storage space and bandwidth)', + 'type': 'range', + 'min': 1, + 'max': 30, + 'snap': 0, + 'ticks': "1|5|10|15|20|25|30", + 'decimals': 0, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seQuality(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Image Quality', + 'description': 'sets the JPEG image quality (higher values produce a better image quality but require more storage space and bandwidth)', + 'type': 'range', + 'min': 1, + 'max': 100, + 'snap': 2, + 'ticks': '1|25|50|75|100', + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seBitrate(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Bitrate', + 'description': 'sets the RTSP stream bitrate (higher values produce a better stream quality but require more storage space and bandwidth)', + 'type': 'number', + 'min': 0, + 'max': 100000000, + 'unit': 'bps', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seZoomx(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Zoom X', + 'description': 'sets the horizontal zoom offset', + 'type': 'range', + 'min': 0, + 'max': 80, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seZoomy(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Zoom Y', + 'description': 'sets the vertical zoom offset', + 'type': 'range', + 'min': 0, + 'max': 80, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seZoomw(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Zoom Width', + 'description': 'sets the zoom width', + 'type': 'range', + 'min': 20, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seZoomh(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Zoom Height', + 'description': 'sets the zoom height', + 'type': 'range', + 'min': 20, + 'max': 100, + 'snap': 2, + 'ticksnum': 5, + 'decimals': 0, + 'unit': '%', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def sePreview(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'HDMI Preview', + 'description': 'enable this if you want to see the preview on an HDMI-connected monitor', + 'type': 'bool', + 'section': 'device', + 'camera': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def streamEyeCameraSeparator3(): + return { + 'type': 'separator', + 'section': 'device', + 'camera': True + } + + +@additional_config +def seIso(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'ISO', + 'description': 'sets a desired ISO level for this camera', + 'type': 'range', + 'min': 100, + 'max': 800, + 'snap': 1, + 'ticksnum': 8, + 'decimals': 0, + 'unit': '', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seShutter(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Shutter Speed', + 'description': 'sets a desired shutter speed for this camera', + 'type': 'number', + 'min': 0, + 'max': 6000000, + 'unit': 'microseconds', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def streamEyeCameraSeparator4(): + return { + 'type': 'separator', + 'section': 'device', + 'camera': True + } + + +@additional_config +def seExposure(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Exposure Mode', + 'description': 'sets a desired exposure mode for this camera', + 'type': 'choices', + 'choices': EXPOSURE_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seEv(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Exposure Compensation', + 'description': 'sets a desired exposure compensation for this camera', + 'type': 'range', + 'min': -25, + 'max': 25, + 'snap': 1, + 'ticksnum': 11, + 'decimals': 0, + 'unit': '', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seAwb(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Automatic White Balance', + 'description': 'sets a desired automatic white balance mode for this camera', + 'type': 'choices', + 'choices': AWB_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seMetering(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Metering Mode', + 'description': 'sets a desired metering mode for this camera', + 'type': 'choices', + 'choices': METERING_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seDrc(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Dynamic Range Compensation', + 'description': 'sets a desired dynamic range compensation level for this camera', + 'type': 'choices', + 'choices': DRC_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seVstab(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Video Stabilization', + 'description': 'enables or disables video stabilization for this camera', + 'type': 'bool', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seDenoise(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Denoise', + 'description': 'enables image denoising', + 'type': 'bool', + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seImxfx(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Image Effect', + 'description': 'sets a desired image effect for this camera', + 'type': 'choices', + 'choices': IMXFX_CHOICES, + 'section': 'device', + 'camera': True, + 'required': True, + 'get': _get_raspimjpeg_settings, + 'set': _set_raspimjpeg_settings, + 'get_set_dict': True + } + + +@additional_config +def seProto(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Streaming Protocol', + 'description': 'the desired streaming protocol (keep in mind that RTSP is experimental)', + 'type': 'choices', + 'choices': PROTO_CHOICES, + 'section': 'streaming', + 'camera': True, + 'required': True, + 'get': _get_streameye_settings, + 'set': _set_streameye_settings, + 'get_set_dict': True + } + + +@additional_config +def sePort(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Streaming Port', + 'description': 'sets the TCP port on which the webcam streaming server listens', + 'type': 'number', + 'min': 0, + 'max': 65535, + 'section': 'streaming', + 'camera': True, + 'required': True, + 'depends': ['seProto==mjpeg'], + 'get': _get_streameye_settings, + 'set': _set_streameye_settings, + 'get_set_dict': True + } + + +@additional_config +def seRTSPPort(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Streaming Port', + 'description': 'sets the TCP port on which the webcam streaming server listens', + 'type': 'number', + 'min': 0, + 'max': 65535, + 'section': 'streaming', + 'camera': True, + 'required': True, + 'depends': ['seProto==rtsp'], + 'get': _get_streameye_settings, + 'set': _set_streameye_settings, + 'get_set_dict': True + } + + +@additional_config +def seAuthMode(): + if not _get_streameye_enabled(): + return None + + return { + 'label': 'Authentication Mode', + 'description': 'the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)', + 'type': 'choices', + 'choices': AUTH_CHOICES, + 'section': 'streaming', + 'camera': True, + 'required': True, + 'depends': ['seProto==mjpeg'], + 'get': _get_streameye_settings, + 'set': _set_streameye_settings, + 'get_set_dict': True + } + diff --git a/board/raspberrypi4/overlay/usr/bin/streameye.sh b/board/raspberrypi4/overlay/usr/bin/streameye.sh new file mode 100755 index 0000000000..9f63710c3c --- /dev/null +++ b/board/raspberrypi4/overlay/usr/bin/streameye.sh @@ -0,0 +1,305 @@ +#!/bin/bash + +RASPIMJPEG_CONF=/data/etc/raspimjpeg.conf +RASPIMJPEG_LOG=/var/log/raspimjpeg.log +GSTREAMER_LOG=/var/log/gstreamer.log +MOTIONEYE_CONF=/data/etc/motioneye.conf +STREAMEYE_CONF=/data/etc/streameye.conf +STREAMEYE_LOG=/var/log/streameye.log + + +test -r ${RASPIMJPEG_CONF} || exit 1 +test -r ${STREAMEYE_CONF} || exit 1 + +function watch() { + source ${STREAMEYE_CONF} + count=0 + while true; do + sleep 5 + if [ "${PROTO}" = "rtsp" ]; then + if ! ps aux | grep test-launch | grep -v grep &>/dev/null; then + logger -t streameye -s "not running, respawning" + start + fi + else + if ! ps aux | grep raspimjpeg.py | grep -v grep &>/dev/null; then + logger -t streameye -s "not running, respawning" + start + fi + fi + done +} + +function configure_v4l2_cam() { + video_arg="-d $1" + + horizontal_flip="0" + hflip=$(grep -e ^hflip ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ "${hflip}" = "true" ]; then horizontal_flip="1"; fi + v4l2-ctl ${video_arg} --set-ctrl=horizontal_flip=${horizontal_flip} &>/dev/null + + vertical_flip="0" + vflip=$(grep -e ^vflip ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ "${vflip}" = "true" ]; then vertical_flip="1"; fi + v4l2-ctl ${video_arg} --set-ctrl=vertical_flip=${vertical_flip} &>/dev/null + + rotate=$(grep -e ^rotation ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${rotate}" ]; then rotate="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=rotate=${rotate} &>/dev/null + + brightness=$(grep -e ^brightness ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${brightness}" ]; then brightness="50"; fi + v4l2-ctl ${video_arg} --set-ctrl=brightness=${brightness} &>/dev/null + + contrast=$(grep -e ^contrast ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${contrast}" ]; then contrast="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=contrast=${contrast} &>/dev/null + + saturation=$(grep -e ^saturation ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${saturation}" ]; then saturation="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=saturation=${saturation} &>/dev/null + + sharpness=$(grep -e ^sharpness ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${sharpness}" ]; then sharpness="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=sharpness=${sharpness} &>/dev/null + + vstab=$(grep -e ^vstab ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${vstab}" ]; then vstab="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=image_stabilization=${vstab} &>/dev/null + + shutter=$(grep -e ^shutter ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${shutter}" ]; then shutter="0"; fi + if [ "${#shutter}" -gt 2 ]; then shutter=$(echo ${shutter/%??/}); else shutter="0"; fi + v4l2-ctl ${video_arg} --set-ctrl=exposure_time_absolute=${shutter} &>/dev/null + + iso=$(grep -e ^iso ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${iso}" ]; then iso="0"; fi + if [ "${iso}" -ge 800 ]; then iso="4" + elif [ "${iso}" -ge 400 ]; then iso="3" + elif [ "${iso}" -ge 200 ]; then iso="2" + elif [ "${iso}" -ge 100 ]; then iso="1" + else iso="0" + fi + v4l2-ctl ${video_arg} --set-ctrl=iso_sensitivity=${iso} &>/dev/null + + awb=$(grep -e ^awb ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ "${awb}" = "off" ]; then awb="0" + elif [ "${awb}" = "auto" ]; then awb="1" + elif [ "${awb}" = "sunlight" ]; then awb="6" + elif [ "${awb}" = "cloudy" ]; then awb="8" + elif [ "${awb}" = "shade" ]; then awb="9" + elif [ "${awb}" = "tungsten" ]; then awb="4" + elif [ "${awb}" = "fluorescent" ]; then awb="3" + elif [ "${awb}" = "incandescent" ]; then awb="2" + elif [ "${awb}" = "flash" ]; then awb="7" + elif [ "${awb}" = "horizon" ]; then awb="5" + else awb="1" + fi + v4l2-ctl ${video_arg} --set-ctrl=white_balance_auto_preset=${awb} &>/dev/null + + metering=$(grep -e ^metering ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ "${metering}" = "spot" ]; then metering="2" + elif [ "${metering}" = "center" ]; then metering="1" + else metering="0" + fi + v4l2-ctl ${video_arg} --set-ctrl=exposure_metering_mode=${awb} &>/dev/null + + scene="0" + exposure=$(grep -e ^exposure ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ "${exposure}" = "off" ]; then + exposure="1" + else + if [ "${exposure}" = "night" ]; then scene="8" + elif [ "${exposure}" = "backlight" ]; then scene="1" + elif [ "${exposure}" = "sports" ]; then scene="11" + elif [ "${exposure}" = "snow" ]; then scene="2" + elif [ "${exposure}" = "beach" ]; then scene="2" + elif [ "${exposure}" = "fireworks" ]; then scene="6" + fi + exposure="0" + fi + v4l2-ctl ${video_arg} --set-ctrl=auto_exposure=${exposure} &>/dev/null + v4l2-ctl ${video_arg} --set-ctrl=scene_mode=${scene} &>/dev/null + + ev=$(grep -e ^ev ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${ev}" ]; then ev="0"; fi + ev=$(expr ${ev} + 25) + ev=$(expr ${ev} / 2) + if [ "${ev}" -gt 24 ]; then ev="24"; fi + v4l2-ctl ${video_arg} --set-ctrl=auto_exposure_bias=${ev} &>/dev/null + + video_bitrate=$(grep -e ^bitrate ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + if [ -z "${video_bitrate}" ]; then video_bitrate="1000000"; fi + v4l2-ctl ${video_arg} --set-ctrl=video_bitrate=${video_bitrate} &>/dev/null +} + +function invalid_opt() { + local e match="$1" + shift + for e; do [[ "${e}" == "${match}" ]] && return 1; done + return 0 +} + +function start() { + source ${STREAMEYE_CONF} + streameye_opts="-p ${PORT}" + if [ -n "${CREDENTIALS}" ] && [ "${AUTH}" = "basic" ]; then + streameye_opts="${streameye_opts} -a basic -c ${CREDENTIALS}" + fi + + if [ "${PROTO}" = "rtsp" ]; then + pid=$(ps | grep test-launch | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1) + if [ -n "${pid}" ]; then + return + fi + + RTSP_PORT=${RTSP_PORT:-554} + + iptables -A INPUT -p tcp -s localhost --dport ${RTSP_PORT} -j ACCEPT + iptables -A INPUT -p tcp --dport ${RTSP_PORT} -j DROP + + audio_opts="" + if [ -n "${AUDIO_DEV}" ]; then + audio_bitrate=$(grep -e ^audio_bitrate ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + audio_channels=$(grep -e ^audio_channels ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + audio_extras="" + if [ -n "${audio_bitrate}" ]; then + audio_extras="${audio_extras},rate=${audio_bitrate}" + fi + if [ -n "${audio_channels}" ]; then + audio_extras="${audio_extras},channels=${audio_channels}" + fi + audio_opts="alsasrc device=${AUDIO_DEV} ! audioresample ! audio/x-raw${audio_extras} ! queue ! voaacenc ! rtpmp4gpay pt=97 name=pay1" + fi + video_path="/dev/video0" + if [ -n "${VIDEO_DEV}" ]; then + video_path=${VIDEO_DEV} + fi + if [ -e "${video_path}" ]; then + # Only configure camera if it is a Pi Cam + if v4l2-ctl -d ${video_path} -D | grep -q 'bm2835 mmal'; then + configure_v4l2_cam ${video_path} + fi + width=$(grep -e ^width ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + height=$(grep -e ^height ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + framerate=$(grep -e ^framerate ${RASPIMJPEG_CONF} | cut -d ' ' -f 2) + video_extras="" + if [ -n "${width}" ]; then + video_extras="${video_extras},width=${width}" + fi + if [ -n "${height}" ]; then + video_extras="${video_extras},height=${height}" + fi + if [ -n "${framerate}" ]; then + video_extras="${video_extras},framerate=${framerate}/1" + fi + video_opts="v4l2src device=${video_path} ! video/x-h264${video_extras} ! h264parse ! rtph264pay name=pay0 pt=96" + + if [ -r ${MOTIONEYE_CONF} ] && grep 'log-level debug' ${MOTIONEYE_CONF} >/dev/null; then + streameye_opts="${streameye_opts} -d" + fi + + test-launch -p ${RTSP_PORT} -m h264 "\"( ${video_opts} ${audio_opts} )\"" &>${GSTREAMER_LOG} & + sleep 10 + gst-launch-1.0 -v rtspsrc location=rtsp://127.0.0.1:${RTSP_PORT}/h264 latency=0 drop-on-latency=1 ! rtph264depay ! h264parse ! omxh264dec ! videorate ! video/x-raw,framerate=5/1 ! jpegenc ! filesink location=/dev/stdout | streameye ${streameye_opts} &>${STREAMEYE_LOG} & + sleep 5 + fi + + iptables -D INPUT -p tcp --dport ${RTSP_PORT} -j DROP + iptables -D INPUT -p tcp -s localhost --dport ${RTSP_PORT} -j ACCEPT + + else + pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1) + if [ -n "${pid}" ]; then + return + fi + + valid_opts=("awb" "brightness" "contrast" "denoise" "drc" "ev" "exposure" "framerate" "height" "hflip" "imxfx" "iso" "metering" "preview" "quality" "rotation" "saturation" "sharpness" "shutter" "vflip" "vstab" "width" "zoom") + raspimjpeg_opts="" + while read line; do + key=$(echo ${line} | cut -d ' ' -f 1) + if invalid_opt "${key}" "${valid_opts[@]}"; then + continue + fi + if echo "${line}" | grep false &>/dev/null; then + continue + fi + if echo "${line}" | grep true &>/dev/null; then + line=${key} + fi + raspimjpeg_opts="${raspimjpeg_opts} --${line}" + done < ${RASPIMJPEG_CONF} + + if [ -r ${MOTIONEYE_CONF} ] && grep 'log-level debug' ${MOTIONEYE_CONF} >/dev/null; then + raspimjpeg_opts="${raspimjpeg_opts} -d" + streameye_opts="${streameye_opts} -d" + fi + + raspimjpeg.py ${raspimjpeg_opts} 2>${RASPIMJPEG_LOG} | streameye ${streameye_opts} &>${STREAMEYE_LOG} & + + fi +} + +function stop() { + # stop the streameye background watch process + ps | grep streameye.sh | grep -v $$ | grep -v S94streameye| grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1 | xargs -r kill + + # stop the raspimjpeg process + raspimjpeg_pid=$(ps | grep raspimjpeg.py | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1) + # stop the gst-launch-1.0 process + gst_launch_pid=$(ps | grep gst-launch-1.0 | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1) + # stop the test-launch process + test_launch_pid=$(ps | grep test-launch | grep -v grep | tr -s ' ' | sed -e 's/^\s//' | cut -d ' ' -f 1) + + if [ -n "${raspimjpeg_pid}" ]; then + kill -HUP "${raspimjpeg_pid}" &>/dev/null + count=0 + while kill -0 "${raspimjpeg_pid}" &>/dev/null && [ ${count} -lt 5 ]; do + sleep 1 + count=$((${count} + 1)) + done + kill -KILL "${raspimjpeg_pid}" &>/dev/null || true + fi + + if [ -n "${gst_launch_pid}" ]; then + kill -HUP "${gst_launch_pid}" &>/dev/null + count=0 + while kill -0 "${gst_launch_pid}" &>/dev/null && [ ${count} -lt 5 ]; do + sleep 1 + count=$((${count} + 1)) + done + kill -KILL "${gst_launch_pid}" &>/dev/null || true + fi + + if [ -n "${test_launch_pid}" ]; then + kill -HUP "${test_launch_pid}" &>/dev/null + count=0 + while kill -0 "${test_launch_pid}" &>/dev/null && [ ${count} -lt 5 ]; do + sleep 1 + count=$((${count} + 1)) + done + kill -KILL "${test_launch_pid}" &>/dev/null || true + fi +} + +case "$1" in + start) + start + watch & + ;; + + stop) + stop + ;; + + restart) + stop + start + watch & + ;; + + *) + echo $"Usage: $0 {start|stop|restart}" + exit 1 +esac +