Changed command dictionary to a command tree. Fixed various requests. OBS reconnection added if identified previously.

This commit is contained in:
Tom
2024-07-19 16:56:41 +00:00
parent e6b3819356
commit 472bfcee5d
56 changed files with 1943 additions and 1553 deletions

View File

@ -1,15 +1,19 @@
using System.Text.Json.Serialization;
namespace TwitchChatTTS.OBS.Socket.Data
{
public class IdentifyMessage
{
public int RpcVersion { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Authentication { get; set; }
public int EventSubscriptions { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? EventSubscriptions { get; set; }
public IdentifyMessage(int version, string auth, int subscriptions)
public IdentifyMessage(int rpcVersion, string? authentication, int? subscriptions)
{
RpcVersion = version;
Authentication = auth;
RpcVersion = rpcVersion;
Authentication = authentication;
EventSubscriptions = subscriptions;
}
}

View File

@ -2,19 +2,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class EventMessageHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 5;
public EventMessageHandler(OBSManager manager, ILogger logger)
public EventMessageHandler(
ILogger logger
)
{
_manager = manager;
_logger = logger;
}
@ -22,6 +21,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not EventMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
switch (message.EventType)
{
@ -31,10 +32,10 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower();
_manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true";
obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true";
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
if (_manager.Streaming == false && state != null && !state.EndsWith("ing"))
if (obs.Streaming == false && state != null && !state.EndsWith("ing"))
{
// Stream ended
}

View File

@ -26,9 +26,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
_logger.Verbose("OBS websocket password: " + password);
if (message.Authentication == null || string.IsNullOrWhiteSpace(password))
if (message.Authentication == null || string.IsNullOrEmpty(password))
{
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
await sender.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144));
return;
}
@ -39,7 +39,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string secret = password + salt;
byte[] bytes = Encoding.UTF8.GetBytes(secret);
string hash = null;
string? hash = null;
using (var sha = SHA256.Create())
{
bytes = sha.ComputeHash(bytes);

View File

@ -2,19 +2,16 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class IdentifiedHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 2;
public IdentifiedHandler(OBSManager manager, ILogger logger)
public IdentifiedHandler(ILogger logger)
{
_manager = manager;
_logger = logger;
}
@ -22,20 +19,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not IdentifiedMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
_manager.Connected = true;
obs.Identified = true;
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
try
{
await _manager.GetGroupList(async groups => await _manager.GetGroupSceneItemList(groups));
await obs.GetGroupList(async groups => await obs.GetGroupSceneItemList(groups));
}
catch (Exception e)
{
_logger.Error(e, "Failed to load OBS group info upon OBS identification.");
}
await _manager.UpdateStreamingState();
await obs.UpdateStreamingState();
}
}
}

View File

@ -5,22 +5,16 @@ using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Context;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestBatchResponseHandler : IWebSocketHandler
{
private readonly IWebSocketHandler _requestResponseHandler;
private readonly ILogger _logger;
public int OperationCode { get; } = 9;
public RequestBatchResponseHandler(
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
ILogger logger
)
public RequestBatchResponseHandler(ILogger logger)
{
_requestResponseHandler = requestResponseHandler;
_logger = logger;
}
@ -28,40 +22,38 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not RequestBatchResponseMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
using (LogContext.PushProperty("obsrid", message.RequestId))
var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages.");
int count = results.Count;
for (int i = 0; i < count; i++)
{
if (results[i] == null)
continue;
var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages.");
int count = results.Count;
for (int i = 0; i < count; i++)
try
{
if (results[i] == null)
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (response == null)
continue;
try
{
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (response == null)
continue;
await _requestResponseHandler.Execute(sender, response);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process an item in a request batch message.");
}
await obs.ExecuteRequest(response);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process an item in a request batch message.");
}
_logger.Debug($"Finished processing all request in this batch.");
}
_logger.Debug($"Finished processing all request in this batch.");
}
}
}

View File

@ -3,19 +3,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestResponseHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 7;
public RequestResponseHandler(OBSManager manager, ILogger logger)
public RequestResponseHandler(
ILogger logger
)
{
_manager = manager;
_logger = logger;
}
@ -23,10 +22,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
if (data is not RequestResponseMessage message || message == null)
return;
if (sender is not OBSSocketClient obs)
return;
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]");
var requestData = _manager.Take(message.RequestId);
var requestData = obs.Take(message.RequestId);
if (requestData == null)
{
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]");
@ -42,7 +43,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
switch (request.RequestType)
{
case "GetOutputStatus":
_logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]");
_logger.Debug($"Fetched stream's live status [live: {obs.Streaming}][obs request id: {message.RequestId}]");
break;
case "GetSceneItemId":
{
@ -206,7 +207,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
foreach (var sceneItem in sceneItems)
_manager.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
obs.AddSourceId(sceneItem.SourceName, sceneItem.SceneItemId);
requestData.ResponseValues = new Dictionary<string, object>()
{
@ -237,9 +238,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
return;
}
_manager.Streaming = outputActive?.ToString()!.ToLower() == "true";
obs.Streaming = outputActive?.ToString()!.ToLower() == "true";
requestData.ResponseValues = message.ResponseData;
_logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming.");
_logger.Information($"OBS is currently {(obs.Streaming ? "" : "not ")}streaming.");
break;
}
default:

