怎么用 JavaScript 构建自定义的 HTML5 视频播放器

2,680 阅读17分钟

在网页中观看和分享视频内容是一个很常见的功能,多年来,视频嵌入网页的方式发生了变化。现在,我们在现代浏览器中使用 <video> 标签就可以添加视频文件到网页上,该标签支持多个视频格式。

video_element.webp

当使用 <video> 标签时的主要警告是渲染的视频播放器会因浏览器而异,如果你想提供一致的用户体验,使用原生操作并不理想。这就是为什么构建自定义控件而不是使用浏览器默认界面很有用的原因。

在这个教程中,我将会带你使用 JavaScript 构建一个自定义的视频播放器。目标是如何利用浏览器 HTML5 Media API 来提升默认设置的体验。

我们将在本教程中构建一个看起来像 YouTube 视频播放器,因为我认为复制大多数人已经熟悉的一些功能是个好主意。

当然,我们并不会实现 YouTube 播放器上的所有功能,因为这会让教程更长、更复杂。然而,一旦你完成了本教程,我相信你能够很轻松地加入新的功能。

你可以查看我们将构建的线上案例,或者在 GitHub 上查看源码。

准备条件

你需要对 JavaScriptDOM 有基本的了解,才能继续学习本教程。我推荐你使用最新版本的谷歌浏览器,因为在本文编写时,我们将添加的一些功能(比如画中画功能)仅适用于谷歌(Webkit 内核)浏览器。

开始

我在 GitHub 中为本教程准备了开始文件。有需要的话,你可以克隆到自己的机器上,并在编辑器中打开。你将分别在 index.htmlstyle.css 中找到播放器的标记文档文件及其样式,以及我们用来测试播放器的视频文件。index.js 将是我们添加播放器工作所需的所有 JavaScript 代码的地方。

在终端中运行 npm install 来安装 browser-sync 作为启动 Web 服务器的开发依赖项,其在任何文件更改时自动刷新页面。然后 npm start 启动项目,监听浏览器 http://localhost:3000

目前都做了些什么

现在,视频播放器保留本机浏览器控件,正如你所期待那样工作。自定义控件已经被定义在 #video-controls 元素,但是它们被隐藏了。

<!-- index.html -->
. . .
<div class="video-controls hidden" id="video-controls">
<!-- Custom controls are defined here -->
</div>
. . .

即使我们要为控件实现自定义界面,保留 <video> 元素上的 controls 属性是个很好的主意,这样用户不管出于什么原因禁用 JavaScript,浏览器本机的控件依旧可使用。对于其他人,本机空间可以轻松隐藏并替换成自定义控件,这稍后进行演示。

海报图像已经添加到视频中,设置 preload属性值为 metadata,这指示浏览器仅获取视频元数据(比如 duration)。为了让事情简单点,我们只添加 MP4 类型的视频源文件,因为该类型的视频被所有主流浏览器兼容,是一个非常安全的默认值。有关视频格式和浏览器兼容性的更多信息,可参考该文档

<!-- index.html -->
. . .
<video controls class="video" id="video" preload="metadata" poster="poster.jpg">
  <source src="video.mp4" type="video/mp4"></source>
</video>
. . .

隐藏自带控件

我们首先需要做的事情是在确认浏览器支持 HTML5 视频后,隐藏默认视频控件并提供我们自己的界面。在你的 index.js 文件中输入下面代码片段来实现上面的功能:

// index.js
// Select elements here
const video = document.getElementById('video');
const videoControls = document.getElementById('video-controls');

const videoWorks = !!document.createElement('video').canPlayType;
if (videoWorks) {
  video.controls = false;
  videoControls.classList.remove('hidden');
}

canPlayType 属性是我们检查浏览器对视频格式支持的方式。要使用它,我们需要创建 <video> 元素的实例并将检查是否支持 canPlayType。如果支持,则可以安全地假设其支持 HTML 视频,然后禁用默认控件,启用我们自定义的控件。

canPlayType_support.webp

