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?

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

Posted at

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

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

Screenshot 2025-03-02 at 21.42.41.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に必要な型を定義します:

// 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に実際のクライアント実装を作成します:

// 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に接続フォームコンポーネントを実装します:

// 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に制御パネルコンポーネントを実装します:

// 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にメインアプリケーションコンポーネントを実装します:

// 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;

アプリケーションの実行

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

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は非常に多くの機能を提供しています。シーン切り替え、ソースの表示/非表示の切り替え、オーディオミキサーの制御など、さまざまなアイデアが実現できそうd。

参考リソース

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?