视频和音频 API

HTML 提供了用于在文档中嵌入富媒体的元素—— <video><audio> ——它们又带有自己的 API,用于控制播放、定位等。本文将向你展示如何执行常见任务,例如创建自定义播放控件。

预备知识 熟悉 HTMLCSSJavaScript,尤其是 JavaScript 对象基础知识以及 DOM 脚本网络请求等核心 API 知识。
目标
  • 什么是编解码器,以及不同的视频和音频格式。
  • 了解与音频和视频相关的关键功能——播放、暂停、停止、快退和快进、持续时间以及当前时间。
  • 使用 HTMLMediaElement API 构建一个基本的自定义媒体播放器,以实现更好的可访问性或跨浏览器的一致性。

HTML 视频和音频

<video><audio> 元素允许我们将视频和音频嵌入到网页中。正如我们在 HTML 视频和音频中所示,典型的实现如下

html
<video controls>
  <source src="rabbit320.mp4" type="video/mp4" />
  <source src="rabbit320.webm" type="video/webm" />
  <p>
    Your browser doesn't support HTML video. Here is a
    <a href="rabbit320.mp4">link to the video</a> instead.
  </p>
</video>

这会在浏览器中创建一个视频播放器,如下所示

你可以在上面链接的文章中查看所有 HTML 功能的作用;对于我们在此处的目的,最有趣的属性是 controls,它启用了默认的播放控件集。如果你不指定此属性,将不会获得播放控件

这对于视频播放来说并不是立即可用的,但它确实有优点。原生浏览器控件的一个大问题是它们在每个浏览器中都不同——这对于跨浏览器支持来说不是很好!另一个大问题是大多数浏览器中的原生控件对键盘的可访问性不是很好。

你可以通过隐藏原生控件(通过删除 controls 属性)并使用 HTML、CSS 和 JavaScript 编程自己的控件来解决这两个问题。在下一节中,我们将介绍可用于执行此操作的基本工具。

HTMLMediaElement API

作为 HTML 规范的一部分,HTMLMediaElement API 提供了允许你以编程方式控制视频和音频播放器的功能——例如 HTMLMediaElement.play()HTMLMediaElement.pause() 等。此接口适用于 <audio><video> 元素,因为你想要实现的功能几乎相同。让我们通过一个示例,逐步添加功能。

我们完成的示例看起来(和功能)如下

入门

