Video.js 加载视频失败切换其它 source

使用 Video.js 加载视频(本例为 m3u8 直播视频源)时,如果失败就会显示错误提示 UI。

一些场景下无法进行手动刷新,所以需要对加载失败的场景进行处理,尝试加载其它可用的视频源。

本例介绍 Video.js 如何切换 source、如何捕获错误,以及一些细节。

Video.js 提供的 API 有多种方式可以实现这个功能,本例只是其中一个方案。

示例代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Video.js 加载失败切换其它视频源</title>
    <link href="https://vjs.zencdn.net/7.15.4/video-js.css" rel="stylesheet" />
    <script src="https://vjs.zencdn.net/7.15.4/video.min.js"></script>
  </head>
  <body>
    <video id="my-video" class="video-js"></video>

    <script>
      // 测试用的视频源
      const sources = [
        {
          src: 'https://aaa/.m3u8',
          type: 'application/x-mpegURL'
        },
        {
          src: 'https://bbb/.m3u8',
          type: 'application/x-mpegURL'
        },
        // 第三个真实可用
        {
          src: 'https://live.unified-streaming.com/scte35/scte35.isml/.m3u8',
          type: 'application/x-mpegURL'
        }
      ]

      let index = 0 // 当前加载的视频源序号

      var player = videojs('my-video', {
        width: 500,
        controls: true,
        preload: 'auto',
        autoplay: 'muted', // 实现视频自动播放的关键
        sources: sources[index]
      })

      videojs.hook('beforeerror', (player, err) => {
        console.log('hook - beforeerror', index, player.src(), err)
        // Video.js 在切换/指定 source 后立即会触发一个 err=null 的错误,这里过滤一下
        if (err !== null) {
          player.src(sources[++index])
        }

        // 清除错误,避免 error 事件在控制台抛出错误
        return null
      })

      player.ready(() => {
      	// 丢失 source 事件处理
        player.tech().on('retryplaylist', function () {
          console.log('event - retryplaylist')
          player.src(sources[++index])
        })
      })

      // 其它可以观察进度的事件和钩子
      // videojs.hook('error', (player, err) => {
      //   console.log('hook - error')
      //   return err
      // })

      // player.on('error', () => {
      //   console.log('event - error')
      // })
      // player.on('loadeddata', () => {
      //   console.log('event - loadeddata')
      // })
      // player.on('loadedmetadata', () => {
      //   console.log('event - loadedmetadata')
      // })
      // player.on('loadstart', () => {
      //   console.log('event - loadstart')
      // })
    </script>
  </body>
</html>

细节说明

自动播放

以前可以通过给 video 元素或 Video.js 选项设置 autoplay 自动播放(非静音的),但是后来 Chrome 做出了限制,禁止音频自动播放。

这就导致当 Video.js 自动或手动调用 player.play() 执行播放时,Chrome 控制台会报错:DOMException: play() failed because the user didn't interact with the document first.

Chrome 的目的是避免音频的自动播放骚扰到用户的意外情况,但也划定了允许视频自动播放的条件,例如设置静音的视频允许自动播放。

你可以在 video 元素上设置 muted 和 autoplay 属性,也可以在调用 Video.js 的 play() 方法前先设置静音,例如:

// 当前视频加载到足够持续播放的数据后触发 loadeddata
// 也可以使用 loadedmetadata 事件,它在加载完视频元数据后触发,也足够播放视频了
player.on('loadeddata', () => {
  // 先设置静音
  player.muted(true)
  // 再执行播放
  player.play()
})

又或者设置 autoplay 选项为 muted,它的效果相当于上面的代码。

虽然官方说 autoplay: 'muted' 选项 会在 loadstart 时自动执行 player() 方法,但是 loadstart 事件只是完成开始加载事件时触发,执行事件处理函数时,并不能保证加载的视频数据片段足够进行播放甚至可能加载失败,所以在 loadstart 事件中手动执行 player() 依然会报错,可是通过设置 autoplay: 'muted' 选项并未报错,大概是调用时机比文档中说的要靠后些。(简单看了下源码不太好找就放弃了)

关于 Chrome 禁止自动播放声音请查看:Chrome 66禁止声音自动播放之后

错误捕获

示例代码中用了两个钩子 beforeerrorerror,它们都是当捕获到错误时触发。

beforererror 钩子

beforererror 用于捕获到错误,并在浏览器控制台抛出错误前触发。

它必须返回一个错误信息或代表错误已清除的 null

如果它返回有效的错误信息(不是 null),则会触发 error 事件和钩子,并作为参数传递过去。

如果返回 null,就不会触发 error 事件和钩子。

默认情况下它就是把错误直接 return。

error 钩子

error 用于捕获 beforerror 返回的错误信息,默认情况下,它会直接将错误展示在浏览器的控制台。

player.on('error', event => {
  // 可以通过 event 获取 player 实例
  const currentPlayer = event.target.player
  // console.log(currentPlayer === player) // true

  // 可以通过 error() 方法获取当前抛出的错误信息(可通过beforeerror钩子自定义返回的错误对象)
  console.log(player.error())
})
使用选择

如果不想在控制台抛出错误信息,可以使用 beforeerror,处理业务逻辑后最终返回 null 清理错误。

beforeerror 在切换/指定 source 后立即会触发一个 err=null 的错误,所以内部还需要额外判断一下触发时是否真的是错误导致。

如果不想进行额外的判断,且无所谓控制台是否显示错误信息,则可以直接使用 error 事件或钩子,简单省事。

请注意:用于钩子处理函数是全局添加的,在实际开发中请记得在必要时机移除(removeHook)钩子处理函数,在存在多个不同处理的视频播放的页面中可能还需要定义判断播放器的逻辑。

丢失视频源

视频已成功播放后,网络突然变得不稳定甚至断网,导致Video.js 请求 .m3u8 返回 404,直播视频就会持续 loading。

Video.js 会在控制台抛出警告:VIDEOJS: WARN: Problem encountered with playlist 0-https://xxxx.m3u8. Trying again since it is the only playlist.

这个期间 Video.js 认为视频源丢失,一直在尝试重新加载,当网络恢复,加载成功后,就又会继续播放。

但有的服务器为直播视频地址设置了有效期,一定时间没有请求就会失效,需要使用新生成的地址。

而 Video.js 的 error 钩子和事件无法监听这个场景。

这样就导致一旦断网时间超过地址有效期,网络恢复后重新加载视频源仍会失败,但是没有定义有效的重载机制,Video.js 就会一直重试这个过期的地址。

好在通过源码搜索这段警告,发现 Video.js 的 tech 在告警重试后主动触发一个 retryplaylist 事件,我们可以通过 tech 监听这个事件执行自定义重载机制。

源码:

if (playlists.length === 1 && blacklistDuration !== Infinity) {
  videojs.log.warn("Problem encountered with playlist " + currentPlaylist.id + ". " + 'Trying again since it is the only playlist.');
  this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay

  return this.masterPlaylistLoader_.load(isFinalRendition);
}

注意: tech 要在播放器准备就绪后才会初始化完成,所以要在 ready 中添加回调函数绑定 retryplaylist 事件处理函数。