默认控件已经被替换成自定义控件

切换播放状态

让我们从基础开始。我们需要通过点击播放按钮来播放或者暂停视频,并且更改应该匹配视频状态的图标。我们从获取视频和播放按钮开始,代码在 index.js 顶部,如下:

// index.js
const playButton = document.getElementById('play');

然后,我们创建一个函数来切换视频播放状态:

// index.js
// Add functions here

// togglePlay toggles the playback state of the video.
// If the video playback is paused or ended, the video is played
// 如果视频播放是暂停或者视频结尾状态,视频播放
// otherwise, the video is paused
// 否则,视频暂停
function togglePlay() {
  if (video.paused || video.ended) {
    video.play();
  } else {
    video.pause();
  }
}

最后,我们创建一个时间监听器,当 playButton 按钮被点击后执行 togglePlay 方法。

// index.js
// Add eventlisteners here
playButton.addEventListener('click', togglePlay);

够简单吧?通过点击浏览器中的播放按钮对其测试。它应该正确地播放和暂停视频。

appropriately_play_and_stop_video.webm.gif

这实际上为本教程的其他部分定下了基调。我们通常会选择一个视频控件,创建一个实现特定功能的函数,通过事件监听器将其连接起来。

我们继续,根据视频状态更新播放按钮。下面是 playButtonHTML 文件:

<!-- index.html -->
. . .
<button data-title="Play (k)" id="play">
  <svg class="playback-icons">
    <use href="#play-icon"></use>
    <use class="hidden" href="#pause"></use>
  </svg>
</button>
. . .

<svg> 元素中,我们有播放和暂停按钮,但是一次我们只能展示其中一个,另一个则隐藏。现在我们要做的就是切换每个图标的 hidden 类,以便根据视频的状态展示正确的图标。

首先,在 index.js 文件顶部选择图标:

// index.js
const playbackIcons = document.querySelectorAll('.playback-icons use');

接着,在 togglePlay 函数下创建一个函数,用来更新播放按钮:

// index.js
// updatePlayButton updates the playback icon and tooltip
// depending on the playback state
// 根据播放状态,updatePlayButton 函数更新播放图标和提示
function updatePlayButton() {
  playbackIcons.forEach(icon => icon.classList.toggle('hidden'));
}

最后,在文件底部添加如下事件监听器:

// index.js
video.addEventListener('play', updatePlayButton);
video.addEventListener('pause', updatePlayButton);

当视频播放或者暂停时,updatePlayButton 函数都会被执行,切换每个按钮中的 hidden 类。因为暂停按钮元素默认值是 hidden 类,一旦视频被播放,这个暂停图标出现,播放图标将会隐藏。如果视频被暂停,则会发生相反的情况。你可以在自己浏览器上测试。

额外要做的事情是,当鼠标移动到播放按钮上,需要更新展示的提示文本。默认提示是 play(k),但是当视频正在播放,需要更新提示信息为 pause(k)k 是我们将在本教程后面添加播放或者暂停视频的键盘快捷键。

如下,更新 updatePlayButton 函数:

// index.js
function updatePlayButton() {
  playbackIcons.forEach(icon => icon.classList.toggle('hidden'));

  if (video.paused) {
    playButton.setAttribute('data-title', 'Play (k)')
  } else {
    playButton.setAttribute('data-title', 'Pause (k)')
  }
}

当视频正在播放或者暂停时,鼠标移动到按钮上,应该设置正确的提示文本。

如果你想知道提示信息是怎么展示的,可以看下相关的 CSS

// style.css
. . .
button::before {
  content: attr(data-title);
  position: absolute;
  display: none;
  right: 0;
  top: -50px;
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-weight: bold;
  padding: 4px 6px;
  word-break: keep-all;
  white-space: pre;
}

button:hover::before {
  display: inline-block;
}
. . .

video-tooltip.gif

展示视频持续时间和经过时间

展示视频时长很必要,因为这是用户首先想看到的,所以我们接下来将讲解。

下面是持续时长和经过时间的元素标记:

