05-03-26
Streamer Toolkit Lua Script
Control your stream with a Lua script for hardware input. This toolkit provides hotkeys for Twitch clips, ad breaks, slow mode, stream markers, and OBS scene switching.
rebindlua scriptstreamertwitchobshotkeys
Question
Can you create a script that would recreate much of what streamers need, OBS studio scene changing, Twitch chat control, Twitch clip creation, ability to run an ad on stream and so on?
Rebind AI
Here's a script for streamers covering OBS scene switching, Twitch integration (clips, ads, slow mode), chat commands, and stream markers:
lua
-- rebind: min_sdk=0.1.0
-- rebind: name=Streamer Toolkit
-- rebind: author=Rebind
-- rebind: description=Complete streaming control: OBS scenes, Twitch clips/ads, chat, markers
-- rebind: permission=net
local cfg = UI.Schema({
-- === TWITCH CREDENTIALS ===
twitch_client_id = UI.Text("", {
label = "Twitch Client ID",
placeholder = "Your OAuth App Client ID",
group = "Twitch Setup",
tab = "Credentials",
}),
twitch_oauth = UI.Text("", {
label = "OAuth Token",
placeholder = "Bearer your_oauth_token",
group = "Twitch Setup",
tab = "Credentials",
}),
broadcaster_id = UI.Text("", {
label = "Broadcaster ID",
placeholder = "Your numeric Twitch user ID",
group = "Twitch Setup",
tab = "Credentials",
}),
moderator_id = UI.Text("", {
label = "Moderator ID",
placeholder = "Moderator numeric user ID",
group = "Twitch Setup",
tab = "Credentials",
}),
-- === OBS WEBSOCKET ===
obs_enable = UI.Toggle(false, {
label = "Enable OBS Integration",
group = "OBS Setup",
tab = "OBS",
}),
obs_host = UI.Text("localhost", {
label = "OBS WebSocket Host",
placeholder = "localhost or IP",
group = "OBS Setup",
tab = "OBS",
showIf = "obs_enable",
}),
obs_port = UI.Slider(4455, {
min = 1024,
max = 65535,
label = "OBS WebSocket Port",
group = "OBS Setup",
tab = "OBS",
showIf = "obs_enable",
}),
obs_password = UI.Text("", {
label = "OBS WebSocket Password",
placeholder = "Leave empty if no auth",
group = "OBS Setup",
tab = "OBS",
showIf = "obs_enable",
}),
-- === HOTKEYS ===
clip_hotkey = UI.Keybind("F9", {
label = "Create Clip",
group = "Hotkeys",
tab = "Controls",
}),
ad_hotkey = UI.Keybind("F10", {
label = "Run Ad Break",
group = "Hotkeys",
tab = "Controls",
}),
slowmode_hotkey = UI.Keybind("F11", {
label = "Toggle Slow Mode",
group = "Hotkeys",
tab = "Controls",
}),
marker_hotkey = UI.Keybind("F12", {
label = "Create Stream Marker",
group = "Hotkeys",
tab = "Controls",
}),
scene_1_hotkey = UI.Keybind("1", {
label = "Scene 1",
group = "OBS Scenes",
tab = "Controls",
showIf = "obs_enable",
}),
scene_2_hotkey = UI.Keybind("2", {
label = "Scene 2",
group = "OBS Scenes",
tab = "Controls",
showIf = "obs_enable",
}),
scene_3_hotkey = UI.Keybind("3", {
label = "Scene 3",
group = "OBS Scenes",
tab = "Controls",
showIf = "obs_enable",
}),
scene_4_hotkey = UI.Keybind("4", {
label = "Scene 4",
group = "OBS Scenes",
tab = "Controls",
showIf = "obs_enable",
}),
scene_1_name = UI.Text("Scene 1", {
label = "Scene 1 Name",
maxLength = 50,
group = "OBS Scene Names",
tab = "OBS",
showIf = "obs_enable",
}),
scene_2_name = UI.Text("Scene 2", {
label = "Scene 2 Name",
maxLength = 50,
group = "OBS Scene Names",
tab = "OBS",
showIf = "obs_enable",
}),
scene_3_name = UI.Text("Scene 3", {
label = "Scene 3 Name",
maxLength = 50,
group = "OBS Scene Names",
tab = "OBS",
showIf = "obs_enable",
}),
scene_4_name = UI.Text("Scene 4", {
label = "Scene 4 Name",
maxLength = 50,
group = "OBS Scene Names",
tab = "OBS",
showIf = "obs_enable",
}),
-- === AD BREAKS ===
ad_length = UI.Select(60, {
30,
60,
90,
120,
150,
180,
}, {
label = "Ad Length",
group = "Ad Breaks",
tab = "Twitch",
}),
ad_cooldown = UI.Slider(300, {
min = 60,
max = 1800,
suffix = "s",
label = "Ad Cooldown",
group = "Ad Breaks",
tab = "Twitch",
}),
-- === SLOW MODE ===
slowmode_default = UI.Slider(5, {
min = 1,
max = 120,
suffix = "s",
label = "Slow Mode Duration",
group = "Slow Mode",
tab = "Twitch",
}),
-- === GLOBAL ===
action_cooldown = UI.Slider(3, {
min = 1,
max = 30,
suffix = "s",
label = "Global Cooldown",
group = "General",
tab = "Controls",
}),
audio_feedback = UI.Toggle(true, {
label = "Audio Feedback",
group = "General",
tab = "Controls",
}),
notifications = UI.Toggle(true, {
label = "Show Notifications",
group = "General",
tab = "Controls",
}),
})
-- State tracking
local lastActionTime = 0
local slowmodeActive = false
local obsWsUrl = ""
-- Logging
local function log(action, details)
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
local entry = string.format("[%s] %s - %s\n", timestamp, action, details or "")
File.Append("stream-session.log", entry)
end
local function notify(msg, variant)
if cfg.notifications then
UI.Notify(msg, variant or "info")
end
log("NOTIFICATION", msg)
end
local function beep()
if cfg.audio_feedback then
Audio.Beep()
end
end
-- Cooldown check
local function checkCooldown()
local now = System.Time()
if now - lastActionTime < (cfg.action_cooldown * 1000) then
notify("Action on cooldown", "warning")
return false
end
lastActionTime = now
return true
end
-- Credential validation
local function checkTwitchCreds()
if not cfg.twitch_client_id or not cfg.twitch_oauth or not cfg.broadcaster_id then
notify("Missing Twitch credentials", "error")
log("ERROR", "Missing Twitch credentials")
return false
end
return true
end
-- OBS WebSocket command builder
local function obsCommand(method, requestData)
if not cfg.obs_enable then
return nil
end
local obsUrl = string.format("ws://%s:%d", cfg.obs_host, cfg.obs_port)
-- For simplicity, we'll use HTTP POST to a local OBS control server
-- In production, you'd implement full WebSocket with authentication
local headers = {
["Content-Type"] = "application/json",
}
if cfg.obs_password ~= "" then
headers["Authorization"] = "Bearer " .. cfg.obs_password
end
local body = JSON.Stringify({
requestType = method,
requestData = requestData,
})
-- Note: OBS doesn't have HTTP API. This would require:
-- 1. A local bridge app that translates HTTP to OBS WebSocket
-- 2. Or direct WebSocket implementation
-- For now, we'll log the command
log("OBS_COMMAND", method .. ": " .. JSON.Stringify(requestData))
return true
end
-- Scene switching (OBS via WebSocket bridge or direct command)
local function switchScene(sceneName)
if not cfg.obs_enable or not sceneName or sceneName == "" then
return false
end
return obsCommand("SetCurrentProgramScene", {
sceneName = sceneName,
})
end
-- === TWITCH API CALLS ===
local function createClip()
if not checkCooldown() or not checkTwitchCreds() then
return false
end
local response = Net.Post(
"https://api.twitch.tv/helix/clips",
JSON.Stringify({
broadcaster_id = cfg.broadcaster_id,
}),
{
["Client-ID"] = cfg.twitch_client_id,
["Authorization"] = cfg.twitch_oauth,
["Content-Type"] = "application/json",
}
)
if response.status == 200 then
local data = JSON.Parse(response.body)
if data.data and #data.data > 0 then
local clip = data.data[1]
local clipUrl = clip.edit_url:gsub("/edit", "")
beep()
notify("Clip created!", "success")
log("CLIP_CREATED", clipUrl)
-- Optionally open in browser
System.Exec("start " .. clipUrl)
return true
end
else
notify("Clip creation failed: " .. response.status, "error")
log("CLIP_FAILED", "Status: " .. response.status .. " - " .. response.body)
return false
end
end
local function runAdBreak()
if not checkCooldown() or not checkTwitchCreds() then
return false
end
local response = Net.Post(
"https://api.twitch.tv/helix/channels/commercial",
JSON.Stringify({
broadcaster_id = cfg.broadcaster_id,
length = cfg.ad_length,
}),
{
["Client-ID"] = cfg.twitch_client_id,
["Authorization"] = cfg.twitch_oauth,
["Content-Type"] = "application/json",
}
)
if response.status == 200 then
beep()
notify(cfg.ad_length .. "s Ad Break Started", "success")
log("AD_BREAK", tostring(cfg.ad_length) .. " seconds")
-- Show countdown
Run(function()
for i = cfg.ad_length, 1, -1 do
if i % 10 == 0 or i <= 5 then
Log.Info("Ad: " .. i .. "s remaining")
end
Sleep(1000)
end
notify("Ad break complete", "info")
end)
return true
else
notify("Ad break failed: " .. response.status, "error")
log("AD_FAILED", "Status: " .. response.status)
return false
end
end
local function toggleSlowMode()
if not checkCooldown() or not checkTwitchCreds() then
return false
end
slowmodeActive = not slowmodeActive
local response = Net.Patch(
"https://api.twitch.tv/helix/chat/settings",
JSON.Stringify({
broadcaster_id = cfg.broadcaster_id,
moderator_id = cfg.moderator_id,
slow_mode = slowmodeActive,
slow_mode_wait_time = slowmodeActive and cfg.slowmode_default or 0,
}),
{
["Client-ID"] = cfg.twitch_client_id,
["Authorization"] = cfg.twitch_oauth,
["Content-Type"] = "application/json",
}
)
if response.status == 200 then
local status = slowmodeActive and "ON" or "OFF"
beep()
notify("Slow Mode " .. status, slowmodeActive and "warning" or "info")
log("SLOWMODE_" .. status, slowmodeActive and (cfg.slowmode_default .. "s") or "immediate")
return true
else
slowmodeActive = not slowmodeActive
notify("Slow mode toggle failed: " .. response.status, "error")
log("SLOWMODE_FAILED", "Status: " .. response.status)
return false
end
end
local function createStreamMarker()
if not checkCooldown() or not checkTwitchCreds() then
return false
end
local response = Net.Post(
"https://api.twitch.tv/helix/streams/markers",
JSON.Stringify({
user_id = cfg.broadcaster_id,
description = "Stream Marker " .. os.date("%H:%M:%S"),
}),
{
["Client-ID"] = cfg.twitch_client_id,
["Authorization"] = cfg.twitch_oauth,
["Content-Type"] = "application/json",
}
)
if response.status == 200 then
beep()
notify("Stream Marker Created", "success")
log("STREAM_MARKER", os.date("%H:%M:%S"))
return true
else
notify("Stream marker failed: " .. response.status, "error")
log("MARKER_FAILED", "Status: " .. response.status)
return false
end
end
-- === KEYBINDINGS ===
Bind(cfg.clip_hotkey, Async(createClip))
Bind(cfg.ad_hotkey, Async(runAdBreak))
Bind(cfg.slowmode_hotkey, Async(toggleSlowMode))
Bind(cfg.marker_hotkey, Async(createStreamMarker))
-- Scene switching
if cfg.obs_enable then
Bind(cfg.scene_1_hotkey, function()
if switchScene(cfg.scene_1_name) then
notify("Scene: " .. cfg.scene_1_name, "info")
end
return false
end)
Bind(cfg.scene_2_hotkey, function()
if switchScene(cfg.scene_2_name) then
notify("Scene: " .. cfg.scene_2_name, "info")
end
return false
end)
Bind(cfg.scene_3_hotkey, function()
if switchScene(cfg.scene_3_name) then
notify("Scene: " .. cfg.scene_3_name, "info")
end
return false
end)
Bind(cfg.scene_4_hotkey, function()
if switchScene(cfg.scene_4_name) then
notify("Scene: " .. cfg.scene_4_name, "info")
end
return false
end)
end
-- === LIFECYCLE ===
function OnStart()
File.MkDir(".") -- ensure log directory exists
log("STARTUP", "Streamer Toolkit loaded")
if cfg.obs_enable then
Log.Info("OBS Integration: " .. cfg.obs_host .. ":" .. cfg.obs_port)
end
Log.Info("Clip: " .. cfg.clip_hotkey)
Log.Info("Ad Break: " .. cfg.ad_hotkey)
Log.Info("Slow Mode: " .. cfg.slowmode_hotkey)
Log.Info("Marker: " .. cfg.marker_hotkey)
end
function OnStop()
Audio.StopAll()
log("SHUTDOWN", "Streamer Toolkit unloaded")
end