行動版網站影片播放

法蘭索瓦博福特
François Beaufort

如何在網路上提供最佳的行動媒體體驗?沒問題!這取決於使用者參與度,以及您在網頁上為媒體提供的重要性。我認為如果影片是吸引使用者造訪的理由,使用者體驗必須具有臨場感,才能吸引他們回流。

行動版網站影片播放

在這篇文章中,我將示範如何透過大量的 Web API,以漸進的方式強化媒體體驗,讓使用者享有更身歷其境的體驗。因此,我們會使用自訂控制項、全螢幕和背景播放功能,打造簡單的行動播放器體驗。您可以立即試用範例,並在我們的 GitHub 存放區中尋找程式碼

自訂控制項

HTML 版面配置
圖 1:HTML 版面配置

如您所見,我們要用於媒體播放器的 HTML 版面配置非常簡單:<div> 根元素包含 <video> 媒體元素,以及影片控制項專用的 <div> 子元素。

我們稍後會說明的影片控制項包括播放/暫停按鈕、全螢幕按鈕、跳轉和向前按鈕,以及追蹤目前時間、時間長度和時間的部分元素。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls"></div>
</div>

讀取影片中繼資料

首先,等待影片中繼資料載入,以便設定影片長度、目前時間,並初始化進度列。請注意,我們編寫的 secondsToTimeCode() 函式是自訂公用程式函式,可將秒數轉換為「hh:mm:ss」格式的字串,後者更適合用於我們的範例。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong>
      <div id="videoCurrentTime"></div>
      <div id="videoDuration"></div>
      <div id="videoProgressBar"></div>
    </strong>
  </div>
</div>
video.addEventListener('loadedmetadata', function () {
  videoDuration.textContent = secondsToTimeCode(video.duration);
  videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
  videoProgressBar.style.transform = `scaleX(${
    video.currentTime / video.duration
  })`;
});
僅限影片中繼資料
圖 2.顯示影片中繼資料的媒體播放器

播放/暫停影片

已載入影片中繼資料,讓我們新增第一個按鈕,讓使用者可根據播放狀態,使用 video.play()video.pause() 播放及暫停影片。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong><button id="playPauseButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
playPauseButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
});

我們不會在 click 事件監聽器中調整影片控制項,而是使用 playpause 影片事件。根據彈性設定控制項事件,我們稍後會介紹 Media Session API,以便讓控制項保持同步。影片開始播放時,我們會將按鈕狀態變更為「暫停」並隱藏影片控制項。影片暫停時,只要將按鈕狀態變更為「播放」並顯示影片控制項,

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

當影片 currentTime 屬性透過 timeupdate 影片事件表示時間有所變更時,我們也會更新自訂控制項 (如果有的話)。

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

影片播放結束後,只要將按鈕狀態變更為「播放」,然後將影片 currentTime 設為 0,即可暫時顯示影片控制項。請注意,如果使用者已啟用某些「AutoPlay」功能,我們也可以選擇自動載入其他影片。

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

倒轉及快轉

我們繼續並新增「向後瀏覽」和「倒轉」按鈕,方便使用者略過部分內容。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <strong
      ><button id="seekForwardButton"></button>
      <button id="seekBackwardButton"></button
    ></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
var skipTime = 10; // Time to skip in seconds

seekForwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});

seekBackwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
});

和先前一樣,我們不會透過這些按鈕的 click 事件監聽器調整影片樣式,而是使用觸發的 seekingseeked 影片事件調整影片亮度。我的自訂 seeking CSS 類別和 filter: brightness(0); 一樣簡單。

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

以下是我們到目前為止建立的內容。在下一節中,我們會實作全螢幕按鈕。

全螢幕

我們會運用多個 Web API,打造完美流暢的全螢幕體驗。如要瞭解如何實際運作,請參閱範例

當然,你不一定要使用所有。只要選擇符合您需求的方案並將其合併,即可建立自訂流程。

禁止自動顯示全螢幕

在 iOS 上,當媒體播放開始時,video 元素會自動進入全螢幕模式。我們想盡可能在各種行動瀏覽器中提供及控制媒體體驗,因此建議您設定 video 元素的 playsinline 屬性,強制在 iPhone 上以內嵌方式播放,不要在開始播放時進入全螢幕模式。請注意,這不會對其他瀏覽器產生副作用。

<div id="videoContainer"></div>
  <video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
  <div id="videoControls">...</div>
</div>

點選即可切換全螢幕按鈕

現在我們阻止了自動全螢幕功能,因此我們需要使用全螢幕 API,自行處理影片的全螢幕模式。使用者按一下「全螢幕按鈕」後,如果文件目前使用全螢幕模式,讓我們使用 document.exitFullscreen() 結束全螢幕模式。否則,請使用 requestFullscreen() 方法要求影片容器的全螢幕畫面,或只在 iOS 裝置上將影片元素改回 webkitEnterFullscreen()

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <strong><button id="fullscreenButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
  }
});

function requestFullscreenVideo() {
  if (videoContainer.requestFullscreen) {
    videoContainer.requestFullscreen();
  } else {
    video.webkitEnterFullscreen();
  }
}

document.addEventListener('fullscreenchange', function () {
  fullscreenButton.classList.toggle('active', document.fullscreenElement);
});

螢幕方向變更時切換全螢幕

隨著使用者以橫向模式旋轉裝置,精益求精,並自動要求全螢幕以打造沉浸式體驗。為此,您需要使用 Screen Orientation API。由於這個 API 目前未在所有環境支援,而且目前在某些瀏覽器中仍加上前置字串。因此,這是我們第一次的進步增強功能。

它是怎樣運作的?我們偵測到螢幕方向變更之後,就會要求在瀏覽器視窗處於橫向模式 (也就是寬度大於高度) 時要求全螢幕。如果沒有,請結束全螢幕模式。以上就是清單中的所有項目。

if ('orientation' in screen) {
  screen.orientation.addEventListener('change', function () {
    // Let's request fullscreen if user switches device in landscape mode.
    if (screen.orientation.type.startsWith('landscape')) {
      requestFullscreenVideo();
    } else if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  });
}

按下按鈕時在橫向模式中鎖定畫面

由於影片以橫向模式播放時比較方便,因此我們建議您在使用者點選「全螢幕按鈕」時,在橫向模式中鎖定畫面。我們將結合先前使用的 Screen Orientation API 和一些媒體查詢,以確保最佳體驗。

水平螢幕鎖定就和呼叫 screen.orientation.lock('landscape') 一樣簡單。不過,只有在裝置處於直向模式並使用 matchMedia('(orientation: portrait)') 時,我們才需要執行這項操作,且能與 matchMedia('(max-device-width: 768px)') 單手握持裝置,因為這對平板電腦使用者而言無法帶來良好體驗。

fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
    <strong>lockScreenInLandscape();</strong>;
  }
});
function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (
    matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
  ) {
    screen.orientation.lock('landscape');
  }
}

裝置螢幕方向變更時解鎖螢幕

您可能注意到,我們剛建立的螢幕鎖定體驗並不完美,但由於無法在螢幕鎖定時接收螢幕方向變更,

為解決這個問題,請使用 Device Orientation API (如果有的話)。這個 API 提供來自測量裝置空間位置和動作的硬體相關資訊:陀螺儀和數位指南針 (用於方向),以及用於控制速度的加速計。我們偵測到裝置螢幕方向改變時,若使用者手持裝置處於直向模式且螢幕處於橫向模式,進而使用 screen.orientation.unlock() 解鎖螢幕。

function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
    screen.orientation.lock('landscape')
    <strong>.then(function() {
      listenToDeviceOrientationChanges();
    })</strong>;
  }
}
function listenToDeviceOrientationChanges() {
  if (!('DeviceOrientationEvent' in window)) {
    return;
  }
  var previousDeviceOrientation, currentDeviceOrientation;
  window.addEventListener(
    'deviceorientation',
    function onDeviceOrientationChange(event) {
      // event.beta represents a front to back motion of the device and
      // event.gamma a left to right motion.
      if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        currentDeviceOrientation = 'landscape';
        return;
      }
      if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        // When device is rotated back to portrait, let's unlock screen orientation.
        if (previousDeviceOrientation == 'landscape') {
          screen.orientation.unlock();
          window.removeEventListener(
            'deviceorientation',
            onDeviceOrientationChange,
          );
        }
      }
    },
  );
}

如您所見,這是我們一直尋求的流暢體驗。 如要瞭解如何實際操作,請參閱範例

背景播放

不再看得見網頁中偵測到的網頁或影片時,建議您更新數據分析以反映這個情況。這可能也會影響目前播放的內容,例如選擇其他音軌、暫停播放,或甚至向使用者顯示自訂按鈕。

在網頁瀏覽權限變更時暫停影片

我們可以透過 Page Visibility API 判斷網頁目前的瀏覽權限,並在瀏覽權限變更時收到通知。下方程式碼會在網頁隱藏時暫停影片。當螢幕鎖定功能啟用或切換分頁例如時,就會發生這種情況。

由於大部分的行動瀏覽器現在都提供瀏覽器以外的控制項,以便繼續播放已暫停的影片,因此建議您只在使用者允許在背景中播放時,才設定這項行為。

document.addEventListener('visibilitychange', function () {
  // Pause video when page is hidden.
  if (document.hidden) {
    video.pause();
  }
});

在影片瀏覽權限變更時顯示/隱藏靜音按鈕