View File

@ -1,34 +0,0 @@
using Serilog;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Socket.Manager;
using CommonSocketLibrary.Common;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSHandlerManager : WebSocketHandlerManager
{
public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
{
var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true);
foreach (var type in types)
{
var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "")
.Replace("Hand#lers", "Handlers")
.ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null)
{
logger.Error("Failed to find obs websocket handler: " + type.AssemblyQualifiedName);
continue;
}
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
Add(handler);
}
}
}
}

View File

@ -1,4 +1,3 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
@ -6,12 +5,12 @@ using Serilog;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager
public class OBSMessageTypeManager : WebSocketMessageTypeManager
{
public OBSHandlerTypeManager(
ILogger factory,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers)
public OBSMessageTypeManager(
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
ILogger logger
) : base(handlers, logger)
{
}
}

View File

@ -1,316 +0,0 @@
using System.Collections.Concurrent;
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSManager
{
private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public bool Connected { get; set; }
public bool Streaming { get; set; }
public OBSManager(Configuration configuration, IServiceProvider serviceProvider, ILogger logger)
{
_configuration = configuration;
_serviceProvider = serviceProvider;
_logger = logger;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>();
}
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
client.OnConnected += (sender, e) =>
{
Connected = true;
_logger.Information("OBS websocket client connected.");
};
client.OnDisconnected += (sender, e) =>
{
Connected = false;
_logger.Information("OBS websocket client disconnected.");
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
}
public void AddSourceId(string sourceName, long sourceId)
{
if (!_sourceIds.TryGetValue(sourceName, out _))
_sourceIds.Add(sourceName, sourceId);
else
_sourceIds[sourceName] = sourceId;
_logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]");
}
public void ClearCache()
{
_sourceIds.Clear();
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(URL))
{
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
return;
}
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
try
{
await client.ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
var list = messages.ToList();
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
// Keep track of requests to know what we requested.
foreach (var message in list)
{
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid);
_requests.Add(message.RequestId, data);
}
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(8, new RequestBatchMessage(uid, list));
}
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
// Keep track of requests to know what we requested.
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid)
{
Callback = callback
};
_requests.Add(message.RequestId, data);
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(6, message);
}
public RequestData? Take(string id)
{
if (id != null && _requests.TryGetValue(id, out var request))
{
_requests.Remove(id);
return request;
}
return null;
}
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
if (action == null)
return;
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (transform == null)
{
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
return;
}
double w = transform.Width;
double h = transform.Height;
int a = transform.Alignment;
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
if (a != (int)OBSAlignment.Center)
{
if (hasBounds)
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
else
transform.Alignment = a = (int)OBSAlignment.Center;
transform.PositionX = transform.PositionX + w / 2;
transform.PositionY = transform.PositionY + h / 2;
}
action?.Invoke(transform);
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
await Send(m3);
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]");
});
});
}
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m1, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
return;
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
await Send(m2);
});
});
}
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
await Send(m);
});
}
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
await Send(m);
});
}
public async Task GetGroupList(Action<IEnumerable<string>>? action)
{
var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary<string, object>());
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("groups", out object? value) || value == null)
return;
var list = (IEnumerable<string>)value;
_logger.Debug("Fetched the list of groups in OBS.");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(string groupName, Action<IEnumerable<OBSSceneItem>>? action)
{
var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", groupName } });
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null)
return;
var list = (IEnumerable<OBSSceneItem>)value;
_logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(IEnumerable<string> groupNames)
{
var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", group } }));
await Send(messages);
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
}
private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
{
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
{
_logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]");
action.Invoke(sourceId);
return;
}
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
return;
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]");
AddSourceId(sceneItemName, sceneItemId);
action.Invoke(sceneItemId);
});
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("N");
}
}
public class RequestData
{
public RequestMessage Message { get; }
public string ParentId { get; }
public Dictionary<string, object> ResponseValues { get; set; }
public Action<Dictionary<string, object>>? Callback { get; set; }
public RequestData(RequestMessage message, string parentId)
{
Message = message;
ParentId = parentId;
}
}
}

View File

