From 54f89266a3fd18fa34370128dff6be1aea45bef7 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Thu, 25 May 2017 17:22:54 -0600 Subject: [PATCH 01/34] Moved to Python3, removed goodthread --- rtsp.py | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/rtsp.py b/rtsp.py index 0eb7029..9bc2928 100644 --- a/rtsp.py +++ b/rtsp.py @@ -6,11 +6,13 @@ # Some text google-translated from Chinese # A bit adopted to be import'able # -jno +# +#Ported to Python3, removed GoodThread +# -killian441 -import sys, re, socket, time, datetime, traceback -import exceptions, urlparse +import sys, re, socket, threading, time, datetime, traceback +import urllib.parse from optparse import OptionParser -import util DEFAULT_SERVER_PORT = 554 TRANSPORT_TYPE_LIST = [] @@ -44,7 +46,7 @@ # Colored Output in Console #-------------------------------------------------------------------------- DEBUG = False -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = range(90, 98) +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) def COLOR_STR(msg, color=WHITE): return '\033[%dm%s\033[0m'%(color, msg) @@ -53,20 +55,21 @@ def PRINT(msg, color=WHITE, out=sys.stdout): out.write(COLOR_STR(msg, color) + '\n') #-------------------------------------------------------------------------- -class RTSPError(exceptions.Exception): pass +class RTSPError(Exception): pass class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass -class RTSPClient(util.GoodThread): +class RTSPClient(threading.Thread): def __init__(self, url, dest_ip=''): global CUR_RANGE - util.GoodThread.__init__(self) + threading.Thread.__init__(self) self._sock = None self._orig_url = url self._cseq = 0 self._session_id= '' self._cseq_map = {} # {CSeq:Method} mapping self._dest_ip = dest_ip + self.running = True self.playing = False self.location = '' self.response_buf = [] @@ -103,25 +106,26 @@ def cache(self, s=None): def close(self): if not self.closed: self.closed = True - self.stop() + self.running = False self.playing = False self._sock.close() def run(self): try: - while not self.stopped(): + while self.running: self.response = msg = self.recv_msg() if msg.startswith('RTSP'): self._process_response(msg) elif msg.startswith('ANNOUNCE'): self._process_announce(msg) - except Exception, e: + except Exception as e: raise RTSPError('Run time error: %s' % e) + self.running = False self.close() def _parse_url(http://github.com/self,%20url): '''Resolve url, return (ip, port, target) triplet''' - parsed = urlparse.urlparse(url) + parsed = urllib.parse.urlparse(url) scheme = parsed.scheme.lower() ip = parsed.hostname port = parsed.port and int(parsed.port) or DEFAULT_SERVER_PORT @@ -146,7 +150,7 @@ def _connect_server(self): try: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._server_ip, self._server_port)) - except socket.error, e: + except socket.error as e: raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._server_ip, self._server_port)) def _update_dest_ip(self): @@ -158,12 +162,12 @@ def _update_dest_ip(self): def recv_msg(self): '''A complete response message or an ANNOUNCE notification message is received''' try: - while not (self.stopped() or HEADER_END_STR in self.cache()): + while not (not self.running or HEADER_END_STR in self.cache()): more = self._sock.recv(2048) if not more: break - self.cache(more) - except socket.error, e: + self.cache(more.decode()) + except socket.error as e: RTSPNetError('Receive data error: %s' % e) msg = '' @@ -256,14 +260,14 @@ def _sendmsg(self, method, url, headers): headers['CSeq'] = str(cseq) if self._session_id: headers['Session'] = self._session_id - for (k, v) in headers.items(): + for (k, v) in list(headers.items()): msg += END_OF_LINE + '%s: %s'%(k, str(v)) msg += HEADER_END_STR # End headers if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: PRINT(self._get_time_str() + END_OF_LINE + msg) try: - self._sock.send(msg) - except socket.error, e: + self._sock.send(msg.encode()) + except socket.error as e: PRINT('Send msg error: %s'%e, RED) raise RTSPNetError(e) @@ -306,7 +310,7 @@ def do_pause(self): def do_teardown(self): self._sendmsg('TEARDOWN', self._orig_url, {}) - self.stop() + self.running = False def do_options(self): self._sendmsg('OPTIONS', self._orig_url, {}) @@ -316,7 +320,7 @@ def do_get_parameter(self): def send_heart_beat_msg(self): '''Timed sending GET_PARAMETER message keep alive''' - if not self.stopped(): + if not self.running: self.do_get_parameter() threading.Timer(HEARTBEAT_INTERVAL, self.send_heart_beat_msg).start() @@ -353,7 +357,7 @@ def input_cmd(): readline.set_completer_delims(' \t\n') readline.parse_and_bind("tab: complete") readline.set_completer(complete) - cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) + cmd = input(COLOR_STR('Input Command # ', CYAN)) PRINT('') # add one line return cmd #----------------------------------------------------------------------- @@ -399,18 +403,18 @@ def main(url, dest_ip): try: rtsp.do_describe() - while rtsp.location and not rtsp.stopped(): + while rtsp.location and rtsp.running: if rtsp.playing: cmd = input_cmd() exec_cmd(rtsp, cmd) # 302 redirect to re-establish chain - if rtsp.stopped() and rtsp.location: + if not rtsp.running and rtsp.location: rtsp = RTSPClient(rtsp.location) rtsp.do_describe() time.sleep(0.5) except KeyboardInterrupt: rtsp.do_teardown() - print '\n^C received, Exit.' + print('\n^C received, Exit.') def play_ctrl_help(): help = COLOR_STR('In running, you can control play by input "' \ From b65cd9b6e2a2f0e9b50ea1f28e968a35e0099263 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 26 May 2017 13:41:19 -0600 Subject: [PATCH 02/34] Modified imports for backwards compatibility, added beginnings of an authentication protocol --- rtsp.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/rtsp.py b/rtsp.py index 9bc2928..3c64beb 100644 --- a/rtsp.py +++ b/rtsp.py @@ -11,8 +11,15 @@ # -killian441 import sys, re, socket, threading, time, datetime, traceback -import urllib.parse from optparse import OptionParser +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse # for python < 3.0 +try: + from hashlib import md5 +except ImportError: + from md5 import md5 # for python < 2.5 DEFAULT_SERVER_PORT = 554 TRANSPORT_TYPE_LIST = [] @@ -125,7 +132,7 @@ def run(self): def _parse_url(http://github.com/self,%20url): '''Resolve url, return (ip, port, target) triplet''' - parsed = urllib.parse.urlparse(url) + parsed = urlparse(url) scheme = parsed.scheme.lower() ip = parsed.hostname port = parsed.port and int(parsed.port) or DEFAULT_SERVER_PORT @@ -195,14 +202,18 @@ def _get_time_str(self): def _process_response(self, msg): '''Process the response message''' status, headers, body = self._parse_response(msg) + print(status,headers) rsp_cseq = int(headers['cseq']) if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': PRINT(self._get_time_str() + '\n' + msg) - if status == 302: + if status == 401: + #self._add_auth(headers['www-authenticate']) + self.do_teardown() + elif status == 302: self.location = headers['location'] - if status != 200: + elif status != 200: self.do_teardown() - if self._cseq_map[rsp_cseq] == 'DESCRIBE': + elif self._cseq_map[rsp_cseq] == 'DESCRIBE': #Implies status 200 track_id_str = self._parse_track_id(body) self.do_setup(track_id_str) elif self._cseq_map[rsp_cseq] == 'SETUP': From c073e769eba62b8ac3df723858780f34dfab013e Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 26 May 2017 13:46:37 -0600 Subject: [PATCH 03/34] Passing headers to all sendmsg commands --- rtsp.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/rtsp.py b/rtsp.py index 3c64beb..1cceb75 100644 --- a/rtsp.py +++ b/rtsp.py @@ -202,7 +202,6 @@ def _get_time_str(self): def _process_response(self, msg): '''Process the response message''' status, headers, body = self._parse_response(msg) - print(status,headers) rsp_cseq = int(headers['cseq']) if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': PRINT(self._get_time_str() + '\n' + msg) @@ -295,8 +294,7 @@ def _get_transport_type(self): transport_str += TRANSPORT_TYPE_MAP[t]%(ip_type, self._dest_ip, CLIENT_PORT_RANGE) return transport_str - def do_describe(self): - headers = {} + def do_describe(self, headers={}): headers['Accept'] = 'application/sdp' if ENABLE_ARQ: headers['x-Retrans'] = 'yes' @@ -305,29 +303,27 @@ def do_describe(self): if NAT_IP_PORT: headers['x-NAT'] = NAT_IP_PORT self._sendmsg('DESCRIBE', self._orig_url, headers) - def do_setup(self, track_id_str=''): - headers = {} + def do_setup(self, headers={}, track_id_str=''): headers['Transport'] = self._get_transport_type() self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) - def do_play(self, range='npt=end-', scale=1): - headers = {} + def do_play(self, headers={}, range='npt=end-', scale=1): headers['Range'] = range headers['Scale'] = scale self._sendmsg('PLAY', self._orig_url, headers) - def do_pause(self): - self._sendmsg('PAUSE', self._orig_url, {}) + def do_pause(self, headers={}): + self._sendmsg('PAUSE', self._orig_url, headers) - def do_teardown(self): - self._sendmsg('TEARDOWN', self._orig_url, {}) + def do_teardown(self, headers={}): + self._sendmsg('TEARDOWN', self._orig_url, headers) self.running = False - def do_options(self): - self._sendmsg('OPTIONS', self._orig_url, {}) + def do_options(self, headers={}): + self._sendmsg('OPTIONS', self._orig_url, headers) - def do_get_parameter(self): - self._sendmsg('GET_PARAMETER', self._orig_url, {}) + def do_get_parameter(self, headers={}): + self._sendmsg('GET_PARAMETER', self._orig_url, headers) def send_heart_beat_msg(self): '''Timed sending GET_PARAMETER message keep alive''' From c60a6d501128c22dd228d60baab86dce149b9c8a Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 26 May 2017 15:50:50 -0600 Subject: [PATCH 04/34] parsed url object is part of class now, also added simple digest auth --- rtsp.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/rtsp.py b/rtsp.py index 1cceb75..94d1055 100644 --- a/rtsp.py +++ b/rtsp.py @@ -10,7 +10,7 @@ #Ported to Python3, removed GoodThread # -killian441 -import sys, re, socket, threading, time, datetime, traceback +import ast, datetime, re, socket, sys, threading, time, traceback from optparse import OptionParser try: from urllib.parse import urlparse @@ -81,8 +81,10 @@ def __init__(self, url, dest_ip=''): self.location = '' self.response_buf = [] self.response = None - self._scheme, self._server_ip, self._server_port, self._target = self._parse_http://github.com/url(url) - if '.sdp' not in self._target.lower(): + #self._scheme, self._server_ip, self._server_port, self._target = self._parse_http://github.com/url(url) + self._parsed_url = self._parse_http://github.com/url(url) + self._server_port = self._parsed_url.port or DEFAULT_SERVER_PORT + if '.sdp' not in self._parsed_url.path.lower(): CUR_RANGE = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() self._update_dest_ip() @@ -149,16 +151,16 @@ def _parse_url(http://github.com/self,%20url): if not ip or not target: raise RTSPURLError('Invalid url: %s (host="%s" port=%u target="%s")' % (url, ip, port, target)) - - return scheme, ip, port, target + #return scheme, ip, port, target + return parsed def _connect_server(self): '''Connect to the server and create a socket''' try: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sock.connect((self._server_ip, self._server_port)) + self._sock.connect((self._parsed_url.hostname, self._server_port)) except socket.error as e: - raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._server_ip, self._server_port)) + raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._parsed_url.hostname, self._server_port)) def _update_dest_ip(self): '''If DEST_IP is not specified, the same IP is used by default with RTSP''' @@ -180,14 +182,57 @@ def recv_msg(self): msg = '' if self.cache(): tmp = self.cache() - (msg, tmp) = tmp.split(HEADER_END_STR, 1) content_length = self._get_content_length(msg) msg += HEADER_END_STR + tmp[:content_length] - self.set_cache(tmp[content_length:]) return msg + def _add_auth(self, msg): + '''Authentication request string, everything after www-authentication''' + #TODO: this is too simplistic and will fail if more than one method is acceptable, among other issues + if msg.lower().startswith('basic'): + pass + elif msg.lower().startswith('digest '): + mod_msg = '{'+msg[7:].replace('=',':')+'}' + mod_msg = mod_msg.replace('realm','"realm"') + mod_msg = mod_msg.replace('nonce','"nonce"') + msg_dict = ast.literal_eval(mod_msg) + response = self._auth_digest(msg_dict) + auth_string = 'Digest ' \ + 'username="{}", ' \ + 'algorithm="MD5", ' \ + 'realm="{}", ' \ + 'nonce="{}", ' \ + 'uri="{}", ' \ + 'response="{}"'.format( + self._parsed_url.username, + msg_dict['realm'], + msg_dict['nonce'], + self._parsed_url.path, + response) + return auth_string + else: # Some other failure + PRINT('Authentication failure') + self.do_teardown() + + def _auth_digest(self, auth_parameters): + '''Creates a response string for digest authorization, only works with MD5 at the moment''' + #TODO expand to more than MD5 + if self._parsed_url.username: + HA1 = md5("{}:{}:{}".format(self._parsed_url.username, + auth_parameters['realm'], + self._parsed_url.password).encode()).hexdigest() + HA2 = md5("{}:{}".format(self._cseq_map[self._cseq], + self._parsed_url.path).encode()).hexdigest() + response = md5("{}:{}:{}".format(HA1, + auth_parameters['nonce'], + HA2).encode()).hexdigest() + return response + else: + PRINT('Authentication failure') + self.do_teardown() + def _get_content_length(self, msg): '''Content-length is parsed from the message''' m = re.search(r'[Cc]ontent-length:\s?(?P\d+)', msg, re.S) @@ -206,8 +251,10 @@ def _process_response(self, msg): if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': PRINT(self._get_time_str() + '\n' + msg) if status == 401: - #self._add_auth(headers['www-authenticate']) - self.do_teardown() + auth_string = self._add_auth(headers['www-authenticate']) + if self._cseq_map[self._cseq] == 'DESCRIBE': + self.do_describe({'Authorization':auth_string}) + #self.do_teardown() elif status == 302: self.location = headers['location'] elif status != 200: @@ -303,11 +350,11 @@ def do_describe(self, headers={}): if NAT_IP_PORT: headers['x-NAT'] = NAT_IP_PORT self._sendmsg('DESCRIBE', self._orig_url, headers) - def do_setup(self, headers={}, track_id_str=''): + def do_setup(self, track_id_str='', headers={}): headers['Transport'] = self._get_transport_type() self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) - def do_play(self, headers={}, range='npt=end-', scale=1): + def do_play(self, range='npt=end-', scale=1, headers={}): headers['Range'] = range headers['Scale'] = scale self._sendmsg('PLAY', self._orig_url, headers) From 4e0a623af822d36065f260542da9a4a666e64f04 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sat, 27 May 2017 21:56:50 -0600 Subject: [PATCH 05/34] Added more robust Autherization headers --- rtsp.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/rtsp.py b/rtsp.py index 94d1055..58ac22f 100644 --- a/rtsp.py +++ b/rtsp.py @@ -70,6 +70,7 @@ class RTSPClient(threading.Thread): def __init__(self, url, dest_ip=''): global CUR_RANGE threading.Thread.__init__(self) + self._auth = None self._sock = None self._orig_url = url self._cseq = 0 @@ -211,7 +212,7 @@ def _add_auth(self, msg): msg_dict['nonce'], self._parsed_url.path, response) - return auth_string + self._auth = auth_string else: # Some other failure PRINT('Authentication failure') self.do_teardown() @@ -251,9 +252,8 @@ def _process_response(self, msg): if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': PRINT(self._get_time_str() + '\n' + msg) if status == 401: - auth_string = self._add_auth(headers['www-authenticate']) - if self._cseq_map[self._cseq] == 'DESCRIBE': - self.do_describe({'Authorization':auth_string}) + self._add_auth(headers['www-authenticate']) + self.do_replay_request() #self.do_teardown() elif status == 302: self.location = headers['location'] @@ -342,6 +342,8 @@ def _get_transport_type(self): return transport_str def do_describe(self, headers={}): + if self._auth: + headers['Authorization'] = self._auth headers['Accept'] = 'application/sdp' if ENABLE_ARQ: headers['x-Retrans'] = 'yes' @@ -351,27 +353,55 @@ def do_describe(self, headers={}): self._sendmsg('DESCRIBE', self._orig_url, headers) def do_setup(self, track_id_str='', headers={}): + if self._auth: + headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) def do_play(self, range='npt=end-', scale=1, headers={}): + if self._auth: + headers['Authorization'] = self._auth headers['Range'] = range headers['Scale'] = scale self._sendmsg('PLAY', self._orig_url, headers) def do_pause(self, headers={}): + if self._auth: + headers['Authorization'] = self._auth self._sendmsg('PAUSE', self._orig_url, headers) def do_teardown(self, headers={}): + if self._auth: + headers['Authorization'] = self._auth self._sendmsg('TEARDOWN', self._orig_url, headers) self.running = False def do_options(self, headers={}): + if self._auth: + headers['Authorization'] = self._auth self._sendmsg('OPTIONS', self._orig_url, headers) def do_get_parameter(self, headers={}): + if self._auth: + headers['Authorization'] = self._auth self._sendmsg('GET_PARAMETER', self._orig_url, headers) + def do_replay_request(self, headers={}): + if self._cseq_map[self._cseq] == 'DESCRIBE': + self.do_describe() + elif self._cseq_map[self._cseq] == 'SETUP': + self.do_setup() + elif self._cseq_map[self._cseq] == 'PLAY': + self.do_play() + elif self._cseq_map[self._cseq] == 'PAUSE': + self.do_pause() + elif self._cseq_map[self._cseq] == 'TEARDOWN': + self.do_teardown() + elif self._cseq_map[self._cseq] == 'OPTIONS': + self.do_options() + elif self._cseq_map[self._cseq] == 'GET_PARAMETER': + self.do_get_parameter() + def send_heart_beat_msg(self): '''Timed sending GET_PARAMETER message keep alive''' if not self.running: From 8c611b7e355a3fce23aaf0d5af1cb308f605b334 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sat, 27 May 2017 22:40:34 -0600 Subject: [PATCH 06/34] Minor bug fixes --- rtsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.py b/rtsp.py index 58ac22f..d93a398 100644 --- a/rtsp.py +++ b/rtsp.py @@ -236,7 +236,7 @@ def _auth_digest(self, auth_parameters): def _get_content_length(self, msg): '''Content-length is parsed from the message''' - m = re.search(r'[Cc]ontent-length:\s?(?P\d+)', msg, re.S) + m = re.search(r'content-length:\s?(?P\d+)', msg.lower(), re.S) return (m and int(m.group('len'))) or 0 def _get_time_str(self): @@ -487,7 +487,7 @@ def main(url, dest_ip): try: rtsp.do_describe() - while rtsp.location and rtsp.running: + while rtsp.running: if rtsp.playing: cmd = input_cmd() exec_cmd(rtsp, cmd) From fe3fada0a5038e7dba452c46d17b0f8037b56dc8 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sun, 28 May 2017 22:09:35 -0600 Subject: [PATCH 07/34] Raise exception on authentication error --- rtsp.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rtsp.py b/rtsp.py index d93a398..9ae72f0 100644 --- a/rtsp.py +++ b/rtsp.py @@ -11,15 +11,12 @@ # -killian441 import ast, datetime, re, socket, sys, threading, time, traceback +from hashlib import md5 from optparse import OptionParser try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse # for python < 3.0 -try: - from hashlib import md5 -except ImportError: - from md5 import md5 # for python < 2.5 DEFAULT_SERVER_PORT = 554 TRANSPORT_TYPE_LIST = [] @@ -214,8 +211,8 @@ def _add_auth(self, msg): response) self._auth = auth_string else: # Some other failure - PRINT('Authentication failure') self.do_teardown() + raise RTSPError('Authentication failure') def _auth_digest(self, auth_parameters): '''Creates a response string for digest authorization, only works with MD5 at the moment''' @@ -231,8 +228,8 @@ def _auth_digest(self, auth_parameters): HA2).encode()).hexdigest() return response else: - PRINT('Authentication failure') self.do_teardown() + raise RTSPError('Authentication required, no username provided') def _get_content_length(self, msg): '''Content-length is parsed from the message''' @@ -251,7 +248,7 @@ def _process_response(self, msg): rsp_cseq = int(headers['cseq']) if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': PRINT(self._get_time_str() + '\n' + msg) - if status == 401: + if status == 401 and not self._auth: self._add_auth(headers['www-authenticate']) self.do_replay_request() #self.do_teardown() From a716ea6b8b7ae12f026057b6a31f32aa53c84e61 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 09:56:45 -0600 Subject: [PATCH 08/34] Don't send username/password in url request --- rtsp.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/rtsp.py b/rtsp.py index 9ae72f0..332429e 100644 --- a/rtsp.py +++ b/rtsp.py @@ -67,21 +67,24 @@ class RTSPClient(threading.Thread): def __init__(self, url, dest_ip=''): global CUR_RANGE threading.Thread.__init__(self) - self._auth = None - self._sock = None - self._orig_url = url - self._cseq = 0 - self._session_id= '' - self._cseq_map = {} # {CSeq:Method} mapping - self._dest_ip = dest_ip - self.running = True - self.playing = False - self.location = '' + self._auth = None + self._sock = None + self._cseq = 0 + self._session_id = '' + self._cseq_map = {} # {CSeq:Method} mapping + self._dest_ip = dest_ip + self.running = True + self.playing = False + self.location = '' self.response_buf = [] - self.response = None + self.response = None #self._scheme, self._server_ip, self._server_port, self._target = self._parse_http://github.com/url(url) - self._parsed_url = self._parse_http://github.com/url(url) + self._parsed_url = self._parse_http://github.com/url(url) self._server_port = self._parsed_url.port or DEFAULT_SERVER_PORT + self._orig_url = self._parsed_url.scheme + "://" + \ + self._parsed_url.hostname + \ + ":" + str(self._server_port) + \ + self._parsed_url.path if '.sdp' not in self._parsed_url.path.lower(): CUR_RANGE = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() From 6406416ddb64dc0a8ceae5f27bebcc4fd7f0dc4c Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 10:50:00 -0600 Subject: [PATCH 09/34] Formatting --- rtsp.py | 82 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/rtsp.py b/rtsp.py index 332429e..4f49b15 100644 --- a/rtsp.py +++ b/rtsp.py @@ -2,7 +2,7 @@ #-*- coding: UTF-8 -*- # Date: 2015-04-09 # -# Stolen here: https://github.com/js2854/python-rtsp-client +# Original project here: https://github.com/js2854/python-rtsp-client # Some text google-translated from Chinese # A bit adopted to be import'able # -jno @@ -12,7 +12,6 @@ import ast, datetime, re, socket, sys, threading, time, traceback from hashlib import md5 -from optparse import OptionParser try: from urllib.parse import urlparse except ImportError: @@ -27,10 +26,10 @@ PING = False TRANSPORT_TYPE_MAP = { - 'ts_over_tcp' : 'MP2T/TCP;%s;interleaved=0-1, ', - 'rtp_over_tcp' : 'MP2T/RTP/TCP;%s;interleaved=0-1, ', - 'ts_over_udp' : 'MP2T/UDP;%s;destination=%s;client_port=%s, ', - 'rtp_over_udp' : 'MP2T/RTP/UDP;%s;destination=%s;client_port=%s, ' + 'ts_over_tcp' : 'MP2T/TCP;%s;interleaved=0-1, ', + 'rtp_over_tcp' : 'MP2T/RTP/TCP;%s;interleaved=0-1, ', + 'ts_over_udp' : 'MP2T/UDP;%s;destination=%s;client_port=%s, ', + 'rtp_over_udp' : 'MP2T/RTP/UDP;%s;destination=%s;client_port=%s, ' } RTSP_VERSION = 'RTSP/1.0' @@ -78,7 +77,6 @@ def __init__(self, url, dest_ip=''): self.location = '' self.response_buf = [] self.response = None - #self._scheme, self._server_ip, self._server_port, self._target = self._parse_http://github.com/url(url) self._parsed_url = self._parse_http://github.com/url(url) self._server_port = self._parsed_url.port or DEFAULT_SERVER_PORT self._orig_url = self._parsed_url.scheme + "://" + \ @@ -148,10 +146,12 @@ def _parse_url(http://github.com/self,%20url): if not scheme: raise RTSPURLError('Bad URL "%s"' % url) if scheme not in ('rtsp',): # 'rtspu'): - raise RTSPURLError('Unsupported scheme "%s" in URL "%s"' % (scheme, url)) + raise RTSPURLError('Unsupported scheme "%s" \ + in URL "%s"' % (scheme, url)) if not ip or not target: - raise RTSPURLError('Invalid url: %s (host="%s" port=%u target="%s")' % - (url, ip, port, target)) + raise RTSPURLError('Invalid url: %s (host="%s" \ + port=%u target="%s")' % + (url, ip, port, target)) #return scheme, ip, port, target return parsed @@ -161,16 +161,19 @@ def _connect_server(self): self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._parsed_url.hostname, self._server_port)) except socket.error as e: - raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._parsed_url.hostname, self._server_port)) + raise RTSPNetError('socket error: %s [%s:%d]' % + (e, self._parsed_url.hostname, self._server_port)) def _update_dest_ip(self): - '''If DEST_IP is not specified, the same IP is used by default with RTSP''' + '''If DEST_IP is not specified, + the same IP is used by default with RTSP''' if not self._dest_ip: self._dest_ip = self._sock.getsockname()[0] PRINT('DEST_IP: %s\n' % self._dest_ip, CYAN) def recv_msg(self): - '''A complete response message or an ANNOUNCE notification message is received''' + '''A complete response message or + an ANNOUNCE notification message is received''' try: while not (not self.running or HEADER_END_STR in self.cache()): more = self._sock.recv(2048) @@ -190,8 +193,10 @@ def recv_msg(self): return msg def _add_auth(self, msg): - '''Authentication request string, everything after www-authentication''' - #TODO: this is too simplistic and will fail if more than one method is acceptable, among other issues + '''Authentication request string, + everything after www-authentication''' + #TODO: this is too simplistic and will fail if more than one method + # is acceptable, among other issues if msg.lower().startswith('basic'): pass elif msg.lower().startswith('digest '): @@ -218,14 +223,17 @@ def _add_auth(self, msg): raise RTSPError('Authentication failure') def _auth_digest(self, auth_parameters): - '''Creates a response string for digest authorization, only works with MD5 at the moment''' + '''Creates a response string for digest authorization, only works + with MD5 at the moment''' #TODO expand to more than MD5 if self._parsed_url.username: HA1 = md5("{}:{}:{}".format(self._parsed_url.username, auth_parameters['realm'], - self._parsed_url.password).encode()).hexdigest() + self._parsed_url.password).encode() + ).hexdigest() HA2 = md5("{}:{}".format(self._cseq_map[self._cseq], - self._parsed_url.path).encode()).hexdigest() + self._parsed_url.path).encode() + ).hexdigest() response = md5("{}:{}:{}".format(HA1, auth_parameters['nonce'], HA2).encode()).hexdigest() @@ -331,14 +339,17 @@ def _sendmsg(self, method, url, headers): def _get_transport_type(self): '''The Transport string parameter that is required to get SETUP''' transport_str = '' - ip_type = 'unicast' #if IPAddress(DEST_IP).is_unicast() else 'multicast' + ip_type = 'unicast' #TODO: if IPAddress(DEST_IP).is_unicast() + # else 'multicast' for t in TRANSPORT_TYPE_LIST: if t not in TRANSPORT_TYPE_MAP: raise RTSPError('Error param: %s' % t) if t.endswith('tcp'): transport_str += TRANSPORT_TYPE_MAP[t]%ip_type else: - transport_str += TRANSPORT_TYPE_MAP[t]%(ip_type, self._dest_ip, CLIENT_PORT_RANGE) + transport_str += TRANSPORT_TYPE_MAP[t]%(ip_type, + self._dest_ip, + CLIENT_PORT_RANGE) return transport_str def do_describe(self, headers={}): @@ -406,7 +417,8 @@ def send_heart_beat_msg(self): '''Timed sending GET_PARAMETER message keep alive''' if not self.running: self.do_get_parameter() - threading.Timer(HEARTBEAT_INTERVAL, self.send_heart_beat_msg).start() + threading.Timer(HEARTBEAT_INTERVAL, + self.send_heart_beat_msg).start() def ping(self, timeout=0.01): '''No exceptions == service available''' @@ -419,6 +431,7 @@ def ping(self, timeout=0.01): # Input with autocompletion #----------------------------------------------------------------------- import readline +from optparse import OptionParser COMMANDS = ( 'backward', 'begin', @@ -501,26 +514,31 @@ def main(url, dest_ip): print('\n^C received, Exit.') def play_ctrl_help(): - help = COLOR_STR('In running, you can control play by input "' \ - +'forward", "backward", "begin", "live", "pause"\n', MAGENTA) - help += COLOR_STR('or "play" with "range" and "scale" parameter, ' \ - +'such as "play range:npt=beginning- scale:2"\n', MAGENTA) - help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to quit\n', MAGENTA) + help = COLOR_STR('In running, you can control play by input "forward"' \ + +', "backward", "begin", "live", "pause"\n', MAGENTA) + help += COLOR_STR('or "play" with "range" and "scale" parameter, such ' \ + +'as "play range:npt=beginning- scale:2"\n', MAGENTA) + help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to ' \ + +'quit\n', MAGENTA) return help if __name__ == '__main__': usage = COLOR_STR('%prog [options] url\n\n', GREEN) + play_ctrl_help() parser = OptionParser(usage=usage) - parser.add_option('-t', '--transport', dest='transport', default='rtp_over_udp', - help='Set transport type when SETUP: ts_over_tcp, ts_over_udp, ' - +' rtp_over_tcp, rtp_over_udp [default]') + parser.add_option('-t', '--transport', dest='transport', + default='rtp_over_udp', + help='Set transport type when SETUP: ts_over_tcp, ' + +'ts_over_udp, rtp_over_tcp, rtp_over_udp[default]') parser.add_option('-d', '--dest_ip', dest='dest_ip', - help='Set dest ip of udp data transmission, default use same ip with rtsp') + help='Set dest ip of udp data transmission, default ' + +'use same ip with rtsp') parser.add_option('-p', '--client_port', dest='client_port', - help='Set client port range when SETUP of udp, default is "10014-10015"') + help='Set client port range when SETUP of udp, default ' + +'is "10014-10015"') parser.add_option('-n', '--nat', dest='nat', - help='Add "x-NAT" when DESCRIBE, arg format "192.168.1.100:20008"') + help='Add "x-NAT" when DESCRIBE, arg format ' + +'"192.168.1.100:20008"') parser.add_option('-r', '--arq', dest='arq', action="http://github.com/store_true", help='Add "x-Retrans:yes" when DESCRIBE') parser.add_option('-f', '--fec', dest='fec', action="http://github.com/store_true", From 73b0b2fb5083b04b25fc0a80ab01acbe1842b71c Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 11:34:18 -0600 Subject: [PATCH 10/34] Added callback function to RSTPClient rather instead of PRINT, more formatting --- rtsp.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/rtsp.py b/rtsp.py index 4f49b15..b6763e4 100644 --- a/rtsp.py +++ b/rtsp.py @@ -7,7 +7,8 @@ # A bit adopted to be import'able # -jno # -#Ported to Python3, removed GoodThread +# Date: 2017-05-30 +# Ported to Python3, removed GoodThread # -killian441 import ast, datetime, re, socket, sys, threading, time, traceback @@ -63,26 +64,27 @@ class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass class RTSPClient(threading.Thread): - def __init__(self, url, dest_ip=''): + def __init__(self, url, dest_ip='', callback=None): global CUR_RANGE threading.Thread.__init__(self) self._auth = None - self._sock = None + self._callback = callback or (lambda x: x) self._cseq = 0 - self._session_id = '' self._cseq_map = {} # {CSeq:Method} mapping self._dest_ip = dest_ip - self.running = True - self.playing = False - self.location = '' - self.response_buf = [] - self.response = None self._parsed_url = self._parse_http://github.com/url(url) self._server_port = self._parsed_url.port or DEFAULT_SERVER_PORT self._orig_url = self._parsed_url.scheme + "://" + \ self._parsed_url.hostname + \ ":" + str(self._server_port) + \ self._parsed_url.path + self._session_id = '' + self._sock = None + self.location = '' + self.playing = False + self.response = None + self.response_buf = [] + self.running = True if '.sdp' not in self._parsed_url.path.lower(): CUR_RANGE = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() @@ -169,7 +171,7 @@ def _update_dest_ip(self): the same IP is used by default with RTSP''' if not self._dest_ip: self._dest_ip = self._sock.getsockname()[0] - PRINT('DEST_IP: %s\n' % self._dest_ip, CYAN) + self._callback('DEST_IP: %s\n' % self._dest_ip) def recv_msg(self): '''A complete response message or @@ -258,11 +260,10 @@ def _process_response(self, msg): status, headers, body = self._parse_response(msg) rsp_cseq = int(headers['cseq']) if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': - PRINT(self._get_time_str() + '\n' + msg) + self._callback(self._get_time_str() + '\n' + msg) if status == 401 and not self._auth: self._add_auth(headers['www-authenticate']) self.do_replay_request() - #self.do_teardown() elif status == 302: self.location = headers['location'] elif status != 200: @@ -280,7 +281,7 @@ def _process_response(self, msg): def _process_announce(self, msg): '''Processes the ANNOUNCE notification message''' global CUR_RANGE, CUR_SCALE - PRINT(msg) + self._callback(msg) headers = self._parse_header_params(msg.splitlines()[1:]) x_notice_val = int(headers['x-notice']) if x_notice_val in (X_NOTICE_EOS, X_NOTICE_BOS): @@ -329,11 +330,11 @@ def _sendmsg(self, method, url, headers): msg += END_OF_LINE + '%s: %s'%(k, str(v)) msg += HEADER_END_STR # End headers if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: - PRINT(self._get_time_str() + END_OF_LINE + msg) + self._callback(self._get_time_str() + END_OF_LINE + msg) try: self._sock.send(msg.encode()) except socket.error as e: - PRINT('Send msg error: %s'%e, RED) + self._callback('Send msg error: %s'%e) raise RTSPNetError(e) def _get_transport_type(self): @@ -489,7 +490,7 @@ def exec_cmd(rtsp, cmd): rtsp.do_play(CUR_RANGE, CUR_SCALE) def main(url, dest_ip): - rtsp = RTSPClient(url, dest_ip) + rtsp = RTSPClient(url, dest_ip, callback=PRINT) if PING: PRINT('PING START', YELLOW) From 1cbd8e41ebc4b9130e9176c68f4c2a93918f2d98 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 12:31:28 -0600 Subject: [PATCH 11/34] Grouped globals by mutability, grouped example code together --- rtsp.py | 66 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/rtsp.py b/rtsp.py index b6763e4..de2129c 100644 --- a/rtsp.py +++ b/rtsp.py @@ -11,21 +11,13 @@ # Ported to Python3, removed GoodThread # -killian441 -import ast, datetime, re, socket, sys, threading, time, traceback +import ast, datetime, re, socket, threading, time, traceback from hashlib import md5 try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse # for python < 3.0 -DEFAULT_SERVER_PORT = 554 -TRANSPORT_TYPE_LIST = [] -CLIENT_PORT_RANGE = '10014-10015' -NAT_IP_PORT = '' -ENABLE_ARQ = False -ENABLE_FEC = False -PING = False - TRANSPORT_TYPE_MAP = { 'ts_over_tcp' : 'MP2T/TCP;%s;interleaved=0-1, ', 'rtp_over_tcp' : 'MP2T/RTP/TCP;%s;interleaved=0-1, ', @@ -35,29 +27,23 @@ RTSP_VERSION = 'RTSP/1.0' DEFAULT_USERAGENT = 'Python Rtsp Client 1.0' -HEARTBEAT_INTERVAL = 10 # 10s - +DEFAULT_SERVER_PORT = 554 END_OF_LINE = '\r\n' HEADER_END_STR = END_OF_LINE*2 -CUR_RANGE = 'npt=end-' -CUR_SCALE = 1 - #x-notice in ANNOUNCE, BOS-Begin of Stream, EOS-End of Stream X_NOTICE_EOS, X_NOTICE_BOS, X_NOTICE_CLOSE = 2101, 2102, 2103 -#-------------------------------------------------------------------------- -# Colored Output in Console -#-------------------------------------------------------------------------- -DEBUG = False -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) -def COLOR_STR(msg, color=WHITE): - return '\033[%dm%s\033[0m'%(color, msg) - -def PRINT(msg, color=WHITE, out=sys.stdout): - if DEBUG and out.isatty() : - out.write(COLOR_STR(msg, color) + '\n') -#-------------------------------------------------------------------------- +#### Variables #### +CUR_RANGE = 'npt=end-' +CUR_SCALE = 1 +TRANSPORT_TYPE_LIST = [] +NAT_IP_PORT = '' +ENABLE_ARQ = False +ENABLE_FEC = False +PING = False +HEARTBEAT_INTERVAL = 10 # 10s +CLIENT_PORT_RANGE = '10014-10015' class RTSPError(Exception): pass class RTSPURLError(RTSPError): pass @@ -431,7 +417,7 @@ def ping(self, timeout=0.01): #----------------------------------------------------------------------- # Input with autocompletion #----------------------------------------------------------------------- -import readline +import readline, sys from optparse import OptionParser COMMANDS = ( 'backward', @@ -455,11 +441,27 @@ def input_cmd(): readline.set_completer_delims(' \t\n') readline.parse_and_bind("tab: complete") readline.set_completer(complete) - cmd = input(COLOR_STR('Input Command # ', CYAN)) + if(sys.version_info > (3, 0)): + cmd = input(COLOR_STR('Input Command # ', CYAN)) + else: + cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) PRINT('') # add one line return cmd #----------------------------------------------------------------------- +#-------------------------------------------------------------------------- +# Colored Output in Console +#-------------------------------------------------------------------------- +DEBUG = False +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) +def COLOR_STR(msg, color=WHITE): + return '\033[%dm%s\033[0m'%(color, msg) + +def PRINT(msg, color=WHITE, out=sys.stdout): + if DEBUG and out.isatty() : + out.write(COLOR_STR(msg, color) + '\n') +#-------------------------------------------------------------------------- + def exec_cmd(rtsp, cmd): '''Execute the operation according to the command''' global CUR_RANGE, CUR_SCALE @@ -489,10 +491,10 @@ def exec_cmd(rtsp, cmd): if cmd not in ('pause', 'exit', 'teardown', 'help'): rtsp.do_play(CUR_RANGE, CUR_SCALE) -def main(url, dest_ip): - rtsp = RTSPClient(url, dest_ip, callback=PRINT) +def main(url, options): + rtsp = RTSPClient(url, options.dest_ip, callback=PRINT) - if PING: + if options.ping: PRINT('PING START', YELLOW) rtsp.ping() PRINT('PING DONE', YELLOW) @@ -562,5 +564,5 @@ def play_ctrl_help(): url = args[0] DEBUG = True - main(url, options.dest_ip) + main(url, options) # EOF # From 3aae2fa72bb4c9640a6666be97609146c44fc1c7 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 15:03:48 -0600 Subject: [PATCH 12/34] Moving globabl variables related to stream into class --- rtsp.py | 84 +++++++++++++++++++++++++++------------------------------ 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/rtsp.py b/rtsp.py index de2129c..e0d9b3e 100644 --- a/rtsp.py +++ b/rtsp.py @@ -34,24 +34,19 @@ #x-notice in ANNOUNCE, BOS-Begin of Stream, EOS-End of Stream X_NOTICE_EOS, X_NOTICE_BOS, X_NOTICE_CLOSE = 2101, 2102, 2103 -#### Variables #### -CUR_RANGE = 'npt=end-' -CUR_SCALE = 1 -TRANSPORT_TYPE_LIST = [] -NAT_IP_PORT = '' -ENABLE_ARQ = False -ENABLE_FEC = False -PING = False -HEARTBEAT_INTERVAL = 10 # 10s -CLIENT_PORT_RANGE = '10014-10015' - class RTSPError(Exception): pass class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass class RTSPClient(threading.Thread): + TRANSPORT_TYPE_LIST = [] + NAT_IP_PORT = '' + ENABLE_ARQ = False + ENABLE_FEC = False + HEARTBEAT_INTERVAL = 10 # 10s + CLIENT_PORT_RANGE = '10014-10015' + def __init__(self, url, dest_ip='', callback=None): - global CUR_RANGE threading.Thread.__init__(self) self._auth = None self._callback = callback or (lambda x: x) @@ -66,13 +61,15 @@ def __init__(self, url, dest_ip='', callback=None): self._parsed_url.path self._session_id = '' self._sock = None + self.cur_range = 'npt=end-' + self.cur_scale = 1 self.location = '' self.playing = False self.response = None self.response_buf = [] self.running = True if '.sdp' not in self._parsed_url.path.lower(): - CUR_RANGE = 'npt=0.00000-' # On demand starts from the beginning + self.cur_range = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() self._update_dest_ip() self.closed = False @@ -259,20 +256,19 @@ def _process_response(self, msg): self.do_setup(track_id_str) elif self._cseq_map[rsp_cseq] == 'SETUP': self._session_id = headers['session'] - self.do_play(CUR_RANGE, CUR_SCALE) + self.do_play(self.cur_range, self.cur_scale) self.send_heart_beat_msg() elif self._cseq_map[rsp_cseq] == 'PLAY': self.playing = True def _process_announce(self, msg): '''Processes the ANNOUNCE notification message''' - global CUR_RANGE, CUR_SCALE self._callback(msg) headers = self._parse_header_params(msg.splitlines()[1:]) x_notice_val = int(headers['x-notice']) if x_notice_val in (X_NOTICE_EOS, X_NOTICE_BOS): - CUR_SCALE = 1 - self.do_play(CUR_RANGE, CUR_SCALE) + self.cur_scale = 1 + self.do_play(self.cur_range, self.cur_scale) elif x_notice_val == X_NOTICE_CLOSE: self.do_teardown() @@ -328,26 +324,28 @@ def _get_transport_type(self): transport_str = '' ip_type = 'unicast' #TODO: if IPAddress(DEST_IP).is_unicast() # else 'multicast' - for t in TRANSPORT_TYPE_LIST: + for t in self.TRANSPORT_TYPE_LIST: if t not in TRANSPORT_TYPE_MAP: raise RTSPError('Error param: %s' % t) if t.endswith('tcp'): - transport_str += TRANSPORT_TYPE_MAP[t]%ip_type + transport_str +=TRANSPORT_TYPE_MAP[t]%ip_type else: - transport_str += TRANSPORT_TYPE_MAP[t]%(ip_type, - self._dest_ip, - CLIENT_PORT_RANGE) + transport_str +=TRANSPORT_TYPE_MAP[t]%(ip_type, + self._dest_ip, + self.CLIENT_PORT_RANGE) return transport_str def do_describe(self, headers={}): if self._auth: headers['Authorization'] = self._auth headers['Accept'] = 'application/sdp' - if ENABLE_ARQ: + if self.ENABLE_ARQ: headers['x-Retrans'] = 'yes' headers['x-Burst'] = 'yes' - if ENABLE_FEC: headers['x-zmssFecCDN'] = 'yes' - if NAT_IP_PORT: headers['x-NAT'] = NAT_IP_PORT + if self.ENABLE_FEC: + headers['x-zmssFecCDN'] = 'yes' + if self.NAT_IP_PORT: + headers['x-NAT'] = self.NAT_IP_PORT self._sendmsg('DESCRIBE', self._orig_url, headers) def do_setup(self, track_id_str='', headers={}): @@ -404,7 +402,7 @@ def send_heart_beat_msg(self): '''Timed sending GET_PARAMETER message keep alive''' if not self.running: self.do_get_parameter() - threading.Timer(HEARTBEAT_INTERVAL, + threading.Timer(self.HEARTBEAT_INTERVAL, self.send_heart_beat_msg).start() def ping(self, timeout=0.01): @@ -464,36 +462,41 @@ def PRINT(msg, color=WHITE, out=sys.stdout): def exec_cmd(rtsp, cmd): '''Execute the operation according to the command''' - global CUR_RANGE, CUR_SCALE if cmd in ('exit', 'teardown'): rtsp.do_teardown() elif cmd == 'pause': - CUR_SCALE = 1; CUR_RANGE = 'npt=now-' + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=now-' rtsp.do_pause() elif cmd == 'help': PRINT(play_ctrl_help()) elif cmd == 'forward': - if CUR_SCALE < 0: CUR_SCALE = 1 - CUR_SCALE *= 2; CUR_RANGE = 'npt=now-' + if rtsp.cur_scale < 0: rtsp.cur_scale = 1 + rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' elif cmd == 'backward': - if CUR_SCALE > 0: CUR_SCALE = -1 - CUR_SCALE *= 2; CUR_RANGE = 'npt=now-' + if rtsp.cur_scale > 0: rtsp.cur_scale = -1 + rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' elif cmd == 'begin': - CUR_SCALE = 1; CUR_RANGE = 'npt=beginning-' + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=beginning-' elif cmd == 'live': - CUR_SCALE = 1; CUR_RANGE = 'npt=end-' + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=end-' elif cmd.startswith('play'): m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) - if m: CUR_RANGE = m.group('range') + if m: rtsp.cur_range = m.group('range') m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) - if m: CUR_SCALE = int(m.group('scale')) + if m: rtsp.cur_scale = int(m.group('scale')) if cmd not in ('pause', 'exit', 'teardown', 'help'): - rtsp.do_play(CUR_RANGE, CUR_SCALE) + rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) def main(url, options): rtsp = RTSPClient(url, options.dest_ip, callback=PRINT) + if options.transport: rtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') + if options.client_port: rtsp.CLIENT_PORT_RANGE = options.client_port + if options.nat: rtsp.NAT_IP_PORT = options.nat + if options.arq: rtsp.ENABLE_ARQ = options.arq + if options.fec: rtsp.ENABLE_FEC = options.fec + if options.ping: PRINT('PING START', YELLOW) rtsp.ping() @@ -554,13 +557,6 @@ def play_ctrl_help(): parser.print_help() sys.exit() - if options.transport: TRANSPORT_TYPE_LIST = options.transport.split(',') - if options.client_port: CLIENT_PORT_RANGE = options.client_port - if options.nat: NAT_IP_PORT = options.nat - if options.arq: ENABLE_ARQ = options.arq - if options.fec: ENABLE_FEC = options.fec - if options.ping: PING = options.ping - url = args[0] DEBUG = True From c6a27bb7bff3fe48658636efeca03081f6ea165d Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 15:30:06 -0600 Subject: [PATCH 13/34] Documentation updates --- README.md | 26 ++++++++++++++------------ rtsp.py | 38 +++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 4418ea6..7fbdc04 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,30 @@ # python-rtsp-client -A rtsp client write in python +A basic rtsp client writen in pure python ![GitHub issues](https://img.shields.io/github/issues/Yadro-Intra/python-rtsp-client.svg) ![GitHub forks](https://img.shields.io/github/forks/Yadro-Intra/python-rtsp-client.svg) ![GitHub stars](https://img.shields.io/github/stars/Yadro-Intra/python-rtsp-client.svg) - Usage: rtsp-client.py [options] url +Usage: rtsp.py [options] url - In running, you can control play by input "forward","backward","begin","live","pause" - or "play" with "range" and "scale" parameter, such as "play range:npt=beginning- scale:2" + While running, you can control play by inputting "forward","backward","begin","live","pause" + or "play" a with "range" and "scale" parameter, such as "play range:npt=beginning- scale:2" You can input "exit","teardown" or ctrl+c to quit Options: -h, --help show this help message and exit -t TRANSPORT, --transport=TRANSPORT - Set transport type when SETUP: tcp, udp, tcp_over_rtp, - udp_over_rtp[default] + Set transport type when issuing SETUP: ts_over_tcp, + ts_over_udp, rtp_over_tcp, rtp_over_udp[default] -d DEST_IP, --dest_ip=DEST_IP - Set dest ip of udp data transmission, default use same - ip with rtsp + Set destination ip of udp data transmission, default + uses same ip as this rtsp client -p CLIENT_PORT, --client_port=CLIENT_PORT - Set client port range of udp, default is "10014-10015" - -n NAT, --nat=NAT Add "x-NAT" when DESCRIBE, arg format + Set client port range when issuing SETUP of udp, + default is "10014-10015" + -n NAT, --nat=NAT Add "x-NAT" when issuing DESCRIBE, arg format "192.168.1.100:20008" - -r, --arq Add "x-zmssRtxSdp:yes" when DESCRIBE - -f, --fec Add "x-zmssFecCDN:yes" when DESCRIBE + -r, --arq Add "x-Retrans:yes" when issuing DESCRIBE + -f, --fec Add "x-zmssFecCDN:yes" when issuing DESCRIBE + -P, --ping Just issue OPTIONS and exit. \ No newline at end of file diff --git a/rtsp.py b/rtsp.py index e0d9b3e..74f0581 100644 --- a/rtsp.py +++ b/rtsp.py @@ -117,7 +117,7 @@ def run(self): self.close() def _parse_url(http://github.com/self,%20url): - '''Resolve url, return (ip, port, target) triplet''' + '''Resolve url, return the urlparse object''' parsed = urlparse(url) scheme = parsed.scheme.lower() ip = parsed.hostname @@ -137,7 +137,6 @@ def _parse_url(http://github.com/self,%20url): raise RTSPURLError('Invalid url: %s (host="%s" \ port=%u target="%s")' % (url, ip, port, target)) - #return scheme, ip, port, target return parsed def _connect_server(self): @@ -151,7 +150,7 @@ def _connect_server(self): def _update_dest_ip(self): '''If DEST_IP is not specified, - the same IP is used by default with RTSP''' + by default the same IP is used as this RTSP client''' if not self._dest_ip: self._dest_ip = self._sock.getsockname()[0] self._callback('DEST_IP: %s\n' % self._dest_ip) @@ -178,8 +177,8 @@ def recv_msg(self): return msg def _add_auth(self, msg): - '''Authentication request string, - everything after www-authentication''' + '''Authentication request string + (i.e. everything after "www-authentication")''' #TODO: this is too simplistic and will fail if more than one method # is acceptable, among other issues if msg.lower().startswith('basic'): @@ -209,7 +208,7 @@ def _add_auth(self, msg): def _auth_digest(self, auth_parameters): '''Creates a response string for digest authorization, only works - with MD5 at the moment''' + with the MD5 algorithm at the moment''' #TODO expand to more than MD5 if self._parsed_url.username: HA1 = md5("{}:{}:{}".format(self._parsed_url.username, @@ -233,8 +232,8 @@ def _get_content_length(self, msg): return (m and int(m.group('len'))) or 0 def _get_time_str(self): - # python 2.6 above only support% f parameters, - # compatible with the lower version of the following wording + '''Python 2.6 and above only supports %f parameters, + compatible with the lower version with the following wording''' dt = datetime.datetime.now() return dt.strftime('%Y-%m-%d %H:%M:%S.') + str(dt.microsecond) @@ -499,7 +498,7 @@ def main(url, options): if options.ping: PRINT('PING START', YELLOW) - rtsp.ping() + rtsp.ping(0.1) PRINT('PING DONE', YELLOW) sys.exit(0) return @@ -534,23 +533,24 @@ def play_ctrl_help(): parser = OptionParser(usage=usage) parser.add_option('-t', '--transport', dest='transport', default='rtp_over_udp', - help='Set transport type when SETUP: ts_over_tcp, ' - +'ts_over_udp, rtp_over_tcp, rtp_over_udp[default]') + help='Set transport type when issuing SETUP: ' + +'ts_over_tcp, ts_over_udp, rtp_over_tcp, ' + +'rtp_over_udp[default]') parser.add_option('-d', '--dest_ip', dest='dest_ip', - help='Set dest ip of udp data transmission, default ' - +'use same ip with rtsp') + help='Set destination ip of udp data transmission, ' + +'default uses same ip as this rtsp client') parser.add_option('-p', '--client_port', dest='client_port', - help='Set client port range when SETUP of udp, default ' - +'is "10014-10015"') + help='Set client port range when issuing SETUP of udp, ' + +'default is "10014-10015"') parser.add_option('-n', '--nat', dest='nat', - help='Add "x-NAT" when DESCRIBE, arg format ' + help='Add "x-NAT" when issuing DESCRIBE, arg format ' +'"192.168.1.100:20008"') parser.add_option('-r', '--arq', dest='arq', action="http://github.com/store_true", - help='Add "x-Retrans:yes" when DESCRIBE') + help='Add "x-Retrans:yes" when issuing DESCRIBE') parser.add_option('-f', '--fec', dest='fec', action="http://github.com/store_true", - help='Add "x-zmssFecCDN:yes" when DESCRIBE') + help='Add "x-zmssFecCDN:yes" when issuing DESCRIBE') parser.add_option('-P', '--ping', dest='ping', action="http://github.com/store_true", - help='Just perform DESCRIBE and exit.') + help='Just issue OPTIONS and exit.') (options, args) = parser.parse_args() if len(args) < 1: From f5053bc934a905cc1c50d30554cb653909068e16 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Tue, 30 May 2017 16:15:34 -0600 Subject: [PATCH 14/34] Seperating out example into its own file --- README.md | 22 ++++-- examples/setupandplay.py | 155 +++++++++++++++++++++++++++++++++++++++ rtsp.py | 152 -------------------------------------- 3 files changed, 172 insertions(+), 157 deletions(-) create mode 100644 examples/setupandplay.py diff --git a/README.md b/README.md index 7fbdc04..8dbcc0e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,23 @@ -# python-rtsp-client +python-rtsp-client +================== + A basic rtsp client writen in pure python -![GitHub issues](https://img.shields.io/github/issues/Yadro-Intra/python-rtsp-client.svg) -![GitHub forks](https://img.shields.io/github/forks/Yadro-Intra/python-rtsp-client.svg) -![GitHub stars](https://img.shields.io/github/stars/Yadro-Intra/python-rtsp-client.svg) +Getting Started +--------------- + +:: + from rtsp import RTSPClient + myrtsp = RTSPClient(url='rtsp://username:password@hostname:port/path',callback=print) + try: + myrtsp.do_describe() + except: + myrtsp.do_teardown() + -Usage: rtsp.py [options] url +Examples +-------- +Usage: setupandplay.py [options] url While running, you can control play by inputting "forward","backward","begin","live","pause" or "play" a with "range" and "scale" parameter, such as "play range:npt=beginning- scale:2" diff --git a/examples/setupandplay.py b/examples/setupandplay.py new file mode 100644 index 0000000..4dbf0d6 --- /dev/null +++ b/examples/setupandplay.py @@ -0,0 +1,155 @@ + +#----------------------------------------------------------------------- +# Input with autocompletion +#----------------------------------------------------------------------- +import readline, sys +sys.path.append('../') +from rtsp import * +from optparse import OptionParser +COMMANDS = ( + 'backward', + 'begin', + 'exit', + 'forward', + 'help', + 'live', + 'pause', + 'play', + 'range:', + 'scale:', + 'teardown', +) + +def complete(text, state): + options = [i for i in COMMANDS if i.startswith(text)] + return (state < len(options) and options[state]) or None + +def input_cmd(): + readline.set_completer_delims(' \t\n') + readline.parse_and_bind("tab: complete") + readline.set_completer(complete) + if(sys.version_info > (3, 0)): + cmd = input(COLOR_STR('Input Command # ', CYAN)) + else: + cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) + PRINT('') # add one line + return cmd +#----------------------------------------------------------------------- + +#-------------------------------------------------------------------------- +# Colored Output in Console +#-------------------------------------------------------------------------- +DEBUG = False +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) +def COLOR_STR(msg, color=WHITE): + return '\033[%dm%s\033[0m'%(color, msg) + +def PRINT(msg, color=WHITE, out=sys.stdout): + if DEBUG and out.isatty() : + out.write(COLOR_STR(msg, color) + '\n') +#-------------------------------------------------------------------------- + +def exec_cmd(rtsp, cmd): + '''Execute the operation according to the command''' + if cmd in ('exit', 'teardown'): + rtsp.do_teardown() + elif cmd == 'pause': + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=now-' + rtsp.do_pause() + elif cmd == 'help': + PRINT(play_ctrl_help()) + elif cmd == 'forward': + if rtsp.cur_scale < 0: rtsp.cur_scale = 1 + rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' + elif cmd == 'backward': + if rtsp.cur_scale > 0: rtsp.cur_scale = -1 + rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' + elif cmd == 'begin': + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=beginning-' + elif cmd == 'live': + rtsp.cur_scale = 1; rtsp.cur_range = 'npt=end-' + elif cmd.startswith('play'): + m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) + if m: rtsp.cur_range = m.group('range') + m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) + if m: rtsp.cur_scale = int(m.group('scale')) + + if cmd not in ('pause', 'exit', 'teardown', 'help'): + rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) + +def main(url, options): + rtsp = RTSPClient(url, options.dest_ip, callback=PRINT) + + if options.transport: rtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') + if options.client_port: rtsp.CLIENT_PORT_RANGE = options.client_port + if options.nat: rtsp.NAT_IP_PORT = options.nat + if options.arq: rtsp.ENABLE_ARQ = options.arq + if options.fec: rtsp.ENABLE_FEC = options.fec + + if options.ping: + PRINT('PING START', YELLOW) + rtsp.ping(0.1) + PRINT('PING DONE', YELLOW) + sys.exit(0) + return + + try: + rtsp.do_describe() + while rtsp.running: + if rtsp.playing: + cmd = input_cmd() + exec_cmd(rtsp, cmd) + # 302 redirect to re-establish chain + if not rtsp.running and rtsp.location: + rtsp = RTSPClient(rtsp.location) + rtsp.do_describe() + time.sleep(0.5) + except KeyboardInterrupt: + rtsp.do_teardown() + print('\n^C received, Exit.') + +def play_ctrl_help(): + help = COLOR_STR('In running, you can control play by input "forward"' \ + +', "backward", "begin", "live", "pause"\n', MAGENTA) + help += COLOR_STR('or "play" with "range" and "scale" parameter, such ' \ + +'as "play range:npt=beginning- scale:2"\n', MAGENTA) + help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to ' \ + +'quit\n', MAGENTA) + return help + +if __name__ == '__main__': + usage = COLOR_STR('%prog [options] url\n\n', GREEN) + play_ctrl_help() + + parser = OptionParser(usage=usage) + parser.add_option('-t', '--transport', dest='transport', + default='rtp_over_udp', + help='Set transport type when issuing SETUP: ' + +'ts_over_tcp, ts_over_udp, rtp_over_tcp, ' + +'rtp_over_udp[default]') + parser.add_option('-d', '--dest_ip', dest='dest_ip', + help='Set destination ip of udp data transmission, ' + +'default uses same ip as this rtsp client') + parser.add_option('-p', '--client_port', dest='client_port', + help='Set client port range when issuing SETUP of udp, ' + +'default is "10014-10015"') + parser.add_option('-n', '--nat', dest='nat', + help='Add "x-NAT" when issuing DESCRIBE, arg format ' + +'"192.168.1.100:20008"') + parser.add_option('-r', '--arq', dest='arq', action="http://github.com/store_true", + help='Add "x-Retrans:yes" when issuing DESCRIBE') + parser.add_option('-f', '--fec', dest='fec', action="http://github.com/store_true", + help='Add "x-zmssFecCDN:yes" when issuing DESCRIBE') + parser.add_option('-P', '--ping', dest='ping', action="http://github.com/store_true", + help='Just issue OPTIONS and exit.') + + (options, args) = parser.parse_args() + if len(args) < 1: + parser.print_help() + sys.exit() + + url = args[0] + + DEBUG = True + main(url, options) +# EOF # + diff --git a/rtsp.py b/rtsp.py index 74f0581..bd77d4d 100644 --- a/rtsp.py +++ b/rtsp.py @@ -410,155 +410,3 @@ def ping(self, timeout=0.01): time.sleep(timeout) self.close() return self.response - -#----------------------------------------------------------------------- -# Input with autocompletion -#----------------------------------------------------------------------- -import readline, sys -from optparse import OptionParser -COMMANDS = ( - 'backward', - 'begin', - 'exit', - 'forward', - 'help', - 'live', - 'pause', - 'play', - 'range:', - 'scale:', - 'teardown', -) - -def complete(text, state): - options = [i for i in COMMANDS if i.startswith(text)] - return (state < len(options) and options[state]) or None - -def input_cmd(): - readline.set_completer_delims(' \t\n') - readline.parse_and_bind("tab: complete") - readline.set_completer(complete) - if(sys.version_info > (3, 0)): - cmd = input(COLOR_STR('Input Command # ', CYAN)) - else: - cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) - PRINT('') # add one line - return cmd -#----------------------------------------------------------------------- - -#-------------------------------------------------------------------------- -# Colored Output in Console -#-------------------------------------------------------------------------- -DEBUG = False -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) -def COLOR_STR(msg, color=WHITE): - return '\033[%dm%s\033[0m'%(color, msg) - -def PRINT(msg, color=WHITE, out=sys.stdout): - if DEBUG and out.isatty() : - out.write(COLOR_STR(msg, color) + '\n') -#-------------------------------------------------------------------------- - -def exec_cmd(rtsp, cmd): - '''Execute the operation according to the command''' - if cmd in ('exit', 'teardown'): - rtsp.do_teardown() - elif cmd == 'pause': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=now-' - rtsp.do_pause() - elif cmd == 'help': - PRINT(play_ctrl_help()) - elif cmd == 'forward': - if rtsp.cur_scale < 0: rtsp.cur_scale = 1 - rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' - elif cmd == 'backward': - if rtsp.cur_scale > 0: rtsp.cur_scale = -1 - rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' - elif cmd == 'begin': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=beginning-' - elif cmd == 'live': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=end-' - elif cmd.startswith('play'): - m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) - if m: rtsp.cur_range = m.group('range') - m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) - if m: rtsp.cur_scale = int(m.group('scale')) - - if cmd not in ('pause', 'exit', 'teardown', 'help'): - rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) - -def main(url, options): - rtsp = RTSPClient(url, options.dest_ip, callback=PRINT) - - if options.transport: rtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') - if options.client_port: rtsp.CLIENT_PORT_RANGE = options.client_port - if options.nat: rtsp.NAT_IP_PORT = options.nat - if options.arq: rtsp.ENABLE_ARQ = options.arq - if options.fec: rtsp.ENABLE_FEC = options.fec - - if options.ping: - PRINT('PING START', YELLOW) - rtsp.ping(0.1) - PRINT('PING DONE', YELLOW) - sys.exit(0) - return - - try: - rtsp.do_describe() - while rtsp.running: - if rtsp.playing: - cmd = input_cmd() - exec_cmd(rtsp, cmd) - # 302 redirect to re-establish chain - if not rtsp.running and rtsp.location: - rtsp = RTSPClient(rtsp.location) - rtsp.do_describe() - time.sleep(0.5) - except KeyboardInterrupt: - rtsp.do_teardown() - print('\n^C received, Exit.') - -def play_ctrl_help(): - help = COLOR_STR('In running, you can control play by input "forward"' \ - +', "backward", "begin", "live", "pause"\n', MAGENTA) - help += COLOR_STR('or "play" with "range" and "scale" parameter, such ' \ - +'as "play range:npt=beginning- scale:2"\n', MAGENTA) - help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to ' \ - +'quit\n', MAGENTA) - return help - -if __name__ == '__main__': - usage = COLOR_STR('%prog [options] url\n\n', GREEN) + play_ctrl_help() - - parser = OptionParser(usage=usage) - parser.add_option('-t', '--transport', dest='transport', - default='rtp_over_udp', - help='Set transport type when issuing SETUP: ' - +'ts_over_tcp, ts_over_udp, rtp_over_tcp, ' - +'rtp_over_udp[default]') - parser.add_option('-d', '--dest_ip', dest='dest_ip', - help='Set destination ip of udp data transmission, ' - +'default uses same ip as this rtsp client') - parser.add_option('-p', '--client_port', dest='client_port', - help='Set client port range when issuing SETUP of udp, ' - +'default is "10014-10015"') - parser.add_option('-n', '--nat', dest='nat', - help='Add "x-NAT" when issuing DESCRIBE, arg format ' - +'"192.168.1.100:20008"') - parser.add_option('-r', '--arq', dest='arq', action="http://github.com/store_true", - help='Add "x-Retrans:yes" when issuing DESCRIBE') - parser.add_option('-f', '--fec', dest='fec', action="http://github.com/store_true", - help='Add "x-zmssFecCDN:yes" when issuing DESCRIBE') - parser.add_option('-P', '--ping', dest='ping', action="http://github.com/store_true", - help='Just issue OPTIONS and exit.') - - (options, args) = parser.parse_args() - if len(args) < 1: - parser.print_help() - sys.exit() - - url = args[0] - - DEBUG = True - main(url, options) -# EOF # From edec192011c989135cf83df9bce2922c4f70b18e Mon Sep 17 00:00:00 2001 From: killian441 Date: Tue, 30 May 2017 16:19:10 -0600 Subject: [PATCH 15/34] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8dbcc0e..af8b02c 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ A basic rtsp client writen in pure python Getting Started --------------- -:: from rtsp import RTSPClient myrtsp = RTSPClient(url='rtsp://username:password@hostname:port/path',callback=print) try: myrtsp.do_describe() + #Open socket to capture frames here except: myrtsp.do_teardown() @@ -39,4 +39,4 @@ Usage: setupandplay.py [options] url "192.168.1.100:20008" -r, --arq Add "x-Retrans:yes" when issuing DESCRIBE -f, --fec Add "x-zmssFecCDN:yes" when issuing DESCRIBE - -P, --ping Just issue OPTIONS and exit. \ No newline at end of file + -P, --ping Just issue OPTIONS and exit. From cbd167337881df40fbc807b6dc6c225bf1edb683 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 31 May 2017 10:08:40 -0600 Subject: [PATCH 16/34] Removed each stage chaining to the next, now each stage must be called explicitly --- README.md | 8 +++++++- examples/setupandplay.py | 10 ++++++++-- rtsp.py | 17 ++++++++++------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af8b02c..d834292 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,14 @@ Getting Started from rtsp import RTSPClient myrtsp = RTSPClient(url='rtsp://username:password@hostname:port/path',callback=print) try: - myrtsp.do_describe() + rtsp.do_describe() + while rtsp.state != 'describe': + time.sleep(0.1) + rtsp.do_setup(rtsp.track_id_str) + while rtsp.state != 'setup': + time.sleep(0.1) #Open socket to capture frames here + rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) except: myrtsp.do_teardown() diff --git a/examples/setupandplay.py b/examples/setupandplay.py index 4dbf0d6..81c039f 100644 --- a/examples/setupandplay.py +++ b/examples/setupandplay.py @@ -95,12 +95,18 @@ def main(url, options): try: rtsp.do_describe() + while rtsp.state != 'describe': + time.sleep(0.1) + rtsp.do_setup(rtsp.track_id_str) + while rtsp.state != 'setup': + time.sleep(0.1) + rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) while rtsp.running: - if rtsp.playing: + if rtsp.state == 'play': cmd = input_cmd() exec_cmd(rtsp, cmd) # 302 redirect to re-establish chain - if not rtsp.running and rtsp.location: + if rtsp.location: rtsp = RTSPClient(rtsp.location) rtsp.do_describe() time.sleep(0.5) diff --git a/rtsp.py b/rtsp.py index bd77d4d..1c3763e 100644 --- a/rtsp.py +++ b/rtsp.py @@ -64,10 +64,11 @@ def __init__(self, url, dest_ip='', callback=None): self.cur_range = 'npt=end-' self.cur_scale = 1 self.location = '' - self.playing = False self.response = None self.response_buf = [] self.running = True + self.state = None + self.track_id_str = '' if '.sdp' not in self._parsed_url.path.lower(): self.cur_range = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() @@ -98,9 +99,9 @@ def cache(self, s=None): def close(self): if not self.closed: - self.closed = True + self.closed = True self.running = False - self.playing = False + self.state = 'closed' self._sock.close() def run(self): @@ -251,14 +252,16 @@ def _process_response(self, msg): elif status != 200: self.do_teardown() elif self._cseq_map[rsp_cseq] == 'DESCRIBE': #Implies status 200 - track_id_str = self._parse_track_id(body) - self.do_setup(track_id_str) + self.track_id_str = self._parse_track_id(body) + #self.do_setup(track_id_str) + self.state = 'describe' elif self._cseq_map[rsp_cseq] == 'SETUP': self._session_id = headers['session'] - self.do_play(self.cur_range, self.cur_scale) + #self.do_play(self.cur_range, self.cur_scale) self.send_heart_beat_msg() + self.state = 'setup' elif self._cseq_map[rsp_cseq] == 'PLAY': - self.playing = True + self.state = 'play' def _process_announce(self, msg): '''Processes the ANNOUNCE notification message''' From dc298501b218797b1f2079e773c68050ffa7eeed Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 31 May 2017 14:21:16 -0600 Subject: [PATCH 17/34] Adding _update_content_base function, updating example --- README.md | 10 +++--- examples/setupandplay.py | 66 ++++++++++++++++++++-------------------- rtsp.py | 9 ++++++ 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d834292..97c050f 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ Getting Started from rtsp import RTSPClient myrtsp = RTSPClient(url='rtsp://username:password@hostname:port/path',callback=print) try: - rtsp.do_describe() - while rtsp.state != 'describe': + myrtsp.do_describe() + while myrtsp.state != 'describe': time.sleep(0.1) - rtsp.do_setup(rtsp.track_id_str) - while rtsp.state != 'setup': + myrtsp.do_setup(rtsp.track_id_str) + while myrtsp.state != 'setup': time.sleep(0.1) #Open socket to capture frames here - rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) + myrtsp.do_play(rtsp.cur_range, rtsp.cur_scale) except: myrtsp.do_teardown() diff --git a/examples/setupandplay.py b/examples/setupandplay.py index 81c039f..5f1cfa1 100644 --- a/examples/setupandplay.py +++ b/examples/setupandplay.py @@ -1,4 +1,4 @@ - +#!/usr/bin/python #----------------------------------------------------------------------- # Input with autocompletion #----------------------------------------------------------------------- @@ -49,69 +49,69 @@ def PRINT(msg, color=WHITE, out=sys.stdout): out.write(COLOR_STR(msg, color) + '\n') #-------------------------------------------------------------------------- -def exec_cmd(rtsp, cmd): +def exec_cmd(myrtsp, cmd): '''Execute the operation according to the command''' if cmd in ('exit', 'teardown'): - rtsp.do_teardown() + myrtsp.do_teardown() elif cmd == 'pause': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=now-' - rtsp.do_pause() + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=now-' + myrtsp.do_pause() elif cmd == 'help': PRINT(play_ctrl_help()) elif cmd == 'forward': - if rtsp.cur_scale < 0: rtsp.cur_scale = 1 - rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' + if myrtsp.cur_scale < 0: myrtsp.cur_scale = 1 + myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-' elif cmd == 'backward': - if rtsp.cur_scale > 0: rtsp.cur_scale = -1 - rtsp.cur_scale *= 2; rtsp.cur_range = 'npt=now-' + if myrtsp.cur_scale > 0: myrtsp.cur_scale = -1 + myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-' elif cmd == 'begin': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=beginning-' + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=beginning-' elif cmd == 'live': - rtsp.cur_scale = 1; rtsp.cur_range = 'npt=end-' + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=end-' elif cmd.startswith('play'): m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) - if m: rtsp.cur_range = m.group('range') + if m: myrtsp.cur_range = m.group('range') m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) - if m: rtsp.cur_scale = int(m.group('scale')) + if m: myrtsp.cur_scale = int(m.group('scale')) if cmd not in ('pause', 'exit', 'teardown', 'help'): - rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) + myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale) def main(url, options): - rtsp = RTSPClient(url, options.dest_ip, callback=PRINT) + myrtsp = RTSPClient(url, options.dest_ip, callback=PRINT) - if options.transport: rtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') - if options.client_port: rtsp.CLIENT_PORT_RANGE = options.client_port - if options.nat: rtsp.NAT_IP_PORT = options.nat - if options.arq: rtsp.ENABLE_ARQ = options.arq - if options.fec: rtsp.ENABLE_FEC = options.fec + if options.transport: myrtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') + if options.client_port: myrtsp.CLIENT_PORT_RANGE = options.client_port + if options.nat: myrtsp.NAT_IP_PORT = options.nat + if options.arq: myrtsp.ENABLE_ARQ = options.arq + if options.fec: myrtsp.ENABLE_FEC = options.fec if options.ping: PRINT('PING START', YELLOW) - rtsp.ping(0.1) + myrtsp.ping(0.1) PRINT('PING DONE', YELLOW) sys.exit(0) return try: - rtsp.do_describe() - while rtsp.state != 'describe': + myrtsp.do_describe() + while myrtsp.state != 'describe': time.sleep(0.1) - rtsp.do_setup(rtsp.track_id_str) - while rtsp.state != 'setup': + myrtsp.do_setup(myrtsp.track_id_str) + while myrtsp.state != 'setup': time.sleep(0.1) - rtsp.do_play(rtsp.cur_range, rtsp.cur_scale) - while rtsp.running: - if rtsp.state == 'play': + myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale) + while myrtsp.running: + if myrtsp.state == 'play': cmd = input_cmd() - exec_cmd(rtsp, cmd) + exec_cmd(myrtsp, cmd) # 302 redirect to re-establish chain - if rtsp.location: - rtsp = RTSPClient(rtsp.location) - rtsp.do_describe() + if myrtsp.location: + myrtsp = RTSPClient(myrtsp.location) + myrtsp.do_describe() time.sleep(0.5) except KeyboardInterrupt: - rtsp.do_teardown() + myrtsp.do_teardown() print('\n^C received, Exit.') def play_ctrl_help(): diff --git a/rtsp.py b/rtsp.py index 1c3763e..3ab1009 100644 --- a/rtsp.py +++ b/rtsp.py @@ -149,6 +149,14 @@ def _connect_server(self): raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._parsed_url.hostname, self._server_port)) + def _update_content_base(self, msg): + m = re.search(r'[Cc]ontent-[Bb]ase:\s?(?P[a-zA-Z0-9_:\/\.]+)', msg) + if (m and m.group('base')): + new_url = m.group('base') + if new_url[-1] == '/': + new_url = new_url[:-1] + self._orig_url = new_url + def _update_dest_ip(self): '''If DEST_IP is not specified, by default the same IP is used as this RTSP client''' @@ -252,6 +260,7 @@ def _process_response(self, msg): elif status != 200: self.do_teardown() elif self._cseq_map[rsp_cseq] == 'DESCRIBE': #Implies status 200 + self._update_content_base(msg) self.track_id_str = self._parse_track_id(body) #self.do_setup(track_id_str) self.state = 'describe' From f215ab860b9c922a4c04c2c5b58e76e4332c0663 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 31 May 2017 15:39:20 -0600 Subject: [PATCH 18/34] do_setup now setups up all tracks by default --- examples/setupandplay.py | 2 +- rtsp.py | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/examples/setupandplay.py b/examples/setupandplay.py index 5f1cfa1..33717b2 100644 --- a/examples/setupandplay.py +++ b/examples/setupandplay.py @@ -97,7 +97,7 @@ def main(url, options): myrtsp.do_describe() while myrtsp.state != 'describe': time.sleep(0.1) - myrtsp.do_setup(myrtsp.track_id_str) + myrtsp.do_setup(0) while myrtsp.state != 'setup': time.sleep(0.1) myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale) diff --git a/rtsp.py b/rtsp.py index 3ab1009..dfd505d 100644 --- a/rtsp.py +++ b/rtsp.py @@ -68,7 +68,7 @@ def __init__(self, url, dest_ip='', callback=None): self.response_buf = [] self.running = True self.state = None - self.track_id_str = '' + self.track_id_lst = [] if '.sdp' not in self._parsed_url.path.lower(): self.cur_range = 'npt=0.00000-' # On demand starts from the beginning self._connect_server() @@ -261,12 +261,10 @@ def _process_response(self, msg): self.do_teardown() elif self._cseq_map[rsp_cseq] == 'DESCRIBE': #Implies status 200 self._update_content_base(msg) - self.track_id_str = self._parse_track_id(body) - #self.do_setup(track_id_str) + self._parse_track_id(body) self.state = 'describe' elif self._cseq_map[rsp_cseq] == 'SETUP': self._session_id = headers['session'] - #self.do_play(self.cur_range, self.cur_scale) self.send_heart_beat_msg() self.state = 'setup' elif self._cseq_map[rsp_cseq] == 'PLAY': @@ -302,8 +300,8 @@ def _parse_header_params(self, header_param_lines): def _parse_track_id(self, sdp): '''Resolves a string of the form trackID = 2 from sdp''' - m = re.search(r'a=control:(?P[\w=\d]+)', sdp, re.S) - return m and m.group('trackid') or '' + m = re.findall(r'a=control:(?P[\w=\d]+)', sdp, re.S) + self.track_id_lst = m def _next_seq(self): self._cseq += 1 @@ -359,11 +357,23 @@ def do_describe(self, headers={}): headers['x-NAT'] = self.NAT_IP_PORT self._sendmsg('DESCRIBE', self._orig_url, headers) - def do_setup(self, track_id_str='', headers={}): + def do_setup(self, track_id_str=None, headers={}): if self._auth: headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() - self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) + #TODO: Currently issues SETUP for all tracks but doesn't keep track + # of them or end all of them. + if isinstance(track_id_str,str): + self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) + elif isinstance(track_id_str, int): + self._sendmsg('SETUP', self._orig_url + + '/' + + self.track_id_lst[track_id_str], headers) + elif self.track_id_lst: + for track in self.track_id_lst: + self._sendmsg('SETUP', self._orig_url+'/'+track, headers) + else: + self._sendmsg('SETUP', self._orig_url, headers) def do_play(self, range='npt=end-', scale=1, headers={}): if self._auth: From 22bc609dc8ae43f78f848f158a325e91e0ff0277 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 7 Jul 2017 15:40:01 -0600 Subject: [PATCH 19/34] Updates --- examples/rtpframes.py | 168 +++++++++++++++++++++++++++++++++ rtp.py | 215 ++++++++++++++++++++++++++++++++++++++++++ rtsp.py | 2 +- 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 examples/rtpframes.py create mode 100644 rtp.py diff --git a/examples/rtpframes.py b/examples/rtpframes.py new file mode 100644 index 0000000..f59a478 --- /dev/null +++ b/examples/rtpframes.py @@ -0,0 +1,168 @@ +#!/usr/bin/python +#----------------------------------------------------------------------- +# Input with autocompletion +#----------------------------------------------------------------------- +import readline, sys +sys.path.append('../') +from rtsp import * +from rtp import * +from optparse import OptionParser +COMMANDS = ( + 'backward', + 'begin', + 'exit', + 'forward', + 'help', + 'live', + 'pause', + 'play', + 'range:', + 'scale:', + 'teardown', +) + +def complete(text, state): + options = [i for i in COMMANDS if i.startswith(text)] + return (state < len(options) and options[state]) or None + +def input_cmd(): + readline.set_completer_delims(' \t\n') + readline.parse_and_bind("tab: complete") + readline.set_completer(complete) + if(sys.version_info > (3, 0)): + cmd = input(COLOR_STR('Input Command # ', CYAN)) + else: + cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) + PRINT('') # add one line + return cmd +#----------------------------------------------------------------------- + +#-------------------------------------------------------------------------- +# Colored Output in Console +#-------------------------------------------------------------------------- +DEBUG = False +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98)) +def COLOR_STR(msg, color=WHITE): + return '\033[%dm%s\033[0m'%(color, msg) + +def PRINT(msg, color=WHITE, out=sys.stdout): + if DEBUG and out.isatty() : + out.write(COLOR_STR(msg, color) + '\n') +#-------------------------------------------------------------------------- + +def exec_cmd(myrtsp, cmd): + '''Execute the operation according to the command''' + if cmd in ('exit', 'teardown'): + myrtsp.do_teardown() + elif cmd == 'pause': + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=now-' + myrtsp.do_pause() + elif cmd == 'help': + PRINT(play_ctrl_help()) + elif cmd == 'forward': + if myrtsp.cur_scale < 0: myrtsp.cur_scale = 1 + myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-' + elif cmd == 'backward': + if myrtsp.cur_scale > 0: myrtsp.cur_scale = -1 + myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-' + elif cmd == 'begin': + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=beginning-' + elif cmd == 'live': + myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=end-' + elif cmd.startswith('play'): + m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) + if m: myrtsp.cur_range = m.group('range') + m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) + if m: myrtsp.cur_scale = int(m.group('scale')) + + if cmd not in ('pause', 'exit', 'teardown', 'help'): + myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale) + +def main(url, options): + myrtsp = RTSPClient(url, options.dest_ip, callback=PRINT) + + if options.transport: myrtsp.TRANSPORT_TYPE_LIST = options.transport.split(',') + if options.client_port: myrtsp.CLIENT_PORT_RANGE = options.client_port + if options.nat: myrtsp.NAT_IP_PORT = options.nat + if options.arq: myrtsp.ENABLE_ARQ = options.arq + if options.fec: myrtsp.ENABLE_FEC = options.fec + + if options.ping: + PRINT('PING START', YELLOW) + myrtsp.ping(0.1) + PRINT('PING DONE', YELLOW) + sys.exit(0) + return + + try: + myrtsp.do_describe() + while myrtsp.state != 'describe': + time.sleep(0.1) + myrtsp.do_setup(0) + while myrtsp.state != 'setup': + time.sleep(0.1) + #Setup up RTP capture here + f=open('test.h264','wb') + rtpframes = RTPReceive([10014],callback=f.write) + while not rtpframes.running: + time.sleep(0.1) + myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale) + while myrtsp.running: + if myrtsp.state == 'play': + cmd = input_cmd() + exec_cmd(myrtsp, cmd) + # 302 redirect to re-establish chain + if myrtsp.location: + myrtsp = RTSPClient(myrtsp.location) + myrtsp.do_describe() + time.sleep(0.5) + except KeyboardInterrupt: + f.close() + myrtsp.do_teardown() + print('\n^C received, Exit.') + +def play_ctrl_help(): + help = COLOR_STR('In running, you can control play by input "forward"' \ + +', "backward", "begin", "live", "pause"\n', MAGENTA) + help += COLOR_STR('or "play" with "range" and "scale" parameter, such ' \ + +'as "play range:npt=beginning- scale:2"\n', MAGENTA) + help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to ' \ + +'quit\n', MAGENTA) + return help + +if __name__ == '__main__': + usage = COLOR_STR('%prog [options] url\n\n', GREEN) + play_ctrl_help() + + parser = OptionParser(usage=usage) + parser.add_option('-t', '--transport', dest='transport', + default='rtp_over_udp', + help='Set transport type when issuing SETUP: ' + +'ts_over_tcp, ts_over_udp, rtp_over_tcp, ' + +'rtp_over_udp[default]') + parser.add_option('-d', '--dest_ip', dest='dest_ip', + help='Set destination ip of udp data transmission, ' + +'default uses same ip as this rtsp client') + parser.add_option('-p', '--client_port', dest='client_port', + help='Set client port range when issuing SETUP of udp, ' + +'default is "10014-10015"') + parser.add_option('-n', '--nat', dest='nat', + help='Add "x-NAT" when issuing DESCRIBE, arg format ' + +'"192.168.1.100:20008"') + parser.add_option('-r', '--arq', dest='arq', action="http://github.com/store_true", + help='Add "x-Retrans:yes" when issuing DESCRIBE') + parser.add_option('-f', '--fec', dest='fec', action="http://github.com/store_true", + help='Add "x-zmssFecCDN:yes" when issuing DESCRIBE') + parser.add_option('-P', '--ping', dest='ping', action="http://github.com/store_true", + help='Just issue OPTIONS and exit.') + + (options, args) = parser.parse_args() + if len(args) < 1: + parser.print_help() + sys.exit() + + url = args[0] + + DEBUG = True + main(url, options) +# EOF # + diff --git a/rtp.py b/rtp.py new file mode 100644 index 0000000..a98cbac --- /dev/null +++ b/rtp.py @@ -0,0 +1,215 @@ +''' +Inspired by post by Sampsa Riikonen here: +https://stackoverflow.com/questions/28022432/receiving-rtp-packets-after-rtsp-setup + +Written 2017 Mike Killian +''' + +import re, socket, threading +import bitstring # if you don't have this from your linux distro, install with "pip install bitstring" + +class RTPReceive(threading.Thread): + ''' + This will open a socket on the client ports sent in RTSP setup request and + return data as its received to the callback function. + ''' + def __init__(self, client_ports, callback=None): + threading.Thread.__init__(self) + self._callback = callback or (lambda x: None) + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._sock.bind(("", client_ports[0])) # we open a port that is visible to the whole internet (the empty string "" takes care of that) + self._sock.settimeout(5) # if the socket is dead for 5 s., its thrown into trash + self.closed = False + self.frame = b'' + self.frame_done = True #Did we get the last packet to fill a whole frame? + self.running = False + self.sprop-parameter-sets = 'Z0IAIJWoFAHmQA==,aM48gA==' + self.start() + + def run(self): + self.running = True + try: + while self.running: + #self.frame = msg = self.recv_msg() + msg = self._sock.recv(2048) + framelet = self.digestpacket(msg) + if framelet: + self.frame += framelet + if self.frame and self.frame_done: + self._callback(self.frame) + self.frame = b'' + except Exception as e: + raise Exception('Run time error: %s' % e) + self.running = False + self.close() + + def close(self): + self.closed = True + self.running = False + self._sock.close() + + def insert_config_info(self, parameters): + pass + + # ********* (2) The routine for handling the RTP stream *********** + + def digestpacket(self, st): + """ This routine takes a UDP packet, i.e. a string of bytes and .. + (a) strips off the RTP header + (b) adds NAL "stamps" to the packets, so that they are recognized as NAL's + (c) Concantenates frames + (d) Returns a packet that can be written to disk as such and that is recognized by stock media players as h264 stream + """ + startbytes = b"\x00\x00\x00\x01" # this is the sequence of four bytes that identifies a NAL packet.. must be in front of every NAL packet. + + bt = bitstring.BitArray(bytes=st) # turn the whole string-of-bytes packet into a string of bits. Very unefficient, but hey, this is only for demoing. + lc = 12 # bytecounter + bc = 12*8 # bitcounter + + version = bt[0:2].uint # Version + p = bt[3] # Padding + x = bt[4] # Extension + cc = bt[4:8].uint # CSRC Count + m = bt[9] # Marker + pt = bt[9:16].uint # Payload Type + sn = bt[16:32].uint # Sequence number + timestamp = bt[32:64].uint # Timestamp + ssrc = bt[64:96].uint # ssrc identifier + # The header format can be found from: + # https://en.wikipedia.org/wiki/Real-time_Transport_Protocol + + lc = 12 # so, we have red twelve bytes + bc = 12*8 # .. and that many bits + + if p: + #TODO: Deal with padding here + print("\n****\nPadding alert!!\n****\n") + + print("*----* Packet Begin *----* (Len: {})".format(len(st))) + print("Ver: {}, P: {}, X: {}, CC: {}, M: {}, PT: {}".format(version,p,x,cc,m,pt)) + print("Sequence number: {}, Timestamp: {}".format(sn,timestamp)) + print("Sync. Source Identifier: {}".format(ssrc)) + + # st=f.read(4*cc) # csrc identifiers, 32 bits (4 bytes) each + cids = [] + for i in range(cc): + cids.append(bt[bc:bc+32].uint) + bc += 32 + lc += 4 + if cids: print("CSRC Identifiers: {}".format(cids)) + + if (x): + # this section haven't been tested.. might fail + hid = bt[bc:bc+16].uint + bc += 16 + lc += 2 + + hlen = bt[bc:bc+16].uint + bc += 16 + lc += 2 + + hst = bt[bc:bc+32*hlen] + bc += 32*hlen + lc += 4*hlen + + print("*----* Extension Header *----*") + print("Ext. Header id: {}, Header len: {}".format(hid,hlen)) + + # OK, now we enter the NAL packet, as described here: + # + # https://tools.ietf.org/html/rfc6184#section-1.3 + # + # Some quotes from that document: + # + """ + 5.3. NAL Unit Header Usage + + + The structure and semantics of the NAL unit header were introduced in + Section 1.3. For convenience, the format of the NAL unit header is + reprinted below: + + +---------------+ + |0|1|2|3|4|5|6|7| + +-+-+-+-+-+-+-+-+ + |F|NRI| Type | + +---------------+ + + This section specifies the semantics of F and NRI according to this + specification. + + """ + """ + Table 3. Summary of allowed NAL unit types for each packetization + mode (yes = allowed, no = disallowed, ig = ignore) + + Payload Packet Single NAL Non-Interleaved Interleaved + Type Type Unit Mode Mode Mode + ------------------------------------------------------------- + 0 reserved ig ig ig + 1-23 NAL unit yes yes no + 24 STAP-A no yes no + 25 STAP-B no no yes + 26 MTAP16 no no yes + 27 MTAP24 no no yes + 28 FU-A no yes yes + 29 FU-B no no yes + 30-31 reserved ig ig ig + """ + # This was also very usefull: + # http://stackoverflow.com/questions/7665217/how-to-process-raw-udp-packets-so-that-they-can-be-decoded-by-a-decoder-filter-i + # A quote from that: + """ + First byte: [ 3 NAL UNIT BITS | 5 FRAGMENT TYPE BITS] + Second byte: [ START BIT | RESERVED BIT | END BIT | 5 NAL UNIT BITS] + Other bytes: [... VIDEO FRAGMENT DATA...] + """ + + fb = bt[bc] # i.e. "F" + nri = bt[bc+1:bc+3].uint # "NRI" + nlu0 = bt[bc:bc+3] # "3 NAL UNIT BITS" (i.e. [F | NRI]) + typ = bt[bc+3:bc+8].uint # "Type" + print(" *-* NAL Header *-*") + print("F: {}, NRI: {}, Type: {}".format(fb, nri, typ)) + print("First three bits together : {}".format(bt[bc:bc+3])) + + if (typ==7 or typ==8): + # this means we have either an SPS or a PPS packet + # they have the meta-info about resolution, etc. + # more reading for example here: + # http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/ + if (typ==7): + print(">>>>> SPS packet") + else: + print(">>>>> PPS packet") + return startbytes+st[lc:] + # .. notice here that we include the NAL starting sequence "startbytes" and the "First byte" + + bc += 8; + lc += 1; # let's go to "Second byte" + # ********* WE ARE AT THE "Second byte" ************ + # The "Type" here is most likely 28, i.e. "FU-A" + start = bt[bc] # start bit + end = bt[bc+2] # end bit + nlu1 = bt[bc+3:bc+8] # 5 nal unit bits + head = b"" + + if (self.frame_done and start): # OK, this is a first fragment in a movie frame + print(">>> first fragment found") + self.frame_done = False + nlu = nlu0+nlu1 # Create "[3 NAL UNIT BITS | 5 NAL UNIT BITS]" + print(" >>> NLU0: {}, NLU1: {}, NLU: {}".format(nlu0,nlu1,nlu)) + head = startbytes+nlu.bytes # .. add the NAL starting sequence + lc += 1 # We skip the "Second byte" + elif (self.frame_done==False and start==False and end==False): # intermediate fragment in a sequence, just dump "VIDEO FRAGMENT DATA" + lc += 1 # We skip the "Second byte" + elif (self.frame_done==False and end==True): # last fragment in a sequence, just dump "VIDEO FRAGMENT DATA" + print("<<<< last fragment found") + self.frame_done = True + lc += 1 # We skip the "Second byte" + + if (typ==28): # This code only handles "Type" = 28, i.e. "FU-A" + return head+st[lc:] + else: + #raise Exception("unknown frame type for this piece of s***") + return None diff --git a/rtsp.py b/rtsp.py index dfd505d..f8ec540 100644 --- a/rtsp.py +++ b/rtsp.py @@ -362,7 +362,7 @@ def do_setup(self, track_id_str=None, headers={}): headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() #TODO: Currently issues SETUP for all tracks but doesn't keep track - # of them or end all of them. + # of all sessions or teardown all of them. if isinstance(track_id_str,str): self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) elif isinstance(track_id_str, int): From 59aa658045846ab8e33e1bfb650bf57da50eab38 Mon Sep 17 00:00:00 2001 From: Lucas Zanella Date: Wed, 25 Oct 2017 00:42:55 -0200 Subject: [PATCH 20/34] adds socks5 capability --- __pycache__/rtsp.cpython-36.pyc | Bin 0 -> 13863 bytes rtsp.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 __pycache__/rtsp.cpython-36.pyc diff --git a/__pycache__/rtsp.cpython-36.pyc b/__pycache__/rtsp.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f06128777b05148ad89c3bd4d84ca8d58e808065 GIT binary patch literal 13863 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 bff2=fT_ffgPWt)hSY6~fRvr;z{H6XE!l$jF literal 0 HcmV?d00001 diff --git a/rtsp.py b/rtsp.py index f8ec540..3a09e4d 100644 --- a/rtsp.py +++ b/rtsp.py @@ -46,7 +46,7 @@ class RTSPClient(threading.Thread): HEARTBEAT_INTERVAL = 10 # 10s CLIENT_PORT_RANGE = '10014-10015' - def __init__(self, url, dest_ip='', callback=None): + def __init__(self, url, dest_ip='', callback=None, socks=None): threading.Thread.__init__(self) self._auth = None self._callback = callback or (lambda x: x) @@ -61,6 +61,7 @@ def __init__(self, url, dest_ip='', callback=None): self._parsed_url.path self._session_id = '' self._sock = None + self._socks = socks self.cur_range = 'npt=end-' self.cur_scale = 1 self.location = '' @@ -143,7 +144,7 @@ def _parse_url(http://github.com/self,%20url): def _connect_server(self): '''Connect to the server and create a socket''' try: - self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock = self._socks or socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._parsed_url.hostname, self._server_port)) except socket.error as e: raise RTSPNetError('socket error: %s [%s:%d]' % From 4a2ebc9d95c30c88fd8d6eca84d56eb38e677ab4 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 3 Nov 2017 16:45:54 -0600 Subject: [PATCH 21/34] Added Try/Except block around header delimiter Headers should end with a carriage return/line feed at the end of each line and then a blank line (with a CRLF) to end the header. In some cases, for example a 406 reponse, the header will not have the blank line and instead the server will close the connection. In this case the code raises a ValueError exception which is not very helpfully. Inserting this try/except block adds clarity to what failed. --- __pycache__/rtsp.cpython-36.pyc | Bin 13863 -> 14046 bytes rtsp.py | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/__pycache__/rtsp.cpython-36.pyc b/__pycache__/rtsp.cpython-36.pyc index f06128777b05148ad89c3bd4d84ca8d58e808065..75de967fb41e86539f9aa99ea7311bcab5296d0d 100644 GIT binary patch delta 2593 zcma);Yitx%6oBWWfLujH5&=mx@r7=D#3(*ciT?5N$7GC;Uq+3I8Z=R2jOWbOs%VHm?ziWj zbMCow?>+b2eLVH~c%V5H3T*xHx6SK1o(mioTUy&&2X8htW)9vukuc1`j&yQjTr(_l za9FoSCWZzxJFStl(a0<_)0j-@Xd29YAbuf(l6%%45uu%b7+|ZMC%1pzK`2~=)^==W3Gk(*_VfW@L0cFVXN0V0XQ}NOEwDh9XT(7h2j>qaGOAd81- z$?c9;)sksdW1C1nNGK+wY-UorqesnrW;THLM5WNY;2p;Xlu>4y?uQ6WGipysE4`6aYVnPzohb1{Jy1 zvKCmZ$U_NJPfq$&eONQC$a$Fh#)l(%&O;lUkHUF=p!^7|<5w&gfgb+Ag6rYU+$Rg- z5VFuXDNkivr?PBNulpAd}!fnQ2>5yVb!!H+Om86!YF1)SWv|=iFIRoc(BQsZ4zc1 zP2ZO6UBF(C;_3 zn6&&*)hd~7!*%nIt73hM&(us)PaBwhnl+JeJR8=>HY=V?DzURCTH+MP-3uLOv+3_1 zkt1iAZdhy~MsZV=(LS;YLY8l-uJqa@s(e@VwU{rjR=1SifQsL-nM)CZSOP)t=fp?@ zzyNPt)P4;Oh;W_=!F(s4(hbdYeA}^9;vTSrG?He%aISr*Tg<;(`~r;f6E(HK`PrHp znI>xB-`Bj|wiPAEw@o+HL@LGZ#2#x#%%-s-nQG=g)A#^7HS-^8cf&Qg9d)B{sp9L; zpU0%P&TNg;nQS-z>GCKRa&edF-mSZ_>%f=;+gUGvuf7si=f0|c4d5nzGWsc2_+1TG!5;onL({ycXy6jW zpDzOEzcy4uA{V|wM4~|{7-0C?WYWxH_mTG&^7iNR;(3&vfNtuBX(fzg{+hD6p!uus z?pZzyl%T~D$+5Vux;2k;dua_A{MgdN8{N6&zJxKXIdXq8k+nn}vfG#7I=pBx## zid;?C)m?j#KF@_)O&-SXF228Uq>f@c@`l*W{hj}gH~SQFUb(Ci?%}JJjdi*H{(;U| zUq|o8o-Ulyy#;gM;64a*J2p4#UsF@!@8#bv8?ARk`Fb}DtnKaTzsTfMS60D6e&ovG z8XCcNBQD}y+f)k=<<>R9-s`tW zI)k{_y7r^}DBy*sSOUH_0sh|dQ*ee)uBe6k`J*eU<@?d_6hE`#=%p0V@lf@~Q%Y%C z2LEBO=0?czW6e$QF8`=`{`Ax6!(Jl1KzNbx4&f9bOju2b5$Mka>mqa$+6Ws6)r6G< zm2ee-9!QoY(A(rbE%}>8MT@N`>=V`@-NHgwmYpEDhfI0>Kemd|elS`wd!HYd7Vxtz zErnr!mA}+4`@Q}!_pV%3y@Sj%gslYkBA1hNmav9zS-E1_GbBDnI7wJcc%0x?g@;Hw NOgKWgk)K|v{{hDyc+mg= delta 2421 zcma)-eQZ-z6u{qo?d!U(-C$pA?5k`mtefjLHiko7?AQhb*f$X7l2ZEK)^^=%x$h0! z)_vdv{(&WM6F}HNBoUJ-5gQSWi>M@iBbpH7BMN``hyEiP_(L%#dnF14W&9rTkK5gQIeDf+I8?K zA8~ALdNiM2jdDSLk{dNfjlo2fh{7b_!;c!^KNqyH{EWPeq^L0}h=%=qHaAHK7d3Qp zP7`a(*f;L9(kh%Sc|()Cv8lt;)Y9nb?CPK!VUU)h#z-aK9?c|5zOX3K9vt6^NI|8b zL_z^oOY`(es>c_WRY-;>B*}fMpRNZ3r%6vhw{aj$CEAXDZRlrE2O?UsFU(G7<}Tj^ zBQL|F*a4h@Cz-@bj0E4QB%e1DGyE3Ce#opLg_^L-s|0+*31Xm6QdGM}Mt)2Hua^7d zNtywI#_F?9lFjU!EI(;u4cQH3F1A10Lu`9MIV_Bft{EAnd)Y5bpGX>o5y~T?Y;{g0 z8Dst&yGb)_a93$;DkobQ0o_gZO3v4A5v?Rd4L~EEbQ}KZ1vm}yJ;v{%eF*D3Y$(Zq zJF%;|gly6ba(J(1R#e)n1_mU$98_A;K)^@C3glN79~6wni|T?fnoZ9!x_Q9=9y8^y zt^F1JVUxKinIt`iQGL@@5;cefH+odS!iWmloXGbZ=u_~|0FRMP0 z!(fWe@Y+2HRYhH>97Fig;;(=z_4@ifY`xtHrM25$lez`8_u*0Sq;UKqVvW6JFC}Cf z`+nKFR1DxW{;fKuSzX~Wp#_FhnWyku*8oT~h&Y2djljBcnjbo8Ccor`gGXmruqY@0 z6xvK97F5}B7|UblirydtY)5f1nPy|fMFNI&u$PLjIM+f93a&(*YY4q(-J!4xgIt-lr?sTLZ{g53Mcfq8!H@Smi@k>+@XKL z!_3CJh9b!#-G|e}TL#-wl9#D3Tt6s>aXF-4BYS8&n<&X8t79`Ia|90V)1^0|Ij%0N zB}bTCR+)MUW0c@hA2icx_F7p1>4{x0s)UPJZSJj|P4n)cT=1O}CFzM!oDuJ!B0bA~ zcJ(*we&((wcSmFE_LgR-F1?N)ehxPC`9xvF1!aOYd61p02*yJtw6=9^Y;Ea$^dkNl zL$kn|!Vwh?pmLbCSN0X*s^}Ep(P7xl%3^Z#^(sQfkD#@_PnbRWG2&CiX9zt^BF#fN z7x4k&Ljx7+X0*uCc1>V$(?&rjO&z zY@#TiXT#OgX&75GVzY%F6OyC|+}h5Xa&m!fuc;jW612|8Xd=2=5J`wP5$_|eAl^e< zN4$;5Mm&jVKr|xohK%2|^(b{93J|Lh*m`I!0xw9tS5dr?=uR1_cd;!fwIJ~P$2-XL zD9s}DvcLoIKaxl>Q+fvI4R+Cx#l}|EBxRWM&FN;rY%*uDxm9%qLuhjlpbWs+Lf-$D87ogfG9* Date: Tue, 7 Nov 2017 17:37:29 -0700 Subject: [PATCH 22/34] Removed .pyc file, added .gitignore --- .gitignore | 1 + __pycache__/rtsp.cpython-36.pyc | Bin 14046 -> 0 bytes 2 files changed, 1 insertion(+) create mode 100644 .gitignore delete mode 100644 __pycache__/rtsp.cpython-36.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/__pycache__/rtsp.cpython-36.pyc b/__pycache__/rtsp.cpython-36.pyc deleted file mode 100644 index 75de967fb41e86539f9aa99ea7311bcab5296d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14046 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 jbaf0P From 3fdc08310db01f86683bf6579cf99bafa3eeb754 Mon Sep 17 00:00:00 2001 From: killian441 Date: Tue, 7 Nov 2017 17:40:18 -0700 Subject: [PATCH 23/34] Added Try/Except block around header delimiter (#3) * Added Try/Except block around header delimiter Headers should end with a carriage return/line feed at the end of each line and then a blank line (with a CRLF) to end the header. In some cases, for example a 406 reponse, the header will not have the blank line and instead the server will close the connection. In this case the code raises a ValueError exception which is not very helpfully. Inserting this try/except block adds clarity to what failed. * Removed .pyc file, added .gitignore --- .gitignore | 1 + __pycache__/rtsp.cpython-36.pyc | Bin 13863 -> 0 bytes rtsp.py | 6 +++++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .gitignore delete mode 100644 __pycache__/rtsp.cpython-36.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/__pycache__/rtsp.cpython-36.pyc b/__pycache__/rtsp.cpython-36.pyc deleted file mode 100644 index f06128777b05148ad89c3bd4d84ca8d58e808065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13863 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 bff2=fT_ffgPWt)hSY6~fRvr;z{H6XE!l$jF diff --git a/rtsp.py b/rtsp.py index 3a09e4d..1332d08 100644 --- a/rtsp.py +++ b/rtsp.py @@ -180,7 +180,11 @@ def recv_msg(self): msg = '' if self.cache(): tmp = self.cache() - (msg, tmp) = tmp.split(HEADER_END_STR, 1) + try: + (msg, tmp) = tmp.split(HEADER_END_STR, 1) + except ValueError as e: + self._callback(self._get_time_str() + '\n' + tmp) + raise RTSPError('Response did not contain double CRLF') content_length = self._get_content_length(msg) msg += HEADER_END_STR + tmp[:content_length] self.set_cache(tmp[content_length:]) From 5ff660430d4043b992736a24159f8c85811b0376 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Fri, 11 May 2018 15:08:05 -0600 Subject: [PATCH 24/34] GET_PARAM now callsback Best I can tell, GET_PARAMETER is skipped being sent to callback because it is part of the heartbeat, and will get called every so often (current default 10s). I suppose I understand the intent, but not sure its the right approach, with the callback parameter, I want to see all traffic between server and client. Also if CSeq isn't in response, teardown() and raise error. --- rtsp.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rtsp.py b/rtsp.py index 1332d08..6f869f9 100644 --- a/rtsp.py +++ b/rtsp.py @@ -254,9 +254,21 @@ def _get_time_str(self): def _process_response(self, msg): '''Process the response message''' status, headers, body = self._parse_response(msg) - rsp_cseq = int(headers['cseq']) - if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': + try: + rsp_cseq = int(headers['cseq']) + except KeyError as e: self._callback(self._get_time_str() + '\n' + msg) + self.do_teardown() + raise RTSPError('Unexpected response from server') + + # Best I can tell, GET_PARAMETER is skipped being sent to callback + # because it is part of the heartbeat, and will get called every so + # often (current default 10s). I suppose I understand the intent, but + # not sure its the right approach, with the callback parameter, I + # want to see all traffic between server and client. + #if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': + # self._callback(self._get_time_str() + '\n' + msg) + self._callback(self._get_time_str() + '\n' + msg) if status == 401 and not self._auth: self._add_auth(headers['www-authenticate']) self.do_replay_request() @@ -325,8 +337,9 @@ def _sendmsg(self, method, url, headers): for (k, v) in list(headers.items()): msg += END_OF_LINE + '%s: %s'%(k, str(v)) msg += HEADER_END_STR # End headers - if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: - self._callback(self._get_time_str() + END_OF_LINE + msg) + #if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: + # self._callback(self._get_time_str() + END_OF_LINE + msg) + self._callback(self._get_time_str() + END_OF_LINE + msg) try: self._sock.send(msg.encode()) except socket.error as e: From 3503ce0e038974bce8532026013eabd4023d4edb Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sat, 12 May 2018 21:01:09 -0600 Subject: [PATCH 25/34] Added default Transport_type --- README.md | 3 ++- rtsp.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 97c050f..cfbca0f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Getting Started myrtsp.do_describe() while myrtsp.state != 'describe': time.sleep(0.1) - myrtsp.do_setup(rtsp.track_id_str) + myrtsp.TRANSPORT_TYPE_LIST = ['rtp_over_udp','rtp_over_tcp'] + myrtsp.do_setup(track_id) while myrtsp.state != 'setup': time.sleep(0.1) #Open socket to capture frames here diff --git a/rtsp.py b/rtsp.py index 6f869f9..6fba0ba 100644 --- a/rtsp.py +++ b/rtsp.py @@ -39,7 +39,7 @@ class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass class RTSPClient(threading.Thread): - TRANSPORT_TYPE_LIST = [] + TRANSPORT_TYPE_LIST = ['rtp_over_tcp'] NAT_IP_PORT = '' ENABLE_ARQ = False ENABLE_FEC = False From 8e5020990b27e8460ea623230d221999fcec67fa Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sun, 13 May 2018 17:06:12 -0600 Subject: [PATCH 26/34] Break recv_msg into two functions _recv_msg will continously poll for incoming data, _parse_msg will try to pull out any data that requires action. --- rtsp.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/rtsp.py b/rtsp.py index 6fba0ba..af3f4f5 100644 --- a/rtsp.py +++ b/rtsp.py @@ -108,7 +108,14 @@ def close(self): def run(self): try: while self.running: - self.response = msg = self.recv_msg() + #Need to refactor this. This should continuously try to recv + # data and if there is data in the cache then try to match it + # to something. Because right now we keep jumping into here, + # we aren't getting all the data before leaving, and then the + # next time in we get the rest of the data. + #self.response = msg = self.recv_msg() + self._recv_msg() + self.response = msg = self._parse_msg() if msg.startswith('RTSP'): self._process_response(msg) elif msg.startswith('ANNOUNCE'): @@ -165,6 +172,37 @@ def _update_dest_ip(self): self._dest_ip = self._sock.getsockname()[0] self._callback('DEST_IP: %s\n' % self._dest_ip) + def _recv_msg(self): + '''Continously check for new data and put it in + cache.''' + try: + while not (not self.running): + more = self._sock.recv(2048) + if not more: + break + self.cache(more.decode()) + except socket.error as e: + RTSPNetError('Receive data error: %s' % e) + + def _parse_msg(self): + '''Read through the cache and pull out a complete + response or ANNOUNCE notification message''' + msg = '' + print('here') + if self.cache(): + print('this') + tmp = self.cache() + try: + (msg, tmp) = tmp.split(HEADER_END_STR, 1) + except ValueError as e: + self._callback(self._get_time_str() + '\n' + tmp) + raise RTSPError('Response did not contain double CRLF') + content_length = self._get_content_length(msg) + msg += HEADER_END_STR + tmp[:content_length] + self.set_cache(tmp[content_length:]) + return msg + + """ def recv_msg(self): '''A complete response message or an ANNOUNCE notification message is received''' @@ -189,6 +227,7 @@ def recv_msg(self): msg += HEADER_END_STR + tmp[:content_length] self.set_cache(tmp[content_length:]) return msg + """ def _add_auth(self, msg): '''Authentication request string From a9bdcb3f18bdb55572ab6ddd5612fe5034f53dc0 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sun, 13 May 2018 17:26:44 -0600 Subject: [PATCH 27/34] Turn off blocking on the socket --- rtsp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rtsp.py b/rtsp.py index af3f4f5..f6ff6fe 100644 --- a/rtsp.py +++ b/rtsp.py @@ -176,6 +176,7 @@ def _recv_msg(self): '''Continously check for new data and put it in cache.''' try: + self._sock.setblocking(0) #Turn off blocking for the socket while not (not self.running): more = self._sock.recv(2048) if not more: @@ -188,9 +189,7 @@ def _parse_msg(self): '''Read through the cache and pull out a complete response or ANNOUNCE notification message''' msg = '' - print('here') if self.cache(): - print('this') tmp = self.cache() try: (msg, tmp) = tmp.split(HEADER_END_STR, 1) From 322d505b81c1d819fe14a68f627250913079df9d Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sun, 13 May 2018 20:16:09 -0600 Subject: [PATCH 28/34] Simplified _recv_msg() --- rtsp.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/rtsp.py b/rtsp.py index f6ff6fe..739cb10 100644 --- a/rtsp.py +++ b/rtsp.py @@ -61,7 +61,7 @@ def __init__(self, url, dest_ip='', callback=None, socks=None): self._parsed_url.path self._session_id = '' self._sock = None - self._socks = socks + self._socks = socks self.cur_range = 'npt=end-' self.cur_scale = 1 self.location = '' @@ -153,6 +153,9 @@ def _connect_server(self): try: self._sock = self._socks or socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._parsed_url.hostname, self._server_port)) + # Turning off blocking here, as the socket is currently monitored + # in its own thread. + self._sock.setblocking(0) except socket.error as e: raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._parsed_url.hostname, self._server_port)) @@ -176,12 +179,8 @@ def _recv_msg(self): '''Continously check for new data and put it in cache.''' try: - self._sock.setblocking(0) #Turn off blocking for the socket - while not (not self.running): - more = self._sock.recv(2048) - if not more: - break - self.cache(more.decode()) + more = self._sock.recv(2048) + self.cache(more.decode()) except socket.error as e: RTSPNetError('Receive data error: %s' % e) From ad57616c1b1ba5529743cdf635f22a43747f95dd Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Sun, 13 May 2018 21:29:45 -0600 Subject: [PATCH 29/34] _parse_msg now waits for full message before sending it out to be processed. Removed the slash from the do_setup() if sending a string, now the user must provide it. Added all basic supported transport type to the default list. If it must be updated for more advanced types, may as well include the basics. --- README.md | 1 - rtsp.py | 28 ++++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cfbca0f..9934383 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Getting Started myrtsp.do_describe() while myrtsp.state != 'describe': time.sleep(0.1) - myrtsp.TRANSPORT_TYPE_LIST = ['rtp_over_udp','rtp_over_tcp'] myrtsp.do_setup(track_id) while myrtsp.state != 'setup': time.sleep(0.1) diff --git a/rtsp.py b/rtsp.py index 739cb10..409a449 100644 --- a/rtsp.py +++ b/rtsp.py @@ -39,7 +39,7 @@ class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass class RTSPClient(threading.Thread): - TRANSPORT_TYPE_LIST = ['rtp_over_tcp'] + TRANSPORT_TYPE_LIST = ['ts_over_tcp','rtp_over_tcp','ts_over_udp','rtp_over_udp'] NAT_IP_PORT = '' ENABLE_ARQ = False ENABLE_FEC = False @@ -188,16 +188,23 @@ def _parse_msg(self): '''Read through the cache and pull out a complete response or ANNOUNCE notification message''' msg = '' - if self.cache(): - tmp = self.cache() + tmp = self.cache() + if tmp: try: - (msg, tmp) = tmp.split(HEADER_END_STR, 1) + # Check here for a header, if the cache isn't empty and there + # isn't a HEADER_END_STR, then there isn't a proper header in + # the response. For now this will generate an error and fail. + (header, body) = tmp.split(HEADER_END_STR, 1) except ValueError as e: self._callback(self._get_time_str() + '\n' + tmp) raise RTSPError('Response did not contain double CRLF') - content_length = self._get_content_length(msg) - msg += HEADER_END_STR + tmp[:content_length] - self.set_cache(tmp[content_length:]) + content_length = self._get_content_length(header) + # If the body of the message is less than the given content_length + # then the full message hasn't been received so bail. + if (len(body) < content_length): + return '' + msg = header + HEADER_END_STR + body[:content_length] + self.set_cache(body[content_length:]) return msg """ @@ -413,13 +420,14 @@ def do_describe(self, headers={}): self._sendmsg('DESCRIBE', self._orig_url, headers) def do_setup(self, track_id_str=None, headers={}): + #TODO: Currently issues SETUP for all tracks but doesn't keep track + # of all sessions or teardown all of them. if self._auth: headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() - #TODO: Currently issues SETUP for all tracks but doesn't keep track - # of all sessions or teardown all of them. + # If a string is supplied, it must contain the proceeding '/' if isinstance(track_id_str,str): - self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) + self._sendmsg('SETUP', self._orig_url + track_id_str, headers) elif isinstance(track_id_str, int): self._sendmsg('SETUP', self._orig_url + '/' + From 0d17fbf1ba1c90d0ebf64103e984b9780a984285 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 16 May 2018 21:57:42 -0600 Subject: [PATCH 30/34] Some logic around setting up tracks --- rtsp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rtsp.py b/rtsp.py index 409a449..7eb5506 100644 --- a/rtsp.py +++ b/rtsp.py @@ -425,8 +425,11 @@ def do_setup(self, track_id_str=None, headers={}): if self._auth: headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() - # If a string is supplied, it must contain the proceeding '/' if isinstance(track_id_str,str): + if not track_id_str.startswith('/') and not track_id_str == '': + track_id_str = '/' + track_id_str + if track_id_str.startswith(self._orig_url): + track_id_str = track_id_str.lstrip(self._orig_url) self._sendmsg('SETUP', self._orig_url + track_id_str, headers) elif isinstance(track_id_str, int): self._sendmsg('SETUP', self._orig_url + From 3f344cbf858ea37f4eb642ebccdff2abfce1ac67 Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 16 May 2018 22:12:17 -0600 Subject: [PATCH 31/34] rearrange setup logic --- rtsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsp.py b/rtsp.py index 7eb5506..0370cc0 100644 --- a/rtsp.py +++ b/rtsp.py @@ -426,10 +426,10 @@ def do_setup(self, track_id_str=None, headers={}): headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() if isinstance(track_id_str,str): - if not track_id_str.startswith('/') and not track_id_str == '': - track_id_str = '/' + track_id_str if track_id_str.startswith(self._orig_url): track_id_str = track_id_str.lstrip(self._orig_url) + elif not track_id_str.startswith('/') and not track_id_str == '': + track_id_str = '/' + track_id_str self._sendmsg('SETUP', self._orig_url + track_id_str, headers) elif isinstance(track_id_str, int): self._sendmsg('SETUP', self._orig_url + From f2fa80f0b48982d749bd90bce967fb4ee98e632b Mon Sep 17 00:00:00 2001 From: Mike Killian Date: Wed, 16 May 2018 22:14:23 -0600 Subject: [PATCH 32/34] revert transport_type_list change --- README.md | 1 + rtsp.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9934383..cfbca0f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Getting Started myrtsp.do_describe() while myrtsp.state != 'describe': time.sleep(0.1) + myrtsp.TRANSPORT_TYPE_LIST = ['rtp_over_udp','rtp_over_tcp'] myrtsp.do_setup(track_id) while myrtsp.state != 'setup': time.sleep(0.1) diff --git a/rtsp.py b/rtsp.py index 0370cc0..cf4b324 100644 --- a/rtsp.py +++ b/rtsp.py @@ -39,7 +39,7 @@ class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass class RTSPClient(threading.Thread): - TRANSPORT_TYPE_LIST = ['ts_over_tcp','rtp_over_tcp','ts_over_udp','rtp_over_udp'] + TRANSPORT_TYPE_LIST = [] NAT_IP_PORT = '' ENABLE_ARQ = False ENABLE_FEC = False From 5b1e63c5032f9a9cce72af73dd51f95518dc4813 Mon Sep 17 00:00:00 2001 From: killian441 Date: Tue, 19 Jun 2018 17:29:45 -0600 Subject: [PATCH 33/34] New functionality to receive data from socket (#4) * Added Try/Except block around header delimiter Headers should end with a carriage return/line feed at the end of each line and then a blank line (with a CRLF) to end the header. In some cases, for example a 406 reponse, the header will not have the blank line and instead the server will close the connection. In this case the code raises a ValueError exception which is not very helpfully. Inserting this try/except block adds clarity to what failed. * Removed .pyc file, added .gitignore * GET_PARAM now callsback Best I can tell, GET_PARAMETER is skipped being sent to callback because it is part of the heartbeat, and will get called every so often (current default 10s). I suppose I understand the intent, but not sure its the right approach, with the callback parameter, I want to see all traffic between server and client. Also if CSeq isn't in response, teardown() and raise error. * Break recv_msg into two functions _recv_msg will continously poll for incoming data, _parse_msg will try to pull out any data that requires action. * Turn off blocking on the socket * _parse_msg now waits for full message before sending it out to be processed. --- README.md | 3 ++- rtsp.py | 75 ++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 97c050f..cfbca0f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Getting Started myrtsp.do_describe() while myrtsp.state != 'describe': time.sleep(0.1) - myrtsp.do_setup(rtsp.track_id_str) + myrtsp.TRANSPORT_TYPE_LIST = ['rtp_over_udp','rtp_over_tcp'] + myrtsp.do_setup(track_id) while myrtsp.state != 'setup': time.sleep(0.1) #Open socket to capture frames here diff --git a/rtsp.py b/rtsp.py index 1332d08..ed66e09 100644 --- a/rtsp.py +++ b/rtsp.py @@ -61,7 +61,7 @@ def __init__(self, url, dest_ip='', callback=None, socks=None): self._parsed_url.path self._session_id = '' self._sock = None - self._socks = socks + self._socks = socks self.cur_range = 'npt=end-' self.cur_scale = 1 self.location = '' @@ -108,7 +108,8 @@ def close(self): def run(self): try: while self.running: - self.response = msg = self.recv_msg() + self._recv_msg() + self.response = msg = self._parse_msg() if msg.startswith('RTSP'): self._process_response(msg) elif msg.startswith('ANNOUNCE'): @@ -146,6 +147,9 @@ def _connect_server(self): try: self._sock = self._socks or socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self._parsed_url.hostname, self._server_port)) + # Turning off blocking here, as the socket is currently monitored + # in its own thread. + self._sock.setblocking(0) except socket.error as e: raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._parsed_url.hostname, self._server_port)) @@ -165,29 +169,36 @@ def _update_dest_ip(self): self._dest_ip = self._sock.getsockname()[0] self._callback('DEST_IP: %s\n' % self._dest_ip) - def recv_msg(self): - '''A complete response message or - an ANNOUNCE notification message is received''' + def _recv_msg(self): + '''Continously check for new data and put it in + cache.''' try: - while not (not self.running or HEADER_END_STR in self.cache()): - more = self._sock.recv(2048) - if not more: - break - self.cache(more.decode()) + more = self._sock.recv(2048) + self.cache(more.decode()) except socket.error as e: RTSPNetError('Receive data error: %s' % e) + def _parse_msg(self): + '''Read through the cache and pull out a complete + response or ANNOUNCE notification message''' msg = '' - if self.cache(): - tmp = self.cache() + tmp = self.cache() + if tmp: try: - (msg, tmp) = tmp.split(HEADER_END_STR, 1) + # Check here for a header, if the cache isn't empty and there + # isn't a HEADER_END_STR, then there isn't a proper header in + # the response. For now this will generate an error and fail. + (header, body) = tmp.split(HEADER_END_STR, 1) except ValueError as e: - self._callback(self._get_time_str() + '\n' + tmp) - raise RTSPError('Response did not contain double CRLF') - content_length = self._get_content_length(msg) - msg += HEADER_END_STR + tmp[:content_length] - self.set_cache(tmp[content_length:]) + self._callback(self._get_time_str() + '\n' + tmp) + raise RTSPError('Response did not contain double CRLF') + content_length = self._get_content_length(header) + # If the body of the message is less than the given content_length + # then the full message hasn't been received so bail. + if (len(body) < content_length): + return '' + msg = header + HEADER_END_STR + body[:content_length] + self.set_cache(body[content_length:]) return msg def _add_auth(self, msg): @@ -254,9 +265,21 @@ def _get_time_str(self): def _process_response(self, msg): '''Process the response message''' status, headers, body = self._parse_response(msg) - rsp_cseq = int(headers['cseq']) - if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': + try: + rsp_cseq = int(headers['cseq']) + except KeyError as e: self._callback(self._get_time_str() + '\n' + msg) + self.do_teardown() + raise RTSPError('Unexpected response from server') + + # Best I can tell, GET_PARAMETER is skipped being sent to callback + # because it is part of the heartbeat, and will get called every so + # often (current default 10s). I suppose I understand the intent, but + # not sure its the right approach, with the callback parameter, I + # want to see all traffic between server and client. + #if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': + # self._callback(self._get_time_str() + '\n' + msg) + self._callback(self._get_time_str() + '\n' + msg) if status == 401 and not self._auth: self._add_auth(headers['www-authenticate']) self.do_replay_request() @@ -325,8 +348,9 @@ def _sendmsg(self, method, url, headers): for (k, v) in list(headers.items()): msg += END_OF_LINE + '%s: %s'%(k, str(v)) msg += HEADER_END_STR # End headers - if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: - self._callback(self._get_time_str() + END_OF_LINE + msg) + #if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: + # self._callback(self._get_time_str() + END_OF_LINE + msg) + self._callback(self._get_time_str() + END_OF_LINE + msg) try: self._sock.send(msg.encode()) except socket.error as e: @@ -363,13 +387,14 @@ def do_describe(self, headers={}): self._sendmsg('DESCRIBE', self._orig_url, headers) def do_setup(self, track_id_str=None, headers={}): + #TODO: Currently issues SETUP for all tracks but doesn't keep track + # of all sessions or teardown all of them. if self._auth: headers['Authorization'] = self._auth headers['Transport'] = self._get_transport_type() - #TODO: Currently issues SETUP for all tracks but doesn't keep track - # of all sessions or teardown all of them. + # If a string is supplied, it must contain the proceeding '/' if isinstance(track_id_str,str): - self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) + self._sendmsg('SETUP', self._orig_url + track_id_str, headers) elif isinstance(track_id_str, int): self._sendmsg('SETUP', self._orig_url + '/' + From 874501e62fd873d79b6eac3d38a04a6867781d79 Mon Sep 17 00:00:00 2001 From: killian441 Date: Tue, 16 Oct 2018 22:27:58 -0600 Subject: [PATCH 34/34] Added Basic Auth and modified _parse_track_id (#6) * modified _parse_track_id I am not parsing all the data returned after an 'a=control' string. Made some changes to fix this. * Added Basic Auth Added Basic autherization per RFC 2068. * Updated how track list is populated --- rtsp.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rtsp.py b/rtsp.py index 32d491d..4d1db73 100644 --- a/rtsp.py +++ b/rtsp.py @@ -11,7 +11,7 @@ # Ported to Python3, removed GoodThread # -killian441 -import ast, datetime, re, socket, threading, time, traceback +import ast, base64, datetime, re, socket, threading, time, traceback from hashlib import md5 try: from urllib.parse import urlparse @@ -155,7 +155,7 @@ def _connect_server(self): (e, self._parsed_url.hostname, self._server_port)) def _update_content_base(self, msg): - m = re.search(r'[Cc]ontent-[Bb]ase:\s?(?P[a-zA-Z0-9_:\/\.]+)', msg) + m = re.search(r'[Cc]ontent-[Bb]ase:\s?(?P[a-zA-Z0-9_:\/\.-]+)', msg) if (m and m.group('base')): new_url = m.group('base') if new_url[-1] == '/': @@ -206,8 +206,13 @@ def _add_auth(self, msg): (i.e. everything after "www-authentication")''' #TODO: this is too simplistic and will fail if more than one method # is acceptable, among other issues + # i.e. REALM-value is case-sensitive, so theres a failure. if msg.lower().startswith('basic'): - pass + response = self._parsed_url.username + ':' + \ + self._parsed_url.password + response = base64.b64encode(response.encode()) + auth_string = 'Basic {}'.format(response) + self._auth = auth_string elif msg.lower().startswith('digest '): mod_msg = '{'+msg[7:].replace('=',':')+'}' mod_msg = mod_msg.replace('realm','"realm"') @@ -328,8 +333,11 @@ def _parse_header_params(self, header_param_lines): def _parse_track_id(self, sdp): '''Resolves a string of the form trackID = 2 from sdp''' - m = re.findall(r'a=control:(?P[\w=\d]+)', sdp, re.S) - self.track_id_lst = m + #m = re.findall(r'a=control:(?P[\w=\d]+)', sdp, re.S) + # The following returns full url after a=control: + m = re.findall(r'a=control:(?P[:/\.\w\d]+[=\d][\d]*)', sdp, re.S) + m.remove(self._orig_url) + self.track_id_lst = [x.replace(self._orig_url+'/','') for x in m] def _next_seq(self): self._cseq += 1