Chrome拡張「MeetScribe」は、会議タブの音声をキャプチャしてGemini APIで文字起こし+要約を自動生成するツールです。開発中に遭遇した技術的なハマりポイントを、同じようなChrome拡張を開発する方の参考になればと思い共有します。
1. Gemini APIのモデル名が次々と使えなくなる問題
最初はClaudeCode指定で gemini-2.0-flash で勝手に実装されますが、APIを叩くと「This model is no longer available to new users」というエラーが返ってきました。
gemini-2.0-flash-001 に変更しても同じエラー。最終的に gemini-2.5-flash が正解でした。まだCludeも最新版ではないのか。
さらに厄介だったのが、Gemini 2.5 Flashは「思考モード(thinking)」がデフォルトONになっていること。従来のAPIレスポンスでは parts[0].text にテキストが入っていましたが、2.5 Flashでは parts[0] が思考パーツになり、実際のテキストが取れません。
解決策: thinkingBudget: 0 で思考モードを無効化し、さらにパーツを走査して thought フラグがないテキストパーツを探すロジックを追加しました。
2. Offscreen Documentのタイミング問題 — 音声が全く入らない
Chrome拡張のManifest V3では、音声録音のような処理は「Offscreen Document」という隠しドキュメントで行います。開発中、録音しても音声データが全く入らない問題が発生しました。
原因: ensureOffscreenDocument() はドキュメントの作成完了を待ちますが、スクリプトのロード完了は待たないのです。つまり、Offscreen Document内のメッセージリスナーが登録される前に start-capture メッセージが送信されていました。
解決策: メッセージ送信を最大10回リトライ(300ms間隔)する sendToOffscreen() 関数を実装。Offscreen側にも sendResponse() を追加して到達確認できるようにしました。
3. Service Workerのスリープ問題 — Manifest V3最大の罠
これが最も苦労したポイントです。
ウィンドウを行き来すると、録音がいつの間にか停止している現象が発生しました。
Manifest V3のService Workerは、約30秒の非アクティブで自動停止されます。従来のManifest V2のbackground pageは常駐できましたが、V3では根本的にアーキテクチャが異なります。Service Workerが停止すると、メモリ上の変数(recordingState、tabIdなど)がすべて消失します。
解決策は3層構造:
- chrome.storage.session で録音状態を永続化(Service Worker再起動後も状態を復元)
- chrome.alarms で24秒ごとのkeep-alive ping(Service Workerを起こし続ける)
- Offscreen Document側でWeb Lock APIを使ってドキュメント破棄を防止
この3つを組み合わせることで、ようやく安定した長時間録音が実現できました。
4. AudioContextのバックグラウンド一時停止
Service Workerの問題を解決した後も、「録音データはあるが中身が無音」というケースが残りました。
原因: ブラウザのウィンドウを切り替えると、AudioContextが自動的に suspended 状態になります。これはブラウザの省電力機能ですが、録音中のChrome拡張にとっては致命的です。
解決策: onstatechange イベントハンドラと、2秒ごとのヘルスチェックインターバルで、AudioContextが suspended になったら即座に resume() を呼ぶようにしました。同時に、MediaRecorderが paused になるケースも対処しています。
5. タブキャプチャの再利用エラー
1回目の録音は成功するのに、2回目で「Cannot capture a tab with an active stream」というエラーが出る問題です。
原因: 前回の録音で使ったOffscreen Documentが残っていて、音声ストリームを保持し続けていました。
解決策: 録音開始前に必ず closeOffscreenDocument() を実行し、前回のストリームを確実にクリーンアップするようにしました。シンプルですが、気づくまでに時間がかかりました。
6. mimeTypeの互換性問題
MediaRecorderは audio/webm;codecs=opus というmimeTypeを返しますが、Gemini APIがこのcodecパラメータ付きのmimeTypeを正しく処理できないことがわかりました。結果として、実際の音声とは全く関係のないサンプルのような文字起こしが返ってきます。
解決策: Gemini APIに送る際は mimeType.split(';')[0] で audio/webm のみに正規化するようにしました。
まとめ: Manifest V3のService Worker制約が最大の敵
6つのハマりポイントの中で、最も根本的だったのはManifest V3のService Workerのライフサイクル管理です。従来のbackground pageとは異なり、常駐できないため、状態管理の設計が根本的に変わります。
Chrome拡張で音声処理や長時間の非同期処理を行う場合、以下の3点を最初から設計に組み込むことを強くお勧めします:
- 状態の永続化 — メモリ変数に頼らず、chrome.storageを使う
- Service Workerのkeep-alive — chrome.alarmsで定期的にpingする
- Offscreen Documentの保護 — Web Lock APIで破棄を防ぐ
MeetScribeはオープンソースで公開しています。
同じような課題に直面している方は、ぜひGitHubリポジトリのコードを参考にしてください。
関連リンク:



