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?

Claude Codeに「配信開始して」と言ったらYouTube Liveが始まった — obs-mcp × OBS WebSocketでAITuber配信を自動化した話

0
Last updated at Posted at 2026-03-21

Claude Codeに「配信開始して」と打ち込むだけで、OBS配信開始・録画・TitleCard表示・AIキャラの開始挨拶まで全自動で走る仕組みを作りました。テスト配信で盛大にハマった失敗談も含めて共有します。

デモ: 何が起きるか

Claude Code(左)からobs-mcpでOBS(右)を操作するデモ。録画開始→TitleCard表示→オーバーレイ切替が自動で走る
Claude Code(左)からobs-mcp経由でOBS(右)を制御。録画開始→TitleCard→オーバーレイ切替が全自動。

「配信開始して」と言うと...

  1. Claude Codeが obs-mcp 経由でOBSに配信開始を指示
  2. OBS WebSocketの StreamStateChanged イベントをOrchestratorが検知
  3. 自動で 録画開始
  4. ブラウザソース(オーバーレイ)を 自動リフレッシュ
  5. TitleCard(オープニング画面)を4秒間表示
  6. AIキャラ2人が 開始挨拶を生成・発話
  7. 以降、YouTube Live Chatのコメントを拾いながら 掛け合いトークが自動進行

「配信終了して」と言うと...

  1. 締めの挨拶プロンプトをキューに投入
  2. AIキャラ2人が お別れの挨拶を生成・発話(セリフ完了まで待機)
  3. TitleCard(エンドカード)を表示
  4. Orchestrator停止 → OBS配信停止録画停止

人間がやるのは「配信開始して」「配信終了して」の2回だけ。あとは全部自動です。

背景: なぜ自動化したのか

AITuber掛け合い配信システム(白紙ちゃん: Live2D × 黒髪ツインテールちゃん: VRM)を開発しています。2キャラがローカルLLM(35B)で台本を生成し、TTSとリップシンクで掛け合いトークを行う仕組みです。

課題は、配信の開始・終了に細かな操作が何度も必要なことでした。配信開始だけでも、OBSの配信・録画開始、ブラウザソースのリロード、OrchestratorやYouTube Pollerの起動など、5〜6ステップあります。終了時にも、ほぼ同じ数の操作が必要です。

さらに、OBSのブラウザソースが配信開始時に古い状態のまま表示されることがあります。そのため、毎回手動でリフレッシュしなければなりませんでした。

開発中は試行錯誤のたびに配信の開始・停止を繰り返すため、この繰り返し作業が地味に負担になります。「Claude Codeにまとめて任せられたら楽なのに」と感じたのが、自動化を考えたきっかけです。

アーキテクチャ: 2層構造

ポイントはClaude Code層Orchestrator層の2層構造です。

なぜ2層なのか

Claude Code層(obs-mcp経由) は「配信開始して」「配信停止して」レベルの宣言的な操作を担当します。obs-mcpはOBS WebSocket v5をMCPプロトコルでラップしたサーバーで、Claude Codeからobs-start-streamobs-stop-streamといったツールを直接呼び出せます。

Orchestrator層(obs-websocket-js直接接続)StreamStateChangedイベントの購読や、TitleCard表示タイミングの制御など、リアルタイムのイベント駆動処理を担当します。これはMCP経由では難しい(MCPはリクエスト/レスポンス型で、イベント購読に向かない)ため、obs-websocket-jsライブラリで直接WebSocket接続しています。

つまり:

  • 何をするか(配信開始/停止)→ Claude Code + obs-mcp
  • どう連動するか(開始を検知して録画やTitleCardを自動制御)→ Orchestrator + obs-websocket-js

実装詳細

1. obs-mcpの導入とClaude Codeからの配信開始

obs-mcpのセットアップは.mcp.jsonに追加するだけです。

{
  "obs-mcp": {
    "command": "npx",
    "args": ["-y", "obs-mcp@latest"],
    "env": {
      "OBS_WEBSOCKET_URL": "ws://localhost:4455"
    }
  }
}

OBS側でWebSocket Server(ポート4455)を有効にしておけば、Claude Codeからobs-start-streamツールを呼ぶだけで配信が始まります。