<!-- index.html -->
<div class="time">
  <time id="time-elapsed">00:00</time>
  <span> / </span>
  <time id="duration">00:00</time>
</div>

通过 index.js 选择这两个控件(元素),如下:

// index.js
const timeElapsed = document.getElementById('time-elapsed');
const duration = document.getElementById('duration');

一旦页面加载完成后,我们将使用 duration 属性展示视频的总时长。这个属性表示的是视频的总秒数,所以在展示之前,我们需要将其转换成分秒。我们创建一个 formatTime 函数,将时间转换成分秒:

// index.js
// formatTime takes a time length in seconds and returns the time in
// minutes and seconds
function formatTime(timeInSeconds) {
  const result = new Date(timeInSeconds * 1000).toISOString().substr(11, 8);

  return {
    minutes: result.substr(3, 2),
    seconds: result.substr(6, 2),
  };
};

接着,我们在 formatTime 函数下创建 initializeVideo 函数:

// index.js
// initializeVideo sets the video duration, and maximum value of the
// progressBar
function initializeVideo() {
  const videoDuration = Math.round(video.duration);
  const time = formatTime(videoDuration);
  duration.innerText = `${time.minutes}:${time.seconds}`;
  duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}

如上所示,视频持续时长被四舍五入,格式化为分秒,然后在屏幕上更新。datetime 同步更新为时间字符串,表示视频持续时长。

接着,如下所示,让我们将 initializeVideo 函数连接到 loadedmetadata 监听器上。当元数据被加载之后,将会更新视频的持续时长。

// index.js
video.addEventListener('loadedmetadata', initializeVideo);

video-duration.webp

同理,当视频播放过程中,我们更新播放经过的时间。下面的函数能帮我们实现这个功能:

// index.js
// updateTimeElapsed indicates how far through the video
// the current playback is
function updateTimeElapsed() {
  const time = formatTime(Math.round(video.currentTime));
  timeElapsed.innerText = `${time.minutes}:${time.seconds}`;
  timeElapsed.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}

我们需要 timeupdate 事件监听视频。无论什么时候,视频的 currentTime 属性值更新了,事件就会触发。

// index.js
video.addEventListener('timeupdate', updateTimeElapsed);

上面的代码确保视频的 currentTime 更新,经过时间也会适当更新。

current-time-update.gif

更新进度条

接下来我们要做的事情是当视频播放,更新进度条。下面是进度条的元素标志:

<!-- index.html -->
. . .
<div class="video-progress">
  <progress id="progress-bar" value="0" min="0"></progress>
  <input class="seek" id="seek" value="0" min="0" type="range" step="1">
  <div class="seek-tooltip" id="seek-tooltip">00:00</div>
</div>
. . .

上面,我们有 progress 元素,用于显示任务的进度条,而 range 类型的 input 允许我们快速无缝浏览视频。两个元素我都用同个样式修饰,所以它们有一样的宽高,但是 input 是透明色(除了与进度条内相同的颜色的指示点)。

如果你很好奇,你可以仔细看 CSS 的内容,看看我是怎么做的。让进度条看起来像一个单一的元素是一种 hack,但是我觉得对我们的用例来说很合理。

两者的 min 属性被设置为 0,两者的 value 属性指向当前时间值。它们还需要一个 max 属性,该属性将设置为视频的持续时间(以秒为单位),该属性值来自 video.duration,如上所示。我们可以在 initializeVideo 函数中实现,但是我们得先选择元素:

// index.js
const progressBar = document.getElementById('progress-bar');
const seek = document.getElementById('seek');

然后如下更新 initializeVideo 函数:

