From: Calin Crisan Date: Sun, 3 May 2015 15:01:57 +0000 (+0300) Subject: major internal camera type/proto refactoring X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=72678ee42f8e37de35cbc053c414e37b478c9660;p=motioneye-debian major internal camera type/proto refactoring --- diff --git a/src/config.py b/src/config.py index 231726a..e044294 100644 --- a/src/config.py +++ b/src/config.py @@ -22,6 +22,7 @@ import os.path import re import shlex import subprocess +import urlparse from tornado.ioloop import IOLoop @@ -208,7 +209,7 @@ def has_local_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.get('@enabled') and utils.local_camera(c)]) + return bool([c for c in cameras if c.get('@enabled') and utils.local_motion_camera(c)]) def get_network_shares(): @@ -270,7 +271,7 @@ def get_camera(camera_id, as_lines=False): no_convert=['@name', '@network_share_name', '@network_server', '@network_username', '@network_password', '@storage_device']) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): # determine the enabled status main_config = get_main() threads = main_config.get('thread', []) @@ -330,7 +331,7 @@ def set_camera(camera_id, camera_config): _camera_config_cache[camera_id] = camera_config camera_config = dict(camera_config) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): old_motion = is_old_motion() # adapt directives to old configuration, if needed @@ -422,6 +423,21 @@ def add_camera(device_details): global _camera_ids_cache global _camera_config_cache + proto = device_details['proto'] + if proto in ['netcam', 'mjpeg']: + host = device_details['host'] + if device_details['port']: + host += ':' + str(device_details['port']) + + if device_details['username'] and proto == 'mjpeg': + if device_details['password']: + host = device_details['username'] + ':' + device_details['password'] + '@' + host + + else: + host = device_details['username'] + '@' + host + + device_details['url'] = urlparse.urlunparse((device_details['scheme'], host, device_details['uri'], '', '', '')) + # determine the last camera id camera_ids = get_camera_ids() @@ -433,14 +449,20 @@ def add_camera(device_details): # prepare a default camera config camera_config = {'@enabled': True} - if device_details['proto'] == 'v4l2': + if proto == 'v4l2': + # find a suitable resolution + for (w, h) in v4l2ctl.list_resolutions(device_details['uri']): + if w > 300: + camera_config['width'] = w + camera_config['height'] = h + break + + camera_config['videodevice'] = device_details['uri'] _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': + elif proto == 'motioneye': camera_config['@proto'] = 'motioneye' + camera_config['@scheme'] = device_details['scheme'] camera_config['@host'] = device_details['host'] camera_config['@port'] = device_details['port'] camera_config['@uri'] = device_details['uri'] @@ -448,12 +470,15 @@ def add_camera(device_details): 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 + elif proto == 'netcam': + camera_config['netcam_url'] = device_details['url'] + if device_details['username']: + camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password'] _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) + + else: # assuming mjpeg + camera_config['@proto'] = 'mjpeg' + camera_config['@url'] = device_details['url'] # write the configuration to file set_camera(camera_id, camera_config) @@ -539,7 +564,8 @@ def main_dict_to_ui(data): return ui -def camera_ui_to_dict(ui): +def camera_ui_to_dict(ui, old_config=None): + old_config = dict(old_config or {}) main_config = get_main() # needed for surveillance password data = { @@ -607,9 +633,14 @@ def camera_ui_to_dict(ui): 'on_event_end': '' } - if ui['proto'] == 'v4l2': - # device - data['videodevice'] = ui['uri'] + if utils.v4l2_camera(old_config): + proto = 'v4l2' + + else: + proto = 'netcam' + + if proto == 'v4l2': + # leave videodevice unchanged # resolution if not ui['resolution']: @@ -650,17 +681,9 @@ def camera_ui_to_dict(ui): else: data['hue'] = max(1, int(round(int(ui['hue']) * 2.55))) - else: # assuming http/netcam - # device - data['netcam_url'] = ui['proto'] + '://' + ui['host'] - if ui['port']: - data['netcam_url'] += ':' + str(ui['port']) - - data['netcam_url'] += ui['uri'] - - if ui['username'] or ui['password']: - data['netcam_userpass'] = (ui['username'] or '') + ':' + (ui['password'] or '') - + else: # assuming netcam + # leave netcam_url unchanged + # leave netcam_userpass unchanged data['netcam_keepalive'] = True data['netcam_tolerant_check'] = True @@ -714,7 +737,7 @@ def camera_ui_to_dict(ui): else: data['text_right'] = ui['custom_right_text'] - if ui['proto'] != 'v4l2' or data['width'] > 320: + if proto == 'netcam' or data['width'] > 320: data['text_double'] = True if ui['still_images']: @@ -735,10 +758,10 @@ def camera_ui_to_dict(ui): data['quality'] = max(1, int(ui['image_quality'])) if ui['motion_movies']: - if ui['proto'] == 'v4l2': + if proto == 'v4l2': max_val = data['width'] * data['height'] * data['framerate'] / 3 - else: + else: # always assume netcam image size of (640x480) - we have no means to test it max_val = 640 * 480 * data['framerate'] / 3 max_val = min(max_val, 9999999) @@ -811,7 +834,9 @@ def camera_ui_to_dict(ui): for name, value in extra_options: data[name] = value or '' - return data + old_config.update(data) + + return old_config def camera_dict_to_ui(data): @@ -893,44 +918,16 @@ def camera_dict_to_ui(data): } 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] - uri = '/' + uri - - else: - host_port, uri = rest, '' - - parts = host_port.split(':') - if len(parts) > 1: - host, port = parts[:2] - - else: - host, port = host_port, '' + ui['device_url'] = data['netcam_url'] + ui['proto'] = 'netcam' - ui['proto'] = proto - ui['host'] = host - ui['port'] = port - ui['uri'] = uri - - userpass = data.get('netcam_userpass') - if userpass: - ui['username'], ui['password'] = userpass.split(':', 1) - - else: - 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['device_url'] = data['videodevice'] 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']) @@ -940,7 +937,7 @@ def camera_dict_to_ui(data): # 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']) + brightness = v4l2ctl.get_brightness(data['videodevice']) if brightness is not None: # has brightness control if data.get('brightness', 0) != 0: ui['brightness'] = brightness @@ -948,7 +945,7 @@ def camera_dict_to_ui(data): else: ui['brightness'] = 50 - contrast = v4l2ctl.get_contrast(ui['uri']) + contrast = v4l2ctl.get_contrast(data['videodevice']) if contrast is not None: # has contrast control if data.get('contrast', 0) != 0: ui['contrast'] = contrast @@ -956,7 +953,7 @@ def camera_dict_to_ui(data): else: ui['contrast'] = 50 - saturation = v4l2ctl.get_saturation(ui['uri']) + saturation = v4l2ctl.get_saturation(data['videodevice']) if saturation is not None: # has saturation control if data.get('saturation', 0) != 0: ui['saturation'] = saturation @@ -964,7 +961,7 @@ def camera_dict_to_ui(data): else: ui['saturation'] = 50 - hue = v4l2ctl.get_hue(ui['uri']) + hue = v4l2ctl.get_hue(data['videodevice']) if hue is not None: # has hue control if data.get('hue', 0) != 0: ui['hue'] = hue @@ -1062,7 +1059,7 @@ def camera_dict_to_ui(data): if utils.v4l2_camera(data): max_val = data['width'] * data['height'] * data['framerate'] / 3 - else: + else: # net camera max_val = 640 * 480 * data['framerate'] / 3 max_val = min(max_val, 9999999) diff --git a/src/handlers.py b/src/handlers.py index 8f61823..162cee2 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -215,7 +215,7 @@ class ConfigHandler(BaseHandler): raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) - if utils.local_camera(local_config): + if utils.local_motion_camera(local_config): ui_config = config.camera_dict_to_ui(local_config) self.finish_json(ui_config) @@ -224,11 +224,13 @@ class ConfigHandler(BaseHandler): 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}}) + 'url': remote.pretty_camera_url(local_config), 'msg': error}}) for key, value in local_config.items(): remote_ui_config[key.replace('@', '')] = value + # replace the real device URI with the remote camera URL + remote_ui_config['device_url'] = remote.pretty_camera_url(local_config) self.finish_json(remote_ui_config) remote.get_config(local_config, on_response) @@ -258,19 +260,9 @@ class ConfigHandler(BaseHandler): raise HTTPError(404, 'no such camera') local_config = config.get_camera(camera_id) - if utils.local_camera(local_config): - # certain parameters cannot be changed via ui_config; - # we must preserve existing values for those params - local_ui_config = config.camera_dict_to_ui(local_config) - ui_config.setdefault('enabled', local_ui_config['enabled']) - ui_config['proto'] = local_ui_config['proto'] - ui_config['host'] = local_ui_config['host'] - ui_config['port'] = local_ui_config['port'] - ui_config['uri'] = local_ui_config['uri'] - ui_config['username'] = local_ui_config['username'] - ui_config['password'] = local_ui_config['password'] - - local_config = config.camera_ui_to_dict(ui_config) + if utils.local_motion_camera(local_config): + local_config = config.camera_ui_to_dict(ui_config, local_config) + config.set_camera(camera_id, local_config) on_finish(None, True) # (no error, motion needs restart) @@ -280,16 +272,17 @@ class ConfigHandler(BaseHandler): local_config['@enabled'] = ui_config['enabled'] config.set_camera(camera_id, local_config) - # when the local_config supplied has only the enabled state, - # the camera was probably disabled due to errors - if ui_config.has_key('name'): def on_finish_wrapper(error=None): return on_finish(error, False) + ui_config['enabled'] = True # never disable the camera remotely remote.set_config(local_config, ui_config, on_finish_wrapper) else: + # when the ui config supplied has only the enabled state + # and no useful fields (such as "name"), + # the camera was probably disabled due to errors on_finish(None, False) def set_main_config(ui_config): @@ -322,14 +315,15 @@ class ConfigHandler(BaseHandler): if normal_credentials != old_normal_credentials: logging.debug('surveillance credentials changed, all camera configs must be updated') + # reconfigure all local cameras to update the stream authentication options for camera_id in config.get_camera_ids(): local_config = config.get_camera(camera_id) - if not utils.local_camera(local_config): + if not utils.local_motion_camera(local_config): continue - # this will update the stream authentication options ui_config = config.camera_dict_to_ui(local_config) - local_config = config.camera_ui_to_dict(ui_config) + local_config = config.camera_ui_to_dict(ui_config, local_config) + config.set_camera(camera_id, local_config) restart = True @@ -475,15 +469,15 @@ class ConfigHandler(BaseHandler): remote.set_preview(camera_config, controls, on_response) - else: + else: # not supported self.finish_json({'error': True}) @BaseHandler.auth() def list_cameras(self): logging.debug('listing cameras') - type = self.get_data().get('type') - if type == 'motioneye': # remote listing + proto = self.get_data().get('proto') + if proto == 'motioneye': # remote listing def on_response(cameras=None, error=None): if error: self.finish_json({'error': error}) @@ -494,7 +488,17 @@ class ConfigHandler(BaseHandler): remote.list_cameras(self.get_data(), on_response) - elif type == 'netcam': + elif proto == 'netcam': + def on_response(cameras=None, error=None): + if error: + self.finish_json({'error': error}) + + else: + self.finish_json({'cameras': cameras}) + + utils.test_mjpeg_url(self.get_data(), auth_modes=['basic'], allow_jpeg=True, callback=on_response) + + elif proto == 'mjpeg': def on_response(cameras=None, error=None): if error: self.finish_json({'error': error}) @@ -502,9 +506,9 @@ class ConfigHandler(BaseHandler): else: self.finish_json({'cameras': cameras}) - utils.test_netcam_url(self.get_data(), on_response) + utils.test_mjpeg_url(self.get_data(), auth_modes=['basic', 'digest'], allow_jpeg=False, callback=on_response) - else: # assuming local listing + else: # assuming local motionEye camera listing cameras = [] camera_ids = config.get_camera_ids() if not config.get_main().get('@enabled'): @@ -521,7 +525,7 @@ class ConfigHandler(BaseHandler): if error: cameras.append({ 'id': camera_id, - 'name': '<' + remote.make_camera_url(local_config) + '>', + 'name': '<' + remote.pretty_camera_url(local_config) + '>', 'enabled': False, 'streaming_framerate': 1, 'framerate': 1 @@ -553,7 +557,7 @@ class ConfigHandler(BaseHandler): if local_config is None: continue - if utils.local_camera(local_config): + if utils.local_motion_camera(local_config): ui_config = config.camera_dict_to_ui(local_config) cameras.append(ui_config) check_finished() @@ -595,23 +599,10 @@ class ConfigHandler(BaseHandler): raise - proto = device_details['proto'] - if proto == 'v4l2': - # find a suitable resolution - for (w, h) in v4l2ctl.list_resolutions(device_details['uri']): - if w > 300: - device_details.setdefault('resolution', str(w) + 'x' + str(h)) - break - - else: - # adjust uri format - if device_details['uri'] and not device_details['uri'].startswith('/'): - device_details['uri'] = '/' + device_details['uri'] - camera_id, camera_config = config.add_camera(device_details) camera_config['@id'] = camera_id - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): motionctl.stop() if settings.SMB_SHARES: @@ -637,13 +628,13 @@ class ConfigHandler(BaseHandler): self.finish_json(remote_ui_config) - remote.get_config(device_details, on_response) + remote.get_config(camera_config, on_response) @BaseHandler.auth(admin=True) def rem_camera(self, camera_id): logging.debug('removing camera %(id)s' % {'id': camera_id}) - local = utils.local_camera(config.get_camera(camera_id)) + local = utils.local_motion_camera(config.get_camera(camera_id)) config.rem_camera(camera_id) if local: @@ -689,7 +680,7 @@ class ConfigHandler(BaseHandler): logging.warn('ignoring event for remote camera with id %s (probably removed)' % camera_id) return self.finish_json() - if not utils.local_camera(camera_config): + if not utils.local_motion_camera(camera_config): logging.warn('ignoring event for remote camera with id %s' % camera_id) return self.finish_json() @@ -774,7 +765,7 @@ class PictureHandler(BaseHandler): return self.try_finish(picture) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): picture = mediafiles.get_current_picture(camera_config, width=width, height=height) @@ -800,7 +791,7 @@ class PictureHandler(BaseHandler): logging.debug('listing pictures for camera %(id)s' % {'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): def on_media_list(media_list): if media_list is None: return self.finish_json({'error': 'Failed to get pictures list.'}) @@ -817,7 +808,7 @@ class PictureHandler(BaseHandler): 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}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json(remote_list) @@ -826,7 +817,7 @@ class PictureHandler(BaseHandler): def frame(self, camera_id): camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config) or self.get_argument('title', None) is not None: + if utils.local_motion_camera(camera_config) or self.get_argument('title', None) is not None: self.render('main.html', frame=True, camera_id=camera_id, @@ -843,7 +834,10 @@ class PictureHandler(BaseHandler): camera_config=camera_config, title=self.get_argument('title', '')) + # issue a fake camera_ui_to_dict() call to transform + # the remote UI values into motion config directives remote_config = config.camera_ui_to_dict(remote_ui_config) + self.render('main.html', frame=True, camera_id=camera_id, @@ -860,7 +854,7 @@ class PictureHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): content = mediafiles.get_media_content(camera_config, filename, 'picture') pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename) @@ -873,7 +867,7 @@ class PictureHandler(BaseHandler): 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}}) + 'url': remote.pretty_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') @@ -889,7 +883,7 @@ class PictureHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): content = mediafiles.get_media_preview(camera_config, filename, 'picture', width=self.get_argument('width', None), height=self.get_argument('height', None)) @@ -925,7 +919,7 @@ class PictureHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): try: mediafiles.del_media_content(camera_config, filename, 'picture') self.finish_json() @@ -937,7 +931,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to delete picture from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json() @@ -952,7 +946,7 @@ class PictureHandler(BaseHandler): logging.debug('serving zip file for group %(group)s of camera %(id)s with key %(key)s' % { 'group': group, 'id': camera_id, 'key': key}) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): data = mediafiles.get_prepared_cache(key) if not data: logging.error('prepared cache data for key "%s" does not exist' % key) @@ -960,7 +954,7 @@ class PictureHandler(BaseHandler): raise HTTPError(404, 'no such key') camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): pretty_filename = camera_config['@name'] + '_' + group pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename) @@ -975,7 +969,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to download zip file from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.set_header('Content-Type', response['content_type']) self.set_header('Content-Disposition', response['content_disposition']) @@ -987,7 +981,7 @@ class PictureHandler(BaseHandler): logging.debug('preparing zip file for group %(group)s of camera %(id)s' % { 'group': group, 'id': camera_id}) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): def on_zip(data): if data is None: return self.finish_json({'error': 'Failed to create zip file.'}) @@ -1003,7 +997,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to make zip file at %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json({'key': response['key']}) @@ -1019,7 +1013,7 @@ class PictureHandler(BaseHandler): logging.debug('serving timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % { 'group': group, 'id': camera_id, 'key': key}) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): data = mediafiles.get_prepared_cache(key) if data is None: logging.error('prepared cache data for key "%s" does not exist' % key) @@ -1027,7 +1021,7 @@ class PictureHandler(BaseHandler): raise HTTPError(404, 'no such key') camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): pretty_filename = camera_config['@name'] + '_' + group pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename) @@ -1042,7 +1036,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to download timelapse movie from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.set_header('Content-Type', response['content_type']) self.set_header('Content-Disposition', response['content_disposition']) @@ -1054,7 +1048,7 @@ class PictureHandler(BaseHandler): logging.debug('checking timelapse movie status for group %(group)s of camera %(id)s' % { 'group': group, 'id': camera_id}) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): status = mediafiles.check_timelapse_movie() if status['progress'] == -1 and status['data']: key = mediafiles.set_prepared_cache(status['data']) @@ -1069,7 +1063,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to check timelapse movie progress at %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) if response['progress'] == -1 and response.get('key'): self.finish_json({'key': response['key'], 'progress': -1}) @@ -1086,7 +1080,7 @@ class PictureHandler(BaseHandler): logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with rate %(framerate)s/%(int)s' % { 'group': group, 'id': camera_id, 'framerate': framerate, 'int': interval}) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): status = mediafiles.check_timelapse_movie() if status['progress'] != -1: self.finish_json({'progress': status['progress']}) # timelapse already active @@ -1099,7 +1093,7 @@ class PictureHandler(BaseHandler): def on_status(response=None, error=None): if error: return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) if response['progress'] != -1: return self.finish_json({'progress': response['progress']}) # timelapse already active @@ -1107,7 +1101,7 @@ class PictureHandler(BaseHandler): def on_make(response=None, error=None): if error: return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json({'progress': -1}) @@ -1122,7 +1116,7 @@ class PictureHandler(BaseHandler): 'group': group, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): try: mediafiles.del_media_group(camera_config, group, 'picture') self.finish_json() @@ -1134,7 +1128,7 @@ class PictureHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to delete picture group from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json() @@ -1189,7 +1183,7 @@ class MovieHandler(BaseHandler): logging.debug('listing movies for camera %(id)s' % {'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): def on_media_list(media_list): if media_list is None: return self.finish_json({'error': 'Failed to get movies list.'}) @@ -1206,7 +1200,7 @@ class MovieHandler(BaseHandler): 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}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json(remote_list) @@ -1218,7 +1212,7 @@ class MovieHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): content = mediafiles.get_media_content(camera_config, filename, 'movie') pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename) @@ -1231,7 +1225,7 @@ class MovieHandler(BaseHandler): 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}}) + 'url': remote.pretty_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') @@ -1247,7 +1241,7 @@ class MovieHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): content = mediafiles.get_media_preview(camera_config, filename, 'movie', width=self.get_argument('width', None), height=self.get_argument('height', None)) @@ -1283,7 +1277,7 @@ class MovieHandler(BaseHandler): 'filename': filename, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): try: mediafiles.del_media_content(camera_config, filename, 'movie') self.finish_json() @@ -1295,7 +1289,7 @@ class MovieHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to delete movie from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json() @@ -1307,7 +1301,7 @@ class MovieHandler(BaseHandler): 'group': group, 'id': camera_id}) camera_config = config.get_camera(camera_id) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): try: mediafiles.del_media_group(camera_config, group, 'movie') self.finish_json() @@ -1319,7 +1313,7 @@ class MovieHandler(BaseHandler): def on_response(response=None, error=None): if error: return self.finish_json({'error': 'Failed to delete movie group from %(url)s: %(msg)s.' % { - 'url': remote.make_camera_url(camera_config), 'msg': error}}) + 'url': remote.pretty_camera_url(camera_config), 'msg': error}}) self.finish_json() diff --git a/src/mediafiles.py b/src/mediafiles.py index 978c330..e6bc5a8 100644 --- a/src/mediafiles.py +++ b/src/mediafiles.py @@ -153,7 +153,7 @@ def cleanup_media(media_type): for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if not utils.local_camera(camera_config): + if not utils.local_motion_camera(camera_config): continue preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0) @@ -239,7 +239,7 @@ def make_next_movie_preview(): count = 0 for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if not utils.local_camera(camera_config): + if not utils.local_motion_camera(camera_config): continue target_dir = camera_config['target_dir'] diff --git a/src/mjpgclient.py b/src/mjpgclient.py index 7e01b9b..b103bd3 100644 --- a/src/mjpgclient.py +++ b/src/mjpgclient.py @@ -267,7 +267,7 @@ def get_jpg(camera_id): 'camera_id': camera_id}) camera_config = config.get_camera(camera_id) - if not camera_config['@enabled'] or not utils.local_camera(camera_config): + if not camera_config['@enabled'] or not utils.local_motion_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 b656dc0..c6c4fd9 100644 --- a/src/motionctl.py +++ b/src/motionctl.py @@ -72,7 +72,7 @@ def find_motion(): def _disable_initial_motion_detection(): for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if not utils.local_camera(camera_config): + if not utils.local_motion_camera(camera_config): continue if not camera_config['@motion_detection']: @@ -253,7 +253,7 @@ def set_motion_detection(camera_id, enabled): logging.error('failed to %(what)s motion detection for camera with id %(id)s: %(msg)s' % { 'what': ['disable', 'enable'][enabled], 'id': camera_id, - 'msg': utils.pretty_http_error(response.error)}) + 'msg': utils.pretty_http_error(response)}) else: logging.debug('successfully %(what)s motion detection for camera with id %(id)s' % { @@ -276,7 +276,7 @@ def _get_thread_id(camera_id): thread_id = 0 for cid in camera_ids: camera_config = config.get_camera(cid) - if utils.local_camera(camera_config): + if utils.local_motion_camera(camera_config): thread_id += 1 if cid == camera_id: diff --git a/src/remote.py b/src/remote.py index 6dc4feb..0bab952 100644 --- a/src/remote.py +++ b/src/remote.py @@ -28,10 +28,10 @@ import utils _DOUBLE_SLASH_REGEX = re.compile('//+') -def _make_request(host, port, username, password, uri, method='GET', data=None, query=None, timeout=None): +def _make_request(scheme, host, port, username, password, uri, method='GET', data=None, query=None, timeout=None): uri = _DOUBLE_SLASH_REGEX.sub('/', uri) url = '%(scheme)s://%(host)s%(port)s%(uri)s' % { - 'scheme': 'http', + 'scheme': scheme, 'host': host, 'port': ':' + str(port) if port else '', 'uri': uri or ''} @@ -74,19 +74,22 @@ def _callback_wrapper(callback): return wrapper -def make_camera_url(local_config, camera=True): +def pretty_camera_url(local_config, camera=True): + scheme = local_config.get('@scheme', local_config.get('scheme')) or 'http' 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')) or '' uri = local_config.get('@uri', local_config.get('uri')) or '' - url = 'motioneye://' + username + '@' + host - if port: + url = scheme + '://' + host + if port and str(port) not in ['80', '443']: url += ':' + str(port) if uri: url += uri - + + if url.endswith('/'): + url = url[:-1] + if camera: if camera is True: url += '/config/' + str(local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))) @@ -97,71 +100,78 @@ def make_camera_url(local_config, camera=True): return url +def _remote_params(local_config): + return ( + local_config.get('@scheme', local_config.get('scheme')) or 'http', + local_config.get('@host', local_config.get('host')), + local_config.get('@port', local_config.get('port')), + local_config.get('@username', local_config.get('username')), + local_config.get('@password', local_config.get('password')), + local_config.get('@uri', local_config.get('uri')) or '', + local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))) + + def list_cameras(local_config, callback): - 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')) - password = local_config.get('@password', local_config.get('password')) - uri = local_config.get('@uri', local_config.get('uri')) or '' + scheme, host, port, username, password, uri, _ = _remote_params(local_config) logging.debug('listing remote cameras on %(url)s' % { - 'url': make_camera_url(local_config, camera=False)}) + 'url': pretty_camera_url(local_config, camera=False)}) - request = _make_request(host, port, username, password, uri + '/config/list/') + request = _make_request(scheme, host, port, username, password, uri + '/config/list/') def on_response(response): if response.error: logging.error('failed to list remote cameras on %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config, camera=False), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config, camera=False), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: response = json.loads(response.body) except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config, camera=False), + 'url': pretty_camera_url(local_config, camera=False), 'msg': unicode(e)}) return callback(error=unicode(e)) - callback(response['cameras']) + cameras = response['cameras'] + + # filter out simple mjpeg cameras + cameras = [c for c in cameras if c['proto'] != 'mjpeg'] + + callback(cameras) http_client = AsyncHTTPClient() http_client.fetch(request, _callback_wrapper(on_response)) def get_config(local_config, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('getting config for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) - request = _make_request(host, port, username, password, uri + '/config/%(id)s/get/' % {'id': camera_id}) + request = _make_request(scheme, 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 %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: response = json.loads(response.body) except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'msg': unicode(e)}) return callback(error=unicode(e)) @@ -176,38 +186,30 @@ def get_config(local_config, callback): def set_config(local_config, ui_config, callback): - host = local_config.get('@host') - port = local_config.get('@port') - username = local_config.get('@username') - password = local_config.get('@password') - uri = local_config.get('@uri') or '' - camera_id = local_config.get('@remote_camera_id') - - # make sure these values never get to the remote instance - local_config.pop('enabled', None) - local_config.pop('proto', None) - local_config.pop('host', None) - local_config.pop('port', None) - local_config.pop('uri', None) - local_config.pop('username', None) - local_config.pop('password', None) - + scheme = local_config.get('@scheme', local_config.get('scheme')) + 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')) + 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 %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) ui_config = json.dumps(ui_config) - request = _make_request(host, port, username, password, uri + '/config/%(id)s/set/' % {'id': camera_id}, method='POST', data=ui_config) + request = _make_request(scheme, 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 %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback() @@ -216,29 +218,24 @@ def set_config(local_config, ui_config, callback): def set_preview(local_config, controls, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('setting preview for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) data = json.dumps(controls) - request = _make_request(host, port, username, password, uri + '/config/%(id)s/set_preview/' % {'id': camera_id}, method='POST', data=data) + request = _make_request(scheme, 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 %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback() @@ -247,16 +244,11 @@ def set_preview(local_config, controls, callback): def get_current_picture(local_config, width, height, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('getting current picture for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) query = {} @@ -266,7 +258,7 @@ def get_current_picture(local_config, width, height, callback): if height: query['height'] = str(height) - request = _make_request(host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query) + request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query) def on_response(response): motion_detected = False @@ -281,10 +273,10 @@ def get_current_picture(local_config, width, height, callback): if response.error: logging.error('failed to get current picture for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback(motion_detected, response.body) @@ -293,40 +285,35 @@ def get_current_picture(local_config, width, height, callback): def list_media(local_config, media_type, prefix, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('getting media list for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_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, uri + '/%(media_type)s/%(id)s/list/' % { + request = _make_request(scheme, 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 %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: response = json.loads(response.body) except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'msg': unicode(e)}) return callback(error=unicode(e)) @@ -338,17 +325,12 @@ def list_media(local_config, media_type, prefix, callback): def get_media_content(local_config, filename, media_type, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('downloading file %(filename)s of remote camera %(id)s on %(url)s' % { 'filename': filename, 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) uri += '/%(media_type)s/%(id)s/download/%(filename)s' % { 'media_type': media_type, @@ -356,17 +338,17 @@ def get_media_content(local_config, filename, media_type, callback): 'filename': filename} # 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, uri, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT) + request = _make_request(scheme, host, port, username, password, uri, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: logging.error('failed to download file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'filename': filename, 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) return callback(response.body) @@ -375,17 +357,12 @@ def get_media_content(local_config, filename, media_type, callback): def make_zipped_content(local_config, media_type, group, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('preparing zip file for group %(group)s of remote camera %(id)s on %(url)s' % { 'group': group, 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) prepare_uri = uri + '/%(media_type)s/%(id)s/zipped/%(group)s/' % { 'media_type': media_type, @@ -393,24 +370,24 @@ def make_zipped_content(local_config, media_type, group, callback): 'group': group} # timeout here is 100 times larger than usual - we expect a big delay - request = _make_request(host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT) + request = _make_request(scheme, host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: logging.error('failed to prepare zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'group': group, 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: key = json.loads(response.body)['key'] except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'msg': unicode(e)}) return callback(error=unicode(e)) @@ -422,18 +399,13 @@ def make_zipped_content(local_config, media_type, group, callback): def get_zipped_content(local_config, media_type, key, group, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('downloading zip file for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) - request = _make_request(host, port, username, password, uri + '/%(media_type)s/%(id)s/zipped/%(group)s/?key=%(key)s' % { + request = _make_request(scheme, host, port, username, password, uri + '/%(media_type)s/%(id)s/zipped/%(group)s/?key=%(key)s' % { 'media_type': media_type, 'group': group, 'id': camera_id, @@ -444,10 +416,10 @@ def get_zipped_content(local_config, media_type, key, group, callback): if response.error: logging.error('failed to download zip file for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback({ 'data': response.body, @@ -460,19 +432,14 @@ def get_zipped_content(local_config, media_type, key, group, callback): def make_timelapse_movie(local_config, framerate, interval, group, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('making timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s' % { 'group': group, 'id': camera_id, 'framerate': framerate, 'int': interval, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) uri += '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&framerate=%(framerate)s' % { 'id': camera_id, @@ -480,26 +447,26 @@ def make_timelapse_movie(local_config, framerate, interval, group, callback): 'framerate': framerate, 'group': group} - request = _make_request(host, port, username, password, uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT) + request = _make_request(scheme, host, port, username, password, uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: logging.error('failed to make timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % { 'group': group, 'id': camera_id, - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'int': interval, 'framerate': framerate, - 'msg': utils.pretty_http_error(response.error)}) + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: response = json.loads(response.body) except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'msg': unicode(e)}) return callback(error=unicode(e)) @@ -511,18 +478,13 @@ def make_timelapse_movie(local_config, framerate, interval, group, callback): def check_timelapse_movie(local_config, group, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('checking timelapse movie status for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) - request = _make_request(host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?check=true' % { + request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?check=true' % { 'id': camera_id, 'group': group}) @@ -530,17 +492,17 @@ def check_timelapse_movie(local_config, group, callback): if response.error: logging.error('failed to check timelapse movie status for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) try: response = json.loads(response.body) except Exception as e: logging.error('failed to decode json answer from %(url)s: %(msg)s' % { - 'url': make_camera_url(local_config), + 'url': pretty_camera_url(local_config), 'msg': unicode(e)}) return callback(error=unicode(e)) @@ -552,18 +514,13 @@ def check_timelapse_movie(local_config, group, callback): def get_timelapse_movie(local_config, key, group, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('downloading timelapse movie for remote camera %(id)s on %(url)s' % { 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) - request = _make_request(host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % { + request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % { 'id': camera_id, 'group': group, 'key': key}, @@ -573,10 +530,10 @@ def get_timelapse_movie(local_config, key, group, callback): if response.error: logging.error('failed to download timelapse movie for remote camera %(id)s on %(url)s: %(msg)s' % { 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback({ 'data': response.body, @@ -589,17 +546,12 @@ def get_timelapse_movie(local_config, key, group, callback): def get_media_preview(local_config, filename, media_type, width, height, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(url)s' % { 'filename': filename, 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) uri += '/%(media_type)s/%(id)s/preview/%(filename)s' % { 'media_type': media_type, @@ -614,17 +566,17 @@ def get_media_preview(local_config, filename, media_type, width, height, callbac if height: query['height'] = str(height) - request = _make_request(host, port, username, password, uri, query=query) + request = _make_request(scheme, host, port, username, password, uri, query=query) def on_response(response): if response.error: 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, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback(response.body) @@ -633,34 +585,29 @@ def get_media_preview(local_config, filename, media_type, width, height, callbac def del_media_content(local_config, filename, media_type, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('deleting file %(filename)s of remote camera %(id)s on %(url)s' % { 'filename': filename, 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) uri += '/%(media_type)s/%(id)s/delete/%(filename)s' % { 'media_type': media_type, 'id': camera_id, 'filename': filename} - request = _make_request(host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT) + request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: logging.error('failed to delete file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'filename': filename, 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback() @@ -669,34 +616,29 @@ def del_media_content(local_config, filename, media_type, callback): def del_media_group(local_config, group, media_type, callback): - 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')) - 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')) + scheme, host, port, username, password, uri, camera_id = _remote_params(local_config) logging.debug('deleting group %(group)s of remote camera %(id)s on %(url)s' % { 'group': group, 'id': camera_id, - 'url': make_camera_url(local_config)}) + 'url': pretty_camera_url(local_config)}) uri += '/%(media_type)s/%(id)s/delete_all/%(group)s/' % { 'media_type': media_type, 'id': camera_id, 'group': group} - request = _make_request(host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT) + request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT) def on_response(response): if response.error: logging.error('failed to delete group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % { 'group': group, 'id': camera_id, - 'url': make_camera_url(local_config), - 'msg': utils.pretty_http_error(response.error)}) + 'url': pretty_camera_url(local_config), + 'msg': utils.pretty_http_error(response)}) - return callback(error=utils.pretty_http_error(response.error)) + return callback(error=utils.pretty_http_error(response)) callback() diff --git a/src/utils.py b/src/utils.py index 101a881..b7aaf87 100644 --- a/src/utils.py +++ b/src/utils.py @@ -210,13 +210,22 @@ def pretty_size(size): return '%.1f %s' % (size, unit) -def pretty_http_error(http_error): - msg = unicode(http_error) +def pretty_http_error(response): + if response.code == 401 or response.error == 'Authentication Error': + return 'authentication failed' + + if not response.error: + return 'ok' + + msg = unicode(response.error) if msg.startswith('HTTP '): msg = msg.split(':', 1)[-1].strip() if msg.startswith('[Errno '): msg = msg.split(']', 1)[-1].strip() + + if 'timeout' in msg.lower() or 'timed out' in msg.lower(): + msg = 'request timed out' return msg @@ -244,40 +253,48 @@ def get_disk_usage(path): return (used_size, total_size) -def local_camera(config): +def local_motion_camera(config): + '''Tells if a camera is managed by the local motion instance.''' return bool(config.get('videodevice') or config.get('netcam_url')) def remote_camera(config): + '''Tells if a camera is managed by a remote motionEye server.''' return config.get('@proto') == 'motioneye' def v4l2_camera(config): + '''Tells if a camera is a v4l2 device managed by the local motion instance.''' return bool(config.get('videodevice')) def net_camera(config): + '''Tells if a camera is a network camera managed by the local motion instance.''' return bool(config.get('netcam_url')) -def test_netcam_url(data, callback): +def simple_mjpeg_camera(config): + '''Tells if a camera is a simple MJPEG camera not managed by any motion instance.''' + return bool(config.get('@proto') == 'mjpeg') + + +def test_mjpeg_url(data, auth_modes, allow_jpeg, callback): data = dict(data) - data.setdefault('proto', 'http') + data.setdefault('scheme', 'http') data.setdefault('host', '127.0.0.1') data.setdefault('port', '80') data.setdefault('uri', '') data.setdefault('username', None) data.setdefault('password', None) - url = '%(proto)s://%(host)s%(port)s%(uri)s' % { - 'proto': data['proto'], + url = '%(scheme)s://%(host)s%(port)s%(uri)s' % { + 'scheme': data['scheme'], 'host': data['host'], 'port': ':' + str(data['port']) if data['port'] else '', 'uri': data['uri'] or ''} called = [False] status_2xx = [False] - auth_modes = ['basic'] # 'digest' netcams are not supported by motion yet def on_header(header): header = header.lower() @@ -285,14 +302,14 @@ def test_netcam_url(data, callback): content_type = header.split(':')[1].strip() called[0] = True - if content_type in ['image/jpg', 'image/jpeg', 'image/pjpg']: + if content_type in ['image/jpg', 'image/jpeg', 'image/pjpg'] and allow_jpeg: callback([{'id': 1, 'name': 'JPEG Network Camera'}]) elif content_type.startswith('multipart/x-mixed-replace'): callback([{'id': 1, 'name': 'MJPEG Network Camera'}]) else: - callback(error='not a network camera') + callback(error='not a supported network camera') else: # check for the status header @@ -324,7 +341,7 @@ def test_netcam_url(data, callback): else: called[0] = True - callback(error=pretty_http_error(response.error) if response.error else 'not a network camera') + callback(error=pretty_http_error(response) if response.error else 'not a supported network camera') username = data['username'] or None password = data['password'] or None diff --git a/src/wsswitch.py b/src/wsswitch.py index 66cd083..be65c27 100644 --- a/src/wsswitch.py +++ b/src/wsswitch.py @@ -80,7 +80,7 @@ def _check_ws(): now = datetime.datetime.now() for camera_id in config.get_camera_ids(): camera_config = config.get_camera(camera_id) - if not utils.local_camera(camera_config): + if not utils.local_motion_camera(camera_config): continue working_schedule = camera_config.get('@working_schedule') diff --git a/static/css/main.css b/static/css/main.css index 95c44bf..11b3a35 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -521,6 +521,10 @@ span#cameraMsgLabel { font-size: 0.7em; } +div#addCameraInfo { + font-size: 0.7em; + max-width: 33em; +} div.media-dialog { } diff --git a/static/css/ui.css b/static/css/ui.css index 15a33b3..ad9acc2 100644 --- a/static/css/ui.css +++ b/static/css/ui.css @@ -176,6 +176,9 @@ input[type=text].time { width: 3.5em; } +input[disabled] { + border: 1px solid #555 !important; +} /* combo box */ @@ -410,6 +413,12 @@ span.dialog-item-label { font-size: 0.9em; } +div.dialog-item-separator { + height: 1px; + border-top: 1px solid #414141; + margin: 0.5em 1em; +} + /* popup message */ diff --git a/static/js/main.js b/static/js/main.js index 9359c69..819f552 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -394,19 +394,6 @@ 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; - } -} - function getCookie(name) { if (document.cookie.length <= 0) { return null; @@ -1155,12 +1142,6 @@ function cameraUi2Dict() { 'auto_brightness': $('#autoBrightnessSwitch')[0].checked, '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, 'extra_options': $('#extraOptionsEntry').html().split(new RegExp('(]*>)|(
)|(

)')).map(function (o) { if (!o) { return null; @@ -1346,15 +1327,29 @@ function dict2CameraUi(dict) { } /* video device */ + var prettyType = ''; + switch (dict['proto']) { + case 'v4l2': + prettyType = 'V4L2 Camera'; + break; + + case 'netcam': + prettyType = 'Network Camera'; + break; + + case 'motioneye': + prettyType = 'Remote motionEye Camera'; + break; + + case 'mjpeg': + prettyType = 'Simple MJPEG Camera'; + break; + } + $('#videoDeviceSwitch')[0].checked = dict['enabled']; $('#deviceNameEntry').val(dict['name']); - $('#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']; + $('#deviceUriEntry').val(dict['device_url']); + $('#deviceTypeEntry').val(prettyType); $('#lightSwitchDetectSwitch')[0].checked = dict['light_switch_detect']; $('#autoBrightnessSwitch')[0].checked = dict['auto_brightness']; @@ -1470,7 +1465,7 @@ function dict2CameraUi(dict) { var mjpgUrl = location.protocol + '//' + location.host.split(':')[0] + ':' + dict.streaming_port; var embedUrl = cameraUrl + 'frame/'; - if (dict.proto == 'motioneye') { + if (dict.proto == 'motioneye') { // TODO what about other protocols /* cannot tell the mjpg streaming url for a remote motionEye camera */ mjpgUrl = ''; } @@ -1730,7 +1725,7 @@ function doApply() { } var instance; - if (config.proto == 'http' || config.proto == 'v4l2') { + if (config.proto == 'netcam' || config.proto == 'v4l2') { instance = ''; } else { /* motioneye */ @@ -2313,7 +2308,7 @@ function getCameraIdsByInstance() { var cameraIdsByInstance = {}; $('div.camera-frame').each(function () { var instance; - if (this.config.proto == 'http' || this.config.proto == 'v4l2') { + if (this.config.proto == 'netcam' || this.config.proto == 'v4l2') { instance = ''; } else { /* motioneye */ @@ -2544,27 +2539,33 @@ function runAddCameraDialog() { '' + 'Device' + '' + - '?' + + '?' + '' + - '' + + '' + 'URL' + '' + - '?' + + '?' + '' + - '' + + '' + 'Username' + '' + '?' + '' + - '' + + '' + 'Password' + '' + '?' + '' + - '' + + '' + 'Camera' + '' + - '?' + + '?' + + '' + + '' + + '

