影片和音訊 API

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

先決條件 JavaScript 基礎知識(請參閱第一步構建塊JavaScript 物件),以及客戶端 API 的基礎知識
目標 學習如何使用瀏覽器 API 控制影片和音訊播放。

HTML 影片和音訊

使用 <video><audio> 元素,我們可以將影片和音訊嵌入網頁中。正如我們在影片和音訊內容中展示的那樣,一個典型的實現如下所示

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> 元素,因為您需要實現的功能幾乎相同。讓我們來看一個示例,在過程中新增功能。

我們完成的示例將如下所示(並具有以下功能)

入門

要開始此示例,請下載我們的 media-player-start.zip 並將其解壓縮到硬碟驅動器上的新目錄中。如果您下載了我們的示例儲存庫,您將在 javascript/apis/video-audio/start/ 中找到它。

此時,如果您載入 HTML,您應該會看到一個完全正常的 HTML 影片播放器,並呈現原生控制元件。

探索 HTML

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

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 屬性的內容。
    • 還有一個計時器 <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 由於某種原因未載入,使用者仍然可以使用原生控制元件來使用影片。
  • 預設情況下,我們為控制元件提供 0.5 的 opacity,以便在您嘗試觀看影片時,它們不會太分散注意力。只有當您將滑鼠懸停/聚焦在播放器上時,控制元件才會以完全不透明度顯示。
  • 我們使用 flexbox (display: flex) 佈局控制元件欄內的按鈕,以簡化操作。

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

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 塊匯入自定義網路字型。這是一種圖示字型——字母表中的所有字元都等同於您可能想要在應用程式中使用的常用圖示。

接下來,我們使用生成內容在每個按鈕上顯示圖示

  • 我們使用 ::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. 在與 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";
    
    這兩行從影片中刪除預設的瀏覽器控制元件,並使自定義控制元件可見。

播放和暫停影片

讓我們實現可能最重要的控制元件——播放/暫停按鈕。

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

更新已用時間

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

在其他程式碼下方新增以下 addEventListener() 程式碼行

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

現在定義 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 按鈕功能並按預期播放/停止影片?這很容易修復。

首先,在 stopMedia() 函式內部新增以下幾行 - 任何位置都可以

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

現在再次新增相同的行,在 playPauseMedia() 函式的開頭(在 if 語句的開頭之前)。

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

另請參閱