行動版網站影片播放

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 事件監聽器中的影片控制項,而是使用 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,並暫時顯示影片控制選項。請注意,如果使用者已啟用某種「自動播放」功能,我們也可以選擇自動載入其他影片。

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);
});

如同先前所述,我們會使用觸發的 seekingseeked 影片事件調整影片亮度,而不是在這些按鈕的 click 事件監聽器中調整影片樣式。我的自訂 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 的酷炫之處在於,通知列並非媒體中繼資料和控制項的唯一顯示位置。媒體通知會自動同步至任何已配對的可穿戴式裝置。這類通知也會顯示在螢幕鎖定畫面上。

意見回饋