让我们创建一个自定义音频播放器

Avatar of Idorenyin Udoh
Idorenyin Udoh

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

HTML 有一个内置的原生音频播放器界面,我们可以使用<audio>元素轻松获得它。 将其指向一个声音文件,就完成了。 我们甚至可以指定多个文件以获得更好的浏览器支持,以及一些小的 CSS 灵活性来设置样式,例如为音频播放器添加边框、圆角,以及可能的一点填充和边距。

但是即使有这一切……呈现的音频播放器本身看起来可能有点,你知道,平淡。

您是否知道可以创建一个自定义音频播放器? 当然可以! 虽然默认的<audio>播放器在很多情况下都很棒,但自定义播放器可能更适合您,例如,如果您运营一个播客,而音频播放器是播客网站上的关键元素。 看看 Chris 和 Dave 在 ShopTalk Show 网站上设置的酷炫自定义播放器。

Showing a black audio player with muted orange controls, including options to jump or rewind 30 seconds on each side of a giant play button, which sits on top of a timeline showing the audio current time and duration at both ends of the timeline, and an option to set the audio speed in the middle.
音频播放器与页面上的其他元素无缝衔接,并带有补充整体设计的控件。

我们将在本文中尝试制作自己的播放器。 所以,戴上耳机,打开音乐,开始工作吧!

音频播放器的元素

首先,让我们检查一下一些流行浏览器提供的默认 HTML 音频播放器。

Google Chrome, Opera, and Microsoft Edge
Blink
Mozilla Firefox
Firefox
Internet Explorer
Internet Explorer

如果我们的目标是匹配这些示例的功能,那么我们需要确保我们的播放器具有

  • 一个播放/暂停按钮,
  • 一个搜索滑块,
  • 当前时间指示器,
  • 声音文件的持续时间,
  • 一种静音音频的方法,以及
  • 一个音量控制滑块。

假设这是我们想要实现的设计

我们不会追求太花哨的东西:只是概念证明的东西,我们可以用它来演示如何制作与默认 HTML 提供的不同东西。

每个元素的基本标记、样式和脚本

在我们开始构建功能和设置样式之前,我们应该首先浏览播放器的语义 HTML 元素。 基于我们上面列出的元素,我们有很多元素可以使用。

播放/暂停按钮

我认为适合此按钮的 HTML 元素是<button>元素。 它将包含播放图标,但暂停图标也应该在此按钮中。 这样,我们就是在两个图标之间切换,而不是通过同时显示两者来占用空间。

在标记中类似这样的东西

<div id="audio-player-container">
  <p>Audio Player</p>
  <!-- swaps with pause icon -->
  <button id="play-icon"></button>
</div>

所以,问题变成了:我们如何从视觉上和功能上在两个按钮之间切换? 当触发播放操作时,暂停图标将替换播放图标。 播放按钮应该在音频暂停时显示,暂停按钮应该在音频播放时显示。

当然,在图标从播放过渡到暂停时,可能会发生一些动画。 有助于我们实现这一点的是 Lottie,这是一个本地渲染 Adobe After Effects 动画的库。 不过,我们不必在 After Effects 上创建动画。 我们将使用的 动画图标Icons8 免费提供。

不熟悉 Lottie? 我写了一篇全面概述,介绍了它的工作原理。

同时,请允许我描述以下 Pen

HTML 部分包含以下内容

  • 一个播放器的容器,
  • 简要描述容器的文本,以及
  • 一个<button>元素用于播放和暂停操作。

CSS 部分包含一些简单的样式。 JavaScript 是我们需要稍微分解一下的部分,因为它做了很多事情

// imports the Lottie library via Skypack
import lottieWeb from 'https://cdn.skypack.dev/lottie-web';

// variable for the button that will contain both icons
const playIconContainer = document.getElementById('play-icon');
// variable that will store the button’s current state (play or pause)
let state = 'play';

// loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method
const animation = lottieWeb.loadAnimation({
  container: playIconContainer,
  path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/pause/pause.json',
  renderer: 'svg',
  loop: false,
  autoplay: false,
  name: "Demo Animation",
});

animation.goToAndStop(14, true);

