Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

51 Commits

Repository files navigation

Getting started:

Consider cloning this repo and running examples, they are self contained. 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 separate namespaces. Also check out issues to report error or have a suggestion and discussions

Table of content

UI

Preview
S.obs_properties_add_button(props, "button1", "Refresh1:",callback)img
S.obs_properties_add_bool(props,"_bool","_bool:")img
S.obs_properties_add_int(props,"_int","_int:",1,100,1)img
S.obs_properties_add_int_slider(props,"_slider","_slider:",1,100,1) img
S.obs_properties_add_text(props, "_text", "_text:", S.OBS_TEXT_DEFAULT) img
S.obs_properties_add_color(props,"_color","_color:") img
S.obs_properties_add_font(props,"_font","_font:") img
S.obs_properties_add_font(props,"_font","_font:") img
bool_p = S.obs_properties_add_bool(props, "_obs_bool", "Yes/No"); S.obs_property_set_long_description(bool_p, "Check if yes,else uncheck")img

See also :
https://obsproject.com/docs/reference-properties.html#property-object-functions

Property modification

defcallback(props, prop, *args, **kwargs): # pass settings implicitlyp=S.obs_properties_get(props, "button")
n=next(counter)
S.obs_property_set_description(p, f"refresh pressed {n} times")
returnTrue
...
defscript_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)
returnprops

Full source
See also :
https://obsproject.com/docs/reference-properties.html#property-modification-functions

Property additional input

defcallback(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)
returnTrueelse:
eg.data=""S.obs_property_set_visible(text_property, False)
returnTrue
...
defscript_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)
...

Full source

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

  • obs_data_get_string
  • obs_data_get_int
  • obs_data_get_double
  • obs_data_get_bool
  • obs_data_get_obj
  • obs_data_get_array

Print all source settings and filter names

source=S.obs_get_source_by_name(self.source_name)
settings=S.obs_source_get_settings(source)
psettings=S.obs_source_get_private_settings(source)
dsettings=S.obs_data_get_defaults(settings)
pdsettings=S.obs_data_get_defaults(psettings)
print("[---------- settings ----------")
print(S.obs_data_get_json(settings))
print("---------- private_settings ----------")
print(S.obs_data_get_json(psettings))
print("---------- default settings for this source type ----------")
print(S.obs_data_get_json(dsettings))
print("---------- default private settings for this source type ----------")
print(S.obs_data_get_json(pdsettings))
... print("[--------- filter names --------")
foriinrange(filter_count):
settings=S.obs_data_array_item(filters, i)
filter_name=S.obs_data_get_string(settings, "name")
S.obs_data_release(settings)
print(filter_name)
print(" filter names of %s --------"%self.source_name)

Full source

Save settings as json

p=Path(__file__).absolute() # current script pathfile=p.parent/"saved_settings.json"try:
content=S.obs_data_get_json(Data._settings_)
withopen(file, "w") asf:
f.write(content)
exceptExceptionase:
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

Source's and filters with identifier string

To identify with obs_source_get_unversioned_id , or creating source/filter.

Source's

NameSource type identifier string
Application Audio Capture (BETA)wasapi_process_output_capture
Browserbrowser_source
Color Sourcecolor_source
Display Capturemonitor_capture
Game Capturegame_capture
Image Slide Showslideshow
Imageimage_source
Media Sourceffmpeg_source
Text (GDI+)text_gdiplus
Window Capturewindow_capture

Filters

NameSource type identifier string
3-Band Equalizerbasic_eq_filter
Async Delayasync_delay_filter
Chroma Key V2chroma_key_filter_v2
Chroma Keychroma_key_filter
Color Correction V2color_filter_v2
Color Correctioncolor_filter
Color Gradecolor_grade_filter
Color Key V2color_key_filter_v2
Color Keycolor_key_filter
Compressorcompressor_filter
Crop/Padcrop_filter
Expanderexpander_filter
GPU Delaygpu_delay_filter
Gaingain_filter
HDR Tone Mapping (Override)hdr_tonemap_filter
Image Mask/Blendmask_filter
Invert Polarityinvert_polarity_filter
Limiterlimiter_filter
Luma Key V2luma_key_filter_v2
Luma Keyluma_key_filter
Mask V2mask_filter_v2
Maskmask_filter
Noise Gatenoise_gate_filter
Noise Suppression V2noise_suppress_filter_v2
Noise Suppressionnoise_suppress_filter
Render Delaygpu_delay
Scaling/Aspect Ratioscale_filter
Scrollscroll_filter
Sharpen V2sharpness_filter_v2
Sharpensharpness_filter
Upward Compressorupward_compressor_filter
VST 2.x Plug-invst_filter
Video Delay (Async)async_delay_filter

Add source

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

Move source

Get current scene , get source name, move source to location

def__init__(self):
pos=S.vec2()
self.location=pos
...
defmove_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")
ifscene_item:
dx, dy=10, 10print("old values", self.location.x)
S.obs_sceneitem_get_pos(
scene_item, self.location
) # update to last position if its changed from OBSself.location.x+=dxself.location.y+=dyprint("new values", self.location.x)
S.obs_sceneitem_set_pos(scene_item, self.location)

