跳转至

19 exsip-SIP通话

作者:蒋骞 | 最后修改:2026-04-28

一、概述

exsip 是一个专门为 网络通话 开发的 LuaOS 扩展库,基于 LuatOS 框架,该库提供简洁、易用的 API 接口,简化了网络通话的开发复杂度。

二、SIP/VoIP 电话协议相关说明

2.1 VoIP

VoIP (Voice over Internet Protocol) = 在 IP 网络上传输语音的技术总称。传统电话使用电路交换(独占一条线路),VoIP 使用分组交换:把语音切成小包,通过互联网发送,在接收端重新组装并播放。

2.2 SIP

SIP (Session Initiation Protocol) 是 VoIP 中最常用的信令协议,负责:

  • 用户定位(在哪里注册),
  • 会话建立(邀请对方加入通话),
  • 会话修改(例如从语音切换到视频),
  • 会话结束(挂断);

SIP 不传输语音数据,只负责建立、管理和结束通话。在 SIP 通话中,SDP 被封装在 SIP 消息体(Body)里,完成媒体协商。

SIP 通话基本流程

SIP(Session Initiation Protocol,会话初始协议)通话流程大致分为四个阶段:

  1. 注册(由 SIP 负责) 终端(如 IP 电话)向 SIP 服务器注册,服务器验证身份后,记录终端当前的 IP 地址和端口。 SIP 客户端必须向服务器(sip_server_addr)告知自己的域名(sip_domain )、端口(sip_server_port)、用户名(sip_username)、密码(sip_password)以便服务器可以将来电路由到正确的设备。

  2. 呼叫建立(邀请与应答,由 SIP 负责)

    • 主叫发送 INVITE 请求到 SIP 服务器
    • 服务器定位被叫并转发 INVITE
    • 被叫振铃,返回 Ringing(180 响应)
    • 被叫接听后返回 OK(200 响应)
    • 主叫发送 ACK 确认,通话建立
  3. 媒体传输(由 RTP/RTCP 负责)

SIP 完成建立连接的“握手”后,就退出了实时数据传输的环节,双方通过 RTP 直接交换语音/视频流,声音被设备麦克风采集、编码、打包成 RTP 包,通过网络发送给对方;对方做相反的解码和播放;通话期间,双方每间隔 5 秒左右发送 RTCP 报告包。

  1. 通话结束(由 SIP 负责)

当一方挂断时,设备会发送一个 SIP 请求(BYE 消息),对方收到后回复 OK,至此,通话完全结束,所有相关的网络资源被释放。

简单示意图(逻辑流程):

主叫 → INVITE → SIP 服务器 → INVITE → 被叫

主叫 ← 180/Ringing ← SIP 服务器 ← 180/Ringing ← 被叫

主叫 ← 200/OK ← SIP 服务器 ← 200/OK ← 被叫

主叫 → ACK → SIP 服务器 → ACK → 被叫

(RTP 媒体流在主叫与被叫之间直通)

主叫/被叫 → BYE → 对方 ← 200 OK

2.3 SDP

