コフス技術ブログ

Picture-in-Picture APIで画像表示

昨今のブラウザではPicture-in-Picture APIを使いPinP表示する事が簡単にできますが、現状対応しているコンテンツは動画のみです。

基本的にvideo要素に表示されている内容のみがPinP表示されるわけですが、canvas要素を用いcaptureStream()を行えば自由なコンテンツをPinP表示する事が可能になります。

たとえば画像をPinP表示する例を示します。

以下の様な特定の画像を表示しつつ、ボタンをクリックするとPinP表示をトグルできるHTMLとJSを用意します。

index.html
<img
id="pip-image-element"
src="https://source.unsplash.com/400x300?random"
style="width: 400px; height: 300px;"
>
<button id="pip-button-element">PinP Toggle</button>
index.ts
interface CanvasElement extends HTMLCanvasElement {
  captureStream(frameRate?: number): MediaStream
}

class PinpImage {
  pipImageElement: HTMLImageElement | null
  pipButtonElement: HTMLButtonElement | null

  image: HTMLImageElement

  constructor() {
    this.pipImageElement = document.querySelector('#pip-image-element')
    this.pipButtonElement = document.querySelector('#pip-button-element')

    this.image = new Image()

    if (!this.pipImageElement) {
      return
    }

    this.image.crossOrigin = 'Anonymous'
    this.image.addEventListener('load', () => this.imageLoaded(), false)
    this.image.src = this.pipImageElement.src
  }

  imageLoaded(): void {
    const canvas = <CanvasElement>document.createElement('canvas')
    canvas.height = this.image.height
    canvas.width = this.image.width

    // Draw something to canvas.
    const ctx = canvas.getContext('2d')
    if (!ctx) {
      return
    }

    ctx.scale(1, 1)
    ctx.drawImage(this.image, 0, 0)

    const video = document.createElement('video')
    video.muted = true
    video.autoplay = true
    video.srcObject = canvas.captureStream(60)
    video.play()

    this.pipButtonElement?.addEventListener('click', async () => {
      try {
        if (video !== document.pictureInPictureElement) {
          await video.requestPictureInPicture()
        } else {
          await document.exitPictureInPicture()
        }
      } catch (error) {
        // some code
      } finally {
        // some code
      }
    })
  }
}

window.addEventListener('load', () => {
  new PinpImage()
})

注目する箇所は以下です。

  1. 画像をdrawImage()canvas要素に表示
  2. それをcaptureStream()video要素のメディアソースとして指定
  3. ボタンクリックでvideo要素をPinP表示

canvasにて表現できるものであれば比較的容易にPinP表示もできる事が分かります。

PinPの様子

アイデア次第でPinPの世界も広がりそうです。

参考: https://www.w3.org/TR/picture-in-picture/
参考: https://googlechrome.github.io/samples/picture-in-picture/audio-playlist
参考: https://sbfl.net/blog/2021/04/30/javascript-html-picture-in-picture/