【TypeScript】OBS WebSocketを使ってブラウザからOBSを操作する


OBS Studio（Open Broadcaster Software）は、ストリーミングや画面録画に広く使われているフリーソフトウェアです。OBS Studio 28.0から標準搭載されたWebSocketプラグインを使えば、外部アプリケーションからOBSを制御できます。

今回は、ReactとTypeScriptを使って、ブラウザからOBSを操作できる簡単なWebアプリケーションを開発する例を作成してみました。

https://github.com/yonaka15/obs-websocket-ts-demo

![Screenshot 2025-03-02 at 21.42.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79562/6ce2a33f-22a8-4c6d-8f66-c66d1d463547.png)


## この記事で学べること

- OBS WebSocket APIの基本的な使い方
- TypeScriptでのOBS WebSocketクライアントの実装方法
- ReactでのOBS操作UIの作成
- 録画機能の制御（開始/停止/一時停止/再開/チャプターマーク）

## 開発環境

- Node.js（14.x以上）
- OBS Studio（28.0.0以上）
- 技術スタック:
  - React 19
  - TypeScript 5.7
  - Vite 6
  - Tailwind CSS 4
  - obs-websocket-js 5.0.6

## OBS WebSocketとは

OBS WebSocketは、WebSocketプロトコルを使ってOBS Studioとの通信を可能にするインターフェースです。OBS Studio 28.0以降では標準で組み込まれており、簡単に有効化できます。

このインターフェースを使うことで、以下のようなことが可能になるそうです：

- 録画やストリーミングの開始・停止
- シーンの切り替え
- ソースアイテムの表示・非表示の切り替え
- 音声ミキサーの調整
- 様々なOBS設定の変更

## OBS WebSocketの設定

1. OBS Studioを起動します
2. メニューから「ツール」→「WebSocketサーバー設定」を選択
3. 「WebSocketサーバーを有効にする」にチェックを入れます
4. 必要に応じてパスワードを設定します
5. 「OK」をクリックして設定を保存


デフォルトでは、WebSocketサーバーはポート4455でリッスンします。

## プロジェクト構造

今回のデモアプリケーションは以下のような構造を持ちます：

```
src/
├── api/
│   ├── ObsWebSocketClient.ts   # OBS WebSocketクライアントラッパー
│   └── types.ts                # 型定義ファイル
├── components/
│   ├── ConnectionForm.tsx      # 接続フォームコンポーネント
│   └── ObsControlsPanel.tsx    # OBS制御パネルコンポーネント
├── App.tsx                     # メインアプリケーションコンポーネント
├── main.tsx                    # エントリーポイント
└── index.css                   # グローバルCSS
```

## APIクライアントの実装

OBS WebSocketとの通信を管理するクライアントクラスを実装します。

### 型定義

まず、`src/api/types.ts`に必要な型を定義します：

```typescript
// src/api/types.ts
import { OBSRequestTypes, OBSResponseTypes } from "obs-websocket-js";

/**
 * OBS WebSocket 接続状態の列挙型
 */
export enum ConnectionStatus {
  DISCONNECTED = "disconnected",
  CONNECTING = "connecting",
  CONNECTED = "connected",
  ERROR = "error",
}

/**
 * 接続情報のインターフェース
 */
export interface ConnectionInfo {
  status: ConnectionStatus;
  error?: string;
  obsVersion?: string;
  wsVersion?: string;
}

/**
 * 接続オプションのインターフェース
 */
export interface ConnectionOptions {
  url?: string;
  password?: string;
  autoConnect?: boolean;
}

// 他の型定義は省略...
```

### WebSocketクライアント

次に、`src/api/ObsWebSocketClient.ts`に実際のクライアント実装を作成します：

```typescript
// src/api/ObsWebSocketClient.ts
import { OBSWebSocket, EventSubscription } from "obs-websocket-js";
import type { OBSRequestTypes, OBSResponseTypes } from "obs-websocket-js";
import { ConnectionStatus, ConnectionInfo, ConnectionOptions } from "./types";

/**
 * OBS WebSocket client wrapper
 * Provides simplified interface for communicating with OBS
 */
export class ObsWebSocketClient {
  private obs: OBSWebSocket;
  private connectionInfo: ConnectionInfo;
  private eventCallbacks: Map<string, Set<Function>>;

  /**
   * Create new OBS WebSocket client instance
   * @param options Connection options
   */
  constructor(options?: ConnectionOptions) {
    this.obs = new OBSWebSocket();
    this.connectionInfo = {
      status: ConnectionStatus.DISCONNECTED,
    };
    this.eventCallbacks = new Map();

    // Setup internal event listeners
    this.setupInternalEvents();

    // Auto-connect if options provided
    if (options?.autoConnect && options.url) {
      this.connect(options.url, options.password);
    }
  }

  /**
   * Setup internal event listeners
   */
  private setupInternalEvents(): void {
    // 接続開始時
    this.obs.on("ConnectionOpened", () => {
      this.updateConnectionStatus({
        status: ConnectionStatus.CONNECTING,
      });
    });

    // 接続終了時
    this.obs.on("ConnectionClosed", (error) => {
      this.updateConnectionStatus({
        status: ConnectionStatus.DISCONNECTED,
        error: error?.message,
      });
    });

    // 接続エラー発生時
    this.obs.on("ConnectionError", (error) => {
      this.updateConnectionStatus({
        status: ConnectionStatus.ERROR,
        error: error?.message || "Unknown connection error",
      });
    });

    // 認証成功時
    this.obs.on("Identified", () => {
      this.updateConnectionStatus({
        status: ConnectionStatus.CONNECTED,
      });
    });
  }

  // 省略: updateConnectionStatus, notifyEventSubscribers メソッド

  /**
   * Connect to OBS WebSocket server
   * @param url Server URL (defaults to ws://localhost:4455)
   * @param password Server password (optional)
   * @returns Promise that resolves on successful connection
   */
  async connect(
    url = "ws://localhost:4455",
    password?: string
  ): Promise<ConnectionInfo> {
    try {
      this.updateConnectionStatus({
        status: ConnectionStatus.CONNECTING,
        error: undefined,
      });

      // 全てのイベントを購読してOBSに接続
      const connectResponse = await this.obs.connect(url, password, {
        eventSubscriptions: EventSubscription.All,
        rpcVersion: 1,
      });

      // バージョン情報を抽出
      this.updateConnectionStatus({
        status: ConnectionStatus.CONNECTED,
        obsVersion: connectResponse.obsVersion || "Unknown",
        wsVersion: connectResponse.obsWebSocketVersion || 
                   `v${connectResponse.negotiatedRpcVersion}`,
      });

      return { ...this.connectionInfo };
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      this.updateConnectionStatus({
        status: ConnectionStatus.ERROR,
        error: errorMessage,
      });
      throw error;
    }
  }

  /**
   * Disconnect from OBS WebSocket server
   */
  async disconnect(): Promise<void> {
    await this.obs.disconnect();
    // ConnectionClosed イベントで状態が更新される
  }

  /**
   * Get current connection info
   * @returns Current connection information
   */
  getConnectionInfo(): ConnectionInfo {
    return { ...this.connectionInfo };
  }

  /**
   * Send request to OBS
   * @param requestType Request type
   * @param requestData Request data
   * @returns Promise that resolves with response data
   */
  async call<T extends keyof OBSRequestTypes>(
    requestType: T,
    requestData?: OBSRequestTypes[T]
  ): Promise<OBSResponseTypes[T]> {
    if (this.connectionInfo.status !== ConnectionStatus.CONNECTED) {
      throw new Error("Not connected to OBS WebSocket server");
    }

    return this.obs.call(requestType, requestData);
  }

  // 省略: イベントハンドリング、その他のヘルパーメソッド

  /**
   * Start recording
   */
  async startRecording(): Promise<void> {
    await this.call("StartRecord");
  }

  /**
   * Stop recording
   */
  async stopRecording(): Promise<OBSResponseTypes["StopRecord"]> {
    return this.call("StopRecord");
  }

  /**
   * Get recording status
   * @returns Promise that resolves with recording status
   */
  async getRecordingStatus(): Promise<OBSResponseTypes["GetRecordStatus"]> {
    return this.call("GetRecordStatus");
  }

  // 省略: その他の録画・ストリーミング関連メソッド
}

export default ObsWebSocketClient;
```

## UIコンポーネントの実装

### 接続フォーム

`src/components/ConnectionForm.tsx`に接続フォームコンポーネントを実装します：

