# 视频自适应 · 降分辨率还是降帧率

> 网络或 CPU 不够时，视频要主动降质保流畅——降分辨率还是降帧率？这一章读 g3doc/adaptation.md:Resource 如何发出 overuse/underuse 信号、QualityScaler 盯 QP、编码耗时当 CPU 代理、以及 DegradationPreference 如何决定牺牲哪个维度。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/video-adaptation
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 视频自适应 · 降分辨率还是降帧率

网络带宽和 CPU 都是有限且多变的。当它们不够用时，视频**主动降质**总比卡死强。但降质有讲究：**降分辨率（画面变糊）还是降帧率（动作变卡）？** 这套「视频自适应（Video Adaptation）」逻辑，`g3doc/adaptation.md` 讲得很清楚。

## 机制：Resource 发信号

`g3doc/adaptation.md` 开门见山：

> Video adaptation ... reduces the bandwidth or CPU consumption by reducing encoded video quality.

自适应的触发机制，是一套 **Resource（资源）** 抽象：

<Callout type="info">
  **每个 Resource 监控一个指标，过载时喊 `SetUsageState(kOveruse)`、宽裕时喊 `kUnderuse`——`VideoStreamEncoder` 据此在两个维度（分辨率 / 帧率）上降质或恢复。** 默认有两个 Resource(`g3doc/adaptation.md`):

  - **QP Scaler（质量伸缩）** —— 盯**编码后帧的 QP（量化参数）**。QP 反映「为了塞进码率预算，画质被压得多狠」：QP **高于**高阈值 = 当前分辨率下画质已经差得不可接受（压太狠了）→ `kOveruse`（该**降分辨率**了，让每个像素分到更多码率）；QP **低于**低阈值 = 画质有富余 → `kUnderuse`（可以升回去）。由 [`QualityScaler`](https://daiw.org/manual/webrtc/video-adaptation) 实现。
  - **Encode Usage（编码耗时）** —— 盯**编码一帧要多久**，作为 **CPU 使用率的代理**（`g3doc/adaptation.md` 的巧思：CPU 越忙，编码越慢）。编码耗时超阈值 → CPU 扛不住 → `kOveruse`。

  两个 Resource，一个管「码率 / 画质」（QP），一个管「CPU」（编码耗时）——分别对应视频自适应要平衡的两大约束。还能[注入自定义 Resource](https://daiw.org/manual/webrtc/video-decode-recovery)（如监控设备温度），`Call::AddAdaptationResource`。
</Callout>

## 降哪个维度：DegradationPreference

过载了，该牺牲**分辨率**还是**帧率**？这取决于 **`DegradationPreference`**（[绑定源时传入](https://daiw.org/manual/webrtc/video-source-encoder)）：

| DegradationPreference | 过载时牺牲 | 适合 |
| --- | --- | --- |
| `MAINTAIN_FRAMERATE` | **降分辨率**（保帧率） | 视频通话（动作流畅重要） |
| `MAINTAIN_RESOLUTION` | **降帧率**（保分辨率） | 屏幕共享 / 演示（文字清晰重要） |
| `BALANCED` | 两者**平衡**取舍 | 通用 |
| `DISABLED` | 不自适应 | 特殊场景 |

<Callout type="info">
  **降分辨率 vs 降帧率，取决于内容「更怕糊还是更怕卡」。** 视频通话里，人脸和动作是主体——宁可画面小一点（降分辨率），也要**动作流畅**（保帧率），所以用 `MAINTAIN_FRAMERATE`。而共享一份**文档 / 代码 / 幻灯片**时，内容基本静止但**文字必须看清**——宁可翻页卡顿一下（降帧率），也要**每个字清晰**（保分辨率），所以用 `MAINTAIN_RESOLUTION`。**同一个“降质”动作，对不同内容有截然不同的最优方向**——这个洞察，让 WebRTC 的自适应不是一刀切，而是贴合场景。`g3doc/adaptation.md` 还指出：[QP scaler](https://daiw.org/manual/webrtc/video-adaptation) 只在 `MAINTAIN_FRAMERATE` 或 `BALANCED` 下启用（因为它管的就是“降分辨率”这个维度）。
</Callout>

## 两个源头：带宽不够 vs 算力不够

自适应其实应对**两种**不同的短缺，别混淆：

- **带宽不够** —— [拥塞控制](https://daiw.org/manual/webrtc/rate-control)把[目标码率](https://daiw.org/manual/webrtc/rate-control)压低了，编码器为塞进预算把 QP 提高、画质变差，[QP scaler](https://daiw.org/manual/webrtc/video-adaptation) 于是触发降分辨率。**根源在网络。**
- **算力不够** —— 设备 CPU 忙不过来，编码耗时变长，encode usage resource 触发降质。**根源在本机。**

两者都通过同一套 Resource 机制表达为 overuse/underuse，`VideoStreamEncoder` 统一响应——**不管是“网慢”还是“机器慢”，都是降质保流畅**。这个统一抽象很漂亮：自适应不关心短缺的来源，只关心“过载了就降、宽裕了就升”。

## 自适应闭环

```mermaid
flowchart TD
    ENC["编码一帧"] --> QP["QualityScaler:看 QP"]
    ENC --> CPU["Encode Usage:看编码耗时"]
    QP -->|"QP 过高"| OVER["Resource: kOveruse"]
    CPU -->|"编码太慢"| OVER
    QP -->|"QP 有富余"| UNDER["Resource: kUnderuse"]
    OVER -->|"按 DegradationPreference"| DOWN["降分辨率 或 降帧率"]
    UNDER --> UP["升回去"]
    DOWN --> VSE["VideoStreamEncoder 调整下一帧"]
    UP --> VSE
    VSE --> ENC
```

这个闭环和[拥塞控制闭环](https://daiw.org/manual/webrtc/rate-control)并行运转：拥塞控制管“码率预算”，自适应管“在预算和算力下怎么分辨率 / 帧率取舍”——两者配合，让视频在**烂网 + 弱机**下也能维持可用体验。

## 小结与下一章

**视频自适应**在网络 / CPU 不够时**降质保流畅**。机制是 **Resource** 发 `kOveruse`/`kUnderuse` 信号：默认两个——**[QualityScaler](https://daiw.org/manual/webrtc/video-adaptation)** 盯 **QP**（画质被压多狠 → 管降分辨率）、**Encode Usage** 盯**编码耗时**（CPU 代理）。降**分辨率还是帧率**由 **`DegradationPreference`** 定：通话 `MAINTAIN_FRAMERATE`（降分辨率保流畅）、共享屏幕 `MAINTAIN_RESOLUTION`（降帧率保清晰）。它统一应对“带宽不够”和“算力不够”两种短缺。

一路发一档视频是自适应，但要**同时**服务多个不同网络的接收者（会议里有人网好有人网差），得**同时发多档**——这就是联播与 SVC。下一章。👉 [联播与 SVC](https://daiw.org/manual/webrtc/simulcast-svc)。

## 参考资料

- [1] libwebrtc 源码.`g3doc/adaptation.md`（Resource、QP scaler、encode usage、degradation preference）。
- [2] libwebrtc 源码.`video/adaptation/`、`modules/video_coding/utility/quality_scaler.h`、`api/adaptation/resource.h`。
- [3] `api/video_codecs/video_encoder.h`（`QpThresholds`、`scaling_settings`）。
