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
+