// adds an event listener to the button so that when it is clicked, the the player toggles between play and pause
playIconContainer.addEventListener('click', () => {
  if(state === 'play') {
    animation.playSegments([14, 27], true);
    state = 'pause';
  } else {
    animation.playSegments([0, 14], true);
    state = 'play';
  }
});

以下是脚本正在做的事情,减去代码

  • 它通过 Skypack 导入 Lottie 库。
  • 它引用了将包含两个图标的按钮,并将其存储在一个变量中。
  • 它定义了一个变量,用于存储按钮的当前状态(播放或暂停)。
  • 它使用 Lottie 的loadAnimation()方法,将播放图标过渡到暂停图标的动画加载到引用的按钮中。
  • 由于音频最初处于暂停状态,因此它在加载时显示播放图标。
  • 它向按钮添加了一个事件监听器,以便当它被点击时,播放器在播放和暂停之间切换。

当前时间和持续时间

当前时间就像一个进度指示器,向您显示音频文件从开始到目前已过去了多少时间。 持续时间? 那只是声音文件有多长。

一个<span>元素可以用来显示它们。 用于当前时间的<span>元素的默认文本内容为0:00,该元素每秒更新一次。 另一方面,用于持续时间的元素是音频持续时间,以mm:ss格式表示。

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <span id="duration" class="time">0:00</span>
</div>

搜索滑块和音量控制滑块

我们需要一种方法可以移动到声音文件中的任何时间点。 所以,如果我想跳到文件的中间,我只需单击并拖动滑块到时间轴上的那个位置即可。

我们还需要一种控制声音音量的方法。 这也可以是某种单击并拖动的滑块东西。

我认为<input type="range">是适合这两种功能的正确 HTML 元素。

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <input type="range" id="volume-slider" max="100" value="100">
</div>

使用 CSS 设置范围输入的样式是完全可能的,但我告诉你:我很难理解它。 这篇文章会有所帮助。 向您表示敬意,Ana。 使用所有这些供应商前缀来处理浏览器支持本身就是 CSS 技巧。 看一下input[type="range"]上需要的所有代码才能获得一致的体验

input[type="range"] {
  position: relative;
  -webkit-appearance: none;
  width: 48%;
  margin: 0;
  padding: 0;
  height: 19px;
  margin: 30px 2.5% 20px 2.5%;
  float: left;
  outline: none;
}
input[type="range"]::-webkit-slider-runnable-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::before {
  position: absolute;
  content: "";
  top: 8px;
  left: 0;
  width: var(--seek-before-width);
  height: 3px;
  background-color: #007db5;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  position: relative;
  -webkit-appearance: none;
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
  margin: -7px 0 0 0;
}
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.2);
  background: #007db5;
}
input[type="range"]::-moz-range-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::-moz-range-progress {
  background-color: #007db5;
}
input[type="range"]::-moz-focus-outer {
  border: 0;
}
input[type="range"]::-moz-range-thumb {
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
}
input[type="range"]:active::-moz-range-thumb {
  transform: scale(1.2);
  background: #007db5;
}
input[type="range"]::-ms-track {
  width: 100%;
  height: 3px;
  cursor: pointer;
  background: transparent;
  border: solid transparent;
  color: transparent;
}
input[type="range"]::-ms-fill-lower {
  background-color: #007db5;
}
input[type="range"]::-ms-fill-upper {
  background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width));
}
input[type="range"]::-ms-thumb {
  box-sizing: content-box;
  border: 1px solid #007db5;
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: #fff;
  cursor: pointer;
}
input[type="range"]:active::-ms-thumb {
  transform: scale(1.2);
  background: #007db5;
}

哇! 这到底是什么意思?

设置范围输入的进度部分的样式是一项棘手的任务。 Firefox 提供了::-moz-range-progress伪元素,而 Internet Explorer 提供了::-ms-fill-lower。 由于 WebKit 浏览器没有提供任何类似的伪元素,因此我们必须使用::before伪元素来临时模拟进度。 这就解释了为什么,如果您注意到,我在 JavaScript 部分添加了事件监听器,以便在每个滑块上触发输入事件时设置自定义 CSS 属性(例如--before-width)。

