├── README.md └── rtsp-client.py /README.md: -------------------------------------------------------------------------------- 1 | # python-rtsp-client 2 | A rtsp client write in python 3 | 4 | Usage: rtsp-client.py [options] url 5 | 6 | In running, you can control play by input "forward","backward","begin","live","pause" 7 | or "play" with "range" and "scale" parameter, such as "play range:npt=beginning- scale:2" 8 | You can input "exit","teardown" or ctrl+c to quit 9 | 10 | 11 | Options: 12 | -h, --help show this help message and exit 13 | -t TRANSPORT, --transport=TRANSPORT 14 | Set transport type when SETUP: tcp, udp, tcp_over_rtp, 15 | udp_over_rtp[default] 16 | -d DEST_IP, --dest_ip=DEST_IP 17 | Set dest ip of udp data transmission, default use same 18 | ip with rtsp 19 | -p CLIENT_PORT, --client_port=CLIENT_PORT 20 | Set client port range of udp, default is "10014-10015" 21 | -n NAT, --nat=NAT Add "x-NAT" when DESCRIBE, arg format 22 | "192.168.1.100:20008" 23 | -r, --arq Add "x-zmssRtxSdp:yes" when DESCRIBE 24 | -f, --fec Add "x-zmssFecCDN:yes" when DESCRIBE 25 | -------------------------------------------------------------------------------- /rtsp-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: UTF-8 -*- 3 | # Date: 2015-04-09 4 | 5 | import sys, re, socket, threading, time, datetime, traceback 6 | from optparse import OptionParser 7 | 8 | DEFAULT_SERVER_PORT = 1554 9 | TRANSPORT_TYPE_LIST = [] 10 | DEST_IP = '' 11 | CLIENT_PORT_RANGE = '10014-10015' 12 | NAT_IP_PORT = '' 13 | ENABLE_ARQ = False 14 | ENABLE_FEC = False 15 | 16 | TRANSPORT_TYPE_MAP = { 17 | 'ts_over_tcp' : 'MP2T/TCP;%s;interleaved=0-1,', 18 | 'rtp_over_tcp' : 'MP2T/RTP/TCP;%s;interleaved=0-1,', 19 | 'ts_over_udp' : 'MP2T/UDP;%s;destination=%s;client_port=%s,', 20 | 'rtp_over_udp' : 'MP2T/RTP/UDP;%s;destination=%s;client_port=%s,' 21 | } 22 | 23 | RTSP_VERSION = 'RTSP/1.0' 24 | DEFAULT_USERAGENT = 'Python Rtsp Client 1.0' 25 | HEARTBEAT_INTERVAL = 10 # 10s 26 | 27 | LINE_SPLIT_STR = '\r\n' 28 | HEADER_END_STR = LINE_SPLIT_STR*2 29 | 30 | CUR_RANGE = 'npt=end-' 31 | CUR_SCALE = 1 32 | 33 | #x-notice in ANNOUNCE, BOS-Begin of Stream, EOS-End of Stream 34 | X_NOTICE_EOS,X_NOTICE_BOS,X_NOTICE_CLOSE = 2101,2102,2103 35 | 36 | #-------------------------------------------------------------------------- 37 | # Colored Output in Console 38 | #-------------------------------------------------------------------------- 39 | BLACK,RED,GREEN,YELLOW,BLUE,MAGENTA,CYAN,WHITE = range(90,98) 40 | def COLOR_STR(msg,color=WHITE): 41 | return '\033[%dm%s\033[0m'%(color,msg) 42 | 43 | def PRINT(msg,color=WHITE): 44 | sys.stdout.write(COLOR_STR(msg,color) + '\n') 45 | #-------------------------------------------------------------------------- 46 | 47 | class RTSPClient(threading.Thread): 48 | def __init__(self,url): 49 | global CUR_RANGE 50 | threading.Thread.__init__(self) 51 | self.setDaemon(True) 52 | self._recv_buf = '' 53 | self._sock = None 54 | self._orig_url = url 55 | self._cseq = 0 56 | self._session_id= '' 57 | self._cseq_map = {} # {CSeq:Method}映射 58 | self._server_ip,self._server_port,self._target = self._parse_url(url) 59 | if not self._server_ip or not self._target: 60 | PRINT('Invalid url: %s'%url,RED); sys.exit(1) 61 | if '.sdp' not in self._target.lower(): 62 | CUR_RANGE = 'npt=0.00000-' # 点播从头开始 63 | self._connect_server() 64 | self._update_dest_ip() 65 | self.running = True 66 | self.playing = False 67 | self.location = '' 68 | self.start() 69 | 70 | def run(self): 71 | try: 72 | while self.running: 73 | msg = self.recv_msg() 74 | if msg.startswith('RTSP'): 75 | self._process_response(msg) 76 | elif msg.startswith('ANNOUNCE'): 77 | self._process_announce(msg) 78 | except Exception, e: 79 | PRINT('Error: %s'%e,RED) 80 | traceback.print_exc() 81 | 82 | self.running = False 83 | self.playing = False 84 | self._sock.close() 85 | 86 | def _parse_url(self,url): 87 | '''解析url,返回(ip,port,target)三元组''' 88 | (ip,port,target) = ('',DEFAULT_SERVER_PORT,'') 89 | m = re.match(r'[rtspRTSP:/]+(?P(\d{1,3}\.){3}\d{1,3})(:(?P\d+))?(?P.*)',url) 90 | if m is not None: 91 | ip = m.group('ip') 92 | port = int(m.group('port')) 93 | target = m.group('target') 94 | #PRINT('ip: %s, port: %d, target: %s'%(ip,port,target), GREEN) 95 | return ip,port,target 96 | 97 | def _connect_server(self): 98 | '''连接服务器,建立socket''' 99 | try: 100 | self._sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 101 | self._sock.connect((self._server_ip,self._server_port)) 102 | #PRINT('Connect [%s:%d] success!'%(self._server_ip,self._server_port), GREEN) 103 | except socket.error, e: 104 | sys.stderr.write('ERROR: %s[%s:%d]'%(e,self._server_ip,self._server_port)) 105 | traceback.print_exc() 106 | sys.exit(1) 107 | 108 | def _update_dest_ip(self): 109 | '''如果未指定DEST_IP,默认与RTSP使用相同IP''' 110 | global DEST_IP 111 | if not DEST_IP: 112 | DEST_IP = self._sock.getsockname()[0] 113 | PRINT('DEST_IP: %s\n'%DEST_IP, CYAN) 114 | 115 | def recv_msg(self): 116 | '''收取一个完整响应消息或ANNOUNCE通知消息''' 117 | try: 118 | while True: 119 | if HEADER_END_STR in self._recv_buf: break 120 | more = self._sock.recv(2048) 121 | if not more: break 122 | self._recv_buf += more 123 | except socket.error, e: 124 | PRINT('Receive data error: %s'%e,RED) 125 | sys.exit(-1) 126 | 127 | msg = '' 128 | if self._recv_buf: 129 | (msg,self._recv_buf) = self._recv_buf.split(HEADER_END_STR,1) 130 | content_length = self._get_content_length(msg) 131 | msg += HEADER_END_STR + self._recv_buf[:content_length] 132 | self._recv_buf = self._recv_buf[content_length:] 133 | return msg 134 | 135 | def _get_content_length(self,msg): 136 | '''从消息中解析Content-length''' 137 | m = re.search(r'[Cc]ontent-length:\s?(?P\d+)',msg,re.S) 138 | return (m and int(m.group('len'))) or 0 139 | 140 | def _get_time_str(self): 141 | # python 2.6以上才支持%f参数,为兼容低版本采用以下写法 142 | dt = datetime.datetime.now() 143 | return dt.strftime('%Y-%m-%d %H:%M:%S.') + str(dt.microsecond) 144 | 145 | def _process_response(self,msg): 146 | '''处理响应消息''' 147 | status,headers,body = self._parse_response(msg) 148 | rsp_cseq = int(headers['cseq']) 149 | if self._cseq_map[rsp_cseq] != 'GET_PARAMETER': 150 | PRINT(self._get_time_str() + '\n' + msg) 151 | if status == 302: 152 | self.location = headers['location'] 153 | if status != 200: 154 | self.do_teardown() 155 | if self._cseq_map[rsp_cseq] == 'DESCRIBE': 156 | track_id_str = self._parse_track_id(body) 157 | self.do_setup(track_id_str) 158 | elif self._cseq_map[rsp_cseq] == 'SETUP': 159 | self._session_id = headers['session'] 160 | self.do_play(CUR_RANGE,CUR_SCALE) 161 | self.send_heart_beat_msg() 162 | elif self._cseq_map[rsp_cseq] == 'PLAY': 163 | self.playing = True 164 | 165 | def _process_announce(self,msg): 166 | '''处理ANNOUNCE通知消息''' 167 | global CUR_RANGE,CUR_SCALE 168 | PRINT(msg) 169 | headers = self._parse_header_params(msg.splitlines()[1:]) 170 | x_notice_val = int(headers['x-notice']) 171 | if x_notice_val in (X_NOTICE_EOS,X_NOTICE_BOS): 172 | CUR_SCALE = 1 173 | self.do_play(CUR_RANGE,CUR_SCALE) 174 | elif x_notice_val == X_NOTICE_CLOSE: 175 | self.do_teardown() 176 | 177 | def _parse_response(self,msg): 178 | '''解析响应消息''' 179 | header,body = msg.split(HEADER_END_STR)[:2] 180 | header_lines = header.splitlines() 181 | version,status = header_lines[0].split(None,2)[:2] 182 | headers = self._parse_header_params(header_lines[1:]) 183 | return int(status),headers,body 184 | 185 | def _parse_header_params(self,header_param_lines): 186 | '''解析头部参数''' 187 | headers = {} 188 | for line in header_param_lines: 189 | if line.strip(): # 跳过空行 190 | key,val = line.split(':', 1) 191 | headers[key.lower()] = val.strip() 192 | return headers 193 | 194 | def _parse_track_id(self,sdp): 195 | '''从sdp中解析trackID=2形式的字符串''' 196 | m = re.search(r'a=control:(?P[\w=\d]+)',sdp,re.S) 197 | return (m and m.group('trackid')) or '' 198 | 199 | def _next_seq(self): 200 | self._cseq += 1 201 | return self._cseq 202 | 203 | def _sendmsg(self,method,url,headers): 204 | '''发送消息''' 205 | msg = '%s %s %s'%(method,url,RTSP_VERSION) 206 | headers['User-Agent'] = DEFAULT_USERAGENT 207 | cseq = self._next_seq() 208 | self._cseq_map[cseq] = method 209 | headers['CSeq'] = str(cseq) 210 | if self._session_id: headers['Session'] = self._session_id 211 | for (k,v) in headers.items(): 212 | msg += LINE_SPLIT_STR + '%s: %s'%(k,str(v)) 213 | msg += HEADER_END_STR # End headers 214 | if method != 'GET_PARAMETER' or 'x-RetransSeq' in headers: 215 | PRINT(self._get_time_str() + LINE_SPLIT_STR + msg) 216 | try: 217 | self._sock.send(msg) 218 | except socket.error, e: 219 | PRINT('Send msg error: %s'%e, RED) 220 | 221 | def _get_transport_type(self): 222 | '''获取SETUP时需要的Transport字符串参数''' 223 | transport_str = '' 224 | ip_type = 'unicast' #if IPAddress(DEST_IP).is_unicast() else 'multicast' 225 | for t in TRANSPORT_TYPE_LIST: 226 | if t not in TRANSPORT_TYPE_MAP: 227 | PRINT('Error param: %s'%t,RED) 228 | sys.exit(1) 229 | if t.endswith('tcp'): 230 | transport_str += TRANSPORT_TYPE_MAP[t]%ip_type 231 | else: 232 | transport_str += TRANSPORT_TYPE_MAP[t]%(ip_type,DEST_IP,CLIENT_PORT_RANGE) 233 | return transport_str 234 | 235 | def do_describe(self): 236 | headers = {} 237 | headers['Accept'] = 'application/sdp' 238 | if ENABLE_ARQ: 239 | headers['x-Retrans'] = 'yes' 240 | headers['x-Burst'] = 'yes' 241 | if ENABLE_FEC: headers['x-zmssFecCDN'] = 'yes' 242 | if NAT_IP_PORT: headers['x-NAT'] = NAT_IP_PORT 243 | self._sendmsg('DESCRIBE',self._orig_url,headers) 244 | 245 | def do_setup(self,track_id_str=''): 246 | headers = {} 247 | headers['Transport'] = self._get_transport_type() 248 | self._sendmsg('SETUP',self._orig_url+'/'+track_id_str,headers) 249 | 250 | def do_play(self,range='npt=end-',scale=1): 251 | headers = {} 252 | headers['Range'] = range 253 | headers['Scale'] = scale 254 | self._sendmsg('PLAY',self._orig_url,headers) 255 | 256 | def do_pause(self): 257 | self._sendmsg('PAUSE',self._orig_url,{}) 258 | 259 | def do_teardown(self): 260 | self._sendmsg('TEARDOWN',self._orig_url,{}) 261 | self.running = False 262 | 263 | def do_options(self): 264 | self._sendmsg('OPTIONS',self._orig_url,{}) 265 | 266 | def do_get_parameter(self): 267 | self._sendmsg('GET_PARAMETER',self._orig_url,{}) 268 | 269 | def send_heart_beat_msg(self): 270 | '''定时发送GET_PARAMETER消息保活''' 271 | if self.running: 272 | self.do_get_parameter() 273 | threading.Timer(HEARTBEAT_INTERVAL, self.send_heart_beat_msg).start() 274 | 275 | #----------------------------------------------------------------------- 276 | # Input with autocompletion 277 | #----------------------------------------------------------------------- 278 | import readline 279 | COMMANDS = ['play','range:','scale:','pause','forward','backward','begin','live','teardown','exit','help'] 280 | def complete(text,state): 281 | options = [i for i in COMMANDS if i.startswith(text)] 282 | return (state < len(options) and options[state]) or None 283 | 284 | def input_cmd(): 285 | readline.set_completer_delims(' \t\n') 286 | readline.parse_and_bind("tab: complete") 287 | readline.set_completer(complete) 288 | cmd = raw_input(COLOR_STR('Input Command # ',CYAN)) 289 | PRINT('') # add one line 290 | return cmd 291 | #----------------------------------------------------------------------- 292 | 293 | def exec_cmd(rtsp,cmd): 294 | '''根据命令执行操作''' 295 | global CUR_RANGE,CUR_SCALE 296 | if cmd in ('exit','teardown'): 297 | rtsp.do_teardown() 298 | elif cmd == 'pause': 299 | CUR_SCALE = 1; CUR_RANGE = 'npt=now-' 300 | rtsp.do_pause() 301 | elif cmd == 'help': 302 | PRINT(play_ctrl_help()) 303 | elif cmd == 'forward': 304 | if CUR_SCALE < 0: CUR_SCALE = 1 305 | CUR_SCALE *= 2; CUR_RANGE = 'npt=now-' 306 | elif cmd == 'backward': 307 | if CUR_SCALE > 0: CUR_SCALE = -1 308 | CUR_SCALE *= 2; CUR_RANGE = 'npt=now-' 309 | elif cmd == 'begin': 310 | CUR_SCALE = 1; CUR_RANGE = 'npt=beginning-' 311 | elif cmd == 'live': 312 | CUR_SCALE = 1; CUR_RANGE = 'npt=end-' 313 | elif cmd.startswith('play'): 314 | m = re.search(r'range[:\s]+(?P[^\s]+)',cmd) 315 | if m: CUR_RANGE = m.group('range') 316 | m = re.search(r'scale[:\s]+(?P[\d\.]+)',cmd) 317 | if m: CUR_SCALE = int(m.group('scale')) 318 | 319 | if cmd not in ('pause','exit','teardown','help'): 320 | rtsp.do_play(CUR_RANGE,CUR_SCALE) 321 | 322 | def main(url): 323 | rtsp = RTSPClient(url) 324 | rtsp.do_describe() 325 | try: 326 | while rtsp.running or rtsp.location: 327 | if rtsp.playing: 328 | cmd = input_cmd() 329 | exec_cmd(rtsp,cmd) 330 | # 302重定向重新建链 331 | if not rtsp.running and rtsp.location: 332 | rtsp = RTSPClient(rtsp.location) 333 | rtsp.do_describe() 334 | time.sleep(0.5) 335 | except KeyboardInterrupt: 336 | rtsp.do_teardown() 337 | print '\n^C received, Exit.' 338 | 339 | def play_ctrl_help(): 340 | help = COLOR_STR('In running, you can control play by input "forward","backward","begin","live","pause"\n',MAGENTA) 341 | help += COLOR_STR('or "play" with "range" and "scale" parameter, such as "play range:npt=beginning- scale:2"\n',MAGENTA) 342 | help += COLOR_STR('You can input "exit","teardown" or ctrl+c to quit\n',MAGENTA) 343 | return help 344 | 345 | if __name__ == '__main__': 346 | usage = COLOR_STR('%prog [options] url\n\n',GREEN) + play_ctrl_help() 347 | parser = OptionParser(usage=usage) 348 | parser.add_option('-t','--transport',dest='transport',default='tcp_over_udp',help='Set transport type when SETUP: ts_over_tcp, ts_over_udp, rtp_over_tcp, rtp_over_udp[default]') 349 | parser.add_option('-d','--dest_ip',dest='dest_ip',help='Set dest ip of udp data transmission, default use same ip with rtsp') 350 | parser.add_option('-p','--client_port',dest='client_port',help='Set client port range when SETUP of udp, default is "10014-10015"') 351 | parser.add_option('-n','--nat',dest='nat',help='Add "x-NAT" when DESCRIBE, arg format "192.168.1.100:20008"') 352 | parser.add_option('-r','--arq',dest='arq',action="store_true",help='Add "x-Retrans:yes" when DESCRIBE') 353 | parser.add_option('-f','--fec',dest='fec',action="store_true",help='Add "x-zmssFecCDN:yes" when DESCRIBE') 354 | (options,args) = parser.parse_args() 355 | if len(args) < 1: 356 | parser.print_help() 357 | sys.exit() 358 | 359 | if options.transport: TRANSPORT_TYPE_LIST = options.transport.split(',') 360 | if options.dest_ip: DEST_IP = options.dest_ip; print DEST_IP 361 | if options.client_port: CLIENT_PORT_RANGE = options.client_port 362 | if options.nat: NAT_IP_PORT = options.nat 363 | if options.arq: ENABLE_ARQ = options.arq 364 | if options.fec: ENABLE_FEC = options.fec 365 | url = args[0] 366 | 367 | main(url) 368 | --------------------------------------------------------------------------------