YouTubeの動画広告をスキップする拡張機能を作ってみた

# はじめに

アウトプットの一環としてObserver系のAPIを使って何か拡張機能を作りたいと思っていたので、YouTubeの広告をスキップするChrome拡張機能を作り、Chromeウェブストアに公開しました。

作ったもの：[YT Ad Skip](https://chrome.google.com/webstore/detail/bohoehddljlgadmcdlccfoikallknnib/)


# 作業の流れ

拡張機能の公開までにやったことは大まかに3つです。

1. 処理をJavaScriptで書く
2. manifest.jsonを用意
3. Chromeウェブストアに公開申請


# 1.処理をJavaScriptで書く

完成したコードがこちらです。

```yt-ad-skip.js
{

  const hostName = location.hostname;
  const subDomain = hostName.split('.')[0];

  const newStyle = document.createElement('style');
  newStyle.innerText = '.ytp-ad-overlay-container{display:none!important;}';
  document.getElementsByTagName('head').item(0).appendChild(newStyle);

  const returnPlayerId = (v) => {
    switch (v) {
      case 'music':
        return 'player';
        break;
      case 'www':
      default:
        return 'ytd-player';
        break;
    }
  };

  const clickSkipButton = () => {
    const $skipButton = document.getElementsByClassName("ytp-ad-skip-button-container")[0];
    if( $skipButton ) $skipButton.click();
  };

  const obConfig = {
    childList: true,
    subtree: true
  };
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if( mutation.addedNodes.length && mutation.addedNodes[0].className === 'ytp-ad-player-overlay' ) clickSkipButton();
    });
  });

  const playerId = returnPlayerId(subDomain);
  const initInterval = setInterval(() => {
    if( document.getElementById(playerId) != null ) {
      const obTarget = document.getElementById(playerId);
      observer.observe(obTarget, obConfig);
      clearInterval(initInterval);
    }
    clickSkipButton();
  }, 1000);

};
```


## コードの解説

### サブドメインを取得

```yt-ad-skip.js
const hostName = location.hostname;
const subDomain = hostName.split('.')[0];
```

「YouTube」と「YouTube Music」で処理を分岐させるため、サブドメインを取得しています。


### オーバーレイ広告を非表示

```yt-ad-skip.js
const newStyle = document.createElement('style');
newStyle.innerText = '.ytp-ad-overlay-container{display:none!important;}';
document.getElementsByTagName('head').item(0).appendChild(newStyle);
```

オーバーレイ広告を非表示にするCSSをhead要素に追加しています。
Observerを使うという趣旨から外れるのですが、とりあえずはCSSで対応しました。


### 監視対象のid名を返す処理を定義

```yt-ad-skip.js
const returnPlayerId = (v) => {
  switch (v) {
    case 'music':
      return 'player';
      break;
    case 'www':
    default:
      return 'ytd-player';
      break;
  }
};
```

サブドメインで条件分岐させ、監視対象であるプレイヤーのid名を返します。


### スキップボタンをクリックする処理を定義

```yt-ad-skip.js
const clickSkipButton = () => {
  const $skipButton = document.getElementsByClassName("ytp-ad-skip-button-container")[0];
  if( $skipButton ) $skipButton.click();
};
```

広告の再生時、「広告をスキップ」ボタン（class名：`ytp-ad-skip-button-container`）が動的に生成されます。
スキップボタンが存在するときに要素をクリックする`clickSkipButton()`を定義します。


### MutationObserverを設定

```yt-ad-skip.js
const obConfig = {
  childList: true,  // 監視対象ノードの子ノードの追加・削除を監視します。
  subtree: true  // 監視対象ノードの子孫ノードも監視対象とします。
};
```

インスタンスメソッド`observe()`に渡すオプションを設定します。


```yt-ad-skip.js
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if( mutation.addedNodes.length && mutation.addedNodes[0].className === 'ytp-ad-player-overlay' ) clickSkipButton();
  });
});
```

MutationObserverをインスタンス化。
Callbackとして、スキップボタンのラッパー（class名：`ytp-ad-player-overlay`）が追加されたとき`clickSkipButton()`を実行します。


### ポーリングを用意

今回の場合、MutationObserverでの監視対象はYouTube側が動的に生成している要素です。
場合によっては要素が生成される前にMutationObserverが実行される（拡張機能が実行される）ことがあるため、その対応としてポーリングを用意しています。
※拡張機能が実行されるタイミングは自分で指定できたりするんですが、その辺は後述します。

```yt-ad-skip.js
const playerId = returnPlayerId(subDomain);
const initInterval = setInterval(() => {
  if( document.getElementById(playerId) != null ) {
    const obTarget = document.getElementById(playerId);
    observer.observe(obTarget, obConfig);
    clearInterval(initInterval);
  }
  clickSkipButton();
}, 1000);
```

監視対象のid名を変数に入れ、setIntervalで1秒毎に`clickSkipButton()`を繰り返し実行すると同時に、監視対象の存在をチェックしています。
監視対象が存在した場合Observerを実行し監視開始、ポーリングをストップします。


# 2.manifest.jsonを用意する

これまでに記述した処理をブラウザに拡張機能として認識させるために`manifest.json`を用意します。
処理を記述したJavaScriptと同じディレクトリに作成します。

```manifest.json
{
  "manifest_version": 2,  // マニフェストファイルのバージョン（現在有効なバージョンは2のみ）
  "name": "YT Ad Skip",
  "version": "1.3.0",
  "description": "This plugin will skip YouTube ads.",
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "content_scripts": [{
    "matches": ["https://*.youtube.com/*"],  // 拡張機能を実行するサイトを設定
    "js": ["yt-ad-skip.js"]  // JavaScriptファイルのパスを指定
  }]
}
```

必須項目は`manifest_version`、`name`、`version`のみです。
その他は適宜記述します。


# 拡張機能が実行されるタイミングについて

拡張機能が実行されるタイミングを説明する前提として、YouTubeでの読み込みフローは以下のようになっています。

1. HTMLのロード
2. HTMLをパース
  1. JavaScriptをロード
  2. JavaScriptをパース・実行
  3. CSSをロード
  4. CSSをパース
  5. CSSOMを構築
3. 2-2 で実行されたJavaScriptが要素を追加~~、DOMを再構築~~
4. DOMの構築（DOMContentLoaded）
5. 画像やフレームなどを読み込み
6. ページの読み込み完了（load）

※便宜上上記の順番付けをしていますが、実際には各ステップが同時に実行されたり、前ステップの途中で次のステップが実行されたりします。


# 実行するタイミングを指定する

拡張機能が実行されるタイミングを指定するには2つの方法があります。
1. JavaScriptで指定
2. `manifest.json`で指定


## 1.JavaScriptで指定

DOMContentLoadedやload、onreadystatechange等のイベントによってタイミングを指定します。


## 2.`manifest.json`で指定

`manifest.json`の`run_at`プロパティでタイミングを指定します。

```manifest.json（記述例）
{
  "content_scripts": [{
    "run_at": "document_idle",
  }]
}
```

`run_at`プロパティに設定できる値は以下のとおりです。

|      設定値       |  説明  |  実行タイミング  |  備考  |
| :--------------: | ----- | -------------- | ----- |
|  document_idle   |  ブラウザが、`document_end`から`window.onload`イベントが発生した直後までの間に拡張機能を実行するタイミングを選択します。<br>拡張機能を実行する正確なタイミングは、ドキュメントの複雑さと読み込みにかかる時間に依存し、ページの読み込み速度に合わせて最適化されます。<br><br>`document_idle`で実行されるコンテンツ拡張機能は、`window.onload`イベントを待機する必要はなく、DOMが完了した後に実行されることが保証されています。`window.onload`の後に拡張機能を実行する必要がある場合、拡張機能は`document.readyState`プロパティを使用して、`onload`が既に発生しているかどうかをチェックすることができます。  |  `4`〜`6`の間で実行されます。  |  初期値/推奨  |
|  document_start  |  拡張機能はCSSファイルの後に実行されますが、他のDOMが構築されたり他のスクリプトが実行される前に実行されます。  |  `2-5`の直後に実行されます。  |  -  |
|  document_end    |  拡張機能は、DOMが完成した直後に実行されますが、画像やフレームなどのサブリソースがロードされる前に実行されます。  |  `4`〜`5`の間で実行されます。  |  -  |


## 実際に実行されるタイミング

前述のように拡張機能が実行されるタイミングを指定しても、動的な要素が生成されてから実行されるという保証はありません。
YouTubeの読み込みフローでいう`6`の後に要素が生成されることもあり、それはloadイベントや`manifest.json`だけでは対応できません。
そのため、ポーリングを用意して監視対象の存在をチェックする必要があります。


# 3.Chromeウェブストアに公開申請

JavaScriptと`manifest.json`の他にアイコン画像を用意したら、あとは公開申請を出すだけです。
細かい手順は[こちら](https://developer.chrome.com/webstore/publish)を参考にしました。

1. 開発者アカウントを作成（$5支払う必要があります）
2. 必要ファイルをzipにして開発者アカウントのダッシュボードからアップロード
3. 拡張機能の名前や説明を入力して申請

審査が通ればChromeウェブストアに自作の拡張機能が追加されます。


# 作ってみて所感

巷の拡張機能を見て「作った人すげぇ」とか思ってましたが、公開するだけなら意外と簡単でした。
技術的に悩んだところもあり、拡張機能が実行されるタイミングの部分はレンダリングについても理解する必要があって、色々と調べてみて知見が広がりました。
Observer系のアウトプットとしてはじめましたが、拡張機能の作り方についても学ぶことができプラスになったと思います。


# 参考URL
- [課金なんて必要ない？YouTubeの広告自動スキップ＆バナー自動削除のChrome拡張作ってみた](https://qiita.com/HirosuguTakeshita/items/fd1da5f6b727fbe611e4)
- [動的なページの読み込みが完了してからChrome拡張機能を実行する方法](https://qiita.com/3mc/items/c3c580ca5de4a2d3990d)
- [Content Scripts - Google Chrome](https://developer.chrome.com/extensions/content_scripts#run_time)
- [Publish in the Chrome Web Store](https://developer.chrome.com/webstore/publish)