我们之前查看过的原生 HTML<audio>示例之一显示了音频的缓冲量。 --buffered-width属性指定了用户可以通过该属性播放音频的百分比,而不必等待浏览器下载。 我使用搜索滑块轨道上的linear-gradient()函数模仿了此功能。 我在linear-gradient()函数中的颜色停止处使用了rgba()函数来显示透明度。 缓冲宽度与轨道其余部分相比具有更深的颜色。 但是,我们将在稍后处理此功能的实际实现。

音量百分比

这是用来显示音量百分比。 当用户通过滑块更改音量时,该元素的文本内容会更新。 由于它基于用户输入,我认为这个元素应该是<output>元素。

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <output id="volume-output">100</output>
  <input type="range" id="volume-slider" max="100" value="100">
</div>

静音按钮

就像播放和暂停操作一样,这应该在一个<button>元素中。 对我们来说幸运的是,Icons8 也有一个 动画静音图标。 因此,我们将像使用播放/暂停按钮一样在这里使用 Lottie 库。

<div id="audio-player-container">
  <p>Audio Player</p>
  <button id="play-icon"></button>
  <span id="current-time" class="time">0:00</span>
  <input type="range" id="seek-slider" max="100" value="0">
  <span id="duration" class="time">0:00</span>
  <output id="volume-output">100</output>
  <input type="range" id="volume-slider" max="100" value="100">
  <button id="mute-icon"></button>
</div>

目前,这就是我们需要的所有基本标记、样式和脚本!

实现功能

HTML 的 `<audio>` 元素有一个 `preload` 属性。该属性为浏览器提供有关如何加载音频文件的指令。它接受三个值之一

  • none – 表示浏览器不应加载音频(除非用户启动播放操作)
  • metadata – 表示仅加载音频的元数据(如长度)
  • auto – 加载完整的音频文件

空字符串等效于 `auto` 值。但是,请注意,这些值仅仅是浏览器的 *提示*。浏览器不必同意这些值。例如,如果用户在 iOS 上的蜂窝网络上,Safari 不会加载音频的任何部分,无论 `preload` 属性如何,除非用户触发播放操作。对于此播放器,我们将使用 `metadata` 值,因为它不需要太多开销,并且我们希望显示音频的长度。

帮助我们实现音频播放器应具有的功能的是 JavaScript 的 `HTMLMediaElement` 接口,`HTMLAudioElement` 接口继承自该接口。为了使我们的音频播放器代码尽可能地自解释,我将 JavaScript 分为两个部分:演示和功能。

首先,我们应该在音频播放器中创建一个具有我们想要的基本功能的 `<audio>` 元素

<div id=”audio-player-container”>
  <audio src=”my-favourite-song.mp3” preload=”metadata” loop>
  <button id="play-icon"></button>
  <!-- ... -->
</div>

显示音频时长

我们想要在浏览器上显示的第一件事是音频的时长,在它可用时。`HTMLAudioElement` 接口有一个 `duration` 属性,它返回音频的时长,以秒为单位。如果不可用,它将返回 `NaN`。

在本例中,我们将 `preload` 设置为 `metadata`,因此浏览器应该在加载时为我们提供该信息……假设它尊重 `preload`。由于我们可以确定时长将在浏览器下载音频元数据时可用,因此我们在 `loadedmetadata` 事件的处理程序中显示它,该事件也是该接口提供的

const audio = document.querySelector('audio');

audio.addEventListener('loadedmetadata', () => {
  displayAudioDuration(audio.duration);
});

这很好,但是,我们再次以秒为单位获得时长。我们可能应该将其转换为 `mm:ss` 格式

const calculateTime = (secs) => {
  const minutes = Math.floor(secs / 60);
  const seconds = Math.floor(secs % 60);
  const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
  return `${minutes}:${returnedSeconds}`;
}

我们使用 `Math.floor()`,因为 `duration` 属性返回的秒数通常以小数形式出现。

第三个变量 `returnedSeconds` 是为了处理时长为 4 分钟 8 秒的情况而必需的。我们希望返回 4:08,而不是 4:8。

