Revised the redeem system, activated via channel point redeems. Added OBS transformation to redeems. Logs changed & writes to logs folder as well. Removed most use of IServiceProvider.

This commit is contained in:
Tom
2024-06-24 22:11:36 +00:00
parent 706cd06930
commit 706eecf2d2
45 changed files with 964 additions and 577 deletions

View File

@ -0,0 +1,8 @@
namespace TwitchChatTTS.OBS.Socket.Data
{
public enum OBSAlignment
{
Center = 0,
TopLeft = 5
}
}

View File

@ -0,0 +1,24 @@
namespace TwitchChatTTS.OBS.Socket.Data
{
public class OBSTransformationData
{
public int Alignment { get; set; }
public int BoundsAlignment { get; set; }
public double BoundsHeight { get; set; }
public string BoundsType { get; set; }
public double BoundsWidth { get; set; }
public int CropBottom { get; set; }
public int CropLeft { get; set; }
public int CropRight { get; set; }
public int CropTop { get; set; }
public double Height { get; set; }
public double PositionX { get; set; }
public double PositionY { get; set; }
public double Rotation { get; set; }
public double ScaleX { get; set; }
public double ScaleY { get; set; }
public double SourceHeight { get; set; }
public double SourceWidth { get; set; }
public double Width { get; set; }
}
}

View File

@ -7,31 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class EventMessageHandler : IWebSocketHandler
{
private ILogger _logger { get; }
private IServiceProvider _serviceProvider { get; }
public int OperationCode { get; set; } = 5;
private readonly ILogger _logger;
public int OperationCode { get; } = 5;
public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider)
public EventMessageHandler(ILogger logger)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (message is not EventMessage obj || obj == null)
if (data is not EventMessage message || message == null)
return;
switch (obj.EventType)
switch (message.EventType)
{
case "StreamStateChanged":
case "RecordStateChanged":
if (sender is not OBSSocketClient client)
return;
string? raw_state = obj.EventData["outputState"].ToString();
string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower();
client.Live = obj.EventData["outputActive"].ToString() == "True";
client.Live = message.EventData["outputActive"].ToString() == "True";
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
if (client.Live == false && state != null && !state.EndsWith("ing"))
@ -40,7 +38,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
break;
default:
_logger.Debug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
_logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
break;
}
}

View File

@ -10,30 +10,30 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class HelloHandler : IWebSocketHandler
{
private ILogger _logger { get; }
public int OperationCode { get; set; } = 0;
private HelloContext _context { get; }
private readonly HelloContext _context;
private readonly ILogger _logger;
public int OperationCode { get; } = 0;
public HelloHandler(ILogger logger, HelloContext context)
public HelloHandler(HelloContext context, ILogger logger)
{
_logger = logger;
_context = context;
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (message is not HelloMessage obj || obj == null)
if (data is not HelloMessage message || message == null)
return;
_logger.Verbose("OBS websocket password: " + _context.Password);
if (obj.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
{
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, string.Empty, 1023 | 262144));
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
return;
}
var salt = obj.Authentication.Salt;
var challenge = obj.Authentication.Challenge;
var salt = message.Authentication.Salt;
var challenge = message.Authentication.Challenge;
_logger.Verbose("Salt: " + salt);
_logger.Verbose("Challenge: " + challenge);
@ -52,7 +52,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
_logger.Verbose("Final hash: " + hash);
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144));
await sender.Send(1, new IdentifyMessage(message.RpcVersion, hash, 1023 | 262144));
}
}
}

View File

@ -7,21 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class IdentifiedHandler : IWebSocketHandler
{
private ILogger Logger { get; }
public int OperationCode { get; set; } = 2;
private readonly ILogger _logger;
public int OperationCode { get; } = 2;
public IdentifiedHandler(ILogger logger)
{
Logger = logger;
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (message is not IdentifiedMessage obj || obj == null)
if (data is not IdentifiedMessage message || message == null)
return;
sender.Connected = true;
Logger.Information("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
await Task.Delay(TimeSpan.FromSeconds(5));
/*var messages = new RequestMessage[] {
//new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 5000 } }),
new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" } }),
};
await _manager.Send(messages);*/
}
}
}

View File

