Skip to content

upgradeQ/libre-macros

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Description

libre-macros is an Extension for OBS Studio built on top of its scripting facilities, utilising built-in embedded LuaJIT interpreter, filter UI and function environment from Lua 5.1

Features

  • Attach Console to any source in real-time
  • Auto run code when OBS starts, load from file, Hot reload expressions
  • Hotkeys support for each Console instance
  • Less boilerplate: an environment provided with already defined namespace and useful functions
    • source - access source reference unique to each Console instance
    • t.pressed - access hotkey state
    • sleep(seconds) - command to pause execution
    • t.tasks - asynchronous event loop
    • obsffi - accessed via obsffi - native linked library
    • View and change all settings for source, and for filter in that source
    • Send, pause, resume, switch, recompile Console instances via GLOBAL(per OBS Studio instance) multi actions pipes
    • Read and write private data, execute Python from Lua, and Lua from Python
    • Create hollow gaps
    • Browser source keyboard and mouse interaction
    • and more... Check out this README for examples

Installation

  • Download source code, unpack/unzip
  • Add console.lua to OBS Studio via Tools > Scripts > "+" button

Usage

  • Left click on any source, add Console filter to it
  • Open Script Log to view Console output
  • Type some code into the text area
  • Press Execute!

Each Console instance has it's own namespace t and custom environment, you can access source which Console is attached to. e.g:

print(obs_source_get_name(source)) 

To access global the state of script do it via _G, when you write x = 5, only that instance of Console will have it

Auto run

If you check Auto run then code from this console will be executed automatically when OBS starts

Loading from file

To load from file you need first select which one to load from properties, see "Settings for internal use", then paste this template into text area:

local f = loadfile(t.p1, "t",getfenv(1))
success, result = pcall(f)
if not success then print(result) end

Hotkeys usage

There are 2 types of hotkeys:

  • First, can be found in settings with prefixed 0; - it will execute code in text area
  • Second, prefixed with 1;, 2;, 3; - it will mutate t.pressed, t.pressed2, t.pressed3 states

EXAMPLES & USAGE

High frequency blinking source:

  • Auto run
while true do 
sleep(0.03)
obs_source_set_enabled(source, true) 
sleep(0.03)
obs_source_set_enabled(source, false) 
end

Print source name while holding hotkey:

repeat
sleep(0.1)
if t.pressed then print_source_name(source) end 
until false 

Push-to-talk release delay, set hotkey for 1; of Audio Input source

repeat
  sleep(0.0)
  if t.pressed 
    then obs_source_set_volume(source,0.5)
  else 
    sleep(0.8) obs_source_set_volume(source,0.0)
  end 
until false 

Play media segments, get the current time (in milliseconds) of the media with get_timing(), length - get_duration()

repeat
  play_once(21012,23528)
  play_once(13012,15528)
  play_once(40012,43528)
  play_once(33012,37528)
until false

Raw image ffi screenshot of any source as 512x288px, scaled

Windows, DirectX only. Bindings written for gs_texture_get_obj and gs_get_device_obj. Note, may hit FPS, check stats

dx_screenshot "Scene 2"
local img = c_u8_p(t.raw_image)
local n = MAX_LEN
print(table.concat({img[0], img[1], img[2], img[3]}, ' '))
print(table.concat({img[n-4], img[n-3], img[n-2], img[n-1]}, ' '))

Hot reload with delay:

print('restarted') -- expression print_source_name(source)
local delay = 0.5
while true do
local f=load( t.hotreload)
setfenv(f,getfenv(1))
success, result = pcall(f)
if not success then print(result) end
sleep(delay)
end

Shake a text source and update its text based on location from scene

(using code from wiki) Paste into Console or load from file this code:

local source_name = obs_source_get_name(source)
local _name = "YOUR CURRENT SCENE NAME YOU ARE ON"
local sceneitem = get_scene_sceneitem(_name, return_source_name(source))
local amplitude , shaken_sceneitem_angle , frequency = 10, 0, 2
local pos = vec2()

local function update_text(source, text)
  local settings = obs_data_create()
  obs_data_set_string(settings, "text", text)
  obs_source_update(source, settings)
  obs_data_release(settings)
end
local function get_position(opts)
  return "pos x: " .. opts.x .. " y: " .. opts.y
end
repeat 
  sleep(0) -- sometimes obs freezes if sceneitem is double clicked
  local angle = shaken_sceneitem_angle + amplitude*math.sin(os.clock()*frequency*2*math.pi)
  obs_sceneitem_set_rot(sceneitem, angle)
  obs_sceneitem_get_pos(sceneitem, pos)
  local result = get_position { x = pos.x, y = pos.y }
  update_text(source, result)
