🖼️

Document Picture-in-Picture APIを使ってなんでもPiPにしちゃうChrome拡張機能を試す

2024/12/18に公開2

🎅GMOペパボ エンジニア Advent Calendar 2024 18日目の記事です。

昨日は新卒同期のyukyanによる、XX駆動学習をいろいろ試している話でした。

自分もAdvent Calendar駆動学習ということで、Document Picture-in-Picture APIについて学んでみました。

Document Picture-in-Picture API

https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API

Document Picture-in-Picture APIは、任意のHTMLコンテンツをPiPウィンドウとして開くことを可能にします。

PiPといえば動画のイメージがあると思いますが、動画のPiPと同様に常に前面に表示される小さなウィンドウとして、動画以外のコンテンツも表示させることができるようになったということです。

Chromeでは116以降で利用できるようになりました。

https://caniuse.com/mdn-api_documentpictureinpicture

従来のPicture-in-Picture APIでは、<video>要素にしか対応していませんでしたが、任意のHTMLコンテンツに対応したということで、Webアプリケーション開発の幅がより広がりそうな予感がしています。

使い方

window.documentPictureInPicture.requestWindowでPiP Windowを取得します。

const pipWindow = await window.documentPictureInPicture.requestWindow({
    width: 360,
    height: 600,
})

あとはこれに対して、PiP内に表示させたい要素をappendChildするだけです。めちゃくちゃ簡単。

pipWindow.document.body.appendChild(targetRef)

気をつけないといけない点としては、コードの通り、PiP Windowに対してHTMLを子として入れ込んでいるだけなので、styleなどは別途ページ内から持ってきたりしてあげる必要があります。

開発段階ではrequestWindow methodにcopyStyleSheetsというoptionがあったそうですが、なくなってしまったようです。

試してみる

これを活用した画期的なWebアプリケーションを作成してみようかと思いましたが、全然思いつかなかったので、ユーザーが任意のページの任意のコンテンツをPiPウィンドウとして開くことができるChrome拡張機能を作成してみました。

https://github.com/nacal/any-pip

demo

ストアに公開しようと思いましたが、記事公開時点では間に合っていないので、触ってみたい方は直接追加してみてください。

開発は、最近流行りのChrome拡張機能開発フレームワーク WXTを利用して作成しています。

https://wxt.dev/

Document Picture-in-Picture APIの利用方法は前述の通りで、拡張機能でWebページ内の任意のHTMLコンテンツを選択し、それをPiPとして開くことができるようになっています。

工夫した点としては、前述の通りstyleを合わせて持ってくる必要があるため、元ページからstyleを丸ごとPiP Windowに持ってくるようにしています。

private static async copyStyles(pipWindow: Window): Promise<void> {
    const styles = document.querySelectorAll('style, link[rel="stylesheet"]')

    for (const style of styles) {
      if (style instanceof HTMLStyleElement) {
        pipWindow.document.head.appendChild(style.cloneNode(true))
      } else if (style instanceof HTMLLinkElement) {
        try {
          const response = await fetch(style.href)
          if (response.ok) {
            const css = await response.text()
            const newStyle = pipWindow.document.createElement('style')
            newStyle.textContent = css
            pipWindow.document.head.appendChild(newStyle)
          }
        } catch (error) {
          console.error('Failed to load stylesheet:', style.href, error)
        }
      }
    }
  }

また、これだけでは親要素がstyleを持っている場合(backgroundなど)に、それが適用されないので親要素も持ってくる力技でどうにかしています。

private static cloneElementWithParents(
    element: Element,
    pipWindow: Window
): {
    clone: Element
    wrapper: Element
} {
    const elements: Element[] = []
    let currentElement: Element | null = element
    while (currentElement && currentElement !== document.body) {
      elements.unshift(currentElement)
      currentElement = currentElement.parentElement
    }

    let wrapper: HTMLElement | null = null
    let lastElement: Element | null = null

    elements.forEach((el, index) => {
      const newElement = pipWindow.document.createElement(
        el.tagName.toLowerCase()
      )

      if (!wrapper) {
        wrapper = newElement as HTMLElement
      } else {
        lastElement!.appendChild(newElement)
      }

      if (el.className) newElement.className = el.className
      if (el.id) newElement.id = el.id
      lastElement = newElement

      if (index === elements.length - 1) {
        newElement.innerHTML = el.innerHTML
      }
    })

    return {
      clone: lastElement!,
      wrapper: wrapper!,
    }
}

これでページ内の特定のスナップショットをPiPウィンドウとして開くことができるような状態になっています。

そのため、PiPウィンドウ内での操作などは元ページには反映されません、これを実現するにはいくつか追加で工夫をする必要があります。

Zennの記事なんかは、とても綺麗にPiP内で開くことができることを確認しているので、記事を参考にしながら何かをするときに便利だったりするかもしれません。

まとめ

今回、Document Picture-in-Picture APIを触ってみて、導入自体は思っていたよりも簡単に実現できることがわかりました。

実際のWebページとの連携をするところまで実装できれば、PiPでの任意の要素表示を活用したWebアプリケーションが登場してくるのも近い気がします。

https://adventar.org/calendars/10036

明日の記事はyumuちゃんです。パブリッククラウド関連の何かを書くとのこと、楽しみ。

Discussion

おっとっとおっとっと

インストールしてみたのですが、manifestがありませんと警告が出てインストールできませんでした。

nacalnacal

.output/chrome-mv3を選択してインストールを試してみてください🙏