通常情况下,浏览器加载音频的速度比平时快。发生这种情况时,`loadedmetadata` 事件会在其监听器被添加到 `<audio>` 元素之前被触发。因此,音频时长不会显示在浏览器上。然而,有一个解决方法。`HTMLMediaElement` 有一个名为 `readyState` 的属性。它返回一个数字,根据 MDN Web 文档,表示媒体的准备状态。以下描述了这些值

  • 0 – 无法获取有关媒体的数据。
  • 1 – 媒体的元数据属性可用。
  • 2 – 数据可用,但不足以播放超过一帧。
  • 3 – 数据可用,但仅用于当前播放位置的少量帧。
  • 4 – 数据可用,这样媒体就可以不间断地播放到最后。

我们想要关注元数据。因此,我们的方法是在音频的元数据可用时显示时长。如果不可用,我们会添加事件监听器。这样,时长始终会显示。

const audio = document.querySelector('audio');
const durationContainer = document.getElementById('duration');

const calculateTime = (secs) => {
  const minutes = Math.floor(secs / 60);
  const seconds = Math.floor(secs % 60);
  const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
  return `${minutes}:${returnedSeconds}`;
}

const displayDuration = () => {
  durationContainer.textContent = calculateTime(audio.duration);
}

if (audio.readyState > 0) {
  displayDuration();
} else {
  audio.addEventListener('loadedmetadata', () => {
    displayDuration();
  });
}

拖动滑块

范围滑块的 `max` 属性的默认值为 100。一般来说,当音频正在播放时,滑块应该是“滑动的”。此外,它应该每秒移动一次,这样在音频结束时它就会到达滑块的末尾。

但是,如果音频时长为 150 秒,而滑块的 `max` 属性值为 100,则滑块将在音频结束之前到达滑块的末尾。这就是为什么有必要将滑块的 `max` 属性值设置为音频时长(以秒为单位)。这样,滑块就会在音频结束时到达滑块的末尾。请记住,这应该是音频时长可用时,浏览器已下载音频元数据时,如下所示

const seekSlider = document.getElementById('seek-slider');

const setSliderMax = () => {
  seekSlider.max = Math.floor(audio.duration);
}

if (audio.readyState > 0) {
  displayDuration();
  setSliderMax();
} else {
  audio.addEventListener('loadedmetadata', () => {
    displayDuration();
    setSliderMax();
  });
}

缓冲量

随着浏览器下载音频,用户可以很清楚地知道他们可以无延迟地跳转到多少音频。`HTMLMediaElement` 接口提供了 `buffered` 和 `seekable` 属性。`buffered` 属性返回一个 `TimeRanges` 对象,该对象指示浏览器已下载的媒体块。根据 MDN Web 文档,`TimeRanges` 对象是一系列不重叠的时间范围,具有开始时间和结束时间。这些块通常是连续的,除非用户跳转到媒体中的另一个部分。`seekable` 属性返回一个 `TimeRanges` 对象,该对象指示媒体的“可跳转”部分,无论它们是否已下载。

请记住,`preload="metadata"` 属性存在于我们的 `<audio>` 元素中。例如,如果音频时长为 100 秒,`buffered` 属性将返回一个类似于以下内容的 `TimeRanges` 对象

Displays a line from o to 100, with a marker at 20. A range from 0 to 20 is highlighted on the line.

当音频开始播放时,`seekable` 属性将返回一个类似于以下内容的 `TimeRanges` 对象

Another timeline, but with four different ranges highlighted on the line, 0 to 20, 30 to 40, 60 to 80, and 90 to 100.

它返回多个媒体块,因为通常情况下,服务器上启用了 字节范围请求。这意味着媒体的多个部分可以同时下载。但是,我们希望显示最接近当前播放位置的缓冲量。这将是第一个块(时间范围为 0 到 20)。这将是第一张图像中的第一个和最后一个块。当音频开始播放时,浏览器开始下载更多块。我们希望显示最接近当前播放位置的块,这将是 `buffered` 属性返回的当前最后一个块。以下代码片段将存储在变量 `bufferedAmount` 中,即 `buffered` 属性返回的 `TimeRanges` 对象中最后一个范围的结束时间。

const audio = document.querySelector('audio');
const bufferedAmount = audio.buffered.end(audio.buffered.length - 1);

