Skip to content

Commit

Permalink
v1.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tayler6000 committed Mar 2, 2021
1 parent 1a2f0a8 commit 812eef1
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 67 deletions.
18 changes: 10 additions & 8 deletions NOTES
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
New in v1.0.0:
Originate Calls
Added blocking feature to readAudio, this makes readAudio not return until there is data to be returned. If blocking is off and data is not available, bytes(length) will be returned.
Now properly generating SIP tags to comply with the RFC.
Other bug fixes
New in v1.5.0:
Fixed bug where pyVoIP would accept all codecs proposed by the server even if not compatible. Will now only accept PCMU, PCMA, and telephone-event.
Added handling of Native Bridging tested with Asterisk 16 SIP re-invite (External RTP bridge), this seems to still have issues with Asterisk 18, but unsure if it's my hardphone.
Changed the audio read function in RTP to return b'\x80'*length instead of bytes(length), doing so stops the popping on the client side when no audio is being written.
Fixed issue with ending phone calls originated by user.
Added handling of 404 Not Found and 503 Service Unavailable errors.
Added compatiblity with Asterisk PJSIP.
Fixed bug with multithreaded calling.

Currently Known Issues:
BYE request on originated calls causes a 500 Error on Asterisk 13 (other versions not tested). Unsure what causes this, reach out if you have a fix.
Currently does not work with PJSIP (Only tested with Asterisk 18)
Some issues with bridiging with Asterisk 18, and possible other versions. Bridging is not supported by all phones so it's unclear if it's supported by the softphone and hardphone I use to do my tests.

Upcoming patches/changes:
Adjust code to be compatible with Asterisk PJSIP.
Add support for CANCEL requests.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# pyVoIP
PyVoIP is a pure python VoIP/SIP/RTP library. Currently, it supports PCMA, PCMU, and telephone-event.

Please note this is is still in development and can only originate calls with PCMU. In future, it will be able to initiate calls in PCMA as well.
Please note this is still in development.

This library does not depend on a sound library, i.e. you can use any sound library that can handle linear sound data i.e. pyaudio or even wave. Keep in mind PCMU only supports 8000Hz, 1 channel, 8 bit, audio.
This library does not depend on a sound library, i.e. you can use any sound library that can handle linear sound data i.e. pyaudio or even wave. Keep in mind PCMU/PCMA only supports 8000Hz, 1 channel, 8 bit, audio.

## Getting Started
Simply put pyVoIP into your site-packages folder.
Expand Down
14 changes: 6 additions & 8 deletions docs/Examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ The following will create a phone that answers and automatically hangs up:
except InvalidStateError:
pass
if __name__=='__main__':
phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computer's local IP>)
if __name__ == '__main__':
phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computer's local IP>)
phone.start()
input('Press enter to disable the phone')
phone.stop()
Expand All @@ -37,13 +37,12 @@ Let's say you want to make a phone that when you call it, it plays an announceme
.. code-block:: python
from pyVoIP.VoIP import VoIPPhone, InvalidStateError, CallState
import audioop
import time
import wave
def answer(call):
try:
f=wave.open('announcment.wav', 'rb')
f = wave.open('announcment.wav', 'rb')
frames = f.getnframes()
data = f.readframes(frames)
f.close()
Expand All @@ -61,7 +60,7 @@ Let's say you want to make a phone that when you call it, it plays an announceme
except:
call.hangup()
if __name__=='__main__':
if __name__ == '__main__':
phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computers local IP>)
phone.start()
input('Press enter to disable the phone')
Expand All @@ -88,13 +87,12 @@ We can use the above code to create `IVR Menus <https://en.wikipedia.org/wiki/In
.. code-block:: python
from pyVoIP.VoIP import VoIPPhone, InvalidStateError, CallState
import audioop
import time
import wave
def answer(call):
try:
f=wave.open('prompt.wav', 'rb')
f = wave.open('prompt.wav', 'rb')
frames = f.getnframes()
data = f.readframes(frames)
f.close()
Expand All @@ -116,7 +114,7 @@ We can use the above code to create `IVR Menus <https://en.wikipedia.org/wiki/In
except:
call.hangup()
if __name__=='__main__':
if __name__ == '__main__':
phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computers local IP>)
phone.start()
input('Press enter to disable the phone')
Expand Down
23 changes: 17 additions & 6 deletions pyVoIP/RTP.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import time

__all__ = ['add_bytes', 'byte_to_bits', 'DynamicPayloadType', 'PayloadType', 'RTPParseError', 'RTPProtocol', 'RTPPacketManager', 'RTPClient', 'TransmitType']
debug = pyVoIP.debug

def byte_to_bits(byte):
byte = bin(ord(byte)).lstrip('-0b')
Expand Down Expand Up @@ -94,6 +95,7 @@ def __str__(self):

#Non-codec
EVENT = "telephone-event", 8000, 0, "telephone-event"
UNKOWN = "UNKOWN", 0, 0, "UNKOWN CODEC"