要开始此示例,请按照以下步骤操作

  1. 在你的硬盘上创建一个名为 custom-video-player 的新目录。

  2. 在其中创建一个名为 index.html 的新文件,并填充以下内容

    html
    <!doctype html>
    <html lang="en-gb">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Video player example</title>
        <link rel="stylesheet" type="text/css" href="style.css" />
      </head>
      <body>
        <div class="player">
          <video controls>
            <source
              src="/shared-assets/videos/sintel-short.mp4"
              type="video/mp4" />
            <source
              src="/shared-assets/videos/sintel-short.webm"
              type="video/webm" />
          </video>
          <div class="controls">
            <button
              class="play"
              data-icon="P"
              aria-label="play pause toggle"></button>
            <button class="stop" data-icon="S" aria-label="stop"></button>
            <div class="timer">
              <div></div>
              <span aria-label="timer">00:00</span>
            </div>
            <button class="rwd" data-icon="B" aria-label="rewind"></button>
            <button class="fwd" data-icon="F" aria-label="fast forward"></button>
          </div>
        </div>
        <p>
          Sintel &copy; copyright Blender Foundation |
          <a href="https://studio.blender.org/films/sintel/"
            >studio.blender.org/films/sintel/</a
          >.
        </p>
        <script src="custom-player.js"></script>
      </body>
    </html>
    
  3. 在其中创建另一个名为 style.css 的新文件,并填充以下内容

    css
    @font-face {
      font-family: "HeydingsControlsRegular";
      src: url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot");
      src:
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot?#iefix")
          format("embedded-opentype"),
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.woff")
          format("woff"),
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.ttf")
          format("truetype");
      font-weight: normal;
      font-style: normal;
    }
    
    video {
      border: 1px solid black;
    }
    
    p {
      position: absolute;
      top: 310px;
    }
    
    .player {
      position: absolute;
    }
    
    .controls {
      visibility: hidden;
      opacity: 0.5;
      width: 400px;
      border-radius: 10px;
      position: absolute;
      bottom: 20px;
      left: 50%;
      margin-left: -200px;
      background-color: black;
      box-shadow: 3px 3px 5px black;
      transition: 1s all;
      display: flex;
    }
    
    .player:hover .controls,
    .player:focus-within .controls {
      opacity: 1;
    }
    
    button,
    .controls {
      background: linear-gradient(to bottom, #222222, #666666);
    }
    
    button::before {
      font-family: "HeydingsControlsRegular";
      font-size: 20px;
      position: relative;
      content: attr(data-icon);
      color: #aaaaaa;
      text-shadow: 1px 1px 0px black;
    }
    
    .play::before {
      font-size: 22px;
    }
    
    button,
    .timer {
      height: 38px;
      line-height: 19px;
      box-shadow: inset 0 -5px 25px #0000004d;
      border-right: 1px solid #333333;
    }
    
    button {
      position: relative;
      border: 0;
      flex: 1;
      outline: none;
    }
    
    .play {
      border-radius: 10px 0 0 10px;
    }
    
    .fwd {
      border-radius: 0 10px 10px 0;
    }
    
    .timer {
      line-height: 38px;
      font-size: 10px;
      font-family: monospace;
      text-shadow: 1px 1px 0px black;
      color: white;
      flex: 5;
      position: relative;
    }
    
    .timer div {
      position: absolute;
      background-color: rgb(255 255 255 / 20%);
      left: 0;
      top: 0;
      width: 0;
      height: 38px;
      z-index: 2;
    }
    
    .timer span {
      position: absolute;
      z-index: 3;
      left: 19px;
    }
    
    button:hover,
    button:focus {
      box-shadow: inset 1px 1px 2px black;
    }
    
    button:active {
      box-shadow: inset 3px 3px 2px black;
    }
    
    .active::before {
      color: red;
    }
    
  4. 在目录中创建另一个名为 custom-player.js 的新文件。暂时留空。

此时,如果加载 HTML,你将看到一个完全正常的 HTML 视频播放器,并渲染了原生控件。

探索 HTML

打开 HTML 索引文件。你会看到许多功能;HTML 主要由视频播放器及其控件组成

  • 整个播放器都包裹在一个 <div> 元素中,因此如果需要,可以将其作为一个单元进行样式设置。
  • <video> 元素包含两个 <source> 元素,以便根据查看网站的浏览器加载不同的格式。
  • 控件 HTML 可能是最有趣的
    • 我们有四个 <button>——播放/暂停、停止、快退和快进。
    • 每个 <button> 都有一个 class 名称、一个用于定义每个按钮应显示哪个图标的 data-icon 属性(我们将在下面一节中展示其工作原理),以及一个 aria-label 属性,用于提供每个按钮的易于理解的描述,因为我们没有在标签内提供人类可读的标签。当屏幕阅读器用户聚焦于包含它们的元素时,aria-label 属性的内容会被屏幕阅读器读出。
    • 还有一个计时器 <div>,它将在视频播放时报告已用时间。为了好玩,我们提供了两种报告机制——一个包含以分钟和秒为单位的已用时间的 <span>,以及一个额外的 <div>,我们将用它创建一个水平指示条,随着时间的推移而变长。

探索 CSS

现在打开 CSS 文件并查看内部。示例的 CSS 并不太复杂,但我们在此处重点介绍最有趣的部分。首先,请注意 .controls 样式

css
.controls {
  visibility: hidden;
  opacity: 0.5;
  width: 400px;
  border-radius: 10px;
  position: absolute;
  bottom: 20px;
  left: 50%;
  margin-left: -200px;
  background-color: black;
  box-shadow: 3px 3px 5px black;
  transition: 1s all;
  display: flex;
}

.player:hover .controls,
.player:focus-within .controls {
  opacity: 1;
}
  • 我们首先将自定义控件的 visibility 设置为 hidden。稍后在我们的 JavaScript 中,我们将把控件设置为 visible,并从 <video> 元素中删除 controls 属性。这样,如果 JavaScript 因某种原因未能加载,用户仍然可以使用带有原生控件的视频。
  • 我们默认给控件 opacity 设置为 0.5,这样在观看视频时它们就不会那么分散注意力。只有当你悬停/聚焦在播放器上时,控件才会以完全不透明度显示。
  • 我们使用 Flexbox (display: flex) 在控制栏内布局按钮,使事情变得更容易。

接下来,让我们看看我们的按钮图标

css
@font-face {
  font-family: "HeydingsControlsRegular";
  src: url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot");
  src:
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot?#iefix")
      format("embedded-opentype"),
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.woff")
      format("woff"),
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.ttf")
      format("truetype");
  font-weight: normal;
  font-style: normal;
}

button::before {
  font-family: "HeydingsControlsRegular";
  font-size: 20px;
  position: relative;
  content: attr(data-icon);
  color: #aaaaaa;
  text-shadow: 1px 1px 0px black;
}

首先,在 CSS 的顶部,我们使用 @font-face 块导入自定义网页字体。这是一种图标字体——字母表中的所有字符都等同于你可能希望在应用程序中使用的常见图标。

接下来,我们使用生成的內容在每个按钮上显示一个图标

  • 我们使用 ::before 选择器在每个 <button> 元素之前显示内容。
  • 我们使用 content 属性将每种情况下要显示的内容设置为等于 data-icon 属性的内容。在我们的播放按钮的情况下,data-icon 包含一个大写字母 "P"。
  • 我们使用 font-family 将自定义网页字体应用于我们的按钮。在这种字体中,“P”实际上是一个“播放”图标,因此播放按钮上显示了一个“播放”图标。

图标字体有许多优点——减少 HTTP 请求,因为你不需要将这些图标作为图像文件下载,出色的可伸缩性,以及你可以使用文本属性(如 colortext-shadow)来设置它们的样式。

最后但并非最不重要的一点,让我们看看计时器的 CSS

css
.timer {
  line-height: 38px;
  font-size: 10px;
  font-family: monospace;
  text-shadow: 1px 1px 0px black;
  color: white;
  flex: 5;
  position: relative;
}

.timer div {
  position: absolute;
  background-color: rgb(255 255 255 / 20%);
  left: 0;
  top: 0;
  width: 0;
  height: 38px;
  z-index: 2;
}

.timer span {
  position: absolute;
  z-index: 3;
  left: 19px;
}
  • 我们将外部 .timer 元素设置为 flex: 5,因此它占据了控制栏的大部分宽度。我们还将其设置为 position: relative,以便我们可以根据其边界而不是 <body> 元素的边界方便地定位其中的元素。
  • 内部的 <div> 绝对定位,直接位于外部 <div> 的顶部。它的初始宽度也设置为 0,因此你根本看不到它。当视频播放时,其宽度将通过 JavaScript 随着视频的流逝而增加。
  • <span> 也绝对定位,位于计时器栏的左侧附近。
  • 我们还为内部 <div><span> 提供了适当的 z-index,以便计时器显示在顶部,内部 <div> 显示在下面。这样,我们确保可以看到所有信息——一个框不会遮挡另一个框。

实现 JavaScript

我们已经有了一个相当完整的 HTML 和 CSS 界面;现在我们只需要连接所有按钮以使控件工作。

  1. custom-player.js 文件的顶部,插入以下代码

    js
    const media = document.querySelector("video");
    const controls = document.querySelector(".controls");
    
    const play = document.querySelector(".play");
    const stop = document.querySelector(".stop");
    const rwd = document.querySelector(".rwd");
    const fwd = document.querySelector(".fwd");
    
    const timerWrapper = document.querySelector(".timer");
    const timer = document.querySelector(".timer span");
    const timerBar = document.querySelector(".timer div");
    

    在这里,我们正在创建常量来保存对我们要操作的所有对象的引用。我们有三个组

    • <video> 元素和控制栏。
    • 播放/暂停、停止、快退和快进按钮。
    • 外部计时器包装器 <div>,数字计时器读数 <span>,以及随着时间流逝而变宽的内部 <div>
  2. 接下来,将以下内容插入到代码的底部

    js
    media.removeAttribute("controls");
    controls.style.visibility = "visible";
    

    这两行代码将视频的默认浏览器控件移除,并使自定义控件可见。

播放和暂停视频

让我们实现可能最重要的控件——播放/暂停按钮。

  1. 首先,将以下内容添加到代码底部,以便在单击播放按钮时调用 playPauseMedia() 函数

    js
    play.addEventListener("click", playPauseMedia);
    
  2. 现在定义 playPauseMedia()——再次在代码底部添加以下内容

    js
    function playPauseMedia() {
      if (media.paused) {
        play.setAttribute("data-icon", "u");
        media.play();
      } else {
        play.setAttribute("data-icon", "P");
        media.pause();
      }
    }
    

    在这里我们使用 if 语句检查视频是否已暂停。HTMLMediaElement.paused 属性在媒体暂停时返回 true,这表示视频未播放的任何时间,包括首次加载后持续时间设置为 0 的情况。如果它已暂停,我们将播放按钮上的 data-icon 属性值设置为 "u",这是一个“已暂停”图标,并调用 HTMLMediaElement.play() 方法播放媒体。

    第二次点击时,按钮将再次切换回来——将再次显示“播放”图标,并且视频将通过 HTMLMediaElement.pause() 暂停。

停止视频

  1. 接下来,让我们添加功能来处理停止视频。在之前添加的行下方添加以下 addEventListener()

    js
    stop.addEventListener("click", stopMedia);
    media.addEventListener("ended", stopMedia);
    

    click 事件是显而易见的——我们希望在点击停止按钮时通过运行 stopMedia() 函数来停止视频。但是,当视频播放结束时,我们也希望停止视频——这由 ended 事件触发表示,因此我们也设置了一个监听器,在事件触发时运行该函数。

  2. 接下来,让我们定义 stopMedia()——在 playPauseMedia() 下面添加以下函数

    js
    function stopMedia() {
      media.pause();
      media.currentTime = 0;
      play.setAttribute("data-icon", "P");
    }
    

    HTMLMediaElement API上没有 stop() 方法——等效的方法是 pause() 视频,并将其 currentTime 属性设置为 0。将 currentTime 设置为某个值(以秒为单位)会立即将媒体跳转到该位置。

    之后要做的就是将显示的图标设置为“播放”图标。无论在按下停止按钮时视频是暂停还是播放,你都希望它之后能够播放。

快退和快进

有多种方法可以实现快退和快进功能;这里我们向你展示一种相对复杂的方法,它在以意外顺序按下不同按钮时不会中断。

  1. 首先,在前面添加的行下方添加以下两个 addEventListener()

    js
    rwd.addEventListener("click", mediaBackward);
    fwd.addEventListener("click", mediaForward);
    
  2. 现在转到事件处理程序函数——在你的前一个函数下面添加以下代码来定义 mediaBackward()mediaForward()

    js
    let intervalFwd;
    let intervalRwd;
    
    function mediaBackward() {
      clearInterval(intervalFwd);
      fwd.classList.remove("active");
    
      if (rwd.classList.contains("active")) {
        rwd.classList.remove("active");
        clearInterval(intervalRwd);
        media.play();
      } else {
        rwd.classList.add("active");
        media.pause();
        intervalRwd = setInterval(windBackward, 200);
      }
    }
    
    function mediaForward() {
      clearInterval(intervalRwd);
      rwd.classList.remove("active");
    
      if (fwd.classList.contains("active")) {
        fwd.classList.remove("active");
        clearInterval(intervalFwd);
        media.play();
      } else {
        fwd.classList.add("active");
        media.pause();
        intervalFwd = setInterval(windForward, 200);
      }
    }
    

    你会注意到,我们首先初始化了两个变量——intervalFwdintervalRwd——你稍后会发现它们的作用。

    让我们逐步讲解 mediaBackward()mediaForward() 的功能完全相同,但方向相反)

    1. 我们清除了快进功能上设置的所有类和间隔——我们这样做是因为如果我们在按下 fwd 按钮后按下 rwd 按钮,我们希望取消任何快进功能并将其替换为快退功能。如果同时尝试执行这两者,播放器将崩溃。
    2. 我们使用 if 语句检查 rwd 按钮上是否已设置 active 类,这表明它已被按下。classList 是一个相当方便的属性,存在于每个元素上——它包含元素上设置的所有类的列表,以及用于添加/删除类等的方法。我们使用 classList.contains() 方法检查列表是否包含 active 类。这返回布尔值 true/false 结果。
    3. 如果 rwd 按钮上已设置 active,我们使用 classList.remove() 将其删除,清除首次按下按钮时设置的间隔(有关更多解释,请参阅下文),并使用 HTMLMediaElement.play() 取消快退并开始正常播放视频。
    4. 如果尚未设置,我们使用 classList.add()active 类添加到 rwd 按钮,使用 HTMLMediaElement.pause() 暂停视频,然后将 intervalRwd 变量设置为等于 setInterval() 调用。当调用时,setInterval() 会创建一个活动的间隔,这意味着它每 x 毫秒运行一次作为第一个参数给定的函数,其中 x 是第二个参数的值。因此,我们这里每 200 毫秒运行一次 windBackward() 函数——我们将使用此函数持续倒回视频。要停止 setInterval() 运行,你必须调用 clearInterval(),并给出要清除的间隔的识别名称,在这种情况下是变量名 intervalRwd(请参阅函数中前面部分的 clearInterval() 调用)。
  3. 最后,我们需要定义在 setInterval() 调用中调用的 windBackward()windForward() 函数。在你的两个前一个函数下面添加以下内容

    js
    function windBackward() {
      if (media.currentTime <= 3) {
        rwd.classList.remove("active");
        clearInterval(intervalRwd);
        stopMedia();
      } else {
        media.currentTime -= 3;
      }
    }
    
    function windForward() {
      if (media.currentTime >= media.duration - 3) {
        fwd.classList.remove("active");
        clearInterval(intervalFwd);
        stopMedia();
      } else {
        media.currentTime += 3;
      }
    }
    

    同样,我们将只运行其中第一个函数,因为它们的工作方式几乎相同,但彼此相反。在 windBackward() 中,我们执行以下操作——请记住,当间隔处于活动状态时,此函数每 200 毫秒运行一次。

    1. 我们首先使用一个 if 语句,检查当前时间是否小于 3 秒,即,如果再倒退三秒钟会使其回退到视频的开头。这会导致奇怪的行为,因此如果出现这种情况,我们通过调用 stopMedia() 停止视频播放,从快退按钮中删除 active 类,并清除 intervalRwd 间隔以停止快退功能。如果我们不执行最后一步,视频将永远倒带。
    2. 如果当前时间不在视频开始的 3 秒内,我们通过执行 media.currentTime -= 3 从当前时间中减去 3 秒。因此,实际上,我们每 200 毫秒将视频倒退 3 秒。

