Video.js 博客

Garrett Singer2018-12-17

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回调。

结果发现,如果我们收到一个错误,我们会中止该组中的其他请求,并等待调用done回调,直到这些请求报告它们已被中止。然而,根据XHR标准,如果请求未发送,中止算法可能不会运行。在这种情况下,当网络断开时,第一个请求在第二个请求有机会发送之前就报告了错误。在我们中止第二个请求后,我们陷入了等待一个永远不会到来的错误的困境。

我们通过在遇到第一个错误时立即回调并返回错误来修复了这个问题,而不是等待每个后续请求完成。

这个问题也应该存在于HLS中,但仅限于片段带有EXT-X-KEY或EXT-X-MAP标签以配合媒体请求的情况。在我们尝试的内容中,这两个标签都不存在,因此它似乎仅限于DASH内容。

故障已解决

我们希望这对于描述我们遇到的一些bug、我们如何进行调查以及解决方案是怎样的有所帮助。我们也乐意听取您的意见。您可以在Video.js Slack的#playback频道找到我们,或者在https://github.com/videojs/http-streaming上查看问题和PR。

现在可以安心享用我的燕麦片和视频了,无需担心播放是否会恢复。