class RTPPacketManager():
def __init__(self):
Expand All @@ -109,7 +111,7 @@ def read(self, length=160):
self.bufferLock.acquire()
packet = self.buffer.read(length)
if len(packet)<length:
packet = packet + bytes(length-len(packet))
packet = packet + (b'\x80' * (length-len(packet)))
self.bufferLock.release()
return packet

Expand Down Expand Up @@ -167,7 +169,7 @@ def parse(self, packet):
byte = byte_to_bits(packet[0:1])
self.version = int(byte[0:2], 2)
if not self.version in self.RTPCompatibleVersions:
raise RTPParseError("RTP Version {} not compatible.".format(version))
raise RTPParseError("RTP Version {} not compatible.".format(self.version))
self.padding = bool(int(byte[2], 2))
self.extension = bool(int(byte[3], 2))
self.CC = int(byte[4:], 2)
Expand Down Expand Up @@ -208,7 +210,16 @@ class RTPClient():
def __init__(self, assoc, inIP, inPort, outIP, outPort, sendrecv, dtmf = None):
self.NSD = True
self.assoc = assoc # Example: {0: PayloadType.PCMU, 101: PayloadType.EVENT}
self.preference = PayloadType.PCMU
debug("Selecting audio codec for transmission")
for m in assoc:
try:
if int(assoc[m]) is not None:
debug(f"Selected {assoc[m]}")
self.preference = assoc[m] #Select the first available actual codec to encode with. TODO: will need to change if video codecs are ever implemented.
break
except:
debug(f"{assoc[m]} cannot be selected as an audio codec")

self.inIP = inIP
self.inPort = inPort
self.outIP = outIP
Expand Down Expand Up @@ -246,7 +257,7 @@ def read(self, length=160, blocking=True):
if not blocking:
return self.pmin.read(length)
packet = self.pmin.read(length)
while packet == bytes(length) and self.NSD:
while packet == (b'\x80'*length) and self.NSD:
time.sleep(0.01)
packet = self.pmin.read(length)
return packet
Expand All @@ -263,7 +274,7 @@ def recv(self):
except BlockingIOError:
time.sleep(0.01)
except RTPParseError as e:
print(str(e))
debug(str(e))
except OSError:
pass

Expand All @@ -284,7 +295,7 @@ def trans(self):
packet += self.outSSRC.to_bytes(4, byteorder='big')
packet += payload

#print(payload)
#debug(payload)

try:
self.sout.sendto(packet, (self.outIP, self.outPort))
Expand Down
69 changes: 45 additions & 24 deletions pyVoIP/SIP.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import time

__all__ = ['Counter', 'InvalidAccountInfoError', 'SIPClient', 'SIPMessage', 'SIPMessageType', 'SIPParseError', 'SIPStatus']
debug = pyVoIP.debug

class InvalidAccountInfoError(Exception):
pass
Expand Down Expand Up @@ -232,8 +233,12 @@ def parseHeader(self, header, data):
contact = raw.split('<sip:')
contact[0] = contact[0].strip('"').strip("'")
address = contact[1].strip('>')
number = address.split('@')[0]
host = address.split('@')[1]
if len(address.split('@')) == 2:
number = address.split('@')[0]
host = address.split('@')[1]
else:
number = None
host = address

