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

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

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

Spotify 最近为其桌面版和网页版播放器客户端发布了 Spotify Miniplayer。迷你播放器旨在通过一个始终位于顶部的紧凑小窗口提供基本播放控件,让用户能够随时随地访问 Spotify。用户一直在呼吁推出此功能,借助此功能,用户可以在不同的窗口和应用中无缝处理多任务,同时在 Spotify 上欣赏喜爱的音乐人、播放列表和播客。

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

“画布黑客”

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

Spotify 将画布与 Media Session API 中设置的适当操作处理脚本结合使用,以控制画中画窗口中显示哪些播放器控件。这样,用户就可以在一个浮动窗口中看到专辑封面和播放器控件,并在专注于其他任务时控制播放。

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

这样,Spotify 便有了基本的迷你播放器。不过,这种方法存在以下几项限制:

  • 不支持在 PiP 窗口中显示视频字幕。由于 Spotify 必须为所有视频显示字幕,因此在视频开始播放后,他们不得不立即关闭 PiP 窗口。
  • 只有在本地播放时,播放器控件才会显示。Spotify 允许使用 Spotify Connect(和其他协议)进行远程播放,并希望用户也能控制此类播放
  • 不支持自定义 PIP 窗口的外观和风格。Spotify 只能显示海报图片并使用 Chrome 提供的播放器控件,无法添加 Spotify 品牌或其他播放器控件。

由于无法控制界面,也无法在此处添加 Spotify 专用功能(例如为曲目点赞),因此他们认为这种方法不适合他们的桌面客户端。

文档画中画:迷你播放器的演变

2023 年初,Spotify 得知 Google Chrome 重新有意推出一项新 API,该 API 允许在 PiP 窗口中显示任意 HTML 内容,称为 Document Picture-in-Picture API。这对 Spotify 来说是一个令人兴奋的进展,因为这将使他们能够完全控制 PiP 窗口的外观。Spotify 在 Chrome 团队的源代码试用期间与其合作,开发了基于 Document Picture-in-Picture API 的全新迷你播放器。

借助 Document PiP API,您可以打开一个新的始终置顶窗口,并在其中附加元素。由于 Spotify Web 播放器是 React Web 应用,因此 Spotify 使用了 ReactDOM 的 createPortal() 方法,将自定义组件从主要应用呈现到 PiP 窗口中,从而完全控制迷你播放器的外观和功能。

新的 Document Picture-in-Picture API 还解决了 Spotify 之前存在的问题:

  • 画中画窗口中的视频是常规视频元素,完全支持字幕。
  • 通过完全控制界面,即使使用 Spotify Connect 远程播放,也可以显示播放器控件。
  • Spotify 能够融入自己的外观和风格以及播放器控件,从而提升用户体验。
  • 他们能够为 Spotify 的桌面客户端引入对 Document PiP API 的支持,从而将迷你播放器引入到数百万桌面用户手中。

新版 Spotify 迷你播放器窗口的屏幕截图。

使用 React 创建画中画窗口

以下示例演示了如何在 React 中使用文档画中画功能,就像 Spotify 团队所做的那样。您将创建两个 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 迷你播放器窗口不同形状的屏幕截图。

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

致谢

感谢 Spotify 中参与构建迷你播放器的所有人。

Spotify 还想感谢 Google Chrome 团队的协作,以及在 Document Picture-in-Picture API 方面采纳了 Spotify 的反馈。