mediasou c++分析--流分析

简单理解 WebRTC Simulcast 的流处理机制:从多路输入到单路输出的完整流程

Producer 保留多路 Simulcast 输入;每个 Consumer 独立选择其中一路,并转换成一条连续的输出媒体流。RTX 和 FlexFEC 都是逐跳终止、逐跳重新生成,不会原样穿透 Router。

Producer 如何识别和管理多路流

Transport 收到 RTP 后,先通过 RtpListener 找到对应 Producer。注册 Producer 时,以下 SSRC 都会关联到同一个 Producer:

编码层 媒体 SSRC RTX SSRC FlexFEC SSRC
LOW 1111 2111 3111
MEDIUM 1222 2222 3222
HIGH 1333 2333 3333

WebRTC Transport 找到 Producer 后调用:

cpp 复制代码
producer->ReceiveRtpPacket(packet);

入口位于 WebRtcTransport.cpp:906,SSRC 注册逻辑位于 RtpListener.cpp:62

Producer 内部主要维护两张表:

text 复制代码
mapSsrcRtpStreamInfo
  media SSRC -> RtpStreamRecv + profile + RTX SSRC

mapFlexFecSsrcRtpStream
  FlexFEC SSRC -> 它保护的 RtpStreamRecv

例如:

text 复制代码
1111 -> RtpStreamRecv LOW,    RTX=2111
1222 -> RtpStreamRecv MEDIUM, RTX=2222
1333 -> RtpStreamRecv HIGH,   RTX=2333

3111 -> LOW RtpStreamRecv
3222 -> MEDIUM RtpStreamRecv
3333 -> HIGH RtpStreamRecv

创建逻辑在 Producer.cpp:841

三类RTP包在Producer中的处理流程

#mermaid-svg-uSNNhlgFZY5Q78Do{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uSNNhlgFZY5Q78Do .error-icon{fill:#552222;}#mermaid-svg-uSNNhlgFZY5Q78Do .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uSNNhlgFZY5Q78Do .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uSNNhlgFZY5Q78Do .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uSNNhlgFZY5Q78Do .marker.cross{stroke:#333333;}#mermaid-svg-uSNNhlgFZY5Q78Do svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uSNNhlgFZY5Q78Do p{margin:0;}#mermaid-svg-uSNNhlgFZY5Q78Do .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster-label text{fill:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster-label span{color:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster-label span p{background-color:transparent;}#mermaid-svg-uSNNhlgFZY5Q78Do .label text,#mermaid-svg-uSNNhlgFZY5Q78Do span{fill:#333;color:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do .node rect,#mermaid-svg-uSNNhlgFZY5Q78Do .node circle,#mermaid-svg-uSNNhlgFZY5Q78Do .node ellipse,#mermaid-svg-uSNNhlgFZY5Q78Do .node polygon,#mermaid-svg-uSNNhlgFZY5Q78Do .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uSNNhlgFZY5Q78Do .rough-node .label text,#mermaid-svg-uSNNhlgFZY5Q78Do .node .label text,#mermaid-svg-uSNNhlgFZY5Q78Do .image-shape .label,#mermaid-svg-uSNNhlgFZY5Q78Do .icon-shape .label{text-anchor:middle;}#mermaid-svg-uSNNhlgFZY5Q78Do .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uSNNhlgFZY5Q78Do .rough-node .label,#mermaid-svg-uSNNhlgFZY5Q78Do .node .label,#mermaid-svg-uSNNhlgFZY5Q78Do .image-shape .label,#mermaid-svg-uSNNhlgFZY5Q78Do .icon-shape .label{text-align:center;}#mermaid-svg-uSNNhlgFZY5Q78Do .node.clickable{cursor:pointer;}#mermaid-svg-uSNNhlgFZY5Q78Do .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uSNNhlgFZY5Q78Do .arrowheadPath{fill:#333333;}#mermaid-svg-uSNNhlgFZY5Q78Do .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uSNNhlgFZY5Q78Do .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uSNNhlgFZY5Q78Do .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uSNNhlgFZY5Q78Do .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uSNNhlgFZY5Q78Do .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uSNNhlgFZY5Q78Do .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster text{fill:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do .cluster span{color:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uSNNhlgFZY5Q78Do .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uSNNhlgFZY5Q78Do rect.text{fill:none;stroke-width:0;}#mermaid-svg-uSNNhlgFZY5Q78Do .icon-shape,#mermaid-svg-uSNNhlgFZY5Q78Do .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uSNNhlgFZY5Q78Do .icon-shape p,#mermaid-svg-uSNNhlgFZY5Q78Do .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uSNNhlgFZY5Q78Do .icon-shape .label rect,#mermaid-svg-uSNNhlgFZY5Q78Do .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uSNNhlgFZY5Q78Do .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uSNNhlgFZY5Q78Do .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uSNNhlgFZY5Q78Do :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-uSNNhlgFZY5Q78Do .decision>*{fill:#fff4d8!important;stroke:#99711d!important;color:#493400!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .decision span{fill:#fff4d8!important;stroke:#99711d!important;color:#493400!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .decision tspan{fill:#493400!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .process>*{fill:#e5f0ff!important;stroke:#336b9e!important;color:#17354f!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .process span{fill:#e5f0ff!important;stroke:#336b9e!important;color:#17354f!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .process tspan{fill:#17354f!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .terminal>*{fill:#f3e4e4!important;stroke:#914646!important;color:#4c2323!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .terminal span{fill:#f3e4e4!important;stroke:#914646!important;color:#4c2323!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .terminal tspan{fill:#4c2323!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .success>*{fill:#e1f3e5!important;stroke:#347848!important;color:#183d24!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .success span{fill:#e1f3e5!important;stroke:#347848!important;color:#183d24!important;}#mermaid-svg-uSNNhlgFZY5Q78Do .success tspan{fill:#183d24!important;} FlexFEC SSRC


