4、同步源与贡献源:SSRC与CSRC标识、混合器与翻译器场景、同步上下文管理
聊到 RTP 的同步机制,有两个标识符你绕不开——SSRC 和 CSRC。说白了,它们就是 RTP 流里每个参与者的「身份证」。但身份证也有不同类型,SSRC 是主卡,CSRC 是副卡。怎么用?什么时候用?我结合项目里的坑跟你聊聊。
4.1 SSRC:同步源的唯一身份证
SSRC(Synchronization Source)是每个 RTP 流的唯一标识。一个 SSRC 对应一个独立的 RTP 流,比如一个摄像头、一个麦克风、一个屏幕共享。它是个 32 位的随机数,由发送端在会话开始时生成。
关键点:SSRC 必须在同一个 RTP 会话内唯一。如果两个流用了同一个 SSRC,接收端会直接丢弃其中一个,或者产生混乱。
我在项目中遇到过一个问题:两个编码器实例不小心配了相同的 SSRC,结果接收端只显示一路视频,另一路完全黑屏。排查了半天才发现是 SSRC 冲突。从那以后,我习惯在代码里加一个 SSRC 冲突检测机制。
SSRC 的生成规则
RFC 3550 建议用随机算法生成 SSRC,但实际工程中,我更喜欢用哈希算法,比如把 IP + 端口 + 时间戳做一次 MD5,取低 32 位。这样冲突概率极低,而且可复现。
// 伪代码示例:生成 SSRC
uint32_t generate_ssrc(const char* ip, uint16_t port) {
char input[64];
snprintf(input, sizeof(input), "%s:%u:%lu", ip, port, time(NULL));
uint8_t hash[16];
md5(input, strlen(input), hash);
return (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3];
}
4.2 CSRC:贡献源的贡献列表
CSRC(Contributing Source)是 RTP 头里的一个列表,用来标识哪些源参与了当前 RTP 包的内容。它只在混合器场景下出现。
举个例子:一个视频会议里,4 个人在说话。混合器把 4 路音频混成 1 路,然后发送。接收端需要知道这路音频里包含了谁的贡献。CSRC 列表就干这个事。
| 字段 | 长度 | 说明 |
|---|---|---|
| CC | 4 bits | CSRC 数量,范围 0-15 |
| CSRC[0..CC-1] | 32 bits each | 每个贡献源的 SSRC |
我的习惯:CSRC 列表的顺序很重要。第一个 CSRC 通常是音量最大的源,或者最近发言的源。接收端可以根据这个顺序做 UI 高亮。
4.3 混合器与翻译器:两种拓扑场景
RTP 定义了两种中间节点:混合器(Mixer)和翻译器(Translator)。它们处理 SSRC/CSRC 的方式完全不同。
混合器(Mixer)
混合器把多路 RTP 流合并成一路。它重新生成 SSRC,并在 RTP 头里插入 CSRC 列表。接收端看到的是混合器的 SSRC,而不是原始源的 SSRC。
- 优点:节省带宽,接收端只需要处理一路流。
- 缺点:丢失了原始源的同步信息,比如时间戳和序列号。
- 典型场景:音频会议、MCU 视频会议。
翻译器(Translator)
翻译器不合并流,它只是转发或转换格式。它保留原始源的 SSRC,不修改 RTP 头。接收端看到的是原始源的 SSRC。
- 优点:保持原始同步信息,适合需要独立解码的场景。
- 缺点:带宽消耗大,接收端需要处理多路流。
- 典型场景:网关、转码器、防火墙穿透。
| 特性 | 混合器 | 翻译器 |
|---|---|---|
| SSRC 处理 | 重新生成 | 保留原始 |
| CSRC 使用 | 必须 | 可选 |
| 带宽消耗 | 低 | 高 |
| 同步信息 | 丢失 | 保留 |
| 典型延迟 | 高(需要混音) | 低(直接转发) |
我曾经踩过的坑:在做一个网关项目时,我错误地把翻译器当混合器用,导致接收端收到了多个相同 SSRC 的流,直接崩溃。后来我加了一个 SSRC 映射表,才解决问题。
4.4 同步上下文管理
同步上下文(Synchronization Context)是接收端用来关联不同 RTP 流的一组信息。它包含 SSRC、CNAME、时间戳基准等。没有它,你没法把音频和视频对齐。
上下文的核心组成
- SSRC 表:记录当前会话中所有活跃的 SSRC。
- CNAME 映射:把 SSRC 映射到 Canonical Name,用于关联同一源的多个流。
- 时间戳基准:每个 SSRC 的 RTP 时间戳与 NTP 时间的对应关系。
- 序列号窗口:用于丢包检测和重排序。
上下文管理的工程实践
我建议用哈希表来管理同步上下文。键是 SSRC,值是一个结构体,包含 CNAME、时间戳基准、序列号状态等。
// 同步上下文结构体
typedef struct {
uint32_t ssrc;
char cname[256];
uint32_t timestamp_base;
uint32_t seq_num_base;
int64_t ntp_timestamp;
int64_t last_received_time;
} SyncContext;
// 哈希表管理
SyncContext* sync_table[1024];
SyncContext* find_or_create_context(uint32_t ssrc) {
int idx = ssrc % 1024;
if (sync_table[idx] == NULL) {
sync_table[idx] = malloc(sizeof(SyncContext));
memset(sync_table[idx], 0, sizeof(SyncContext));
sync_table[idx]->ssrc = ssrc;
}
return sync_table[idx];
}
我的经验:同步上下文需要定期清理。如果一个 SSRC 超过 30 秒没有收到任何包,我建议把它标记为过期,然后释放资源。否则内存会一直增长。
4.5 核心逻辑流程图
下面这张图展示了 SSRC/CSRC 在混合器和翻译器场景下的处理流程。你可以看到,混合器重新生成 SSRC,而翻译器保留原始 SSRC。
4.6 避坑指南
最后,我总结几个实际项目中容易踩的坑:
- SSRC 冲突:两个源用了相同的 SSRC,接收端会丢弃其中一个。解决方案:用哈希算法生成,或者加一个冲突检测。
- CSRC 列表溢出:CC 字段只有 4 位,最多 15 个 CSRC。如果混合器有 16 个源,只能记录前 15 个。解决方案:按音量或优先级排序,只保留最重要的。
- 混合器时间戳不一致:混合器重新生成时间戳,可能导致接收端无法对齐。解决方案:混合器需要维护一个统一的时间戳基准。
- 翻译器 SSRC 泄漏:翻译器如果错误地修改了 SSRC,接收端会认为是一个新流。解决方案:翻译器只转发,不修改 RTP 头。
我曾经犯过的错:在一个大型会议系统中,我忘了处理 CSRC 列表的排序问题。结果接收端总是把静音的人高亮显示,而真正说话的人反而被忽略了。后来我加了一个音量检测逻辑,才解决这个问题。