这将是第一张图像中 0 到 20 范围内的 20。以下代码片段将存储在变量 `seekableAmount` 中,即 `seekable` 属性返回的 `TimeRanges` 对象中最后一个范围的结束时间。

const audio = document.querySelector('audio');
const seekableAmount = audio.seekable.end(audio.seekable.length - 1);

但是,这将是第二张图像中 90 到 100 范围内的 100,也就是整个音频时长。请注意,`TimeRanges` 对象中存在一些空洞,因为浏览器只下载音频的某些部分。这意味着整个时长将显示给用户作为缓冲量。同时,音频中的某些部分尚不可用。由于这不会提供最佳的用户体验,因此我们应该使用第一个代码片段。

随着浏览器下载音频,用户应该期望滑块上的缓冲量宽度增加。`HTMLMediaElement` 提供了一个事件,即进度事件,当浏览器加载媒体时触发。当然,我正在想你正在想什么!缓冲量应该在音频进度事件的处理程序中递增。

最后,我们应该在拖动滑块上实际显示缓冲量。我们通过设置之前谈论过的属性 `--buffered-width` 来实现,该属性是滑块的 `max` 属性值的百分比。是的,也在进度事件的处理程序中。此外,由于浏览器加载音频的速度比平时快,因此我们应该在 `loadedmetadata` 事件及其前面的检查音频准备状态的条件块中更新该属性。以下 Pen 结合了我们迄今为止介绍的所有内容

当前时间

当用户沿范围输入滑动滑块时,范围值应该反映在包含音频当前时间的 `<span>` 元素中。这告诉用户音频的当前播放位置。我们在滑块的输入事件监听器的处理程序中执行此操作。

如果你认为要监听的正确事件应该是 change 事件,我不同意。假设用户将滑块从值 0 移动到 20。输入事件会在值 1 到 20 时触发。但是,change 事件只会

我们创建了这个函数,它接收以秒为单位的时间,并将其转换为 `mm:ss` 格式。如果你在想,“哦,但滑块的值不是以秒为单位的时间”,让我解释一下。实际上,它是。回想一下,我们设置了滑块的 `max` 属性的值为音频的时长,当音频可用时。假设音频时长为 100 秒。如果用户将滑块的拇指滑到滑块的中间,滑块的值将为 50。我们不希望 50 出现在当前时间框中,因为它不符合 `mm:ss` 格式。当我们将 50 传递给该函数时,该函数返回 0:50,这将更好地表示播放位置。

我在我们的 JavaScript 代码中添加了下面的代码片段。

const currentTimeContainer = document.getElementById('current-time');

seekSlider.addEventListener('input', () => {
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
});

要查看它的实际效果,你可以在以下 Pen 中来回移动寻求滑块的拇指

播放/暂停

现在我们将根据用户触发的相应操作来设置音频播放或暂停。如果你还记得,我们创建了一个名为 `playState` 的变量来存储按钮的状态。该变量将帮助我们了解何时播放或暂停音频。如果它的值为 `play` 并且点击了按钮,我们的脚本预计将执行以下操作

  • 播放音频
  • 将图标从播放更改为暂停
  • 将 `playState` 值更改为 `pause`

我们已经在按钮的点击事件处理程序中实现了第二和第三个操作。我们需要做的是在事件处理程序中添加播放和暂停音频的语句

playIconContainer.addEventListener('click', () => {
  if(playState === 'play') {
    audio.play();
    playAnimation.playSegments([14, 27], true);
    playState = 'pause';
  } else {
    audio.pause();
    playAnimation.playSegments([0, 14], true);
    playState = 'play';
  }
});

用户可能想要跳到音频中的特定部分。在这种情况下,我们将音频的 `currentTime` 属性的值设置为寻求滑块的值。滑块的更改事件将在此处派上用场。如果我们使用输入事件,音频的各个部分将在很短的时间内播放。

回想一下我们从 1 到 20 的值的场景。现在想象一下用户将拇指从 1 滑到 20,比如在两秒钟内。也就是说,20 秒的音频在两秒钟内播放。这就像以 3 倍的速度听 Busta Rhymes 的歌。我建议我们使用 `change` 事件。只有在用户完成寻求后,音频才会播放。这就是我要说的

