我们在双方通话时,有时呼叫在必要情况下,会临时中断,或需要转接。系统需要能够始终保持一种可控状态。今天,因为场景变化比较快,我们再深入了解其机制。
SIP 呼叫保持实现
SIP 呼叫保持是在不释放底层的前提下临时中断双向实时媒体传输。在通信系统内部,呼叫保持由状态机迁移、SDP 媒体协商、网络地址穿透维护以及媒体编解码管道的重构共同组成。既然是临时的就需要恢复机制来实现。
实现呼叫保持的核心机制是在已经建立的会话中执行重协商。发起方通过在对话内发送修改了流方向或连接地址的 SDP Offer,改变对端的实时传输协议(RTP)媒体包处理行为。在需要背景音的场景中,发起方或媒体服务器会同时向对端注入单向保持音乐(MoH)。
呼叫保持的信令机制与演进
SIP 通话建立后,通信双方维持一个确认对话。呼叫保持通过在对话内发送重新邀请()请求或请求触发。

re-INVITE 使用与初始 INVITE 相同的报文结构,但遵循对话内路由规则:
• Call-ID 保持不变,标识同一呼叫。
• From 与 To 头部保留初始对话生成的 tag 参数。
• CSeq 序号在发起端本地序号基础上单调递增 1。
• 请求 URI 使用对端在先前的响应或请求中通过 Contact 头提供的直接通信地址。
SDP 规范在演进过程中经历了从连接层置零到方向属性协商的变化。
历史方案:RFC 2543 与零地址
早期 RFC 2543 规范要求发起方在 SDP 的连接数据行( c= )中填入全零 IP 地址:
c=IN IP4 0.0.0.0
这种语法告知对端停止向网络接口发送 RTP 数据。但在现代 NAT 与防火墙环境中,全零地址存在明显缺陷:
1. NAT 映射失效 :对端停止发送所有数据包后,中间路由器上的 UDP 端口映射表项迅速超时并被丢弃。
2. RTCP 监控中断 :零地址切断了双向 链路,导致丢包率与网络抖动等质量监控数据中断。
3. 防火墙拦截 :部分网络设备会将目的地址或源地址为 0.0.0.0 的数据包判定为畸形数据并直接丢弃。
现代规范:RFC 3264 方向属性
RFC 3264 废弃了全零地址机制,改为在媒体描述行( m= )或会话级别使用明确的媒体流方向属性,同时在 c= 行保留真实的单播网络地址:
• a=sendonly :发起方仅向远端发送 RTP 流(例如背景音乐),不再接收对端媒体。
• a=inactive :发起方完全停止双向 RTP 媒体传输。
• a=recvonly :发起方仅接收媒体,不向外发送媒体。该属性通常用于响应对端的 sendonly 请求。
保留真实的单播 IP 地址后,终端可以在保持期间持续发送 RTCP 接收端报告,维持防火墙上的 UDP 穿透针孔。
Alice (发起方) Bob (接收方)
| |
| re-INVITE (SDP: a=sendonly) |
|--------------------------------------->|
| 100 Trying (可选) |
|<---------------------------------------|
| 200 OK (SDP: a=recvonly) |
|<---------------------------------------|
| ACK |
|--------------------------------------->|
| |
|====== RTP 单向保持音乐 (MoH) =========>|
|...... RTCP 双向监控信令 ..............|
|<......................................>|
当双方完成 re-INVITE、200 OK 与 ACK 的三次握手后,呼叫保持状态正式生效,双向语音通话转换为受控的单向媒体或静默状态。
SDP 属性协商规则与状态机
SDP Offer/Answer 模型要求属性协商符合明确的对称规则。Answer 端必须根据 RFC 3264 的映射矩阵做出应答。
方向映射矩阵
接收端收到包含流方向属性的 Offer 时,应答属性遵循以下映射关系:
| Offer 属性 | Answer 对应属性 | 媒体流实际行为 |
|---|---|---|
sendonly | recvonly | 发起端发送单向 RTP(如背景音),接收端仅收不发 |
inactive | inactive | 双方完全停止收发 RTP 数据包 |
recvonly | sendonly | 接收端向发起端发送单向媒体,发起端不发包 |
sendrecv | sendrecv | 双方恢复全双工媒体传输,通话恢复正常 |
如果接收端因本地资源限制无法支持 recvonly ,可以在 Answer 中将属性降级为 a=inactive ,但不可将其更改为 sendrecv 。
会话版本号管理
SDP 消息的 o= (Owner/Creator)字段格式如下: o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
• sess-id :会话唯一标识符,整个通话生命周期内保持不变。
• sess-version :会话描述的版本号。
每次发起端发送修改了媒体属性、端口或连接地址的 SDP Offer 时, sess-version 必须单调递增。若 sess-version 未发生变化,遵循 RFC 3264 的 SIP 协议栈会判定媒体参数无改动,进而忽略该重协商请求。
INVITE sip:bob@192.0.2.20:5060 SIP/2.0
Via: SIP/2.0/UDP 192.0.2.10:5060;branch=z9hG4bK-hold-01
Max-Forwards: 70
To: Bob <sip:bob@example.com>;tag=bob-tag-987
From: Alice <sip:alice@example.com>;tag=alice-tag-123
Call-ID: c84931a0-432a-11ee-be56-0242ac120002
CSeq: 102 INVITE
Contact: <sip:alice@192.0.2.10:5060>
Content-Type: application/sdp
Content-Length: 215
v=0
o=alice 1693520000 1693520001 IN IP4 192.0.2.10
s=VoIP Call
c=IN IP4 192.0.2.10
t=0 0
m=audio 40002 RTP/AVP 0 101
a=rtpmap:0 PCMU/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-16
a=sendonly
媒体通道处理与音乐保持
用户触发呼叫保持后,终端或媒体服务器会对音频管道执行重构。
终端本地处理
在对等通信(P2P)模式下,发起保持的客户端执行以下操作:
1. 停止麦克风采集 :关闭麦克风输入并暂停向编码器供给音频数据。
2. 静音扬声器 :停止解码或丢弃对端可能传来的残余 RTP 数据包。
3. 注入本地保持音乐 :若协商为 a=sendonly ,客户端音频管道切换输入源,将本地音频文件(如 PCM 格式背景音)送入编码器,持续向对端推送单向 RTP 流。
媒体服务器与 B2BUA 处理
在企业 PBX 或软交换系统(如 FreeSWITCH、Asterisk)中,服务端作为(B2BUA)分别维持两条信令腿(Leg A 与 Leg B):
Alice (用户端) FreeSWITCH / B2BUA Bob (用户端)
| | |
|-- re-INVITE (sendonly) ---->| |
|<- 200 OK (recvonly) --------| |
|-- ACK --------------------->| |
| | |
| |-- re-INVITE (sendonly) ->|
| |<- 200 OK (recvonly) -----|
| |-- ACK ------------------>|
| | |
| (媒体暂停) |=== RTP 背景音乐 (MoH) ===>
Alice 触发保持后,B2BUA 拆开两端原有的 RTP 桥接通道,将发往 Bob 的媒体端口切换至内置的音频播放模块,由服务端周期性发送背景音乐 RTP 包。
Note
切勿在呼叫保持期间将 SDP 媒体描述行的端口设置为 0(如 m=audio 0 RTP/AVP 0)。在 SDP 规范中,将端口设为 0 表示永久关闭并释放该媒体通道。多数协议栈在收到端口为 0 的 Answer 后将无法通过后续的 re-INVITE 恢复通话。
异常场景与故障排查
呼叫保持涉及两端异步状态交互,在网络抖动或高频操作下容易出现竞态问题。
并发请求冲突与 491 状态码
若通话双方几乎同时按下保持按键,两个 re-INVITE 会在网络中发生碰撞。
根据 RFC 3261 规范,SIP 对话内同一时刻只允许存在一个正在处理的 INVITE 事务。当一方收到对端的 re-INVITE,且自身发出的 re-INVITE 仍在等待最终响应时,该端必须回复 491 Request Pending 状态码。
Alice (端点 1) Bob (端点 2)
| |
|---- re-INVITE (CSeq: 102) ------------------------>| (碰撞)
|<--- re-INVITE (CSeq: 501) -------------------------|
| |
|---- 491 Request Pending -------------------------->|
|<--- 491 Request Pending ---------------------------|
|---- ACK ------------------------------------------>|
|<--- ACK -------------------------------------------|
| |
| [定时器: 2.1s - 4.0s] [定时器: 0.0s - 2.0s]
| (呼叫发起方退避) (呼叫应答方退避)
为避免双方无限重试冲突,协议规定了确定性的随机退避策略:
• 区分角色 :创建初始呼叫的请求发起方属于所有者(Owner),应答方属于非所有者(Non-owner)。
• 所有者等待时长 :在 2.1 秒至 4.0 秒区间内随机选取:
• 非所有者等待时长 :在 0.0 秒至 2.0 秒区间内随机选取:
非所有者优先超时并重发 re-INVITE,此时所有者处于等待期,可以正常接收并处理该请求,解除死锁。
NAT 老化与恢复单通故障
若呼叫保持采用 a=inactive 模式,双方媒体流完全中断。部分防火墙上的 UDP 映射超时阈值仅为 30 到 60 秒。
若保持状态持续时间超过防火墙超时阈值,原有 NAT 映射将被清除。后续恢复通话( a=sendrecv )时,对端发来的 RTP 媒体包会被防火墙当成未经请求的外部连接直接丢弃,导致恢复后出现单向无声或双向无声。
缓解方案包括:
1. 维持后台 RTCP 传输 :即便媒体流暂停,双方仍应周期性发送 RTCP Receiver Report 包,刷新 NAT 映射。
2. 优先采用 MoH 方案 :保持期间向远端持续发送单向媒体包,利用背景音乐维持端口活跃。
3. OPTIONS 探测包 :边缘代理或 SBC 在媒体静默期间向终端发送 OPTIONS 消息维持信令链路。
编解码器漂移
部分客户端在生成保持或恢复的 SDP 时,会重新读取系统可用编码列表,导致 Offer 中的载荷类型或编解码器优先级与初始通话不一致。若接收端协议栈无法动态重构解码器与抖动缓冲区,会直接出现解码失败。
在整个会话生命周期内,重协商必须维持编解码器的载荷类型编号(PT)、时钟频率及关键属性参数与初次协商结果严格一致。
呼叫保持实现代码示例
基于 PJSIP 协议栈开发时,呼叫保持通过调整媒体通道方向标志并驱动会话重协商实现。
/* 发起呼叫保持 */
pj_status_t sip_hold_call(pjsua_call_id call_id) {
pjsua_call_setting opt;
pjsua_call_setting_default(&opt);
opt.flag = PJSUA_CALL_UPDATE_CONTACT;
/* PJSIP 内部会将 SDP 属性设置为 a=sendonly 或 a=inactive */
/* 并自动递增 o= 字段中的 sess-version 版本号 */
return pjsua_call_set_hold(call_id, &opt);
}
/* 恢复正常通话 */
pj_status_t sip_unhold_call(pjsua_call_id call_id) {
pjsua_call_setting opt;
pjsua_call_setting_default(&opt);
/* 触发重协商恢复 a=sendrecv */
return pjsua_call_reinvite(call_id, PJ_TRUE, &opt);
}
在调用 pjsua_call_set_hold 时,底层协议栈的处理步骤如下:
1. 校验当前会话是否处于 PJSIP_INV_STATE_CONFIRMED 稳定对话状态。
2. 克隆本地媒体配置,将音频方向标志由 PJMEDIA_DIR_ENCODING_DECODING 调整为 PJMEDIA_DIR_ENCODING 或 PJMEDIA_DIR_NONE 。
3. 将本地 SDP o= 行中的 sess-version 序号递增 1。
4. 构建并发送包含新 SDP 的 re-INVITE 请求。
5. 收到对端的 200 OK 后,协议栈自动回复 ACK ,并停止本地麦克风音频采集管道。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。