影片和音訊 API

HTML 提供了用於在文件中嵌入富媒體的元素—— <video><audio> ——它們又帶有自己的 API,用於控制播放、定位等。本文將向你展示如何執行常見任務,例如建立自定義播放控制元件。

預備知識 熟悉 HTMLCSSJavaScript,尤其是 JavaScript 物件基礎知識以及 DOM 指令碼網路請求等核心 API 知識。
目標
  • 什麼是編解碼器,以及不同的影片和音訊格式。
  • 瞭解與音訊和影片相關的關鍵功能——播放、暫停、停止、快退和快進、持續時間以及當前時間。
  • 使用 HTMLMediaElement API 構建一個基本的自定義媒體播放器,以實現更好的可訪問性或跨瀏覽器的一致性。

HTML 影片和音訊

<video><audio> 元素允許我們將影片和音訊嵌入到網頁中。正如我們在 HTML 影片和音訊中所示,典型的實現如下

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> 元素,因為你想要實現的功能幾乎相同。讓我們透過一個示例,逐步新增功能。

我們完成的示例看起來(和功能)如下

入門

要開始此示例,請按照以下步驟操作

  1. 在你的硬碟上建立一個名為 custom-video-player 的新目錄。

  2. 在其中建立一個名為 index.html 的新檔案,並填充以下內容

    html
    <!doctype html>
    <html lang="en-gb">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Video player example</title>
        <link rel="stylesheet" type="text/css" href="style.css" />
      </head>
      <body>
        <div class="player">
          <video controls>
            <source
              src="/shared-assets/videos/sintel-short.mp4"
              type="video/mp4" />
            <source
              src="/shared-assets/videos/sintel-short.webm"
              type="video/webm" />
          </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>
        <p>
          Sintel &copy; copyright Blender Foundation |
          <a href="https://studio.blender.org/films/sintel/"
            >studio.blender.org/films/sintel/</a
          >.
        </p>
        <script src="custom-player.js"></script>
      </body>
    </html>
    
  3. 在其中建立另一個名為 style.css 的新檔案,並填充以下內容

    css
    @font-face {
      font-family: "HeydingsControlsRegular";
      src: url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot");
      src:
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot?#iefix")
          format("embedded-opentype"),
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.woff")
          format("woff"),
        url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.ttf")
          format("truetype");
      font-weight: normal;
      font-style: normal;
    }
    
    video {
      border: 1px solid black;
    }
    
    p {
      position: absolute;
      top: 310px;
    }
    
    .player {
      position: absolute;
    }
    
    .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;
    }
    
    button,
    .controls {
      background: linear-gradient(to bottom, #222222, #666666);
    }
    
    button::before {
      font-family: "HeydingsControlsRegular";
      font-size: 20px;
      position: relative;
      content: attr(data-icon);
      color: #aaaaaa;
      text-shadow: 1px 1px 0px black;
    }
    
    .play::before {
      font-size: 22px;
    }
    
    button,
    .timer {
      height: 38px;
      line-height: 19px;
      box-shadow: inset 0 -5px 25px #0000004d;
      border-right: 1px solid #333333;
    }
    
    button {
      position: relative;
      border: 0;
      flex: 1;
      outline: none;
    }
    
    .play {
      border-radius: 10px 0 0 10px;
    }
    
    .fwd {
      border-radius: 0 10px 10px 0;
    }
    
    .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;
    }
    
    button:hover,
    button:focus {
      box-shadow: inset 1px 1px 2px black;
    }
    
    button:active {
      box-shadow: inset 3px 3px 2px black;
    }
    
    .active::before {
      color: red;
    }
    
  4. 在目錄中建立另一個名為 custom-player.js 的新檔案。暫時留空。

此時,如果載入 HTML,你將看到一個完全正常的 HTML 影片播放器,並渲染了原生控制元件。

探索 HTML

開啟 HTML 索引檔案。你會看到許多功能;HTML 主要由影片播放器及其控制元件組成

  • 整個播放器都包裹在一個 <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 因某種原因未能載入,使用者仍然可以使用帶有原生控制元件的影片。
  • 我們預設給控制元件 opacity 設定為 0.5,這樣在觀看影片時它們就不會那麼分散注意力。只有當你懸停/聚焦在播放器上時,控制元件才會以完全不透明度顯示。
  • 我們使用 Flexbox (display: flex) 在控制欄內佈局按鈕,使事情變得更容易。

接下來,讓我們看看我們的按鈕圖示

css
@font-face {
  font-family: "HeydingsControlsRegular";
  src: url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot");
  src:
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.eot?#iefix")
      format("embedded-opentype"),
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/fonts/heydings_controls-webfont.woff")
      format("woff"),
    url("https://mdn.github.io/learning-area/javascript/apis/video-audio/finished/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: #aaaaaa;
  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. custom-player.js 檔案的頂部,插入以下程式碼

    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>
  2. 接下來,將以下內容插入到程式碼的底部

    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 秒,即,如果再倒退三秒鐘會使其回退到影片的開頭。這會導致奇怪的行為,因此如果出現這種情況,我們透過呼叫 stopMedia() 停止影片播放,從快退按鈕中刪除 active 類,並清除 intervalRwd 間隔以停止快退功能。如果我們不執行最後一步,影片將永遠倒帶。
    2. 如果當前時間不在影片開始的 3 秒內,我們透過執行 media.currentTime -= 3 從當前時間中減去 3 秒。因此,實際上,我們每 200 毫秒將影片倒退 3 秒。

更新已用時間

我們媒體播放器要實現的最後一個功能是已用時間顯示。為此,我們將在 <video> 元素上每次觸發 timeupdate 事件時執行一個函式來更新時間顯示。此事件觸發的頻率取決於你的瀏覽器、CPU 效能等(請參閱此 Stack Overflow 帖子)。

  1. 在其他行下方新增以下 addEventListener()

    js
    media.addEventListener("timeupdate", setTime);
    
  2. 現在定義 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 按鈕功能並像你期望的那樣播放/停止影片?這很容易修復。

  1. 首先,在 stopMedia() 函式中新增以下幾行——任何位置都可以

    js
    rwd.classList.remove("active");
    fwd.classList.remove("active");
    clearInterval(intervalRwd);
    clearInterval(intervalFwd);
    
  2. 現在,在 playPauseMedia() 函式的最開始(就在 if 語句開始之前)再次新增相同的行。

  3. 此時,你可以從 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);
    };
    

另見