self.headers[header] = {'raw': raw, 'tag': tag, 'address': address, 'number': number, 'caller': contact[0], 'host': host}
elif header=="CSeq":
Expand All @@ -243,9 +248,11 @@ def parseHeader(self, header, data):
elif header=="Content-Length":
self.headers[header] = int(data)
elif header=='WWW-Authenticate' or header=="Authorization":
info = data.split(", ")
data = data.replace("Digest", "")
info = data.split(",")
header_data = {}
for x in info:
x = x.strip()
header_data[x.split('=')[0]] = x.split('=')[1].strip('"')
self.headers[header] = header_data
self.authentication = header_data
Expand Down Expand Up @@ -499,15 +506,15 @@ def __init__(self, server, port, username, password, myIP=None, myPort=5060, cal

self.registerThread = None
self.recvLock = Lock()


def recv(self):
while self.NSD:
self.recvLock.acquire()
self.s.setblocking(False)
try:
message = SIPMessage(self.s.recv(8192))
#print(message.summary())
raw = self.s.recv(8192)
message = SIPMessage(raw)
debug(message.summary())
self.parseMessage(message)
except BlockingIOError:
self.s.setblocking(True)
Expand All @@ -519,9 +526,13 @@ def recv(self):
request = self.genSIPVersionNotSupported(message)
self.out.sendto(request.encode('utf8'), (self.server, self.port))
else:
print(str(e))
#except Exception as e:
#print("SIP recv error: "+str(e))
debug(f"SIPParseError in SIP.recv: {type(e)}, {e}")
except Exception as e:
debug(f"SIP.recv error: {type(e)}, {e}\n\n{str(raw, 'utf8')}")
if pyVoIP.DEBUG:
self.s.setblocking(True)
self.recvLock.release()
raise
self.s.setblocking(True)
self.recvLock.release()

Expand All @@ -530,17 +541,23 @@ def parseMessage(self, message):
if message.status == SIPStatus.OK:
if self.callCallback != None:
self.callCallback(message)
elif message.status == SIPStatus.NOT_FOUND:
if self.callCallback != None:
self.callCallback(message)
elif message.status == SIPStatus.SERVICE_UNAVAILABLE:
if self.callCallback != None:
self.callCallback(message)
elif message.status == SIPStatus.TRYING or message.status == SIPStatus.RINGING:
pass
else:
print("TODO: Add 500 Error on Receiving SIP Response")#:\r\n"+message.summary())
debug("TODO: Add 500 Error on Receiving SIP Response:\r\n"+message.summary(), "TODO: Add 500 Error on Receiving SIP Response")
self.s.setblocking(True)
return
elif message.method == "INVITE":
if self.callCallback == None:
request = self.genBusy(message)
self.out.sendto(request.encode('utf8'), (self.server, self.port))
else:
request = self.genRinging(message)
self.out.sendto(request.encode('utf8'), (self.server, self.port))
self.callCallback(message)
elif message.method == "BYE":
self.callCallback(message)
Expand All @@ -549,12 +566,13 @@ def parseMessage(self, message):
elif message.method == "ACK":
return
else:
print("TODO: Add 400 Error on non processable request")
debug("TODO: Add 400 Error on non processable request")

def start(self):
self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#self.out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.bind((self.myIP, self.myPort))
self.out = self.s
register = self.register()
t = Timer(1, self.recv)
t.name = "SIP Recieve"
Expand Down Expand Up @@ -668,7 +686,7 @@ def genRinging(self, request):
def genAnswer(self, request, sess_id, ms, sendtype):
#Generate body first for content length
body = "v=0\r\n"
body += "o=pyVoIP "+sess_id+" "+sess_id+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "o=pyVoIP "+sess_id+" "+str(int(sess_id)+2)+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "s=pyVoIP """+pyVoIP.__version__+"\r\n"
body += "c=IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "t=0 0\r\n"
Expand Down Expand Up @@ -706,7 +724,7 @@ def genAnswer(self, request, sess_id, ms, sendtype):
def genInvite(self, number, sess_id, ms, sendtype, branch, call_id):
#Generate body first for content length
body = "v=0\r\n"
body += "o=pyVoIP "+sess_id+" "+sess_id+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "o=pyVoIP "+sess_id+" "+str(int(sess_id)+2)+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "s=pyVoIP """+pyVoIP.__version__+"\r\n"
body += "c=IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6
body += "t=0 0\r\n"
Expand Down Expand Up @@ -754,7 +772,7 @@ def genBye(self, request):
byeRequest += "To: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n"
byeRequest += "From: "+request.headers['To']['raw']+";tag="+tag+"\r\n"
byeRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n"
byeRequest += "CSeq: "+str(self.byeCounter.next())+" BYE\r\n"
byeRequest += "CSeq: "+str(int(request.headers['CSeq']['check'])+1)+" BYE\r\n"
byeRequest += "Contact: <sip:"+self.username+"@"+self.myIP+":"+str(self.myPort)+">\r\n"
byeRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n"
byeRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n"
Expand Down Expand Up @@ -785,22 +803,21 @@ def invite(self, number, ms, sendtype):
self.recvLock.acquire()
#print("Locked")
self.out.sendto(invite.encode('utf8'), (self.server, self.port))
#print('Invited')
debug('Invited')
response = SIPMessage(self.s.recv(8192))

while response.status != SIPStatus(401) and response.status != SIPStatus(100) and response.status != SIPStatus(180):
while (response.status != SIPStatus(401) and response.status != SIPStatus(100) and response.status != SIPStatus(180)) or response.headers['Call-ID'] != call_id:
if not self.NSD:
break
if response.status == SIPStatus(100) or response.status == SIPStatus(180):
return SIPMessage(invite.encode('utf8')), call_id, sess_id
self.parseMessage(response)
response = SIPMessage(self.s.recv(8192))

#print("Received Response: "+response.summary())

if response.status == SIPStatus(100) or response.status == SIPStatus(180):
return SIPMessage(invite.encode('utf8')), call_id, sess_id
debug("Received Response: "+response.summary())
ack = self.genAck(response)
self.out.sendto(ack.encode('utf8'), (self.server, self.port))
#print("Acknowledged")
debug("Acknowledged")
authhash = self.genAuthorization(response)
nonce = response.authentication['nonce']
auth = 'Authorization: Digest username="'+self.username
Expand All @@ -815,6 +832,7 @@ def invite(self, number, ms, sendtype):
self.out.sendto(invite.encode('utf8'), (self.server, self.port))

self.recvLock.release()
#print("Released")
return SIPMessage(invite.encode('utf8')), call_id, sess_id

def bye(self, request):
Expand Down Expand Up @@ -869,6 +887,9 @@ def register(self):
else:
self.parseMessage(response)

#debug(response.summary())
#debug(response.raw)

regRequest = self.genRegister(response)

self.out.sendto(regRequest.encode('utf8'), (self.server, self.port))
Expand Down
Loading

0 comments on commit 812eef1

Please sign in to comment.