The PJSUA2 Python sample application (pygui) is broken out of the box. I can't add a buddy, I get an exception that mentions one of the C code files. I can't make a call without adding a buddy, and anyway I don't really need a GUI (tcl/tk in this case) to accomplish what I'm trying to do here.
So I set out to create a simple, easy-to-understand soft phone with the bare minimum required by the PJSUA2 library to make a phone call. I signed up with a SIP host that allows me to call POTS numbers and I tested that already using PyVoIP (which is too simplistic for the project I'm working on).
I've tried writing my own, simple PJSUA2 soft phone program but it fails utterly when I try to make a call:
import sys
import pjsua2 as pj
class Settings:
def __init__(self, sip_user=None, sip_pass=None, sip_registrar_uri=None):
self.sip_user = sip_user
self.sip_pass = sip_pass
self.sip_registrar_uri = sip_registrar_uri
# Subclass to extend the Account and get notifications etc.
class Account(pj.Account):
def onRegState(self, prm):
# print("***OnRegState: " + prm.reason)
pass
# Subclass the Call class to define callbacks etc.
class Call(pj.Call):
def __init__(self, acc, peer_uri='', chat=None, call_id=pj.PJSUA_INVALID_ID):
pj.Call.__init__(self, acc, call_id)
def onCallState(self, prm):
call_info = self.getInfo()
self.connected = call_info.state == pj.PJSIP_INV_STATE_CONFIRMED
def onCallMediaState(self, prm):
ep = pj.Endpoint()
call_info = self.getInfo()
print(f'call_info: {call_info}')
# for media_item in call_info.media:
# if media_item.type == pj.PJMEDIA_TYPE_AUDIO and (
# media_item.status == pj.PJSUA_CALL_MEDIA_ACTIVE or
# media_item.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD
# ):
# media = self.getMedia(media_item.index)
# audio_media = pj.AudioMedia.typecastFromMedia(media)
# Connect ports.
# LEFT OFF HERE
def read_settings() -> Settings:
# TODO: SECURITY: Rip these out, pass in via env vars.
return Settings(
sip_user='-redacted-',
sip_pass='-redacted-',
sip_registrar_uri='seattle1.voip.ms', # Seattle VoIP.ms PoP
)
def main():
# Retrieve required settings.
settings = read_settings()
# Set up endpoint.
endpoint_config = pj.EpConfig()
endpoint = pj.Endpoint()
endpoint.libCreate()
endpoint.libInit(endpoint_config)
# Set up SIP transport.
sip_transport_config = pj.TransportConfig()
sip_transport_config.port = 5060
endpoint.transportCreate(pj.PJSIP_TRANSPORT_UDP, sip_transport_config)
# Start the library.
endpoint.libStart()
# Set up account.
account_config = pj.AccountConfig()
account_config.idUri = f'sip:{settings.sip_user}'
account_config.regConfig.registrarUri = f'sip:{settings.sip_registrar_uri}'
credentials = pj.AuthCredInfo('digest', '*', settings.sip_user, 0,
settings.sip_pass)
account_config.sipConfig.authCreds.append(credentials)
# Create account.
account = Account()
account.create(account_config)
# Create call.
call = Call(account)
call_param = pj.CallOpParam()
call_param.opt.audioCount = 1 # Also tried 2 here, for stereo? Same error...
call_param.opt.videoCount = 0
# Dial.
try:
call.makeCall('-redacted-', call_param)
except Exception as error:
print(f'Dialing error: {error}')
sys.exit(1)
# Main event loop.
while True:
try:
endpoint.libHandleEvents(50)
except Exception as error:
print(f'Error: {error}')
break
endpoint.libDestroy()
if __name__ == "__main__":
main()
Running that code gives me a valid SIP registration, followed by a log messages that indicate it's trying to start the call. I didn't include all of that here because I read through it and there isn't anything relevant there, but it's a lot of info and I'd have to redact a bunch of stuff. Anyway, the error I get is here:
20:59:26.067 pjsua_acc.c ..Acc 0: Registration sent
20:59:26.067 pjsua_call.c Making call with acc #0 to -redacted-
20:59:26.067 pjsua_aud.c .Set sound device: capture=-1, playback=-2, mode=0, use_default_settings=0
20:59:26.067 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@16000/1/20ms
20:59:26.068 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.068 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@44100/1/20ms
20:59:26.068 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.068 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@48000/1/20ms
20:59:26.068 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.068 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@32000/1/20ms
20:59:26.069 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.069 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@16000/1/20ms
20:59:26.069 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.069 pjsua_aud.c ..Opening sound device (speaker + mic) PCM@8000/1/20ms
20:59:26.069 alsa_dev.c ...Unable to set a channel count of 1 for playback device 'hw:CARD=PCH,DEV=0'
20:59:26.069 pjsua_aud.c ..Unable to open sound device: Unknown error from audio driver (PJMEDIA_EAUD_SYSERR) [status=420002]
20:59:26.069 pjsua_media.c .Call 0: deinitializing media..
Is this something as simple as initializing the sound devices myself, without relying on auto-detect? I should also note that I have this entire thing built in a Docker image. That image does have the sound library installed and I am attaching /dev/snd as a device when running the container. I might not be doing it correctly, but I'm doing it. :)
Dockerfile:
FROM python:3-slim AS build_pjsip
ENV PJSIP_VERSION="2.13.1"
WORKDIR /src
RUN apt-get update && apt-get install -y \
wget \
build-essential \
swig \
libasound2-dev
RUN wget https://github.com/pjsip/pjproject/archive/refs/tags/${PJSIP_VERSION}.tar.gz && \
tar xfz ${PJSIP_VERSION}.tar.gz
RUN mv pjproject-${PJSIP_VERSION} pjproject && \
cd pjproject && \
./configure CFLAGS="-fPIC" && \
make dep && \
make clean && \
make
RUN cd pjproject/pjsip-apps/src/swig/python && \
make && \
make install
FROM python:3-slim as release
RUN apt-get update && apt-get install -y \
libasound2
ADD . /opt/btm
WORKDIR /opt/btm
COPY --from=build_pjsip /root/.local/lib /root/.local/lib
CMD ["/bin/bash"]
dev (chmod +x to make it executable, then to enter container just ./dev after building the image with the tag btm-make-a-call
)
#!/bin/bash
docker run \
--rm \
-it \
-v .:/opt/btm \
--device /dev/snd:/dev/snd \
btm-make-a-call
Possible problems are:
Finally, I don't write C++ code and so I'd really like to come up with a solution in Python.
Thank you in advance!
It turns out that I was incorrectly trying to assign a single audio channel to my sound device, which only operates in stereo (2 channels).
Trying to directly initialize your sound hardware with incompatible parameters causes a hard error the alsa sound library. When accessing your underlying hardware directly, you must use a configuration that exactly matches that underlying hardware.
The change that made my application actually run was the following:
endpoint_config.medConfig.channelCount = 2