使用子母畫面觀看影片

François Beaufort
François Beaufort

子母畫面 (PiP) 功能可讓使用者在浮動視窗中 (一律位於其他視窗上方) 觀看影片,以便在與其他網站或應用程式互動時隨時留意正在觀看的影片。

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

背景

2016 年 9 月,Safari 透過 macOS Sierra 中的 WebKit API 新增子母畫面支援。六個月後,Chrome 使用原生 Android API 發布 Android O 時,會在行動裝置上自動播放子母畫面影片。六個月後,我們宣布我們的目標是建構出與 Safari 相容的 Web API,並為其標準化,讓網頁開發人員能夠建立及管理子母畫面的完整體驗。我們到了!

取得程式碼

進入子母畫面

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

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

只在回應使用者手勢時要求子母畫面,且一律不得在 videoElement.play() 傳回的 promise 中。這是因為承諾「尚未」傳播使用者手勢。請改為在 pipButtonElement 的點擊處理常式中呼叫 requestPictureInPicture(),如下所示。您必須負責處理使用者點擊兩次時會發生的情況。

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

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

承諾解析後,Chrome 會將影片縮小為小視窗,讓使用者可以任意移動畫面,並在其他視窗上方放置影片。

大功告成!做得好!你們停止朗讀,好好休息一下。可惜,情況並非如此。這項承諾可能會因為下列原因而拒絕

  • 系統不支援子母畫面。
  • 由於設有限制的權限政策,因此文件無法使用子母畫面。
  • 尚未載入影片中繼資料 (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() 來退出,這表示影片會顯示在原始分頁中。請注意,這個方法也會傳回 promise。

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

聆聽「子母畫面」活動

作業系統通常會將子母畫面功能限制在單一視窗,因此 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 (例如直播動態饋給)。如要確保系統一律會顯示播放/暫停按鈕,請針對「播放」和「暫停」媒體事件設定一些媒體工作階段動作處理常式,如下所示。

// 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.
}

建議您不要直接掛斷重新調整大小的事件,因為每次對子母畫面視窗大小所做的微小變更,都會觸發單獨的事件;如果每次調整大小都要耗費大量資源,可能就會導致效能問題。換句話說,調整大小作業會反覆地快速觸發事件。建議您使用節流和去彈跳等常見技巧來解決這個問題。

功能支援

可能不支援子母畫面 Web API,因此必須偵測該 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 影片支援

在 Chrome 71 中播放 MediaStream 物件的影片 (例如 getUserMedia()getDisplayMedia()canvas.captureStream()) 也支援子母畫面。這表示您可以查看子母畫面視窗,其中包含使用者的網路攝影機影片串流、多媒體影片串流,甚至是畫布元素。請注意,影片元素不必附加至 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 部分。

不久後就會看到下列內容:

瀏覽器支援

子母畫面 Web API 適用於 Chrome、Edge、Opera 和 Safari。詳情請參閱 MDN

資源

感謝 Mounir Lamouri 和 Jennifer Apacible 對子母畫面的貢獻,也感謝以下文章提供的協助。感謝大家參與標準化工作付出的心力。