Skip to content

Commit 4d1be3e

Browse files
committed
feat: Add advanced_call.py example for power users
Demonstrates: - Custom caller ID and display name - Custom SIP headers (multiple) - Codec preferences - Connection timeout - Custom User-Agent - Event callbacks (ringing, answered, hangup)
1 parent 1f89d5e commit 4d1be3e

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

examples/advanced_call.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Advanced Outbound Call Example
4+
5+
Demonstrates power-user features:
6+
- Custom caller ID and display name
7+
- Custom SIP headers
8+
- Codec preferences
9+
- Event callbacks (ringing, answered, hangup)
10+
- Connection timeout
11+
12+
Usage:
13+
python advanced_call.py --to 1234567890 --user alice --pass secret --server sip.example.com
14+
python advanced_call.py --to 1234567890 --caller-id "sip:support@company.com" --display-name "Support"
15+
"""
16+
17+
import argparse
18+
import asyncio
19+
import os
20+
import sys
21+
22+
# Add parent directory to path for development
23+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24+
25+
from dotenv import load_dotenv
26+
from PySIP import SIPClient
27+
from PySIP.exceptions import (
28+
CallFailedError,
29+
CallRejectedError,
30+
CallTimeoutError,
31+
RegistrationError,
32+
)
33+
34+
# Load environment variables
35+
load_dotenv()
36+
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))
37+
38+
39+
def get_args():
40+
"""Parse command line arguments."""
41+
parser = argparse.ArgumentParser(
42+
description="Advanced outbound call with custom configuration",
43+
formatter_class=argparse.RawDescriptionHelpFormatter,
44+
epilog="""
45+
Examples:
46+
# Basic call with custom caller ID
47+
python advanced_call.py --to 123 --caller-id "sip:sales@company.com"
48+
49+
# With display name and custom headers
50+
python advanced_call.py --to 123 --display-name "Sales Team" --header "X-Campaign:promo2024"
51+
52+
# Multiple custom headers
53+
python advanced_call.py --to 123 --header "X-Account:12345" --header "X-Priority:high"
54+
"""
55+
)
56+
57+
# Required
58+
parser.add_argument('--to', '-t', default=os.getenv('TEST_NUMBER'),
59+
help='Destination number')
60+
parser.add_argument('--user', '-u', default=os.getenv('SIP_USERNAME'),
61+
help='SIP username')
62+
parser.add_argument('--pass', '-p', dest='password', default=os.getenv('SIP_PASSWORD'),
63+
help='SIP password')
64+
parser.add_argument('--server', '-s', default=os.getenv('SIP_SERVER'),
65+
help='SIP server')
66+
parser.add_argument('--port', type=int, default=int(os.getenv('SIP_PORT', '5060')),
67+
help='SIP port')
68+
69+
# Advanced options
70+
parser.add_argument('--caller-id', default=None,
71+
help='Custom caller ID (SIP URI)')
72+
parser.add_argument('--display-name', default=None,
73+
help='Display name shown on recipient phone')
74+
parser.add_argument('--header', action='append', default=[],
75+
help='Custom SIP header (format: "Name:Value"). Can be used multiple times.')
76+
parser.add_argument('--codecs', default='pcmu,pcma',
77+
help='Comma-separated codec preference (default: pcmu,pcma)')
78+
parser.add_argument('--timeout', type=int, default=30,
79+
help='Connection timeout in seconds (default: 30)')
80+
parser.add_argument('--user-agent', default=None,
81+
help='Custom User-Agent header')
82+
parser.add_argument('--message', '-m',
83+
default="Hello! This call was made using advanced PySIP features. Goodbye!",
84+
help='TTS message to play')
85+
86+
return parser.parse_args()
87+
88+
89+
def on_ringing():
90+
"""Called when the remote party is ringing."""
91+
print(" 📞 Remote party is ringing...")
92+
93+
94+
def on_answered():
95+
"""Called when the call is answered."""
96+
print(" ✅ Call answered!")
97+
98+
99+
def on_hangup():
100+
"""Called when the call ends."""
101+
print(" 📴 Call ended")
102+
103+
104+
async def main():
105+
args = get_args()
106+
107+
# Validate required arguments
108+
missing = []
109+
if not args.user:
110+
missing.append('--user or SIP_USERNAME')
111+
if not args.password:
112+
missing.append('--pass or SIP_PASSWORD')
113+
if not args.server:
114+
missing.append('--server or SIP_SERVER')
115+
if not args.to:
116+
missing.append('--to or TEST_NUMBER')
117+
118+
if missing:
119+
print("Error: Missing required arguments:")
120+
for m in missing:
121+
print(f" - {m}")
122+
sys.exit(1)
123+
124+
# Parse custom headers
125+
custom_headers = {}
126+
for header in args.header:
127+
if ':' in header:
128+
name, value = header.split(':', 1)
129+
custom_headers[name.strip()] = value.strip()
130+
else:
131+
print(f"Warning: Invalid header format '{header}', expected 'Name:Value'")
132+
133+
# Parse codecs
134+
codecs = [c.strip() for c in args.codecs.split(',')]
135+
136+
print("Advanced Call Demo")
137+
print("==================")
138+
print(f"Server: {args.server}:{args.port}")
139+
print(f"Destination: {args.to}")
140+
print()
141+
print("Configuration:")
142+
if args.caller_id:
143+
print(f" Caller ID: {args.caller_id}")
144+
if args.display_name:
145+
print(f" Display Name: {args.display_name}")
146+
if custom_headers:
147+
print(f" Custom Headers: {custom_headers}")
148+
print(f" Codecs: {codecs}")
149+
print(f" Timeout: {args.timeout}s")
150+
if args.user_agent:
151+
print(f" User-Agent: {args.user_agent}")
152+
print()
153+
154+
async with SIPClient(
155+
username=args.user,
156+
password=args.password,
157+
server=args.server,
158+
port=args.port,
159+
) as client:
160+
print("Registering with SIP server...")
161+
162+
try:
163+
await client.register()
164+
print("Registration successful!")
165+
except RegistrationError as e:
166+
print(f"Registration failed: {e}")
167+
sys.exit(1)
168+
169+
print(f"\nCreating call to {args.to}...")
170+
171+
try:
172+
# Create unconfigured call
173+
call = client.create_call(args.to)
174+
175+
# Configure caller identity
176+
if args.caller_id:
177+
call.set_caller_id(args.caller_id)
178+
if args.display_name:
179+
call.set_display_name(args.display_name)
180+
181+
# Add custom SIP headers
182+
for name, value in custom_headers.items():
183+
call.add_header(name, value)
184+
185+
# Set codec preferences
186+
call.set_codecs(codecs)
187+
188+
# Set timeout
189+
call.set_timeout(args.timeout)
190+
191+
# Override user agent if specified
192+
if args.user_agent:
193+
call.set_user_agent(args.user_agent)
194+
195+
# Register event callbacks
196+
call.on("ringing", on_ringing)
197+
call.on("answered", on_answered)
198+
call.on("hangup", on_hangup)
199+
200+
# Now connect
201+
print("\nConnecting...")
202+
await call.connect()
203+
204+
print(f"Call-ID: {call.call_id}")
205+
206+
# Play message
207+
print("\nPlaying message...")
208+
await call.say(args.message)
209+
210+
# Hang up
211+
print("\nHanging up...")
212+
await call.hangup()
213+
214+
print(f"\nCall duration: {call.duration:.1f} seconds")
215+
216+
except CallTimeoutError:
217+
print("Call failed: No answer (timeout)")
218+
except CallRejectedError as e:
219+
print(f"Call rejected: {e.status_code} {e.reason}")
220+
except CallFailedError as e:
221+
print(f"Call failed: {e}")
222+
223+
224+
if __name__ == "__main__":
225+
try:
226+
asyncio.run(main())
227+
except KeyboardInterrupt:
228+
print("\nInterrupted by user")
229+

0 commit comments

Comments
 (0)