Getting started:
Consider cloning this repo and running examples(they are self contained) in OBS Studio, most of them will operate on existing text soure. Tip: you can create a copy of script, rename it, and add to OBS. So two of identical scripts will be run in parallel with separated namespaces. Also check out issues if you found any error or have a suggestion and discussions for collaboration,ideas and Q&A.
- UI
- Property modification
- Additional input
- obs_data
- save settings as json
- Source's and filters with identifier string
- Add source
- Move source
- Duplicate source
- Add filter to source
- Toggle sceneitem visibility
- Set current scene
- Get set order in scene
- Add scene with sources to current scene
- Events
- Program state
- Signals
- Timing (sequential primitives)
- Hotkeys
- Play sound
- Read and write private data from scripts or plugins
- Browser source interaction
- Acess source dB volume level
- Get current profile settings via ffi
- Debug
- Security
- Docs and code examples
- Links
- Contribute
See also :
https://obsproject.com/docs/reference-properties.html#property-object-functions
def callback(props, prop, *args, **kwargs): # pass settings implicitly
p = S.obs_properties_get(props, "button")
n = next(counter)
S.obs_property_set_description(p, f"refresh pressed {n} times")
return True
...
def script_properties():
props = S.obs_properties_create()
b = S.obs_properties_add_button(
props, "button", "refresh pressed 0 times", refresh_pressed
)
S.obs_property_set_modified_callback(b, callback)
return props
Full source
See also :
https://obsproject.com/docs/reference-properties.html#property-modification-functions
def callback(props, prop, settings):
_number = S.obs_data_get_int(settings, "_int")
_text_value = S.obs_data_get_string(settings, "_text")
text_property = S.obs_properties_get(props, "_text")
if _number > 50:
eg.data = _text_value + str(_number)
S.obs_property_set_visible(text_property, True)
return True
else:
eg.data = ""
S.obs_property_set_visible(text_property, False)
return True
...
def script_properties(): # ui
...
number = S.obs_properties_add_int(props, "_int", "Number", 1, 100, 1)
text_value = S.obs_properties_add_text(
props, "_text", "Additional input:", S.OBS_TEXT_DEFAULT
)
S.obs_property_set_visible(text_value, False)
S.obs_property_set_modified_callback(number, callback)
...
Note: properties share similar structure , in Python, Lua, C.
Example C
See also :
https://obsproject.com/docs/reference-properties.html#property-modification-functions
obs_data_get_string
obs_data_get_int
obs_data_get_double
obs_data_get_bool
obs_data_get_obj
obs_data_get_array
p = Path(__file__).absolute() # current script path
file = p.parent / "saved_settings.json"
try:
content = S.obs_data_get_json(Data._settings_)
with open(file, "w") as f:
f.write(content)
except Exception as e:
print(e, "cannot write to file")
Full source
See also :
https://obsproject.com/docs/reference-settings.html
https://obsproject.com/docs/scripting.html#getting-the-current-script-s-path
To identify with obs_source_get_unversioned_id
, or creating source/filter.
Name | Source type identifier string |
---|---|
Browser | browser_source |
Color Source | color_source |
Display Capture | monitor_capture |
Game Capture | game_capture |
Image | image_source |
Image Slide Show | slideshow |
Media Source | ffmpeg_source |
Text (GDI+) | text_gdiplus |
Window Capture | window_capture |
Name | Source type identifier string |
---|---|
Async Delay | async_delay_filter |
Chroma Key | chroma_key_filter |
Chroma Key V2 | chroma_key_filter_v2 |
Color Correction | color_filter |
Color Correction V2 | color_filter_v2 |
Color Key | color_key_filter |
Color Key V2 | color_key_filter_v2 |
Color Grade | color_grade_filter |
Compressor | compressor_filter |
Crop/Pad | crop_filter |
Expander | expander_filter |
Gain | gain_filter |
GPU Delay | gpu_delay_filter |
Image Mask/Blend | mask_filter |
Invert Polarity | invert_polarity_filter |
Limiter | limiter_filter |
Luma Key | luma_key_filter |
Luma Key V2 | luma_key_filter_v2 |
Mask | mask_filter |
Mask V2 | mask_filter_v2 |
Noise Gate | noise_gate_filter |
Noise Suppression | noise_suppress_filter |
Noise Suppression V2 | noise_suppress_filter_v2 |
Render Delay | gpu_delay |
Scaling/Aspect Ratio | scale_filter |
Scroll | scroll_filter |
Sharpen | sharpness_filter |
Sharpen V2 | sharpness_filter_v2 |
Video Delay (Async) | async_delay_filter |
VST 2.x Plug-in | vst_filter |
Create source and add it to current scene
S.obs_data_set_string(settings, "text", "The quick brown fox jumps over the lazy dog")
source = S.obs_source_create_private("text_gdiplus", "test_py", settings)
S.obs_scene_add(scene, source)
Full source
See also :
https://obsproject.com/docs/reference-scenes.html
Get current scene , get source name, move source to location
def __init__(self):
pos = S.vec2()
self.location = pos
...
def move_text_source(self):
current_scene = S.obs_frontend_get_current_scene()
source = S.obs_get_source_by_name("test_py")
scene = S.obs_scene_from_source(current_scene)
scene_item = S.obs_scene_find_source(scene, "test_py")
if scene_item:
dx, dy = 10, 10
print("old values", self.location.x)
S.obs_sceneitem_get_pos(
scene_item, self.location
) # update to last position if its changed from OBS
self.location.x += dx
self.location.y += dy
print("new values", self.location.x)
S.obs_sceneitem_set_pos(scene_item, self.location)
def dup(self):
current_scene = S.obs_scene_from_source(S.obs_frontend_get_current_scene())
scene_item = S.obs_scene_find_source(current_scene, self.source_name)
info = S.obs_transform_info()
crop = S.obs_sceneitem_crop()
S.obs_sceneitem_get_info(scene_item,info)
S.obs_sceneitem_get_crop(scene_item,crop)
duplicate = S.obs_sceneitem_get_source(scene_item)
duplicated = S.obs_source_duplicate(
duplicate, "duplicate" + self.source_name, False
)
scenes = S.obs_frontend_get_scenes()
for scene in scenes:
name = S.obs_source_get_name(scene)
if name == self.scene_name:
scene = S.obs_scene_from_source(scene)
scene_item2 = S.obs_scene_add(scene, duplicated)
S.obs_sceneitem_set_info(scene_item2,info)
S.obs_sceneitem_set_crop(scene_item2,crop)
Filters are sources,they are not listed in obspython module, you need to know its id from obs_source_info
S.obs_data_set_int(settings, "opacity", 50)
source_color = S.obs_source_create_private(
"color_filter", "opacity to 50", settings
)
S.obs_source_filter_add(source, source_color)
Full source
See also :
Color correction source
https://obsproject.com/docs/reference-sources.html
def toggle(self):
current_scene = S.obs_scene_from_source(S.obs_frontend_get_current_scene())
scene_item = S.obs_scene_find_source(current_scene, self.source_name)
boolean = not S.obs_sceneitem_visible(scene_item)
S.obs_sceneitem_set_visible(scene_item, boolean)
def set_current_scene(self):
scenes = S.obs_frontend_get_scenes()
for scene in scenes:
name = S.obs_source_get_name(scene)
if name == self.scene_name:
S.obs_frontend_set_current_scene(scene)
...
scenes = S.obs_frontend_get_scenes() # Dropdown menu UI
for scene in scenes:
name = S.obs_source_get_name(scene)
S.obs_property_list_add_string(p, name, name)
def get_order(scene_items=None):
order = list()
for i, s in enumerate(scene_items):
source = S.obs_sceneitem_get_source(s)
name = S.obs_source_get_name(source)
order.append({"index": i, "name": name, "scene_item": s})
return order
def reorder():
current_scene = S.obs_frontend_get_current_scene()
with scene_ar(current_scene) as scene:
with scene_enum(scene) as scene_items:
order = get_order(scene_items)
# change second index with pre last
order[1]["index"], order[-2]["index"] = (
order[-2]["index"],
order[1]["index"],
)
for s in sorted(order, key=lambda i: i["index"]):
S.obs_sceneitem_set_order_position(s["scene_item"], s["index"])
def add_random_text_source(scene):
r = " random text # " + str(randint(0, 10))
with data_ar() as settings:
S.obs_data_set_string(settings, "text", f"random text value {r}")
with source_create_ar("text_ft2_source", f"random text{r}", settings) as source:
pos = S.vec2()
pos.x = randint(0, 1920)
pos.y = randint(0, 1080)
scene_item = S.obs_scene_add(scene, source)
S.obs_sceneitem_set_pos(scene_item, pos)
def add_scene_with_sources():
current_scene_source = S.obs_frontend_get_current_scene()
with scene_from_source_ar(current_scene_source) as scene_source:
with scene_create_ar("_nested_scene") as _scene:
py_scene_source = S.obs_scene_get_source(_scene)
with scene_from_source_ar(py_scene_source) as scene:
add_random_text_source(scene)
add_random_text_source(scene)
add_random_text_source(scene)
# add created scene to current scene ( nested scene)
_scene_source = S.obs_scene_get_source(scene)
S.obs_scene_add(scene_source, _scene_source)
Note: sometimes OBS crashes if one of such scenes has been deleted.
def on_event(event):
if event == S.OBS_FRONTEND_EVENT_SCENE_CHANGED:
raise Exception("Triggered when the current scene has changed.")
def script_load(settings):
S.obs_frontend_add_event_callback(on_event)
Full source
See also:
https://obsproject.com/docs/reference-frontend-api.html#structures-enumerations
Those functions return true or false :
S.obs_frontend_preview_program_mode_active()
S.obs_frontend_replay_buffer_active()
S.obs_frontend_recording_active()
S.obs_frontend_recording_paused()
S.obs_frontend_streaming_active()
Signals , callbacks , differences from C
sh = S.obs_get_signal_handler()
S.signal_handler_connect(sh,"source_create",callback)
def callback(calldata):
source = S.calldata_source(cd,"source")
print(S.obs_source_get_name(source))
source_create, source_destroy, source_remove, source_save, source_load, source_activate, source_deactivate, source_show, source_hide, source_rename, source_volume, source_transition_start, source_transition_video_stop, source_transition_stop, channel_change, master_volume, hotkey_layout_change, hotkey_register, hotkey_unregister, hotkey_bindings_changed
https://obsproject.com/docs/reference-core.html#core-obs-signals
def connect_cur_scene():
source = S.obs_frontend_get_current_scene()
sh = S.obs_source_get_signal_handler(source)
S.signal_handler_connect(sh, "item_add", callback)
S.obs_source_release(source)
def callback(calldata):
scene_item = S.calldata_sceneitem(calldata, "item")
#scene = S.calldata_source(cd,"scene") # bad utf symbols
scene = S.obs_sceneitem_get_scene(scene_item)
name = S.obs_source_get_name
source = S.obs_sceneitem_get_source
scene_source = S.obs_scene_get_source
scene_name = name(scene_source(scene))
scene_item_name = name(source(scene_item))
print(f"item {scene_item_name} has been added to scene {scene_name}")
item_add, item_remove, reorder, refresh, item_visible, item_locked, item_select, item_deselect, item_transform
https://obsproject.com/docs/reference-scenes.html#scene-signals
sh = S.obs_source_get_signal_handler(some_source)
S.signal_handler_connect(sh,"show",callback)
def callback(calldata):
source = S.calldata_source(cd,"source")
print("on source show",S.obs_source_get_name(source))
destroy, remove, save, load, activate, deactivate, show, hide, mute, push_to_mute_changed, push_to_mute_delay, push_to_talk_changed, push_to_talk_delay, enable, rename, volume, update_properties, update_flags, audio_sync, audio_mixers, filter_add, filter_remove, reorder_filters, transition_start, transition_video_stop, transition_stop, media_started, media_ended, media_pause, media_play, media_restart, media_stopped, media_next, media_previous
https://obsproject.com/docs/reference-sources.html#source-signals
def connect_to_rec():
sh = S.obs_output_get_signal_handler(S.obs_frontend_get_recording_output())
S.signal_handler_connect(sh, "pause", callback)
def callback(calldata):
#out = S.calldata_ptr(calldata, "output") # bad type
print('output paused')
start, stop, pause, unpause, starting, stopping, activate, deactivate, reconnect, reconnect_success
https://obsproject.com/docs/reference-outputs.html#output-signals
def script_update(settings):
eg.source_name = S.obs_data_get_string(settings, "source")
S.timer_remove(eg.update_text)
if eg.source_name != "":
S.timer_add(eg.update_text, 1 * 1000)
Full source
Note: each time script updated it's removed first
See also :
Version with globals and only one timer allowed.
https://obsproject.com/docs/scripting.html#script-timers
def callback(pressed):
if pressed:
toggle_thread()
def busy_thread():
while True:
if not data.thread_paused:
sleep(0.02)
data.status = "active"
# print to stdoud crashes OBS on exit
else:
sleep(0.5)
data.status = "inactive"
print('Press the "~" to toggle on/off')
hook("OBS_KEY_ASCIITILDE", "id_", callback)
S.timer_add(lambda: print(data.status), 500)
t = threading.Thread(target=busy_thread)
t.start()
This hotkey example will create hotkeys in settings , but you need to bind it manually.
class Hotkey:
def __init__(self, callback, obs_settings, _id):
self.obs_data = obs_settings
self.hotkey_id = S.OBS_INVALID_HOTKEY_ID
self.hotkey_saved_key = None
self.callback = callback
self._id = _id
self.load_hotkey()
self.register_hotkey()
self.save_hotkey()
...
class h:
htk_copy = None # this attribute will hold instance of Hotkey
...
h1 = h()
h2 = h()
...
def script_load(settings):
h1.htk_copy = Hotkey(cb1, settings, "h1_id")
h2.htk_copy = Hotkey(cb2, settings, "h2_id")
def script_save(settings):
h1.htk_copy.save_hotkey()
h2.htk_copy.save_hotkey()
This hotkey example will create hotkeys on fly from json settings , but you need to know internal id.
ID = "htk_id"
JSON_DATA = '{"%s":[{"key":"OBS_KEY_1"}]}' % ID
def on_obs_key_1(pressed):
if pressed:
raise Exception("hotkey 1 pressed")
def script_load(settings):
s = S.obs_data_create_from_json(JSON_DATA)
a = S.obs_data_get_array(s, ID)
h = S.obs_hotkey_register_frontend(ID, ID, on_obs_key_1)
S.obs_hotkey_load(h, a)
Here is how send hotkey to OBS
def send_hotkey(obs_htk_id, key_modifiers=None):
if key_modifiers:
shift = key_modifiers.get("shift")
control = key_modifiers.get("control")
alt = key_modifiers.get("alt")
command = key_modifiers.get("command")
...
combo = S.obs_key_combination()
combo.modifiers = modifiers
combo.key = S.obs_key_from_name(obs_htk_id)
...
S.obs_hotkey_inject_event(combo, False)
S.obs_hotkey_inject_event(combo, True)
S.obs_hotkey_inject_event(combo, False)
See also:
https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h
obsproject/obs-websocket#595
def play_sound():
...
mediaSource = S.obs_source_create_private(
"ffmpeg_source", "Global Media Source", None
)
s = S.obs_data_create()
S.obs_data_set_string(s, "local_file", script_path() + "alert.mp3")
S.obs_source_update(mediaSource, s)
S.obs_source_set_monitoring_type(
mediaSource, S.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT
)
...
Write in one script
def send_to_private_data(data_type, field, result):
settings = S.obs_data_create()
set = getattr(obs, f"obs_data_set_{data_type}")
set(settings, field, result)
S.obs_apply_private_data(settings)
S.obs_data_release(settings)
def write_private_data():
result = "private value from " + str(__file__) + " " + str(randint(1, 10))
send_to_private_data("string", "__private__", result)
Read from another
@contextmanager
def p_data_ar(data_type, field):
settings = S.obs_get_private_data()
get = getattr(obs, f"obs_data_get_{data_type}")
try:
yield get(settings, field)
finally:
S.obs_data_release(settings)
def print_private_data():
with p_data_ar("string", "__private__") as value:
print(value)
Lua is also supported
local obs = obslua
local settings = S.obs_data_create()
S.obs_data_set_int(settings,"__private__", 7)
S.obs_apply_private_data(settings)
S.obs_data_release(settings)
def send_hotkey_to_browser(source, obs_htk_id, key_modifiers=None, key_up=False):
key = S.obs_key_from_name(obs_htk_id)
vk = S.obs_key_to_virtual_key(key)
event = S.obs_key_event()
event.native_vkey = vk
event.modifiers = get_modifiers(key_modifiers)
event.native_modifiers = event.modifiers # https://doc.qt.io/qt-5/qkeyevent.html
event.native_scancode = vk
event.text = ""
S.obs_source_send_key_click(source, event, key_up)
def press_tab(*p):
with source_auto_release(G.source_name) as source:
send_hotkey_to_browser(source, "OBS_KEY_TAB")
send_hotkey_to_browser(source, "OBS_KEY_TAB", key_up=True)
def press_shift_tab(*p):
with source_auto_release(G.source_name) as source:
send_hotkey_to_browser(source, "OBS_KEY_TAB", {"shift": True})
send_hotkey_to_browser(source, "OBS_KEY_TAB", {"shift": True}, key_up=True)
There is FFI ctypes
module in Python to wrap native obs
lib.
However,to run it on GNU/Linux you must start obs with LD_PRELOAD
.
ubsdrive3@usbdrive3:~$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpython3.9.so obs
It might be in different directory, use find /usr -name 'libpython*'
to
find exact location.
volmeter_callback_t = CFUNCTYPE(
None, c_void_p, POINTER(c_float), POINTER(c_float), POINTER(c_float)
)
...
wrap(
"obs_volmeter_add_callback",
None,
argtypes=[POINTER(Volmeter), volmeter_callback_t, c_void_p],
)
...
@volmeter_callback_t
def volmeter_callback(data, mag, peak, input):
G.noise = float(peak[0])
wrap("obs_frontend_get_profile_config", POINTER(Config), use_lib=G.obsffi_front)
# const char *config_get_string(config_t *config, const char *section,
# const char *name)
wrap("config_get_string", c_char_p, argtypes=[POINTER(Config), c_char_p, c_char_p])
wrap("config_num_sections", c_size_t, argtypes=[POINTER(Config)])
wrap("config_get_section", c_char_p, argtypes=[POINTER(Config), c_size_t])
def output_to_stdout():
cfg = G.obs_frontend_get_profile_config()
e = lambda x: x.encode("utf-8")
s = G.config_get_string(cfg, e("SimpleOutput"), e("FilePath"))
l = G.config_num_sections(cfg)
for i in range(l):
tag = G.config_get_section(cfg, c_size_t(i))
print(s, l)
There is no stdin therefore you can't use pdb , options are:
- using
print
- using generated log file (contains information about memory leaks)
- using pycharm remote debugging (localhost)
- using threading
- using vscode attach to the process:
- Load python extension
- open script file ,
pip install debugpy
, placedebugpy.breakpoint()
somewhere- Run (F5) select configuration ( Attach using Process ID)
- select obs (on windows
obs64.exe
) - View select Debug Console (ctrl+shift+y)
- Example debugpy obs
- "Calling home" - see
#4044
- On GNU/Linux there is handy program called
tcpdump
, to run a check against OBS Studio - use this command (it is active on 443 port on start and on end)
# tcpdump -i <your_interface_e_g_wifi_or_wire> 'port 443'
- Avoid using
sudo
or admin, check hashsums, do backups, do updates, etc... - There is no confirmation for loading Lua or Python scripts - they can be added/overwritten via .json key
- Also solutions for Python source code obfuscation & loading from shared (compiled) library do exist.Applying that will make it a bit harder to reverse-engineer your code.
contains all variables and functions available in obspython
formatted with markdown. Table consist of links to appropriate search terms in OBS Studio repository and obswebsocket,links to scripts in obspython
and obslua
with each script within github code search.gs_*
and matrix_*
functions exluded from that table.
Export names
- Scripts forum , Github topic
obs-scripts
, Github topicobs-script
- OBS Studio Repo , obs-scripting-python.c
- Docs , Docs/scripting , Docs/plugins , Docs index
- obspython Gist , Github , grep.app
- obslua Gist , Github , grep.app
- A Python bundle for integration with OBS scripting
- Lua tips and tricks
- Python 3.6.8 , 64 bit installer, for Microsoft Windows
Contributions are welcome!