Full source

Duplicate source

defdup(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()
forsceneinscenes:
name=S.obs_source_get_name(scene)
ifname==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)

Full source

Add filter to source

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

Toggle sceneitem visibility

deftoggle(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=notS.obs_sceneitem_visible(scene_item)
S.obs_sceneitem_set_visible(scene_item, boolean)

Full source

Set current scene

defset_current_scene(self):
scenes=S.obs_frontend_get_scenes()
forsceneinscenes:
name=S.obs_source_get_name(scene)
ifname==self.scene_name:
S.obs_frontend_set_current_scene(scene)
...
scenes=S.obs_frontend_get_scenes() # Dropdown menu UIforsceneinscenes:
name=S.obs_source_get_name(scene)
S.obs_property_list_add_string(p, name, name) 

Full source

Get set order in scene

defget_order(scene_items=None):
order=list()
fori, sinenumerate(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})
returnorderdefreorder():
current_scene=S.obs_frontend_get_current_scene()
withscene_ar(current_scene) asscene:
withscene_enum(scene) asscene_items:
order=get_order(scene_items)
# change second index with pre lastorder[1]["index"], order[-2]["index"] = (
order[-2]["index"],
order[1]["index"],
)
forsinsorted(order, key=lambdai: i["index"]):
S.obs_sceneitem_set_order_position(s["scene_item"], s["index"])

Full source

Add scene with sources to current scene

defadd_random_text_source(scene):
r=" random text # "+str(randint(0, 10))
withdata_ar() assettings:
S.obs_data_set_string(settings, "text", f"random text value {r}")
withsource_create_ar("text_ft2_source", f"random text{r}", settings) assource:
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)
defadd_scene_with_sources():
current_scene_source=S.obs_frontend_get_current_scene()
withscene_from_source_ar(current_scene_source) asscene_source:
withscene_create_ar("_nested_scene") as_scene:
py_scene_source=S.obs_scene_get_source(_scene)
withscene_from_source_ar(py_scene_source) asscene:
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.

Events

defon_event(event):
ifevent==S.OBS_FRONTEND_EVENT_SCENE_CHANGED:
raiseException("Triggered when the current scene has changed.")
defscript_load(settings):
S.obs_frontend_add_event_callback(on_event)

Full source
See also:
https://obsproject.com/docs/reference-frontend-api.html#structures-enumerations

Program state

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

Signals , callbacks , differences from C

Core signals

sh=S.obs_get_signal_handler()
S.signal_handler_connect(sh,"source_create",callback)
defcallback(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

Scene signals

defconnect_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)
defcallback(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_namesource=S.obs_sceneitem_get_sourcescene_source=S.obs_scene_get_sourcescene_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

Source signals

sh=S.obs_source_get_signal_handler(some_source)
S.signal_handler_connect(sh,"show",callback)
defcallback(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, update, hooked, unhooked

https://obsproject.com/docs/reference-sources.html#source-signals

Output signals

defconnect_to_rec():
sh=S.obs_output_get_signal_handler(S.obs_frontend_get_recording_output())
S.signal_handler_connect(sh, "pause", callback)
defcallback(calldata):
#out = S.calldata_ptr(calldata, "output") # bad typeprint('output paused')

start, stop, pause, unpause, starting, stopping, activate, deactivate, reconnect, reconnect_success

https://obsproject.com/docs/reference-outputs.html#output-signals

Timers and threads

defscript_update(settings):
eg.source_name=S.obs_data_get_string(settings, "source")
S.timer_remove(eg.update_text)
ifeg.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

Thread

defcallback(pressed):
ifpressed:
toggle_thread()
defbusy_thread():
whileTrue:
ifnotdata.thread_paused:
sleep(0.02)
data.status="active"# print to stdoud crashes OBS on exitelse:
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()

Hotkeys

This hotkey example will create hotkeys in settings , but you need to bind it manually.

classHotkey:
def__init__(self, callback, obs_settings, _id):
self.obs_data=obs_settingsself.hotkey_id=S.OBS_INVALID_HOTKEY_IDself.hotkey_saved_key=Noneself.callback=callbackself._id=_idself.load_hotkey()
self.register_hotkey()
self.save_hotkey()
...
classh:
htk_copy=None# this attribute will hold instance of Hotkey
...
h1=h()
h2=h()
...
defscript_load(settings):
h1.htk_copy=Hotkey(cb1, settings, "h1_id")
h2.htk_copy=Hotkey(cb2, settings, "h2_id")
defscript_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"}]}'%IDdefon_obs_key_1(pressed):
ifpressed:
raiseException("hotkey 1 pressed")
defscript_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

defsend_hotkey(obs_htk_id, key_modifiers=None):
ifkey_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=modifierscombo.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.hobsproject/obs-websocket#595

Play sound

defplay_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
)
...

Read and write private data from scripts or plugins

Write in one script

defsend_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)
defwrite_private_data():
result="private value from "+str(__file__) +" "+str(randint(1, 10))
send_to_private_data("string", "__private__", result)

