Of course! Building a Python SIP client is a common task for VoIP applications, softphones, or integrating with telephony systems. The most popular and robust library for this is pjsip.
Here's a comprehensive guide covering:
- Why
pjsip? (The best choice) - Installation (The tricky part)
- A Simple SIP Client Example (Making a call)
- An Advanced Example (Handling incoming calls and media)
- Alternative Libraries
Why pjsip?
While Python has other SIP-related libraries (like py SIP), pjsip is the industry standard for several reasons:
- Full-featured: It implements the entire SIP RFCs, not just a subset. It handles SIP, SDP (for media negotiation), RTP (for audio/video transport), and more.
- Stable & Mature: It's a battle-tested C library with a Python wrapper (
pjsua2). It's used in production systems worldwide. - Media Support: It has built-in support for audio codecs (like G.711, Opus) and can interface with sound cards (PortAudio, ALSA, CoreAudio) to capture and play audio.
- Pythonic Wrapper: The
pjsua2wrapper provides a clean, object-oriented Python interface to the underlying C library.
Installation (The Most Important Step)
This is the most challenging part. pjsip is not a simple pip install. You need to compile the C library and its Python bindings.
Prerequisites
You'll need a C/C++ compiler and build tools:
- On Ubuntu/Debian:
sudo apt-get update sudo apt-get install -y python3-dev python3-pip build-essential portaudio19-dev libssl-dev libasound2-dev
- On macOS (using Homebrew):
brew install portaudio pjsip
Note: Homebrew has a
pjsippackage, but it might not always have the Python bindings enabled. Compiling from source (as below) is more reliable for development.
Recommended Method: Compile from Source
This gives you the most control and ensures compatibility.
-
Install Dependencies: Use the commands from the "Prerequisites" section above for your OS.
-
Clone and Compile
pjsip:# Clone the repository git clone https://github.com/pjsip/pjproject.git cd pjproject # Configure the build. The --with-python flag is crucial. # The --enable-shared flag is often needed for the Python module to load correctly. ./configure --with-python --enable-shared # Compile and install make dep make sudo make install
-
Install the Python Package: The compilation above creates the Python bindings. Now, you just need to
pip installthem. Thesetup.pyscript will find the compiled library automatically.pip install pjsua2
If you have multiple Python versions, you might need
pip3 install pjsua2.
You can verify the installation by running:
import pjsua2 as pj print(pj.__version__)
A Simple SIP Client Example (Making an Outgoing Call)
This example demonstrates the most basic functionality: initializing the library, registering with a SIP server, and making a call.
Code: simple_client.py
import pjsua2 as pj
import time
import threading
# --- Configuration ---
SIP_ACCOUNT = "sip:1001@sip.example.com" # Your SIP account URI
SIP_PASSWORD = "your_password"
SIP_SERVER = "sip.example.com" # Your SIP server's domain or IP
AUDIO_DEV = None # Use default audio device
# --- Global objects ---
ep = None # Endpoint object
call = None # Call object
# --- Custom Call Class ---
class MyCall(pj.Call):
"""
Custom Call class to handle call events.
"""
def __init__(self, account, call_id):
super().__init__(account, call_id)
print("Call created")
def onCallState(self, prm):
print("Call state:", prm.state)
if prm.state == pj.PJSIP_INV_STATE_DISCONNECTED:
print("Call disconnected")
global call
call = None
def onCallMediaState(self, prm):
print("Call media state")
# When media is established, connect the call to the sound device
if self.getMediaStatus() == pj.PJSIP_INV_STATE_CONFIRMED:
self.connectAudio()
# --- Custom Account Class ---
class MyAccount(pj.Account):
"""
Custom Account class to handle account events.
"""
def __init__(self, ep):
super().__init__()
self.ep = ep
self.call = None
def onRegState(self, prm):
print("Registration state:", prm.code, prm.reason)
if prm.code == 200:
print("Successfully registered to", SIP_ACCOUNT)
def makeCall(self, dest_uri):
"""Helper to make a call."""
global call
call = MyCall(self, -1)
call.makeCall(dest_uri, pj.CallOpParam())
print("Calling", dest_uri)
# --- Main Application Logic ---
def main():
global ep, call
# 1. Create and initialize the library
ep = pj.Endpoint()
ep.libCreate()
ep.libInit(pj.EpConfig())
# 2. Configure transport (SIP signaling)
transport_config = pj.TransportConfig()
transport_config.port = 5060
transport_config.publicAddress = "YOUR_PUBLIC_IP_OR_HOSTNAME"
transport = ep.transportCreate(pj.PJSIP_TRANSPORT_UDP, transport_config)
# 3. Configure audio device
aud_dev_manager = ep.getAudioDevManager()
aud_dev_manager.setNullDev() # Start with no device
if AUDIO_DEV is not None:
# You can iterate and select a specific device if needed
pass
# 4. Start the library
ep.libStart()
print("PJSUA2 started")
# 5. Create and register the account
acc = MyAccount(ep)
acc_cfg = pj.AccountConfig()
acc_cfg.idUri = SIP_ACCOUNT
acc_cfg.regConfig.registrarUri = "sip:" + SIP_SERVER
acc_cfg.sipConfig.authCreds.append(pj.AuthCred("digest", "*", SIP_PASSWORD))
acc.create(acc_cfg)
# Wait for registration
time.sleep(2)
# 6. Make a call
dest_uri = "sip:1002@sip.example.com" # The number/URI you want to call
acc.makeCall(dest_uri)
# 7. Run the main loop until the call is finished
try:
while call is not None:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down...")
# 8. Clean up
if call:
call.hangup()
if acc:
acc.delete()
ep.libDestroy()
print("PJSUA2 destroyed")
if __name__ == "__main__":
main()
How to Run:
- Fill in your
SIP_ACCOUNT,SIP_PASSWORD, andSIP_SERVER. - Replace
YOUR_PUBLIC_IP_OR_HOSTNAMEwith the public IP of the machine running the script. This is crucial for NAT traversal. - Run the script:
python simple_client.py - It will register, make the call, and then wait. Press
Ctrl+Cto hang up and exit.
Advanced Example (Handling Incoming Calls & Media)
This example builds on the previous one by showing how to handle incoming calls and demonstrates media handling.
Key additions:
- A
MyCallclass that handles incoming call requests (onIncomingCall). - A custom
AudioMediaclass to demonstrate capturing and playing audio.
Code: advanced_client.py
import pjsua2 as pj
import time
import threading
# --- Configuration (same as before) ---
SIP_ACCOUNT = "sip:1001@sip.example.com"
SIP_PASSWORD = "your_password"
SIP_SERVER = "sip.example.com"
AUDIO_DEV = None
# --- Global objects ---
ep = None
incoming_call = None
# --- Custom Call Class ---
class MyCall(pj.Call):
def __init__(self, account, call_id):
super().__init__(account, call_id)
self.tx_dev = None
self.rx_dev = None
def onCallState(self, prm):
print("Call state:", prm.state)
if prm.state == pj.PJSIP_INV_STATE_DISCONNECTED:
print("Call disconnected")
global incoming_call
incoming_call = None
if self.tx_dev:
self.tx_dev.delete()
if self.rx_dev:
self.rx_dev.delete()
def onCallMediaState(self, prm):
print("Call media state")
if self.getMediaStatus() == pj.PJSIP_INV_STATE_CONFIRMED:
# Get the default audio device
aud_dev_manager = ep.getAudioDevManager()
cap_dev = aud_dev_manager.getCaptureDev()
play_dev = aud_dev_manager.getPlaybackDev()
# Create media ports and connect them
self.tx_dev = pj.AudioMedia.createMedia(ep)
self.rx_dev = pj.AudioMedia.createMedia(ep)
self.tx_dev.startTransmit(self.getStream(pj.PJMEDIA_TYPE_AUDIO).getTransmitter(pj.PJMEDIA_DIR_CAPTURE))
play_dev.startTransmit(self.rx_dev)
self.getStream(pj.PJMEDIA_TYPE_AUDIO).getReceiver(pj.PJMEDIA_DIR_PLAYBACK).startTransmit(self.tx_dev)
print("Audio streams connected.")
def onIncomingCall(self, prm):
print("Incoming call from:", prm.remoteUri)
global incoming_call
incoming_call = MyCall(self, prm.callId)
incoming_call.answer(pj.CallOpParam())
# --- Main Application Logic ---
def main():
global ep, incoming_call
# 1. Create and initialize the library
ep = pj.Endpoint()
ep.libCreate()
ep.libInit(pj.EpConfig())
# 2. Configure transport
transport_config = pj.TransportConfig()
transport_config.port = 5060
transport_config.publicAddress = "YOUR_PUBLIC_IP_OR_HOSTNAME"
ep.transportCreate(pj.PJSIP_TRANSPORT_UDP, transport_config)
# 3. Configure audio device
ep.audDevManager().setNullDev()
# 4. Start the library
ep.libStart()
print("PJSUA2 started. Waiting for calls...")
# 5. Create and register the account
acc = pj.Account()
acc_cfg = pj.AccountConfig()
acc_cfg.idUri = SIP_ACCOUNT
acc_cfg.regConfig.registrarUri = "sip:" + SIP_SERVER
acc_cfg.sipConfig.authCreds.append(pj.AuthCred("digest", "*", SIP_PASSWORD))
acc.create(acc_cfg)
time.sleep(2)
# 6. Run the main loop
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down...")
# 7. Clean up
if incoming_call:
incoming_call.hangup()
acc.delete()
ep.libDestroy()
print("PJSUA2 destroyed")
if __name__ == "__main__":
main()
Alternative Libraries
While pjsua2 is the most powerful, it's heavy. For simpler use cases, consider these:
| Library | Description | Pros | Cons |
|---|---|---|---|
py SIP |
A pure Python SIP library. | Easy to install (pip install pysip), lightweight, pure Python. |
Does not handle media (RTP). You'd need another library for audio. Less stable for complex scenarios. |
sipsimple |
A higher-level Python framework for SIP. | More abstract than pjsua2, focuses on application logic. Handles some media. |
Can be complex to set up. The project seems less active than pjsip. |
async-pj |
An async/await wrapper for pjsua2. |
Allows pjsua2 to be used in modern async applications (e.g., FastAPI, asyncio). |
Requires understanding of both pjsua2 and asyncio. |
Summary
| Task | Recommended Tool |
|---|---|
| Building a full-featured softphone | pjsua2 |
| Simple SIP signaling (no media) | py SIP |
| Integrating SIP into an async app | async-pj or pjsua2 in a separate thread |
For any serious project, investing the time to learn and use pjsua2 is the best path forward. Its power and stability are unmatched in the Python ecosystem for SIP.
