视频和音频 API

HTML 附带了用于在文档中嵌入富媒体的元素(<video><audio>),而这些元素又附带了自己的 API,用于控制播放、搜索等。本文将向你展示如何执行常见任务,例如创建自定义播放控件。

¥HTML comes with elements for embedding rich media in documents — <video> and <audio> — which in turn come with their own APIs for controlling playback, seeking, etc. This article shows you how to do common tasks such as creating custom playback controls.

先决条件: JavaScript 基础知识(参见 第一步架构模块JavaScript 对象)、 客户端 API 基础知识
目标: 了解如何使用浏览器 API 来控制视频和音频播放。

HTML 视频和音频

¥HTML video and audio

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

¥The <video> and <audio> elements allow us to embed video and audio into web pages. As we showed in Video and audio content, a typical implementation looks like this:

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>

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

¥This creates a video player inside the browser like so:

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

¥You can review what all the HTML features do in the article linked above; for our purposes here, the most interesting attribute is controls, which enables the default set of playback controls. If you don't specify this, you get no playback controls:

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

¥This is not as immediately useful for video playback, but it does have advantages. One big issue with the native browser controls is that they are different in each browser — not very good for cross-browser support! Another big issue is that the native controls in most browsers aren't very keyboard-accessible.

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

¥You can solve both these problems by hiding the native controls (by removing the controls attribute), and programming your own with HTML, CSS, and JavaScript. In the next section, we'll look at the basic tools we have available to do this.

HTMLMediaElement API

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

¥Part of the HTML spec, the HTMLMediaElement API provides features to allow you to control video and audio players programmatically — for example HTMLMediaElement.play(), HTMLMediaElement.pause(), etc. This interface is available to both <audio> and <video> elements, as the features you'll want to implement are nearly identical. Let's go through an example, adding features as we go.

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

¥Our finished example will look (and function) something like the following:

入门

¥Getting started

要开始使用此示例,请执行 下载我们的媒体播放器-start.zip 并将其解压缩到硬盘驱动器上的新目录中。如果你 下载我们的示例存储库,你会在 javascript/apis/video-audio/start/ 中找到它。

¥To get started with this example, download our media-player-start.zip and unzip it into a new directory on your hard drive. If you downloaded our examples repo, you'll find it in javascript/apis/video-audio/start/.

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

¥At this point, if you load the HTML you should see a perfectly normal HTML video player, with the native controls rendered.

探索 HTML

¥Exploring the HTML

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

¥Open the HTML index file. You'll see a number of features; the HTML is dominated by the video player and its controls:

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 属性的元素时,屏幕阅读器会读出 aria-label 属性的内容。
    • 还有一个定时器 <div>,它会报告视频播放时经过的时间。只是为了好玩,我们提供了两种报告机制 - <span> 包含以分钟和秒为单位的经过时间,以及额外的 <div>,我们将使用它来创建水平指示条,随着时间的推移,指示条会变得更长。为了了解成品的外观,查看我们的完成版本.

探索 CSS

¥Exploring the CSS

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

¥Now open the CSS file and have a look inside. The CSS for the example is not too complicated, but we'll highlight the most interesting bits here. First of all, notice the .controls styling:

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)在控制栏内布置按钮,以使事情变得更容易。

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

¥Next, let's look at our button icons:

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 块导入自定义 Web 字体。这是一种图标字体 - 字母表中的所有字符都相当于你可能想要在应用中使用的常见图标。

¥First of all, at the top of the CSS we use a @font-face block to import a custom web font. This is an icon font — all the characters of the alphabet equate to common icons you might want to use in an application.

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

¥Next, we use generated content to display an icon on each button:

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

图标字体非常酷的原因有很多 - 减少 HTTP 请求,因为你不需要将这些图标下载为图片文件、出色的可扩展性,以及你可以使用文本属性来设置它们的样式 - 例如 colortext-shadow

¥Icon fonts are very cool for many reasons — cutting down on HTTP requests because you don't need to download those icons as image files, great scalability, and the fact that you can use text properties to style them — like color and text-shadow.

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

¥Last but not least, let's look at the CSS for the timer:

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

¥Implementing the JavaScript

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

¥We've got a fairly complete HTML and CSS interface already; now we just need to wire up all the buttons to get the controls working.

  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";
    
    这两行从视频中删除默认浏览器控件,并使自定义控件可见。

播放和暂停视频

¥Playing and pausing the video

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

