1
0

obs-cocoa.m 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. /******************************************************************************
  2. Copyright (C) 2013 by Ruwen Hahn <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "util/dstr.h"
  15. #include "obs.h"
  16. #include "obs-internal.h"
  17. #include <unistd.h>
  18. #include <sys/types.h>
  19. #include <sys/sysctl.h>
  20. #include <Carbon/Carbon.h>
  21. #include <IOKit/hid/IOHIDDevice.h>
  22. #include <IOKit/hid/IOHIDManager.h>
  23. #import <AppKit/AppKit.h>
  24. // MARK: macOS Bundle Management
  25. const char *get_module_extension(void)
  26. {
  27. return "";
  28. }
  29. void add_default_module_paths(void)
  30. {
  31. NSURL *pluginURL = [[NSBundle mainBundle] builtInPlugInsURL];
  32. NSString *pluginModulePath = [[pluginURL path] stringByAppendingString:@"/%module%.plugin/Contents/MacOS/"];
  33. NSString *pluginDataPath = [[pluginURL path] stringByAppendingString:@"/%module%.plugin/Contents/Resources/"];
  34. obs_add_module_path(pluginModulePath.UTF8String, pluginDataPath.UTF8String);
  35. }
  36. char *find_libobs_data_file(const char *file)
  37. {
  38. NSBundle *frameworkBundle = [NSBundle bundleWithIdentifier:@"com.obsproject.libobs"];
  39. NSString *libobsDataPath =
  40. [[[frameworkBundle bundleURL] path] stringByAppendingFormat:@"/%@/%s", @"Resources", file];
  41. size_t path_length = strlen(libobsDataPath.UTF8String);
  42. char *path = bmalloc(path_length + 1);
  43. snprintf(path, (path_length + 1), "%s", libobsDataPath.UTF8String);
  44. return path;
  45. }
  46. // MARK: - macOS Hardware Info Helpers
  47. static void log_processor_name(void)
  48. {
  49. char *name = NULL;
  50. size_t size;
  51. int ret;
  52. ret = sysctlbyname("machdep.cpu.brand_string", NULL, &size, NULL, 0);
  53. if (ret != 0)
  54. return;
  55. name = bmalloc(size);
  56. ret = sysctlbyname("machdep.cpu.brand_string", name, &size, NULL, 0);
  57. if (ret == 0)
  58. blog(LOG_INFO, "CPU Name: %s", name);
  59. bfree(name);
  60. }
  61. static void log_processor_speed(void)
  62. {
  63. size_t size;
  64. long long freq;
  65. int ret;
  66. size = sizeof(freq);
  67. ret = sysctlbyname("hw.cpufrequency", &freq, &size, NULL, 0);
  68. if (ret == 0)
  69. blog(LOG_INFO, "CPU Speed: %lldMHz", freq / 1000000);
  70. }
  71. static void log_model_name(void)
  72. {
  73. char *name = NULL;
  74. size_t size;
  75. int ret;
  76. ret = sysctlbyname("hw.model", NULL, &size, NULL, 0);
  77. if (ret != 0)
  78. return;
  79. name = bmalloc(size);
  80. ret = sysctlbyname("hw.model", name, &size, NULL, 0);
  81. if (ret == 0)
  82. blog(LOG_INFO, "Model Identifier: %s", name);
  83. bfree(name);
  84. }
  85. static void log_processor_cores(void)
  86. {
  87. blog(LOG_INFO, "Physical Cores: %d, Logical Cores: %d", os_get_physical_cores(), os_get_logical_cores());
  88. }
  89. static void log_emulation_status(void)
  90. {
  91. blog(LOG_INFO, "Rosetta translation used: %s", os_get_emulation_status() ? "true" : "false");
  92. }
  93. static void log_available_memory(void)
  94. {
  95. size_t size;
  96. long long memory_available;
  97. int ret;
  98. size = sizeof(memory_available);
  99. ret = sysctlbyname("hw.memsize", &memory_available, &size, NULL, 0);
  100. if (ret == 0)
  101. blog(LOG_INFO, "Physical Memory: %lldMB Total", memory_available / 1024 / 1024);
  102. }
  103. static void log_os(void)
  104. {
  105. NSProcessInfo *pi = [NSProcessInfo processInfo];
  106. blog(LOG_INFO, "OS Name: macOS");
  107. blog(LOG_INFO, "OS Version: %s", [[pi operatingSystemVersionString] UTF8String]);
  108. }
  109. static void log_kernel_version(void)
  110. {
  111. char kernel_version[1024];
  112. size_t size = sizeof(kernel_version);
  113. int ret;
  114. ret = sysctlbyname("kern.osrelease", kernel_version, &size, NULL, 0);
  115. if (ret == 0)
  116. blog(LOG_INFO, "Kernel Version: %s", kernel_version);
  117. }
  118. void log_system_info(void)
  119. {
  120. log_processor_name();
  121. log_processor_speed();
  122. log_processor_cores();
  123. log_available_memory();
  124. log_model_name();
  125. log_os();
  126. log_emulation_status();
  127. log_kernel_version();
  128. }
  129. // MARK: - Type Conversion Utilities
  130. static bool dstr_from_cfstring(struct dstr *str, CFStringRef ref)
  131. {
  132. CFIndex length = CFStringGetLength(ref);
  133. CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
  134. assert(max_size > 0);
  135. dstr_reserve(str, max_size);
  136. if (!CFStringGetCString(ref, str->array, max_size, kCFStringEncodingUTF8))
  137. return false;
  138. str->len = strlen(str->array);
  139. return true;
  140. }
  141. // MARK: - Graphics Thread Wrappers
  142. void *obs_graphics_thread_autorelease(void *param)
  143. {
  144. @autoreleasepool {
  145. return obs_graphics_thread(param);
  146. }
  147. }
  148. bool obs_graphics_thread_loop_autorelease(struct obs_graphics_context *context)
  149. {
  150. @autoreleasepool {
  151. return obs_graphics_thread_loop(context);
  152. }
  153. }
  154. // MARK: - macOS Hotkey Management
  155. typedef struct obs_key_code {
  156. int code;
  157. bool is_valid;
  158. } obs_key_code_t;
  159. typedef struct macOS_glyph_desc {
  160. UniChar glyph;
  161. char *desc;
  162. bool is_glyph;
  163. bool is_valid;
  164. } macOS_glyph_desc_t;
  165. typedef struct obs_key_desc {
  166. char *desc;
  167. bool is_valid;
  168. } obs_key_desc_t;
  169. static int INVALID_KEY = 0xFF;
  170. /* clang-format off */
  171. static const obs_key_code_t virtual_keys[OBS_KEY_LAST_VALUE] = {
  172. [OBS_KEY_A] = {.code = kVK_ANSI_A, .is_valid = true},
  173. [OBS_KEY_B] = {.code = kVK_ANSI_B, .is_valid = true},
  174. [OBS_KEY_C] = {.code = kVK_ANSI_C, .is_valid = true},
  175. [OBS_KEY_D] = {.code = kVK_ANSI_D, .is_valid = true},
  176. [OBS_KEY_E] = {.code = kVK_ANSI_E, .is_valid = true},
  177. [OBS_KEY_F] = {.code = kVK_ANSI_F, .is_valid = true},
  178. [OBS_KEY_G] = {.code = kVK_ANSI_G, .is_valid = true},
  179. [OBS_KEY_H] = {.code = kVK_ANSI_H, .is_valid = true},
  180. [OBS_KEY_I] = {.code = kVK_ANSI_I, .is_valid = true},
  181. [OBS_KEY_J] = {.code = kVK_ANSI_J, .is_valid = true},
  182. [OBS_KEY_K] = {.code = kVK_ANSI_K, .is_valid = true},
  183. [OBS_KEY_L] = {.code = kVK_ANSI_L, .is_valid = true},
  184. [OBS_KEY_M] = {.code = kVK_ANSI_M, .is_valid = true},
  185. [OBS_KEY_N] = {.code = kVK_ANSI_N, .is_valid = true},
  186. [OBS_KEY_O] = {.code = kVK_ANSI_O, .is_valid = true},
  187. [OBS_KEY_P] = {.code = kVK_ANSI_P, .is_valid = true},
  188. [OBS_KEY_Q] = {.code = kVK_ANSI_Q, .is_valid = true},
  189. [OBS_KEY_R] = {.code = kVK_ANSI_R, .is_valid = true},
  190. [OBS_KEY_S] = {.code = kVK_ANSI_S, .is_valid = true},
  191. [OBS_KEY_T] = {.code = kVK_ANSI_T, .is_valid = true},
  192. [OBS_KEY_U] = {.code = kVK_ANSI_U, .is_valid = true},
  193. [OBS_KEY_V] = {.code = kVK_ANSI_V, .is_valid = true},
  194. [OBS_KEY_W] = {.code = kVK_ANSI_W, .is_valid = true},
  195. [OBS_KEY_X] = {.code = kVK_ANSI_X, .is_valid = true},
  196. [OBS_KEY_Y] = {.code = kVK_ANSI_Y, .is_valid = true},
  197. [OBS_KEY_Z] = {.code = kVK_ANSI_Z, .is_valid = true},
  198. [OBS_KEY_1] = {.code = kVK_ANSI_1, .is_valid = true},
  199. [OBS_KEY_2] = {.code = kVK_ANSI_2, .is_valid = true},
  200. [OBS_KEY_3] = {.code = kVK_ANSI_3, .is_valid = true},
  201. [OBS_KEY_4] = {.code = kVK_ANSI_4, .is_valid = true},
  202. [OBS_KEY_5] = {.code = kVK_ANSI_5, .is_valid = true},
  203. [OBS_KEY_6] = {.code = kVK_ANSI_6, .is_valid = true},
  204. [OBS_KEY_7] = {.code = kVK_ANSI_7, .is_valid = true},
  205. [OBS_KEY_8] = {.code = kVK_ANSI_8, .is_valid = true},
  206. [OBS_KEY_9] = {.code = kVK_ANSI_9, .is_valid = true},
  207. [OBS_KEY_0] = {.code = kVK_ANSI_0, .is_valid = true},
  208. [OBS_KEY_RETURN] = {.code = kVK_Return, .is_valid = true},
  209. [OBS_KEY_ESCAPE] = {.code = kVK_Escape, .is_valid = true},
  210. [OBS_KEY_BACKSPACE] = {.code = kVK_Delete, .is_valid = true},
  211. [OBS_KEY_TAB] = {.code = kVK_Tab, .is_valid = true},
  212. [OBS_KEY_SPACE] = {.code = kVK_Space, .is_valid = true},
  213. [OBS_KEY_MINUS] = {.code = kVK_ANSI_Minus, .is_valid = true},
  214. [OBS_KEY_EQUAL] = {.code = kVK_ANSI_Equal, .is_valid = true},
  215. [OBS_KEY_BRACKETLEFT] = {.code = kVK_ANSI_LeftBracket, .is_valid = true},
  216. [OBS_KEY_BRACKETRIGHT] = {.code = kVK_ANSI_RightBracket, .is_valid = true},
  217. [OBS_KEY_BACKSLASH] = {.code = kVK_ANSI_Backslash, .is_valid = true},
  218. [OBS_KEY_SEMICOLON] = {.code = kVK_ANSI_Semicolon, .is_valid = true},
  219. [OBS_KEY_QUOTE] = {.code = kVK_ANSI_Quote, .is_valid = true},
  220. [OBS_KEY_DEAD_GRAVE] = {.code = kVK_ANSI_Grave, .is_valid = true},
  221. [OBS_KEY_COMMA] = {.code = kVK_ANSI_Comma, .is_valid = true},
  222. [OBS_KEY_PERIOD] = {.code = kVK_ANSI_Period, .is_valid = true},
  223. [OBS_KEY_SLASH] = {.code = kVK_ANSI_Slash, .is_valid = true},
  224. [OBS_KEY_CAPSLOCK] = {.code = kVK_CapsLock, .is_valid = true},
  225. [OBS_KEY_SECTION] = {.code = kVK_ISO_Section, .is_valid = true},
  226. [OBS_KEY_F1] = {.code = kVK_F1, .is_valid = true},
  227. [OBS_KEY_F2] = {.code = kVK_F2, .is_valid = true},
  228. [OBS_KEY_F3] = {.code = kVK_F3, .is_valid = true},
  229. [OBS_KEY_F4] = {.code = kVK_F4, .is_valid = true},
  230. [OBS_KEY_F5] = {.code = kVK_F5, .is_valid = true},
  231. [OBS_KEY_F6] = {.code = kVK_F6, .is_valid = true},
  232. [OBS_KEY_F7] = {.code = kVK_F7, .is_valid = true},
  233. [OBS_KEY_F8] = {.code = kVK_F8, .is_valid = true},
  234. [OBS_KEY_F9] = {.code = kVK_F9, .is_valid = true},
  235. [OBS_KEY_F10] = {.code = kVK_F10, .is_valid = true},
  236. [OBS_KEY_F11] = {.code = kVK_F11, .is_valid = true},
  237. [OBS_KEY_F12] = {.code = kVK_F12, .is_valid = true},
  238. [OBS_KEY_HELP] = {.code = kVK_Help, .is_valid = true},
  239. [OBS_KEY_HOME] = {.code = kVK_Home, .is_valid = true},
  240. [OBS_KEY_PAGEUP] = {.code = kVK_PageUp, .is_valid = true},
  241. [OBS_KEY_DELETE] = {.code = kVK_ForwardDelete, .is_valid = true},
  242. [OBS_KEY_END] = {.code = kVK_End, .is_valid = true},
  243. [OBS_KEY_PAGEDOWN] = {.code = kVK_PageDown, .is_valid = true},
  244. [OBS_KEY_RIGHT] = {.code = kVK_RightArrow, .is_valid = true},
  245. [OBS_KEY_LEFT] = {.code = kVK_LeftArrow, .is_valid = true},
  246. [OBS_KEY_DOWN] = {.code = kVK_DownArrow, .is_valid = true},
  247. [OBS_KEY_UP] = {.code = kVK_UpArrow, .is_valid = true},
  248. [OBS_KEY_CLEAR] = {.code = kVK_ANSI_KeypadClear, .is_valid = true},
  249. [OBS_KEY_NUMSLASH] = {.code = kVK_ANSI_KeypadDivide, .is_valid = true},
  250. [OBS_KEY_NUMASTERISK] = {.code = kVK_ANSI_KeypadMultiply, .is_valid = true},
  251. [OBS_KEY_NUMMINUS] = {.code = kVK_ANSI_KeypadMinus, .is_valid = true},
  252. [OBS_KEY_NUMPLUS] = {.code = kVK_ANSI_KeypadPlus, .is_valid = true},
  253. [OBS_KEY_ENTER] = {.code = kVK_ANSI_KeypadEnter, .is_valid = true},
  254. [OBS_KEY_NUM1] = {.code = kVK_ANSI_Keypad1, .is_valid = true},
  255. [OBS_KEY_NUM2] = {.code = kVK_ANSI_Keypad2, .is_valid = true},
  256. [OBS_KEY_NUM3] = {.code = kVK_ANSI_Keypad3, .is_valid = true},
  257. [OBS_KEY_NUM4] = {.code = kVK_ANSI_Keypad4, .is_valid = true},
  258. [OBS_KEY_NUM5] = {.code = kVK_ANSI_Keypad5, .is_valid = true},
  259. [OBS_KEY_NUM6] = {.code = kVK_ANSI_Keypad6, .is_valid = true},
  260. [OBS_KEY_NUM7] = {.code = kVK_ANSI_Keypad7, .is_valid = true},
  261. [OBS_KEY_NUM8] = {.code = kVK_ANSI_Keypad8, .is_valid = true},
  262. [OBS_KEY_NUM9] = {.code = kVK_ANSI_Keypad9, .is_valid = true},
  263. [OBS_KEY_NUM0] = {.code = kVK_ANSI_Keypad0, .is_valid = true},
  264. [OBS_KEY_NUMPERIOD] = {.code = kVK_ANSI_KeypadDecimal, .is_valid = true},
  265. [OBS_KEY_NUMEQUAL] = {.code = kVK_ANSI_KeypadEquals, .is_valid = true},
  266. [OBS_KEY_F13] = {.code = kVK_F13, .is_valid = true},
  267. [OBS_KEY_F14] = {.code = kVK_F14, .is_valid = true},
  268. [OBS_KEY_F15] = {.code = kVK_F15, .is_valid = true},
  269. [OBS_KEY_F16] = {.code = kVK_F16, .is_valid = true},
  270. [OBS_KEY_F17] = {.code = kVK_F17, .is_valid = true},
  271. [OBS_KEY_F18] = {.code = kVK_F18, .is_valid = true},
  272. [OBS_KEY_F19] = {.code = kVK_F19, .is_valid = true},
  273. [OBS_KEY_F20] = {.code = kVK_F20, .is_valid = true},
  274. [OBS_KEY_CONTROL] = {.code = kVK_Control, .is_valid = true},
  275. [OBS_KEY_SHIFT] = {.code = kVK_Shift, .is_valid = true},
  276. [OBS_KEY_ALT] = {.code = kVK_Option, .is_valid = true},
  277. [OBS_KEY_META] = {.code = kVK_Command, .is_valid = true},
  278. };
  279. static const obs_key_desc_t key_descriptions[OBS_KEY_LAST_VALUE] = {
  280. [OBS_KEY_SPACE] = {.desc = "Space", .is_valid = true},
  281. [OBS_KEY_NUMEQUAL] = {.desc = "= (Keypad)", .is_valid = true},
  282. [OBS_KEY_NUMASTERISK] = {.desc = "* (Keypad)", .is_valid = true},
  283. [OBS_KEY_NUMPLUS] = {.desc = "+ (Keypad)", .is_valid = true},
  284. [OBS_KEY_NUMMINUS] = {.desc = "- (Keypad)", .is_valid = true},
  285. [OBS_KEY_NUMPERIOD] = {.desc = ". (Keypad)", .is_valid = true},
  286. [OBS_KEY_NUMSLASH] = {.desc = "/ (Keypad)", .is_valid = true},
  287. [OBS_KEY_NUM0] = {.desc = "0 (Keypad)", .is_valid = true},
  288. [OBS_KEY_NUM1] = {.desc = "1 (Keypad)", .is_valid = true},
  289. [OBS_KEY_NUM2] = {.desc = "2 (Keypad)", .is_valid = true},
  290. [OBS_KEY_NUM3] = {.desc = "3 (Keypad)", .is_valid = true},
  291. [OBS_KEY_NUM4] = {.desc = "4 (Keypad)", .is_valid = true},
  292. [OBS_KEY_NUM5] = {.desc = "5 (Keypad)", .is_valid = true},
  293. [OBS_KEY_NUM6] = {.desc = "6 (Keypad)", .is_valid = true},
  294. [OBS_KEY_NUM7] = {.desc = "7 (Keypad)", .is_valid = true},
  295. [OBS_KEY_NUM8] = {.desc = "8 (Keypad)", .is_valid = true},
  296. [OBS_KEY_NUM9] = {.desc = "9 (Keypad)", .is_valid = true},
  297. [OBS_KEY_MOUSE1] = {.desc = "Mouse 1", .is_valid = true},
  298. [OBS_KEY_MOUSE2] = {.desc = "Mouse 2", .is_valid = true},
  299. [OBS_KEY_MOUSE3] = {.desc = "Mouse 3", .is_valid = true},
  300. [OBS_KEY_MOUSE4] = {.desc = "Mouse 4", .is_valid = true},
  301. [OBS_KEY_MOUSE5] = {.desc = "Mouse 5", .is_valid = true},
  302. [OBS_KEY_MOUSE6] = {.desc = "Mouse 6", .is_valid = true},
  303. [OBS_KEY_MOUSE7] = {.desc = "Mouse 7", .is_valid = true},
  304. [OBS_KEY_MOUSE8] = {.desc = "Mouse 8", .is_valid = true},
  305. [OBS_KEY_MOUSE9] = {.desc = "Mouse 9", .is_valid = true},
  306. [OBS_KEY_MOUSE10] = {.desc = "Mouse 10", .is_valid = true},
  307. [OBS_KEY_MOUSE11] = {.desc = "Mouse 11", .is_valid = true},
  308. [OBS_KEY_MOUSE12] = {.desc = "Mouse 12", .is_valid = true},
  309. [OBS_KEY_MOUSE13] = {.desc = "Mouse 13", .is_valid = true},
  310. [OBS_KEY_MOUSE14] = {.desc = "Mouse 14", .is_valid = true},
  311. [OBS_KEY_MOUSE15] = {.desc = "Mouse 15", .is_valid = true},
  312. [OBS_KEY_MOUSE16] = {.desc = "Mouse 16", .is_valid = true},
  313. [OBS_KEY_MOUSE17] = {.desc = "Mouse 17", .is_valid = true},
  314. [OBS_KEY_MOUSE18] = {.desc = "Mouse 18", .is_valid = true},
  315. [OBS_KEY_MOUSE19] = {.desc = "Mouse 19", .is_valid = true},
  316. [OBS_KEY_MOUSE20] = {.desc = "Mouse 20", .is_valid = true},
  317. [OBS_KEY_MOUSE21] = {.desc = "Mouse 21", .is_valid = true},
  318. [OBS_KEY_MOUSE22] = {.desc = "Mouse 22", .is_valid = true},
  319. [OBS_KEY_MOUSE23] = {.desc = "Mouse 23", .is_valid = true},
  320. [OBS_KEY_MOUSE24] = {.desc = "Mouse 24", .is_valid = true},
  321. [OBS_KEY_MOUSE25] = {.desc = "Mouse 25", .is_valid = true},
  322. [OBS_KEY_MOUSE26] = {.desc = "Mouse 26", .is_valid = true},
  323. [OBS_KEY_MOUSE27] = {.desc = "Mouse 27", .is_valid = true},
  324. [OBS_KEY_MOUSE28] = {.desc = "Mouse 28", .is_valid = true},
  325. [OBS_KEY_MOUSE29] = {.desc = "Mouse 29", .is_valid = true},
  326. };
  327. static const macOS_glyph_desc_t key_glyphs[(keyCodeMask >> 8)] = {
  328. [kVK_Return] = {.glyph = 0x21A9, .is_glyph = true, .is_valid = true},
  329. [kVK_Escape] = {.glyph = 0x238B, .is_glyph = true, .is_valid = true},
  330. [kVK_Delete] = {.glyph = 0x232B, .is_glyph = true, .is_valid = true},
  331. [kVK_Tab] = {.glyph = 0x21e5, .is_glyph = true, .is_valid = true},
  332. [kVK_CapsLock] = {.glyph = 0x21EA, .is_glyph = true, .is_valid = true},
  333. [kVK_ANSI_KeypadClear] = {.glyph = 0x2327, .is_glyph = true, .is_valid = true},
  334. [kVK_ANSI_KeypadEnter] = {.glyph = 0x2305, .is_glyph = true, .is_valid = true},
  335. [kVK_Help] = {.glyph = 0x003F, .is_glyph = true, .is_valid = true},
  336. [kVK_Home] = {.glyph = 0x2196, .is_glyph = true, .is_valid = true},
  337. [kVK_PageUp] = {.glyph = 0x21de, .is_glyph = true, .is_valid = true},
  338. [kVK_ForwardDelete] = {.glyph = 0x2326, .is_glyph = true, .is_valid = true},
  339. [kVK_End] = {.glyph = 0x2198, .is_glyph = true, .is_valid = true},
  340. [kVK_PageDown] = {.glyph = 0x21df, .is_glyph = true, .is_valid = true},
  341. [kVK_Control] = {.glyph = kControlUnicode, .is_glyph = true, .is_valid = true},
  342. [kVK_Shift] = {.glyph = kShiftUnicode, .is_glyph = true, .is_valid = true},
  343. [kVK_Option] = {.glyph = kOptionUnicode, .is_glyph = true, .is_valid = true},
  344. [kVK_Command] = {.glyph = kCommandUnicode, .is_glyph = true, .is_valid = true},
  345. [kVK_RightControl] = {.glyph = kControlUnicode, .is_glyph = true, .is_valid = true},
  346. [kVK_RightShift] = {.glyph = kShiftUnicode, .is_glyph = true, .is_valid = true},
  347. [kVK_RightOption] = {.glyph = kOptionUnicode, .is_glyph = true, .is_valid = true},
  348. [kVK_F1] = {.desc = "F1", .is_valid = true},
  349. [kVK_F2] = {.desc = "F2", .is_valid = true},
  350. [kVK_F3] = {.desc = "F3", .is_valid = true},
  351. [kVK_F4] = {.desc = "F4", .is_valid = true},
  352. [kVK_F5] = {.desc = "F5", .is_valid = true},
  353. [kVK_F6] = {.desc = "F6", .is_valid = true},
  354. [kVK_F7] = {.desc = "F7", .is_valid = true},
  355. [kVK_F8] = {.desc = "F8", .is_valid = true},
  356. [kVK_F9] = {.desc = "F9", .is_valid = true},
  357. [kVK_F10] = {.desc = "F10", .is_valid = true},
  358. [kVK_F11] = {.desc = "F11", .is_valid = true},
  359. [kVK_F12] = {.desc = "F12", .is_valid = true},
  360. [kVK_F13] = {.desc = "F13", .is_valid = true},
  361. [kVK_F14] = {.desc = "F14", .is_valid = true},
  362. [kVK_F15] = {.desc = "F15", .is_valid = true},
  363. [kVK_F16] = {.desc = "F16", .is_valid = true},
  364. [kVK_F17] = {.desc = "F17", .is_valid = true},
  365. [kVK_F18] = {.desc = "F18", .is_valid = true},
  366. [kVK_F19] = {.desc = "F19", .is_valid = true},
  367. [kVK_F20] = {.desc = "F20", .is_valid = true}
  368. };
  369. /* clang-format on */
  370. struct obs_hotkeys_platform {
  371. volatile long refs;
  372. CFMachPortRef eventTap;
  373. bool is_key_down[OBS_KEY_LAST_VALUE];
  374. TISInputSourceRef tis;
  375. CFDataRef layout_data;
  376. UCKeyboardLayout *layout;
  377. };
  378. // MARK: macOS Hotkey Implementation
  379. #define OBS_COCOA_MODIFIER_SIZE (int) 7
  380. static char string_control[OBS_COCOA_MODIFIER_SIZE];
  381. static char string_option[OBS_COCOA_MODIFIER_SIZE];
  382. static char string_shift[OBS_COCOA_MODIFIER_SIZE];
  383. static char string_command[OBS_COCOA_MODIFIER_SIZE];
  384. static dispatch_once_t onceToken;
  385. static void hotkeys_retain(obs_hotkeys_platform_t *platform)
  386. {
  387. os_atomic_inc_long(&platform->refs);
  388. }
  389. static void hotkeys_release(obs_hotkeys_platform_t *platform)
  390. {
  391. if (os_atomic_dec_long(&platform->refs) == -1) {
  392. if (platform->tis) {
  393. CFRelease(platform->tis);
  394. platform->tis = NULL;
  395. }
  396. if (platform->layout_data) {
  397. CFRelease(platform->layout_data);
  398. platform->layout_data = NULL;
  399. }
  400. if (platform->eventTap) {
  401. CGEventTapEnable(platform->eventTap, false);
  402. CFRelease(platform->eventTap);
  403. platform->eventTap = NULL;
  404. }
  405. bfree(platform);
  406. }
  407. }
  408. static bool obs_key_to_localized_string(obs_key_t key, struct dstr *str)
  409. {
  410. if (key < OBS_KEY_LAST_VALUE && !key_descriptions[key].is_valid) {
  411. return false;
  412. }
  413. dstr_copy(str, obs_get_hotkey_translation(key, key_descriptions[key].desc));
  414. return true;
  415. }
  416. static bool key_code_to_string(int code, struct dstr *str)
  417. {
  418. if (code < INVALID_KEY) {
  419. macOS_glyph_desc_t glyph = key_glyphs[code];
  420. if (glyph.is_valid && glyph.is_glyph && glyph.glyph > 0) {
  421. dstr_from_wcs(str, (wchar_t[]) {glyph.glyph, 0});
  422. } else if (glyph.is_valid && glyph.desc) {
  423. dstr_copy(str, glyph.desc);
  424. } else {
  425. return false;
  426. }
  427. }
  428. return true;
  429. }
  430. static bool log_layout_name(TISInputSourceRef tis)
  431. {
  432. struct dstr layout_name = {0};
  433. CFStringRef sid = (CFStringRef) TISGetInputSourceProperty(tis, kTISPropertyInputSourceID);
  434. if (!sid) {
  435. blog(LOG_ERROR, "hotkeys-cocoa: Unable to get input source ID");
  436. return false;
  437. }
  438. if (!dstr_from_cfstring(&layout_name, sid)) {
  439. blog(LOG_ERROR, "hotkeys-cocoa: Unable to convert input source ID");
  440. dstr_free(&layout_name);
  441. return false;
  442. }
  443. blog(LOG_INFO, "hotkeys-cocoa: Using keyboard layout '%s'", layout_name.array);
  444. dstr_free(&layout_name);
  445. return true;
  446. }
  447. // MARK: macOS Hotkey CoreFoundation Callbacks
  448. static CGEventRef KeyboardEventProc(CGEventTapProxy proxy __unused, CGEventType type, CGEventRef event, void *userInfo)
  449. {
  450. obs_hotkeys_platform_t *platform = userInfo;
  451. const CGEventFlags flags = CGEventGetFlags(event);
  452. platform->is_key_down[OBS_KEY_SHIFT] = !!(flags & kCGEventFlagMaskShift);
  453. platform->is_key_down[OBS_KEY_ALT] = !!(flags & kCGEventFlagMaskAlternate);
  454. platform->is_key_down[OBS_KEY_META] = !!(flags & kCGEventFlagMaskCommand);
  455. platform->is_key_down[OBS_KEY_CONTROL] = !!(flags & kCGEventFlagMaskControl);
  456. switch (type) {
  457. case kCGEventKeyDown: {
  458. const int64_t keycode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
  459. platform->is_key_down[obs_key_from_virtual_key(keycode)] = true;
  460. break;
  461. }
  462. case kCGEventKeyUp: {
  463. const int64_t keycode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
  464. platform->is_key_down[obs_key_from_virtual_key(keycode)] = false;
  465. break;
  466. }
  467. case kCGEventFlagsChanged: {
  468. break;
  469. }
  470. case kCGEventTapDisabledByTimeout: {
  471. blog(LOG_DEBUG, "[hotkeys-cocoa]: Hotkey event tap disabled by timeout. Reenabling...");
  472. CGEventTapEnable(platform->eventTap, true);
  473. break;
  474. }
  475. default: {
  476. blog(LOG_WARNING, "[hotkeys-cocoa]: Received unexpected event with code '%d'", type);
  477. }
  478. }
  479. return event;
  480. }
  481. static void InputMethodChangedProc(CFNotificationCenterRef center __unused, void *observer,
  482. CFNotificationName name __unused, const void *object __unused,
  483. CFDictionaryRef userInfo __unused)
  484. {
  485. struct obs_core_hotkeys *hotkeys = observer;
  486. obs_hotkeys_platform_t *platform = hotkeys->platform_context;
  487. pthread_mutex_lock(&hotkeys->mutex);
  488. if (platform->layout_data) {
  489. CFRelease(platform->layout_data);
  490. }
  491. platform->tis = TISCopyCurrentKeyboardLayoutInputSource();
  492. platform->layout_data = (CFDataRef) TISGetInputSourceProperty(platform->tis, kTISPropertyUnicodeKeyLayoutData);
  493. if (!platform->layout_data) {
  494. blog(LOG_ERROR, "hotkeys-cocoa: Failed to retrieve keyboard layout data");
  495. hotkeys->platform_context = NULL;
  496. pthread_mutex_unlock(&hotkeys->mutex);
  497. hotkeys_release(platform);
  498. return;
  499. }
  500. CFRetain(platform->layout_data);
  501. platform->layout = (UCKeyboardLayout *) CFDataGetBytePtr(platform->layout_data);
  502. pthread_mutex_unlock(&hotkeys->mutex);
  503. }
  504. // MARK: macOS Hotkey API Implementation
  505. bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys)
  506. {
  507. CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), hotkeys, InputMethodChangedProc,
  508. kTISNotifySelectedKeyboardInputSourceChanged, NULL,
  509. CFNotificationSuspensionBehaviorDeliverImmediately);
  510. obs_hotkeys_platform_t *platform = bzalloc(sizeof(obs_hotkeys_platform_t));
  511. const bool has_event_access = CGPreflightListenEventAccess();
  512. if (has_event_access) {
  513. platform->eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly,
  514. CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) |
  515. CGEventMaskBit(kCGEventFlagsChanged),
  516. KeyboardEventProc, platform);
  517. if (!platform->eventTap) {
  518. blog(LOG_WARNING, "[hotkeys-cocoa]: Couldn't create hotkey event tap.");
  519. hotkeys_release(platform);
  520. platform = NULL;
  521. return false;
  522. }
  523. CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, platform->eventTap, 0);
  524. CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
  525. CFRelease(source);
  526. CGEventTapEnable(platform->eventTap, true);
  527. } else {
  528. blog(LOG_WARNING, "[hotkeys-cocoa]: No event permissions, could not add global hotkeys.");
  529. }
  530. platform->tis = TISCopyCurrentKeyboardLayoutInputSource();
  531. platform->layout_data = (CFDataRef) TISGetInputSourceProperty(platform->tis, kTISPropertyUnicodeKeyLayoutData);
  532. if (!platform->layout_data) {
  533. blog(LOG_ERROR, "hotkeys-cocoa: Failed to retrieve keyboard layout data");
  534. hotkeys_release(platform);
  535. platform = NULL;
  536. return false;
  537. }
  538. CFRetain(platform->layout_data);
  539. platform->layout = (UCKeyboardLayout *) CFDataGetBytePtr(platform->layout_data);
  540. obs_hotkeys_platform_t *currentPlatform;
  541. pthread_mutex_lock(&hotkeys->mutex);
  542. currentPlatform = hotkeys->platform_context;
  543. if (platform && currentPlatform && platform->layout_data == currentPlatform->layout_data) {
  544. pthread_mutex_unlock(&hotkeys->mutex);
  545. hotkeys_release(platform);
  546. return true;
  547. }
  548. hotkeys->platform_context = platform;
  549. log_layout_name(platform->tis);
  550. pthread_mutex_unlock(&hotkeys->mutex);
  551. calldata_t parameters = {0};
  552. signal_handler_signal(hotkeys->signals, "hotkey_layout_change", &parameters);
  553. if (currentPlatform) {
  554. hotkeys_release(currentPlatform);
  555. }
  556. bool hasPlatformContext = hotkeys->platform_context != NULL;
  557. return hasPlatformContext;
  558. }
  559. void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys)
  560. {
  561. CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDistributedCenter(), hotkeys);
  562. hotkeys_release(hotkeys->platform_context);
  563. }
  564. int obs_key_to_virtual_key(obs_key_t key)
  565. {
  566. if (virtual_keys[key].is_valid) {
  567. return virtual_keys[key].code;
  568. } else {
  569. return INVALID_KEY;
  570. }
  571. }
  572. obs_key_t obs_key_from_virtual_key(int keyCode)
  573. {
  574. for (size_t i = 0; i < OBS_KEY_LAST_VALUE; i++) {
  575. if (virtual_keys[i].is_valid && virtual_keys[i].code == keyCode) {
  576. return (obs_key_t) i;
  577. }
  578. }
  579. return OBS_KEY_NONE;
  580. }
  581. bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *platform, obs_key_t key)
  582. {
  583. if (key >= OBS_KEY_MOUSE1 && key <= OBS_KEY_MOUSE29) {
  584. int button = key - 1;
  585. NSUInteger buttons = [NSEvent pressedMouseButtons];
  586. if ((buttons & (1 << button)) != 0) {
  587. return true;
  588. } else {
  589. return false;
  590. }
  591. }
  592. if (!platform) {
  593. return false;
  594. }
  595. if (key >= OBS_KEY_LAST_VALUE) {
  596. return false;
  597. }
  598. return platform->is_key_down[key];
  599. }
  600. static void unichar_to_utf8(const UniChar *character, char *buffer)
  601. {
  602. CFStringRef string = CFStringCreateWithCharactersNoCopy(NULL, character, 2, kCFAllocatorNull);
  603. if (!string) {
  604. blog(LOG_ERROR, "hotkey-cocoa: Unable to create CFStringRef with UniChar character");
  605. return;
  606. }
  607. if (!CFStringGetCString(string, buffer, OBS_COCOA_MODIFIER_SIZE, kCFStringEncodingUTF8)) {
  608. blog(LOG_ERROR, "hotkey-cocoa: Error while populating buffer with glyph %d (0x%x)", character[0], character[0]);
  609. }
  610. CFRelease(string);
  611. }
  612. void obs_key_combination_to_str(obs_key_combination_t key, struct dstr *str)
  613. {
  614. struct dstr keyString = {0};
  615. if (key.key != OBS_KEY_NONE) {
  616. obs_key_to_str(key.key, &keyString);
  617. }
  618. dispatch_once(&onceToken, ^{
  619. const UniChar controlCharacter[] = {kControlUnicode, 0};
  620. const UniChar optionCharacter[] = {kOptionUnicode, 0};
  621. const UniChar shiftCharacter[] = {kShiftUnicode, 0};
  622. const UniChar commandCharacter[] = {kCommandUnicode, 0};
  623. unichar_to_utf8(controlCharacter, string_control);
  624. unichar_to_utf8(optionCharacter, string_option);
  625. unichar_to_utf8(shiftCharacter, string_shift);
  626. unichar_to_utf8(commandCharacter, string_command);
  627. });
  628. const char *modifier_control = (key.modifiers & INTERACT_CONTROL_KEY) ? string_control : "";
  629. const char *modifier_option = (key.modifiers & INTERACT_ALT_KEY) ? string_option : "";
  630. const char *modifier_shift = (key.modifiers & INTERACT_SHIFT_KEY) ? string_shift : "";
  631. const char *modifier_command = (key.modifiers & INTERACT_COMMAND_KEY) ? string_command : "";
  632. const char *modifier_key = keyString.len ? keyString.array : "";
  633. dstr_printf(str, "%s%s%s%s%s", modifier_control, modifier_option, modifier_shift, modifier_command, modifier_key);
  634. dstr_free(&keyString);
  635. }
  636. void obs_key_to_str(obs_key_t key, struct dstr *str)
  637. {
  638. const UniCharCount maxLength = 16;
  639. UniChar buffer[16];
  640. if (obs_key_to_localized_string(key, str)) {
  641. return;
  642. }
  643. int code = obs_key_to_virtual_key(key);
  644. if (key_code_to_string(code, str)) {
  645. return;
  646. }
  647. if (code == INVALID_KEY) {
  648. const char *keyName = obs_key_to_name(key);
  649. blog(LOG_ERROR, "hotkey-cocoa: Got invalid key while translating '%d' (%s)", key, keyName);
  650. dstr_copy(str, keyName);
  651. return;
  652. }
  653. struct obs_hotkeys_platform *platform = NULL;
  654. if (obs) {
  655. pthread_mutex_lock(&obs->hotkeys.mutex);
  656. platform = obs->hotkeys.platform_context;
  657. hotkeys_retain(platform);
  658. pthread_mutex_unlock(&obs->hotkeys.mutex);
  659. }
  660. if (!platform) {
  661. const char *keyName = obs_key_to_name(key);
  662. blog(LOG_ERROR, "hotkey-cocoa: Could not get hotkey platform while translating '%d' (%s)", key, keyName);
  663. dstr_copy(str, keyName);
  664. return;
  665. }
  666. UInt32 deadKeyState = 0;
  667. UniCharCount length = 0;
  668. OSStatus err = UCKeyTranslate(platform->layout, code, kUCKeyActionDown, ((alphaLock >> 8) & 0xFF), LMGetKbdType(),
  669. kUCKeyTranslateNoDeadKeysBit, &deadKeyState, maxLength, &length, buffer);
  670. if (err == noErr && length <= 0 && deadKeyState) {
  671. err = UCKeyTranslate(platform->layout, kVK_Space, kUCKeyActionDown, ((alphaLock >> 8) & 0xFF), LMGetKbdType(),
  672. kUCKeyTranslateNoDeadKeysBit, &deadKeyState, maxLength, &length, buffer);
  673. }
  674. hotkeys_release(platform);
  675. if (err != noErr) {
  676. const char *keyName = obs_key_to_name(key);
  677. blog(LOG_ERROR, "hotkey-cocoa: Error while translating '%d' (0x%x, %s) to string: %d", key, code, keyName, err);
  678. dstr_copy(str, keyName);
  679. return;
  680. }
  681. if (length == 0) {
  682. const char *keyName = obs_key_to_name(key);
  683. blog(LOG_ERROR, "hotkey-cocoa: Got zero length string while translating '%d' (0x%x, %s) to string", key, code,
  684. keyName);
  685. dstr_copy(str, keyName);
  686. return;
  687. }
  688. CFStringRef string = CFStringCreateWithCharactersNoCopy(NULL, buffer, length, kCFAllocatorNull);
  689. if (!string) {
  690. const char *keyName = obs_key_to_name(key);
  691. blog(LOG_ERROR, "hotkey-cocoa: Could not create CFStringRef while translating '%d' (0x%x, %s) to string", key,
  692. code, keyName);
  693. dstr_copy(str, keyName);
  694. return;
  695. }
  696. if (!dstr_from_cfstring(str, string)) {
  697. const char *keyName = obs_key_to_name(key);
  698. blog(LOG_ERROR, "hotkey-cocoa: Could not translate CFStringRef to CString while translating '%d' (0x%x, %s)",
  699. key, code, keyName);
  700. CFRelease(string);
  701. dstr_copy(str, keyName);
  702. return;
  703. }
  704. CFRelease(string);
  705. return;
  706. }