This article demonstrates how to automate OBS Studio programmatically using C#.

The prerequisite is that OBS Studio is already running and properly configured.

The first step is to establish a connection to OBS Studio via the WebSocket API.

public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
	if (IsConnected) return true;

	await _connectLock.WaitAsync(ct);
	try
	{
		if (IsConnected) return true;

		await DisposeSocketAsync();

		_ws = new ClientWebSocket();
		_ws.Options.AddSubProtocol("obswebsocket.json");
		await _ws.ConnectAsync(opt.Uri, ct);

		// Hello (op=0)
		var hello = await ReceiveJsonAsync(_ws, ct);
		if (hello.GetProperty("op").GetInt32() != 0)
			throw new Exception("Expected OBS Hello (op=0).");

		var helloD = hello.GetProperty("d");
		int rpcVersion = helloD.GetProperty("rpcVersion").GetInt32();

		string? auth = null;
		if (helloD.TryGetProperty("authentication", out var authObj))
		{
			var challenge = authObj.GetProperty("challenge").GetString()!;
			var salt = authObj.GetProperty("salt").GetString()!;
			auth = CreateObsAuthString(opt.Password, salt, challenge);
		}

		// Identify (op=1)
		var identify = new
		{
			op = 1,
			d = new
			{
				rpcVersion,
				authentication = auth,
				eventSubscriptions = opt.EventSubscriptions
			}
		};
		await SendJsonAsync(_ws, identify, ct);

		var identified = await ReceiveJsonAsync(_ws, ct);
		if (identified.GetProperty("op").GetInt32() != 2)
			throw new Exception("OBS Identify failed (expected op=2).");

		_isConnected = true;

		var ws = _ws;
		_receiveLoop = Task.Run(() => ReceiveLoopAsync(ws, CancellationToken.None));

		await loggerAsync.Log($"OBS connected: {opt.Uri}");
		return true;
	}
	catch (OperationCanceledException)
	{
		await loggerAsync.Log("OBS connect canceled.");
		_isConnected = false;
		await DisposeSocketAsync();
		return false;
	}
	catch (Exception ex)
	{
		_isConnected = false;
		await loggerAsync.Log(ex);

		await DisposeSocketAsync();
		return false;
	}
	finally
	{
		_connectLock.Release();
	}
}

Sending a command to OBS Studio is implemented as follows:

public async Task<bool> SendAsync(string requestType, object? requestData = null, CancellationToken ct = default)
{
	await loggerAsync.Log($"requestType: {requestType}");
	if (!await ConnectAsync(ct))
	{
		return false;
	}

	var ws = _ws;
	if (ws is null || ws.State != WebSocketState.Open || !_isConnected)
		return false;

	var requestId = Guid.NewGuid().ToString("N");
	var tcs = new TaskCompletionSource<JsonElement>(TaskCreationOptions.RunContinuationsAsynchronously);
	_pending[requestId] = tcs;

	try
	{
		var payload = new
		{
			op = 6,
			d = new { requestType, requestId, requestData = requestData ?? new { } }
		};

		await _sendLock.WaitAsync(ct);
		try
		{
			await SendJsonAsync(ws, payload, ct);
		}
		finally
		{
			_sendLock.Release();
		}

		using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
		timeoutCts.CancelAfter(TimeSpan.FromSeconds(15));

		var msg = await tcs.Task.WaitAsync(timeoutCts.Token);

		var d = msg.GetProperty("d");
		var status = d.GetProperty("requestStatus");
		if (!status.GetProperty("result").GetBoolean())
		{
			var code = status.TryGetProperty("code", out var c) ? c.GetInt32() : -1;
			var comment = status.TryGetProperty("comment", out var cm) ? cm.GetString() : "(no comment)";
			throw new Exception($"OBS request '{requestType}' failed. code={code}, comment={comment}");
		}

		return true;
	}
	catch (Exception ex)
	{
		await loggerAsync.Log(ex);
		return false;
	}
	finally
	{
		_pending.TryRemove(requestId, out _);
	}
}

Starting a recording looks like this:

public async Task StartAsync(StartStopRecordingCommand command, CancellationToken ct = default)
{
	var format = BuildFilenameFormat(command, opt.RecordDirectory) ?? opt.FilenameFormatting;
	await loggerAsync.Log($"Starting recording with filename format: {format}");

	if (!string.IsNullOrWhiteSpace(opt.RecordDirectory))
	{
		try
		{
			await obs.SendAsync("SetRecordDirectory", new { recordDirectory = opt.RecordDirectory }, ct);
			await loggerAsync.Log($"Set recording directory to: {opt.RecordDirectory}");
		}
		catch
		{
			await obs.SendAsync("SetProfileParameter", new
			{
				parameterCategory = "SimpleOutput",
				parameterName = "FilePath",
				parameterValue = opt.RecordDirectory
			}, ct);
		}
	}

	try
	{
		await obs.SendAsync("SetProfileParameter", new
		{
			parameterCategory = "Output",
			parameterName = "FilenameFormatting",
			parameterValue = format
		}, ct);
	}
	catch
	{
	}

	await obs.SendAsync("StartRecord", null, ct);
}
Full example download from here