seekSlider.addEventListener('change', () => {
  audio.currentTime = seekSlider.value;
});

除此之外,还需要在音频播放时做一些事情。即,将滑块的值设置为音频的当前时间。或者每秒将滑块的拇指移动一个刻度。由于音频时长与滑块的 `max` 值相同,当音频结束时,拇指会到达滑块的末端。现在,`HTMLMediaElement` 接口的 `timeupdate` 事件应该适合这种情况。当媒体的 `currentTime` 属性的值更新时,此事件会被触发,大约每秒触发四次。因此,在该事件的处理程序中,我们可以将滑块的值设置为音频的当前时间。这应该可以正常工作

audio.addEventListener('timeupdate', () => {
  seekSlider.value = Math.floor(audio.currentTime);
});

但是,这里有一些需要注意的地方

  1. 当音频正在播放,并且寻求滑块的值正在更新时,用户无法与滑块进行交互。如果音频暂停,滑块将无法从用户那里接收输入,因为它一直在不断更新。
  2. 在处理程序中,我们更新了滑块的值,但它的输入事件没有触发。这是因为该事件仅在用户在浏览器中更新滑块的值时才会触发,而不会在它以编程方式更新时触发。

让我们考虑第一个问题。

为了能够在音频播放时与滑块进行交互,我们必须在它接收到输入时暂停更新其值的进程。然后,当滑块失去焦点时,我们恢复该进程。但是,我们无法访问此进程。我的解决方法是使用 `requestAnimationFrame()` 全局方法来完成此过程。但这次我们不会为此使用 `timeupdate` 事件,因为它仍然不起作用。动画将永远播放,直到音频暂停,而这不是我们想要的。因此,我们使用播放/暂停按钮的点击事件。

要为此功能使用 `requestAnimationFrame()` 方法,我们必须完成以下步骤

  1. 创建一个函数来保存我们的“更新滑块值”语句。
  2. 在先前创建的函数中初始化一个变量来存储该函数返回的请求 ID(这将用于暂停更新进程)。
  3. 在播放/暂停按钮点击事件处理程序中添加语句,以便分别在相应的块中启动和暂停该进程。

以下代码片段说明了这一点

let rAF = null;

const whilePlaying = () => {
  seekSlider.value = Math.floor(audio.currentTime);
  rAF = requestAnimationFrame(whilePlaying);
}

playIconContainer.addEventListener('click', () => {
  if(playState === 'play') {
    audio.play();
    playAnimation.playSegments([14, 27], true);
    requestAnimationFrame(whilePlaying);
    playState = 'pause';
  } else {
    audio.pause();
    playAnimation.playSegments([0, 14], true);
    cancelAnimationFrame(rAF);
    playState = 'play';
  }
});

但这并不能完全解决我们的问题。只有在音频暂停时,该进程才会暂停。如果该进程正在执行(即如果音频正在播放),并且用户想要与滑块进行交互,我们也需要暂停该进程。然后,在滑块失去焦点后,如果该进程之前正在进行(即如果音频正在播放),我们重新启动该进程。为此,我们将使用滑块的输入事件处理程序来暂停该进程。要重新启动该进程,我们将使用 `change` 事件,因为该事件在用户完成滑动拇指后触发。以下是实现

seekSlider.addEventListener('input', () => {
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
  if(!audio.paused) {
    cancelAnimationFrame(raf);
  }
});

seekSlider.addEventListener('change', () => {
  audio.currentTime = seekSlider.value;
  if(!audio.paused) {
    requestAnimationFrame(whilePlaying);
  }
});

我能够为第二个问题想出一些办法。我在寻求滑块的输入事件处理程序中添加了语句,并将它们添加到 `whilePlaying()` 函数中。回想一下,寻求滑块的输入事件有两个事件监听器:一个用于演示,另一个用于功能。在添加了处理程序中的两个语句之后,我们的 `whilePlaying()` 函数如下所示

const whilePlaying = () => {
  seekSlider.value = Math.floor(audio.currentTime);
  currentTimeContainer.textContent = calculateTime(seekSlider.value);
  audioPlayerContainer.style.setProperty('--seek-before-width', `${seekSlider.value / seekSlider.max * 100}%`);
  raf = requestAnimationFrame(whilePlaying);
}