// index.js
function initializeVideo() {
  const videoDuration = Math.round(video.duration);
  seek.setAttribute('max', videoDuration);
  progressBar.setAttribute('max', videoDuration);
  const time = formatTime(videoDuration);
  duration.innerText = `${time.minutes}:${time.seconds}`;
  duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`)
}

现在,进度条元素的范围输入在 0 和以秒为单位的视频持续时长之间,如属性 minmax 属性。正如你将看到的,这使得我们能够在任何时间点轻松地将进度条和时间范围同步。

继续,当视频被播放我们就更新上述元素的值,以便进度条发挥作用。如下,创建 updateProgress 函数:

// index.js
// updateProgress indicates how far through the video
// the current playback is by updating the progress bar
function updateProgress() {
  seek.value = Math.floor(video.currentTime);
  progressBar.value = Math.floor(video.currentTime);
}

然后,在第一个事件监听器下,为 video 添加一个新的名为 timeupdate 事件监听器:

// index.js
video.addEventListener('timeupdate', updateProgress);

刷新你的浏览器,然后尝试。当视频被播放,你应该看到进度条更新。

progress-bar-update.gif

预先跳转

大多数的播放器都允许你点击进度条跳转到视频指定的点,我们的视频播放器也将一样。首先,我们需要选择提示信息元素:

// index.js
const seekTooltip = document.getElementById('seek-tooltip');

然后,添加一个函数,用来当光标移动到进度条上在信息元素里展示时间戳:

// index.js
// updateSeekTooltip uses the position of the mouse on the progress bar to
// roughly work out what point in the video the user will skip to if
// the progress bar is clicked at that point
function updateSeekTooltip(event) {
  const skipTo = Math.round((event.offsetX / event.target.clientWidth) * parseInt(event.target.getAttribute('max'), 10));
  seek.setAttribute('data-seek', skipTo)
  const t = formatTime(skipTo);
  seekTooltip.textContent = `${t.minutes}:${t.seconds}`;
  const rect = video.getBoundingClientRect();
  seekTooltip.style.left = `${event.pageX - rect.left}px`;
}

此函数在 seek 元素中,使用光标位置粗略计算用户悬停范围输入框的地方,然后将位置信息存放在 data-seek 属性中,同时更新提示信息以反映该位置的时间戳。

seek 控制器中关联 updateSeekTooltip 函数和 mousemove 来查看效果:

// index.js
seek.addEventListener('mousemove', updateSeekTooltip);

mousemove-tooltip.gif

不管是点击或者拖拽指示点,一旦 seek 元素值发生更改,我们希望跳转到 data-seek 属性设置的时间点。

updateSeekTooltip 函数下,创建一个新的名为 skipAhead 的函数:

// index.js
// skipAhead jumps to a different point in the video when
// the progress bar is clicked
function skipAhead(event) {
  const skipTo = event.target.dataset.seek ? event.target.dataset.seek : event.target.value;
  video.currentTime = skipTo;
  progressBar.value = skipTo;
  seek.value = skipTo;
}

使用 input 事件监控 seek 元素发生更改时,将执行此函数。然后,我们获取 data-seek 的值并检查其是否有效。如果有效,我们获取该值并更新视频播放过的时间和进度条的位置。如果 data-seek 属性不存在(比如在手机端),改为使用 seek 元素的值。

这产生跳转到视频指定位置的效果。

// index.js
seek.addEventListener('input', skipAhead);

jump-ahead-demo.gif

音频控制

<!-- index.html -->
. . .
<div class="volume-controls">
  <button data-title="Mute (m)" class="volume-button" id="volume-button">
    <svg>
      <use class="hidden" href="#volume-mute"></use>
      <use class="hidden" href="#volume-low"></use>
      <use href="#volume-high"></use>
    </svg>
  </button>

  <input class="volume" id="volume" value="1" type="range" max="1" min="0" step="0.01">
</div>
. . .

在上面代码片段中,你可以找到所有相关音频控件的标记。我们有一个按钮,根据视频音频的状态展示,和一个控制音频范围的 input 元素。

首先,当 #volume 元素的值发生更改,我们要做的就是更改视频的音频大小。我们也要更新视频当前的图标。

正如你所见,音频的输入范围是 01,并以 0.01 的值递增。以这种方式设置它是为了使其与视频的音量属性值保持一致,该属性值的范围也是从 01,其中 0 是最低音量,1 是最高音量。

继续,我们选择按钮,图标和输入框,如下 index.js 所示:

// index.js
const volumeButton = document.getElementById('volume-button');
const volumeIcons = document.querySelectorAll('.volume-button use');
const volumeMute = document.querySelector('use[href="#volume-mute"]');
const volumeLow = document.querySelector('use[href="#volume-low"]');
const volumeHigh = document.querySelector('use[href="#volume-high"]');
const volume = document.getElementById('volume');

接着,创建一个新的名为 updateVolume 函数,当音频输入框值发生更改,该函数更新视频音频值:

// index.js
// updateVolume updates the video's volume
// and disables the muted state if active
function updateVolume() {
  if (video.muted) {
    video.muted = false;
  }

  video.volume = volume.value;
}

然后,将其和 volume 元素关联起来,如下:

// index.js
volume.addEventListener('input', updateVolume);

到这里,你将意识到当你左滑输入框时,音量减少,反之音量增加。我们需要添加另一个函数来在音量变化时更新图标:

// index.js
// updateVolumeIcon updates the volume icon so that it correctly reflects
// the volume of the video
function updateVolumeIcon() {
  volumeIcons.forEach(icon => {
    icon.classList.add('hidden');
  });

  volumeButton.setAttribute('data-title', 'Mute (m)')

  if (video.muted || video.volume === 0) {
    volumeMute.classList.remove('hidden');
    volumeButton.setAttribute('data-title', 'Unmute (m)')
  } else if (video.volume > 0 && video.volume <= 0.5) {
    volumeLow.classList.remove('hidden');
  } else {
    volumeHigh.classList.remove('hidden');
  }
}

当这函数执行,所有的图标都会隐藏,然后会根据条件显示其中一个图标。

我们可以通过监听视频 volumechange 事件,在每次音量发生变化时运行 updateVolumeIcon 函数,如下:

// index.js
video.addEventListener('volumechange', updateVolumeIcon);

添加上面的更改后,在你浏览上你可以看到下面的效果:

volume-change.gif

我们需要添加的另一个事件是能够通过单击音量图标使得视频静音和取消静音。我们将创建一个名为 toggleMute 函数:

// index.js
// toggleMute mutes or unmutes the video when executed
// When the video is unmuted, the volume is returned to the value
// it was set to before the video was muted
function toggleMute() {
  video.muted = !video.muted;

  if (video.muted) {
    volume.setAttribute('data-volume', volume.value);
    volume.value = 0;
  } else {
    volume.value = volume.dataset.volume;
  }
}

volumeButton 被点击后运行该函数:

// index.js
volumeButton.addEventListener('click', toggleMute);

该函数切换视频 muted 属性的状态为真或者假。当视频被静音,音频值就会存放在 volume 元素 data-volume 属性上,以便当视频取消静音时,我们可以恢复音频状态之前的值。

这里是实操效果:

volume-muted-unmuted.gif

点击视频播放或者暂停

在很多视频播放器应用中,点击视频本身能够快速进行播放或者暂停,所以,在我们的播放器中也实现它。

我们要做的就是监听 video 上的 click 事件,当事件触发就运行 togglePlay 函数:

// index.js
video.addEventListener('click', togglePlay);

虽然这可行,但是让我们通过在播放或者暂停视频时添加一些反馈让这更有趣,就像 YouTube 或者 Netflix 上一样。

这是我们动画的 HTML

<!-- index.html -->
. . .
<div class="playback-animation" id="playback-animation">
  <svg class="playback-icons">
    <use class="hidden" href="#play-icon"></use>
    <use href="#pause"></use>
  </svg>
</div>
. . .

下面是相关的 CSS

// style.css
.playback-animation {
  pointer-events: none;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -40px;
  margin-top: -40px;
  width: 80px;
  height: 80px;
  border-radius: 80px;
  background-color: rgba(0, 0, 0, 0.6);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
}

.playback-animation 元素通过 opacity 属性,设置默认值是透明色。为了复制 YouTube 中的动效,我们将会使用 Web Animations API 来实现该元素透明度和缩放效果。

index.js 文件顶部先选中该元素:

// index.js
const playbackAnimation = document.getElementById('playback-animation');

然后在其他函数下创建下面的函数:

// index.js
// animatePlayback displays an animation when
// the video is played or paused
function animatePlayback() {
  playbackAnimation.animate([
    {
      opacity: 1,
      transform: "scale(1)",
    },
    {
      opacity: 0,
      transform: "scale(1.3)",
    }], {
    duration: 500,
  });
}

animate 函数接受一个关键帧对象数组和一个控制动画时间等的可选对象。

现在,为 video 元素添加第二个 click 事件:

// index.js
video.addEventListener('click', animatePlayback);

现在当你点击播放或者暂停视频,可以看到简短的动画效果。

play-pause-video.gif

视频全屏

接下来,我们实现全屏功能按钮。为了让视频全屏(包括控制器),我们需要选择 .video-container 元素,然后询问浏览器去全屏放置它(及其子元素)。

index.js 文件中选择按钮和视频容器:

// index.js
const fullscreenButton = document.getElementById('fullscreen-button');
const videoContainer = document.getElementById('video-container');

然后创建一个新的名为 toggleFullScreen 函数:

// index.js
// toggleFullScreen toggles the full screen state of the video
// If the browser is currently in fullscreen mode,
// then it should exit and vice versa.
function toggleFullScreen() {
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else if (document.webkitFullscreenElement) {
    // Need this to support Safari
    document.webkitExitFullscreen();
  } else if (videoContainer.webkitRequestFullscreen) {
    // Need this to support Safari
    videoContainer.webkitRequestFullscreen();
  } else {
    videoContainer.requestFullscreen();
  }
}

然后,为 fullScreenButton 元素添加一个 click 事件,如下:

// index.js
fullscreenButton.onclick = toggleFullScreen;

toggleFullScreen 函数会先检查 document 是否是全屏模式,如果是则退出到浏览器模式。否则,则将 videoContainer 元素放置在全屏。

在该章节,我们还要做的是当鼠标悬停在按钮上更新全屏图片和提示文本。首先,选择图标:

// index.js
const fullscreenIcons = fullscreenButton.querySelectorAll('use');

然后创建一个函数,当 videoContainer 进行全屏或者退出全屏模式时候更新按钮:

// index.js
// updateFullscreenButton changes the icon of the full screen button
// and tooltip to reflect the current full screen state of the video
function updateFullscreenButton() {
  fullscreenIcons.forEach(icon => icon.classList.toggle('hidden'));

  if (document.fullscreenElement) {
    fullscreenButton.setAttribute('data-title', 'Exit full screen (f)')
  } else {
    fullscreenButton.setAttribute('data-title', 'Full screen (f)')
  }
}

最后,为 videoContainer 元素分配 updateFullscreenButton 函数到 onfullscreenchange 事件处理器:

// index.js
videoContainer.addEventListener('fullscreenchange', updateFullscreenButton);

嗯,它按预期工作!你可以在自己浏览器上测试或者看下面的 GIF 图。

full-screen-video.gif

添加画中画支持

Picture-in-Picture(PiP) API 允许用户在浮动窗口(其中位于其他窗口之上) 中观看视频,这样他们就可以在观看视频的同时将注意力放在其他站点或者应用上。

到目前为止,这个 API 只被少数浏览器支持,所以我们需要对不支持的浏览器隐藏该 PiP 按钮,以便他们看不到使用不了的功能。

picture-in-picture-ability.webp

请参考 caniuse.com 获取最新的表格信息。

下面的代码能帮我们实现该功能。在其他事件监听器下添加此代码。

// index.js
document.addEventListener('DOMContentLoaded', () => {
  if (!('pictureInPictureEnabled' in document)) {
    pipButton.classList.add('hidden');
  }
});

正如本教程我们要做的那样,我们先需要选中相关的控制器:

// index.js
const pipButton = document.getElementById('pip-button')

然后创建切换 Picture-in-Picture 模式的函数:

// index.js
// togglePip toggles Picture-in-Picture mode on the video
async function togglePip() {
  try {
    if (video !== document.pictureInPictureElement) {
      pipButton.disabled = true;
      await video.requestPictureInPicture();
    } else {
      await document.exitPictureInPicture();
    }
  } catch (error) {
    console.error(error)
  } finally {
    pipButton.disabled = false;
  }
}

我创建了一个名为 togglePip 的异步函数,以便我们可以在 requestPictureInPicture() 方法拒绝时捕获到错误,这可能由于多种原因导致。在真实的应用中,你可能想向用户展示错误信息,而不是打印到控制台上。

接着,在 pipButton 元素上添加 click 事件,然后添加 togglePip 函数到该事件处理器中。

// index.js
pipButton.addEventListener('click', togglePip);

现在,添加 pipButton 应该进入或者退出画中画模式。你也可以通过点击(画中画模式)右上角的关闭按钮关闭 PiP 窗口。

PiP-mode.gif

切换视频控件

视频控件会占用一些空间并阻挡用户查看一些内容。当它们不被使用的时候将其隐藏起来比较好,然后当鼠标移动到视频上方再显示它们。

为了实现这个目标,我们编写两个函数,如下:

// index.js
// hideControls hides the video controls when not in use
// if the video is paused, the controls must remain visible
function hideControls() {
  if (video.paused) {
    return;
  }

  videoControls.classList.add('hide');
}

// showControls displays the video controls
function showControls() {
  videoControls.classList.remove('hide');
}

这里我们想做的就是,当鼠标离开视频上方就隐藏控件。但是当视频停止播放的时候,我们确保控件总是展示的,所以在 hideControls() 函数中添加条件判断。

为了实现这个,我们将在 video 元素和 videoControls 元素上使用 onmouseenteronmouseleave 事件处理器,如下:

// index.js
video.addEventListener('mouseenter', showControls);
video.addEventListener('mouseleave', hideControls);
videoControls.addEventListener('mouseenter', showControls);
videoControls.addEventListener('mouseleave', hideControls);

添加键盘快捷键

我们将添加到播放器的最后一个特性是使用快捷键控制视频播放。实际上,就是当我们按下特定的键时,运行我们指定函数的事情。我们将实现的快捷键如下:

  • k:播放或者暂停视频
  • m:视频静音或者取消静音
  • f:切换全屏
  • p:切换画中画模式

这里我们要做的就是监听 documentkeyup 事件,检测按下的快捷键并返回相关的函数。

// index.js
// keyboardShortcuts executes the relevant functions for
// each supported shortcut key
function keyboardShortcuts(event) {
  const { key } = event;
  switch(key) {
    case 'k':
      togglePlay();
      animatePlayback();
      if (video.paused) {
        showControls();
      } else {
        setTimeout(() => {
          hideControls();
        }, 2000);
      }
      break;
    case 'm':
      toggleMute();
      break;
    case 'f':
      toggleFullScreen();
      break;
    case 'p':
      togglePip();
      break;
  }
}

如上,一个 switch 声明被用来检测哪个快捷键被按下,然后执行相关的代码。两秒后调用 hideControl 函数的原因是模仿 YouTube 上的行为,当使用快捷键播放视频时候,控件不会立马消失,而是有一个短暂的延时。

// index.js
document.addEventListener('keyup', keyboardShortcuts);

总结

改进视频播放器的方法还有很多,但是本教程篇幅已经很长了,所以我不得不在这里停下来。如果你对额外的功能感兴趣,下面是些想法:

  • 添加对字幕的支持
  • 添加对播放速度的支持
  • 添加快速前进或者倒放视频的功能
  • 添加选择视频分辨率(720p, 480p, 360p, 240p)的功能

我希望本教程对你有帮助。相关代码 GitHub

Thanks for reading, and happy coding!

参考

原文地址 - freshman.tech/custom-html…