1
0
Эх сурвалжийг харах

libobs: Add global hotkey support

Palana 11 жил өмнө
parent
commit
5ad553d06d

+ 4 - 0
libobs/CMakeLists.txt

@@ -216,6 +216,8 @@ set(libobs_libobs_SOURCES
 	obs.c
 	obs-properties.c
 	obs-data.c
+	obs-hotkey.c
+	obs-hotkey-name-map.c
 	obs-module.c
 	obs-display.c
 	obs-view.c
@@ -233,6 +235,8 @@ set(libobs_libobs_HEADERS
 	obs-properties.h
 	obs-data.h
 	obs-interaction.h
+	obs-hotkey.h
+	obs-hotkeys.h
 	obs-module.h
 	obs-scene.h
 	obs-source.h

+ 46 - 0
libobs/obs-cocoa.c

@@ -182,3 +182,49 @@ void log_system_info(void)
 	log_os();
 	log_kernel_version();
 }
+
+
+
+
+struct obs_hotkeys_platform {
+};
+
+void obs_key_to_str(obs_key_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+void obs_key_combination_to_str(obs_key_combination_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+obs_key_t obs_key_from_virtual_key(int code)
+{
+	UNUSED_PARAMETER(code);
+
+	return OBS_KEY_NONE;
+}
+
+bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+
+	return true;
+}
+
+void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+}
+
+bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *plat,
+		obs_key_t key)
+{
+	UNUSED_PARAMETER(plat);
+	UNUSED_PARAMETER(key);
+
+	return false;
+}

+ 1 - 1
libobs/obs-encoder.c

