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

François Beaufort
François Beaufort

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

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

背景

2016 年 9 月、Safari は macOS Sierra で WebKit API を介してピクチャー イン ピクチャーのサポートを追加しました。6 か月後、Chrome は Android O のリリースにより、ネイティブ 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 に感謝します。また、この記事も紹介してください。標準化の取り組みに関わったすべての方々に心より感謝いたします。