さらに、この操作をSkill(/stream-start)として定義し、「配信開始して」の一言で必要な前処理(VRAM確認、コンポーネント起動確認)を含めた一連のフローが走るようにしました。

2. StreamStateChanged連動(Orchestrator側)

配信開始をトリガーにした自動処理は、Orchestrator内でobs-websocket-jsを使って実装しています。

import OBSWebSocket from "obs-websocket-js";

const obsStream = new OBSWebSocket();
await obsStream.connect("ws://127.0.0.1:4455");

obsStream.on("StreamStateChanged", async ({ outputActive, outputState }) => {
  if (outputState === "OBS_WEBSOCKET_OUTPUT_STARTED" && !running) {
    // 1. 録画も同時開始
    await obsStream.call("StartRecord");

    // 2. ブラウザソースをリフレッシュ(URLにクエリパラメータ付加)
    for (const src of ["統合オーバーレイ", "コメントオーバーレイ"]) {
      const { inputSettings } = await obsStream.call("GetInputSettings",
        { inputName: src });
      const url = (inputSettings.url || "").replace(/[?&]r=\d+/, "");
      await obsStream.call("SetInputSettings", {
        inputName: src,
        inputSettings: { url: `${url}?r=${Date.now()}` },
        overlay: true,
      });
    }

    // 3. TitleCard(オープニング)を4秒表示
    await setTitleCard(obsStream, true, 10);  // scene=10: オープニング
    await sleep(4000);
    await setTitleCard(obsStream, false);

    // 4. 開始挨拶をキューに入れて掛け合い開始
    commentQueue.push({
      author: "🎬 配信開始",
      text: "配信が始まりました!視聴者に元気に挨拶してください。",
      isSystem: true,
    });
    mainLoop();
  }
});

StreamStateChangedイベントは、OBSの配信状態が変わるたびに発火します。OBS_WEBSOCKET_OUTPUT_STARTEDを検知したら、録画開始・ブラウザソースリフレッシュ・TitleCard表示・Orchestrator起動を一気に実行します。

ブラウザソースのリフレッシュは、URLにタイムスタンプ付きのクエリパラメータを付加することで実現しています。OBSのブラウザソースはURLが変わると再読み込みするため、?r=1710912345678のようなパラメータを付けるだけで強制リロードできます。

3. /end-stream グレースフル終了

配信終了は単にobs-stop-streamを呼ぶだけでは不十分です。AIキャラが締めの挨拶を言い終わる前に配信が切れてしまいます。

そこで、Orchestratorに/end-streamエンドポイントを実装しました。

// POST /end-stream — グレースフル配信終了
app.post("/end-stream", async (req, res) => {
  endStreamInProgress = true;

  // 1. 締めセリフをキューに投入
  commentQueue.push({
    author: "🎬 配信終了",
    text: "配信が終了します。視聴者に感謝を伝えて、お別れの挨拶をしてください。",
    isSystem: true,
  });

  // Phase 1: キューから取り出される(=サイクルに取り込まれる)のを待つ
  while (running) {
    await sleep(1000);
    if (!commentQueue.some(c => c.isSystem)) break;
  }

  // Phase 2: セリフの再生完了を待つ
  const pickedUpAt = Date.now();
  while (running) {
    await sleep(2000);
    if (lastCycleDoneAt > pickedUpAt) break;  // 全セリフ再生完了
  }

  // 3. TitleCard(エンドカード)表示
  await setTitleCard(obsStreamConn, true, 11); // scene=11: エンドカード
  await sleep(2000);

  // 4. Orchestrator停止 → OBS配信停止 → 録画停止
  running = false;
  await obsStreamConn.call("StopStream");
  await obsStreamConn.call("StopRecord");
  endStreamInProgress = false;
});

ポイントは2段階の待機判定です。Phase 1で「キューから取り出された」(=LLMが台本生成を開始した)ことを確認し、Phase 2でlastCycleDoneAt(最後のセリフ再生完了タイムスタンプ)が更新されるのを待ちます。この2段階により、締めセリフが完全に再生されてからTitleCard表示→配信停止という流れが確実に実現できます。

また、endStreamInProgressフラグにより、OBSのStreamStateChangedOBS_WEBSOCKET_OUTPUT_STOPPEDが発火しても、グレースフル終了中であれば強制停止処理をスキップする設計にしています。

テスト配信でハマった3つの落とし穴

