python
import pjsua2 as pj
import time
import socket
def get_local_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "0.0.0.0"
class MyAudioMediaPlayer(pj.AudioMediaPlayer):
def __init__(self, call):
super().__init__()
self.call = call
self.played = False # 防止重复回调
def onEof(self):
print("音频播放完成")
self.played = True
# 不要在此处挂断或删除播放器,由 Call 负责清理
super().onEof()
class MyCall(pj.Call):
def __init__(self, acc, call_id):
super().__init__(acc, call_id)
self.player = None
self.playing = False
def onCallState(self, prm):
try:
try:
info = self.getInfo()
except AttributeError:
info = self.info()
state = info.state
if state == pj.PJSIP_INV_STATE_CONFIRMED:
print("✅ 通话已接通")
# 只播放一次,防止重复触发
if not self.playing:
self._play_audio()
elif state == pj.PJSIP_INV_STATE_DISCONNECTED:
print(f"通话已挂断,原因: {info.lastReason}")
self._cleanup_player()
except Exception as e:
print(f"onCallState 异常: {e}")
def _cleanup_player(self):
"""安全清理播放器"""
if self.player:
try:
self.player.delete()
except Exception as e:
print(f"删除播放器异常: {e}")
self.player = None
self.playing = False
def _play_audio(self):
# 先清理旧播放器(保险)
self._cleanup_player()
try:
self.player = MyAudioMediaPlayer(self)
wav_path = r"D:\Client\hxtiyanzhongxin\linphone\test.wav"
self.player.createPlayer(wav_path)
call_media = self.getAudioMedia(-1)
self.player.startTransmit(call_media)
self.playing = True
print("音频开始播放... (对端将听到声音)")
except Exception as e:
print(f"播放音频失败: {e}")
self._cleanup_player()
try:
self.hangup(pj.CallOpParam(True))
except:
pass
class MyAccount(pj.Account):
def __init__(self):
super().__init__()
self.current_call = None
def onRegState(self, prm):
if prm.code == 200:
print(f"✅ 注册成功: {prm.reason}")
else:
print(f"❌ 注册失败 ({prm.code}): {prm.reason}")
def onIncomingCall(self, prm):
print(f"📞 收到来电: {prm.callId}")
if prm.callId == pj.PJSUA_INVALID_ID:
print("无效 Call ID,忽略")
return
# 每个来电独立 Call 对象
self.current_call = MyCall(self, prm.callId)
call_op = pj.CallOpParam(True)
call_op.statusCode = 200
self.current_call.answer(call_op)
print("✅ 已自动接听")
def main():
ep = None
acc = None
try:
ep = pj.Endpoint()
ep.libCreate()
ep_cfg = pj.EpConfig()
# 使用 -1 表示 null 设备(绕过声卡驱动)
ep_cfg.medConfig.sndDevId = -1
print("已设置音频设备为 null (sndDevId=-1)")
local_ip = get_local_ip()
ep_cfg.uaConfig.publicAddress = local_ip
print(f"设置公共 IP: {local_ip}")
ep_cfg.logConfig.level = 6
ep.libInit(ep_cfg)
# 创建传输,显式绑定本地 IP
tcfg = pj.TransportConfig()
tcfg.port = 0
tcfg.publicAddress = local_ip
tcfg.bindAddress = local_ip
ep.transportCreate(pj.PJSIP_TRANSPORT_UDP, tcfg)
ep.libStart()
acfg = pj.AccountConfig()
acfg.idUri = "sip:104@192.168.31.109"
acfg.regConfig.registrarUri = "sip:192.168.31.109:5060"
cred = pj.AuthCredInfo("digest", "*", "104", 0, "104")
acfg.sipConfig.authCreds.append(cred)
acc = MyAccount()
acc.create(acfg)
print("正在注册到 192.168.31.109:5060 (用户名: 104)")
print("等待来电... (按 Ctrl+C 退出)")
while True:
ep.libHandleEvents(50)
except KeyboardInterrupt:
print("\n用户中断,正在退出...")
except Exception as e:
print(f"主循环异常: {e}")
import traceback
traceback.print_exc()
finally:
# 清理顺序:先挂断通话,再删除账号,最后销毁端点
try:
if acc and acc.current_call:
acc.current_call.hangup(pj.CallOpParam(True))
# 等待一小段时间让挂断消息发送
time.sleep(0.5)
acc.current_call._cleanup_player()
if acc:
acc.delete()
if ep:
ep.libDestroy()
except Exception as e:
print(f"清理时出错: {e}")
print("程序已退出")
if __name__ == "__main__":
main()
环境需要安装
- **安装 `pjsua2-wheel` 包**
打开命令提示符 (cmd) 或 PowerShell,运行以下命令:
```bash
pip install pjsua2-wheel
```
这个包提供了适用于 Windows 的预编译 wheel 文件。
- **验证安装**
在 Python 环境中尝试导入,确认是否成功:
```python
import pjsua2 as pj
print("pjsua2 导入成功!")