如何打造最佳的網路行動媒體體驗?非常容易!這完全取決於使用者參與度,以及您對網頁上媒體的重視程度。我想大家都同意,如果使用者造訪網站是為了觀看影片,就必須提供身歷其境且能吸引他們回訪的體驗。
本文將說明如何透過漸進式方式強化媒體體驗,並運用多種 Web API 打造更身歷其境的體驗。因此,我們將打造簡單的行動播放器體驗,提供自訂控制項、全螢幕和背景播放功能。您可以立即試用範例,並在 GitHub 存放區中找到程式碼。
自訂控制項
如您所見,我們要用於媒體播放器的 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
})`;
});
播放/暫停影片
現在影片中繼資料已載入,接著新增第一個按鈕,讓使用者根據播放狀態,使用 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 的優點在於,通知匣並非唯一顯示媒體中繼資料和控制項的位置。媒體通知會自動同步到任何配對的穿戴式裝置。也會顯示在螢幕鎖定畫面上。