使用子母畫面觀看影片

François Beaufort
François Beaufort

使用者可透過子母畫面 (PiP) 功能,在浮動式視窗 (一律位於其他視窗頂端) 中觀看影片,讓他們在與其他網站或應用程式互動時,仍能持續觀看影片。

您可以使用Picture-in-Picture Web API,在網站上啟動及控制 Picture-in-Picture 影片元素。歡迎試試我們的官方子母畫面範例

背景

2016 年 9 月,Safari 透過 macOS Sierra 中的 WebKit API 新增了子母畫面支援功能。六個月後,Chrome 在 Android O 發布後,使用原生 Android API 在行動裝置上自動播放子母畫面影片。六個月後,我們宣布了我們的意圖,要建構並標準化與 Safari 相容的 Web API 功能,讓網路開發人員能夠建立及控制 Picture-in-Picture 的完整體驗。大功告成!

進入程式碼

進入子母畫面模式

我們先從影片元素和使用者與影片互動的方式 (例如按鈕元素) 開始。

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

請只在回應使用者手勢時要求子母畫面,絕對不要在 videoElement.play() 傳回的承諾中要求。這是因為承諾尚未傳播使用者手勢。請改為在 pipButtonElement 的點擊事件處理常式中呼叫 requestPictureInPicture(),如下所示。您必須負責處理使用者點兩次按鈕的情況。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

當承諾解析時,Chrome 會將影片縮小成一個小視窗,讓使用者可以移動並將其置於其他視窗上方。

完成後,做得好!您可以停止閱讀,並前往享受難得的假期。但很遺憾,實際情況並非總是如此。承諾可能會拒絕,原因如下:

  • 系統不支援子母畫面功能。
  • 由於受限的權限政策,文件無法使用 Picture-in-Picture。
  • 影片中繼資料尚未載入 (videoElement.readyState === 0)。
  • 影片檔案僅含音訊。
  • 影片元素上會顯示新的 disablePictureInPicture 屬性。
  • 呼叫並未在使用者手勢事件處理常式 (例如按鈕點擊) 中發出。自 Chrome 74 版起,只有在子母畫面中沒有元素時,這項設定才適用

請參閱下方的「功能支援」一節,瞭解如何根據這些限制啟用/停用按鈕。

讓我們新增 try...catch 區塊,擷取這些潛在錯誤,並讓使用者瞭解發生了什麼事。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

無論是否處於子母畫面模式,影片元素的行為都相同:事件會觸發,呼叫方法會運作。它會反映子母畫面視窗中的狀態變更 (例如播放、暫停、快轉等),也可以透過程式設計在 JavaScript 中變更狀態。

退出子母畫面模式

接著,讓按鈕切換進入和退出子母畫面。我們必須先檢查只讀物件 document.pictureInPictureElement 是否為影片元素。如果不是,我們會傳送要求,以便進入上述的子母畫面模式。否則,我們會呼叫 document.exitPictureInPicture() 要求離開,這表示影片會回到原始分頁。請注意,這個方法也會傳回承諾。

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

監聽子母畫面事件

作業系統通常會將 Picture-in-Picture 限制在一個視窗,因此 Chrome 的實作方式會遵循這項模式。也就是說,使用者一次只能播放一個子母畫面影片。即使您未要求使用者退出,他們還是可以退出畫中畫模式。

新的 enterpictureinpictureleavepictureinpicture 事件處理常式可讓我們為使用者打造客製化體驗。包括瀏覽影片目錄,或顯示直播聊天室等。

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

自訂子母畫面視窗

Chrome 74 支援在分割畫面視窗中顯示的播放/暫停、上一首和下一首曲目按鈕,您可以使用 Media Session API 進行控制。

子母畫面視窗中的媒體播放控制項
圖 1. 子母畫面視窗中的媒體播放控制項

根據預設,除非影片播放 MediaStream 物件 (例如 getUserMedia()getDisplayMedia()canvas.captureStream()),或是影片的 MediaSource 持續時間設為 +Infinity (例如即時動態),否則系統一律會在畫中畫視窗中顯示播放/暫停按鈕。為確保播放/暫停按鈕一律顯示,請為「Play」和「Pause」媒體事件設定 somesee Media Session 動作處理常式,如下所示。

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

顯示「上一曲」和「下一曲」視窗控制項的方式也類似。為這些項目設定媒體工作階段動作處理常式,即可在子母畫面視窗中顯示這些項目,並處理這些動作。

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

如要查看實際運作情形,請試用官方的媒體工作階段範例

取得子母畫面視窗大小

如果您想在影片進入及離開子母畫面時調整影片品質,就必須瞭解子母畫面視窗大小,並在使用者手動調整視窗大小時收到通知。

以下範例說明如何在建立或調整子母畫面視窗大小時,取得其寬度和高度。

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

建議您不要直接連結至調整大小事件,因為每次對 Picture-in-Picture 視窗大小進行微調都會觸發個別事件,如果每次調整大小時都執行耗用大量資源的作業,可能會導致效能問題。換句話說,調整大小作業會非常快速地不斷觸發事件。建議您使用常見的技術 (例如節流和去抖動) 來解決這個問題。

功能支援

系統可能不支援 Picture-in-Picture Web API,因此您必須偵測這項情況,才能提供漸進式增強功能。即使系統支援這項功能,使用者仍可能將其關閉,或因權限政策而停用。幸運的是,您可以使用新的布林值 document.pictureInPictureEnabled 來判斷這項資訊。

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

將此方法套用至影片的特定按鈕元素,即可處理圖片內圖片按鈕的顯示方式。

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

MediaStream 影片支援

播放 MediaStream 物件的影片 (例如 getUserMedia()getDisplayMedia()canvas.captureStream()) 也支援 Chrome 71 中的子母畫面。也就是說,您可以顯示含有使用者網路攝影機影片串流、顯示影片串流,甚至是畫布元素的子母畫面視窗。請注意,如要進入子母畫面,視訊元素不必附加至 DOM,如下所示。

在子母畫面視窗中顯示使用者的網路攝影機畫面

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

在子母畫面視窗中顯示螢幕畫面

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

在子母畫面視窗中顯示畫布元素

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();

// Later on, video.requestPictureInPicture();

canvas.captureStream()Media Session API 結合,即可在 Chrome 74 中建立音訊播放清單視窗。請參閱官方音訊播放清單範例

子母畫面視窗中的音訊播放清單
圖 2. 子母畫面視窗中的音訊播放清單

範例、試用版和程式碼研究室

請查看我們的官方子母畫面範例,試用子母畫面 Web API。

我們會在後續提供相關的示範和程式碼研究室。

後續步驟

首先,請查看導入狀態頁面,瞭解 Chrome 和其他瀏覽器目前已導入哪些 API 部分。

以下說明近期可能發生的異動:

瀏覽器支援

Chrome、Edge、Opera 和 Safari 皆支援 Picture-in-Picture Web API。詳情請參閱 MDN

資源

感謝 Mounir Lamouri 和 Jennifer Apacible 在開發 Picture-in-Picture 功能和撰寫本文時提供的協助。也要感謝所有參與標準化工作的人員。