# RTC 入门 04：丢了音频包，重传、FEC 和 PLC 怎么选

作者：Hex4C59 · 发布于 2026-10-15

原文：https://hex4c59.cc/articles/rtc-04-loss-recovery/


接收端已经拿到 200、201、203，缺少 202。203 对应的声音还没到播放时刻，202 却快要用到了。

[第三篇](https://hex4c59.cc/articles/rtc-03-sequence-gap/)说明，这时需要分别记录序号缺口和播放可用性。现在继续处理这个缺口：能否把 202 要回来？能否利用 203 带来的信息补出前一段声音？如果都来不及，播放怎样继续？

三种办法改变的环节不同。重传让发送端再送一份原来编码的数据；FEC（Forward Error Correction，前向纠错）提前发送恢复信息；PLC（Packet Loss Concealment，丢包隐藏）在接收端产生缺失区间的替代声音。选择它们时，既要看手里有什么数据，也要看完成处理后还能否赶上播放。

本篇延续前面的固定时长 Opus 示例，先算同一份时间预算，再运行一个真实 libopus 实验。时间预算实验只需 Python 3.9+；真实编解码还需要导出相关接口的 libopus 动态库。后一个实验不录音、不联网，也不测音质。

读完本篇，你应该能区分“原数据重发”“靠额外信息恢复”“没有新信息时产生替代声音”，并按剩余时间与现有数据判断哪些路径仍可行。阅读需要前两篇的声音分块概念，以及第三篇的包序和播放计划；不要求先会读 C 接口。先完成时间预算和新条件练习，再按需要阅读接口与真实编解码实验。

## 缺了 20 毫秒，为什么不能把后面的声音直接接上

假设连续声音分为 A、B、C 三段，每段 20 毫秒，B 没到，C 到了。直接把 C 提前 20 毫秒接在 A 后面，会跳过 B 的声音，也改动原来的媒体时间安排；让输出设备一直停住等 B，则会把后面的声音一起推迟。

本篇采用固定播放计划，目标是在轮到 B 的区间时仍提供相应长度的输出。可用信息决定了不同办法的能力：重传去找 B 的原编码内容；FEC 使用发送端事先附带的恢复信息；PLC 在缺少可用内容时，利用已有状态产生替代样本。具体系统还可能调整播放节奏，这里先不改变时间安排。

下面继续使用第三篇的包号 202、203。202 是缺失的那段声音；203 是它后面的声音，不是“替 202 播一下”就能解决的内容。

## 先确定 202 最晚什么时候能用

沿用第三篇的教学条件：每包 20 毫秒声音，202 在接收端本地第 60 毫秒开始播放，恢复完成到可输出还预留 5 毫秒。所有时刻都使用接收端同一条时间轴。

```text
计划播放时刻：60 毫秒
后续准备成本：5 毫秒
最晚取得可用输入：60 − 5 = 55 毫秒
```

这里暂把恢复解码、排队和输出准备合并为固定的 5 毫秒，方便比较；它不是实测或产品默认值。后面的真实编解码实验只检查数据和接口行为，不用运行速度替代这笔预算。

在第 40 毫秒，203 到达，接收端发现 202 的序号缺口。距离播放还有 20 毫秒，但留给取回数据的时间只有 15 毫秒。把这两个数字混用，就可能发出已经赶不上播放的请求。

下图使用同一个 60 毫秒期限比较三条路径。FEC 一行先假设 203 确实带有可用冗余；怎样检查这个条件，后面会实际验证。

<figure class="interactive-diagram" style="margin:1.8rem 0;padding:1.3rem 0;border-top:1px solid var(--code-border);border-bottom:1px solid var(--code-border);container-type:inline-size">
    <iframe id="interactive-diagram-9e67f8073e20d55a6c1f42ef8fe6e4c5-0" src="https://hex4c59.cc/articles/rtc-04-loss-recovery/recovery-timeline.html" title="202 的三条处理路径：重传在 72 毫秒准备好，FEC 在 45 毫秒准备好，PLC 按 60 毫秒输出" width="100%" height="660" loading="lazy" sandbox="allow-scripts" style="display:block;width:100%;border:0;background:transparent;--figure-height:660px;--figure-mobile-height:920px;--figure-compact-height:980px"></iframe>
    <figcaption style="font-size:.78rem;line-height:1.8;margin-top:.7rem;color:var(--content-secondary)">202 的三条处理路径：重传在 72 毫秒准备好，FEC 在 45 毫秒准备好，PLC 按 60 毫秒输出 · <a href="https://hex4c59.cc/articles/rtc-04-loss-recovery/recovery-timeline.html" target="_blank" rel="noopener">在独立窗口中查看</a></figcaption>
  </figure>


## 请求重传：要算完整的返回过程

NACK 是 Negative Acknowledgement，即否定确认。RTP 的 Generic NACK 反馈用于指出哪些包没有收到；它提供请求恢复所需的信息，并不保证发送端一定能重传。[^nack]

假设接收端在 40 毫秒发现缺口，等 2 毫秒发送反馈。随后发生：

| 过程 | 本段耗时 | 累计到本地哪个时刻 |
| --- | --- | --- |
| 发现缺口并等待反馈发送 | 2 毫秒 | 42 毫秒：发出 NACK |
| 请求传到发送端 | 10 毫秒 | 52 毫秒 |
| 发送端查找缓存的原包 | 1 毫秒 | 53 毫秒 |
| 等待发送调度 | 4 毫秒 | 57 毫秒 |
| 重传数据返回接收端 | 10 毫秒 | 67 毫秒 |
| 接收端完成后续准备 | 5 毫秒 | 72 毫秒 |

最终到 72 毫秒才准备好，比 60 毫秒晚了 12 毫秒。只拿“两段网络各 10 毫秒，往返 20 毫秒”与剩余 20 毫秒相比，会漏掉反馈等待、发送端处理、发送排队和播放准备。

计算应当覆盖整个过程：

```text
准备好时刻
= 发现缺口时刻 + 反馈等待
+ 请求网络耗时 + 发送端处理与排队 + 返回网络耗时
+ 接收端后续准备
```

RTP 重传的适用性需要考虑应用允许的延迟。[^rtx] 在这组条件下，继续等待这次返回不能满足原定播放；这不等于重传在所有通话中都没用。

把两个方向的网络耗时各降为 5 毫秒，发送排队降为 1 毫秒，其他条件不变：

```text
40 + 2 + 5 + 1 + 1 + 5 + 5 = 59 毫秒
```

这次能赶上，余量是 1 毫秒。余量很小，实际处理或网络波动仍可能改变结果。生产实现需要用适合当前路径的估计，不能把一次探测值当作稳定承诺。

### 还有一个先于时间的条件：原包仍在缓存中

即使算出 59 毫秒，发送端已经删除了 202，也没有这份原包可供返回。接收端发出 NACK，只说明请求已发出；还要跟踪发送端是否找到原数据、是否排入发送，以及返回数据最终是否可用。[^rtx]

NACK 是反馈机制，RTX 是 RTP 重传负载格式，两者也不等同。这里计算的是恢复 202 所需内容何时返回，没有生成或解析 RTX 的独立包序与封装。[^rtx]

## Opus 带内 FEC：把前一段的恢复信息提前带过来

先用一个只需加减法的假设例子理解“额外信息”。发送端原来要传两个数字 A=6、B=9；它还算出一份辅助信息 R=A+B=15。如果 A 丢了，但 B 和 R 都到了，接收端可以算出 `A=15−9=6`。如果 A、B 都没到，只知道和为 15，就无法唯一确定两个数。

这里没有把缺失内容凭空猜出来：恢复依据是发送端额外传来的关系信息，代价也正是多传了数据。这个算术模型只解释冗余为何有用，**不是 Opus 的编码算法，也不是可直接用于网络协议的设计**。

前向纠错的关键是发送端不等待针对这次缺失的反馈，就随编码数据发送恢复信息，让接收端在条件满足时利用它。

“带内”表示恢复信息与当前声音的编码一起放在 Opus 包里。本文只讨论这种 Opus 带内 FEC：在语音编码路径中，当前包可以携带此前声音的低码率冗余。对于固定 20 毫秒的这个例子，可以把 203 理解为包含自己的声音编码，以及用于恢复 202 的信息。是否加入冗余由编码器决定，需要结合预期丢包、容量和信号等条件。[^opus-fec]

这种恢复针对此前的音频表示，不会补回丢失的 RTP 报头，也不保证原封不动重建整份编码包；恢复输出也不保证与正常解码逐样本相同。[^lbrr]

它没有等待接收端发出一次请求。若 203 在 40 毫秒已经到达，里面又有可用冗余，按本篇统一的 5 毫秒恢复与准备预算，可以在 45 毫秒准备好，早于 60 毫秒播放。

但“已经开启 FEC”还缺少三个检查：

| 检查 | 不满足时会怎样 |
| --- | --- |
| 后续包是否到达 | 恢复信息可能也没到手 |
| 该包是否含有适用于缺失区间的冗余 | 开关已开，也可能无相应信息 |
| 恢复与准备是否赶得上 | 有冗余，仍可能错过原定播放 |

例如只把 203 的到达改为 58 毫秒，准备好就到 63 毫秒，已经迟到。FEC 不等反馈往返，也仍要等承载恢复信息的包到达。RFC 7587 因此要求接收缓冲能够把含有 FEC 的后续负载提供给解码器。[^opus-fec]

### 连续丢包会改变可恢复范围

仍限定为当前包保护紧邻前一个 20 毫秒包的情形：

| 收到的内容 | 能检查的恢复机会 |
| --- | --- |
| 202 缺失，203 到达 | 检查 203 是否能恢复 202 |
| 202、203 都缺失，204 到达 | 204 可能帮助恢复 203，不能据此承诺恢复更早的 202 |
| 203 到了，但没有相应冗余 | 不能用这次 FEC 请求证明 202 已恢复 |

这是本篇固定包长与相邻保护的边界。Opus 的模式、帧长切换和其他冗余方案还有各自的组织方式，不能把上表推广成所有 FEC 的能力。[^lbrr]

## PLC：没有新包，也要推进播放时间

丢包隐藏由接收端利用已有解码状态，也就是此前解码时保留的信息，为缺失区间产生替代输出，尽量让声音继续。它不会因为生成了样本，就把缺失的原始信息从网络上找回来。[^plc]

这里的“替代”不等于恢复了原话。例如缺失区间恰好发生音节变化，仅凭此前的声音状态不能保证知道对方原本要说什么。PLC 的作用是尽量维持输出，不能据此把缺失包记为已经收到。

回到 60 毫秒的播放计划，如果统一预留的本地生成和准备成本是 5 毫秒，就需要最晚在 55 毫秒开始处理。播放调度器知道当前要输出哪个媒体区间，不必一直等到更大的序号出现，才决定缺失区间如何输出。

连续缺失、语音突然变化等情况，会让“根据过去产生替代声音”遇到更难处理的内容。即使输出连续，也不能保证对方说的门牌号或名字被准确重建。本篇不比较听感算法，只区分使用了哪些输入、产生了什么结果。

## 三种办法付出的成本不同

把它们放在同一组维度里比较：

| 办法 | 需要的新信息 | 时间约束 | 主要成本 |
| --- | --- | --- | --- |
| 重传 | 发送端缓存中的原编码内容 | 请求与返回全程要赶上 | 反馈、额外发送、排队与缓存 |
| Opus 带内 FEC | 后续包中的恢复冗余 | 后续包和恢复处理要赶上 | 冗余占用编码或发送预算，可能需要额外等待 |
| PLC | 不需要新包，使用已有解码状态 | 本地生成和输出准备要赶上 | 内容是估计或替代，不能保证还原原始信息 |

这些机制可以协作，并非整通电话只选一个。例如先评估重传能否赶上，在收到后续包后检查 FEC，到播放调度的截止点仍没有可用内容，则为该区间产生替代输出。具体优先级与解码状态处理由实现决定，上表不是一份可直接用于生产的调度算法。

### 冗余数据也要放进带宽预算

用一笔独立的教学预算说明：假设主音频负载是 32 kbit/s，另允许 8 kbit/s 恢复数据，那么编码与恢复负载合计 40 kbit/s。若仍每 20 毫秒一包，并沿用第三篇“无选项 IPv4、UDP、12 字节 RTP 固定头”的每包 40 字节开销：

```text
每秒 50 包，头开销 = 40 × 50 × 8 = 16 kbit/s
原来：32 + 16 = 48 kbit/s
额外增加 8 kbit/s 恢复负载后：32 + 8 + 16 = 56 kbit/s
```

这是增加数据预算的算术示例，没有给 Opus 设置“冗余固定 8 kbit/s”的参数。真实 Opus 编码可以在给定总预算下调整主要声音与保护信息的分配；提高抗丢失能力也可能改变无丢失时的质量取舍。[^encoder]

若可用容量没有增加，却继续叠加恢复流量，排队可能变长。RFC 4588 要求把原始流与重传一同纳入可接受的发送速率；恢复流量不能游离在拥塞控制以外。[^congestion] 更多恢复数据若最终使包更迟到，未必有利于原定播放。

## 先运行时间预算，改变一个条件

[下载 https://hex4c59.cc/articles/rtc-04-loss-recovery/recovery_budget.py](recovery_budget.py)。在保存脚本的目录使用 Python 3.9 或更新版本运行，不需要额外依赖：

```bash
python3 recovery_budget.py
```

关键结果应包含：

```text
Latest usable input: 55 ms
NACK request: 42 ms
Retransmission arrives: 67 ms; ready: 72 ms
Retransmission: too late
Next packet prepared: 45 ms; redundancy assumed present: True
FEC recovery: on time
```

脚本明确把两个网络方向的耗时总和设为 20 毫秒，没有发网络探测。改为前面的快速返回条件：

```bash
python3 recovery_budget.py --network-rtt-ms 10 --send-queue-ms 1
```

重传在 54 毫秒返回、59 毫秒准备好，输出变为 `Retransmission: on time`。分别再试两种反例：

```bash
python3 recovery_budget.py --next-packet-ms 58
python3 recovery_budget.py --no-fec
```

前者的 FEC 准备时刻是 63 毫秒，赶不上 60 毫秒。在这个模型里，序号缺口也要到 203 到达才被发现，所以 NACK 路径同样后移。后者虽然准备时间仍可能足够，但假设包内没有恢复冗余，结果是 `FEC recovery: unavailable`。

还可以增加 `--cache-miss` 检查发送端无原包的情况。脚本没有动态缓冲、反馈抑制、真实重传、解码或播放；PLC 的开始时间只是按既定成本反推的调度位置，不能从输出的毫秒数得出某台设备的实际处理能力。

## 换一组恢复条件

下面分别计算重传与 FEC 的独立条件：缺失段计划在 100 毫秒播放，恢复解码与输出准备统一需要 6 毫秒。第 1 题中，在 75 毫秒发现缺口；反馈等待 2 毫秒，请求传输 5 毫秒，发送端查找与排队共 3 毫秒，返回传输 5 毫秒。

1. 发送端仍有原包，重传何时准备好，是否来得及？
2. 一个后续包在 92 毫秒到达，带有适用于缺失区间的冗余。FEC 来得及吗？若改为 96 毫秒到达呢？
3. 重传缓存中没有原包，后续包里也没有冗余，但解码器仍产生了等长输出。能否说缺失内容已经成功恢复？

<details>
<summary>展开判断依据</summary>

1. 原包在 `75 + 2 + 5 + 3 + 5 = 90` 毫秒返回，再准备 6 毫秒，到 96 毫秒就绪，比期限早 4 毫秒。这里已经给出缓存存在的条件。
2. 92 毫秒到达时，`92 + 6 = 98` 毫秒就绪，可以赶上。96 毫秒到达时，102 毫秒才就绪，晚 2 毫秒。有冗余与能及时利用冗余，是两项不同检查。
3. 不能。已知两个新信息来源都不可用，等长输出可以来自 PLC。必须检查实际输入与执行路径；输出样本数没有证明原信息被恢复。

</details>

如果第二题只看包里有冗余就判为成功，回到播放期限；如果第三题把“有声音”当成“原话恢复”，回看 PLC 和 FEC 请求回退的区别。

## 进阶：把恢复过程对应到解码器调用

libopus 是 Opus 的编解码实现库，程序通过它的接口进行编码和解码。前面的判断只需要知道输入来自哪里、结果何时可用。下面把这些动作对应到真实接口，方便继续做编解码实验；前面的新条件练习不要求掌握这些调用。

这里改用 16 kHz、单声道 PCM，20 毫秒对应 `16,000 × 0.020 = 320` 个每声道样本。第三篇讲的 Opus RTP 时间戳仍按每秒 48,000 刻度计数；同一个 20 毫秒区间可以同时对应 320 个解码样本和 960 个 RTP 刻度，两个数使用不同单位。[^opus-clock]

### 后续包为什么要交给解码器两次

要先处理缺失的 202 区间，再处理 203 自己的区间。以 16 kHz、单声道、每段 20 毫秒为例，每段是 320 个每声道样本：

```c
/* 示意接口调用，省略错误检查与缓冲分配，不是完整程序。 */
opus_decode(decoder, packet_203, bytes_203, pcm_202, 320, 1);
opus_decode(decoder, packet_203, bytes_203, pcm_203, 320, 0);
```

第一次把 `decode_fec` 设为 1，请求其中的前向纠错数据，产生缺失区间的输出；第二次设为 0，正常解码当前包自己的声音。两次调用使用同一个持续的解码状态，第二次没有把 203 的声音重复播放。[^decoder]

这与第三篇中丢弃网络重复包不矛盾：203 的当前声音只正常解码一次，另一次请求的目标是此前缺失的媒体区间。

### 没有可用数据时，怎样请求替代输出

对 libopus，可以用空数据指针和零长度表示丢失。在这里的 16 kHz、20 毫秒条件下，调用形态为：

```c
/* 仍为接口示意，320 表示这个缺失区间的每声道样本数。 */
opus_decode(decoder, NULL, 0, pcm_missing, 320, 0);
```

这里没有传入 202 的负载。程序取得 320 个输出样本，只证明该区间有了替代输出，不能据此增加“原包收到”或“原始声音成功恢复”的计数。

### `decode_fec=1` 返回了样本，也可能只是 PLC

libopus 解码接口明确规定：请求 FEC 而当前输入没有这种信息时，会按丢失情形处理。[^decoder]

因此，下面这条推理不成立：

```text
设置 decode_fec=1 → 返回 320 个样本 → 一定使用了恢复冗余
```

应当另查包内是否含有冗余，并结合执行路径记录结果。本文使用 `opus_packet_has_lbrr()`；LBRR 是 Low Bit-Rate Redundancy，即低码率冗余。[^lbrr] 这个检查再与实际 FEC 调用关联，才能把“用冗余恢复”和“请求后回退到 PLC”区分开。

## 再用真实 libopus 检查 FEC 与回退

这个实验是可选的进一步验证，不是完成基础练习的前提。时间条件能够成立，还要确认解码器实际读到了什么。[下载 opus_recovery.py](opus_recovery.py)，使用本机的 libopus 动态库运行这个独立实验。它通过 Python 标准库 `ctypes` 调用真实编解码接口，不是用公式模拟出 PCM。

### 运行条件与输出

需要 Python 3.9+，以及导出 `opus_packet_has_lbrr` 的 libopus。本文核验版本为 1.6.1。FFmpeg 支持 Opus，不必然表示 Python 能找到可单独加载的动态库；脚本先用系统方法查找，再尝试已有 `pkg-config` 提供的目录。若仍找不到，可用 `--opus-library` 指定实际动态库文件。

```bash
python3 opus_recovery.py --output-dir opus-recovery-on
```

脚本只生成合成信号，不采集麦克风。输出目录必须是新目录，重复实验时换一个名称，程序不会覆盖已有结果。找不到库或缺少所需接口时会明确报错，不能只安装同名 Python 包就视为准备完成。

实验固定为 16 kHz、单声道、16 位整数，20 毫秒一块，合计 2 秒、100 块。每块有 `16,000 × 0.020 = 320` 个样本，占 640 字节。索引从 0 开始，丢弃索引 12 的包，它对应源媒体的 240—260 毫秒区间；这里的索引不是前面 RTP 的序列号。

合成输入包含变化的基频、谐波与幅度，不是真人语音录音。编码设置是面向网络语音的应用模式（`OPUS_APPLICATION_VOIP`）、24 kbit/s 目标、复杂度 5、语音信号提示、最大宽带模式、关闭不连续发送，并向编码器提供 20% 的预期丢包。本次保留编码器默认的可变码率，24 kbit/s 不表示每包固定为 60 字节。实际模拟只丢弃指定的一个包，**预期丢包参数不等于本次实际丢包比例**。这些设置是实验选择，不是通话默认值。[^encoder]

先用相同合成 PCM 分别关闭和开启 FEC 编码，记录编码字节数。随后三条解码路径都读取开启组的同一批包，避免比较接收方法时又改变编码输入：

| 解码路径 | 索引 12 的动作 | 索引 13 的动作 |
| --- | --- | --- |
| `normal` | 正常解码包 12 | 正常解码包 13 |
| `plc` | 输入空数据，请求 PLC | 正常解码包 13 |
| `fec_request` | 用包 13 请求 FEC | 在同一解码状态下正常解码包 13 |

三条路径使用各自独立、持续推进的解码实例；每条内部按媒体顺序处理。后续包在本地内存里已经可用，因此这个实验能检查 FEC 机制，不能检查公网等待或播放是否及时。

程序输出四份 WAV：`input.wav`、`normal.wav`、`plc.wav`、`fec_request.wav`。另有 `report.json` 保存版本、配置与输出摘要，`frames.csv` 记录每块读取的包、字节数、冗余标志和执行动作。没有自动播放音频。

### 本次运行，哪一条证据说明使用了冗余

2026 年 10 月 15 日，在 macOS arm64、Python 3.9.6、libopus 1.6.1 下，使用脚本默认合成输入与索引 12，得到：

```text
Runtime: libopus 1.6.1
Encoder FEC enabled: True
Selected stream: 5456 bytes; FEC-disabled reference: 5482 bytes
Packet 13 has LBRR: 1
Missing-block action: fec_from_next
All paths: 32000 samples/channel
Missing block FEC-request equals PLC: False
```

包 13 的 LBRR 检查为 1，缺失区间实际执行了 FEC 请求；该区间的输出与独立 PLC 路径不同。三条路径各输出 32,000 个每声道样本、64,000 字节 PCM，但等长不等于内容相同，也不代表与原始 PCM 无损一致。

在 CSV 中对照 `fec_request` 路径里 `block_index` 为 12、13 的两行，还能看到它们都引用包 13：第一行标为 `fec_from_next`，下一行标为 `normal`。这两行对应不同的媒体区间，而非把同一段声音排进输出两遍。

需要特别解释 5456 和 5482：开启组的整体编码字节数比关闭组少了 26 字节。这个差值不能解释成“冗余没有成本”或“开启 FEC 一定省带宽”。两组使用相同目标预算，编码器还会改变内容分配；这里只记录总字节数，没有单独测量主声音和冗余各占多少。合成信号也不能代表真实语音上的普遍大小与质量结果。

### 关闭冗余，再提出同一个 FEC 请求

保持输入和解码程序不变，只关闭编码器的带内 FEC：

```bash
python3 opus_recovery.py --disable-fec --output-dir opus-recovery-off
```

本次运行的关键输出为：

```text
Encoder FEC enabled: False
Packet 13 has LBRR: 0
Missing-block action: fec_request_plc_fallback
All paths: 32000 samples/channel
Missing block FEC-request equals PLC: True
```

请求仍然返回了与缺失区间等长的样本，但包内没有 LBRR，缺失块输出与 PLC 相同。这样就把“调用了 FEC 接口”与“确实利用恢复信息”分开验证了。

也可以换 `--loss-index`，程序接受 0—98，保证缺失位置后还有一个包。换位置或版本后重新检查实际冗余标志，不能把默认位置的结果照搬过去。这个实验没有声学播放、音质评分、输入输出延迟补偿、连续丢包或动态缓冲调度；判断质量还需要对齐声音、选择相应输入和评价方法。

## 排查时，把请求、收到、恢复与隐藏分别记下来

如果日志只有“请求恢复成功”或“解码输出了样本”，仍无法判断刚才那段声音发生了什么。对同一媒体区间，可以关联这些记录：

| 事件 | 至少保留的判断依据 |
| --- | --- |
| 发出重传请求 | 缺失包身份、发出时刻、剩余播放预算 |
| 重传数据返回 | 对应哪个原包、接收时刻、何时准备好 |
| 请求带内 FEC | 使用哪个后续包、是否有相应冗余、产生哪个媒体区间 |
| 生成 PLC | 缺失区间、替代样本数、输出安排 |
| 提交与实际播放 | 是否赶上，以及最终输出采用了哪一路内容 |

统计时也保留这些区别：发了多少请求、实际收到多少重传、多少恢复赶上播放、多少 FEC 请求回退到 PLC。替代样本让时间轴继续，并不能让“原包收到”的计数自动增加；一次重传已到，也可能只增加了迟到接收。

202 的选择因而取决于两个条件：恢复数据是否存在，以及处理后是否赶得上。本文的慢重传到 72 毫秒才就绪，及时到达的冗余在 45 毫秒就绪；若冗余也不可用，接收端仍需要为 60 毫秒的输出安排生成替代声音。改变到达、缓存或冗余条件，结果就会改变。

[第五篇](https://hex4c59.cc/articles/rtc-05-send-queue/)把恢复流量放回整条发送路径：当编码、原包和重传一起争用有限带宽，队列为什么越积越长，又怎样根据网络反馈限制发送。增加保护之前，先确认它没有把更多内容推过播放期限。

[回看第三篇：从一段收包记录读懂 RTP](https://hex4c59.cc/articles/rtc-03-sequence-gap/) · [系列目录](https://hex4c59.cc/articles/rtc-from-zero/)

## 参考资料

资料核对于 2026 年 10 月 15 日。播放时间与网络预算是教学构造；libopus 输出是上述合成输入、版本和参数的实际运行结果。

[^nack]: [RFC 4585 §6.2.1：Generic NACK](https://www.rfc-editor.org/rfc/rfc4585.html#section-6.2.1)，2006 年 7 月发布，定义用于报告 RTP 包缺失的反馈格式。
[^rtx]: [RFC 4588 §1、§4、§6.3 与 §8.1：重传、反馈与缓存保留时间](https://www.rfc-editor.org/rfc/rfc4588.html)，2006 年 7 月发布，讨论延迟约束、独立重传流与发送端处理。
[^opus-fec]: [RFC 7587 §3.3：Opus 带内 FEC](https://www.rfc-editor.org/rfc/rfc7587.html#section-3.3)，2015 年 6 月发布，说明前一包恢复信息、编码器决策以及接收缓冲与解码器的配合。
[^lbrr]: [RFC 6716 §4.2 与 §4.2.5：LBRR 的组织](https://www.rfc-editor.org/rfc/rfc6716.html#section-4.2.5)，2012 年 9 月发布，说明低码率恢复信息及帧长和模式变化的边界。`opus_packet_has_lbrr` 的返回含义见 libopus 解码器文档。
[^decoder]: [libopus 1.6 解码器 API](https://opus-codec.org/docs/opus_api-1.6/group__opus__decoder.html)，说明 `decode_fec`、没有相应冗余时的丢失处理、持续解码状态，以及 PLC/FEC 所需的缺失时长参数。
[^plc]: [RFC 6716 §4.4：丢包隐藏](https://www.rfc-editor.org/rfc/rfc6716.html#section-4.4)，以及上述 libopus 解码器 API 的空输入约定。
[^encoder]: [libopus 1.6 编码器控制 API](https://opus-codec.org/docs/opus_api-1.6/group__opus__encoderctls.html)，说明带内 FEC、预期丢包百分比、编码预算与质量取舍。代码中的请求编号依据对应版本公开头文件 `opus_defines.h`。
[^congestion]: [RFC 4588 §7：重传与拥塞控制](https://www.rfc-editor.org/rfc/rfc4588.html#section-7)，说明原始数据和重传应共同受可接受发送速率约束。

[^opus-clock]: [RFC 7587 §4.1：Opus RTP 时间戳](https://www.rfc-editor.org/rfc/rfc7587.html#section-4.1)，规定 RTP 时钟固定为 48 kHz，与编解码时使用的采样率分别理解；2026 年 10 月 15 日复核。

