预加载音频和视频,从而快速播放

如何通过主动预加载资源来加快媒体播放速度。

François Beaufort
François Beaufort

更快的开始播放速度意味着有更多人观看你的视频或收听你的音乐 音频。这是众所周知的事实。在本文中,我将为您介绍 您可以采用什么技巧来通过主动 根据您的用例预加载资源。

版权所有 Blender Foundation |www.blender.org

我将说明三种预加载媒体文件的方法,首先介绍它们的优点 和缺点

很棒... 但是...
视频预加载属性 易于使用,用于托管在网络服务器上的唯一文件。 浏览器可能会完全忽略该属性。
当 HTML 文档完全加载并 已解析。
Media Source Extensions (MSE) 会忽略媒体元素上的 preload 属性,因为应用需负责 向 MSE 提供媒体
链接预加载 强制浏览器在不屏蔽的情况下请求视频资源 触发文档的 onload 事件。 HTTP 范围请求不兼容。
与 MSE 和文件段兼容。 应仅用于获取完整资源时小型媒体文件 (<5 MB)。
手动缓冲 完全控制 复杂的错误处理由网站负责。

视频预加载属性

如果视频源是托管在网络服务器上的唯一文件,您可能需要将 使用视频 preload 属性来提示浏览器如何 需要预加载的信息或内容。这意味着 Media Source Extensions 扩展 (MSE)preload 不兼容。

只有在创建了初始 HTML 文档后,系统才会开始提取资源 已完全加载并解析(例如,DOMContentLoaded 事件已触发) 而在调用资源时会触发完全不同的load事件 实际已被提取的内容

preload 属性设置为 metadata 表示用户 预计需要该视频,但会提取其元数据(尺寸、跟踪 列表、持续时间等)是可取的。请注意,从 Chrome 中开始 64,则 preload 的默认值为 metadata。(过去是 auto )。

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

preload 属性设置为 auto 表示浏览器可能会缓存 提供足够的数据,无需停止播放,即可完整播放 进一步缓冲。

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

不过,有几点需要注意。由于这只是一种提示 并忽略 preload 属性。在撰写本文时,请注意以下规则 已在 Chrome 中应用:

  • 启用流量节省程序后,Chrome 会强制将 preload 值设为 none
  • 在 Android 4.3 中,由于 Androidpreloadnone bug
  • 使用移动网络连接(2G、3G 和 4G)时,Chrome 会强制 preloadmetadata

提示

如果您的网站包含同一个网域中的许多视频资源, 建议您将 preload 值设置为 metadata,或定义 poster 属性,并将 preload 设置为 none。这样,您就可以避免 到同一网域的 HTTP 连接数上限(根据 HTTP 1.1 规范),这可能会挂起资源加载。请注意,这也可以 如果视频并非核心用户体验的一部分,可以提高网页加载速度。

如其他文章所述链接预加载是一种声明式提取, 您可以强制浏览器请求资源 在网页下载期间屏蔽 load 事件。资源 通过 <link rel="preload"> 加载的文件会存储在本地浏览器中, 直到在 DOM、JavaScript 和 或 CSS 提供商。

预加载与预提取的不同之处在于,它侧重于当前的导航和 根据资源类型(脚本、样式、字体、 视频、音频等)。它应该用于为当前的 会话。

预加载完整视频

下面介绍了如何在您的网站上预加载完整视频, JavaScript 请求获取视频内容,并将其作为资源从缓存中读取 可能已被浏览器缓存。如果没有 则会进行常规网络提取。

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

由于预加载的资源将由 Google Cloud 中的 在此示例中,as 预加载链接的值为 video。如果是音频内容 元素,则属性为 as="audio"

预加载第一个片段

以下示例展示了如何使用 <link rel="preload"> 预加载视频的第一段,并将其与 Media Source Extensions 搭配使用。如果您不熟悉 使用 MSE JavaScript API,请参阅 MSE 基础知识

为简单起见,我们假设整个视频 file_1.webmfile_2.webmfile_3.webm 等较小的文件。

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

支持

您可以使用as<link rel=preload> 代码段:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

手动缓冲

在深入了解 Cache API 和 Service Worker 之前,我们先来看看 如何使用 MSE 手动缓冲视频。以下示例假定您的 服务器支持 HTTP Range 但使用 .zip 文件 细分。请注意,有些中间件库如 Google 的 Shaka PlayerJW PlayerVideo.js 专门为您处理这一问题。

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

注意事项

由于您现在可以控制整个媒体缓冲体验,因此建议您 考虑设备的电池电量、“流量节省程序模式”用户偏好设置和 在考虑预加载时需要考虑哪些网络信息

电池感知

考虑用户设备的电池电量然后再考虑 如何预加载视频这将在电池电量达到 30% 时延长电池续航时间 较低。

在以下情况下,停用预加载或至少预加载 设备电量耗尽

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

检测“流量节省程序”

使用 Save-Data 客户端提示请求标头提供快速和轻量级提示 向已选择“节省流量”的用户提供应用模式 。通过标识此请求标头,您的应用可以自定义和 针对受成本和性能限制的情况提供优化的用户体验 用户。

如需了解详情,请参阅使用 Save-Data 交付快速的轻量级应用

根据网络信息智能加载

您可能需要在预加载之前检查 navigator.connection.type。时间 设置为 cellular,则可以防止预加载并建议用户 他们的移动网络运营商可能会收取带宽费用, 自动播放之前缓存的内容

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

查看网络信息示例,了解如何对网络做出反应 也会随之更改

预缓存多个第一个片段

如果我想推测预加载某些媒体内容 知道用户最终会选择哪种媒体?如果用户使用的是 的网页,那么我们可能有足够的内存来提取一个 每个片段文件,但我们绝对不应创建 10 个隐藏 <video> 元素和 10 个 MediaSource 对象,并开始馈送这些数据。

下面的两部分示例展示了如何预缓存 了解如何使用功能强大且易于使用的 Cache API。请注意, 也可以使用 IndexedDB 来实现。我们尚未使用 Service Worker, 您还可以通过 window 对象访问 Cache API。

提取和缓存

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

请注意,如果我要使用 HTTP Range 请求,则必须手动重新创建 Response 对象,因为 Cache API 目前不支持 Range 响应。是 请注意,调用 networkResponse.arrayBuffer() 会提取全部内容 将响应一次性放入渲染器内存,这也是为什么您可能需要使用 范围很小

我修改了上面的部分示例以保存 HTTP 范围,以供参考 向视频预缓存发送多个请求

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

播放视频

当用户点击播放按钮时,我们会提取视频的第一段 ,以便立即开始播放(如果可用)。 否则,我们将直接从网络中提取。请注意 且用户可以决定清除缓存

如前所述,我们使用 MSE 将视频的第一段馈送到视频中 元素。

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

使用 Service Worker 创建范围响应

如果您已经将整个视频文件 并将其保存在 Cache API?当浏览器发送 HTTP Range 请求时,您肯定不会 需要将整个视频放入渲染器内存中,因为 Cache API 不需要 目前支持 Range 响应。

我来展示一下如何拦截这些请求并返回自定义的 Range 从 Service Worker 返回。

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

请务必注意,我使用 response.blob() 重新创建了此 Slice 因为这仅会为我提供一个文件句柄 response.arrayBuffer() 将整个文件引入渲染器内存。

我的自定义 X-From-Cache HTTP 标头可用于了解此请求是否 来自缓存或网络。可供其他玩家使用 ShakaPlayer 忽略响应时间作为指标 网络速度

已了解官方示例媒体应用,尤其是其 ranged-response.js 文件,获取有关如何处理 Range 的完整解决方案 请求。