' + + '' + + '' + + '
blah blah sdfsdf ana are mere multe verzi si vesele si mari
' + '' + ''); @@ -2574,6 +2575,7 @@ function runAddCameraDialog() { var usernameEntry = content.find('#usernameEntry'); var passwordEntry = content.find('#passwordEntry'); var addCameraSelect = content.find('#addCameraSelect'); + var addCameraInfo = content.find('#addCameraInfo'); var cameraMsgLabel = content.find('#cameraMsgLabel'); /* make validators */ @@ -2583,16 +2585,17 @@ function runAddCameraDialog() { makeComboValidator(addCameraSelect, true); /* ui interaction */ - content.find('tr.motioneye, tr.netcam').css('display', 'none'); - function updateUi() { - content.find('tr.motioneye, tr.netcam').css('display', 'none'); + content.find('tr.motioneye, tr.netcam, tr.mjpeg').css('display', 'none'); if (deviceSelect.val() == 'motioneye') { content.find('tr.motioneye').css('display', 'table-row'); addCameraSelect.hide(); usernameEntry.val('admin'); usernameEntry.attr('readonly', 'readonly'); + addCameraInfo.html( + 'Remote motionEye cameras are cameras installed behind another motionEye server. ' + + 'Adding them here will allow you to view and manage them remotely.'); } else if (deviceSelect.val() == 'netcam') { usernameEntry.removeAttr('readonly'); @@ -2607,6 +2610,28 @@ function runAddCameraDialog() { content.find('tr.netcam').css('display', 'table-row'); addCameraSelect.hide(); + addCameraInfo.html( + 'Network cameras (or IP cameras) are devices that natively stream MJPEG videos or plain JPEG images. ' + + "Consult your device's manual to find out the correct MJPEG (or JPEG) URL."); + } + else if (deviceSelect.val() == 'mjpeg') { + usernameEntry.removeAttr('readonly'); + + /* make sure there is one trailing slash so that + * an URI can be detected */ + var url = urlEntry.val().trim(); + var m = url.match(new RegExp('/', 'g')); + if (m && m.length < 3 && !url.endsWith('/')) { + urlEntry.val(url + '/'); + } + + content.find('tr.mjpeg').css('display', 'table-row'); + addCameraSelect.hide(); + addCameraInfo.html( + 'Adding your device as a simple MJPEG camera instead of as a network camera will improve the framerate, ' + + 'but no motion detection or other advanced features will be available for it. ' + + 'The camera must be accessible to both your server and your browser. ' + + 'This type of camera is not compatible with Internet Explorer.'); } updateModalDialogPosition(); @@ -2617,7 +2642,7 @@ function runAddCameraDialog() { this.validate(); }); - if (content.is(':visible') && uiValid() && (deviceSelect.val() == 'motioneye' || deviceSelect.val() == 'netcam')) { + if (content.is(':visible') && uiValid() && (deviceSelect.val() in {'motioneye': 1, 'netcam': 1, 'mjpeg': 1})) { fetchRemoteCameras(); } } @@ -2645,7 +2670,7 @@ function runAddCameraDialog() { function splitCameraUrl(url) { var parts = url.split('://'); - var proto = parts[0]; + var scheme = parts[0]; var index = parts[1].indexOf('/'); var host = null; var uri = ''; @@ -2669,7 +2694,7 @@ function runAddCameraDialog() { } return { - proto: proto, + scheme: scheme, host: host, port: port, uri: uri @@ -2686,7 +2711,7 @@ function runAddCameraDialog() { var data = splitCameraUrl(urlEntry.val()); data.username = usernameEntry.val(); data.password = passwordEntry.val(); - data.type = deviceSelect.val(); + data.proto = deviceSelect.val(); cameraMsgLabel.html(''); @@ -2738,6 +2763,7 @@ function runAddCameraDialog() { deviceSelect.append(''); deviceSelect.append(''); + //deviceSelect.append(''); updateUi(); @@ -2764,6 +2790,13 @@ function runAddCameraDialog() { data = splitCameraUrl(urlEntry.val()); data.username = usernameEntry.val(); data.password = passwordEntry.val(); + data.proto = 'netcam'; + } + else if (deviceSelect.val() == 'mjpeg') { + data = splitCameraUrl(urlEntry.val()); + data.username = usernameEntry.val(); + data.password = passwordEntry.val(); + data.proto = 'mjpeg'; } else { /* assuming v4l2 */ data.proto = 'v4l2'; diff --git a/templates/main.html b/templates/main.html index cbc5016..92df18e 100644 --- a/templates/main.html +++ b/templates/main.html @@ -205,7 +205,11 @@ Camera Device - + + + + Camera Type +