
2026/01/21 5:05
**Google Meet リアクション:WebRTCチャネルを逆解析し、絵文字を実装する**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary
Google Meetの最近の絵文字リアクション拡張により、適切なものを見つけることが難しくなっています。新しいChrome拡張機能 「Google Meet Reactions」 は、最もよく使われる絵文字をすばやくアクセスできる検索可能なリストを追加することでこの問題に対処します。このツールはWebRTCのデータチャネル(
RTCPeerConnection.prototype.createDataChannel)にフックしてリアクションメッセージをインターセプトし、これらはprotobufでエンコードされたバイナリパケットとして送信されます。受信したリアクションを再構築する際にはデバイス/ユーザーIDを作成し、gzipで圧縮してからMessageEventをディスパッチします。検索機能はBM25ランキングとLevenshtein距離(タイプミス耐性)を用いて1,900以上の絵文字をカバーし、頻繁に選択されるものは自動的に優先表示されます。この拡張機能はページロード時(document_start)に実行されるため、Meetがチャネルを確立する前から準備完了です。ピクチャー・イン・ピクチャーモードでも動作しますが、Extended Reactionsが有効なGoogle Workspaceアカウントでのみ機能し、個人のGmailユーザーはデフォルトの9つの絵文字しか表示されません。
Links:
- Chrome Web Store: Google Meet Reactions
- Website:
googlemeetreactions.com - Project page for more details.
本文
私は Google Meet に多くの時間を費やしています――1日 3〜4 時間ほど。最近、Google は大量の新しい絵文字リアクションを追加し、私たちはそれらを積極的に使っています。しかし、それらを探す UX が…あまり良いものではありません。仲間が「かっこいい」新絵文字を送ってくる度に、私はちょうどその 1 個を見つけるのに苦労しています。
もちろん、熱心なプログラマーなら UX を即座に改善できます!結果として登場したのが Google Meet Reactions ― Chrome 拡張機能で、Meet のインターフェースに直接絵文字検索を追加します。私にとって最も重要なのは――自分が使った絵文字や仲間が送った絵文字を覚えておき、それらを検索結果の上位に表示してくれる点です。
UI から WebRTC へ
最初に思いついたアイデアは単純でした:DOM 上の絵文字ボタンを見つけて、クリックをシミュレートする。
しかし Google Meet は
.b1bzTb や .VfPpkd-rymPhb のような難読化されたクラス名で圧倒的に隠蔽されており、ポップアップ内の全絵文字リストを掘り下げるのはあまり良いアプローチではありません。
そこで通話中に
chrome://webrtc-internals を開いてみたところ、数十もの RTCDataChannel の中に 「reactions」 という名前のチャンネルがあることに気付きました――そして絵文字はこのチャンネルを介して送信されているようです。もしこのチャネルへの参照を取得し、メッセージフォーマットを解析できれば、プログラムからリアクションを送ることが可能になります。
DataChannel の作成を傍受
WebRTC の DataChannel は
RTCPeerConnection.prototype.createDataChannel() で生成されます。Meet が呼び出す前にこのメソッドをパッチし、参照を保存します:
const origCreate = RTCPeerConnection.prototype.createDataChannel; RTCPeerConnection.prototype.createDataChannel = function (label, options) { const channel = origCreate.call(this, label, options); if (label === 'reactions') { console.log('Gotcha!'); capturedChannel = channel; } return channel; };
アイデアはシンプルですが、コード注入に少し問題があります。
チャネルが使われる前に注入
Chrome 拡張機能では複数の方法でページへコードを注入できます。
Content スクリプトは隔離されたワールドで動作し、ページ上の
RTCPeerConnection にアクセスできません。そのためスクリプトを直接ページコンテキストに挿入する必要があります。
標準的なアプローチ:
const script = document.createElement('script'); script.src = chrome.runtime.getURL('rtcHook.js'); document.documentElement.appendChild(script);
しかし
script.src = URL はネットワークリクエストを伴います。その間に Meet が「reactions」チャンネルを既に作成してしまっていると、フックが失敗します。
解決策は二つの要素の組み合わせです:
- マニフェストで
を指定し、Content スクリプトを DOM がロードされる前に実行runAt: 'document_start' - フックスクリプトを Web‑accessible resource として宣言し、できるだけ早く読み込む
// wxt.config.ts export default defineConfig({ manifest: { web_accessible_resources: [ { resources: ['rtcHook.js'], matches: ['https://meet.google.com/*'], }, ], }, });
ほとんどの場合、フックは Meet がチャネルを作る前にインストールされます。競合状態が残る可能性はありますが、実際には起きていません。
絵文字の送信
チャンネルが取得できて開いている (
channel.readyState === 'open') と確認したら、受信メッセージを監視しつつ、自分で送る内容も観測できます。メッセージフォーマットはネストされた長さ付きフィールドを持つ比較的単純な protobuf でした。以下のように構築して送信します:
function buildEmojiMessage(emoji) { const emojiBytes = [...new TextEncoder().encode(emoji)]; // 内側から外側へ protobuf を作成 – 監視した手法をコピー // 10 = フィールド1、ワイヤタイプ2(長さ付き) // 8 = フィールド1、ワイヤタイプ0(varint)、値2 const i1 = [10, emojiBytes.length, ...emojiBytes]; const i2 = [10, i1.length, ...i1]; const i3 = [8, 2, 18, i2.length, ...i2]; const i4 = [10, i3.length, ...i3]; return new Uint8Array([10, i4.length, ...i4]); } // 送信はワンライナー capturedChannel.send(buildEmojiMessage('🔥').buffer);
自己注入
絵文字を送る際、自分自身もそのリアクションを見ることが欲しいですよね。
実際の Google Meet は内部コードで処理していますが、フェイクユーザーとフェイク ID を使って受信 WebRTC メッセージハンドラを呼び出すだけでも十分です。
// デバイス 0 は実際に使用されていないので競合しません const SELF_DEVICE_ID = 'spaces/self/devices/0'; // 英語 UI では「You」で送信したように見えます const SELF_USER_NAME = 'You'; function buildFullEmojiMessage(emoji) { const emojiBytes = [...new TextEncoder().encode(emoji)]; const deviceIdBytes = [...new TextEncoder().encode(SELF_DEVICE_ID)]; const userNameBytes = [...new TextEncoder().encode(SELF_USER_NAME)]; // Emoji ブロック: [0a LL emoji] [10 01] const emojiBlock = [0x0a, emojiBytes.length, ...emojiBytes, 0x10, 0x01]; // フィールド4(デバイス情報): [0a LL deviceId] [10 01] const field4Content = [0x0a, deviceIdBytes.length, ...deviceIdBytes, 0x10, 0x01]; const field4 = [0x22, field4Content.length, ...field4Content]; // フィールド6(ユーザー情報): [0a LL deviceId] [12 LL userName] [18 01] const field6Content = [ 0x0a, deviceIdBytes.length, ...deviceIdBytes, 0x12, userNameBytes.length, ...userNameBytes, 0x18, 0x01, ]; const field6 = [0x32, field6Content.length, ...field6Content]; // ネスト構造を作る const emojiContainer = [0x0a, emojiBlock.length, ...emojiBlock]; const innerContent = [...emojiContainer, ...field4, ...field6]; const inner = [0x0a, innerContent.length, ...innerContent]; const outer = [0x0a, inner.length, ...inner]; return new Uint8Array(outer); } async function injectToSelf(emoji) { if (!capturedChannel) return false; const fullMessage = buildFullEmojiMessage(emoji); // gzip 圧縮 const cs = new CompressionStream('gzip'); const cw = cs.writable.getWriter(); cw.write(fullMessage); cw.close(); const cr = cs.readable.getReader(); const chunks = []; while (true) { const { done, value } = await cr.read(); if (done) break; chunks.push(value); } const compressed = new Uint8Array(chunks.flatMap((c) => [...c])); // [18, length, gzip_data](フィールド2、長さ付き)でラップ const wrapped = new Uint8Array([18, compressed.length, ...compressed]); const fakeEvent = new MessageEvent('message', { data: wrapped.buffer }); // Meet のクライアントはこのイベントを他人から送られたリアクションとして処理します capturedChannel.dispatchEvent(fakeEvent); return true; }
タイポ許容検索も実装
数個の必須機能だけに留まるわけにはいきません。
拡張機能は以下を備えています:
- 1900+ 絵文字を対象としたタイポ補正付き検索(BM25 + Levenshtein 距離)
- パーソナライズ:自分が使った絵文字や仲間が送ったものを優先表示し、同じリアクションを簡単に共有できるように
- ピクチャ―イン―ピクチャ モードでも動作
制限事項
拡張機能は Google Workspace アカウントで Extended Reactions が有効なミーティングでのみ機能します。
個人 Gmail アカウントから作成されたミーティングでは標準の 9 絵文字しか利用できず、拡張機能の価値はほとんどありません。
役立ちましたか?
仕事で Google Meet を使っているなら、ぜひ試してみてください。サイドプロジェクト作者として、追加すべき機能や不要なものを削除するべき点について意見をいただけると嬉しいです。何がうまくいき、どこを改善できるでしょうか?
リンク
- Google Meet Reactions – 拡張機能のインストール
- googlemeetreactions.com – 拡張機能公式サイト
- プロジェクトページ – 詳細情報