27、弱网推流优化:动态码率、丢帧策略、缓存控制、网络状态监测

说实话,做音视频推流这么多年,我最怕听到的一句话就是——“用户那边网络又卡了”。

你想想看,你辛辛苦苦把编码、采集、渲染都调得稳稳当当,结果网络一抖,画面直接卡成PPT,甚至断流。这感觉,就像你做好了满汉全席,结果外卖小哥半路摔了一跤。

所以这一章,咱们就来聊聊弱网环境下的推流优化。说白了,就是当网络不给力的时候,我们怎么让推流“苟”住,尽量不让用户骂娘。

27.1 网络状态监测:你得先知道“网”怎么了

优化弱网,第一步不是动手改代码,而是先搞清楚网络到底有多差。我个人习惯,在推流 SDK 里内置一个网络探测模块,实时监测三个核心指标:

  • RTT(往返时延):数据包从客户端到服务器再回来的时间。RTT 超过 200ms,基本就能感觉到延迟了。
  • 丢包率:这个最要命。丢包率超过 5%,画面就开始花屏、卡顿。
  • 可用带宽:不是你的网速上限,而是当前实际能用的吞吐量。

我在项目中遇到过一种情况:用户 Wi-Fi 信号满格,但丢包率高达 20%。后来一查,是路由器太老,并发连接数一多就开始丢包。所以光看信号强度是不够的,必须实时计算这些底层指标。

核心思路:每 1~2 秒做一次网络评估,根据结果动态调整推流策略。不要等卡死了再反应,要“预判”网络的波动。

27.2 动态码率:让视频“自适应”网速

动态码率,说白了就是让编码器根据当前网络带宽,自动调整输出码率。网络好时用高码率,网络差时自动降码率,保证流畅度优先。

我常用的策略是 基于探测带宽的码率调整,流程大概是:

  1. 网络模块估算出当前可用带宽,比如 1.5 Mbps。
  2. 编码器将目标码率设为带宽的 80%(留点余量),即 1.2 Mbps。
  3. 如果连续 3 次探测到带宽下降,就再降一档码率。
  4. 如果带宽恢复,逐步回升码率,不要一下跳太高。

这里有个坑——码率不能降得太频繁。我曾经试过每 500ms 调整一次码率,结果画面一会儿清晰一会儿模糊,用户体验反而更差。后来我改成每 3~5 秒调整一次,平滑多了。

我的经验:码率调整建议用“阶梯式”,比如预设 500kbps、800kbps、1.2Mbps、2Mbps 几档,根据网络状态在这几档之间切换,而不是连续变化。这样编码器内部参数更稳定,画质波动也小。

27.3 丢帧策略:该扔就扔,别犹豫

网络差的时候,码率降到最低还是卡,怎么办?这时候就得考虑丢帧了。

丢帧不是随便扔,得有策略。我一般分两种场景:

  • B帧优先丢:B帧依赖前后帧,丢了不影响其他帧的解码,只是画面会稍微跳一下。这是最安全的丢帧方式。
  • 非关键帧丢:如果 B 帧丢完了还不行,就丢 P 帧。但注意,P 帧丢了会导致后续帧解码出错,直到下一个关键帧(I帧)到来才能恢复。

嗯,这里要注意:绝对不能丢 I 帧。I 帧是关键帧,丢了整个画面就崩了,必须等到下一个 I 帧才能恢复。我曾经在项目里犯过这个错,结果画面黑了整整两秒,被测试同学追着骂了一周。

避坑指南:丢帧时一定要同步更新编码器的 timestamp,否则接收端的时间戳会乱掉,导致播放器认为视频“快进”或“卡住”。

27.4 缓存控制:给网络一点“缓冲”时间

缓存控制,说白了就是在发送端和接收端各放一个缓冲区,用来吸收网络的抖动。

我常用的做法是:

  • 发送端缓存:维护一个 500ms~1s 的数据队列。网络好时,队列很快清空;网络差时,队列堆积,我们就触发降码率或丢帧。
  • 接收端缓存:播放器端也保留 200~500ms 的缓冲,防止因为网络抖动导致播放断断续续。

但缓存不是越大越好。你想想看,缓存越大,延迟就越高。做直播时,如果缓存设到 3 秒,用户看到的画面比真实世界晚了 3 秒,互动体验就很差了。

我个人习惯:互动直播场景,缓存控制在 500ms 以内;普通直播场景,可以放宽到 1~2 秒。

27.5 整体策略:一张图看懂

说了这么多,咱们用一张图把整个弱网推流优化的流程串起来:

弱网推流优化整体策略流程图 网络状态监测模块 评估 RTT / 丢包率 / 可用带宽 每 1~2 秒更新一次 动态码率调整 阶梯式升降,平滑过渡 丢帧策略 B帧优先 → P帧 → 保留I帧 缓存控制 发送端 500ms~1s / 接收端 200~500ms

27.6 代码示例:网络状态回调与码率调整

光说不练假把式。下面我给出一段伪代码,展示如何在推流 SDK 中实现网络状态回调与动态码率调整:

// 网络状态回调接口
interface NetworkMonitorCallback {
    void onNetworkStatusChanged(NetworkStatus status);
}

class NetworkStatus {
    int rttMs;          // 往返时延,单位毫秒
    float lossRate;     // 丢包率,0.0 ~ 1.0
    int availableKbps;  // 可用带宽,单位 kbps
}

// 动态码率调整器
class AdaptiveBitrateController {
    private static final int[] BITRATE_TABLE = {500, 800, 1200, 2000}; // kbps
    private int currentLevel = 2; // 默认 1200kbps
    private int stableCount = 0;

    public void onNetworkUpdate(NetworkStatus status) {
        // 根据可用带宽选择码率档位
        int targetBitrate = status.availableKbps * 80 / 100; // 留 20% 余量

        // 找到最接近的档位
        int newLevel = findClosestLevel(targetBitrate);

        if (newLevel != currentLevel) {
            stableCount++;
            // 连续 3 次评估结果一致才切换,防止抖动
            if (stableCount >= 3) {
                currentLevel = newLevel;
                encoder.setBitrate(BITRATE_TABLE[currentLevel]);
                stableCount = 0;
            }
        } else {
            stableCount = 0;
        }

        // 如果丢包率超过 10%,强制降一档
        if (status.lossRate > 0.1f && currentLevel > 0) {
            currentLevel--;
            encoder.setBitrate(BITRATE_TABLE[currentLevel]);
        }
    }

    private int findClosestLevel(int target) {
        int level = 0;
        for (int i = 0; i < BITRATE_TABLE.length; i++) {
            if (BITRATE_TABLE[i] <= target) {
                level = i;
            }
        }
        return level;
    }
}

小提示:实际项目中,建议把码率调整和丢帧策略放在同一个线程里处理,避免多线程竞争。我习惯用一个 HandlerThread 专门处理这些控制逻辑。

27.7 总结一下

弱网推流优化,说白了就是四个字:动态适应。网络好时尽情发挥,网络差时懂得“缩回来”。

我做了这么多年音视频,最大的体会就是:不要试图对抗网络,要学会和它共处。你永远无法保证用户的网络永远稳定,但你可以让你的推流 SDK 在任何网络下都能“体面”地工作。

嗯,这一章的内容就到这里。代码里的细节,建议你实际跑一遍,改改参数试试效果。只有亲手调过,才能真正理解这些策略的微妙之处。