@ -3,21 +3,365 @@ using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System.Text.Json;
using System.Collections.Concurrent;
using TwitchChatTTS.OBS.Socket.Data;
using System.Timers;
using System.Net.WebSockets;
namespace TwitchChatTTS.OBS.Socket
{
public class OBSSocketClient : WebSocketClient
{
private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private System.Timers.Timer _reconnectTimer;
public bool Connected { get; set; }
public bool Identified { get; set; }
public bool Streaming { get; set; }
public OBSSocketClient(
ILogger logger,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
Configuration configuration,
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger
) : base(handlers, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})
}, logger)
{
_configuration = configuration;
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
_reconnectTimer.Enabled = false;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>();
}
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
OnConnected += (sender, e) =>
{
Connected = true;
_reconnectTimer.Enabled = false;
_logger.Information("OBS websocket client connected.");
};
OnDisconnected += (sender, e) =>
{
_reconnectTimer.Enabled = Identified;
_logger.Information($"OBS websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
Connected = false;
Identified = false;
Streaming = false;
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
}
public void AddSourceId(string sourceName, long sourceId)
{
if (!_sourceIds.TryGetValue(sourceName, out _))
_sourceIds.Add(sourceName, sourceId);
else
_sourceIds[sourceName] = sourceId;
_logger.Debug($"Added OBS scene item to cache [scene item: {sourceName}][scene item id: {sourceId}]");
}
public void ClearCache()
{
_sourceIds.Clear();
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(URL))
{
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
return;
}
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
try
{
await ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
}
}
public async Task ExecuteRequest(RequestResponseMessage message) {
if (!_handlers.TryGetValue(7, out var handler) || handler == null)
{
_logger.Error("Failed to find the request response handler for OBS.");
return;
}
await handler.Execute(this, message);
}
private async Task Reconnect(ElapsedEventArgs e)
{
if (Connected)
{
try
{
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
}
catch (Exception)
{
_logger.Error("Failed to disconnect from OBS websocket server.");
}
}
try
{
await Connect();
}
catch (WebSocketException wse) when (wse.Message.Contains("502"))
{
_logger.Error("OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings.");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to reconnect to OBS websocket server.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
var list = messages.ToList();
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
// Keep track of requests to know what we requested.
foreach (var message in list)
{
message.RequestId = GenerateUniqueIdentifier();
var data = new RequestData(message, uid);
_requests.Add(message.RequestId, data);
}
_logger.Debug($"Generated uid for all OBS request messages in batch [obs request batch id: {uid}][obs request ids: {string.Join(", ", list.Select(m => m.RequestType + "=" + m.RequestId))}]");
await Send(8, new RequestBatchMessage(uid, list));
}
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
// Keep track of requests to know what we requested.
message.RequestId = uid;
var data = new RequestData(message, uid)
{
Callback = callback
};
_requests.Add(message.RequestId, data);
await Send(6, message);
}
public RequestData? Take(string id)
{
if (id != null && _requests.TryGetValue(id, out var request))
{
_requests.Remove(id);
return request;
}
return null;
}
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
if (action == null)
return;
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (transform == null)
{
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m2.RequestId}].");
return;
}
double w = transform.Width;
double h = transform.Height;
int a = transform.Alignment;
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
if (a != (int)OBSAlignment.Center)
{
if (hasBounds)
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
else
transform.Alignment = a = (int)OBSAlignment.Center;
transform.PositionX = transform.PositionX + w / 2;
transform.PositionY = transform.PositionY + h / 2;
}
action?.Invoke(transform);
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
await Send(m3);
_logger.Debug($"New transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obs request id: {m3.RequestId}]");
});
});
}
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m1, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemEnabled", out object? visible) || visible == null)
return;
var m2 = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", visible.ToString().ToLower() == "true" ? false : true } });
await Send(m2);
});
});
}
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
await Send(m);
});
}
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
{
await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
await Send(m);
});
}
public async Task GetGroupList(Action<IEnumerable<string>>? action)
{
var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary<string, object>());
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("groups", out object? value) || value == null)
return;
var list = (IEnumerable<string>)value;
_logger.Debug("Fetched the list of groups in OBS.");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(string groupName, Action<IEnumerable<OBSSceneItem>>? action)
{
var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", groupName } });
await Send(m, (d) =>
{
if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null)
return;
var list = (IEnumerable<OBSSceneItem>)value;
_logger.Debug($"Fetched the list of OBS scene items in a group [group: {groupName}]");
if (list != null)
action?.Invoke(list);
});
}
public async Task GetGroupSceneItemList(IEnumerable<string> groupNames)
{
var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary<string, object>() { { "sceneName", group } }));
await Send(messages);
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
}
private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
{
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
{
_logger.Debug($"Fetched scene item id from cache [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sourceId}]");
action.Invoke(sourceId);
return;
}
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m, async (d) =>
{
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
return;
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][scene item: {sceneItemName}][scene item id: {sceneItemId}][obs request id: {m.RequestId}]");
AddSourceId(sceneItemName, sceneItemId);
action.Invoke(sceneItemId);
});
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("N");
}
}
public class RequestData
{
public RequestMessage Message { get; }
public string ParentId { get; }
public Dictionary<string, object>? ResponseValues { get; set; }
public Action<Dictionary<string, object>>? Callback { get; set; }
public RequestData(RequestMessage message, string parentId)
{
Message = message;
ParentId = parentId;
}
}
}