我正在写我的学士论文: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 法,而不是编程)。
不幸的是,我无法让它工作,所以我决定分享我的代码和日志,所以任何比我更了解 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]
对于这么长的报告,我深表歉意,在此先感谢您帮助我解决这个问题。我不是死胡同,不知道下一步该尝试什么。
谢谢!