Bugpost:断开与重连
在我以前的公寓里,每当我用微波炉热一碗燕麦片时,我的WIFI就会断开。这很烦人,但我肚子饿了,公寓太小无法将路由器搬离微波炉更远,而且我也不想买任何在不同频率下运行的东西。所以我会启动微波炉,观看笔记本电脑上正在播放的视频直到缓冲区耗尽,然后看着加载指示器旋转直到微波炉发出三声提示音。
值得庆幸的是,视频播放器通常对网络连接(和不稳定的微波炉)等问题具有相当强的鲁棒性,并且在网络恢复后视频会继续播放。但最近我们发现,对于在 Video.js 中播放的某些内容,视频没有恢复播放。更糟糕的是,加载指示器直接消失了,屏幕上留下了一个冻结的画面。
您可以自己查看此行为:下载 Video.js 7.3.0,加载 https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd 作为源,然后使用 Chrome 开发者工具断开并重新连接网络。
以下是发生的情况以及我们如何修复它的快速回顾。
加载指示器
为了解决这个bug,我们从最明显的问题开始:加载指示器的消失。该指示器由 Video.js 管理。在代码中,当技术触发 waiting
事件时,vjs-waiting
类会添加到 HTML 元素中,并在下一个 timeupdate
事件时移除。
/**
* Retrigger the `waiting` event that was triggered by the {@link Tech}.
*
* @fires Player#waiting
* @listens Tech#waiting
* @private
*/
handleTechWaiting_() {
this.addClass('vjs-waiting');
/**
* A readyState change on the DOM element has caused playback to stop.
*
* @event Player#waiting
* @type {EventTarget~Event}
*/
this.trigger('waiting');
this.one('timeupdate', () => this.removeClass('vjs-waiting'));
}
在记录了播放器发出的事件后,问题变得清晰起来。有些浏览器在waiting
事件之后立即发出了timeupdate
事件。
因此 timeupdate
事件本身并不可靠,那么还有什么可以用来更好地判断我们是否在等待呢?我们检查了 timeupdate
事件上的 player.currentTime()
,发现当我们捕获到最后一个 timeupdate
事件,即 waiting
事件之后的那个时,播放器的时间与 waiting
事件之前的 timeupdate
事件的时间相同(也与 waiting
事件触发时的时间相同)。
`timeupdate` triggered, player.currentTime = 11
`timeupdate` triggered, player.currentTime = 11.250
`waiting` triggered, player.currentTime = 11.250
`timeupdate` triggered, player.currentTime = 11.250
修复方法相当简单。只需在播放器收到 waiting
事件时记录其时间,然后不在下一个 timeupdate
事件时移除 vjs-waiting
类,而是在下一个时间与我们记录的时间不同的 timeupdate
事件时移除 vjs-waiting
类。
您可以在此处查看相关的PR,并且它已在Video.js 7.4.0中提供。
断开连接后恢复播放
加载指示器恢复后,下一个问题是确定为什么在网络重新连接时视频没有恢复播放。首先,我们测试了几种不同的内容。我们测试的 HLS 源确实重新连接了,但 DASH 源没有。更糟糕的是,当使用 DASH 源时,Chrome 的调试控制台冻结了,因为它以计算机能处理的速度累积了请求和控制台错误。
控制台崩溃
想象一下您的控制台被以下两条消息重复填充数千次
GET https://dash.akamaized.net/akamai/bbb_30fps/bbb_a64k/bbb_a64k_8.m4a net::ERR_INTERNET_DISCONNECTED
VIDEOJS: WARN: Problem encountered with the current HLS playlist. Trying again since it is the final playlist.
当您的控制台冻结时,调试可能会很困难。这就像在微波炉工作时尝试看视频一样。因此我们首先解决了这个问题。
查看错误消息,我们发现它反复重试最后一个内容片段。这是预期行为,因为我们很久以前就将其添加到我们的 HLS 播放列表加载器中。然而,当我们添加 DASH 支持时,我们创建了一个不同的播放列表加载器,但我们从未为重新加载最终的 DASH 播放列表添加相同的逻辑。
在添加节流之后,调试变得容易得多,我们得以再次使用控制台并深入研究为什么 HLS 能够重新连接而 DASH 不能。
多个请求,多个问题
最后一个问题被证明是最难调试的。最初,我们认为是解复用内容的问题,因为拥有一个单独的音频片段加载器从音频播放列表加载,可能会在主片段加载器尝试从视频播放列表加载时同时出现错误而导致问题。然而,HLS 也可以有解复用内容,而且在一个示例源证明可以正常恢复后,这种方法就被排除了。
通过检查 Chrome 的网络日志,DASH 内容在某个点之后停止请求视频片段,而只请求音频片段。但 DASH 的请求不止两个。视频片段本身包括两个请求:一个用于初始化片段,另一个用于视频数据。
VHS 中的请求由一个名为 media-segment-request
的模块管理,该模块负责请求一个片段所需的所有内容,无论是密钥、初始化片段、媒体数据,还是所有这些,然后才通知片段加载器其请求已完成。对于视频请求,media-segment-request
在断开连接后从未调用完成回调。
结果发现,如果我们收到一个错误,我们就会中止该组中的其他请求,并等待调用 done
回调,直到这些请求报告它们已被中止。然而,根据 XHR 标准,如果请求未发送,中止算法可能不会运行。在这种情况下,当网络断开连接时,第一个请求在第二个请求有机会发送之前就报告了错误。在我们中止第二个请求后,我们陷入了等待一个永远不会发生的错误的困境。
我们通过在遇到第一个错误时立即回调并返回错误来解决这个问题,而不是等待每个后续请求完成。
这个问题也应该存在于HLS中,但仅限于片段带有EXT-X-KEY或EXT-X-MAP标签以配合媒体请求的情况。在我们尝试的内容中,这两个标签都不存在,因此它似乎仅限于DASH内容。
Bug 已解决
我们希望这些内容对您有所帮助,能让您了解我们遇到的一些 bug、我们如何进行调查以及解决方案是什么样的。我们也乐于听取您的意见。您可以在 Video.js Slack 的 #playback 频道找到我们,或者在 https://github.com/videojs/http-streaming 上查看 Issues 和 PRs。
现在可以安心享用我的燕麦片和视频了,无需担心播放是否会恢复。