]> www.vanbest.org Git - motioneye-debian/commitdiff
added RTSP support
authorCalin Crisan <ccrisan@gmail.com>
Sun, 9 Aug 2015 12:59:07 +0000 (15:59 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Sun, 9 Aug 2015 17:21:58 +0000 (20:21 +0300)
src/config.py
src/handlers.py
src/utils.py
src/v4l2ctl.py
static/js/main.js

index a786efa2bbc3a8984f31c2cf6e75935f9a9c2046..5c7d91cb7bacd6142785ab4e7cda23e338fe5efb 100644 (file)
@@ -57,7 +57,7 @@ _KNOWN_MOTION_OPTIONS = set([
     'snapshot_filename', 'snapshot_interval', 'stream_auth_method', 'stream_authentication', 'stream_localhost', 'stream_maxrate', 'stream_motion', 'stream_port', 'stream_quality',
     'target_dir', 'text_changes', 'text_double', 'text_left', 'text_right', 'threshold', 'videodevice', 'width',
     'webcam_localhost', 'webcam_port', 'webcam_maxrate', 'webcam_quality', 'webcam_motion', 'ffmpeg_cap_new', 'output_normal', 'output_motion', 'jpeg_filename', 'output_all', 'gap', 'locate',
-    'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive'
+    'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive', 'rtsp_uses_tcp'
 ])
 
 
@@ -503,8 +503,17 @@ def add_camera(device_details):
     elif proto == 'netcam':
         camera_config['netcam_url'] = device_details['url']
         camera_config['text_double'] = True
+        
         if device_details['username']:
             camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password']
+        
+        if device_details.get('camera_index') == 'udp':
+            camera_config['rtsp_uses_tcp'] = False
+        
+        if camera_config['netcam_url'].startswith('rtsp'):
+            camera_config['width'] = 640
+            camera_config['height'] = 480
+
         _set_default_motion_camera(camera_id, camera_config)
 
     else: # assuming mjpeg
@@ -721,8 +730,18 @@ def motion_camera_ui_to_dict(ui, old_config=None):
         # leave netcam_userpass unchanged
         data['netcam_keepalive'] = True
         data['netcam_tolerant_check'] = True
-
-        threshold = int(float(ui['frame_change_threshold']) * 640 * 480 / 100)
+        
+        if data.get('netcam_url', old_config.get('netcam_url', '')).startswith('rtsp'):
+            # motion uses the configured width and height for RTSP cameras
+            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)
+        
+        else: # width & height are not available for other netcams
+            threshold = int(float(ui['frame_change_threshold']) * 640 * 480 / 100)
 
     data['threshold'] = threshold
 
@@ -963,10 +982,19 @@ def motion_camera_dict_to_ui(data):
         ui['device_url'] = data['netcam_url']
         ui['proto'] = 'netcam'
 
-        # 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)
-    
+        # resolutions
+        if data['netcam_url'].startswith('rtsp'):
+            # motion uses the configured width and height for RTSP cameras
+            resolutions = utils.COMMON_RESOLUTIONS
+            ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
+            ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
+
+            threshold = data['threshold'] * 100.0 / (data['width'] * data['height'])
+
+        else: # width & height are not available for other 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'
@@ -1303,7 +1331,7 @@ def is_old_motion():
         
         if version.startswith('trunkREV'): # e.g. trunkREV599
             version = int(version[8:])
-            return version < _LAST_OLD_CONFIG_VERSIONS[0]
+            return version <= _LAST_OLD_CONFIG_VERSIONS[0]
         
         elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
             return False # all git versions are assumed to be new
@@ -1315,6 +1343,27 @@ def is_old_motion():
         return False
 
 
+def motion_rtsp_support():
+    import motionctl
+
+    try:
+        binary, version = motionctl.find_motion()  # @UnusedVariable
+        
+        if version.startswith('trunkREV'): # e.g. trunkREV599
+            version = int(version[8:])
+            if version > _LAST_OLD_CONFIG_VERSIONS[0]:
+                return ['tcp']
+        
+        elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
+            return ['tcp', 'udp'] # all git versions are assumed to support both transport protocols
+        
+        else: # stable release, should be in the format x.y.z
+            return []
+
+    except:
+        return []
+
+
 def invalidate():
     global _main_config_cache
     global _camera_config_cache
index 5921ac75c19c9705ff7e78119c475b4adfc1591c..120d48105a1d78791cfef5e5cd601237e9eb7a94 100644 (file)
@@ -497,6 +497,8 @@ class ConfigHandler(BaseHandler):
             remote.list(self.get_data(), on_response)
         
         elif proto == 'netcam':
