voip 语音通话(SIP/RTP 媒体)
一、概述
voip 是合宙 LuatOS 的 SIP 通话媒体库,负责把语音按 RTP 打包收发、编解码、抖动缓冲(jitter buffer)与回声消除(AEC)。它只做“媒体面”,信令面(REGISTER/INVITE/SDP 协商)由 SIP 协议栈完成,两者配合才是一通完整的 SIP 电话。
要点:
1、模块名为 voip。典型流程是 voip.start(opts) 建链 → voip.on("state", cb) 观察状态 → voip.stop() 挂断;参数里的对端地址与端口必须来自 SDP 协商结果,不能自己编;
2、voip 与 cc 是两个不同的库:cc 面向运营商 VoLTE 通话(走蜂窝语音通道),voip 面向纯 IP 的 SIP/RTP 通话(走 socket),同型号模组上两者可以并存但用途不同;
3、音频有两种工作模式:voip.AUDIO_MODE_I2S(默认,库直接驱动 codec/I2S 硬件)与 voip.AUDIO_MODE_BRIDGE(纯 PCM 桥接,库里不碰 codec,由脚本用 pcmIn/pcmOut 自己搬数据)。后者用于没有 codec 芯片、或要把语音接到别处(如 CC 桥接)的场景;
4、通话中的播报(TTS/文件)、录音、DTMF、SRTP 加密都是可裁剪的子功能,各自有编译开关,固件没开时对应函数直接是 nil。
⚠️ 该库需要固件在编译时打开 VoIP 组件(源码标记
LUAT_USE_VOIP)。子功能开关分别是LUAT_USE_VOIP_PLAY(playTTS/playFile)、LUAT_USE_VOIP_RECORD(record)、LUAT_USE_VOIP_BRIDGE(pcmIn/pcmOut/pcmResampler/setAudioMode/bridgeTone/bridgeMute)、LUAT_USE_SRTP(security)。脚本里voip为 nil 表示整库缺失,voip.playTTS为 nil 表示只缺子功能,两种情况都要找对接 FAE 确认固件版本。⚠️
voip.G729、voip.G722也是条件导出的常量:固件没编入对应编解码器时它们是nil,此时voip.start里传 2 或 3 会失败,判断方法就是先看常量是否存在。
二、核心示例
-- 前提:SIP 信令已完成 SDP 协商,拿到对端 IP、端口、编解码与 ptime
voip.on("state", function(state)
log.info("voip", "state", state) -- starting / running / stopping / idle
end)
voip.on("error", function(err)
log.error("voip", "error", err)
end)
-- 每 5 秒回调一次统计,便于观察丢包与抖动
voip.on("stats", function(s)
log.info("voip", "tx", s.tx_packets, "rx", s.rx_packets, "lost", s.rx_lost)
end)
local ok = voip.start({
remote_ip = "192.168.1.100", -- SDP 里的对端地址
remote_port = 10000, -- SDP 里的对端端口
local_port = 10000, -- 本端端口,必须与 SDP 里通告的一致
transport = "udp",
codec = voip.PCMU, -- 与协商结果一致
ptime = 20,
jitter_depth = 3,
aec = true,
aec_mode = "speex",
stats_interval = 5000,
})
if not ok then
log.error("voip", "start failed")
return
end
-- 通话中按键:先取当前会话号,避免旧按键串到下一通电话
local session = voip.getSession()
if session then
voip.sendDtmf("2", 160, session)
end
-- 挂断
voip.stop()
桥接模式(无 codec 芯片、自己做采集播放)的写法:
voip.setAudioMode(voip.AUDIO_MODE_BRIDGE)
voip.start({ remote_ip = "192.168.1.100", remote_port = 10000,
local_port = 10000, transport = "udp", codec = voip.PCMU })
sys.taskInit(function()
while voip.isRunning() do
local pcm = voip.pcmOut(160) -- 取出下行 8kHz PCM16
if pcm then
audio_play(pcm) -- 业务侧送到喇叭
end
sys.wait(20)
end
end)
三、常量详解
3.1 编解码常量
| 常量 | 值 | 含义 |
|---|---|---|
| voip.PCMU | 0 | G.711 μ-law(PCMU,payload type 0),8kHz 窄带,默认编码 |
| voip.PCMA | 1 | G.711 A-law(PCMA,payload type 8),8kHz 窄带 |
| voip.G729 | 2 | G.729A,仅 8kHz,ptime 必须是 10ms 的整数倍;需固件启用 LUAT_SUPPORT_G729 |
| voip.G722 | 3 | G.722,仅 16kHz 宽带 64kbps,ptime 必须是 20ms 的整数倍,启用后 AEC 自动关闭;需固件启用 LUAT_SUPPORT_G722 |
注意事项:
1、voip.G729 / voip.G722 未编入固件时是 nil,用之前要判断,不要直接传数字 2/3;
2、G.722 是宽带编码,采样率必须是 16kHz;把 sample_rate 留成默认的 8000 会和协商结果对不上,对端听到的是变调的声音;
3、编解码必须与 SDP 协商结果一致,不能本端随便挑一个更省带宽的。
3.2 传输方式常量
| 常量 | 值 | 含义 |
|---|---|---|
| voip.TRANSPORT_UDP | 0 | 默认,RTP over UDP |
| voip.TRANSPORT_TCP | 1 | RTP over TCP,本端主动连对端 |
| voip.TRANSPORT_TCP_PASSIVE | 2 | RTP over TCP,本端监听,等平台连进来 |
TCP 承载时 RTP 按 RFC 4571 做长度定界,与纯 UDP 的报文边界不同,所以 transport 必须与 SDP 里 m=audio ... TCP/RTP/AVP 的写法一致,否则对端解不出包。
3.3 音频模式常量
| 常量 | 值 | 含义 |
|---|---|---|
| voip.AUDIO_MODE_I2S | 0 | 默认,库直接驱动音频硬件(codec/I2S),脚本不用管 PCM |
| voip.AUDIO_MODE_BRIDGE | 1 | 纯 PCM 桥接,库不碰 codec,由脚本用 pcmIn/pcmOut 搬数据;需 LUAT_USE_VOIP_BRIDGE |
3.4 版本探测常量
| 常量 | 值 | 含义 |
|---|---|---|
| voip.DTMF_VERSION | 1 | RTP DTMF(RFC 4733)接口版本号,用于探测固件是否支持 sendDtmf 与 dtmf 事件;老固件里该常量为 nil |
四、函数详解
4.1 voip.start(opts)
功能
启动一路 SIP/RTP 媒体会话:绑定本端端口、连上对端、按协商参数开始收发语音。
参数
opts
参数含义:通话配置表
数据类型:table
取值范围:见下表字段
是否必选:是
注意事项:不是 table 时直接返回 false 并打印错误日志,不会抛 Lua 错误。
opts 各字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| remote_ip | string | 无 | 对端 IP,来自 SDP 的 c= 行。tcp_passive 时可省略 |
| remote_port | number | 0 | 对端 RTP 端口,来自 SDP 的 m= 行。tcp_passive 时可省略 |
| local_port | number | 0 | 本端端口,必须与 SDP 通告一致;tcp_passive 时必填 |
| transport | string/number | "udp" |
"udp" / "tcp"(主动连)/ "tcp_passive"(监听),也可直接传 3.2 节的传输方式常量值 |
| codec | number | voip.PCMU |
编码方式,取 3.1 中的常量 |
| ptime | number | 20 | 打包时长(毫秒)。G729 须为 10 的倍数,G722 须为 20 的倍数 |
| ssrc | number | 0 | 发送端 SSRC,0 表示随机生成;GB28181 的 SDP y= 行要用它 |
| sample_rate | number | 8000 | 采样率,窄带 8000、G722 用 16000 |
| jitter_depth | number | 3 | 抖动缓冲深度(包数),越大越抗抖动但延迟越高 |
| multimedia_id | number | 0 | 多媒体通道号,与 audio/codec 侧的通道对应 |
| stats_interval | number | 5000 | 统计回调间隔(毫秒),0 表示不回调 |
| aec | boolean | false | 是否开启回声消除 |
| aec_mode | string | "speex" |
"speex" 或 "bk"(黑盒模式),写错直接返回 false |
| aec_denoise | boolean | 跟随 aec | 是否开降噪;不填时等于 aec 的取值 |
| aec_agc | boolean | 见说明 | 自动增益。不填时:带 V2 DAC 同步的平台上为关,其它平台等于 aec_denoise |
| aec_delay_samples | number | 160 | 参考信号与麦克风的延迟对齐样本数 |
| aec_tail | number | 120 | 回声拖尾长度(毫秒),房间越大要越大 |
| tx_mute | boolean | false | 只收不发(喊话/广播场景) |
| dtmf_rx_payload_type | number | 0 | 接收 DTMF 的 payload type,0 表示未协商、关闭 RTP DTMF 接收;范围 0~127 |
| dtmf_tx_payload_type | number | 0 | 发送 DTMF 的 payload type,0 表示关闭;范围 0~127 |
| dtmf_clock_rate | number | 8000 | DTMF 的时钟频率,范围 0~8000 |
| dtmf_events | number | 65535 | DTMF 事件位图,范围 0~65535 |
| adapter | number | 默认网卡 | 指定走哪块网卡(多网卡/多链路时用) |
| security | userdata | 无 | voip.securitySetRemote 返回的句柄,启用 SRTP;固件未开 SRTP 时传了会返回 media_security_unsupported |
四个 DTMF 字段必须与 SDP 协商结果一致:不填就不会收发 RTP DTMF,此时 voip.sendDtmf 会返回 dtmf_not_negotiated。
返回值
含义说明:是否成功启动媒体会话
数据类型:boolean
取值范围:true 成功;false 失败
注意事项:多数参数校验失败只返回 false 并在日志里打印原因;DTMF 参数越界与 SRTP 句柄非法这两种情况会额外返回第二个值("invalid_dtmf_negotiation" / "invalid_media_security" / "media_security_unsupported")。
返回示例:true
注意事项
1、非 tcp_passive 模式下 remote_ip 与 remote_port 必填,缺一个就返回 false;
2、tcp_passive 必须给 local_port,因为对端是平台侧主动连进来的,本端只需要监听端口;
3、返回 true 只代表媒体会话已开始工作,不代表对端已经收到声音——对端是否真的在发流,要看 voip.stats() 里的 rx_packets 是否增长。
示例
local ok = voip.start({
remote_ip = "192.168.1.100", remote_port = 10000, local_port = 10000,
transport = "udp", codec = voip.PCMA, ptime = 20,
aec = true, aec_mode = "speex",
})
4.2 voip.stop()
功能
停止当前媒体会话,释放 RTP 端口与音频资源。
参数
无。
返回值
含义说明:是否停止成功
数据类型:boolean
取值范围:true 成功;false 当前没有会话或内部状态不允许
返回示例:true
注意事项
1、停止是异步收尾的,调用后要等 voip.on("state") 收到 "stopped"(或 getState() 变成 "idle")再启动下一通,否则可能因为端口还没释放而启动失败;
2、bridgeMute 的静音状态在会话启停时会自动恢复为未静音,不用手工复位。
示例
voip.stop()
4.3 voip.on(event, callback)
功能
注册 voip 事件回调。同一个事件重复注册会覆盖旧回调,传 nil 表示取消注册。
参数
event
参数含义:事件名
数据类型:string
取值范围:"state"、"stats"、"error"、"record"、"play"、"dtmf"
是否必选:是
注意事项:写错的事件名只打印一条警告并忽略,不会报错,容易误以为“没生效”。
callback
参数含义:回调函数
数据类型:function 或 nil
取值范围:任意
是否必选:是
注意事项:不是函数也不是 nil 时会抛参数错误。
各事件的回调参数:
| 事件 | 回调参数 | 说明 |
|---|---|---|
| state | state(string) |
会话状态,取值为 starting/running/stopping/idle/error 等 |
| stats | stats(table) |
统计表,字段见 4.7;按 stats_interval 周期回调 |
| error | err(string) |
内部错误原因,多用于排障 |
| play | (event, info) 两个参数 |
event 为 started/done/stopped/error;info 见 4.12 |
| record | (event, info) 两个参数 |
录音状态变化,info 字段同 4.15 |
| dtmf | event(table) |
一个表,含 event(rx/rx_end/tx_done/tx_failed)、digit、session、id、duration_ms、complete |
⚠️
play与record的回调是两个参数(event, info),而state/stats/error只有一个参数。写成单参数的函数去接 play 事件时,第二个参数会丢失,这是最容易踩的坑。
返回值
无。
示例
voip.on("play", function(event, info)
log.info("voipplay", event, info.id, info.state)
end)
voip.on("dtmf", function(e)
if e.event == "rx" then
log.info("dtmf", "收到按键", e.digit)
end
end)
4.4 voip.getState()
功能
读取当前会话状态。
参数
无。
返回值
含义说明:会话状态
数据类型:string
取值范围:idle(空闲)/ starting(正在建链)/ running(通话中)/ stopping(正在停止)/ error(出错)/ unknown
返回示例:"running"
示例
log.info("voip", voip.getState())
4.5 voip.isRunning()
功能
判断媒体会话是否处于运行中,常用于循环里的退出条件。
参数
无。
返回值
含义说明:是否正在运行
数据类型:boolean
取值范围:true/false
返回示例:true
示例
while voip.isRunning() do
sys.wait(100)
end
4.6 voip.getSession()
功能
取当前媒体会话编号。作用是给按键、播报这类异步操作带上“会话归属”,避免上一通电话的残留操作落到下一通上。
参数
无。
返回值
含义说明:当前媒体会话编号
数据类型:number 或 nil
取值范围:有会话时返回正整数;没有会话返回 nil
注意事项:编号在每次 start 后变化,不要缓存跨通话使用。
返回示例:1
示例
local session = voip.getSession()
if session then
voip.sendDtmf("1", 160, session)
end
4.7 voip.stats()
功能
读取累计统计,用于判断链路质量(丢包、乱序、抖动、编解码耗时、AEC 状态)。
参数
无。
返回值
含义说明:统计表
数据类型:table
取值范围:字段见下表,全部为累计值(从本次 start 起算)
返回示例:{tx_packets = 1024, rx_packets = 1018, rx_lost = 6, ...}
主要字段分四组:
| 分组 | 字段 | 含义 |
|---|---|---|
| 发送 | tx_packets / tx_bytes / tx_errors | 已发 RTP 包数、字节数、发送失败次数 |
| 接收 | rx_packets / rx_bytes / rx_lost / rx_out_of_order | 收到包数、字节数、按序列号推算的丢包数、乱序包数 |
| 接收异常 | rx_parse_fail / rx_bad_payload / rx_security_fail / rx_queue_dropped / rx_frame_dropped / rx_decode_dropped / rx_concealed / rx_decoder_resets | 解析失败、载荷非法、SRTP 校验失败、队列丢包、整帧丢弃、解码丢弃、丢包补偿次数、解码器重置次数 |
| 编解码与音频 | encode_frames / encode_ms / encode_max_ms / decode_frames / decode_ms / decode_max_ms / jb_played / jb_silence / audio_tx_commit_fail / audio_tx_underrun / audio_rx_dropped / audio_rx_samples / audio_render_samples / audio_normalized_samples / aec_mode | 编解码帧数与耗时(含峰值)、抖动缓冲播出与补静音次数、音频提交失败/欠载/丢弃、采样统计、当前 AEC 模式 |
注意事项
1、rx_lost 是按 RTP 序列号推算的,不是网络层统计,参考价值在于“有没有持续增长”;
2、排查“对方听不到我”时先看 tx_packets 有没有涨,排查“我听不到对方”看 rx_packets;两个都涨但没声音,问题在音频通路(codec/模式)而不是网络;
3、encode_max_ms / decode_max_ms 突然变大,通常是主循环被别的任务长时间占用。
示例
local s = voip.stats()
log.info("voip", "tx", s.tx_packets, "rx", s.rx_packets, "lost", s.rx_lost, "aec", s.aec_mode)
4.8 voip.sendDtmf(digit, duration_ms, expected_session)
功能
在通话中异步发送一个 RFC 4733 网络按键,不影响音频收发。用于拨分机号、IVR 菜单选择。
参数
digit
参数含义:要发送的按键
数据类型:string
取值范围:单个字符,0-9、*、#、A-D
是否必选:是
注意事项:长度必须为 1,传整串号码直接失败。
duration_ms
参数含义:按键持续时间
数据类型:number
取值范围:100~1000 毫秒,默认 160
是否必选:否
注意事项:太短对端可能识别不到,太长会拖慢后续按键。
expected_session
参数含义:期望的媒体会话号
数据类型:number
取值范围:voip.getSession() 的返回值
是否必选:是
注意事项:与实际会话不一致时返回 stale_media_session,这是防止旧按键串到下一通电话的保护。
返回值
含义说明:是否受理 + 请求编号或失败原因
数据类型:boolean + number/string
取值范围:受理返回 true 与请求编号 id;失败返回 false 与原因
失败原因:stale_media_session(会话已变)/ dtmf_not_negotiated(SDP 未协商 DTMF)/ invalid_dtmf(按键或时长非法)/ dtmf_busy(上一次还没发完)/ dtmf_queue_failed(入队失败)
返回示例:true, 3
注意事项
1、是否真的发出去了要看 voip.on("dtmf") 的 tx_done;tx_done 只表示本端网络发送完成,不代表对端 IVR 已经处理;
2、接收侧每按一次键只上报一次 rx,rx_end 带结束包时长,不要拿 rx_end 再转发一遍同一个按键;
3、必须先让 SDP 协商出 DTMF payload type(dtmf_rx_payload_type/dtmf_tx_payload_type 非 0),否则本接口恒返回 dtmf_not_negotiated。
示例
local ok, id = voip.sendDtmf("2", 160, voip.getSession())
if not ok then
log.warn("voip", "dtmf failed", id)
end
4.9 voip.playTTS(text, opts)
功能
把一段文本转成语音,直接播给通话对端,不占用本机音频设备。
参数
text
参数含义:要朗读的文本
数据类型:string
取值范围:UTF-8 文本,不能含 \0
是否必选:是
注意事项:TTS 只支持中文(依赖固件是否带 TTS 资源)。
opts
参数含义:播报配置
数据类型:table,可省略
取值范围:target="sip"/"cc"/"both"(默认 "sip",cc/both 仅纯 PCM 桥接可用);mode="replace"/"mix"(默认 "replace");gain(总增益,0~1);speech_gain(语音增益,0~1)
是否必选:否
注意事项:值越界或枚举写错会直接失败并返回 invalid_options。
返回值
含义说明:播放 ID 或失败原因
数据类型:number 或 nil + string
取值范围:成功返回播放 ID(正整数);失败返回 nil 与原因
失败原因:invalid_options(参数非法)/ busy(同一时刻只允许一个播报任务)/ 其它资源类原因
返回示例:1
注意事项
1、同一时刻只能有一个播报任务,busy 时要等 play 事件的 done 或 playStatus().state 变回 idle 再发下一段;
2、mode="replace" 会用播报替换原来的通话语音,"mix" 是叠加在通话语音上,广播/提示音场景一般用 mix;
3、gain 与 speech_gain 都是 0~1 的浮点,不是分贝值。
示例
local id, err = voip.playTTS("您好,请稍候", { target = "sip", mode = "replace", gain = 0.8 })
4.10 voip.playFile(path, opts)
功能
把本地音频文件播给通话对端,支持 PCM16 裸流、PCM WAV 与 MP3。
参数
path
参数含义:文件路径
数据类型:string
取值范围:文件系统里存在的路径,如 /luadb/prompt.mp3
是否必选:是
注意事项:裸 PCM 必须是 16bit 小端单声道,采样率默认 8000。
opts
参数含义:播报配置,含义同 4.9
数据类型:table,可省略
取值范围:在 4.9 的基础上,裸 PCM 还支持 sample_rate(8000~48000)与 channels(1 或 2)
是否必选:否
注意事项:WAV/MP3 的采样率由文件头决定,不要再用 sample_rate 覆盖。
返回值
含义说明:播放 ID 或失败原因
数据类型:number 或 nil + string
取值范围:成功返回播放 ID;失败返回 nil 与原因(invalid_options/busy/文件不存在等)
返回示例:2
示例
local id, err = voip.playFile("/luadb/prompt.mp3", { target = "both", mode = "mix" })
4.11 voip.playStop(id)
功能
异步停止指定的播报任务。
参数
id
参数含义:playTTS/playFile 返回的播放 ID
数据类型:number
取值范围:大于 0 且不超过 32 位无符号范围
是否必选:是
注意事项:传 0 或负数返回 false 与 invalid。
返回值
含义说明:是否接受停止请求
数据类型:boolean + string
取值范围:接受返回 true;不接受返回 false 与原因
返回示例:true
注意事项
停止是异步的:playStatus().state 变成 idle、或收到 play 事件的 stopped/done,才表示资源已释放、可以再次播报。
示例
voip.playStop(id)
4.12 voip.playStatus()
功能
查询当前播报状态。
参数
无。
返回值
含义说明:播报状态表
数据类型:table
取值范围:字段见下
返回示例:{id = 1, session = 1, state = "playing", target = "sip", mode = "replace", sip_samples = 8000, cc_samples = 0, reason = nil}
| 字段 | 含义 |
|---|---|
| id | 播放 ID,空闲时为 0 |
| session | 所属媒体会话号 |
| state | idle / loading / playing / stopping |
| target | sip / cc / both |
| mode | replace / mix |
| sip_samples | 已提交到 SIP 发送接口的 8kHz 源样本数 |
| cc_samples | 已提交到 CC 发送接口的 8kHz 源样本数 |
| reason | 失败/结束原因,正常时为 nil |
⚠️
sip_samples/cc_samples是“已提交给发送接口”的样本数,不是对端已播放的进度,不能拿它做精确的播放进度条。
4.13 voip.recordStart(path, opts)
功能
开始本地双声道 WAV 录音(本端麦克风 + 对端下行各一路)。
参数
path
参数含义:录音文件路径
数据类型:string
取值范围:建议以 .wav 结尾
是否必选:是
注意事项:路径非法返回 invalid_path。
opts
参数含义:录音配置
数据类型:table,可省略
取值范围:max_seconds 最长录音秒数,默认 7200
是否必选:否
返回值
含义说明:是否开始成功
数据类型:boolean + string/nil
取值范围:成功 true 与 nil;失败 false 与原因
失败原因:invalid_path / invalid_state(当前状态不允许,如没有通话)/ already_recording / no_memory
返回示例:true, nil
注意事项
录音依赖 LUAT_USE_VOIP_RECORD;同一时刻只能有一路录音,重复调用返回 already_recording。空间不足时会丢帧,可通过 recordStatus().dropped_frames 观察。
示例
voip.recordStart("/luadb/call.wav", { max_seconds = 600 })
4.14 voip.recordStop()
功能
异步停止并收尾当前录音(写完 WAV 头)。
参数
无。
返回值
含义说明:是否发出停止请求
数据类型:boolean + string/nil
取值范围:成功 true 与 nil;失败 false 与 "invalid_state"
返回示例:true, nil
注意事项
返回 true 只代表请求已受理,要等 recordStatus().state 变成 idle(或收到 record 事件)再认为文件已经写完;此时立刻重启录音会得到 invalid_state。
示例
voip.recordStop()
4.15 voip.recordStatus()
功能
查询录音状态。
参数
无。
返回值
含义说明:录音状态表
数据类型:table
取值范围:字段见下
返回示例:{state = "recording", path = "/luadb/call.wav", bytes = 320000, duration_ms = 10000, queued_frames = 0, dropped_frames = 0, reason = nil}
| 字段 | 含义 |
|---|---|
| state | 录音状态字符串(idle/recording/stopping 等,由内部状态名给出) |
| path | 当前录音路径 |
| bytes | 已写入字节数 |
| duration_ms | 已录时长(毫秒) |
| queued_frames | 待写队列帧数,持续变大说明写卡跟不上 |
| dropped_frames | 因队列满丢弃的帧数,大于 0 表示录音有缺口 |
| reason | 停止/失败原因,正常为 nil |
4.16 voip.setAudioMode(mode)
功能
切换音频工作模式:硬件直驱(I2S)或纯 PCM 桥接。
参数
mode
参数含义:音频模式
数据类型:number
取值范围:voip.AUDIO_MODE_I2S(0) / voip.AUDIO_MODE_BRIDGE(1)
是否必选:是
注意事项:桥接模式需要固件开 LUAT_USE_VOIP_BRIDGE,否则该常量与 pcmIn/pcmOut 都是 nil。
返回值
含义说明:是否切换成功
数据类型:boolean
取值范围:true/false
返回示例:true
注意事项
应在 voip.start 之前调用;通话中途切换会导致音频通路上出现杂音或静音。
示例
voip.setAudioMode(voip.AUDIO_MODE_BRIDGE)
4.17 voip.pcmIn(data)
功能
桥接模式下的上行注入:把外部采集的 PCM 送给 voip,由它编码后按 RTP 发出去。
参数
data
参数含义:PCM 数据
数据类型:string
取值范围:16bit 单声道小端;长度应为偶数
是否必选:是
注意事项:奇数长度时尾部半个样本被忽略(按 len/2 计算样本数)。
返回值
含义说明:实际被消耗的样本数
数据类型:number
取值范围:0 ~ len/2;0 表示本次没吃进去(缓冲满)
注意事项:返回值小于传入样本数时,说明尾部没被消费,要保留下来下次再送,不能直接丢掉。
返回示例:160
示例
local consumed = voip.pcmIn(pcm_chunk)
if consumed < #pcm_chunk / 2 then
keep_tail(pcm_chunk, consumed) -- 保留未消费的尾部
end
4.18 voip.pcmOut(max_samples)
功能
桥接模式下的下行取出:把对端发来的语音解码成 PCM 交给脚本播放。
参数
max_samples
参数含义:本次最多请求的样本数
数据类型:number
取值范围:大于 0
是否必选:是
注意事项:按 20ms@8kHz 算,一帧是 160 样本。
返回值
含义说明:解码后的 PCM 数据
数据类型:string 或 nil
取值范围:有数据返回 PCM16 小端数据;无数据、参数为 0 或内存不足返回 nil
注意事项:nil 是正常情况(还没收到包),不要当错误处理,下一轮继续取。
返回示例:"\x00\x01..."(320 字节 = 160 样本)
示例
local pcm = voip.pcmOut(160)
if pcm then
audio_play(pcm)
end
4.19 voip.pcmResampler(input_rate, output_rate, gain)
功能
创建有状态的采样率转换器,用于把 16kHz 的麦克风数据降到 8kHz 送给 voip,或把 8kHz 的下行升到 16kHz 播放。
参数
input_rate / output_rate
参数含义:输入与输出采样率
数据类型:number
取值范围:仅支持 16000→8000 或 8000→16000
是否必选:是
注意事项:其它组合直接抛 Lua 错误(lua_error),不是返回 nil。
gain
参数含义:转换增益
数据类型:number
取值范围:16k→8k 时为 1~8,默认 1;8k→16k 时只能为 1
是否必选:否
注意事项:越界抛参数错误。
返回值
含义说明:转换器对象(userdata)
数据类型:userdata
取值范围:提供 :process(data) 与 :reset() 两个方法
返回示例:userdata: 0x...
对象方法:
| 方法 | 说明 |
|---|---|
rs:process(data) |
返回转换后的 PCM 字符串与本次被限幅的样本数;输入为空返回空串与 0;块长度非法抛参数错误且不改变转换器状态 |
rs:reset() |
清空滤波历史(通话开始/结束时调用) |
注意事项
1、16k→8k 用 7 抽头二项式低通抽取,8k→16k 用相邻样本线性插值,两者都是有状态的,滤波历史跨 process 调用保留,所以不能中途新建实例,否则会有咔哒声;
2、每次 16k 输入最多 512 字节且长度为 4 的倍数,8k 输入最多 320 字节且长度为 2 的倍数;
3、每路音频用独立实例,不要共用;通话开始与结束时各 reset() 一次。
示例
local mic = voip.pcmResampler(16000, 8000, 1)
local spk = voip.pcmResampler(8000, 16000)
local pcm8k = mic:process(pcm16k_512)
voip.pcmIn(pcm8k)
local down = voip.pcmOut(160)
if down then
local pcm16k = spk:process(down)
end
4.20 voip.bridgeTone(on)
功能
控制桥接模式内部产生的早期提示音(如拨号音/回铃音的本地生成)。
参数
on
参数含义:开关
数据类型:boolean
取值范围:true 启动,false 停止
是否必选:是
返回值
含义说明:是否成功
数据类型:boolean
取值范围:true/false
返回示例:true
示例
voip.bridgeTone(true)
4.21 voip.bridgeMute(on)
功能
静音运行中的纯 PCM 桥接双向通话语音,TTS/音频注入和媒体节拍不受影响——也就是“人声静音但播报照常”。
参数
on
参数含义:静音开关
数据类型:boolean
取值范围:true 静音原通话语音,false 恢复
是否必选:是
注意事项:必须传 boolean,传数字会抛类型错误。
返回值
含义说明:是否成功
数据类型:boolean
取值范围:true 成功;false 当前不在运行中的纯 PCM 桥接会话
返回示例:true
注意事项
只对 cc.AUDIO_MODE_BRIDGE_PCM 这个后端有效;会话启动和停止时静音状态自动恢复为未静音,不用手工复位。
示例
voip.bridgeMute(true)
4.22 voip.securityIdentity()
功能
创建运行期的 DTLS 证书身份,用于 DTLS-SRTP 安全协商。私钥不导出。
参数
无。
返回值
含义说明:身份句柄
数据类型:userdata
取值范围:成功返回句柄;失败返回 nil 与原因
返回示例:userdata: 0x...
注意事项
该函数需要 LUAT_USE_SRTP;句柄由 GC 自动释放,也可以显式 securityRelease。
4.23 voip.securityCreate(mode, identity)
功能
创建本地安全媒体协商材料。同一个 SDP 重传必须复用同一个句柄,不能每次重传都新建。
参数
mode
参数含义:安全模式
数据类型:string
取值范围:"sdes" 或 "dtls-srtp"
是否必选:是
注意事项:其它取值返回 nil 与 unsupported。
identity
参数含义:DTLS 运行期身份
数据类型:userdata,可省略
取值范围:voip.securityIdentity() 的返回值
是否必选:否(仅 dtls-srtp 需要)
返回值
含义说明:安全会话句柄
数据类型:userdata 或 nil + string
取值范围:成功返回句柄;失败返回 nil 与原因
返回示例:userdata: 0x...
4.24 voip.securityInfo(handle)
功能
取本地 SDP 协商信息,用于填到 SDP 的 a=crypto 或 a=fingerprint 行。
参数
handle
参数含义:安全会话句柄
数据类型:userdata
取值范围:voip.securityCreate 的返回值
是否必选:是
返回值
含义说明:协商信息表
数据类型:table 或 nil + string
取值范围:SDES 时为 {mode="sdes", suite=..., key=...};DTLS 时为 {mode="dtls-srtp", hash="sha-256", fingerprint=...}
返回示例:{mode = "sdes", suite = "AES_CM_128_HMAC_SHA1_80", key = "..."}
⚠️ SDES 的
key是会话密钥,属敏感信息,只能通过经验证的 TLS 信令通道发送,明文信令里发出去等于把加密作废。
4.25 voip.securitySetRemote(handle, remote)
功能
用本地协商材料加上对端协商结果,生成一个已协商的独立会话句柄,交给 voip.start 的 security 字段使用。原句柄保持不变。
参数
handle
参数含义:本地安全会话句柄
数据类型:userdata
是否必选:是
remote
参数含义:对端协商结果
数据类型:table
取值范围:SDES 为 {mode="sdes", suite=..., key=...};DTLS 为 {mode="dtls-srtp", hash="sha-256", fingerprint=..., role="active"/"passive"}
是否必选:是
注意事项:mode 必须与本地一致,否则返回 invalid;SDES 的 key 文本长度、DTLS 的 fingerprint 文本长度都有固定校验,长度不对直接拒绝。
返回值
含义说明:新的已协商句柄
数据类型:userdata 或 nil + string
取值范围:成功返回新句柄;失败返回 nil 与原因
返回示例:userdata: 0x...
4.26 voip.securityRelease(handle)
功能
显式释放安全会话或身份句柄。
参数
handle
参数含义:安全会话或身份句柄
数据类型:userdata
是否必选:是
返回值
含义说明:是否释放成功
数据类型:boolean
取值范围:true 成功;false 句柄类型不对或已释放
返回示例:true
注意事项
句柄挂在 GC 上,正常流程可以不显式释放;重复调用是安全的(第二次返回 false)。
五、产品支持说明
1、voip 需要固件在编译时包含 VoIP 组件(LUAT_USE_VOIP)。目前合宙公开固件里带 SIP/voip 的主要是 EC7xx 的 13/113 号与 22/122 号固件:13/113 号用于纯 SIP 通话,22/122 号在此基础上带 airui 图形界面、SRTP 与 G.729/G.722 编解码;具体到某个型号是否支持,以该型号固件页的“固件支持核心库列表”为准;
2、voip.playTTS / playFile(V2056 起修复了“开了 TTS 却未开 ONCHIP 导致误走外置资源路径、TTS 播不出来”的问题)、record*、pcm*/bridge*、security* 分别由 LUAT_USE_VOIP_PLAY、LUAT_USE_VOIP_RECORD、LUAT_USE_VOIP_BRIDGE、LUAT_USE_SRTP 控制,脚本里对应函数为 nil 即表示该子功能未编入;
3、Air780EPM 这类不带 audio/codec 的型号不支持 voip(与 cc 的差异:cc 的能力表同样标不支持),需要 SIP 通话请选 Air780EHM/EHV/EGH/EGG 或 Air8784、Air8783H、Air8605、Air8701 这类做过音频处理的型号;
4、Air8101 系列不支持 voip。
以上判定最终以各型号固件页的核心库支持表与对接 FAE 的答复为准。