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