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 ""%(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: