视频播放器样式基础

在上一篇《跨浏览器视频播放器》文章中,我们介绍了如何使用 Media 和 Fullscreen API 构建一个跨浏览器的 HTML 视频播放器。本文将进一步探讨如何为这个自定义播放器添加样式,包括如何使其响应式。

HTML 标记

相比上一篇文章中的 HTML 标记,这里做了一些修改。自定义视频控件和 <progress> 元素现在被包含在 <div> 元素中,而不是直接放在无序列表项内。

自定义控件的标记现在看起来如下:

html
<div id="video-controls" class="controls" data-state="hidden">
  <button id="play-pause" type="button" data-state="play">Play/Pause</button>
  <button id="stop" type="button" data-state="stop">Stop</button>
  <div class="progress">
    <progress id="progress" value="0">
      <span id="progress-bar"></span>
    </progress>
  </div>
  <button id="mute" type="button" data-state="mute">Mute/Unmute</button>
  <button id="vol-inc" type="button" data-state="vol-up">Vol+</button>
  <button id="vol-dec" type="button" data-state="vol-down">Vol-</button>
  <button id="fs" type="button" data-state="go-fullscreen">Fullscreen</button>
</div>

为了样式设置的需要,在多个地方使用了 data-state 属性,这些属性是通过 JavaScript 设置的。具体的实现将在下面的适当位置介绍。

样式

此处使用的视频播放器样式相当基础,这是故意的,目的是展示如何为视频播放器添加样式并使其响应式。

注意: 在某些情况下,一些基础 CSS 被省略了,因为它们的使用要么很明显,要么与视频播放器的样式设置关系不大。

基础样式

视频控件容器本身需要一些样式设置,以确保其布局正确。

css
.controls {
  display: flex;
  align-items: center;
  overflow: hidden;
  width: 100%;
  height: 2rem;
  position: relative;
}

position 设置为 relative,这是使其具有响应性的前提(稍后会详细介绍)。

如前所述,data-state 属性用于指示视频控件是否可见,因此需要相应的 CSS 声明。

css
.controls[data-state="hidden"] {
  display: none;
}

按钮

要解决的第一个主要样式问题是让视频控件的按钮看起来和用起来都像真正的按钮。

每个按钮都具有一些基础样式。

css
.controls button {
  width: 2rem;
  height: 2rem;
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  border: none;
  cursor: pointer;
  color: transparent;
  background-color: transparent;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

每个按钮的宽度和高度都设置为 2rem。默认情况下,所有 <button> 元素都有边框,这里将其移除。由于将使用背景图像来显示相应的图标,因此按钮的背景颜色设置为透明,不重复,并且元素应完全包含图像。此外,还有一些不应在屏幕上可见的标签文本,因此文本颜色设置为透明。

然后为每个按钮设置 :hover:focus 状态,这会改变按钮的不透明度。

css
.controls button:hover,
.controls button:focus {
  opacity: 0.5;
}

为了获得合适的按钮图像,从网上下载了一套免费的通用控件图标集。然后,每个图像都被转换为 base64 编码的字符串(使用在线 base64 图片编码器),因为图像很小,所以生成的编码字符串也很短。

由于某些按钮具有双重功能(例如,播放/暂停,静音/取消静音),这些按钮有不同的状态需要进行样式设置。如前所述,data-state 变量用于指示这些按钮当前所处的状态。

例如,播放/暂停按钮具有以下背景图像定义(为简洁起见,已省略完整的 base64 字符串)。

css
.controls button[data-state="play"] {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAA…");
}

.controls button[data-state="pause"] {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAA…");
}

当按钮的 data-state 更改时,相应的图像也会更改。所有其他按钮也以类似的方式处理。

进度条

<progress> 元素的 <div> 容器启用了 flex-grow,使其能够扩展以填满控件中剩余的空间。它还显示一个指针光标,表示它是可交互的。

css
.controls .progress {
  flex-grow: 1;
  cursor: pointer;
  height: 80%;
}

<progress> 元素具有以下基本样式设置:

css
.controls progress {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
  color: #0095dd;
  border-radius: 2px;
  margin: 0 auto;
}

<button> 元素一样,<progress> 元素也有默认边框,这里将其移除。出于美观考虑,还为其设置了轻微的圆角。

需要设置一些特定于浏览器的属性,以确保 Firefox 和 Chrome 使用所需的进度条颜色。

css
.controls progress::-moz-progress-bar {
  background-color: #0095dd;
}

.controls progress::-webkit-progress-value {
  background-color: #0095dd;
}

尽管这些属性被设置为相同的值,但这些规则需要单独定义,否则如果其中一个选择器不被识别,整个声明可能会变得无效。

全屏

现在我们来设置全屏模式下的控件样式。由于 <figure> 元素被置于全屏,我们可以使用 :fullscreen 伪类来定位它。我们做了几件事:

  • 使用 height: 100% 使 figure 占据整个屏幕。
  • 使用 flexbox 使控件栏固定在底部,同时视频保持居中。
  • 使容器透明,以显示原生的背景色。
  • 隐藏 figcaption
  • 恢复控件行的背景颜色,以确保我们的黑色按钮在背景为黑色时仍然可见。
css
figure:fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  max-width: 100%;
  height: 100%;
  background-color: transparent;
}
figure:fullscreen video {
  margin-top: auto;
  margin-bottom: auto;
}
figure:fullscreen figcaption {
  display: none;
}
figure:fullscreen .controls {
  background-color: #666666;
}

响应式样式

现在播放器已经具备了基本的外观和感觉,还需要进行一些其他的样式更改——涉及媒体查询——以使其具有响应性。

我们希望在小屏幕(680px/42.5em)上观看时自定义控件布局,因此在此定义了一个断点。我们调整按钮和进度条的尺寸和位置属性,使它们的排列方式不同。

css
@media screen and (width <= 42.5em) {
  .controls {
    height: auto;
  }

  .controls button {
    width: calc(100% / 6);
    margin-top: 2.5rem;
  }

  .controls .progress {
    position: absolute;
    top: 0;
    width: 100%;
    margin-top: 0;
    height: 2rem;
  }

  .controls .progress progress {
    width: 98%;
  }

  figcaption {
    text-align: center;
  }
}

`.progress` 容器现在通过 position:absolute 移动到控件集的顶部,因此它和所有按钮都需要更宽。此外,按钮需要被推到进度条容器下方,以便它们可见。

JavaScript

这就是即时的样式设置;接下来的任务是进行一系列 JavaScript 更改,以确保一切正常工作,主要重构按钮逻辑。

播放/暂停和静音

现在按钮看起来像按钮,并且具有指示其功能的图像,需要进行一些更改,以便“双功能”按钮(如播放/暂停按钮)处于正确的“状态”并显示正确的图像。为了实现这一点,定义了一个名为 changeButtonState() 的新函数,它接受一个 type 变量,指示按钮的功能。

js
function changeButtonState(type) {
  if (type === "play-pause") {
    // Play/Pause button
    if (video.paused || video.ended) {
      playPause.setAttribute("data-state", "play");
    } else {
      playPause.setAttribute("data-state", "pause");
    }
  } else if (type === "mute") {
    // Mute button
    mute.setAttribute("data-state", video.muted ? "unmute" : "mute");
  }
}

然后,相关的事件处理程序会调用这个函数。

js
video.addEventListener("play", () => {
  changeButtonState("play-pause");
});

video.addEventListener("pause", () => {
  changeButtonState("play-pause");
});

stop.addEventListener("click", (e) => {
  video.pause();
  video.currentTime = 0;
  progress.value = 0;

  // Update the play/pause button's 'data-state' which allows the
  // correct button image to be set via CSS
  changeButtonState("play-pause");
});

mute.addEventListener("click", (e) => {
  video.muted = !video.muted;
  changeButtonState("mute");
});

您可能已经注意到,在响应视频的 playpause 事件的地方新增了处理程序。这是有原因的!尽管浏览器默认的视频控件集已被禁用,但许多浏览器通过右键单击 HTML 视频都可以访问它们。这意味着用户可以从这些控件播放/暂停视频,这会导致自定义控件集中的按钮不同步。如果用户使用了默认控件,就会触发定义的 Media API 事件(如 playpause),因此可以利用这一点来确保自定义控件按钮保持同步。我们的点击也会在调用 play()pause() 方法时触发 playpause 事件,所以这里不需要更改。

js
playPause.addEventListener("click", (e) => {
  if (video.paused || video.ended) {
    video.play();
  } else {
    video.pause();
  }
});

音量

当播放器的音量按钮被点击时调用的 alterVolume() 函数也发生了变化——它现在调用一个名为 checkVolume() 的新函数。

js
function checkVolume(dir) {
  if (dir) {
    const currentVolume = Math.floor(video.volume * 10) / 10;
    if (dir === "+" && currentVolume < 1) {
      video.volume += 0.1;
    } else if (dir === "-" && currentVolume > 0) {
      video.volume -= 0.1;
    }

    // If the volume has been turned off, also set it as muted
    // Note: can only do this with the custom control set as when the 'volumechange' event is raised,
    // there is no way to know if it was via a volume or a mute change
    video.muted = currentVolume <= 0;
  }
  changeButtonState("mute");
}

function alterVolume(dir) {
  checkVolume(dir);
}
volInc.addEventListener("click", (e) => {
  alterVolume("+");
});
volDec.addEventListener("click", (e) => {
  alterVolume("-");
});

这个新的 checkVolume() 函数与 alterVolume() 作用相同,但它还会根据视频的当前音量设置来设置静音按钮的状态。当触发 volumechange 事件时,也会调用 checkVolume()

js
video.addEventListener("volumechange", () => {
  checkVolume();
});

进度和全屏

进度条和全屏的实现没有改变。

结果

警告: 示例视频声音可能很大!