- Details
- Written by: Stanko Milosev
- Category: C#
- Hits: 9
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
- Details
- Written by: Stanko Milosev
- Category: C#
- Hits: 109
Chat _chat;
OllamaApiClient? _ollamaApiClient;
uri = new Uri(txtOllamaUri.Text);
_ollamaApiClient = new OllamaApiClient(uri);
_ollamaApiClient.SelectedModel = txtModel.Text;
_chat = new Chat(_ollamaApiClient);
await foreach (var answerToken in _chat.SendAsync(txtMessage.Text))
Console.Write(answerToken);
Example download from here.
- Details
- Written by: Stanko Milosev
- Category: C#
- Hits: 147
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5259",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://0.0.0.0:7216;https://0.0.0.0:7217",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
This means that the controller was accessible via the following endpoint:
https://192.168.2.50:7217/WeatherForecastIn OpenWebUI tools, I wrote:
import requests
import urllib3
class Tools:
def today_from_csharp_http(self) -> str:
"""
Get the current weather.
"""
url = "https://192.168.2.50:7217/WeatherForecast"
try:
r = requests.get(url, timeout=5, verify=False)
r.raise_for_status()
data = r.json()
if not isinstance(data, list) or len(data) == 0:
return f"Unexpected JSON: {type(data).__name__}: {data}"
first = data[0]
date = first.get("date")
summary = first.get("summary")
temp = first.get("temperatureC")
return f"OK: {date} | {summary} | {temp}°C"
except Exception as e:
return f"HTTP request failed: {e}"
The tool must be enabled for this model by checking the corresponding tool checkbox.
- Details
- Written by: Stanko Milosev
- Category: C#
- Hits: 1427
Here is my Example how to post Joomla! article from .NET.
Four steps are needed:
- Open Joomla! admin page
- Login to Joomla!
- Open add article page
- Save and close
First we will create HttpClient and same instance I will use for every step:
HttpClientHandler httpClientHandler = new HttpClientHandler
{
CookieContainer = new CookieContainer(),
UseCookies = true,
AllowAutoRedirect = true
};
HttpClient client = new HttpClient(httpClientHandler);
For every step we will need token
static string ExtractTokenName(string html)
{
var regex = new Regex(@"""csrf\.token"":""(?<token>[a-f0-9]{32})""");
var match = regex.Match(html);
if (match.Success)
{
return match.Groups["token"].Value;
}
throw new Exception("CSRF-Token not found.");
}
- Open Joomla! admin page:
async Task<string> OpenJoomlaAdminPage(HttpClient httpClient, string url) { HttpResponseMessage getResponse = await httpClient.GetAsync(url); string html = await getResponse.Content.ReadAsStringAsync(); return html; } - Login to Joomla!:
async Task<bool> LoginToJoomla(HttpClient httpClient, string url, string username, string password, string joomlaAdminPagehtml) { var tokenName = ExtractTokenName(joomlaAdminPagehtml); var tokenValue = "1"; var formContent = new FormUrlEncodedContent([ new KeyValuePair<string, string>("username", username), new KeyValuePair<string, string>("passwd", password), new KeyValuePair<string, string>("option", "com_login"), new KeyValuePair<string, string>("task", "login"), new KeyValuePair<string, string>(tokenName, tokenValue) ]); HttpResponseMessage postResponse = await httpClient.PostAsync(url, formContent); string postResult = await postResponse.Content.ReadAsStringAsync(); return postResult.Contains("mod_quickicon") || postResult.Contains("cpanel"); } - Open add article page:
async Task<string> OpenAddArticle(HttpClient httpClient, string addArticleUrl) { HttpResponseMessage createResponse = await httpClient.GetAsync(addArticleUrl); string createHtml = await createResponse.Content.ReadAsStringAsync(); return ExtractTokenName(createHtml); } - Save and close:
async Task<bool> PostArticleToJoomla(HttpClient httpClient, string url, string articleToken, string title, string catid, string articletext) { var formData = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("jform[title]", title), new KeyValuePair<string, string>("jform[catid]", catid), new KeyValuePair<string, string>("jform[language]", "*"), new KeyValuePair<string, string>("jform[state]", "1"), new KeyValuePair<string, string>("jform[articletext]", articletext), new KeyValuePair<string, string>("task", "article.save"), new KeyValuePair<string, string>(articleToken, "1") }); HttpResponseMessage postResponse = await httpClient.PostAsync(url, formData); string postResultHtml = await postResponse.Content.ReadAsStringAsync(); return postResultHtml.Contains("Article saved."); }