请注意,第四行中的语句是我们之前在演示部分创建的 `showRangeProgress()` 函数中寻求滑块的相应语句。

现在我们只剩下音量控制功能了。呼!但在我们开始着手处理它之前,这里有一个涵盖了我们迄今为止所有操作的 Pen

音量控制

对于音量控制,我们使用第二个滑块 `#volume-slider`。当用户与滑块进行交互时,滑块的值会反映在音频的音量和我们之前创建的 `` 元素中。

滑块的 `max` 属性的默认值为 100。这使得在它更新时很容易在 `` 元素中显示它的值。我们可以在滑块的输入事件处理程序中实现这一点。但是,要在音频的音量中实现这一点,我们必须做一些数学运算。

`HTMLMediaElement` 接口提供了一个 `volume` 属性,它返回一个介于 0 和 1 之间的数值,其中 1 是最大音量。这意味着如果用户将滑块的值设置为 50,我们必须将音量属性设置为 0.5。由于 0.5 是 50 的百分之一,我们可以将音量设置为滑块值的百分之一。

const volumeSlider = document.getElementById('volume-slider');
const outputContainer = document.getElementById('volume-output');

volumeSlider.addEventListener('input', (e) => {
  const value = e.target.value;

  outputContainer.textContent = value;
  audio.volume = value / 100;
});

还不错吧?

静音音频

接下来是扬声器图标,点击它可以静音和取消静音音频。要静音音频,我们将使用它的 `muted` 属性,该属性也可以通过 `HTMLMediaElement` 作为布尔类型使用。它的默认值为 `false`,即未静音。要静音音频,我们将该属性设置为 `true`。如果你还记得,我们向扬声器图标添加了一个点击事件监听器来进行演示(Lottie 动画)。要静音和取消静音音频,我们应该将语句添加到该处理程序中的相应条件块中,如下所示

const muteIconContainer = document.getElementById('mute-icon');

muteIconContainer.addEventListener('click', () => {
  if(muteState === 'unmute') {
    muteAnimation.playSegments([0, 15], true);
    audio.muted = true;
    muteState = 'mute';
  } else {
    muteAnimation.playSegments([15, 25], true);
    audio.muted = false;
    muteState = 'unmute';
  }
});

完整演示

这是我们自定义音频播放器的完整演示,展现了它的全部魅力!

但在我们结束之前,我想介绍一些东西——一些东西可以让用户访问媒体播放,而不仅仅是自定义音频播放器所在的浏览器标签页。

请允许我隆重介绍,请敲响鼓声…

媒体会话 API

基本上,这个 API 允许用户暂停、播放和/或执行其他媒体播放操作,但不能使用我们的音频播放器。根据设备或浏览器,用户通过通知区域、媒体中心或浏览器或操作系统提供的任何其他界面来启动这些操作。 我有一篇关于这方面的文章,你可以参考它来了解更多信息。

以下 Pen 包含媒体会话 API 的实现

如果你在手机上查看此 Pen,请偷偷查看通知区域。如果你在电脑上的 Chrome 浏览器上,请查看媒体中心。如果你的智能手表已配对,我建议你查看它。你也可以告诉你的语音助手对音频执行一些操作。我敢说,这会让你露出微笑。🤓

还有一件事…

如果你需要在网页上使用音频播放器,那么该网页很可能包含其他内容。这就是为什么我认为将音频播放器及其所有所需代码分组到一个 Web 组件中非常明智的原因。这样,网页就拥有了一种关注点分离的形式。我把我们所做的一切都转移到了一个 Web 组件中,并得到了以下结果

总结来说,我认为使用 `HTMLMediaElement` 接口可以创建无限的媒体播放器。它提供了许多用于各种功能的属性和方法。还有媒体会话 API,可提供更丰富的体验。

俗话说得好,能力越大,责任越大,对吧?想想我们为了最终实现一个简陋的自定义音频播放器而不得不考虑的所有各种控件、元素和边缘情况。这足以证明音频播放器不仅仅是播放和暂停。正确地制定功能需求无疑将有助于提前规划代码并节省大量时间。