I am trying to build a real-time voice assistant using Asterisk 22 and the Asterisk REST Interface (ARI). My goal is to stream live call audio to a Python WebSocket server for speech recognition using the externalMedia
feature.
The Problem:
My Python ARI application successfully sends the POST /ari/channels/externalMedia
command. The Asterisk REST API returns a “200 OK” status, and the Python logs show that the bridge and media channel are created successfully. However, Asterisk never attempts to connect to my WebSocket server running on ws://127.0.0.1:8089
. The handler in my Python script which should be triggered by a new WebSocket connection is never called.
My Environment:
- Asterisk Version: 22.4.1
- OS: Ubuntu
- ARI Client: Python with
requests
andwebsockets
libraries.
What I Have Verified:
- WebSocket Server is Working: I have written a separate, simple Python WebSocket client and successfully connected to the server on port 8089 while my main ARI application is running. This proves the server is running correctly.
- No Firewall Issue: The successful connection test from a separate client on the same machine confirms that there is no local firewall (like
ufw
) blocking port 8089. - Clean Bridge/Channel Logic: My Python code first creates a mixing bridge, adds the main call channel, and then creates the
externalMedia
channel while specifying thebridgeId
during creation. The Asterisk logs show all these steps completing without any errors.
The Core Question:
Given that all ARI commands succeed and there are no network/firewall issues, what could be preventing Asterisk from physically initiating the WebSocket client connection to the external_host
? Is there a known issue, a missing module (format_slin
, etc.), or a subtle PJSIP configuration that could cause this “silent failure” in Asterisk 22?
Below are my cleaned-up configuration files and the final “successful-looking but not working” Asterisk log. Any help or ideas would be greatly appreciated.
def make_call(self, number):
data = {
"endpoint": f"PJSIP/{number}@netgsm_trunk",
"context": "outbound-netgsm",
"extension": number,
"priority": 1,
"callerId": "xxxxxx",
"timeout": 30,
"app": "netgsm-realtime",
"appArgs": number
}
try:
response = self.session.post(f"{ARI_URL}/channels", json=data)
if response.status_code == 200:
channel_info = response.json()
channel_id = channel_info.get('id')
return channel_id
else:
print(f"❌ Call Error: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"❌ Call Error: {e}")
return None
def start_speech_recognition(self, channel_id):
print(f"🎙️ Starting speech recognition for {channel_id}")
try:
bridge_response = self.session.post(f"{ARI_URL}/bridges", json={"type": "mixing"})
bridge_response.raise_for_status()
bridge_id = bridge_response.json().get('id')
print(f"✅ Bridge created: {bridge_id}")
self.session.post(f"{ARI_URL}/bridges/{bridge_id}/addChannel", json={"channel": channel_id})
print(f"✅ Main channel ({channel_id}) added to bridge.")
external_host = "127.0.0.1:8089"
external_media_response = self.session.post(
f"{ARI_URL}/channels/externalMedia",
json={
"app": "netgsm-realtime",
"bridgeId": bridge_id,
"external_host": external_host,
"format": "slin"
}
)
external_media_response.raise_for_status()
external_channel_id = external_media_response.json().get('id')
print(f"✅ External Media channel created ({external_channel_id}) and added to bridge.")
self.active_calls[channel_id] = {
'start_time': datetime.now(),
'is_recognizing': True,
'transcript_file': f"/tmp/call_transcript_{channel_id}.txt",
'external_channel_id': external_channel_id,
'bridge_id': bridge_id
}
print(f"✅ Speech recognition started successfully: {channel_id}")
except requests.exceptions.HTTPError as e:
print(f"❌ HTTP Error (start_speech_recognition): {e.response.status_code} - {e.response.text}")
except Exception as e:
print(f"❌ General error while starting speech recognition: {e}")
pjsip.conf
[transport-ws]
type=transport
protocol=ws
bind=0.0.0.0:8088
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
[netgsm_trunk_auth]
type=auth
auth_type=userpass
username=xxxxxx
password=xxxxxx
realm=sip.netgsm.com.tr
[netgsm_trunk_aor]
type=aor
contact=sip:xxxxxx@sip.netgsm.com.tr:5060
qualify_frequency=60
[netgsm_trunk]
type=endpoint
context=outbound-netgsm
disallow=all
allow=ulaw
allow=alaw
allow=slin
aors=netgsm_trunk_aor
auth=netgsm_trunk_auth
outbound_auth=netgsm_trunk_auth
force_rport=yes
rewrite_contact=yes
transport=transport-udp
from_user=xxxxxxxx
from_domain=sip.netgsm.com.tr
[netgsm_registration]
type=registration
transport=transport-udp
outbound_auth=netgsm_trunk_auth
server_uri=sip:sip.netgsm.com.tr:5060
client_uri=sip:xxxxxx@sip.netgsm.com.tr
contact_user=xxxxxxxx
expiration=3600
retry_interval=60
max_retries=10
extentions.conf (for outbound calls)
[outbound-netgsm]
exten => _X.,1,NoOp(Giden Arama: ${CALLERID(num)} -> ${EXTEN})
exten => _X.,n,Set(CHANNEL(language)=tr) ;
exten => _X.,n,Dial(PJSIP/${EXTEN}@netgsm-trunk,30,tTr) ;
exten => _X.,n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?answered:notanswered) ;
exten => _X.,n(answered),Stasis(netgsm-realtime,${EXTEN}) ;
exten => _X.,n,Hangup()
exten => _X.,n(notanswered),NoOp(Arama yanıtlanmadı.)
exten => _X.,n,Hangup()
Asterisk CLI output once calling is ongoing:
tahsilist-virtual-machine*CLI> core set debug 5
Core debug is still 5.
tahsilist-virtual-machine*CLI> core set verbose 5
Console verbose was 3 and is now 5.
-- Called 5345164540@netgsm_trunk
> 0x7f404c2f0710 -- Strict RTP learning after remote address set to: 185.88.7.206:11866
-- PJSIP/netgsm_trunk-00000001 is making progress
-- PJSIP/netgsm_trunk-00000001 answered
> Launching Stasis(netgsm-realtime,5345164540) on PJSIP/netgsm_trunk-00000001
-- <PJSIP/netgsm_trunk-00000001> Playing 'tr/test-voice.slin' (language 'en')
> 0x7f404c2f0710 -- Strict RTP switching to RTP target address 185.88.7.206:11866 as source
> 0x7f404c2f0710 -- Strict RTP learning complete - Locking on source address 185.88.7.206:11866
-- Channel PJSIP/netgsm_trunk-00000001 joined 'simple_bridge' stasis-bridge <84249a77-76a3-4c15-ade0-20298fef21ce>
> 0x7f3f78014040 -- Strict RTP learning after remote address set to: 127.0.0.1:8089
-- Called 127.0.0.1:8089/c(slin)
-- UnicastRTP/127.0.0.1:8089-0x7f3f780130b0 answered
> Launching Stasis(netgsm-realtime) on UnicastRTP/127.0.0.1:8089-0x7f3f780130b0
-- Channel PJSIP/netgsm_trunk-00000001 left 'simple_bridge' stasis-bridge <84249a77-76a3-4c15-ade0-20298fef21ce>