はじめに
YouTube視聴中、急に音量デカすぎ広告が始まることがあるので自分の耳を保護するために作成しました。
広告開始を検知して即座にミュートしてくれるし、終わったらアンミュートもしてくれるので大変快適に。
初めての拡張機能作成でしたが、↓の記事を参考に無事作成できました。
YouTubeの動画広告をスキップする拡張機能を作ってみた
作業の流れ
基本的には参考元と同じです。
- 処理をJavaScriptで書く
- manifest.jsonを用意
- フォルダ/ファイルを用意
- 拡張機能を有効にする
私の場合は自分用に作成していますので、自分が使っているChromeで有効にするだけですね。
1. 処理をJavaScriptで書く
コードはこちら。
const hostName = location.hostname;
const subDomain = hostName.split('.')[0];
const returnPlayerId = (v) => {
    switch (v) {
        case 'music':
            return 'player';
            break;
        case 'www':
        default:
            return 'ytd-player';
            break;
    }
};
const clickMuteButton = () => {
    const muteButton = document.querySelector(".ytp-mute-button.ytp-button");
    if (muteButton) muteButton.click();
};
const isMute = () => {
    const muteButton = document.querySelector(".ytp-mute-button.ytp-button");
    if (muteButton) {
        const titleText = muteButton.getAttribute('title');
        const ariaLabel = muteButton.getAttribute('aria-label');
        const labelText = titleText || ariaLabel;  // マウスホバーの状態によってtitleText/ariaLabelどちらが存在するか変わる。存在する方を使う
        if (labelText) {
            return labelText.includes('解除');
        } else {
            console.error('Both title and aria-label attributes are not present');
        }
    } else {
        console.error('muteButton element is not found');
    }
};
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') {
            if (!isMute()) {
                clickMuteButton();
            }
        };
        // 特定のノード(広告)が削除された場合の処理
        if (mutation.removedNodes.length && mutation.removedNodes[0].className === 'ytp-ad-player-overlay') {
            if (isMute()) {
                clickMuteButton();
            }
        }
    });
});
const playerId = returnPlayerId(subDomain);
const initInterval = setInterval(() => {
    if (document.getElementById(playerId) != null) {
        const obTarget = document.getElementById(playerId);
        observer.observe(obTarget, obConfig);
        clearInterval(initInterval);
    }
}, 1000);
コードの解説
やりたいのは、広告が表示されている間はミュートして、広告が消えたらミュートを解除するという処理なので、
- 現在のミュート状態の判定
- 広告の終わりの検知
- ミュートボタンのクリック
あたりがポイントでしょうか。1.は地味にハマったポイントです。
上記以外の部分は参考元記事様が詳しいため説明は割愛します。
1. 現在のミュート状態の判定
const isMute = () => {
    const muteButton = document.querySelector(".ytp-mute-button.ytp-button");
    if (muteButton) {
        const titleText = muteButton.getAttribute('title');
        const ariaLabel = muteButton.getAttribute('aria-label');
        const labelText = titleText || ariaLabel;  // マウスホバーの状態によってtitleText/ariaLabelどちらが存在するか変わる。存在する方を使う
        if (labelText) {
            return labelText.includes('解除');
        } else {
            console.error('Both title and aria-label attributes are not present');
        }
    } else {
        console.error('muteButton element is not found');
    }
};
ハマりポイントはこれconst labelText = titleText || ariaLabel;。
コメントにも書いていますが、マウスホバーの有無によってtitle属性かaria-label属性どっちを取得できるかが変わります。
最初、デベロッパーツールを確認してaria-labelが判定に使えそうと思ったんですが、実際にやってみたら上手くいかず。aria-label attribute is not presentと表示されます。おかしいなーと思ってあちこちconsole.log(document.querySelector(".ytp-mute-button.ytp-button"))を確認してたら気づきました。
2. 広告の終わりの検知
        // 特定のノード(広告)が削除された場合の処理
        if (mutation.removedNodes.length && mutation.removedNodes[0].className === 'ytp-ad-player-overlay') {
            if (isMute()) {
                clickMuteButton();
            }
        }
広告が終わったら、つまり'ytp-ad-player-overlay'がDOMから削除された場合の動作を追加します。
removedNodesプロパティをチェックすれば広告の終了を検知できます。
3. ミュートボタンのクリック
const clickMuteButton = () => {
    const muteButton = document.querySelector(".ytp-mute-button.ytp-button");
    if (muteButton) muteButton.click();
};
参考記事ではdocument.getElementsByClassNameを使ってましたが、こっちではdocument.querySelectorを使いました。取得できればどっちでもいいか、と思ったんですがどうなんでしょう……
2. manifest.jsonを用意
{
    "manifest_version": 3,
    "name": "YTMuteAndSkip",
    "version": "1.0",
    "description": "Automatically controls video player based on specified triggers.",
    "permissions": [
        "activeTab",
        "storage"
    ],
    "action": {
        "default_icon": {
            "16": "icons/icon16.png",
            "48": "icons/icon48.png",
            "128": "icons/icon128.png"
        },
        "default_title": "Toggle Video Controls"
    },
    "background": {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": [
                "https://*.youtube.com/*"
            ],
            "js": [
                "content.js"
            ],
            "run_at": "document_end"
        }
    ],
    "icons": {
        "16": "icons/icon16.png",
        "48": "icons/icon48.png",
        "128": "icons/icon128.png"
    }
}
参考記事ではマニフェストファイルのバージョンを2にしていましたが、こちらでは3を使っています。Chromeで機能を有効にする際に、v2じゃなくてv3を使ってね、みたいなことを言われたのでそのようにしました。
backgroundは必須?らしいです。空でいいので、ディレクトリにファイルを置く必要があるっぽい。
こちらも参考に。Google Chrome 拡張機能(Chrome extensions V3)の作り方
3. フォルダ/ファイルを用意
└── src
     ├── content.js
     ├── manifest.json
     ├── background.js
     └── icons
          ├── icon128.png
          ├── icon48.png
          └── icon16.png
こんな感じ。iconsは必須ではないです。フォルダの名前もsrcじゃなくてもいいはず。
4. 拡張機能を有効にする
Chromeの場合です。Edgeとかでも大体同じはずです。
- ブラウザ右上の縦に3つの点が並んだアイコンをクリック
 ※このアイコン、ケバブメニューという名前らしいです。
- 拡張機能→拡張機能を管理
- 右上のデベロッパーモードをONにする
- パッケージ化されていない拡張機能を読み込む
- 3.で作成したフォルダを選択
 ※エラーがあった場合は、読み込み時に「エラー」ボタンが表示されます
 ※ソースを変更して、変更を反映させるにはリロードアイコンもしくは「更新」ボタンを押す必要があります。