OBS Studio Lua Scripting. Part 2

Hello everyone, in this part of the tutorial we will consider:

filters, scenes, scene objects, Frontend API, creating functional filters, and more ...

The first part can be found at this link.



KDPV made in OBS 26.0.0



Quick reference for this part



  • Source - Sources are used to render audio / video, for example: capture of a camera, game, sound. Using sources, you can create filters, transitions



  • Filter - Source that complements other sources



  • — , ( )



  • — , : , /, , / ..



  • Frontend API — OBS Studio, :



    • , /
    • / /






, obs_source_get_unversioned_id



compressor_filter
expander_filter
gain_filter
invert_polarity_filter
limiter_filter
noise_gate_filter
noise_suppress_filter
VST 2.x vst_filter
() async_delay_filter
chroma_key_filter
color_filter
color_key_filter
crop_filter
/ mask_filter
luma_key_filter
gpu_delay
/ scale_filter
scroll_filter
sharpness_filter


:



: 1 100.



"" - , . , OBS:

> >

, ~/basic>scenes>_.json

color_filter color_key_filter ( ).

settings opacity.

, —





function add_filter_to_source(random_n)
  source = obs.obs_get_source_by_name(source_name)


opacity



settings = obs.obs_data_create()
obs.obs_data_set_int(settings, "opacity",random_n)


,



_color_filter = obs.obs_source_get_filter_by_name(source,"opacity_random")
if _color_filter == nil then -- if not exists
  _color_filter = obs.obs_source_create_private( "color_filter", "opacity_random", settings)
  obs.obs_source_filter_add(source, _color_filter)
end




  obs.obs_source_update(_color_filter,settings)

  obs.obs_source_release(source)
  obs.obs_data_release(settings)
  obs.obs_source_release(_color_filter)
end




function htk_1_cb(pressed) 
  if pressed then
    n = math.random(1,100)
    add_filter_to_source(n)
  end
end


GIF



local obs = obslua
source_name = ''

function htk_1_cb(pressed) 
  if pressed then
    n = math.random(1,100)
    add_filter_to_source(n)
  end
end

function add_filter_to_source(random_n)
  source = obs.obs_get_source_by_name(source_name)
  settings = obs.obs_data_create()

  obs.obs_data_set_int(settings, "opacity",random_n)

  _color_filter = obs.obs_source_get_filter_by_name(source,"opacity_random")
  if _color_filter == nil then -- if not exists
    _color_filter = obs.obs_source_create_private( "color_filter", "opacity_random", settings)
    obs.obs_source_filter_add(source, _color_filter)
  end

  obs.obs_source_update(_color_filter,settings)

  obs.obs_source_release(source)
  obs.obs_data_release(settings)
  obs.obs_source_release(_color_filter)
end

function script_properties()
  -- source https://raw.githubusercontent.com/insin/obs-bounce/master/bounce.lua
  local props = obs.obs_properties_create()
  local source = obs.obs_properties_add_list(
    props,
    'source',
    'Source:',
    obs.OBS_COMBO_TYPE_EDITABLE,
    obs.OBS_COMBO_FORMAT_STRING)
  for _, name in ipairs(get_source_names()) do
    obs.obs_property_list_add_string(source, name, name)
  end
  return props
end

function script_update(settings)
  source_name = obs.obs_data_get_string(settings, 'source')
end

--- get a list of source names, sorted alphabetically
function get_source_names()
  local sources = obs.obs_enum_sources()
  local source_names = {}
  if sources then
    for _, source in ipairs(sources) do
      -- exclude Desktop Audio and Mic/Aux by their capabilities
      local capability_flags = obs.obs_source_get_output_flags(source)
      if bit.band(capability_flags, obs.OBS_SOURCE_DO_NOT_SELF_MONITOR) == 0 and
        capability_flags ~= bit.bor(obs.OBS_SOURCE_AUDIO, obs.OBS_SOURCE_DO_NOT_DUPLICATE) then
        table.insert(source_names, obs.obs_source_get_name(source))
      end
    end
  end
  obs.source_list_release(sources)
  table.sort(source_names, function(a, b)
    return string.lower(a) < string.lower(b)
  end)
  return source_names
end