更新已用时间

我们媒体播放器要实现的最后一个功能是已用时间显示。为此,我们将在 <video> 元素上每次触发 timeupdate 事件时运行一个函数来更新时间显示。此事件触发的频率取决于你的浏览器、CPU 性能等(请参阅此 Stack Overflow 帖子)。

  1. 在其他行下方添加以下 addEventListener()

    js
    media.addEventListener("timeupdate", setTime);
    
  2. 现在定义 setTime() 函数。将以下内容添加到文件的底部

    js
    function setTime() {
      const minutes = Math.floor(media.currentTime / 60);
      const seconds = Math.floor(media.currentTime - minutes * 60);
    
      const minuteValue = minutes.toString().padStart(2, "0");
      const secondValue = seconds.toString().padStart(2, "0");
    
      const mediaTime = `${minuteValue}:${secondValue}`;
      timer.textContent = mediaTime;
    
      const barLength =
        timerWrapper.clientWidth * (media.currentTime / media.duration);
      timerBar.style.width = `${barLength}px`;
    }
    

这是一个相当长的函数,所以让我们一步一步地进行

  1. 首先,我们计算 HTMLMediaElement.currentTime 值中的分钟和秒数。
  2. 然后我们初始化另外两个变量——minuteValuesecondValue。我们使用 padStart() 使每个值长度为 2 个字符,即使数字值只有一个数字。
  3. 要显示的实际时间值设置为 minuteValue 加上一个冒号字符再加上 secondValue
  4. 计时器的 Node.textContent 值被设置为时间值,因此它会在 UI 中显示。
  5. 内层 <div> 的长度应通过以下方式计算:首先计算外层 <div> 的宽度(任何元素的 clientWidth 属性将包含其长度),然后将其乘以 HTMLMediaElement.currentTime 除以媒体的总 HTMLMediaElement.duration
  6. 我们将内部 <div> 的宽度设置为等于计算出的条形长度,加上“px”,这样它将被设置为该像素数。

