2024-03-12 14:05:27 -04:00
using CommonSocketLibrary.Common ;
using CommonSocketLibrary.Abstract ;
using Microsoft.Extensions.DependencyInjection ;
2024-06-16 20:19:31 -04:00
using Serilog ;
2024-03-12 14:05:27 -04:00
using System.Text.Json ;
2024-07-19 12:56:41 -04:00
using System.Collections.Concurrent ;
using TwitchChatTTS.OBS.Socket.Data ;
using System.Timers ;
using System.Net.WebSockets ;
2024-03-12 14:05:27 -04:00
namespace TwitchChatTTS.OBS.Socket
{
2024-06-16 20:19:31 -04:00
public class OBSSocketClient : WebSocketClient
{
2024-07-19 12:56:41 -04:00
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 ; }
2024-03-12 14:05:27 -04:00
public OBSSocketClient (
2024-07-19 12:56:41 -04:00
Configuration configuration ,
[FromKeyedServices("obs")] IEnumerable < IWebSocketHandler > handlers ,
[FromKeyedServices("obs")] MessageTypeManager < IWebSocketHandler > typeManager ,
ILogger logger
) : base ( handlers , typeManager , new JsonSerializerOptions ( )
2024-06-16 20:19:31 -04:00
{
2024-03-12 14:05:27 -04:00
PropertyNameCaseInsensitive = false ,
PropertyNamingPolicy = JsonNamingPolicy . CamelCase
2024-07-19 12:56:41 -04:00
} , 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" ) )
{
2024-08-04 19:46:10 -04:00
_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}]" ) ;
2024-07-19 12:56:41 -04:00
}
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 )
2024-06-16 20:19:31 -04:00
{
2024-07-19 12:56:41 -04:00
Message = message ;
ParentId = parentId ;
2024-03-12 14:05:27 -04:00
}
}
}