]> www.vanbest.org Git - motioneye-debian/commitdiff
major config refactorization; initial support for netcams
authorCalin Crisan <ccrisan@gmail.com>
Sat, 23 Aug 2014 10:45:13 +0000 (13:45 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Sat, 23 Aug 2014 10:45:13 +0000 (13:45 +0300)
src/config.py
src/handlers.py
src/mediafiles.py
src/mjpgclient.py
src/motionctl.py
src/remote.py
src/utils.py
static/css/main.css
static/js/main.js
static/js/ui.js
templates/main.html

index 2ea241a25b1d86499cd4a2559e88e7968ab6aed4..95c723f1d10c42b47c74028a3e1f9dd7a5554526 100644 (file)
@@ -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)
     
index 8f2f926382d764129cea8e7b8a508edf363f6d2f..5f5722f9024bc339ddf3648f0a9e25d1f3765d6a 100644 (file)
@@ -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': '&lt;' + utils.make_camera_url(local_config) + '&gt;',
+                            'name': '&lt;' + remote.make_camera_url(local_config) + '&gt;',
                             '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):
index 1b09d628fe26938e4e721ba1638edfac00decf6a..b3874b5e07bca37a3b83b0c61fd13dd8cf3aa11b 100644 (file)
@@ -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']
index 686be37f2faf09c54f66485f8e12ca62f2ea8de1..2c57ec7f237982bb4a4b04d069cb58a76148b718 100644 (file)
@@ -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})
             
index f29e23bde4f624cd73fc7a800503bf93cb9495e7..aab9a5b20fc9a912e3fee097a615d264848df0d7 100644 (file)
@@ -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
     
 
index e766816faabfec5caf3088420e5a21de753768f3..a85f8a4a912bac8acb06405a1ca88f740d2a5759 100644 (file)
@@ -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)
 
index 32dca3053ed1aa0191efcfc8d1abfcc935c5a92f..7c76f03182eb14f8ee65c54c3f41cdca2e0ce342 100644 (file)
@@ -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'}])
index 67c2262b29d452f00f4e7453e853faade5b831c3..cf84b41b27f1009435969f383e4645a0043753f9 100644 (file)
@@ -425,6 +425,11 @@ table.add-camera-dialog input[type=password] {
     width: 17em;
 }
 
+span#cameraMsgLabel {
+    color: red;
+    font-size: 0.7em;
+}
+
 
 div.media-dialog {
 }
index c12685375a459b10dd49e99d8ce30f50657c1614..9f43849a908e626ea982944bb9356ae096c49688 100644 (file)
@@ -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() {
                     '<td class="dialog-item-value"><select class="styled" id="deviceSelect"></select></td>' +
                     '<td><span class="help-mark" title="the device you wish to add to motionEye">?</span></td>' +
                 '</tr>' +
-                '<tr class="remote">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Host</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" class="styled" id="hostEntry" placeholder="e.g. 192.168.1.2"></td>' +
-                    '<td><span class="help-mark" title="the remote motionEye host (e.g. 192.168.1.2)">?</span></td>' +
+                '<tr class="motioneye netcam">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">URL</span></td>' +
+                    '<td class="dialog-item-value"><input type="text" class="styled" id="urlEntry" placeholder="http://example.com:8080/cams/..."></td>' +
+                    '<td><span class="help-mark" title="the camera URL (e.g. http://example.com:8080/cams/)">?</span></td>' +
                 '</tr>' +
-                '<tr class="remote">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Port</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" class="styled" id="portEntry" placeholder="e.g. 80"></td>' +
-                    '<td><span class="help-mark" title="the remote motionEye port (e.g. 80)">?</span></td>' +
-                '</tr>' +
-                '<tr class="remote">' +
+                '<tr class="motioneye netcam">' +
                     '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
                     '<td class="dialog-item-value"><input type="text" class="styled" id="usernameEntry" placeholder="username..."></td>' +
-                    '<td><span class="help-mark" title="the remote administrator\'s username">?</span></td>' +
+                    '<td><span class="help-mark" title="the username for the URL, if required (e.g. admin)">?</span></td>' +
                 '</tr>' +
-                '<tr class="remote">' +
+                '<tr class="motioneye netcam">' +
                     '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
                     '<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
-                    '<td><span class="help-mark" title="the remote administrator\'s password">?</span></td>' +
+                    '<td><span class="help-mark" title="the password for the URL, if required">?</span></td>' +
                 '</tr>' +
-                '<tr class="remote">' +
+                '<tr class="motioneye netcam">' +
                     '<td class="dialog-item-label"><span class="dialog-item-label">Camera</span></td>' +
-                    '<td class="dialog-item-value"><select class="styled" id="cameraSelect"></select></td>' +
-                    '<td><span class="help-mark" title="the remote camera you wish to add to motionEye">?</span></td>' +
+                    '<td class="dialog-item-value"><select class="styled" id="cameraSelect"></select><span id="cameraMsgLabel"></span></td>' +
+                    '<td><span class="help-mark" title="the camera you wish to add to motionEye">?</span></td>' +
                 '</tr>' +
             '</table>');
     
     /* 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 = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
         
         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('<option value="' + device.device_uri + '">' + device.name + '</option>');
+                deviceSelect.append('<option value="' + device.uri + '">' + device.name + '</option>');
             }
         });
         
-        deviceSelect.append('<option value="remote">Remote motionEye camera...</option>');
+        deviceSelect.append('<option value="netcam">Network camera...</option>');
+        deviceSelect.append('<option value="motioneye">Remote motionEye camera...</option>');
         
         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();
index a1e3ce0655704c09aff3c7a1461d16d5b10266e2..723697bc603d07a4db4fa0ae931cc406063ea9e7 100644 (file)
@@ -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;
index 7ead94723284a8ce339128ae1a3822c3f0664323..8a0337a02635f7eb41ad331cd863123520a863ad 100644 (file)
                         <td class="settings-item-value"><input type="checkbox" class="styled device" id="lightSwitchDetectSwitch"></td>
                         <td><span class="help-mark" title="enable this if you want sudden changes in light to not be treated as motion">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Automatic Brightness</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled device" id="autoBrightnessSwitch"></td>
-                        <td><span class="help-mark" title="enables software automatic brightness (not needed for most cameras)">?</span></td>
-                    </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Brightness</span></td>
                         <td class="settings-item-value"><input type="text" class="range styled device" id="brightnessSlider"></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Web Hook URL</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="webHookUrlEntry" placeholder="e.g. http://www.example.com/notify/"></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications" id="webHookUrlEntry" placeholder="e.g. http://example.com/notify/"></td>
                         <td><span class="help-mark" title="a URL to be requested when motion is detected; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">