Global Hotkey Registration and Handling with obs_hotkey Functions in OBS Studio

OBS Studio implements a centralized hotkey subsystem in libobs/obs-hotkey.c that enables global hotkey registration via obs_hotkey_register_frontend, persists bindings to JSON, and processes input through a dedicated 40 Hz polling thread.

The obsproject/obs-studio repository provides a robust global hotkey framework that allows both the core application and third-party plugins to register system-wide keyboard shortcuts. Understanding how to leverage the obs_hotkey API is essential for developers extending OBS Studio's functionality.

How the OBS Hotkey Subsystem Works

The architecture divides responsibilities into three distinct layers: registration, binding persistence, and runtime handling. The public API surface is declared in libobs/obs-hotkey.h, while the core implementation resides in libobs/obs-hotkey.c.

Layer Responsibility Key API Core Implementation
Registration Create a hotkey object and associate it with a registerer (frontend, encoder, source, etc.) obs_hotkey_register_frontend, obs_hotkey_register_source obs_hotkey_register_internal in libobs/obs-hotkey.c (lines 189-206)
Binding Persistence Load and save key-combination bindings to OBS-style JSON obs_hotkey_load_bindings, obs_hotkey_save Loading helpers in obs-hotkey.c (lines 88-100)
Runtime Handling Detect key presses, match against registered bindings, and invoke callbacks obs_hotkey_inject_event, obs_hotkey_thread Polling thread and dispatch helpers in obs-hotkey.c (lines 1555-1580)

Registering Global Hotkeys with obs_hotkey_register_frontend

The Registration API

To expose a global shortcut to users, call obs_hotkey_register_frontend with a unique identifier, human-readable description, and callback function:

obs_hotkey_id id = obs_hotkey_register_frontend(
        "OBSBasic.Screenshot",               // internal identifier
        "Take Screenshot",                   // description shown in UI
        screenshot_cb,                       // callback invoked on press/release
        nullptr);                            // user data passed to callback

The returned obs_hotkey_id serves as a handle for subsequent operations such as loading bindings or unregistering the hotkey.

Internal Implementation in obs-hotkey.c

Under the hood, obs_hotkey_register_frontend forwards to obs_hotkey_register_internal with the OBS_HOTKEY_REGISTERER_FRONTEND type. This function, located at lines 189-206 in libobs/obs-hotkey.c, allocates a new obs_hotkey_t structure, populates it with the provided metadata, and inserts it into the global hash table obs->hotkeys.hotkeys. If saved bindings exist in the current profile context, they are automatically loaded and associated with the new hotkey instance.

Persisting Hotkey Bindings to JSON

Loading Bindings with obs_hotkey_load_bindings

The UI widget OBSHotkeyWidget calls the atomic update helper to replace bindings safely:

obs_hotkey_load_bindings(id,
    combinations.data(), combinations.size());

This function acquires the global hotkey lock, removes existing bindings via remove_bindings, creates new obs_hotkey_binding_t entries through create_binding, and emits a hotkey_bindings_changed signal. The implementation resides in libobs/obs-hotkey.c around lines 88-112.

Saving Configuration with obs_hotkey_save

Saving operates in reverse: obs_hotkey_save iterates over all bindings, builds a JSON array via save_bindings_helper, and returns the serialized data to the caller (lines 290-314 in obs-hotkey.c). The frontend then writes this JSON to the user’s profile configuration.

Runtime Handling and the Hotkey Thread

The 40Hz Polling Loop

OBS runs a dedicated hotkey thread that polls the physical keyboard state at approximately 40 Hz (25 ms intervals) and dispatches events to registered hotkeys:

void *obs_hotkey_thread(void *arg)
{
    while (os_event_timedwait(obs->hotkeys.stop_event, 25) == ETIMEDOUT) {
        if (!lock())
            continue;

        query_hotkeys();          // check current modifier state + pressed keys
        unlock();
    }
    return NULL;
}

The query_hotkeys function builds a modifier mask (INTERACT_SHIFT_KEY, etc.), enumerates every binding via enum_bindings, and calls handle_binding. This handler decides whether to fire (press_released_binding) or release (release_pressed_binding) based on current key state and strict modifier settings (lines 1555-1580 in obs-hotkey.c).

Event Injection with obs_hotkey_inject_event

Plugins can inject synthetic hotkey events programmatically—useful for remote controllers or automation:

obs_hotkey_inject_event(combination, pressed);

This walks all bindings and forces them into the pressed state if the supplied combination matches (lines 969-987 in obs-hotkey.c).

Advanced Callback Routing

OBS can reroute all hotkey callbacks through a central router function, enabling scripts to intercept hotkeys globally:

