Spotify 如何使用 Picture-in-Picture API 构建 Spotify Miniplayer

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

Spotify 是全球最受欢迎的音频在线播放订阅服务,不断致力于改进用户消费音频和视频内容的方式。该应用提供内容丰富的音乐、播客和有声读物库,每天都能满足移动设备、PC 和其他平台上的数百万用户的需求。

最近 Spotify 发布了适用于桌面和网页播放器客户端的 Spotify 迷你播放器。迷你播放器旨在通过一个小小的紧凑窗口提供基本的播放控件,窗口始终保持在顶部,以便用户始终如一地访问 Spotify。这是一项用户长期以来呼声最高的功能,它可以让用户在不同窗口和应用中无缝地进行多任务处理,同时在 Spotify 上欣赏自己喜爱的音乐人、播放列表和播客。

下面将详细介绍迷你播放器的开发过程,从最初的“画布黑客”到基于新的 Document Picture-in-Picture API 构建的更高级、更人性化的版本。

“画布黑客”

迷你播放器的初始版本于 2019 年作为黑客项目在 Spotify 的 Web Player 上推出。我们的目标是使用浏览器的适用于 <video> 的画中画 (PiP) API,以便在始终在顶部的窗口中显示专辑封面。不过,此 API 主要是为视频元素设计的,无法显示专辑封面图片。Spotify 规避了这一点,他们将专辑封面呈现为画布元素,并使用 HTMLCanvasElement captureStream() 方法获取实时 MediaStream 对象。然后,此视频流会用作 PiP API 所用视频的来源。此方法基于 Google Chrome 的“音频播放列表”示例

Spotify 将画布与 Media Session API 中设置的适当操作处理程序结合在一起,以控制哪些播放器控件将在画中画窗口中显示。这为用户提供了一个带有专辑封面和播放器控件的浮动窗口,他们可以使用这些控件来控制播放,同时专注于其他任务。

基本 Spotify Miniplayer 窗口的屏幕截图。

这让 Spotify 得以打造一款基本的迷你播放器。不过,这种方法有几个局限性:

  • 画中画窗口不支持视频字幕。因为 Spotify 要求在所有视频上显示字幕,所以一旦视频开始播放,他们就不得不关闭画中画窗口。
  • 仅当播放是在本地进行时,播放器控件才会显示。Spotify 允许使用 Spotify Connect(和其他协议)进行远程播放,并希望用户也能控制此播放
  • 不支持自定义画中画窗口的外观和风格。Spotify 只能展示海报图片并使用 Chrome 提供的播放器控件,因此无法添加 Spotify 品牌信息或其他播放器控件。

缺乏对界面的控制,以及无法在此处添加 Spotify 专有功能(例如,点赞曲目)意味着,他们认为这种方法并不适合他们的桌面客户端。

记录画中画:迷你播放器的演变

2023 年初,Spotify 了解到 Google Chrome 重新有兴趣推出一款可在画中画窗口中显示任意 HTML 内容的新 API(称为 Document Picture-in-Picture API)。这一发展对于 Spotify 来说是激动人心的,因为它可以让他们完全控制画中画窗口的外观。Spotify 在源试用期间与 Chrome 团队合作,开发了基于 Document Picture-in-Picture API 构建的全新迷你播放器。

借助 Document PiP API,您可以打开一个新的“始终开启”窗口,以便向其附加元素。由于 Spotify Web Player 是 React Web 应用,因此 Spotify 使用 ReactDOM 的 createPortal() 方法将自定义组件从主应用渲染到画中画窗口中,从而完全控制迷你播放器的外观和功能。

新的 Document Picture-in-Picture API 还解决了 Spotify 以前出现的问题:

  • 画中画窗口中的视频是常规视频元素,完全支持字幕。
  • 凭借对界面的完全控制,即使在使用 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 不断推进和创新,他们仍然致力于增强 Miniplayer 的功能,并计划进一步优化其功能和用户体验。尽管他们还不能承诺推出某些特定功能,但他们对迷你播放器的未来发展充满期待。

Spotify Miniplayer 窗口的不同形状的屏幕截图。

Document Picture-in-Picture API 提供了灵活性和可控性,可帮助您创建更直观且更易用的迷你播放器。希望其他浏览器供应商能够注意到此 API 带来的机会,并考虑加入对它的支持。这样一来,无论用户选择哪种浏览器,Spotify 都能为所有用户提供一致且更出色的体验。

致谢

感谢 Spotify 全体员工参与开发迷你播放器。

Spotify 也要感谢 Google Chrome 团队的合作,并感谢 Spotify 将 Spotify 的反馈纳入 Document Picture-in-Picture API 方面的考量。