@@ -42,7 +42,7 @@ static bool init_encoder(struct obs_encoder *encoder, const char *name,
 	pthread_mutex_init_value(&encoder->callbacks_mutex);
 	pthread_mutex_init_value(&encoder->outputs_mutex);
 
-	if (!obs_context_data_init(&encoder->context, settings, name))
+	if (!obs_context_data_init(&encoder->context, settings, name, NULL))
 		return false;
 	if (pthread_mutex_init(&encoder->callbacks_mutex, NULL) != 0)
 		return false;

+ 411 - 0
libobs/obs-hotkey-name-map.c

@@ -0,0 +1,411 @@
+/******************************************************************************
+    Copyright (C) 2014 by Ruwen Hahn <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include <string.h>
+#include <assert.h>
+
+#include <util/bmem.h>
+#include <util/c99defs.h>
+#include <util/darray.h>
+
+#include "obs-internal.h"
+
+struct obs_hotkey_name_map_edge;
+struct obs_hotkey_name_map_node;
+struct obs_hotkey_name_map;
+
+typedef struct obs_hotkey_name_map_edge obs_hotkey_name_map_edge_t;
+typedef struct obs_hotkey_name_map_node obs_hotkey_name_map_node_t;
+typedef struct obs_hotkey_name_map obs_hotkey_name_map_t;
+
+struct obs_hotkey_name_map_node {
+	bool is_leaf;
+	union {
+		int val;
+		DARRAY(obs_hotkey_name_map_edge_t) children;
+	};
+};
+
+struct obs_hotkey_name_map {
+	obs_hotkey_name_map_node_t root;
+	obs_hotkey_name_map_node_t *leaves;
+	size_t num_leaves;
+	size_t next_leaf;
+};
+
+struct obs_hotkey_name_map_edge_prefix {
+	uint8_t prefix_len;
+	char *prefix;
+};
+
+#define NAME_MAP_COMPRESS_LENGTH \
+	(sizeof(struct obs_hotkey_name_map_edge_prefix) - sizeof(uint8_t))
+
+struct obs_hotkey_name_map_edge {
+	union {
+		struct {
+			uint8_t prefix_len;
+			char *prefix;
+		};
+		struct {
+			uint8_t compressed_len;
+			char compressed_prefix[NAME_MAP_COMPRESS_LENGTH];
+		};
+	};
+	struct obs_hotkey_name_map_node *node;
+};
+
+
+static inline obs_hotkey_name_map_node_t *new_node(void)
+{
+	return bzalloc(sizeof(obs_hotkey_name_map_node_t));
+}
+
+static inline obs_hotkey_name_map_node_t *new_leaf(void)
+{
+	obs_hotkey_name_map_t *name_map = obs->hotkeys.name_map;
+	obs_hotkey_name_map_node_t *node =
+		&name_map->leaves[name_map->next_leaf];
+	name_map->next_leaf += 1;
+
+	node->is_leaf = true;
+	return node;
+}
+
+static inline char *get_prefix(obs_hotkey_name_map_edge_t *e)
+{
+	return e->prefix_len >= NAME_MAP_COMPRESS_LENGTH ?
+		e->prefix : e->compressed_prefix;
+}
+
+static void set_prefix(obs_hotkey_name_map_edge_t *e, const char *prefix,
+		size_t l)
+{
+	assert(e->prefix_len == 0);
+
+	e->compressed_len = (uint8_t)l;
+	if (l < NAME_MAP_COMPRESS_LENGTH)
+		strncpy(e->compressed_prefix, prefix, l);
+	else
+		e->prefix = bstrdup_n(prefix, l);
+}
+
+static obs_hotkey_name_map_edge_t *add_leaf(obs_hotkey_name_map_node_t *node,
+		const char *key, size_t l, int v)
+{
+	obs_hotkey_name_map_edge_t *e = da_push_back_new(node->children);
+
+	set_prefix(e, key, l);
+
+	e->node = new_leaf();
+	e->node->val = v;
+
+	return e;
+}
+
+static void shrink_prefix(obs_hotkey_name_map_edge_t *e, size_t l)
+{
+	bool old_comp = e->prefix_len < NAME_MAP_COMPRESS_LENGTH;
+	bool new_comp = l < NAME_MAP_COMPRESS_LENGTH;
+
+	char *str = get_prefix(e);
+
+	e->prefix_len = (uint8_t)l;
+	if (get_prefix(e) != str)
+		strncpy(get_prefix(e), str, l);
+	else
+		str[l] = 0;
+
+	if (!old_comp && new_comp)
+		bfree(str);
+}
+
+static void connect(obs_hotkey_name_map_edge_t *e,
+		obs_hotkey_name_map_node_t *n)
+{
+	e->node = n;
+}
+
+static void reduce_edge(obs_hotkey_name_map_edge_t *e, const char *key,
+		size_t l, int v)
+{
+	const char *str = get_prefix(e), *str_ = key;
+	size_t common_length = 0;
+	while (*str == *str_) {
+		common_length += 1;
+		str  += 1;
+		str_ += 1;
+	}
+
+	obs_hotkey_name_map_node_t *new_node_ = new_node();
+	obs_hotkey_name_map_edge_t *tail =
+		da_push_back_new(new_node_->children);
+
+	connect(tail, e->node);
+	set_prefix(tail, str, e->prefix_len - common_length);
+
+	add_leaf(new_node_, str_, l - common_length, v);
+
+	connect(e, new_node_);
+	shrink_prefix(e, common_length);
+}
+
+enum obs_hotkey_name_map_edge_compare_result {
+	RES_MATCHES,
+	RES_NO_MATCH,
+	RES_COMMON_PREFIX,
+	RES_PREFIX_MATCHES,
+};
+
+static enum obs_hotkey_name_map_edge_compare_result compare_prefix(
+		obs_hotkey_name_map_edge_t *edge, const char *key, size_t l)
+{
+	uint8_t pref_len = edge->prefix_len;
+	const char *str = get_prefix(edge);
+	size_t i = 0;
+
+	for (; i < l && i < pref_len; i++)
+		if (str[i] != key[i])
+			break;
+
+
+	if (i != 0 && pref_len == i)
+		return l == i ? RES_MATCHES : RES_PREFIX_MATCHES;
+	if (i != 0)
+		return pref_len == i ? RES_PREFIX_MATCHES : RES_COMMON_PREFIX;
+	return RES_NO_MATCH;
+}
+
+static void insert(obs_hotkey_name_map_edge_t *edge,
+		obs_hotkey_name_map_node_t *node,
+		const char *key, size_t l, int v)
+{
+	if (node->is_leaf && l > 0) {
+		obs_hotkey_name_map_node_t *new_node_ = new_node();
+		connect(edge, new_node_);
+
+		obs_hotkey_name_map_edge_t *edge =
+			da_push_back_new(new_node_->children);
+		connect(edge, node);
+		add_leaf(new_node_, key, l, v);
+		return;
+	}
+
+	if (node->is_leaf && l == 0) {
+		node->val = v;
+		return;
+	}
+
+	for (size_t i = 0; i < node->children.num; i++) {
+		obs_hotkey_name_map_edge_t *e = &node->children.array[i];
+
+		switch (compare_prefix(e, key, l)) {
+		case RES_NO_MATCH:
+			continue;
+
+		case RES_MATCHES:
+		case RES_PREFIX_MATCHES:
+			insert(e, e->node, key + e->prefix_len,
+					l - e->prefix_len, v);
+			return;
+
+		case RES_COMMON_PREFIX:
+			reduce_edge(e, key, l, v);
+			return;
+		}
+	}
+
+	add_leaf(node, key, l, v);
+}
+
+static void obs_hotkey_name_map_insert(obs_hotkey_name_map_t *trie,
+		const char *key, int v)
+{
+	if (!trie || !key)
+		return;
+
+	insert(NULL, &trie->root, key, strlen(key), v);
+}
+
+static bool obs_hotkey_name_map_lookup(obs_hotkey_name_map_t *trie,
+		const char *key, int *v)
+{
+	if (!trie || !key)
+		return false;
+
+	size_t len = strlen(key);
+	obs_hotkey_name_map_node_t *n = &trie->root;
+
+	size_t i = 0;
+	for (; i < n->children.num;) {
+		obs_hotkey_name_map_edge_t *e = &n->children.array[i];
+
+		switch (compare_prefix(e, key, len)) {
+		case RES_NO_MATCH:
+			i++;
+			continue;
+
+		case RES_COMMON_PREFIX:
+			return false;
+
+		case RES_PREFIX_MATCHES:
+			key += e->prefix_len;
+			len -= e->prefix_len;
+			n = e->node;
+			i = 0;
+			continue;
+
+		case RES_MATCHES:
+			n = e->node;
+			if (!n->is_leaf) {
+				for (size_t j = 0; j < n->children.num; j++) {
+					if (n->children.array[j].prefix_len)
+						continue;
+
+					if (v) *v =
+						n->children.array[j].node->val;
+					return true;
+				}
+				return false;
+			}
+
+			if (v) *v = n->val;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static void show_node(obs_hotkey_name_map_node_t *node, int in)
+{
+	if (node->is_leaf) {
+		printf(": % 3d\n", node->val);
+		return;
+	}
+
+	printf("\n");
+	for (int i = 0; i < in; i += 2)
+		printf("| ");
+	printf("%zu:\n", node->children.num);
+
+	for (size_t i = 0; i < node->children.num; i++) {
+		for (int i = 0; i < in; i += 2)
+			printf("| ");
+		printf("\\ ");
+
+		obs_hotkey_name_map_edge_t *e = &node->children.array[i];
+		printf("%s", get_prefix(e));
+		show_node(e->node, in+2);
+	}
+}
+
+void trie_print_size(obs_hotkey_name_map_t *trie)
+{
+	show_node(&trie->root, 0);
+}
+
+static const char* obs_key_names[] = {
+#define OBS_HOTKEY(x) #x,
+#include "obs-hotkeys.h"
+#undef OBS_HOTKEY
+};
+
+const char* obs_key_to_name(obs_key_t key)
+{
+	if (key >= OBS_KEY_LAST_VALUE) {
+		blog(LOG_ERROR, "obs-hotkey.c: queried unknown key "
+				"with code %d", (int)key);
+		return "";
+	}
+
+	return obs_key_names[key];
+}
+
+static obs_key_t obs_key_from_name_fallback(const char *name)
+{
+#define OBS_HOTKEY(x) if (strcmp(#x, name) == 0) return x;
+#include "obs-hotkeys.h"
+#undef OBS_HOTKEY
+	return OBS_KEY_NONE;
+}
+
+static void init_name_map(void)
+{
+	obs->hotkeys.name_map = bzalloc(sizeof(struct obs_hotkey_name_map));
+	obs_hotkey_name_map_t *name_map = obs->hotkeys.name_map;
+
+#define OBS_HOTKEY(x) name_map->num_leaves += 1;
+#include "obs-hotkeys.h"
+#undef OBS_HOTKEY
+
+	size_t size = sizeof(obs_hotkey_name_map_node_t) * name_map->num_leaves;
+	name_map->leaves = bzalloc(size);
+
+#define OBS_HOTKEY(x) obs_hotkey_name_map_insert(name_map, #x, x);
+#include "obs-hotkeys.h"
+#undef OBS_HOTKEY
+}
+
+obs_key_t obs_key_from_name(const char *name)
+{
+	if (!obs)
+		return obs_key_from_name_fallback(name);
+
+	if (pthread_once(&obs->hotkeys.name_map_init_token, init_name_map))
+		return obs_key_from_name_fallback(name);
+
+	int v = 0;
+	if (obs_hotkey_name_map_lookup(obs->hotkeys.name_map, name, &v))
+		return v;
+
+	return OBS_KEY_NONE;
+}
+
+static void free_node(obs_hotkey_name_map_node_t *node, bool release);
+
+static void free_edge(obs_hotkey_name_map_edge_t *edge)
+{
+	free_node(edge->node, true);
+
+	if (edge->prefix_len < NAME_MAP_COMPRESS_LENGTH)
+		return;
+
+	bfree(get_prefix(edge));
+}
+
+static void free_node(obs_hotkey_name_map_node_t *node, bool release)
+{
+	if (!node->is_leaf) {
+		for (size_t i = 0; i < node->children.num; i++)
+			free_edge(&node->children.array[i]);
+
+		da_free(node->children);
+	}
+
+	if (release && !node->is_leaf) bfree(node);
+}
+
+void obs_hotkey_name_map_free(void)
+{
+	if (!obs || !obs->hotkeys.name_map)
+		return;
+
+	free_node(&obs->hotkeys.name_map->root, false);
+	bfree(obs->hotkeys.name_map->leaves);
+	bfree(obs->hotkeys.name_map);
+}

+ 1497 - 0
libobs/obs-hotkey.c

@@ -0,0 +1,1497 @@
+/******************************************************************************
+    Copyright (C) 2014-2015 by Ruwen Hahn <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include <inttypes.h>
+
+#include "obs-internal.h"
+
+static inline bool lock(void)
+{
+	if (!obs)
+		return false;
+
+	pthread_mutex_lock(&obs->hotkeys.mutex);
+	return true;
+}
+
+static inline void unlock(void)
+{
+	pthread_mutex_unlock(&obs->hotkeys.mutex);
+}
+
+obs_hotkey_id obs_hotkey_get_id(const obs_hotkey_t *key)
+{
+	return key->id;
+}
+
+const char *obs_hotkey_get_name(const obs_hotkey_t *key)
+{
+	return key->name;
+}
+
+const char *obs_hotkey_get_description(const obs_hotkey_t *key)
+{
+	return key->description;
+}
+
+obs_hotkey_registerer_t obs_hotkey_get_registerer_type(const obs_hotkey_t *key)
+{
+	return key->registerer_type;
+}
+
+void *obs_hotkey_get_registerer(const obs_hotkey_t *key)
+{
+	return key->registerer;
+}
+
+obs_hotkey_id obs_hotkey_get_pair_partner_id(const obs_hotkey_t *key)
+{
+	return key->pair_partner_id;
+}
+
+obs_key_combination_t obs_hotkey_binding_get_key_combination(
+		obs_hotkey_binding_t *binding)
+{
+	return binding->key;
+}
+
+obs_hotkey_id obs_hotkey_binding_get_hotkey_id(obs_hotkey_binding_t *binding)
+{
+	return binding->hotkey_id;
+}
+
+obs_hotkey_t *obs_hotkey_binding_get_hotkey(obs_hotkey_binding_t *binding)
+{
+	return binding->hotkey;
+}
+
+static void hotkey_signal(const char *signal, obs_hotkey_t *hotkey)
+{
+	calldata_t data;
+	calldata_init(&data);
+	calldata_set_ptr(&data, "key", hotkey);
+
+	signal_handler_signal(obs->hotkeys.signals, signal, &data);
+
+	calldata_free(&data);
+}
+
+static inline void fixup_pointers(void);
+static inline void load_bindings(obs_hotkey_t *hotkey, obs_data_array_t *data);
+
+static inline void context_add_hotkey(struct obs_context_data *context,
+		obs_hotkey_id id)
+{
+	da_push_back(context->hotkeys, &id);
+}
+
+static inline obs_hotkey_id obs_hotkey_register_internal(
+		obs_hotkey_registerer_t type, void *registerer,
+		struct obs_context_data *context,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data)
+{
+	if ((obs->hotkeys.next_id + 1) == OBS_INVALID_HOTKEY_ID)
+		blog(LOG_WARNING, "obs-hotkey: Available hotkey ids exhausted");
+
+	obs_hotkey_t *base_addr = obs->hotkeys.hotkeys.array;
+	obs_hotkey_id result    = obs->hotkeys.next_id++;
+	obs_hotkey_t *hotkey    = da_push_back_new(obs->hotkeys.hotkeys);
+
+	hotkey->id              = result;
+	hotkey->name            = bstrdup(name);
+	hotkey->description     = bstrdup(description);
+	hotkey->func            = func;
+	hotkey->data            = data;
+	hotkey->registerer_type = type;
+	hotkey->registerer      = registerer;
+	hotkey->pair_partner_id = OBS_INVALID_HOTKEY_PAIR_ID;
+
+	if (context) {
+		obs_data_array_t *data =
+			obs_data_get_array(context->hotkey_data, name);
+		load_bindings(hotkey, data);
+		obs_data_array_release(data);
+
+		context_add_hotkey(context, result);
+	}
+
+	if (base_addr != obs->hotkeys.hotkeys.array)
+		fixup_pointers();
+
+	hotkey_signal("hotkey_register", hotkey);
+
+	return result;
+}
+
+obs_hotkey_id obs_hotkey_register_frontend(const char *name,
+		const char *description, obs_hotkey_func func, void *data)
+{
+	if (!lock())
+		return OBS_INVALID_HOTKEY_ID;
+
+	obs_hotkey_id id =  obs_hotkey_register_internal(
+			OBS_HOTKEY_REGISTERER_FRONTEND, NULL, NULL,
+			name, description, func, data);
+
+	unlock();
+	return id;
+}
+
+obs_hotkey_id obs_hotkey_register_encoder(obs_encoder_t *encoder,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data)
+{
+	if (!encoder || !lock())
+		return OBS_INVALID_HOTKEY_ID;
+
+	obs_hotkey_id id = obs_hotkey_register_internal(
+			OBS_HOTKEY_REGISTERER_ENCODER,
+			obs_encoder_get_weak_encoder(encoder),
+			&encoder->context, name, description,
+			func, data);
+
+	unlock();
+	return id;
+}
+
+obs_hotkey_id obs_hotkey_register_output(obs_output_t *output,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data)
+{
+	if (!output || !lock())
+		return OBS_INVALID_HOTKEY_ID;
+
+	obs_hotkey_id id = obs_hotkey_register_internal(
+			OBS_HOTKEY_REGISTERER_OUTPUT,
+			obs_output_get_weak_output(output),
+			&output->context, name, description,
+			func, data);
+
+	unlock();
+	return id;
+}
+
+obs_hotkey_id obs_hotkey_register_service(obs_service_t *service,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data)
+{
+	if (!service || !lock())
+		return OBS_INVALID_HOTKEY_ID;
+
+	obs_hotkey_id id = obs_hotkey_register_internal(
+			OBS_HOTKEY_REGISTERER_SERVICE,
+			obs_service_get_weak_service(service),
+			&service->context, name, description,
+			func, data);
+
+	unlock();
+	return id;
+}
+
+obs_hotkey_id obs_hotkey_register_source(obs_source_t *source, const char *name,
+		const char *description, obs_hotkey_func func, void *data)
+{
+	if (!source || !lock())
+		return OBS_INVALID_HOTKEY_ID;
+
+	obs_hotkey_id id = obs_hotkey_register_internal(
+			OBS_HOTKEY_REGISTERER_SOURCE,
+			obs_source_get_weak_source(source),
+			&source->context, name, description,
+			func, data);
+
+	unlock();
+	return id;
+}
+
+static inline void fixup_pair_pointers(void);
+
+static obs_hotkey_pair_t *create_hotkey_pair(struct obs_context_data *context,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	if ((obs->hotkeys.next_pair_id + 1) == OBS_INVALID_HOTKEY_PAIR_ID)
+		blog(LOG_WARNING, "obs-hotkey: Available hotkey pair ids "
+				"exhausted");
+
+	obs_hotkey_pair_t *base_addr = obs->hotkeys.hotkey_pairs.array;
+	obs_hotkey_pair_t *pair      =
+		da_push_back_new(obs->hotkeys.hotkey_pairs);
+
+	pair->pair_id = obs->hotkeys.next_pair_id++;
+	pair->func[0] = func0;
+	pair->func[1] = func1;
+	pair->id[0] = OBS_INVALID_HOTKEY_ID;
+	pair->id[1] = OBS_INVALID_HOTKEY_ID;
+	pair->data[0] = data0;
+	pair->data[1] = data1;
+
+	if (context)
+		da_push_back(context->hotkey_pairs, &pair->pair_id);
+
+	if (base_addr != obs->hotkeys.hotkey_pairs.array)
+		fixup_pair_pointers();
+
+	return pair;
+}
+
+static void obs_hotkey_pair_first_func(void *data,
+		obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed)
+{
+	UNUSED_PARAMETER(id);
+
+	obs_hotkey_pair_t *pair = data;
+	if (pair->pressed1)
+		return;
+
+	if (pair->pressed0 && !pressed)
+		pair->pressed0 = false;
+	else if (pair->func[0](pair->data[0], pair->pair_id, hotkey, pressed))
+		pair->pressed0 = pressed;
+}
+
+static void obs_hotkey_pair_second_func(void *data,
+		obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed)
+{
+	UNUSED_PARAMETER(id);
+
+	obs_hotkey_pair_t *pair = data;
+	if (pair->pressed0)
+		return;
+
+	if (pair->pressed1 && !pressed)
+		pair->pressed1 = false;
+	else if (pair->func[1](pair->data[0], pair->pair_id, hotkey, pressed))
+		pair->pressed1 = pressed;
+}
+
+static inline bool find_id(obs_hotkey_id id, size_t *idx);
+static obs_hotkey_pair_id register_hotkey_pair_internal(
+		obs_hotkey_registerer_t type, void *registerer,
+		void *(*weak_ref)(void*),
+		struct obs_context_data *context,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+
+	if (!lock())
+		return OBS_INVALID_HOTKEY_PAIR_ID;
+
+	obs_hotkey_pair_t *pair = create_hotkey_pair(context,
+			func0, func1, data0, data1);
+
+	pair->id[0] = obs_hotkey_register_internal(
+			type, weak_ref(registerer), context,
+			name0, description0,
+			obs_hotkey_pair_first_func, pair);
+
+	pair->id[1] = obs_hotkey_register_internal(
+			type, weak_ref(registerer), context,
+			name1, description1,
+			obs_hotkey_pair_second_func, pair);
+
+	size_t idx;
+	if (find_id(pair->id[0], &idx))
+		obs->hotkeys.hotkeys.array[idx].pair_partner_id = pair->id[1];
+
+	if (find_id(pair->id[1], &idx))
+		obs->hotkeys.hotkeys.array[idx].pair_partner_id = pair->id[0];
+
+	obs_hotkey_pair_id id = pair->pair_id;
+
+	unlock();
+	return id;
+}
+
+static inline void *obs_id_(void *id_)
+{
+	return id_;
+}
+
+obs_hotkey_pair_id obs_hotkey_pair_register_frontend(
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	return register_hotkey_pair_internal(
+			OBS_HOTKEY_REGISTERER_FRONTEND, NULL, obs_id_, NULL,
+			name0, description0, name1, description1,
+			func0, func1, data0, data1);
+}
+
+static inline void *weak_encoder_ref(void *ref)
+{
+	return obs_encoder_get_weak_encoder(ref);
+}
+
+obs_hotkey_pair_id obs_hotkey_pair_register_encoder(obs_encoder_t *encoder,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	if (!encoder) return OBS_INVALID_HOTKEY_PAIR_ID;
+	return register_hotkey_pair_internal(
+			OBS_HOTKEY_REGISTERER_ENCODER, encoder,
+			weak_encoder_ref,
+			&encoder->context,
+			name0, description0, name1, description1,
+			func0, func1, data0, data1);
+}
+
+static inline void *weak_output_ref(void *ref)
+{
+	return obs_output_get_weak_output(ref);
+}
+
+obs_hotkey_pair_id obs_hotkey_pair_register_output(obs_output_t *output,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	if (!output) return OBS_INVALID_HOTKEY_PAIR_ID;
+	return register_hotkey_pair_internal(
+			OBS_HOTKEY_REGISTERER_OUTPUT, output,
+			weak_output_ref,
+			&output->context,
+			name0, description0, name1, description1,
+			func0, func1, data0, data1);
+}
+
+static inline void *weak_service_ref(void *ref)
+{
+	return obs_service_get_weak_service(ref);
+}
+
+obs_hotkey_pair_id obs_hotkey_pair_register_service(obs_service_t *service,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	if (!service) return OBS_INVALID_HOTKEY_PAIR_ID;
+	return register_hotkey_pair_internal(
+			OBS_HOTKEY_REGISTERER_SERVICE, service,
+			weak_service_ref,
+			&service->context,
+			name0, description0, name1, description1,
+			func0, func1, data0, data1);
+}
+
+static inline void *weak_source_ref(void *ref)
+{
+	return obs_source_get_weak_source(ref);
+}
+
+obs_hotkey_pair_id obs_hotkey_pair_register_source(obs_source_t *source,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1)
+{
+	if (!source) return OBS_INVALID_HOTKEY_PAIR_ID;
+	return register_hotkey_pair_internal(
+			OBS_HOTKEY_REGISTERER_SOURCE, source,
+			weak_source_ref,
+			&source->context,
+			name0, description0, name1, description1,
+			func0, func1, data0, data1);
+}
+
+typedef bool (*obs_hotkey_internal_enum_func)(void *data,
+		size_t idx, obs_hotkey_t *hotkey);
+
+static inline void enum_hotkeys(obs_hotkey_internal_enum_func func, void *data)
+{
+	const size_t num    = obs->hotkeys.hotkeys.num;
+	obs_hotkey_t *array = obs->hotkeys.hotkeys.array;
+	for (size_t i = 0; i < num; i++) {
+		if (!func(data, i, &array[i]))
+			break;
+	}
+}
+
+typedef bool (*obs_hotkey_pair_internal_enum_func)(size_t idx,
+		obs_hotkey_pair_t *pair, void *data);
+
+static inline void enum_hotkey_pairs(obs_hotkey_pair_internal_enum_func func,
+		void *data)
+{
+	const size_t num         = obs->hotkeys.hotkey_pairs.num;
+	obs_hotkey_pair_t *array = obs->hotkeys.hotkey_pairs.array;
+	for (size_t i = 0; i < num; i++) {
+		if (!func(i, &array[i], data))
+			break;
+	}
+}
+
+typedef bool (*obs_hotkey_binding_internal_enum_func)(void *data,
+		size_t idx, obs_hotkey_binding_t *binding);
+
+static inline void enum_bindings(obs_hotkey_binding_internal_enum_func func,
+		void *data)
+{
+	const size_t         num    = obs->hotkeys.bindings.num;
+	obs_hotkey_binding_t *array = obs->hotkeys.bindings.array;
+	for (size_t i = 0; i < num; i++) {
+		if (!func(data, i, &array[i]))
+			break;
+	}
+}
+
+struct obs_hotkey_internal_find_forward {
+	obs_hotkey_id id;
+	bool          found;
+	size_t        idx;
+};
+
+static inline bool find_id_helper(void *data, size_t idx, obs_hotkey_t *hotkey)
+{
+	struct obs_hotkey_internal_find_forward *find = data;
+	if (hotkey->id != find->id)
+		return true;
+
+	find->idx   = idx;
+	find->found = true;
+	return false;
+}
+
+static inline bool find_id(obs_hotkey_id id, size_t *idx)
+{
+	struct obs_hotkey_internal_find_forward find = {id, false, 0};
+	enum_hotkeys(find_id_helper, &find);
+	*idx = find.idx;
+	return find.found;
+}
+
+static inline bool pointer_fixup_func(void *data,
+		size_t idx, obs_hotkey_binding_t *binding)
+{
+	UNUSED_PARAMETER(idx);
+	UNUSED_PARAMETER(data);
+
+	size_t idx_;
+	if (!find_id(binding->hotkey_id, &idx_)) {
+		bcrash("obs-hotkey: Could not find hotkey id '%" PRIuMAX "' "
+				"for binding '%s' (modifiers 0x%x)",
+				(uintmax_t)binding->hotkey_id,
+				obs_key_to_name(binding->key.key),
+				binding->key.modifiers);
+		binding->hotkey = NULL;
+		return true;
+	}
+
+	binding->hotkey = &obs->hotkeys.hotkeys.array[idx_];
+
+	return true;
+}
+
+static inline void fixup_pointers(void)
+{
+	enum_bindings(pointer_fixup_func, NULL);
+}
+
+struct obs_hotkey_internal_find_pair_forward {
+	obs_hotkey_pair_id id;
+	bool               found;
+	size_t             idx;
+};
+
+static inline bool find_pair_id_helper(size_t idx, obs_hotkey_pair_t *pair,
+		void *data)
+{
+	struct obs_hotkey_internal_find_pair_forward *find = data;
+	if (pair->pair_id != find->id)
+		return true;
+
+	find->idx   = idx;
+	find->found = true;
+	return false;
+}
+
+static inline bool find_pair_id(obs_hotkey_pair_id id, size_t *idx)
+{
+	struct obs_hotkey_internal_find_pair_forward find = {id, false, 0};
+	enum_hotkey_pairs(find_pair_id_helper, &find);
+	*idx = find.idx;
+	return find.found;
+}
+
+static inline bool pair_pointer_fixup_func(size_t idx,
+		obs_hotkey_pair_t *pair, void *data)
+{
+	UNUSED_PARAMETER(idx);
+	UNUSED_PARAMETER(data);
+
+	if (find_id(pair->id[0], &idx))
+		obs->hotkeys.hotkeys.array[idx].data = pair;
+
+	if (find_id(pair->id[1], &idx))
+		obs->hotkeys.hotkeys.array[idx].data = pair;
+
+	return true;
+}
+
+static inline void fixup_pair_pointers(void)
+{
+	enum_hotkey_pairs(pair_pointer_fixup_func, NULL);
+}
+
+static inline void enum_context_hotkeys(struct obs_context_data *context,
+		obs_hotkey_internal_enum_func func, void *data)
+{
+	const size_t num           = context->hotkeys.num;
+	const obs_hotkey_id *array = context->hotkeys.array;
+	obs_hotkey_t *hotkey_array = obs->hotkeys.hotkeys.array;
+	for (size_t i = 0; i < num; i++) {
+		size_t idx;
+		if (!find_id(array[i], &idx))
+			continue;
+
+		if (!func(data, idx, &hotkey_array[idx]))
+			break;
+	}
+}
+
+static inline void load_modifier(uint32_t *modifiers, obs_data_t *data,
+		const char *name, uint32_t flag)
+{
+	if (obs_data_get_bool(data, name))
+		*modifiers |= flag;
+}
+
+static inline void create_binding(obs_hotkey_t *hotkey,
+		obs_key_combination_t combo)
+{
+	obs_hotkey_binding_t *binding = da_push_back_new(obs->hotkeys.bindings);
+	if (!binding)
+		return;
+
+	binding->key = combo;
+	binding->hotkey_id = hotkey->id;
+	binding->hotkey    = hotkey;
+}
+
+static inline void load_binding(obs_hotkey_t *hotkey, obs_data_t *data)
+{
+	if (!hotkey || !data)
+		return;
+
+	obs_key_combination_t combo = {0};
+	uint32_t *modifiers = &combo.modifiers;
+	load_modifier(modifiers, data, "shift", INTERACT_SHIFT_KEY);
+	load_modifier(modifiers, data, "control", INTERACT_CONTROL_KEY);
+	load_modifier(modifiers, data, "alt", INTERACT_ALT_KEY);
+	load_modifier(modifiers, data, "command", INTERACT_COMMAND_KEY);
+
+	combo.key = obs_key_from_name(obs_data_get_string(data, "key"));
+	if (!modifiers && (combo.key == OBS_KEY_NONE ||
+			   combo.key >= OBS_KEY_LAST_VALUE))
+		return;
+
+	create_binding(hotkey, combo);
+}
+
+static inline void load_bindings(obs_hotkey_t *hotkey, obs_data_array_t *data)
+{
+	const size_t count = obs_data_array_count(data);
+	for (size_t i = 0; i < count; i++) {
+		obs_data_t *item = obs_data_array_item(data, i);
+		load_binding(hotkey, item);
+		obs_data_release(item);
+	}
+
+	hotkey_signal("hotkey_bindings_changed", hotkey);
+}
+
+static inline void remove_bindings(obs_hotkey_id id);
+
+void obs_hotkey_load_bindings(obs_hotkey_id id,
+		obs_key_combination_t *combinations, size_t num)
+{
+	size_t idx;
+
+	if (!lock())
+		return;
+
+	if (find_id(id, &idx)) {
+		obs_hotkey_t *hotkey = &obs->hotkeys.hotkeys.array[idx];
+		remove_bindings(id);
+		for (size_t i = 0; i < num; i++)
+			create_binding(hotkey, combinations[i]);
+
+		hotkey_signal("hotkey_bindings_changed", hotkey);
+	}
+	unlock();
+}
+
+void obs_hotkey_load(obs_hotkey_id id, obs_data_array_t *data)
+{
+	size_t idx;
+
+	if (!lock())
+		return;
+
+	if (find_id(id, &idx)) {
+		remove_bindings(id);
+		load_bindings(&obs->hotkeys.hotkeys.array[idx], data);
+	}
+	unlock();
+}
+
+static inline bool enum_load_bindings(void *data,
+		size_t idx, obs_hotkey_t *hotkey)
+{
+	UNUSED_PARAMETER(idx);
+
+	obs_data_array_t *hotkey_data = obs_data_get_array(data, hotkey->name);
+	if (!hotkey_data)
+		return true;
+
+	load_bindings(hotkey, hotkey_data);
+	obs_data_array_release(hotkey_data);
+	return true;
+}
+
+void obs_hotkeys_load_encoder(obs_encoder_t *encoder, obs_data_t *hotkeys)
+{
+	if (!encoder || !hotkeys)
+		return;
+	if (!lock())
+		return;
+
+	enum_context_hotkeys(&encoder->context, enum_load_bindings, hotkeys);
+	unlock();
+}
+
+void obs_hotkeys_load_output(obs_output_t *output, obs_data_t *hotkeys)
+{
+	if (!output || !hotkeys)
+		return;
+	if (!lock())
+		return;
+
+	enum_context_hotkeys(&output->context, enum_load_bindings, hotkeys);
+	unlock();
+}
+
+void obs_hotkeys_load_service(obs_service_t *service, obs_data_t *hotkeys)
+{
+	if (!service || !hotkeys)
+		return;
+	if (!lock())
+		return;
+
+	enum_context_hotkeys(&service->context, enum_load_bindings, hotkeys);
+	unlock();
+}
+
+void obs_hotkeys_load_source(obs_source_t *source, obs_data_t *hotkeys)
+{
+	if (!source || !hotkeys)
+		return;
+	if (!lock())
+		return;
+
+	enum_context_hotkeys(&source->context, enum_load_bindings, hotkeys);
+	unlock();
+}
+
+void obs_hotkey_pair_load(obs_hotkey_pair_id id, obs_data_array_t *data0,
+		obs_data_array_t *data1)
+{
+	if ((!data0 && !data1) || !lock())
+		return;
+
+	size_t idx;
+	if (!find_pair_id(id, &idx))
+		goto unlock;
+
+	obs_hotkey_pair_t *pair = &obs->hotkeys.hotkey_pairs.array[idx];
+
+	if (find_id(pair->id[0], &idx)) {
+		remove_bindings(pair->id[0]);
+		load_bindings(&obs->hotkeys.hotkeys.array[idx], data0);
+	}
+	if (find_id(pair->id[1], &idx)) {
+		remove_bindings(pair->id[1]);
+		load_bindings(&obs->hotkeys.hotkeys.array[idx], data1);
+	}
+
+unlock:
+	unlock();
+}
+
+static inline void save_modifier(uint32_t modifiers, obs_data_t *data,
+		const char *name, uint32_t flag)
+{
+	if ((modifiers & flag) == flag)
+		obs_data_set_bool(data, name, true);
+}
+
+struct save_bindings_helper_t {
+	obs_data_array_t *array;
+	obs_hotkey_t     *hotkey;
+};
+
+static inline bool save_bindings_helper(void *data,
+		size_t idx, obs_hotkey_binding_t *binding)
+{
+	UNUSED_PARAMETER(idx);
+	struct save_bindings_helper_t *h = data;
+
+	if (h->hotkey->id != binding->hotkey_id)
+		return true;
+
+	obs_data_t *hotkey = obs_data_create();
+
+	uint32_t modifiers = binding->key.modifiers;
+	save_modifier(modifiers, hotkey, "shift", INTERACT_SHIFT_KEY);
+	save_modifier(modifiers, hotkey, "control", INTERACT_CONTROL_KEY);
+	save_modifier(modifiers, hotkey, "alt", INTERACT_ALT_KEY);
+	save_modifier(modifiers, hotkey, "command", INTERACT_COMMAND_KEY);
+
+	obs_data_set_string(hotkey, "key", obs_key_to_name(binding->key.key));
+
+	obs_data_array_push_back(h->array, hotkey);
+
+	obs_data_release(hotkey);
+
+	return true;
+}
+
+static inline obs_data_array_t *save_hotkey(obs_hotkey_t *hotkey)
+{
+	obs_data_array_t *data = obs_data_array_create();
+
+	struct save_bindings_helper_t arg = {data, hotkey};
+	enum_bindings(save_bindings_helper, &arg);
+
+	return data;
+}
+
+obs_data_array_t *obs_hotkey_save(obs_hotkey_id id)
+{
+	size_t idx;
+	obs_data_array_t *result = NULL;
+
+	if (!lock())
+		return result;
+
+	if (find_id(id, &idx))
+		result = save_hotkey(&obs->hotkeys.hotkeys.array[idx]);
+	unlock();
+
+	return result;
+}
+
+static inline bool enum_save_hotkey(void *data,
+		size_t idx, obs_hotkey_t *hotkey)
+{
+	UNUSED_PARAMETER(idx);
+
+	obs_data_array_t *hotkey_data = save_hotkey(hotkey);
+	obs_data_set_array(data, hotkey->name, hotkey_data);
+	obs_data_array_release(hotkey_data);
+	return true;
+}
+
+static inline obs_data_t *save_context_hotkeys(struct obs_context_data *context)
+{
+	if (!context->hotkeys.num)
+		return NULL;
+
+	obs_data_t *result = obs_data_create();
+	enum_context_hotkeys(context, enum_save_hotkey, result);
+	return result;
+}
+
+obs_data_t *obs_hotkeys_save_encoder(obs_encoder_t *encoder)
+{
+	obs_data_t *result = NULL;
+
+	if (!lock())
+		return result;
+
+	result = save_context_hotkeys(&encoder->context);
+	unlock();
+
+	return result;
+}
+
+obs_data_t *obs_hotkeys_save_output(obs_output_t *output)
+{
+	obs_data_t *result = NULL;
+
+	if (!lock())
+		return result;
+
+	result = save_context_hotkeys(&output->context);
+	unlock();
+
+	return result;
+}
+
+obs_data_t *obs_hotkeys_save_service(obs_service_t *service)
+{
+	obs_data_t *result = NULL;
+
+	if (!lock())
+		return result;
+
+	result = save_context_hotkeys(&service->context);
+	unlock();
+
+	return result;
+}
+
+obs_data_t *obs_hotkeys_save_source(obs_source_t *source)
+{
+	obs_data_t *result = NULL;
+
+	if (!lock())
+		return result;
+
+	result = save_context_hotkeys(&source->context);
+	unlock();
+
+	return result;
+}
+
+struct binding_find_data {
+	obs_hotkey_id id;
+	size_t *idx;
+	bool found;
+};
+
+static inline bool binding_finder(void *data,
+		size_t idx, obs_hotkey_binding_t *binding)
+{
+	struct binding_find_data *find = data;
+	if (binding->hotkey_id != find->id)
+		return true;
+
+	*find->idx  = idx;
+	find->found = true;
+	return false;
+}
+
+static inline bool find_binding(obs_hotkey_id id, size_t *idx)
+{
+	struct binding_find_data data = {id, idx, false};
+	enum_bindings(binding_finder, &data);
+	return data.found;
+}
+
+static inline void release_pressed_binding(obs_hotkey_binding_t *binding);
+
+static inline void remove_bindings(obs_hotkey_id id)
+{
+	size_t idx;
+	while (find_binding(id, &idx)) {
+		obs_hotkey_binding_t *binding =
+			&obs->hotkeys.bindings.array[idx];
+
+		if (binding->pressed)
+			release_pressed_binding(binding);
+
+		da_erase(obs->hotkeys.bindings, idx);
+	}
+}
+
+static void release_registerer(obs_hotkey_t *hotkey)
+{
+	switch (hotkey->registerer_type) {
+	case OBS_HOTKEY_REGISTERER_FRONTEND:
+		break;
+
+	case OBS_HOTKEY_REGISTERER_ENCODER:
+		obs_weak_encoder_release(hotkey->registerer);
+		break;
+
+	case OBS_HOTKEY_REGISTERER_OUTPUT:
+		obs_weak_output_release(hotkey->registerer);
+		break;
+
+	case OBS_HOTKEY_REGISTERER_SERVICE:
+		obs_weak_service_release(hotkey->registerer);
+		break;
+
+	case OBS_HOTKEY_REGISTERER_SOURCE:
+		obs_weak_source_release(hotkey->registerer);
+		break;
+	}
+
+	hotkey->registerer = NULL;
+}
+
+static inline bool unregister_hotkey(obs_hotkey_id id)
+{
+	if (id >= obs->hotkeys.next_id)
+		return false;
+
+	size_t idx;
+	if (!find_id(id, &idx))
+		return false;
+
+	obs_hotkey_t *hotkey = &obs->hotkeys.hotkeys.array[idx];
+
+	hotkey_signal("hotkey_unregister", hotkey);
+
+	release_registerer(hotkey);
+
+	bfree(hotkey->name);
+	bfree(hotkey->description);
+
+	if (hotkey->registerer_type == OBS_HOTKEY_REGISTERER_SOURCE)
+		obs_weak_source_release(hotkey->registerer);
+
+	da_erase(obs->hotkeys.hotkeys, idx);
+	remove_bindings(id);
+
+	return obs->hotkeys.hotkeys.num >= idx;
+}
+
+static inline bool unregister_hotkey_pair(obs_hotkey_pair_id id)
+{
+	if (id >= obs->hotkeys.next_pair_id)
+		return false;
+
+	size_t idx;
+	if (!find_pair_id(id, &idx))
+		return false;
+
+	obs_hotkey_pair_t *pair = &obs->hotkeys.hotkey_pairs.array[idx];
+
+	bool need_fixup = unregister_hotkey(pair->id[0]);
+	need_fixup = unregister_hotkey(pair->id[1]) || need_fixup;
+	if (need_fixup)
+		fixup_pointers();
+
+	da_erase(obs->hotkeys.hotkey_pairs, idx);
+	return obs->hotkeys.hotkey_pairs.num >= idx;
+}
+
+void obs_hotkey_unregister(obs_hotkey_id id)
+{
+	if (!lock())
+		return;
+	if (unregister_hotkey(id))
+		fixup_pointers();
+	unlock();
+}
+
+void obs_hotkey_pair_unregister(obs_hotkey_pair_id id)
+{
+	if (!lock())
+		return;
+
+	if (unregister_hotkey_pair(id))
+		fixup_pair_pointers();
+
+	unlock();
+}
+
+static void context_release_hotkeys(struct obs_context_data *context)
+{
+	if (!context->hotkeys.num)
+		goto cleanup;
+
+	bool need_fixup = false;
+	for (size_t i = 0; i < context->hotkeys.num; i++)
+		need_fixup = unregister_hotkey(context->hotkeys.array[i]) ||
+			need_fixup;
+
+	if (need_fixup)
+		fixup_pointers();
+
+cleanup:
+	da_free(context->hotkeys);
+}
+
+static void context_release_hotkey_pairs(struct obs_context_data *context)
+{
+	if (!context->hotkey_pairs.num)
+		goto cleanup;
+
+	bool need_fixup = false;
+	for (size_t i = 0; i < context->hotkey_pairs.num; i++)
+		need_fixup =
+			unregister_hotkey_pair(context->hotkey_pairs.array[i])
+			|| need_fixup;
+
+	if (need_fixup)
+		fixup_pair_pointers();
+
+cleanup:
+	da_free(context->hotkey_pairs);
+}
+
+void obs_hotkeys_context_release(struct obs_context_data *context)
+{
+	if (!lock())
+		return;
+
+	context_release_hotkeys(context);
+	context_release_hotkey_pairs(context);
+
+	obs_data_release(context->hotkey_data);
+	unlock();
+}
+
+void obs_hotkeys_free(void)
+{
+	const size_t num      = obs->hotkeys.hotkeys.num;
+	obs_hotkey_t *hotkeys = obs->hotkeys.hotkeys.array;
+	for (size_t i = 0; i < num; i++) {
+		bfree(hotkeys[i].name);
+		bfree(hotkeys[i].description);
+
+		release_registerer(&hotkeys[i]);
+	}
+	da_free(obs->hotkeys.bindings);
+	da_free(obs->hotkeys.hotkeys);
+	da_free(obs->hotkeys.hotkey_pairs);
+
+	for (size_t i = 0; i < OBS_KEY_LAST_VALUE; i++) {
+		if (obs->hotkeys.translations[i]) {
+			bfree(obs->hotkeys.translations[i]);
+			obs->hotkeys.translations[i] = NULL;
+		}
+	}
+}
+
+struct obs_hotkey_internal_enum_forward {
+	obs_hotkey_enum_func func;
+	void                 *data;
+};
+
+static inline bool enum_hotkey(void *data, size_t idx, obs_hotkey_t *hotkey)
+{
+	UNUSED_PARAMETER(idx);
+
+	struct obs_hotkey_internal_enum_forward *forward = data;
+	return forward->func(forward->data, hotkey->id, hotkey);
+}
+
+void obs_enum_hotkeys(obs_hotkey_enum_func func, void *data)
+{
+	struct obs_hotkey_internal_enum_forward forwarder = {func, data};
+	if (!lock())
+		return;
+
+	enum_hotkeys(enum_hotkey, &forwarder);
+	unlock();
+}
+
+void obs_enum_hotkey_bindings(obs_hotkey_binding_enum_func func, void *data)
+{
+	if (!lock())
+		return;
+
+	enum_bindings(func, data);
+	unlock();
+}
+
+static inline bool modifiers_match(obs_hotkey_binding_t *binding,
+		uint32_t modifiers_, bool strict_modifiers)
+{
+	uint32_t modifiers = binding->key.modifiers;
+	return !modifiers ||
+		(!strict_modifiers && (modifiers & modifiers_) == modifiers) ||
+		(strict_modifiers && modifiers == modifiers_);
+}
+
+static inline bool is_pressed(obs_key_t key)
+{
+	return obs_hotkeys_platform_is_pressed(obs->hotkeys.platform_context,
+			key);
+}
+
+static inline void press_released_binding(obs_hotkey_binding_t *binding)
+{
+	binding->pressed = true;
+
+	obs_hotkey_t *hotkey = binding->hotkey;
+	if (hotkey->pressed++)
+		return;
+
+	if (!obs->hotkeys.reroute_hotkeys)
+		hotkey->func(hotkey->data, hotkey->id, hotkey, true);
+	else if (obs->hotkeys.router_func)
+		obs->hotkeys.router_func(obs->hotkeys.router_func_data,
+				hotkey->id, true);
+}
+
+static inline void release_pressed_binding(obs_hotkey_binding_t *binding)
+{
+	binding->pressed = false;
+
+	obs_hotkey_t *hotkey = binding->hotkey;
+	if (--hotkey->pressed)
+		return;
+
+	if (!obs->hotkeys.reroute_hotkeys)
+		hotkey->func(hotkey->data, hotkey->id, hotkey, false);
+	else if (obs->hotkeys.router_func)
+		obs->hotkeys.router_func(obs->hotkeys.router_func_data,
+				hotkey->id, false);
+}
+
+static inline void handle_binding(obs_hotkey_binding_t *binding,
+		uint32_t modifiers, bool no_press, bool strict_modifiers,
+		bool *pressed)
+{
+	bool modifiers_match_ = modifiers_match(binding, modifiers,
+							strict_modifiers);
+	bool modifiers_only   = binding->key.key == OBS_KEY_NONE;
+
+	if (!binding->key.modifiers)
+		binding->modifiers_match = true;
+
+	if (modifiers_only)
+		pressed = &modifiers_only;
+
+	if (!binding->key.modifiers && modifiers_only)
+		goto reset;
+
+	if ((!binding->modifiers_match && !modifiers_only) || !modifiers_match_)
+		goto reset;
+
+	if ((pressed && !*pressed) ||
+			(!pressed && !is_pressed(binding->key.key)))
+		goto reset;
+
+	if (binding->pressed || no_press)
+		return;
+
+	press_released_binding(binding);
+	return;
+
+reset:
+	binding->modifiers_match = modifiers_match_;
+	if (!binding->pressed)
+		return;
+
+	release_pressed_binding(binding);
+}
+
+struct obs_hotkey_internal_inject {
+	obs_key_combination_t hotkey;
+	bool                  pressed : 1;
+	bool                  strict_modifiers : 1;
+};
+
+static inline bool inject_hotkey(void *data,
+		size_t idx, obs_hotkey_binding_t *binding)
+{
+	UNUSED_PARAMETER(idx);
+	struct obs_hotkey_internal_inject *event = data;
+
+	if (modifiers_match(binding, event->hotkey.modifiers,
+				event->strict_modifiers)) {
+		bool pressed = binding->key.key == event->hotkey.key &&
+			event->pressed;
+		handle_binding(binding, event->hotkey.modifiers, false,
+				event->strict_modifiers, &pressed);
+	}
+
+	return true;
+}
+
+void obs_hotkey_inject_event(obs_key_combination_t hotkey, bool pressed)
+{
+	if (!lock())
+		return;
+
+	struct obs_hotkey_internal_inject event = {
+		{hotkey.modifiers, hotkey.key},
+		pressed, obs->hotkeys.strict_modifiers,
+	};
+	enum_bindings(inject_hotkey, &event);
+	unlock();
+}
+
+void obs_hotkey_enable_background_press(bool enable)
+{
+	if (!lock())
+		return;
+
+	obs->hotkeys.thread_disable_press = !enable;
+	unlock();
+}
+
+void obs_hotkey_enable_strict_modifiers(bool enable)
+{
+	if (!lock())
+		return;
+
+	obs->hotkeys.strict_modifiers = enable;
+	unlock();
+}
+
+struct obs_query_hotkeys_helper {
+	uint32_t modifiers;
+	bool     no_press : 1;
+	bool     strict_modifiers : 1;
+};
+
+static inline bool query_hotkey(void *data,
+		size_t idx, obs_hotkey_binding_t *binding)
+{
+	UNUSED_PARAMETER(idx);
+
+	struct obs_query_hotkeys_helper *param =
+		(struct obs_query_hotkeys_helper*)data;
+	handle_binding(binding, param->modifiers, param->no_press,
+			param->strict_modifiers, NULL);
+
+	return true;
+}
+
+static inline void query_hotkeys()
+{
+	uint32_t modifiers = 0;
+	if (is_pressed(OBS_KEY_SHIFT))
+		modifiers |= INTERACT_SHIFT_KEY;
+	if (is_pressed(OBS_KEY_CONTROL))
+		modifiers |= INTERACT_CONTROL_KEY;
+	if (is_pressed(OBS_KEY_ALT))
+		modifiers |= INTERACT_ALT_KEY;
+	if (is_pressed(OBS_KEY_META))
+		modifiers |= INTERACT_COMMAND_KEY;
+
+	struct obs_query_hotkeys_helper param = {
+		modifiers,
+		obs->hotkeys.thread_disable_press,
+		obs->hotkeys.strict_modifiers,
+	};
+	enum_bindings(query_hotkey, &param);
+}
+
+void *obs_hotkey_thread(void *arg)
+{
+	UNUSED_PARAMETER(arg);
+	while (os_event_timedwait(obs->hotkeys.stop_event, 25) == ETIMEDOUT) {
+		if (!lock())
+			continue;
+
+		query_hotkeys();
+		unlock();
+	}
+	return NULL;
+}
+
+void obs_hotkey_trigger_routed_callback(obs_hotkey_id id, bool pressed)
+{
+	if (!lock())
+		return;
+
+	if (!obs->hotkeys.reroute_hotkeys)
+		goto unlock;
+
+	size_t idx;
+	if (!find_id(id, &idx))
+		goto unlock;
+
+	obs_hotkey_t *hotkey = &obs->hotkeys.hotkeys.array[idx];
+	hotkey->func(hotkey->data, id, hotkey, pressed);
+
+unlock:
+	unlock();
+}
+
+void obs_hotkey_set_callback_routing_func(obs_hotkey_callback_router_func func,
+		void *data)
+{
+	if (!lock())
+		return;
+
+	obs->hotkeys.router_func = func;
+	obs->hotkeys.router_func_data = data;
+	unlock();
+}
+
+void obs_hotkey_enable_callback_rerouting(bool enable)
+{
+	if (!lock())
+		return;
+
+	obs->hotkeys.reroute_hotkeys = enable;
+	unlock();
+}
+
+static void obs_set_key_translation(obs_key_t key, const char *translation)
+{
+	bfree(obs->hotkeys.translations[key]);
+	obs->hotkeys.translations[key] = NULL;
+
+	if (translation)
+		obs->hotkeys.translations[key] = bstrdup(translation);
+}
+
+void obs_hotkeys_set_translations_s(
+		struct obs_hotkeys_translations *translations, size_t size)
+{
+#define ADD_TRANSLATION(key_name, var_name) \
+	if (t.var_name) \
+		obs_set_key_translation(key_name, t.var_name);
+
+	struct obs_hotkeys_translations t = {0};
+	struct dstr numpad = {0};
+	struct dstr mouse = {0};
+	struct dstr button = {0};
+
+	if (!translations) {
+		return;
+	}
+
+	memcpy(&t, translations, (size < sizeof(t)) ? size : sizeof(t));
+
+	ADD_TRANSLATION(OBS_KEY_INSERT, insert);
+	ADD_TRANSLATION(OBS_KEY_DELETE, del);
+	ADD_TRANSLATION(OBS_KEY_HOME, home);
+	ADD_TRANSLATION(OBS_KEY_END, end);
+	ADD_TRANSLATION(OBS_KEY_PAGEUP, page_up);
+	ADD_TRANSLATION(OBS_KEY_PAGEDOWN, page_down);
+	ADD_TRANSLATION(OBS_KEY_NUMLOCK, num_lock);
+	ADD_TRANSLATION(OBS_KEY_SCROLLLOCK, scroll_lock);
+	ADD_TRANSLATION(OBS_KEY_CAPSLOCK, caps_lock);
+	ADD_TRANSLATION(OBS_KEY_BACKSPACE, backspace);
+	ADD_TRANSLATION(OBS_KEY_TAB, tab);
+	ADD_TRANSLATION(OBS_KEY_PRINT, print);
+	ADD_TRANSLATION(OBS_KEY_PAUSE, pause);
+	ADD_TRANSLATION(OBS_KEY_SHIFT, shift);
+	ADD_TRANSLATION(OBS_KEY_ALT, alt);
+	ADD_TRANSLATION(OBS_KEY_CONTROL, control);
+	ADD_TRANSLATION(OBS_KEY_META, meta);
+	ADD_TRANSLATION(OBS_KEY_MENU, menu);
+	ADD_TRANSLATION(OBS_KEY_SPACE, space);
+#ifdef __APPLE__
+	const char *numpad_str = t.apple_keypad_num;
+	ADD_TRANSLATION(OBS_KEY_NUMSLASH, apple_keypad_divide);
+	ADD_TRANSLATION(OBS_KEY_NUMASTERISK, apple_keypad_multiply);
+	ADD_TRANSLATION(OBS_KEY_NUMMINUS, apple_keypad_minus);
+	ADD_TRANSLATION(OBS_KEY_NUMPLUS, apple_keypad_plus);
+	ADD_TRANSLATION(OBS_KEY_NUMPERIOD, apple_keypad_decimal);
+	ADD_TRANSLATION(OBS_KEY_NUMEQUAL, apple_keypad_equal);
+#else
+	const char *numpad_str = t.numpad_num;
+	ADD_TRANSLATION(OBS_KEY_NUMSLASH, numpad_divide);
+	ADD_TRANSLATION(OBS_KEY_NUMASTERISK, numpad_multiply);
+	ADD_TRANSLATION(OBS_KEY_NUMMINUS, numpad_minus);
+	ADD_TRANSLATION(OBS_KEY_NUMPLUS, numpad_plus);
+	ADD_TRANSLATION(OBS_KEY_NUMPERIOD, numpad_decimal);
+#endif
+
+	if (numpad_str) {
+		dstr_copy(&numpad, numpad_str);
+		dstr_depad(&numpad);
+
+		if (dstr_find(&numpad, "%1") == NULL) {
+			dstr_cat(&numpad, " %1");
+		}
+
+#define ADD_NUMPAD_NUM(idx) \
+		dstr_copy_dstr(&button, &numpad); \
+		dstr_replace(&button, "%1", #idx); \
+		obs_set_key_translation(OBS_KEY_NUM ## idx, button.array)
+
+		ADD_NUMPAD_NUM(0);
+		ADD_NUMPAD_NUM(1);
+		ADD_NUMPAD_NUM(2);
+		ADD_NUMPAD_NUM(3);
+		ADD_NUMPAD_NUM(4);
+		ADD_NUMPAD_NUM(5);
+		ADD_NUMPAD_NUM(6);
+		ADD_NUMPAD_NUM(7);
+		ADD_NUMPAD_NUM(8);
+		ADD_NUMPAD_NUM(9);
+	}
+
+	if (t.mouse_num) {
+		dstr_copy(&mouse, t.mouse_num);
+		dstr_depad(&mouse);
+
+		if (dstr_find(&mouse, "%1") == NULL) {
+			dstr_cat(&mouse, " %1");
+		}
+
+#define ADD_MOUSE_NUM(idx) \
+		dstr_copy_dstr(&button, &mouse); \
+		dstr_replace(&button, "%1", #idx); \
+		obs_set_key_translation(OBS_KEY_MOUSE ## idx, button.array)
+
+		ADD_MOUSE_NUM(1);
+		ADD_MOUSE_NUM(2);
+		ADD_MOUSE_NUM(3);
+		ADD_MOUSE_NUM(4);
+		ADD_MOUSE_NUM(5);
+		ADD_MOUSE_NUM(6);
+		ADD_MOUSE_NUM(7);
+		ADD_MOUSE_NUM(8);
+		ADD_MOUSE_NUM(9);
+		ADD_MOUSE_NUM(10);
+		ADD_MOUSE_NUM(11);
+		ADD_MOUSE_NUM(12);
+		ADD_MOUSE_NUM(13);
+		ADD_MOUSE_NUM(14);
+		ADD_MOUSE_NUM(15);
+		ADD_MOUSE_NUM(16);
+		ADD_MOUSE_NUM(17);
+		ADD_MOUSE_NUM(18);
+		ADD_MOUSE_NUM(19);
+		ADD_MOUSE_NUM(20);
+		ADD_MOUSE_NUM(21);
+		ADD_MOUSE_NUM(22);
+		ADD_MOUSE_NUM(23);
+		ADD_MOUSE_NUM(24);
+		ADD_MOUSE_NUM(25);
+		ADD_MOUSE_NUM(26);
+		ADD_MOUSE_NUM(27);
+		ADD_MOUSE_NUM(28);
+		ADD_MOUSE_NUM(29);
+	}
+
+	dstr_free(&numpad);
+	dstr_free(&mouse);
+	dstr_free(&button);
+}
+
+const char *obs_get_hotkey_translation(obs_key_t key, const char *def)
+{
+	if (key == OBS_KEY_NONE) {
+		return NULL;
+	}
+
+	return obs->hotkeys.translations[key] ?
+		obs->hotkeys.translations[key] : def;
+}
+
+void obs_hotkey_update_atomic(obs_hotkey_atomic_update_func func, void *data)
+{
+	if (!lock()) return;
+
+	func(data);
+
+	unlock();
+}

+ 281 - 0
libobs/obs-hotkey.h

@@ -0,0 +1,281 @@
+/******************************************************************************
+    Copyright (C) 2014-2015 by Ruwen Hahn <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef size_t obs_hotkey_id;
+#define OBS_INVALID_HOTKEY_ID (~(obs_hotkey_id)0)
+typedef size_t obs_hotkey_pair_id;
+#define OBS_INVALID_HOTKEY_PAIR_ID (~(obs_hotkey_pair_id)0)
+
+enum obs_key {
+#define OBS_HOTKEY(x) x,
+#include "obs-hotkeys.h"
+#undef OBS_HOTKEY
+	OBS_KEY_LAST_VALUE //not an actual key
+};
+typedef enum obs_key obs_key_t;
+
+struct obs_key_combination {
+	uint32_t  modifiers;
+	obs_key_t key;
+};
+typedef struct obs_key_combination obs_key_combination_t;
+
+typedef struct obs_hotkey obs_hotkey_t;
+typedef struct obs_hotkey_binding obs_hotkey_binding_t;
+
+enum obs_hotkey_registerer_type {
+	OBS_HOTKEY_REGISTERER_FRONTEND,
+	OBS_HOTKEY_REGISTERER_SOURCE,
+	OBS_HOTKEY_REGISTERER_OUTPUT,
+	OBS_HOTKEY_REGISTERER_ENCODER,
+	OBS_HOTKEY_REGISTERER_SERVICE,
+};
+typedef enum obs_hotkey_registerer_type obs_hotkey_registerer_t;
+
+EXPORT obs_hotkey_id obs_hotkey_get_id(const obs_hotkey_t *key);
+EXPORT const char *obs_hotkey_get_name(const obs_hotkey_t *key);
+EXPORT const char *obs_hotkey_get_description(const obs_hotkey_t *key);
+EXPORT obs_hotkey_registerer_t obs_hotkey_get_registerer_type(
+		const obs_hotkey_t *key);
+EXPORT void *obs_hotkey_get_registerer(const obs_hotkey_t *key);
+EXPORT obs_hotkey_id obs_hotkey_get_pair_partner_id(const obs_hotkey_t *key);
+
+
+EXPORT obs_key_combination_t obs_hotkey_binding_get_key_combination(
+		obs_hotkey_binding_t *binding);
+EXPORT obs_hotkey_id obs_hotkey_binding_get_hotkey_id(
+		obs_hotkey_binding_t *binding);
+EXPORT obs_hotkey_t *obs_hotkey_binding_get_hotkey(
+		obs_hotkey_binding_t *binding);
+
+struct obs_hotkeys_translations {
+	const char *insert;
+	const char *del;
+	const char *home;
+	const char *end;
+	const char *page_up;
+	const char *page_down;
+	const char *num_lock;
+	const char *scroll_lock;
+	const char *caps_lock;
+	const char *backspace;
+	const char *tab;
+	const char *print;
+	const char *pause;
+	const char *left;
+	const char *right;
+	const char *up;
+	const char *down;
+	const char *shift;
+	const char *alt;
+	const char *control;
+	const char *meta; /* windows/super key */
+	const char *menu;
+	const char *space;
+	const char *numpad_num; /* For example, "Numpad %1" */
+	const char *numpad_divide;
+	const char *numpad_multiply;
+	const char *numpad_minus;
+	const char *numpad_plus;
+	const char *numpad_decimal;
+	const char *apple_keypad_num; /* For example, "%1 (Keypad)" */
+	const char *apple_keypad_divide;
+	const char *apple_keypad_multiply;
+	const char *apple_keypad_minus;
+	const char *apple_keypad_plus;
+	const char *apple_keypad_decimal;
+	const char *apple_keypad_equal;
+	const char *mouse_num; /* For example, "Mouse %1" */
+};
+
+/* This function is an optional way to provide translations for specific keys
+ * that may not have translations.  If the operating system can provide
+ * translations for these keys, it will use the operating system's translation
+ * over these translations.  If no translations are specified, it will use
+ * the default english translations for that specific operating system. */
+EXPORT void obs_hotkeys_set_translations_s(
+		struct obs_hotkeys_translations *translations, size_t size);
+
+#define obs_hotkeys_set_translations(translations) \
+	obs_hotkeys_set_translations_s(translations, \
+			sizeof(struct obs_hotkeys_translations))
+
+
+/* registering hotkeys (giving hotkeys a name and a function) */
+
+typedef void (*obs_hotkey_func)(void *data,
+		obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed);
+
+EXPORT obs_hotkey_id obs_hotkey_register_frontend(const char *name,
+		const char *description, obs_hotkey_func func, void *data);
+
+EXPORT obs_hotkey_id obs_hotkey_register_encoder(obs_encoder_t *encoder,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data);
+
+EXPORT obs_hotkey_id obs_hotkey_register_output(obs_output_t *output,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data);
+
+EXPORT obs_hotkey_id obs_hotkey_register_service(obs_service_t *service,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data);
+
+EXPORT obs_hotkey_id obs_hotkey_register_source(obs_source_t *source,
+		const char *name, const char *description,
+		obs_hotkey_func func, void *data);
+
+typedef bool (*obs_hotkey_active_func)(void *data,
+		obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed);
+
+EXPORT obs_hotkey_pair_id obs_hotkey_pair_register_frontend(
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1);
+
+EXPORT obs_hotkey_pair_id obs_hotkey_pair_register_encoder(
+		obs_encoder_t *encoder,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1);
+
+EXPORT obs_hotkey_pair_id obs_hotkey_pair_register_output(
+		obs_output_t *output,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1);
+
+EXPORT obs_hotkey_pair_id obs_hotkey_pair_register_service(
+		obs_service_t *service,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1);
+
+EXPORT obs_hotkey_pair_id obs_hotkey_pair_register_source(
+		obs_source_t *source,
+		const char *name0, const char *description0,
+		const char *name1, const char *description1,
+		obs_hotkey_active_func func0, obs_hotkey_active_func func1,
+		void *data0, void *data1);
+
+EXPORT void obs_hotkey_unregister(obs_hotkey_id id);
+
+EXPORT void obs_hotkey_pair_unregister(obs_hotkey_pair_id id);
+
+/* loading hotkeys (associating a hotkey with a physical key and modifiers) */
+
+EXPORT void obs_hotkey_load_bindings(obs_hotkey_id id,
+		obs_key_combination_t *combinations, size_t num);
+
+EXPORT void obs_hotkey_load(obs_hotkey_id id, obs_data_array_t *data);
+
+EXPORT void obs_hotkeys_load_encoder(obs_encoder_t *encoder,
+		obs_data_t *hotkeys);
+
+EXPORT void obs_hotkeys_load_output(obs_output_t *output, obs_data_t *hotkeys);
+
+EXPORT void obs_hotkeys_load_service(obs_service_t *service,
+		obs_data_t *hotkeys);
+
+EXPORT void obs_hotkeys_load_source(obs_source_t *source, obs_data_t *hotkeys);
+
+EXPORT void obs_hotkey_pair_load(obs_hotkey_pair_id id, obs_data_array_t *data0,
+		obs_data_array_t *data1);
+
+
+EXPORT obs_data_array_t *obs_hotkey_save(obs_hotkey_id id);
+
+EXPORT obs_data_t *obs_hotkeys_save_encoder(obs_encoder_t *encoder);
+
+EXPORT obs_data_t *obs_hotkeys_save_output(obs_output_t *output);
+
+EXPORT obs_data_t *obs_hotkeys_save_service(obs_service_t *service);
+
+EXPORT obs_data_t *obs_hotkeys_save_source(obs_source_t *source);
+
+/* enumerating hotkeys */
+
+typedef bool (*obs_hotkey_enum_func)(void *data,
+		obs_hotkey_id id, obs_hotkey_t *key);
+
+EXPORT void obs_enum_hotkeys(obs_hotkey_enum_func func, void *data);
+
+/* enumerating bindings */
+
+typedef bool (*obs_hotkey_binding_enum_func)(void *data,
+		size_t idx, obs_hotkey_binding_t* binding);
+
+EXPORT void obs_enum_hotkey_bindings(obs_hotkey_binding_enum_func func,
+		void *data);
+
+/* hotkey event control */
+
+EXPORT void obs_hotkey_inject_event(obs_key_combination_t hotkey, bool pressed);
+
+EXPORT void obs_hotkey_enable_background_press(bool enable);
+
+EXPORT void obs_hotkey_enable_strict_modifiers(bool enable);
+
+/* hotkey callback routing (trigger callbacks through e.g. a UI thread) */
+
+typedef void (*obs_hotkey_callback_router_func)(void *data,
+		obs_hotkey_id id, bool pressed);
+
+EXPORT void obs_hotkey_set_callback_routing_func(obs_hotkey_callback_router_func
+		func, void *data);
+
+EXPORT void obs_hotkey_trigger_routed_callback(obs_hotkey_id id, bool pressed);
+
+/* hotkey callbacks won't be processed if callback rerouting is enabled and no
+ * router func is set */
+EXPORT void obs_hotkey_enable_callback_rerouting(bool enable);
+
+/* misc */
+
+typedef void (*obs_hotkey_atomic_update_func)(void *);
+EXPORT void obs_hotkey_update_atomic(obs_hotkey_atomic_update_func func,
+		void *data);
+
+struct dstr;
+EXPORT void obs_key_to_str(obs_key_t key, struct dstr *str);
+EXPORT void obs_key_combination_to_str(obs_key_combination_t key,
+		struct dstr *str);
+
+EXPORT obs_key_t obs_key_from_virtual_key(int code);
+EXPORT int obs_key_to_virtual_key(obs_key_t key);
+
+EXPORT const char *obs_key_to_name(obs_key_t key);
+EXPORT obs_key_t obs_key_from_name(const char *name);
+
+inline bool obs_key_combination_is_empty(obs_key_combination_t combo)
+{
+	return !combo.modifiers && combo.key == OBS_KEY_NONE;
+}
+
+#ifdef __cplusplus
+}
+#endif

