From 21eb11228074e450395e13b8b71f642fc4b54db6 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 23 Aug 2014 13:45:13 +0300 Subject: [PATCH] major config refactorization; initial support for netcams --- src/config.py | 425 ++++++++++++++++++++++++-------------------- src/handlers.py | 293 +++++++++++++++--------------- src/mediafiles.py | 4 +- src/mjpgclient.py | 3 +- src/motionctl.py | 2 +- src/remote.py | 175 +++++++++--------- src/utils.py | 41 ++++- static/css/main.css | 5 + static/js/main.js | 256 ++++++++++++++++---------- static/js/ui.js | 38 +++- templates/main.html | 7 +- 11 files changed, 718 insertions(+), 531 deletions(-) diff --git a/src/config.py b/src/config.py index 2ea241a..95c723f 100644 --- a/src/config.py +++ b/src/config.py @@ -194,7 +194,7 @@ def has_enabled_cameras(): camera_ids = get_camera_ids() cameras = [get_camera(camera_id) for camera_id in camera_ids] - return bool([c for c in cameras if c['@enabled'] and c['@proto'] == 'v4l2']) + return bool([c for c in cameras if c.get('@enabled') and utils.local_camera(c)]) def get_network_shares(): @@ -254,10 +254,8 @@ def get_camera(camera_id, as_lines=False): camera_config = _conf_to_dict(lines) - camera_config.setdefault('@proto', 'v4l2') - - # determine the enabled status - if camera_config['@proto'] == 'v4l2': + if utils.local_camera(camera_config): + # determine the enabled status main_config = get_main() threads = main_config.get('thread', []) camera_config['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads @@ -292,7 +290,7 @@ def get_camera(camera_id, as_lines=False): if 'gap' in camera_config: camera_config['event_gap'] = camera_config.pop('gap') - _set_default_motion_camera(camera_id, camera_config, False) + _set_default_motion_camera(camera_id, camera_config) _camera_config_cache[camera_id] = dict(camera_config) @@ -302,10 +300,11 @@ def get_camera(camera_id, as_lines=False): def set_camera(camera_id, camera_config): global _camera_config_cache + camera_config = dict(camera_config) camera_config['@id'] = camera_id - _camera_config_cache[camera_id] = dict(camera_config) + _camera_config_cache[camera_id] = camera_config - if camera_config['@proto'] == 'v4l2': + if utils.local_camera(camera_config): old_motion = _is_old_motion() # adapt directives to old configuration, if needed @@ -334,6 +333,8 @@ def set_camera(camera_id, camera_config): camera_config['webcam_localhost'] = camera_config.pop('stream_localhost') if 'event_gap' in camera_config: camera_config['gap'] = camera_config.pop('event_gap') + + camera_config['netcam_tolerant_check'] = True _set_default_motion_camera(camera_id, camera_config, old_motion) @@ -400,66 +401,39 @@ def add_camera(device_details): logging.info('adding new camera with id %(id)s...' % {'id': camera_id}) - # add the default camera config - proto = device_details['proto'] - - data = OrderedDict() - data['@proto'] = proto - data['@enabled'] = device_details.get('enabled', True) - - if proto == 'v4l2': - data['@name'] = 'Camera' + str(camera_id) - data['videodevice'] = device_details['device_uri'] - if 'width' in device_details: - data['width'] = device_details['width'] - data['height'] = device_details['height'] - data['ffmpeg_bps'] = device_details['ffmpeg_bps'] - - if ('root_directory' in device_details) and ('storage_device' in device_details): - if ((device_details['storage_device'] == 'network-share') and settings.SMB_SHARES and - ('network_server' in device_details) and ('network_share_name' in device_details) and - ('network_username' in device_details)): - - mount_point = smbctl.make_mount_point( - device_details['network_server'], device_details['network_share_name'], device_details['network_username']) - - if device_details['root_directory'].startswith('/'): - device_details['root_directory'] = device_details['root_directory'][1:] - data['target_dir'] = os.path.normpath(os.path.join(mount_point, device_details['root_directory'])) - data['@storage_device'] = 'network-share' - - elif device_details['storage_device'].startswith('local-disk'): - target_dev = device_details['storage_device'][10:].replace('-', '/') - mounted_partitions = diskctl.list_mounted_partitions() - partition = mounted_partitions[target_dev] - mount_point = partition['mount_point'] - - if device_details['root_directory'].startswith('/'): - device_details['root_directory'] = device_details['root_directory'][1:] - data['target_dir'] = os.path.normpath(os.path.join(mount_point, device_details['root_directory'])) - data['@storage_device'] = device_details['storage_device'] - - else: - data['target_dir'] = device_details['root_directory'] - data['@storage_device'] = 'custom-path' - - else: # remote - data['@host'] = device_details['host'] - data['@port'] = device_details['port'] - data['@username'] = device_details['username'] - data['@password'] = device_details['password'] - data['@remote_camera_id'] = device_details['remote_camera_id'] - data['@enabled'] = device_details.get('enabled', True) + # prepare a default camera config + camera_config = {'@enabled': True} + if device_details['proto'] == 'v4l2': + _set_default_motion_camera(camera_id, camera_config) + camera_config_ui = camera_dict_to_ui(camera_config) + camera_config_ui.update(device_details) + camera_config = camera_ui_to_dict(camera_config_ui) + + elif device_details['proto'] == 'motioneye': + camera_config['@proto'] = 'motioneye' + camera_config['@host'] = device_details['host'] + camera_config['@port'] = device_details['port'] + camera_config['@uri'] = device_details['uri'] + camera_config['@username'] = device_details['username'] + camera_config['@password'] = device_details['password'] + camera_config['@remote_camera_id'] = device_details['remote_camera_id'] + + else: # assuming netcam + camera_config['netcam_url'] = 'http://dummy' # used only to identify it as a netcam + _set_default_motion_camera(camera_id, camera_config) + camera_config_ui = camera_dict_to_ui(camera_config) + camera_config_ui.update(device_details) + camera_config = camera_ui_to_dict(camera_config_ui) # write the configuration to file - set_camera(camera_id, data) + set_camera(camera_id, camera_config) _camera_ids_cache = None _camera_config_cache = {} - data = get_camera(camera_id) + camera_config = get_camera(camera_id) - return camera_id, data + return camera_id, camera_config def rem_camera(camera_id): @@ -528,23 +502,11 @@ def main_dict_to_ui(data): def camera_ui_to_dict(ui): - if not ui['resolution']: # avoid errors for empty resolution setting - ui['resolution'] = '352x288' - - width = int(ui['resolution'].split('x')[0]) - height = int(ui['resolution'].split('x')[1]) - threshold = int(float(ui['frame_change_threshold']) * width * height / 100) - data = { # device '@name': ui['name'], '@enabled': ui['enabled'], - '@proto': ui['proto'], - 'videodevice': ui['device_uri'], 'lightswitch': int(ui['light_switch_detect']) * 50, - 'auto_brightness': ui['auto_brightness'], - 'width': width, - 'height': height, 'framerate': int(ui['framerate']), 'rotate': int(ui['rotation']), @@ -586,7 +548,6 @@ def camera_ui_to_dict(ui): # motion detection 'text_changes': ui['show_frame_changes'], 'locate_motion_mode': ui['show_frame_changes'], - 'threshold': threshold, 'noise_tune': ui['auto_noise_detect'], 'noise_level': max(1, int(round(int(ui['noise_level']) * 2.55))), 'event_gap': int(ui['event_gap']), @@ -600,35 +561,65 @@ def camera_ui_to_dict(ui): 'on_event_start': '' } - on_event_start = [] - - if 'brightness' in ui: - if int(ui['brightness']) == 50: - data['brightness'] = 0 - - else: - data['brightness'] = max(1, int(round(int(ui['brightness']) * 2.55))) - - if 'contrast' in ui: - if int(ui['contrast']) == 50: - data['contrast'] = 0 + if ui['proto'] == 'v4l2': + # device + data['videodevice'] = ui['uri'] + + # resolution + if not ui['resolution']: + ui['resolution'] = '320x240' + + width = int(ui['resolution'].split('x')[0]) + height = int(ui['resolution'].split('x')[1]) + data['width'] = width + data['height'] = height + + threshold = int(float(ui['frame_change_threshold']) * width * height / 100) + + if 'brightness' in ui: + if int(ui['brightness']) == 50: + data['brightness'] = 0 + + else: + data['brightness'] = max(1, int(round(int(ui['brightness']) * 2.55))) + + if 'contrast' in ui: + if int(ui['contrast']) == 50: + data['contrast'] = 0 + + else: + data['contrast'] = max(1, int(round(int(ui['contrast']) * 2.55))) + + if 'saturation' in ui: + if int(ui['saturation']) == 50: + data['saturation'] = 0 + + else: + data['saturation'] = max(1, int(round(int(ui['saturation']) * 2.55))) - else: - data['contrast'] = max(1, int(round(int(ui['contrast']) * 2.55))) + if 'hue' in ui: + if int(ui['hue']) == 50: + data['hue'] = 0 + + else: + data['hue'] = max(1, int(round(int(ui['hue']) * 2.55))) - if 'saturation' in ui: - if int(ui['saturation']) == 50: - data['saturation'] = 0 - - else: - data['saturation'] = max(1, int(round(int(ui['saturation']) * 2.55))) + else: # assuming http/netcam + # device + data['netcam_url'] = ui['proto'] + '://' + ui['host'] + if ui['port']: + data['netcam_url'] += ':' + ui['port'] - if 'hue' in ui: - if int(ui['hue']) == 50: - data['hue'] = 0 - - else: - data['hue'] = max(1, int(round(int(ui['hue']) * 2.55))) + data['netcam_url'] += ui['uri'] + + if ui['username'] or ui['password']: + data['necam_userpass'] = (ui['username'] or '') + ':' + (ui['password'] or '') + + data['netcam_http'] = '1.1' + + threshold = int(float(ui['frame_change_threshold']) * 640 * 480 / 100) + + data['threshold'] = threshold if (ui['storage_device'] == 'network-share') and settings.SMB_SHARES: mount_point = smbctl.make_mount_point(ui['network_server'], ui['network_share_name'], ui['network_username']) @@ -670,7 +661,7 @@ def camera_ui_to_dict(ui): else: data['text_right'] = ui['custom_right_text'] - if data['width'] > 320: + if ui['proto'] != 'v4l2' or data['width'] > 320: data['text_double'] = True if ui['still_images']: @@ -690,11 +681,29 @@ def camera_ui_to_dict(ui): data['quality'] = max(1, int(ui['image_quality'])) if ui['motion_movies']: - max_val = data['width'] * data['height'] * data['framerate'] / 3 + if ui['proto'] == 'v4l2': + max_val = data['width'] * data['height'] * data['framerate'] / 3 + + else: + max_val = 640 * 480 * data['framerate'] / 3 + max_val = min(max_val, 9999999) data['ffmpeg_bps'] = int(ui['movie_quality']) * max_val / 100 + # working schedule + if ui['working_schedule']: + data['@working_schedule'] = ( + ui['monday_from'] + '-' + ui['monday_to'] + '|' + + ui['tuesday_from'] + '-' + ui['tuesday_to'] + '|' + + ui['wednesday_from'] + '-' + ui['wednesday_to'] + '|' + + ui['thursday_from'] + '-' + ui['thursday_to'] + '|' + + ui['friday_from'] + '-' + ui['friday_to'] + '|' + + ui['saturday_from'] + '-' + ui['saturday_to'] + '|' + + ui['sunday_from'] + '-' + ui['sunday_to']) + + # event start notifications + on_event_start = [] if ui['email_notifications_enabled']: send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py') send_mail_path = os.path.abspath(send_mail_path) @@ -708,7 +717,7 @@ def camera_ui_to_dict(ui): 'password': ui['email_notifications_smtp_password'], 'tls': ui['email_notifications_smtp_tls'], 'to': emails}) - + if ui['web_hook_notifications_enabled']: web_hook_path = os.path.join(settings.PROJECT_PATH, 'webhook.py') web_hook_path = os.path.abspath(web_hook_path) @@ -723,16 +732,6 @@ def camera_ui_to_dict(ui): commands = ui['command_notifications_exec'].split(';') on_event_start += [c.strip() for c in commands] - if ui['working_schedule']: - data['@working_schedule'] = ( - ui['monday_from'] + '-' + ui['monday_to'] + '|' + - ui['tuesday_from'] + '-' + ui['tuesday_to'] + '|' + - ui['wednesday_from'] + '-' + ui['wednesday_to'] + '|' + - ui['thursday_from'] + '-' + ui['thursday_to'] + '|' + - ui['friday_from'] + '-' + ui['friday_to'] + '|' + - ui['saturday_from'] + '-' + ui['saturday_to'] + '|' + - ui['sunday_from'] + '-' + ui['sunday_to']) - if on_event_start: data['on_event_start'] = '; '.join(on_event_start) @@ -740,31 +739,12 @@ def camera_ui_to_dict(ui): def camera_dict_to_ui(data): - usage = utils.get_disk_usage(data['target_dir']) - if usage: - disk_used, disk_total = usage - - else: - disk_used, disk_total = 0, 0 - - resolutions = v4l2ctl.list_resolutions(data['videodevice']) - resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions] - - threshold = data['threshold'] * 100.0 / (data['width'] * data['height']) - ui = { # device 'name': data['@name'], 'enabled': data['@enabled'], 'id': data['@id'], - 'proto': data['@proto'], - 'host': data.get('@host', ''), - 'port': data.get('@port', ''), - 'device_uri': data['videodevice'], 'light_switch_detect': data['lightswitch'] > 0, - 'auto_brightness': data['auto_brightness'], - 'resolution': str(data['width']) + 'x' + str(data['height']), - 'available_resolutions': resolutions, 'framerate': int(data['framerate']), 'rotation': int(data['rotate']), @@ -775,8 +755,8 @@ def camera_dict_to_ui(data): 'network_share_name': data['@network_share_name'], 'network_username': data['@network_username'], 'network_password': data['@network_password'], - 'disk_used': disk_used, - 'disk_total': disk_total, + 'disk_used': 0, + 'disk_total': 0, 'available_disks': diskctl.list_mounted_disks(), # text overlay @@ -810,13 +790,17 @@ def camera_dict_to_ui(data): # motion detection 'show_frame_changes': data['text_changes'] or data['locate_motion_mode'], - 'frame_change_threshold': threshold, 'auto_noise_detect': data['noise_tune'], 'noise_level': int(int(data['noise_level']) / 2.55), 'event_gap': int(data['event_gap']), 'pre_capture': int(data['pre_capture']), 'post_capture': int(data['post_capture']), + # motion notifications + 'email_notifications_enabled': False, + 'web_hook_notifications_enabled': False, + 'command_notifications_enabled': False, + # working schedule 'working_schedule': False, 'monday_from': '09:00', 'monday_to': '17:00', @@ -827,45 +811,89 @@ def camera_dict_to_ui(data): 'saturday_from': '09:00', 'saturday_to': '17:00', 'sunday_from': '09:00', 'sunday_to': '17:00' } - - on_event_start = data.get('on_event_start') or [] - if on_event_start: - on_event_start = [e.strip() for e in on_event_start.split(';')] - - # the brightness & co. keys in the ui dictionary - # indicate the presence of these controls - # we must call v4l2ctl functions to determine the available controls - brightness = v4l2ctl.get_brightness(ui['device_uri']) - if brightness is not None: # has brightness control - if data.get('brightness', 0) != 0: - ui['brightness'] = brightness - + + if utils.net_camera(data): + netcam_url = data.get('netcam_url') + proto, rest = netcam_url.split('://') + parts = rest.split('/', 1) + if len(parts) > 1: + host_port, uri = parts[:2] + else: - ui['brightness'] = 50 + host_port, uri = rest, '' - contrast = v4l2ctl.get_contrast(ui['device_uri']) - if contrast is not None: # has contrast control - if data.get('contrast', 0) != 0: - ui['contrast'] = contrast + parts = host_port.split(':') + if len(parts) > 1: + host, port = parts[:2] else: - ui['contrast'] = 50 + host, port = host_port, '' + + ui['proto'] = proto + ui['host'] = host + ui['port'] = port + ui['uri'] = uri - saturation = v4l2ctl.get_saturation(ui['device_uri']) - if saturation is not None: # has saturation control - if data.get('saturation', 0) != 0: - ui['saturation'] = saturation + userpass = data.get('netcam_userpass') + if userpass: + ui['username'], ui['password'] = userpass.split(':', 1) else: - ui['saturation'] = 50 + ui['username'], ui['password'] = '', '' + + # width & height are not available for netcams, + # we have no other choice but use something like 640x480 as reference + threshold = data['threshold'] * 100.0 / (640 * 480) + + else: # assuming v4l2 + ui['proto'] = 'v4l2' + ui['host'], ui['port'] = None, None + ui['uri'] = data['videodevice'] + ui['username'], ui['password'] = None, None + + # resolutions + resolutions = v4l2ctl.list_resolutions(data['videodevice']) + ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions] + ui['resolution'] = str(data['width']) + 'x' + str(data['height']) + + # the brightness & co. keys in the ui dictionary + # indicate the presence of these controls + # we must call v4l2ctl functions to determine the available controls + brightness = v4l2ctl.get_brightness(ui['uri']) + if brightness is not None: # has brightness control + if data.get('brightness', 0) != 0: + ui['brightness'] = brightness + + else: + ui['brightness'] = 50 + + contrast = v4l2ctl.get_contrast(ui['uri']) + if contrast is not None: # has contrast control + if data.get('contrast', 0) != 0: + ui['contrast'] = contrast + + else: + ui['contrast'] = 50 + + saturation = v4l2ctl.get_saturation(ui['uri']) + if saturation is not None: # has saturation control + if data.get('saturation', 0) != 0: + ui['saturation'] = saturation + + else: + ui['saturation'] = 50 + + hue = v4l2ctl.get_hue(ui['uri']) + if hue is not None: # has hue control + if data.get('hue', 0) != 0: + ui['hue'] = hue + + else: + ui['hue'] = 50 - hue = v4l2ctl.get_hue(ui['device_uri']) - if hue is not None: # has hue control - if data.get('hue', 0) != 0: - ui['hue'] = hue + threshold = data['threshold'] * 100.0 / (data['width'] * data['height']) - else: - ui['hue'] = 50 + ui['frame_change_threshold'] = threshold if (data['@storage_device'] == 'network-share') and settings.SMB_SHARES: mount_point = smbctl.make_mount_point(data['@network_server'], data['@network_share_name'], data['@network_username']) @@ -886,6 +914,11 @@ def camera_dict_to_ui(data): else: ui['root_directory'] = data['target_dir'] + # disk usage + usage = utils.get_disk_usage(data['target_dir']) + if usage: + ui['disk_used'], ui['disk_total'] = usage + text_left = data['text_left'] text_right = data['text_right'] if text_left or text_right: @@ -938,12 +971,35 @@ def camera_dict_to_ui(data): ui['image_quality'] = data['quality'] ffmpeg_bps = data['ffmpeg_bps'] - if ffmpeg_bps is not None: - max_val = data['width'] * data['height'] * data['framerate'] / 3 + if ffmpeg_bps is not None: + if utils.v4l2_camera(data): + max_val = data['width'] * data['height'] * data['framerate'] / 3 + + else: + max_val = 640 * 480 * data['framerate'] / 3 + max_val = min(max_val, 9999999) ui['movie_quality'] = min(100, int(round(ffmpeg_bps * 100.0 / max_val))) + + # working schedule + working_schedule = data['@working_schedule'] + if working_schedule: + days = working_schedule.split('|') + ui['monday_from'], ui['monday_to'] = days[0].split('-') + ui['tuesday_from'], ui['tuesday_to'] = days[1].split('-') + ui['wednesday_from'], ui['wednesday_to'] = days[2].split('-') + ui['thursday_from'], ui['thursday_to'] = days[3].split('-') + ui['friday_from'], ui['friday_to'] = days[4].split('-') + ui['saturday_from'], ui['saturday_to'] = days[5].split('-') + ui['sunday_from'], ui['sunday_to'] = days[6].split('-') + ui['working_schedule'] = True + # event start notifications + on_event_start = data.get('on_event_start') or [] + if on_event_start: + on_event_start = [e.strip() for e in on_event_start.split(';')] + command_notifications = [] for e in on_event_start: if e.count('sendmail.py') and e.count('motion_start'): @@ -975,18 +1031,6 @@ def camera_dict_to_ui(data): ui['command_notifications_enabled'] = True ui['command_notifications_exec'] = '; '.join(command_notifications) - working_schedule = data['@working_schedule'] - if working_schedule: - days = working_schedule.split('|') - ui['monday_from'], ui['monday_to'] = days[0].split('-') - ui['tuesday_from'], ui['tuesday_to'] = days[1].split('-') - ui['wednesday_from'], ui['wednesday_to'] = days[2].split('-') - ui['thursday_from'], ui['thursday_to'] = days[3].split('-') - ui['friday_from'], ui['friday_to'] = days[4].split('-') - ui['saturday_from'], ui['saturday_to'] = days[5].split('-') - ui['sunday_from'], ui['sunday_to'] = days[6].split('-') - ui['working_schedule'] = True - return ui @@ -1186,19 +1230,21 @@ def _set_default_motion(data): data.setdefault('@wifi_key', '') -def _set_default_motion_camera(camera_id, data, old_motion): +def _set_default_motion_camera(camera_id, data, old_motion=False): data.setdefault('@name', 'Camera' + str(camera_id)) data.setdefault('@enabled', False) - data.setdefault('@proto', 'v4l2') - data.setdefault('videodevice', '/dev/video0') + data.setdefault('@id', camera_id) + + if not utils.net_camera(data): + data.setdefault('videodevice', '/dev/video0') + data.setdefault('brightness', 0) + data.setdefault('contrast', 0) + data.setdefault('saturation', 0) + data.setdefault('hue', 0) + data.setdefault('width', 352) + data.setdefault('height', 288) + data.setdefault('lightswitch', 50) - data.setdefault('auto_brightness', False) - data.setdefault('brightness', 0) - data.setdefault('contrast', 0) - data.setdefault('saturation', 0) - data.setdefault('hue', 0) - data.setdefault('width', 352) - data.setdefault('height', 288) data.setdefault('framerate', 2) data.setdefault('rotate', 0) @@ -1222,6 +1268,7 @@ def _set_default_motion_camera(camera_id, data, old_motion): data.setdefault('stream_maxrate', 5) data.setdefault('stream_quality', 85) data.setdefault('stream_motion', False) + data.setdefault('@webcam_resolution', 100) data.setdefault('@webcam_server_resize', False) diff --git a/src/handlers.py b/src/handlers.py index 8f2f926..5f5722f 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -203,11 +203,16 @@ class ConfigHandler(BaseHandler): raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) - if local_config['@proto'] != 'v4l2': - def on_response(remote_ui_config): - if remote_ui_config is None: - return self.finish_json({'error': 'Failed to get remote camera configuration for %(url)s.' % { - 'url': utils.make_camera_url(local_config)}}) + if utils.local_camera(local_config): + ui_config = config.camera_dict_to_ui(local_config) + + self.finish_json(ui_config) + + else: + def on_response(remote_ui_config=None, error=None): + if error: + return self.finish_json({'error': 'Failed to get remote camera configuration for %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(local_config)}, 'msg': error}) for key, value in local_config.items(): remote_ui_config[key.replace('@', '')] = value @@ -216,11 +221,6 @@ class ConfigHandler(BaseHandler): remote.get_config(local_config, on_response) - else: - ui_config = config.camera_dict_to_ui(local_config) - - self.finish_json(ui_config) - else: logging.debug('getting main config') @@ -246,20 +246,13 @@ class ConfigHandler(BaseHandler): raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) - if local_config['@proto'] == 'v4l2': - # overwrite some fields whose values should not be changed this way - ui_config['device_uri'] = local_config['videodevice'] - ui_config['proto'] = 'v4l2' - ui_config['host'] = '' - ui_config['port'] = '' - ui_config.setdefault('enabled', True) - + if utils.local_camera(local_config): local_config = config.camera_ui_to_dict(ui_config) config.set_camera(camera_id, local_config) on_finish(None, True) # (no error, motion needs restart) - else: + else: # remote camera # update the camera locally local_config['@enabled'] = ui_config['enabled'] config.set_camera(camera_id, local_config) @@ -267,15 +260,11 @@ class ConfigHandler(BaseHandler): # when the local_config supplied has only the enabled state, # the camera was probably disabled due to errors - if ui_config.has_key('device_uri'): - # delete some fields that should not get to the remote side as they are - del ui_config['device_uri'] - del ui_config['proto'] - del ui_config['host'] - del ui_config['port'] + if ui_config.has_key('name'): + # never disable a remote camera on the remote host itself del ui_config['enabled'] - def on_finish_wrapper(error): + def on_finish_wrapper(error=None): return on_finish(error, False) remote.set_config(local_config, ui_config, on_finish_wrapper) @@ -400,7 +389,7 @@ class ConfigHandler(BaseHandler): raise camera_config = config.get_camera(camera_id) - if camera_config['@proto'] == 'v4l2': + if utils.v4l2_camera(camera_config): device = camera_config['videodevice'] if 'brightness' in controls: @@ -429,32 +418,46 @@ class ConfigHandler(BaseHandler): self.finish_json({}) - else: - def on_response(response): - if response is None: - self.finish_json({'error': True}) + elif utils.remote_camera(camera_config): + def on_response(error=None): + if error: + self.finish_json({'error': error}) else: self.finish_json() remote.set_preview(camera_config, controls, on_response) + + else: + self.finish_json({'error': True}) @BaseHandler.auth() def list_cameras(self): logging.debug('listing cameras') - - if 'host' in self.get_data(): # remote listing - def on_response(cameras): - if cameras is None: - self.finish_json({'error': 'Failed to list remote cameras.'}) + + type = self.get_data().get('type') + if type == 'motioneye': # remote listing + def on_response(cameras=None, error=None): + if error: + self.finish_json({'error': error}) else: cameras = [c for c in cameras if c.get('enabled')] self.finish_json({'cameras': cameras}) remote.list_cameras(self.get_data(), on_response) + + elif type == 'netcam': + def on_response(cameras=None, error=None): + if error: + self.finish_json({'error': error}) + + else: + self.finish_json({'cameras': cameras}) + + utils.test_netcam_url(self.get_data(), on_response) - else: # local listing + else: # assuming local v4l2 listing cameras = [] camera_ids = config.get_camera_ids() if not config.get_main().get('@enabled'): @@ -467,11 +470,11 @@ class ConfigHandler(BaseHandler): self.finish_json({'cameras': cameras}) def on_response_builder(camera_id, local_config): - def on_response(remote_ui_config): - if remote_ui_config is None: + def on_response(remote_ui_config=None, error=None): + if error: cameras.append({ 'id': camera_id, - 'name': '<' + utils.make_camera_url(local_config) + '>', + 'name': '<' + remote.make_camera_url(local_config) + '>', 'enabled': False, 'streaming_framerate': 1, 'framerate': 1 @@ -496,13 +499,13 @@ class ConfigHandler(BaseHandler): for camera_id in camera_ids: local_config = config.get_camera(camera_id) - if local_config['@proto'] == 'v4l2': + if utils.local_camera(local_config): ui_config = config.camera_dict_to_ui(local_config) cameras.append(ui_config) check_finished() else: # remote camera - if local_config['@enabled']: + if local_config.get('@enabled'): remote.get_config(local_config, on_response_builder(camera_id, local_config)) else: # don't try to reach the remote of the camera is disabled @@ -518,10 +521,10 @@ class ConfigHandler(BaseHandler): configured_devices = {} for camera_id in config.get_camera_ids(): data = config.get_camera(camera_id) - if data['@proto'] == 'v4l2': + if utils.v4l2_camera(data): configured_devices[data['videodevice']] = True - devices = [{'device_uri': d[0], 'name': d[1], 'configured': d[0] in configured_devices} + devices = [{'uri': d[0], 'name': d[1], 'configured': d[0] in configured_devices} for d in v4l2ctl.list_devices()] self.finish_json({'devices': devices}) @@ -541,23 +544,22 @@ class ConfigHandler(BaseHandler): proto = device_details['proto'] if proto == 'v4l2': # find a suitable resolution - for (w, h) in v4l2ctl.list_resolutions(device_details['device_uri']): + for (w, h) in v4l2ctl.list_resolutions(device_details['uri']): if w > 300: - device_details['width'] = w - device_details['height'] = h - # compute the ffmpeg bps - - max_val = w * h * 2 / 3 - max_val = min(max_val, 9999999) - val = max_val * 75 / 100 - device_details['ffmpeg_bps'] = val - + device_details.setdefault('resolution', str(w) + 'x' + str(h)) break + + else: + # adjust uri format + if device_details['uri'] and device_details['uri'].startswith('/'): + device_details['uri'] = '/' + device_details['uri'] + while device_details['uri'] and device_details['uri'].endswith('/'): + device_details['uri'] = device_details['uri'][:-1] camera_id, camera_config = config.add_camera(device_details) camera_config['@id'] = camera_id - if proto == 'v4l2': + if utils.local_camera(camera_config): motionctl.stop() if settings.SMB_SHARES: @@ -573,10 +575,10 @@ class ConfigHandler(BaseHandler): self.finish_json(ui_config) - else: - def on_response(remote_ui_config): - if remote_ui_config is None: - self.finish_json({'error': True}) + else: # remote camera + def on_response(remote_ui_config=None, error=None): + if error: + self.finish_json({'error': error}) for key, value in camera_config.items(): remote_ui_config[key.replace('@', '')] = value @@ -589,7 +591,7 @@ class ConfigHandler(BaseHandler): def rem_camera(self, camera_id): logging.debug('removing camera %(id)s' % {'id': camera_id}) - local = config.get_camera(camera_id).get('@proto') == 'v4l2' + local = utils.local_camera(config.get_camera(camera_id)) config.rem_camera(camera_id) if local: @@ -642,7 +644,7 @@ class PictureHandler(BaseHandler): return self.try_finish(picture) camera_config = config.get_camera(camera_id) - if camera_config['@proto'] == 'v4l2': + if utils.local_camera(camera_config): picture = mediafiles.get_current_picture(camera_config, width=width, height=height) @@ -652,8 +654,8 @@ class PictureHandler(BaseHandler): self.try_finish(picture) - else: - def on_response(picture): + else: # remote camera + def on_response(picture=None, error=None): if sequence and picture: mediafiles.set_picture_cache(camera_id, sequence, width, picture) @@ -669,17 +671,7 @@ class PictureHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(remote_list): - if remote_list is None: - return self.finish_json({'error': 'Failed to get picture list for %(url)s.' % { - 'url': utils.make_camera_url(camera_config)}}) - - self.finish_json(remote_list) - - remote.list_media(camera_config, on_response, media_type='picture', prefix=self.get_argument('prefix', None)) - - else: + if utils.local_camera(camera_config): def on_media_list(media_list): if media_list is None: return self.finish_json({'error': 'Failed to get pictures list.'}) @@ -692,6 +684,16 @@ class PictureHandler(BaseHandler): mediafiles.list_media(camera_config, media_type='picture', callback=on_media_list, prefix=self.get_argument('prefix', None)) + else: # remote camera + def on_response(remote_list=None, error=None): + if error: + return self.finish_json({'error': 'Failed to get picture list for %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config)}, 'msg': error}) + + self.finish_json(remote_list) + + remote.list_media(camera_config, on_response, media_type='picture', prefix=self.get_argument('prefix', None)) + @BaseHandler.auth() def frame(self, camera_id): camera_config = config.get_camera(camera_id) @@ -709,21 +711,7 @@ class PictureHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(response): - if response is None: - return self.finish_json({'error': 'Failed to download picture from %(url)s.' % { - 'url': utils.make_camera_url(camera_config)}}) - - pretty_filename = os.path.basename(filename) # no camera name available w/o additional request - self.set_header('Content-Type', 'image/jpeg') - self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';') - - self.finish(response) - - remote.get_media_content(camera_config, on_response, filename=filename, media_type='picture') - - else: + if utils.local_camera(camera_config): content = mediafiles.get_media_content(camera_config, filename, 'picture') pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename) @@ -731,7 +719,20 @@ class PictureHandler(BaseHandler): self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';') self.finish(content) + + else: # remote camera + def on_response(response=None, error=None): + if error: + return self.finish_json({'error': 'Failed to download picture from %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config)}, 'msg': error}) + + pretty_filename = os.path.basename(filename) # no camera name available w/o additional request + self.set_header('Content-Type', 'image/jpeg') + self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';') + + self.finish(response) + remote.get_media_content(camera_config, on_response, filename=filename, media_type='picture') @BaseHandler.auth() def preview(self, camera_id, filename): @@ -742,22 +743,7 @@ class PictureHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(content): - if content: - self.set_header('Content-Type', 'image/jpeg') - - else: - self.set_header('Content-Type', 'image/svg+xml') - content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() - - self.finish(content) - - remote.get_media_preview(camera_config, on_response, filename=filename, media_type='picture', - width=self.get_argument('width', None), - height=self.get_argument('height', None)) - - else: + if utils.local_camera(camera_config): content = mediafiles.get_media_preview(camera_config, filename, 'picture', width=self.get_argument('width', None), height=self.get_argument('height', None)) @@ -770,6 +756,21 @@ class PictureHandler(BaseHandler): content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() self.finish(content) + + else: + def on_response(content=None, error=None): + if content: + self.set_header('Content-Type', 'image/jpeg') + + else: + self.set_header('Content-Type', 'image/svg+xml') + content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() + + self.finish(content) + + remote.get_media_preview(camera_config, on_response, filename=filename, media_type='picture', + width=self.get_argument('width', None), + height=self.get_argument('height', None)) def try_finish(self, content): try: @@ -807,17 +808,7 @@ class MovieHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(remote_list): - if remote_list is None: - return self.finish_json({'error': 'Failed to get movie list for %(url)s.' % { - 'url': utils.make_camera_url(camera_config)}}) - - self.finish_json(remote_list) - - remote.list_media(camera_config, on_response, media_type='movie', prefix=self.get_argument('prefix', None)) - - else: + if utils.local_camera(camera_config): def on_media_list(media_list): if media_list is None: return self.finish_json({'error': 'Failed to get movies list.'}) @@ -829,7 +820,17 @@ class MovieHandler(BaseHandler): mediafiles.list_media(camera_config, media_type='movie', callback=on_media_list, prefix=self.get_argument('prefix', None)) + + else: + def on_response(remote_list=None, error=None): + if error: + return self.finish_json({'error': 'Failed to get movie list for %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config)}, 'msg': error}) + + self.finish_json(remote_list) + remote.list_media(camera_config, on_response, media_type='movie', prefix=self.get_argument('prefix', None)) + @BaseHandler.auth() def download(self, camera_id, filename): logging.debug('downloading movie %(filename)s of camera %(id)s' % { @@ -839,11 +840,20 @@ class MovieHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(response): - if response is None: - return self.finish_json({'error': 'Failed to download movie from %(url)s.' % { - 'url': utils.make_camera_url(camera_config)}}) + if utils.local_camera(camera_config): + content = mediafiles.get_media_content(camera_config, filename, 'movie') + + pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename) + self.set_header('Content-Type', 'video/mpeg') + self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';') + + self.finish(content) + + else: + def on_response(response=None, error=None): + if error: + return self.finish_json({'error': 'Failed to download movie from %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config)}, 'msg': error}) pretty_filename = os.path.basename(filename) # no camera name available w/o additional request self.set_header('Content-Type', 'video/mpeg') @@ -852,15 +862,6 @@ class MovieHandler(BaseHandler): self.finish(response) remote.get_media_content(camera_config, on_response, filename=filename, media_type='movie') - - else: - content = mediafiles.get_media_content(camera_config, filename, 'movie') - - pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename) - self.set_header('Content-Type', 'video/mpeg') - self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';') - - self.finish(content) @BaseHandler.auth() def preview(self, camera_id, filename): @@ -871,22 +872,7 @@ class MovieHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) - if camera_config['@proto'] != 'v4l2': - def on_response(content): - if content: - self.set_header('Content-Type', 'image/jpeg') - - else: - self.set_header('Content-Type', 'image/svg+xml') - content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() - - self.finish(content) - - remote.get_media_preview(camera_config, on_response, filename=filename, media_type='movie', - width=self.get_argument('width', None), - height=self.get_argument('height', None)) - - else: + if utils.local_camera(camera_config): content = mediafiles.get_media_preview(camera_config, filename, 'movie', width=self.get_argument('width', None), height=self.get_argument('height', None)) @@ -899,6 +885,21 @@ class MovieHandler(BaseHandler): content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() self.finish(content) + + else: + def on_response(content=None, error=None): + if content: + self.set_header('Content-Type', 'image/jpeg') + + else: + self.set_header('Content-Type', 'image/svg+xml') + content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read() + + self.finish(content) + + remote.get_media_preview(camera_config, on_response, filename=filename, media_type='movie', + width=self.get_argument('width', None), + height=self.get_argument('height', None)) class UpdateHandler(BaseHandler): diff --git a/src/mediafiles.py b/src/mediafiles.py index 1b09d62..b3874b5 100644 --- a/src/mediafiles.py +++ b/src/mediafiles.py @@ -129,7 +129,7 @@ def cleanup_media(media_type): for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if camera_config.get('@proto') != 'v4l2': + if not utils.local_camera(camera_config): continue preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0) @@ -203,7 +203,7 @@ def make_next_movie_preview(): count = 0 for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if camera_config.get('@proto') != 'v4l2': + if not utils.local_camera(camera_config): continue target_dir = camera_config['target_dir'] diff --git a/src/mjpgclient.py b/src/mjpgclient.py index 686be37..2c57ec7 100644 --- a/src/mjpgclient.py +++ b/src/mjpgclient.py @@ -25,6 +25,7 @@ from tornado import iostream, ioloop import config import motionctl import settings +import utils class MjpgClient(iostream.IOStream): @@ -188,7 +189,7 @@ def get_jpg(camera_id): 'camera_id': camera_id}) camera_config = config.get_camera(camera_id) - if not camera_config['@enabled'] or camera_config['@proto'] != 'v4l2': + if not camera_config['@enabled'] or not utils.local_camera(camera_config): logging.error('could not start mjpg client for camera id %(camera_id)s: not enabled or not local' % { 'camera_id': camera_id}) diff --git a/src/motionctl.py b/src/motionctl.py index f29e23b..aab9a5b 100644 --- a/src/motionctl.py +++ b/src/motionctl.py @@ -135,7 +135,7 @@ def stop(): raise Exception('could not terminate the motion process') except OSError as e: - if e.errno != errno.ECHILD: + if e.errno not in (errno.ESRCH, errno.ECHILD): raise diff --git a/src/remote.py b/src/remote.py index e766816..a85f8a4 100644 --- a/src/remote.py +++ b/src/remote.py @@ -24,11 +24,11 @@ import settings def _make_request(host, port, username, password, uri, method='GET', data=None, query=None, timeout=None): - url = '%(scheme)s://%(host)s:%(port)s%(uri)s' % { + url = '%(scheme)s://%(host)s%(port)s%(uri)s' % { 'scheme': 'http', 'host': host, - 'port': port, - 'uri': uri} + 'port': ':' + str(port) if port else '', + 'uri': uri or ''} if query: url += '?' + '&'.join([(n + '=' + v) for (n, v) in query.iteritems()]) @@ -42,8 +42,27 @@ def _make_request(host, port, username, password, uri, method='GET', data=None, return request -def make_camera_uri(camera_id): - return '/config/%(camera_id)s' % {'camera_id': camera_id} +def make_camera_url(local_config, camera=True): + host = local_config.get('@host', local_config.get('host')) + port = local_config.get('@port', local_config.get('port')) + username = local_config.get('@username', local_config.get('username')) + uri = local_config.get('@uri', local_config.get('uri')) or '' + + url = 'motioneye://' + username + '@' + host + if port: + url += ':' + str(port) + + if uri: + url += uri + + if camera: + if camera is True: + url += '/config/' + str(local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))) + + else: + url += '/config/' + str(camera) + + return url def list_cameras(local_config, callback): @@ -51,32 +70,30 @@ def list_cameras(local_config, callback): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' - logging.debug('listing remote cameras on %(host)s:%(port)s' % { - 'host': host, - 'port': port}) + logging.debug('listing remote cameras on %(url)s' % { + 'url': make_camera_url(local_config, camera=False)}) - request = _make_request(host, port, username, password, '/config/list/') + request = _make_request(host, port, username, password, uri + '/config/list/') def on_response(response): if response.error: - logging.error('failed to list remote cameras on %(host)s:%(port)s: %(msg)s' % { - 'host': host, - 'port': port, + logging.error('failed to list remote cameras on %(url)s: %(msg)s' % { + 'url': make_camera_url(local_config, camera=False), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) try: response = json.loads(response.body) except Exception as e: - logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % { - 'host': host, - 'port': port, + logging.error('failed to decode json answer from %(url)s: %(msg)s' % { + 'url': make_camera_url(local_config, camera=False), 'msg': unicode(e)}) - return callback(None) + return callback(error=unicode(e)) return callback(response['cameras']) @@ -89,39 +106,36 @@ def get_config(local_config, callback): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('getting config for remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('getting config for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) - request = _make_request(host, port, username, password, '/config/%(id)s/get/' % {'id': camera_id}) + request = _make_request(host, port, username, password, uri + '/config/%(id)s/get/' % {'id': camera_id}) def on_response(response): if response.error: - logging.error('failed to get config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to get config for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) try: response = json.loads(response.body) except Exception as e: - logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % { - 'host': host, - 'port': port, + logging.error('failed to decode json answer from %(url)s: %(msg)s' % { + 'url': make_camera_url(local_config), 'msg': unicode(e)}) - return callback(None) + return callback(error=unicode(e)) response['host'] = host response['port'] = port - response['device_uri'] = make_camera_uri(camera_id) callback(response) @@ -134,28 +148,27 @@ def set_config(local_config, ui_config, callback): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('setting config for remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('setting config for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) ui_config = json.dumps(ui_config) - request = _make_request(host, port, username, password, '/config/%(id)s/set/' % {'id': camera_id}, method='POST', data=ui_config) + request = _make_request(host, port, username, password, uri + '/config/%(id)s/set/' % {'id': camera_id}, method='POST', data=ui_config) def on_response(response): if response.error: - logging.error('failed to set config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to set config for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(response.error) + return callback(error=unicode(response.error)) - callback(None) + callback() http_client = AsyncHTTPClient() http_client.fetch(request, on_response) @@ -166,28 +179,27 @@ def set_preview(local_config, controls, callback): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('setting preview for remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('setting preview for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) data = json.dumps(controls) - request = _make_request(host, port, username, password, '/config/%(id)s/set_preview/' % {'id': camera_id}, method='POST', data=data) + request = _make_request(host, port, username, password, uri + '/config/%(id)s/set_preview/' % {'id': camera_id}, method='POST', data=data) def on_response(response): if response.error: - logging.error('failed to set preview for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to set preview for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) - callback('') + callback() http_client = AsyncHTTPClient() http_client.fetch(request, on_response) @@ -198,12 +210,12 @@ def get_current_picture(local_config, callback, width, height): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('getting current picture for remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('getting current picture for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) query = {} @@ -213,17 +225,16 @@ def get_current_picture(local_config, callback, width, height): if height: query['height'] = str(height) - request = _make_request(host, port, username, password, '/picture/%(id)s/current/' % {'id': camera_id}, query=query) + request = _make_request(host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query) def on_response(response): if response.error: - logging.error('failed to get current picture for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to get current picture for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) callback(response.body) @@ -236,41 +247,39 @@ def list_media(local_config, callback, media_type, prefix=None): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('getting media list for remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('getting media list for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) query = {} if prefix is not None: query['prefix'] = prefix # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list - request = _make_request(host, port, username, password, '/%(media_type)s/%(id)s/list/' % { + request = _make_request(host, port, username, password, uri + '/%(media_type)s/%(id)s/list/' % { 'id': camera_id, 'media_type': media_type}, query=query, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: - logging.error('failed to get media list for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to get media list for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) try: response = json.loads(response.body) except Exception as e: - logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % { - 'host': host, - 'port': port, + logging.error('failed to decode json answer from %(url)s: %(msg)s' % { + 'url': make_camera_url(local_config), 'msg': unicode(e)}) - return callback(None) + return callback(error=unicode(e)) return callback(response) @@ -283,15 +292,15 @@ def get_media_content(local_config, callback, filename, media_type): port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('downloading file %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('downloading file %(filename)s of remote camera %(id)s on %(url)s' % { 'filename': filename, 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) - uri = '/%(media_type)s/%(id)s/download/%(filename)s' % { + uri += '/%(media_type)s/%(id)s/download/%(filename)s' % { 'media_type': media_type, 'id': camera_id, 'filename': filename} @@ -301,14 +310,13 @@ def get_media_content(local_config, callback, filename, media_type): def on_response(response): if response.error: - logging.error('failed to download file %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to download file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'filename': filename, 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) return callback(response.body) @@ -321,15 +329,15 @@ def get_media_preview(local_config, callback, filename, media_type, width, heigh port = local_config.get('@port', local_config.get('port')) username = local_config.get('@username', local_config.get('username')) password = local_config.get('@password', local_config.get('password')) + uri = local_config.get('@uri', local_config.get('uri')) or '' camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id')) - logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % { + logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(url)s' % { 'filename': filename, 'id': camera_id, - 'host': host, - 'port': port}) + 'url': make_camera_url(local_config)}) - uri = '/%(media_type)s/%(id)s/preview/%(filename)s' % { + uri += '/%(media_type)s/%(id)s/preview/%(filename)s' % { 'media_type': media_type, 'id': camera_id, 'filename': filename} @@ -346,14 +354,13 @@ def get_media_preview(local_config, callback, filename, media_type, width, heigh def on_response(response): if response.error: - logging.error('failed to get file preview for %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + logging.error('failed to get file preview for %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'filename': filename, 'id': camera_id, - 'host': host, - 'port': port, + 'url': make_camera_url(local_config), 'msg': unicode(response.error)}) - return callback(None) + return callback(error=unicode(response.error)) return callback(response.body) diff --git a/src/utils.py b/src/utils.py index 32dca30..7c76f03 100644 --- a/src/utils.py +++ b/src/utils.py @@ -19,8 +19,6 @@ import datetime import logging import os -import remote - def pretty_date_time(date_time, tzinfo=None): if date_time is None: @@ -210,10 +208,37 @@ def get_disk_usage(path): return (used_size, total_size) -def make_camera_url(config): - proto = config.get('proto', config.get('@proto', '')) - host = config.get('host', config.get('@host', '')) - port = config.get('port', config.get('@port', '')) - device_uri = config.get('device_uri', config.get('videodevice', remote.make_camera_uri(config.get('@remote_camera_id')))) +def local_camera(config): + return bool(config.get('videodevice') or config.get('netcam_url')) + + +def remote_camera(config): + return config.get('@proto') == 'motioneye' + + +def v4l2_camera(config): + return bool(config.get('videodevice')) + + +def net_camera(config): + return bool(config.get('netcam_url')) + + +def test_netcam_url(data, callback): + url = '%(proto)s://%(host)s%(port)s%(uri)s' % { + 'proto': data['proto'], + 'host': data['host'], + 'port': ':' + str(data['port']) if data['port'] else '', + 'uri': data['uri'] or ''} - return proto + '://' + host + (':' + str(port) if port else '') + device_uri + logging.debug('testing netcam at %s' % url) + + import time + time.sleep(1) + + username = data['username'] + password = data['password'] + + # TODO implement me + #callback(error='General failure') + callback([{'id': 1, 'name': 'Network Camera'}]) diff --git a/static/css/main.css b/static/css/main.css index 67c2262..cf84b41 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -425,6 +425,11 @@ table.add-camera-dialog input[type=password] { width: 17em; } +span#cameraMsgLabel { + color: red; + font-size: 0.7em; +} + div.media-dialog { } diff --git a/static/js/main.js b/static/js/main.js index c126853..9f43849 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -159,6 +159,19 @@ String.prototype.replaceAll = String.prototype.replaceAll || function (oldStr, n return s.toString(); }; +function makeDeviceUrl(dict) { + switch (dict.proto) { + case 'v4l2': + return dict.proto + '://' + dict.uri; + + case 'motioneye': + return dict.proto + '://' + dict.host + (dict.port ? ':' + dict.port : '') + dict.uri + '/config/' + dict.remote_camera_id; + + default: /* assuming netcam */ + return dict.proto + '://' + dict.host + (dict.port ? ':' + dict.port : '') + dict.uri; + } +} + /* UI initialization */ @@ -251,7 +264,6 @@ function initUI() { $('#showAdvancedSwitch').change(updateConfigUi); $('#wifiSwitch').change(updateConfigUi); $('#storageDeviceSelect').change(updateConfigUi); - $('#autoBrightnessSwitch').change(updateConfigUi); $('#resolutionSelect').change(updateConfigUi); $('#leftTextSelect').change(updateConfigUi); $('#rightTextSelect').change(updateConfigUi); @@ -404,6 +416,26 @@ function updateConfigUi() { $('tr.advanced-setting, div.advanced-setting, table.advanced-setting').each(markHide); } + /* video device */ + if ($('#brightnessSlider').val() == '') { + $('#brightnessSlider').parents('tr:eq(0)').each(markHide); + } + if ($('#contrastSlider').val() == '') { + $('#contrastSlider').parents('tr:eq(0)').each(markHide); + } + if ($('#saturationSlider').val() == '') { + $('#saturationSlider').parents('tr:eq(0)').each(markHide); + } + if ($('#hueSlider').val() == '') { + $('#hueSlider').parents('tr:eq(0)').each(markHide); + } + if ($('#contrastSlider').val() == '') { + $('#contrastSlider').parents('tr:eq(0)').each(markHide); + } + if ($('#resolutionSelect')[0].selectedIndex == -1) { + $('#resolutionSelect').parents('tr:eq(0)').each(markHide); + } + /* storage device */ if ($('#storageDeviceSelect').val() !== 'network-share') { $('#networkServerEntry').parents('tr:eq(0)').each(markHide); @@ -412,11 +444,6 @@ function updateConfigUi() { $('#networkShareNameEntry').parents('tr:eq(0)').each(markHide); } - /* auto brightness */ - if ($('#autoBrightnessSwitch').get(0).checked) { - $('#brightnessSlider').parents('tr:eq(0)').each(markHide); - } - /* text */ if ($('#leftTextSelect').val() !== 'custom-text') { $('#leftTextEntry').parents('tr:eq(0)').each(markHide); @@ -584,42 +611,19 @@ function cameraUi2Dict() { }; } - var deviceUrl = $('#deviceEntry').val(); - var proto = ''; - var hostPort = ''; - var deviceUri = ''; - var host = ''; - var port = ''; - - var parts; - if (deviceUrl) { - parts = deviceUrl.split('://'); - proto = parts[0]; - if (parts.length > 1) { - parts = parts[1].split('/'); - hostPort = parts[0]; - deviceUri = '/' + parts.slice(1).join('/'); - parts = hostPort.split(':'); - host = parts[0]; - if (parts.length > 1) { - port = parts[1] || ''; - } - } - } - var dict = { /* video device */ 'enabled': $('#videoDeviceSwitch')[0].checked, 'name': $('#deviceNameEntry').val(), - 'proto': proto, - 'host': host, - 'port': port, - 'device_uri': deviceUri, 'light_switch_detect': $('#lightSwitchDetectSwitch')[0].checked, - 'auto_brightness': $('#autoBrightnessSwitch')[0].checked, - 'resolution': $('#resolutionSelect').val(), 'rotation': $('#rotationSelect').val(), 'framerate': $('#framerateSlider').val(), + 'proto': $('#deviceEntry')[0].proto, + 'host': $('#deviceEntry')[0].host, + 'port': $('#deviceEntry')[0].port, + 'uri': $('#deviceEntry')[0].uri, + 'username': $('#deviceEntry')[0].username, + 'password': $('#deviceEntry')[0].password, /* file storage */ 'storage_device': $('#storageDeviceSelect').val(), @@ -700,6 +704,10 @@ function cameraUi2Dict() { 'sunday_to': $('#sundayTo').val(), }; + if ($('#resolutionSelect')[0].selectedIndex != -1) { + dict.resolution = $('#resolutionSelect').val(); + } + if ($('#brightnessSlider').val() !== '') { dict.brightness = $('#brightnessSlider').val(); } @@ -736,14 +744,42 @@ function dict2CameraUi(dict) { /* video device */ $('#videoDeviceSwitch')[0].checked = dict['enabled']; $('#deviceNameEntry').val(dict['name']); - $('#deviceEntry').val(dict['proto'] + '://' + dict['host'] + (dict['port'] ? ':' + dict['port'] : '') + dict['device_uri']); + $('#deviceEntry').val(makeDeviceUrl(dict)); + $('#deviceEntry')[0].proto = dict['proto']; + $('#deviceEntry')[0].host = dict['host']; + $('#deviceEntry')[0].port = dict['port']; + $('#deviceEntry')[0].uri= dict['uri']; + $('#deviceEntry')[0].username = dict['username']; + $('#deviceEntry')[0].password = dict['password']; $('#lightSwitchDetectSwitch')[0].checked = dict['light_switch_detect']; - $('#autoBrightnessSwitch')[0].checked = dict['auto_brightness']; - $('#brightnessSlider').val(dict['brightness']); - $('#contrastSlider').val(dict['contrast']); - $('#saturationSlider').val(dict['saturation']); - $('#hueSlider').val(dict['hue']); + if (dict['brightness'] != null) { + $('#brightnessSlider').val(dict['brightness']); + } + else { + $('#brightnessSlider').val(''); + } + + if (dict['contrast'] != null) { + $('#contrastSlider').val(dict['contrast']); + } + else { + $('#contrastSlider').val(''); + } + + if (dict['saturation'] != null) { + $('#saturationSlider').val(dict['saturation']); + } + else { + $('#saturationSlider').val(''); + } + + if (dict['hue'] != null) { + $('#hueSlider').val(dict['hue']); + } + else { + $('#hueSlider').val(''); + } $('#resolutionSelect').html(''); if (dict['available_resolutions']) { @@ -1459,68 +1495,65 @@ function runAddCameraDialog() { '' + '?' + '' + - '' + - 'Host' + - '' + - '?' + + '' + + 'URL' + + '' + + '?' + '' + - '' + - 'Port' + - '' + - '?' + - '' + - '' + + '' + 'Username' + '' + - '?' + + '?' + '' + - '' + + '' + 'Password' + '' + - '?' + + '?' + '' + - '' + + '' + 'Camera' + - '' + - '?' + + '' + + '?' + '' + ''); /* collect ui widgets */ var deviceSelect = content.find('#deviceSelect'); - var hostEntry = content.find('#hostEntry'); - var portEntry = content.find('#portEntry'); + var urlEntry = content.find('#urlEntry'); var usernameEntry = content.find('#usernameEntry'); var passwordEntry = content.find('#passwordEntry'); var cameraSelect = content.find('#cameraSelect'); + var cameraMsgLabel = content.find('#cameraMsgLabel'); /* make validators */ - makeTextValidator(hostEntry, true); - makeNumberValidator(portEntry, 1, 65535, false, false, true); - makeTextValidator(usernameEntry, true); - makeTextValidator(deviceSelect, true); + makeUrlValidator(urlEntry, true); + makeTextValidator(usernameEntry, false); + makeTextValidator(deviceSelect, false); makeComboValidator(cameraSelect, true); /* ui interaction */ - content.find('tr.remote').css('display', 'none'); + content.find('tr.motioneye, tr.netcam').css('display', 'none'); function updateUi() { - if (deviceSelect.val() === 'remote') { - content.find('tr.remote').css('display', 'table-row'); + content.find('tr.motioneye, tr.netcam').css('display', 'none'); + if (deviceSelect.val() == 'motioneye') { + content.find('tr.motioneye').css('display', 'table-row'); + cameraSelect.hide(); } - else { - content.find('tr.remote').css('display', 'none'); + else if (deviceSelect.val() == 'netcam') { + content.find('tr.netcam').css('display', 'table-row'); + cameraSelect.hide(); } updateModalDialogPosition(); cameraSelect.html(''); - + /* re-validate all the validators */ content.find('.validator').each(function () { this.validate(); }); - if (content.is(':visible') && uiValid() && deviceSelect.val() == 'remote') { + if (content.is(':visible') && uiValid() && (deviceSelect.val() == 'motioneye' || deviceSelect.val() == 'netcam')) { fetchRemoteCameras(); } } @@ -1546,32 +1579,64 @@ function runAddCameraDialog() { return valid; } + function splitUrl(url) { + var parts = url.split('://'); + var proto = parts[0]; + var index = parts[1].indexOf('/'); + var host = null; + var uri = ''; + if (index >= 0) { + host = parts[1].substring(0, index); + uri = parts[1].substring(index); + } + else { + host = parts[1]; + } + + var port = ''; + parts = host.split(':'); + if (parts.length >= 2) { + host = parts[0]; + port = parts[1]; + } + + if (uri == '/') { + uri = ''; + } + + return { + proto: proto, + host: host, + port: port, + uri: uri + }; + } + function fetchRemoteCameras() { var progress = $('
'); cameraSelect.hide(); - cameraSelect.before(progress); cameraSelect.parent().find('div').remove(); /* remove any previous progress div */ + cameraSelect.before(progress); - var data = { - host: hostEntry.val(), - port: portEntry.val(), - username: usernameEntry.val(), - password: passwordEntry.val() - }; + var data = splitUrl(urlEntry.val()); + data.username = usernameEntry.val(); + data.password = passwordEntry.val(); + data.type = deviceSelect.val(); + + cameraMsgLabel.html(''); ajax('GET', '/config/list/', data, function (data) { + progress.remove(); + if (data == null || data.error) { - progress.remove(); - if (passwordEntry.val()) { /* only show an error message when a password is supplied */ - showErrorMessage(data && data.error); - } + //showErrorMessage(data && data.error); + cameraMsgLabel.html(data && data.error); return; } cameraSelect.html(''); - progress.remove(); if (data.error || !data.cameras) { return; @@ -1586,8 +1651,7 @@ function runAddCameraDialog() { } deviceSelect.change(updateUi); - hostEntry.change(updateUi); - portEntry.change(updateUi); + urlEntry.change(updateUi); usernameEntry.change(updateUi); passwordEntry.change(updateUi); updateUi(); @@ -1605,11 +1669,12 @@ function runAddCameraDialog() { /* add available devices */ data.devices.forEach(function (device) { if (!device.configured) { - deviceSelect.append(''); + deviceSelect.append(''); } }); - deviceSelect.append(''); + deviceSelect.append(''); + deviceSelect.append(''); updateUi(); @@ -1622,19 +1687,24 @@ function runAddCameraDialog() { if (!uiValid(true)) { return false; } - + var data = {}; - if (deviceSelect.val() == 'remote') { - data.proto = 'http'; - data.host = hostEntry.val(); - data.port = portEntry.val(); + + if (deviceSelect.val() == 'motioneye') { + data = splitUrl(urlEntry.val()); + data.proto = 'motioneye'; data.username = usernameEntry.val(); data.password = passwordEntry.val(); data.remote_camera_id = cameraSelect.val(); } - else { + else if (deviceSelect.val() == 'netcam') { + data = splitUrl(urlEntry.val()); + data.username = usernameEntry.val(); + data.password = passwordEntry.val(); + } + else { /* assuming v4l2 */ data.proto = 'v4l2'; - data.device_uri = deviceSelect.val(); + data.uri = deviceSelect.val(); } beginProgress(); diff --git a/static/js/ui.js b/static/js/ui.js index a1e3ce0..723697b 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -484,7 +484,7 @@ function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { function makeTimeValidator($input) { function isValid(strVal) { - return strVal.match('^[0-2][0-9]:[0-5][0-9]$') != null; + return strVal.match(new RegExp('^[0-2][0-9]:[0-5][0-9]$')) != null; } var msg = 'enter a valid time in the following format: HH:MM'; @@ -523,6 +523,42 @@ function makeTimeValidator($input) { }); } +function makeUrlValidator($input) { + function isValid(strVal) { + return strVal.match(new RegExp('^([a-zA-Z]+)://([\\w\-.]+)(:\\d+)?(/.*)?$')) != null; + } + + var msg = 'enter a valid URL (e.g. http://example.com:8080/cams/)'; + + function validate() { + if (!$input.parents('tr:eq(0)').is(':visible')) { + return true; /* an invisible element is considered always valid */ + } + + var strVal = $input.val(); + if (isValid(strVal)) { + $input.attr('title', ''); + $input.removeClass('error'); + $input[0].invalid = false; + } + else { + $input.attr('title', msg); + $input.addClass('error'); + $input[0].invalid = true; + } + } + + $input.keyup(validate); + $input.blur(validate); + $input.change(validate).change(); + + $input.addClass('validator'); + $input.addClass('url-validator'); + $input.each(function () { + this.validate = validate; + }); +} + function makeRegexValidator($input, regex, required) { if (required == null) { required = true; diff --git a/templates/main.html b/templates/main.html index 7ead947..8a0337a 100644 --- a/templates/main.html +++ b/templates/main.html @@ -121,11 +121,6 @@ ? - - Automatic Brightness - - ? - Brightness @@ -467,7 +462,7 @@ Web Hook URL - + ? -- 2.39.5