YorVeX / ObsCSharpExample

Example for an OBS plugin written in C# containing various standard items like output, filter, source or a settings dialog in the OBS Tools menu. Meant to be used both to learn writing OBS plugins in C# and as a template for creating new plugins or plugin content like a source or an output.
MIT License
12 stars 2 forks source link

OBS C# Example

Example for an OBS plugin written in C# containing various standard items like output, filter, source or a settings dialog in the OBS Tools menu. Meant to be used both to learn writing OBS plugins and C# and as a template for creating new plugins or plugin content like a source or an output.

image

OBS Classic still had a CLR Host Plugin, but with OBS Studio writing plugins in C# wasn't possible anymore. This has changed as of recently. With the release of .NET 7 that includes NativeAOT it is now possible to build native code libraries that can be loaded by OBS Studio. This repository is here to show you how.

Prerequisites

FAQ

C# programming for OBS with NetObsBindings

Type differences

Managed vs. unmanaged, instance vs. static

For various objects different approaches were used so that all of these variants are demonstrated. For each of these objects there will be a description which approach was used. What fits best for you will depend on your project.

Basically there is two ways how to handle callbacks from OBS for objects like outputs or sources:

  1. Store everything in the unmanaged object that is passed to and from OBS. --> Do this when only unmanaged fields need to be stored and managed code doesn't need to be applied to any of the data, e.g. when a filter does its job solely based on calling OBS functions. See here for another example outside of this repo for an example how this can be done and how the fields are accessed from the callbacks.
  2. Store everything in a managed object, which cannot be passed to and from OBS, so the OBS data is only used to identify/index the right managed data from a list. Then either the static callback directly works with the data from that list or invokes instance functions on an object from the list, which then can simply work with their own instance variables. --> Do this when you want/need to use managed code, e.g. to provide a small HTTP server or use an HTTP client from .NET base functionality. This introduces some extra complexity so should only be done when needed.

Mixtures of both variants are also possible.

Content

This plugin code demonstrates different concepts about using unmanaged vs. managed code within a C# OBS plugin for the different items and source code files it includes. What exactly each file demonstrates is explained in the next sections.

Disclaimer: The main focus of this project is to give a general idea of how an OBS plugin can be implemented with C#. I am new to OBS plugin programming myself, while I try my best to show how each of the items is properly implemented I might have made mistakes there that an experienced OBS plugin programmer wouldn't have made. Feel free to file GitHub issues for mistakes that you find either in the documentation text here or in the code and I will correct them.

ObsCSharpExample.csproj

This is the project file that defines the project to include NetObsBindings assembly and be compiled as a NativeAOT application. Feel free to adapt this to your needs. There is only this and no solution file (.sln) since Visual Studio wouldn't add anything to this except bloat ;-)

Module.cs

This is the central file for the module. As you can see from this example a plugin module can include multiple objects like sources, filters and outputs but they need to be registered and managed from a central instance, which is this module.

A secondary job this class has is providing global utility functionality to other classes in the module, e.g. a log function or an implementation for the OBS locale system (which is also on module level, meaning that all objects living in a module share the same locale files) with the ObsText(String) methods.

There is also a GetString() method which automatically frees memory from unmanaged strings from OBS before returning their managed string representation. Note that whether this should be used depends on the function the string is retrieved from. E.g. ObsData.obs_data_get_string() returns a string from a settings object that will continue to live after that method call so you need to leave the memory for it allocated, whereas Obs.obs_module_get_config_path() returns a string for your temporary use that should be "bfreed" afterwards, so use GetString() on that one for convenience.

Last but not least the Module class also takes care of talking to the frontend API to register an item for the Tools menu.

locale/en-US.ini

This file contains the locale strings used for this project. If you need new text items just add them here.

SettingsDialog.cs

This is not exactly the example of a standard procedure to register a dialog that can be called from the Tools main menu in OBS, more like a small hack. Instead of bringing our own Qt implementation to add a GUI object (the dialog window for our output settings) we create a dummy source that has properties attached to it and then show these properties using the ObsFrontendApi.obs_frontend_open_source_properties() method when clicking the Tools menu entry.

This smart idea was blatantly stolen from fzwoch, who used this method to register the settings dialog for his very nice obs-teleport plugin. I liked it so much that I thought it should definitely be part of an example project. But beware, as with all hacks it might break because of future changes to it that don't accomodate for this unintended way of using things. On the other hand it has the advantage that it will keep on working regardless of Qt (or in general UI) library changes in OBS and always respect theme settings correctly.

In addition to example settings (called "properties" in OBS) there is also buttons to start and stop the example output that is implemented in the Output class in Output.cs. Note that as soon as an output is active certain settings in OBS are locked, e.g. the resolution, so that would also be a way to test whether the output was really started after you clicked the Start button.

Output.cs

This class registers an output in OBS. When it's active it receives all the frame data from OBS. This specific output doesn't register for audio data, though it still shows the function signature necessary for the raw_audio callback, which needs to be provided even when the output is only registering itself for video data. The video data is itself is also not really processed, for the sake of this example the plugin is merely logging the frame timestamps as OBS debug log messages.

Managed vs. unmanaged, instance vs. static

The assumption is that there will be only one output, so everything is based on static fields and methods. A "Context" struct is used to show the basic concept behind this for the OBS related objects, this is the struct that will be passed to OBS and passed back to the plugin by OBS when callbacks are invoked, although it is not really used. Other things are simply stored in global variables (they are named with preceding underscores), since everything is static and only one output uses this it won't be a problem.

Source.cs

This class registers a source in OBS. To give you a template for the callbacks there are very many implemented here, even when they don't do anything but logging that they were called so that you can test when which callback is invoked by OBS. On module level it also prepares an image downloaded from the internet that is used as a texture, so it also has some managed code when using the HttpClient class for this.

It also shows the basic concept of surrounding graphics functions with Obs.obs_enter_graphics() and Obs.obs_leave_graphics() calls and image_source_video_render() shows how a texture can be drawn.

Example properties of various types are added to show how a source could be made configurable.

Managed vs. unmanaged, instance vs. static

There can be more than one source of this type, however, the callback code doesn't do anything source specific. Instead the texture to be drawn is simply shared between all sources so much like for the Output class this is simply stored in global variables.

Filter.cs

This class registers a filter in OBS, which internally is also just a source with some specific flags and a few different callbacks. A getFilter() helper function makes the transition from a static callback context to the instance context easy, based on the object that OBS hands over for each callback.

Example properties of various types are added to show how a source could be made configurable.

Managed vs. unmanaged, instance vs. static

Potentially an infinite number of filters could be added to various sources or even the same source, hence data needs to be stored per filter. However, the callbacks are still static so unlike for the Output class we really need the data passed to us by OBS to identify the filter the code is called for. For the sake of this example let's pretend managed code is needed, therefore in this case the Context structure is only used to identify the filter by an ID number from a list and store everything else in instance variables. Then we can invoke instance functions and these can work within the context of their instance. This is demonstrated here with the ProcessFrame() function which is a fully managed function working with its instance variables.

Building

This plugin depends on the NetObsBindings for building, you don't need to build this from source though, the project file provided in this repo already includes this as a NuGet package. Generally the included build.cmd file is executing the necessary command to create the build, but some prerequisites need to be installed in the system first.

Preparing the build environment

Build without VS Code

Build and working with VS Code

Credits

Many thanks to kostya9 for laying the groundwork of C# OBS Studio plugin creation, without him this plugin (and hopefully many more C# plugins following in the future) wouldn't exist. Read about his ventures into this area in his blog posts here and here.