Skip to content

增加 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 中增加一个固定测试分机:

text
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

也就是:

java
public class EchoMediaProcessor implements MediaProcessor {

  @Override
  public AudioFrame process(AudioFrame input, CallSession session) {
    return input;
  }
}

这个实现非常简单,但在系统联调阶段价值极高。


五、改造方案 ​

本次改造采用的方案是:

在 SIP 建链阶段,根据被叫号码动态选择 MediaProcessor。

整体上分为三个步骤。

1. 增加固定测试分机号 ​

在 SipUdpServerHandler 中定义一个固定号码:

java
private static final String ECHO_TEST_EXTENSION = "9196";

这个号码用于标识“回声测试入口”。

2. 在 SipUdpServerHandler 中同时持有两个处理器 ​

原本 SipUdpServerHandler 通常只持有一个统一的 mediaProcessor,用于所有呼叫。 本次改造后,改为:

  • 一个默认处理器:通常是 RealtimeMediaProcessor
  • 一个固定的回声处理器:EchoMediaProcessor

例如:

java
private final MediaProcessor defaultMediaProcessor;
private final MediaProcessor echoMediaProcessor = new EchoMediaProcessor();

这样在收到 INVITE 时,就可以根据被叫号码决定当前会话使用哪一个处理器。

3. 在 handleInvite() 中动态选择处理器 ​

在创建 CallSession 并分配 RTP 服务前,先判断当前呼叫是否拨打到 9196。

如果是,则选择:

java
echoMediaProcessor

否则选择:

java
defaultMediaProcessor

随后调用:

java
rtpServerManager.allocateAndStart(session, selectedMediaProcessor);

这样一来,媒体处理路径就从建链开始被确定下来。


六、关键实现 ​

1. EchoMediaProcessor ​

回声处理器本身无需改动,仍然保持最简单实现:

java
package nexus.io.sip.rtp.media;

import nexus.io.sip.model.CallSession;

public class EchoMediaProcessor implements MediaProcessor {

  @Override
  public AudioFrame process(AudioFrame input, CallSession session) {
    return input;
  }
}

它的职责非常清晰:

  • 不做模型调用
  • 不做额外音频变换
  • 只返回输入音频本身

2. 在 SipUdpServerHandler 中增加处理器选择逻辑 ​

本次改造的核心在 SipUdpServerHandler。

关键思路包括:

增加默认处理器与回声处理器 ​

java
private final MediaProcessor defaultMediaProcessor;
private final MediaProcessor echoMediaProcessor = new EchoMediaProcessor();

根据被叫号码选择处理器 ​

java
private MediaProcessor chooseMediaProcessor(SipRequest req) {
  String calledNumber = resolveCalledNumber(req);
  if (ECHO_TEST_EXTENSION.equals(calledNumber)) {
    return echoMediaProcessor;
  }
  return defaultMediaProcessor;
}

在 handleInvite() 中使用选中的处理器 ​

java
MediaProcessor selectedMediaProcessor = chooseMediaProcessor(req);
rtpServerManager.allocateAndStart(session, selectedMediaProcessor);

3. 从 SIP 头中提取被叫号码 ​

为了判断是否拨打到 9196,需要从 SIP To 头中提取 user 部分。 例如:

text
To: <sip:9196@192.168.1.10>
To: sip:9196@192.168.1.10
To: "test" <sip:9196@192.168.1.10>;tag=abc

因此增加一个解析方法:

java
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;
}

然后通过:

java
private String resolveCalledNumber(SipRequest req) {
  return extractUserFromSipHeader(req.getHeader("To"));
}

完成被叫号码提取。


七、完整行为说明 ​

本次改造完成后,系统在收到 INVITE 时的行为如下。

情况一:拨打 9196 ​

如果被叫号码是:

text
9196

那么系统流程为:

  1. 正常解析 SIP INVITE
  2. 正常进行 SDP 协商
  3. 正常创建 CallSession
  4. 正常分配 RTP 端口
  5. 在选择媒体处理器时命中 EchoMediaProcessor
  6. RTP 收到音频后,解码成 PCM
  7. 交给 EchoMediaProcessor
  8. EchoMediaProcessor 原样返回输入音频
  9. RTP 再按当前 codec 编码并发回

最终效果就是:

用户在电话中听到自己的回声

这条链路可以很好地验证:

  • SIP 信令
  • SDP 协商
  • RTP 端口分配
  • 编解码
  • 音频帧处理
  • RTP 回发

是否全部正常。

情况二:拨打其他号码 ​

如果被叫号码不是 9196,则系统行为保持不变:

  1. SIP 建链
  2. SDP 协商
  3. 创建 CallSession
  4. 分配 RTP 端口
  5. 选择默认处理器 RealtimeMediaProcessor
  6. 进入现有 Realtime 模型链路

也就是说,本次功能不会影响原有 AI 语音能力。


八、测试结果 ​

经过实际测试,当前功能已经验证通过:

  • 拨打 9196 可正常进入回声测试
  • 用户可以听到自己的回声
  • 其他号码仍然继续走 RealtimeMediaProcessor
  • SIP / SDP / RTP 主链路未受影响

这说明本次方案已经达到预期目标。


九、设计收益 ​

增加 9196 回声测试分机之后,系统获得了几个非常实际的收益。

1. 大幅提升联调效率 ​

当电话链路有问题时,可以先拨打 9196 做最小验证。 如果 9196 正常,而 Realtime 链路不正常,就说明问题更多集中在模型桥接层。

2. 建立了最小闭环测试入口 ​

9196 提供了一个不依赖模型、不依赖外部业务逻辑的最小通话闭环,便于:

  • 新环境部署验证
  • codec 调试
  • RTP 排障
  • 终端兼容性测试

3. 媒体架构更加灵活 ​

这次改造证明了现有 MediaProcessor 抽象是有效的。 系统已经能够根据呼叫入口动态切换不同媒体处理器,这为后续继续扩展提供了很好的基础。

例如未来还可以继续增加:

  • 测试分机
  • 播报分机
  • 录音分机
  • 语音质检分机
  • 指定机器人技能入口

十、后续可扩展方向 ​

虽然当前版本已经能正常工作,但未来还可以继续做一些增强。

1. 将 9196 改为可配置项 ​

当前实现中 9196 是写死在代码中的:

java
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 增加一个固定测试分机:

text
9196

当用户拨打该号码时,系统不再进入 Realtime 模型链路,而是直接进入最简单的 EchoMediaProcessor,把收到的音频原样回发,从而形成回声测试闭环。

这个功能看似简单,但在实际系统中非常有价值。它提供了一个:

  • 最小
  • 稳定
  • 可预期
  • 易排障

的电话链路测试入口,可以显著提升系统联调和故障定位效率。

从架构角度看,这次改造也再次验证了当前设计的合理性:

  • SIP 层负责建链和路由
  • RTP 层负责编解码和媒体收发
  • MediaProcessor 负责音频内容处理
  • 不同业务场景通过选择不同的 MediaProcessor 实现差异化行为

可以把这一篇的核心价值概括成一句话:

9196 回声测试分机并不是一个额外的 demo 功能,而是当前 SIP 媒体平台中非常重要的最小链路验证入口。

MIT Licensed