OBS Studio(Open Broadcaster Software)は、ストリーミングや画面録画に広く使われているフリーソフトウェアです。OBS Studio 28.0から標準搭載されたWebSocketプラグインを使えば、外部アプリケーションからOBSを制御できます。
今回は、ReactとTypeScriptを使って、ブラウザからOBSを操作できる簡単なWebアプリケーションを開発する例を作成してみました。
この記事で学べること
- 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の設定
- OBS Studioを起動します
- メニューから「ツール」→「WebSocketサーバー設定」を選択
- 「WebSocketサーバーを有効にする」にチェックを入れます
- 必要に応じてパスワードを設定します
- 「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
にアクセスすると、アプリケーションが表示されます。
アプリケーションの使い方
- OBS Studioを起動し、WebSocketサーバーが有効になっていることを確認します
- アプリケーションの接続フォームでサーバーURL(デフォルト:
ws://localhost:4455
)とパスワード(設定した場合)を入力 - 「Connect」ボタンをクリックして接続
- 接続に成功すると、OBS情報と録画コントロールが表示されます
- 「Start Recording」ボタンで録画を開始、「Stop Recording」で停止
- 録画中は「Pause Recording」で一時停止、「Resume Recording」で再開可能
- 録画中に「Add Chapter Mark」でチャプターマークを追加できます
まとめ
今回は、TypeScriptとReactを使って、OBS WebSocketと通信する簡単なアプリケーションを作成しました。このアプリケーションを通じて、以下のことを学びました:
- OBS WebSocketの基本的な仕組みと設定方法
- TypeScriptでの型安全なOBS WebSocketクライアントの実装
このデモアプリケーションは基本的な機能のみを実装していますが、OBS WebSocket APIは非常に多くの機能を提供しています。シーン切り替え、ソースの表示/非表示の切り替え、オーディオミキサーの制御など、さまざまなアイデアが実現できそうd。
参考リソース
- OBS Studio公式サイト
- OBS WebSocketドキュメント
- obs-websocket-js - 今回使用したJavaScriptライブラリ