如果您使用新的 Intersection Observer API,則無須付費即可使用更精細的功能。這個 API 可讓您瞭解觀察到的元素進入或離開瀏覽器的可視區域。

你可以根據頁面中的影片瀏覽權限顯示/隱藏靜音按鈕。如果系統正在播放影片,但目前看不到畫面,頁面右下角會顯示迷你靜音按鈕,讓使用者能自行控制影片音效。volumechange 影片事件可用來更新靜音按鈕樣式。

<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
  // Show/hide mute button based on video visibility in the page.
  function onIntersection(entries) {
    entries.forEach(function (entry) {
      muteButton.hidden = video.paused || entry.isIntersecting;
    });
  }
  var observer = new IntersectionObserver(onIntersection);
  observer.observe(video);
}

muteButton.addEventListener('click', function () {
  // Mute/unmute video on button click.
  video.muted = !video.muted;
});

video.addEventListener('volumechange', function () {
  muteButton.classList.toggle('active', video.muted);
});

一次只能播放一部影片

如果網頁上有多部影片,建議您只播放一部影片,並自動暫停其他影片,這樣使用者就不必同時播放多個音軌。

// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));

videos.forEach(function (video) {
  video.addEventListener('play', pauseOtherVideosPlaying);
});

function pauseOtherVideosPlaying(event) {
  var videosToPause = videos.filter(function (video) {
    return !video.paused && video != event.target;
  });
  // Pause all other videos currently playing.
  videosToPause.forEach(function (video) {
    video.pause();
  });
}

自訂媒體通知

使用 Media Session API 時,您也可以提供目前播放影片的中繼資料,藉此自訂媒體通知。您還可以處理媒體相關事件,例如搜尋或追蹤來自通知或媒體按鍵的變更。如要瞭解實際的運作方式,請參閱範例

當網頁應用程式播放音訊或視訊時,通知匣中會顯示媒體通知。在 Android 中,Chrome 會使用文件標題和盡可能找到的圖示圖片,盡可能顯示適當的資訊。

讓我們一起瞭解如何使用 Media Session API 設定部分媒體工作階段中繼資料,例如標題、演出者、專輯名稱和圖片,藉此自訂這則媒體通知。

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    <strong>.then(function() {
      setMediaSession();
    });</strong>
  } else {
    video.pause();
  }
});
function setMediaSession() {
  if (!('mediaSession' in navigator)) {
    return;
  }
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
      {src: 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png'},
      {
        src: 'https://dummyimage.com/128x128',
        sizes: '128x128',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/192x192',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/256x256',
        sizes: '256x256',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/384x384',
        sizes: '384x384',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/512x512',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  });
}

播放完畢後,通知會自動消失,因此不必「釋放」媒體工作階段。請注意,每次開始播放時,都會使用目前的 navigator.mediaSession.metadata。因此,您必須進行更新,以確保媒體通知始終顯示相關資訊。

如果您的網頁應用程式提供播放清單,則可能需要讓使用者直接透過媒體通知瀏覽播放清單,並加上「上一首曲目」和「下一首曲目」圖示。

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', function () {
    // User clicked "Previous Track" media notification icon.
    playPreviousVideo(); // load and play previous video
  });
  navigator.mediaSession.setActionHandler('nexttrack', function () {
    // User clicked "Next Track" media notification icon.
    playNextVideo(); // load and play next video
  });
}

請注意,系統會保留媒體動作處理常式。這與事件監聽器模式非常類似,差別在於處理事件表示瀏覽器會停止執行任何預設行為,並將這項行為視為網頁應用程式支援媒體動作的信號。因此,除非您設定適當的動作處理常式,否則不會顯示媒體動作控制項。

對了,取消設定媒體動作處理常式,就和指派給 null 一樣簡單。

如果想控制略過的時間長度,Media Session API 可讓您顯示「Seek Backward」和「Seek Forward」媒體通知圖示。

if ('mediaSession' in navigator) {
  let skipTime = 10; // Time to skip in seconds

  navigator.mediaSession.setActionHandler('seekbackward', function () {
    // User clicked "Seek Backward" media notification icon.
    video.currentTime = Math.max(video.currentTime - skipTime, 0);
  });
  navigator.mediaSession.setActionHandler('seekforward', function () {
    // User clicked "Seek Forward" media notification icon.
    video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
  });
}

「播放/暫停」圖示一律會顯示在媒體通知中,且瀏覽器會自動處理相關事件。如果預設行為因故無法運作,您仍可處理「播放」和「暫停」媒體事件

Media Session API 的優點是通知匣不是唯一會顯示媒體中繼資料和控制項的地方。媒體通知會自動同步到任何配對的穿戴式裝置。也會顯示在螢幕鎖定畫面上

意見回饋