@ -11,18 +11,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestBatchResponseHandler : IWebSocketHandler
{
private OBSRequestBatchManager _manager { get; }
private IServiceProvider _serviceProvider { get; }
private ILogger _logger { get; }
private JsonSerializerOptions _options;
public int OperationCode { get; set; } = 9;
private readonly IWebSocketHandler _requestResponseHandler;
private readonly ILogger _logger;
public int OperationCode { get; } = 9;
public RequestBatchResponseHandler(OBSRequestBatchManager manager, JsonSerializerOptions options, IServiceProvider serviceProvider, ILogger logger)
public RequestBatchResponseHandler(
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
ILogger logger
)
{
_manager = manager;
_serviceProvider = serviceProvider;
_requestResponseHandler = requestResponseHandler;
_logger = logger;
_options = options;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
@ -32,54 +31,37 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
using (LogContext.PushProperty("obsrid", message.RequestId))
{
var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages.");
var requestData = _manager.Take(message.RequestId);
if (requestData == null || !results.Any())
{
_logger.Verbose($"Received request batch response of {results.Count} messages.");
return;
}
IList<Task> tasks = new List<Task>();
int count = Math.Min(results.Count, requestData.RequestTypes.Count);
int count = results.Count;
for (int i = 0; i < count; i++)
{
Type type = requestData.RequestTypes[i];
using (LogContext.PushProperty("type", type.Name))
if (results[i] == null)
continue;
try
{
try
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString(), new JsonSerializerOptions()
{
var handler = GetResponseHandlerForRequestType(type);
_logger.Verbose($"Request handled by {handler.GetType().Name}.");
tasks.Add(handler.Execute(sender, results[i]));
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process an item in a request batch message.");
}
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.");
}
}
_logger.Verbose($"Waiting for processing to complete.");
await Task.WhenAll(tasks);
_logger.Debug($"Finished processing all request in this batch.");
}
}
private IWebSocketHandler? GetResponseHandlerForRequestType(Type type)
{
if (type == typeof(RequestMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestresponse");
else if (type == typeof(RequestBatchMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestbatcresponse");
else if (type == typeof(IdentifyMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-identified");
return null;
}
}
}

View File

@ -2,36 +2,100 @@ 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 ILogger Logger { get; }
public int OperationCode { get; set; } = 7;
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 7;
public RequestResponseHandler(ILogger logger)
public RequestResponseHandler(OBSManager manager, ILogger logger)
{
Logger = logger;
_manager = manager;
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (message is not RequestResponseMessage obj || obj == null)
if (data is not RequestResponseMessage message || message == null)
return;
switch (obj.RequestType)
{
case "GetOutputStatus":
if (sender is not OBSSocketClient client)
return;
_logger.Debug($"Received an OBS request response [response id: {message.RequestId}]");
if (obj.RequestId == "stream")
{
client.Live = obj.ResponseData["outputActive"].ToString() == "True";
Logger.Warning("Updated stream's live status to " + client.Live);
}
break;
var requestData = _manager.Take(message.RequestId);
if (requestData == null)
{
_logger.Warning($"OBS Request Response not being processed: request not stored [response id: {message.RequestId}]");
return;
}
var request = requestData.Message;
if (request == null)
return;
try
{
switch (request.RequestType)
{
case "GetOutputStatus":
if (sender is not OBSSocketClient client)
return;
if (message.RequestId == "stream")
{
client.Live = message.ResponseData["outputActive"].ToString() == "True";
_logger.Warning($"Updated stream's live status to {client.Live} [response id: {message.RequestId}]");
}
break;
case "GetSceneItemId":
if (!request.RequestData.TryGetValue("sceneName", out object sceneName))
{
_logger.Warning($"Failed to find the scene name that was requested [response id: {message.RequestId}]");
return;
}
if (!request.RequestData.TryGetValue("sourceName", out object sourceName))
{
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][response id: {message.RequestId}]");
return;
}
if (!message.ResponseData.TryGetValue("sceneItemId", out object sceneItemId)) {
_logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][response id: {message.RequestId}]");
return;
}
_logger.Information($"Added scene item id [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][response id: {message.RequestId}].");
_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), long.Parse(sceneItemId.ToString()));
requestData.ResponseValues = new Dictionary<string, object>
{
{ "sceneItemId", sceneItemId }
};
break;
case "GetSceneItemTransform":
if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData))
{
_logger.Warning($"Failed to find the OBS scene item [response id: {message.RequestId}]");
return;
}
_logger.Verbose("Fetching OBS transformation data: " + transformData?.ToString());
requestData.ResponseValues = new Dictionary<string, object>
{
{ "sceneItemTransform", transformData }
};
break;
default:
_logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? new string[0])}]");
break;
}
}
finally
{
if (requestData.Callback != null)
requestData.Callback(requestData.ResponseValues);
}
}
}

View File

