[Shtoom] patch: refactor packets
Zooko O'Whielacronx
zooko at zooko.com
Sun Jan 23 12:35:07 CET 2005
Anthony:
This patch (as discussed on IRC) refactors the RTP Packet object and
the parser in a way that I like.
It separates the PT (Payload Type) byte from the Content Type object.
It makes parsing a stateless function instead of a parser object. It
makes all values of an RTP packet passed as ctor args instead of poked
into place after construction. It handles up to one RTP extension
header. It puts the RTP header stuff into a separate object, which is
convenient if you have a function that needs to get the netbytes for
the header separately from the netbytes for the payload (which I have).
This patch also refactors the "send an RTP packet" functionality of RTP
Protocol. There is a nice unified "_send_packet()" and a nice
"_send_cn_packet()".
I've been using this patch on my proprietary branch since last October.
Shtoom passes unit tests after application of this patch. I made a
test call with Shtoom after application of this patch and it worked.
Further patches will be forthcoming which depend on and complement this
patch.
Please apply.
Regards,
Zooko
The patch is inlined *and* attached:
P.S. If you don't like nested classes, move Header outside of Packet,
or mention it and I'll do so.
Index: shtoom/test/test_playout.py
===================================================================
--- shtoom/test/test_playout.py (revision 1157)
+++ shtoom/test/test_playout.py (working copy)
@@ -68,8 +68,7 @@
ts = 0
seq = 10017
while True:
- p = RTPPacket(data='', pt=None, ts=ts)
- p.seq = seq
+ p = RTPPacket(0, seq, ts, '')
yield p
ts += 160
seq += 1
Index: shtoom/test/test_codecs.py
===================================================================
--- shtoom/test/test_codecs.py (revision 1157)
+++ shtoom/test/test_codecs.py (working copy)
@@ -56,10 +56,10 @@
ae = self.assertEquals
ae(p.encode('frobozulate'), 'frobozulate')
ae(p.decode('frobozulate'), 'frobozulate')
- p = RTPPacket(PT_RAW, 'farnarkling', ts=None)
+ p = RTPPacket(0, 0, 0, 'farnarkling', ct=PT_RAW)
ae(c.decode(p), 'farnarkling')
ae(c.encode('farnarkling').data, 'farnarkling')
- ae(c.encode('farnarkling').pt, PT_RAW)
+ ae(c.encode('farnarkling').header.pt, PT_RAW.pt)
# XXX testing other codecs - endianness issues? crap.
@@ -72,7 +72,7 @@
ae(c.getDefaultFormat(), PT_PCMU)
ae(len(c.encode(instr).data), 160)
ae(c.encode(instr).data, ulawout)
- ae(c.encode(instr).pt, PT_PCMU)
+ ae(c.encode(instr).header.ct, PT_PCMU)
def testGSMCodec(self):
if codecs.gsm is None:
Index: shtoom/test/test_rtp.py
===================================================================
--- shtoom/test/test_rtp.py (revision 1157)
+++ shtoom/test/test_rtp.py (working copy)
@@ -45,33 +45,29 @@
ae(pt, RTPDict[(pt.name.lower(),pt.clock,pt.params or 1)])
def testRTPPacketRoundTrip(self):
- from shtoom.rtp.packets import RTPParser, RTPPacket
+ from shtoom.rtp.packets import RTPPacket
+ from shtoom.rtp.protocol import parse_rtppacket
from shtoom.rtp.formats import PT_PCMU, PT_SPEEX, PT_GSM, PT_CN
ae = self.assertEquals
- ptdict = {}
- for pt, byte in ((PT_PCMU,
0),(PT_GSM,3),(PT_SPEEX,101),(PT_CN,13)):
- ptdict[pt] = byte
- ptdict[byte] = pt
- parser = RTPParser(ptdict)
ts = 12345678
seq = 12345
ssrc = 100001
packets = [
- RTPPacket(PT_PCMU, ''.join([chr(x) for x in range(160)]),
0),
- RTPPacket(PT_GSM, '\0'*33, 0, 1),
- RTPPacket(PT_GSM, '\0'*33, 0, 0),
- RTPPacket(PT_CN, chr(127), 0, 0),
- RTPPacket(PT_CN, chr(127), 0, 1),
+ RTPPacket(ssrc, seq, ts, ''.join([chr(x) for x in
range(160)]), 0, ct=PT_PCMU),
+ RTPPacket(ssrc, seq, ts, '\0'*33, 1, ct=PT_GSM),
+ RTPPacket(ssrc, seq, ts, '\0'*33, 0, ct=PT_GSM),
+ RTPPacket(ssrc, seq, ts, chr(127), 0, ct=PT_CN),
+ RTPPacket(ssrc, seq, ts, chr(127), 1, ct=PT_CN),
]
for pack in packets:
- rpack = parser.fromnet(parser.tonet(pack, seq, ts, ssrc),
None)
- ae(pack.pt, rpack.pt)
- ae(pack.marker, rpack.marker)
+ rpack = parse_rtppacket(pack.netbytes())
+ ae(pack.header.pt, rpack.header.pt)
+ ae(pack.header.marker, rpack.header.marker)
ae(pack.data, rpack.data)
- ae(rpack.seq, seq)
- ae(rpack.ts, ts)
- ae(rpack.ssrc, ssrc)
+ ae(rpack.header.seq, seq)
+ ae(rpack.header.ts, ts)
+ ae(rpack.header.ssrc, ssrc)
def testSDPGen(self):
from shtoom.rtp.formats import SDPGenerator, PTMarker
Index: shtoom/stun.py
===================================================================
--- shtoom/stun.py (revision 1157)
+++ shtoom/stun.py (working copy)
@@ -16,7 +16,7 @@
from shtoom.defcache import DeferredCache
from shtoom.nat import BaseMapper
-STUNVERBOSE = True
+STUNVERBOSE = False
# If we're going to follow RFC recommendation, make this 7
MAX_RETRANSMIT = 5
@@ -397,7 +397,10 @@
else:
if STUNVERBOSE:
print "giving up on %r"%(address,)
- del self._potentialStuns[tid]
+ if hasattr(self, '_potentialStuns') and
self._potentialStuns.has_key(tid):
+ del self._potentialStuns[tid]
+ else:
+ print "xxxxx what's going on here? getattr(self,
'_potentialStuns'): %s, val: %s" % (getattr(self, '_potentialStuns'),
getattr(self, '_potentialStuns', {}).get(tid),)
if not self._potentialStuns:
if STUNVERBOSE:
print "stun state 1 timeout - no internet UDP
possible"
Index: shtoom/app/phone.py
===================================================================
--- shtoom/app/phone.py (revision 1157)
+++ shtoom/app/phone.py (working copy)
@@ -35,7 +35,7 @@
self._currentCall = None
self._muted = False
self._rtpProtocolClass = None
- self._debugrev = 10
+ self._debugrev = 17
def needsThreadedUI(self):
return self.ui.threadedUI
Index: shtoom/audio/converters.py
===================================================================
--- shtoom/audio/converters.py (revision 1157)
+++ shtoom/audio/converters.py (working copy)
@@ -202,15 +202,15 @@
encaudio = codec.encode(bytes)
if not encaudio:
return None
- return RTPPacket(self.format, encaudio, ts=None)
+ return RTPPacket(0, 0, 0, encaudio, ct=self.format)
def decode(self, packet):
"Accepts an RTPPacket, emits audio as bytes"
if not packet.data:
return None
- codec = self.codecs.get(packet.pt)
+ codec = self.codecs.get(packet.header.ct)
if not codec:
- raise ValueError("can't decode format %r"%packet.pt)
+ raise ValueError("can't decode format %r"%packet.header.ct)
encaudio = codec.decode(packet.data)
return encaudio
Index: shtoom/audio/playout.py
===================================================================
--- shtoom/audio/playout.py (revision 1157)
+++ shtoom/audio/playout.py (working copy)
@@ -61,22 +61,22 @@
raise ValueError("playout got %s instead of
bytes"%(type(bytes)))
if self.expect_seq == None:
- self.expect_seq = packet.seq # First packet. Initialize seq
+ self.expect_seq = packet.header.seq # First packet.
Initialize seq
backlog = len(self.queue)
- if packet.seq == self.expect_seq:
- self.expect_seq = packet.seq+1
+ if packet.header.seq == self.expect_seq:
+ self.expect_seq = packet.header.seq+1
if backlog < self.backlog+1:
self.queue.append(bytes)
elif DEBUG:
log.msg("BacklogPlayout discarding")
else:
- offset = packet.seq-self.expect_seq
+ offset = packet.header.seq-self.expect_seq
if offset > 0 and offset < 3:
# Fill with empty packets
self.queue += [None]*offset
self.queue.append(bytes)
- self.expect_seq = packet.seq+1
+ self.expect_seq = packet.header.seq+1
if DEBUG:
log.msg("BacklogPlayout got hole at %d"%offset)
elif offset < 0 and offset > -backlog:
Index: shtoom/rtp/packets.py
===================================================================
--- shtoom/rtp/packets.py (revision 1157)
+++ shtoom/rtp/packets.py (working copy)
@@ -6,75 +6,80 @@
import struct
from time import sleep, time
+# This class supports extension headers, but only one per packet.
class RTPPacket:
- "An RTPPacket contains RTP data to/from the RTP object"
- def __init__(self, pt, data, ts, marker=0):
- self.pt = pt
- self.data = data
- self.ts = ts
- self.marker = marker
- self.ssrc = None
- self.seq = None
+ """ Contains RTP data. """
+ class Header:
+ def __init__(self, ssrc, pt, ct, seq, ts, marker=0,
xhdrtype=None, xhdrdata=''):
+ """
+ If xhdrtype is not None then it is required to be an int
>= 0 and < 2**16 and xhdrdata is required to be a string.
+ """
+ assert isinstance(ts, (int, long,)), "ts: %s :: %s" % (ts,
type(ts),)
+ assert isinstance(ssrc, (int, long,))
+ assert xhdrtype is None or isinstance(xhdrtype, int) and
xhdrtype >= 0 and xhdrtype < 2**16
+ assert xhdrtype is None or isinstance(xhdrdata, str)
- def __repr__(self):
- return "<RTPPacket containing %r at %x>"%(self.pt, id(self))
+ self.ssrc, self.pt, self.ct, self.seq, self.ts,
self.marker, self.xhdrtype, self.xhdrdata = ssrc, pt, ct, seq, ts,
marker, xhdrtype, xhdrdata
+ def netbytes(self):
+ "Return network-formatted header."
+ assert isinstance(self.pt, int) and self.pt >= 0 and
self.pt < 2**8, "pt is required to be a simple byte, suitable for
stuffing into an RTP packet and sending. pt: %s" % self.pt
+ if self.xhdrtype is not None:
+ firstbyte = 0x90
+ xhdrnetbytes = struct.pack('!HH', self.xhdrtype,
len(self.xhdrdata)) + self.xhdrdata
+ else:
+ firstbyte = 0x80
+ xhdrnetbytes = ''
+ return struct.pack('!BBHII', firstbyte, self.pt |
self.marker << 7, self.seq % 2**16, self.ts, self.ssrc) + xhdrnetbytes
-class RTPParser:
- """ An RTPParser creates RTPPacket objects from a bytestring. It is
- created with a mapping of RTP PT bytes to PT markers"""
- def __init__(self, ptdict):
- self.ptdict = ptdict
+ def __init__(self, ssrc, seq, ts, data, pt=None, ct=None,
marker=0, authtag='', xhdrtype=None, xhdrdata=''):
+ assert pt is None or isinstance(pt, int) and pt >= 0 and pt <
2**8, "pt is required to be a simple byte, suitable for stuffing into
an RTP packet and sending. pt: %s" % pt
+ self.header = RTPPacket.Header(ssrc, pt, ct, seq, ts, marker,
xhdrtype, xhdrdata)
+ self.data = data
+ self.authtag = authtag # please leave this alone even if it
appears unused -- it is required for SRTP
- def tonet(self, packet, seq, ts, ssrc):
- "Return network formatted packet"
- pt = packet.pt
- # XXX handle cc and x!
- byte0 = 0x80
- fmt = self.ptdict[pt] & 127
- if packet.marker: fmt = fmt | 128
- hdr = struct.pack('!BBHII', byte0, fmt, seq, ts, ssrc)
- # Padding?
- return hdr + packet.data
+ def __repr__(self):
+ if self.header.ct is not None:
+ ptrepr = "%r" % (self.header.ct,)
+ else:
+ ptrepr = "pt %d" % (self.header.pt,)
- # XXX Note that the MediaLayer will create it's own RTPPackets -
this is
- # purely for packets coming off the network.
- def fromnet(self, bytes, fromaddr):
- hdr = struct.unpack('!BBHII', bytes[:12])
- padding = hdr[0]&32 and 1 or 0
- cc = hdr[0]&15 and 1 or 0
- x = hdr[0]&16 and 1 or 0
- pt = hdr[1]&127
- if not self.ptdict.get(pt):
- # Could be any old garbage, such as a late STUN packet
- return None
- pt = self.ptdict[pt]
- marker = hdr[1]&128 and 1 or 0
- seq = hdr[2]
- ts = hdr[3]
- ssrc = hdr[4]
- headerlen = 12
- if cc > 1:
- headerlen = headerlen + 4 * cc
- data = bytes[headerlen:]
- if x:
- # Mmm. Tasty tasty header extensions. We eats them.
- xhdrtype,xhdrlen = struct.unpack('!HH', data[:4])
- data = data[4+4*xhdrlen:]
- if padding:
- padcount = ord(data[-1])
- if padcount:
- data = data[:-padcount]
- packet = RTPPacket(pt, data, ts, marker)
- packet.ssrc = ssrc
- packet.seq = seq
- return packet
+ if self.header.xhdrtype is not None:
+ return "<%s #%d (%s) %s [%s] at
%x>"%(self.__class__.__name__, self.header.seq, self.header.xhdrtype,
ptrepr, repr(self.header.xhdrdata), id(self))
+ else:
+ return "<%s #%d %s at %x>"%(self.__class__.__name__,
self.header.seq, ptrepr, id(self))
- def haspt(self, key):
- return self.ptdict.has_key(key)
+ def netbytes(self):
+ "Return network-formatted packet."
+ return self.header.netbytes() + self.data + self.authtag
+def parse_rtppacket(bytes):
+ hdr = struct.unpack('!BBHII', bytes[:12])
+ padding = hdr[0]&32 and 1 or 0
+ cc = hdr[0]&15 and 1 or 0
+ x = hdr[0]&16 and 1 or 0
+ pt = hdr[1]&127
+ marker = hdr[1]&128 and 1 or 0
+ seq = hdr[2]
+ ts = hdr[3]
+ ssrc = hdr[4]
+ headerlen = 12
+ if cc > 1:
+ headerlen = headerlen + 4 * cc
+ data = bytes[headerlen:]
+ if x:
+ # Mmm. Tasty tasty header extensions. We eats them. Except
for the first one.
+ xhdrtype,xhdrlen = struct.unpack('!HH', data[:4])
+ xhdrdata = data[4:4+4*xhdrlen]
+ data = data[4+4*xhdrlen:]
+ else:
+ xhdrtype, xhdrdata = None, None
+ if padding:
+ padcount = ord(data[-1])
+ if padcount:
+ data = data[:-padcount]
+ return RTPPacket(ssrc, seq, ts, data, marker=marker, pt=pt,
xhdrtype=xhdrtype, xhdrdata=xhdrdata)
-
class NTE:
"An object representing an RTP NTE (rfc2833)"
# XXX at some point, this should be hooked into the
RTPPacketFactory.
Index: shtoom/rtp/protocol.py
===================================================================
--- shtoom/rtp/protocol.py (revision 1157)
+++ shtoom/rtp/protocol.py (working copy)
@@ -12,7 +12,7 @@
from twisted.python import log
from shtoom.rtp.formats import SDPGenerator, PT_CN, PT_xCN
-from shtoom.rtp.packets import RTPPacket, RTPParser
+from shtoom.rtp.packets import RTPPacket, parse_rtppacket
TWO_TO_THE_16TH = 2<<16
TWO_TO_THE_32ND = 2<<32
@@ -46,14 +46,12 @@
def setSDP(self, sdp):
"This is the canonical SDP for the call"
- from shtoom.rtp.packets import RTPParser
self.app.selectDefaultFormat(self.cookie, sdp)
rtpmap = sdp.getMediaDescription('audio').rtpmap
- ptdict = {}
+ self.ptdict = {}
for pt, (text, marker) in rtpmap.items():
- ptdict[pt] = marker
- ptdict[marker] = pt
- self.rtpParser = RTPParser(ptdict)
+ self.ptdict[pt] = marker
+ self.ptdict[marker] = pt
def createRTPSocket(self, locIP, needSTUN=False):
""" Start listening on UDP ports for RTP and RTCP.
@@ -226,6 +224,26 @@
d.addCallback(lambda x: self.rtpListener.stopListening())
d.addCallback(lambda x: self.rtcpListener.stopListening())
+ def _send_packet(self, pt, data, xhdrtype=None, xhdrdata=''):
+ packet = RTPPacket(self.ssrc, self.seq, self.ts, data, pt=pt,
xhdrtype=xhdrtype, xhdrdata=xhdrdata)
+
+ self.seq += 1
+ self.transport.write(packet.netbytes(), self.dest)
+
+ def _send_cn_packet(self):
+ # PT 13 is CN.
+ if self.ptdict.has_key(PT_CN):
+ cnpt = PT_CN.pt
+ elif self.ptdict.has_key(PT_xCN):
+ cnpt = PT_xCN.pt
+ else:
+ # We need to send SOMETHING!?!
+ cnpt = 0
+
+ log.msg("sending CN(%s) to seed firewall to %s:%d"%(cnpt,
self.dest[0], self.dest[1]), system='rtp')
+
+ self._send_packet(cnpt, chr(127))
+
def startSendingAndReceiving(self, dest, fp=None):
self.dest = dest
self.prevInTime = self.prevOutTime = time()
@@ -248,32 +266,14 @@
self.LC.start(0.020)
# Now send a single CN packet to seed any firewalls that might
# need an outbound packet to let the inbound back.
- # PT 13 is CN.
- if self.rtpParser.haspt(PT_CN):
- cnpt = PT_CN.pt
- elif self.rtpParser.haspt(PT_xCN):
- cnpt = PT_xCN.pt
- else:
- cnpt = None
- if cnpt:
- log.msg("sending CN(%s) to seed firewall to %s:%d"%(cnpt,
- self.dest[0], self.dest[1]),
system='rtp')
- hdr = struct.pack('!BBHII', 0x80, cnpt, self.seq,
self.ts,self.ssrc)
- self.transport.write(hdr+chr(0), self.dest)
- else:
- log.msg("hack - seeding firewall with PT_PCMU")
- hdr = struct.pack('!BBHII', 0x80, 0, self.seq,
self.ts,self.ssrc)
- self.transport.write(hdr+(160*chr(0)), self.dest)
- # We need to send SOMETHING!?!
+ self._send_cn_packet()
if hasattr(self.transport, 'connect'):
self.transport.connect(*self.dest)
def datagramReceived(self, datagram, addr):
# XXX check for late STUN packets here. see, e.g
datagramReceived in sip.py
- if self.rtpParser is None:
- log.msg("early(?) rtp packet, no rtpParser available")
- return
- packet = self.rtpParser.fromnet(datagram, addr)
+ packet = parse_rtppacket(datagram)
+ packet.header.ct = self.ptdict[packet.header.pt]
if packet:
self.app.receiveRTP(self.cookie, packet)
@@ -340,26 +340,19 @@
# when we go from silent->talking, set the marker bit. Other
end
# can use this as an excuse to adjust playout buffer.
if self.sample is not None:
- packet = self.sample
+ pt = self.ptdict[self.sample.header.ct]
+ self._send_packet(pt, self.sample.data)
self.sent += 1
- data = self.rtpParser.tonet(packet, self.seq, self.ts,
self.ssrc)
- self.transport.write(data, self.dest)
self.sample = None
- else:
- if (self.packets - self.sent) % 100 == 0:
- if self.rtpParser.haspt(PT_CN):
- cn = RTPPacket(PT_CN, chr(127), 0)
- cn = self.rtpParser.tonet(cn, self.seq, self.ts,
self.ssrc)
- self.transport.write(cn, self.dest)
- self.seq += 1
+ elif (self.packets - self.sent) % 100 == 0:
+ self._send_cn_packet()
+
# Now send any pending DTMF keystrokes
if self._pendingDTMF:
payload = self._pendingDTMF[0].getPayload(self.ts)
if payload:
# XXX Hack. telephone-event isn't always 101!
- hdr = struct.pack('!BBHII', 0x80, 101, self.seq,
self.ts, self.ssrc)
- self.transport.write(hdr+payload, self.dest)
- self.seq += 1
+ self._send_packet(pt=101, data=payload)
if self._pendingDTMF[0].isDone():
self._pendingDTMF = self._pendingDTMF[1:]
try:
-------------- next part --------------
An embedded and charset-unspecified text was scrubbed...
Name: refactor-packets.patch.txt
URL: <http://mail.python.org/pipermail/shtoom/attachments/20050123/99c74a5f/attachment.txt>
More information about the Shtoom
mailing list