obs_hotkey_set_callback_routing_func(router_cb, nullptr);
obs_hotkey_enable_callback_rerouting(true);

All callbacks now forward to router_cb instead of their original handlers. The router receives the hotkey ID and pressed state, allowing the caller to decide whether to forward to the original callback via obs_hotkey_trigger_routed_callback (lines 1262-1270 in obs-hotkey.c).

Complete Code Example: Registering a Screenshot Hotkey

The following pattern demonstrates registering a frontend hotkey that triggers an existing Qt action:

/*-------------------------------------------------------------
 * 1. Define the callback that will be executed on press/release
 *------------------------------------------------------------*/
static void screenshot_cb(void *data, obs_hotkey_id id,
                         obs_hotkey_t *hotkey, bool pressed)
{
    if (!pressed)          // act only on key-down
        return;

    /* Take a screenshot – same as the menu action */
    QAction *act = (QAction *)data;
    QMetaObject::invokeMethod(act, "trigger", Qt::QueuedConnection);
}

/*-------------------------------------------------------------
 * 2. Register the hotkey during application start-up
 *------------------------------------------------------------*/
void register_screenshot_hotkey(void)
{
    QAction *screenshotAction = /* obtain the existing QAction */;
    obs_hotkey_id hk = obs_hotkey_register_frontend(
            "OBSBasic.Screenshot",               // internal name
            "Take Screenshot",                   // description shown in UI
            screenshot_cb,                       // callback
            screenshotAction);                  // user data (Qt action)

    /* Optional: expose the hotkey ID to the UI so it can be edited */
    hotkey_widget->SetId(hk);
}

This implementation creates a persistent global shortcut that appears in the OBS Settings → Hotkeys panel, stores its bindings in the user profile, and executes the screenshot action when triggered.

Key Source Files for Reference

Path Role
libobs/obs-hotkey.h Public declarations (obs_hotkey_id, obs_key_combination_t, registration APIs).
libobs/obs-hotkey.c Core implementation: registration, binding storage, hotkey thread, injection, routing.
frontend/widgets/OBSBasic_Hotkeys.cpp Example of frontend hotkey registration for built-in actions (e.g., screenshot, transition).
frontend/settings/OBSHotkeyWidget.cpp UI widget that lets the user view/edit bindings; uses obs_hotkey_load_bindings & obs_hotkey_update_atomic.
frontend/settings/OBSHotkeyEdit.cpp Handles individual key-combination editing, duplicate detection, and UI feedback.

Summary

  • Registration: Use obs_hotkey_register_frontend (or source/encoder variants) to create hotkeys stored in the global obs->hotkeys.hotkeys hash table inside libobs/obs-hotkey.c.
  • Persistence: Bindings are serialized to JSON via obs_hotkey_save and restored via obs_hotkey_load_bindings, enabling configuration across OBS sessions.
  • Runtime: A dedicated thread (obs_hotkey_thread) polls the keyboard at 40 Hz, invoking query_hotkeys and handle_binding to match pressed keys against registered combinations.
  • Injection: Synthetic events can be triggered programmatically using obs_hotkey_inject_event for automation or remote control scenarios.
  • Routing: Advanced use cases can intercept all hotkey traffic via obs_hotkey_set_callback_routing_func to implement custom filtering or scripting layers.

Frequently Asked Questions

How do I register a global hotkey for a frontend action in OBS Studio?

Call obs_hotkey_register_frontend with a unique identifier, human-readable description, callback function, and optional user data. This function, defined in libobs/obs-hotkey.c, allocates an obs_hotkey_t structure and inserts it into the global registry, making the shortcut available in Settings → Hotkeys.

What is the difference between obs_hotkey_register_frontend and obs_hotkey_register_source?

obs_hotkey_register_frontend creates hotkeys associated with the main application UI (stored with OBS_HOTKEY_REGISTERER_FRONTEND), while obs_hotkey_register_source attaches hotkeys to specific OBS sources (scenes, audio inputs, etc.). Both ultimately call obs_hotkey_register_internal but tag the hotkey with different registerer types for organizational and cleanup purposes.

How are hotkey bindings saved and loaded across OBS Studio sessions?

Bindings persist through obs_hotkey_save, which serializes active combinations to JSON, and obs_hotkey_load_bindings, which restores them from the profile configuration. The UI layer in frontend/settings/OBSHotkeyWidget.cpp invokes these functions when the user modifies key combinations in the settings dialog, ensuring changes survive application restarts.

Can plugins inject synthetic hotkey events programmatically?

Yes, plugins can call obs_hotkey_inject_event to simulate key presses without physical keyboard input. This function iterates through all registered bindings and forces matching combinations into the pressed state, enabling automation, remote control integrations, or scripted testing of hotkey callbacks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →