带宽估计与拥塞控制到底在干什么:实时音视频为什么必须边传边降边试探

前言:为什么实时音视频最怕的,不是带宽低,而是你根本不知道现在到底还能发多少

前面那篇《WebRTC 为什么难》里,我一直在反复强调一件事:

实时音视频不是静态系统,而是动态系统。

这句话背后,最典型、最硬核的一条主线,其实就是:

  • 带宽估计(Bandwidth Estimation, BWE)
  • 拥塞控制(Congestion Control)

很多人第一次看到这两个词时,会下意识把它们理解成比较抽象的网络优化概念。

比如:

  • 带宽估计,就是“测网速”
  • 拥塞控制,就是“别发太多”

这样理解不算完全错,但还是太浅了。

因为真实的实时音视频系统里,你面对的不是:

我有一条稳定 4Mbps 的链路,那我就一直按 4Mbps 发。

而更可能是:

  • 刚才还能发 4Mbps,现在突然只剩 2.5Mbps
  • 上一秒 RTT 还正常,这一秒突然抖了
  • 码率没超很多,但队列开始积压了
  • 丢包变多了,但到底是链路拥塞、Wi-Fi 抖动,还是接收端处理不过来
  • 你要不要立刻降码率,降多少,什么时候再试探着升回去

你会发现,这根本不是“测一个值然后照着发”这么简单。

它真正要解决的问题是:

在一个不停变化、而且你永远看不全的网络里,尽量猜出当前还能安全发多少,并且在发错之前及时收手。

这件事特别像开车下雾天山路:

  • 你看不远
  • 路况一直变
  • 开慢了效率低
  • 开快了就可能冲出去
  • 所以你只能边走边试,边看边调

这就是实时音视频里的带宽估计和拥塞控制。

而且这次我会按你特别强调的要求来,把核心代码示例放进去,不再只是嘴上讲流程。

重点讲清:

  • 带宽估计到底在估什么
  • 拥塞控制到底在控什么
  • 为什么它们几乎决定了 WebRTC 体验上限
  • 一个发送端到底是怎么根据反馈动态调码率的
  • 接收端的到达时间、丢包、RTT 是怎么参与判断的
  • 如果你自己实现一个简化版 BWE/CC 框架,核心代码骨架会长什么样

这篇我会继续放:

  • 结构图
  • 时序图
  • 关键状态机图
  • 核心伪代码 / C++ 风格示例

我希望这篇读完之后,你脑子里对“边传边降边试探”这件事,不再只是一个模糊口号,而是真能看到系统是怎么动起来的。


一、先给一句最重要的话:带宽估计不是在“测最大值”,而是在持续寻找当前网络还能承受的发送区间

如果让我先压一句最重要的话,我会这样说:

带宽估计不是在找一个永远正确的固定值,而是在持续逼近“当前网络还能承受、且不会明显恶化体验”的发送区间。

这句话特别重要。

因为很多人一说“带宽估计”,脑子里会自动出现测速软件。

但实时音视频根本不是那种场景。

测速软件更像是在做:

  • 现在尽量压榨带宽上限是多少

而实时音视频更像在做:

  • 在不把链路打爆的前提下,我现在大概还能安全发多少
  • 如果网络变差了,我要尽快收
  • 如果网络变好了,我也不能一下子冲太猛

所以它不是“测极限”,而是:

持续试探、持续修正、持续保守地靠近可用上限。

这也直接解释了为什么它总和拥塞控制绑在一起。

因为一旦估错,系统就不是“分数差一点”,而是:

  • 延迟堆积
  • 卡顿上升
  • 丢包飙升
  • 画质猛降
  • 用户开始明显感知异常

二、先看全局:带宽估计和拥塞控制在整条 WebRTC 链路里的位置

先上总图。

flowchart LR
    A[采集/编码] --> B[发送端码率控制]
    B --> C[RTP发送]
    C --> D[网络]
    D --> E[接收端统计到达情况]
    E --> F[RTCP/反馈信息]
    F --> G[发送端带宽估计器]
    G --> B

这张图最关键的意思是:

  • 编码器不是自己想发多少就发多少
  • 发送端码率不是写死的
  • 接收端不是只负责播
  • 网络反馈不是装饰品

