视频和音频 API

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

先决条件 JavaScript 基础知识(请参阅第一步构建块JavaScript 对象),以及客户端 API 的基础知识
目标 学习如何使用浏览器 API 控制视频和音频播放。

HTML 视频和音频

使用 <video><audio> 元素,我们可以将视频和音频嵌入网页中。正如我们在视频和音频内容中展示的那样,一个典型的实现如下所示

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> 元素,因为您需要实现的功能几乎相同。让我们来看一个示例,在过程中添加功能。

我们完成的示例将如下所示(并具有以下功能)

入门

要开始此示例,请下载我们的 media-player-start.zip 并将其解压缩到硬盘驱动器上的新目录中。如果您下载了我们的示例存储库,您将在 javascript/apis/video-audio/start/ 中找到它。

此时,如果您加载 HTML,您应该会看到一个完全正常的 HTML 视频播放器,并呈现原生控件。

探索 HTML

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

html
<div class="player">
  <video controls>
    <source src="video/sintel-short.mp4" type="video/mp4" />
    <source src="video/sintel-short.webm" type="video/webm" />
    <!-- fallback content here -->
  </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>
  • 整个播放器都包装在一个 <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 由于某种原因未加载,用户仍然可以使用原生控件来使用视频。
  • 默认情况下,我们为控件提供 0.5 的 opacity,以便在您尝试观看视频时,它们不会太分散注意力。只有当您将鼠标悬停/聚焦在播放器上时,控件才会以完全不透明度显示。
  • 我们使用 flexbox (display: flex) 布局控件栏内的按钮,以简化操作。

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

css
@font-face {
  font-family: "HeydingsControlsRegular";
  src: url("fonts/heydings_controls-webfont.eot");
  src:
    url("fonts/heydings_controls-webfont.eot?#iefix")
      format("embedded-opentype"),
    url("fonts/heydings_controls-webfont.woff") format("woff"),
    url("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: #aaa;
  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. 在与 index.html 文件相同的目录级别创建一个新的 JavaScript 文件。将其命名为 custom-player.js
  2. 在此文件的顶部,插入以下代码
    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>
  3. 接下来,在代码底部插入以下内容
    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 秒,即,如果再快退 3 秒是否会将其快退到视频的开头之前。这会导致奇怪的行为,因此,如果是这种情况,我们通过调用 stopMedia() 停止视频播放,从快退按钮中删除 active 类,并清除 intervalRwd 间隔以停止快退功能。如果我们不执行此最后一步,视频将一直快退。
    2. 如果当前时间距离视频的开头不到 3 秒,我们通过执行 media.currentTime -= 3 从当前时间中减去 3 秒。因此,实际上,我们每 200 毫秒快退视频 3 秒。

更新已用时间

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

在其他代码下方添加以下 addEventListener() 代码行

js
media.addEventListener("timeupdate", setTime);

现在定义 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 按钮功能并按预期播放/停止视频?这很容易修复。

首先,在 stopMedia() 函数内部添加以下几行 - 任何位置都可以

js
rwd.classList.remove("active");
fwd.classList.remove("active");
clearInterval(intervalRwd);
clearInterval(intervalFwd);

现在再次添加相同的行,在 playPauseMedia() 函数的开头(在 if 语句的开头之前)。

此时,您可以从 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);
    };
    

另请参阅