Skip to content
WarmUpTill edited this page Sep 25, 2024 · 14 revisions

This section will describe how to use the scripting API to add custom macro conditions and actions.
The API is not limited to scripts or Python, but the examples showcased here will only be using Python for brevity.

Examples

Custom macro condition

This examples shows how to register a new condition type which will randomly evaluate to true based on provided user input percentage in the range from 0 to 100.

What it looks like

This is the custom condition type we will define with this script in action:

RandomCondition

As you can see the frequency at which this condition returns true depends on the provided input value, just as we expect it to.

The code

import obspython as obs
import threading  # Required by advss helpers
import random

CONDITION_NAME = "Random condition"


###############################################################################
# This function will define the UI for the custom condition type based on a obs_properties object.
# In this case only a single float selection field for specifying the probability of returning true.
# This isn't mandatory.
# You can also have a condition without any additional UI elements.
###############################################################################
def get_condition_properties():
    props = obs.obs_properties_create()
    obs.obs_properties_add_float(
        props, "probability", "Probability of returning true", 0, 100, 0.1
    )
    return props


###############################################################################
# You can provide default values for each of the settings you have defined earlier.
# This isn't mandatory.
# You can also have a condition without any settings to be modified.
###############################################################################
def get_condition_defaults():
    default_settings = obs.obs_data_create()
    obs.obs_data_set_default_double(default_settings, "probability", 33.3)
    return default_settings


###############################################################################
# This function will be used by the advanced scene switcher to determine if a condition evaluates to true or false.
# The settings for each instance of this condition type will be passed via the data parameter.
###############################################################################
def my_python_condition(data):
    target = obs.obs_data_get_double(data, "probability")
    value = random.uniform(0, 100)
    return value <= target


###############################################################################
# Let's register the new condition type
###############################################################################
def script_load(settings):
    advss_register_condition(
        CONDITION_NAME,
        my_python_condition,
        get_condition_properties,
        get_condition_defaults(),
    )


###############################################################################
# Deregistering is useful if you plan unloading the script files
###############################################################################
def script_unload():
    advss_deregister_condition(CONDITION_NAME)


###############################################################################

# Advanced Scene Switcher helper functions below:
# Omitted on this wiki page for brevity!

The Advanced Scene Switcher helper functions boilerplate code mentioned above can be found here for Python and Lua.
Usually you can just copy it directly into your script without any modifications.

Discord chat bot action

The following example will add an action which will allow you to send discord message to specified channels.
As the process is very similar to the example above the descriptions will be a bit more limited in detail.

This example assumes you have install the discord python package:
pip3 install discord

What it looks like

The bot credentials can be configured in the scripts menu:

The action itself will allow you to configure the message to send and the channel to send it to.

The code

import obspython as obs
import threading  # Required by advss helpers

import discord
from discord.ext import commands
import asyncio

action_name = "Discord action"
token = None
loop = None
bot = None
bot_thread = None


###############################################################################
# Discord functions
###############################################################################


def create_bot():
    global bot
    intents = discord.Intents.default()
    bot = commands.Bot(command_prefix="!", intents=intents)

    @bot.event
    async def on_ready():
        obs.script_log(obs.LOG_WARNING, f"Logged in as {bot.user.name}!")

    return bot


async def send_message(channel_id, content):
    channel = bot.get_channel(channel_id)
    if channel:
        await channel.send(content)


def start_bot():
    global loop, bot
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    bot = create_bot()
    loop.create_task(bot.start(token))
    loop.run_forever()


async def stop_bot():
    await bot.close()


def restart_bot():
    global token, bot_thread

    if loop and bot.is_closed() == False:
        asyncio.run_coroutine_threadsafe(stop_bot(), loop).result()

    bot_thread = threading.Thread(target=start_bot)
    bot_thread.start()


###############################################################################
# Macro action functions
###############################################################################


def run_action(data):
    message = obs.obs_data_get_string(data, "message")
    channel_id_string = obs.obs_data_get_string(data, "channel_id")
    try:
        channel_id = int(channel_id_string)
    except ValueError:
        obs.script_log(
            obs.LOG_WARNING, f"Invalid channel ID string {channel_id_string}!"
        )
        return
    asyncio.run_coroutine_threadsafe(send_message(channel_id, message), loop)


