jbscript.dev
My First Lua (and OBS) Script
April 24, 2020
I wrote some Lua for the first time last night to bounce scene items around in Open Broadcaster Software (OBS), in the style of the bouncing DVD logo.

I’ve always wanted to play about with Lua, as my younger kids are Roblox mad and it’s the language used to script games in Roblox Studio, so that’s potentially a way to get them into coding in the future…
Demo
Here’s a demo of the finished script bouncing a background logo around in an OBS scene (sound ON):
https://www.youtube.com/embed/FbtzencagAM?rel=0
Anatomy of an OBS scene
When you’re using OBS, you create scenes which contain the different elements you want to display, which are known as sources.

Scenes can also contain other scenes, so they can be used a bit like a UI component model to break the final UI you want to create into smaller, reusable chunks. For example, it’s common to have a scene containing anything you want to appear as an overlay (such as notifications) and include that as the top layer in every other scene you create.
When you’re using the OBS API, a scene is an obs_scene_t object and a source within a scene is an obs_sceneitem_t object, but they’re both also obs_source_t objects. In fact, everything you can see in the screenshot above is a obs_source_t object, including the audio inputs.
This is something you have to keep in mind, as different API methods require the use of specific object types. For example, the API functions to get the width and height of a source take an obs_source_t as input, but the function to get the position of a source within a scene takes an obs_sceneitem_t.
Finding a scene item
The first thing the script needs to do is find a scene item by name within the current scene.
The fun begins immediately, as the obs.obs_frontend_get_current_scene() function actually returns an obs_source_t, so we need to use it to get a reference to an obs_scene_t in order to search for the scene item:
local source = obs.obs_frontend_get_current_scene()local scene = obs.obs_scene_from_source(source)-- more on this line later…
obs.obs_source_release(source)-- scene_item is a variable already declared at the module level
scene_item = obs.obs_scene_find_source(scene, source_name)
If you guessed that obs.obs_scene_find_source() would return an obs_sceneitem_t because that’s the opposite of what it says it does, you guessed correctly 🙂
Position and dimensions
In order to move a scene item around, we need to know 3 things:
Its current position
Its dimensions (width and height)
The dimensions of the scene it’s in
local pos = obs.vec2()
obs.obs_sceneitem_get_pos(scene_item, pos)-- the functions for width and height need an obs_source_tlocal source = obs.obs_sceneitem_get_source(scene_item)local width = obs.obs_source_get_width(source)local height = obs.obs_source_get_height(source)-- note that we don't use obs.obs_source_release(source) here…
For the scene dimensions, we can go back and do this when we’re looking for the scene item:
local source = obs.obs_frontend_get_current_scene()local scene = obs.obs_scene_from_source(source)-- module level variables
scene_width = obs.obs_source_get_width(source)
scene_height = obs.obs_source_get_height(source)
obs.obs_source_release(source)
Movement code
Before I started, I thought I’d have to figure out the maths for velocity and bounce angles, thankfully it turns out you don’t need to for linear movement.
For each frame it’s rendering, OBS will call a script_tick() function provided by our script.
On each tick, we need to look at the current position of the scene item’s box, determine if there’s room for it to move in its current direction and move it by a number of pixels (its speed) in that direction.
In the following code, direction is controlled by a pair of booleans: moving_right for horizontal direction (true = right, false = left) and moving_down for vertical direction (true = down, false = up):
local speed =10local next_pos = obs.vec2()-- calculate the next horizontal positionif moving_right and pos.x + width < scene_width then
next_pos.x = math.min(pos.x + speed, scene_width - width)else
moving_right =false
next_pos.x = math.max(pos.x - speed,0)if next_pos.x ==0then moving_right =trueendend-- calculate the next vertical positionif moving_down and pos.y + height < scene_height then
next_pos.y = math.min(pos.y + speed, scene_height - height)else
moving_down =false
next_pos.y = math.max(pos.y - speed,0)if next_pos.y ==0then moving_down =trueendend-- move the scene item to its new position
obs.obs_sceneitem_set_pos(scene_item, next_pos)
Here’s our initial movement code in action:

Resized scene items
The logo we’re bouncing around is quite big, so it doesn’t have much room to move, which makes the bouncing effect a bit jarring.
What happens if we resize it in OBS?

Our script is still getting the original size of the source, so it’s bouncing in the same pattern.
When you resize a scene item in OBS, it applies a scaling factor, which we need to account for when determining the scene item’s dimensions:
local scale = obs.vec2()
obs.obs_sceneitem_get_scale(scene_item, scale)local width =round((obs.obs_source_get_width(source)* scale.x)local height =round((obs.obs_source_get_height(source)* scale.y)

Cropped scene items
Our resized logo has a chunk of transparent space around it, so it still looks like it’s bouncing around the middle of the scene.
OBS also allows you to crop scene items, so we can crop out the extra space instead of creating a separate version of the logo:


Once again, we’ll need to account for the cropped area when determining the scene item’s dimensions. Cropping is done against the source’s original size, so we’ll need to subtract the cropped area before accounting for scaling:
local crop = obs.obs_sceneitem_crop()local crop = obs.obs_sceneitem_get_crop(scene_item, crop)local width =round((obs.obs_source_get_width(source)- crop.left - crop.right)* scale.x)local height =round((obs.obs_source_get_height(source)- crop.top - crop.bottom)* scale.y)

Final script
The full source for the final script is here:
https://github.com/insin/obs-bounce/blob/master/bounce.lua
Extra details we haven’t gone into are left as an exercise for the reader:
Setting up configuration UI, with a dropdown list of source names
Toggling starting and stopping with a button or an OBS hotkey
Returning the source item to its original position when stopped
Addendum 1: TIL OBS Scripting API
C-style APIs are a bit of a PITA
Having to create an object then pass it into a function call to populate it is just noise when you’re writing and reading code, so I ended up writing my own wrappers for these:
--- convenience wrapper for getting a scene item's pos in a single statementfunctionget_scene_item_pos(scene_item)local pos = obs.vec2()
obs.obs_sceneitem_get_pos(scene_item, pos)return pos
end
I also found an API function which needs a wrapper for scripting compatibility but doesn’t have one.
But do pretend you’re writing C!
The following warning in the documentation is not to be trifled with:
Please treat the API bindings as though you were writing a C program: read the documentation for functions you use, and release/destroy objects you reference or create via the API.
After using obs_frontend_get_current_scene() to get a reference to the source object for the current scene, an earlier version of the script didn’t release its reference with obs_source_release(source), which caused OBS to crash on exit with a very… not-helpful error message.
As you’re writing a script, you need to pay attention to the API documentation for each function you’re using - as you’ve seen, function names are not always your friend.
If the docs say a function returns “a new reference” or explicitly point you to the the relevant release function, you’ll need to release the reference when you’re finished with it.
If the docs say “does not increment the reference”, you’re good.
Perhaps adopting a variable naming convention such as a _ref suffix for anything which needs to be released later would help?
Addendum 2: TIL Lua
I didn’t have to do anything which required me to get deep into Lua when writing this script - I only bumped into intriguing terms like “metamethods” when browsing the docs - so these are pretty limited.
Lua has a dedicated string concatenation operator
I can’t remember having used one of these before.
print('width='.. width ..', height='.. height)
Lua doesn’t have an array type
Lua has a table type which is an associative array. Instead of a dedicated array type (like JavaScript’s Array, which is effectively managing an Object with numeric keys under the hood), you use a table with numeric keys and table manipulation functions which do the right thing with numeric keys:
local names ={}
table.insert(names,'Lynn Benfield')
table.insert(names,'Alan Partridge')
table.sort(names,function(a, b)return string.lower(a)< string.lower(b)end)
Lua doesn’t provide a math.round() function
The round() function used in the examples above is our own:
--- round a number to the nearest integerfunctionround(n)return math.floor(n +0.5)end
I didn’t mind the end syntax
if, for, function etc. statements must end with end. Using a 3-space indent ties in with end in a visually pleasing way.
I didn’t like these much in Ruby, turns out I just didn’t like Ruby 😜

A blog about programming, web dev and whatnot by Jonny Buchanan.