SDP(Session Description Protocol,会话描述协议) 是一种纯文本格式的协议,用于描述多媒体会话(如 VoIP 通话、视频会议、流媒体)的参数。它不传输媒体数据本身,而是双方协商 RTP 的接收 IP、端口、编解码器、打包时长、媒体方向等:

  • 往哪个 IP 和端口发 RTP
  • 用什么编解码器(如本扩展库只支持 PCMU 和 PCMA
  • 打包时长——每包包含多少毫秒的语音(ptime)
  • 媒体方向(仅发送、仅接收、双向发送接收、非活跃)

在 SIP 通话中,SDP 被封装在 SIP 消息体(Body)里,完成媒体协商。

SDP 基本格式

SDP 由多行 <类型>=<值> 组成,每行代表一个属性。典型的一个 SDP 如下:

v=0
o=amy 123456 789012 IN IP4 192.168.1.5
s=Call with Candy
c=IN IP4 192.168.1.5
t=0 0
m=audio 40000 RTP/AVP 0 8
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=ptime:20
a=sendrecv

SDP 字段详解:

字段
含义
示例
解释
`v=`
协议版本
`v=0`
目前固定为 0
`o=`
会话发起者(Origin)
`o=amy 123456 789012 IN IP4 192.168.1.5`
用户名、会话ID、版本、网络类型、地址类型、地址
`s=`
会话名称
`s=Call with Candy`
必填,可为任意字符串
`c=`
连接信息(Connection)
`c=IN IP4 192.168.1.5`
网络类型(IN)、地址类型(IP4/IP6)、IP地址(RTP接收地址)
`t=`
会话起始与结束时间
`t=0 0`
0 表示无限制(实时会话)
`m=`
媒体描述(Media)
`m=audio 40000 RTP/AVP 0 8`
媒体类型(audio/video)、RTP接收端口、传输协议(RTP/AVP)、负载类型(payload type)列表
`a=`
属性(Attribute)
见下表
扩展属性,如编码器映射、打包时长、媒体方向

a=常用属性

属性
示例
说明
`rtpmap`
`a=rtpmap:0 PCMU/8000`
将 payload type 0 映射为 PCMU 编码,采样率 8000 Hz
`ptime`
`a=ptime:20`
每 RTP 包包含 20ms 的语音
`sendrecv`
`a=sendrecv`
双向发送和接收(默认)
`sendonly`
`a=sendonly`
只发送不接收
`recvonly`
`a=recvonly`
只接收不发送
`inactive`
`a=inactive`
不发送也不接收

2.4 RTP

RTP (Real-time Transport Protocol) 专门用于在互联网上传输实时媒体(语音、视频),负责:

  • 对语音包进行序列号标记(接收方可按序重组)
  • 携带时间戳(接收方可平滑播放)
  • 识别载荷类型(PCMU、PCMA、OPUS 等编码)

RTP 通常运行在 UDP 之上,因为它对实时性要求高,允许少量丢包但不能忍受重传带来的延迟。

RTP 端口规则

  • RTP 通常使用偶数端口(如 40000)
  • RTCP 使用下一个奇数端口(40001)
  • 一个通话至少需要一对端口(RTP + RTCP)
  • 每包内容:RTP 头 + 20ms 的 PCMU 编码语音数据(约 160 字节)。
  • 发送速率:每秒 50 包(20ms 周期)。
  • RTP 头中的序列号:每发一包加 1,用于检测丢包和乱序。
  • 时间戳:反映采样时刻,接收端平滑播放。

2.5 RTCP

RTCP (RTP Control Protocol) 与 RTP 配合使用,负责:

  • 报告传输质量(丢包率、抖动、往返延迟)
  • 提供每个参与者的统计信息
  • 偶尔同步不同媒体流(如音视频同步)

VoIP 终端可以根据 RTCP 反馈调整抖动缓冲、编码码率;运营商可据此监控通话质量。通话期间,双方每间隔 5 秒左右发送 RTCP 报告包(使用 RTP 端口 + 1):

  • 发送方报告 (SR):已发送的 RTP 包数、字节数、RTP 时间戳与 NTP 映射。
  • 接收方报告 (RR):接收到的包数、丢包数、抖动(jitter)、往返时延(RTT)。

RTP 与 RTCP 的关系

  • RTP 负责传输数据,RTCP 负责监控传输质量
  • RTCP 流量通常不超过 RTP 流量的 5%
  • 两者使用相邻的 UDP 端口(RTP 偶数,RTCP 奇数)

2.6 底层传输协议:TCP 与 UDP 的选择

SIP 信令:

  • 可使用 UDP(默认 5060)或 TCP(5060)或 TLS(5061)
  • UDP 更轻量,但需要应用层处理丢包(SIP 有重传机制)
  • TCP 更可靠,但可能增加呼叫建立延迟

RTP 媒体传输:

  • 必须使用 UDP(或可选的 SRTP over UDP)
  • 实时性要求远高于可靠性:重传会导致更大延迟,宁可丢包也不重传

结论:SIP 信令可用 TCP/UDP,RTP 媒体只用 UDP,当前 demo 只实现了 TCP 方式的 SIP 信令传输,UDP 传输方式还在调试。

2.7 总结

VoIP 是总称,它使用 SIP 建立、修改和结束通话(SDP 被封装在 SIP 消息体(Body)里,完成媒体协商),使用 RTP 传输语音/视频数据RTCP 监控通话质量,而 UDP 是承载 RTP/RTCP 的首选传输层协议SIP 信令传输可以选择 TCP\UDP,TCP 可靠性更高。

核心概念一览:

名称
作用
备注
SIP 客户端 (UA)
发起/接收通话的用户代理
客户端
SIP 服务器
注册管理、呼叫路由、认证
SIP服务器
SIP 协议
信令控制(建立、修改、结束会话)
传输REGISTER 、 INVITE、BYE等消息
SDP 协议
媒体协商(编码器、IP、端口等)
在 INVITE/200 OK 中携带,描述 RTP 参数
RTP 协议
传输实时语音数据
在协商好的 UDP 端口上发送语音包
RTCP 协议
监控 RTP 质量(丢包、抖动、延迟)
周期性发送报告
TCP/UDP
传输层协议
SIP 可用 TCP 或 UDP;RTP/RTCP 必须用 UDP

我们通过一个分层架构来进一步加深理解:

层级
名称
作用
4
应用/信令层(SIP)
负责“打电话”的动作:拨号、振铃、接听、挂断;SDP封装在SIP协议中,负责完成媒体协商
3
媒体传输层(RTP)
负责“说话的内容”:把声音/视频打包、传输、还原
2
传输控制层(RTCP)
负责“如何可靠/快速发送数据包”
1
网络/物理层(IP)
负责数据包从一台设备到另一台设备的物理传递

三、核心示例

1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;

2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;

3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/sip;

四、常量详解

核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;

每个常量对应的常量取值仅做日志打印时查询使用,不要将这个常量取值用做具体的业务逻辑判断,因为 LuatOS 内核固件可能会变更每个常量对应的常量取值;

如果用做具体的业务逻辑判断,一旦常量取值发生改变,业务逻辑就会出错;

4.1 exsip.TRANSPORT_UDP

常量含义:SIP信令传输协议,表示当前SIP信令选择的是UDP传输协议
数据类型:string;
示例代码:
local SIP_CONFIG = {
    sip_server_addr = "xxx.xxx.xxx.xxx",
    sip_server_port= xxxx,
    sip_domain = "xxx.xxx.xxx.xxx",
    sip_username= "1001",
    sip_password= "xxxxxxxxxxxxxxxxx",
    sip_transport = exsip.TRANSPORT_UDP
}

4.2 exsip.TRANSPORT_TCP

常量含义:SIP信令传输协议,表示当前SIP信令选择的是TCP传输协议
数据类型:string;
示例代码:
local SIP_CONFIG = {
    sip_server_addr = "xxx.xxx.xxx.xxx",
    sip_server_port= xxxx,
    sip_domain = "xxx.xxx.xxx.xxx",
    sip_username= "1001",
    sip_password= "xxxxxxxxxxxxxxxxx",
    sip_transport = exsip.TRANSPORT_TCP
}

五、函数详解

5.1 exsip.init(config)

功能

配置 SIP 参数

参数

config

参数含义:SIP 参数表。需要配置的只有以下 8 个参数:sip_server_addr、sip_server_port、sip_domain 、sip_username、sip_password、sip_transport 、auto_answer、delay_auto_answer,其中前五个参数必选。

数据类型:table,table 内容及格式说明如下:

{

​  参数含义:SIP服务器地址,用于客户端建立 TCP/UDP 连接,发送和接收 SIP 信令(REGISTER、        INVITE、BYE 等),它是客户端实际访问服务器的物理或逻辑地址,是`socket`连接的目标地址。

  数据类型:string

​  取值范围:有效的IP地址或域名;

  是否必选:是

  注意事项:

1. 服务器地址不能以 `sip:` 为前缀,也不应包含协议名称。
2. 在 NAT 环境中,服务器地址通常是公网 IP 或可公网解析的域名,不能填写客户端的私有 IP
3. 使用 IP 地址比域名更可靠(避免 DNS 解析失败或延迟),但域名便于负载均衡和更换后端服务器。
4. 如果服务器要求 TLS 加密,则地址不变,但端口通常改为 5061

 **参数名称:*****sip_server_addr***

参数含义:SIP 服务器监听的网络端口号,客户端通过该端口与服务器建立连接并收发 SIP 信令(REGISTER、INVITE、BYE 等)

数据类型:number

取值范围:1 ~ 65535,标准端口:5060(UDP/TCP);加密端口(TLS):5061

是否必选:是

注意事项:

  1. 必须与服务器实际监听的端口一致。
  2. 如果服务器使用非标准端口,客户端需显式配置,否则默认 5060 可能无法连接。
  3. 防火墙需放行该端口(UDP/TCP 取决于传输协议)。
  4. 某些运营商或云服务商会封锁 5060 端口,建议改用高位端口(如 5080、8910 等)

参数名称:sip_server_port

参数含义:SIP域名,SIP用户的逻辑归属域,标识用户所属组织、租户或者虚拟域。作用是构造用户标识: SIP URI 格式为sip:user@domain(如sip:``1001@example.com,域名为example.com),服务器可根据域名决定将呼叫路由到哪个域或租户

数据类型:string

取值范围:推荐使用域名格式(建议全小写),也可以使用与服务器地址相同的IP地址(内部测试/小规模部署)

是否必选:是

注意事项:

  1. SIP 域名必须与服务器上为该用户配置的 realm 完全一致(可通过抓包查看 WWW-Authenticate 头中的 realm= 值)。
  2. 如果服务器返回 401/403 认证错误,首先检查域名是否匹配。
  3. 在多租户场景下,不同租户使用不同的域名,但可能连接同一个服务器地址。
  4. 域名不能包含协议头(sip:)、端口号、路径或 URI 参数。
  5. 在构造 From/To 头时,域名会出现在 SIP URI 的 @ 后面,因此不要填写用户号码。
  6. 使用 IP 地址作为域名,此时 SIP URI 会变成 1001@IP地址,一般在开发、内部测试或者无域名解析服务的封闭网络(如工业内网)情况下使用,优点是配置简单,方便快速测试;缺点是灵活性差,TLS安全降级,多租户支持弱,兼容风险高

参数名称:sip_domain

参数含义:SIP用户名,用户在 SIP 服务器上的唯一标识,通常为分机号、电话号码或用户 ID,用于注册认证和来电路由。

数据类型:string

取值范围:4 ~ 20位数字或数字与字母的组合,不能包含空格,例如“1001”

是否必选:是

注意事项:

  1. 用户名必须与服务器上为该用户创建的账号完全一致。
  2. 同一个 SIP 域名下,用户名必须唯一。
  3. 用户名不应包含 @、空格、分号等特殊字符,否则需要进行转义。

转义规则:将字符的 ASCII 码(或 UTF-8 字节)用 % 后跟两位十六进制数表示。ASCII码表对照:ASCII码对照表,ASCII码一览表(非常详细) - C语言中文网 - @%40 - 空格 → %20 - ;%3B - 中文字符“张” → UTF-8 为 E5 BC A0%E5%BC%A0 示例:

正常用户名"1001"的SIP URI:sip:``1001@example.com → 用户名 = 1001,域名 = example.com

需要转义的用户名"user@company"的SIP URI:sip:``user@company@example.com , 解析器无法知道第一个 @ 是用户名的一部分还是分隔符。因此必须转义

转义后用户名:user%40company

完整 SIP URI:sip:user%``40company@example.com

参数名称:sip_username

参数含义:SIP密码,与用户名配套的认证凭证,用于 SIP Digest 摘要认证,验证用户身份合法性。

数据类型:string

取值范围:长度不少于6字节的可见字符组合,通常为字母、数字、标点符号,部分高要求场景要求密码长度至少8字节或12字节,常见的长度上限有32个字节,也有更高的128个字节或256个字节。长度在12 ~ 32个字节之间是一个相对安全且兼容性较好的范围。关键在于确保密码设置能够同时被SIP服务器和客户端设备所接受。(如果密码仅使用ASCII字符:英文字母、数字、基本符号,则1个字符 = 1字节,6字符 = 6字节)

是否必选:是

注意事项:

  1. 密码以明文形式配置在客户端,需注意保密(避免硬编码在公开代码中)。
  2. 密码不参与网络传输,而是用于计算 MD5 摘要(HA1 = MD5(username:realm:password)),因此网络抓包无法直接获取明文密码。
  3. 如果密码错误,服务器会返回 401 Unauthorized 或 403 Forbidden。
  4. 更改密码后,客户端需同步更新配置并重新注册。
  5. 某些服务器支持空密码,但强烈不推荐。

参数名称:sip_password

参数含义:SIP 信令所使用的底层传输层协议,决定了数据包的传输方式和可靠性

数据类型:string

取值范围:exsip.TRANSPORT_TCP、exsip.TRANSPORT_UDP

是否必选:否,默认为exsip.TRANSPORT_TCP

注意事项:

  1. 传输协议必须与服务器支持的协议一致。
  2. UDP 下 SIP 消息长度受 MTU 限制(通常 1500 字节),过大的消息(如携带大 SDP)可能导致分片或丢失,建议使用 TCP。
  3. TCP 会增加连接建立开销,但避免了 UDP 的 NAT 超时问题,更适合移动网络。
  4. TLS 需要客户端和服务器端配置证书,且增加 CPU 负载和延迟。
  5. 在 NAT 环境中,TCP 通常比 UDP 更稳定(NAT 对 TCP 映射超时时间更长)。
  6. 传输协议的选择不影响 RTP 媒体流(RTP 始终使用 UDP)。

参数名称:sip_transport

参数含义:控制 SIP 客户端在收到来电(INVITE)时是否自动接听,无需用户手动操作。启用后,当有来电呼入时,客户端自动回复 200 OK 并建立通话。

数据类型:boolean

取值范围:true启用自动接听,false禁用自动接听

是否必选:否,默认为false

注意事项:

  1. 自动接听适用于无人值守设备(如门禁对讲、广播终端、安防报警器等场景)。
  2. 启用自动接听后,设备会忽略用户接听动作,直接建立通话,可能导致隐私泄露或意外接听。
  3. 某些 SIP 服务器或终端支持通过 Alert-Info 头携带 auto-answer 标识,可配合实现更精细的控制。
  4. 自动接听时仍会执行完整的 SDP 协商(编码器、RTP 端口等),与手动接听流程一致。
  5. 若同时配置了延迟接听(delay_auto_answer),则会在延迟结束后才接听。

参数名称:auto_answer

参数含义:延迟接听的秒数,当自动接听(auto_answer)启用时,客户端在收到来电后等待指定的秒数再自动接听。可用于留出时间让用户取消接听或播放预提示音。

数据类型:number 取值范围:0 ~ 10 秒

是否必选:否,默认为0

注意事项:

  1. 延迟接听仅在 auto_answer`` = true 时生效。
  2. 延迟期间客户端通常仍会振铃或播放提示音(如“将在 X 秒后接听”),用户可通过挂断操作取消接听。
  3. 延迟接听可用于模拟“自动应答前提示”,避免意外接听后对方听到环境杂音。
  4. 如果延迟时间过长,主叫方可能提前挂断(超时)。

参数名称:delay_auto_answer

} 取值范围:参考参数含义内各字段说明

是否必选:

注意事项:参考参数含义内各字段说明

参数示例:-- 配置 SIP

``` local SIP_CONFIG = { sip_server_addr = "xxx.xxx.xxx.xxx", sip_server_port= xxxx, sip_domain = "xxx.xxx.xxx.xxx", sip_username= "1001", sip_password= "xxxxxxxxxxxxxxxxx", sip_transport = exsip.TRANSPORT_TCPauto_answer = false, delay_auto_answer = 0 }

返回值

 local init_result = exsip.init(config)

 init_result 

```Lua
含义说明:是否配置成功;
数据类型:boolean;
取值范围:true/false;
注意事项:true表示配置成功,false表示配置失败;
返回示例:
    if exsip.init(SIP_CONFIG) then
        log.info("sip", "配置完成")
    else
        log.info("sip", "配置失败")
    end

注:sip_server_addr和sip_domain 区别


sip_server_addr(SIP 服务器地址)
sip_domain (SIP 域名)
定义
SIP 服务器的网络位置(IP 或可解析的域名)
用户的逻辑归属域,通常与认证域(realm)相同
用途
建立 TCP/UDP 连接,发送/接收 SIP 信令
构造 SIP URI(`user@domain`),用于 Digest 认证,标识用户所属组织
出现位置
`socket.connect` 的目标地址,不在 SIP 头中直接出现(除非等于域名)
出现在 `From`、`To`、`Contact` 头的 URI 中,以及 `realm` 认证参数中
取值要求
必须是可路由的 IP 地址或 DNS 可解析的域名(可含端口,但通常端口单独配置)
通常是域名格式(如 `example.com`),也可以使用 IP 地址,但推荐域名
配置方式
客户端配置项如 `server`、`outbound_proxy`
客户端配置项如 `domain`、`realm`

5.2 exsip.start()

功能

启动已初始化的 SIP 客户端服务,包括设置 VoIP 回调、建立与 SIP 服务器的连接、发起注册,并开始处理信令和媒体事件。该函数必须在 exsip.init() 成功调用之后执行。

参数

返回值

local start_result = exsip.start()

start_result

含义说明:是否启动成功;
数据类型:boolean
取值范围:true/falsetrue表示启动成功false表示启动失败
注意事项:
    调用前必须已通过 exsip.init(config) 完成配置,且配置表中必须包含 sip_server_addrsip_usernamesip_password 等必要字段。
    启动成功后,SIP 客户端会自动向服务器发起注册,注册结果通过 exsip.on() 设置的回调函数中的 "register" 事件返回。
返回示例:true

示例

if exsip.init(SIP_CONFIG) then
    log.info("sip", "配置完成")
    if exsip.start() then
        log.info("sip", "启动成功")
    end
end

5.3 exsip.stop()

功能

停止已启动的 SIP 客户端服务,释放相关资源。包括停止 VoIP 引擎(挂断当前通话并关闭 RTP 媒体)、停止 SIP 信令任务(关闭 socket 连接、停止注册续租定时器)、重置运行状态标志和当前通话信息。

参数

返回值

注意事项

  1. 停止后,SIP 客户端会从服务器注销(或等待注册自动过期),不再接收来电和信令。
  2. 停止操作是同步的,但底层 socket 关闭和任务退出是异步的,会有短暂延迟,所以停止之后等待100 ~ 200毫秒再调用 exsip.start(),否则会出现端口冲突、注册失败、资源泄露等问题。
  3. 停止后可以再次调用 exsip.start() 重新启动服务,配置保持为上次 init 的值。
  4. 在设备进入休眠、网络断开或应用退出前调用该函数,以优雅释放资源。

示例

exsip.stop()

5.4 exsip.dial(target)

功能

发起一个外呼电话,目标可以是分机号码、完整电话号码或 SIP URI。该函数通过向 SIP 服务器发送 INVITE 请求来建立通话,通话建立成功后的事件通过 exsip.on() 回调返回。

参数

target

参数含义:目标号码
数据类型:string
取值范围:4 ~ 20位数字或数字与字母的组合,不能包含空格,例如“1001”
是否必选:是,
注意事项:
    用户名必须与服务器上为该用户创建的账号完全一致。
    同一个 SIP 域名下,用户名必须唯一。
    用户名不应包含 @、空格、分号等特殊字符,否则需要进行转义
参数示例:exsip.dial("1002")
点击查看转义规则

返回值

local dail_result = exsip.dail(target)

dail_result

含义说明:是否拨打成功;
数据类型:boolean
取值范围:true/false
注意事项:此处返回值只代表发起请求拨打的结果,true表示请求成功false表示请求失败
        会通过exsip.on注册一个通话的事件回调,根据传入的事件判断通话状态;
        具体使用参照exsip.on说明
返回示例:true

注意事项

  1. 调用前必须确保 SIP 服务已启动(exsip.start() 成功且注册完成),否则返回 false 并打印错误日志。
  2. 如果当前已有通话存在(无论是拨出还是来电未接),再次调用 dial 会被拒绝,新通话会被忽略,不影响原通话,日志打印会出现 sip busy。
  3. 拨号操作是异步的,函数返回 true 仅表示命令已发送,实际通话建立或失败需通过 call 事件(如 "connected""failed")获知。
  4. 拨号后可通过 exsip.hangUp() 取消尚未接通的呼叫(发送 CANCEL)。

示例

if exsip.dail("1002") then
    log.info("sip", "正在发起拨打电话的请求")
else
    log.info("sip", "请求拨号失败")
end

5.5 exsip.accept()

功能

接听当前等待中的来电(即应答对方发起的 INVITE 请求),发送 200 OK 响应并建立通话。该函数适用于手动接听模式(即未开启自动接听时)。

参数

返回值

local accept_result = exsip.accept()

accept_result

含义说明:是否成功接听;
数据类型:boolean
取值范围:true/false
注意事项:此处返回值只代表发起请求接听的结果,true表示请求成功false表示请求失败
        会通过exsip.on注册一个通话的事件回调,根据传入的事件判断通话状态;
        具体使用参照exsip.on说明
返回示例:true

注意事项

  1. 调用前必须确保 SIP 服务已启动(exsip.start() 成功),且存在来电(收到 call 事件中的 "incoming" 子事件)。
  2. 若没有来电或来电已被处理(已接听、已拒绝、已挂断),调用该函数会失败(返回 false)并打印错误日志。
  3. 接听操作是异步的,函数返回 true 仅表示命令已发送,实际通话建立需通过 call 事件中的 "connected" 子事件确认。
  4. 如果已开启自动接听(auto_answer`` = true),通常无需手动调用 accept(),但调用也不会出错(可能重复接听)。
  5. 接听后,SDP 协商(编解码器、RTP 端口等)会自动完成,媒体会话建立后触发 media 事件中的 "ready"
  6. 接听前可通过 exsip.hangUp() 拒绝来电(发送 486 Busy Here)。
  7. 接听成功后,可通过 exsip.get_current_call() 获取当前通话信息。

示例

if exsip.accept() then
    log.info("sip", "正在发起接听电话的请求")
else
    log.info("sip", "请求接听失败")
end

5.6 exsip.hangUp()

功能

挂断当前进行中的通话,或拒绝尚未接听的来电。对于已建立的通话,发送 BYE 请求结束会话;对于振铃中的来电,发送 486 Busy Here 拒绝接听;对于正在呼叫尚未接通的外呼,发送 CANCEL 取消呼叫。

参数

返回值

local hangup_result = exsip.hangUp()

hangup_result

含义说明:是否成功挂断;
数据类型:boolean
取值范围:true/false
注意事项:此处返回值只代表发起请求挂断的结果,true表示请求成功false表示请求失败
        会通过exsip.on注册一个通话的事件回调,根据传入的事件判断通话状态;
        具体使用参照exsip.on说明
返回示例:true

注意事项

  1. 调用前必须确保 SIP 服务已启动(exsip.start() 成功),否则返回 false
  2. 若当前没有通话(无振铃来电、无进行中的呼叫、无已建立的通话),调用该函数会返回 true 但不会产生实际信令。
  3. 挂断操作是异步的,函数返回 true 仅表示命令已发送,实际挂断完成需通过 call 事件中的 "ended" 子事件确认。
  4. 对于外呼未接通状态,hangUp 会发送 CANCEL 请求,而非 BYE。
  5. 对于来电振铃状态,hangUp 会发送 486 Busy Here 拒绝,并清除来电缓存。
  6. 挂断成功后,RTP 媒体流结束。

示例

if exsip.hangUp() then
    log.info("sip", "正在挂断...")
else
    log.info("sip", "挂断失败")
end

5.7 exsip.on(callback)

功能

注册事件回调函数

参数

callback

参数含义:事件回调函数;回调函数的格式为:
function sip_callback(event, arg1, arg2, arg3)
参数
{
参数含义:触发回调的事件
数据类型:string
取值范围:
"register":注册状态事件;
"ready":服务就绪事件;
"call":通话状态事件;
"media":媒体状态事件;
“message”:即时消息事件
"voip":VoIP 状态事件;
"lifecycle":生命周期事件;
"error":错误事件;
参数名称:event

arg1 为注册状态,数据类型为:boolean(true 或 false),true 表示注册成功,false 表示注册失败
arg2 为附加数据,数据类型为:table,
{
arg2.expires 表示注册有效期,单位:秒;
arg2.headers 表示 SIP 响应头;
}
event = "ready":无额外参数(arg1、arg2、arg3 均为 nil)。
event = "call":
arg1 为子事件 sub_event,包括:"incoming"、"ringing"、"connected"、"ended";
arg1="incoming",arg2 为通话信息表;数据类型为 table,
{
from, -- 来电号码,数据类型:string
call_id, -- 通话 ID,数据类型:string
headers, -- SIP 请求头,数据类型:table
body, -- SDP 内容,数据类型:string
remote_sdp -- 解析后的远端 SDP,数据类型:table<br / };
arg1=""ringing",arg2=nil;arg3=nil
arg1="connected",arg2=nil;arg3=nil
arg1="ended",arg2 数据类型为 table,
{
reason, -- 结束原因,数据类型:string
dialog -- 通话对象,数据类型:table
}
event = "media":
arg1 为子事件 sub_event,包括:"ready"、"stop";
arg1="ready", arg2 数据类型为 table,
{
call_id, -- 通话 ID,数据类型:string
remote_ip, -- 远端 IP 地址,数据类型:string
remote_port, -- 远端端口,数据类型:number
remote_direction, -- 媒体方向,数据类型:string
remote_codecs, -- 远端编解码器列表,数据类型:table
local_rtp_port, -- 本地 RTP 端口,数据类型:number
local_codecs, -- 本地编解码器列表,数据类型:table
codec, -- 选定的编解码器 ("PCMU"/"PCMA"),数据类型:string
payload_type, -- RTP payload type,数据类型:number
sample_rate, -- 采样率,数据类型:number
channels, -- 声道数,数据类型:number
bits, -- 位深,数据类型:number
ptime, -- 打包时长(毫秒),数据类型:number
local_sdp, -- 本地 SDP,数据类型:table
remote_sdp, -- 远端原始 SDP,数据类型:string
source -- 来源标识,数据类型:string
}
arg1="stop",arg2 数据类型为 table,
{
reason, -- 停止原因,数据类型:string
session -- 媒体会话对象,数据类型:table
}
event = "message":
arg1 为子事件 sub_event,包括:"rx"、"sent";
arg1="rx",,arg2 数据类型为 table,
{
body, -- 消息内容,数据类型:string
text -- 消息文本(同 body),数据类型:string
}
arg1="sent",arg2 数据类型为 table,
{
message, -- 消息对象,数据类型:table
to, -- 接收者号码,数据类型:string
text, -- 消息文本,数据类型:string
body, -- 消息内容,数据类型:string
Code -- 响应码 (200/202),数据类型:number
}
event = "voip":
arg1 为子事件 sub_event,包括:"state"、"stats"、"error";
arg1="state",arg2 数据类型为 string,表示 voip 状态变化;
arg1="stats",arg2 数据类型为 table,
{
tx_packets, -- 发送的 RTP 包数,数据类型:number
tx_bytes, -- 发送的字节数,数据类型:number
rx_packets, -- 接收的 RTP 包数,数据类型:number
rx_bytes, -- 接收的字节数,数据类型:number
rx_parse_fail, -- RTP 解析失败数,数据类型:number
rx_bad_payload, -- 无效 payload 数,数据类型:number
rx_lost, -- 丢包数,数据类型:number
rx_out_of_order, -- 乱序包数,数据类型:number
jb_played, -- jitter buffer 播放数,数据类型:number
jb_silence, -- jitter buffer 静音填充数,数据类型:number
}
arg1="error",arg2 数据类型为 string/number,表示 voip 错误信息或错误码;
event = "lifecycle":
arg1 为 sip 服务在线状态,包括:"online","offline","stopped";
arg1="online",arg2 数据类型为 table,
{
server, -- SIP 服务器地址,数据类型:string
port, -- SIP 服务器端口,数据类型:number
transport, -- 传输协议 ("udp"/"tcp"/"tls"),数据类型:string
local_ip -- 本地 IP 地址,数据类型:string
}
arg1="offline",arg2 数据类型为 table,
{
reason, -- 下线原因,数据类型:string
}
arg1="stopped",arg2 数据类型为 nil;
event = "error":
arg1 为错误类型,包括:"net","rx_failed";
arg1="net", arg2 数据类型为 table,
{
event, -- socket 事件,数据类型:string
param -- socket 参数,数据类型:number
}
arg1="rx_failed", arg2 为 nil
参数名称:arg1、arg2、arg3
}
是否必选:是
注意事项:

  1. 多次调用 exsip.on() 会覆盖之前注册的回调。
  2. 回调函数应保持简短,避免长时间阻塞,否则可能影响 SIP 信令处理或媒体流。
  3. 若要取消特定事件的回调,可调用 exsip.off(event_type);若要取消所有回调,可对每个事件类型调用 exsip.off 或传入一个空函数 exsip.on(nil)
  4. 不同事件类型对应的后续参数不同,具体可参考 API 文档或示例代码中的分发逻辑。
  5. 回调中不要调用可能引起重入的操作(如再次 exsip.start()exsip.stop()),以免状态混乱。 参数示例:exsip.on(callback)

返回值

示例

-- 注册状态回调_
local function sip_callback(event, arg1, arg2, arg3)
    if event == "register" then
        local status, data = arg1, arg2
        if status == "ok" then
            log.info("sip", "注册成功,有效期:", data.expires, "SIP响应头:", data.headers)
        elseif status == "failed" then
            log.error("sip", "注册失败")
        end
    elseif event == "ready" then
        log.info("sip", "SIP 服务已就绪")
    elseif event == "call" then
        local sub_event, data = arg1, arg2
        if sub_event == "incoming" then
            log.info("sip", "来电:", data.fromdata.call_iddata.uridata.headers,bodydata.remote_sdp)
        elseif sub_event == "ringing" then
            log.info("sip", "对方响铃中")
        elseif sub_event == "connected" then
            log.info("sip", "通话已建立")
        elseif sub_event == "ended" then
            log.info("sip", "通话已结束,结束原因为:"data.reason,"通话对象:","data.dialog")
        end
    elseif event == "media" then
        local sub_event, session = arg1, arg2
        if sub_event == "ready" then
            log.info("sip", "媒体通道就绪", session.remote_ip .. ":" .. session.remote_port)
        elseif sub_event == "stop" then
            log.info("sip", "媒体通道已关闭,关闭原因:",session.reason)
        end
    elseif event == "message" then
        local sub_event, data = arg1, arg2
        if sub_event == "rx" then
            log.info("sip", "收到消息:", data.from, data.body)
        elseif sub_event == "sent" then
            log.info("sip", "消息已发送到:", data.to,"消息内容为:",data.body)
        end
    elseif event == "voip" then
        local sub_event, data = arg1, arg2
        if sub_event == "state" then
            log.info("voip", "状态:", data)
        elseif sub_event == "stats" then
            log.info("voip", "统计 - 发送:", data.tx_packets,
                "接收:", data.rx_packets,
                "丢失:", data.rx_lost)
        elseif sub_event == "error" then
            log.error("voip", "错误:", data)
        end
    elseif event == "lifecycle" then
        local action = arg1
        log.info("sip", "lifecycle event:", action)
        if action == "online" then
            g_sip_started = true
            log.info("sip", "SIP 服务已在线")
        elseif action == "offline" or action == "stopped" then
            g_sip_started = false
            log.info("sip", "SIP 服务已离线/停止")
        end
    elseif event == "error" then
        local action, payload = arg1, arg2
        log.error("sip", "错误:", action, payload.event, payload.param)
    end
end

function sip_accept.init()
    exsip.on(sip_callback)
    ... ...
end

5.8 exsip.off(event)

功能

取消事件回调。移除通过 exsip.on() 注册的回调函数,移除后,该事件类型将不再触发回调。

参数

event

参数含义:事件名称
数据类型:string
取值范围:"register"、"ready"、"call"、"media"、"message"、"voip"、"error"。
        如果传入其他字符串,不会报错,但无任何效果(因为没有对应的事件回调可移除)
是否必选:是,
注意事项:
参数示例:exsip.off("call")

返回值

注意事项:

  1. 该函数仅移除指定事件类型的回调,不影响其他事件类型的回调。
  2. 如果从未为指定事件注册过回调,调用 exsip.off(event) 无副作用。
  3. 若要移除所有事件的回调,需要依次对每个支持的事件类型调用 exsip.off(event)
  4. 移除回调后,后续发生的该事件将不再调用任何回调函数(除非重新通过 exsip.on() 注册)。
  5. 传入 nil 或非字符串类型不会产生错误,但也不会移除任何回调(建议传入有效的事件名称)。

示例

exsip.off("call")

5.9 exsip.get_current_call()

功能

返回当前正在进行的通话或振铃中的来电的信息表。若无任何通话(无振铃、无呼叫中、无已建立通话),则返回 nil

参数

返回值

local current_call = exsip.get_current_call()

current_call

含义说明:;
数据类型:table
取值范围:有通话时返回一个表单,内容包括from:主叫方 SIP URI 或显示号码(字符串)或call_id:本次通话的唯一标识(字符串);
         无通话时返回nil
注意事项:该函数仅返回当前通话的信息,不支持多路通话;
返回示例:
local current_call = exsip.get_current_call()
if current_call then
    log.info("来电号码:", current_call .from)
end

注意事项

  1. 该函数仅返回当前通话的信息,不支持多路通话。
  2. call 事件的 "incoming" 子事件触发后,get_current_call() 即可返回来电信息。
  3. 返回的表是内部引用的副本或直接引用,不应修改其内容,以免影响内部状态。
  4. 如果同时存在振铃来电和已建立通话(极端情况),本库只保留最后活跃的通话信息,建议在事件处理中自行缓存。

示例

local function sip_callback(event, arg1, arg2, arg3)
    if ...
    elseif event == "call" then
        local sub_event, data = arg1, arg2
        if sub_event == "incoming" then
            log.info("sip", "来电:", data.from)
            local current_call = exsip.get_current_call()
            if current_call then
                log.info("来电号码:", current_call.from)
            end
            exsip.answer()
            ...
     end
   end
end