[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