0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Meetの会議状態を自動検知してWebhookで通知するChrome拡張を作ってみた。

Posted at

はじめに

10月から人生初のフルリモート生活を送っているのですが、オンラインミーティング中に家族(特に子供)が乱入してくることがあります。

また、「何時から会議なの?」とよく聞かれることもあるので、オンラインミーティングの開始/終了を検知し、それを家族に通知したいと考えました。

少し調べてみてた感じ、Google Meet公式にはオンラインミーティングの開始/終了を通知してくれる機能はないみたいなので、今回はChromeの拡張機能を自作し、対応してみたいと思います。

作ったもの

ソースコードは以下で公開してみました。

オンラインミーティングの開始/終了を検知し、任意のWebhookに通知する仕組みです。

以下、少し内容紹介をします。

処理内容

会議の状態

このツールはmeet.google.comを開いている場合のみ動作し、以下の状態の状態遷移を監視します。

状態 意味
IDLE 初期状態
PRE_MEETING 会議開始前
IN_MEETING 会議中
content.js
const STATES = {
  IDLE: 'IDLE',
  PRE_MEETING: 'PRE_MEETING',
  IN_MEETING: 'IN_MEETING'
};

DOM要素で状態判定

状態判定は、各状態でのみ存在する、今すぐ参加通話から退出などのDOM要素の有無で判定します。

content.js
const SELECTORS = {
  LEAVE_BUTTON: 'button[aria-label*="Leave call"], button[aria-label*="通話から退出"]',
  JOIN_BUTTON_TEXT: ['Join now', '今すぐ参加']
};
content.js
  const detectState = () => {
    const leaveButton = document.querySelector(SELECTORS.LEAVE_BUTTON);

    if (leaveButton) return STATES.IN_MEETING;
    
    const hasJoinButton = Array.from(document.querySelectorAll('button span'))
      .some(span => SELECTORS.JOIN_BUTTON_TEXT.some(text => span.textContent.includes(text)));
    
    return hasJoinButton ? STATES.PRE_MEETING : STATES.IDLE;
  };

最初は動画や音声の有無で判定してたのですが、不安定な部分があったので今の方法に落ち着きました。

状態監視

状態監視はMutationObserverでDOM要素の変更を監視することで対応してみました。

content.js
  const observer = new MutationObserver(throttledCheckState);
  observer.observe(document.body, { childList: true, subtree: true });

また、変更検知後の判定は最大で100msの間隔をあけるようにし、負荷軽減をしています。

content.js
  let throttleTimer = null;
  const throttledCheckState = () => {
    if (throttleTimer) return;
    throttleTimer = setTimeout(() => {
      checkState();
      throttleTimer = null;
    }, 100);
  };

状態変更時にWebhook起動

以下のパターンの状態遷移を検知した場合のみ、Webhookを起動しています。

状態(旧) 状態(新) Webhook
初期状態 会議中 会議開始通知
会議開始前 会議中 会議開始通知
会議中 初期状態 会議終了通知
会議中 会議開始前 会議終了通知
content.js
  // 状態変化ハンドラ
  const handleStateChange = (oldState, newState) => {    
    if (newState === STATES.IN_MEETING && oldState !== STATES.IN_MEETING) {
      console.log('✅ 会議開始を検知');
      sendWebhook('meeting_started');
    } else if (oldState === STATES.IN_MEETING && newState !== STATES.IN_MEETING) {
      console.log('❌ 会議終了を検知');
      sendWebhook('meeting_ended');
    }
  };
  
  // 状態チェック
  const checkState = () => {
    const newState = detectState();
    if (newState !== currentState) {
      handleStateChange(currentState, newState);
      currentState = newState;
    }
  };

Webhook

Webhookは非同期で実施しています。

content.js
  const sendWebhook = async (status) => {
    try {
      const response = await fetch(WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          status: status,
          timestamp: new Date().toISOString()
        })
      });
    } catch (error) {
      console.error(`[Webhook] ${status} 送信エラー:`, error);
    }
  };

リクエストパラメタは以下のようになります。

{
  "status": "meeting_started or meeting_ended",
  "timestamp": "2025-10-28T14:30:45.123Z",
}

残課題

タブ閉じ対応

会議中にタブ閉じで会議を終了した場合の対応ができていません。

beforeunloadpagehideイベントを監視することで動作確認をしてみましたが上手く発砲しませんでした。

content.js
  window.addEventListener('beforeunload', () => {
    if (currentState === STATES.IN_MEETING) {
      const data = new Blob([JSON.stringify({
        status: 'meeting_ended',
        timestamp: new Date().toISOString()
      })], { type: 'application/json' });
      navigator.sendBeacon(WEBHOOK_URL, data);
    }
  });

まとめ

今回はミーティングの開始/終了を検知する仕組みづくりまで作成しました!
次は、実際に通知するところまで作っていきます!

誰かのお役に立てば幸いです〜!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?