落とし穴1: 音声の二重取り込み(OBSモニタリング設定)

症状: 配信を開始したら、音声が何重にも重なって聞こえる。自分の声(TTSの出力)が遅れてエコーのように響く。

原因: BGMソース(BGM_2CHAR)のモニタリング設定がMONITOR_AND_OUTPUTになっていました。OBSには「デスクトップ音声キャプチャ」もあるため、BGMが「モニタリング出力→デスクトップ音声→OBSに再入力」というループで二重に取り込まれていたのです。

解決: BGMソースのモニタリングをOBS_MONITORING_TYPE_NONEに変更。

// obs-mcp経由で修正
await obsStream.call("SetInputAudioMonitorType", {
  inputName: "BGM_2CHAR",
  monitorType: "OBS_MONITORING_TYPE_NONE",
});

OBS配信で音声が重複する場合、まず疑うべきはモニタリング設定とデスクトップ音声キャプチャの組み合わせです。「ハウリング」と呼ばれがちですが、実態はループバック/二重取り込みであることが多い。OBSの「オーディオの詳細プロパティ」でモニタリング列を確認してみてください。

落とし穴2: /end-stream のタイミング地獄(3回のイテレーション)

グレースフル終了の実装は、3回の試行錯誤を経て完成しました。

v1(失敗): キュー空判定 + 固定待機

キュー空 → 8秒待機 → TitleCard表示 → 配信停止

問題: キューが空になるのは「前のサイクルが全セリフを消化した」タイミングかもしれない。締めプロンプトの処理ではなく、直前の通常サイクル完了でTitleCardが出てしまった。

v2(失敗): cycleCount判定

cycleCount記録 → cycleCountが増えたら → TitleCard表示

問題: cycleCount++がサイクルの冒頭にあるため、LLM生成・TTS再生の前にインクリメントされる。カウントが増えた時点ではまだセリフは再生されていない。

v3(解決): 2段階判定

Phase 1: isSystemメッセージがキューから消える → 取り込まれた
Phase 2: lastCycleDoneAt が更新される → 全セリフ再生完了

教訓: 非同期処理のタイミング制御では、「何を待っているのか」を明確にすること。v1は「何かが終わった」、v2は「何かが始まった」を見ていただけで、「特定の処理が完了した」を正しく判定できていなかった。Phase 1で「取り込み」、Phase 2で「完了」を分けたことで、意図通りの動作になりました。

落とし穴3: OBS配信停止ボタン直押し事故

状況: /end-streamエンドポイントを実装し、Claude Codeから「配信終了して」で呼び出す設計にしたのに、テスト中にOBSの配信停止ボタンを直接押してしまった。グレースフル終了はスキップされ、締めセリフなしで配信が突然終了。

原因: 操作フローの周知不足。OBSのUIに配信停止ボタンが目の前にあれば、人間は自然とそれを押してしまう。

対策:

  1. StreamStateChangedOBS_WEBSOCKET_OUTPUT_STOPPEDを検知した際、endStreamInProgressフラグで分岐。グレースフル終了中でなければフォールバック(強制停止)として処理
  2. /stream-end Skillの説明に「OBSのボタンは押さないでください」と明記

教訓: 自動化は技術だけでは完結しない。人間の操作導線まで設計する必要がある。「正しい操作方法」を用意しても、既存のUIに慣れた人間は旧来の方法で操作してしまう。フォールバック処理を入れておくことが重要です。

まとめ

  • obs-mcp(OBS WebSocket v5のMCPラッパー)を使えば、Claude Codeから自然言語でOBSを操作できる
  • 配信開始/停止のような宣言的操作はobs-mcp、イベント連動のようなリアルタイム制御はobs-websocket-js直接接続という2層構造が実用的
  • グレースフル終了のタイミング制御は「何を待っているか」を明確にする2段階判定が鍵
  • 自動化では技術だけでなく人間の操作導線も設計せよ

次回は、AITuber 2キャラ掛け合いシステムの内部——ローカルLLMによる台本生成、感情表現、ダミーコメント生成——について書く予定です。


📝 本記事の内容は RTX 5090 (32GB VRAM) / Windows 11 環境で筆者が実際にテスト配信を行い検証しています。執筆にはClaude Codeを補助利用しました。

使用モデルのクレジット

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?