深入了解 SIP 呼叫保持实现机制

我们在双方通话时,有时呼叫在必要情况下,会临时中断,或需要转接。系统需要能够始终保持一种可控状态。今天,因为场景变化比较快,我们再深入了解其机制。

SIP 呼叫保持实现

SIP 呼叫保持是在不释放底层的前提下临时中断双向实时媒体传输。在通信系统内部,呼叫保持由状态机迁移、SDP 媒体协商、网络地址穿透维护以及媒体编解码管道的重构共同组成。既然是临时的就需要恢复机制来实现。

实现呼叫保持的核心机制是在已经建立的会话中执行重协商。发起方通过在对话内发送修改了流方向或连接地址的 SDP Offer,改变对端的实时传输协议(RTP)媒体包处理行为。在需要背景音的场景中,发起方或媒体服务器会同时向对端注入单向保持音乐(MoH)。

呼叫保持的信令机制与演进

SIP 通话建立后,通信双方维持一个确认对话。呼叫保持通过在对话内发送重新邀请()请求或请求触发。

深入了解 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 对应属性媒体流实际行为
sendonlyrecvonly发起端发送单向 RTP(如背景音),接收端仅收不发
inactiveinactive双方完全停止收发 RTP 数据包
recvonlysendonly接收端向发起端发送单向媒体,发起端不发包
sendrecvsendrecv双方恢复全双工媒体传输,通话恢复正常

如果接收端因本地资源限制无法支持 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 举报,一经查实,本站将立刻删除。

赞 (0)

相关推荐