# 草原指南 4K 原片播放缓冲优化通知

> 日期：2026-07-16
>
> 前端仓库：`mmg/hl-ui`
>
> 影响页面：草原指南 H5 详情/播放页
>
> 本次后端接口、字段和 OSS 地址均无变化

> [!IMPORTANT]
> 2026-07-16 线上验证发现：原通知中的“主动暂停并等待固定缓冲秒数”会与浏览器原生媒体加载策略形成死锁，已经撤销。前端必须按独立更正通知
> [`16_fix_grassland_guide_playback_buffer_deadlock.md`](./16_fix_grassland_guide_playback_buffer_deadlock.md)
> 处理，不得再以 `8/15` 秒阈值阻塞播放或切换倍速。

## 1. 问题与诊断结论

草原指南详情直接播放后端返回的 OSS 原始 MP4。正式环境真实 4K 样本的媒体参数为：

- 文件约 `451.74 MiB`，时长约 `95` 秒。
- 分辨率 `3812 × 2160`，`60 fps`。
- H.264 Main Profile，Level `5.2`。
- 平均码率约 `40.20 Mbps`。
- `1x` 播放时短时峰值约 `72.98 Mbps`。
- `2x` 播放时平均网络消耗约 `80.40 Mbps`；短时峰值约 `142.17 Mbps`。

正式 OSS 已支持 HTTP Range，请求返回 `206 Partial Content`。同一诊断环境连续读取 OSS Range 的平均速度约 `230.52 Mbps`，高于该视频 `2x` 播放的短时峰值。因此目前没有证据表明 Java 服务或 OSS Range 能力是主要瓶颈；但该结果不代表每个用户到 OSS 的实时链路都能达到相同速度。

现已确认 `1x` 也会偶发卡顿。结合 `1x` 接近 `73 Mbps` 的短时峰值，更可能是用户链路瞬时波动与浏览器前向缓冲较浅共同造成：即使平均网速高于视频平均码率，只要短时间下载速度低于瞬时消耗速度，缓冲仍可能耗尽。因此首次 `1x` 播放和卡顿恢复也必须执行缓冲水位保护，不能只处理 `2x`。

此外，`4K 60 fps` 在 `2x` 下相当于设备需要承担接近 `120 fps` 的解码节奏。部分设备即使网络充足，也可能因硬件解码能力不足出现掉帧；前端需要把“等待网络缓冲”和“设备解码掉帧”分别记录。

## 2. 前端处理要求

### 2.1 保持原生 Range 播放

- 继续直接使用接口返回的 `videoUrl` 作为 `<video src>`。
- 设置 `preload="auto"`，允许浏览器提前加载媒体数据。
- 不要使用 `fetch`/`axios` 把完整 MP4 下载为 Blob 后再播放，避免一次性占用数百 MiB 内存并破坏原生 Range 调度。
- 不要改写 OSS 域名，不转成 HLS/M3U8，不接直播播放器。
- 本轮不降低码率、不转码、不新增清晰度档位。

### 2.2 计算真实前向缓冲

不能只读取 `video.buffered.end(video.buffered.length - 1)`。应找到包含 `currentTime` 的 buffered 区间，再计算：

```js
function getBufferAhead(video) {
  const currentTime = video.currentTime

  for (let index = 0; index < video.buffered.length; index += 1) {
    const start = video.buffered.start(index)
    const end = video.buffered.end(index)

    if (currentTime >= start && currentTime <= end) {
      return Math.max(0, end - currentTime)
    }
  }

  return 0
}
```

### 2.3 `1x` 与倍速播放缓冲策略