key_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'
json_s = key_1
default_hotkeys = {
  {id='htk_1',des=' 1 ',callback=htk_1_cb},
}

function script_load(settings)

  s = obs.obs_data_create_from_json(json_s)
  for _,v in pairs(default_hotkeys) do 
    a = obs.obs_data_get_array(s,v.id)
    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)
    obs.obs_hotkey_load(h,a)
    obs.obs_data_array_release(a)
  end
  obs.obs_data_release(s)
end


obs_source_enum_filters

, obspython,

.



function check()
  source = obs.obs_get_source_by_name(source_name)

  result = obs.obs_source_enum_filters(source)
  for k,v in pairs(result) do 
    name = obs.obs_source_get_name(v)
    print('name'.. name)
  end

  obs.source_list_release(result)
  obs.obs_source_release(source)
end




: , .mp3 .



.



function on_event(event) 
  if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED
    then obs_play_sound_release_source()
  end 
end


, : alert.mp3

, obs_source_set_monitoring_type .



function play_sound()
  mediaSource = obs.obs_source_create_private("ffmpeg_source", "Global Media Source", nil)
  local s = obs.obs_data_create()
  obs.obs_data_set_string(s, "local_file",script_path() .. "alert.mp3")
  obs.obs_source_update(mediaSource,s)
  obs.obs_source_set_monitoring_type(mediaSource,obs.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)
  obs.obs_data_release(s)

  obs.obs_set_output_source(outputIndex, mediaSource)
  return mediaSource
end

function obs_play_sound_release_source()
  r = play_sound()
  obs.obs_source_release(r)
end


local obs = obslua
mediaSource = nil -- Null pointer
outputIndex = 63 -- Last index

function play_sound()
  mediaSource = obs.obs_source_create_private("ffmpeg_source", "Global Media Source", nil)
  local s = obs.obs_data_create()
  obs.obs_data_set_string(s, "local_file",script_path() .. "alert.mp3")
  obs.obs_source_update(mediaSource,s)
  obs.obs_source_set_monitoring_type(mediaSource,obs.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)
  obs.obs_data_release(s)

  obs.obs_set_output_source(outputIndex, mediaSource)
  return mediaSource
end

function obs_play_sound_release_source()
  r = play_sound()
  obs.obs_source_release(r)
end

function on_event(event) 
  if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED
    then obs_play_sound_release_source()
  end 
end

function script_load(settings)
  obs.obs_frontend_add_event_callback(on_event)
end

function script_unload()
  obs.obs_set_output_source(outputIndex, nil)
end




, "a" — ( ) "content", "w" — .



io.output(io.open(script_path() .. "out.txt","a"))
io.write("content")
io.close()


print(os.date("%c"))
--     






  • obs_sceneitem_get_source
  • obs_scene_from_source
  • obs_scene_find_source
  • obs_frontend_get_scenes — , source_list_release
  • obs_frontend_get_current_scene
  • obs_scene_enum_items — , sceneitem_list_release


: ( ).





function toggle_source()
  scenes = obs.obs_frontend_get_scenes()
  for _,scene in pairs(scenes) do
    scene_source = obs.obs_scene_from_source(scene)
    items = obs.obs_scene_enum_items(scene_source)
...


, source_name boolean .



...
for _,scene_item in pairs(items) do
  _source = obs.obs_sceneitem_get_source(scene_item)
  _name = obs.obs_source_get_name(_source)
  if _name == source_name then
    boolean = not boolean 
    obs.obs_sceneitem_set_visible(scene_item, boolean)
  end
end
...


GIF



local obs = obslua
source_name = ''
boolean = true

function htk_1_cb(pressed) 
  if pressed then
    toggle_source()
  end
end

function toggle_source()
  scenes = obs.obs_frontend_get_scenes()
  for _,scene in pairs(scenes) do
    scene_source = obs.obs_scene_from_source(scene)
    items = obs.obs_scene_enum_items(scene_source)

    for _,scene_item in pairs(items) do
      _source = obs.obs_sceneitem_get_source(scene_item)
      _name = obs.obs_source_get_name(_source)
      if _name == source_name then
        boolean = not boolean 
        obs.obs_sceneitem_set_visible(scene_item, boolean)
      end
    end
    obs.sceneitem_list_release(items)
  end
  obs.source_list_release(scenes)