```tsx
// src/components/ConnectionForm.tsx
import React, { useState } from "react";
import { ConnectionStatus } from "../api/types";

interface ConnectionFormProps {
  connectionStatus: ConnectionStatus;
  obsVersion?: string;
  wsVersion?: string;
  error?: string;
  onConnect: (url: string, password: string) => Promise<void>;
  onDisconnect: () => Promise<void>;
}

const ConnectionForm: React.FC<ConnectionFormProps> = ({
  connectionStatus,
  obsVersion,
  wsVersion,
  error,
  onConnect,
  onDisconnect,
}) => {
  // フォームの状態管理
  const [url, setUrl] = useState<string>("ws://localhost:4455");
  const [password, setPassword] = useState<string>("");
  const [showPassword, setShowPassword] = useState<boolean>(false);
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  // フォーム送信ハンドラ
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      setIsSubmitting(true);

      if (connectionStatus === ConnectionStatus.CONNECTED) {
        await onDisconnect();
      } else {
        await onConnect(url, password);
      }
    } finally {
      setIsSubmitting(false);
    }
  };

  // 省略: getButtonText, getStatusLabel, isFormDisabled の実装

  return (
    <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <h2 className="text-xl font-bold mb-4">OBS WebSocket Connection</h2>

      {/* 接続ステータス表示 */}
      <div className="mb-4 flex items-center">
        {/* 省略: ステータスインジケーター */}
      </div>

      {/* バージョン情報表示 */}
      {connectionStatus === ConnectionStatus.CONNECTED && (
        <div className="text-sm text-gray-600 mb-4">
          {obsVersion && <p>OBS Version: {obsVersion}</p>}
          {wsVersion && <p>WebSocket Version: {wsVersion}</p>}
        </div>
      )}

      {/* エラーメッセージ表示 */}
      {connectionStatus === ConnectionStatus.ERROR && error && (
        <div className="text-sm text-red-500 mb-4">Error: {error}</div>
      )}

      {/* 接続フォーム */}
      <form onSubmit={handleSubmit}>
        {/* サーバーURL入力フィールド */}
        <div className="mb-4">
          {/* 省略: URLフィールドの実装 */}
        </div>

        {/* パスワード入力フィールド */}
        <div className="mb-6">
          {/* 省略: パスワードフィールドの実装 */}
        </div>

        {/* 接続/切断ボタン */}
        <div className="flex items-center justify-between">
          {/* 省略: ボタンの実装 */}
        </div>
      </form>
    </div>
  );
};

export default ConnectionForm;
```

### OBS制御パネル

`src/components/ObsControlsPanel.tsx`に制御パネルコンポーネントを実装します：

```tsx
// src/components/ObsControlsPanel.tsx
import React, { useState, useEffect } from "react";
import ObsWebSocketClient from "../api/ObsWebSocketClient";

interface ObsControlsPanelProps {
  obsClient: ObsWebSocketClient;
}

interface ObsVersionInfo {
  obsVersion?: string;
  obsWebSocketVersion?: string;
  rpcVersion?: number;
  availableRequests?: string[];
  supportedImageFormats?: string[];
  platform?: string;
  platformDescription?: string;
}

interface RecordingStatus {
  outputActive: boolean;
  outputPaused: boolean;
  outputTimecode?: string;
  outputDuration?: number;
  outputBytes?: number;
}

const ObsControlsPanel: React.FC<ObsControlsPanelProps> = ({ obsClient }) => {
  const [versionInfo, setVersionInfo] = useState<ObsVersionInfo | null>(null);
  const [recordingStatus, setRecordingStatus] =
    useState<RecordingStatus | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);

  // バージョン情報を取得
  const fetchVersionInfo = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await obsClient.callGeneric("GetVersion");
      setVersionInfo(response);
    } catch (err) {
      setError("Failed to get OBS version information");
      console.error("Error fetching version info:", err);
    } finally {
      setLoading(false);
    }
  };

  // 録画状態を取得
  const fetchRecordingStatus = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await obsClient.callGeneric("GetRecordStatus");
      setRecordingStatus(response);
    } catch (err) {
      setError("Failed to get recording status");
      console.error("Error fetching recording status:", err);
    } finally {
      setLoading(false);
    }
  };

  // 録画の開始/停止
  const toggleRecording = async () => {
    try {
      setLoading(true);
      setError(null);
      await obsClient.callGeneric("ToggleRecord");
      // 状態が変わるまで少し待機
      setTimeout(() => {
        fetchRecordingStatus();
      }, 500);
    } catch (err) {
      setError("Failed to toggle recording");
      console.error("Error toggling recording:", err);
      setLoading(false);
    }
  };

  // 省略: toggleRecordingPause, createRecordChapter メソッド

  // コンポーネントマウント時に情報を取得
  useEffect(() => {
    fetchVersionInfo();
    fetchRecordingStatus();

    // 5秒ごとに録画状態を更新
    const statusInterval = setInterval(() => {
      if (obsClient.getConnectionInfo().status === "connected") {
        fetchRecordingStatus();
      }
    }, 5000);

    return () => {
      clearInterval(statusInterval);
    };
  }, [obsClient]);

  return (
    <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <h2 className="text-xl font-bold mb-4">OBS Controls</h2>

      {/* エラー/成功メッセージ表示 */}
      {error && (
        <div
          className={`p-2 mb-4 rounded ${
            error.includes("success")
              ? "bg-green-100 text-green-800"
              : "bg-red-100 text-red-800"
          }`}
        >
          {error}
        </div>
      )}

      {/* OBS情報セクション */}
      <div className="mb-6">
        <h3 className="text-lg font-semibold mb-2">OBS Information</h3>
        {/* 省略: バージョン情報表示 */}
      </div>

      {/* 録画コントロールセクション */}
      <div className="mb-6">
        <h3 className="text-lg font-semibold mb-2">Recording Controls</h3>
        {/* 省略: 録画ステータス表示と操作ボタン */}
      </div>
    </div>
  );
};

export default ObsControlsPanel;
```

