From fs at beebits.com Thu Sep 15 19:08:50 2005 From: fs at beebits.com (Frank Scholz) Date: Thu, 15 Sep 2005 19:08:50 +0200 Subject: [Shtoom] doug DTMF problems when connected to asterisk Message-ID: <4329AAA2.2080707@beebits.com> Hi, I'm trying to build some doug app connected to asterisk and I'm fighting with a few obstacles. The app registers with asterisk, I checked this with shmessage.py and it works fine. Now the next step was to check the DTMF functions, but there I'm out of luck. With asterisk dtmfmode rfc2833 - I get an exeption: can't decode format with dtmfmode=info - we have Content-Type: application/dtmf-relay Content-Length: 24 Signal=1 Duration=250 but nothing from doug. same with dtmfmode=inband Any advice? And btw. has anybody shtoomcu.py running as an asterisk extension? I can call in but don't get any client audio back. Regards, Frank From solipsis at pitrou.net Fri Sep 16 13:03:22 2005 From: solipsis at pitrou.net (Antoine Pitrou) Date: Fri, 16 Sep 2005 13:03:22 +0200 Subject: [Shtoom] STUN retransmit delayed calls not cleaned up Message-ID: <1126868602.27418.10.camel@p-dvsi-418-1.rd.francetelecom.fr> Hi, (is shtoom still active? "svn up" has not given changes for a long time) I've found a potential problem with the STUN implementation. The delayed calls which handle message restranmission are not cleaned up at the end. This means if you contact several servers at once, and one of them does not answer readily (or the DNS resolution takes some time), the STUN protocol handler will still try to contact it even after it has finished with another server. It seems to cause problems with Twisted 2.0 in that the "self.transport" in a DatagramProtocol is reset to None when we stopListening(). It leads to errors like "NoneType object has no attribute 'write'" when the _StunBase tries to send a request. Here is the correction I've done to stun.py (modifications inside ... ): class _StunBase(object): def sendRequest(self, server, tid=None, avpairs=()): # if not self.transport: print "No transport defined, cannot send STUN request" return # if tid is None: tid = getRandomTID() mt = 0x1 # binding request avstr = '' # add any attributes if not avpairs: avpairs = ('CHANGE-REQUEST', CHANGE_NONE), for a,v in avpairs: avstr = avstr + struct.pack('!hh', StunTypes[a], len(v)) + v pktlen = len(avstr) if pktlen > 65535: raise ValueError, "stun request too big (%d bytes)"%pktlen pkt = struct.pack('!hh16s', mt, pktlen, tid) + avstr if STUNVERBOSE: print "sending request %r with %d avpairs to %r (in state %s)"%( hexify(tid), len(avpairs), server, self._stunState) self.transport.write(pkt, server) class StunDiscoveryProtocol(DatagramProtocol, _StunBase): stunDiscoveryRetries = 0 def __init__(self, servers=DefaultServers, *args, **kwargs): # Potential STUN servers self._potentialStuns = {} # See flowchart ascii art at bottom of file. self._stunState = '1' self._finished = False self._altStunAddress = None self.externalAddress = None self.localAddress = None self.expectedTID = None self.oldTIDs = sets.Set() self.natType = None # # self.servers = [(socket.gethostbyname(host), port) # for host, port in servers] self.servers = [(host, port) for host, port in servers] # super(StunDiscoveryProtocol, self).__init__(*args, **kwargs) def initialStunRequest(self, address): # if self._finished: return # tid = getRandomTID() delayed = reactor.callLater(INITIAL_TIMEOUT, self.retransmitInitial, address, tid) self._potentialStuns[tid] = delayed self.oldTIDs.add(tid) self.sendRequest(address, tid=tid) def retransmitInitial(self, address, tid, count=1): # if self._finished: return # if count <= MAX_RETRANSMIT: t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF) delayed = reactor.callLater(t, self.retransmitInitial, address, tid, count+1) self._potentialStuns[tid] = delayed self.sendRequest(address, tid=tid) else: if STUNVERBOSE: print "giving up on %r"%(address,) del self._potentialStuns[tid] if not self._potentialStuns: if STUNVERBOSE: print "stun state 1 timeout - no internet UDP possible" self.natType = NatTypeUDPBlocked self._finishedStun() def datagramReceived(self, dgram, address): if self._finished: return mt, pktlen, tid = struct.unpack('!hh16s', dgram[:20]) # Check tid is one we sent and haven't had a reply to yet if tid in self._potentialStuns: delayed = self._potentialStuns.get(tid) if delayed is not None: delayed.cancel() del self._potentialStuns[tid] if self._stunState == '1': # We got a (potentially) working STUN server! # Cancel the retransmit timers for the other ones for k in self._potentialStuns.keys(): self._potentialStuns[k].cancel() self._potentialStuns[k] = None resdict = _parseStunResponse(dgram, address, self.expectedTID, self.oldTIDs) if not resdict: return self.handleStunState1(resdict, address) else: # We already have a working STUN server to play with. pass return resdict = _parseStunResponse(dgram, address, self.expectedTID, self.oldTIDs) if not resdict: return if STUNVERBOSE: print 'calling handleStunState%s'%(self._stunState) getattr(self, 'handleStunState%s'%(self._stunState))(resdict, address) def handleStunState1(self, resdict, address): self.__dict__.update(resdict) if self.externalAddress and self._altStunAddress: if self.localAddress == self.externalAddress[0]: self._stunState = '2a' else: self._stunState = '2b' self.expectedTID = tid = getRandomTID() self.oldTIDs.add(tid) self.state2DelayedCall = reactor.callLater(INITIAL_TIMEOUT, self.retransmitStunState2, address, tid) self.sendRequest(address, tid, avpairs=( ('CHANGE-REQUEST', CHANGE_BOTH),)) def handleStunState2a(self, resdict, address): self.state2DelayedCall.cancel() del self.state2DelayedCall if STUNVERBOSE: print "2a", resdict self.natType = NatTypeNone self._finishedStun() def handleStunState2b(self, resdict, address): self.state2DelayedCall.cancel() del self.state2DelayedCall if STUNVERBOSE: print "2b", resdict self.natType = NatTypeFullCone self._finishedStun() def retransmitStunState2(self, address, tid, count=1): # if self._finished: return # if count <= MAX_RETRANSMIT: t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF) self.state2DelayedCall = reactor.callLater(t, self.retransmitStunState2, address, tid, count+1) self.sendRequest(address, tid, avpairs=( ('CHANGE-REQUEST', CHANGE_BOTH),)) elif self._stunState == '2a': self.natType = NatTypeSymUDP self._finishedStun() else: # 2b # Off to state 3 we go! self._stunState = '3' self.state3DelayedCall = reactor.callLater(INITIAL_TIMEOUT, self.retransmitStunState3, address, tid) self.expectedTID = tid = getRandomTID() self.oldTIDs.add(tid) self.sendRequest(self._altStunAddress, tid) def handleStunState3(self, resdict, address): self.state3DelayedCall.cancel() del self.state3DelayedCall if STUNVERBOSE: print "3", resdict if self.externalAddress == resdict['externalAddress']: # State 4! wheee! self._stunState = '4' self.expectedTID = tid = getRandomTID() self.oldTIDs.add(tid) self.state4DelayedCall = reactor.callLater(INITIAL_TIMEOUT, self.retransmitStunState4, address, tid) self.expectedTID = tid = getRandomTID() self.oldTIDs.add(tid) self.sendRequest(address, tid, avpairs=( ('CHANGE-REQUEST', CHANGE_PORT),)) else: self.natType = NatTypeSymmetric self._finishedStun() def retransmitStunState3(self, address, tid, count=1): # if self._finished: return # if count <= (2 * MAX_RETRANSMIT): t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF) self.state3DelayedCall = reactor.callLater(t, self.retransmitStunState3, address, tid, count+1) self.sendRequest(self._altStunAddress, tid) else: log.err("STUN Failed in state 3, retrying") # We should do _something_ here. a new type BrokenNAT? self.stunDiscoveryRetries = self.stunDiscoveryRetries + 1 if self.stunDiscoveryRetries < 5: reactor.callLater(0.2, self.startDiscovery) def handleStunState4(self, resdict, address): self.state4DelayedCall.cancel() del self.state4DelayedCall self.natType = NatTypeRestrictedCone self._finishedStun() def retransmitStunState4(self, address, tid, count = 1): # if self._finished: return # if count < MAX_RETRANSMIT: t = BACKOFF_TIME * 2**min(count, MAX_BACKOFF) self.state4DelayedCall = reactor.callLater(t, self.retransmitStunState4, address, tid, count+1) self.sendRequest(address, tid, avpairs=( ('CHANGE-REQUEST', CHANGE_PORT),)) else: self.natType = NatTypePortRestricted self._finishedStun() def _finishedStun(self): self._finished = True self.finishedStun() def finishedStun(self): # Override in a subclass if STUNVERBOSE: print "firewall type is", self.natType def startDiscovery(self): from shtoom.nat import isBogusAddress, getLocalIPAddress if _ForceStunType is not None: self.natType = _ForceStunType reactor.callLater(0, self._finishedStun) return localAddress = self.transport.getHost().host if isBogusAddress(localAddress): d = getLocalIPAddress() d.addCallback(self._resolveStunServers) else: self._resolveStunServers(localAddress) def _resolveStunServers(self, localAddress): self.localAddress = localAddress # reactor.resolve the hosts! for host, port in self.servers: d = reactor.resolve(host) d.addCallback(lambda x,p=port: self.initialStunRequest((x, p))) From fs at beebits.com Fri Sep 16 13:44:55 2005 From: fs at beebits.com (Frank Scholz) Date: Fri, 16 Sep 2005 13:44:55 +0200 Subject: [Shtoom] STUN retransmit delayed calls not cleaned up In-Reply-To: <1126868602.27418.10.camel@p-dvsi-418-1.rd.francetelecom.fr> References: <1126868602.27418.10.camel@p-dvsi-418-1.rd.francetelecom.fr> Message-ID: <432AB037.3000506@beebits.com> Hi Antoine, >> Here is the correction I've done to stun.py (modifications inside > > ... ): just in case, with "svn diff > patch-file" there is an much easier way to extract your changes. http://svnbook.red-bean.com/en/1.1/ch03s05.html#svn-ch-3-sect-5.3.2 I don't hope shtoom is dead, as I just decided to build one of my apps around it. ;-) Btw. does anybody know more about zfone? http://www.philzimmermann.com/EN/zfone/index.html Regards, Frank From fs at beebits.com Sun Sep 18 12:15:08 2005 From: fs at beebits.com (Frank Scholz) Date: Sun, 18 Sep 2005 12:15:08 +0200 Subject: [Shtoom] [patch] - solves issue with ignored and overwritten config-file options Message-ID: <432D3E2C.4000103@beebits.com> Hi, this patch solves among probably other things the issue with the always STUN and UPNP discovery despite them being disabled in the config-file. Change line 105 in shtoom/Options.py from if val is not NoDefaultOption and val is not None: to if o.value is NoDefaultOption and val is not NoDefaultOption and val is not None: Regards, Frank -------------- next part -------------- An embedded and charset-unspecified text was scrubbed... Name: options.patch URL: From fs at beebits.com Sun Sep 18 18:42:27 2005 From: fs at beebits.com (Frank Scholz) Date: Sun, 18 Sep 2005 18:42:27 +0200 Subject: [Shtoom] [patch] doug DTMF problems when connected to asterisk In-Reply-To: <4329AAA2.2080707@beebits.com> References: <4329AAA2.2080707@beebits.com> Message-ID: <432D98F3.5080500@beebits.com> Hi, > I'm trying to build some doug app connected to asterisk and > I'm fighting with a few obstacles.[...] [...] > With asterisk dtmfmode rfc2833 > - I get an exeption: can't decode format After some modifications it seems to move in the desired direction. In shtoom/app/doug.py - incomingRTP I changed - if packet.header.pt is PT_NTE: + if packet.header.ct is PT_NTE: and I added to shtoom/doug/voiceapp.py quite some things from shtoom/doug/leg.py regarding the DTMF processing. Now the ConferencingApp's announceFile aborts if a DTMF event occurs. My changes are more based on guesses than on knowledge ;-) especially I'm uncertain if they follow Anthonys intentions, therefore any advice would be appreciated! Regards, Frank -------------- next part -------------- An embedded and charset-unspecified text was scrubbed... Name: dtmf.patch URL: From anthony at interlink.com.au Tue Sep 20 08:27:13 2005 From: anthony at interlink.com.au (Anthony Baxter) Date: Tue, 20 Sep 2005 16:27:13 +1000 Subject: [Shtoom] [patch] - solves issue with ignored and overwritten config-file options In-Reply-To: <432D3E2C.4000103@beebits.com> References: <432D3E2C.4000103@beebits.com> Message-ID: <200509201627.16385.anthony@interlink.com.au> On Sunday 18 September 2005 20:15, Frank Scholz wrote: > Hi, > > this patch solves among probably other things the issue with the > always STUN and UPNP discovery despite them being disabled in the > config-file. That didn't fix it for me, but I dug in a little and found the problem. It's been fixed, now. Anthony -- Anthony Baxter It's never too late to have a happy childhood. From tdc at phreaker.net Sat Sep 24 19:50:26 2005 From: tdc at phreaker.net (tdc) Date: Sat, 24 Sep 2005 19:50:26 +0200 Subject: [Shtoom] shtoom on win32 Message-ID: <433591E2.3080906@phreaker.net> Hi, I've tested several versions of shtoom on win32, but none of it works. Not only svn checkouts, but binary releases too (0.2 from sourceforge). When I use binary version, it starts up without problems and when i try to place a call from different machine to shtoom/win, shtoom eats up 40% CPU time and does nothing. When in the same time on shtoom/win is some audio source attached, shtoom takes 100% CPU time and hangs. When killed, other side still thinks it's connected, which is bit weird (it doesn't keep connection status?). SVN revisions usually don't work at all, as there are several bugs in fast.py, rtp/protocol.py and maybe others (some of them are trivial, but not all of them). OS used is generic Windows2000 Pro + SB Live! 5.1 soundcard with recent drivers and servicepacks. Is there anybody with working shtoom/win? Doesn't matter if binary or svn revision. I'd like to know if it's only some local misconfiguration. Thanks in advance Dave From zooko at zooko.com Wed Sep 28 04:37:05 2005 From: zooko at zooko.com (zooko at zooko.com) Date: Tue, 27 Sep 2005 23:37:05 -0300 Subject: [Shtoom] big patch for dev to try and others to look at Message-ID: <20050928023705.9E1A0EC5@yumyum.zooko.com> I'm sorry to send such a big patch, but I lack the time to make it into several smaller patches. This isn't the final version -- it is just a draft for dev and me to work on and anyone else who wants to. For one thing, this patch applies to SVN r1406! For another, it's not well tested. Also it's incomplete, e.g. I broke all the audio devices and then fixed only a couple of them. I have never used or completely understood doug, and I haven't tested it, therefore I'm sure that this patch breaks doug even though I tried to make what appeared to be the appropriate changes to doug as I went along. So what's good about this patch? Many things are much simpler! Cleanliness and elegance are starting to shine through the cruft. The audio stack is completely rewritten, including PT/CT mapping and so on. SIP/SDP is somewhat tidied up, a few bugs fixed, but not completely rewritten. Initialization and configuration is simplified in a few ways. Much unreachable code and unused (as far as I could tell) features are removed. More details, in the order that I notice them while browsing through the patch: * There are two audio devices: one for reading from the microphone and one for writing to the speakers. The one for reading from the microphone makes calls to you to give you microphone data -- you do not make calls to it to ask for microphone data. * Initialization is done, whenever possible, by passing objects to the __init__ of larger objects, such as "Phone(audioplaydev, audiomicdev)" instead of having Phone itself dynamically create an audio device by a combination of configuration and detection. * All call cookies are gone -- A Phone instance is capable of handling only one call at a time. In fact, in the context of shtoom.app.Phone call cookies were never actually working, and when removing them I discovered that they were causing a bug, which is (of course) now fixed. * New scheme for muting playout of voice during playout of a wav file. This fixes various bugs, and it is nice and clean and encapsulated. * SDP negotiation code is simpler. * Mapping from PT byte to decoder is done by a dict from, well, from PT byte to decoder object. That dict is held by audio.converters.PlayDev. * Choosing an outgoing encodec is done by passing a reference to an encoder object when you open the audio microphone read device. * I cleaned up SIP state machine a bit, and I think I fixed a bug or two. I don't recall exactly at the time of this writing. * I did update the unit tests and make them pass for everything except the stuff that I knew I was breaking. Warning: there was a lot of stuff that I wasn't sure if it was actually used or needed, and I decided to delete it and then put it back after it was missed. You may miss it before I do. Sorry about that! Hope this helps! I intend to keep hacking on it in my (ahem) spare time. Regards, Zooko MIME attachment, application/octet-stream, unified diff, gzipped for armor against nasty meddling mail programs. Also appended in-line for your viewing pleasure. Also you can get it via darcs or http from: http://zooko.com:www/repos/shtoom/1406-plus-zooko-and-dev-patches -------------- next part -------------- A non-text attachment was scrubbed... Name: not available Type: application/octet-stream Size: 22390 bytes Desc: big.patch.gz URL: -------------- next part -------------- Tue Sep 27 23:34:41 ADT 2005 zooko at zooko.com * the big patch, first draft To: shtoom at python.org From: zooko at zooko.com Subject: big patch for dev to try and others to look at Date: 2005-09-27 -------- I'm sorry to send such a big patch, but I lack the time to make it into several smaller patches. This isn't the final version -- it is just a draft for dev and me to work on and anyone else who wants to. For one thing, this patch applies to SVN r1406! For another, it's not well tested. Also it's incomplete, e.g. I broke all the audio devices and then fixed only a couple of them. I have never used or completely understood doug, and I haven't tested it, therefore I'm sure that this patch breaks doug even though I tried to make what appeared to be the appropriate changes to doug as I went along. So what's good about this patch? Many things are much simpler! Cleanliness and elegance are starting to shine through the cruft. The audio stack is completely rewritten, including PT/CT mapping and so on. SIP/SDP is somewhat tidied up, a few bugs fixed, but not completely rewritten. Initialization and configuration is simplified in a few ways. Much unreachable code and unused (as far as I could tell) features are removed. More details, in the order that I notice them while browsing through the patch: * There are two audio devices: one for reading from the microphone and one for writing to the speakers. The one for reading from the microphone makes calls to you to give you microphone data -- you do not make calls to it to ask for microphone data. * Initialization is done, whenever possible, by passing objects to the __init__ of larger objects, such as "Phone(audioplaydev, audiomicdev)" instead of having Phone itself dynamically create an audio device by a combination of configuration and detection. * All call cookies are gone -- A Phone instance is capable of handling only one call at a time. In fact, in the context of shtoom.app.Phone call cookies were never actually working, and when removing them I discovered that they were causing a bug, which is (of course) now fixed. * New scheme for muting playout of voice during playout of a wav file. This fixes various bugs, and it is nice and clean and encapsulated. * SDP negotiation code is simpler. * Mapping from PT byte to decoder is done by a dict from, well, from PT byte to decoder object. That dict is held by audio.converters.PlayDev. * Choosing an outgoing encodec is done by passing a reference to an encoder object when you open the audio microphone read device. * I cleaned up SIP state machine a bit, and I think I fixed a bug or two. I don't recall exactly at the time of this writing. * I did update the unit tests and make them pass for everything except the stuff that I knew I was breaking. Warning: there was a lot of stuff that I wasn't sure if it was actually used or needed, and I decided to delete it and then put it back after it was missed. You may miss it before I do. Sorry about that! Hope this helps! I intend to keep hacking on it in my (ahem) spare time. Regards, Zooko diff -rN -u old-newfrom1406/trunk/shtoom/scripts/shreadder.py new-newfrom1406/trunk/shtoom/scripts/shreadder.py --- old-newfrom1406/trunk/shtoom/scripts/shreadder.py 2005-09-27 23:36:20.433369368 -0300 +++ new-newfrom1406/trunk/shtoom/scripts/shreadder.py 2005-09-27 23:36:20.495359944 -0300 @@ -4,7 +4,6 @@ import math, random, struct, sys sys.path.append(sys.path.pop(0)) import shtoom.audio -from shtoom.rtp import formats from shtoom.rtp.packets import RTPPacket @@ -54,25 +53,15 @@ def main(Recorder = Recorder): - from shtoom.audio import getAudioDevice - from shtoom.rtp import formats + from shtoom.audio import getAudioPlayDevice, getAudioMicDevice from twisted.internet.task import LoopingCall from twisted.internet import reactor - from twisted.python import log import sys - log.startLogging(sys.stdout) - dev = getAudioDevice() + playdev = getAudioPlayDevice() + micdev = getAudioMicDevice() dev.close() - if len(sys.argv) > 1: - fmt = sys.argv[1] - if not hasattr(formats, fmt): - print "unknown PT marker %s"%(fmt) - sys.exit(1) - dev.selectDefaultFormat([getattr(formats,fmt),]) - else: - dev.selectDefaultFormat([formats.PT_RAW,]) - rec = Recorder(dev, play=True) + rec = Recorder(playdev, micdev, play=True) dev.reopen(mediahandler=rec) reactor.run() diff -rN -u old-newfrom1406/trunk/shtoom/scripts/shtoomphone.py new-newfrom1406/trunk/shtoom/scripts/shtoomphone.py --- old-newfrom1406/trunk/shtoom/scripts/shtoomphone.py 2005-09-27 23:36:20.429369976 -0300 +++ new-newfrom1406/trunk/shtoom/scripts/shtoomphone.py 2005-09-27 23:36:20.495359944 -0300 @@ -11,6 +11,13 @@ else: sys.path.append(f) +from twisted.python import log + +from shtoom.audio import getAudioPlayDevice, getAudioMicDevice +from shtoom.ui.textui import ShtoomMain + +from pyutil.timeutil import isoformat + app = None def main(): @@ -20,24 +27,12 @@ from shtoom.app.phone import Phone global app - app = Phone() - app.boot(args=sys.argv[1:]) + tui = ShtoomMain() - audioPref = app.getPref('audio') - audio_in = app.getPref('audio_infile') - audio_out = app.getPref('audio_outfile') - if audio_in and audio_out: - aF = ( audio_in, audio_out ) - else: - aF = None - - from twisted.python import log - log.msg("Getting new audio device", system='phone') - - from shtoom.audio import getAudioDevice - # XXX Aarrgh. - app._audio = getAudioDevice() - log.msg("Got new audio device %s :: %s" % (app._audio, type(app._audio),)) + log.msg("timestamp: %s about to construct Phone" % (isoformat(),), system='phone') + app = Phone(getAudioPlayDevice(), getAudioMicDevice(), tui) + log.msg("timestamp: %s finished constructing Phone" % (isoformat(),), system='phone') + app.boot(args=sys.argv[1:]) def run_it(): app.start() diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/app/phone.py new-newfrom1406/trunk/shtoom/shtoom/app/phone.py --- old-newfrom1406/trunk/shtoom/shtoom/app/phone.py 2005-09-27 23:36:20.427370280 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/app/phone.py 2005-09-27 23:36:20.518356448 -0300 @@ -2,11 +2,15 @@ # The Phone app. -import os, sys, threading +import os, platform, sys, threading, time from twisted.internet import defer, protocol from twisted.python import log, threadable +from shtoom.credcache import CredCache +from shtoom.rtp.protocol import RTPProtocol +from shtoom.audio.converters import PlayDev, MicDev +from shtoom.audio import aufile from shtoom.app.interfaces import Application from shtoom.app.base import BaseApplication from shtoom.exceptions import CallFailed @@ -17,23 +21,37 @@ from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_DVI4 +from pyutil.assertutil import _assert, precondition, postcondition + class Phone(BaseApplication): __implements__ = ( Application, ) _startReactor = True - def __init__(self, ui=None, audio=None): - # Mapping from callcookies to rtp object - self._rtp = {} - # Mapping from callcookies to call objects - self._calls = {} - self._pendingRTP = {} - self._audio = audio + def __init__(self, audioplaydev, audiomicdev, ui=None): + precondition(audioplaydev and hasattr(audioplaydev, 'open') and hasattr(audioplaydev, 'write'), audioplaydev) + precondition(audiomicdev and hasattr(audiomicdev, 'open'), audiomicdev) + + self.rtpmap = None + self.encformat = None + self._audioplaydev = PlayDev(audioplaydev) + self._audiomicdev = MicDev(audiomicdev) + self._audiomicdev.open() # This is to read microphone samples and dump them into /dev/random. + + self._rtp = None + self.call = None self.ui = ui - self._currentCall = None - self._muted = False + self._muteplayout = False self._rtpProtocolClass = None - self._debugrev = 10 + self._develrevision = "dev-and-zooko-hack-branch-from-1406" + self.wavfileplayer = None + log.msg("Shtoom devel revision %s, platform: %s" % (self._develrevision, platform.platform(),)) + self.statusMessage('Ready.') + + def notifyEvent(self, methodName, *args, **kw): + method = getattr(self, methodName, None) + if method is not None: + method(*args, **kw) def find_resource_file(self, fname): """ Return the fully-qualified path to the desired resource file that came bundled with Shtoom. On failure, it returns fname. @@ -83,7 +101,8 @@ def register(self): register_uri = self.getPref('register_uri') if register_uri is not None: - return self.sip.register() + self.sip.register() + self.statusMessage('Registering...') def start(self): "Start the application." @@ -91,7 +110,7 @@ if not self._startReactor: log.msg("Not starting reactor - test mode?") return - if self.needsThreadedUI(): + if self.ui.threadedUI: threadable.init(1) from twisted.internet import reactor t = threading.Thread(target=reactor.run, kwargs={ @@ -107,132 +126,95 @@ self.play_wav_file('ring_back_file') def acceptCall(self, call): - log.msg("dialog is %r"%(call.dialog)) - cookie = self.getCookie() - self._calls[cookie] = call + log.msg("acceptCall %s" % (call,)) + self.call = call calltype = call.dialog.getDirection() if calltype == 'inbound': self.statusMessage('Incoming call') self.play_wav_file('incoming_ring_file') - uidef = self.ui.incomingCall(call.dialog.getCaller(), cookie) + uidef = self.ui.incomingCall(call.dialog.getCaller()) uidef.addCallback(lambda x: self._createRTP(x, call.getLocalSIPAddress()[0], call.getSTUNState())) return uidef elif calltype == 'outbound': - return self._createRTP(cookie, call.getLocalSIPAddress()[0], - call.getSTUNState()) + return self._createRTP(call.getLocalSIPAddress()[0], + call.getSTUNState()) else: raise ValueError, "unknown call type %s"%(calltype) return d - def _createRTP(self, cookie, localIP, withSTUN): - from shtoom.rtp.protocol import RTPProtocol - from shtoom.exceptions import CallFailed - if isinstance(cookie, CallFailed): - del self._calls[cookie.cookie] - return defer.succeed(cookie) - - self.ui.callStarted(cookie) - if self._rtpProtocolClass is None: - rtp = RTPProtocol(self, cookie) - else: - rtp = self._rtpProtocolClass(self, cookie) - self._rtp[cookie] = rtp - d = rtp.createRTPSocket(localIP,withSTUN) + def _createRTP(self, localIP, withSTUN): + self.ui.callStarted() + + self._rtp = RTPProtocol(self) + + rtpext.RTPExt(self._rtp) + + d = self._rtp.createRTPSocket(localIP,withSTUN) return d - def selectDefaultFormat(self, callcookie, sdp, format=None): - oldmediahandler = (self._audio and self._audio.codecker - and self._audio.codecker.handler) - self._audio.close() - if not sdp: - self._audio.selectDefaultFormat([format,]) - return + def getSDP(self): + sdp = SDP() + + self._rtp.set_sdp_addr(sdp) + md = sdp.getMediaDescription('audio') - rtpmap = md.rtpmap - ptlist = [ x[1] for x in rtpmap.values() ] - self._audio.selectDefaultFormat(ptlist) - if oldmediahandler: - self._audio.reopen(oldmediahandler) - - def getSDP(self, callcookie, othersdp=None): - rtp = self._rtp[callcookie] - sdp = rtp.getSDP(othersdp) + for pt in KnownCodecs: + md.addRtpMap(pt) + return sdp - def startCall(self, callcookie, remoteSDP, cb): - log.msg("startCall reopening %r %r"%(self._currentCall, self._audio)) + def startCall(self, remoteSDP): md = remoteSDP.getMediaDescription('audio') + precondition(md.rtpmap is not None) ipaddr = md.ipaddr or remoteSDP.ipaddr remoteAddr = (ipaddr, md.port) - log.msg("call Start %r %r"%(callcookie, remoteAddr)) - self._currentCall = callcookie - self._rtp[callcookie].start(remoteAddr) - mediahandler = lambda x,c=callcookie: self.outgoingRTP(c, x) - self.openAudioDevice([PT_PCMU,], mediahandler) - log.msg("startCall opened %r %r"%(self._currentCall, self._audio)) - cb(callcookie) - - def outgoingRTP(self, cookie, sample): - # XXX should the mute/nonmute be in the audio layer? - if not self._muted: - self._rtp[cookie].handle_media_sample(sample) - - def endCall(self, callcookie, reason=''): - rtp = self._rtp.get(callcookie) - log.msg("endCall clearing %r"%(callcookie)) - self._currentCall = None - if rtp: - rtp = self._rtp[callcookie] - rtp.stopSendingAndReceiving() - del self._rtp[callcookie] - if self._calls.get(callcookie): - del self._calls[callcookie] - self.closeAudioDevice() - self.ui.callDisconnected(callcookie, reason) - - def openAudioDevice(self, fmts=[PT_PCMU,], mediahandler=None): - assert isinstance(fmts, (list, tuple,)), fmts - assert self._audio - self._audio.close() - self._audio.selectDefaultFormat(fmts) - self._audio.reopen(mediahandler) - - def closeAudioDevice(self): - self._audio.close() - - def incomingRTP(self, callcookie, packet): - from shtoom.rtp.formats import PT_NTE - if packet.header.ct == PT_NTE: - return None - if self._currentCall != callcookie: - return None - try: - self._audio.write(packet) - except IOError: - pass + log.msg("startCall %r to %r" % (self._rtp, remoteAddr,)) + self.rtpmap = md.rtpmap + self._audioplaydev.open(self.rtpmap) + + markers = {} + for (pt, (text, marker,)) in md.rtpmap.items(): + markers[marker] = pt + + self.enccodec = None + self.enccodecpt = None + for preferredcodec in KnownVoCodecs: + if markers.has_key(preferredcodec): + self.enccodec = preferredcodec + self.enccodecpt = markers[self.enccodec] + break + + if not self.enccodec: + raise ValueError, "We do not know how to produce any codec that the recipient can consume. The final SDP codec set was %s. Our KnownVoCodecs is %s" % (md.rtpmap, KnownVoCodecs,) + + self._audiomicdev.open(self.enccodec, self.enccodecpt, self._rtp) + self._rtp.start(remoteAddr) + + self.statusMessage("Call Connected") + log.msg("startCall()") + + def endCall(self, reason=''): + log.msg("endCall clearing") + if self._rtp: + self.call.dropCall() + self.call = None + self._rtp.stopSendingAndReceiving() + self._rtp = None + self._audioplaydev.close() + self._audiomicdev.close() + self.statusMessage("Call disconnected") + self.ui.callDisconnected(reason) + + def incomingRTP(self, packet): + if not self._muteplayout: + self._audioplaydev.write(packet) def placeCall(self, sipURL): return self.sip.placeCall(sipURL) - def dropCall(self, cookie): - call = self._calls.get(cookie) - if call: - d = call.dropCall() - # xxx Add callback. - #else: - # self.ui.callDisconnected(None, "no call") - - def startDTMF(self, cookie, digit): - rtp = self._rtp[cookie] - rtp.startDTMF(digit) - - def stopDTMF(self, cookie, digit): - rtp = self._rtp[cookie] - rtp.stopDTMF(digit) - def statusMessage(self, message): self.ui.statusMessage(message) @@ -290,19 +272,38 @@ else: return defer.fail(CallFailed("No auth available")) - def muteCall(self, callcookie): - if self._currentCall is not callcookie: - raise ValueError("call %s is current call, not %s"%( - self._currentCall, callcookie)) - else: - self._muted = True + def mutePlayout(self): + self._muteplayout = True + + def unmutePlayout(self, dummy=None): + self._muteplayout = False - def unmuteCall(self, callcookie): - if self._currentCall is not callcookie: - raise ValueError("call %s is current call, not %s"%( - self._currentCall, callcookie)) +class WaveFileOutPlayer: + """ + Mute playout of voice packets, play out wav file, then unmute voice packets. + """ + def __init__(self, fname, device, app): + self.fname = fname + self.device = device + self.app = app + + self.app.mutePlayout() + self.wavefileobj = aufile.WavReader(self.fname) + + self.device.open(self._refill) + + def _refill(self): + d = self.wavefileobj.read() + if d: + self.device.write(d) else: - self._muted = False + self.device.close() + self._audio_file_done() + + def stop(self): + self._audio_file_done() + + def _audio_file_done(self): + self.app.unmutePlayout() + self.wavefileobj = None - def switchCallAudio(self, callcookie): - self._currentCall = callcookie diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py new-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py 2005-09-27 23:36:20.376378032 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/__init__.py 2005-09-27 23:36:20.530354624 -0300 @@ -2,21 +2,22 @@ """ from twisted.python import log -from shtoom.audio.converters import MediaLayer +# from the pyutil library +from pyutil.assertutil import _assert, precondition, postcondition +from shtoom.avail import audio as av_audio +from shtoom.exceptions import NoAudioDevice + +def findAudioInterface(audioinfile=None, audiooutfile=None, audiopref=None): + precondition((audioinfile is None and audiooutfile is None) or audiopref == 'file', "You are required to pass an audioinfile or audiooutfile argument *only* if audiopref is 'file'.", audioinfile, audiooutfile, audiopref) -def findAudioInterface(): - # Ugh. Circular import hell - from shtoom.avail import audio as av_audio audioOptions = { 'oss': av_audio.ossaudio, 'alsa': av_audio.alsaaudio, 'fast': av_audio.fastaudio, 'port': av_audio.fastaudio, 'osx': av_audio.osxaudio, 'core': av_audio.osxaudio, - 'file': av_audio.fileaudio, - 'echo': av_audio.echoaudio, } allAudioOptions = [ av_audio.alsaaudio, @@ -25,40 +26,33 @@ av_audio.osxaudio ] - audioPref = attempts = None + if audioinfile or audiooutfile: + audioPref = 'file' - try: - from __main__ import app - except: - app = None - - if app: - audioPref = app.getPref('audio') - - print "audioPref is", audioPref - if audioPref: - audioint = audioOptions.get(audioPref) - if not audioint: - log.msg("requested audio interface %s unavailable"%(audioPref,)) - else: + if audiopref: + audioint = audioOptions.get(audiopref) + if audioint: return audioint + log.msg("Requested audio interface %s not available. Trying other interfaces." % (audiopref,)) for audioint in allAudioOptions: if audioint: return audioint -_device = None - -def getAudioDevice(_testAudioInt=None): - from shtoom.exceptions import NoAudioDevice - global _device - if _testAudioInt is not None: - return MediaLayer(_testAudioInt.Device()) - - if _device is None: - audioint = findAudioInterface() - if audioint is None: - raise NoAudioDevice("no working audio interface found") - dev = audioint.Device() - _device = MediaLayer(dev) - return _device +def getAudioPlayDevice(_testAudioWrite=None): + if _testAudioWrite is not None: + return _testAudioWrite + + audioint = findAudioInterface() + if audioint is None: + raise NoAudioDevice("no working audio interface found") + return audioint.PlayDevice() + +def getAudioMicDevice(_testAudioRead=None): + if _testAudioRead is not None: + return _testAudioRead + + audioint = findAudioInterface() + if audioint is None: + raise NoAudioDevice("no working audio interface found") + return audioint.MicDevice() diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py new-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py 2005-09-27 23:36:20.368379248 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/alsa.py 2005-09-27 23:36:20.529354776 -0300 @@ -1,81 +1,106 @@ # Copyright (C) 2004 Anthony Baxter -# from pyalsa +# from pyalsaaudio import alsaaudio -# from Shtoom -import baseaudio - # from Twisted from twisted.python import log +from twisted.internet import reactor from twisted.internet.task import LoopingCall -import time -import audioop - -opened = None -DEFAULT_ALSA_DEVICE = 'default' - -class ALSAAudioDevice(baseaudio.AudioDevice): - - def openDev(self): - try: - from __main__ import app - except: - app = None - if app is None: - device = DEFAULT_ALSA_DEVICE - else: - device = app.getPref('audio_device', DEFAULT_ALSA_DEVICE) - log.msg("alsaaudiodev opening device %s" % (device)) - writedev = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, - alsaaudio.PCM_NONBLOCK, device) - self.writechannels = writedev.setchannels(1) - writedev.setrate(8000) - writedev.setformat(alsaaudio.PCM_FORMAT_S16_LE) - writedev.setperiodsize(160) - self.writedev = writedev - - readdev = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, - alsaaudio.PCM_NONBLOCK, device) - self.readchannels = readdev.setchannels(1) - readdev.setrate(8000) - readdev.setformat(alsaaudio.PCM_FORMAT_S16_LE) - readdev.setperiodsize(160) - self.readdev = readdev +PERIODSIZE=160 +RATE=8000 +SECS_PER_PERIOD=float(PERIODSIZE) / RATE +BYTES_PER_SECOND=RATE * 2 + +class MicDevice: + def __init__(self): + self.readdev = None + self.micdatahandler = None + self.LC = None + + def __repr__(self): + return "alsa.MicDevice" + + def open(self, micdatahandler): + self.close() + log.msg("%s opening" % (self,)) + self.micdatahandler = micdatahandler + + self.readdev = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK) + self.readdev.setchannels(1) + self.readdev.setrate(RATE) + self.readdev.setperiodsize(PERIODSIZE) self.LC = LoopingCall(self._push_up_some_data) - self.LC.start(0.010) + self.LC.start(SECS_PER_PERIOD) def _push_up_some_data(self): + assert self.readdev, "It is required that _push_up_some_data() be called only when the read device is open for reading." (l, data,) = self.readdev.read() - if self.readchannels == 2: - data = audioop.tomono(data, 2, 1, 1) - if self.encoder and data: - self.encoder.handle_audio(data) + while data: + self.micdatahandler.handle_data(data) + (l, data,) = self.readdev.read() + + def isOpen(self): + return self.readdev is not None + + def close(self): + if self.isOpen(): + log.msg("alsaaudiodev closing") + self.LC.stop() + self.LC = None + self.readdev = None + +class PlayDevice: + def __init__(self): + self.writedev = None + self.refillcb = None # refill callback gets called when playout chunk is half-finished + self.refilldc = None # refill delayed call is the object that will invoke refillcb after a delay + + def __repr__(self): + return "alsa.PlayDevice" + + def open(self, refillcb=None): + """ + If not None, then refillcb will be called anytime the playout buffer + becomes at least half empty. + """ + self.close() + log.msg("%s opening" % (self,)) + + if self.refilldc: + self.refilldc.cancel() + self.refilldc = None + self.refillcb = refillcb + self.writedev = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK) + self.writedev.setchannels(1) + self.writedev.setrate(RATE) + self.writedev.setperiodsize(PERIODSIZE) + + if self.refillcb: + self.refillcb() + + def _give_refill_cb(self): + assert self.refillcb + self.refilldc = None + self.refillcb() def write(self, data): - if not hasattr(self, 'LC'): - return - assert self.isOpen(), "calling write() on closed %s"%(self,) - if self.writechannels == 2: - data = audioop.tostereo(data, 2, 1, 1) wrote = self.writedev.write(data) - if not wrote: log.msg("ALSA overrun") + if wrote != len(data): log.msg("ALSA overrun -- tried to write %d bytes but wrote %d bytes" % (len(data), wrote,)) + if self.refillcb: + refilldelay = (float(wrote) / BYTES_PER_SECOND) / 2 # how many seconds until half of this data is finished playing out + if self.refilldc: + self.refilldc.reset(refilldelay) + else: + self.refilldc = reactor.callLater(refilldelay, self._give_refill_cb) def isOpen(self): - return hasattr(self, 'writedev') + return self.writedev is not None def close(self): if self.isOpen(): - log.msg("alsaaudiodev closing") - try: - self.LC.stop() - except AttributeError: - # ? bug in Twisted? Not sure. This catch-and-ignore is a temporary workaround. --Zooko - pass - del self.LC - del self.writedev - del self.readdev + log.msg("%s closing" % (self,)) + self.writedev = None -Device = ALSAAudioDevice diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py new-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py 2005-09-27 23:36:20.356381072 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/baseaudio.py 2005-09-27 23:36:20.529354776 -0300 @@ -1,19 +1,15 @@ # Copyright (C) 2004 Anthony Baxter class AudioDevice(object): - encoder = None - def __init__(self, mode='ignored'): - self.openDev() self._closed = False + self.sink = None - def set_encoder(self, encoder): + def set_sink(self, sink): """ - The encoder object will subsequently receive calls to its - handle_audio() method when audio is available - it passes it on - to the rest of the system (eventually, to the network). + The sink object will subsequently receive calls to its handle_data() method. """ - self.encoder = encoder + self.sink = sink def close(self): if not self._closed: @@ -22,6 +18,7 @@ def reopen(self): self.close() + print "baseaudio: reopen" self.openDev() self._closed = False diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/converters.py new-newfrom1406/trunk/shtoom/shtoom/audio/converters.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/converters.py 2005-09-27 23:36:20.353381528 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/converters.py 2005-09-27 23:36:20.529354776 -0300 @@ -1,62 +1,28 @@ -# Copyright (C) 2004 Anthony Baxter -from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_DVI4, PT_RAW -from shtoom.rtp.formats import PT_PCMA, PT_ILBC -from shtoom.rtp.formats import PT_CN, PT_xCN, AudioPTMarker +# Copyright (C) 2004,2005 Anthony Baxter + +# from Shtoom +from shtoom.rtp.formats import PT_PCMU, PT_GSM, PT_SPEEX, PT_RAW +from shtoom.rtp.formats import PT_PCMA +from shtoom.rtp.formats import PT_CN, PT_xCN, PT_NTE from shtoom.avail import codecs -from shtoom.audio import aufile, playout + from shtoom.lwc import Interface, implements +# from the Twisted library +from twisted.internet import defer, reactor from twisted.python import log +# from the Python Standard Library import sets, struct +# from the pyutil Library +from pyutil.assertutil import _assert, precondition, postcondition + try: import audioop except ImportError: audioop = None -class NullEncoder: - def handle_audio(self, data): - pass - -nullencoder = NullEncoder() - -class MediaSample: - def __init__(self, ct, data): - self.ct = ct - self.data = data - - def __repr__(self): - return "<%s/%s, %s>" % (self.__class__.__name__, self.ct, `self.data`,) - -class NullConv: - # Should be refactored away - def __init__(self, device): - self._d = device - def getDevice(self): - return self._d - def setDevice(self, d): - self._d = d - def getFormats(self): - if self._d: - return self._d.getFormats() - def write(self, data): - if self._d: - return self._d.write(data) - def close(self): - if self._d: - log.msg("audio device %r close"%(self._d,), system="audio") - return self._d.close() - def reopen(self): - if self._d: - log.msg("audio device %r reopen ..."%(self._d,), system="audio") - return self._d.reopen() - def isOpen(self): - if self._d: - return self._d.isOpen() - def __repr__(self): - return '<%s wrapped around %r>'%(self.__class__.__name__, self._d) - def isLittleEndian(): import struct p = struct.pack('H', 1) @@ -67,41 +33,42 @@ else: raise ValueError("insane endian-check result %r"%(p)) -class IAudioCodec: - def buffer_and_encode(self, bytes): - "encode bytes, a string of audio" - def decode(self, bytes): - "decode bytes, a string of audio" - -class _Codec: - "Base class for codecs" - implements(IAudioCodec) - def __init__(self, samplesize): +class Encoder: + "Base class for encoders" + def __init__(self, pt, handler, samplesize): + """ configure it to buffer up samplesize bytes of audio data before + calling _encode() to generate a new media frame. handler's + handle_media_sample() will get called whenever there is a new encoded + media sample to be had """ + self.pt = pt + self.handler = handler self.samplesize = samplesize self.b = '' - def buffer_and_encode(self, bytes): + def handle_data(self, bytes): self.b += bytes - res = [] while len(self.b) >= self.samplesize: sample, self.b = self.b[:self.samplesize], self.b[self.samplesize:] - res.append(self._encode(sample)) - return res + self.handler.handle_media_sample(self.pt, self._encode(sample)) -class GSMCodec(_Codec): - def __init__(self): - _Codec.__init__(self, 320) +class GSMEncoder(Encoder): + def __init__(self, pt, handler): + Encoder.__init__(self, pt, handler, 320) if isLittleEndian(): self.enc = codecs.gsm.gsm(codecs.gsm.LITTLE) - self.dec = codecs.gsm.gsm(codecs.gsm.LITTLE) else: self.enc = codecs.gsm.gsm(codecs.gsm.BIG) - self.dec = codecs.gsm.gsm(codecs.gsm.BIG) def _encode(self, bytes): - assert isinstance(bytes, str), bytes return self.enc.encode(bytes) +class GSMDecoder: + def __init__(self): + if isLittleEndian(): + self.dec = codecs.gsm.gsm(codecs.gsm.LITTLE) + else: + self.dec = codecs.gsm.gsm(codecs.gsm.BIG) + def decode(self, bytes): assert isinstance(bytes, str), bytes if len(bytes) != 33: @@ -110,18 +77,19 @@ return None return self.dec.decode(bytes) -class SpeexCodec(_Codec): - "A codec for Speex" - - def __init__(self): +class SpeexEncoder(Encoder): + def __init__(self, pt, handler): self.enc = codecs.speex.new(8) - self.dec = codecs.speex.new(8) - _Codec.__init__(self, 320) + Encoder.__init__(self, pt, handler, 320) def _encode(self, bytes, unpack=struct.unpack): frames = list(unpack('160h', bytes)) return self.enc.encode(frames) +class SpeexDecoder: + def __init__(self): + self.dec = codecs.speex.new(8) + def decode(self, bytes): if len(bytes) != 40: log.msg("speex: short read on decode %d != 40"%len(bytes), @@ -131,232 +99,112 @@ ostr = struct.pack('160h', *frames) return ostr -class MulawCodec(_Codec): - "A codec for mulaw encoded audio (e.g. G.711U)" - - def __init__(self): - _Codec.__init__(self, 320) +class MulawEncoder(Encoder): + "An encoder for mulaw encoded audio (e.g. G.711U)" + def __init__(self, pt, handler): + Encoder.__init__(self, pt, handler, 320) def _encode(self, bytes): return audioop.lin2ulaw(bytes, 2) +class MulawDecoder: + "A decoder for mulaw encoded audio (e.g. G.711U)" def decode(self, bytes): if len(bytes) != 160: - log.msg("mulaw: short read on decode, %d != 160"%len(bytes), + log.msg("mulaw: short read on decode, %d != 160"%len(bytes), system="codec") return audioop.ulaw2lin(bytes, 2) -class AlawCodec(_Codec): - "A codec for alaw encoded audio (e.g. G.711A)" - - def __init__(self): - _Codec.__init__(self, 320) +class AlawEncoder(Encoder): + "An encoder for alaw encoded audio (e.g. G.711A)" + def __init__(self, pt, handler): + Encoder.__init__(self, pt, handler, 320) def _encode(self, bytes): return audioop.lin2alaw(bytes, 2) +class AlawDecoder: + "A decoder for alaw encoded audio (e.g. G.711A)" def decode(self, bytes): if len(bytes) != 160: - log.msg("alaw: short read on decode, %d != 160"%len(bytes), + log.msg("alaw: short read on decode, %d != 160"%len(bytes), system="codec") return audioop.alaw2lin(bytes, 2) -class NullCodec(_Codec): - "A codec that consumes/emits nothing (e.g. for confort noise)" - - def __init__(self): - _Codec.__init__(self, 1) - - def _encode(self, bytes): - return None - +class NullDecoder: + "A decoder that emits nothing (e.g. for confort noise)" def decode(self, bytes): return None -class PassthruCodec(_Codec): - "A codec that leaves it's input alone" - def __init__(self): - _Codec.__init__(self, None) - decode = lambda self, bytes: bytes - buffer_and_encode = lambda self, bytes: [bytes] - -def make_codec_set(): - format_to_codec = {} - format_to_codec[PT_CN] = NullCodec() - format_to_codec[PT_xCN] = NullCodec() - format_to_codec[PT_RAW] = PassthruCodec() - assert codecs.mulaw - if codecs.mulaw is not None: - format_to_codec[PT_PCMU] = MulawCodec() - if codecs.alaw is not None: - format_to_codec[PT_PCMA] = AlawCodec() - if codecs.gsm is not None: - format_to_codec[PT_GSM] = GSMCodec() - if codecs.speex is not None: - format_to_codec[PT_SPEEX] = SpeexCodec() - #if codecs.dvi4 is not None: - # format_to_codec[PT_DVI4] = DVI4Codec() - #if codecs.ilbc is not None: - # format_to_codec[PT_ILBC] = ILBCCodec() - return format_to_codec - -known_formats = (sets.ImmutableSet(make_codec_set().keys()) - - sets.ImmutableSet([PT_CN, PT_xCN,])) - -class Codecker: - def __init__(self, format): - self.format_to_codec = make_codec_set() - if not format in known_formats: - raise ValueError("Can't handle codec %r"%format) - self.format = format - self.handler = None +def make_encoder(format, pt, mediahandler): + if format is PT_PCMU: + return MulawEncoder(pt, mediahandler) + elif format is PT_PCMA: + return AlawEncoder(pt, mediahandler) + elif format is PT_GSM: + return GSMEncoder(pt, mediahandler) + elif format is PT_SPEEX: + return SpeexEncoder(pt, mediahandler) + +NULL_FORMATS = [PT_CN, PT_xCN, PT_NTE,] + +def make_decoder(format): + if format in NULL_FORMATS: + return NullDecoder() + elif format is PT_PCMU: + return MulawDecoder() + elif format is PT_PCMA: + return AlawDecoder() + elif format is PT_GSM: + return GSMDecoder() + elif format is PT_SPEEX: + return SpeexDecoder() + +class MicDev: + """ The MicDev sits between the network and the raw audio input device. It + converts the audio to the codec on the network from the format used by + the lower-level audio device (16 bit signed ints at 8KHz). + """ + def __init__(self, readdevice): + """ + rtpmap is a dict with key PT byte and value a tuple of (text, PT_Marker instance,) + """ + self.readdevice = readdevice - def set_handler(self, handler): + def open(self, mediahandler, format=None, pt=None): """ - handler will subsequently receive calls to handle_media_sample(). + mediahandler will subsequently receive calls to handle_media_sample(). """ - self.handler = handler + precondition((format is None) == (pt is None) == (mediahandler is None), "You are required to pass a format, a PT number, and a mediahandler or else none of those three things.", format, pt, mediahandler) + encoder = make_encoder(format, pt, mediahandler) + self.readdevice.open(encoder) - def getDefaultFormat(self): - return self.format + def close(self): + self.readdevice.close() - def handle_audio(self, bytes): - "Accept audio as bytes, emits MediaSamples." - if not bytes: - return None - codec = self.format_to_codec.get(self.format) - if not codec: - raise ValueError("can't encode format %r"%self.format) - encaudios = codec.buffer_and_encode(bytes) - for encaudio in encaudios: - samp = MediaSample(self.format, encaudio) - if self.handler is not None: - self.handler(samp) - else: - return samp - - def decode(self, packet): - "Accepts an RTPPacket, emits audio as bytes" - if not packet.data: - return None - codec = self.format_to_codec.get(packet.header.ct) - if not codec: - raise ValueError("can't decode format %r"%packet.header.ct) - encaudio = codec.decode(packet.data) - return encaudio - -class MediaLayer(NullConv): - """ The MediaLayer sits between the network and the raw - audio device. It converts the audio to/from the codec on - the network to the format used by the lower-level audio - devices (16 bit signed ints at an integer multiple of 8KHz). +class PlayDev: + """ The PlayDev sits between the network and the raw audio output device. It + converts the audio from the codec on the network to the format used by + the lower-level audio device (16 bit signed ints at 8KHz). """ - def __init__(self, device, *args, **kwargs): - self.playout = None - self.codecker = None - self.defaultFormat = None - # this sets self._d = device - NullConv.__init__(self, device, *args, **kwargs) + def __init__(self, writedev): + self.writedev = writedev + # dict k: pt bytes, v: decoder object + self.decoders = None + + def open(self, rtpmap): + """ + rtpmap is a dict with key PT byte and value a tuple of (text, PT_Marker instance,) + """ + self.decoders = {} + for (pt, (text, marker,),) in rtpmap.items(): + self.decoders[pt] = make_decoder(marker) + self.writedev.open() - def getFormat(self): - return self.defaultFormat + def close(self): + self.decoders = None + self.writedev.close() def write(self, packet): - if self.playout is None: - log.msg("write before reopen, discarding") - return 0 - audio = self.codecker.decode(packet) - if audio: - return self.playout.write(audio, packet.header.seq) - else: - self.playout.write('', packet.header.seq) - return 0 + self.writedev.write(self.decoders[packet.header.pt].decode(packet.data)) - def selectDefaultFormat(self, fmts=[PT_PCMU,]): - assert isinstance(fmts, (list, tuple,)), fmts - assert not self._d or not self._d.isOpen(), \ - "close device %r before calling selectDefaultFormat()" % (self._d,) - - for f in fmts: - if f in known_formats: - self.defaultFormat = f - break - else: - raise ValueError("No working formats!") - - def reopen(self, mediahandler=None): - """ - mediahandler, if not None, is a callable that will be called with - a media sample is available. - - This flushes codec buffers. The audio playout buffers and microphone - readin buffers *ought* to be flushed by the lower-layer audio device - when we call reopen() on it. - """ - assert self.defaultFormat, "must call selectDefaultFormat()"+\ - "before (re-)opening the device." - - self.codecker = Codecker(self.defaultFormat) - self._d.reopen() - if mediahandler: - self.codecker.set_handler(mediahandler) - self._d.set_encoder(self.codecker) - else: - self._d.set_encoder(nullencoder) - - if self.playout: - log.msg("playout already started") - else: - self.playout = playout.Playout(self) - - def play_wave_file(self, fname): - """ - Note that calling write() in a while loop like this - effectively blocks the whole Twisted app until we finish - spooling the whole audio file into the output audio buffer. - Hopefully this is good enough for now. In the future it would - be nice if we could just initiate a playout here, the way Doug - does. That would also allow you to answer the phone before the - ring file finishes playing... --Zooko 2004-10-21 - P.S. Oh, and if your output device is ALSA, this will - immediately overflow the output FIFO, so only the first few - milliseconds of the wav file will be heard. Whoops. This - really needs to be fixed... --Zooko 2005-02-25 - """ - if not self._d.isOpen(): - self.selectDefaultFormat([PT_PCMU,]) - self.reopen() - try: - wavefileobj = aufile.WavReader(fname) - data = wavefileobj.read() - # Note that calling write() in a while loop like this effectively - # blocks the whole Twisted app until the audio file is all buffered - # up by the underlying audio output device. Hopefully this is good - # enough for now. In the future it would be nice if we could just - # initiate a playout here, the way doug does. That would also - # allow you to answer the phone before the ring file finishes - # playing... --Zooko 2004-10-21 - while data: - self._d.write(data) - data = wavefileobj.read() - except (IOError, ValueError, EOFError,), le: - print "warning: wave file error: %r, %s, %s" % (le, le, le.args,) - pass - - def close(self): - self.playout = None - self.codecker = None - self._d.set_encoder(nullencoder) - NullConv.close(self) - -class DougConverter(MediaLayer): - "Specialised converter for Doug." - # XXX should be refactored away to just use a Codecker directly - def __init__(self, defaultFormat=PT_PCMU, *args, **kwargs): - self.codecker = Codecker(defaultFormat) - self.convertInbound = self.codecker.decode - self.convertOutbound = self.codecker.handle_audio - self.set_handler = self.codecker.set_handler - if not kwargs.get('device'): - kwargs['device'] = None - NullConv.__init__(self, *args, **kwargs) diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py new-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py 2005-09-27 23:36:20.297390040 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/interfaces.py 2005-09-27 23:36:20.529354776 -0300 @@ -1,6 +1,5 @@ from twisted.python.components import Interface -# XXX TODO update! class IAudio(Interface): '''Lowlevel interface to audio source/sink.''' diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/audio/playout.py new-newfrom1406/trunk/shtoom/shtoom/audio/playout.py --- old-newfrom1406/trunk/shtoom/shtoom/audio/playout.py 2005-09-27 23:36:20.296390192 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/audio/playout.py 2005-09-27 23:36:20.528354928 -0300 @@ -64,7 +64,7 @@ i2 += 1 return True -class Playout: +class SmartPlayout: """ Theory of operation: you have two modes: "playout" mode and "refill" mode. When you are in playout mode then you play out sequential audio packets from @@ -140,7 +140,7 @@ while ((t() + PLAYOUT_BUFFER_SECONDS >= self.drytime) and self.b and self.b[0][0] == (self.s + 1)): (seq, bytes,) = self.b.pop(0) - self.medialayer._d.write(bytes) + self.medialayer._d.write(bytes, seq) self.s = seq packetlen = len(bytes) / float(16000) if self.drytime is None: @@ -205,11 +205,21 @@ log.msg("xxxxxxx catchup! dropping %s" % seq) self.s = self.b[0][0] - 1 # prime it for the next packet -class NullPlayout: + +class DumbPlayout: def __init__(self, medialayer): self.medialayer = medialayer + self.device = d = medialayer._d + ## Cut down on one extra python method call overhead by + ## setting AudioMonitorDelegate.write to self.write. + ## AudioMonitorDelegate thus gets the sequence number + ## which it may then pass to C. + self.write = d.write + + def close(self): + print "CLOSE CALLED." + + +Playout = DumbPlayout - def write(self, bytes, seq, t=time.time): - self.medialayer._d.write(bytes) -#Playout=NullPlayout diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/avail/audio.py new-newfrom1406/trunk/shtoom/shtoom/avail/audio.py --- old-newfrom1406/trunk/shtoom/shtoom/avail/audio.py 2005-09-27 23:36:20.283392168 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/avail/audio.py 2005-09-27 23:36:20.530354624 -0300 @@ -41,8 +41,6 @@ else: osxaudio = None -from shtoom.audio import fileaudio - try: import alsaaudio except ImportError: @@ -52,8 +50,6 @@ if alsaaudio is not None: from shtoom.audio import alsa as alsaaudio -from shtoom.audio import fileaudio, echoaudio - def listAudio(): all = globals().copy() diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py new-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py --- old-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py 2005-09-27 23:36:20.281392472 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/rtp/formats.py 2005-09-27 23:36:20.519356296 -0300 @@ -85,33 +85,23 @@ PT_MP2T = VideoPTMarker('MP2T', clock=90000, pt=33) PT_H263 = VideoPTMarker('H263', clock=90000, pt=34) -TryCodecs = OrderedDict() -TryCodecs[PT_GSM] = codecs.gsm -TryCodecs[PT_SPEEX] = codecs.speex -TryCodecs[PT_DVI4] = codecs.dvi4 -TryCodecs[PT_ILBC] = codecs.ilbc +# This list of known voice codecs is in descending order of preference. +KnownVoCodecs = [] +if codecs.speex: + KnownVoCodecs.append(PT_SPEEX) +if codecs.mulaw: + KnownVoCodecs.append(PT_PCMU) +if codecs.gsm: + KnownVoCodecs.append(PT_GSM) +if codecs.dvi4: + KnownVoCodecs.append(PT_DVI4) +if codecs.ilbc: + KnownVoCodecs.append(PT_ILBC) -class SDPGenerator: - "Responsible for generating SDP for the RTPProtocol" - - def getSDP(self, rtp, extrartp=None): - from shtoom.sdp import SDP, MediaDescription - if extrartp: - raise ValueError("can't handle multiple RTP streams in a call yet") - s = SDP() - addr = rtp.getVisibleAddress() - s.setServerIP(addr[0]) - md = MediaDescription() # defaults to type 'audio' - s.addMediaDescription(md) - md.setServerIP(addr[0]) - md.setLocalPort(addr[1]) - for pt, test in TryCodecs.items(): - if test is not None: - md.addRtpMap(pt) - md.addRtpMap(PT_PCMU) - md.addRtpMap(PT_CN) - md.addRtpMap(PT_NTE) - return s +KnownCodecs = [] +KnownCodecs.extend(KnownVoCodecs) +KnownCodecs.append(PT_CN) +KnownCodecs.append(PT_NTE) RTPDict = {} all = globals() diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py new-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py --- old-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py 2005-09-27 23:36:20.277393080 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/rtp/protocol.py 2005-09-27 23:36:20.519356296 -0300 @@ -10,9 +10,9 @@ from twisted.internet.protocol import DatagramProtocol from twisted.python import log -from shtoom.rtp.formats import SDPGenerator, PT_CN, PT_xCN, PT_NTE +from shtoom.sdp import MediaDescription +from shtoom.rtp.formats import PT_CN, PT_xCN, PT_NTE from shtoom.rtp.packets import RTPPacket, parse_rtppacket -from shtoom.audio.converters import MediaSample TWO_TO_THE_16TH = 2<<16 TWO_TO_THE_32ND = 2<<32 @@ -29,14 +29,13 @@ _stunAttempts = 0 _cbDone = None + LC = None Done = False - def __init__(self, app, cookie, *args, **kwargs): + def __init__(self, app, *args, **kwargs): self.app = app - self.cookie = cookie - self._pendingDTMF = [] #DatagramProtocol.__init__(self, *args, **kwargs) - self.ptdict = {} + self.format = None # which PT_* format we encode into when sending voice in this call self.seq = self.genRandom(bits=16) self.ts = self.genInitTS() self.ssrc = self.genSSRC() @@ -45,21 +44,18 @@ # media sample handler instead of this RTP object as the media sample handler. self.sending = False - def getSDP(self, othersdp=None): - sdp = SDPGenerator().getSDP(self) - if othersdp: - sdp.intersect(othersdp) - self.setSDP(sdp) - return sdp - - def setSDP(self, sdp): - "This is the canonical SDP for the call" - self.app.selectDefaultFormat(self.cookie, sdp) - rtpmap = sdp.getMediaDescription('audio').rtpmap - self.ptdict = {} - for pt, (text, marker) in rtpmap.items(): - self.ptdict[pt] = marker - self.ptdict[marker] = pt + def register_rtpext(self, rtpext): + self._rtpext = rtpext + + def set_sdp_addr(self, sdp): + addr = self.getVisibleAddress() + sdp.setServerIP(addr[0]) + + md = MediaDescription() # defaults to type 'audio' + md.setServerIP(addr[0]) + md.setLocalPort(addr[1]) + + sdp.addMediaDescription(md) def createRTPSocket(self, locIP, needSTUN=False): """ Start listening on UDP ports for RTP and RTCP. @@ -118,7 +114,7 @@ self._extIP = locIP d = self._socketCompleteDef del self._socketCompleteDef - d.callback(self.cookie) + d.callback(self) else: # If the NAT is doing port translation as well, we will just # have to try STUN and hope that the RTP/RTCP ports are on @@ -218,7 +214,7 @@ self._stunAttempts = 0 d = self._socketCompleteDef del self._socketCompleteDef - d.callback(self.cookie) + d.callback(self) def connectionRefused(self): log.err("RTP got a connection refused, ending call") @@ -246,23 +242,13 @@ try: self.transport.write(packet.netbytes(), self.dest) except Exception, le: - pass + print "We attempted to send a packet before start() was called. This can happen because we are attempting to send a packet in response to an incoming packet. The outgoing packet will be dropped, exactly as if it had been successfully sent and then disappeared into the void. Our protocol is required to deal with this possibility anyway, since we use an unreliable transport. As soon as the SIP ACK message arrives then start() will be called and after that attempts to send will work. pt: %s, data: %s, xhdrtype: %s, xhdrdata: %s, le: %s" % (pt, `data`, xhdrtype, `xhdrdata`, le,) def _send_cn_packet(self): - assert hasattr(self, 'dest'), "_send_cn_packet called before start %r" % (self,) + assert hasattr(self, 'dest'), "It is required that start() is called before _send_cn_packet() is called. This requirement has not been met. self: %s :: %s" % (self, type(self),) + log.msg("sending CN(13) to seed firewall to %s:%d"%(self.dest[0], self.dest[1]), system='rtp') # 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)) + self._send_packet(13, chr(127)) def start(self, dest, fp=None): self.dest = dest @@ -277,23 +263,14 @@ self._send_cn_packet() def datagramReceived(self, datagram, addr, t=time): - packet = parse_rtppacket(datagram) - - try: - packet.header.ct = self.ptdict[packet.header.pt] - except KeyError: - if packet.header.pt == 19: - # Argh nonstandardness suckage - packet.header.pt = 13 - packet.header.ct = self.ptdict[packet.header.pt] - else: - # XXX This could overflow the log. Ideally we would have a - # "previous message repeated N times" feature... --Zooko 2004-10-18 - log.msg("received packet with unknown PT %s" % packet.header.pt) - return # drop the packet on the floor + if self._rtpext: + packet = self._rtpext.process_incoming(datagram) + if not packet: + return + else: + packet = parse_rtppacket(datagram) - packet.header.ct = self.ptdict[packet.header.pt] - self.app.incomingRTP(self.cookie, packet) + self.app.incomingRTP(packet) def genSSRC(self): # Python-ish hack at RFC1889, Appendix A.6 @@ -344,7 +321,7 @@ hex = m.hexdigest() return int(hex[:bits//4],16) - def handle_media_sample(self, sample): + def handle_media_sample(self, pt, sample): if self.Done: if self._cbDone: self._cbDone() @@ -354,13 +331,13 @@ # can use this as an excuse to adjust playout buffer. if not self.sending: if not hasattr(self, 'warnedaboutthis'): - log.msg(("%s.handle_media_sample() should only be called" + - " only when it is in sending mode.") % (self,)) + log.msg(("WARNING: This is required to be synchronized so " + + "that %s.handle_media_sample() gets called only when it is in " + + "sending mode.") % (self,)) self.warnedaboutthis = True return - - pt = self.ptdict[sample.ct] - self._send_packet(pt, sample.data) + + self._send_packet(pt, sample) self.ts += 160 # Wrapping if self.ts >= TWO_TO_THE_32ND: diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/sdp.py new-newfrom1406/trunk/shtoom/shtoom/sdp.py --- old-newfrom1406/trunk/shtoom/shtoom/sdp.py 2005-09-27 23:36:20.244398096 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/sdp.py 2005-09-27 23:36:20.535353864 -0300 @@ -139,7 +139,7 @@ class MediaDescription: "The MediaDescription encapsulates all of the SDP media descriptions" def __init__(self, text=None): - self.media = None + self.media = 'audio' self.nettype = 'IN' self.addrfamily = 'IP4' self.ipaddr = None @@ -149,7 +149,6 @@ self._d = {} self._a = {} self.rtpmap = OrderedDict() - self.media = 'audio' self.transport = 'RTP/AVP' self.keyManagement = None if text: @@ -161,13 +160,11 @@ pt = int(pt) if pt < 97: try: - PT = RTPDict[pt] + self.addRtpMap(RTPDict[pt]) except KeyError: # We don't know this one - hopefully there's an # a=rtpmap entry for it. continue - self.addRtpMap(PT) - # XXX the above line is unbound local variable error if not RTPDict.has_key(pt) --Zooko 2004-09-29 self.formats = formats def setMedia(self, media): diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/sip.py new-newfrom1406/trunk/shtoom/shtoom/sip.py --- old-newfrom1406/trunk/shtoom/shtoom/sip.py 2005-09-27 23:36:20.236399312 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/sip.py 2005-09-27 23:36:20.535353864 -0300 @@ -224,7 +224,7 @@ class Call(object): '''State machine for a phone call (inbound or outbound).''' - cookie = uri = None + uri = None nonce_count = 1 sip = None _invite = None @@ -238,14 +238,9 @@ self.dialog = Dialog() self.state = 'NEW' self._needSTUN = False - self.cancel_trigger = None if callid: self.setCallID(callid) - def callStart(self): - self.compDef = defer.Deferred() - return self.compDef - def setupLocalSIP(self, uri=None, via=None): ''' Setup SIP stuff at this end. Call with either a t.p.sip.URL (for outbound calls) or a t.p.sip.Via object (for inbound calls). @@ -270,7 +265,7 @@ def abortCall(self): # Bail out. - self.state = 'ABORTED' + self.setState('ABORTED') def setCallID(self, callid=None): if callid: @@ -323,12 +318,11 @@ peer = protocol.transport.getPeer() remAddress = (peer.host,peer.port) port.stopListening() + self.sip.app.notifyEvent('discoveredIP', locAddress[0]) log.msg("discovered local address %r, remote %r"%(locAddress, remAddress), system='sip') _CACHED_LOCAL_IP = locAddress[0] else: - self.compDef.errback(ValueError("couldn't connect to %s"%( - host))) self.abortCall() return None pol = getPolicy() @@ -362,6 +356,7 @@ else: host,port = '','' log.msg("after NAT mapping,external address is %s:%s"%(host, port), system='sip') + self.sip.app.notifyEvent('discoveredStunnedIP', host, port) self._localIP = host self._localPort = port # XXX Check for multiple firings! @@ -404,17 +399,18 @@ if isinstance(response, CallFailed): return self.rejectedIncoming(response) else: + assert isinstance(response, basestring) return self.acceptedIncoming(response) - def acceptedIncoming(self, cookie): - log.msg("acceptIncoming setting cookie to %r"%(cookie), system='sip') - self.cookie = cookie + def acceptedIncoming(self, response): + log.msg("acceptIncoming %r"%(response), system='sip') lhost, lport = self.getLocalSIPAddress() username = self.sip.app.getPref('username') self.dialog.setContact(username, lhost, lport) othersdp = SDP(self._invite.body) - sdp = self.sip.app.getSDP(self.cookie, othersdp) + sdp = self.sip.app.getSDP() + sdp.intersect(othersdp) if not sdp.hasMediaDescriptions(): self.sendResponse(message, 406) self.setState('ABORTED') @@ -437,7 +433,6 @@ self.compDef.errback(response) def failedIncoming(self, failure): - log.msg('failedIncoming because %r'%(failure,), system='sip') log.msg('exception: %r'%(failure.value.args,), system='sip') # XXX Can I produce a more specific error than 500? self.sendResponse(self._invite, 500) @@ -457,8 +452,7 @@ print "call terminated on a", message.code log.msg("call terminated on a %s"%message.code, system="sip") d.errback(exc('%s: %s'%(message.code, message.phrase))) - self.sip.app.endCall(self.cookie, - 'other end sent\n%s'%message.toString()) + self.sip.app.endCall('other end sent\n%s'%message.toString()) def sendResponse(self, message, code, body=None): @@ -536,21 +530,17 @@ self.sendResponse(invite, 180) if self.getState() != 'ABORTED': self.setState('SENT_RINGING') - self.installTeardownTrigger() def startSendInvite(self, toAddr, init=0): #print "startSendInvite", init if init: d = self.sip.app.acceptCall(call=self) - d.addCallback(lambda x:self.sendInvite(toAddr, cookie=x, init=init) + d.addCallback(lambda x:self.sendInvite(toAddr, init=init) ).addErrback(log.err) else: self.sendInvite(toAddr, init=0) - def sendInvite(self, toAddr, cookie=None, auth=None, authhdr=None, init=0): - if cookie: - #print "sendinvite setting cookie to", cookie - self.cookie = cookie + def sendInvite(self, toAddr, auth=None, authhdr=None, init=0): lhost, lport = self.getLocalSIPAddress() username = self.sip.app.getPref('username') self.dialog.setContact(username, lhost, lport) @@ -575,7 +565,7 @@ #print auth, authhdr invite.addHeader(authhdr, auth) invite.addHeader('contact', str(self.dialog.getContact())) - s = self.sip.app.getSDP(self.cookie) + s = self.sip.app.getSDP() sdp = s.show() invite.addHeader('content-length', len(sdp)) invite.bodyDataReceived(sdp) @@ -591,18 +581,17 @@ self.setState('ABORTED') else: self.setState('SENT_INVITE') - self.installTeardownTrigger() def extractURI(self, val): name,uri,params = tpsip.parseAddress(val) return uri.toString() - def sendAck(self, okmessage, startRTP=0): + def _sendAck(self, okmessage): username = self.sip.app.getPref('username') if okmessage.code == 200: oksdp = SDP(okmessage.body) - sdp = self.sip.app.getSDP(self.cookie, oksdp) - if not sdp.hasMediaDescriptions(): + oksdp.intersect(self.sip.app.getSDP()) + if not oksdp.hasMediaDescriptions(): self.sendResponse(okmessage, 406) self.setState('ABORTED') # compDef.errback? XXX @@ -646,17 +635,15 @@ if hasattr(self, 'compDef'): cb = self.compDef.callback del self.compDef - else: - cb = lambda *args: None + addr = self._remoteURI.host, self._remoteURI.port or 5060 log.msg("sending ACK to %s %s"%addr, system='sip') - #print "sending ACK to %s %s"%addr + log.msg("sending ACK to %r\n%s"%(addr, ack.toString()), system="sip") self.sip.transport.write(ack.toString(), _hostportToIPPort(addr)) + oldstate = self.getState() self.setState('CONNECTED') - if startRTP: - self.sip.app.startCall(self.cookie, oksdp, cb) - log.msg("sending ACK to %r\n%s"%(addr, ack.toString()), system="sip") - self.sip.app.statusMessage("Call Connected") + if oldstate != 'CONNECTED': + self.sip.app.startCall(oksdp) def sendBye(self, toAddr="ignored", auth=None, authhdr=None): username = self.sip.app.getPref('username') @@ -713,18 +700,18 @@ """ self.sendResponse(message, 487) self.setState('ABORTED') - self.sip.app.endCall(self.cookie) + self.sip.app.endCall() def recvAck(self, message): ''' The remote UAC has ACKed our response to their INVITE. Start sending and receiving audio. ''' - sdp = SDP(self._invite.body) + oldstate = self.getState() self.setState('CONNECTED') - if hasattr(self, 'compDef'): - d, self.compDef = self.compDef, None - self.sip.app.startCall(self.cookie, sdp, - d.callback) + if oldstate != 'CONNECTED': + sdp = SDP(self._invite.body) + sdp.intersect(self.sip.app.getSDP()) # Not sure if this is necessary, but it can't hurt. --Zooko 2005-03-21 + self.sip.app.startCall(sdp) def recvOptions(self, message): """ Received an OPTIONS request from a remote UA. @@ -732,39 +719,22 @@ otherwise an error (XXXXX) """ - def installTeardownTrigger(self): - from twisted.internet import reactor - if 0 and self.cancel_trigger is None: - t = reactor.addSystemEventTrigger('before', - 'shutdown', - self.dropCall, - appTeardown=True) - self.cancel_trigger = t - - def dropCall(self, appTeardown=False): + def dropCall(self): '''Drop call ''' - from twisted.internet import reactor - # XXX return a deferred, and handle responses properly - if not appTeardown and self.cancel_trigger is not None: - reactor.removeSystemEventTrigger(self.cancel_trigger) state = self.getState() log.msg("dropcall in state %r"%state, system="sip") if state == 'NONE': self.sip.app.debugMessage("no call to drop") - return defer.succeed('no call to drop') elif state in ( 'CONNECTED', ): self.sendBye() self.dropDef = defer.Deferred() - return self.dropDef # XXX callLater to give up... elif state in ( 'SENT_INVITE', ): self.sendCancel() self.setState('SENT_CANCEL') - return defer.succeed('cancelled') # XXX callLater to give up... elif state in ( 'NEW', ): self.setState('ABORTED') - return defer.succeed('aborted') def _getHashingImplementation(self, algorithm): if algorithm.lower() == 'md5': @@ -831,12 +801,11 @@ self.auth_attempts = 0 if state == 'SENT_INVITE': self.sip.app.debugMessage("Got Response 200\n") - self.sendAck(message, startRTP=1) + self._sendAck(message) elif state == 'CONNECTED': self.sip.app.debugMessage('Got duplicate OK to our ACK') - #self.sendAck(message) elif state == 'SENT_BYE': - self.sip.app.endCall(self.cookie) + self.sip.app.endCall() self.sip._delCallObject(self.getCallID()) self.sip.app.debugMessage("Hung up on call %s"%self.getCallID()) self.sip.app.statusMessage("Call Disconnected") @@ -847,7 +816,7 @@ elif message.code - (message.code%100) == 400: if state == 'SENT_CANCEL' and message.code == 487: #print "cancelled" - self.sip.app.endCall(self.cookie) + self.sip.app.endCall() self.sip._delCallObject(self.getCallID()) self.sip.app.debugMessage("Hung up on call %s"%self.getCallID()) self.sip.app.statusMessage("Call Disconnected") @@ -906,7 +875,7 @@ else: log.err("Unknown state '%s' for a 401/407"%(state)) else: - if self.state == 'SENT_BYE': + if self.getState() == 'SENT_BYE': d, self.dropDef = self.dropDef, None # XXX failed to drop call. need exception here d.errback(message.code) @@ -919,7 +888,7 @@ self.sip.app.statusMessage("Call Failed: %s %s"%(message.code, message.phrase)) elif message.code - (message.code%100) == 500: - if self.state == 'SENT_BYE': + if self.getState() == 'SENT_BYE': d, self.dropDef = self.dropDef, None # XXX failed to drop call. need exception here d.errback(message.code) @@ -931,7 +900,7 @@ self.sip.app.statusMessage("Call Failed: %s %s"%(message.code, message.phrase)) elif message.code - (message.code%100) == 600: - if self.state == 'SENT_BYE': + if self.getState() == 'SENT_BYE': d, self.dropDef = self.dropDef, None # XXX failed to drop call. need exception here d.errback(message.code) @@ -939,7 +908,6 @@ if self.state == 'SENT_INVITE': self.sendAck(message) self.terminateCall(message) - #self.sip.app.endCall(self.cookie, 'Other end sent %s'%message.toString()) self.sip._delCallObject(self.getCallID()) self.sip.app.statusMessage("Call Failed: %s %s"%(message.code, message.phrase)) @@ -953,16 +921,14 @@ self.sip = phone self.regServer = None self.regAOR = None - self.state = 'NEW' self._needSTUN = False self.cseq = random.randint(1000,5000) self.dialog = Dialog() self.nonce_count = 0 - self.cancel_trigger = None self.register_attempts = 0 self._outboundProxyURI = None - def startRegistration(self): + self.state = 'NEW' self.compDef = defer.Deferred() self.regServer = Address(self.sip.app.getPref('register_uri')) self.regURI = copy.copy(self.regServer) @@ -975,7 +941,6 @@ #self.regURI.port = None d = self.setupLocalSIP(self.regServer) d.addCallback(self.sendRegistration).addErrback(log.err) - return self.compDef def sendRegistration(self, cb=None, auth=None, authhdr=None): # XXX parameterise the retransmit timer! @@ -989,7 +954,7 @@ invite.addHeader('cseq', '%s REGISTER'%self.dialog.getCSeq(incr=1)) invite.addHeader('to', str(self.regAOR)) invite.addHeader('from', str(self.regAOR)) - state = self.getState() + state = self.getState() if state in ( 'NEW', 'SENT_REGISTER', 'REGISTERED' ): invite.addHeader('expires', 900) elif state in ( 'CANCEL_REGISTER' ): @@ -1018,7 +983,6 @@ self.sendRegistration(auth=auth, authhdr=authhdr) def recvResponse(self, message): - from twisted.internet import reactor state = self.getState() if message.code in ( 401, 407 ): self.register_attempts += 1 @@ -1068,17 +1032,11 @@ log.err("Unknown registration state '%s' for a 401/407"%(state)) elif message.code in ( 200, ): self.sip.app.statusMessage("Registration: OK") - if hasattr(self.sip.app, 'registrationOK'): - self.sip.app.registrationOK(self) # Woo. registration succeeded. + self.sip.app.notifyEvent('registrationOK', self) self.register_attempts = 0 if state == 'SENT_REGISTER': self.setState('REGISTERED') - if 0 and self.cancel_trigger is None: - t = reactor.addSystemEventTrigger('before', - 'shutdown', - self.cancelRegistration) - self.cancel_trigger = t elif state == 'CANCEL_REGISTER': self.setState('UNREGISTERED') d = self._cancelDef @@ -1141,17 +1099,17 @@ d.addCallbacks(self.register, log.err) else: log.msg("no outstanding registrations, registering", system="sip") - r = Registration(self) - d = r.startRegistration() - return d + Registration(self) def _newCallObject(self, to=None, callid=None): call = Call(self, uri=to, callid=callid) - d = call.callStart() + + self.compDef = defer.Deferred() + if call.getState() != 'ABORTED': if call.getCallID(): self._calls[call.getCallID()] = call - return call, d + return call, self.compDef def updateCallObject(self, call, callid): "Used when Call setup returns a deferred result (e.g. STUN)" @@ -1171,7 +1129,7 @@ callid = callid[1:-1] del self._calls[callid] - def placeCall(self, uri, fromuri=None, cookie=None): + def placeCall(self, uri, fromuri=None): """Place a call. uri should be a string, an address of the person we are calling, @@ -1190,8 +1148,6 @@ if call is None: _d.errback(CallFailed) return _d - if cookie is not None: - call.cookie = cookie invite = call.startOutboundCall(uri, fromAddr=fromuri) return _d diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_app.py new-newfrom1406/trunk/shtoom/shtoom/test/test_app.py --- old-newfrom1406/trunk/shtoom/shtoom/test/test_app.py 2005-09-27 23:36:19.862456160 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/test/test_app.py 2005-09-27 23:36:20.524355536 -0300 @@ -7,17 +7,20 @@ class TestUI: pass -class TestAudio: +class TestPlayAudio: pass +class TestMicAudio: + def open(self, handler): + pass + class AppStartup(unittest.TestCase): def buildPhone(self): ui = TestUI() - audio = TestAudio() from shtoom.app.phone import Phone - p = Phone(ui, audio) + p = Phone(TestPlayAudio(), TestMicAudio(), ui) # Disable the NAT Mapping code for these tests p._NATMapping = False return p diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py new-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py --- old-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py 2005-09-27 23:36:19.860456464 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/test/test_callcontrol.py 2005-09-27 23:36:20.523355688 -0300 @@ -45,49 +45,39 @@ (Call.acceptedCall(), Call.rejectedCall()) """ -class TestAudio: +class TestPlayAudio: def __init__(self): self.actions = [] - def selectDefaultFormat(self, fmt): - self.actions.append('select') - if TDEBUG: print "selecting fake audio format" - - def listFormat(self): - self.actions.append('list') - return [] - - def reopen(self, mediahandler): - self.actions.append('reopen') - if TDEBUG: print "reopening fake audio" + def open(self, refillcb=None): + pass def close(self): self.actions.append('close') if TDEBUG: print "closing fake audio" - def read(self): - self.actions.append('read') - return '' - def write(self, bytes): self.actions.append('write') pass - def play_wave_file(self, file): - self.actions.append('wave') +class TestMicAudio: + def __init__(self): + self.actions = [] + + def open(self, mediahandler=None): pass + def close(self): + self.actions.append('close') + if TDEBUG: print "closing fake audio" + class TestUI: threadedUI = False - cookie = None def __init__(self, stopOnDisconnect=True): self.stopOnDisconnect = stopOnDisconnect self.actions = [] - def statusMessage(self, *args): - pass - def connectApplication(self, app): self.app = app @@ -235,8 +225,9 @@ def testOutboundLocalBye(self, loopcount=4): from shtoom.app.phone import Phone ui = TestUI() - au = TestAudio() - p = Phone(ui=ui, audio=au) + aum = TestMicAudio() + aup = TestPlayAudio() + p = Phone(aum, aup, ui) p._rtpProtocolClass = TestRTP ui.connectApplication(p) p.connectSIP = lambda x=None: None @@ -250,7 +241,8 @@ reactor.callLater(0, ui.dropCall) p.start() twisted.trial.util.wait(testdef) - self.assertEquals(au.actions, ['close', 'select', 'reopen', 'close']) + self.assertEquals(aum.actions, ['reopen', 'close']) + self.assertEquals(aup.actions, ['reopen', 'close']) self.assertEquals(TestRTP.actions, ['create', 'start', 'stop']) actions = ui.actions if TDEBUG: print actions @@ -260,7 +252,8 @@ actions = [x[0] for x in actions] self.assertEquals(actions, ['start', 'connected', 'drop', 'disconnected']) ui.actions = [] - au.actions = [] + aup.actions = [] + aum.actions = [] def testOutboundRemoteBye(self): from shtoom.app.phone import Phone @@ -279,7 +272,7 @@ reactor.callLater(0.3, lambda : p.sip.dropCall(ui.cookie)) p.start() twisted.trial.util.wait(testdef) - self.assertEquals(au.actions, ['close', 'select', 'reopen', 'close']) + self.assertEquals(au.actions, ['reopen', 'close']) self.assertEquals(TestRTP.actions, ['create', 'start', 'stop']) actions = ui.actions cookie = actions[0][1] @@ -293,7 +286,8 @@ from shtoom.app.phone import Phone ui = TestUI() au = TestAudio() - p = Phone(ui=ui, audio=au) + p = Phone(ui=ui) + p.set_audio_device(au) p._startReactor = False TestRTP.actions = [] p._rtpProtocolClass = TestRTP @@ -306,7 +300,7 @@ d.addCallback(p.sip.dropFakeInbound) p.start() twisted.trial.util.wait(testdef) - self.assertEquals(au.actions, ['wave', 'close', 'select', 'reopen', 'close']) + self.assertEquals(au.actions, ['reopen', 'close']) self.assertEquals(TestRTP.actions, ['create', 'start', 'stop']) actions = ui.actions cookie = actions[0][1] diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py new-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py --- old-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py 2005-09-27 23:36:19.837459960 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/test/test_codecs.py 2005-09-27 23:36:20.525355384 -0300 @@ -56,17 +56,17 @@ class Foo: def handle_media_sample(self, sample): ae(sample.data, 'frobozulate') - c.set_handler(Foo().handle_media_sample) + c.set_handler(Foo()) - c.handle_audio('frobozulate') + c.handle_data('frobozulate') class Foo: def handle_media_sample(self, sample): ae(sample.data, 'farnarkling') ae(sample.ct, PT_RAW) - c.set_handler(Foo().handle_media_sample) + c.set_handler(Foo()) - c.handle_audio('farnarkling') + c.handle_data('farnarkling') # XXX testing other codecs - endianness issues? crap. @@ -82,8 +82,8 @@ ae(len(sample.data), 160) ae(sample.data, ulawout) ae(sample.ct, PT_PCMU) - c.set_handler(Foo().handle_media_sample) - c.handle_audio(instr) + c.set_handler(Foo()) + c.handle_data(instr) def testGSMCodec(self): if codecs.gsm is None: @@ -98,22 +98,22 @@ ae(sample.ct, PT_GSM) p = RTPPacket(0, 0, 0, data=sample.data, ct=sample.ct) ae(len(c.decode(p)), 320) - c.set_handler(Foo().handle_media_sample) - - c.handle_audio(instr) + c.set_handler(Foo()) + c.handle_data(instr) + c = Codecker(PT_GSM) ae(c.getDefaultFormat(), PT_GSM) class Foo: def handle_media_sample(self, sample, tester=self): tester.fail("WRONG. The decoding of 32 zeroes (a short GSM frame) is required to be None, but it came out: %s" % (sample,)) - c.set_handler(Foo().handle_media_sample) + c.set_handler(Foo()) - c.handle_audio('\0'*32) + c.handle_data('\0'*32) def testSpeexCodec(self): - if codecs.gsm is None: + if codecs.speex is None: raise unittest.SkipTest("no speex support") ae = self.assertEquals c = Codecker(PT_SPEEX) @@ -125,16 +125,16 @@ ae(sample.ct, PT_SPEEX) p = RTPPacket(0, 0, 0, data=sample.data, ct=sample.ct) ae(len(c.decode(p)), 320) - c.set_handler(Foo().handle_media_sample) + c.set_handler(Foo()) - p = c.handle_audio(instr) + p = c.handle_data(instr) class Foo: def handle_media_sample(self, sample, tester=self): tester.fail("WRONG. The decoding of 30 zeroes (a short Speex frame) is required to be None, but it came out: %s" % (sample,)) - c.set_handler(Foo().handle_media_sample) + c.set_handler(Foo()) - c.handle_audio('\0'*30) + c.handle_data('\0'*30) def testMediaLayer(self): ae = self.assertEquals diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py new-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py --- old-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py 2005-09-27 23:36:19.820462544 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/test/test_rtp.py 2005-09-27 23:36:20.525355384 -0300 @@ -69,15 +69,3 @@ ae(rpack.header.ts, ts) ae(rpack.header.ssrc, ssrc) - def testSDPGen(self): - from shtoom.rtp.formats import SDPGenerator, PTMarker - from shtoom.sdp import SDP - a_ = self.assert_ - class DummyRTP: - def getVisibleAddress(self): - return ('127.0.0.1', 23456) - rtp = DummyRTP() - sdp = SDP(SDPGenerator().getSDP(rtp).show()) - rtpmap = sdp.getMediaDescription('audio').rtpmap - for pt, (entry, ptmarker) in rtpmap.items(): - a_(isinstance(ptmarker, PTMarker)) diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py new-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py --- old-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py 2005-09-27 23:36:19.819462696 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/test/test_sipauth.py 2005-09-27 23:36:20.525355384 -0300 @@ -7,11 +7,19 @@ from twisted.trial import unittest +class TestApp: + def getPref(self, prefnamethingie): + return "foo" + +class TestSip: + def __init__(self): + self.app = TestApp() + class AuthTest(unittest.TestCase): def test_sipSimpleAuth(self): from shtoom.sip import Registration - reg = Registration(None) + reg = Registration(TestSip()) resp = reg.calcAuth('REGISTER', 'sip:divmod.com', 'Digest realm="asterisk", nonce="24a52b3a"', ('anthony', 'foo')) diff -rN -u old-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py new-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py --- old-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py 2005-09-27 23:36:19.818462848 -0300 +++ new-newfrom1406/trunk/shtoom/shtoom/ui/textui/main.py 2005-09-27 23:36:20.516356752 -0300 @@ -5,11 +5,11 @@ from twisted.protocols import basic from shtoom.exceptions import CallRejected, CallNotAnswered +from shtoom.exceptions import CallRejected + class ShtoomMain(basic.LineReceiver, ShtoomBaseUI): - _cookie = None _pending = None _debug = True - _incoming_timeout = None from os import linesep as delimiter sipURL = None @@ -26,18 +26,13 @@ def shutdown(self): from twisted.internet import reactor - # XXX Hang up any calls - if self._cookie: - self.app.dropCall(self._cookie) reactor.stop() - def incomingCall(self, description, cookie): + def incomingCall(self, description): from twisted.internet import reactor if self._pending is not None: return defer.fail(UserBusy()) - defresp = defer.Deferred() - self._pending = ( cookie, defresp ) - self._incoming_timeout = reactor.callLater(20, lambda :self._timeout_incoming(self._pending)) + self._pending = defer.Deferred() self.transport.write("INCOMING CALL: %s\n"%description) self.transport.write("Type 'accept' to accept, 'reject' to reject\n") return defresp @@ -68,9 +63,6 @@ def cmd_call(self, line): "call sip:whatever -- place a new outbound call" - if self._cookie is not None: - self.transport.write("error: only one call at a time for now\n") - return args = line.split() if len(args) != 2: self.transport.write("error: call \n") @@ -82,27 +74,21 @@ deferred = self.app.placeCall(self.sipURL) deferred.addCallbacks(self.callConnected, self.callFailed).addErrback(log.err) - def callStarted(self, cookie): - self._cookie = cookie - log.msg("Call %s STARTED"%(self._cookie), system='ui') + def callStarted(self): + log.msg("Call STARTED", system='ui') - def callConnected(self, cookie): + def callConnected(self): log.msg("Call to %s CONNECTED"%(self.sipURL), system='ui') def callFailed(self, e, message=None): log.msg("Call to %s FAILED: %r"%(self.sipURL, e), system='ui') - def callDisconnected(self, cookie, message): + def callDisconnected(self, message): log.msg("Call to %s DISCONNECTED"%(self.sipURL), system='ui') - self._cookie = None def cmd_hangup(self, line): "hangup -- hangs up current call" - if self._cookie is not None: - self.app.dropCall(self._cookie) - self._cookie = None - else: - self.transport.write("error: no active call\n") + self.app.endCall() def cmd_quit(self, line): "quit -- exit the program" @@ -113,22 +99,17 @@ if not self._pending: self.transport.write("no pending calls") return - self._incoming_timeout.cancel() - self._incoming_timeout = None - self._cookie, resp = self._pending + self._pending.callback(None) self._pending = None resp.callback(self._cookie) def cmd_reject(self, line): "reject -- reject an incoming call" - self._incoming_timeout.cancel() - self._incoming_timeout = None if not self._pending: self.transport.write("no pending calls") return - cookie, resp = self._pending + self._pending.callback(CallRejected('no thanks')) self._pending = None - resp.callback(CallRejected('no thanks', cookie)) def cmd_auth(self, line): "auth realm user password -- add a user/password" @@ -154,14 +135,7 @@ continue n = float(n) reactor.callLater(initial+n*(duration+delay), - lambda k=key: self.app.startDTMF(self._cookie, k)) + lambda k=key: self.app.startDTMF(k)) reactor.callLater(initial+n*(duration+delay)+duration, - lambda k=key: self.app.stopDTMF(self._cookie, k)) + lambda k=key: self.app.stopDTMF(k)) - def _timeout_incoming(self, which): - # Not using 'which' for now - self.transport.write("CALL NOT ANSWERED\n") - self._incoming_timeout = None - cookie, resp = self._pending - self._pending = None - resp.callback(CallNotAnswered('not answering', cookie)) From zooko at zooko.com Wed Sep 28 04:53:29 2005 From: zooko at zooko.com (zooko at zooko.com) Date: Tue, 27 Sep 2005 23:53:29 -0300 Subject: [Shtoom] small patch: fix bug in aufile.py Message-ID: <20050928025329.368DDEC5@yumyum.zooko.com> HACK yumyum:~/playground/zfone-and-shtoom/shtoom/tailor1406$ darcs diff -u -paufile Tue Sep 27 23:52:50 ADT 2005 zooko at zooko.com * fix bug in aufile diff -rN -u old-tailor1406/shtoom/shtoom/audio/aufile.py new-tailor1406/shtoom/shtoom/audio/aufile.py --- old-tailor1406/shtoom/shtoom/audio/aufile.py 2005-09-27 23:53:15.490057296 -0300 +++ new-tailor1406/shtoom/shtoom/audio/aufile.py 2005-09-27 23:53:15.589042248 -0300 @@ -19,7 +19,7 @@ raise ValueError("Incorrect file format %r"%(p,)) self.comptype = p[4] if p[0] == 2: - self._cvt = lambda x,ch=p[1],c=self._cvt: tomono(c(x),ch,0.5,0.5) + self._cvt = lambda x,c=self._cvt: audiop.tomono(c(x)) elif p[0] != 1: raise ValueError("can only handle mono/stereo, not %d"%p[0]) if p[1] != 2: From zooko at zooko.com Wed Sep 28 04:57:46 2005 From: zooko at zooko.com (zooko at zooko.com) Date: Tue, 27 Sep 2005 23:57:46 -0300 Subject: [Shtoom] big patch for dev to try and others to look at In-Reply-To: <20050928023705.9E1A0EC5@yumyum.zooko.com> References: <20050928023705.9E1A0EC5@yumyum.zooko.com> Message-ID: <20050928025746.9E963EC5@yumyum.zooko.com> > I have never used or > completely understood doug, and I haven't tested it, therefore I'm sure that > this patch breaks doug even though I tried to make what appeared to be the > appropriate changes to doug as I went along. Oh, but I omitted the changes to doug from that patch. Here are my changes to doug: -------------- next part -------------- A non-text attachment was scrubbed... Name: not available Type: application/octet-stream Size: 2975 bytes Desc: doug.patch.gz URL: -------------- next part -------------- Tue Sep 27 23:55:24 ADT 2005 zooko at zooko.com * untested attempt to keep doug up to date with the big refactoring patch diff -rN -u old-tailor1406/shtoom/shtoom/app/doug.py new-tailor1406/shtoom/shtoom/app/doug.py --- old-tailor1406/shtoom/shtoom/app/doug.py 2005-09-27 23:56:17.160439200 -0300 +++ new-tailor1406/shtoom/shtoom/app/doug.py 2005-09-27 23:56:17.287419896 -0300 @@ -203,11 +203,6 @@ except IOError: pass - def outgoingRTP(self, cookie, sample): - rtp = self._rtp.get(cookie) - if rtp and sample: - rtp.handle_media_sample(sample) - def placeCall(self, cookie, nleg, sipURL, fromURI=None): ncookie = self.getCookie() nleg.setCookie(ncookie) diff -rN -u old-tailor1406/shtoom/shtoom/doug/leg.py new-tailor1406/shtoom/shtoom/doug/leg.py --- old-tailor1406/shtoom/shtoom/doug/leg.py 2005-09-27 23:56:17.158439504 -0300 +++ new-tailor1406/shtoom/shtoom/doug/leg.py 2005-09-27 23:56:17.291419288 -0300 @@ -9,11 +9,9 @@ """ from shtoom.doug.source import Source, SilenceSource, convertToSource -from shtoom.audio.converters import DougConverter from shtoom.doug.events import * from shtoom.doug.exceptions import * from twisted.python import log -from twisted.internet.task import LoopingCall class Leg(object): @@ -22,13 +20,12 @@ _acceptDeferred = None _voiceapp = None - def __init__(self, cookie, dialog, voiceapp=None): + def __init__(self, cookie, dialog): """ Create a new leg """ self._cookie = cookie self._dialog = dialog self._acceptDeferred = None - self.__converter = DougConverter() self.__playoutList = [] self.__recordDest = None self.__silenceSource = SilenceSource() @@ -37,26 +34,7 @@ self.__collectedDTMFKeys = '' self.__dtmfSingleMode = True self.__inbandDTMFdetector = None - self._voiceapp = voiceapp self._connectSource(self.__silenceSource) - self._startAudio() - - def _startAudio(self): - #print self, "starting audio" - self.LC = LoopingCall(self._get_some_audio) - self.LC.start(0.020) - - def _stopAudio(self): - if self.LC is not None: - #print self, "stopping audio", self.LC, self.LC.call - self.LC.stop() - self.LC = None - - def _get_some_audio(self): - if self._voiceapp is not None: - data = self.__connected.read() - sample = self.__converter.convertOutbound(data) - self._voiceapp.va_outgoingRTP(sample) def getDialog(self): return self._dialog @@ -122,9 +100,7 @@ self._cookie), system='doug') def hangupCall(self): - self._stopAudio() - if self._voiceapp: - self._voiceapp.va_hangupCall(self._cookie) + self._voiceapp.va_hangupCall(self._cookie) def sendDTMF(self, digits, duration=0.1, delay=0.05): self._voiceapp.sendDTMF(digits, cookie=self._cookie, @@ -196,9 +172,6 @@ dtmf, self.__collectedDTMFKeys = self.__collectedDTMFKeys, '' self._voiceapp._triggerEvent(DTMFReceivedEvent(dtmf, self)) - def selectDefaultFormat(self, ptlist): - return self.__converter.selectDefaultFormat(ptlist) - def leg_stopDTMFevent(self, dtmf): # For now, I only care about dtmf start events if dtmf == self.__currentDTMFKey: @@ -230,9 +203,6 @@ self.__inbandDTMFdetector = None # XXX handle timeout - def __repr__(self): - return ''%(id(self), self._voiceapp) - class BridgeSource(Source): "A BridgeSource connects a leg to another leg via a bridge" diff -rN -u old-tailor1406/shtoom/shtoom/doug/statemachine.py new-tailor1406/shtoom/shtoom/doug/statemachine.py --- old-tailor1406/shtoom/shtoom/doug/statemachine.py 2005-09-27 23:56:17.142441936 -0300 +++ new-tailor1406/shtoom/shtoom/doug/statemachine.py 2005-09-27 23:56:17.290419440 -0300 @@ -17,20 +17,16 @@ class StateMachine(object): - _statemachine_debug = False - def __init__(self, defer, **kwargs): self._doneDeferred = defer self._deferredState = None def returnResult(self, result): - self._cleanup() d, self._doneDeferred = self._doneDeferred, None if d: d.callback(result) def returnError(self, exc): - self._cleanup() d, self._doneDeferred = self._doneDeferred, None if d: d.errback(exc) @@ -57,8 +53,6 @@ else: log.msg("No matching event for %s in state %s"%( event.getEventName(), self.getCurrentState())) - if self._statemachine_debug: - log.msg("current transitions: %r"%(self.getCurrentEvents())) self.returnError(EventNotSpecifiedError( "No matching event for %s in state %s" %(event.getEventName(), self.getCurrentState()))) @@ -75,14 +69,6 @@ def _doState(self, callable, evt=None): self._curState = callable.__name__ - if self._statemachine_debug: - if evt is not None: - log.msg("%s switching to state %s (no event)"%( - self.__class__.__name__, self._curState)) - else: - log.msg("%s switching to state %s (%s)"%( - self.__class__.__name__, self._curState, - evt.__class__.__name__)) if evt: em = callable(evt) else: @@ -114,7 +100,3 @@ def __start__(self): raise NotImplementedError - - def _cleanup(self): - # Override in subclass if needed - pass diff -rN -u old-tailor1406/shtoom/shtoom/doug/voiceapp.py new-tailor1406/shtoom/shtoom/doug/voiceapp.py --- old-tailor1406/shtoom/shtoom/doug/voiceapp.py 2005-09-27 23:56:17.136442848 -0300 +++ new-tailor1406/shtoom/shtoom/doug/voiceapp.py 2005-09-27 23:56:17.289419592 -0300 @@ -42,15 +42,13 @@ super(VoiceApp, self).__init__(defer, **kwargs) def getDefaultLeg(self): - if self.__legs: - return self.__legs.values()[0] + return self.__legs.values()[0] def getLeg(self, cookie): return self.__legs.get(cookie) def setLeg(self, leg, cookie): self.__legs[cookie] = leg - #self.leg.hijackLeg(self) def va_selectDefaultFormat(self, ptlist, callcookie): return self.getLeg(callcookie).selectDefaultFormat(ptlist) @@ -58,9 +56,6 @@ def va_incomingRTP(self, packet, callcookie): return self.getLeg(callcookie).leg_incomingRTP(packet) - def va_outgoingRTP(self, sample): - self.__appl.outgoingRTP(self.__cookie, sample) - def va_start(self): self._start(callstart=0) @@ -87,18 +82,8 @@ system='doug') self._triggerEvent(CallRejectedEvent(leg)) - def _clear_legs(self): - from shtoom.util import stack - #print self, "clearing running legs %r"%(self.__legs.items())#,stack(8) - for name, leg in self.__legs.items(): - leg._stopAudio() - del self.__legs[name] - - _cleanup = _clear_legs - def va_abort(self): self.mediaStop() - self._clear_legs() self._triggerEvent(CallEndedEvent(None)) def mediaPlay(self, playlist, leg=None): @@ -114,8 +99,7 @@ def mediaStop(self, leg=None): if leg is None: leg = self.getDefaultLeg() - if leg is not None: - leg.mediaStop() + leg.mediaStop() def setTimer(self, delay): return Timer(self, delay) @@ -137,7 +121,7 @@ def placeCall(self, toURI, fromURI=None): from shtoom.doug.leg import Leg - nleg = Leg(cookie=None, dialog=None, voiceapp=self) + nleg = Leg(cookie=None, dialog=None) self.__appl.placeCall(self.__cookie, nleg, toURI, fromURI) def va_hangupCall(self, cookie): diff -rN -u old-tailor1406/shtoom/shtoom/test/test_doug.py new-tailor1406/shtoom/shtoom/test/test_doug.py --- old-tailor1406/shtoom/shtoom/test/test_doug.py 2005-09-27 23:56:17.125444520 -0300 +++ new-tailor1406/shtoom/shtoom/test/test_doug.py 2005-09-27 23:56:17.292419136 -0300 @@ -24,6 +24,10 @@ # ring ring pass + def notifyEvent(self, event, arg): + # Okay. + pass + def acceptErrors(self, cookie, error): #print "cookie %s got error %r"%(cookie, error) if self._trial_def is not None: @@ -43,17 +47,6 @@ #print "started!" self.returnResult('hello world') -class NullListenApp(VoiceApp): - """ This application does nothing but return """ - - def __start__(self): - #print "started!" - return ( ( CallStartedEvent, self.started), ) - - def started(self, event): - event.leg.rejectCall(CallRejected('you suck')) - self.returnResult('hello world') - class SimpleListenApp(VoiceApp): """ This application listens, connects a call, then either disconnects it, or waits for the other end to disconnect it @@ -87,7 +80,6 @@ """ This application places a call, then then either disconnects it, or waits for the other end to disconnect it """ - _statemachine_debug = True callURL = None localDisconnect = False @@ -165,9 +157,8 @@ self.assertEquals(s.val, 'hello world') app.stopSIP() - # This test is fundamentally broken. - def not_test_callAndStartup(self): - lapp = TestDougApplication(NullListenApp) + def test_callAndStartup(self): + lapp = TestDougApplication(NullApp) lapp.boot(args=['--listenport', '0']) # Now get the port number we actually listened on port = lapp.sipListener.getHost().port From zooko at zooko.com Wed Sep 28 05:00:20 2005 From: zooko at zooko.com (zooko at zooko.com) Date: Wed, 28 Sep 2005 00:00:20 -0300 Subject: [Shtoom] remove-DTMF.patch Message-ID: <20050928030020.B1775EC5@yumyum.zooko.com> A non-text attachment was scrubbed... Name: not available Type: application/octet-stream Size: 700 bytes Desc: remove-DTMF.patch.gz URL: -------------- next part -------------- Tue Sep 27 23:58:32 ADT 2005 zooko at zooko.com * remove DTMF The big refactoring broke DTMF because it changed where PT bytes were stored. This patch removes the broken left-overs of DTMF. Perhaps a patch that fixed DTMF would be preferable... diff -rN -u old-tailor1406/shtoom/shtoom/rtp/protocol.py new-tailor1406/shtoom/shtoom/rtp/protocol.py --- old-tailor1406/shtoom/shtoom/rtp/protocol.py 2005-09-27 23:59:41.127431504 -0300 +++ new-tailor1406/shtoom/shtoom/rtp/protocol.py 2005-09-27 23:59:41.255412048 -0300 @@ -312,13 +312,6 @@ ts = ts & (2**32 - 1) return ts - def startDTMF(self, digit): - self._pendingDTMF.append(NTE(digit, self.ts)) - - def stopDTMF(self, digit): - if self._pendingDTMF[-1].getKey() == digit: - self._pendingDTMF[-1].end() - def genRandom(self, bits): """Generate up to 128 bits of randomness.""" if os.path.exists("/dev/urandom"): @@ -352,13 +345,3 @@ # Wrapping if self.ts >= TWO_TO_THE_32ND: self.ts = self.ts - TWO_TO_THE_32ND - - # Now send any pending DTMF keystrokes - if self._pendingDTMF: - payload = self._pendingDTMF[0].getPayload(self.ts) - if payload: - ntept = self.ptdict.get(PT_NTE) - if ntept is not None: - self._send_packet(pt=ntept, data=payload) - if self._pendingDTMF[0].isDone(): - self._pendingDTMF = self._pendingDTMF[1:]