Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
YorVeX committed Feb 12, 2023
0 parents commit 3ed81cb
Show file tree
Hide file tree
Showing 8 changed files with 835 additions and 0 deletions.
481 changes: 481 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"-c",
"Release",
"-o",
"publish",
"-r",
"win-x64",
"/p:NativeLib=Shared",
"/p:SelfContained=true"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "test",
"type": "shell",
"command": ".\\test.local.cmd"
},
{
"label": "publish-and-test",
"type": "shell",
"command": ".\\test.local.cmd",
"dependsOrder": "sequence",
"dependsOn": ["publish"],
"problemMatcher": "$msCompile"
}
]
}
164 changes: 164 additions & 0 deletions BrowserFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Runtime.InteropServices;
using System.Text;
using ObsInterop;
namespace ObsCSharpExample;

public class BrowserFilter
{
public unsafe struct Context
{
public obs_source* Source;
public obs_data* Settings;
public bool Active;
public float SecondsWaited;
public int RefreshIntervalSeconds;
}

#region Helper methods
public static unsafe void Register()
{
var sourceInfo = new obs_source_info();
fixed (byte* id = Encoding.UTF8.GetBytes(Module.ModuleName + " Browser Filter"))
{
sourceInfo.id = (sbyte*)id;
sourceInfo.type = obs_source_type.OBS_SOURCE_TYPE_FILTER;
sourceInfo.output_flags = ObsSource.OBS_SOURCE_VIDEO;
sourceInfo.get_name = &filter_get_name;
sourceInfo.create = &filter_create;
sourceInfo.show = &filter_show;
sourceInfo.hide = &filter_hide;
sourceInfo.destroy = &filter_destroy;
sourceInfo.get_defaults = &filter_get_defaults;
sourceInfo.get_properties = &filter_get_properties;
sourceInfo.save = &filter_save;
sourceInfo.video_tick = &filter_tick;
ObsSource.obs_register_source_s(&sourceInfo, (nuint)Marshal.SizeOf(sourceInfo));
}
}

public static unsafe void RefreshBrowserSource(Context* context)
{
//TODO: feature: freeze the source display for a configurable amount of time during refresh to prevent flickering
Module.Log("RefreshBrowserSource called", ObsLogLevel.Debug);
var browserSource = Obs.obs_filter_get_parent(context->Source);
if ((browserSource == null) || !Convert.ToBoolean(Obs.obs_source_active(browserSource)))
return;

new Thread(() =>
{
var sourceProperties = Obs.obs_source_properties(browserSource);
fixed (byte* refreshButtonId = Encoding.UTF8.GetBytes("refreshnocache"))
{
var property = ObsProperties.obs_properties_get(sourceProperties, (sbyte*)refreshButtonId);
if (property != null) // could be null if this filter is applied on something that is not a browser source
{
Module.Log("Refreshing browser...", ObsLogLevel.Debug);
ObsProperties.obs_property_button_clicked(property, browserSource);
}
else
{
string sourceDisplayName = Marshal.PtrToStringUTF8((IntPtr)Obs.obs_source_get_name(browserSource))!;
Module.Log("Failed to refresh source \"" + sourceDisplayName + "\", is this filter really applied to a browser source?", ObsLogLevel.Error);
}
ObsProperties.obs_properties_destroy(sourceProperties);
}
}).Start();
}
#endregion Helper methods

#region Filter API methods
[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe sbyte* filter_get_name(void* data)
{
Module.Log("filter_get_name called", ObsLogLevel.Debug);
fixed (byte* logMessagePtr = Encoding.UTF8.GetBytes("Browser Auto-refresh"))
return (sbyte*)logMessagePtr;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void* filter_create(obs_data* settings, obs_source* source)
{
Module.Log("filter_create called", ObsLogLevel.Debug);

Context* context = (Context*)Marshal.AllocCoTaskMem(sizeof(Context));
context->Source = source;
context->Settings = settings;
fixed (byte* intervalId = Encoding.UTF8.GetBytes("interval"))
context->RefreshIntervalSeconds = (int)ObsData.obs_data_get_int(settings, (sbyte*)intervalId);
return (void*)context;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_show(void* data)
{
Module.Log("filter_show called", ObsLogLevel.Debug);

var context = (Context*)data;
context->SecondsWaited = 0; // if a browser refresh on show is wanted this can be configured in the browser source, the assumption here is that the interval starts from the time the browser source is being shown
context->Active = true;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_hide(void* data)
{
Module.Log("filter_hide called", ObsLogLevel.Debug);
((Context*)data)->Active = false;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_destroy(void* data)
{
Module.Log("filter_destroy called", ObsLogLevel.Debug);
Marshal.FreeCoTaskMem((IntPtr)data);
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe obs_properties* filter_get_properties(void* data)
{
Module.Log("filter_get_properties called", ObsLogLevel.Debug);

var properties = ObsProperties.obs_properties_create();
fixed (byte*
intervalId = Encoding.UTF8.GetBytes("interval"),
intervalCaption = Module.ObsText("IntervalCaption"),
intervalText = Module.ObsText("IntervalText")
)
{
var prop = ObsProperties.obs_properties_add_int(properties, (sbyte*)intervalId, (sbyte*)intervalCaption, 1, int.MaxValue, 1);
ObsProperties.obs_property_set_long_description(prop, (sbyte*)intervalText);
}
return properties;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_get_defaults(obs_data* settings)
{
Module.Log("filter_get_defaults called", ObsLogLevel.Debug);
fixed (byte* intervalId = Encoding.UTF8.GetBytes("interval"))
ObsData.obs_data_set_default_int(settings, (sbyte*)intervalId, 60);
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_save(void* data, obs_data* settings)
{
var context = (Context*)data;
Module.Log("filter_save called", ObsLogLevel.Debug);
fixed (byte* intervalId = Encoding.UTF8.GetBytes("interval"))
context->RefreshIntervalSeconds = (int)ObsData.obs_data_get_int(settings, (sbyte*)intervalId);
Module.Log("Browser auto refresh interval was set to " + context->RefreshIntervalSeconds + " second(s)", ObsLogLevel.Debug);
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void filter_tick(void* data, float seconds)
{
var context = (Context*)data;
if (context->Active && ((context->SecondsWaited += seconds) >= context->RefreshIntervalSeconds))
{
context->SecondsWaited -= context->RefreshIntervalSeconds;
RefreshBrowserSource(context);
}
}
#endregion Filter API methods


}
128 changes: 128 additions & 0 deletions Module.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Runtime.InteropServices;
using System.Text;
using ObsInterop;
namespace ObsCSharpExample;

public enum ObsLogLevel : int
{
Error = ObsBase.LOG_ERROR,
Warning = ObsBase.LOG_WARNING,
Info = ObsBase.LOG_INFO,
Debug = ObsBase.LOG_DEBUG
}

public static class Module
{

const bool DebugLog = false; // set this to true and recompile to get debug messages from this plug-in only (unlike getting the full log spam when enabling debug log globally in OBS)
const string DefaultLocale = "en-US";
public static string ModuleName = "xObsBrowserAutoRefresh";
static string _locale = DefaultLocale;
static unsafe obs_module* _obsModule = null;
public static unsafe obs_module* ObsModule { get => _obsModule; }
static unsafe text_lookup* _textLookupModule = null;

#region Helper methods
public static unsafe void Log(string text, ObsLogLevel logLevel)
{
if (DebugLog && (logLevel == ObsLogLevel.Debug))
logLevel = ObsLogLevel.Info;
// need to escape %, otherwise they are treated as format items, but since we provide null as arguments list this crashes OBS
fixed (byte* logMessagePtr = Encoding.UTF8.GetBytes("[" + ModuleName + "] " + text.Replace("%", "%%")))
ObsBase.blogva((int)logLevel, (sbyte*)logMessagePtr, null);
}

public static unsafe byte[] ObsText(string identifier, params object[] args)
{
return Encoding.UTF8.GetBytes(string.Format(ObsTextString(identifier), args));
}

public static unsafe byte[] ObsText(string identifier)
{
return Encoding.UTF8.GetBytes(ObsTextString(identifier));
}

public static unsafe string ObsTextString(string identifier, params object[] args)
{
return string.Format(ObsTextString(identifier), args);
}

public static unsafe string ObsTextString(string identifier)
{
fixed (byte* lookupVal = Encoding.UTF8.GetBytes(identifier))
{
sbyte* lookupResult = null;
ObsTextLookup.text_lookup_getstr(_textLookupModule, (sbyte*)lookupVal, &lookupResult);
var resultString = Marshal.PtrToStringUTF8((IntPtr)lookupResult);
if (string.IsNullOrEmpty(resultString))
return "<MissingLocale:" + _locale + ":" + identifier + ">";
else
return resultString;
}
}
#endregion Helper methods

#region OBS module API methods
[UnmanagedCallersOnly(EntryPoint = "obs_module_set_pointer", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void obs_module_set_pointer(obs_module* obsModulePointer)
{
Log("obs_module_set_pointer called", ObsLogLevel.Debug);
ModuleName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name!;
_obsModule = obsModulePointer;
}

[UnmanagedCallersOnly(EntryPoint = "obs_module_ver", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static uint obs_module_ver()
{
var major = (uint)Obs.Version.Major;
var minor = (uint)Obs.Version.Minor;
var patch = (uint)Obs.Version.Build;
var version = (major << 24) | (minor << 16) | patch;
return version;
}

[UnmanagedCallersOnly(EntryPoint = "obs_module_load", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe bool obs_module_load()
{
Log("Loading...", ObsLogLevel.Debug);

BrowserFilter.Register();
var assemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName();
Version version = assemblyName.Version!;
Log("Version " + version.Major + "." + version.Minor + " loaded.", ObsLogLevel.Info);
return true;
}

[UnmanagedCallersOnly(EntryPoint = "obs_module_unload", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void obs_module_unload()
{
Log("obs_module_unload called", ObsLogLevel.Debug);
}

[UnmanagedCallersOnly(EntryPoint = "obs_module_set_locale", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void obs_module_set_locale(char* locale)
{
Log("obs_module_set_locale called", ObsLogLevel.Debug);
var localeString = Marshal.PtrToStringUTF8((IntPtr)locale);
if (!string.IsNullOrEmpty(localeString))
{
_locale = localeString;
Log("Locale is set to: " + _locale, ObsLogLevel.Debug);
}
if (_textLookupModule != null)
ObsTextLookup.text_lookup_destroy(_textLookupModule);
fixed (byte* defaultLocale = Encoding.UTF8.GetBytes(DefaultLocale), currentLocale = Encoding.UTF8.GetBytes(_locale))
_textLookupModule = Obs.obs_module_load_locale(_obsModule, (sbyte*)defaultLocale, (sbyte*)currentLocale);
}

[UnmanagedCallersOnly(EntryPoint = "obs_module_free_locale", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static unsafe void obs_module_free_locale()
{
Log("obs_module_free_locale called", ObsLogLevel.Debug);
if (_textLookupModule != null)
ObsTextLookup.text_lookup_destroy(_textLookupModule);
_textLookupModule = null;
}
#endregion OBS module API methods

}
2 changes: 2 additions & 0 deletions build.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dotnet publish -c Release -o publish -r win-x64 /p:NativeLib=Shared /p:SelfContained=true
pause
2 changes: 2 additions & 0 deletions locale/de-DE.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IntervalCaption="⏱️ Aktualisierungsintervall (Sekunden)"
IntervalText="Das Intervall in Sekunden, in dem die Browser-Quelle automatisch aktualisiert werden soll."
2 changes: 2 additions & 0 deletions locale/en-US.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IntervalCaption="⏱️ Refresh interval (seconds)"
IntervalText="The interval in seconds in which the browser source should be auto-refreshed."
17 changes: 17 additions & 0 deletions xObsBrowserAutoRefresh.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishAot>true</PublishAot>
<Authors>YorVeX (yorvex@yorvex.tv, @YorVeX)</Authors>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NetObsBindings" Version="0.0.1.29-alpha" />
</ItemGroup>

</Project>

0 comments on commit 3ed81cb

Please sign in to comment.