marketplace/streaming

OBS Stream Deck Controller

Hotkey control for OBS scenes, audio mute, streaming, recording, and camera via built-in OBS WebSocket.

@rebindv0.1.1free

Other ways to install
$rebind install @rebind/obs-scene-switcher

Gallery

OBS Stream Deck Controller preview

Readme

Turn any keyboard or mouse into a dedicated OBS Stream Deck. Switch scenes, mute audio inputs, toggle streaming/recording, save replay buffers, and toggle virtual camera directly through OBS Studio's built-in WebSocket server.

Features

  • Direct OBS WebSocket v5 Connection: Connects directly to OBS Studio without any third-party plugins, intermediate relays, or external Python/Node scripts.
  • Scene Switching: Dedicated hotkeys for up to 5 custom scene names.
  • Audio Controls: One-touch microphone mute/unmute.
  • Output Toggles: Start/stop streaming and recording, or trigger instant replay buffer saves.
  • Virtual Camera: Quick toggle for OBS Virtual Camera.
  • Authentication Support: Works seamlessly whether password protection is enabled or disabled.
  • Auto-Reconnect: Automatically reconnects if OBS is launched or restarted while the script is active.
  • Audio Feedback: Optional chime confirmation on completed commands.
  • Configurable UI: Adjust all hotkeys, scene names, source names, and connection settings directly in Rebind's Options tab without touching code.

Setup in OBS Studio

  1. Open OBS Studio (v28.0 or newer).
  2. Go to Tools → WebSocket Server Settings.
  3. Check Enable WebSocket server.
  4. Note the Server Port (default is 4455).
  5. (Optional) If you enable authentication, copy the password and paste it into the Server Password field in Rebind's Options tab.
  6. Click Apply and OK.

Default Hotkeys

ActionDefault KeyConfig Tab
Scene AMouse4Scenes
Scene BMouse5Scenes
Scene CF1Scenes
Scene DF2Scenes
Scene EF3Scenes
Toggle Mic MuteF9Controls
Start StreamF5Controls
Stop StreamF6Controls
Toggle StreamF10Controls
Toggle RecordF11Controls
Save Replay BufferF8Controls
Toggle Virtual CameraF7Controls

All keybindings and scene names can be customized under the script's Options tab.

Reviews

No reviews yet. Be the first, from the app.

Source

main.luau

--[[
  rebind: min_sdk=3.0.0
]]

local cfg = UI.Schema({
  -- Tab: Scenes
  scene_a_btn = UI.Keybind("Mouse4", { label = "Scene A Key", tab = "Scenes" }),
  scene_a_name = UI.Text("Starting Soon", { label = "Scene A Name", tab = "Scenes" }),

  scene_b_btn = UI.Keybind("Mouse5", { label = "Scene B Key", tab = "Scenes" }),
  scene_b_name = UI.Text("Gameplay", { label = "Scene B Name", tab = "Scenes" }),

  scene_c_btn = UI.Keybind("F1", { label = "Scene C Key", tab = "Scenes" }),
  scene_c_name = UI.Text("Just Chatting", { label = "Scene C Name", tab = "Scenes" }),

  scene_d_btn = UI.Keybind("F2", { label = "Scene D Key", tab = "Scenes" }),
  scene_d_name = UI.Text("BRB", { label = "Scene D Name", tab = "Scenes" }),

  scene_e_btn = UI.Keybind("F3", { label = "Scene E Key", tab = "Scenes" }),
  scene_e_name = UI.Text("Ending", { label = "Scene E Name", tab = "Scenes" }),

  -- Tab: Controls
  mute_btn = UI.Keybind("F9", { label = "Toggle Mic Mute Key", group = "Audio", tab = "Controls" }),
  mic_source = UI.Text("Mic/Aux", { label = "Mic Source Name", group = "Audio", tab = "Controls" }),

  stream_start_btn = UI.Keybind("F5", { label = "Start Stream Key", group = "Stream", tab = "Controls" }),
  stream_stop_btn = UI.Keybind("F6", { label = "Stop Stream Key", group = "Stream", tab = "Controls" }),
  stream_toggle_btn = UI.Keybind("F10", { label = "Toggle Stream Key", group = "Stream", tab = "Controls" }),
  record_toggle_btn = UI.Keybind("F11", { label = "Toggle Record Key", group = "Output", tab = "Controls" }),
  replay_save_btn = UI.Keybind("F8", { label = "Save Replay Buffer Key", group = "Output", tab = "Controls" }),

  cam_toggle_btn = UI.Keybind("F7", { label = "Toggle Virtual Cam Key", group = "Camera", tab = "Controls" }),

  -- Tab: Connection
  ws_url = UI.Text("ws://127.0.0.1:4455", { label = "OBS WebSocket URL", tab = "Connection" }),
  password = UI.Text("", { label = "Server Password (Optional)", tab = "Connection" }),
  beep = UI.Toggle(true, { label = "Audio Feedback on Actions", tab = "Connection" }),
})

local ws_conn = nil
local is_ready = false
local reconnect_timer = nil
local req_counter = 0

local function play_feedback()
  if cfg.beep then
    Audio.Beep()
  end
end

