ピクチャー イン ピクチャーで動画を視聴する

François Beaufort
François Beaufort

ピクチャー イン ピクチャー(PIP)を使用すると、フローティング ウィンドウ(他のウィンドウの上に常に表示されるウィンドウ)で動画を視聴できるため、他のサイトやアプリを操作しながら視聴中の動画を監視できます。

Picture-in-Picture Web API を使用すると、ウェブサイト上の動画要素のピクチャー イン ピクチャーを開始、制御できます。公式のピクチャー イン ピクチャー サンプルで試すことができます。

背景

2016 年 9 月、Safari は macOS Sierra で WebKit API を介してピクチャー イン ピクチャーのサポートを追加しました。6 か月後、Android O のリリースにより、Chrome は ネイティブ Android API を使用してモバイルでピクチャー イン ピクチャー動画を自動的に再生できるようになりました。6 か月後、Google は、ウェブ デベロッパーがピクチャー イン ピクチャーに関するエクスペリエンスをすべて作成、制御できるように、Safari と互換性のある Web API を構築して標準化するという意向を発表しました。これで終わりです

コードを確認する

ピクチャー イン ピクチャーを開始

まず、動画要素と、ユーザーが動画を操作するための方法(ボタン要素など)を簡単に作成しましょう。

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

ピクチャー イン ピクチャーは、ユーザー操作に応じてのみリクエストしてください。videoElement.play() から返されたPromise ではリクエストしないでください。これは、Promise がユーザー ジェスチャーをまだ伝播していないためです。代わりに、以下に示すように pipButtonElement のクリック ハンドラで requestPictureInPicture() を呼び出します。ユーザーが 2 回クリックした場合の処理は、デベロッパーの責任で行う必要があります。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

Promise が解決すると、Chrome は動画を小さなウィンドウに縮小します。このウィンドウは、ユーザーが移動して他のウィンドウの上に配置できます。

これで完了です。パフォーマンスは良好です。ここまでお読みいただき、有給休暇を満喫してください。残念ながら、そうとは限りません。次のいずれかの理由で、Promise が拒否されることがあります。

  • ピクチャー イン ピクチャーはシステムでサポートされていません。
  • 制限付きの権限ポリシーにより、ドキュメントでピクチャー イン ピクチャーを使用できません。
  • 動画メタデータがまだ読み込まれていない(videoElement.readyState === 0)。
  • 動画ファイルが音声のみである。
  • 動画要素に新しい disablePictureInPicture 属性が存在します。
  • 呼び出しがユーザー ジェスチャー イベント ハンドラ(ボタンのクリックなど)で行われなかった。Chrome 74 以降、この機能は、ピクチャー イン ピクチャーに要素がまだない場合にのみ適用されます。

以下の機能のサポートセクションでは、これらの制限に基づいてボタンを有効または無効にする方法について説明します。

try...catch ブロックを追加して、これらの潜在的なエラーをキャプチャし、ユーザーに状況を通知しましょう。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

動画要素は、ピクチャー イン ピクチャーの有無にかかわらず同じ動作をします。イベントが発生し、呼び出しメソッドが機能します。ピクチャー イン ピクチャー ウィンドウの状態の変化(再生、一時停止、シークなど)が反映されます。また、JavaScript でプログラム的に状態を変更することもできます。

ピクチャー イン ピクチャーを終了

次に、ボタンでピクチャー イン ピクチャーの開始と終了を切り替えられるようにします。まず、読み取り専用オブジェクト document.pictureInPictureElement が動画要素かどうかを確認する必要があります。有効になっていない場合は、上記のようにピクチャー イン ピクチャーを開始するリクエストを送信します。それ以外の場合は、document.exitPictureInPicture() を呼び出して離脱をリクエストします。これにより、動画は元のタブに戻ります。このメソッドも Promise を返します。

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

ピクチャー イン ピクチャー イベントをリッスンする

通常、オペレーティング システムはピクチャー イン ピクチャーを 1 つのウィンドウに制限しているため、Chrome の実装もこのパターンに従っています。つまり、一度に再生できるピクチャー イン ピクチャー動画は 1 つのみです。ユーザーが、リクエストしなくてもピクチャー イン ピクチャーを終了することを想定する必要があります。

新しい enterpictureinpicture イベント ハンドラと leavepictureinpicture イベント ハンドラを使用すると、ユーザーに合わせてエクスペリエンスを調整できます。動画カタログのブラウジングから、ライブ配信チャットの表示まで、さまざまな操作が可能です。

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

ピクチャー イン ピクチャー ウィンドウをカスタマイズする