end

function script_properties()
  -- source https://raw.githubusercontent.com/insin/obs-bounce/master/bounce.lua
  local props = obs.obs_properties_create()
  local source = obs.obs_properties_add_list(
    props,
    'source',
    'Source:',
    obs.OBS_COMBO_TYPE_EDITABLE,
    obs.OBS_COMBO_FORMAT_STRING)
  for _, name in ipairs(get_source_names()) do
    obs.obs_property_list_add_string(source, name, name)
  end
  obs.obs_property_set_long_description(source,"?" )
  return props
end

function script_update(settings)
  source_name = obs.obs_data_get_string(settings, 'source')
end

--- get a list of source names, sorted alphabetically
function get_source_names()
  local sources = obs.obs_enum_sources()
  local source_names = {}
  if sources then
    for _, source in ipairs(sources) do
      -- exclude Desktop Audio and Mic/Aux by their capabilities
      local capability_flags = obs.obs_source_get_output_flags(source)
      if bit.band(capability_flags, obs.OBS_SOURCE_DO_NOT_SELF_MONITOR) == 0 and
        capability_flags ~= bit.bor(obs.OBS_SOURCE_AUDIO, obs.OBS_SOURCE_DO_NOT_DUPLICATE) then
        table.insert(source_names, obs.obs_source_get_name(source))
      end
    end
  end
  obs.source_list_release(sources)
  table.sort(source_names, function(a, b)
    return string.lower(a) < string.lower(b)
  end)
  return source_names
end

key_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'
json_s = key_1
default_hotkeys = {
  {id='htk_1',des=' 1 ',callback=htk_1_cb},
}

function script_load(settings)

  s = obs.obs_data_create_from_json(json_s)
  for _,v in pairs(default_hotkeys) do 
    a = obs.obs_data_get_array(s,v.id)
    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)
    obs.obs_hotkey_load(h,a)
    obs.obs_data_array_release(a)
  end
  obs.obs_data_release(s)
end




obslua obs_register_source,

( ).

,

. , , .



: , .



, -.



local obs = obslua
local bit = require("bit")

local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.html
info.id = "uniq_filter_id"
info.type = obs.OBS_SOURCE_TYPE_FILTER
info.output_flags = bit.bor(obs.OBS_SOURCE_VIDEO)

info.get_name = function() return 'default filter name' end


,



info.create = function(settings,source) 
  local filter = {}
  filter.context = source


, .



filter.hotkeys = {
  htk_stop = "[stop] ",
  htk_restart = "[start] ",
}
filter.hotkey_mapping = function(hotkey,data)
  if hotkey == "htk_stop" then
    print('stop '.. data.srsn .. " : " .. data.filn)
  elseif hotkey == "htk_restart" then
    print('restart ' .. data.srsn .. " : " .. data.filn)
  end
end

filter.hk = {}
for k,v in pairs(filter.hotkeys) do 
  filter.hk[k] = obs.OBS_INVALID_HOTKEY_ID
end


( . )

return



filter._reg_htk = function()
    info.reg_htk(filter,settings)
  end
  obs.timer_add(filter._reg_htk,100) -- callback to register hotkeys, one time only


,obs_filter_get_parent

. .



info.reg_htk = function(filter,settings) -- register hotkeys after 100 ms since filter was created
  local target = obs.obs_filter_get_parent(filter.context)
  local srsn = obs.obs_source_get_name(target) 
  local filn =  obs.obs_source_get_name(filter.context)
  local data = {srsn = srsn, filn = filn} 

  for k, v in pairs(filter.hotkeys) do 
    filter.hk[k] = obs.obs_hotkey_register_frontend(k, v .. srsn .. " : " .. filn, function(pressed)
    if pressed then filter.hotkey_mapping(k,data) end end)
    local a = obs.obs_data_get_array(settings, k)
    obs.obs_hotkey_load(filter.hk[k], a)
    obs.obs_data_array_release(a)
  end

  obs.remove_current_callback()
end


, ""