+            scheme = self.get_data().get('scheme', 'http')
+
             def on_response(cameras=None, error=None):
                 if error:
                     self.finish_json({'error': error})
@@ -504,8 +506,15 @@ class ConfigHandler(BaseHandler):
                 else:
                     self.finish_json({'cameras': cameras})
             
-            utils.test_mjpeg_url(self.get_data(), auth_modes=['basic'], allow_jpeg=True, callback=on_response)
+            if scheme in ['http', 'https']:
+                utils.test_mjpeg_url(self.get_data(), auth_modes=['basic'], allow_jpeg=True, callback=on_response)
+                
+            elif config.motion_rtsp_support() and scheme == 'rtsp':
+                utils.test_rtsp_url(self.get_data(), callback=on_response)
                 
+            else:
+                on_response(error='protocol %s not supported' % scheme)
+
         elif proto == 'mjpeg':
             def on_response(cameras=None, error=None):
                 if error:
index 32eb0c0501f6c281f2b683c1b57199c05e71ee5f..d31893715f9c4293c56fcf3409d44012f4268818 100644 (file)
@@ -21,11 +21,13 @@ import hashlib
 import logging
 import os
 import re
+import socket
 import time
 import urllib
 import urlparse
 
 from tornado.httpclient import AsyncHTTPClient, HTTPRequest
+from tornado.iostream import IOStream
 
 import settings
 
@@ -40,6 +42,22 @@ except:
 _SIGNATURE_REGEX = re.compile('[^a-zA-Z0-9/?_.=&{}\[\]":, _-]')
 
 
+COMMON_RESOLUTIONS = [
+    (320, 240),
+    (640, 480),
+    (800, 480),
+    (1024, 576),
+    (1024, 768),
+    (1280, 720),
+    (1280, 800),
+    (1280, 960),
+    (1280, 1024),
+    (1440, 960),
+    (1440, 1024),
+    (1600, 1200)
+]
+
+
 def pretty_date_time(date_time, tzinfo=None, short=False):
     if date_time is None:
         return '('+  _('never') + ')'
@@ -329,6 +347,22 @@ def test_mjpeg_url(data, auth_modes, allow_jpeg, callback):
     called = [False]
     status_2xx = [False]
 
+    def do_request(on_response):
+        if data['username']:
+            auth = auth_modes[0]
+            
+        else:
+            auth = 'no'
+
+        logging.debug('testing (m)jpg netcam at %s using %s authentication' % (url, auth))
+
+        request = HTTPRequest(url, auth_username=username, auth_password=password, auth_mode=auth_modes.pop(0),
+                connect_timeout=settings.REMOTE_REQUEST_TIMEOUT, request_timeout=settings.REMOTE_REQUEST_TIMEOUT,
+                header_callback=on_header)
+
+        http_client = AsyncHTTPClient(force_instance=True)    
+        http_client.fetch(request, on_response)
+
     def on_header(header):
         header = header.lower()
         if header.startswith('content-type') and status_2xx[0]:
@@ -350,22 +384,6 @@ def test_mjpeg_url(data, auth_modes, allow_jpeg, callback):
             if m and int(m.group(1)) / 100 == 2:
                 status_2xx[0] = True
 
-    def do_request(on_response):
-        if data['username']:
-            auth = auth_modes[0]
-            
-        else:
-            auth = 'no'
-
-        logging.debug('testing netcam at %s using %s authentication' % (url, auth))
-
-        request = HTTPRequest(url, auth_username=username, auth_password=password, auth_mode=auth_modes.pop(0),
-                connect_timeout=settings.REMOTE_REQUEST_TIMEOUT, request_timeout=settings.REMOTE_REQUEST_TIMEOUT,
-                header_callback=on_header)
-
-        http_client = AsyncHTTPClient(force_instance=True)    
-        http_client.fetch(request, on_response)
-
     def on_response(response):
         if not called[0]:
             if response.code == 401 and auth_modes and data['username']:
@@ -382,6 +400,131 @@ def test_mjpeg_url(data, auth_modes, allow_jpeg, callback):
     do_request(on_response)
 
 