Chrome 74 では、Media Session API を使用して制御できる、ピクチャー イン ピクチャー ウィンドウの再生/一時停止、前のトラック、次のトラックのボタンがサポートされています。

ピクチャー イン ピクチャー ウィンドウのメディア再生コントロール
図 1: ピクチャー イン ピクチャー ウィンドウのメディア再生コントロール

デフォルトでは、動画で MediaStream オブジェクト(getUserMedia()getDisplayMedia()canvas.captureStream() など)が再生されている場合や、動画の MediaSource の長さが +Infinity に設定されている場合(ライブフィードなど)を除き、ピクチャー イン ピクチャー ウィンドウに再生/一時停止ボタンが常に表示されます。再生/一時停止ボタンを常に表示するには、以下のように「再生」と「一時停止」のメディア イベントの両方に Somesee メディア セッション アクション ハンドラを設定します。

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

[前のトラック] ウィンドウ コントロールと [次のトラック] ウィンドウ コントロールの表示も同様です。これらのメディア セッション アクション ハンドラを設定すると、ピクチャー イン ピクチャー ウィンドウに表示され、これらのアクションを処理できるようになります。

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

実際に動作を確認するには、公式のメディア セッション サンプルをお試しください。

ピクチャー イン ピクチャー ウィンドウのサイズを取得する

動画がピクチャー イン ピクチャーの状態になったときと終了したときに動画の画質を調整するには、ピクチャー イン ピクチャー ウィンドウのサイズを把握し、ユーザーがウィンドウを手動でサイズ変更した場合に通知する必要があります。

次の例は、ピクチャー イン ピクチャー ウィンドウの作成時またはサイズ変更時に、その幅と高さを取得する方法を示しています。

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

ピクチャー イン ピクチャー ウィンドウのサイズを少し変更するたびに、個別のイベントが発生するため、サイズ変更のたびに負荷の高いオペレーションを実行するとパフォーマンスの問題が発生する可能性があるため、サイズ変更イベントに直接フックしないことをおすすめします。つまり、サイズ変更オペレーションは、イベントを非常に高速に何度もトリガーします。この問題に対処するには、スロットリングとデバウンスなどの一般的な手法を使用することをおすすめします。

機能のサポート

Picture-in-Picture Web API がサポートされていない可能性があるため、これを検出してプログレッシブ エンハンスメントを提供する必要があります。サポートされている場合でも、ユーザーによってオフにされたり、権限ポリシーによって無効にされたりすることがあります。幸い、新しいブール値 document.pictureInPictureEnabled を使用してこれを判断できます。

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

動画の特定のボタン要素に適用すると、ピクチャー イン ピクチャー ボタンの公開設定を次のように処理できます。

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

MediaStream 動画のサポート

動画を再生する MediaStream オブジェクト(getUserMedia()getDisplayMedia()canvas.captureStream() など)も、Chrome 71 でピクチャー イン ピクチャーをサポートしています。つまり、ユーザーのウェブカメラ動画ストリーム、ディスプレイ動画ストリーム、キャンバス要素を含むピクチャー イン ピクチャー ウィンドウを表示できます。以下に示すように、ピクチャー イン ピクチャーを開始するために動画要素を DOM に接続する必要はありません。

ピクチャー イン ピクチャー ウィンドウにユーザーのウェブカメラを表示する

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

ディスプレイをピクチャー イン ピクチャー ウィンドウに表示する

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

ピクチャー イン ピクチャー ウィンドウにキャンバス要素を表示する

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

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

// Later on, video.requestPictureInPicture();

canvas.captureStream()Media Session API を組み合わせると、Chrome 74 でオーディオ プレイリスト ウィンドウを作成できます。公式のオーディオ再生リストのサンプルをご覧ください。

ピクチャー イン ピクチャー ウィンドウ内の音声プレイリスト
図 2.ピクチャー イン ピクチャー ウィンドウのオーディオ プレイリスト

サンプル、デモ、Codelab

公式の Picture-in-Picture サンプルで、Picture-in-Picture Web API を試す。

デモと Codelab も後日公開されます。

次のステップ

まず、実装ステータス ページで、Chrome や他のブラウザで現在実装されている API の部分を確認します。

今後の予定は次のとおりです。

ブラウザ サポート

Picture-in-Picture Web API は、Chrome、Edge、Opera、Safari でサポートされています。詳しくは、MDN をご覧ください。

リソース

ピクチャー イン ピクチャーの開発と、この記事の作成にご協力いただいた Mounir Lamouri 氏と Jennifer Apacible 氏に感謝いたします。標準化の取り組みに関わったすべての方々に心より感謝いたします。