def get_action_properties():
    props = obs.obs_properties_create()
    obs.obs_properties_add_text(props, "message", "Message", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_text(props, "channel_id", "Channel ID", obs.OBS_TEXT_DEFAULT)
    return props


def get_action_defaults():
    default_settings = obs.obs_data_create()
    obs.obs_data_set_default_string(default_settings, "message", "Your message here!")
    obs.obs_data_set_default_string(default_settings, "channel_id", "0")
    return default_settings


###############################################################################
# Script settings and description
###############################################################################


def script_description():
    return f'Adds the macro action "{action_name}" for the advanced scene switcher'


def script_update(settings):
    global token
    token = obs.obs_data_get_string(settings, "token")


def script_defaults(settings):
    obs.obs_data_set_default_string(settings, "token", "enter your bot token here")


def restart_pressed(props, prop):
    restart_bot()


def script_properties():
    props = obs.obs_properties_create()
    obs.obs_properties_add_text(props, "token", "Bot Token", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_button(props, "button", "Restart bot", restart_pressed)
    return props


###############################################################################
# Main script entry point
###############################################################################


def script_load(settings):
    global action_name
    advss_register_action(
        action_name,
        run_action,
        get_action_properties,
        get_action_defaults(),
    )
    bot_thread = threading.Thread(target=start_bot)
    bot_thread.start()


def script_unload():
    global action_name
    advss_deregister_action(action_name)


###############################################################################

# Advanced Scene Switcher helper functions below:
# Omitted on this wiki page for brevity!

Open Sound Control server condition

The following example will add a condition which will allow you to receive OSC messages.
As the process is very similar to the examples above the descriptions will be a bit more limited in detail.

This example assumes you have install the python-osc python package:
pip3 install python-osc

What it looks like

The OSC server details can be configured in the scripts menu and the message to look for can be configured in each condition instance.

OSC

Note that this is only a very simple example which probably does not cover all edge cases required for this condition type to be useful.

The code

import obspython as obs
import threading  # Required by advss helpers


from pythonosc.dispatcher import Dispatcher
from pythonosc import osc_server

condition_name = "Open Sound Control"
server = None
ip = None
port = None
received_messages = []


###############################################################################
# OSC server helper functions
###############################################################################


def append_message(addr, message, *args):
    global received_messages
    print(f"received message! {message}")
    received_messages.append(message)


def start_server():
    global server, ip, port
    if ip is None or port is None:
        return
    dispatcher = Dispatcher()
    dispatcher.map("/testing", append_message)

    server = osc_server.ThreadingOSCUDPServer((ip, port), dispatcher)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.start()


def stop_server():
    global received_messages
    if server is not None:
        server.shutdown()
    received_messages.clear()


def restart_server():
    stop_server()
    start_server()


###############################################################################
# Macro condition functions
###############################################################################


def check_condition(data):
    global received_messages
    expected_message = obs.obs_data_get_string(data, "value")
    return_value = False
    print(received_messages)
    for message in received_messages:
        if message == expected_message:
            return_value = True
    received_messages.clear()
    return return_value 


def get_condition_properties():
    props = obs.obs_properties_create()
    obs.obs_properties_add_text(props, "value", "Expected Value:", obs.OBS_TEXT_DEFAULT)
    return props


def get_condition_defaults():
    default_settings = obs.obs_data_create()
    obs.obs_data_set_default_string(
        default_settings, "value", "Your expected value here!"
    )
    return default_settings


###############################################################################
# Script settings and description
###############################################################################


def script_description():
    return (
        f'Adds the macro condition "{condition_name}" for the advanced scene switcher'
    )


def script_update(settings):
    global ip, port
    ip = obs.obs_data_get_string(settings, "ip")
    port = obs.obs_data_get_int(settings, "port")


def script_defaults(settings):
    obs.obs_data_set_default_string(settings, "ip", "127.0.0.1")
    obs.obs_data_set_default_int(settings, "port", 5005)


def restart_pressed(props, prop):
    restart_server()


def script_properties():
    props = obs.obs_properties_create()
    obs.obs_properties_add_text(props, "ip", "IP Address", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_int(props, "port", "Port", 0, 65535, 1)
    obs.obs_properties_add_button(props, "button", "Restart OSC server", restart_pressed)
    return props


###############################################################################
# Main script entry point
###############################################################################


def script_load(settings):
    global condition_name
    advss_register_condition(
        condition_name,
        check_condition,
        get_condition_properties,
        get_condition_defaults(),
    )
    start_server()

def script_unload():
    global condition_name
    advss_deregister_condition(condition_name)


###############################################################################

# Advanced Scene Switcher helper functions below:
# Omitted on this wiki page for brevity!

Other examples:

General remarks

Note that if you should choose to unload the scripts, which define custom macro segments, while custom macro segments are still in use, the plugin will no longer be able to query settings of those custom macro segments.
If such a state should be saved (e.g. because OBS was closed with the script no longer being loaded) the plugin will instead replace the custom macro segments with macro segments of type Unknown.
Even if you should choose to reload the script at a later point in time the settings for those Unknown macro segments will be lost and you will have to reconfigure them.

Detailed API description

The advanced scene switcher offers the following procedure handlers:

  • bool advss_register_script_action(in string name, in ptr default_settings, out string properties_signal_name, out string trigger_signal_name)
  • bool advss_register_script_condition(in string name, in ptr default_settings, out string properties_signal_name, out string trigger_signal_name)
  • bool advss_deregister_script_action(in string name)
  • bool advss_deregister_script_condition(in string name)
  • bool advss_get_variable_value(in string name, out string value)
  • bool advss_set_variable_value(in string name, in string value)

advss_register_script_action

name

The name field of calldata object associated with this procedure should specify the id of the action type you want to register. It will also be the user facing name in the action type selection.

default_settings (optional)

The name field of calldata object associated with this procedure should specify the a pointer to an obs_data_t object. It should contain the default values for the settings for your custom action type.
The ownership and thus responsibility to free this obs_data_t* pointer will be that of the advanced scene switcher.
Thus you must not free / release this pointer yourself or you risk a crash of OBS.

This value can be null, if you do not which to provide any default settings.

properties_signal_name (optional)

The properties_signal_name field of calldata object associated with this procedure will specify the name of the signal, which will be called by the advanced scene switcher, when it requests a new obs_properties_t object.
These objects are used to determine which settings UI elements should be shown to the user when creating an instance of your custom action type.

The signal will be registered by the advanced scene switcher.
You only have to connect to it.

When the signal is called by the advanced scene switcher you will have to pass the pointer to the obs_properties_t object you created via calldata_set_ptr in the field named properties.

You can ignore this field, if you do not wish to provide any controls to the user to modify the settings of this action type.

trigger_signal_name

The trigger_signal_name field of calldata object associated with this procedure will specify the name of the signal, which will be called by the advanced scene switcher, when your custom action type needs to be executed.

The signal will be registered by the advanced scene switcher.
You only have to connect to it.

Handling the trigger signal

In the following section trigger_signal_name is just a placeholder for the actual signal name.

When trigger_signal_name is called by the advanced scene switcher, the settings for the instance of this action type will be passed as a pointer to an obs_data_t object.
You can access it via calldata_ptr in the settings field of calldata object associated with this procedure.

When trigger_signal_name is called by the advanced scene switcher you will have to pass the result of your operation via calldata_set_bool in the field named result.
In case of a macro action this should always be true unless a catastrophic error occurred which should abort the whole macro's execution.

When trigger_signal_name is called by the advanced scene switcher it will pass completion_id field, which is a unique id for each instance of your custom action being triggered.

When trigger_signal_name is called by the advanced scene switcher it will also pass the name of the signal you will to emit when your action type's operation is done in the completion_signal_name field.

When you emit this completion signal you will also have to pass the completion_id value you have received via trigger_signal_name using calldata_set_int.

The signal will be registered by the advanced scene switcher.
You only have to emit it.

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

Example

def advss_register_action_type(name, callback, get_properties, default_settings):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)
    obs.calldata_set_ptr(data, "default_settings", default_settings)

    obs.proc_handler_call(proc_handler, "advss_register_script_action", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to register custom action "{name}"')
        obs.calldata_destroy(data)
        return

    # Run in separate thread to avoid blocking main OBS signal handler.
    # Operation completion will be indicated via signal completion_signal_name.
    def run_helper(data):
        completion_signal_name = obs.calldata_string(data, "completion_signal_name")
        id = obs.calldata_int(data, "completion_id")

        def thread_func(settings):
            settings = obs.obs_data_create_from_json(
                obs.calldata_string(data, "settings")
            )

            callback(settings)

            reply_data = obs.calldata_create()
            obs.calldata_set_int(reply_data, "completion_id", id)
            obs.calldata_set_bool(reply_data, "result", True)
            signal_handler = obs.obs_get_signal_handler()
            obs.signal_handler_signal(
                signal_handler, completion_signal_name, reply_data
            )
            obs.obs_data_release(settings)
            obs.calldata_destroy(reply_data)

        threading.Thread(target=thread_func, args={data}).start()

    def properties_helper(data):
        if get_properties is not None:
            properties = get_properties()
        else:
            properties = None
        obs.calldata_set_ptr(data, "properties", properties)

    trigger_signal_name = obs.calldata_string(data, "trigger_signal_name")
    property_signal_name = obs.calldata_string(data, "properties_signal_name")

    signal_handler = obs.obs_get_signal_handler()
    obs.signal_handler_connect(signal_handler, trigger_signal_name, run_helper)
    obs.signal_handler_connect(signal_handler, property_signal_name, properties_helper)

    obs.calldata_destroy(data)

advss_register_script_condition

This is almost identical to advss_register_script_action with the only difference being the handling of the result field.

name

The name field of calldata object associated with this procedure should specify the id of the condition type you want to register. It will also be the user facing name in the condition type selection.

default_settings (optional)

The name field of calldata object associated with this procedure should specify the a pointer to an obs_data_t object. It should contain the default values for the settings for your custom condition type.
The ownership and thus responsibility to free this obs_data_t* pointer will be that of the advanced scene switcher.
Thus you must not free / release this pointer yourself or you risk a crash of OBS.

This value can be null, if you do not which to provide any default settings.

properties_signal_name (optional)

The properties_signal_name field of calldata object associated with this procedure will specify the name of the signal, which will be called by the advanced scene switcher, when it requests a new obs_properties_t object.
These objects are used to determine which settings UI elements should be shown to the user when creating an instance of your custom condition type.

The signal will be registered by the advanced scene switcher.
You only have to connect to it.

When the signal is called by the advanced scene switcher you will have to pass the pointer to the obs_properties_t object you created via calldata_set_ptr in the field named properties.

You can ignore this field, if you do not wish to provide any controls to the user to modify the settings of this condition type.

trigger_signal_name

The trigger_signal_name field of calldata object associated with this procedure will specify the name of the signal, which will be called by the advanced scene switcher, when your custom condition check needs to be executed.

The signal will be registered by the advanced scene switcher.
You only have to connect to it.

Handling the trigger signal

In the following section trigger_signal_name is just a placeholder for the actual signal name.

When trigger_signal_name is called by the advanced scene switcher, the settings for the instance of this condition type will be passed as a pointer to an obs_data_t object.
You can access it via calldata_ptr in the settings field of calldata object associated with this procedure.

When trigger_signal_name is called by the advanced scene switcher you will have to pass the result of your condition check via calldata_set_bool in the field named result.

When trigger_signal_name is called by the advanced scene switcher it will pass completion_id field, which is a unique id for each instance of your custom condition being triggered.

When trigger_signal_name is called by the advanced scene switcher it will also pass the name of the signal you will to emit when your condition type's operation is done in the completion_signal_name field.

When you emit this completion signal you will also have to pass the completion_id value you have received via trigger_signal_name using calldata_set_int.

The signal will be registered by the advanced scene switcher.
You only have to emit it.

Example

def advss_register_condition_type(name, callback, get_properties, default_settings):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)
    obs.calldata_set_ptr(data, "default_settings", default_settings)

    obs.proc_handler_call(proc_handler, "advss_register_script_condition", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to register custom condition "{name}"')
        obs.calldata_destroy(data)
        return

    # Run in separate thread to avoid blocking main OBS signal handler.
    # Operation completion will be indicated via signal completion_signal_name.
    def run_helper(data):
        completion_signal_name = obs.calldata_string(data, "completion_signal_name")
        id = obs.calldata_int(data, "completion_id")

        def thread_func(settings):
            settings = obs.obs_data_create_from_json(
                obs.calldata_string(data, "settings")
            )

            callback_result = callback(settings)

            reply_data = obs.calldata_create()
            obs.calldata_set_int(reply_data, "completion_id", id)
            obs.calldata_set_bool(reply_data, "result", callback_result)
            signal_handler = obs.obs_get_signal_handler()
            obs.signal_handler_signal(
                signal_handler, completion_signal_name, reply_data
            )
            obs.obs_data_release(settings)
            obs.calldata_destroy(reply_data)

        threading.Thread(target=thread_func, args={data}).start()

    def properties_helper(data):
        if get_properties is not None:
            properties = get_properties()
        else:
            properties = None
        obs.calldata_set_ptr(data, "properties", properties)

    trigger_signal_name = obs.calldata_string(data, "trigger_signal_name")
    property_signal_name = obs.calldata_string(data, "properties_signal_name")

    signal_handler = obs.obs_get_signal_handler()
    obs.signal_handler_connect(signal_handler, trigger_signal_name, run_helper)
    obs.signal_handler_connect(signal_handler, property_signal_name, properties_helper)

    obs.calldata_destroy(data)

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

advss_deregister_script_action

name

The name field of calldata object associated with this procedure should specify the id of the action type you want to deregister. It will also be the user facing name in the condition type selection.

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

Example

def advss_deregister_action(name):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)

    obs.proc_handler_call(proc_handler, "advss_deregister_script_action", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to deregister custom action"{name}"')

    obs.calldata_destroy(data)

advss_deregister_script_condition

This is identical to advss_deregister_script_action.

name

The name field of calldata object associated with this procedure should specify the id of the condition type you want to deregister.

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

Example

def advss_deregister_condition(name):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)

    obs.proc_handler_call(proc_handler, "advss_deregister_script_condition", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to deregister custom condition "{name}"')

    obs.calldata_destroy(data)

advss_get_variable_value

name

The name field of calldata object associated with this procedure should specify the id of the name of the variable for which you want to get the current value.

value

The value field of calldata object associated with this procedure will contain the value of the specified variable, if the operation was successful.

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

Example

def advss_get_variable_value(name):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)
    obs.proc_handler_call(proc_handler, "advss_get_variable_value", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to get value for variable "{name}"')
        obs.calldata_destroy(data)
        return None

    value = obs.calldata_string(data, "value")

    obs.calldata_destroy(data)
    return value

advss_set_variable_value

name

The name field of calldata object associated with this procedure should specify the id of the name of the variable for which you want to change the value.

value

The value field of calldata object associated with this procedure should specify the value you want to set for the specified variable.

Return value

The return value can be queried via success from the calldata object associated with this procedure. Returns true, if the operation was successful, and false otherwise.

Example

def advss_set_variable_value(name, value):
    proc_handler = obs.obs_get_proc_handler()
    data = obs.calldata_create()

    obs.calldata_set_string(data, "name", name)
    obs.calldata_set_string(data, "value", value)
    obs.proc_handler_call(proc_handler, "advss_set_variable_value", data)

    success = obs.calldata_bool(data, "success")
    if success == False:
        obs.script_log(obs.LOG_WARNING, f'failed to set value for variable "{name}"')

    obs.calldata_destroy(data)
    return success

Example guides

Explanations

Clone this wiki locally