整条链路真正形成的是一个闭环:

发 -> 过网 -> 被观察 -> 反馈回来 -> 再调发。

所以带宽估计和拥塞控制,本质上不是一个“附加优化模块”,而是:

WebRTC 动态系统的大脑之一。


三、带宽估计到底在估什么

这个问题如果不说透,后面都容易飘。

3.1 它估的不是裸带宽上限

真正工程上更有用的说法是:

它估的是当前这条实时链路,在现有网络状态、延迟预算和丢包情况之下,较合适的发送码率范围。

注意这里几个词:

  • 当前
  • 实时链路
  • 较合适
  • 范围

这几个词缺一个都不准。

因为你不是在实验室里测网卡上限,而是在实时业务里做决策。

3.2 这个估计值通常会受哪些东西影响

典型包括:

  • 接收端包到达时间间隔
  • 一段时间内的吞吐量
  • 丢包率
  • RTT 变化
  • 排队延迟趋势
  • 是否出现明显突发拥塞

所以它更像一个综合判断,而不是单指标结论。


四、拥塞控制到底在控什么

如果说带宽估计负责“猜现在还能发多少”,那拥塞控制负责的就是:

把这个猜测变成一个真实可执行、且不会太激进的发送行为。

它通常会控制这些东西:

  • 目标码率
  • 是否立刻降码率
  • 是否允许缓慢升码率
  • 是否请求编码器降分辨率 / 帧率
  • pacing 发送节奏
  • 突发数据是否需要抹平

也就是说,拥塞控制不是一句“慢点发”。

它更像一套行为策略:

  • 什么时候该踩刹车
  • 什么时候该轻踩油门
  • 什么时候只能观察,别乱动

五、为什么实时音视频必须“边传边降边试探”

这是整篇最核心的主旨之一。

5.1 因为网络条件是时变的

今天你能跑 3Mbps,不代表 3 秒后还行。

比如这些场景都很常见:

  • 手机从 Wi-Fi 切到蜂窝
  • 同一 Wi-Fi 下别的设备突然占网
  • 路由器缓冲开始膨胀
  • 基站侧调度变化
  • 上下行链路暂时不对称

这意味着固定码率策略很容易出事。

5.2 因为你看不到网络内部,只能通过症状反推

发送端并不能直接看到整条公网路径里发生了什么。

它看到的通常只是外在症状:

  • 到达变慢了
  • RTT 变大了
  • 丢包多了
  • 吞吐下降了

所以它做的其实不是“知道”,而是:

根据反馈去猜,再根据猜测去控。

5.3 因为过冲的代价特别大

如果你发猛了,最糟糕的不是平均吞吐差一点,而是会直接导致:

  • 队列积压
  • 延迟上涨
  • jitter 变大
  • 丢包增加
  • 接收端 jitter buffer 被迫变厚
  • 体验从“清晰”迅速变成“卡、糊、慢”

所以实时系统非常怕过冲。

这也是为什么它们通常:

  • 降码率比较果断
  • 升码率比较谨慎

这就形成了你看到的那种典型风格:

边传边看,出事就降,没事再慢慢试探。


六、一个典型发送端决策闭环长什么样

先看决策图。

flowchart TD
    A[发送RTP数据] --> B[接收端统计到达情况]
    B --> C[生成RTCP/反馈]
    C --> D[发送端更新网络状态]
    D --> E{检测到拥塞迹象?}
    E -- 是 --> F[快速降目标码率]
    E -- 否 --> G{网络稳定一段时间?}
    G -- 是 --> H[缓慢升码率]
    G -- 否 --> I[保持当前码率]
    F --> J[通知编码器调整输出]
    H --> J
    I --> J
    J --> A

这张图几乎就是一个简化版 WebRTC 码率控制核心。

重点不是它有多复杂,而是你要看见:

它是一个循环系统,而不是一次性配置。


七、先上第一段核心代码:一个最小带宽估计器的数据结构该怎么长

这一节开始上代码。

下面这个不是某个现成库源码,而是我给你整理出来的一个简化版工程骨架,重点是帮助你理解“系统到底怎么组织”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct PacketFeedback {
uint64_t send_time_ms;
uint64_t recv_time_ms;
size_t size_bytes;
bool lost;
};