媒体 SSRC
RTX SSRC




收到一个 RTP 包
SSRC 属于哪一类?
交给对应层的

FlexFecReceiveHandler
恢复出媒体包?
FEC 包到此结束

不直接转发
以 isRecover=true

重新进入 ReceiveRtpPacket
找到对应 RtpStreamRecv
将媒体包送入 FEC 接收器

作为恢复参考
ReceivePacket

乱序、重复、丢包统计
找到对应媒体流
ReceiveRtxPacket

去掉 RTX OSN

恢复媒体 PT/SSRC/Seq
包有效?
丢弃
Producer 暂停?
映射 PayloadType

映射 RTP 扩展 ID
通知 Router

携带 packet + profile

普通媒体包的处理

  1. 根据媒体 SSRC 找到 RtpStreamRecv
  2. 先把包交给 FlexFEC 模块,作为后续恢复其他丢包的参考。
  3. 调用 RtpStreamRecv::ReceivePacket() 做序列号、丢包、抖动和 NACK 检测。
  4. 携带该流的 profile 转给 Router。

对应 Producer.cpp:309

RTX重传包的处理

RTX 包不会直接转发给 Consumer。

  1. 根据 RTX SSRC 找到它对应的媒体 RtpStreamRecv
  2. ReceiveRtxPacket() 把 RTX 包还原为原媒体包:
    • RTX SSRC 改回媒体 SSRC
    • RTX PT 改回媒体 PT
    • 从 RTX Payload 取出原始序列号 OSN
    • 去掉 RTX 额外头
  3. 后续把它当作普通媒体包转发。

对应 Producer.cpp:350

发布端发来的 FlexFEC 包

FlexFEC 包同样不会直接转发。

  1. 根据 FEC SSRC 找到它保护的媒体流。
  2. 交给 FlexFEC 解码器。
  3. 如果恢复出媒体 RTP,触发 OnRecoveredPacket()
  4. 恢复包以 isRecover=true 重新进入正常 Producer 流程。
  5. 原始 FlexFEC 包处理完成后直接结束。

对应 Producer.cpp:270Producer.cpp:429

因此进入 Router 的只有:

text 复制代码
正常媒体包
+ RTX 恢复出来的媒体包
+ FlexFEC 恢复出来的媒体包