+ 477 - 0
libobs/obs-hotkeys.h

@@ -0,0 +1,477 @@
+OBS_HOTKEY(OBS_KEY_NONE)
+
+OBS_HOTKEY(OBS_KEY_RETURN)
+OBS_HOTKEY(OBS_KEY_ENTER)
+OBS_HOTKEY(OBS_KEY_ESCAPE)
+OBS_HOTKEY(OBS_KEY_TAB)
+OBS_HOTKEY(OBS_KEY_BACKTAB)
+OBS_HOTKEY(OBS_KEY_BACKSPACE)
+OBS_HOTKEY(OBS_KEY_INSERT)
+OBS_HOTKEY(OBS_KEY_DELETE)
+OBS_HOTKEY(OBS_KEY_PAUSE)
+OBS_HOTKEY(OBS_KEY_PRINT)
+OBS_HOTKEY(OBS_KEY_SYSREQ)
+OBS_HOTKEY(OBS_KEY_CLEAR)
+OBS_HOTKEY(OBS_KEY_HOME)
+OBS_HOTKEY(OBS_KEY_END)
+OBS_HOTKEY(OBS_KEY_LEFT)
+OBS_HOTKEY(OBS_KEY_UP)
+OBS_HOTKEY(OBS_KEY_RIGHT)
+OBS_HOTKEY(OBS_KEY_DOWN)
+OBS_HOTKEY(OBS_KEY_PAGEUP)
+OBS_HOTKEY(OBS_KEY_PAGEDOWN)
+
+OBS_HOTKEY(OBS_KEY_SHIFT)
+OBS_HOTKEY(OBS_KEY_CONTROL)
+OBS_HOTKEY(OBS_KEY_META)
+OBS_HOTKEY(OBS_KEY_ALT)
+OBS_HOTKEY(OBS_KEY_ALTGR)
+OBS_HOTKEY(OBS_KEY_CAPSLOCK)
+OBS_HOTKEY(OBS_KEY_NUMLOCK)
+OBS_HOTKEY(OBS_KEY_SCROLLLOCK)
+
+OBS_HOTKEY(OBS_KEY_F1)
+OBS_HOTKEY(OBS_KEY_F2)
+OBS_HOTKEY(OBS_KEY_F3)
+OBS_HOTKEY(OBS_KEY_F4)
+OBS_HOTKEY(OBS_KEY_F5)
+OBS_HOTKEY(OBS_KEY_F6)
+OBS_HOTKEY(OBS_KEY_F7)
+OBS_HOTKEY(OBS_KEY_F8)
+OBS_HOTKEY(OBS_KEY_F9)
+OBS_HOTKEY(OBS_KEY_F10)
+OBS_HOTKEY(OBS_KEY_F11)
+OBS_HOTKEY(OBS_KEY_F12)
+OBS_HOTKEY(OBS_KEY_F13)
+OBS_HOTKEY(OBS_KEY_F14)
+OBS_HOTKEY(OBS_KEY_F15)
+OBS_HOTKEY(OBS_KEY_F16)
+OBS_HOTKEY(OBS_KEY_F17)
+OBS_HOTKEY(OBS_KEY_F18)
+OBS_HOTKEY(OBS_KEY_F19)
+OBS_HOTKEY(OBS_KEY_F20)
+OBS_HOTKEY(OBS_KEY_F21)
+OBS_HOTKEY(OBS_KEY_F22)
+OBS_HOTKEY(OBS_KEY_F23)
+OBS_HOTKEY(OBS_KEY_F24)
+OBS_HOTKEY(OBS_KEY_F25)
+OBS_HOTKEY(OBS_KEY_F26)
+OBS_HOTKEY(OBS_KEY_F27)
+OBS_HOTKEY(OBS_KEY_F28)
+OBS_HOTKEY(OBS_KEY_F29)
+OBS_HOTKEY(OBS_KEY_F30)
+OBS_HOTKEY(OBS_KEY_F31)
+OBS_HOTKEY(OBS_KEY_F32)
+OBS_HOTKEY(OBS_KEY_F33)
+OBS_HOTKEY(OBS_KEY_F34)
+OBS_HOTKEY(OBS_KEY_F35)
+
+OBS_HOTKEY(OBS_KEY_MENU)
+OBS_HOTKEY(OBS_KEY_HYPER_L)
+OBS_HOTKEY(OBS_KEY_HYPER_R)
+OBS_HOTKEY(OBS_KEY_HELP)
+OBS_HOTKEY(OBS_KEY_DIRECTION_L)
+OBS_HOTKEY(OBS_KEY_DIRECTION_R)
+
+OBS_HOTKEY(OBS_KEY_SPACE)
+OBS_HOTKEY(OBS_KEY_EXCLAM)
+OBS_HOTKEY(OBS_KEY_QUOTEDBL)
+OBS_HOTKEY(OBS_KEY_NUMBERSIGN)
+OBS_HOTKEY(OBS_KEY_DOLLAR)
+OBS_HOTKEY(OBS_KEY_PERCENT)
+OBS_HOTKEY(OBS_KEY_AMPERSAND)
+OBS_HOTKEY(OBS_KEY_APOSTROPHE)
+OBS_HOTKEY(OBS_KEY_PARENLEFT)
+OBS_HOTKEY(OBS_KEY_PARENRIGHT)
+OBS_HOTKEY(OBS_KEY_ASTERISK)
+OBS_HOTKEY(OBS_KEY_PLUS)
+OBS_HOTKEY(OBS_KEY_COMMA)
+OBS_HOTKEY(OBS_KEY_MINUS)
+OBS_HOTKEY(OBS_KEY_PERIOD)
+OBS_HOTKEY(OBS_KEY_SLASH)
+OBS_HOTKEY(OBS_KEY_0)
+OBS_HOTKEY(OBS_KEY_1)
+OBS_HOTKEY(OBS_KEY_2)
+OBS_HOTKEY(OBS_KEY_3)
+OBS_HOTKEY(OBS_KEY_4)
+OBS_HOTKEY(OBS_KEY_5)
+OBS_HOTKEY(OBS_KEY_6)
+OBS_HOTKEY(OBS_KEY_7)
+OBS_HOTKEY(OBS_KEY_8)
+OBS_HOTKEY(OBS_KEY_9)
+OBS_HOTKEY(OBS_KEY_NUMEQUAL)
+OBS_HOTKEY(OBS_KEY_NUMASTERISK)
+OBS_HOTKEY(OBS_KEY_NUMPLUS)
+OBS_HOTKEY(OBS_KEY_NUMCOMMA)
+OBS_HOTKEY(OBS_KEY_NUMMINUS)
+OBS_HOTKEY(OBS_KEY_NUMPERIOD)
+OBS_HOTKEY(OBS_KEY_NUMSLASH)
+OBS_HOTKEY(OBS_KEY_NUM0)
+OBS_HOTKEY(OBS_KEY_NUM1)
+OBS_HOTKEY(OBS_KEY_NUM2)
+OBS_HOTKEY(OBS_KEY_NUM3)
+OBS_HOTKEY(OBS_KEY_NUM4)
+OBS_HOTKEY(OBS_KEY_NUM5)
+OBS_HOTKEY(OBS_KEY_NUM6)
+OBS_HOTKEY(OBS_KEY_NUM7)
+OBS_HOTKEY(OBS_KEY_NUM8)
+OBS_HOTKEY(OBS_KEY_NUM9)
+OBS_HOTKEY(OBS_KEY_COLON)
+OBS_HOTKEY(OBS_KEY_SEMICOLON)
+OBS_HOTKEY(OBS_KEY_QUOTE)
+OBS_HOTKEY(OBS_KEY_LESS)
+OBS_HOTKEY(OBS_KEY_EQUAL)
+OBS_HOTKEY(OBS_KEY_GREATER)
+OBS_HOTKEY(OBS_KEY_QUESTION)
+OBS_HOTKEY(OBS_KEY_AT)
+OBS_HOTKEY(OBS_KEY_A)
+OBS_HOTKEY(OBS_KEY_B)
+OBS_HOTKEY(OBS_KEY_C)
+OBS_HOTKEY(OBS_KEY_D)
+OBS_HOTKEY(OBS_KEY_E)
+OBS_HOTKEY(OBS_KEY_F)
+OBS_HOTKEY(OBS_KEY_G)
+OBS_HOTKEY(OBS_KEY_H)
+OBS_HOTKEY(OBS_KEY_I)
+OBS_HOTKEY(OBS_KEY_J)
+OBS_HOTKEY(OBS_KEY_K)
+OBS_HOTKEY(OBS_KEY_L)
+OBS_HOTKEY(OBS_KEY_M)
+OBS_HOTKEY(OBS_KEY_N)
+OBS_HOTKEY(OBS_KEY_O)
+OBS_HOTKEY(OBS_KEY_P)
+OBS_HOTKEY(OBS_KEY_Q)
+OBS_HOTKEY(OBS_KEY_R)
+OBS_HOTKEY(OBS_KEY_S)
+OBS_HOTKEY(OBS_KEY_T)
+OBS_HOTKEY(OBS_KEY_U)
+OBS_HOTKEY(OBS_KEY_V)
+OBS_HOTKEY(OBS_KEY_W)
+OBS_HOTKEY(OBS_KEY_X)
+OBS_HOTKEY(OBS_KEY_Y)
+OBS_HOTKEY(OBS_KEY_Z)
+OBS_HOTKEY(OBS_KEY_BRACKETLEFT)
+OBS_HOTKEY(OBS_KEY_BACKSLASH)
+OBS_HOTKEY(OBS_KEY_BRACKETRIGHT)
+OBS_HOTKEY(OBS_KEY_ASCIICIRCUM)
+OBS_HOTKEY(OBS_KEY_UNDERSCORE)
+OBS_HOTKEY(OBS_KEY_QUOTELEFT)
+OBS_HOTKEY(OBS_KEY_BRACELEFT)
+OBS_HOTKEY(OBS_KEY_BAR)
+OBS_HOTKEY(OBS_KEY_BRACERIGHT)
+OBS_HOTKEY(OBS_KEY_ASCIITILDE)
+OBS_HOTKEY(OBS_KEY_NOBREAKSPACE)
+OBS_HOTKEY(OBS_KEY_EXCLAMDOWN)
+OBS_HOTKEY(OBS_KEY_CENT)
+OBS_HOTKEY(OBS_KEY_STERLING)
+OBS_HOTKEY(OBS_KEY_CURRENCY)
+OBS_HOTKEY(OBS_KEY_YEN)
+OBS_HOTKEY(OBS_KEY_BROKENBAR)
+OBS_HOTKEY(OBS_KEY_SECTION)
+OBS_HOTKEY(OBS_KEY_DIAERESIS)
+OBS_HOTKEY(OBS_KEY_COPYRIGHT)
+OBS_HOTKEY(OBS_KEY_ORDFEMININE)
+OBS_HOTKEY(OBS_KEY_GUILLEMOTLEFT)
+OBS_HOTKEY(OBS_KEY_NOTSIGN)
+OBS_HOTKEY(OBS_KEY_HYPHEN)
+OBS_HOTKEY(OBS_KEY_REGISTERED)
+OBS_HOTKEY(OBS_KEY_MACRON)
+OBS_HOTKEY(OBS_KEY_DEGREE)
+OBS_HOTKEY(OBS_KEY_PLUSMINUS)
+OBS_HOTKEY(OBS_KEY_TWOSUPERIOR)
+OBS_HOTKEY(OBS_KEY_THREESUPERIOR)
+OBS_HOTKEY(OBS_KEY_ACUTE)
+OBS_HOTKEY(OBS_KEY_MU)
+OBS_HOTKEY(OBS_KEY_PARAGRAPH)
+OBS_HOTKEY(OBS_KEY_PERIODCENTERED)
+OBS_HOTKEY(OBS_KEY_CEDILLA)
+OBS_HOTKEY(OBS_KEY_ONESUPERIOR)
+OBS_HOTKEY(OBS_KEY_MASCULINE)
+OBS_HOTKEY(OBS_KEY_GUILLEMOTRIGHT)
+OBS_HOTKEY(OBS_KEY_ONEQUARTER)
+OBS_HOTKEY(OBS_KEY_ONEHALF)
+OBS_HOTKEY(OBS_KEY_THREEQUARTERS)
+OBS_HOTKEY(OBS_KEY_QUESTIONDOWN)
+OBS_HOTKEY(OBS_KEY_AGRAVE)
+OBS_HOTKEY(OBS_KEY_AACUTE)
+OBS_HOTKEY(OBS_KEY_ACIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_ATILDE)
+OBS_HOTKEY(OBS_KEY_ADIAERESIS)
+OBS_HOTKEY(OBS_KEY_ARING)
+OBS_HOTKEY(OBS_KEY_AE)
+OBS_HOTKEY(OBS_KEY_CCEDILLA)
+OBS_HOTKEY(OBS_KEY_EGRAVE)
+OBS_HOTKEY(OBS_KEY_EACUTE)
+OBS_HOTKEY(OBS_KEY_ECIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_EDIAERESIS)
+OBS_HOTKEY(OBS_KEY_IGRAVE)
+OBS_HOTKEY(OBS_KEY_IACUTE)
+OBS_HOTKEY(OBS_KEY_ICIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_IDIAERESIS)
+OBS_HOTKEY(OBS_KEY_ETH)
+OBS_HOTKEY(OBS_KEY_NTILDE)
+OBS_HOTKEY(OBS_KEY_OGRAVE)
+OBS_HOTKEY(OBS_KEY_OACUTE)
+OBS_HOTKEY(OBS_KEY_OCIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_OTILDE)
+OBS_HOTKEY(OBS_KEY_ODIAERESIS)
+OBS_HOTKEY(OBS_KEY_MULTIPLY)
+OBS_HOTKEY(OBS_KEY_OOBLIQUE)
+OBS_HOTKEY(OBS_KEY_UGRAVE)
+OBS_HOTKEY(OBS_KEY_UACUTE)
+OBS_HOTKEY(OBS_KEY_UCIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_UDIAERESIS)
+OBS_HOTKEY(OBS_KEY_YACUTE)
+OBS_HOTKEY(OBS_KEY_THORN)
+OBS_HOTKEY(OBS_KEY_SSHARP)
+OBS_HOTKEY(OBS_KEY_DIVISION)
+OBS_HOTKEY(OBS_KEY_YDIAERESIS)
+OBS_HOTKEY(OBS_KEY_MULTI_KEY)
+OBS_HOTKEY(OBS_KEY_CODEINPUT)
+OBS_HOTKEY(OBS_KEY_SINGLECANDIDATE)
+OBS_HOTKEY(OBS_KEY_MULTIPLECANDIDATE)
+OBS_HOTKEY(OBS_KEY_PREVIOUSCANDIDATE)
+OBS_HOTKEY(OBS_KEY_MODE_SWITCH)
+OBS_HOTKEY(OBS_KEY_KANJI)
+OBS_HOTKEY(OBS_KEY_MUHENKAN)
+OBS_HOTKEY(OBS_KEY_HENKAN)
+OBS_HOTKEY(OBS_KEY_ROMAJI)
+OBS_HOTKEY(OBS_KEY_HIRAGANA)
+OBS_HOTKEY(OBS_KEY_KATAKANA)
+OBS_HOTKEY(OBS_KEY_HIRAGANA_KATAKANA)
+OBS_HOTKEY(OBS_KEY_ZENKAKU)
+OBS_HOTKEY(OBS_KEY_HANKAKU)
+OBS_HOTKEY(OBS_KEY_ZENKAKU_HANKAKU)
+OBS_HOTKEY(OBS_KEY_TOUROKU)
+OBS_HOTKEY(OBS_KEY_MASSYO)
+OBS_HOTKEY(OBS_KEY_KANA_LOCK)
+OBS_HOTKEY(OBS_KEY_KANA_SHIFT)
+OBS_HOTKEY(OBS_KEY_EISU_SHIFT)
+OBS_HOTKEY(OBS_KEY_EISU_TOGGLE)
+OBS_HOTKEY(OBS_KEY_HANGUL)
+OBS_HOTKEY(OBS_KEY_HANGUL_START)
+OBS_HOTKEY(OBS_KEY_HANGUL_END)
+OBS_HOTKEY(OBS_KEY_HANGUL_HANJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_JAMO)
+OBS_HOTKEY(OBS_KEY_HANGUL_ROMAJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_JEONJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_BANJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_PREHANJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_POSTHANJA)
+OBS_HOTKEY(OBS_KEY_HANGUL_SPECIAL)
+OBS_HOTKEY(OBS_KEY_DEAD_GRAVE)
+OBS_HOTKEY(OBS_KEY_DEAD_ACUTE)
+OBS_HOTKEY(OBS_KEY_DEAD_CIRCUMFLEX)
+OBS_HOTKEY(OBS_KEY_DEAD_TILDE)
+OBS_HOTKEY(OBS_KEY_DEAD_MACRON)
+OBS_HOTKEY(OBS_KEY_DEAD_BREVE)
+OBS_HOTKEY(OBS_KEY_DEAD_ABOVEDOT)
+OBS_HOTKEY(OBS_KEY_DEAD_DIAERESIS)
+OBS_HOTKEY(OBS_KEY_DEAD_ABOVERING)
+OBS_HOTKEY(OBS_KEY_DEAD_DOUBLEACUTE)
+OBS_HOTKEY(OBS_KEY_DEAD_CARON)
+OBS_HOTKEY(OBS_KEY_DEAD_CEDILLA)
+OBS_HOTKEY(OBS_KEY_DEAD_OGONEK)
+OBS_HOTKEY(OBS_KEY_DEAD_IOTA)
+OBS_HOTKEY(OBS_KEY_DEAD_VOICED_SOUND)
+OBS_HOTKEY(OBS_KEY_DEAD_SEMIVOICED_SOUND)
+OBS_HOTKEY(OBS_KEY_DEAD_BELOWDOT)
+OBS_HOTKEY(OBS_KEY_DEAD_HOOK)
+OBS_HOTKEY(OBS_KEY_DEAD_HORN)
+OBS_HOTKEY(OBS_KEY_BACK)
+OBS_HOTKEY(OBS_KEY_FORWARD)
+OBS_HOTKEY(OBS_KEY_STOP)
+OBS_HOTKEY(OBS_KEY_REFRESH)
+OBS_HOTKEY(OBS_KEY_VOLUMEDOWN)
+OBS_HOTKEY(OBS_KEY_VOLUMEMUTE)
+OBS_HOTKEY(OBS_KEY_VOLUMEUP)
+OBS_HOTKEY(OBS_KEY_BASSBOOST)
+OBS_HOTKEY(OBS_KEY_BASSUP)
+OBS_HOTKEY(OBS_KEY_BASSDOWN)
+OBS_HOTKEY(OBS_KEY_TREBLEUP)
+OBS_HOTKEY(OBS_KEY_TREBLEDOWN)
+OBS_HOTKEY(OBS_KEY_MEDIAPLAY)
+OBS_HOTKEY(OBS_KEY_MEDIASTOP)
+OBS_HOTKEY(OBS_KEY_MEDIAPREVIOUS)
+OBS_HOTKEY(OBS_KEY_MEDIANEXT)
+OBS_HOTKEY(OBS_KEY_MEDIARECORD)
+OBS_HOTKEY(OBS_KEY_MEDIAPAUSE)
+OBS_HOTKEY(OBS_KEY_MEDIATOGGLEPLAYPAUSE)
+OBS_HOTKEY(OBS_KEY_HOMEPAGE)
+OBS_HOTKEY(OBS_KEY_FAVORITES)
+OBS_HOTKEY(OBS_KEY_SEARCH)
+OBS_HOTKEY(OBS_KEY_STANDBY)
+OBS_HOTKEY(OBS_KEY_OPENURL)
+OBS_HOTKEY(OBS_KEY_LAUNCHMAIL)
+OBS_HOTKEY(OBS_KEY_LAUNCHMEDIA)
+OBS_HOTKEY(OBS_KEY_LAUNCH0)
+OBS_HOTKEY(OBS_KEY_LAUNCH1)
+OBS_HOTKEY(OBS_KEY_LAUNCH2)
+OBS_HOTKEY(OBS_KEY_LAUNCH3)
+OBS_HOTKEY(OBS_KEY_LAUNCH4)
+OBS_HOTKEY(OBS_KEY_LAUNCH5)
+OBS_HOTKEY(OBS_KEY_LAUNCH6)
+OBS_HOTKEY(OBS_KEY_LAUNCH7)
+OBS_HOTKEY(OBS_KEY_LAUNCH8)
+OBS_HOTKEY(OBS_KEY_LAUNCH9)
+OBS_HOTKEY(OBS_KEY_LAUNCHA)
+OBS_HOTKEY(OBS_KEY_LAUNCHB)
+OBS_HOTKEY(OBS_KEY_LAUNCHC)
+OBS_HOTKEY(OBS_KEY_LAUNCHD)
+OBS_HOTKEY(OBS_KEY_LAUNCHE)
+OBS_HOTKEY(OBS_KEY_LAUNCHF)
+OBS_HOTKEY(OBS_KEY_LAUNCHG)
+OBS_HOTKEY(OBS_KEY_LAUNCHH)
+OBS_HOTKEY(OBS_KEY_MONBRIGHTNESSUP)
+OBS_HOTKEY(OBS_KEY_MONBRIGHTNESSDOWN)
+OBS_HOTKEY(OBS_KEY_KEYBOARDLIGHTONOFF)
+OBS_HOTKEY(OBS_KEY_KEYBOARDBRIGHTNESSUP)
+OBS_HOTKEY(OBS_KEY_KEYBOARDBRIGHTNESSDOWN)
+OBS_HOTKEY(OBS_KEY_POWEROFF)
+OBS_HOTKEY(OBS_KEY_WAKEUP)
+OBS_HOTKEY(OBS_KEY_EJECT)
+OBS_HOTKEY(OBS_KEY_SCREENSAVER)
+OBS_HOTKEY(OBS_KEY_WWW)
+OBS_HOTKEY(OBS_KEY_MEMO)
+OBS_HOTKEY(OBS_KEY_LIGHTBULB)
+OBS_HOTKEY(OBS_KEY_SHOP)
+OBS_HOTKEY(OBS_KEY_HISTORY)
+OBS_HOTKEY(OBS_KEY_ADDFAVORITE)
+OBS_HOTKEY(OBS_KEY_HOTLINKS)
+OBS_HOTKEY(OBS_KEY_BRIGHTNESSADJUST)
+OBS_HOTKEY(OBS_KEY_FINANCE)
+OBS_HOTKEY(OBS_KEY_COMMUNITY)
+OBS_HOTKEY(OBS_KEY_AUDIOREWIND)
+OBS_HOTKEY(OBS_KEY_BACKFORWARD)
+OBS_HOTKEY(OBS_KEY_APPLICATIONLEFT)
+OBS_HOTKEY(OBS_KEY_APPLICATIONRIGHT)
+OBS_HOTKEY(OBS_KEY_BOOK)
+OBS_HOTKEY(OBS_KEY_CD)
+OBS_HOTKEY(OBS_KEY_CALCULATOR)
+OBS_HOTKEY(OBS_KEY_TODOLIST)
+OBS_HOTKEY(OBS_KEY_CLEARGRAB)
+OBS_HOTKEY(OBS_KEY_CLOSE)
+OBS_HOTKEY(OBS_KEY_COPY)
+OBS_HOTKEY(OBS_KEY_CUT)
+OBS_HOTKEY(OBS_KEY_DISPLAY)
+OBS_HOTKEY(OBS_KEY_DOS)
+OBS_HOTKEY(OBS_KEY_DOCUMENTS)
+OBS_HOTKEY(OBS_KEY_EXCEL)
+OBS_HOTKEY(OBS_KEY_EXPLORER)
+OBS_HOTKEY(OBS_KEY_GAME)
+OBS_HOTKEY(OBS_KEY_GO)
+OBS_HOTKEY(OBS_KEY_ITOUCH)
+OBS_HOTKEY(OBS_KEY_LOGOFF)
+OBS_HOTKEY(OBS_KEY_MARKET)
+OBS_HOTKEY(OBS_KEY_MEETING)
+OBS_HOTKEY(OBS_KEY_MENUKB)
+OBS_HOTKEY(OBS_KEY_MENUPB)
+OBS_HOTKEY(OBS_KEY_MYSITES)
+OBS_HOTKEY(OBS_KEY_NEWS)
+OBS_HOTKEY(OBS_KEY_OFFICEHOME)
+OBS_HOTKEY(OBS_KEY_OPTION)
+OBS_HOTKEY(OBS_KEY_PASTE)
+OBS_HOTKEY(OBS_KEY_PHONE)
+OBS_HOTKEY(OBS_KEY_CALENDAR)
+OBS_HOTKEY(OBS_KEY_REPLY)
+OBS_HOTKEY(OBS_KEY_RELOAD)
+OBS_HOTKEY(OBS_KEY_ROTATEWINDOWS)
+OBS_HOTKEY(OBS_KEY_ROTATIONPB)
+OBS_HOTKEY(OBS_KEY_ROTATIONKB)
+OBS_HOTKEY(OBS_KEY_SAVE)
+OBS_HOTKEY(OBS_KEY_SEND)
+OBS_HOTKEY(OBS_KEY_SPELL)
+OBS_HOTKEY(OBS_KEY_SPLITSCREEN)
+OBS_HOTKEY(OBS_KEY_SUPPORT)
+OBS_HOTKEY(OBS_KEY_TASKPANE)
+OBS_HOTKEY(OBS_KEY_TERMINAL)
+OBS_HOTKEY(OBS_KEY_TOOLS)
+OBS_HOTKEY(OBS_KEY_TRAVEL)
+OBS_HOTKEY(OBS_KEY_VIDEO)
+OBS_HOTKEY(OBS_KEY_WORD)
+OBS_HOTKEY(OBS_KEY_XFER)
+OBS_HOTKEY(OBS_KEY_ZOOMIN)
+OBS_HOTKEY(OBS_KEY_ZOOMOUT)
+OBS_HOTKEY(OBS_KEY_AWAY)
+OBS_HOTKEY(OBS_KEY_MESSENGER)
+OBS_HOTKEY(OBS_KEY_WEBCAM)
+OBS_HOTKEY(OBS_KEY_MAILFORWARD)
+OBS_HOTKEY(OBS_KEY_PICTURES)
+OBS_HOTKEY(OBS_KEY_MUSIC)
+OBS_HOTKEY(OBS_KEY_BATTERY)
+OBS_HOTKEY(OBS_KEY_BLUETOOTH)
+OBS_HOTKEY(OBS_KEY_WLAN)
+OBS_HOTKEY(OBS_KEY_UWB)
+OBS_HOTKEY(OBS_KEY_AUDIOFORWARD)
+OBS_HOTKEY(OBS_KEY_AUDIOREPEAT)
+OBS_HOTKEY(OBS_KEY_AUDIORANDOMPLAY)
+OBS_HOTKEY(OBS_KEY_SUBTITLE)
+OBS_HOTKEY(OBS_KEY_AUDIOCYCLETRACK)
+OBS_HOTKEY(OBS_KEY_TIME)
+OBS_HOTKEY(OBS_KEY_HIBERNATE)
+OBS_HOTKEY(OBS_KEY_VIEW)
+OBS_HOTKEY(OBS_KEY_TOPMENU)
+OBS_HOTKEY(OBS_KEY_POWERDOWN)
+OBS_HOTKEY(OBS_KEY_SUSPEND)
+OBS_HOTKEY(OBS_KEY_CONTRASTADJUST)
+OBS_HOTKEY(OBS_KEY_MEDIALAST)
+OBS_HOTKEY(OBS_KEY_CALL)
+OBS_HOTKEY(OBS_KEY_CAMERA)
+OBS_HOTKEY(OBS_KEY_CAMERAFOCUS)
+OBS_HOTKEY(OBS_KEY_CONTEXT1)
+OBS_HOTKEY(OBS_KEY_CONTEXT2)
+OBS_HOTKEY(OBS_KEY_CONTEXT3)
+OBS_HOTKEY(OBS_KEY_CONTEXT4)
+OBS_HOTKEY(OBS_KEY_FLIP)
+OBS_HOTKEY(OBS_KEY_HANGUP)
+OBS_HOTKEY(OBS_KEY_NO)
+OBS_HOTKEY(OBS_KEY_SELECT)
+OBS_HOTKEY(OBS_KEY_YES)
+OBS_HOTKEY(OBS_KEY_TOGGLECALLHANGUP)
+OBS_HOTKEY(OBS_KEY_VOICEDIAL)
+OBS_HOTKEY(OBS_KEY_LASTNUMBERREDIAL)
+OBS_HOTKEY(OBS_KEY_EXECUTE)
+OBS_HOTKEY(OBS_KEY_PRINTER)
+OBS_HOTKEY(OBS_KEY_PLAY)
+OBS_HOTKEY(OBS_KEY_SLEEP)
+OBS_HOTKEY(OBS_KEY_ZOOM)
+OBS_HOTKEY(OBS_KEY_CANCEL)
+
+
+#ifndef OBS_MOUSE_BUTTON
+#define OBS_MOUSE_BUTTON(x) OBS_HOTKEY(x)
+#define OBS_MOUSE_BUTTON_DEFAULT 1
+#endif
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE1)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE2)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE3)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE4)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE5)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE6)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE7)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE8)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE9)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE10)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE11)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE12)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE13)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE14)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE15)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE16)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE17)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE18)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE19)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE20)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE21)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE22)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE23)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE24)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE25)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE26)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE27)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE28)
+OBS_MOUSE_BUTTON(OBS_KEY_MOUSE29)
+#ifdef OBS_MOUSE_BUTTON_DEFAULT
+#undef OBS_MOUSE_BUTTON
+#undef OBS_MOUSE_BUTTON_DEFAULT
+#endif