until false

Permanent storage in private source settings

settings1 = obs_source_get_private_settings(source)
obs_data_set_int(settings1,"__private__", 7)
obs_apply_private_data(settings1)
obs_data_release(settings1)

Those settings are global for a source, e.g in the next Console filter

settings2 = obs_source_get_private_settings(source)
local xc = obs_data_get_int(settings2,"__private__")
print(xc)
obs_data_release(settings2)

Tasks

Print a source name every second while also print current filters attached to source in t.tasks, shutdown this task after 10 seconds

function print_filters()
  repeat
  local filters_list = obs_source_enum_filters(source)
  for _, fs in pairs(filters_list) do
    print_source_name(fs)
  end
  source_list_release(filters_list)
  sleep(math.random())
  until false
end

t.tasks[1] = run(print_filters)
function shutdown_all()
 for task, _coro in pairs(t.tasks) do 
   t.tasks[task] = nil
 end
end

t.tasks[2] = run(function()
  sleep(10)
  shutdown_all()
end)

repeat 
sleep(1)
print_source_name(source)
until false

Internal settings redirection

Using move-transition plugin with its move-audio filter, redirect to t.mv2, then show value of t.mv2 in Script Log

repeat 
sleep(0.3)
print(t.mv2)
until false

Start virtual camera as a triggered named callback:

local description = 'OBSBasic.StartVirtualCam'
trigger_from_hotkey_callback(description)

Send hotkey combination to OBS:

send_hotkey('OBS_KEY_2', {shift=true})

Hook state of right and left mouse buttons:

hook_mouse_buttons()
repeat 
sleep(0.1)
print(tostring(LMB))
print(tostring(RMB))
until false

Access sceneitem from scene:

local sceneitem = get_scene_sceneitem("Scene 2", sname(source))
repeat 
sleep(0.01)
if sceneitem then
  obs_sceneitem_set_rot(sceneitem, math.sin(math.random() * 100))
  end
until false

Route audio move value filter from obs-move-transition to change console settings Attach console to image source, add images to directory with console.lua In audio move set Input Peak Sample, select Move value[0, 100] 1 base value 1, factor 100

function update_image(state)
  local settings = obs_data_create()
  obs_data_set_string(settings, "file", script_path() .. state)
  obs_source_update(source, settings)
  obs_data_release(settings)
end
local skip, scream, normal, silent = false, 30, 20, 20
while true do ::continue::
  sleep(0.03)
  if t.mv2 > scream then update_image("scream.png") skip = false
    sleep(0.5) goto continue end
  if t.mv2 > normal then update_image("normal.png") skip = false
    sleep(0.3) goto continue
  end -- pause for a moment then goto start
  if t.mv2 < silent then if skip then goto continue end
    update_image("silent.png") 
    skip = true -- do not update afterwards
  end
end

Result: gif

Browser source interaction

Send mouse move

repeat sleep(1)
send_mouse_move_tbs(source, 12, 125) 
local get_t = function() return math.random(125, 140) end
for i=12, 200, 6 do 
  sleep(0.03)
  send_mouse_move_tbs(source, i, get_t()) 
end
until false

Example gif - 2 consoles are sending mouse move events into browser sources: gif Website link: https://zennohelpers.github.io/Browser-Events-Testers/Mouse/Mouse.html?

Send Click

repeat sleep(1)
--send_mouse_move_tbs(source, 95, 80) -- 300x300 browser source
_opts = {x=95, y=80, button_type=MOUSE_LEFT, mouse_up=false, click_count=0}
send_mouse_click_tbs(source, _opts) 
-- here might be delay which specifies how long mouse is pressed
_opts.mouse_up, _opts.click_count = true, 2
send_mouse_click_tbs(source, _opts) 
until false

Wheel does not work with default CSS

Note: currently does not work on 27.2.4

repeat sleep(1)
--send_mouse_move_tbs(source, 95, 80) -- 300x300 browser source
_opts = {x=95, y=80, y_delta=3}
send_mouse_wheel_tbs(source, _opts) 
until false

Keyboard interaction

-- Send tab
send_hotkey_tbs1(source, "OBS_KEY_TAB", false)
send_hotkey_tbs1(source, "OBS_KEY_TAB", true)

-- Send tab with shift modifier
send_hotkey_tbs1(source, "OBS_KEY_TAB", false, {shift=true})
send_hotkey_tbs1(source, "OBS_KEY_TAB", true, {shift=true})

send_hotkey_tbs1(source, "OBS_KEY_RETURN", false)
send_hotkey_tbs1(source, "OBS_KEY_RETURN", true)


