增加 9196 回声测试分机
一、背景
当前项目基于 tio-core 自研了一套轻量级 SIP Server,用于承接语音呼叫接入,并为后续语音机器人、ASR、TTS、Realtime LLM 对话等能力提供底层通话链路。
在前面的几个阶段中,系统已经逐步完成了以下能力:
- SIP TCP / UDP 监听
INVITE / ACK / BYE基本流程处理- SDP Offer / Answer 协商
- RTP 端口动态分配
- G.711 / G.722 编解码
MediaProcessor媒体处理抽象RealtimeMediaProcessor与实时模型桥接- 会话级 codec / resampler 资源管理
到目前为止,系统已经具备两类能力:
1. 基础语音链路能力
即:
- SIP 建链
- SDP 协商
- RTP 音频收发
- 编解码
- 音频帧级处理
2. 智能语音能力
即:
- 将 RTP 音频转换为模型输入格式
- 发送给 Realtime 模型
- 接收模型输出
- 转换回当前会话格式并回发
也就是说,系统已经不再只是一个“能接电话”的 SIP Server,而是一个具备实时语音处理和 AI 对话能力的媒体平台。
二、现阶段问题
虽然系统已经具备了 Realtime 模型接入能力,但在联调、排障和基础验证阶段,还缺少一个非常重要的能力:
不经过模型、直接验证电话链路本身是否正常的测试入口。
在实际开发和运维过程中,经常会遇到以下场景:
1. 需要快速验证 RTP 音频链路是否正常
例如需要确认:
- SIP 建链是否正常
- SDP 协商是否正确
- RTP 端口是否分配成功
- 本端是否收到了对端音频
- codec 编解码是否工作正常
- RTP 回发链路是否正确
这类问题如果直接走 Realtime 模型链路,会引入很多额外变量:
- 模型连接状态
- 模型响应延迟
- Realtime Session 是否建立成功
- 模型输入输出格式是否匹配
- 模型侧是否有静音或生成延迟
这样一来,即使用户听不到声音,也很难第一时间判断问题究竟出在哪一层。
2. 需要一个最简单、最确定的功能分机
在电话系统中,一个非常常见的调试能力就是:
- 拨打某个测试分机
- 系统直接把收到的音频原样返回
- 用户立刻就能听到自己的回声
这种“回声测试”功能的价值非常高,因为它可以用最小成本证明:
- SIP 信令是通的
- RTP 链路是通的
- 编解码是通的
- 媒体处理链路是通的
也就是说,它可以把问题范围迅速缩小到“模型层之外”或者“模型层之内”。
三、目标
本次改造的目标,是在现有 SIP Server 中增加一个固定测试分机:
9196
当用户拨打该号码时,系统不进入 RealtimeMediaProcessor,而是直接进入最简单的音频回声处理流程。
整体目标包括三个方面。
1. 拨打 9196 时进入回声测试
当 SIP INVITE 的被叫号码为 9196 时:
- 系统仍然正常完成 SIP / SDP 协商
- 正常分配 RTP 端口
- 正常建立通话
- 但媒体处理器不再使用
RealtimeMediaProcessor - 而是使用
EchoMediaProcessor
这样,用户在通话中听到的就是自己的回声。
2. 其他号码继续走现有 Realtime 流程
本次改造不是替换原有能力,而是增加一个专用测试入口。
也就是说:
- 拨打
9196→ 回声测试 - 拨打其他号码 → 继续走
RealtimeMediaProcessor
这样可以保证:
- 原有实时语音机器人链路不受影响
- 新增功能只作为独立测试入口存在
3. 保持现有媒体架构不变
本次改造不改变以下核心设计:
- RTP 层继续负责 codec decode / encode
MediaProcessor继续作为媒体处理抽象EchoMediaProcessor继续负责最简单的“输入即输出”RealtimeMediaProcessor继续负责模型桥接
也就是说,本次功能本质上只是:
在 SIP 建链阶段,根据被叫号码选择不同的
MediaProcessor。
四、设计原则
为了避免功能写散、职责混乱,本次设计遵循以下原则。
1. 按被叫号码选择媒体处理器
“是否进入回声测试”是一个呼叫入口级决策,它应该在 SIP 建链阶段就确定,而不应该等到 RTP 处理阶段再临时判断。
原因很简单:
- SIP
INVITE阶段已经拿到了完整的被叫信息 - RTP 层只负责收发媒体,不应该承担业务路由职责
RealtimeMediaProcessor也不应该混入“特殊号码分支逻辑”
因此,这个决策最合理的位置就是:
SipUdpServerHandler.handleInvite()
2. 回声测试不应依赖 Realtime 组件
拨打 9196 的目标是验证基础语音链路,而不是验证模型链路。 因此,进入回声测试后应尽量避免:
- 创建
SipRealtimeSession - 连接 Realtime 模型
- 走模型输入输出流程
这样才能让 9196 成为真正“最小、最纯净”的测试路径。
3. EchoMediaProcessor 继续保持最简单实现
回声测试本身不需要复杂逻辑,它只需要:
- 收到
AudioFrame - 原样返回
AudioFrame
也就是:
public class EchoMediaProcessor implements MediaProcessor {
@Override
public AudioFrame process(AudioFrame input, CallSession session) {
return input;
}
}
这个实现非常简单,但在系统联调阶段价值极高。
五、改造方案
本次改造采用的方案是:
在 SIP 建链阶段,根据被叫号码动态选择
MediaProcessor。
整体上分为三个步骤。
1. 增加固定测试分机号
在 SipUdpServerHandler 中定义一个固定号码:
private static final String ECHO_TEST_EXTENSION = "9196";
这个号码用于标识“回声测试入口”。
2. 在 SipUdpServerHandler 中同时持有两个处理器
原本 SipUdpServerHandler 通常只持有一个统一的 mediaProcessor,用于所有呼叫。 本次改造后,改为:
- 一个默认处理器:通常是
RealtimeMediaProcessor - 一个固定的回声处理器:
EchoMediaProcessor
例如:
private final MediaProcessor defaultMediaProcessor;
private final MediaProcessor echoMediaProcessor = new EchoMediaProcessor();
这样在收到 INVITE 时,就可以根据被叫号码决定当前会话使用哪一个处理器。
3. 在 handleInvite() 中动态选择处理器
在创建 CallSession 并分配 RTP 服务前,先判断当前呼叫是否拨打到 9196。
如果是,则选择:
echoMediaProcessor
否则选择:
defaultMediaProcessor
随后调用:
rtpServerManager.allocateAndStart(session, selectedMediaProcessor);
这样一来,媒体处理路径就从建链开始被确定下来。
六、关键实现
1. EchoMediaProcessor
回声处理器本身无需改动,仍然保持最简单实现:
package com.litongjava.sip.rtp.media;
import com.litongjava.sip.model.CallSession;
public class EchoMediaProcessor implements MediaProcessor {
@Override
public AudioFrame process(AudioFrame input, CallSession session) {
return input;
}
}
它的职责非常清晰:
- 不做模型调用
- 不做额外音频变换
- 只返回输入音频本身
2. 在 SipUdpServerHandler 中增加处理器选择逻辑
本次改造的核心在 SipUdpServerHandler。
关键思路包括:
增加默认处理器与回声处理器
private final MediaProcessor defaultMediaProcessor;
private final MediaProcessor echoMediaProcessor = new EchoMediaProcessor();
根据被叫号码选择处理器
private MediaProcessor chooseMediaProcessor(SipRequest req) {
String calledNumber = resolveCalledNumber(req);
if (ECHO_TEST_EXTENSION.equals(calledNumber)) {
return echoMediaProcessor;
}
return defaultMediaProcessor;
}
在 handleInvite() 中使用选中的处理器
MediaProcessor selectedMediaProcessor = chooseMediaProcessor(req);
rtpServerManager.allocateAndStart(session, selectedMediaProcessor);
3. 从 SIP 头中提取被叫号码
为了判断是否拨打到 9196,需要从 SIP To 头中提取 user 部分。 例如:
To: <sip:9196@192.168.1.10>
To: sip:9196@192.168.1.10
To: "test" <sip:9196@192.168.1.10>;tag=abc
因此增加一个解析方法:
private String extractUserFromSipHeader(String headerValue) {
if (headerValue == null) {
return null;
}
String value = headerValue.trim();
int sipIdx = value.toLowerCase().indexOf("sip:");
if (sipIdx < 0) {
return null;
}
String sub = value.substring(sipIdx + 4);
int atIdx = sub.indexOf('@');
if (atIdx < 0) {
int semiIdx = sub.indexOf(';');
int endIdx = semiIdx >= 0 ? semiIdx : sub.length();
String user = sub.substring(0, endIdx).trim();
return user.isEmpty() ? null : user;
}
String user = sub.substring(0, atIdx).trim();
return user.isEmpty() ? null : user;
}
然后通过:
private String resolveCalledNumber(SipRequest req) {
return extractUserFromSipHeader(req.getHeader("To"));
}
完成被叫号码提取。
七、完整行为说明
本次改造完成后,系统在收到 INVITE 时的行为如下。
情况一:拨打 9196
如果被叫号码是:
9196
那么系统流程为:
- 正常解析 SIP
INVITE - 正常进行 SDP 协商
- 正常创建
CallSession - 正常分配 RTP 端口
- 在选择媒体处理器时命中
EchoMediaProcessor - RTP 收到音频后,解码成 PCM
- 交给
EchoMediaProcessor EchoMediaProcessor原样返回输入音频- RTP 再按当前 codec 编码并发回
最终效果就是:
用户在电话中听到自己的回声
这条链路可以很好地验证:
- SIP 信令
- SDP 协商
- RTP 端口分配
- 编解码
- 音频帧处理
- RTP 回发
是否全部正常。
情况二:拨打其他号码
如果被叫号码不是 9196,则系统行为保持不变:
- SIP 建链
- SDP 协商
- 创建
CallSession - 分配 RTP 端口
- 选择默认处理器
RealtimeMediaProcessor - 进入现有 Realtime 模型链路
也就是说,本次功能不会影响原有 AI 语音能力。
八、测试结果
经过实际测试,当前功能已经验证通过:
- 拨打
9196可正常进入回声测试 - 用户可以听到自己的回声
- 其他号码仍然继续走
RealtimeMediaProcessor - SIP / SDP / RTP 主链路未受影响
这说明本次方案已经达到预期目标。
九、设计收益
增加 9196 回声测试分机之后,系统获得了几个非常实际的收益。
1. 大幅提升联调效率
当电话链路有问题时,可以先拨打 9196 做最小验证。 如果 9196 正常,而 Realtime 链路不正常,就说明问题更多集中在模型桥接层。
2. 建立了最小闭环测试入口
9196 提供了一个不依赖模型、不依赖外部业务逻辑的最小通话闭环,便于:
- 新环境部署验证
- codec 调试
- RTP 排障
- 终端兼容性测试
3. 媒体架构更加灵活
这次改造证明了现有 MediaProcessor 抽象是有效的。 系统已经能够根据呼叫入口动态切换不同媒体处理器,这为后续继续扩展提供了很好的基础。
例如未来还可以继续增加:
- 测试分机
- 播报分机
- 录音分机
- 语音质检分机
- 指定机器人技能入口
十、后续可扩展方向
虽然当前版本已经能正常工作,但未来还可以继续做一些增强。
1. 将 9196 改为可配置项
当前实现中 9196 是写死在代码中的:
private static final String ECHO_TEST_EXTENSION = "9196";
后续可以改成配置项,例如从环境变量读取,方便不同环境灵活调整。
2. 支持更多测试分机
未来可以按同样方式扩展更多特殊号码,例如:
9196:回声测试9197:播放固定音频9198:TTS 测试9199:Realtime 模型测试
3. TCP Handler 同步支持
如果系统同时支持 SIP TCP 和 SIP UDP,那么同样的号码分流逻辑也应该补到对应的 TCP Handler 中,保证两种传输方式行为一致。
十一、总结
本次改造的核心目标,是为当前 SIP Server 增加一个固定测试分机:
9196
当用户拨打该号码时,系统不再进入 Realtime 模型链路,而是直接进入最简单的 EchoMediaProcessor,把收到的音频原样回发,从而形成回声测试闭环。
这个功能看似简单,但在实际系统中非常有价值。它提供了一个:
- 最小
- 稳定
- 可预期
- 易排障
的电话链路测试入口,可以显著提升系统联调和故障定位效率。
从架构角度看,这次改造也再次验证了当前设计的合理性:
- SIP 层负责建链和路由
- RTP 层负责编解码和媒体收发
MediaProcessor负责音频内容处理- 不同业务场景通过选择不同的
MediaProcessor实现差异化行为
可以把这一篇的核心价值概括成一句话:
9196 回声测试分机并不是一个额外的 demo 功能,而是当前 SIP 媒体平台中非常重要的最小链路验证入口。