struct NetworkMetrics {
double throughput_bps = 0.0;
double loss_rate = 0.0;
double rtt_ms = 0.0;
double delay_gradient_ms = 0.0;
};

class BandwidthEstimator {
public:
void OnFeedback(const std::vector<PacketFeedback>& feedbacks,
double rtt_ms) {
metrics_.throughput_bps = ComputeThroughput(feedbacks);
metrics_.loss_rate = ComputeLossRate(feedbacks);
metrics_.delay_gradient_ms = ComputeDelayGradient(feedbacks);
metrics_.rtt_ms = rtt_ms;

UpdateEstimate();
}

double target_bitrate_bps() const {
return target_bitrate_bps_;
}

private:
void UpdateEstimate() {
if (IsOverusing()) {
target_bitrate_bps_ *= 0.85;
} else if (IsUnderusing()) {
target_bitrate_bps_ *= 1.05;
} else {
target_bitrate_bps_ *= 1.00;
}

target_bitrate_bps_ = Clamp(target_bitrate_bps_, min_bitrate_bps_, max_bitrate_bps_);
}

bool IsOverusing() const {
return metrics_.loss_rate > 0.10 ||
metrics_.delay_gradient_ms > 15.0 ||
metrics_.rtt_ms > 250.0;
}

bool IsUnderusing() const {
return metrics_.loss_rate < 0.02 &&
metrics_.delay_gradient_ms < 5.0 &&
metrics_.throughput_bps > target_bitrate_bps_ * 1.2;
}

static double Clamp(double v, double lo, double hi) {
return std::max(lo, std::min(v, hi));
}

private:
NetworkMetrics metrics_;
double target_bitrate_bps_ = 800000.0;
double min_bitrate_bps_ = 150000.0;
double max_bitrate_bps_ = 5000000.0;
};

这段代码最该看什么

不是每个阈值本身,而是它体现出来的结构:

  1. 先收反馈
  2. 把反馈转成可判断的网络指标
  3. 根据指标判断当前是过载、欠载还是稳定
  4. 再产出新的目标码率

这就是一个带宽估计器最基础的骨架。


八、第二段核心代码:吞吐量、丢包率、延迟梯度这些指标到底怎么从反馈里算出来

这一步特别关键,因为很多文章喜欢直接讲“根据反馈调整码率”,但不写中间这层,读者其实还是没抓住系统。

8.1 计算吞吐量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double ComputeThroughput(const std::vector<PacketFeedback>& feedbacks) {
if (feedbacks.empty()) return 0.0;

uint64_t first_recv = UINT64_MAX;
uint64_t last_recv = 0;
size_t total_bytes = 0;

for (const auto& pkt : feedbacks) {
if (pkt.lost) continue;
first_recv = std::min(first_recv, pkt.recv_time_ms);
last_recv = std::max(last_recv, pkt.recv_time_ms);
total_bytes += pkt.size_bytes;
}

if (last_recv <= first_recv) return 0.0;

double duration_s = (last_recv - first_recv) / 1000.0;
return (total_bytes * 8.0) / duration_s;
}

这里的直觉是什么

它不是在问“链路理论速度多少”,而是在问:

这一小段时间里,我实际看见有多少比特真正送到了接收侧。

这才是实时系统更关心的“有效吞吐”。

8.2 计算丢包率

1
2
3
4
5
6
7
8
9
10
11
double ComputeLossRate(const std::vector<PacketFeedback>& feedbacks) {
if (feedbacks.empty()) return 0.0;

int lost = 0;
for (const auto& pkt : feedbacks) {
if (pkt.lost) {
++lost;
}
}
return static_cast<double>(lost) / feedbacks.size();
}

这里要注意什么

丢包率不是唯一依据,但它是非常敏感的拥塞信号之一。

不过真实工程里也不能一看丢包就武断认定“全是网络拥塞”,因为有时候还可能混入:

  • 无线链路波动
  • 接收端处理不过来
  • 队列溢出

所以它通常是信号之一,不是唯一裁决者。

8.3 计算延迟梯度