修复播放和暂停

还有一个问题需要解决。如果播放/暂停或停止按钮在快退或快进功能处于活动状态时被按下,它们就无法工作。我们如何修复它,使它们取消 rwd/fwd 按钮功能并像你期望的那样播放/停止视频?这很容易修复。

  1. 首先,在 stopMedia() 函数中添加以下几行——任何位置都可以

    js
    rwd.classList.remove("active");
    fwd.classList.remove("active");
    clearInterval(intervalRwd);
    clearInterval(intervalFwd);
    
  2. 现在,在 playPauseMedia() 函数的最开始(就在 if 语句开始之前)再次添加相同的行。

  3. 此时,你可以从 windBackward()windForward() 函数中删除等效的行,因为该功能已在 stopMedia() 函数中实现。

注意:你还可以通过创建一个单独的函数来运行这些行,然后在需要的地方调用它,而不是在代码中多次重复这些行,从而进一步提高代码的效率。但我们将把这个留给你。

总结

我认为我们在这篇文章中已经教你足够了。HTMLMediaElement API 提供了丰富的功能来创建简单的视频和音频播放器,但这仅仅是冰山一角。请参阅下面的“另请参阅”部分,获取指向更复杂和有趣功能的链接。

以下是一些你可以增强我们已构建的现有示例的方法建议

  1. 如果视频时长达到一小时或更长(嗯,它不会显示小时,只会显示分钟和秒),当前时间显示就会中断。你能想出如何修改示例以使其显示小时吗?

  2. 因为 <audio> 元素也具有相同的 HTMLMediaElement 功能,所以你也可以轻松地让这个播放器适用于 <audio> 元素。尝试这样做。

  3. 你能想出一种方法,将计时器内部的 <div> 元素变成一个真正的进度条/滚动条吗——也就是说,当你点击条上的某个位置时,它会跳到视频播放中的那个相对位置?作为提示,你可以通过 getBoundingClientRect() 方法获取元素的左/右和上/下侧的 X 和 Y 值,你还可以通过在 Document 对象上调用的点击事件的事件对象获取鼠标点击的坐标。例如

    js
    document.onclick = function (e) {
      console.log(e.x, e.y);
    };
    

另见