不会进入 Router 的是:

text 复制代码
原始 RTX 包
原始 FlexFEC 包

3. Router 如何转发

Router 不负责选层。它只是遍历该 Producer 关联的所有 Consumer:

cpp 复制代码
for (auto* consumer : consumers)
{
    if (consumer->IsEnabled())
        consumer->SendRtpPacket(packet, profile);
}

Router.cpp:2501

同一个高层包可能会依次送给 A、B、C 三个 Consumer,但每个 Consumer 会根据自己的 effectiveProfile 决定接收还是丢弃。

例如:

输入包 profile Consumer A Consumer B Consumer C
LOW 转发 丢弃 丢弃
MEDIUM 丢弃 转发 丢弃
HIGH 丢弃 丢弃 转发

所以一个 Producer 可以同时让:

text 复制代码
弱网用户看 LOW
普通用户看 MEDIUM
大屏用户看 HIGH

4. Consumer 如何把多路输入变成单路输出

Consumer 只创建一个 RtpStreamSend,并要求至少存在一个输出 encoding,见 Consumer.cpp:152

对选中的输入包,Consumer 会依次执行:

text 复制代码
输入媒体 SSRC/Seq/Timestamp
          ↓
检查 profile 是否等于 effectiveProfile
          ↓
必要时处理编码层/时间层
          ↓
重写 SSRC 为 Consumer 的媒体 SSRC
          ↓
重写 Sequence Number,保持连续
          ↓
重写 Timestamp,保持连续
          ↓
存入 RtpStreamSend 重传缓存
          ↓
发送媒体包
          ↓
根据重写后的媒体包生成 Consumer FlexFEC

关键代码位于 Consumer.cpp:514

这里非常重要:

接收端不会看到 Producer 的 1111、1222、1333 三个媒体 SSRC。它只看到 Consumer 自己协商的一条媒体 SSRC,例如 5001。

5. Simulcast 切层怎么保证连续

假设 Consumer 当前发送 LOW,准备切换到 HIGH:
发布端 Producer Router Consumer 发布端 Producer Router Consumer #mermaid-svg-LwVRWRttzVNTI2lQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LwVRWRttzVNTI2lQ .error-icon{fill:#552222;}#mermaid-svg-LwVRWRttzVNTI2lQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LwVRWRttzVNTI2lQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LwVRWRttzVNTI2lQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LwVRWRttzVNTI2lQ .marker.cross{stroke:#333333;}#mermaid-svg-LwVRWRttzVNTI2lQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LwVRWRttzVNTI2lQ p{margin:0;}#mermaid-svg-LwVRWRttzVNTI2lQ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LwVRWRttzVNTI2lQ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LwVRWRttzVNTI2lQ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LwVRWRttzVNTI2lQ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LwVRWRttzVNTI2lQ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LwVRWRttzVNTI2lQ .sequenceNumber{fill:white;}#mermaid-svg-LwVRWRttzVNTI2lQ #sequencenumber{fill:#333;}#mermaid-svg-LwVRWRttzVNTI2lQ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LwVRWRttzVNTI2lQ .messageText{fill:#333;stroke:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LwVRWRttzVNTI2lQ .labelText,#mermaid-svg-LwVRWRttzVNTI2lQ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .loopText,#mermaid-svg-LwVRWRttzVNTI2lQ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LwVRWRttzVNTI2lQ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LwVRWRttzVNTI2lQ .noteText,#mermaid-svg-LwVRWRttzVNTI2lQ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LwVRWRttzVNTI2lQ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LwVRWRttzVNTI2lQ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LwVRWRttzVNTI2lQ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LwVRWRttzVNTI2lQ .actorPopupMenu{position:absolute;}#mermaid-svg-LwVRWRttzVNTI2lQ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LwVRWRttzVNTI2lQ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LwVRWRttzVNTI2lQ .actor-man circle,#mermaid-svg-LwVRWRttzVNTI2lQ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LwVRWRttzVNTI2lQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} targetProfile = HIGH请求关键帧发送 PLIHIGH 普通帧profile=HIGH尚未收到关键帧,不能切换HIGH 关键帧profile=HIGHeffectiveProfile = HIGH重同步 Seq 和 Timestamp输出仍使用同一个媒体 SSRC

