-
-
Save matt2005/3d4fcccdf7560a294daad874eccf4ff9 to your computer and use it in GitHub Desktop.
Sample Python script to employ RTSP/RTP to play a stream from an IP-cam (from stackoverflow)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
http://stackoverflow.com/questions/28022432/receiving-rtp-packets-after-rtsp-setup | |
A demo python code that .. | |
1) Connects to an IP cam with RTSP | |
2) Draws RTP/NAL/H264 packets from the camera | |
3) Writes them to a file that can be read with any stock video player (say, mplayer, vlc & other ffmpeg based video-players) | |
Done for educative/demonstrative purposes, not for efficiency..! | |
written 2015 by Sampsa Riikonen. | |
""" | |
import socket | |
import re | |
import bitstring # if you don't have this from your linux distro, install with "pip install bitstring" | |
# ************************ FOR QUICK-TESTING EDIT THIS AREA ********************************************************* | |
ip="192.168.1.112" # IP address of your cam | |
#adr="rtsp://admin:[email protected]:554/onvif1" # username, passwd, etc. | |
adr="rtsp://192.168.1.112/onvif1" # username, passwd, etc. | |
clientports=[60784,60785] # the client ports we are going to use for receiving video | |
fname="stream.h264" # filename for dumping the stream | |
rn=5000 # receive this many packets | |
# After running this program, you can try your file defined in fname with "vlc fname" or "mplayer fname" from the command line | |
# you might also want to install h264bitstream to analyze your h264 file | |
# ******************************************************************************************************************* | |
dest=("DESCRIBE "+adr+" RTSP/1.0\r\nCSeq: 0\r\nUser-Agent: python\r\nAccept: application/sdp\r\n\r\n").encode() | |
setu=("SETUP "+adr+"/track1 RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: python\r\nTransport: RTP/AVP;unicast;client_port="+str(clientports[0])+"-"+str(clientports[1])+"\r\n\r\n").encode() | |
optio=("OPTIONS "+adr+" RTSP/1.0\r\nCSeq: 2\r\nUser-Agent: python\r\n\r\n").encode() | |
play=("PLAY "+adr+" RTSP/1.0\r\nCSeq: 3\r\nUser-Agent: python\r\nSession: SESID\r\nRange: npt=0.000-\r\n\r\n").encode() | |
# File organized as follows: | |
# 1) Strings manipulation routines | |
# 2) RTP stream handling routine | |
# 3) Main program | |
# *** (1) First, some string searching/manipulation for handling the rtsp strings *** | |
def getPorts(searchst,st): | |
""" Searching port numbers from rtsp strings using regular expressions | |
""" | |
pat=re.compile(searchst+"=\d*-\d*") | |
pat2=re.compile('\d+') | |
mstring=pat.findall(st)[0] # matched string .. "client_port=1000-1001" | |
nums=pat2.findall(mstring) | |
numas=[] | |
for num in nums: | |
numas.append(int(num)) | |
return numas | |
def getLength(st): | |
""" Searching "content-length" from rtsp strings using regular expressions | |
""" | |
pat=re.compile("Content-Length: \d*") | |
pat2=re.compile('\d+') | |
mstring=pat.findall(st)[0] # matched string.. "Content-Length: 614" | |
num=int(pat2.findall(mstring)[0]) | |
return num | |
def printrec(recst): | |
""" Pretty-printing rtsp strings | |
""" | |
recs=(recst).decode().split('\r\n') | |
for rec in recs: | |
print(rec) | |
def sessionid(recst): | |
""" Search session id from rtsp strings | |
""" | |
recs=recst.decode().split('\r\n') | |
for rec in recs: | |
ss=rec.split() | |
# print ">",ss | |
if (ss[0].strip()=="Session:"): | |
return str(ss[1].split(";")[0].strip()) | |
def setsesid(recst,idn): | |
""" Sets session id in an rtsp string | |
""" | |
return (recst.replace("SESID",str(idn))).encode() | |
# ********* (2) The routine for handling the RTP stream *********** | |
def digestpacket(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=("\x00\x00\x00\x01").encode()# this is the sequence of four bytes that identifies a NAL packet.. must be in front of every NAL packet. | |
st=(st.decode(errors='ignore')).encode() | |
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] # P | |
x=bt[4] # X | |
cc=bt[4:8].uint # CC | |
m=bt[9] # M | |
pt=bt[9:16].uint # PT | |
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 | |
print("version, p, x, cc, m, pt",version,p,x,cc,m,pt) | |
print("sequence number, timestamp",sn,timestamp) | |
print("sync. source identifier",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 | |
print("csrc identifiers:",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 | |
print("ext. header id, header len",hid,hlen) | |
hst=bt[bc:bc+32*hlen] | |
bc+=32*hlen | |
lc+=4*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("F, NRI, Type :", fb, nri, typ) | |
print("first three bits together :",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 | |
if (start): # OK, this is a first fragment in a movie frame | |
print(">>> first fragment found") | |
nlu=nlu0+nlu1 # Create "[3 NAL UNIT BITS | 5 NAL UNIT BITS]" | |
head=startbytes+nlu.bytes # .. add the NAL starting sequence | |
lc+=1 # We skip the "Second byte" | |
if (start==False and end==False): # intermediate fragment in a sequence, just dump "VIDEO FRAGMENT DATA" | |
head="" | |
lc+=1 # We skip the "Second byte" | |
elif (end==True): # last fragment in a sequence, just dump "VIDEO FRAGMENT DATA" | |
head="" | |
print("<<<< last fragment found") | |
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: | |
print("unknown frame type for this piece of s***") | |
#raise(Exception,"unknown frame type for this piece of s***") | |
# *********** (3) THE MAIN PROGRAM STARTS HERE **************** | |
# Create an TCP socket for RTSP communication | |
# further reading: | |
# https://docs.python.org/2.7/howto/sockets.html | |
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
s.connect((ip,554)) # RTSP should peek out from port 554 | |
print() | |
print("*** SENDING DESCRIBE ***") | |
print() | |
print(dest.decode()) | |
s.send(dest) | |
recst=s.recv(4096) | |
print() | |
print("*** GOT ****") | |
print() | |
printrec(recst) | |
print() | |
print("*** SENDING OPTIONS ***") | |
print() | |
print(optio.decode()) | |
s.send(optio) | |
recst=s.recv(4096) | |
print() | |
print("*** GOT ****") | |
print() | |
printrec(recst) | |
print() | |
print("*** SENDING SETUP ***") | |
print() | |
print(setu.decode()) | |
s.send(setu) | |
recst=s.recv(4096) | |
print() | |
print("*** GOT ****") | |
print() | |
printrec(recst) | |
idn=sessionid(recst) | |
serverports=getPorts("server_port",recst.decode()) | |
clientports=getPorts("client_port",recst.decode()) | |
print("****") | |
print("ip,serverports",ip,serverports) | |
print("****") | |
s1=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
s1.bind(("", clientports[0])) # we open a port that is visible to the whole internet (the empty string "" takes care of that) | |
s1.settimeout(5) # if the socket is dead for 5 s., its thrown into trash | |
# further reading: | |
# https://wiki.python.org/moin/UdpCommunication | |
# Now our port is open for receiving shitloads of videodata. Give the camera the PLAY command.. | |
print() | |
print("*** SENDING PLAY ***") | |
print() | |
play=setsesid(play.decode(),idn) | |
s.send(play) | |
recst=s.recv(4096) | |
print() | |
print("*** GOT ****") | |
print() | |
printrec(recst) | |
print() | |
print() | |
print("** STRIPPING RTP INFO AND DUMPING INTO FILE **") | |
f=open(fname,'w') | |
for i in range(rn): | |
print() | |
print() | |
recst=s1.recv(4096) | |
print("read",len(recst),"bytes") | |
st=digestpacket(recst) | |
print("dumping",len(st),"bytes") | |
f.write(st) | |
f.close() | |
# Before closing the sockets, we should give the "TEARDOWN" command via RTSP, but I am feeling lazy today (after googling, wireshark-analyzing, among other-things). | |
s.close() | |
s1.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment