Tips and tricks for Lua scripts

bfxdev

New Member
Hey @John_ , sorry for the delay, I was on a different project the past 3 weeks. Yes, in fact there is such a lot to do with these typemaps! Help is welcome!

First of all, obviously, is to have a compiling environment. I'm doing everything in Windows, with CMake, VSCode as IDE, Visual Studio Community 2019 only as compiler (my settings of CMake in VS2019 never worked properly), plus the QT and SWIG dependencies delivered as archives.

Second is to understand the concept of "typemap" by SWIG: typemaps in general of different kinds, then for Python and for Lua.

It is a lot of documentation! And it is very challenging to understand because it is about code generation in a mix of C, Python/Lua plus the typemap definition syntax.

My very basic understanding is that:
  1. SWIG reads OBS header files and generates code, as defined in the files obspython.i and obslua.i, where there is a list of included files and the definition of the typemaps
  2. For each C function encountered in some header file, SWIG will generate one C wrapper function that binds the original C function to the scripting language
  3. The purpose of the C wrapper function is to implement the conversions from/to Python/Lua, i.e. it will first check the Python/Lua parameters consistency, then convert to C structures, then call the original C function, then converts the C-encoded data back to Python/Lua and finally return the data if necessary.
  4. SWIG generates such C wrapper functions by assembling C code defined in the typemaps at the different stages of the C wrapper function. This is why there are several kinds of typemaps (in, typecheck, out, arginit...), e.g. typemap(in, .....) is used to convert function arguments from the target language to C
  5. There is a bunch of pre-defined fields that you can use in a typemap, which are expanded with function-specific values during code generation, e.g. $1will be replaced by the name of the variable used by SWIG as argument when calling the original C function, $inputis the Python/Lua object passed by the scripting language.
  6. The way typemaps are applied can be controlled. In general a typemap can be applied to all arguments of all functions, or can be limited to the arguments with a particular name, and a particular C function signature can be re-written to force the use of particular typemaps (this is what I did with the function gs_stagesurface_map above)
  7. Typemaps can be applied on a sequence of arguments too
  8. SWIG generates as well C wrapper functions as getters and setters for members of a struct
  9. There are tons of additional definitions in SWIG.

Finally, with that in mind (it is a lot to learn at the beginning, but once the basic typemap concept is understood, the rest seems logical), tweaking OBS bindings is done in just a few lines of code in the .i files.

For some reason, the typemap feature was never used in OBS and for example the frontend functions, which need non-classical type conversions, were written manually out of the SWIG scope. As a result, these functions do not appear in the obspython.py file (and do not appear if you are trying to use auto-completion).

At the end, the remaining work is to:
  • Identify the functions that can be improved with typemaps (see my previous posts), due to memory leaks or functions that are just not useable currently
  • Find a good way to improve with a typemap, that ideally does not break any existing script
My implementation of the bindings for gs_stagesurface_map works somehow, but is not free of memory leaks. Anyway the use is limited because the transfer of data back to RAM can be slow depending on the graphic card.

I would better look at the bindings for accessing the buffer of a texture_data member of the gs_image_file structure. In my opinion this is really a missing feature in OBS scripting.
 

NoeAL

Member
Hi, I am really new to LUA, I would like to add a checkbox to enable or disable the reading of a file and a reset button to remove the selected path. I have been unable to do so. How should I do it?

An example of how it should look:

lua_monitor.png


Thanks in advance. Excellent work! ;)
 

bfxdev

New Member
Hello, I would suggest the Source Shake tutorial for an introduction to scripting and an example of editable properties.

It looks like this in the tutorial:
Lua:
-- Called to display the properties GUI
function script_properties()
  props = obs.obs_properties_create()
  obs.obs_properties_add_text(props, "source_name", "Source name", obs.OBS_TEXT_DEFAULT)
  obs.obs_properties_add_float_slider(props, "frequency", "Shake frequency", 0.1, 20, 0.1)
  obs.obs_properties_add_int_slider(props, "amplitude", "Shake amplitude", 0, 90, 1)
  return props
end

For a checkbox you will need obs.obs_properties_add_bool, and for a button obs.obs_properties_add_button.

There is an example of a button with a callback function in the same tutorial.
 

upgradeQ

Member
I did post about LuaJIT FFI usage in the past Turns out it is not thread safe. The volmeter thread callback, for some reason, seg faults on GNU/Linux and that also true for Microsoft Windows. Is there a way to construct a C thread callback which calls Lua often and not crashes OBS Studio ? It just works on Python.
 

iraytrace

New Member
I would really like to see a script example for creating a dock window to put UI elements into. Some scripts really need their own window, rather than having to display next to the list of scripts. Would be nice to be able to create a dock window, put buttons and other UI controls in there so the user can put the UI for the script where they want.
 

autoharplive

New Member
Thank you all for these write ups they are super helpful! I do have a question though, how would I go about moving a source by 10 pixels. Essentially I would like to press a hotkey and it moves a source 10 pixels to the left.
 
Thank you all for these write ups they are super helpful! I do have a question though, how would I go about moving a source by 10 pixels. Essentially I would like to press a hotkey and it moves a source 10 pixels to the left.
The Source Shake tutorial explains a lot of the overall structure you would need. But instead of calling obs_sceneitem_set_rot to rotate a source, you would call obs_sceneitem_get_pos to get the current position, tweak it by 10, then call obs_sceneitem_set_pos
 

imHugoLeandro

New Member
Hello, I started working on an import overlay script. I wanted to know if is possible and how it's done. How can I create a function to create a nested scene, look through the scenes I have, and import that X scene to the Y scene. Thank you! bfxdev
 

imHugoLeandro

New Member
I came up with this code to create a nested scene source inside another source

function create_scene_source(source, name, new_scene, scene, xpos, ypos, xscale, yscale)
local pos = obs.vec2()
local scale = obs.vec2()
local sc_settings = obs.obs_data_create()
obs.obs_data_set_string(sc_settings, "add_existing", source)
local sc_source = obs.obs_source_create("scene", "(" .. source .. ")", sc_settings, nil)
obs.obs_scene_add(new_scene, sc_source)
local sc_sceneitem = obs.obs_scene_find_source(scene, name)
local sc_location = pos
local sc_scale = scale
if sc_sceneitem then
sc_location.x = xpos
sc_location.y = ypos
sc_scale.x = xscale
sc_scale.y = yscale
obs.obs_sceneitem_set_pos(sc_sceneitem, sc_location)
obs.obs_sceneitem_set_scale(sc_sceneitem, sc_scale)
end
obs.obs_source_update(sc_source, sc_settings)
obs.obs_data_release(sc_settings)
obs.obs_source_release(sc_source)
end

This creates scene item but it doesn't pick another scene it creates a new one with this name. If someone could help me thank you!
 

upgradeQ

Member
Config usage via ffi

To test those snippets download https://obsproject.com/forum/resources/obs-libre-macros.1228 3.2.0 Version

Recordings path manipulation, do not run this while recording.


Lua:
local function try_load_library(alias, name)
  if ffi.os == "OSX" then name = name .. ".0.dylib" end
  ok, _G[alias] = pcall(ffi.load, name)
  if not ok then
    print(("WARNING:%s:Has failed to load, %s is nil"):format(name, alias))
  end
end
try_load_library("obsffi", "obs")
try_load_library("frontendC", "obs-frontend-api")

ffi.cdef[[
struct config_data;
typedef struct config_data config_t;
config_t *obs_frontend_get_profile_config(void);
void config_set_string(config_t *config, const char *section, const char *name, const char *value);
int config_save(config_t *config);
]]

local my_profile = frontendC.obs_frontend_get_profile_config()
local path = 'C:/Users/user_name/some_none_existent_path'
if not os_file_exists(path) then
  os_mkdir(path)
end
obsffi.config_set_string(my_profile, "AdvOut", "RecFilePath", path)
obsffi.config_set_string(my_profile, "SimpleOutput", "FilePath", path)
local result = tonumber(obsffi.config_save(my_profile))
local map = {['0']='CONFIG_SUCCESS',['-1']= 'CONFIG_FILENOTFOUND',['-2']='CONFIG_ERROR'}
print(map[tostring(result)])
There is also config_set_string(my_profile,"AdvOut", "FFFilePath", path) if using FFmpeg in advanced options.
All those paths also shared with replay buffer.


Encoders and outputs from the ground up.
This example is presented as a snippet and inspired by this project
Lua:
function create_match_video_output()
  print("Creating match video OBS output")
  if t.output then print("WARNING: Match video OBS output already exists") return end
 --- create output for match video files ---------------------------------------
  t.output = obs_output_create("ffmpeg_muxer", "match_file_output", nil, nil)
  res_defer {res = t.output, defer = obs_output_release }
  if not t.output then print("ERROR: Could not create match video output") return end
  --- create output video encoder for match video files
  output_video_settings = obs_data_create()
  obs_data_set_string(output_video_settings, "rate_control", "CQP")
  obs_data_set_int(output_video_settings, "cqp", 13)
  t.output_video_encoder = obs_video_encoder_create("jim_hevc_nvenc", "match_video_encoder", output_video_settings, nil)
  res_defer {res = t.output_video_encoder, defer = obs_encoder_release }
  obs_data_release(output_video_settings)
  if not t.output_video_encoder then print("ERROR: Could not create match video encoder exit now!") return end
  if not obs_encoder_get_codec(t.output_video_encoder) then print("ERROR: Invalid codec for match video encoder exit") return end
  obs_encoder_set_video(t.output_video_encoder, obs_get_video())
  if not obs_encoder_video(t.output_video_encoder) then print("ERROR: Could not set video handler exit") return end
  obs_output_set_video_encoder(t.output, t.output_video_encoder)
  if not obs_output_get_video_encoder(t.output) then print("ERROR: Could not set video encoder to output exit") return end
  --- create output audio encoder for match video files
  output_audio_settings = obs_data_create()
  obs_data_set_int(output_audio_settings, "bitrate", 320)
  t.output_audio_encoder = obs_audio_encoder_create("ffmpeg_aac", "match_audio_encoder", output_audio_settings, 0, nil)
  res_defer {res = t.output_audio_encoder, defer = obs_encoder_release }
  obs_data_release(output_audio_settings)
  if not t.output_audio_encoder then print("ERROR: Could not create match audio encoder") return end
  if not obs_encoder_get_codec(t.output_audio_encoder) then print("ERROR: Invalid codec for match audio encoder") return end
  obs_encoder_set_audio(t.output_audio_encoder, obs_get_audio())
  if not obs_encoder_audio(t.output_audio_encoder) then print("ERROR: Could not set audio handler") return end
  obs_output_set_audio_encoder(t.output, t.output_audio_encoder, 0)
  if not obs_output_get_audio_encoder(t.output, 0) then print("ERROR: Could not set audio encoder to output") return end
end
function start_recording()
  if obs_output_active(t.output) then print("WARNING: Currently recording ") return end
  local p = 'Full/path/to/file.mkv'
  output_settings = obs_data_create()
  obs_data_set_string(output_settings, "path", p)
  obs_output_update(t.output, output_settings)
  obs_data_release(output_settings)
  if not obs_output_start(t.output) then
      print('ERROR: Could not start match recording') print(obs_output_get_last_error(t.output)) return end
  print("Recording started")
end
create_match_video_output(); start_recording() sleep(3) obs_output_stop(t.output)
This will create a new recording session for 3 seconds, will output to specified file path. There is no visual indications of recording.

See also related post about current recording session path
 

upgradeQ

Member
construction_3d.png

Rendering complex shaders in OBS Studio.

Recently, while working on the mouse cursor shaders, as I couldn't find any made by community members, I came across this thread about multi-pass shaders by @khaver .
Surprisingly, I did not find any example Lua code for reference, only C/C++ sources, so I converted them
to Lua - it works, but not 1 to 1.
The first shader is simple procedural one, the code goes as follows :

Lua:
function SourceDef:video_render()
  S.gs_texrender_reset(self.texture_a)
  S.gs_effect_set_float(self.params.itime, self.current_time+0.0) -- other params omitted
  if S.gs_texrender_begin(self.texture_a, self.width, self.height) then
    local tex =  S.gs_texrender_get_texture(self.texture_a)
    S.gs_effect_set_texture(self.params.image,tex)
    while S.gs_effect_loop(self.effect, "Draw") do
      S.gs_draw_sprite(tex,0,self.width,self.height)
    end
    S.gs_texrender_end(self.texture_a)
  end
  S.gs_effect_set_float(self.params.itime, self.current_time + 0.0)
  local tex2 =  S.gs_texrender_get_texture(self.texture_a)
  S.gs_effect_set_texture(self.params.image, tex2)
  while S.gs_effect_loop(self.effect, "Draw2") do
    S.gs_draw_sprite(tex2,0,self.width,self.height)
  end
end


As you can see we start with rendering to texture, after "Draw" technique we get the result texture and
set this result to the next "Draw2".
I re-created a shader from Shadertoy, linked below.

The second shader is more complex though, it uses two textures, 3 technique; in the first we invert the colour,
Then we split the screen vertically and paint one split to colour, not touching another split at all. Finally we paint
a thick line between (uv.y=0.1 and uv.y=0.5); In the end we get normal colour in this line, which is a proof that
texture sharing between techniques works. To get to this point, the video_render function should look like this:

Lua:
function SourceDef:video_render()
  S.gs_texrender_reset(self.texture_a)
  S.gs_effect_set_float(self.params.itime, self.current_time+0.0) -- params omitted
  if S.gs_texrender_begin(self.texture_a, self.width, self.height) then
    if not S.obs_source_process_filter_begin(self.source, S.GS_RGBA, S.OBS_ALLOW_DIRECT_RENDERING) then return end
    S.obs_source_process_filter_tech_end(self.source, self.effect, self.width, self.height,"Draw")
    S.gs_texrender_end(self.texture_a)
  end
  S.gs_texrender_reset(self.texture_b)
  if S.gs_texrender_begin(self.texture_b, self.width, self.height) then
    local tex2 =  S.gs_texrender_get_texture(self.texture_a)
    S.gs_effect_set_texture(self.params.image, tex2)
    while S.gs_effect_loop(self.effect, "Draw2") do
      S.gs_draw_sprite(tex2,0,self.width,self.height)
    end
    S.gs_texrender_end(self.texture_b)
  end
  local tex3 =  S.gs_texrender_get_texture(self.texture_b)
  S.gs_effect_set_texture(self.params.image, tex3)
  while S.gs_effect_loop(self.effect, "Draw3") do
    S.gs_draw_sprite(tex3,0,self.width,self.height)
  end
end


The key function to get source texture is this:
S.obs_source_process_filter_tech_end(self.source, self.effect, self.width, self.height,"Draw")
The gs_effect_loop seems not to be working there.

All comments and suggestions regarding performance improvement or refactoring are very much appreciated.
Link to full shaders source below, the code is noisy between "passes" where you usually set parameters for effect:
Links to related C/C++ plugins which use multi passes: 1 , 2 , 3
 

upgradeQ

Member
Continuing the previous post, I've skipped over recursive shaders a.k.a. those that reuse the previous frame, the solution to this problem is to save texture data at the end of render calls then set it at the beginning when the next frame render starts.
Like this:
Lua:
function SourceDef:video_render()
  S.gs_texrender_reset(self.texture_a)
  S.gs_effect_set_float(self.params.itime, self.current_time+0.0) -- params skipped
  if S.gs_texrender_begin(self.texture_a, self.width, self.height) then
    if not S.obs_source_process_filter_begin(self.source, S.GS_RGBA, S.OBS_ALLOW_DIRECT_RENDERING) then return end
    S.obs_source_process_filter_tech_end(self.source, self.effect, self.width, self.height,"Draw")
    S.gs_effect_set_texture(self.params.image, tex3)
    while S.gs_effect_loop(self.effect, "Draw2") do
      S.gs_draw_sprite(nil,0,self.width,self.height) -- nil??
    end
    S.gs_texrender_end(self.texture_a)
  end
  S.gs_effect_set_float(self.params.itime, self.current_time + 0.0)
  S.gs_texrender_reset(self.texture_b)
  if S.gs_texrender_begin(self.texture_b, self.width, self.height) then
    local tex2 =  S.gs_texrender_get_texture(self.texture_a)
    S.gs_effect_set_texture(self.params.image, tex2)
    while S.gs_effect_loop(self.effect, "Draw") do
      S.gs_draw_sprite(tex2,0,self.width,self.height)
    end
    S.gs_texrender_end(self.texture_b)
  end
  tex3 = S.gs_texrender_get_texture(self.texture_b) -- this frame will be reused
  S.gs_effect_set_texture(self.params.image, tex3)
  while S.gs_effect_loop(self.effect, "Draw") do
    S.gs_draw_sprite(tex3,0,self.width,self.height)
  end
end
The obs_source_process_filter_tech_end is not relevant to the shaders I've ported and could be commented, but I'm assuming it's responsible for the actual chain of filters and rendering of the source texture.

Now for the tips:
  • SHADERed + Shadertoy plugin + Save as hlsl - with this method you can port many shadertoy glsl shaders.
  • To get built in feedback loop, open the projector with desktop capture, you'll get the previous frame and if your "recursive" shader is active you will see it working right even without reusing previous frame in video_render()
  • Obvious, but effective is to use color source with your shader + chroma key filter to get rid of solid color background. You can try using my ported cursor trail shaders 1, 2 that way.
 

upgradeQ

Member
Hey @bfxdev , @John_ it turns out that you can convert OBS SWIG objects to pure ctypes, so it is possible to read any of source frames.
Unfortunately, AFAIK, there is no solution to do the same thing with LuaJIT ffi, because the GIL there does not work in the same way as in Python.
Here is a sample code, add a colour source with 1920x1080 and name it big123, open the console log, the first 100 pixels will update their values every time you use the colour correction filter or similar.

Python:
if source and S.gs_texrender_begin(G.render_texture, 1920, 1080):
    S.obs_source_video_render(source)
    S.gs_texrender_end(G.render_texture)
    if not G.surface:
        G.surface = ffi.gs_stagesurface_create(
            c_uint(1920), c_uint(1080), c_int(S.GS_RGBA)
        )
    tex = S.gs_texrender_get_texture(G.render_texture)
    tex = c_void_p(int(tex))
    tex = cast(tex, POINTER(TexRender))
    ffi.gs_stage_texture(G.surface, tex)
    data = POINTER(c_ubyte)()

    if ffi.gs_stagesurface_map(G.surface, byref(data), byref(c_uint(G.rgba_4b))):
        print(data[0:100])
        ffi.gs_stagesurface_unmap(G.surface)
    S.gs_texrender_reset(G.render_texture)
Full Source
 

Rivulet62

New Member
Hey everyone, would anyone be able to help me with something? I'm looking to start a video (Media Source) at a place other than the start, does anyone know how to do that?
 
Seems like it is what i want, yeah. I'm very new to this stuff, how would i get this in the script?

This thread seems to have a random assortment of related and unrelated topics. If you don't have experience with scripting OBS in Lua or Python, there are a number of good tutorials on this website. The classic being https://obsproject.com/wiki/Scripting-Tutorial-Source-Shake

Once you have a bit of feel, you can start doing your own stuff.

For starting a video source not at the beginning, it seems that I snipped the wrong function. The one you want is a bit further down in the doc file. C prototype is:
void obs_source_media_set_time(obs_source_t *source, int64_t ms)

In Lua or Python, the first argument would be your "source", and the second would be an integer to specify the number of milliseconds to where you want to start.
 

upgradeQ

Member
This tip is inspired by the recent thread about javascript clipboard access.
The gist of it - is that you can use it as a way to communicate with the built-in CEF browser within OBS Studio.
I've created a simple example to demonstrate this, also be aware that instead of sending clicks, you can create
proc_handler_call (available since version 29.1) with custom text and subscribe to that event in the browser.

Test page - add it as Browser Source.


HTML:
<!DOCTYPE html>
<html lang="en">
  <head>
    <button onclick="myFunction()">Click to copy</button>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1 id="my_el_id">Hello world</h1>
    <script>
      function myFunction() {
        let el = document.getElementById("my_el_id");
        let range = document.createRange();
        range.selectNode(el);
        window.getSelection().removeAllRanges();
        window.getSelection().addRange(range);
        document.execCommand("copy");
      }
    </script>
  </body>
</html>

LuaJIT ffi version:


Lua:
S = obslua
ffi = require "ffi" -- for native libs and C code access

g_config = {}

function send_mouse_click_tbs(source, opts)
  local event = S.obs_mouse_event()
  event.modifiers = 0 -- check docs if you need to have ctrl,alt etc present
  event.x = opts.x
  event.y = opts.y
  S.obs_source_send_mouse_click(
    source, event, opts.button_type, opts.mouse_up, opts.click_count
    )
end

function send_click_at()
  local _opts = {x=5, y=5, button_type=S.MOUSE_LEFT, mouse_up=false, click_count=0}
  local source = S.obs_get_source_by_name(g_config.source_name)
  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)
  S.obs_source_release(source)
end

ffi.cdef[[
enum {
    CF_TEXT     = 1,
    CF_BITMAP   = 2,
    CF_DIB      = 8
     };

int      OpenClipboard(void*);
void*    GetClipboardData(unsigned);
int      CloseClipboard();
void*    GlobalLock(void*);
int      GlobalUnlock(void*);
size_t   GlobalSize(void*);
]]

function clipboard()
  local ok1    = ffi.C.OpenClipboard(nil)
  local handle = ffi.C.GetClipboardData( ffi.C.CF_TEXT )
  local size   = ffi.C.GlobalSize( handle )
  local mem    = ffi.C.GlobalLock( handle )
  local text   = ffi.string( mem, size )
  local ok     = ffi.C.GlobalUnlock( handle )
  local ok3    = ffi.C.CloseClipboard()
  return text
end

function cb_clipboard()
  error(clipboard()) -- shows clipboard contents by bringing console log window up.
end

function script_update(settings)
  g_config.source_name = S.obs_data_get_string(settings, "source")
end

function script_properties()
  props = S.obs_properties_create()
  p = S.obs_properties_add_list(
    props,
    "source",
    "Browser source",
    S.OBS_COMBO_TYPE_EDITABLE,
    S.OBS_COMBO_FORMAT_STRING
  )
  S.obs_properties_add_button(props, "button1", "copy_into_buffer", send_click_at)
  S.obs_properties_add_button(props, "button2", "print_buffer", cb_clipboard)
  sources = S.obs_enum_sources()
  if sources then
    for i,source in ipairs(sources) do
      source_id = S.obs_source_get_unversioned_id(source)
      if source_id == "browser_source" then
        name = S.obs_source_get_name(source)
        S.obs_property_list_add_string(p, name, name)
      end
    end
    S.source_list_release(sources)
  end
  return props
end
 

LeBlux

New Member
This tip is inspired by the recent thread about javascript clipboard access.
The gist of it - is that you can use it as a way to communicate with the built-in CEF browser within OBS Studio.
I've created a simple example to demonstrate this, also be aware that instead of sending clicks, you can create
proc_handler_call (available since version 29.1) with custom text and subscribe to that event in the browser.

Test page - add it as Browser Source.


HTML:
<!DOCTYPE html>
<html lang="en">
  <head>
    <button onclick="myFunction()">Click to copy</button>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1 id="my_el_id">Hello world</h1>
    <script>
      function myFunction() {
        let el = document.getElementById("my_el_id");
        let range = document.createRange();
        range.selectNode(el);
        window.getSelection().removeAllRanges();
        window.getSelection().addRange(range);
        document.execCommand("copy");
      }
    </script>
  </body>
</html>

LuaJIT ffi version:


Lua:
S = obslua
ffi = require "ffi" -- for native libs and C code access

g_config = {}

function send_mouse_click_tbs(source, opts)
  local event = S.obs_mouse_event()
  event.modifiers = 0 -- check docs if you need to have ctrl,alt etc present
  event.x = opts.x
  event.y = opts.y
  S.obs_source_send_mouse_click(
    source, event, opts.button_type, opts.mouse_up, opts.click_count
    )
end

function send_click_at()
  local _opts = {x=5, y=5, button_type=S.MOUSE_LEFT, mouse_up=false, click_count=0}
  local source = S.obs_get_source_by_name(g_config.source_name)
  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)
  S.obs_source_release(source)
end

ffi.cdef[[
enum {
    CF_TEXT     = 1,
    CF_BITMAP   = 2,
    CF_DIB      = 8
     };

int      OpenClipboard(void*);
void*    GetClipboardData(unsigned);
int      CloseClipboard();
void*    GlobalLock(void*);
int      GlobalUnlock(void*);
size_t   GlobalSize(void*);
]]

function clipboard()
  local ok1    = ffi.C.OpenClipboard(nil)
  local handle = ffi.C.GetClipboardData( ffi.C.CF_TEXT )
  local size   = ffi.C.GlobalSize( handle )
  local mem    = ffi.C.GlobalLock( handle )
  local text   = ffi.string( mem, size )
  local ok     = ffi.C.GlobalUnlock( handle )
  local ok3    = ffi.C.CloseClipboard()
  return text
end

function cb_clipboard()
  error(clipboard()) -- shows clipboard contents by bringing console log window up.
end

function script_update(settings)
  g_config.source_name = S.obs_data_get_string(settings, "source")
end

function script_properties()
  props = S.obs_properties_create()
  p = S.obs_properties_add_list(
    props,
    "source",
    "Browser source",
    S.OBS_COMBO_TYPE_EDITABLE,
    S.OBS_COMBO_FORMAT_STRING
  )
  S.obs_properties_add_button(props, "button1", "copy_into_buffer", send_click_at)
  S.obs_properties_add_button(props, "button2", "print_buffer", cb_clipboard)
  sources = S.obs_enum_sources()
  if sources then
    for i,source in ipairs(sources) do
      source_id = S.obs_source_get_unversioned_id(source)
      if source_id == "browser_source" then
        name = S.obs_source_get_name(source)
        S.obs_property_list_add_string(p, name, name)
      end
    end
    S.source_list_release(sources)
  end
  return props
end
really interesting but I can't get it to work I get :

[testClick.lua] Error loading file: [string "C:/Program Files/obs-studio/data/obs-plugins/..."]:31: unfinished long string near '<eof>'

does it needs any dependancies ( should I install LuaJIT ffi somehow ? )
 
Top