local function connect()
  if ws_conn then
    pcall(function() ws_conn:Close() end)
    ws_conn = nil
  end
  is_ready = false

  local conn = nil
  local ok, err = pcall(function()
    conn = Net.WSConnect(cfg.ws_url, {
      OnOpen = function()
        Log.Info("Connecting to OBS WebSocket at " .. cfg.ws_url)
      end,

      OnMessage = function(payload, is_binary)
        if is_binary then return end
        local msg = JSON.Parse(payload)
        if not msg or type(msg.op) ~= "number" then return end

        local client = conn or ws_conn
        if not client then return end

        if msg.op == 0 then
          -- OpCode 0: Hello from OBS
          local d = msg.d or {}
          local auth_data = d.authentication
          local identify_payload = {
            rpcVersion = 1,
          }

          if auth_data and auth_data.challenge and auth_data.salt then
            local pw = cfg.password or ""
            local secret = Codec.Base64(Codec.HexDecode(Hash.SHA256(pw .. auth_data.salt)))
            local auth_str = Codec.Base64(Codec.HexDecode(Hash.SHA256(secret .. auth_data.challenge)))
            identify_payload.authentication = auth_str
          end

          local send_ok, send_err = pcall(function()
            client:Send(JSON.Stringify({
              op = 1,
              d = identify_payload,
            }))
          end)
          if not send_ok then
            Log.Error("Failed to send Identify to OBS: " .. tostring(send_err))
          end
        elseif msg.op == 2 then
          -- OpCode 2: Identified (ready to receive commands)
          is_ready = true
          Log.Info("OBS WebSocket authenticated and ready")
          UI.Notify("Connected to OBS", "success")
        elseif msg.op == 7 then
          -- OpCode 7: RequestResponse
          local d = msg.d or {}
          local status = d.requestStatus or {}
          if status.result then
            play_feedback()
            UI.Notify("OBS: " .. (d.requestType or "Command") .. " OK", "info")
          else
            UI.Notify("OBS Error: " .. (status.comment or "Request failed"), "warning")
          end
        end
      end,

      OnClose = function()
        if is_ready then
          Log.Warn("OBS WebSocket disconnected")
          UI.Notify("OBS Disconnected", "warning")
        else
          Log.Warn("OBS WebSocket closed before identification")
        end
        is_ready = false
        ws_conn = nil
      end,
    })
  end)

  if ok and conn then
    ws_conn = conn
  else
    ws_conn = nil
    is_ready = false
    if not ok then
      Log.Error("Failed to connect to OBS WebSocket: " .. tostring(err))
    end
  end
end

local function send_command(request_type, request_data)
  if not ws_conn or not is_ready then
    connect()
    UI.Notify("Connecting to OBS... Try again in a moment.", "warning")
    return
  end

  req_counter = req_counter + 1
  local msg = {
    op = 6,
    d = {
      requestType = request_type,
      requestId = tostring(req_counter),
      requestData = request_data,
    },
  }

  local ok = pcall(function()
    ws_conn:Send(JSON.Stringify(msg))
  end)

  if not ok then
    UI.Notify("Failed to send command to OBS", "error")
  end
end

function OnStart()
  Log.Info("OBS Stream Deck Controller starting")
  connect()

  -- Check connection health and auto-reconnect if OBS starts or restarts
  reconnect_timer = Timer.Every(5000, function()
    if not ws_conn or not is_ready then
      connect()
    end
  end)
end

function OnStop()
  if reconnect_timer then
    reconnect_timer:Cancel()
    reconnect_timer = nil
  end

  if ws_conn then
    pcall(function() ws_conn:Close() end)
    ws_conn = nil
  end

  is_ready = false
  Audio.StopAll()
end

-- Scenes A through E
Bind(cfg.scene_a_btn, function()
  send_command("SetCurrentProgramScene", { sceneName = cfg.scene_a_name })
end)

Bind(cfg.scene_b_btn, function()
  send_command("SetCurrentProgramScene", { sceneName = cfg.scene_b_name })
end)

Bind(cfg.scene_c_btn, function()
  send_command("SetCurrentProgramScene", { sceneName = cfg.scene_c_name })
end)

Bind(cfg.scene_d_btn, function()
  send_command("SetCurrentProgramScene", { sceneName = cfg.scene_d_name })
end)

Bind(cfg.scene_e_btn, function()
  send_command("SetCurrentProgramScene", { sceneName = cfg.scene_e_name })
end)

-- Audio Control
Bind(cfg.mute_btn, function()
  send_command("ToggleInputMute", { inputName = cfg.mic_source })
end)

-- Stream & Recording
Bind(cfg.stream_start_btn, function()
  send_command("StartStream")
end)

Bind(cfg.stream_stop_btn, function()
  send_command("StopStream")
end)

Bind(cfg.stream_toggle_btn, function()
  send_command("ToggleStream")
end)

Bind(cfg.record_toggle_btn, function()
  send_command("ToggleRecord")
end)

Bind(cfg.replay_save_btn, function()
  send_command("SaveReplayBuffer")
end)

-- Virtual Camera Toggle
Bind(cfg.cam_toggle_btn, function()
  send_command("ToggleVirtualCam")
end)