Read from another

@contextmanagerdefp_data_ar(data_type, field):
settings=S.obs_get_private_data()
get=getattr(obs, f"obs_data_get_{data_type}")
try:
yieldget(settings, field)
finally:
S.obs_data_release(settings)
defprint_private_data():
withp_data_ar("string", "__private__") asvalue:
print(value)

Lua is also supported

localobs=obslualocalsettings=S.obs_data_create()
S.obs_data_set_int(settings,"__private__", 7)
S.obs_apply_private_data(settings)
S.obs_data_release(settings)

Browser source interaction

defsend_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=vkevent.modifiers=get_modifiers(key_modifiers)
event.native_modifiers=event.modifiers# https://doc.qt.io/qt-5/qkeyevent.htmlevent.native_scancode=vkevent.text=""S.obs_source_send_key_click(source, event, key_up)
defpress_tab(*p):
withsource_auto_release(G.source_name) assource:
send_hotkey_to_browser(source, "OBS_KEY_TAB")
send_hotkey_to_browser(source, "OBS_KEY_TAB", key_up=True)
defpress_shift_tab(*p):
withsource_auto_release(G.source_name) assource:
send_hotkey_to_browser(source, "OBS_KEY_TAB", {"shift": True})
send_hotkey_to_browser(source, "OBS_KEY_TAB", {"shift": True}, key_up=True)

Browser source receive JSON data

cd=S.calldata_create()
ph=S.obs_source_get_proc_handler(source)
S.calldata_set_string(cd, "eventName", "my-test-event")
S.calldata_set_string(cd, "jsonString", '{"key123": "\\nvalue123"}')
S.proc_handler_call(ph, "javascript_event", cd)
S.calldata_destroy(cd)

Page source code, currently has no permission requirements.

<!DOCTYPEhtml><bodystyle="background-color:aquamarine;"><h1>HTML h1 in body element</h1></body><scripttype="text/javascript">
window.addEventListener('my-test-event', function(event) {document.body.innerHTML+=(event.detail['key123']);})
</script>

Access source dB volume level

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_tdefvolmeter_callback(data, mag, peak, input):
G.noise=float(peak[0])

Get current profile settings via ffi

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])
defoutput_to_stdout():
cfg=G.obs_frontend_get_profile_config()
e=lambdax: x.encode("utf-8")
s=G.config_get_string(cfg, e("SimpleOutput"), e("FilePath"))
l=G.config_num_sections(cfg)
foriinrange(l):
tag=G.config_get_section(cfg, c_size_t(i))
print(s, l)

Convert from SWIG type to ctype

cfg=cast(
c_void_p(int(S.obs_frontend_get_profile_config())), POINTER(Config)
)

Note,that this uses obspython.obs_frontend_get_profile_config so there is no need to load additional libraries and write glue ctypes code.

Set current stream key

service=S.obs_frontend_get_streaming_service()
settings=S.obs_service_get_settings(service)
S.obs_data_set_string(settings, "key", _G._my_key)
S.obs_service_update(service, settings)
S.obs_data_release(settings)
S.obs_frontend_save_streaming_service()

Raw frames

It is possible to grab raw frame from source see: Get video frames programmatically - this is a viable option if you want check frame data in memory, with no plugins or recompilation. Other methods:

  • obs-screenshot-plugin - Fork + Windows only, it has Output to Named Shared Memory Output among other things.
  • obs-python - Cython obs plugin with raw frames support
  • obs-virtualcam - Plugin, read webcam frames, related
  • obs-rs - Uses the decoupled hook implementation of OBS Studio, written in Rust, uses Windows API, possible to port to Python ctypes.
  • DXcam - Current maintained standalone DirectX high performance screen capture written in Python ctypes.

Debug

There is no stdin therefore you can't use pdb , options are:

  • ⭐ using print
  • using generated log file (contains information about memory leaks), also accessible via Help>Log Files>View Current Log
  • using pycharm remote debugging (localhost)
  • using vscode attach to the process:
    • Load python extension
    • open script file , pip install debugpy , place debugpy.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

screenshot

Security

  • "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, setup a firewall, do hardening, 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 private code.
  • Legal info (trademark) link
  • Privacy policy - https://obsproject.com/privacy-policy - keyword downlodable software

Docs and code examples

Contains all variables and functions available in obspython formatted with markdown. Table consist of links to appropriate search terms in OBS Studio repository, links to scripts in obspython and obslua with each script within GitHub code search.gs_* and matrix_* functions exluded from that table. Export names

Changes between versions

  • 28.0.0 version - Most Python 3 versions will work now, so there is no restriction to 3.6.8
  • 28.0.0 version - NO support for Windows 7 & 8, macOS 10.13 & 10.14, Ubuntu 18.04 and all 32-bit operating systems
  • 28.0.0 version - Added native support for websocket
  • 30.0.0 version - Python 3.11 support for Windows and mac OS
  • 30.0.0 version - Lua binary libraries loading fix
  • 30.1.0 version - Premultiplied Alpha option for game capture on Windows

Links

Contribute

Something missing? Write a PR!