+ 95 - 1
libobs/obs-internal.h

@@ -95,6 +95,65 @@ static inline bool check_path(const char *data, const char *path,
 }
 
 
+/* ------------------------------------------------------------------------- */
+/* hotkeys */
+
+struct obs_hotkey {
+	obs_hotkey_id               id;
+	char                        *name;
+	char                        *description;
+
+	obs_hotkey_func             func;
+	void                        *data;
+	int                         pressed;
+
+	obs_hotkey_registerer_t     registerer_type;
+	void                        *registerer;
+
+	obs_hotkey_id               pair_partner_id;
+};
+
+struct obs_hotkey_pair {
+	obs_hotkey_pair_id          pair_id;
+	obs_hotkey_id               id[2];
+	obs_hotkey_active_func      func[2];
+	bool                        pressed0 : 1;
+	bool                        pressed1 : 1;
+	void                        *data[2];
+};
+
+typedef struct obs_hotkey_pair obs_hotkey_pair_t;
+
+typedef struct obs_hotkeys_platform obs_hotkeys_platform_t;
+
+void *obs_hotkey_thread(void *param);
+
+struct obs_core_hotkeys;
+bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys);
+void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys);
+bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *context,
+		obs_key_t key);
+
+const char *obs_get_hotkey_translation(obs_key_t key, const char *def);
+
+struct obs_context_data;
+void obs_hotkeys_context_release(struct obs_context_data *context);
+
+void obs_hotkeys_free(void);
+
+struct obs_hotkey_binding {
+	obs_key_combination_t       key;
+	bool                        pressed : 1;
+	bool                        modifiers_match : 1;
+
+	obs_hotkey_id               hotkey_id;
+	obs_hotkey_t                *hotkey;
+};
+
+struct obs_hotkey_name_map;
+void obs_hotkey_name_map_free(void);
+
+
 /* ------------------------------------------------------------------------- */
 /* views */
 