- 用户点击播放时直接调用原生 `video.play()`，不得先暂停等待固定缓冲秒数。
- 用户切换 `2x` 时只设置 `video.playbackRate = 2`，不得调用 `pause()`，也不得重新执行“初始缓冲门槛”。
- `waiting` 事件只负责显示“正在缓冲”和记录诊断；不得在事件处理器内再次调用 `pause()`。
- 保持原生播放请求后，浏览器会继续 Range 取流；数据恢复后通过 `playing` 事件关闭缓冲提示。
- `stalled` 用于记录网络加载停滞；仅当仍有播放意图且 `readyState < HTMLMediaElement.HAVE_FUTURE_DATA` 时显示缓冲提示，不主动暂停。
- `timeupdate` 只采集缓冲指标，不得因低于人为水位而主动暂停。
- 用户主动暂停、拖动进度或离开页面时清除缓冲提示，避免与真实播放意图混淆。
- `preload="auto"` 只是浏览器提示，不能强制浏览器预取指定秒数。

`video.buffered` 用于诊断和观测，不再作为阻塞 `play()` 或切换倍速的硬门槛。简单的原生 `<video>` 无法保证“暂停后一定继续预取到 N 秒”；若业务将来要求确定性分片缓冲，需要另行评估 MSE/HLS 或 CDN，不应在原生 MP4 播放器中模拟。

### 2.4 记录卡顿证据

至少监听并记录以下事件和状态：

- 事件：`loadstart`、`loadedmetadata`、`canplay`、`canplaythrough`、`progress`、`waiting`、`stalled`、`playing`、`seeking`、`seeked`、`error`。
- 状态：`currentTime`、`playbackRate`、前向缓冲秒数、`readyState`、`networkState`。
- 浏览器支持时记录 `getVideoPlaybackQuality()` 的 `droppedVideoFrames` 和 `totalVideoFrames`。

日志不得记录完整带签名 URL、Token 或其他凭证。建议只记录 `videoId`、事件时间和上述播放指标。

判断口径：

- 出现 `waiting`/`stalled`，同时前向缓冲接近 `0`：网络或缓冲调度不足。
- 前向缓冲充足、没有 `waiting`，但 `droppedVideoFrames` 持续上升：设备解码能力不足。

## 3. 接口契约

播放接口保持不变：

```http
GET /admin/grassland-guide/public/videos/{videoId}/play
```

前端继续使用：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `videoId` | string | 视频业务 ID |
| `videoUrl` | string | OSS 原始 MP4 地址，直接交给原生 `<video>` |
| `durationSeconds` | integer | 后端从素材解析的时长 |
| `effectiveCoverUrl` | string | 生效封面 |

雪花 ID 必须按字符串处理。本次不增加缓冲、码率或清晰度字段，前端不得等待后端返回这些字段后才处理播放。

相关接口说明见：

- [`16_feat_grassland_guide_public_play.md`](./16_feat_grassland_guide_public_play.md)
- [`12_feat_grassland_guide_admin_mp_oss.md`](./12_feat_grassland_guide_admin_mp_oss.md)

## 4. 前端验收清单

- [ ] `<video>` 使用接口原始 `videoUrl`，并设置 `preload="auto"`。
- [ ] 没有将完整 MP4 下载为 Blob。
- [ ] 首次 `1x` 播放不受固定缓冲秒数阻塞。
- [ ] 切换 `2x` 不调用 `pause()`，不等待 `15` 秒缓冲。
- [ ] `waiting`/`stalled` 只更新提示与日志，不主动暂停；`playing` 能可靠关闭提示。
- [ ] 用户暂停、拖动和离开页面时不会被自动恢复播放。
- [ ] 能区分并记录缓冲耗尽与设备解码掉帧。
- [ ] 使用正式 4K 样本分别连续验证 `1x` 和 `2x`，记录等待次数、累计等待时长、最小前向缓冲和掉帧数。
- [ ] Chrome 桌面端和目标移动设备均完成验证。

若完成上述缓冲策略后，多个地区和设备仍普遍出现“前向缓冲耗尽”，再单独评估 OSS 前置 CDN Range 缓存或 OSS 传输加速；该基础设施调整会新增费用，不属于本次前端通知范围。
