Skip to content

Commit 22bc609

Browse files
committed
Updates
1 parent f215ab8 commit 22bc609

File tree

3 files changed

+384
-1
lines changed

3 files changed

+384
-1
lines changed

examples/rtpframes.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/python
2+
#-----------------------------------------------------------------------
3+
# Input with autocompletion
4+
#-----------------------------------------------------------------------
5+
import readline, sys
6+
sys.path.append('../')
7+
from rtsp import *
8+
from rtp import *
9+
from optparse import OptionParser
10+
COMMANDS = (
11+
'backward',
12+
'begin',
13+
'exit',
14+
'forward',
15+
'help',
16+
'live',
17+
'pause',
18+
'play',
19+
'range:',
20+
'scale:',
21+
'teardown',
22+
)
23+
24+
def complete(text, state):
25+
options = [i for i in COMMANDS if i.startswith(text)]
26+
return (state < len(options) and options[state]) or None
27+
28+
def input_cmd():
29+
readline.set_completer_delims(' \t\n')
30+
readline.parse_and_bind("tab: complete")
31+
readline.set_completer(complete)
32+
if(sys.version_info > (3, 0)):
33+
cmd = input(COLOR_STR('Input Command # ', CYAN))
34+
else:
35+
cmd = raw_input(COLOR_STR('Input Command # ', CYAN))
36+
PRINT('') # add one line
37+
return cmd
38+
#-----------------------------------------------------------------------
39+
40+
#--------------------------------------------------------------------------
41+
# Colored Output in Console
42+
#--------------------------------------------------------------------------
43+
DEBUG = False
44+
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA,CYAN,WHITE = list(range(90, 98))
45+
def COLOR_STR(msg, color=WHITE):
46+
return '\033[%dm%s\033[0m'%(color, msg)
47+
48+
def PRINT(msg, color=WHITE, out=sys.stdout):
49+
if DEBUG and out.isatty() :
50+
out.write(COLOR_STR(msg, color) + '\n')
51+
#--------------------------------------------------------------------------
52+
53+
def exec_cmd(myrtsp, cmd):
54+
'''Execute the operation according to the command'''
55+
if cmd in ('exit', 'teardown'):
56+
myrtsp.do_teardown()
57+
elif cmd == 'pause':
58+
myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=now-'
59+
myrtsp.do_pause()
60+
elif cmd == 'help':
61+
PRINT(play_ctrl_help())
62+
elif cmd == 'forward':
63+
if myrtsp.cur_scale < 0: myrtsp.cur_scale = 1
64+
myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-'
65+
elif cmd == 'backward':
66+
if myrtsp.cur_scale > 0: myrtsp.cur_scale = -1
67+
myrtsp.cur_scale *= 2; myrtsp.cur_range = 'npt=now-'
68+
elif cmd == 'begin':
69+
myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=beginning-'
70+
elif cmd == 'live':
71+
myrtsp.cur_scale = 1; myrtsp.cur_range = 'npt=end-'
72+
elif cmd.startswith('play'):
73+
m = re.search(r'range[:\s]+(?P<range>[^\s]+)', cmd)
74+
if m: myrtsp.cur_range = m.group('range')
75+
m = re.search(r'scale[:\s]+(?P<scale>[\d\.]+)', cmd)
76+
if m: myrtsp.cur_scale = int(m.group('scale'))
77+
78+
if cmd not in ('pause', 'exit', 'teardown', 'help'):
79+
myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale)
80+
81+
def main(url, options):
82+
myrtsp = RTSPClient(url, options.dest_ip, callback=PRINT)
83+
84+
if options.transport: myrtsp.TRANSPORT_TYPE_LIST = options.transport.split(',')
85+
if options.client_port: myrtsp.CLIENT_PORT_RANGE = options.client_port
86+
if options.nat: myrtsp.NAT_IP_PORT = options.nat
87+
if options.arq: myrtsp.ENABLE_ARQ = options.arq
88+
if options.fec: myrtsp.ENABLE_FEC = options.fec
89+
90+
if options.ping:
91+
PRINT('PING START', YELLOW)
92+
myrtsp.ping(0.1)
93+
PRINT('PING DONE', YELLOW)
94+
sys.exit(0)
95+
return
96+
97+
try:
98+
myrtsp.do_describe()
99+
while myrtsp.state != 'describe':
100+
time.sleep(0.1)
101+
myrtsp.do_setup(0)
102+
while myrtsp.state != 'setup':
103+
time.sleep(0.1)
104+
#Setup up RTP capture here
105+
f=open('test.h264','wb')
106+
rtpframes = RTPReceive([10014],callback=f.write)
107+
while not rtpframes.running:
108+
time.sleep(0.1)
109+
myrtsp.do_play(myrtsp.cur_range, myrtsp.cur_scale)
110+
while myrtsp.running:
111+
if myrtsp.state == 'play':
112+
cmd = input_cmd()
113+
exec_cmd(myrtsp, cmd)
114+
# 302 redirect to re-establish chain
115+
if myrtsp.location:
116+
myrtsp = RTSPClient(myrtsp.location)
117+
myrtsp.do_describe()
118+
time.sleep(0.5)
119+
except KeyboardInterrupt:
120+
f.close()
121+
myrtsp.do_teardown()
122+
print('\n^C received, Exit.')
123+
124+
def play_ctrl_help():
125+
help = COLOR_STR('In running, you can control play by input "forward"' \
126+
+', "backward", "begin", "live", "pause"\n', MAGENTA)
127+
help += COLOR_STR('or "play" with "range" and "scale" parameter, such ' \
128+
+'as "play range:npt=beginning- scale:2"\n', MAGENTA)
129+
help += COLOR_STR('You can input "exit", "teardown" or ctrl+c to ' \
130+
+'quit\n', MAGENTA)
131+
return help
132+
133+
if __name__ == '__main__':
134+
usage = COLOR_STR('%prog [options] url\n\n', GREEN) + play_ctrl_help()
135+
136+
parser = OptionParser(usage=usage)
137+
parser.add_option('-t', '--transport', dest='transport',
138+
default='rtp_over_udp',
139+
help='Set transport type when issuing SETUP: '
140+
+'ts_over_tcp, ts_over_udp, rtp_over_tcp, '
141+
+'rtp_over_udp[default]')
142+
parser.add_option('-d', '--dest_ip', dest='dest_ip',
143+
help='Set destination ip of udp data transmission, '
144+
+'default uses same ip as this rtsp client')
145+
parser.add_option('-p', '--client_port', dest='client_port',
146+
help='Set client port range when issuing SETUP of udp, '
147+
+'default is "10014-10015"')
148+
parser.add_option('-n', '--nat', dest='nat',
149+
help='Add "x-NAT" when issuing DESCRIBE, arg format '
150+
+'"192.168.1.100:20008"')
151+
parser.add_option('-r', '--arq', dest='arq', action="store_true",
152+
help='Add "x-Retrans:yes" when issuing DESCRIBE')
153+
parser.add_option('-f', '--fec', dest='fec', action="store_true",
154+
help='Add "x-zmssFecCDN:yes" when issuing DESCRIBE')
155+
parser.add_option('-P', '--ping', dest='ping', action="store_true",
156+
help='Just issue OPTIONS and exit.')
157+
158+
(options, args) = parser.parse_args()
159+
if len(args) < 1:
160+
parser.print_help()
161+
sys.exit()
162+
163+
url = args[0]
164+
165+
DEBUG = True
166+
main(url, options)
167+
# EOF #
168+

