SIP 的一个简单接听例子

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()

环境需要安装

  1. **安装 `pjsua2-wheel` 包**

打开命令提示符 (cmd) 或 PowerShell,运行以下命令:

```bash

pip install pjsua2-wheel

```

这个包提供了适用于 Windows 的预编译 wheel 文件。

  1. **验证安装**

在 Python 环境中尝试导入,确认是否成功:

```python

import pjsua2 as pj

print("pjsua2 导入成功!")