@@ -213,6 +272,35 @@ struct obs_core_data {
 	volatile bool                   valid;
 };
 
+/* user hotkeys */
+struct obs_core_hotkeys {
+	pthread_mutex_t                 mutex;
+	DARRAY(obs_hotkey_t)            hotkeys;
+	obs_hotkey_id                   next_id;
+	DARRAY(obs_hotkey_pair_t)       hotkey_pairs;
+	obs_hotkey_pair_id              next_pair_id;
+
+	pthread_t                       hotkey_thread;
+	bool                            hotkey_thread_initialized;
+	os_event_t                      *stop_event;
+	bool                            thread_disable_press : 1;
+	bool                            strict_modifiers : 1;
+	bool                            reroute_hotkeys : 1;
+	DARRAY(obs_hotkey_binding_t)    bindings;
+
+	obs_hotkey_callback_router_func router_func;
+	void                            *router_func_data;
+
+	obs_hotkeys_platform_t          *platform_context;
+
+	pthread_once_t                  name_map_init_token;
+	struct obs_hotkey_name_map      *name_map;
+
+	signal_handler_t                *signals;
+
+	char                            *translations[OBS_KEY_LAST_VALUE];
+};
+
 struct obs_core {
 	struct obs_module               *first_module;
 	DARRAY(struct obs_module_path)  module_paths;
@@ -236,6 +324,7 @@ struct obs_core {
 	struct obs_core_video           video;
 	struct obs_core_audio           audio;
 	struct obs_core_data            data;
+	struct obs_core_hotkeys         hotkeys;
 };
 
 extern struct obs_core *obs;
@@ -253,6 +342,10 @@ struct obs_context_data {
 	signal_handler_t                *signals;
 	proc_handler_t                  *procs;
 
+	DARRAY(obs_hotkey_id)           hotkeys;
+	DARRAY(obs_hotkey_pair_id)      hotkey_pairs;
+	obs_data_t                      *hotkey_data;
+
 	DARRAY(char*)                   rename_cache;
 	pthread_mutex_t                 rename_cache_mutex;
 
@@ -264,7 +357,8 @@ struct obs_context_data {
 extern bool obs_context_data_init(
 		struct obs_context_data *context,
 		obs_data_t              *settings,
-		const char              *name);
+		const char              *name,
+		obs_data_t              *hotkey_data);
 extern void obs_context_data_free(struct obs_context_data *context);
 
 extern void obs_context_data_insert(struct obs_context_data *context,

+ 51 - 0
libobs/obs-nix.c

@@ -227,3 +227,54 @@ void log_system_info(void)
 	log_distribution_info();
 #endif
 }
+
+
+struct obs_hotkeys_platform {
+};
+
+bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+
+	return true;
+}
+
+void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+}
+
+bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *context,
+		obs_key_t key)
+{
+	UNUSED_PARAMETER(context);
+	UNUSED_PARAMETER(key);
+
+	return false;
+}
+
+void obs_key_to_str(obs_key_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+void obs_key_combination_to_str(obs_key_combination_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+obs_key_t obs_key_from_virtual_key(int code)
+{
+	UNUSED_PARAMETER(code);
+
+	return OBS_KEY_NONE;
+}
+
+int obs_key_to_virtual_key(obs_key_t key)
+{
+	UNUSED_PARAMETER(key);
+
+	return 0;
+}

+ 1 - 1
libobs/obs-output.c

@@ -49,7 +49,7 @@ static const char *output_signals[] = {
 static bool init_output_handlers(struct obs_output *output, const char *name,
 		obs_data_t *settings)
 {
-	if (!obs_context_data_init(&output->context, settings, name))
+	if (!obs_context_data_init(&output->context, settings, name, NULL))
 		return false;
 
 	signal_handler_add_array(output->context.signals, output_signals);

+ 1 - 1
libobs/obs-service.c

@@ -46,7 +46,7 @@ obs_service_t *obs_service_create(const char *id, const char *name,
 
 	service = bzalloc(sizeof(struct obs_service));
 
-	if (!obs_context_data_init(&service->context, settings, name)) {
+	if (!obs_context_data_init(&service->context, settings, name, NULL)) {
 		bfree(service);
 		return NULL;
 	}

+ 1 - 1
libobs/obs-source.c

@@ -96,7 +96,7 @@ static const char *source_signals[] = {
 bool obs_source_init_context(struct obs_source *source,
 		obs_data_t *settings, const char *name)
 {
-	if (!obs_context_data_init(&source->context, settings, name))
+	if (!obs_context_data_init(&source->context, settings, name, NULL))
 		return false;
 
 	return signal_handler_add_array(source->context.signals,

+ 52 - 0
libobs/obs-windows.c

@@ -194,3 +194,55 @@ void log_system_info(void)
 	log_available_memory();
 	log_windows_version();
 }
+
+
+struct obs_hotkeys_platform {
+	bool blank;
+};
+
+bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+
+	return true;
+}
+
+void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys)
+{
+	UNUSED_PARAMETER(hotkeys);
+}
+
+bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *context,
+		obs_key_t key)
+{
+	UNUSED_PARAMETER(context);
+	UNUSED_PARAMETER(key);
+
+	return false;
+}
+
+void obs_key_to_str(obs_key_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+void obs_key_combination_to_str(obs_key_combination_t key, struct dstr *str)
+{
+	UNUSED_PARAMETER(key);
+	UNUSED_PARAMETER(str);
+}
+
+obs_key_t obs_key_from_virtual_key(int code)
+{
+	UNUSED_PARAMETER(code);
+
+	return OBS_KEY_NONE;
+}
+
+int obs_key_to_virtual_key(obs_key_t key)
+{
+	UNUSED_PARAMETER(key);
+
+	return 0;
+}

+ 96 - 5
libobs/obs.c

@@ -609,6 +609,11 @@ static const char *obs_signals[] = {
 	"void channel_change(int channel, in out ptr source, ptr prev_source)",
 	"void master_volume(in out float volume)",
 
+	"void hotkey_layout_change()",
+	"void hotkey_register(ptr hotkey)",
+	"void hotkey_unregister(ptr hotkey)",
+	"void hotkey_bindings_changed(ptr hotkey)",
+
 	NULL
 };
 
@@ -625,6 +630,69 @@ static inline bool obs_init_handlers(void)
 	return signal_handler_add_array(obs->signals, obs_signals);
 }
 
+static pthread_once_t obs_pthread_once_init_token = PTHREAD_ONCE_INIT;
+static inline bool obs_init_hotkeys(void)
+{
+	struct obs_core_hotkeys *hotkeys = &obs->hotkeys;
+	pthread_mutexattr_t attr;
+	bool success = false;
+
+	assert(hotkeys != NULL);
+
+	da_init(hotkeys->hotkeys);
+	hotkeys->signals = obs->signals;
+	hotkeys->name_map_init_token = obs_pthread_once_init_token;
+
+	if (!obs_hotkeys_platform_init(hotkeys))
+		return false;
+
+	if (pthread_mutexattr_init(&attr) != 0)
+		return false;
+	if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0)
+		goto fail;
+	if (pthread_mutex_init(&hotkeys->mutex, &attr) != 0)
+		goto fail;
+
+	if (os_event_init(&hotkeys->stop_event, OS_EVENT_TYPE_MANUAL) != 0)
+		goto fail;
+	if (pthread_create(&hotkeys->hotkey_thread, NULL,
+			obs_hotkey_thread, NULL))
+		goto fail;
+
+	hotkeys->hotkey_thread_initialized = true;
+
+	success = true;
+
+fail:
+	pthread_mutexattr_destroy(&attr);
+	return success;
+}
+
+static inline void stop_hotkeys(void)
+{
+	struct obs_core_hotkeys *hotkeys = &obs->hotkeys;
+	void *thread_ret;
+
+	if (hotkeys->hotkey_thread_initialized) {
+		os_event_signal(hotkeys->stop_event);
+		pthread_join(hotkeys->hotkey_thread, &thread_ret);
+		hotkeys->hotkey_thread_initialized = false;
+	}
+
+	os_event_destroy(hotkeys->stop_event);
+	obs_hotkeys_free();
+}
+
+static inline void obs_free_hotkeys(void)
+{
+	struct obs_core_hotkeys *hotkeys = &obs->hotkeys;
+
+	obs_hotkey_name_map_free();
+
+	obs_hotkeys_platform_free(hotkeys);
+	pthread_mutex_destroy(&hotkeys->mutex);
+}
+
 extern const struct obs_source_info scene_info;
 
 extern void log_system_info(void);
@@ -639,6 +707,8 @@ static bool obs_init(const char *locale)
 		return false;
 	if (!obs_init_handlers())
 		return false;
+	if (!obs_init_hotkeys())
+		return false;
 
 	obs->locale = bstrdup(locale);
 	obs_register_source(&scene_info);
@@ -687,9 +757,11 @@ void obs_shutdown(void)
 	da_free(obs->modeless_ui_callbacks);
 
 	stop_video();
+	stop_hotkeys();
 
 	obs_free_data();
 	obs_free_video();
+	obs_free_hotkeys();
 	obs_free_graphics();
 	obs_free_audio();
 	proc_handler_destroy(obs->procs);
@@ -1347,6 +1419,7 @@ static obs_source_t *obs_load_source_type(obs_data_t *source_data,
 	const char   *name    = obs_data_get_string(source_data, "name");
 	const char   *id      = obs_data_get_string(source_data, "id");
 	obs_data_t   *settings = obs_data_get_obj(source_data, "settings");
+	obs_data_t   *hotkeys  = obs_data_get_obj(source_data, "hotkeys");
 	double       volume;
 	int64_t      sync;
 	uint32_t     flags;
@@ -1354,6 +1427,10 @@ static obs_source_t *obs_load_source_type(obs_data_t *source_data,
 
 	source = obs_source_create(type, id, name, settings);
 
+	obs_data_release(source->context.hotkey_data);
+	source->context.hotkey_data = hotkeys;
+	obs_hotkeys_load_source(source, hotkeys);
+
 	obs_data_set_default_double(source_data, "volume", 1.0);
 	volume = obs_data_get_double(source_data, "volume");
 	obs_source_set_volume(source, (float)volume);
@@ -1439,6 +1516,8 @@ obs_data_t *obs_save_source(obs_source_t *source)
 	obs_data_array_t *filters = obs_data_array_create();
 	obs_data_t *source_data = obs_data_create();
 	obs_data_t *settings    = obs_source_get_settings(source);
+	obs_data_t *hotkey_data = source->context.hotkey_data;
+	obs_data_t *hotkeys;
 	float      volume      = obs_source_get_volume(source);
 	uint32_t   mixers      = obs_source_get_audio_mixers(source);
 	int64_t    sync        = obs_source_get_sync_offset(source);
@@ -1449,6 +1528,13 @@ obs_data_t *obs_save_source(obs_source_t *source)
 	bool       muted       = obs_source_muted(source);
 
 	obs_source_save(source);
+	hotkeys = obs_hotkeys_save_source(source);
+
+	if (hotkeys) {
+		obs_data_release(hotkey_data);
+		source->context.hotkey_data = hotkeys;
+		hotkey_data = hotkeys;
+	}
 
 	obs_data_set_string(source_data, "name",     name);
 	obs_data_set_string(source_data, "id",       id);
@@ -1459,6 +1545,7 @@ obs_data_t *obs_save_source(obs_source_t *source)
 	obs_data_set_double(source_data, "volume",   volume);
 	obs_data_set_bool  (source_data, "enabled",  enabled);
 	obs_data_set_bool  (source_data, "muted",    muted);
+	obs_data_set_obj   (source_data, "hotkeys",  hotkey_data);
 
 	pthread_mutex_lock(&source->filter_mutex);
 
@@ -1522,7 +1609,8 @@ static inline char *dup_name(const char *name)
 static inline bool obs_context_data_init_wrap(
 		struct obs_context_data *context,
 		obs_data_t              *settings,
-		const char              *name)
+		const char              *name,
+		obs_data_t              *hotkey_data)
 {
 	assert(context);
 	memset(context, 0, sizeof(*context));
@@ -1539,17 +1627,19 @@ static inline bool obs_context_data_init_wrap(
 	if (!context->procs)
 		return false;
 
-	context->name     = dup_name(name);
-	context->settings = obs_data_newref(settings);
+	context->name        = dup_name(name);
+	context->settings    = obs_data_newref(settings);
+	context->hotkey_data = obs_data_newref(hotkey_data);
 	return true;
 }
 
 bool obs_context_data_init(
 		struct obs_context_data *context,
 		obs_data_t              *settings,
-		const char              *name)
+		const char              *name,
+		obs_data_t              *hotkey_data)
 {
-	if (obs_context_data_init_wrap(context, settings, name)) {
+	if (obs_context_data_init_wrap(context, settings, name, hotkey_data)) {
 		return true;
 	} else {
 		obs_context_data_free(context);
@@ -1559,6 +1649,7 @@ bool obs_context_data_init(
 
 void obs_context_data_free(struct obs_context_data *context)
 {
+	obs_hotkeys_context_release(context);
 	signal_handler_destroy(context->signals);
 	proc_handler_destroy(context->procs);
 	obs_data_release(context->settings);

+ 1 - 0
libobs/obs.h

@@ -72,6 +72,7 @@ typedef struct obs_weak_service obs_weak_service_t;
 #include "obs-output.h"
 #include "obs-service.h"
 #include "obs-audio-controls.h"
+#include "obs-hotkey.h"
 
 /**
  * @file