这个指标很像“排队是不是在变严重”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
double ComputeDelayGradient(const std::vector<PacketFeedback>& feedbacks) {
if (feedbacks.size() < 2) return 0.0;

std::vector<double> one_way_delays;
one_way_delays.reserve(feedbacks.size());

for (const auto& pkt : feedbacks) {
if (pkt.lost) continue;
double d = static_cast<double>(pkt.recv_time_ms - pkt.send_time_ms);
one_way_delays.push_back(d);
}

if (one_way_delays.size() < 2) return 0.0;

double first = one_way_delays.front();
double last = one_way_delays.back();
return last - first;
}

这个指标为什么重要

因为很多拥塞在早期未必马上表现成高丢包,但会先表现成:

  • 包越来越晚到
  • 队列越来越长
  • 单向延迟开始上爬

所以如果你只盯丢包,往往会反应偏慢。


九、第三段核心代码:为什么发送端通常“降得快,升得慢”

这是拥塞控制里特别核心的行为风格。

下面是一个简化版 AIMD 风格策略骨架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class CongestionController {
public:
void Update(const NetworkMetrics& metrics) {
if (IsCongested(metrics)) {
// 乘性下降,快速收缩
target_bitrate_bps_ *= 0.80;
state_ = State::kDecrease;
} else if (CanProbeUp(metrics)) {
// 加性增长,缓慢试探
target_bitrate_bps_ += 30000.0;
state_ = State::kIncrease;
} else {
state_ = State::kHold;
}

target_bitrate_bps_ = std::max(min_bitrate_bps_,
std::min(target_bitrate_bps_, max_bitrate_bps_));
}

double target_bitrate_bps() const { return target_bitrate_bps_; }

private:
enum class State {
kHold,
kIncrease,
kDecrease
};

bool IsCongested(const NetworkMetrics& m) const {
return m.loss_rate > 0.08 ||
m.delay_gradient_ms > 12.0 ||
m.rtt_ms > 200.0;
}

bool CanProbeUp(const NetworkMetrics& m) const {
return m.loss_rate < 0.02 &&
m.delay_gradient_ms < 3.0 &&
m.rtt_ms < 120.0;
}

private:
State state_ = State::kHold;
double target_bitrate_bps_ = 1000000.0;
double min_bitrate_bps_ = 120000.0;
double max_bitrate_bps_ = 6000000.0;
};

这段代码背后的工程直觉

为什么降得快

因为一旦检测到拥塞,继续犹豫的成本很高:

  • 队列越积越大
  • 时延继续涨
  • 丢包可能变严重
  • 用户体验会更快恶化

所以要果断收。

为什么升得慢

因为你并不知道“看起来没事”是否就代表还有大量余量。

如果升太猛,就很容易:

  • 刚恢复一点,又马上打爆
  • 形成抖动
  • 系统在高低码率之间来回抽搐

所以升码率一般更像试探,而不是冲刺。


十、第四段核心代码:发送端怎么把新的目标码率真正作用到编码器上

这一层特别重要,因为很多文章讲到码率控制就停了,但系统真正落地时,得把结论传给编码器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class VideoSender {
public:
void OnNetworkFeedback(const std::vector<PacketFeedback>& fb, double rtt_ms) {
estimator_.OnFeedback(fb, rtt_ms);
controller_.Update(metrics_from_estimator());

double target = controller_.target_bitrate_bps();
encoder_.SetTargetBitrate(static_cast<int>(target));

AdaptVideoStream(target);
}

private:
NetworkMetrics metrics_from_estimator() {
// 简化示意,真实工程中通常从 estimator 内部导出更完整指标
NetworkMetrics m;
m.rtt_ms = current_rtt_ms_;
m.loss_rate = current_loss_rate_;
m.delay_gradient_ms = current_delay_gradient_ms_;
m.throughput_bps = estimator_.target_bitrate_bps();
return m;
}

void AdaptVideoStream(double target_bps) {
if (target_bps < 300000) {
encoder_.SetResolution(640, 360);
encoder_.SetFps(15);
} else if (target_bps < 800000) {
encoder_.SetResolution(960, 540);
encoder_.SetFps(20);
} else {
encoder_.SetResolution(1280, 720);
encoder_.SetFps(25);
}
}

private:
BandwidthEstimator estimator_;
CongestionController controller_;
VideoEncoder encoder_;

double current_rtt_ms_ = 0.0;
double current_loss_rate_ = 0.0;
double current_delay_gradient_ms_ = 0.0;
};

