mac-display-capture.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. #include <stdlib.h>
  2. #include <obs-module.h>
  3. #include <util/threading.h>
  4. #include <pthread.h>
  5. #import <CoreGraphics/CGDisplayStream.h>
  6. #import <Cocoa/Cocoa.h>
  7. #include "window-utils.h"
  8. enum crop_mode {
  9. CROP_NONE,
  10. CROP_MANUAL,
  11. CROP_TO_WINDOW,
  12. CROP_TO_WINDOW_AND_MANUAL,
  13. CROP_INVALID
  14. };
  15. static inline bool requires_window(enum crop_mode mode)
  16. {
  17. return mode == CROP_TO_WINDOW || mode == CROP_TO_WINDOW_AND_MANUAL;
  18. }
  19. struct display_capture {
  20. obs_source_t *source;
  21. gs_samplerstate_t *sampler;
  22. gs_effect_t *effect;
  23. gs_texture_t *tex;
  24. gs_vertbuffer_t *vertbuf;
  25. NSScreen *screen;
  26. unsigned display;
  27. NSRect frame;
  28. bool hide_cursor;
  29. enum crop_mode crop;
  30. CGRect crop_rect;
  31. struct cocoa_window window;
  32. CGRect window_rect;
  33. bool on_screen;
  34. bool hide_when_minimized;
  35. os_event_t *disp_finished;
  36. CGDisplayStreamRef disp;
  37. IOSurfaceRef current, prev;
  38. pthread_mutex_t mutex;
  39. };
  40. static inline bool crop_mode_valid(enum crop_mode mode)
  41. {
  42. return CROP_NONE <= mode && mode < CROP_INVALID;
  43. }
  44. static void destroy_display_stream(struct display_capture *dc)
  45. {
  46. if (dc->disp) {
  47. CGDisplayStreamStop(dc->disp);
  48. os_event_wait(dc->disp_finished);
  49. }
  50. if (dc->tex) {
  51. gs_texture_destroy(dc->tex);
  52. dc->tex = NULL;
  53. }
  54. if (dc->current) {
  55. IOSurfaceDecrementUseCount(dc->current);
  56. CFRelease(dc->current);
  57. dc->current = NULL;
  58. }
  59. if (dc->prev) {
  60. IOSurfaceDecrementUseCount(dc->prev);
  61. CFRelease(dc->prev);
  62. dc->prev = NULL;
  63. }
  64. if (dc->disp) {
  65. CFRelease(dc->disp);
  66. dc->disp = NULL;
  67. }
  68. if (dc->screen) {
  69. [dc->screen release];
  70. dc->screen = nil;
  71. }
  72. os_event_destroy(dc->disp_finished);
  73. }
  74. static void display_capture_destroy(void *data)
  75. {
  76. struct display_capture *dc = data;
  77. if (!dc)
  78. return;
  79. obs_enter_graphics();
  80. destroy_display_stream(dc);
  81. if (dc->sampler)
  82. gs_samplerstate_destroy(dc->sampler);
  83. if (dc->vertbuf)
  84. gs_vertexbuffer_destroy(dc->vertbuf);
  85. obs_leave_graphics();
  86. destroy_window(&dc->window);
  87. pthread_mutex_destroy(&dc->mutex);
  88. bfree(dc);
  89. }
  90. static inline void update_window_params(struct display_capture *dc)
  91. {
  92. if (!requires_window(dc->crop))
  93. return;
  94. NSArray *arr = (NSArray*)CGWindowListCopyWindowInfo(
  95. kCGWindowListOptionIncludingWindow,
  96. dc->window.window_id);
  97. if (arr.count) {
  98. NSDictionary *dict = arr[0];
  99. NSDictionary *ref = dict[(NSString*)kCGWindowBounds];
  100. CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)ref,
  101. &dc->window_rect);
  102. dc->on_screen = dict[(NSString*)kCGWindowIsOnscreen] != nil;
  103. dc->window_rect =
  104. [dc->screen convertRectToBacking:dc->window_rect];
  105. } else {
  106. if (find_window(&dc->window, NULL, false))
  107. update_window_params(dc);
  108. else
  109. dc->on_screen = false;
  110. }
  111. [arr release];
  112. }
  113. static inline void display_stream_update(struct display_capture *dc,
  114. CGDisplayStreamFrameStatus status, uint64_t display_time,
  115. IOSurfaceRef frame_surface, CGDisplayStreamUpdateRef update_ref)
  116. {
  117. UNUSED_PARAMETER(display_time);
  118. UNUSED_PARAMETER(update_ref);
  119. if (status == kCGDisplayStreamFrameStatusStopped) {
  120. os_event_signal(dc->disp_finished);
  121. return;
  122. }
  123. IOSurfaceRef prev_current = NULL;
  124. if (frame_surface && !pthread_mutex_lock(&dc->mutex)) {
  125. prev_current = dc->current;
  126. dc->current = frame_surface;
  127. CFRetain(dc->current);
  128. IOSurfaceIncrementUseCount(dc->current);
  129. update_window_params(dc);
  130. pthread_mutex_unlock(&dc->mutex);
  131. }
  132. if (prev_current) {
  133. IOSurfaceDecrementUseCount(prev_current);
  134. CFRelease(prev_current);
  135. }
  136. size_t dropped_frames = CGDisplayStreamUpdateGetDropCount(update_ref);
  137. if (dropped_frames > 0)
  138. blog(LOG_INFO, "%s: Dropped %zu frames",
  139. obs_source_get_name(dc->source),
  140. dropped_frames);
  141. }
  142. static bool init_display_stream(struct display_capture *dc)
  143. {
  144. if (dc->display >= [NSScreen screens].count)
  145. return false;
  146. dc->screen = [[NSScreen screens][dc->display] retain];
  147. dc->frame = [dc->screen convertRectToBacking:dc->screen.frame];
  148. NSNumber *screen_num = dc->screen.deviceDescription[@"NSScreenNumber"];
  149. CGDirectDisplayID disp_id = (CGDirectDisplayID)screen_num.pointerValue;
  150. NSDictionary *rect_dict = CFBridgingRelease(
  151. CGRectCreateDictionaryRepresentation(
  152. CGRectMake(0, 0,
  153. dc->screen.frame.size.width,
  154. dc->screen.frame.size.height)));
  155. CFBooleanRef show_cursor_cf =
  156. dc->hide_cursor ? kCFBooleanFalse : kCFBooleanTrue;
  157. NSDictionary *dict = @{
  158. (__bridge NSString*)kCGDisplayStreamSourceRect: rect_dict,
  159. (__bridge NSString*)kCGDisplayStreamQueueDepth: @5,
  160. (__bridge NSString*)kCGDisplayStreamShowCursor:
  161. (id)show_cursor_cf,
  162. };
  163. os_event_init(&dc->disp_finished, OS_EVENT_TYPE_MANUAL);
  164. const CGSize *size = &dc->frame.size;
  165. dc->disp = CGDisplayStreamCreateWithDispatchQueue(disp_id,
  166. size->width, size->height, 'BGRA',
  167. (__bridge CFDictionaryRef)dict,
  168. dispatch_queue_create(NULL, NULL),
  169. ^(CGDisplayStreamFrameStatus status,
  170. uint64_t displayTime,
  171. IOSurfaceRef frameSurface,
  172. CGDisplayStreamUpdateRef updateRef)
  173. {
  174. display_stream_update(dc, status, displayTime,
  175. frameSurface, updateRef);
  176. }
  177. );
  178. return !CGDisplayStreamStart(dc->disp);
  179. }
  180. bool init_vertbuf(struct display_capture *dc)
  181. {
  182. struct gs_vb_data *vb_data = gs_vbdata_create();
  183. vb_data->num = 4;
  184. vb_data->points = bzalloc(sizeof(struct vec3) * 4);
  185. if (!vb_data->points)
  186. return false;
  187. vb_data->num_tex = 1;
  188. vb_data->tvarray = bzalloc(sizeof(struct gs_tvertarray));
  189. if (!vb_data->tvarray)
  190. return false;
  191. vb_data->tvarray[0].width = 2;
  192. vb_data->tvarray[0].array = bzalloc(sizeof(struct vec2) * 4);
  193. if (!vb_data->tvarray[0].array)
  194. return false;
  195. dc->vertbuf = gs_vertexbuffer_create(vb_data, GS_DYNAMIC);
  196. return dc->vertbuf != NULL;
  197. }
  198. void load_crop(struct display_capture *dc, obs_data_t *settings);
  199. static void *display_capture_create(obs_data_t *settings,
  200. obs_source_t *source)
  201. {
  202. UNUSED_PARAMETER(source);
  203. UNUSED_PARAMETER(settings);
  204. struct display_capture *dc = bzalloc(sizeof(struct display_capture));
  205. dc->source = source;
  206. dc->hide_cursor = !obs_data_get_bool(settings, "show_cursor");
  207. dc->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT);
  208. if (!dc->effect)
  209. goto fail;
  210. obs_enter_graphics();
  211. struct gs_sampler_info info = {
  212. .filter = GS_FILTER_LINEAR,
  213. .address_u = GS_ADDRESS_CLAMP,
  214. .address_v = GS_ADDRESS_CLAMP,
  215. .address_w = GS_ADDRESS_CLAMP,
  216. .max_anisotropy = 1,
  217. };
  218. dc->sampler = gs_samplerstate_create(&info);
  219. if (!dc->sampler)
  220. goto fail;
  221. if (!init_vertbuf(dc))
  222. goto fail;
  223. obs_leave_graphics();
  224. init_window(&dc->window, settings);
  225. load_crop(dc, settings);
  226. dc->display = obs_data_get_int(settings, "display");
  227. pthread_mutex_init(&dc->mutex, NULL);
  228. if (!init_display_stream(dc))
  229. goto fail;
  230. return dc;
  231. fail:
  232. obs_leave_graphics();
  233. display_capture_destroy(dc);
  234. return NULL;
  235. }
  236. static void build_sprite(struct gs_vb_data *data, float fcx, float fcy,
  237. float start_u, float end_u, float start_v, float end_v)
  238. {
  239. struct vec2 *tvarray = data->tvarray[0].array;
  240. vec3_set(data->points+1, fcx, 0.0f, 0.0f);
  241. vec3_set(data->points+2, 0.0f, fcy, 0.0f);
  242. vec3_set(data->points+3, fcx, fcy, 0.0f);
  243. vec2_set(tvarray, start_u, start_v);
  244. vec2_set(tvarray+1, end_u, start_v);
  245. vec2_set(tvarray+2, start_u, end_v);
  246. vec2_set(tvarray+3, end_u, end_v);
  247. }
  248. static inline void build_sprite_rect(struct gs_vb_data *data,
  249. float origin_x, float origin_y, float end_x, float end_y)
  250. {
  251. build_sprite(data, fabs(end_x - origin_x), fabs(end_y - origin_y),
  252. origin_x, end_x,
  253. origin_y, end_y);
  254. }
  255. static void display_capture_video_tick(void *data, float seconds)
  256. {
  257. UNUSED_PARAMETER(seconds);
  258. struct display_capture *dc = data;
  259. if (!dc->current)
  260. return;
  261. if (!obs_source_showing(dc->source))
  262. return;
  263. IOSurfaceRef prev_prev = dc->prev;
  264. if (pthread_mutex_lock(&dc->mutex))
  265. return;
  266. dc->prev = dc->current;
  267. dc->current = NULL;
  268. pthread_mutex_unlock(&dc->mutex);
  269. if (prev_prev == dc->prev)
  270. return;
  271. if (requires_window(dc->crop) && !dc->on_screen)
  272. goto cleanup;
  273. CGPoint origin = { 0.f };
  274. CGPoint end = { 0.f };
  275. switch (dc->crop) {
  276. float x, y;
  277. case CROP_INVALID:
  278. break;
  279. case CROP_MANUAL:
  280. origin.x += dc->crop_rect.origin.x;
  281. origin.y += dc->crop_rect.origin.y;
  282. end.y -= dc->crop_rect.size.height;
  283. end.x -= dc->crop_rect.size.width;
  284. case CROP_NONE:
  285. end.y += dc->frame.size.height;
  286. end.x += dc->frame.size.width;
  287. break;
  288. case CROP_TO_WINDOW_AND_MANUAL:
  289. origin.x += dc->crop_rect.origin.x;
  290. origin.y += dc->crop_rect.origin.y;
  291. end.y -= dc->crop_rect.size.height;
  292. end.x -= dc->crop_rect.size.width;
  293. case CROP_TO_WINDOW:
  294. origin.x += x = dc->window_rect.origin.x - dc->frame.origin.x;
  295. origin.y += y = dc->window_rect.origin.y - dc->frame.origin.y;
  296. end.y += dc->window_rect.size.height + y;
  297. end.x += dc->window_rect.size.width + x;
  298. break;
  299. }
  300. obs_enter_graphics();
  301. build_sprite_rect(gs_vertexbuffer_get_data(dc->vertbuf),
  302. origin.x, origin.y, end.x, end.y);
  303. if (dc->tex)
  304. gs_texture_rebind_iosurface(dc->tex, dc->prev);
  305. else
  306. dc->tex = gs_texture_create_from_iosurface(dc->prev);
  307. obs_leave_graphics();
  308. cleanup:
  309. if (prev_prev) {
  310. IOSurfaceDecrementUseCount(prev_prev);
  311. CFRelease(prev_prev);
  312. }
  313. }
  314. static void display_capture_video_render(void *data, gs_effect_t *effect)
  315. {
  316. UNUSED_PARAMETER(effect);
  317. struct display_capture *dc = data;
  318. if (!dc->tex || (requires_window(dc->crop) && !dc->on_screen))
  319. return;
  320. gs_vertexbuffer_flush(dc->vertbuf);
  321. gs_load_vertexbuffer(dc->vertbuf);
  322. gs_load_indexbuffer(NULL);
  323. gs_load_samplerstate(dc->sampler, 0);
  324. gs_technique_t *tech = gs_effect_get_technique(dc->effect, "Draw");
  325. gs_effect_set_texture(gs_effect_get_param_by_name(dc->effect, "image"),
  326. dc->tex);
  327. gs_technique_begin(tech);
  328. gs_technique_begin_pass(tech, 0);
  329. gs_draw(GS_TRISTRIP, 0, 4);
  330. gs_technique_end_pass(tech);
  331. gs_technique_end(tech);
  332. }
  333. static const char *display_capture_getname(void *unused)
  334. {
  335. UNUSED_PARAMETER(unused);
  336. return obs_module_text("DisplayCapture");
  337. }
  338. #define CROPPED_LENGTH(rect, origin_, length) \
  339. fabs((rect ## .size. ## length - dc->crop_rect.size. ## length) - \
  340. (rect ## .origin. ## origin_ + dc->crop_rect.origin. ## origin_))
  341. static uint32_t display_capture_getwidth(void *data)
  342. {
  343. struct display_capture *dc = data;
  344. float crop = dc->crop_rect.origin.x + dc->crop_rect.size.width;
  345. switch (dc->crop) {
  346. case CROP_NONE:
  347. return dc->frame.size.width;
  348. case CROP_MANUAL:
  349. return fabs(dc->frame.size.width - crop);
  350. case CROP_TO_WINDOW:
  351. return dc->window_rect.size.width;
  352. case CROP_TO_WINDOW_AND_MANUAL:
  353. return fabs(dc->window_rect.size.width - crop);
  354. case CROP_INVALID:
  355. break;
  356. }
  357. return 0;
  358. }
  359. static uint32_t display_capture_getheight(void *data)
  360. {
  361. struct display_capture *dc = data;
  362. float crop = dc->crop_rect.origin.y + dc->crop_rect.size.height;
  363. switch (dc->crop) {
  364. case CROP_NONE:
  365. return dc->frame.size.height;
  366. case CROP_MANUAL:
  367. return fabs(dc->frame.size.height - crop);
  368. case CROP_TO_WINDOW:
  369. return dc->window_rect.size.height;
  370. case CROP_TO_WINDOW_AND_MANUAL:
  371. return fabs(dc->window_rect.size.height - crop);
  372. case CROP_INVALID:
  373. break;
  374. }
  375. return 0;
  376. }
  377. static void display_capture_defaults(obs_data_t *settings)
  378. {
  379. obs_data_set_default_int(settings, "display", 0);
  380. obs_data_set_default_bool(settings, "show_cursor", true);
  381. obs_data_set_default_int(settings, "crop_mode", CROP_NONE);
  382. window_defaults(settings);
  383. }
  384. void load_crop_mode(enum crop_mode *mode, obs_data_t *settings)
  385. {
  386. *mode = obs_data_get_int(settings, "crop_mode");
  387. if (!crop_mode_valid(*mode))
  388. *mode = CROP_NONE;
  389. }
  390. void load_crop(struct display_capture *dc, obs_data_t *settings)
  391. {
  392. load_crop_mode(&dc->crop, settings);
  393. #define CROP_VAR_NAME(var, mode) (mode "." #var)
  394. #define LOAD_CROP_VAR(var, mode) \
  395. dc->crop_rect.var = obs_data_get_double(settings, \
  396. CROP_VAR_NAME(var, mode));
  397. switch (dc->crop) {
  398. case CROP_MANUAL:
  399. LOAD_CROP_VAR(origin.x, "manual");
  400. LOAD_CROP_VAR(origin.y, "manual");
  401. LOAD_CROP_VAR(size.width, "manual");
  402. LOAD_CROP_VAR(size.height, "manual");
  403. break;
  404. case CROP_TO_WINDOW_AND_MANUAL:
  405. LOAD_CROP_VAR(origin.x, "window");
  406. LOAD_CROP_VAR(origin.y, "window");
  407. LOAD_CROP_VAR(size.width, "window");
  408. LOAD_CROP_VAR(size.height, "window");
  409. break;
  410. case CROP_NONE:
  411. case CROP_TO_WINDOW:
  412. case CROP_INVALID:
  413. break;
  414. }
  415. #undef LOAD_CROP_VAR
  416. }
  417. static void display_capture_update(void *data, obs_data_t *settings)
  418. {
  419. struct display_capture *dc = data;
  420. load_crop(dc, settings);
  421. if (requires_window(dc->crop))
  422. update_window(&dc->window, settings);
  423. unsigned display = obs_data_get_int(settings, "display");
  424. bool show_cursor = obs_data_get_bool(settings, "show_cursor");
  425. if (dc->display == display && dc->hide_cursor != show_cursor)
  426. return;
  427. obs_enter_graphics();
  428. destroy_display_stream(dc);
  429. dc->display = display;
  430. dc->hide_cursor = !show_cursor;
  431. init_display_stream(dc);
  432. obs_leave_graphics();
  433. }
  434. static bool switch_crop_mode(obs_properties_t *props, obs_property_t *p,
  435. obs_data_t *settings)
  436. {
  437. UNUSED_PARAMETER(p);
  438. enum crop_mode crop;
  439. load_crop_mode(&crop, settings);
  440. const char *name;
  441. bool visible;
  442. #define LOAD_CROP_VAR(var, mode) \
  443. name = CROP_VAR_NAME(var, mode); \
  444. obs_property_set_visible(obs_properties_get(props, name), visible);
  445. visible = crop == CROP_MANUAL;
  446. LOAD_CROP_VAR(origin.x, "manual");
  447. LOAD_CROP_VAR(origin.y, "manual");
  448. LOAD_CROP_VAR(size.width, "manual");
  449. LOAD_CROP_VAR(size.height, "manual");
  450. visible = crop == CROP_TO_WINDOW_AND_MANUAL;
  451. LOAD_CROP_VAR(origin.x, "window");
  452. LOAD_CROP_VAR(origin.y, "window");
  453. LOAD_CROP_VAR(size.width, "window");
  454. LOAD_CROP_VAR(size.height, "window");
  455. #undef LOAD_CROP_VAR
  456. show_window_properties(props, visible || crop == CROP_TO_WINDOW);
  457. return true;
  458. }
  459. static const char *crop_names[] = {
  460. "CropMode.None",
  461. "CropMode.Manual",
  462. "CropMode.ToWindow",
  463. "CropMode.ToWindowAndManual"
  464. };
  465. #ifndef COUNTOF
  466. #define COUNTOF(x) (sizeof(x)/sizeof(x[0]))
  467. #endif
  468. static obs_properties_t *display_capture_properties(void *unused)
  469. {
  470. UNUSED_PARAMETER(unused);
  471. obs_properties_t *props = obs_properties_create();
  472. obs_property_t *list = obs_properties_add_list(props,
  473. "display", obs_module_text("DisplayCapture.Display"),
  474. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  475. for (unsigned i = 0; i < [NSScreen screens].count; i++) {
  476. char buf[10];
  477. sprintf(buf, "%u", i);
  478. obs_property_list_add_int(list, buf, i);
  479. }
  480. obs_properties_add_bool(props, "show_cursor",
  481. obs_module_text("DisplayCapture.ShowCursor"));
  482. obs_property_t *crop = obs_properties_add_list(props, "crop_mode",
  483. obs_module_text("CropMode"),
  484. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  485. obs_property_set_modified_callback(crop, switch_crop_mode);
  486. for (unsigned i = 0; i < COUNTOF(crop_names); i++) {
  487. const char *name = obs_module_text(crop_names[i]);
  488. obs_property_list_add_int(crop, name, i);
  489. }
  490. add_window_properties(props);
  491. show_window_properties(props, false);
  492. obs_property_t *p;
  493. const char *name;
  494. float min;
  495. #define LOAD_CROP_VAR(var, mode) \
  496. name = CROP_VAR_NAME(var, mode); \
  497. p = obs_properties_add_float(props, name, \
  498. obs_module_text("Crop."#var), min, 4096.f, .5f); \
  499. obs_property_set_visible(p, false);
  500. min = 0.f;
  501. LOAD_CROP_VAR(origin.x, "manual");
  502. LOAD_CROP_VAR(origin.y, "manual");
  503. LOAD_CROP_VAR(size.width, "manual");
  504. LOAD_CROP_VAR(size.height, "manual");
  505. min = -4096.f;
  506. LOAD_CROP_VAR(origin.x, "window");
  507. LOAD_CROP_VAR(origin.y, "window");
  508. LOAD_CROP_VAR(size.width, "window");
  509. LOAD_CROP_VAR(size.height, "window");
  510. #undef LOAD_CROP_VAR
  511. return props;
  512. }
  513. struct obs_source_info display_capture_info = {
  514. .id = "display_capture",
  515. .type = OBS_SOURCE_TYPE_INPUT,
  516. .get_name = display_capture_getname,
  517. .create = display_capture_create,
  518. .destroy = display_capture_destroy,
  519. .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |
  520. OBS_SOURCE_DO_NOT_DUPLICATE,
  521. .video_tick = display_capture_video_tick,
  522. .video_render = display_capture_video_render,
  523. .get_width = display_capture_getwidth,
  524. .get_height = display_capture_getheight,
  525. .get_defaults = display_capture_defaults,
  526. .get_properties = display_capture_properties,
  527. .update = display_capture_update,
  528. };