ピクチャー イン ピクチャー(PIP)を使用すると、ユーザーはフローティング ウィンドウで動画を視聴できるため(常に他のウィンドウの上に表示される)、他のサイトやアプリを操作しながら視聴しているコンテンツを確認できます。
Picture-in-Picture Web API を使用して、ウェブサイト上の動画要素のピクチャー イン ピクチャーの開始と制御を行うことができます。Google の公式のピクチャー イン ピクチャー サンプルでお試しください。
背景
2016 年 9 月、Safari は macOS Sierra で WebKit API を介したピクチャー イン ピクチャーのサポートを追加しました。6 か月後、Android O のリリースに伴い、Chrome ではネイティブ Android API を使用して、ピクチャー イン ピクチャー動画をモバイルで自動的に再生しました。6 か月後、Google は Safari と互換性のあるウェブ 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 を使用して制御できます。
デフォルトでは、再生/一時停止ボタンは常にピクチャー イン ピクチャー ウィンドウに表示されます。ただし、動画の MediaStream オブジェクト(getUserMedia()
、getDisplayMedia()
、canvas.captureStream()
など)を再生している場合や、動画の MediaSource 再生時間が +Infinity
に設定されている場合を除きます(ライブフィードなど)。再生/一時停止ボタンが必ず表示されるようにするには、「再生」と「一時停止」の両方のメディア イベントに対して、somesee の Media Session アクション ハンドラを以下のように設定します。
// 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.
});
実際の動作を確認するには、公式の Media Session サンプルをご覧ください。
ピクチャー イン ピクチャーのウィンドウ サイズを取得する
ピクチャー イン ピクチャーの開始時と終了時に動画の画質を調整する場合は、ピクチャー イン ピクチャーのウィンドウ サイズを把握し、ユーザーが手動でウィンドウのサイズを変更したときに通知されるようにする必要があります。
以下の例は、ピクチャー イン ピクチャー ウィンドウの作成時またはサイズ変更時に、そのウィンドウの幅と高さを取得する方法を示しています。
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 動画のサポート
Chrome 71 では、動画を再生する MediaStream オブジェクト(getUserMedia()
、getDisplayMedia()
、canvas.captureStream()
など)もピクチャー イン ピクチャーをサポートしています。つまり、ユーザーのウェブカメラの動画ストリーム、ディスプレイ動画ストリーム、さらにはキャンバス要素を含むピクチャー イン ピクチャー ウィンドウを表示できます。以下に示すように、ピクチャー イン ピクチャーを開始するために動画要素を 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 ではオーディオ プレイリスト ウィンドウを作成できます。公式のオーディオ プレイリストのサンプルをご覧ください。
サンプル、デモ、Codelab
Picture-in-Picture Web API を試すには、公式の Picture-in-Picture サンプルをご覧ください。
その後にデモと Codelab で学習します。
次のステップ
まず、実装ステータスのページを参照して、現在 Chrome や他のブラウザに実装されている API の部分を確認します。
今後予定されている内容は次のとおりです。
- ウェブ デベロッパーは、ピクチャー イン ピクチャーのカスタム コントロールを追加できます。
- 任意の
HTMLElement
オブジェクトをフローティング ウィンドウに表示する新しい Web API が提供されます。
ブラウザ サポート
Picture-in-Picture Web API は、Chrome、Edge、Opera、Safari でサポートされています。 詳しくは、MDN をご覧ください。
リソース
- Chrome 機能のステータス: https://www.chromestatus.com/feature/5729206566649856
- Chrome の実装バグ: https://crbug.com/?q=component:Blink>Media>PictureInPicture
- ピクチャー イン ピクチャー ウェブ API の仕様: https://wicg.github.io/picture-in-picture
- 仕様に関する問題: https://github.com/WICG/picture-in-picture/issues
- サンプル: https://googlechrome.github.io/samples/picture-in-picture/
- 非公式のピクチャー イン ピクチャーのポリフィル: https://github.com/gbentaieb/pip-polyfill/
ピクチャー イン ピクチャーの作業とこの記事の手助けをした Mounir Lamouri と Jennifer Apacible に感謝します。標準化の取り組みに 関わってくれたすべての人に感謝します