### メインアプリケーション

最後に、`src/App.tsx`にメインアプリケーションコンポーネントを実装します：

```tsx
// src/App.tsx
import React, { useState, useEffect, useCallback } from "react";
import ObsWebSocketClient from "./api/ObsWebSocketClient";
import { ConnectionStatus, ConnectionInfo } from "./api/types";
import ConnectionForm from "./components/ConnectionForm";
import ObsControlsPanel from "./components/ObsControlsPanel";

const App: React.FC = () => {
  // OBS WebSocketクライアントのインスタンスを作成
  const [obsClient] = useState<ObsWebSocketClient>(
    () => new ObsWebSocketClient()
  );

  // 接続情報の状態管理
  const [connectionInfo, setConnectionInfo] = useState<ConnectionInfo>({
    status: ConnectionStatus.DISCONNECTED,
  });

  // OBS接続情報の更新
  const handleConnectionChange = useCallback((info: ConnectionInfo) => {
    setConnectionInfo(info);
    console.log("Connection status changed:", info);
  }, []);

  // 接続・切断ハンドラ
  const handleConnect = useCallback(
    async (url: string, password: string) => {
      try {
        await obsClient.connect(url, password);
      } catch (error) {
        console.error("Failed to connect to OBS:", error);
      }
    },
    [obsClient]
  );

  const handleDisconnect = useCallback(async () => {
    try {
      await obsClient.disconnect();
    } catch (error) {
      console.error("Failed to disconnect from OBS:", error);
    }
  }, [obsClient]);

  // 接続状態の監視を設定
  useEffect(() => {
    obsClient.on("ConnectionStatusChanged", handleConnectionChange);

    // 初期状態を取得
    setConnectionInfo(obsClient.getConnectionInfo());

    // クリーンアップ
    return () => {
      obsClient.off("ConnectionStatusChanged", handleConnectionChange);
    };
  }, [obsClient, handleConnectionChange]);

  return (
    <div className="container mx-auto px-4 py-8">
      <header className="mb-8 text-center">
        <h1 className="text-3xl font-bold mb-2">OBS WebSocket Demo</h1>
        <p className="text-gray-600">
          Connect to OBS Studio and control it remotely using WebSockets
        </p>
      </header>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div>
          <ConnectionForm
            connectionStatus={connectionInfo.status}
            obsVersion={connectionInfo.obsVersion}
            wsVersion={connectionInfo.wsVersion}
            error={connectionInfo.error}
            onConnect={handleConnect}
            onDisconnect={handleDisconnect}
          />
        </div>

        <div>
          {connectionInfo.status === ConnectionStatus.CONNECTED ? (
            <ObsControlsPanel obsClient={obsClient} />
          ) : (
            <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
              <h2 className="text-xl font-bold mb-4">OBS Controls</h2>
              <p className="text-gray-600">
                Connect to OBS to access controls.
              </p>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

export default App;
```

## アプリケーションの実行

プロジェクトのセットアップが完了したら、以下のコマンドでアプリケーションを実行できます：

```bash
npm run dev
```

ブラウザで `http://localhost:5173` にアクセスすると、アプリケーションが表示されます。

## アプリケーションの使い方

1. OBS Studioを起動し、WebSocketサーバーが有効になっていることを確認します
2. アプリケーションの接続フォームでサーバーURL（デフォルト: `ws://localhost:4455`）とパスワード（設定した場合）を入力
3. 「Connect」ボタンをクリックして接続
4. 接続に成功すると、OBS情報と録画コントロールが表示されます
5. 「Start Recording」ボタンで録画を開始、「Stop Recording」で停止
6. 録画中は「Pause Recording」で一時停止、「Resume Recording」で再開可能
7. 録画中に「Add Chapter Mark」でチャプターマークを追加できます

## まとめ

今回は、TypeScriptとReactを使って、OBS WebSocketと通信する簡単なアプリケーションを作成しました。このアプリケーションを通じて、以下のことを学びました：

- OBS WebSocketの基本的な仕組みと設定方法
- TypeScriptでの型安全なOBS WebSocketクライアントの実装

このデモアプリケーションは基本的な機能のみを実装していますが、OBS WebSocket APIは非常に多くの機能を提供しています。シーン切り替え、ソースの表示/非表示の切り替え、オーディオミキサーの制御など、さまざまなアイデアが実現できそうｄ。

## 参考リソース

- [OBS Studio公式サイト](https://obsproject.com/)
- [OBS WebSocketドキュメント](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md)
- [obs-websocket-js](https://github.com/obs-websocket-community-projects/obs-websocket-js) - 今回使用したJavaScriptライブラリ