@ -1,60 +0,0 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSRequestBatchManager
{
private IDictionary<string, OBSRequestBatchData> _requests;
private IServiceProvider _serviceProvider;
private ILogger _logger;
public OBSRequestBatchManager(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task Send(long broadcasterId, IEnumerable<WebSocketMessage> messages) {
string uid = GenerateUniqueIdentifier();
var data = new OBSRequestBatchData(broadcasterId, uid, new List<Type>());
_logger.Debug($"Sending request batch of {messages.Count()} messages.");
foreach (WebSocketMessage message in messages)
data.RequestTypes.Add(message.GetType());
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(8, new RequestBatchMessage(uid, messages));
}
public OBSRequestBatchData? Take(string id) {
if (_requests.TryGetValue(id, out var request)) {
_requests.Remove(id);
return request;
}
return null;
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("X");
}
}
public class OBSRequestBatchData
{
public long BroadcasterId { get; }
public string RequestId { get; }
public IList<Type> RequestTypes { get; }
public OBSRequestBatchData(long bid, string rid, IList<Type> types) {
BroadcasterId = bid;
RequestId = rid;
RequestTypes = types;
}
}
}

View File

@ -26,7 +26,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
continue;
}
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
Add(handler);
}
}

View File

@ -0,0 +1,210 @@
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 IDictionary<string, RequestData> _requests;
private IDictionary<string, IDictionary<string, long>> _sourceIds;
private IServiceProvider _serviceProvider;
private ILogger _logger;
public OBSManager(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, IDictionary<string, long>>();
}
public void AddSourceId(string sceneName, string sourceName, long sourceId)
{
if (!_sourceIds.TryGetValue(sceneName, out var scene))
{
scene = new Dictionary<string, long>();
_sourceIds.Add(sceneName, scene);
}
if (scene.ContainsKey(sourceName))
scene[sourceName] = sourceId;
else
scene.Add(sourceName, sourceId);
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obsid: {uid}].");
// Keep track of requests to know what we requested.
foreach (var message in messages)
{
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 [obsid: {uid}]: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(8, new RequestBatchMessage(uid, messages));
}
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [obsid: {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 UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m1, async (d) =>
{
if (!d.TryGetValue("sceneItemId", out object value) || !long.TryParse(value.ToString(), out long sceneItemId))
return;
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][obsid: {m1.RequestId}]: {sceneItemId}");
var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) =>
{
if (d == null)
return;
if (!d.TryGetValue("sceneItemTransform", out object transformData))
return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]: {transformData}");
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}][obsid: {m2.RequestId}].");
return;
}
//double fr = (transform.Rotation + rotation) % 360;
double w = transform.Width;
double h = transform.Height;
// double ox = w * Math.Cos(r) - h * Math.Sin(r);
// double oy = w * Math.Sin(r) + h * Math.Cos(r);
//var oo = (fr > 45 && fr < 225 ? 0 : 1);
// var ww = fr >= 135 && fr < 225 ? h : w;
// var hh = fr >= 315 || fr < 45 ? h : w;
//double dx = h * Math.Sin(r);
//double dy = w * Math.Cos(fr > 90 && fr < 270 ? Math.PI - r : r); // * (fr >= 135 && fr < 225 || fr >= 315 || fr <= 45 ? -1 : 1);
int a = transform.Alignment;
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
if (hasBounds)
{
// Take care of bounds, for most cases.
// 'Crop to Bounding Box' might be unsupported.
w = transform.BoundsWidth;
h = transform.BoundsHeight;
a = transform.BoundsAlignment;
}
else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0)
{
w -= transform.CropLeft + transform.CropRight;
h -= transform.CropTop + transform.CropBottom;
}
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);
// double ax = w * Math.Cos(ir) - h * Math.Sin(ir);
// double ay = w * Math.Sin(ir) + h * Math.Cos(ir);
// _logger.Information($"ax: {ax} ay: {ay}");
// double bx = w * Math.Cos(r) - h * Math.Sin(r);
// double by = w * Math.Sin(r) + h * Math.Cos(r);
// _logger.Information($"bx: {bx} by: {by}");
// double ddx = bx - ax;
// double ddy = by - ay;
// _logger.Information($"dx: {ddx} dy: {ddy}");
// double arctan = Math.Atan(ddy / ddx);
// _logger.Information("Angle: " + arctan);
// var xs = new int[] { 0, 0, 1, 1 };
// var ys = new int[] { 0, 1, 1, 0 };
// int i = ((int)Math.Floor(fr / 90) + 8) % 4;
// double dx = xs[i] * w * Math.Cos(rad) - ys[i] * h * Math.Sin(rad);
// double dy = xs[i] * w * Math.Sin(rad) + ys[i] * h * Math.Cos(rad);
//transform.Rotation = fr;
//_logger.Information($"w: {w} h: {h} fr: {fr} r: {r} rot: {rotation}");
//_logger.Information($"dx: {dx} ox: {ox} oox: {oox}");
//_logger.Information($"dy: {dy} oy: {oy} ooy: {ooy}");
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
await Send(m3);
});
});
}
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;
}
}
}