info.video_render = function(filter, effect) 
  -- called every frame
  local target = obs.obs_filter_get_parent(filter.context)
  if target ~= nil then
    filter.width = obs.obs_source_get_base_width(target)
    filter.height = obs.obs_source_get_base_height(target)
  end
  obs.obs_source_skip_video_filter(filter.context) 
end

info.get_width = function(filter)
  return filter.width
end

info.get_height = function(filter)
  return filter.height
end


.save , . .

obs.obs_register_source(info) — ,



info.save = function(filter,settings)
  for k, v in pairs(filter.hotkeys) do
    local a = obs.obs_hotkey_save(filter.hk[k])
    obs.obs_data_set_array(settings, k, a)
    obs.obs_data_array_release(a)
  end
end
obs.obs_register_source(info)


info.loadscript_load, ,

. .update, .get_properties

script_update, script_properties.



GIF



local obs = obslua
local bit = require("bit")

local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.html
info.id = "uniq_filter_id"
info.type = obs.OBS_SOURCE_TYPE_FILTER
info.output_flags = bit.bor(obs.OBS_SOURCE_VIDEO)

info.get_name = function() return 'default filter name' end

info.create = function(settings,source) 
  local filter = {}
  filter.context = source

  filter.hotkeys = {
    htk_stop = "[stop] ",
    htk_restart = "[start] ",
  }
  filter.hotkey_mapping = function(hotkey,data)
    if hotkey == "htk_stop" then
      print('stop '.. data.srsn .. " : " .. data.filn)
    elseif hotkey == "htk_restart" then
      print('restart ' .. data.srsn .. " : " .. data.filn)
    end
  end

  filter.hk = {}
  for k,v in pairs(filter.hotkeys) do 
    filter.hk[k] = obs.OBS_INVALID_HOTKEY_ID
  end

  filter._reg_htk = function()
    info.reg_htk(filter,settings)
  end
  obs.timer_add(filter._reg_htk,100) -- callback to register hotkeys, one time only

  return filter
end

info.reg_htk = function(filter,settings) -- register hotkeys after 100 ms since filter was created
  local target = obs.obs_filter_get_parent(filter.context)
  local srsn = obs.obs_source_get_name(target) 
  local filn =  obs.obs_source_get_name(filter.context)
  local data = {srsn = srsn, filn = filn} 

  for k, v in pairs(filter.hotkeys) do 
    filter.hk[k] = obs.obs_hotkey_register_frontend(k, v .. srsn .. " : " .. filn, function(pressed)
    if pressed then filter.hotkey_mapping(k,data) end end)
    local a = obs.obs_data_get_array(settings, k)
    obs.obs_hotkey_load(filter.hk[k], a)
    obs.obs_data_array_release(a)
  end

  obs.remove_current_callback()
end

info.video_render = function(filter, effect) 
  -- called every frame
  local target = obs.obs_filter_get_parent(filter.context)
  if target ~= nil then
    filter.width = obs.obs_source_get_base_width(target)
    filter.height = obs.obs_source_get_base_height(target)
  end
  obs.obs_source_skip_video_filter(filter.context) 
end

info.get_width = function(filter)
  return filter.width
end

info.get_height = function(filter)
  return filter.height
end

--info.load = function(filter,settings) -- restart required
--... same code as in info.reg_htk, but filters will be created from scratch every time
--obs restarts, there is no reason to define it here again becuase hotkeys will be duplicated
--end

info.save = function(filter,settings)
  for k, v in pairs(filter.hotkeys) do
    local a = obs.obs_hotkey_save(filter.hk[k])
    obs.obs_data_set_array(settings, k, a)
    obs.obs_data_array_release(a)
  end
end

obs.obs_register_source(info)


obspython



OBS Python, Windows 3.6 , Linux (. ),

MacOS Python (26.0.0) .

Lua , ,

. wrapper -.

.:







, ,

script_tick( )

logs,

: Number of memory leaks: 0,

. OBS .



3)[] " "

,

( ) (scroll_filter),

/

0 1000 / 50% .



GIF



4)[] ""

.

— ( error())



5)[ ] "-"

,

, "",

UI , .

picture



6)[ ] ""

, .

, . .

GIF



7) [ ] " "

2 .

GIF



Github








All Articles