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,会话初始协议)通话流程大致分为四个阶段:
-
注册(由 SIP 负责) 终端(如 IP 电话)向 SIP 服务器注册,服务器验证身份后,记录终端当前的 IP 地址和端口。 SIP 客户端必须向服务器(sip_server_addr)告知自己的域名(sip_domain )、端口(sip_server_port)、用户名(sip_username)、密码(sip_password)以便服务器可以将来电路由到正确的设备。
-
呼叫建立(邀请与应答,由 SIP 负责)
- 主叫发送
INVITE请求到 SIP 服务器 - 服务器定位被叫并转发
INVITE - 被叫振铃,返回
Ringing(180 响应) - 被叫接听后返回
OK(200 响应) - 主叫发送
ACK确认,通话建立
- 主叫发送
-
媒体传输(由 RTP/RTCP 负责)
SIP 完成建立连接的“握手”后,就退出了实时数据传输的环节,双方通过 RTP 直接交换语音/视频流,声音被设备麦克风采集、编码、打包成 RTP 包,通过网络发送给对方;对方做相反的解码和播放;通话期间,双方每间隔 5 秒左右发送 RTCP 报告包。
- 通话结束(由 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
是否必选:是
注意事项:
- 必须与服务器实际监听的端口一致。
- 如果服务器使用非标准端口,客户端需显式配置,否则默认 5060 可能无法连接。
- 防火墙需放行该端口(UDP/TCP 取决于传输协议)。
- 某些运营商或云服务商会封锁 5060 端口,建议改用高位端口(如 5080、8910 等)
参数名称:sip_server_port
参数含义:SIP域名,SIP用户的逻辑归属域,标识用户所属组织、租户或者虚拟域。作用是构造用户标识: SIP URI 格式为
sip:user@domain(如sip:``1001@example.com,域名为example.com),服务器可根据域名决定将呼叫路由到哪个域或租户数据类型:string
取值范围:推荐使用域名格式(建议全小写),也可以使用与服务器地址相同的IP地址(内部测试/小规模部署)
是否必选:是
注意事项:
- SIP 域名必须与服务器上为该用户配置的
realm完全一致(可通过抓包查看WWW-Authenticate头中的realm=值)。- 如果服务器返回 401/403 认证错误,首先检查域名是否匹配。
- 在多租户场景下,不同租户使用不同的域名,但可能连接同一个服务器地址。
- 域名不能包含协议头(
sip:)、端口号、路径或 URI 参数。- 在构造
From/To头时,域名会出现在 SIP URI 的@后面,因此不要填写用户号码。- 使用 IP 地址作为域名,此时 SIP URI 会变成
1001@IP地址,一般在开发、内部测试或者无域名解析服务的封闭网络(如工业内网)情况下使用,优点是配置简单,方便快速测试;缺点是灵活性差,TLS安全降级,多租户支持弱,兼容风险高参数名称:sip_domain
参数含义:SIP用户名,用户在 SIP 服务器上的唯一标识,通常为分机号、电话号码或用户 ID,用于注册认证和来电路由。
数据类型:string
取值范围:4 ~ 20位数字或数字与字母的组合,不能包含空格,例如“1001”
是否必选:是
注意事项:
- 用户名必须与服务器上为该用户创建的账号完全一致。
- 同一个 SIP 域名下,用户名必须唯一。
- 用户名不应包含
@、空格、分号等特殊字符,否则需要进行转义。转义规则:将字符的 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字节)
是否必选:是
注意事项:
- 密码以明文形式配置在客户端,需注意保密(避免硬编码在公开代码中)。
- 密码不参与网络传输,而是用于计算 MD5 摘要(HA1 = MD5(username:realm:password)),因此网络抓包无法直接获取明文密码。
- 如果密码错误,服务器会返回 401 Unauthorized 或 403 Forbidden。
- 更改密码后,客户端需同步更新配置并重新注册。
- 某些服务器支持空密码,但强烈不推荐。
参数名称:sip_password
参数含义:SIP 信令所使用的底层传输层协议,决定了数据包的传输方式和可靠性
数据类型:string
取值范围:exsip.TRANSPORT_TCP、exsip.TRANSPORT_UDP
是否必选:否,默认为exsip.TRANSPORT_TCP
注意事项:
- 传输协议必须与服务器支持的协议一致。
- UDP 下 SIP 消息长度受 MTU 限制(通常 1500 字节),过大的消息(如携带大 SDP)可能导致分片或丢失,建议使用 TCP。
- TCP 会增加连接建立开销,但避免了 UDP 的 NAT 超时问题,更适合移动网络。
- TLS 需要客户端和服务器端配置证书,且增加 CPU 负载和延迟。
- 在 NAT 环境中,TCP 通常比 UDP 更稳定(NAT 对 TCP 映射超时时间更长)。
- 传输协议的选择不影响 RTP 媒体流(RTP 始终使用 UDP)。
参数名称:sip_transport
参数含义:控制 SIP 客户端在收到来电(INVITE)时是否自动接听,无需用户手动操作。启用后,当有来电呼入时,客户端自动回复 200 OK 并建立通话。
数据类型:boolean
取值范围:true启用自动接听,false禁用自动接听
是否必选:否,默认为false
注意事项:
- 自动接听适用于无人值守设备(如门禁对讲、广播终端、安防报警器等场景)。
- 启用自动接听后,设备会忽略用户接听动作,直接建立通话,可能导致隐私泄露或意外接听。
- 某些 SIP 服务器或终端支持通过
Alert-Info头携带auto-answer标识,可配合实现更精细的控制。- 自动接听时仍会执行完整的 SDP 协商(编码器、RTP 端口等),与手动接听流程一致。
- 若同时配置了延迟接听(
delay_auto_answer),则会在延迟结束后才接听。参数名称:auto_answer
参数含义:延迟接听的秒数,当自动接听(
auto_answer)启用时,客户端在收到来电后等待指定的秒数再自动接听。可用于留出时间让用户取消接听或播放预提示音。数据类型:number 取值范围:0 ~ 10 秒
是否必选:否,默认为0
注意事项:
- 延迟接听仅在
auto_answer`` = true时生效。- 延迟期间客户端通常仍会振铃或播放提示音(如“将在 X 秒后接听”),用户可通过挂断操作取消接听。
- 延迟接听可用于模拟“自动应答前提示”,避免意外接听后对方听到环境杂音。
- 如果延迟时间过长,主叫方可能提前挂断(超时)。
参数名称: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_TCP, auto_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/false;true表示启动成功,false表示启动失败;
注意事项:
调用前必须已通过 exsip.init(config) 完成配置,且配置表中必须包含 sip_server_addr、sip_username、sip_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 连接、停止注册续租定时器)、重置运行状态标志和当前通话信息。
参数
无
返回值
无
注意事项
- 停止后,SIP 客户端会从服务器注销(或等待注册自动过期),不再接收来电和信令。
- 停止操作是同步的,但底层 socket 关闭和任务退出是异步的,会有短暂延迟,所以停止之后等待100 ~ 200毫秒再调用
exsip.start(),否则会出现端口冲突、注册失败、资源泄露等问题。 - 停止后可以再次调用
exsip.start()重新启动服务,配置保持为上次init的值。 - 在设备进入休眠、网络断开或应用退出前调用该函数,以优雅释放资源。
示例
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
注意事项
- 调用前必须确保 SIP 服务已启动(
exsip.start()成功且注册完成),否则返回false并打印错误日志。 - 如果当前已有通话存在(无论是拨出还是来电未接),再次调用
dial会被拒绝,新通话会被忽略,不影响原通话,日志打印会出现 sip busy。 - 拨号操作是异步的,函数返回
true仅表示命令已发送,实际通话建立或失败需通过call事件(如"connected"、"failed")获知。 - 拨号后可通过
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
注意事项
- 调用前必须确保 SIP 服务已启动(
exsip.start()成功),且存在来电(收到call事件中的"incoming"子事件)。 - 若没有来电或来电已被处理(已接听、已拒绝、已挂断),调用该函数会失败(返回
false)并打印错误日志。 - 接听操作是异步的,函数返回
true仅表示命令已发送,实际通话建立需通过call事件中的"connected"子事件确认。 - 如果已开启自动接听(
auto_answer`` = true),通常无需手动调用accept(),但调用也不会出错(可能重复接听)。 - 接听后,SDP 协商(编解码器、RTP 端口等)会自动完成,媒体会话建立后触发
media事件中的"ready"。 - 接听前可通过
exsip.hangUp()拒绝来电(发送 486 Busy Here)。 - 接听成功后,可通过
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
注意事项
- 调用前必须确保 SIP 服务已启动(
exsip.start()成功),否则返回false。 - 若当前没有通话(无振铃来电、无进行中的呼叫、无已建立的通话),调用该函数会返回
true但不会产生实际信令。 - 挂断操作是异步的,函数返回
true仅表示命令已发送,实际挂断完成需通过call事件中的"ended"子事件确认。 - 对于外呼未接通状态,
hangUp会发送 CANCEL 请求,而非 BYE。 - 对于来电振铃状态,
hangUp会发送 486 Busy Here 拒绝,并清除来电缓存。 - 挂断成功后,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":错误事件;
参数名称:eventarg1 为注册状态,数据类型为: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
}
是否必选:是
注意事项:
- 多次调用
exsip.on()会覆盖之前注册的回调。- 回调函数应保持简短,避免长时间阻塞,否则可能影响 SIP 信令处理或媒体流。
- 若要取消特定事件的回调,可调用
exsip.off(event_type);若要取消所有回调,可对每个事件类型调用exsip.off或传入一个空函数exsip.on(nil)。- 不同事件类型对应的后续参数不同,具体可参考 API 文档或示例代码中的分发逻辑。
- 回调中不要调用可能引起重入的操作(如再次
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.from,data.call_id,data.uri,data.headers,body,data.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")
返回值
无
注意事项:
- 该函数仅移除指定事件类型的回调,不影响其他事件类型的回调。
- 如果从未为指定事件注册过回调,调用
exsip.off(event)无副作用。 - 若要移除所有事件的回调,需要依次对每个支持的事件类型调用
exsip.off(event)。 - 移除回调后,后续发生的该事件将不再调用任何回调函数(除非重新通过
exsip.on()注册)。 - 传入
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
注意事项
- 该函数仅返回当前通话的信息,不支持多路通话。
- 在
call事件的"incoming"子事件触发后,get_current_call()即可返回来电信息。 - 返回的表是内部引用的副本或直接引用,不应修改其内容,以免影响内部状态。
- 如果同时存在振铃来电和已建立通话(极端情况),本库只保留最后活跃的通话信息,建议在事件处理中自行缓存。
示例
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