using CommonSocketLibrary.Common; 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 _requests; private readonly IDictionary _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( Configuration configuration, [FromKeyedServices("obs")] IEnumerable handlers, [FromKeyedServices("obs")] MessageTypeManager 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(); _sourceIds = new Dictionary(); } 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 override 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 [code: {wse.ErrorCode}]"); } catch (Exception ex) { _logger.Error(ex, "Failed to reconnect to OBS websocket server."); } } public async Task Send(IEnumerable 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>? 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 action) { if (action == null) return; await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) => { var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary() { { "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(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() { { "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() { { "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() { { "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() { { "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() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); await Send(m); }); } public async Task GetGroupList(Action>? action) { var m = new RequestMessage("GetGroupList", string.Empty, new Dictionary()); await Send(m, (d) => { if (d == null || !d.TryGetValue("groups", out object? value) || value == null) return; var list = (IEnumerable)value; _logger.Debug("Fetched the list of groups in OBS."); if (list != null) action?.Invoke(list); }); } public async Task GetGroupSceneItemList(string groupName, Action>? action) { var m = new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "sceneName", groupName } }); await Send(m, (d) => { if (d == null || !d.TryGetValue("sceneItems", out object? value) || value == null) return; var list = (IEnumerable)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 groupNames) { var messages = groupNames.Select(group => new RequestMessage("GetGroupSceneItemList", string.Empty, new Dictionary() { { "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 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() { { "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? ResponseValues { get; set; } public Action>? Callback { get; set; } public RequestData(RequestMessage message, string parentId) { Message = message; ParentId = parentId; } } }