从 FreeSWITCH 媒体腿视角剖析 park / transfer 两种挂载方式的控制平面、媒体路径、编解码责任
覆盖呼入与呼出两种场景 · 聚焦通用技术原理
1. 引言:用"腿"重新定义语音机器人
FreeSWITCH 中,一次通话由一条或多条媒体腿(leg)构成。bridge 把两条腿的 RTP 在 FS 内部直连;park 让通道单腿悬停,等待外部命令驱动。
语音机器人从 FS 媒体腿的视角看,有两种根本不同的挂载方式:
| 单腿:park + ESL 旁路 | 双腿:transfer + SIP UA |
|---|---|
通道 park 在 FS,只有 A-leg。Java 通过 ESL 旁路控制,媒体不离开 FS。Java 是"外挂指挥官",FS 是"执行者"。 | 通道 transfer 到 SIP 分机,A-leg + B-leg 桥接。Java 作为 B-leg 终端接管 RTP。Java 是"媒体中转网关",承担编解码责任。 |
腿数决定了媒体路径、控制平面、编解码责任、延迟构成的根本差异。本文从原理层面剖析这两种架构,并覆盖呼入与呼出两种场景。
2. FreeSWITCH 媒体腿原语
2.1 A-leg 与 B-leg
当一通电话进入 FS,通道创建,这就是 A-leg。当 FS 再向另一方发起呼叫并通过 bridge 连通,第二段通道就是 B-leg。两腿在 FS 内部 RTP 直连,FS 不解封装 payload,只做媒体转发。
单腿状态下,通道没有 bridge_uuid;双腿桥接后,B-leg 有独立 UUID,A-leg 的 Bridge-A-Unique-ID 指向 B-leg。
2.2 四个核心原语
| 原语 | 语义 | 腿数 | 关键特征 |
|---|---|---|---|
park | 通道停在 parking lot,等待 ESL 命令驱动 | 1 | FS 既是信令终端也是媒体终端 |
bridge | 两腿 RTP 在 FS 内部直连,不解封装 payload | 2 | FS 只做媒体转发,不感知 payload 内容 |
transfer | 转接到目标分机,FS 向目标发 INVITE 建立 B-leg,自动 bridge | 2 | 目标可以是外线、内部分机、或 SIP UA |
uuid_audio_fork | 把通道 PCM fork 一份到 WebSocket,只读旁路 | 1 | 不消耗媒体、不建立新腿、单向复制 |
fork 与 bridge 的本质区别:fork 是"只读旁路",原始 RTP 流不受影响,仍在 FS 内部正常转发或播放;bridge 是"媒体接管",两腿的 RTP 在 FS 内部直连。fork 不产生新腿,bridge 产生 B-leg。
3. 单腿架构原理:park + ESL 旁路控制
3.1 媒体腿结构
通道停在 FS,只有 A-leg。媒体在 FS 内部循环——无操作时静音空转,执行 playback 时播放音频。Java 不感知 RTP 包,不参与媒体流转。
主叫 ←──RTP──→ FS (A-leg, park)
│
├── uuid_audio_fork (只读旁路) ──→ WebSocket ──→ ASR
│
└── uuid_broadcast (FS 本地播放) ←── TTS 音频
3.2 控制平面:ESL inbound
Java 作为 ESL 客户端连入 FS,通过命令远程操控通道。核心命令分三类:
| 类别 | 命令 | 作用 |
|---|---|---|
| 取音 | uuid_audio_fork {uuid} start {wsUrl} | 把 PCM 旁路复制到 ASR WebSocket,输出已是解码后的 PCM |
| 放音 | uuid_broadcast {uuid} http://host/tts.mp3 aleg | FS 主动拉流并在通道内播放,可指定 aleg/bleg/both 方向 |
| 控制 | uuid_break / uuid_transfer / uuid_kill | 打断放音 / 转接目标 / 挂断通道 |
3.3 AI 管道
分层管道:ASR → Intent → LLM → TTS,每层独立网络往返。取音通过 uuid_audio_fork 旁路到 ASR;放音通过 TTS 生成音频 + HTTP 接口供 FS 拉流。Java 是"外挂指挥官"——发出 ESL 命令;FS 是"执行者"——在通道内完成媒体动作。
3.4 打断(Barge-in)
ASR 检测到用户开始说话时,Java 调用 uuid_break 中断当前 TTS 播放。媒体腿不变,只是放音动作被取消。打断延迟 = ASR 首字检测延迟 + ESL 命令往返延迟。
4. 双腿架构原理:transfer + SIP UA 桥接
4.1 媒体腿结构
FS 通过 transfer 把通道转给 Java 注册的 SIP 分机,建立 B-leg。A-leg(主叫 → FS)与 B-leg(FS → Java)在 FS 内部 bridge,RTP 直连。Java 是 B-leg 的 terminating endpoint。
主叫 ←──RTP──→ FS(A-leg) ←──RTP──→ FS(B-leg) ←──RTP──→ Java(SIP UA)
│
├── RtpReceiver (收 PCMU)
├── G.711 解码 → 重采样 8k→16k
├── 模型 WebSocket
├── 重采样 24k→8k → G.711 编码
└── RtpSender (发 PCMU)
4.2 控制平面:Java 作为对等 SIP UA
Java 用 SIP 协议栈(如 JAIN-SIP)注册分机到 FS,直接收发 SIP 信令:
- INVITE:FS 转接时向 Java 分机发起 INVITE,Java 回 100 Trying / 180 Ringing / 200 OK + SDP Answer。
- BYE:任一方挂断,通过 SIP BYE 通知。
- REGISTER:Java 向 FS 注册分机,周期性刷新(Expires 通常 3600s),处理 401/407 摘要认证。
SDP 协商确定 RTP 端点:Java 在 SDP Answer 中声明自己的 RTP 端口和 payload type。Java 是对等 UA,不是外挂控制器。
4.3 媒体接管:RTP 直收直发
Java 直接接收 B-leg 的 RTP 包,并承担全部媒体处理责任:
| 入向(用户 → 模型) | 出向(模型 → 用户) |
|---|---|
|
|
对称 RTP(关键约束):收发必须共用同一个 UDP socket。RTP 协议要求对端根据收包源地址回复媒体,如果收发端口不同,对端无法正确回复。这是双腿架构必须遵守的硬约束。
4.4 AI 管道
端到端单模型:Java 把 PCM 桥接到模型的 WebSocket,模型内部完成 ASR + 对话推理 + TTS 生成。单次 WS 往返完成完整对话循环。Java 是"媒体中转网关"——一端连 FS 的 RTP,另一端连模型的 WebSocket。
4.5 打断(Barge-in)
端到端模型自身检测用户首字,通过事件通知 Java。Java 清空本地播放队列(丢弃待发的 RTP 包),新 TTS 音频到达后恢复播放。与单腿的区别:单腿打断需要 Java → ESL → FS 三跳;双腿打断在 Java 本地完成,不涉及外部命令往返。
5. 呼入场景
5.1 呼入的通用机制
外部来电的入口是 SIP INVITE。运营商/中继网关将用户呼叫转成 SIP 信令,向 FS 的 SIP profile 发送 INVITE。INVITE 携带:
- Request-URI:被叫号码,FS 解析为
destination_number - From header:主叫号码,FS 解析为
Caller-Caller-ID-Number - SDP body:主叫侧的 RTP 地址、端口、编码能力
FS 收到 INVITE 后创建 A-leg 通道,进入 inbound dialplan 进行路由决策。dialplan 可以是 XML 配置或 Lua 脚本。Lua 脚本的优势是支持数据库查询、动态比例路由、灵活的上下文注入。
5.2 单腿呼入流程
INVITE → dialplan → set bot_id/bot_type (channel variable)
→ park
→ CHANNEL_PARK 事件 → MQ 投递
→ Java 消费 FsEventConsumer
→ 从事件 headers 读 variable_bot_id
→ ESL uuid_answer 接通
→ uuid_audio_fork 启动取音
→ AI 管道启动
上下文传递:通过 channel variable。Lua 执行 session:setVariable("bot_id", xxx),这些变量存储在 FS 通道变量表中,随事件投递到 MQ。Java 从事件 headers 中以 variable_bot_id 的 key 读取。
channel variable 只在 FS 内部可见(事件、ESL 命令),不进入 SIP 信令。这是单腿架构的上下文传递方式——信息不离开 FS。
5.3 双腿呼入流程
INVITE → dialplan → set sip_h_X-Bot-Id (SIP 自定义头)
→ transfer 8801 XML default
→ FS 向 Java 分机发 INVITE (携带 X-Bot-Id 头)
→ Java SipAgent.handleInvite:
→ 发 100 Trying + 180 Ringing
→ 解析 SDP (FS 的 RTP 端点)
→ 解析 X-Bot-Id 自定义头
→ 200 OK + SDP Answer (声明 Java RTP 端口)
→ B-leg 建立,A/B 两腿在 FS 内部 bridge
→ 启动音频桥接线程
上下文传递:通过 SIP 自定义头。Lua 执行 session:setVariable("sip_h_X-Bot-Id", xxx),sip_h_X- 前缀是 FS 的约定:设置后,FS 在后续发出的 SIP 消息中携带对应的 X- 自定义头。自定义头随 INVITE 传到 Java,可穿越 SIP 网络。
5.4 呼入两种架构对比
| 维度 | 单腿(park) | 双腿(transfer) |
|---|---|---|
| 路由动作 | park | transfer 8801 XML default |
| 上下文传递 | channel variable(FS 内部) | SIP 自定义头(穿越 SIP 网络) |
| 来电事件 | CHANNEL_PARK(MQ 消息) | INVITE(SIP 请求) |
| 挂断事件 | CHANNEL_HANGUP(MQ 消息) | BYE(SIP 请求) |
| DTMF | DTMF 事件(MQ 消息) | RFC 2833 RTP 事件(媒体流内携带) |
| 媒体建立触发 | Java 调 uuid_answer 后 | Java 发 200 OK 后 |
| 取音方式 | uuid_audio_fork 旁路复制 PCM | Java RtpReceiver 直收 RTP + G.711 解码 |
| 放音方式 | uuid_broadcast 在通道内播放 | Java RtpSender 构造 RTP 包发回 FS |
| 编解码责任 | FS 内部完成,Java 只处理 PCM | Java 自己做 G.711 编解码 + 采样率转换 |
| 打断机制 | ASR 首字 → ESL uuid_break → FS 停止播放 | 模型首字事件 → Java 清空本地播放队列 |
天然隔离:两种模式的路由决策点相同(都在 dialplan 的 Lua 脚本)。分支点在 Lua 内部:bot_type 决定走 park 还是 transfer。Pipeline 只认 CHANNEL_PARK 事件,双腿只认带自定义头的 INVITE,不会抢同一通电话。
6. 呼出场景
6.1 originate 命令
FreeSWITCH 的 originate 命令是主动发起外呼的核心原语。通用语法:
originate {变量列表}拨号目标 [接通后动作]
| 组成部分 | 说明 | 示例 |
|---|---|---|
| 变量列表 | {key1=val1,key2=val2} 包裹,设置在 originate 创建的通道上 | {origination_uuid=xxx,origination_caller_id_number=153xxx} |
| 拨号目标 | 指定呼叫谁,格式决定路由方式 | sofia/external/phone@gateway / user/extension |
| 接通后动作 | 被叫接听后通道进入什么状态 | &park() / &bridge(user/8801) / 9999 XML public |
拨号目标格式
| 场景 | 格式 | 含义 |
|---|---|---|
| 外部 IP:Port | sofia/external/phone@ip:port | 通过 external profile 呼到指定 IP:Port |
| 命名网关 | sofia/gateway/gwname/phone | 通过 FS 配置的命名 SIP 网关呼出 |
| 内部分机 | user/extension | 呼叫注册到 FS 的本地 SIP UA |
关键变量
| 变量 | 含义 |
|---|---|
origination_uuid | 预分配通道 UUID,Java 用此值跟踪通话 |
origination_caller_id_number | 主叫显示号码 |
absolute_codec_string | 强制编解码协商(PCMU/PCMA) |
call_timeout | 呼叫超时秒数 |
ignore_early_media | 忽略早期媒体(180 带 SDP) |
6.2 单腿呼出流程
Java → ESL originate {vars}sofia/external/phone@gateway 9999 XML public
│
▼
FS → SIP 网关 → 运营商 → 用户手机响铃
│
▼ 用户接听
FS 产生 CHANNEL_ANSWER 事件 → MQ 投递
│
▼ Java 消费
ChannelAnswerHandler:
├── setWriteLevel(uuid, "4") # 放音音量最大
├── uuid_audio_fork(uuid, wsUrl, 8000) # 启动取音旁路
└── greetingService.handleChannelAnswer() # 流式打招呼
│
▼
通道进入 dialplan extension 9999 → 录音 + park
│
▼ 产生 CHANNEL_PARK
进入正常事件处理链路,后续由 Java 通过 ESL 命令驱动
接通后动作:9999 XML public 让通道接通后进入 dialplan 的 extension 9999(public context),在那里统一执行录音、park 等动作,然后产生 CHANNEL_PARK 事件进入正常处理链路。也可以直接用 &park()。
6.3 双腿呼出流程
方式一:originate &bridge 一步到位
originate {vars}sofia/external/phone@gateway &bridge(user/8801)
Java → ESL originate {vars}sofia/external/phone@gateway &bridge(user/8801)
│
▼
FS 先呼外线(A-leg):sofia/external/phone@gateway
│
▼ 用户接听 → A-leg ANSWER
FS 执行 &bridge(user/8801) → 向 8801 分机发 INVITE(B-leg)
│
▼ Java SipAgent 收到 INVITE
解析 SDP + 自定义头 → 200 OK + SDP Answer
│
▼ A-leg 和 B-leg 在 FS 内部 RTP 直连
媒体桥接启动:FS(A) ←─RTP─→ FS(B) ←─RTP─→ Java ←─WS─→ 模型
方式二:originate + uuid_transfer 两阶段
# 阶段1:先呼外线,接通后 park
originate {vars}sofia/external/phone@gateway &park()
# 阶段2:Java 收到 CHANNEL_ANSWER 后,预连模型 + uuid_transfer
uuid_transfer {uuid} 8801 XML default
阶段1:originate ... &park()
→ 外线接通(A-leg) → CHANNEL_ANSWER → MQ → Java 消费
→ Java 创建 FsCallSession + 预连模型 WS(异步)
→ set sip_h_X-FS-Session-Id={sessionId} # 注入会话标识
→ uuid_transfer 8801 XML default
阶段2:FS 向 Java 分机发 INVITE
→ Java handleOutboundTransferIncoming:
→ 从 X-FS-Session-Id 匹配预注册 session
→ 从 SDP 解析 FS 的 RTP 端点
→ session.initRtp(remoteIp, remotePort) # 补全 RTP 信息
→ sipAgent.answerCall() → 200 OK + SDP Answer
→ session.start() → 启动音频桥接线程
→ onBridgeComplete → sendGreetingIfOutbound() → 发 SayHello
两阶段设计的工程原因:&bridge 模式下 FS 自动完成桥接,Java 无法在 bridge 前插入自定义逻辑(如预连模型 WS、注入会话标识)。而 Java 需要在收到 INVITE 时从 SDP 获取 RTP 端点,才能初始化 RtpReceiver/RtpSender。两阶段方案让 Java 在收到 INVITE 时有完整的控制权:阶段1 预连模型,阶段2 收到 INVITE 时补全 RTP 信息后应答。
6.4 呼出两种架构对比
| 维度 | 单腿 | 双腿 |
|---|---|---|
| originate 接通后动作 | 9999 XML public(进入 dialplan 后 park) | &bridge(user/ext) 或 &park() + uuid_transfer |
| 接通事件获取 | CHANNEL_ANSWER(MQ) | CHANNEL_ANSWER(MQ)+ SIP INVITE |
| 媒体建立 | audio_fork 旁路 | B-leg RTP 直收 |
| 问候语触发 | Java 通过 ESL 播放 TTS | Java 发 SayHello 事件给模型 |
| 会话标识传递 | channel variable | SIP 自定义头(X-FS-Session-Id) |
6.5 呼入 vs 呼出
| 维度 | 呼入 | 呼出 |
|---|---|---|
| 发起方 | FS 被动接收 INVITE | Java 主动调用 originate |
| callerId 来源 | 运营商在 INVITE From 头提供 | Java 在 originate 变量中设置 |
| 网关选择 | 由运营商/中继决定入口 | Java 选择 SIP 网关 |
| 问候语时机 | 用户打进后机器人先说话 | 外线接通后机器人先说话 |
| 入口 dialplan | inbound dialplan(destination_number 路由) | originate 的接通后动作(&app 或 extension XML context) |
7. 媒体路径对比
7.1 取音路径
| 维度 | 单腿 | 双腿 |
|---|---|---|
| 路径 | FS 内部 PCM → uuid_audio_fork 旁路复制 → WebSocket → ASR | FS 把 A-leg RTP 转发到 B-leg → Java RtpReceiver → G.711 解码 → 重采样 → 模型 WS |
| 媒体是否离开 FS | 否,fork 是只读旁路 | 是,RTP 流出 FS 进入 Java |
| Java 感知 RTP | 否 | 是 |
| 编解码 | FS 内部自动完成,Java 收到的是 PCM | Java 自己做 G.711 解码 |
7.2 放音路径
| 维度 | 单腿 | 双腿 |
|---|---|---|
| 路径 | TTS 音频 → HTTP 接口 → FS 拉流 → uuid_broadcast 在通道内播放 | 模型 WS 回包 PCM → Java G.711 编码 → RtpSender 发 RTP → FS(B→A) → 主叫 |
| 音频由谁产生 | FS 自行播放 | Java 构造 RTP 包发回 FS |
| 放音方向 | 可指定 aleg/bleg/both | 固定双向(bridge 后两腿都收到) |
7.3 编解码责任
| 单腿 | 双腿 |
|---|---|
编解码在 FS 内部完成。uuid_audio_fork 输出的已经是 PCM,uuid_broadcast 播放时 FS 自行处理格式转换。Java 只处理 PCM 文本/音频数据。 | Java 必须自己实现 G.711 编解码 + 采样率转换。FS 传过来的是 PCMU RTP 包,Java 需要解码为 PCM、重采样、送给模型;模型返回 PCM 需要重采样、编码为 PCMU、构造 RTP 包发回。 |
7.4 延迟构成
| 维度 | 单腿 | 双腿 |
|---|---|---|
| AI 处理往返 | 分层管道各层串行:ASR WS + LLM HTTP + TTS WS | 端到端模型单次 WS 往返 |
| 编解码开销 | FS 内部完成,不计入 Java 延迟 | Java 侧 G.711 编解码 + 重采样(毫秒级) |
| 打断延迟 | ASR 首字 + ESL 命令往返 + FS 执行 | 模型首字事件 + Java 本地清空队列 |
8. 控制平面对比
8.1 控制权归属
| 维度 | 单腿 | 双腿 |
|---|---|---|
| 媒体主导者 | FS | Java |
| 控制方式 | ESL 命令驱动 | SIP 信令 |
| Java 角色 | 外挂控制器 | 对等 SIP UA |
8.2 事件获取方式
| 事件类型 | 单腿 | 双腿 |
|---|---|---|
| 来电 | CHANNEL_PARK(MQ 消息) | INVITE(SIP 请求) |
| 接通 | CHANNEL_ANSWER(MQ 消息) | 200 OK(SIP 响应) |
| 挂断 | CHANNEL_HANGUP(MQ 消息) | BYE(SIP 请求) |
| DTMF | DTMF 事件(MQ 消息) | RFC 2833 RTP 事件(媒体流内携带) |
| 基础设施 | 需要 MQ(exchange + queue + consumer) | 不需要额外基础设施 |
| 事件种类 | 丰富(CHANNEL_CREATE/ANSWER/BRIDGE/HANGUP/DTMF/PLAYBACK_START 等) | SIP 层事件(INVITE/BYE/CANCEL) |
8.3 状态机归属
| 单腿:双状态机 | 双腿:单一状态机 |
|---|---|
| FS 通道状态机 + Java 业务状态机,两者通过事件同步。需要处理状态不一致的情况(如 FS 通道已挂断但 Java 还在等 ASR 结果)。 | Java 完全掌控 B-leg 生命周期。通道的建立、媒体协商、挂断都由 Java 的 SIP 信令驱动,FS 的通道状态被动跟随。 |
8.4 上下文传递机制对比
| 维度 | channel variable(单腿) | SIP 自定义头(双腿) |
|---|---|---|
| 设置方式 | set var=xxx | set sip_h_X-Field=xxx |
| 可见范围 | 仅 FS 内部(事件、ESL 命令) | SIP 信令层,可穿越 SIP 网络 |
| 传递距离 | 不离开 FS | 随 INVITE 传到 B-leg 终端 |
| Java 读取 | 从 MQ 事件 headers 读 variable_xxx | 从 SIP INVITE 头读 X-Field |
| 是否污染 SIP 信令 | 否 | 是(占用 SIP 头空间) |
9. 并发与故障域
9.1 并发模型
| 维度 | 单腿 | 双腿 |
|---|---|---|
| FS 侧 | 单进程承载所有通道,媒体在内核级转发 | 单进程承载所有通道,A/B 两腿 RTP 直连 |
| Java 侧并发隔离 | MQ 分片或哈希线程池(UUID.hashCode() % N),同 UUID 事件保序 | 每会话独立线程组,ConcurrentHashMap 隔离 |
| 事件顺序保证 | consistent-hash exchange 按 UUID 分片到固定队列 | SIP 事务层天然有序 |
9.2 故障域
| 单腿:分层隔离 | 双腿:耦合故障 |
|---|---|
| AI 管道分层,某层故障可独立降级(如 LLM 超时切 backup 厂商、TTS 降级到备用音色)。媒体腿不受 AI 管道故障影响——ASR 断了通道还在,TTS 挂了可以换一家重试。 | Java 进程是 B-leg 终端,Java 挂掉则 B-leg 断,FS 检测到 RTP 断流后挂断 A-leg。端到端模型断连同样波及整条腿——模型 WS 断开,Java 无法产生出向 RTP,B-leg 无媒体流,通话终止。 |
9.3 资源消耗
| 维度 | 单腿 | 双腿 |
|---|---|---|
| Java CPU | 不处理 RTP,CPU 消耗集中在 AI 管道的网络 IO | 处理每路通话的 RTP 编解码 + 重采样,CPU 与并发路数线性相关 |
| 网络带宽 | ESL 命令是短文本协议,带宽开销小 | RTP 媒体流带宽 + 模型 WS 音频流 |
| 基础设施 | 需要 MQ(exchange + queue + consumer) | 不需要额外 MQ |
10. 媒体工程要点
10.1 采样率转换(双腿独有)
FS 电话侧固定 8kHz PCMU,端到端模型通常要求 16kHz 输入 / 24kHz 输出。Java 必须在两个方向上做采样率转换。
- 上采样(8k → 16k):相对简单,线性插值即可。
- 下采样(24k → 8k):需要先低通滤波抗混叠再抽取。FIR 滤波器必须有状态——保存跨帧的历史样本,避免帧边界零填充导致的音频不连续("弹簧音")。
10.2 静音保活(双腿独有)
RTP 协议中,通话静音期间如果不发包,对端可能误判为媒体断开。FS 在一定时间内收不到 B-leg 的 RTP 包,可能触发媒体超时挂断。
解决方案:静音期间持续发送 RTP 静音包(PCMU 静音码为 0xFF),维持媒体流活跃。
10.3 噪声门控(双腿独有)
Java 直接收 RTP,收到的 PCM 数据包含背景噪声。双阈值噪声门控是常见工程方案:信号超过高阈值时开门(开始发送),低于低阈值时关门(停止发送),门控开关时加淡入淡出避免咔嗒声。
10.4 双向挂断防重入(双腿独有)
双腿架构中,挂断可能从 A-leg 侧(用户挂机 → FS 发 BYE → Java)或模型侧(模型 WS 断连 → Java 需要通过 ESL 让 FS 挂断 A-leg)发起。两条路径可能同时触发,需要用 CAS(compareAndSet)防止重复清理资源。
10.5 放音方向控制(单腿独有)
单腿 park 状态下,uuid_broadcast 可指定 aleg / bleg / both 方向。在转人工场景(临时建立 B-leg 桥接到坐席)中用于控制 AI 只对一侧放音,不让另一侧听到。
10.6 对称 RTP(双腿硬约束)
收发必须共用同一个 UDP socket。这是 RTP 协议的要求——对端根据收包源地址回复媒体,如果收发端口不同,对端无法正确回复。Java 实现中 RtpSender 必须从 RtpReceiver 获取共享 socket。
11. 总结
单腿与双腿是 FreeSWITCH 媒体腿数量的两种选择,对应控制平面与媒体平面的不同分工:
| 单腿(park + ESL 旁路) | 双腿(transfer + SIP UA) |
|---|---|
|
|
腿数是技术架构的第一性选择,决定了媒体路径、控制方式、编解码边界、故障隔离粒度的根本差异。理解这个差异,是做语音机器人架构选型的前提。
呼入与呼出的统一性:无论呼入还是呼出,两种架构的媒体腿结构、控制平面、编解码责任都是一致的。差异只在于发起方式(被动接收 INVITE vs 主动调用 originate)和上下文传递时机的细节。腿数选择与呼入/呼出场景正交。