はじめに
皆さんはPiP(ピクチャー イン ピクチャー)をご存知でしょうか。以降はPiPと呼びます。
ふとYoutubeを使っていて以前はPiPが標準の機能として備わっていた気がしますが、現在では「ミニプレイヤー」という機能があります。ミニプレイヤーはブラウザ内でYoutubeを開いているタブ内であればミニプレイヤーとして他の動画をブラウジングすることが可能ですが、別のタブに切り替えたり、ブラウザを最小化するとミニプレイヤーとして見ることができません。そこでPiPをする方法を調べているとChrome拡張機能で提供されていることを知りました。この自給自足シリーズでは実際に自分で『作ってみる』ことでより技術への理解が深まると思いPiPの機能を持つChrome拡張機能を実際に作って見ることにしました。
対象読者
- Chrome拡張機能を作ってみたい人
- PiPの仕組みが気になる人
- JavaScriptの基礎はわかるけど、ブラウザAPIはあまり触ったことがない人
作ったもの
YouTube・Netflix・Prime Videoなど17サービスに対応した、ワンクリックでPiPを起動・停止できるChrome拡張機能です。
アイコンをクリックするだけでPiPが起動し、もう一度クリックすると停止します。ポップアップや設定画面は一切なく、シンプルな設計にしました。
Chrome拡張機能の構造
まずChrome拡張機能がどういうファイル構成で成り立っているかを理解する必要があります。今回のプロジェクトはこんな感じです。
video-pip/
├── manifest.json # 拡張機能の設定ファイル
├── background.js # アイコンクリックのハンドラ
├── content.js # ページに注入するスクリプト
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
たったこれだけです。外部ライブラリも使っていないので、実質3ファイルで動いています。
manifest.json ─ 拡張機能の設計図
manifest.json はChrome拡張機能における「設計図」のようなファイルです。拡張機能の名前・バージョン・権限・どのファイルを使うかなど、すべての基本情報をここで定義します。
{
"manifest_version": 3,
"name": "ピクチャーinピクチャー",
"version": "1.0.0",
"description": "ワンクリックで動画をピクチャーinピクチャーに。YouTube・Netflix・Prime Video など対応",
"permissions": ["scripting", "tabs", "activeTab"],
"host_permissions": [
"https://www.youtube.com/*",
"https://www.netflix.com/*",
...
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://www.youtube.com/*", ...],
"js": ["content.js"]
}
],
"action": {
"default_title": "PiP を切替"
}
}
"manifest_version": 3
Chromeの拡張機能APIのバージョンです。現在はManifest V3(MV3)が主流で、古いV2とはAPIの設計がけっこう変わっています。たとえばV2では background.js を「常時起動のページ」として動かしていましたが、V3では Service Worker という仕組みに変わりました。必要なときだけ起動して、処理が終わったら停止するイメージです。
"permissions"
拡張機能がブラウザに対して「これをやらせてください」と申告するところです。
-
"scripting"─ ページにスクリプトを実行する権限 -
"tabs"─ タブの情報を取得する権限 -
"activeTab"─ ユーザーが操作した瞬間だけアクティブタブにアクセスできる権限
"activeTab"がポイントで、ユーザーがアイコンをクリックした瞬間だけタブにアクセスできます。常時監視はしていないので、プライバシー的にも安心です。
"host_permissions"
スクリプトを実行できるドメインのリストです。ここに書いたサービス以外のページでは拡張機能は動きません。"https://www.youtube.com/*" の /* はそのドメイン配下のすべてのパスを意味します。
"background" と "content_scripts" の違い
ここが最初にちょっと混乱したポイントです。
- background(Service Worker) ─ ブラウザのバックグラウンドで動く。特定のページには属さず、アイコンクリックなどのイベントを受け取る役割。
- content_scripts ─ 指定したページを開いたときに、そのページに注入されるスクリプト。ページのDOMにアクセスできる。
background.js ─ クリックを受け取る司令塔
background.js はService Workerとして動作し、アイコンがクリックされたときの処理を担当します。
アイコンクリックのイベントを受け取る
chrome.action.onClicked.addListener(async (tab) => {
// クリックされたときの処理
});
chrome.action.onClicked はツールバーのアイコンがクリックされたときに発火するイベントです。addListener でコールバック関数を登録しています。引数の tab にはクリックされたときのタブ情報が入ります。
ページにスクリプトを注入する
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: togglePiP,
});
chrome.scripting.executeScript が今回の肝です。target でどのタブに対して実行するかを指定し、func で実行する関数を渡します。これにより、background.js(Service Worker)から直接DOMにアクセスできないため、PiPを切り替える関数をページ側に「送り込む」わけです。
PiPを実現するブラウザAPI
ここがこの記事のメインです。PiPを実現しているのはブラウザ標準の Picture-in-Picture API です。
動画要素を取得する
const videos = document.querySelectorAll('video');
ページ内のすべての <video> 要素を取得します。動画配信サービスのページには広告用の小さな動画など、複数の <video> 要素が含まれていることがあります。
一番大きい動画を選ぶ
const video = Array.from(videos).reduce((prev, curr) => {
return (curr.offsetWidth * curr.offsetHeight) > (prev.offsetWidth * prev.offsetHeight)
? curr : prev;
});
offsetWidth * offsetHeight で各動画の表示面積を計算し、一番大きいものを選んでいます。広告や小さなサムネイル動画を無視して、メインの動画を自動的に判別するための工夫です。
PiPの起動・停止を切り替える
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
} else {
await video.requestPictureInPicture();
}
document.pictureInPictureElement は現在PiP表示中の要素を返します。すでにPiPが起動していれば exitPictureInPicture() で停止し、そうでなければ requestPictureInPicture() でPiPを起動します。このたった2つのメソッドがPiPのすべてです。
まとめると:
| API | 役割 |
|---|---|
video.requestPictureInPicture() |
PiPを起動する |
document.exitPictureInPicture() |
PiPを停止する |
document.pictureInPictureElement |
現在PiP中の要素を取得(起動中かチェック) |
全体の流れまとめ
[アイコンをクリック]
│
▼
background.js(Service Worker)がクリックイベントを受け取る
│
│ chrome.scripting.executeScript() でページにPiP関数を注入
▼
対象ページ(YouTube・Netflixなど)
│
├─ document.querySelectorAll('video') で動画要素を取得
├─ 最大サイズの video 要素を選択
│
├─ PiP中なら → document.exitPictureInPicture()
└─ 通常時は → video.requestPictureInPicture()
作ってみた感想
正直最初はPiPの仕組みを全く知らなかったのですが、調べてみると requestPictureInPicture() というブラウザAPIが1行で用意されていて、「思ったより簡単じゃん」というのが第一印象でした。
Chrome拡張機能は取っ付きにくそうに見えて、実はHTMLとJavaScriptだけで作れます。気になった人はぜひ自分でも作ってみてください。