¥Let's implement probably the most important control — the play/pause button.

  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 语句来检查视频是否暂停。如果媒体暂停,即视频未播放的任何时间,包括首次加载后将其设置为 0 持续时间时,HTMLMediaElement.paused 属性将返回 true。如果暂停,我们将播放按钮上的 data-icon 属性值设置为 "u",即 "paused" 图标,并调用 HTMLMediaElement.play() 方法来播放媒体。 第二次单击时,该按钮将再次切换回来 - "play" 图标将再次显示,视频将暂停并显示 HTMLMediaElement.pause()

停止视频

¥Stopping the video

  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 设置为一个值(以秒为单位)会立即将介质跳转到该位置。 之后剩下要做的就是将显示的图标设置为 "play" 图标。无论视频是在按下停止按钮时暂停还是播放,你都希望视频随后可以播放。

来回寻找

¥Seeking back and forth

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

¥There are many ways that you can implement rewind and fast-forward functionality; here we are showing you a relatively complex way of doing it, which doesn't break when the different buttons are pressed in an unexpected order.

  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 秒。因此,实际上,我们将视频快退 3 秒,每 200 毫秒一次。

更新经过的时间

¥Updating the elapsed time

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

¥The very last piece of our media player to implement is the time-elapsed displays. To do this we'll run a function to update the time displays every time the timeupdate event is fired on the <video> element. The frequency with which this event fires depends on your browser, CPU power, etc. (see this StackOverflow post).

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

¥Add the following addEventListener() line just below the others:

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

现在定义 setTime() 函数。在文件底部添加以下内容:

¥Now to define the setTime() function. Add the following at the bottom of your file:

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`;
}

这是一个相当长的函数,所以让我们逐步完成它:

¥This is a fairly long function, so let's go through it step by step:

  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",因此它将设置为该像素数。

修复播放和暂停

¥Fixing play and pause

还有一个问题需要解决。如果在快退或快进功能处于活动状态时按下播放/暂停或停止按钮,它们将不起作用。我们如何解决这个问题,以便他们取消 rwd/fwd 按钮功能并按照你的预期播放/停止视频?这很容易解决。

¥There is one problem left to fix. If the play/pause or stop buttons are pressed while the rewind or fast forward functionality is active, they just don't work. How can we fix it so that they cancel the rwd/fwd button functionality and play/stop the video as you'd expect? This is fairly easy to fix.

首先,在 stopMedia() 函数中添加以下几行 - 任何地方都可以:

¥First of all, add the following lines inside the stopMedia() function — anywhere will do:

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

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

¥Now add the same lines again, at the very start of the playPauseMedia() function (just before the start of the if statement).

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

¥At this point, you could delete the equivalent lines from the windBackward() and windForward() functions, as that functionality has been implemented in the stopMedia() function instead.

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

¥Note: You could also further improve the efficiency of the code by creating a separate function that runs these lines, then calling that anywhere it is needed, rather than repeating the lines multiple times in the code. But we'll leave that one up to you.

概括

¥Summary

我想我们在这篇文章中已经教给你足够多的内容了。HTMLMediaElement API 提供了丰富的功能,可用于创建简单的视频和音频播放器,而这只是冰山一角。请参阅下面的 "也可以看看" 部分,获取更复杂和有趣的功能的链接。

¥I think we've taught you enough in this article. The HTMLMediaElement API makes a wealth of functionality available for creating simple video and audio players, and that's only the tip of the iceberg. See the "See also" section below for links to more complex and interesting functionality.

以下是一些增强我们构建的现有示例的建议:

¥Here are some suggestions for ways you could enhance the existing example we've built up:

  1. 目前,如果视频长度为一小时或更长,时间显示就会中断(嗯,它不会显示小时;仅显示分钟和秒)。你能弄清楚如何更改示例以使其显示小时吗?
  2. 由于 <audio> 元素具有相同的 HTMLMediaElement 功能,因此你也可以轻松地让该播放器为 <audio> 元素工作。尝试这样做。
  3. 你能否找到一种方法将计时器内部 <div> 元素变成真正的搜索栏/滚动条 - 即,当你单击栏上的某个位置时,它会跳转到视频播放中的相对位置?提示一下,你可以通过 getBoundingClientRect() 方法找到元素左/右和上/下边的 X 和 Y 值,并且你可以通过单击事件的事件对象找到鼠标单击的坐标,调用 on Document 对象。例如:
    js
    document.onclick = function (e) {
      console.log(e.x, e.y);
    };
    

也可以看看

¥See also