'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'
])
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
# 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
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'
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
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
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})
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:
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
_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') + ')'
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]:
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']:
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')]
'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
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());