行動版網站影片播放

François Beaufort
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 事件監聽器中的影片控制項,而是使用 play 和 pause 影片事件。將控制項設為事件導向,有助於提高彈性 (稍後會介紹 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,並顯示影片控制項即可。請注意,如果使用者啟用了某種「自動播放」功能,我們也可以選擇自動載入另一部影片。

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 事件監聽器中調整影片樣式,而是使用觸發的 seeking 和 seeked 影片事件來調整影片亮度。我的自訂 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>

點選按鈕切換全螢幕

現在我們已禁止自動進入全螢幕模式,因此需要使用 Fullscreen 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,但當時並非所有平台都支援,且部分瀏覽器仍有前置字元。因此,這將是我們的第一個漸進增強。

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

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 顯示「倒轉」和「快轉」媒體通知圖示。

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 的優點在於,通知匣並非唯一顯示媒體中繼資料和控制項的位置。媒體通知會自動同步到任何配對的穿戴式裝置。也會顯示在螢幕鎖定畫面上。

意見回饋