1

I am working on my bachelor thesis: VOIP Video Doorbell with one-way video.

I am fighting with this a issue for a lot of hours. The issue is that I am unable to make videocalls via SIP work. I sucessfully connected audio calls, and even other requested features like "unlocking doors" (switching relay on GPIO when right DTMG code is entered) works.

Just the video is not working.

For development, I am using Raspberry Pi 4 as it compiles faster the Pi Zero 2W, but after sucesfull setup, I will recompile the PJSIP (version 2.10) and make everything work on Pi Zero 2W.

My other HW is a Respeaker 2Mics HAT and Pi Camera v1.3 (provided by my university).

I tried nearly everything I was able to with my programming skills (I study IT security and IT law, not programming).

  1. I checked that codecs are supported on both sides (using MicroSIP on Windows as the other side)
  2. I contected the SIP provider (odorik.cz) and checked my setup is compatible with videocalls - their team was helpful and confirmed my calls are not being blocked on their side.
  3. I tried numerous options how to compile the PJSIP library with or without FFMPEG, with V4L2 support. And I read hundreds of posts talking about video support, so I can make it work by myself.
  4. I even tried local calls without any SIP providers (Directly calling to an IP, as both devices are in the same local network)

Unfortunately, I was unable to make it work, so I decided to share my code and logs, so anybode that understand the PJSIP library and python better than me can help me get out of this hell of hunders of hours in this issue.

here is my main file Doorbelly.py that controls everything:

import sys
import time
import threading
import configparser
from os.path import exists

import RPi.GPIO as GPIO
import lock

import pjsua2 as pj
import endpoint
import settings
import account
import call

BUTTON = 13
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
write=sys.stdout.write

configPath = sys.argv[1]
if(not exists(configPath)):
    print("Path to config is not set or is not valid path!")
    quit()



# Load config
userConfig = configparser.ConfigParser()
userConfig.read(configPath)

# Prepare lock class
lockInst = lock.Lock(userConfig['lock']['passcode'])

# Accounts
accList = []
ep = endpoint.Endpoint()
ep.libCreate()
# Default config
ep.autoTransmitOutgoing = True
ep.autoShowIncoming = False
appConfig = settings.AppConfig()



# Threading
appConfig.epConfig.uaConfig.threadCnt = 0
appConfig.epConfig.uaConfig.mainThreadOnly = True

# Config

appConfig.epConfig.uaConfig.maxCalls = 1
appConfig.epConfig.medConfig.clockRate = 44100  # Set the sample rate to 41000 Hz
appConfig.epConfig.medConfig.maxCodecCnt = 1
appConfig.epConfig.medConfig.maxActiveCodecCnt = 2
appConfig.epConfig.medConfig.defaultCaptureDevice = 0 # Use the first available capture device
ep.libInit(appConfig.epConfig)
ep.transportCreate(appConfig.udp.type, appConfig.udp.config)


#configure the library
config = pj.AccountConfig()
config.idUri = userConfig['pjsua2']['id']
config.regConfig.registrarUri = userConfig['pjsua2']['registrarUri']




config.presConfig.publishEnabled = True

cred = pj.AuthCredInfo()
cred.realm = userConfig['pjsua2']['realm']
cred.scheme = userConfig['pjsua2']['scheme']
cred.username = userConfig['pjsua2']['username']
cred.data = userConfig['pjsua2']['password']

config.sipConfig.authCreds.append(cred)

if not endpoint.validateSipUri(config.idUri):
    print("ERROR IN ID URI")

if not endpoint.validateSipUri(config.regConfig.registrarUri):
    print("ERROR IN REGISTRAR URI")

if not endpoint.validateSipUri(config.regConfig.contactParams):
    print("ERROR IN CONTANT PARAMS")

account = account.Account()
account.create(config)

# Start library
ep.libStart()
ep.libHandleEvents(10)


# Initialize an ongoing_call variable before the loop
ongoing_call = None

while True:
    input = GPIO.input(BUTTON)
    if input == GPIO.HIGH:  # Button is pressed
        # Check if there's an ongoing call
        if ongoing_call is None or ongoing_call.isCallDisconnected():
            print("No call detected, creating a new one to " + userConfig['lock']['targetVoipUri'])
            call_param = pj.CallOpParam()
            call_param.opt.audioCount = 1
            call_param.opt.videoCount = 1
            call_setting = pj.CallSetting()
            call_setting.audioCount = 1
            call_setting.videoCount = 1
            call_param.opt.callSetting = call_setting

            ongoing_call = call.Call(account, userConfig['lock']['targetVoipUri'], lock=lockInst)
            ongoing_call.makeCall(userConfig['lock']['targetVoipUri'], call_param)




        else:
            print("There's an ongoing call, can't make a new one.")
        ep.libHandleEvents(10)
    ep.libHandleEvents(10)
    time.sleep(0.1)  # Add a short delay to avoid excessive CPU usage

Then here is call.py file that controls the call itself:

# $Id$

import sys

import random
import pjsua2 as pj
import endpoint as ep
import lock
import time


