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
事件时移除该类。
您可以在此处查看相关的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不仅仅只有两个请求。视频片段本身包含两个请求:一个用于init片段,另一个用于视频数据。
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上浏览问题和拉取请求。
现在可以安心享用我的燕麦片和视频了,无需担心播放是否会恢复。