diff --git a/changelogs/2026-07/16_fix_grassland_guide_playback_buffer_deadlock.md b/changelogs/2026-07/16_fix_grassland_guide_playback_buffer_deadlock.md new file mode 100644 index 0000000..9e35914 --- /dev/null +++ b/changelogs/2026-07/16_fix_grassland_guide_playback_buffer_deadlock.md @@ -0,0 +1,108 @@ +# 草原指南切换 2x 后永久缓冲更正 + +> 日期:2026-07-16 +> +> 前端仓库:`mmg/hl-ui` +> +> 影响组件:`src/views/h5/grassland-guide/components/GuideVideoPlayer.vue` +> +> 影响辅助逻辑:`src/views/h5/grassland-guide/components/playbackBuffer.js` +> +> 本次后端接口、字段、OSS 地址和视频文件均无变化 + +## 1. 现象 + +视频在 `1x` 已经开始播放后切换到 `2x`,页面停在“正在缓冲”,即使等待较长时间也不能恢复。 + +Network 中可以看到同一个 MP4 存在多条大小不同的 `206 Partial Content` 请求。这是 Chrome 原生媒体加载器根据 MP4 元数据、当前播放位置和缓存状态发起的 HTTP Range 请求,Range 大小不固定是正常行为,不能据此判断 OSS 分片异常。 + +## 2. 已确认根因 + +线上播放器当前调用链为: + +```text +切换 2x + → 设置 video.playbackRate = 2 + → evaluateBuffer({ initial: true }) + → 要求 bufferAhead >= 15 秒 + → 未达到阈值 + → pauseForBuffering() + → video.pause() +``` + +播放器暂停后,浏览器可以降低甚至停止后续媒体预取。当前代码又只依赖 `progress`、`canplay` 等媒体事件重新执行 `evaluateBuffer()`,没有保证这些事件一定继续产生,因此可能永远达不到 `15` 秒阈值,形成状态机死锁。 + +同类问题还存在于: + +- 首次播放前等待固定 `8` 秒。 +- `timeupdate` 检测到低水位后主动 `pause()`。 +- `waiting`/`stalled` 事件再次主动 `pause()`。 + +这些逻辑把浏览器原生的“缺数据时等待并继续下载”变成了应用层“暂停后等待浏览器继续下载”,两者行为并不等价。 + +## 3. 必须修改的前端逻辑 + +### 3.1 播放与切换倍速 + +```js +async function play() { + playbackRequested.value = true + await videoRef.value?.play() +} + +function togglePlaybackRate() { + const video = videoRef.value + if (!video) return + + playbackRate.value = playbackRate.value === 1 ? 2 : 1 + video.playbackRate = playbackRate.value + + // 禁止在这里 pause() + // 禁止在这里重新执行固定秒数的初始缓冲门槛 +} +``` + +### 3.2 缓冲状态由原生事件驱动 + +```js +function handleWaiting() { + if (!playbackRequested.value) return + buffering.value = true + recordPlaybackEvent('waiting') +} + +function handleStalled() { + const video = videoRef.value + if (playbackRequested.value && video?.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) { + buffering.value = true + } + recordPlaybackEvent('stalled') +} + +function handlePlaying() { + buffering.value = false + recordPlaybackEvent('playing') +} +``` + +事件处理器内不得调用 `video.pause()`。只要用户没有主动暂停,就保留原生播放意图,让浏览器在缺数据时自动等待、继续 Range 取流,并在数据恢复后自行继续播放。 + +### 3.3 删除主动低水位暂停 + +- 删除 `pauseForBuffering()` 对自动缓冲流程的使用。 +- `handleTimeUpdate()` 只同步时间、记录 `bufferAhead` 和掉帧指标,不得检测低水位后暂停。 +- `evaluateBuffer()` 不再阻塞首次播放或倍速切换;如保留该方法,只能用于诊断,不能控制 `play/pause`。 +- 删除 `BUFFER_POLICIES` 中作为播放硬门槛的 `initial/low/resume`,避免后续重新引入死锁。 + +## 4. 验收要求 + +- [ ] `1x` 点击播放后能直接进入原生播放流程,不等待固定 `8` 秒。 +- [ ] 播放中切换 `2x` 不触发 `pause` 事件。 +- [ ] 切换 `2x` 后,即使触发 `waiting`,后续 Range 请求仍继续。 +- [ ] 数据恢复后触发 `playing`,缓冲遮罩自动消失,播放继续。 +- [ ] `1x ↔ 2x` 连续切换 10 次,不出现永久缓冲。 +- [ ] 拖动进度后仍可重新播放,用户主动暂停不会被自动恢复。 +- [ ] Console 中不再出现 `ratechange → initial-buffering → pause` 的调用序列。 +- [ ] 使用 DevTools 测试真实用户表现时关闭 `Disable cache`;需要模拟弱网时单独选择网络限速,不把禁用缓存结果当作正常生产表现。 + +修复状态机后,若 `1x` 或 `2x` 仍出现能够自行恢复的短时 `waiting`,再根据 `bufferAhead`、实际下载速度和掉帧数判断是用户网络还是设备解码能力问题。播放器逻辑修复不能提高用户带宽;需要跨地区稳定承载原始 4K 高码率视频时,应另行评估 OSS 前置 CDN Range 缓存。 diff --git a/changelogs/2026-07/16_notice_grassland_guide_4k_playback_buffering.md b/changelogs/2026-07/16_notice_grassland_guide_4k_playback_buffering.md index 5349de1..e3197fb 100644 --- a/changelogs/2026-07/16_notice_grassland_guide_4k_playback_buffering.md +++ b/changelogs/2026-07/16_notice_grassland_guide_4k_playback_buffering.md @@ -8,6 +8,11 @@ > > 本次后端接口、字段和 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 样本的媒体参数为: @@ -58,15 +63,16 @@ function getBufferAhead(video) { ### 2.3 `1x` 与倍速播放缓冲策略 -- 用户首次点击 `1x` 播放时,若前向缓冲不足 `8` 秒,先显示“正在缓冲”;达到 `8` 秒后再开始播放。 -- `1x` 播放过程中前向缓冲低于 `3` 秒时进入缓冲状态,恢复到 `8~10` 秒后继续播放。 -- 用户选择 `2x` 时,若前向缓冲不足 `15` 秒,先保持暂停并显示“正在缓冲”,达到 `15` 秒后再以 `2x` 播放。 -- `2x` 播放过程中前向缓冲低于 `5` 秒时进入缓冲状态,恢复到 `12~15` 秒后继续播放。 -- 原生播放器触发 `waiting` 或 `stalled` 时,无论当前倍速是多少,都进入统一缓冲流程并按当前 `playbackRate` 对应的恢复水位继续播放。 -- 用户主动暂停、拖动进度或离开页面时取消自动恢复,避免播放器违背用户操作。 -- `preload="auto"` 只是浏览器提示,不能单独保证缓冲量;必须以 `video.buffered` 的实际结果作为判断依据。 +- 用户点击播放时直接调用原生 `video.play()`,不得先暂停等待固定缓冲秒数。 +- 用户切换 `2x` 时只设置 `video.playbackRate = 2`,不得调用 `pause()`,也不得重新执行“初始缓冲门槛”。 +- `waiting` 事件只负责显示“正在缓冲”和记录诊断;不得在事件处理器内再次调用 `pause()`。 +- 保持原生播放请求后,浏览器会继续 Range 取流;数据恢复后通过 `playing` 事件关闭缓冲提示。 +- `stalled` 用于记录网络加载停滞;仅当仍有播放意图且 `readyState < HTMLMediaElement.HAVE_FUTURE_DATA` 时显示缓冲提示,不主动暂停。 +- `timeupdate` 只采集缓冲指标,不得因低于人为水位而主动暂停。 +- 用户主动暂停、拖动进度或离开页面时清除缓冲提示,避免与真实播放意图混淆。 +- `preload="auto"` 只是浏览器提示,不能强制浏览器预取指定秒数。 -阈值是针对当前约 `40 Mbps` 的 4K 原片给出的首轮参数。前端应按播放倍速集中定义为常量,便于根据真机数据调整,不要散落魔法值。 +`video.buffered` 用于诊断和观测,不再作为阻塞 `play()` 或切换倍速的硬门槛。简单的原生 `