这段代码最值钱的地方

你能直接看到:

带宽估计不是最后答案,它真正的意义,是驱动编码器和视频层级一起调整。

也就是说,拥塞控制最终通常要落到这些具体动作上:

  • 改目标码率
  • 改分辨率
  • 改帧率
  • 改编码复杂度

这才是真正能被用户体验感知到的结果。


十一、第五段核心代码:接收端反馈怎么组织,为什么它不是“随便回个统计值”

下面给一个简化版接收端反馈收集器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ReceiverFeedbackCollector {
public:
void OnPacketReceived(uint16_t seq,
uint64_t send_time_ms,
uint64_t recv_time_ms,
size_t size_bytes) {
PacketFeedback fb;
fb.send_time_ms = send_time_ms;
fb.recv_time_ms = recv_time_ms;
fb.size_bytes = size_bytes;
fb.lost = false;
feedbacks_.push_back(fb);

DetectLoss(seq);
}

std::vector<PacketFeedback> ConsumeFeedbackBatch() {
auto out = feedbacks_;
feedbacks_.clear();
return out;
}

private:
void DetectLoss(uint16_t current_seq) {
if (!has_last_seq_) {
last_seq_ = current_seq;
has_last_seq_ = true;
return;
}

uint16_t expected = last_seq_ + 1;
while (expected != current_seq) {
PacketFeedback lost_fb;
lost_fb.lost = true;
feedbacks_.push_back(lost_fb);
++expected;
}
last_seq_ = current_seq;
}

private:
std::vector<PacketFeedback> feedbacks_;
uint16_t last_seq_ = 0;
bool has_last_seq_ = false;
};

这里为什么重要

因为发送端看到的“网络状态”,本质上是接收端组织后的反馈视图。

如果接收端反馈做得太粗:

  • 发送端判断会迟钝
  • 误判会更多
  • 码率调整会更抖

所以这层不是可有可无的小模块,而是闭环里的观测器。


十二、一张时序图看懂 BWE/CC 为什么像“边开边修的控制系统”

sequenceDiagram
    participant S as Sender
    participant N as Network
    participant R as Receiver
    participant FB as Feedback Logic

    S->>N: RTP packets at current bitrate
    N->>R: packets arrive with jitter/loss/delay
    R->>FB: arrival stats / loss / timing
    FB->>S: RTCP / transport feedback
    S->>S: estimate bandwidth
    S->>S: update congestion state
    S->>S: reduce/increase target bitrate
    S->>N: next RTP packets with new pacing/bitrate

这张图你如果看懂了,基本就知道为什么这套系统不可能只靠一个静态配置活下去。


十三、为什么很多时候“丢包触发降码率”还不够,你还得盯住延迟趋势

这是实际工程里非常关键的一点。

13.1 只盯丢包,常常会慢半拍

如果一个链路开始拥塞,真实发生的顺序常常更像:

  1. 队列先开始变长
  2. 延迟和 jitter 先升
  3. 再往后才开始明显丢包

也就是说,丢包很多时候是比较晚的症状。

如果你只等丢包高了才降,系统往往已经开始难受了。

13.2 所以很多控制器会把“排队趋势”看得很重

这也是为什么:

  • arrival time
  • one-way delay trend
  • RTT gradient

这些指标在实时系统里特别有价值。

它们不是最终答案,但很像“拥塞前兆”。


十四、一个更像真实系统的状态机应该怎么想

下面给一个更接近工程思维的简化状态机。

stateDiagram-v2
    [*] --> Hold
    Hold --> Increase: 网络稳定且余量看起来足够
    Hold --> Decrease: 检测到拥塞迹象
    Increase --> Hold: 继续观察
    Increase --> Decrease: 试探过猛出现拥塞
    Decrease --> Hold: 收缩后等待稳定

这张图想表达的是:

  • 不是每次反馈都必须动作
  • 不是永远涨或者永远降
  • 很多时候最好的决策恰恰是“先稳住,别乱动”

这点特别像真正做系统的人会有的判断。


十五、发送 pacing 为什么也很重要,它和单纯改码率不是一回事

很多人一说拥塞控制,只想到“改 bitrate”。

但真实工程里,pacing 也非常关键。