-- char_to_obskey (ASCII only)
send_hotkey_tbs1(source, char_to_obskey('j'), false, {shift=true})
send_hotkey_tbs1(source, char_to_obskey('j'), true, {shift=true})
-- or use
send_hotkey_tbs1(source, c2o('j'), false)
send_hotkey_tbs1(source, c2o('j'), true)
-- might work with unicode input
send_hotkey_tbs2(source, 'q', false)
send_hotkey_tbs2(source, 'й', false)

Execute python(must load helper script)

exec_py(
[=[def print_hello():
   print('hello world')
   a = [ x for x in range(10) ][0]
   return a
print_hello()
]=])

React on source signals

register_on_show(function()
print('on show')
sleep(3)
print('on show exit')
 end)

Run multiactions

Example

Console instance with this entries in first and second text area

okay("pipe1")
print('exposing pipe 1')

Actual code, write it in second text area in each instance of Console

print(os.time()) print('  start 11111') ; sleep (0.5) ; print(os.time())
print_source_name(source) ; sleep(2) print('done 11111')

Another Console instance with same code first text area but different in second

okay("pipe2")
print('exposing pipe 2')

And in multiaction text area add this

print(os.time()) print('start ss22222ssssss2ss') ; sleep (2.5 ) ; print(os.time())
print_source_name(source) ; sleep(2) print('done 2222')

Main Console instance. This will start pipe1 then after sec pipe2

offer('pipe1')
sleep(1)
offer('pipe2')
  • okay - exposes actions
  • offer - starts actions
  • stall - pause
  • forward - continue
  • switch - pause/continue
  • recompile - restarts actions

Gaps sources

Only usable through attaching via filter to scene (not groups)

  • Add gap:
add_gap {x=300,y=500, width = 100, height = 100}
  • Add outer gaps - add_outer_gap(100)
  • Resize outer gaps - resize_outer_gaps(30)
  • Delete all gaps on scene - delete_all_gaps()

View and set settings

  • print_settings(source) - shows all settings
  • print_settings2(source, filter_name) - shows all settings for a filter on that source
  • set_settings2(source, filter_name, opts) - sets one settings
  • set_settings3(source, filter_name, json_string) - sets one settings
  • set_settings4(source, json_string) - sets settings for source

Examples:

set_settings2(source, "Color Correction", {_type ="double", _field= "gamma", _value= 0})
local my_json_string = [==[
{"brightness":0.0,"color_add":0,"color_multiply":16777215,
"contrast":0.0,"gamma":0.0,"hue_shift":0.0,"opacity":1.0,"saturation":0.0}
]==]
set_settings3(source, "Color Correction", my_json_string)

Save filter settings and restore them

stash "Retro Effects"

Click Execute! to save filter state of settings into the stash, Click again to restore it Duplicate Console filter if you want another stash Note: Built-in filters work differently and you may want to press Defaults first, then restore from stash

Useful functions

Also read source to know exactly how they work in section which defines general purpose functions

execute(command_line, current_directory) - executes command line command without console blinking WINDOWS ONLY Example:

if execute[["C:\full\path\to\python.exe" "C:\Users\YOUR_USERNAME\path\to\program.py" ]] then
error('done') else error('not done') end

pp_execute - works roughly same as above, based on util.h from libobs see also

sname(source) - returns source name as string

sceneitem = get_scene_sceneitem(scene_name, scene_item_name)

click_property(source, property_name) - This will refresh browser source click_property(source, "refreshnocache")

click_property_filter_ffi(source, filter_name, prop_name) - This will press Execute! button click_property_filter_ffi(source, "Console", "button1")

Notes on exceptions

There might be exceptions in your code, it is recommended to add print('start') and print('end') statements to debug code in Console

Snippets

  • On/off sceneitem every 2.5 seconds - source must be a scene
  • Loop media source between start and end via hotkey - adds two hotkeys to set and clear loop (1; and 2;)
  • Write internal stats to text source - based on OBS-Stats-on-Stream use FreeType2 for it, it's more efficient
  • Update browser every 15 minutes
  • Overwrite maximum render delay limit

Contribute

Contributions are welcome! You might take a look into source code for translation of UI to your language. Any contribution that is done will include your given name under the credits section.

On the Roadmap

  • Add more stuff to control & interact with browser source
  • Add snippets and examples
  • Add common functions and more usefull FFI code

See also

License

The libre-macros is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. That means that IF users interacting with it remotely(through a network) - they are entitled to source code. And if it is not modified, then you can direct them here, but if you had modified it, you simply have to publish your modifications. The easiest way to do this is to have a public Github repository of your fork or create a PR upstream. Otherwise, you will be in violation of the license. The relevant part of the license is under section 13 of the AGPLv3.