Consumer 使用三个状态:

  • preferredProfile:用户希望的层。
  • targetProfile:正在尝试切换到的层。
  • effectiveProfile:当前真正向接收端发送的层。

升层通常先做带宽探测;网络不健康时降层。确定目标层后,需要等目标层关键帧才真正切换,见 Consumer.cpp:1277

切换后:

  • 输出 SSRC 不变。
  • 输出序列号连续。
  • 输出时间戳连续。
  • 清空旧的重传缓存。
  • 后续非目标 profile 的包全部丢弃。

因此接收端感觉是一条流发生了清晰度变化,而不是换了三条 RTP 流。

6. 下游 RTX 和 FlexFEC 如何产生

下游 RTX

Consumer 发送媒体包时,RtpStreamSend 会缓存已经重写后的输出包。

接收端发来 NACK 后:

  1. Transport 根据反馈中的媒体 SSRC 找到 Consumer。
  2. Consumer 从自己的发送缓存找到丢失包。
  3. 如果协商了 RTX,调用 RtxEncode()
  4. 使用 Consumer 自己的 RTX SSRC 和 RTX PT 发送。

Consumer.cpp:941Consumer.cpp:1235

text 复制代码
Producer RTX 2111
    ↓ 解包
内部普通媒体包
    ↓ Consumer 重写
Consumer 媒体 SSRC 5001
    ↓ 下游 NACK
重新封装
    ↓
Consumer RTX SSRC 6001

下游 FlexFEC

Consumer 启用时,根据自己的 RTP 参数创建 FlexFEC Sender,见 Consumer.cpp:1158

发送顺序是:

text 复制代码
选中的媒体包
    ↓
重写为 Consumer SSRC/Seq/Timestamp
    ↓
FlexFEC 编码器生成保护包并暂存
    ↓
先发送媒体包
    ↓
再发送 FlexFEC 包

因此下游 FlexFEC 保护的是 Consumer 重写后的输出流,不是 Producer 原始 SSRC。

最终总结

text 复制代码
发布端三层 Simulcast
  每层 = media + RTX + FlexFEC
                │
                ▼
Producer 分别维护三个 RtpStreamRecv
  RTX 在这里解包
  FlexFEC 在这里恢复媒体包
                │
                ▼
Router 将恢复后的普通媒体包广播给所有 Consumer
                │
                ▼
每个 Consumer 独立选择 LOW / MEDIUM / HIGH
                │
                ▼
重写成一条连续输出媒体流
                │
                ├── 下游丢包时重新生成 RTX
                └── 根据输出媒体包重新生成 FlexFEC

所以它不是"把 Producer 的多条流和修复流直接搬到 Consumer",而是:

Producer 侧终止上游传输保护,Router 分发规范化媒体包,Consumer 侧完成选层、连续化,并重新建立下游 RTX/FlexFEC 保护。

相关推荐
糖炒栗子03262 小时前
learn-claude-code 简要记录
笔记·python
PC2005-cloud3 小时前
Elasticsearch 学习笔记:集群实战(3 控制节点 + 3 数据节点部署与故障转移)
笔记·学习·elasticsearch
头茬韭菜3 小时前
功能点 1:项目骨架与启动流程 —— 源码阅读笔记
运维·笔记·debian·fluss
墨雨晨曦883 小时前
2026/08/16 AI学习笔记
笔记·学习
CarIise4 小时前
C语言基础课程笔记:硬件原理、数据类型、结构体与格式化输入输出详解
c语言·开发语言·笔记
今儿敲了吗4 小时前
CN——数据链路层(下)
网络·笔记
RainCityLucky4 小时前
Java Swing 自定义组件库分享(十六)
java·笔记·后端
wy_hhxx5 小时前
AI Agent & Harness Engineering 笔记
人工智能·笔记
lengjingzju5 小时前
一天掌握Vim,精华使用笔记
笔记·vim·excel