+def test_rtsp_url(data, callback):
+    import config
+    
+    data = dict(data)
+    data.setdefault('scheme', 'rtsp')
+    data.setdefault('host', '127.0.0.1')
+    data['port'] = data.get('port') or '554'
+    data.setdefault('uri', '')
+    data.setdefault('username', None)
+    data.setdefault('password', None)
+
+    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]
+    stream = None
+
+    def connect():
+        logging.debug('testing rtsp netcam at %s' % url)
+
+        stream = IOStream(socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0))
+        stream.set_close_callback(on_close)
+        stream.connect((data['host'], int(data['port'])), on_connect)
+        
+        return stream
+    
+    def on_connect():
+        if not stream:
+            return logging.error('failed to connect to rtsp netcam') 
+
+        logging.debug('connected to rtsp netcam')
+        
+        stream.write('\r\n'.join([
+            'OPTIONS %s RTSP/1.0' % url.encode('utf8'),
+            'CSeq: 1',
+            'User-Agent: motionEye',
+            '',
+            ''
+        ]))
+
+        seek_rtsp()
+        
+    def seek_rtsp():
+        if check_error():
+            return
+
+        stream.read_until_regex('RTSP/1.0 \d+ ', on_rtsp)
+
+    def on_rtsp(data):
+        if data.endswith('200 '):
+            seek_server()
+
+        else:
+            handle_error('rtsp netcam returned erroneous response: %s' % data)
+        
+    def seek_server():
+        if check_error():
+            return
+
+        stream.read_until_regex('Server: .*', on_server)
+
+    def on_server(data):
+        identifier = re.findall('Server: (.*)', data)[0].strip()
+        logging.debug('rtsp netcam identifier is "%s"' % identifier)
+
+        handle_success(identifier)
+
+    def on_close():
+        if called[0]:
+            return
+        if not check_error():
+            handle_error('connection closed')
+
+    def handle_success(identifier):
+        if called[0]:
+            return
+        
+        called[0] = True
+        cameras = []
+        rtsp_support = config.motion_rtsp_support()
+        if 'udp' in rtsp_support:
+            cameras.append({'id': 'udp', 'name': '%s RTSP/UDP Camera' % identifier})
+        
+        if 'tcp' in rtsp_support:
+            cameras.append({'id': 'tcp', 'name': '%s RTSP/TCP Camera' % identifier})
+
+        callback(cameras)
+
+    def handle_error(e):
+        if called[0]:
+            return
+        
+        called[0] = True
+        logging.error('rtsp client error: %s' % unicode(e))
+
+        try:
+            stream.close()
+        
+        except:
+            pass
+        
+        callback(error=unicode(e))
+
+    def check_error():
+        error = getattr(stream, 'error', None)
+        if error and getattr(error, 'errno', None) != 0:
+            handle_error(error.strerror)
+            return True
+
+        if stream and stream.socket is None:
+            logging.warning('rtsp client connection is closed')
+            handle_error('connection closed')
+            stream.close()
+
+            return True
+        
+        return False
+
+    stream = connect()
+
+
 def compute_signature(method, uri, body, key):
     parts = list(urlparse.urlsplit(uri))
     query = [q for q in urlparse.parse_qsl(parts[3], keep_blank_values=True) if (q[0] != '_signature')]
index b466e2b07c4768fa4142dc8dea344c2b43d989e5..08dab1ab4c4e486e5097b89cc93502fa905e6fc6 100644 (file)
@@ -178,21 +178,10 @@ def list_resolutions(device):
                 'device': device, 'width': width, 'height': height})
     
     if not resolutions:
-        logging.debug('no resolutions found for device %(device)s, adding the defaults' % {'device': device})
-        
+        logging.debug('no resolutions found for device %(device)s, using common values' % {'device': device})
+
         # no resolution returned by v4l2-ctl call, add common default resolutions
-        resolutions.add((320, 240))
-        resolutions.add((640, 480))
-        resolutions.add((800, 480))
-        resolutions.add((1024, 576))
-        resolutions.add((1024, 768))
-        resolutions.add((1280, 720))
-        resolutions.add((1280, 800))
-        resolutions.add((1280, 960))
-        resolutions.add((1280, 1024))
-        resolutions.add((1440, 960))
-        resolutions.add((1440, 1024))
-        resolutions.add((1600, 1200))
+        resolutions += utils.COMMON_RESOLUTIONS
 
     resolutions = list(sorted(resolutions, key=lambda r: (r[0], r[1])))
     _resolutions_cache[device] = resolutions
index beabd444f1587634c57d44d59d42ae44524d719e..af2b7e3f40b87f1df47ddd59e9d6c170481b3f10 100644 (file)
@@ -2910,6 +2910,7 @@ function runAddCameraDialog() {
                 data.username = usernameEntry.val();
                 data.password = passwordEntry.val();
                 data.proto = 'netcam';
+                data.camera_index = addCameraSelect.val();
             }
             else if (typeSelect.val() == 'mjpeg') {
                 data = splitCameraUrl(urlEntry.val());