因为即便平均码率没超,如果你发得太突发,也可能把链路打出问题。

15.1 一个简化版 pacing 发送器骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Pacer {
public:
void SetRate(double bps) {
pacing_rate_bps_ = bps;
}

void Enqueue(const EncodedPacket& pkt) {
queue_.push(pkt);
}

void Process(uint64_t now_ms) {
double budget_bits = pacing_rate_bps_ * (now_ms - last_process_ms_) / 1000.0;
last_process_ms_ = now_ms;

while (!queue_.empty()) {
const auto& pkt = queue_.front();
double pkt_bits = pkt.size_bytes * 8.0;
if (pkt_bits > budget_bits) break;

SendToNetwork(pkt);
budget_bits -= pkt_bits;
queue_.pop();
}
}

private:
std::queue<EncodedPacket> queue_;
double pacing_rate_bps_ = 1000000.0;
uint64_t last_process_ms_ = 0;
};

15.2 这段代码说明什么

它说明拥塞控制不只是“发多少”,还包括:

怎么把这些数据更平滑地送出去。

这对减少网络瞬时压力非常有帮助。


十六、为什么这个系统本质上永远在做“次优解”

这一点我特别想强调。

因为很多人学算法时容易潜意识觉得:

应该有一个正确码率,系统只要算出来就好了。

但真实网络不是数学题。

你拿到的永远都是:

  • 延迟的滞后反馈
  • 局部观测
  • 噪声很大的信号
  • 时刻在变的链路

所以带宽估计和拥塞控制本质上不是“求真值”,而是:

在不完整信息下做尽量不蠢的动态决策。

这也是为什么不同实现会有不同风格:

  • 有的更激进
  • 有的更保守
  • 有的更偏延迟优先
  • 有的更偏吞吐优先

它们很多时候不是谁绝对对,而是谁更适合当前场景。


十七、最后给一句更像工程师的话:BWE/CC 的真正目标,不是把带宽吃满,而是把体验打稳

写到这里,最想落下的一句话是:

实时音视频里的带宽估计与拥塞控制,最终目标不是吃满链路,而是让实时体验尽量稳定。

这个目标非常重要。

因为如果你把目标错设成“尽量打满带宽”,系统就会特别容易:

  • 过冲
  • 抖动
  • 延迟膨胀
  • 频繁降级
  • 体验不稳定

而如果你把目标设成:

  • 尽量稳住可交流体验
  • 在安全区间里慢慢逼近上限
  • 出现风险时及时刹车

那这套系统的很多设计选择就都能解释通了。

所以真正成熟的实时系统,不会像测速软件那样兴奋地冲极限,而更像一个老练司机:

  • 看不清时先收
  • 稳住了再探
  • 宁可少赚一点,也别翻车

十八、总结

最后把这篇收成几句话。

1. 带宽估计不是测固定网速,而是在持续逼近当前可安全发送区间

它关心的是实时体验下的可用发送范围,不是理论最大值。

2. 拥塞控制不是一句“少发点”,而是一整套动态行为策略

它会决定:

  • 降不降码率
  • 升不升码率
  • 升降速度多快
  • 是否改分辨率 / 帧率
  • pacing 怎么做

3. 真正的闭环是:发 -> 观察 -> 反馈 -> 再调发

这也是 WebRTC 像动态控制系统的根本原因。

4. 核心代码最值得看的,是系统结构而不是阈值本身

也就是:

  • 如何组织反馈
  • 如何计算指标
  • 如何判断拥塞
  • 如何把目标码率作用到编码器
  • 如何做 pacing

5. BWE/CC 的最终目标不是把带宽吃满,而是把实时体验打稳

这才是更像工程师的理解。

如果你愿意,我下一篇建议直接接:

《NACK、PLI、FEC 到底怎么配合:丢包之后,实时音视频系统到底怎么补救》

这篇会把“反馈回来以后怎么救”这条线单独讲透,而且我也会继续放核心代码示例。


带宽估计与拥塞控制到底在干什么:实时音视频为什么必须边传边降边试探
https://breaker505.github.io/2026/05/01/webrtc-bandwidth-estimation-and-congestion-control/
作者
爱发呆的鱼
发布于
2026年5月1日
许可协议