Spotify 如何運用 Picture-in-Picture API 打造 Spotify 迷你播放器

Guido Kessels
Guido Kessels
François Beaufort
François Beaufort

Spotify 是全球最受歡迎的音訊串流訂閱服務,不斷致力改善使用者觀看音訊和影片內容的方式。這項服務提供豐富的音樂、Podcast 和有聲書,每天都吸引數百萬名使用者在行動裝置、電腦及其他平台上使用。

Spotify 最近為電腦版和網路播放器用戶端推出了 Spotify Miniplayer。迷你播放器的設計旨在在小巧的小型視窗內提供重要的播放控制項,讓使用者持續存取 Spotify。這項功能一直以來都有求進步,可讓使用者在不同的視窗和應用程式中流暢地進行多工處理,同時在 Spotify 聆聽喜愛的藝人、播放清單和 Podcast。

以下會詳細說明迷你播放器的開發流程,從最初的「畫布入侵」到較容易使用的全新 Document Picture-in-Picture API 上即可。

「畫布入侵」

迷你播放器於 2019 年在 Spotify 的網頁播放器推出,做為駭客專案推出。我們的目標是運用瀏覽器的子母畫面 (PiP) API 的 <video> API,在螢幕不間斷的視窗中顯示專輯封面。不過,這個 API 主要是為影片元素設計,無法顯示專輯封面圖片。為規避此問題,Spotify 會將專輯封面算繪至畫布元素,並使用 HTMLCanvasElement captureStream() 方法取得即時 MediaStream 物件。接著,這個串流就會做為子母畫面 API 使用的影片來源。這個方法使用 Google Chrome 的「音訊播放清單」範例建立而成。

Spotify 將畫布與 Media Session API 中設定的適當動作處理常式結合,以控制子母畫面視窗中要顯示哪些播放器控制項。使用者可透過浮動式視窗查看專輯封面和播放器控制項,並能同時控製播放方式,同時專心處理其他工作。

基本 Spotify Miniplayer 視窗的螢幕截圖。

這讓 Spotify 擁有基本的迷你播放器。然而,此方法有一些限制:

  • 子母畫面視窗中不支援影片字幕。因為根據 Spotify 規定,所有影片都必須顯示字幕,觀眾在影片開始播放後,就會被強制關閉子母畫面視窗。
  • 只有在使用者在本機播放時,畫面上才會顯示播放器控制項。Spotify 允許使用 Spotify Connect (及其他通訊協定) 進行遠端播放,因此希望使用者也能控製播放內容
  • 無法自訂子母畫面視窗的外觀和風格。Spotify 只能顯示圖片並使用 Chrome 提供的播放器控制項,因而無法新增 Spotify 品牌宣傳元素或其他播放器控制項。

由於缺乏對使用者介面的掌控,也無法在這裡新增 Spotify 專屬功能 (例如對曲目表示喜歡),因此他們認為這種方法不適合電腦版用戶端。

文件子母畫面:迷你播放器的演進

2023 年初,Spotify 越來越重視 Google Chrome 推出新的 API,這個 API 允許在子母畫面視窗中顯示任意 HTML 內容 (稱為 Document Picture-in-Picture API)。Spotify 可以透過這項新功能全面掌控子母畫面視窗的外觀,所以這項開發工作相當令人期待。Spotify 在來源試用期間與 Chrome 團隊合作,開發出以 Document Picture-in-Picture API 為基礎的全新迷你播放器。

你可以開啟 Document PiP API,開啟一個新的一律在頂部視窗來附加元素。由於 Spotify Web Player 是 React 網頁應用程式,因此 Spotify 透過 ReactDOM 的 createPortal() 方法,將自訂元件從主應用程式算繪到子母畫面視窗,以便完全掌控迷你播放器的外觀和功能。

新的 Document Picture-in-Picture API 也能解決 Spotify 先前的問題:

  • 子母畫面視窗中的影片是一般的影片元素,可以完整支援字幕。
  • 由於你可以完全掌控 UI,即使使用 Spotify Connect 從遠端播放,仍然會顯示播放器控制項。
  • Spotify 整合了外觀和風格與播放器控制項,提升了使用者體驗。
  • 他們成功在 Spotify 的桌面用戶端支援 Document PiP API,讓數以百萬計的電腦使用者也能享受迷你播放器。

全新 Spotify Miniplayer 視窗的螢幕截圖。

使用 React 建立子母畫面視窗

以下範例說明如何像 Spotify 團隊一樣,在 React 中使用文件子母畫面。您將建立兩個 React 元件:MyFeaturePiPContainer

MyFeature 元件負責管理子母畫面視窗。這會顯示切換子母畫面視窗的按鈕,並轉譯 PiPContainer 元件。也會訂閱子母畫面視窗的 "pagehide" 事件,在視窗關閉時更新狀態。

const MyFeature = () => {
  const [pipWindow, setPiPWindow] = useState<Window | null>(
    documentPictureInPicture.window
  );

  const handleClick = useCallback(async () => {
    if (pipWindow) {
      pipWindow.close();
    } else {
      const newWindow = await documentPictureInPicture.requestWindow();
      setPiPWindow(newWindow);
    }
  }, [pipWindow]);

  useEffect(() => {
    const handleWindowClose = (): void => {
      setPiPWindow(null);
    };

    pipWindow?.addEventListener("pagehide", handleWindowClose);

    return () => {
      pipWindow?.removeEventListener("pagehide", handleWindowClose);
    };
  }, [pipWindow]);

  return (
    <>
      <button onClick={handleClick}>
        {pipWindow ? "Close PiP Window" : "Open PiP Window"}
      </button>
      <PiPContainer pipWindow={pipWindow}>Hello World 👋!</PiPContainer>
    </>
  );
};

PiPContainer 元件使用 ReactDOM 的 createPortal() 方法,將內容算繪到子母畫面視窗。

type Props = PropsWithChildren<{
  pipWindow: Window | null;
}>;

const PiPContainer = ({ pipWindow, children }: Props) => {
  useEffect(() => {
    if (pipWindow) {
      cloneStyles(window.document, pipWindow.document);
    }
  }, [pipWindow]);

  return pipWindow ? createPortal(children, pipWindow.document.body) : null;
};

後續步驟

隨著 Spotify 持續發展及革新,他們會繼續致力強化迷你播放器,並計劃進一步修正其功能和使用者體驗。儘管他們還無法承諾推出特定功能,但也很期待迷你播放器未來的可能性。

Spotify Miniplayer 視窗各種形狀的螢幕截圖。

Document Picture-in-Picture API 具有彈性和控制權,才能打造更直覺易用的迷你播放器。我們希望其他瀏覽器廠商會記下這個 API 帶來的商機,並考慮如何融入其支援。如此一來,無論使用者選用哪種瀏覽器,Spotify 都能持續提供更一致且更優質的體驗。

特別銘謝

感謝所有參與 Spotify 打造迷你播放器的 Spotify 使用者。

另外,Spotify 也感謝 Google Chrome 團隊參與協作,並將 Spotify 提供的意見回饋納入 Document Picture-in-Picture API。