使用 PJSIP (PJSUA2) 和 Pi Camera 在 Raspberry Pi 上进行视频通话

问题描述 投票:0回答:0

我正在写我的学士论文:VOIP Video Doorbell with one-way video.

我为这个问题奋斗了很多个小时。问题是我无法通过 SIP 工作进行视频通话。我成功地连接了音频呼叫,甚至其他要求的功能,如“开门”(输入正确的 DTMG 代码时在 GPIO 上切换继电器)都可以工作。

只是视频不工作。

对于开发,我使用 Raspberry Pi 4,因为它在 Pi Zero 2W 上的编译速度更快,但在成功设置后,我将重新编译 PJSIP(2.10 版)并使一切在 Pi Zero 2W 上运行。

我的另一个硬件是 Respeaker 2Mics HAT 和 Pi Camera v1.3(由我的大学提供)。

我用我的编程技能尝试了几乎所有我能做的事情(我学习 IT 安全和 IT 法,而不是编程)。

  1. 我检查过双方都支持编解码器(另一方使用 Windows 上的 MicroSIP)
  2. 我联系了 SIP 提供商 (odorik.cz) 并检查了我的设置是否与视频通话兼容——他们的团队提供了帮助,并确认我的通话没有被他们阻止。
  3. 我尝试了很多选项来编译带有或不带有 FFMPEG 的 PJSIP 库,并支持 V4L2。我阅读了数百篇关于视频支持的帖子,所以我可以自己解决它。
  4. 我什至尝试了没有任何 SIP 提供商的本地呼叫(直接呼叫 IP,因为两个设备都在同一个本地网络中)

不幸的是,我无法让它工作,所以我决定分享我的代码和日志,所以任何比我更了解 PJSIP 库和 python 的人都可以帮助我摆脱这个问题的数百小时地狱。

这是我控制一切的主文件 Doorbelly.py:

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

然后这里是控制调用本身的call.py文件:

# $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

只需链接其他文件,如用于解锁门的 lock.py:https://pastebin.com/nB3hGz4s

这是我通过 doorbell.py 加载的配置,用于控制呼叫目的地和锁定密码:

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

[lock]
passcode = 123
targetVoipUri = sip:[email protected]

最后,这是我的通话记录:https://pastebin.com/fFMN2HnG

但是日志中最重要的问题是(至少我认为):

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

据我了解,它试图打开一个 RX 流及其窗口,但它失败了,因为 RPi 没有附加屏幕(并且它不会在项目中)

所以我的想法是,一旦呼叫建立,就重新尝试打开一个只有传出方向的新视频流。但这也失败了。

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]

对于这么长的报告,我深表歉意,在此先感谢您帮助我解决这个问题。我不是死胡同,不知道下一步该尝试什么。

谢谢!

raspberry-pi sip voip pjsip pjsua2
© www.soinside.com 2019 - 2024. All rights reserved.