Search code examples
pythonpjsippjsua2libalsa

Simple PJSUA2 project fails with 'alsa_dev.c ...Unable to set a channel count of 1 for playback device' while attempting to open sound device


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:

  1. Unavailable audio hardware in the container runtime due to me not attaching things properly, or failing to mount a device file into the container perhaps.
  2. I'm instantiating the endpoint with an invalid config for my audio hardware. I don't see any examples of that for PJSUA2 though...
  3. I'm starting the library with an invalid config for my audio hardware.
  4. I have an actual audio hardware problem, though that's unlikely since both Linux and Windows both use my audio just fine (I dual boot this machine).

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!


Solution

  • 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