mac-sck-audio-capture.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #include "mac-sck-common.h"
  2. const char *sck_audio_capture_getname(void *unused __unused)
  3. {
  4. return obs_module_text("SCK.Audio.Name");
  5. }
  6. API_AVAILABLE(macos(13.0)) static void destroy_audio_screen_stream(struct screen_capture *sc)
  7. {
  8. if (sc->disp && !sc->capture_failed) {
  9. [sc->disp stopCaptureWithCompletionHandler:^(NSError *_Nullable error) {
  10. if (error && error.code != SCStreamErrorAttemptToStopStreamState) {
  11. MACCAP_ERR("destroy_audio_screen_stream: Failed to stop stream with error %s\n",
  12. [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
  13. }
  14. }];
  15. }
  16. if (sc->stream_properties) {
  17. [sc->stream_properties release];
  18. sc->stream_properties = NULL;
  19. }
  20. if (sc->disp) {
  21. [sc->disp release];
  22. sc->disp = NULL;
  23. }
  24. os_event_destroy(sc->stream_start_completed);
  25. }
  26. API_AVAILABLE(macos(13.0)) static void sck_audio_capture_destroy(void *data)
  27. {
  28. struct screen_capture *sc = data;
  29. if (!sc)
  30. return;
  31. destroy_audio_screen_stream(sc);
  32. if (sc->shareable_content) {
  33. os_sem_wait(sc->shareable_content_available);
  34. [sc->shareable_content release];
  35. os_sem_destroy(sc->shareable_content_available);
  36. sc->shareable_content_available = NULL;
  37. }
  38. if (sc->capture_delegate) {
  39. [sc->capture_delegate release];
  40. }
  41. [sc->application_id release];
  42. pthread_mutex_destroy(&sc->mutex);
  43. bfree(sc);
  44. }
  45. API_AVAILABLE(macos(13.0)) static bool init_audio_screen_stream(struct screen_capture *sc)
  46. {
  47. SCContentFilter *content_filter;
  48. if (sc->capture_failed) {
  49. sc->capture_failed = false;
  50. obs_source_update_properties(sc->source);
  51. }
  52. sc->stream_properties = [[SCStreamConfiguration alloc] init];
  53. os_sem_wait(sc->shareable_content_available);
  54. SCDisplayRef (^get_target_display)(void) = ^SCDisplayRef {
  55. for (SCDisplay *display in sc->shareable_content.displays) {
  56. if (display.displayID == sc->display) {
  57. return display;
  58. }
  59. }
  60. return nil;
  61. };
  62. switch (sc->audio_capture_type) {
  63. case ScreenCaptureAudioDesktopStream: {
  64. SCDisplay *target_display = get_target_display();
  65. NSArray *empty = [[NSArray alloc] init];
  66. content_filter = [[SCContentFilter alloc] initWithDisplay:target_display excludingWindows:empty];
  67. [empty release];
  68. } break;
  69. case ScreenCaptureAudioApplicationStream: {
  70. SCDisplay *target_display = get_target_display();
  71. SCRunningApplication *target_application = nil;
  72. for (SCRunningApplication *application in sc->shareable_content.applications) {
  73. if ([application.bundleIdentifier isEqualToString:sc->application_id]) {
  74. target_application = application;
  75. break;
  76. }
  77. }
  78. NSArray *target_application_array = [[NSArray alloc] initWithObjects:target_application, nil];
  79. NSArray *empty = [[NSArray alloc] init];
  80. content_filter = [[SCContentFilter alloc] initWithDisplay:target_display
  81. includingApplications:target_application_array
  82. exceptingWindows:empty];
  83. [target_application_array release];
  84. [empty release];
  85. } break;
  86. }
  87. os_sem_post(sc->shareable_content_available);
  88. [sc->stream_properties setQueueDepth:8];
  89. [sc->stream_properties setCapturesAudio:TRUE];
  90. [sc->stream_properties setExcludesCurrentProcessAudio:TRUE];
  91. struct obs_audio_info audio_info;
  92. BOOL did_get_audio_info = obs_get_audio_info(&audio_info);
  93. if (!did_get_audio_info) {
  94. MACCAP_ERR("init_audio_screen_stream: No audio configured, returning %d\n", did_get_audio_info);
  95. [content_filter release];
  96. return did_get_audio_info;
  97. }
  98. int channel_count = get_audio_channels(audio_info.speakers);
  99. if (channel_count > 1) {
  100. [sc->stream_properties setChannelCount:2];
  101. } else {
  102. [sc->stream_properties setChannelCount:channel_count];
  103. }
  104. sc->disp = [[SCStream alloc] initWithFilter:content_filter configuration:sc->stream_properties
  105. delegate:sc->capture_delegate];
  106. [content_filter release];
  107. //add a dummy video stream output to silence errors from SCK. frames are dropped by the delegate
  108. NSError *error = nil;
  109. BOOL did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeScreen
  110. sampleHandlerQueue:nil
  111. error:&error];
  112. if (!did_add_output) {
  113. MACCAP_ERR("init_audio_screen_stream: Failed to add video stream output with error %s\n",
  114. [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
  115. [error release];
  116. [sc->disp release];
  117. sc->disp = NULL;
  118. return !did_add_output;
  119. }
  120. did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeAudio sampleHandlerQueue:nil
  121. error:&error];
  122. if (!did_add_output) {
  123. MACCAP_ERR("init_audio_screen_stream: Failed to add audio stream output with error %s\n",
  124. [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
  125. [error release];
  126. [sc->disp release];
  127. sc->disp = NULL;
  128. return !did_add_output;
  129. }
  130. os_event_init(&sc->stream_start_completed, OS_EVENT_TYPE_MANUAL);
  131. __block BOOL did_stream_start = false;
  132. [sc->disp startCaptureWithCompletionHandler:^(NSError *_Nullable error2) {
  133. did_stream_start = (BOOL) (error2 == nil);
  134. if (!did_stream_start) {
  135. MACCAP_ERR("init_audio_screen_stream: Failed to start capture with error %s\n",
  136. [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
  137. // Clean up disp so it isn't stopped
  138. [sc->disp release];
  139. sc->disp = NULL;
  140. }
  141. os_event_signal(sc->stream_start_completed);
  142. }];
  143. os_event_wait(sc->stream_start_completed);
  144. return did_stream_start;
  145. }
  146. static void sck_audio_capture_defaults(obs_data_t *settings)
  147. {
  148. obs_data_set_default_string(settings, "application", NULL);
  149. obs_data_set_default_int(settings, "type", ScreenCaptureAudioDesktopStream);
  150. }
  151. API_AVAILABLE(macos(13.0)) static void *sck_audio_capture_create(obs_data_t *settings, obs_source_t *source)
  152. {
  153. struct screen_capture *sc = bzalloc(sizeof(struct screen_capture));
  154. sc->source = source;
  155. sc->audio_only = true;
  156. sc->audio_capture_type = (unsigned int) obs_data_get_int(settings, "type");
  157. os_sem_init(&sc->shareable_content_available, 1);
  158. screen_capture_build_content_list(sc, sc->audio_capture_type == ScreenCaptureAudioDesktopStream);
  159. sc->capture_delegate = [[ScreenCaptureDelegate alloc] init];
  160. sc->capture_delegate.sc = sc;
  161. sc->display = CGMainDisplayID();
  162. sc->application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];
  163. pthread_mutex_init(&sc->mutex, NULL);
  164. if (!init_audio_screen_stream(sc))
  165. goto fail;
  166. return sc;
  167. fail:
  168. sck_audio_capture_destroy(sc);
  169. return NULL;
  170. }
  171. #pragma mark - obs_properties
  172. API_AVAILABLE(macos(13.0))
  173. static bool audio_capture_method_changed(void *data, obs_properties_t *props, obs_property_t *list __unused,
  174. obs_data_t *settings)
  175. {
  176. struct screen_capture *sc = data;
  177. unsigned int capture_type_id = (unsigned int) obs_data_get_int(settings, "type");
  178. obs_property_t *app_list = obs_properties_get(props, "application");
  179. switch (capture_type_id) {
  180. case ScreenCaptureAudioDesktopStream: {
  181. obs_property_set_visible(app_list, false);
  182. break;
  183. }
  184. case ScreenCaptureAudioApplicationStream: {
  185. obs_property_set_visible(app_list, true);
  186. screen_capture_build_content_list(sc, capture_type_id == ScreenCaptureAudioDesktopStream);
  187. build_application_list(sc, props);
  188. break;
  189. }
  190. }
  191. return true;
  192. }
  193. API_AVAILABLE(macos(13.0))
  194. static bool reactivate_capture(obs_properties_t *props __unused, obs_property_t *property, void *data)
  195. {
  196. struct screen_capture *sc = data;
  197. if (!sc->capture_failed) {
  198. MACCAP_LOG(LOG_WARNING, "Tried to reactivate capture that hadn't failed.");
  199. return false;
  200. }
  201. destroy_audio_screen_stream(sc);
  202. sc->capture_failed = false;
  203. init_audio_screen_stream(sc);
  204. obs_property_set_enabled(property, false);
  205. return true;
  206. }
  207. API_AVAILABLE(macos(13.0)) static obs_properties_t *sck_audio_capture_properties(void *data)
  208. {
  209. struct screen_capture *sc = data;
  210. obs_properties_t *props = obs_properties_create();
  211. obs_property_t *capture_type = obs_properties_add_list(props, "type", obs_module_text("SCK.Method"),
  212. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  213. obs_property_list_add_int(capture_type, obs_module_text("DesktopAudioCapture"), 0);
  214. obs_property_list_add_int(capture_type, obs_module_text("ApplicationAudioCapture"), 1);
  215. obs_property_set_modified_callback2(capture_type, audio_capture_method_changed, data);
  216. obs_property_t *app_list = obs_properties_add_list(props, "application", obs_module_text("Application"),
  217. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
  218. obs_property_t *reactivate =
  219. obs_properties_add_button2(props, "reactivate_capture", obs_module_text("SCK.Restart"), reactivate_capture, sc);
  220. obs_property_set_enabled(reactivate, sc->capture_failed);
  221. if (sc) {
  222. switch (sc->audio_capture_type) {
  223. case 0: {
  224. obs_property_set_visible(app_list, false);
  225. break;
  226. }
  227. case 1: {
  228. obs_property_set_visible(app_list, true);
  229. break;
  230. }
  231. }
  232. }
  233. return props;
  234. }
  235. API_AVAILABLE(macos(13.0)) static void sck_audio_capture_update(void *data, obs_data_t *settings)
  236. {
  237. struct screen_capture *sc = data;
  238. ScreenCaptureAudioStreamType capture_type = (ScreenCaptureAudioStreamType) obs_data_get_int(settings, "type");
  239. NSString *application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];
  240. destroy_audio_screen_stream(sc);
  241. sc->audio_capture_type = capture_type;
  242. [sc->application_id release];
  243. sc->application_id = application_id;
  244. init_audio_screen_stream(sc);
  245. }
  246. #pragma mark - obs_source_info
  247. API_AVAILABLE(macos(13.0))
  248. struct obs_source_info sck_audio_capture_info = {
  249. .id = "sck_audio_capture",
  250. .type = OBS_SOURCE_TYPE_INPUT,
  251. .get_name = sck_audio_capture_getname,
  252. .create = sck_audio_capture_create,
  253. .destroy = sck_audio_capture_destroy,
  254. .output_flags = OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_AUDIO,
  255. .get_defaults = sck_audio_capture_defaults,
  256. .get_properties = sck_audio_capture_properties,
  257. .update = sck_audio_capture_update,
  258. .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
  259. };