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/README.md b/README.md index 4418ea6..cfbca0f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,49 @@ -# python-rtsp-client -A rtsp client write in python +python-rtsp-client +================== -![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) +A basic rtsp client writen in pure python - Usage: rtsp-client.py [options] url +Getting Started +--------------- + + from rtsp import RTSPClient + myrtsp = RTSPClient(url='rtsp://username:password@hostname:port/path',callback=print) + try: + 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) + #Open socket to capture frames here + myrtsp.do_play(rtsp.cur_range, rtsp.cur_scale) + except: + myrtsp.do_teardown() + + +Examples +-------- +Usage: setupandplay.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. 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/examples/setupandplay.py b/examples/setupandplay.py new file mode 100644 index 0000000..33717b2 --- /dev/null +++ b/examples/setupandplay.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +#----------------------------------------------------------------------- +# 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(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) + 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: + 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 0eb7029..4d1db73 100644 --- a/rtsp.py +++ b/rtsp.py @@ -2,78 +2,76 @@ #-*- 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 +# +# Date: 2017-05-30 +# Ported to Python3, removed GoodThread +# -killian441 -import sys, re, socket, time, datetime, traceback -import exceptions, urlparse -from optparse import OptionParser -import util - -DEFAULT_SERVER_PORT = 554 -TRANSPORT_TYPE_LIST = [] -CLIENT_PORT_RANGE = '10014-10015' -NAT_IP_PORT = '' -ENABLE_ARQ = False -ENABLE_FEC = False -PING = False +import ast, base64, 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 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' 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 = 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') -#-------------------------------------------------------------------------- - -class RTSPError(exceptions.Exception): pass +class RTSPError(Exception): pass class RTSPURLError(RTSPError): pass class RTSPNetError(RTSPError): pass -class RTSPClient(util.GoodThread): - def __init__(self, url, dest_ip=''): - global CUR_RANGE - util.GoodThread.__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.playing = False - self.location = '' +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, socks=None): + threading.Thread.__init__(self) + self._auth = None + self._callback = callback or (lambda x: x) + self._cseq = 0 + self._cseq_map = {} # {CSeq:Method} mapping + self._dest_ip = dest_ip + 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._socks = socks + self.cur_range = 'npt=end-' + self.cur_scale = 1 + self.location = '' + self.response = None 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(): - CUR_RANGE = 'npt=0.00000-' # On demand starts from the beginning + self.running = True + self.state = None + 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() self._update_dest_ip() self.closed = False @@ -102,26 +100,28 @@ def cache(self, s=None): def close(self): if not self.closed: - self.closed = True - self.stop() - self.playing = False + self.closed = True + self.running = False + self.state = 'closed' self._sock.close() def run(self): try: - while not self.stopped(): - self.response = msg = self.recv_msg() + while self.running: + self._recv_msg() + self.response = msg = self._parse_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) + '''Resolve url, return the urlparse object''' + parsed = urlparse(url) scheme = parsed.scheme.lower() ip = parsed.hostname port = parsed.port and int(parsed.port) or DEFAULT_SERVER_PORT @@ -134,89 +134,183 @@ 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)) - - return scheme, ip, port, target + raise RTSPURLError('Invalid url: %s (host="%s" \ + port=%u target="%s")' % + (url, 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)) - except socket.error, e: - raise RTSPNetError('socket error: %s [%s:%d]' % (e, self._server_ip, self._server_port)) + 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)) + + 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, the same IP is used by default with RTSP''' + '''If DEST_IP is not specified, + by default the same IP is used as this RTSP client''' 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 an ANNOUNCE notification message is received''' + def _recv_msg(self): + '''Continously check for new data and put it in + cache.''' try: - while not (self.stopped() or HEADER_END_STR in self.cache()): - more = self._sock.recv(2048) - if not more: - break - self.cache(more) - except socket.error, e: + 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() - - (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:]) + tmp = self.cache() + if tmp: + try: + # 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(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): + '''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 + # i.e. REALM-value is case-sensitive, so theres a failure. + if msg.lower().startswith('basic'): + 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"') + 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) + self._auth = auth_string + else: # Some other failure + self.do_teardown() + raise RTSPError('Authentication failure') + + def _auth_digest(self, auth_parameters): + '''Creates a response string for digest authorization, only works + 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, + 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: + self.do_teardown() + raise RTSPError('Authentication required, no username provided') + 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): - # 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) 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': - PRINT(self._get_time_str() + '\n' + msg) - if status == 302: + 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() + elif status == 302: self.location = headers['location'] - if status != 200: + elif status != 200: self.do_teardown() - if self._cseq_map[rsp_cseq] == 'DESCRIBE': - track_id_str = self._parse_track_id(body) - self.do_setup(track_id_str) + elif self._cseq_map[rsp_cseq] == 'DESCRIBE': #Implies status 200 + self._update_content_base(msg) + self._parse_track_id(body) + self.state = 'describe' elif self._cseq_map[rsp_cseq] == 'SETUP': self._session_id = headers['session'] - self.do_play(CUR_RANGE, 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''' - 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): - 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() @@ -239,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.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) + # 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 @@ -256,69 +353,119 @@ 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) + #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) - except socket.error, e: - PRINT('Send msg error: %s'%e, RED) + self._sock.send(msg.encode()) + except socket.error as e: + self._callback('Send msg error: %s'%e) raise RTSPNetError(e) 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' - for t in TRANSPORT_TYPE_LIST: + ip_type = 'unicast' #TODO: if IPAddress(DEST_IP).is_unicast() + # else 'multicast' + 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 = {} + 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 = {} + 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() - self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers) + if isinstance(track_id_str,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 + + '/' + + 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 = {} + 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): - self._sendmsg('PAUSE', self._orig_url, {}) - - def do_teardown(self): - self._sendmsg('TEARDOWN', self._orig_url, {}) - self.stop() - - def do_options(self): - self._sendmsg('OPTIONS', self._orig_url, {}) - - def do_get_parameter(self): - self._sendmsg('GET_PARAMETER', self._orig_url, {}) + 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.stopped(): + if not self.running: self.do_get_parameter() - threading.Timer(HEARTBEAT_INTERVAL, self.send_heart_beat_msg).start() + threading.Timer(self.HEARTBEAT_INTERVAL, + self.send_heart_beat_msg).start() def ping(self, timeout=0.01): '''No exceptions == service available''' @@ -326,134 +473,3 @@ def ping(self, timeout=0.01): time.sleep(timeout) self.close() return self.response - -#----------------------------------------------------------------------- -# Input with autocompletion -#----------------------------------------------------------------------- -import readline -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) - cmd = raw_input(COLOR_STR('Input Command # ', CYAN)) - PRINT('') # add one line - return cmd -#----------------------------------------------------------------------- - -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.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-' - elif cmd == 'backward': - if CUR_SCALE > 0: CUR_SCALE = -1 - CUR_SCALE *= 2; CUR_RANGE = 'npt=now-' - elif cmd == 'begin': - CUR_SCALE = 1; CUR_RANGE = 'npt=beginning-' - elif cmd == 'live': - CUR_SCALE = 1; CUR_RANGE = 'npt=end-' - elif cmd.startswith('play'): - m = re.search(r'range[:\s]+(?P[^\s]+)', cmd) - if m: CUR_RANGE = m.group('range') - m = re.search(r'scale[:\s]+(?P[\d\.]+)', cmd) - if m: CUR_SCALE = int(m.group('scale')) - - 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) - - if PING: - PRINT('PING START', YELLOW) - rtsp.ping() - PRINT('PING DONE', YELLOW) - sys.exit(0) - return - - try: - rtsp.do_describe() - while rtsp.location and not rtsp.stopped(): - if rtsp.playing: - cmd = input_cmd() - exec_cmd(rtsp, cmd) - # 302 redirect to re-establish chain - if rtsp.stopped() 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 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') - parser.add_option('-p', '--client_port', dest='client_port', - 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"') - 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", - help='Add "x-zmssFecCDN:yes" when DESCRIBE') - parser.add_option('-P', '--ping', dest='ping', action="http://github.com/store_true", - help='Just perform DESCRIBE and exit.') - - (options, args) = parser.parse_args() - if len(args) < 1: - 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 - main(url, options.dest_ip) -# EOF #