rtp.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
'''
2+
Inspired by post by Sampsa Riikonen here:
3+
https://stackoverflow.com/questions/28022432/receiving-rtp-packets-after-rtsp-setup
4+
5+
Written 2017 Mike Killian
6+
'''
7+
8+
import re, socket, threading
9+
import bitstring # if you don't have this from your linux distro, install with "pip install bitstring"
10+
11+
class RTPReceive(threading.Thread):
12+
'''
13+
This will open a socket on the client ports sent in RTSP setup request and
14+
return data as its received to the callback function.
15+
'''
16+
def __init__(self, client_ports, callback=None):
17+
threading.Thread.__init__(self)
18+
self._callback = callback or (lambda x: None)
19+
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
20+
self._sock.bind(("", client_ports[0])) # we open a port that is visible to the whole internet (the empty string "" takes care of that)
21+
self._sock.settimeout(5) # if the socket is dead for 5 s., its thrown into trash
22+
self.closed = False
23+
self.frame = b''
24+
self.frame_done = True #Did we get the last packet to fill a whole frame?
25+
self.running = False
26+
self.sprop-parameter-sets = 'Z0IAIJWoFAHmQA==,aM48gA=='
27+
self.start()
28+
29+
def run(self):
30+
self.running = True
31+
try:
32+
while self.running:
33+
#self.frame = msg = self.recv_msg()
34+
msg = self._sock.recv(2048)
35+
framelet = self.digestpacket(msg)
36+
if framelet:
37+
self.frame += framelet
38+
if self.frame and self.frame_done:
39+
self._callback(self.frame)
40+
self.frame = b''
41+
except Exception as e:
42+
raise Exception('Run time error: %s' % e)
43+
self.running = False
44+
self.close()
45+
46+
def close(self):
47+
self.closed = True
48+
self.running = False
49+
self._sock.close()
50+
51+
def insert_config_info(self, parameters):
52+
pass
53+
54+
# ********* (2) The routine for handling the RTP stream ***********
55+
56+
def digestpacket(self, st):
57+
""" This routine takes a UDP packet, i.e. a string of bytes and ..
58+
(a) strips off the RTP header
59+
(b) adds NAL "stamps" to the packets, so that they are recognized as NAL's
60+
(c) Concantenates frames
61+
(d) Returns a packet that can be written to disk as such and that is recognized by stock media players as h264 stream
62+
"""
63+
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.
64+
65+
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.
66+
lc = 12 # bytecounter
67+
bc = 12*8 # bitcounter
68+
69+
version = bt[0:2].uint # Version
70+
p = bt[3] # Padding
71+
x = bt[4] # Extension
72+
cc = bt[4:8].uint # CSRC Count
73+
m = bt[9] # Marker
74+
pt = bt[9:16].uint # Payload Type
75+
sn = bt[16:32].uint # Sequence number
76+
timestamp = bt[32:64].uint # Timestamp
77+
ssrc = bt[64:96].uint # ssrc identifier
78+
# The header format can be found from:
79+
# https://en.wikipedia.org/wiki/Real-time_Transport_Protocol
80+
81+
lc = 12 # so, we have red twelve bytes
82+
bc = 12*8 # .. and that many bits
83+
84+
if p:
85+
#TODO: Deal with padding here
86+
print("\n****\nPadding alert!!\n****\n")
87+
88+
print("*----* Packet Begin *----* (Len: {})".format(len(st)))
89+
print("Ver: {}, P: {}, X: {}, CC: {}, M: {}, PT: {}".format(version,p,x,cc,m,pt))
90+
print("Sequence number: {}, Timestamp: {}".format(sn,timestamp))
91+
print("Sync. Source Identifier: {}".format(ssrc))
92+
93+
# st=f.read(4*cc) # csrc identifiers, 32 bits (4 bytes) each
94+
cids = []
95+
for i in range(cc):
96+
cids.append(bt[bc:bc+32].uint)
97+
bc += 32
98+
lc += 4
99+
if cids: print("CSRC Identifiers: {}".format(cids))
100+
101+
if (x):
102+
# this section haven't been tested.. might fail
103+
hid = bt[bc:bc+16].uint
104+
bc += 16
105+
lc += 2
106+
107+
hlen = bt[bc:bc+16].uint
108+
bc += 16
109+
lc += 2
110+
111+
hst = bt[bc:bc+32*hlen]
112+
bc += 32*hlen
113+
lc += 4*hlen
114+
115+
print("*----* Extension Header *----*")
116+
print("Ext. Header id: {}, Header len: {}".format(hid,hlen))
117+
118+
# OK, now we enter the NAL packet, as described here:
119+
#
120+
# https://tools.ietf.org/html/rfc6184#section-1.3
121+
#
122+
# Some quotes from that document:
123+
#
124+
"""
125+
5.3. NAL Unit Header Usage
126+
127+
128+
The structure and semantics of the NAL unit header were introduced in
129+
Section 1.3. For convenience, the format of the NAL unit header is
130+
reprinted below:
131+
132+
+---------------+
133+
|0|1|2|3|4|5|6|7|
134+
+-+-+-+-+-+-+-+-+
135+
|F|NRI| Type |
136+
+---------------+
137+
138+
This section specifies the semantics of F and NRI according to this
139+
specification.
140+
141+
"""
142+
"""
143+
Table 3. Summary of allowed NAL unit types for each packetization
144+
mode (yes = allowed, no = disallowed, ig = ignore)
145+
146+
Payload Packet Single NAL Non-Interleaved Interleaved
147+
Type Type Unit Mode Mode Mode
148+
-------------------------------------------------------------
149+
0 reserved ig ig ig
150+
1-23 NAL unit yes yes no
151+
24 STAP-A no yes no
152+
25 STAP-B no no yes
153+
26 MTAP16 no no yes
154+
27 MTAP24 no no yes
155+
28 FU-A no yes yes
156+
29 FU-B no no yes
157+
30-31 reserved ig ig ig
158+
"""
159+
# This was also very usefull:
160+
# http://stackoverflow.com/questions/7665217/how-to-process-raw-udp-packets-so-that-they-can-be-decoded-by-a-decoder-filter-i
161+
# A quote from that:
162+
"""
163+
First byte: [ 3 NAL UNIT BITS | 5 FRAGMENT TYPE BITS]
164+
Second byte: [ START BIT | RESERVED BIT | END BIT | 5 NAL UNIT BITS]
165+
Other bytes: [... VIDEO FRAGMENT DATA...]
166+
"""
167+
168+
fb = bt[bc] # i.e. "F"
169+
nri = bt[bc+1:bc+3].uint # "NRI"
170+
nlu0 = bt[bc:bc+3] # "3 NAL UNIT BITS" (i.e. [F | NRI])
171+
typ = bt[bc+3:bc+8].uint # "Type"
172+
print(" *-* NAL Header *-*")
173+
print("F: {}, NRI: {}, Type: {}".format(fb, nri, typ))
174+
print("First three bits together : {}".format(bt[bc:bc+3]))
175+
176+
if (typ==7 or typ==8):
177+
# this means we have either an SPS or a PPS packet
178+
# they have the meta-info about resolution, etc.
179+
# more reading for example here:
180+
# http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/
181+
if (typ==7):
182+
print(">>>>> SPS packet")
183+
else:
184+
print(">>>>> PPS packet")
185+
return startbytes+st[lc:]
186+
# .. notice here that we include the NAL starting sequence "startbytes" and the "First byte"
187+
188+
bc += 8;
189+
lc += 1; # let's go to "Second byte"
190+
# ********* WE ARE AT THE "Second byte" ************
191+
# The "Type" here is most likely 28, i.e. "FU-A"
192+
start = bt[bc] # start bit
193+
end = bt[bc+2] # end bit
194+
nlu1 = bt[bc+3:bc+8] # 5 nal unit bits
195+
head = b""
196+
197+
if (self.frame_done and start): # OK, this is a first fragment in a movie frame
198+
print(">>> first fragment found")
199+
self.frame_done = False
200+
nlu = nlu0+nlu1 # Create "[3 NAL UNIT BITS | 5 NAL UNIT BITS]"
201+
print(" >>> NLU0: {}, NLU1: {}, NLU: {}".format(nlu0,nlu1,nlu))
202+
head = startbytes+nlu.bytes # .. add the NAL starting sequence
203+
lc += 1 # We skip the "Second byte"
204+
elif (self.frame_done==False and start==False and end==False): # intermediate fragment in a sequence, just dump "VIDEO FRAGMENT DATA"
205+
lc += 1 # We skip the "Second byte"
206+
elif (self.frame_done==False and end==True): # last fragment in a sequence, just dump "VIDEO FRAGMENT DATA"
207+
print("<<<< last fragment found")
208+
self.frame_done = True
209+
lc += 1 # We skip the "Second byte"
210+
211+
if (typ==28): # This code only handles "Type" = 28, i.e. "FU-A"
212+
return head+st[lc:]
213+
else:
214+
#raise Exception("unknown frame type for this piece of s***")
215+
return None

rtsp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ def do_setup(self, track_id_str=None, headers={}):
362362
headers['Authorization'] = self._auth
363363
headers['Transport'] = self._get_transport_type()
364364
#TODO: Currently issues SETUP for all tracks but doesn't keep track
365-
# of them or end all of them.
365+
# of all sessions or teardown all of them.
366366
if isinstance(track_id_str,str):
367367
self._sendmsg('SETUP', self._orig_url+'/'+track_id_str, headers)
368368
elif isinstance(track_id_str, int):

0 commit comments

Comments
 (0)