PJSUA2 media plane — audio finally reaches the classifier #8
@@ -63,6 +63,72 @@ GATEWAY_SIP_PORT=21062 # must differ from the Asterisk port
|
||||
> engine is assigned — `main.py`'s lifespan calls `build_sip_engine()` after
|
||||
> construction. A harness that skips that step silently tests the mock.
|
||||
|
||||
## The softphone (transfer target)
|
||||
|
||||
**Asterisk is the registrar for devices, not Hold Slayer.** The gateway reaches
|
||||
a desk phone by dialling extension `2001`, which Asterisk routes to whatever
|
||||
has registered as `softphone`. This deliberately avoids Hold Slayer's own SIP
|
||||
listener, which answers `200 OK` to any REGISTER with no digest challenge.
|
||||
|
||||
The `pjsua` CLI built alongside the Python bindings is the test device — same
|
||||
library stack as the gateway, no extra dependency. It needs an RPATH patch like
|
||||
the bindings did:
|
||||
|
||||
```bash
|
||||
cp ~/src/pjproject/pjsip-apps/bin/pjsua-x86_64-pc-linux-gnu ~/.local/bin/pjsua
|
||||
patchelf --set-rpath $HOME/.local/lib ~/.local/bin/pjsua
|
||||
```
|
||||
|
||||
Register it (config file avoids shell-quoting pain):
|
||||
|
||||
```bash
|
||||
cat > softphone.cfg <<'EOF'
|
||||
--null-audio
|
||||
--auto-answer=200
|
||||
--max-calls=4
|
||||
--local-port=21070
|
||||
--id=sip:softphone@127.0.0.1
|
||||
--registrar=sip:127.0.0.1:21061
|
||||
--realm=asterisk
|
||||
--username=softphone
|
||||
--password=labphone
|
||||
--log-level=3
|
||||
EOF
|
||||
|
||||
# pjsua is an interactive console app: it exits ~8s after start if stdin is
|
||||
# closed or /dev/null. Hold a fifo open on stdin — `script -qfc` and
|
||||
# `setsid </dev/null` both look like they work (registration succeeds) and
|
||||
# then the process dies, leaving a stale contact in Asterisk that routes
|
||||
# INVITEs to a port nobody is listening on.
|
||||
mkfifo sp.fifo
|
||||
setsid sh -c 'exec 3<>sp.fifo; pjsua --config-file softphone.cfg <&3 >softphone.log 2>&1' &
|
||||
```
|
||||
|
||||
Verify — **check the port is actually bound**, not just that Asterisk holds a
|
||||
contact, since a stale registration outlives the process:
|
||||
|
||||
```bash
|
||||
ss -lnup | grep 21070 # must be listening
|
||||
docker compose -f docker-compose.lab.yml exec asterisk \
|
||||
asterisk -rx "pjsip show contacts" # must show softphone
|
||||
```
|
||||
|
||||
Then place a call to `2001`. Both legs should show `Up` under one bridge id:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.lab.yml exec asterisk \
|
||||
asterisk -rx "core show channels concise"
|
||||
```
|
||||
|
||||
> **`--realm=asterisk`, not `--realm='*'`** — the wildcard fails with
|
||||
> `PJSIP_EFAILEDCREDENTIAL` against Asterisk's digest challenge.
|
||||
|
||||
> **Qualify is off** for this AOR (`qualify_frequency = 0`): the pjsua console
|
||||
> does not answer `OPTIONS`, so polling marks a working softphone `Unavail` and
|
||||
> the dialplan refuses to ring it. The `2001` guard therefore tests
|
||||
> `PJSIP_AOR(softphone,contact)` rather than `DEVICE_STATE`. A real hardphone
|
||||
> answers OPTIONS and can have qualify re-enabled.
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -128,6 +128,23 @@ exten => 1008,1,NoOp(LAB 1008: silence)
|
||||
same => n,Wait(40)
|
||||
same => n,Hangup()
|
||||
|
||||
; --- 2001: ring the registered softphone ----------------------------------
|
||||
; The transfer target. Asterisk is the registrar for devices, so the gateway
|
||||
; reaches a desk phone by dialling this rather than by registering it itself.
|
||||
; Fails fast when nothing is registered — a silent 30s ring would look like a
|
||||
; gateway bug rather than an absent softphone.
|
||||
exten => 2001,1,NoOp(LAB 2001: ring softphone)
|
||||
; Count registered contacts rather than DEVICE_STATE: device state follows
|
||||
; the OPTIONS qualify, which is off for this AOR (the pjsua CLI does not
|
||||
; answer OPTIONS), so a registered softphone would still read UNAVAILABLE.
|
||||
same => n,GotoIf($[${PJSIP_AOR(softphone,contact)} = ""]?nodevice)
|
||||
same => n,Dial(PJSIP/softphone,30)
|
||||
same => n,Hangup()
|
||||
same => n(nodevice),NoOp(LAB 2001: no softphone registered)
|
||||
same => n,Answer()
|
||||
same => n,Playback(lab-speech)
|
||||
same => n,Hangup()
|
||||
|
||||
; --- echo test ------------------------------------------------------------
|
||||
; Not a scenario — a debugging aid. Echoes audio back so you can confirm
|
||||
; bidirectional RTP by ear when something looks wrong.
|
||||
|
||||
@@ -34,14 +34,21 @@ local_net = {{ asterisk_local_net }}
|
||||
; ---------------------------------------------------------------------------
|
||||
; Hold Slayer authenticates as this endpoint to place calls into the lab.
|
||||
|
||||
; Identify the endpoint by source address. Asterisk's default matching uses
|
||||
; the From-header domain, which Hold Slayer populates from its SIP bind
|
||||
; address (0.0.0.0 on a wildcard bind) — never a value Asterisk can match.
|
||||
; Matching on where the packet actually came from sidesteps that.
|
||||
; Identify the endpoint by source address *and port*. Asterisk's default
|
||||
; matching uses the From-header domain, which Hold Slayer populates from its
|
||||
; SIP bind address (0.0.0.0 on a wildcard bind) — never a value Asterisk can
|
||||
; match. Matching on where the packet came from sidesteps that.
|
||||
;
|
||||
; The port is essential when the softphone runs on the same host: a
|
||||
; host-only match claims *every* packet from that address, so the
|
||||
; softphone's REGISTER would be attributed to this endpoint and checked
|
||||
; against the gateway's password ("Failed to authenticate", confusingly).
|
||||
; Endpoints that authenticate by username (the softphone) must not be
|
||||
; covered by an identify block.
|
||||
[hold-slayer]
|
||||
type = identify
|
||||
endpoint = hold-slayer
|
||||
match = {{ asterisk_match_host }}
|
||||
match = {{ asterisk_match_host }}:{{ asterisk_gateway_port }}
|
||||
|
||||
[hold-slayer]
|
||||
type = endpoint
|
||||
@@ -68,6 +75,56 @@ auth_type = userpass
|
||||
username = {{ asterisk_sip_username }}
|
||||
password = {{ asterisk_sip_password }}
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Softphone endpoint — the transfer target
|
||||
; ---------------------------------------------------------------------------
|
||||
; Asterisk is the registrar for devices, not Hold Slayer. A softphone REGISTERs
|
||||
; here and the gateway transfers a live call to it by dialling extension 2001.
|
||||
;
|
||||
; Deliberate: Hold Slayer's own SIP listener answers 200 OK to any REGISTER
|
||||
; with no digest challenge, so anything on the network could register as a
|
||||
; device and receive transferred calls. Keeping registration in Asterisk means
|
||||
; the lab does not depend on that path, and the softphone is authenticated.
|
||||
;
|
||||
; Test with the pjsua CLI built alongside the Python bindings:
|
||||
; pjsua --null-audio --auto-answer=200 \
|
||||
; --id=sip:softphone@<asterisk-host> \
|
||||
; --registrar=sip:<asterisk-host>:21061 \
|
||||
; --realm='*' --username=softphone --password=<pw> \
|
||||
; --local-port=<free port>
|
||||
|
||||
[softphone]
|
||||
type = endpoint
|
||||
context = hold-slayer-lab
|
||||
disallow = all
|
||||
allow = ulaw
|
||||
allow = alaw
|
||||
auth = softphone-auth
|
||||
aors = softphone
|
||||
dtmf_mode = rfc4733
|
||||
direct_media = no
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
rtp_symmetric = yes
|
||||
|
||||
[softphone-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = {{ asterisk_softphone_username }}
|
||||
password = {{ asterisk_softphone_password }}
|
||||
|
||||
[softphone]
|
||||
type = aor
|
||||
; The device's contact is learned from its REGISTER rather than configured —
|
||||
; a softphone's port is not known in advance.
|
||||
max_contacts = 1
|
||||
remove_existing = yes
|
||||
; No qualify: the pjsua CLI does not answer OPTIONS while sitting at its
|
||||
; console prompt, so polling marks a perfectly working softphone Unavail and
|
||||
; the dialplan refuses to ring it. Registration itself is the liveness signal
|
||||
; here. A real hardphone answers OPTIONS and can have qualify re-enabled.
|
||||
qualify_frequency = 0
|
||||
|
||||
[hold-slayer]
|
||||
type = aor
|
||||
max_contacts = 2
|
||||
|
||||
Reference in New Issue
Block a user