# Call class
class Call(pj.Call):
    """
    High level Python Call object, derived from pjsua2's Call object.
    """
    def __init__(self, acc, peer_uri='', chat=None, call_id = pj.PJSUA_INVALID_ID, lock=None):
        pj.Call.__init__(self, acc, call_id)
        self.acc = acc
        self.peerUri = peer_uri
        self.chat = chat
        self.connected = False
        self.onhold = False
        self.lockInst = lock

    def onCallState(self, prm):
        ci = self.getInfo()
        self.connected = ci.state == pj.PJSIP_INV_STATE_CONFIRMED

        if ci.state == pj.PJSIP_INV_STATE_CONFIRMED:
                time.sleep(3)
                print("---------Call answered---------")
                param = pj.CallVidSetStreamParam()
                param.dir = pj.PJMEDIA_DIR_CAPTURE
                param.medIdx = 1
                self.vidSetStream(pj.PJSUA_CALL_VID_STRM_ADD, param)



    def onCallMediaState(self, prm):
        ci = self.getInfo()
        print("Call state:", ci.state)

        for mi in ci.media:
            if mi.type == pj.PJMEDIA_TYPE_AUDIO and \
              (mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE or \
               mi.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD):
                m = self.getMedia(mi.index)
                am = pj.AudioMedia.typecastFromMedia(m)
                # connect ports
                ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am)
                am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia())


                if mi.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD and not self.onhold:
                    self.chat.addMessage(None, "'%s' sets call onhold" % (self.peerUri))
                    self.onhold = True
                elif mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE and self.onhold:
                    self.chat.addMessage(None, "'%s' sets call active" % (self.peerUri))
                    self.onhold = False




        if self.chat:
            self.chat.updateCallMediaState(self, ci)

    def onDtmfDigit(self, prm):
        print("Received DTMF digit: " + prm.digit)

        if(prm.digit == "#"):
            print("Sending OK")
            self.lockInst.receiveOk()
            return

        if(prm.digit == "*"):
            print("Sending reset")
            self.lockInst.receiveReset()
            return

        self.lockInst.addDigit(prm.digit)

    def onCallMediaTransportState(self, prm):
        #msgbox.showinfo("pygui", "Media transport state")
        pass

    def setLock(self, lock):
        self.lockInst = lock

    # Check for call disconnection to prevent crashes when button is pressed repeatedly
    def isCallDisconnected(self):
        try:
         call_info = self.getInfo()
         return call_info.state in (pj.PJSIP_INV_STATE_DISCONNECTED, pj.PJSIP_INV_STATE_NULL)
        except pj.Error:
         return True

And just linking other files like lock.py used for unlocking the door: https://pastebin.com/nB3hGz4s

Here is my config loaded by the doorbell.py and used for controlling the call destination and lock password:

[pjsua2]
id = sip:XXXXX@sip.odorik.cz
registrarUri = sip:sip.odorik.cz
realm = sip.odorik.cz
scheme = digest
username = XXXXXX
password = YYYYYY

[lock]
passcode = 123
targetVoipUri = sip:CALLEDID@sip.odorik.cz

And finally, here is my log of the call: https://pastebin.com/fFMN2HnG

But the most important problems from the log are (at least I think):

17:59:29.376    inv0xf293bc  ....SDP negotiation done: Success
17:59:29.376  pjsua_media.c  .....Call 0: updating media..
17:59:29.376  pjsua_media.c  .......Media stream call00:0 is destroyed
17:59:29.376    pjsua_aud.c  ......Audio channel update..
17:59:29.376   strm0xf3d0d4  .......VAD temporarily disabled
17:59:29.376   strm0xf3d0d4  .......Encoder stream started
17:59:29.376   strm0xf3d0d4  .......Decoder stream started
17:59:29.377  pjsua_media.c  ......Audio updated, stream #0: GSM (sendrecv)
17:59:29.377  pjsua_media.c  .......Media stream call00:1 is destroyed
17:59:29.377    pjsua_vid.c  ......Video channel update..
17:59:29.403 vstenc0xf4238c  .......Encoder stream started
17:59:29.403 vstdec0xf4238c  .......Decoder stream started
17:59:29.403    pjsua_vid.c  .......Setting up RX..
17:59:29.403    pjsua_vid.c  ........Creating video window: type=stream, cap_id=-1, rend_id=1480776
17:59:29.403    pjsua_vid.c  .........Window 0: destroying..
17:59:29.403  pjsua_media.c  ......pjsua_vid_channel_update() failed for call_id 0 media 1: Invalid video device (PJMEDIA_EVID_INVDEV)
17:59:29.403    pjsua_vid.c  .......Stopping video stream..
17:59:29.405  pjsua_media.c  .......Media stream call00:1 is destroyed
17:59:29.405  pjsua_media.c  ......Error updating media call00:1: Invalid video device (PJMEDIA_EVID_INVDEV)
17:59:29.406    pjsua_aud.c  .....Conf connect: 0 --> 1

What I understand from this is that it tries to open an RX stream and its window, but it failes as there is no screen attached to the RPi (and it will not be in the project)

So my idea was to retry opening a new video stream with just outgoing direction once the call is estabilished. But that fails also.

17:59:32.410    pjsua_vid.c !.....Call 0: set video stream, op=1
17:59:32.410  pjsua_media.c  ......RTP socket reachable at 10.9.0.41:4004
17:59:32.410  pjsua_media.c  ......RTCP socket reachable at 10.9.0.41:4005
17:59:32.411    pjsua_vid.c  ......Unable to create re-INVITE: Invalid operation (PJ_EINVALIDOP) [status=70013]
17:59:32.411       call.cpp  .....pjsua_call_set_vid_strm(id, op, &prm) error: Invalid operation (PJ_EINVALIDOP) (status=70013) [../src/pjsua2/call.cpp:826]

I am sorry for such a long report and I thank you in advance for helping me with this issue. I am not a dead end and have no idea what to try next.

Thank you!

DJ_Ironic
  • 21
  • 1
  • 4
  • You're right, your Pi is trying to receive stream and send it somewhere. I was implementing similar project in cpp and to solve this I register dummy video device which just did nothing with received frames. – DaszuOne Jul 06 '23 at 20:53

0 Answers0